// Клиент к backend-API (Express, порт 4000, публикуется nginx под /api). // База берётся из NEXT_PUBLIC_API_URL (например https://nika-gallery.ru/api). // В dev по умолчанию — http://localhost:4000/api. import type { Auction, Bid, Paginated, Work } from './types'; export const API_BASE = ( process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api' ).replace(/\/$/, ''); export class ApiError extends Error { status: number; constructor(message: string, status: number) { super(message); this.name = 'ApiError'; this.status = status; } } type FetchOpts = RequestInit & { revalidate?: number }; async function request(path: string, opts: FetchOpts = {}): Promise { const { revalidate, ...init } = opts; const url = `${API_BASE}${path.startsWith('/') ? path : `/${path}`}`; const res = await fetch(url, { ...init, headers: { Accept: 'application/json', ...(init.headers || {}) }, // ISR: обновляем витрину периодически; переопределяемо на месте вызова next: revalidate !== undefined ? { revalidate } : { revalidate: 60 }, }); if (!res.ok) { let msg = `${res.status} ${res.statusText}`; try { const body = await res.json(); if (body?.error || body?.message) msg = body.error || body.message; } catch { /* тело не JSON — оставляем статус */ } throw new ApiError(msg, res.status); } return (await res.json()) as T; } // Нормализуем список: бэкенд может вернуть массив или {items,total}. function toArray(data: T[] | Paginated): T[] { return Array.isArray(data) ? data : data.items ?? []; } export const api = { // ── Работы ───────────────────────────────────────────── async listWorks(params?: { status?: string; category?: string; limit?: number; }): Promise { const q = new URLSearchParams(); if (params?.status) q.set('status', params.status); if (params?.category) q.set('category', params.category); if (params?.limit) q.set('limit', String(params.limit)); const qs = q.toString() ? `?${q}` : ''; return toArray(await request>(`/works${qs}`)); }, async getWork(idOrSlug: string): Promise { return request(`/works/${encodeURIComponent(idOrSlug)}`); }, // ── Аукционы ─────────────────────────────────────────── async listAuctions(params?: { status?: string }): Promise { const q = new URLSearchParams(); if (params?.status) q.set('status', params.status); const qs = q.toString() ? `?${q}` : ''; return toArray( await request>(`/auctions${qs}`), ); }, async getAuction(id: string): Promise { return request(`/auctions/${encodeURIComponent(id)}`); }, async getAuctionBids(id: string): Promise { return toArray( await request>( `/auctions/${encodeURIComponent(id)}/bids`, ), ); }, }; // Безопасная обёртка для серверных компонентов: при недоступном backend // возвращаем fallback вместо падения страницы (graceful degradation). export async function safe( fn: () => Promise, fallback: T, ): Promise<{ data: T; ok: boolean }> { try { return { data: await fn(), ok: true }; } catch { return { data: fallback, ok: false }; } }