mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, memo } from 'react';
|
||||
import { useCallback, useState, memo } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -7,7 +7,10 @@ import { AdminBackButton } from '../components/admin';
|
||||
import { Toggle } from '../components/admin/Toggle';
|
||||
import { useHapticFeedback } from '../platform/hooks/useHaptic';
|
||||
import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
|
||||
import type { InfoPageListItem } from '../api/infoPages';
|
||||
import { cn } from '../lib/utils';
|
||||
import type { InfoPageListItem, InfoPageType } from '../api/infoPages';
|
||||
|
||||
type FilterTab = 'all' | 'page' | 'faq';
|
||||
|
||||
// Icons
|
||||
const PlusIcon = () => (
|
||||
@@ -118,6 +121,15 @@ const PageRow = memo(function PageRow({
|
||||
<span className="rounded-full bg-dark-700 px-2 py-0.5 font-mono text-[10px] font-medium text-dark-300">
|
||||
/{page.slug}
|
||||
</span>
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${
|
||||
page.page_type === 'faq'
|
||||
? 'bg-warning-500/20 text-warning-400'
|
||||
: 'bg-accent-500/20 text-accent-400'
|
||||
}`}
|
||||
>
|
||||
{page.page_type === 'faq' ? 'FAQ' : t('admin.infoPages.typePage')}
|
||||
</span>
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${
|
||||
page.is_active
|
||||
@@ -212,14 +224,17 @@ export default function AdminInfoPages() {
|
||||
const haptic = useHapticFeedback();
|
||||
const confirm = useDestructiveConfirm();
|
||||
const currentLocale = i18n.language.split('-')[0];
|
||||
const [activeFilter, setActiveFilter] = useState<FilterTab>('all');
|
||||
|
||||
const filterParam: InfoPageType | undefined = activeFilter === 'all' ? undefined : activeFilter;
|
||||
|
||||
const {
|
||||
data: pages,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ['admin', 'info-pages', 'list'],
|
||||
queryFn: () => infoPagesApi.getAdminPages(),
|
||||
queryKey: ['admin', 'info-pages', 'list', activeFilter],
|
||||
queryFn: () => infoPagesApi.getAdminPages(filterParam),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
|
||||
@@ -283,6 +298,17 @@ export default function AdminInfoPages() {
|
||||
>
|
||||
<RefreshIcon />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
haptic.buttonPress();
|
||||
navigate('/admin/info-pages/create?type=faq');
|
||||
}}
|
||||
className="flex min-h-[44px] items-center gap-2 rounded-lg bg-warning-500/80 px-4 py-2.5 text-white transition-colors hover:bg-warning-500"
|
||||
aria-label={t('admin.infoPages.createFaq')}
|
||||
>
|
||||
<PlusIcon />
|
||||
<span className="hidden sm:inline">{t('admin.infoPages.createFaq')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
haptic.buttonPress();
|
||||
@@ -297,6 +323,25 @@ export default function AdminInfoPages() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter tabs */}
|
||||
<div className="flex gap-1">
|
||||
{(['all', 'page', 'faq'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
type="button"
|
||||
onClick={() => setActiveFilter(tab)}
|
||||
className={cn(
|
||||
'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium transition-colors',
|
||||
activeFilter === tab
|
||||
? 'bg-accent-500 text-white'
|
||||
: 'bg-dark-700 text-dark-300 hover:bg-dark-600 hover:text-dark-100',
|
||||
)}
|
||||
>
|
||||
{t(`admin.infoPages.filter.${tab}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pages list */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user