mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix(admin): harden legal pages editor state handling
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import apiClient from './client';
|
import apiClient from './client';
|
||||||
|
import type { InfoPageDisplayMode } from './infoPages';
|
||||||
|
|
||||||
export type LegalDisplayMode = 'bot' | 'web' | 'both';
|
export type LegalDisplayMode = InfoPageDisplayMode;
|
||||||
|
|
||||||
export interface LegalDocumentItem {
|
export interface LegalDocumentItem {
|
||||||
language: string;
|
language: string;
|
||||||
@@ -26,7 +27,7 @@ export interface RulesItem {
|
|||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RulesResponse {
|
export interface AdminRulesResponse {
|
||||||
display_mode: LegalDisplayMode;
|
display_mode: LegalDisplayMode;
|
||||||
display_mode_env_locked: boolean;
|
display_mode_env_locked: boolean;
|
||||||
items: RulesItem[];
|
items: RulesItem[];
|
||||||
@@ -110,13 +111,16 @@ export const adminLegalPagesApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getRules: async (): Promise<RulesResponse> => {
|
getRules: async (): Promise<AdminRulesResponse> => {
|
||||||
const response = await apiClient.get<RulesResponse>('/cabinet/admin/legal-pages/rules');
|
const response = await apiClient.get<AdminRulesResponse>('/cabinet/admin/legal-pages/rules');
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateRules: async (data: RulesUpdateRequest): Promise<RulesResponse> => {
|
updateRules: async (data: RulesUpdateRequest): Promise<AdminRulesResponse> => {
|
||||||
const response = await apiClient.put<RulesResponse>('/cabinet/admin/legal-pages/rules', data);
|
const response = await apiClient.put<AdminRulesResponse>(
|
||||||
|
'/cabinet/admin/legal-pages/rules',
|
||||||
|
data,
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -4158,6 +4158,7 @@
|
|||||||
"save": "Save",
|
"save": "Save",
|
||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
"saveError": "Failed to save. Please try again.",
|
"saveError": "Failed to save. Please try again.",
|
||||||
|
"unsavedWarning": "Unsaved changes will be lost. Continue?",
|
||||||
"faqPages": "Questions and answers",
|
"faqPages": "Questions and answers",
|
||||||
"addQuestion": "Add question",
|
"addQuestion": "Add question",
|
||||||
"newQuestion": "New question",
|
"newQuestion": "New question",
|
||||||
|
|||||||
@@ -4703,6 +4703,7 @@
|
|||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"saving": "Сохранение...",
|
"saving": "Сохранение...",
|
||||||
"saveError": "Не удалось сохранить. Попробуйте ещё раз.",
|
"saveError": "Не удалось сохранить. Попробуйте ещё раз.",
|
||||||
|
"unsavedWarning": "Несохранённые изменения будут потеряны. Продолжить?",
|
||||||
"faqPages": "Вопросы и ответы",
|
"faqPages": "Вопросы и ответы",
|
||||||
"addQuestion": "Добавить вопрос",
|
"addQuestion": "Добавить вопрос",
|
||||||
"newQuestion": "Новый вопрос",
|
"newQuestion": "Новый вопрос",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
@@ -99,7 +99,13 @@ function LanguageTabs({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DocumentEditor({ kind }: { kind: 'privacy-policy' | 'public-offer' }) {
|
function DocumentEditor({
|
||||||
|
kind,
|
||||||
|
onDirtyChange,
|
||||||
|
}: {
|
||||||
|
kind: 'privacy-policy' | 'public-offer';
|
||||||
|
onDirtyChange: (dirty: boolean) => void;
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const haptic = useHapticFeedback();
|
const haptic = useHapticFeedback();
|
||||||
@@ -110,17 +116,18 @@ function DocumentEditor({ kind }: { kind: 'privacy-policy' | 'public-offer' }) {
|
|||||||
const [populated, setPopulated] = useState(false);
|
const [populated, setPopulated] = useState(false);
|
||||||
const [saveError, setSaveError] = useState<string | null>(null);
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading, isFetching } = useQuery({
|
||||||
queryKey: ['admin', 'legal-pages', kind],
|
queryKey: ['admin', 'legal-pages', kind],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
kind === 'privacy-policy'
|
kind === 'privacy-policy'
|
||||||
? adminLegalPagesApi.getPrivacyPolicy()
|
? adminLegalPagesApi.getPrivacyPolicy()
|
||||||
: adminLegalPagesApi.getPublicOffer(),
|
: adminLegalPagesApi.getPublicOffer(),
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
|
gcTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data || populated) return;
|
if (!data || isFetching || populated) return;
|
||||||
setDisplayMode(data.display_mode);
|
setDisplayMode(data.display_mode);
|
||||||
const nextContents: Record<string, string> = {};
|
const nextContents: Record<string, string> = {};
|
||||||
const nextEnabled: Record<string, boolean> = {};
|
const nextEnabled: Record<string, boolean> = {};
|
||||||
@@ -134,7 +141,21 @@ function DocumentEditor({ kind }: { kind: 'privacy-policy' | 'public-offer' }) {
|
|||||||
setActiveLang(data.items[0].language);
|
setActiveLang(data.items[0].language);
|
||||||
}
|
}
|
||||||
setPopulated(true);
|
setPopulated(true);
|
||||||
}, [data, populated]);
|
}, [data, isFetching, populated]);
|
||||||
|
|
||||||
|
const isDirty =
|
||||||
|
populated &&
|
||||||
|
!!data &&
|
||||||
|
(displayMode !== data.display_mode ||
|
||||||
|
data.items.some(
|
||||||
|
(item) =>
|
||||||
|
(contents[item.language] ?? '') !== item.content ||
|
||||||
|
(enabled[item.language] ?? false) !== item.is_enabled,
|
||||||
|
));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDirtyChange(isDirty);
|
||||||
|
}, [isDirty, onDirtyChange]);
|
||||||
|
|
||||||
const saveMutation = useMutation({
|
const saveMutation = useMutation({
|
||||||
mutationFn: () => {
|
mutationFn: () => {
|
||||||
@@ -187,7 +208,10 @@ function DocumentEditor({ kind }: { kind: 'privacy-policy' | 'public-offer' }) {
|
|||||||
<label className="label">{t('admin.legalPages.content')}</label>
|
<label className="label">{t('admin.legalPages.content')}</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={contents[activeLang] ?? ''}
|
value={contents[activeLang] ?? ''}
|
||||||
onChange={(e) => setContents((prev) => ({ ...prev, [activeLang]: e.target.value }))}
|
onChange={(e) => {
|
||||||
|
setSaveError(null);
|
||||||
|
setContents((prev) => ({ ...prev, [activeLang]: e.target.value }));
|
||||||
|
}}
|
||||||
rows={16}
|
rows={16}
|
||||||
className="input min-h-[320px] w-full font-mono text-sm"
|
className="input min-h-[320px] w-full font-mono text-sm"
|
||||||
placeholder={t('admin.legalPages.contentPlaceholder')}
|
placeholder={t('admin.legalPages.contentPlaceholder')}
|
||||||
@@ -205,7 +229,7 @@ function DocumentEditor({ kind }: { kind: 'privacy-policy' | 'public-offer' }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RulesEditor() {
|
function RulesEditor({ onDirtyChange }: { onDirtyChange: (dirty: boolean) => void }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const haptic = useHapticFeedback();
|
const haptic = useHapticFeedback();
|
||||||
@@ -215,14 +239,15 @@ function RulesEditor() {
|
|||||||
const [populated, setPopulated] = useState(false);
|
const [populated, setPopulated] = useState(false);
|
||||||
const [saveError, setSaveError] = useState<string | null>(null);
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading, isFetching } = useQuery({
|
||||||
queryKey: ['admin', 'legal-pages', 'rules'],
|
queryKey: ['admin', 'legal-pages', 'rules'],
|
||||||
queryFn: adminLegalPagesApi.getRules,
|
queryFn: adminLegalPagesApi.getRules,
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
|
gcTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data || populated) return;
|
if (!data || isFetching || populated) return;
|
||||||
setDisplayMode(data.display_mode);
|
setDisplayMode(data.display_mode);
|
||||||
const nextContents: Record<string, string> = {};
|
const nextContents: Record<string, string> = {};
|
||||||
for (const item of data.items) {
|
for (const item of data.items) {
|
||||||
@@ -233,7 +258,17 @@ function RulesEditor() {
|
|||||||
setActiveLang(data.items[0].language);
|
setActiveLang(data.items[0].language);
|
||||||
}
|
}
|
||||||
setPopulated(true);
|
setPopulated(true);
|
||||||
}, [data, populated]);
|
}, [data, isFetching, populated]);
|
||||||
|
|
||||||
|
const isDirty =
|
||||||
|
populated &&
|
||||||
|
!!data &&
|
||||||
|
(displayMode !== data.display_mode ||
|
||||||
|
data.items.some((item) => (contents[item.language] ?? '') !== item.content));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDirtyChange(isDirty);
|
||||||
|
}, [isDirty, onDirtyChange]);
|
||||||
|
|
||||||
const saveMutation = useMutation({
|
const saveMutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
@@ -273,7 +308,10 @@ function RulesEditor() {
|
|||||||
<label className="label">{t('admin.legalPages.content')}</label>
|
<label className="label">{t('admin.legalPages.content')}</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={contents[activeLang] ?? ''}
|
value={contents[activeLang] ?? ''}
|
||||||
onChange={(e) => setContents((prev) => ({ ...prev, [activeLang]: e.target.value }))}
|
onChange={(e) => {
|
||||||
|
setSaveError(null);
|
||||||
|
setContents((prev) => ({ ...prev, [activeLang]: e.target.value }));
|
||||||
|
}}
|
||||||
rows={16}
|
rows={16}
|
||||||
className="input min-h-[320px] w-full font-mono text-sm"
|
className="input min-h-[320px] w-full font-mono text-sm"
|
||||||
placeholder={t('admin.legalPages.contentPlaceholder')}
|
placeholder={t('admin.legalPages.contentPlaceholder')}
|
||||||
@@ -299,6 +337,7 @@ function FaqQuestionRow({
|
|||||||
onMoveDown,
|
onMoveDown,
|
||||||
onDelete,
|
onDelete,
|
||||||
onSaved,
|
onSaved,
|
||||||
|
onDirtyChange,
|
||||||
}: {
|
}: {
|
||||||
page: FaqPageItem;
|
page: FaqPageItem;
|
||||||
canMoveUp: boolean;
|
canMoveUp: boolean;
|
||||||
@@ -307,20 +346,34 @@ function FaqQuestionRow({
|
|||||||
onMoveDown: () => void;
|
onMoveDown: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onSaved: () => void;
|
onSaved: () => void;
|
||||||
|
onDirtyChange: (id: number, dirty: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const haptic = useHapticFeedback();
|
const haptic = useHapticFeedback();
|
||||||
const [title, setTitle] = useState(page.title);
|
const [title, setTitle] = useState(page.title);
|
||||||
const [content, setContent] = useState(page.content);
|
const [content, setContent] = useState(page.content);
|
||||||
const [isActive, setIsActive] = useState(page.is_active);
|
const [isActive, setIsActive] = useState(page.is_active);
|
||||||
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const isDirty = title !== page.title || content !== page.content || isActive !== page.is_active;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDirtyChange(page.id, isDirty);
|
||||||
|
return () => onDirtyChange(page.id, false);
|
||||||
|
}, [page.id, isDirty, onDirtyChange]);
|
||||||
|
|
||||||
const saveMutation = useMutation({
|
const saveMutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
adminLegalPagesApi.updateFaqPage(page.id, { title, content, is_active: isActive }),
|
adminLegalPagesApi.updateFaqPage(page.id, { title, content, is_active: isActive }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
haptic.success();
|
haptic.success();
|
||||||
|
setSaveError(null);
|
||||||
onSaved();
|
onSaved();
|
||||||
},
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
haptic.error();
|
||||||
|
setSaveError(extractErrorDetail(err) ?? t('admin.legalPages.saveError'));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -328,7 +381,10 @@ function FaqQuestionRow({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setSaveError(null);
|
||||||
|
setTitle(e.target.value);
|
||||||
|
}}
|
||||||
className="input flex-1"
|
className="input flex-1"
|
||||||
placeholder={t('admin.legalPages.questionTitle')}
|
placeholder={t('admin.legalPages.questionTitle')}
|
||||||
/>
|
/>
|
||||||
@@ -366,11 +422,15 @@ function FaqQuestionRow({
|
|||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
value={content}
|
value={content}
|
||||||
onChange={(e) => setContent(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setSaveError(null);
|
||||||
|
setContent(e.target.value);
|
||||||
|
}}
|
||||||
rows={4}
|
rows={4}
|
||||||
className="input w-full font-mono text-sm"
|
className="input w-full font-mono text-sm"
|
||||||
placeholder={t('admin.legalPages.questionContent')}
|
placeholder={t('admin.legalPages.questionContent')}
|
||||||
/>
|
/>
|
||||||
|
{saveError && <p className="text-sm text-error-400">{saveError}</p>}
|
||||||
<button
|
<button
|
||||||
onClick={() => saveMutation.mutate()}
|
onClick={() => saveMutation.mutate()}
|
||||||
disabled={saveMutation.isPending}
|
disabled={saveMutation.isPending}
|
||||||
@@ -382,7 +442,7 @@ function FaqQuestionRow({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FaqEditor() {
|
function FaqEditor({ onDirtyChange }: { onDirtyChange: (dirty: boolean) => void }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const haptic = useHapticFeedback();
|
const haptic = useHapticFeedback();
|
||||||
@@ -391,21 +451,36 @@ function FaqEditor() {
|
|||||||
const [activeLang, setActiveLang] = useState('ru');
|
const [activeLang, setActiveLang] = useState('ru');
|
||||||
const [populated, setPopulated] = useState(false);
|
const [populated, setPopulated] = useState(false);
|
||||||
const [saveError, setSaveError] = useState<string | null>(null);
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
|
const [dirtyRows, setDirtyRows] = useState<number[]>([]);
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading, isFetching } = useQuery({
|
||||||
queryKey: ['admin', 'legal-pages', 'faq'],
|
queryKey: ['admin', 'legal-pages', 'faq'],
|
||||||
queryFn: adminLegalPagesApi.getFaq,
|
queryFn: adminLegalPagesApi.getFaq,
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
|
gcTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data || populated) return;
|
if (!data || isFetching || populated) return;
|
||||||
setDisplayMode(data.display_mode);
|
setDisplayMode(data.display_mode);
|
||||||
if (data.settings.length > 0 && !data.settings.some((s) => s.language === 'ru')) {
|
if (data.settings.length > 0 && !data.settings.some((s) => s.language === 'ru')) {
|
||||||
setActiveLang(data.settings[0].language);
|
setActiveLang(data.settings[0].language);
|
||||||
}
|
}
|
||||||
setPopulated(true);
|
setPopulated(true);
|
||||||
}, [data, populated]);
|
}, [data, isFetching, populated]);
|
||||||
|
|
||||||
|
const handleRowDirtyChange = useCallback((id: number, dirty: boolean) => {
|
||||||
|
setDirtyRows((prev) => {
|
||||||
|
if (dirty) return prev.includes(id) ? prev : [...prev, id];
|
||||||
|
return prev.includes(id) ? prev.filter((rowId) => rowId !== id) : prev;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hasDirtyRows = dirtyRows.length > 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDirtyChange(hasDirtyRows);
|
||||||
|
}, [hasDirtyRows, onDirtyChange]);
|
||||||
|
|
||||||
const invalidate = () =>
|
const invalidate = () =>
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'legal-pages', 'faq'] });
|
queryClient.invalidateQueries({ queryKey: ['admin', 'legal-pages', 'faq'] });
|
||||||
@@ -413,10 +488,10 @@ function FaqEditor() {
|
|||||||
const settingsMutation = useMutation({
|
const settingsMutation = useMutation({
|
||||||
mutationFn: (payload: { display_mode?: LegalDisplayMode; settings?: FaqSettingItem[] }) =>
|
mutationFn: (payload: { display_mode?: LegalDisplayMode; settings?: FaqSettingItem[] }) =>
|
||||||
adminLegalPagesApi.updateFaq(payload),
|
adminLegalPagesApi.updateFaq(payload),
|
||||||
onSuccess: () => {
|
onSuccess: (resp) => {
|
||||||
haptic.success();
|
haptic.success();
|
||||||
setSaveError(null);
|
setSaveError(null);
|
||||||
invalidate();
|
queryClient.setQueryData(['admin', 'legal-pages', 'faq'], resp);
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
haptic.error();
|
haptic.error();
|
||||||
@@ -434,8 +509,13 @@ function FaqEditor() {
|
|||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
haptic.success();
|
haptic.success();
|
||||||
|
setSaveError(null);
|
||||||
invalidate();
|
invalidate();
|
||||||
},
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
haptic.error();
|
||||||
|
setSaveError(extractErrorDetail(err) ?? t('admin.legalPages.saveError'));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const reorderMutation = useMutation({
|
const reorderMutation = useMutation({
|
||||||
@@ -445,6 +525,13 @@ function FaqEditor() {
|
|||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
haptic.success();
|
haptic.success();
|
||||||
|
setSaveError(null);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
haptic.error();
|
||||||
|
setSaveError(extractErrorDetail(err) ?? t('admin.legalPages.saveError'));
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
invalidate();
|
invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -453,8 +540,13 @@ function FaqEditor() {
|
|||||||
mutationFn: (id: number) => adminLegalPagesApi.deleteFaqPage(id),
|
mutationFn: (id: number) => adminLegalPagesApi.deleteFaqPage(id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
haptic.success();
|
haptic.success();
|
||||||
|
setSaveError(null);
|
||||||
invalidate();
|
invalidate();
|
||||||
},
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
haptic.error();
|
||||||
|
setSaveError(extractErrorDetail(err) ?? t('admin.legalPages.saveError'));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading || !data) {
|
if (isLoading || !data) {
|
||||||
@@ -475,7 +567,15 @@ function FaqEditor() {
|
|||||||
}}
|
}}
|
||||||
disabled={data.display_mode_env_locked}
|
disabled={data.display_mode_env_locked}
|
||||||
/>
|
/>
|
||||||
<LanguageTabs languages={languages} active={activeLang} onChange={setActiveLang} />
|
<LanguageTabs
|
||||||
|
languages={languages}
|
||||||
|
active={activeLang}
|
||||||
|
onChange={async (lang) => {
|
||||||
|
if (lang === activeLang) return;
|
||||||
|
if (hasDirtyRows && !(await confirm(t('admin.legalPages.unsavedWarning')))) return;
|
||||||
|
setActiveLang(lang);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={langEnabled}
|
checked={langEnabled}
|
||||||
@@ -484,6 +584,7 @@ function FaqEditor() {
|
|||||||
settings: [{ language: activeLang, is_enabled: !langEnabled }],
|
settings: [{ language: activeLang, is_enabled: !langEnabled }],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
disabled={settingsMutation.isPending}
|
||||||
aria-label={t('admin.legalPages.enabled')}
|
aria-label={t('admin.legalPages.enabled')}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-dark-300">{t('admin.legalPages.enabled')}</span>
|
<span className="text-sm text-dark-300">{t('admin.legalPages.enabled')}</span>
|
||||||
@@ -511,8 +612,8 @@ function FaqEditor() {
|
|||||||
<FaqQuestionRow
|
<FaqQuestionRow
|
||||||
key={page.id}
|
key={page.id}
|
||||||
page={page}
|
page={page}
|
||||||
canMoveUp={index > 0}
|
canMoveUp={index > 0 && !reorderMutation.isPending}
|
||||||
canMoveDown={index < pages.length - 1}
|
canMoveDown={index < pages.length - 1 && !reorderMutation.isPending}
|
||||||
onMoveUp={() => reorderMutation.mutate({ a: page, b: pages[index - 1] })}
|
onMoveUp={() => reorderMutation.mutate({ a: page, b: pages[index - 1] })}
|
||||||
onMoveDown={() => reorderMutation.mutate({ a: page, b: pages[index + 1] })}
|
onMoveDown={() => reorderMutation.mutate({ a: page, b: pages[index + 1] })}
|
||||||
onDelete={async () => {
|
onDelete={async () => {
|
||||||
@@ -520,6 +621,7 @@ function FaqEditor() {
|
|||||||
if (confirmed) deleteMutation.mutate(page.id);
|
if (confirmed) deleteMutation.mutate(page.id);
|
||||||
}}
|
}}
|
||||||
onSaved={invalidate}
|
onSaved={invalidate}
|
||||||
|
onDirtyChange={handleRowDirtyChange}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -530,7 +632,16 @@ function FaqEditor() {
|
|||||||
|
|
||||||
export default function AdminLegalPages() {
|
export default function AdminLegalPages() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const confirm = useDestructiveConfirm();
|
||||||
const [activeTab, setActiveTab] = useState<LegalTab>('privacy');
|
const [activeTab, setActiveTab] = useState<LegalTab>('privacy');
|
||||||
|
const [dirty, setDirty] = useState(false);
|
||||||
|
|
||||||
|
const handleTabChange = async (tab: LegalTab) => {
|
||||||
|
if (tab === activeTab) return;
|
||||||
|
if (dirty && !(await confirm(t('admin.legalPages.unsavedWarning')))) return;
|
||||||
|
setDirty(false);
|
||||||
|
setActiveTab(tab);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -547,7 +658,7 @@ export default function AdminLegalPages() {
|
|||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => handleTabChange(tab)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium transition-colors',
|
'min-h-[44px] rounded-lg px-4 py-2.5 text-sm font-medium transition-colors',
|
||||||
activeTab === tab
|
activeTab === tab
|
||||||
@@ -560,10 +671,14 @@ export default function AdminLegalPages() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeTab === 'privacy' && <DocumentEditor key="privacy" kind="privacy-policy" />}
|
{activeTab === 'privacy' && (
|
||||||
{activeTab === 'offer' && <DocumentEditor key="offer" kind="public-offer" />}
|
<DocumentEditor key="privacy" kind="privacy-policy" onDirtyChange={setDirty} />
|
||||||
{activeTab === 'rules' && <RulesEditor />}
|
)}
|
||||||
{activeTab === 'faq' && <FaqEditor />}
|
{activeTab === 'offer' && (
|
||||||
|
<DocumentEditor key="offer" kind="public-offer" onDirtyChange={setDirty} />
|
||||||
|
)}
|
||||||
|
{activeTab === 'rules' && <RulesEditor onDirtyChange={setDirty} />}
|
||||||
|
{activeTab === 'faq' && <FaqEditor onDirtyChange={setDirty} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user