94 lines
4.3 KiB
JavaScript
94 lines
4.3 KiB
JavaScript
// Клиент 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;
|
||
}),
|
||
};
|