diff --git a/src/api/infoPages.ts b/src/api/infoPages.ts index 089e604..994560b 100644 --- a/src/api/infoPages.ts +++ b/src/api/infoPages.ts @@ -1,10 +1,13 @@ import apiClient from './client'; +export type InfoPageType = 'page' | 'faq'; + export interface InfoPage { id: number; slug: string; title: Record; content: Record; + 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; + page_type: InfoPageType; is_active: boolean; sort_order: number; icon: string | null; @@ -26,6 +30,7 @@ export interface InfoPageCreateRequest { slug: string; title: Record; content: Record; + page_type: InfoPageType; is_active: boolean; sort_order: number; icon: string | null; @@ -35,19 +40,27 @@ export interface InfoPageUpdateRequest { slug?: string; title?: Record; content?: Record; + 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 => { - const response = await apiClient.get('/cabinet/info-pages'); + getPages: async (pageType?: InfoPageType): Promise => { + const params = pageType ? { page_type: pageType } : undefined; + const response = await apiClient.get('/cabinet/info-pages', { params }); return response.data; }, @@ -59,8 +72,11 @@ export const infoPagesApi = { }, // Admin endpoints - getAdminPages: async (): Promise => { - const response = await apiClient.get('/cabinet/admin/info-pages'); + getAdminPages: async (pageType?: InfoPageType): Promise => { + const params = pageType ? { page_type: pageType } : undefined; + const response = await apiClient.get('/cabinet/admin/info-pages', { + params, + }); return response.data; }, diff --git a/src/locales/en.json b/src/locales/en.json index a5eda77..46a1db5 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -3804,6 +3804,7 @@ "title": "Information Pages", "subtitle": "Manage static information pages", "create": "Create Page", + "createFaq": "Create FAQ", "edit": "Edit", "save": "Save", "saving": "Saving...", @@ -3815,6 +3816,7 @@ "notFound": "Page not found", "active": "Active", "inactive": "Inactive", + "typePage": "Page", "localeLabel": "Content Language", "fields": { "slug": "URL Slug", @@ -3822,7 +3824,34 @@ "content": "Content", "isActive": "Active", "sortOrder": "Sort Order", - "icon": "Icon (emoji)" + "icon": "Icon (emoji)", + "pageType": "Page Type" + }, + "filter": { + "all": "All", + "page": "Pages", + "faq": "FAQ" + }, + "pageTypes": { + "page": "Page", + "faq": "FAQ" + }, + "faq": { + "questions": "Questions & Answers", + "questionsCount": "questions", + "noQuestions": "No questions yet. Click the button below to add one.", + "questionNumber": "Question #{{n}}", + "question": "Question", + "answer": "Answer", + "questionPlaceholder": "Enter question...", + "answerPlaceholder": "Enter answer (HTML supported)...", + "htmlHint": "HTML markup is supported", + "addQuestion": "Add Question", + "removeQuestion": "Remove question", + "moveUp": "Move up", + "moveDown": "Move down", + "searchPlaceholder": "Search questions...", + "noResults": "No results found" }, "locales": { "ru": "Russian", diff --git a/src/locales/fa.json b/src/locales/fa.json index a570208..056c1cf 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -3372,6 +3372,7 @@ "title": "صفحات اطلاعات", "subtitle": "مدیریت صفحات اطلاعات ثابت", "create": "ایجاد صفحه", + "createFaq": "ایجاد FAQ", "edit": "ویرایش", "save": "ذخیره", "saving": "در حال ذخیره...", @@ -3383,6 +3384,7 @@ "notFound": "صفحه پیدا نشد", "active": "فعال", "inactive": "غیرفعال", + "typePage": "صفحه", "localeLabel": "زبان محتوا", "fields": { "slug": "شناسه URL", @@ -3390,7 +3392,34 @@ "content": "محتوا", "isActive": "فعال", "sortOrder": "ترتیب", - "icon": "آیکون (ایموجی)" + "icon": "آیکون (ایموجی)", + "pageType": "نوع صفحه" + }, + "filter": { + "all": "همه", + "page": "صفحات", + "faq": "FAQ" + }, + "pageTypes": { + "page": "صفحه", + "faq": "FAQ" + }, + "faq": { + "questions": "سوالات و پاسخ‌ها", + "questionsCount": "سوال", + "noQuestions": "هنوز سوالی وجود ندارد. روی دکمه زیر کلیک کنید.", + "questionNumber": "سوال #{{n}}", + "question": "سوال", + "answer": "پاسخ", + "questionPlaceholder": "سوال را وارد کنید...", + "answerPlaceholder": "پاسخ را وارد کنید (HTML پشتیبانی می‌شود)...", + "htmlHint": "نشانه‌گذاری HTML پشتیبانی می‌شود", + "addQuestion": "افزودن سوال", + "removeQuestion": "حذف سوال", + "moveUp": "انتقال به بالا", + "moveDown": "انتقال به پایین", + "searchPlaceholder": "جستجو در سوالات...", + "noResults": "نتیجه‌ای یافت نشد" }, "locales": { "ru": "روسی", diff --git a/src/locales/ru.json b/src/locales/ru.json index 9f4def2..873ea88 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -4348,6 +4348,7 @@ "title": "Информационные страницы", "subtitle": "Управление статическими информационными страницами", "create": "Создать страницу", + "createFaq": "Создать FAQ", "edit": "Редактировать", "save": "Сохранить", "saving": "Сохранение...", @@ -4359,6 +4360,7 @@ "notFound": "Страница не найдена", "active": "Активна", "inactive": "Неактивна", + "typePage": "Страница", "localeLabel": "Язык контента", "fields": { "slug": "URL-адрес", @@ -4366,7 +4368,34 @@ "content": "Содержание", "isActive": "Активна", "sortOrder": "Порядок сортировки", - "icon": "Иконка (эмодзи)" + "icon": "Иконка (эмодзи)", + "pageType": "Тип страницы" + }, + "filter": { + "all": "Все", + "page": "Страницы", + "faq": "FAQ" + }, + "pageTypes": { + "page": "Страница", + "faq": "FAQ" + }, + "faq": { + "questions": "Вопросы и ответы", + "questionsCount": "вопросов", + "noQuestions": "Вопросов пока нет. Нажмите кнопку ниже, чтобы добавить.", + "questionNumber": "Вопрос #{{n}}", + "question": "Вопрос", + "answer": "Ответ", + "questionPlaceholder": "Введите вопрос...", + "answerPlaceholder": "Введите ответ (поддерживается HTML)...", + "htmlHint": "Поддерживается HTML-разметка", + "addQuestion": "Добавить вопрос", + "removeQuestion": "Удалить вопрос", + "moveUp": "Переместить вверх", + "moveDown": "Переместить вниз", + "searchPlaceholder": "Поиск по вопросам...", + "noResults": "Ничего не найдено" }, "locales": { "ru": "Русский", diff --git a/src/locales/zh.json b/src/locales/zh.json index 0eb5726..9748c77 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -3371,6 +3371,7 @@ "title": "信息页面", "subtitle": "管理静态信息页面", "create": "创建页面", + "createFaq": "创建FAQ", "edit": "编辑", "save": "保存", "saving": "保存中...", @@ -3382,6 +3383,7 @@ "notFound": "页面未找到", "active": "已激活", "inactive": "未激活", + "typePage": "页面", "localeLabel": "内容语言", "fields": { "slug": "URL标识", @@ -3389,7 +3391,34 @@ "content": "内容", "isActive": "已激活", "sortOrder": "排序", - "icon": "图标(表情)" + "icon": "图标(表情)", + "pageType": "页面类型" + }, + "filter": { + "all": "全部", + "page": "页面", + "faq": "FAQ" + }, + "pageTypes": { + "page": "页面", + "faq": "FAQ" + }, + "faq": { + "questions": "问答", + "questionsCount": "个问题", + "noQuestions": "暂无问题。点击下方按钮添加。", + "questionNumber": "问题 #{{n}}", + "question": "问题", + "answer": "答案", + "questionPlaceholder": "输入问题...", + "answerPlaceholder": "输入答案(支持HTML)...", + "htmlHint": "支持HTML标记", + "addQuestion": "添加问题", + "removeQuestion": "删除问题", + "moveUp": "上移", + "moveDown": "下移", + "searchPlaceholder": "搜索问题...", + "noResults": "未找到结果" }, "locales": { "ru": "俄语", diff --git a/src/pages/AdminInfoPageEditor.tsx b/src/pages/AdminInfoPageEditor.tsx index 7be6149..2b242eb 100644 --- a/src/pages/AdminInfoPageEditor.tsx +++ b/src/pages/AdminInfoPageEditor.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useMemo, useRef, useCallback } from 'react'; -import { useNavigate, useParams } from 'react-router'; +import { useNavigate, useParams, useSearchParams } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useEditor, EditorContent } from '@tiptap/react'; @@ -17,6 +17,7 @@ import { AdminBackButton } from '../components/admin'; import { Toggle } from '../components/admin/Toggle'; import { useHapticFeedback } from '../platform/hooks/useHaptic'; import { cn } from '../lib/utils'; +import type { InfoPageType, FaqItem } from '../api/infoPages'; const AVAILABLE_LOCALES = ['ru', 'en', 'zh', 'fa'] as const; type LocaleCode = (typeof AVAILABLE_LOCALES)[number]; @@ -204,14 +205,203 @@ function generateSlug(title: string): string { .substring(0, 100); } +// --- FAQ Q&A Item Icons --- +const ChevronUpIcon = () => ( + + + +); + +const ChevronDownIcon = () => ( + + + +); + +const TrashSmallIcon = () => ( + + + +); + +const PlusSmallIcon = () => ( + + + +); + +// --- FAQ Q&A Builder --- +interface FaqBuilderProps { + items: FaqItem[]; + onChange: (items: FaqItem[]) => void; + locale: string; + localeLabel: string; +} + +function FaqBuilder({ items, onChange, locale, localeLabel }: FaqBuilderProps) { + const { t } = useTranslation(); + + const handleQuestionChange = useCallback( + (index: number, value: string) => { + const updated = items.map((item, i) => (i === index ? { ...item, q: value } : item)); + onChange(updated); + }, + [items, onChange], + ); + + const handleAnswerChange = useCallback( + (index: number, value: string) => { + const updated = items.map((item, i) => (i === index ? { ...item, a: value } : item)); + onChange(updated); + }, + [items, onChange], + ); + + const handleRemove = useCallback( + (index: number) => { + onChange(items.filter((_, i) => i !== index)); + }, + [items, onChange], + ); + + const handleAdd = useCallback(() => { + onChange([...items, { q: '', a: '' }]); + }, [items, onChange]); + + const handleMoveUp = useCallback( + (index: number) => { + if (index === 0) return; + const updated = [...items]; + [updated[index - 1], updated[index]] = [updated[index], updated[index - 1]]; + onChange(updated); + }, + [items, onChange], + ); + + const handleMoveDown = useCallback( + (index: number) => { + if (index >= items.length - 1) return; + const updated = [...items]; + [updated[index], updated[index + 1]] = [updated[index + 1], updated[index]]; + onChange(updated); + }, + [items, onChange], + ); + + void locale; + + return ( +
+
+ + + {items.length} {t('admin.infoPages.faq.questionsCount')} + +
+ + {items.length === 0 && ( +
+ {t('admin.infoPages.faq.noQuestions')} +
+ )} + + {items.map((item, index) => ( +
+
+ + {t('admin.infoPages.faq.questionNumber', { n: index + 1 })} + +
+ + + +
+
+ +
+
+ + handleQuestionChange(index, e.target.value)} + className="input text-sm" + placeholder={t('admin.infoPages.faq.questionPlaceholder')} + /> +
+
+ +