import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import type { ReactNode } from 'react' import Swal from 'sweetalert2' import type { SiteData } from '../api' import { useSiteData } from '../api' import { adminApi, getToken, HttpError, setToken } from './adminClient' import { ENTITIES, isTranslatable } from './entities' export type SaveStatus = 'idle' | 'saving' | 'error' /** زبان ویرایش محتوا — رابط مدیریتی فارسی می‌ماند؛ این فقط تعیین می‌کند کدام ستون زبان ذخیره شود */ export type EditLang = 'fa' | 'en' interface AdminContextValue { isAuthenticated: boolean isEditMode: boolean toggleEditMode: () => void login: (username: string, password: string) => Promise logout: () => void saveStatus: SaveStatus /** اجرای یک عملیات ذخیره — وضعیت نوار ادمین را به‌روز می‌کند و خطاها را با دکمه تلاش دوباره نشان می‌دهد */ runSave: (action: () => Promise) => Promise /** زبان محتوایی که در حالت ویرایش ذخیره می‌شود (فارسی یا انگلیسی) */ editLang: EditLang toggleEditLang: () => void } const AdminContext = createContext(null) export function useAdmin(): AdminContextValue { const ctx = useContext(AdminContext) if (!ctx) throw new Error('useAdmin باید داخل AdminProvider استفاده شود') return ctx } function showSaveError(error: unknown): void { const message = error instanceof HttpError ? error.message : 'ارتباط با سرور برقرار نشد — اتصال اینترنت را بررسی کنید' Swal.fire({ icon: 'error', title: 'ذخیره نشد', text: message, background: '#1e1a16', color: '#F5F3EF', iconColor: '#d5874c', confirmButtonText: 'باشه', timer: 4000, timerProgressBar: true, buttonsStyling: false, customClass: { popup: 'font-pinar border border-ivory/10 rounded-none', title: 'font-pinar font-bold text-lg text-ivory', htmlContainer: 'font-pinar font-light text-sm text-ivory/70', confirmButton: 'bg-copper text-char font-bold px-8 py-2.5', }, }) } export function AdminProvider({ children }: { children: ReactNode }) { const [isAuthenticated, setIsAuthenticated] = useState(() => !!getToken()) const [isEditMode, setIsEditMode] = useState( () => sessionStorage.getItem('aramesh_edit_mode') === '1', ) const [editLang, setEditLang] = useState( () => (sessionStorage.getItem('aramesh_edit_lang') === 'en' ? 'en' : 'fa'), ) const [saveStatus, setSaveStatus] = useState('idle') const { reload } = useSiteData() const toggleEditMode = useCallback(() => { setIsEditMode((v) => { sessionStorage.setItem('aramesh_edit_mode', v ? '0' : '1') return !v }) }, []) const toggleEditLang = useCallback(() => { setEditLang((v) => { const next: EditLang = v === 'fa' ? 'en' : 'fa' sessionStorage.setItem('aramesh_edit_lang', next) return next }) }, []) const login = useCallback(async (username: string, password: string) => { await adminApi.login(username, password) setIsAuthenticated(true) // داده سایت دوباره بارگذاری می‌شود تا پیش‌نویس مقاله‌ها هم ادغام شوند reload() }, [reload]) const logout = useCallback(() => { setToken(null) setIsAuthenticated(false) setIsEditMode(false) sessionStorage.removeItem('aramesh_edit_mode') // داده سایت دوباره بارگذاری می‌شود تا پیش‌نویس‌ها از درخت داده حذف شوند reload() }, [reload]) const runSave = useCallback( async (action: () => Promise): Promise => { setSaveStatus('saving') try { await action() setSaveStatus('idle') return true } catch (error) { setSaveStatus('error') showSaveError(error) if (error instanceof HttpError && error.status === 401) { logout() reload() } return false } }, [logout, reload], ) // در حالت ویرایش، کلیک روی لینک‌ها مسیریابی نمی‌کند تا ویرایش متن ممکن باشد useEffect(() => { if (!isEditMode) return const onClick = (e: MouseEvent) => { const anchor = (e.target as HTMLElement | null)?.closest('a') // لینک‌های خود لایه مدیریت (مثل «نوشتن مقاله جدید») کار می‌کنند if (anchor && !anchor.dataset.adminLink) e.preventDefault() } document.addEventListener('click', onClick, true) return () => document.removeEventListener('click', onClick, true) }, [isEditMode]) // در حین ذخیره‌سازی، از بسته‌شدن تصادفی صفحه جلوگیری می‌شود useEffect(() => { if (saveStatus !== 'saving') return const guard = (e: BeforeUnloadEvent) => { e.preventDefault() } window.addEventListener('beforeunload', guard) return () => window.removeEventListener('beforeunload', guard) }, [saveStatus]) const value = useMemo( () => ({ isAuthenticated, isEditMode, toggleEditMode, login, logout, saveStatus, runSave, editLang, toggleEditLang, }), [isAuthenticated, isEditMode, toggleEditMode, login, logout, saveStatus, runSave, editLang, toggleEditLang], ) return {children} } /** * پرکاربردترین کمک‌ساز ویرایش: یک فیلد واحد را PATCH می‌کند و پس از موفقیت * همان تغییر را به‌صورت خوش‌بینانه در درخت داده سایت اعمال می‌کند. * * در حالت ویرایش انگلیسی، کلیدهای قابل ترجمه به پسوند _en نگاشت می‌شوند؛ چون * برخی mutatorهای کامپوننت‌ها نام فیلد را جداگانه می‌نویسند، مقدار قبلی فیلدهای * فارسی اسنپ‌شات می‌شود و پس از اجرای mutator بازگردانده می‌شود تا متن انگلیسی * هرگز جای متن فارسی در درخت خام ننشیند. */ export function useFieldSaver() { const { runSave, editLang } = useAdmin() const { updateSite } = useSiteData() return useCallback( ( resource: string, id: number, changes: Record, apply: (data: SiteData) => void, ): Promise => runSave(async () => { const originalKeys = Object.keys(changes) if (editLang === 'en') { const mapped: Record = {} for (const [key, value] of Object.entries(changes)) { mapped[isTranslatable(resource, key) ? `${key}_en` : key] = value } for (const key of Object.keys(changes)) delete changes[key] Object.assign(changes, mapped) } await adminApi.patch(resource, id, changes) updateSite((d) => { const config = ENTITIES[resource] let node: Record | undefined if (config) { const target = d[config.dataKey as keyof SiteData] as unknown node = config.kind === 'singleton' ? (target as Record | undefined) : (target as { id: number }[] | undefined)?.find((x) => x.id === id) } const snapshot = editLang === 'en' && node ? Object.fromEntries(originalKeys.filter((k) => k !== 'id').map((k) => [k, node![k]])) : null apply(d) if (snapshot && node) { Object.assign(node, snapshot) Object.assign(node, changes) } }) }), [runSave, updateSite, editLang], ) } /** * نام فیلد متناظر با زبان ویرایش جاری — در حالت انگلیسی فیلدهای قابل ترجمه * پسوند _en می‌گیرند. برای جاهایی که نام فیلد جدا از شیء تغییرات ذخیره می‌شود. */ export function useEditField() { const { editLang } = useAdmin() return useCallback( (resource: string, field: string) => editLang === 'en' && isTranslatable(resource, field) ? `${field}_en` : field, [editLang], ) }