import { useCallback, useEffect, useRef, useState } from 'react' import { Controller, useController, useForm } from 'react-hook-form' import { Link, Navigate, useNavigate, useParams } from 'react-router-dom' import { ArrowRightIcon, CheckIcon, EyeIcon, PlusIcon, Trash2Icon, XIcon } 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 { JalaliDatePicker } from '../components/admin/JalaliDatePicker' import { CategorySelect, LuxeTagsSelect } from '../components/admin/LuxeSelect' import type { CategoryOption } from '../components/admin/LuxeSelect' import { ARTICLE_CATEGORIES } from './entities' import { FEATURED_IMAGE_ALT_SUFFIX } from '../data/constants' import { formatReadingTime } from '../utils/readingTime' import { toArticleBodyDoc } from '../utils/tiptapDoc' import type { Article, ArticleBodyDoc } from '../data/site' const EMPTY_DOC = { type: 'doc', content: [{ type: 'paragraph' }] } as unknown as ArticleBodyDoc /** پیش‌نویس صفحه نوشتن — همان شکل Article با بدنه ساختاریافته دو زبانه */ type Draft = Omit & { body: ArticleBodyDoc; body_en: ArticleBodyDoc } /** * فیلدهای متادیتای مقاله که با React Hook Form مدیریت می‌شوند: * عنوان (مبنای Alt پویا)، دسته‌بندی و متادیتای تصویر شاخص. * `category` در فرم یک شیء تک‌مقداری {value,label} یا null است * که هنگام ذخیره به رشته (ستون category در بک‌اند) نگاشت می‌شود. */ type ArticleMetaForm = { title: string category: CategoryOption | null image_alt: string image_title: string } /** مقدار رشته‌ای ذخیره‌شده → گزینه react-select (یا null وقتی خالی است) */ function toCategoryOption(value: string): CategoryOption | null { const category = (value ?? '').trim() return category ? { label: category, value: category } : null } /** گزینه react-select → مقدار رشته‌ای ذخیره‌سازی در بک‌اند */ function categoryValue(option: CategoryOption | null): string { return option?.value.trim() ?? '' } /** داده‌های قدیمی ممکن است هشتگ را رشته کاماشده داشته باشند */ function normalizeTags(value: unknown): string[] { if (Array.isArray(value)) return value.map((t) => String(t).trim()).filter(Boolean) if (typeof value === 'string') return value.split(',').map((t) => t.trim()).filter(Boolean) return [] } /** یک ردیف پرسش‌وپاسخ مقاله */ type ArticleFaq = { question: string; answer: string } /** * پنل پرسش‌وپاسخ‌های پایان مقاله — مثل بخش متا دیسکریپشن زیر ویرایشگر. * در زبان فارسی ستون faqs و در زبان انگلیسی faqs_en را ویرایش می‌کند؛ * نسخه انگلیسیِ خالی در سایت به فارسی برمی‌گردد. ذخیره‌سازی خودکار است. */ function FaqPanel({ faqs, onChange, }: { faqs: ArticleFaq[] onChange: (next: ArticleFaq[]) => void }) { const update = (index: number, patch: Partial) => { const next = faqs.map((f, i) => (i === index ? { ...f, ...patch } : f)) onChange(next) } const remove = (index: number) => onChange(faqs.filter((_, i) => i !== index)) const add = () => onChange([...faqs, { question: '', answer: '' }]) return (
{faqs.map((faq, i) => (
پرسش {i + 1}
update(i, { question: e.target.value })} placeholder="پرسش مخاطب…" aria-label={`متن پرسش ${i + 1}`} className="w-full border border-ivory/15 bg-char px-3 py-2 text-sm font-light text-ivory focus:border-copper focus:outline-none" />