import { useCallback, useEffect, useRef, useState } from 'react' import { Link, Navigate, useNavigate, useParams } from 'react-router-dom' import { ArrowRightIcon, CheckIcon, EyeIcon, Trash2Icon } from 'lucide-react' import Swal from 'sweetalert2' import { useSiteData } from '../api' import { useAdmin } from './AdminContext' import { adminApi } from './adminClient' import { ArticleEditor } from '../components/admin/ArticleEditor' import { SeoChecklist } from '../components/admin/SeoChecklist' import { ImageField } from '../components/admin/ImageField' import type { ArticleBodyDoc } from '../data/site' const EMPTY_DOC = { type: 'doc', content: [{ type: 'paragraph' }] } as unknown as ArticleBodyDoc type Draft = { id: number slug: string title: string title_en: string date: string date_en: string category: string category_en: string image: string read: string read_en: string excerpt: string excerpt_en: string body: ArticleBodyDoc body_en: ArticleBodyDoc status: 'draft' | 'published' seo_keyphrase: string } const STATUS_TEXT: Record = { idle: 'ذخیره شد', saving: 'در حال ذخیره…', error: 'خطا در ذخیره', } /** * صفحه تمام‌عرض نوشتن/ویرایش مقاله — محیط نوشتن متمرکز بدون چرخه سایت. * مسیرها: /admin/write (مقاله جدید) و /admin/write/:id (ویرایش). */ export function ArticleWritePage() { const { isAuthenticated, saveStatus, runSave, editLang, toggleEditLang } = useAdmin() const { id } = useParams<{ id: string }>() const navigate = useNavigate() const { data, updateSite } = useSiteData() const existing = id ? data?.articles.find((a) => a.id === Number(id)) : undefined const [fetched, setFetched] = useState(null) // مقاله تازه همیشه پیش‌نویس شروع می‌شود؛ با دکمه «انتشار در سایت» عمومی می‌شود const [draft, setDraft] = useState({ id: 0, slug: `article-${Date.now()}`, title: '', title_en: '', date: new Date().toLocaleDateString('fa-IR'), date_en: '', category: 'یادداشت', category_en: '', image: '', read: '۵ دقیقه', read_en: '', excerpt: '', excerpt_en: '', body: EMPTY_DOC, body_en: EMPTY_DOC, status: 'draft', seo_keyphrase: '', }) const [createdId, setCreatedId] = useState(null) const [deleting, setDeleting] = useState(false) const draftRef = useRef(draft) draftRef.current = draft const createdRef = useRef(createdId) createdRef.current = createdId const pendingRef = useRef | null>(null) const timerRef = useRef | null>(null) // اگر مقاله در داده سایت نبود (مثلاً پیش از ادغام پیش‌نویس‌ها) مستقیم از انتهای مدیریتی خوانده می‌شود useEffect(() => { if (!id || existing) return let cancelled = false adminApi .get('articles', Number(id)) .then((a) => { if (!cancelled) setFetched(a) }) .catch(() => { /* مقاله وجود ندارد — فرم «مقاله جدید» می‌ماند */ }) return () => { cancelled = true } }, [id, existing]) const article = existing ?? fetched // وقتی مقاله موجود شناخته شد، فرم و شناسه ساخته‌شده یک‌بار مقداردهی می‌شود useEffect(() => { if (!article || createdRef.current) return const rawEnBody = (article as unknown as { body_en?: unknown }).body_en setDraft({ ...(article as unknown as Draft), body: (Array.isArray(article.body) ? EMPTY_DOC : article.body) as ArticleBodyDoc, body_en: rawEnBody && typeof rawEnBody === 'object' && !Array.isArray(rawEnBody) ? (rawEnBody as ArticleBodyDoc) : EMPTY_DOC, }) setCreatedId(article.id) // eslint-disable-next-line react-hooks/exhaustive-deps }, [article?.id]) const doSave = async (changes: Record) => { if (createdRef.current) { await adminApi.patch('articles', createdRef.current, changes) updateSite((d) => { const a = d.articles.find((x) => x.id === createdRef.current) if (a) Object.assign(a, changes) }) } else { // اولین ذخیره = ساخت پیش‌نویس: مقاله ساخته می‌شود اما تا «انتشار» در سایت عمومی دیده نمی‌شود const payload = { ...draftRef.current, ...changes } if (!payload.title) payload.title = 'بدون عنوان' const created = await adminApi.create('articles', payload) setCreatedId(created.id) updateSite((d) => { d.articles.unshift(created as never) }) navigate(`/admin/write/${created.id}`, { replace: true }) } } const flush = useCallback(async () => { if (timerRef.current) { clearTimeout(timerRef.current) timerRef.current = null } const changes = pendingRef.current if (!changes) return pendingRef.current = null await doSave(changes) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const queueSave = (changes: Record) => { pendingRef.current = { ...(pendingRef.current ?? {}), ...changes } setDraft((d) => ({ ...d, ...changes }) as Draft) if (timerRef.current) clearTimeout(timerRef.current) timerRef.current = setTimeout(() => void flush(), 800) } // ذخیره تغییرات در انتظار هنگام ترک صفحه — فقط برای مقاله ساخته‌شده // (create در unmount ممنوع: با draft قدیمی، مقاله سرگردانی می‌سازد) useEffect(() => { return () => { if (timerRef.current) clearTimeout(timerRef.current) const changes = pendingRef.current if (!changes || !createdRef.current) return void adminApi.patch('articles', createdRef.current, changes).catch(() => { /* صفحه در حال بستن است */ }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const forceSave = () => void runSave(flush) // محافظ مسیر — پس از همه هوک‌ها تا ترتیب فراخوانی هوک‌ها همیشه یکسان بماند if (!isAuthenticated) return const remove = async () => { if (!createdId) return const result = await Swal.fire({ icon: 'warning', title: 'حذف مقاله', text: 'این مقاله برای همیشه حذف شود؟', background: '#1e1a16', color: '#F5F3EF', iconColor: '#d5874c', showCancelButton: true, confirmButtonText: 'حذف', cancelButtonText: 'انصراف', buttonsStyling: false, customClass: { popup: 'font-pinar border border-ivory/10', confirmButton: 'bg-red-500 text-white font-bold px-8 py-2.5 mx-2', cancelButton: 'border border-ivory/30 text-ivory font-light px-8 py-2.5 mx-2', }, }) if (!result.isConfirmed) return setDeleting(true) await runSave(async () => { await adminApi.remove('articles', createdId) navigate('/blog') }) setDeleting(false) } const textField = (name: keyof Draft) => ({ value: String(draft[name] ?? ''), onChange: (e: React.ChangeEvent) => queueSave({ [name]: e.target.value }), }) // در زبان انگلیسی، فیلدهای متنی به ستون‌های _en می‌روند؛ اسلاگ/تصویر/وضعیت مشترک‌اند const suf = (name: N) => `${name}${editLang === 'en' ? '_en' : ''}` as keyof Draft return (
{/* نوار بالا */}
بازگشت به وبلاگ
{saveStatus === 'idle' && } {STATUS_TEXT[saveStatus]} {draft.status === 'draft' ? 'پیش‌نویس' : 'منتشر شده'} {createdId && ( <> دیدن در سایت )}

{!createdId ? 'نوشتن مقاله جدید — به‌صورت پیش‌نویس ذخیره می‌شود؛ با دکمه «انتشار در سایت» عمومی می‌شود' : draft.status === 'draft' ? 'پیش‌نویس — تا انتشار در سایت عمومی دیده نمی‌شود؛ تغییرها ذخیره می‌شوند' : 'مقاله منتشرشده — تغییرها همان لحظه در سایت اعمال می‌شود'}

{/* عنوان — در ویرایش انگلیسی، متن فارسی فعلی جای‌نگهدار می‌ماند */}
{/* تصویر شاخص */}
{ queueSave({ image: url }) return true }} />
{/* متن — ویرایشگر روی سند زبان جاری کار می‌کند */}
} onChange={(json) => queueSave({ [suf('body')]: json })} />
{/* چکیده و متادیتا */}

جزئیات انتشار

queueSave({ seo_keyphrase: e.target.value })} className="mt-2 w-full border border-ivory/15 bg-char px-3 py-2 text-sm font-light text-ivory focus:border-copper focus:outline-none" />