feat: information pages — admin editor with TipTap, public viewer

Admin panel:
- New section "Информационные страницы" in admin nav
- Admin list: slug, title, active toggle, edit/delete, sort order
- Admin editor: full TipTap (bold, italic, headings, lists, links,
  image/video upload, alignment, highlight, code blocks)
  with locale tabs (RU/EN/ZH/FA) for title and content
- Create/edit via URL params, slug auto-generation

Public:
- /info/:slug route with DOMPurify sanitized content
- Locale fallback, Telegram safe area, loading skeleton, 404

i18n: all 4 locales
This commit is contained in:
Fringg
2026-04-24 08:11:20 +03:00
parent d43638b34e
commit 7d6d0ba344
10 changed files with 1748 additions and 4 deletions

96
src/api/infoPages.ts Normal file
View File

@@ -0,0 +1,96 @@
import apiClient from './client';
export interface InfoPage {
id: number;
slug: string;
title: Record<string, string>;
content: Record<string, string>;
is_active: boolean;
sort_order: number;
icon: string | null;
created_at: string;
updated_at: string | null;
}
export interface InfoPageListItem {
id: number;
slug: string;
title: Record<string, string>;
is_active: boolean;
sort_order: number;
icon: string | null;
updated_at: string | null;
}
export interface InfoPageCreateRequest {
slug: string;
title: Record<string, string>;
content: Record<string, string>;
is_active: boolean;
sort_order: number;
icon: string | null;
}
export interface InfoPageUpdateRequest {
slug?: string;
title?: Record<string, string>;
content?: Record<string, string>;
is_active?: boolean;
sort_order?: number;
icon?: string | null;
}
export interface InfoPageReorderRequest {
items: Array<{ id: number; sort_order: number }>;
}
export const infoPagesApi = {
// Public endpoints
getPages: async (): Promise<InfoPageListItem[]> => {
const response = await apiClient.get<InfoPageListItem[]>('/cabinet/info-pages');
return response.data;
},
getPageBySlug: async (slug: string): Promise<InfoPage> => {
const response = await apiClient.get<InfoPage>(
`/cabinet/info-pages/${encodeURIComponent(slug)}`,
);
return response.data;
},
// Admin endpoints
getAdminPages: async (): Promise<InfoPageListItem[]> => {
const response = await apiClient.get<InfoPageListItem[]>('/cabinet/admin/info-pages');
return response.data;
},
getAdminPage: async (id: number): Promise<InfoPage> => {
const response = await apiClient.get<InfoPage>(`/cabinet/admin/info-pages/${id}`);
return response.data;
},
createPage: async (data: InfoPageCreateRequest): Promise<InfoPage> => {
const response = await apiClient.post<InfoPage>('/cabinet/admin/info-pages', data);
return response.data;
},
updatePage: async (id: number, data: InfoPageUpdateRequest): Promise<InfoPage> => {
const response = await apiClient.put<InfoPage>(`/cabinet/admin/info-pages/${id}`, data);
return response.data;
},
deletePage: async (id: number): Promise<void> => {
await apiClient.delete(`/cabinet/admin/info-pages/${id}`);
},
toggleActive: async (id: number): Promise<InfoPage> => {
const response = await apiClient.post<InfoPage>(
`/cabinet/admin/info-pages/${id}/toggle-active`,
);
return response.data;
},
reorder: async (data: InfoPageReorderRequest): Promise<void> => {
await apiClient.post('/cabinet/admin/info-pages/reorder', data);
},
};