feat: FAQ pages — Q&A builder in admin, accordion view for users

Admin editor:
- page_type selector: "Страница" / "FAQ" radio buttons
- When FAQ: FaqBuilder component replaces TipTap editor
  - Question input + answer textarea per Q&A item
  - Add/remove items, up/down reorder buttons
  - Per-locale Q&A (stored as JSON array in content JSONB)
- "Создать FAQ" button in list alongside "Создать страницу"
- Filter tabs: Все / Страницы / FAQ
- Page type badge on each row (accent for pages, amber for FAQ)

Public view (InfoPageView):
- Auto-detects page_type='faq' and renders FaqView component
- Accordion with smooth height animation (300ms ease-in-out)
- Search filter appears when >3 questions
- DOMPurify sanitization on FAQ answers
- Single-item expand (click to open/close)

i18n: full FAQ keys for all 4 locales (questions, answers,
search, counts, empty states, builder labels)
This commit is contained in:
Fringg
2026-04-24 14:02:03 +03:00
parent 596f638cdd
commit 1ee0f18343
8 changed files with 781 additions and 192 deletions

View File

@@ -1,10 +1,13 @@
import apiClient from './client';
export type InfoPageType = 'page' | 'faq';
export interface InfoPage {
id: number;
slug: string;
title: Record<string, string>;
content: Record<string, string>;
page_type: InfoPageType;
is_active: boolean;
sort_order: number;
icon: string | null;
@@ -16,6 +19,7 @@ export interface InfoPageListItem {
id: number;
slug: string;
title: Record<string, string>;
page_type: InfoPageType;
is_active: boolean;
sort_order: number;
icon: string | null;
@@ -26,6 +30,7 @@ export interface InfoPageCreateRequest {
slug: string;
title: Record<string, string>;
content: Record<string, string>;
page_type: InfoPageType;
is_active: boolean;
sort_order: number;
icon: string | null;
@@ -35,19 +40,27 @@ export interface InfoPageUpdateRequest {
slug?: string;
title?: Record<string, string>;
content?: Record<string, string>;
page_type?: InfoPageType;
is_active?: boolean;
sort_order?: number;
icon?: string | null;
}
/** Single FAQ Q&A item stored in content JSONB. */
export interface FaqItem {
q: string;
a: string;
}
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');
getPages: async (pageType?: InfoPageType): Promise<InfoPageListItem[]> => {
const params = pageType ? { page_type: pageType } : undefined;
const response = await apiClient.get<InfoPageListItem[]>('/cabinet/info-pages', { params });
return response.data;
},
@@ -59,8 +72,11 @@ export const infoPagesApi = {
},
// Admin endpoints
getAdminPages: async (): Promise<InfoPageListItem[]> => {
const response = await apiClient.get<InfoPageListItem[]>('/cabinet/admin/info-pages');
getAdminPages: async (pageType?: InfoPageType): Promise<InfoPageListItem[]> => {
const params = pageType ? { page_type: pageType } : undefined;
const response = await apiClient.get<InfoPageListItem[]>('/cabinet/admin/info-pages', {
params,
});
return response.data;
},