// Клиент REST API. В разработке /api проксирует Vite, в проде — nginx. import { attribution } from './lib/attribution.js'; import { reachGoal } from './lib/metrika.js'; export class ApiError extends Error { constructor(status, code, message, payload) { super(message); this.status = status; this.code = code; this.payload = payload; } } async function request(method, path, { params, body, signal } = {}) { let url = `/api/v1${path}`; if (params) { const qs = new URLSearchParams(); for (const [k, v] of Object.entries(params)) { if (v !== undefined && v !== null && v !== '') qs.set(k, v); } const s = qs.toString(); if (s) url += `?${s}`; } const res = await fetch(url, { method, signal, headers: body ? { 'Content-Type': 'application/json' } : undefined, body: body ? JSON.stringify(body) : undefined, }); let json = null; try { json = await res.json(); } catch { // пустой или не-JSON ответ — разберёмся по статусу ниже } if (!res.ok) { throw new ApiError( res.status, json?.error?.code || 'http_error', json?.error?.message || 'Не удалось загрузить данные', json, ); } return { data: json?.data, meta: json?.meta || {} }; } export const api = { services: (o) => request('GET', '/services', o), // path — «detskaya-klinika» или «detskaya-klinika/priem-pediatra»; слэши оставляем как есть service: (path, o) => request('GET', `/services/${path.split('/').map(encodeURIComponent).join('/')}`, o), checkups: (o) => request('GET', '/checkups', o), checkup: (slug, o) => request('GET', `/checkups/${encodeURIComponent(slug)}`, o), promos: (o) => request('GET', '/promos', o), promo: (slug, o) => request('GET', `/promos/${encodeURIComponent(slug)}`, o), doctors: (params, o) => request('GET', '/doctors', { ...o, params }), doctor: (slug, o) => request('GET', `/doctors/${encodeURIComponent(slug)}`, o), // подтверждение номера телефона звонком (Wait-Call) waitCallInit: (phone, o) => request('POST', '/auth/wait-call/init', { ...o, body: { phone } }), waitCallStatus: (sessionId, o) => request('GET', `/auth/wait-call/status?session_id=${encodeURIComponent(sessionId)}`, o), // свободные окна врача из 1С и запись на конкретное окно doctorSlots: (slug, o) => request('GET', `/doctors/${encodeURIComponent(slug)}/slots`, o), createAppointment: (body) => request('POST', '/appointments', { body: { utm: attribution(), ...body } }).then((res) => { reachGoal('lead', { form: 'appointment' }); return res; }), specialties: (o) => request('GET', '/specialties', o), prices: (params, o) => request('GET', '/prices', { ...o, params }), // короткий список разделов прайса (без позиций) — для меню в шапке priceSections: (o) => request('GET', '/price-sections', o), reviews: (params, o) => request('GET', '/reviews', { ...o, params }), // kind — «news» или «articles»: адреса разделов совпадают со старым сайтом posts: (kind, o) => request('GET', `/${kind}`, { ...o, params: { limit: 100 } }), post: (kind, slug, o) => request('GET', `/${kind}/${encodeURIComponent(slug)}`, o), partners: (o) => request('GET', '/partners', o), documents: (kind, o) => request('GET', '/documents', { ...o, params: { kind } }), vacancies: (o) => request('GET', '/vacancies', o), faq: (o) => request('GET', '/faq', o), branches: (o) => request('GET', '/branches', o), banners: (params, o) => request('GET', '/banners', { ...o, params }), settings: (o) => request('GET', '/settings', o), forms: (o) => request('GET', '/forms', o), search: (params, o) => request('GET', '/search', { ...o, params }), page: (slug, o) => request('GET', `/pages/${encodeURIComponent(slug)}`, o), // цель в Метрике — только код формы, без имени и телефона createLead: (body) => request('POST', '/leads', { body: { utm: attribution(), ...body } }).then((res) => { reachGoal('lead', { form: body.form }); return res; }), };