Files
umbyte-landing/frontend/src/api/client.ts
T
zeroday 29d1386dd9 merge: свести ветку визиток из Gitea с локальной работой по лендингу
Две линии разработки разъехались и месяц жили порознь: в Gitea уехали
электронные визитки с QR-кодом, а в рабочей копии на fortress остались шесть
коммитов, которые до репозитория не дошли и, значит, не попали на сайт —
английская версия с переключателем языков, карточка Бонитировки, логотипы
продуктов, реквизиты компании в подвале, убранная кнопка Telegram, имя
директора латиницей. Плюс незакоммиченное бургер-меню.

Что разрешено вручную:

1. Landing.tsx, блок контактов. Столкнулись наша расстановка (почта главной
   кнопкой с подписью из cta_primary, телефон второй) и правка из Gitea,
   переводящая оба контакта с href на onClick, чтобы адрес и номер не лежали
   в разметке и их не собирали боты. Взято и то, и другое: защита от ботов
   сохранена, порядок и оформление наши, адрес почты в подписи не светится.

2. Коллизия номеров миграций: обе стороны завели номер 003. На живой базе
   применены 001, 002 и 003_business_cards, а 003_product_url_and_requisites
   не выполнялась никогда — она и переименована в 004, чтобы раннер (он
   отслеживает миграции по имени файла) отработал её штатно.

3. package-lock пересобран под слитый манифест, а не выбран со стороны.

Проверено: tsc чист, vite build проходит, 118 модулей, 241 КБ.
2026-08-25 22:27:25 +03:00

144 lines
4.5 KiB
TypeScript

const API_BASE = import.meta.env.VITE_API_URL || '';
export interface Product {
id: number;
slug: string;
site_url?: string | null;
title: string;
subtitle: string | null;
description: string;
status: 'production' | 'development' | 'planned';
audience: 'B2B' | 'B2C' | 'global' | null;
icon_key: string | null;
tags: string[];
sort_order: number;
title_en?: string | null;
subtitle_en?: string | null;
description_en?: string | null;
logo_url?: string | null;
brand_color?: string | null;
}
export interface ApproachItem {
id: number;
title: string;
description: string;
icon_key: string | null;
sort_order: number;
title_en?: string | null;
description_en?: string | null;
}
export interface BusinessCard {
id: number;
slug: string;
name: string;
role: string | null;
company: string | null;
phone: string | null;
email: string | null;
telegram: string | null;
photo: string | null;
product_title: string | null;
product_subtitle: string | null;
product_url: string | null;
site_url: string | null;
is_published?: boolean;
}
export interface LandingContent {
sections: Record<string, Record<string, unknown>>;
sections_en?: Record<string, Record<string, unknown>>;
products: Product[];
approach: ApproachItem[];
settings: Record<string, string>;
}
async function request<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...(options.headers || {}),
},
...options,
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `request_failed_${res.status}`);
}
return res.json();
}
export const api = {
getContent: () => request<LandingContent>('/api/content'),
login: (login: string, password: string) =>
request<{ ok: true; login: string }>('/api/admin/login', {
method: 'POST',
body: JSON.stringify({ login, password }),
}),
logout: () =>
request<{ ok: true }>('/api/admin/logout', { method: 'POST' }),
me: () => request<{ admin: { id: number; login: string } }>('/api/admin/me'),
listProducts: () =>
request<{ products: Product[] }>('/api/admin/products'),
updateProduct: (id: number, data: Partial<Product>) =>
request<{ product: Product }>(`/api/admin/products/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
createProduct: (data: Omit<Product, 'id'>) =>
request<{ product: Product }>('/api/admin/products', {
method: 'POST',
body: JSON.stringify(data),
}),
deleteProduct: (id: number) =>
request<{ ok: true }>(`/api/admin/products/${id}`, {
method: 'DELETE',
}),
listSections: () => request<{ sections: unknown[] }>('/api/admin/sections'),
updateSection: (key: string, data: { content_json?: unknown; title?: string }) =>
request<{ section: unknown }>(`/api/admin/sections/${key}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
listApproach: () => request<{ items: ApproachItem[] }>('/api/admin/approach'),
updateApproach: (id: number, data: Partial<ApproachItem>) =>
request<{ item: ApproachItem }>(`/api/admin/approach/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
listSettings: () =>
request<{ settings: Record<string, string> }>('/api/admin/settings'),
updateSetting: (key: string, value: string) =>
request<{ ok: true }>(`/api/admin/settings/${key}`, {
method: 'PUT',
body: JSON.stringify({ value }),
}),
getCard: (slug: string) =>
request<{ card: BusinessCard }>(`/api/cards/${slug}`),
listCards: () =>
request<{ cards: BusinessCard[] }>('/api/admin/cards'),
createCard: (data: Partial<BusinessCard>) =>
request<{ card: BusinessCard }>('/api/admin/cards', {
method: 'POST',
body: JSON.stringify(data),
}),
updateCard: (id: number, data: Partial<BusinessCard>) =>
request<{ card: BusinessCard }>(`/api/admin/cards/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
deleteCard: (id: number) =>
request<{ ok: true }>(`/api/admin/cards/${id}`, { method: 'DELETE' }),
};