You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

957 lines
46 KiB

import React, { useState, useMemo } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Ic } from '@/icons'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogBody,
DialogFooter,
} from '@/components/ui/dialog'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import { toast } from '@/stores/toast'
import { toFa } from '@/lib/utils'
import { formatDualDate } from '@/lib/date-utils'
import { useTranslation } from '@/i18n'
import {
fetchAdminBlogs,
saveAdminArticle,
deleteAdminArticle,
toggleAdminArticleStatus,
type BlogArticleItem,
type SaveBlogArticlePayload,
} from '../services/blogs-api'
const LANGUAGE_CONFIG = [
{ code: 'fa', label: 'فارسی', flag: '🇮🇷', dir: 'rtl' },
{ code: 'ar', label: 'العربية', flag: '🇸🇦', dir: 'rtl' },
{ code: 'en', label: 'English', flag: '🇬🇧', dir: 'ltr' },
{ code: 'ru', label: 'Русский', flag: '🇷🇺', dir: 'ltr' },
{ code: 'id', label: 'Indonesia', flag: '🇮🇩', dir: 'ltr' },
] as const
type SupportedLang = (typeof LANGUAGE_CONFIG)[number]['code']
const FILTER_LANGUAGE_OPTIONS = [
{ code: 'ALL', label: 'همه زبان‌ها', flag: '🌐' },
...LANGUAGE_CONFIG,
]
interface FormTranslationState {
title: string
summary: string
content: string
tag_names: string[]
}
export function BlogsServiceView() {
const { language } = useTranslation()
const qc = useQueryClient()
const [selectedFilterLang, setSelectedFilterLang] = useState<string>('ALL')
const [statusFilter, setStatusFilter] = useState<string>('ALL')
const [searchQuery, setSearchQuery] = useState<string>('')
// Dialog state
const [isModalOpen, setIsModalOpen] = useState<boolean>(false)
const [editingArticleId, setEditingArticleId] = useState<number | null>(null)
const [formGroupKey, setFormGroupKey] = useState<string>('')
const [formBaseSlug, setFormBaseSlug] = useState<string>('')
const [formCover, setFormCover] = useState<string>('')
const [formStatus, setFormStatus] = useState<boolean>(true)
const [formIsTopList, setFormIsTopList] = useState<boolean>(false)
const [activeFormLang, setActiveFormLang] = useState<SupportedLang>('fa')
const [previewMode, setPreviewMode] = useState<boolean>(false)
// Translations dictionary for the 5 languages
const [formTranslations, setFormTranslations] = useState<Record<SupportedLang, FormTranslationState>>({
fa: { title: '', summary: '', content: '', tag_names: ['زیارت', 'عقیله'] },
ar: { title: '', summary: '', content: '', tag_names: ['زيارة', 'عقيلة'] },
en: { title: '', summary: '', content: '', tag_names: ['Pilgrimage', 'Aqila'] },
ru: { title: '', summary: '', content: '', tag_names: ['Паломничество', 'Акила'] },
id: { title: '', summary: '', content: '', tag_names: ['Ziarah', 'Aqila'] },
})
// Query articles list
const { data: articles = [], isLoading, refetch } = useQuery({
queryKey: ['admin-blogs', selectedFilterLang, statusFilter],
queryFn: () =>
fetchAdminBlogs({
language_code: selectedFilterLang !== 'ALL' ? selectedFilterLang : undefined,
status: statusFilter !== 'ALL' ? statusFilter : undefined,
}),
})
// Mutations
const saveMutation = useMutation({
mutationFn: (payload: SaveBlogArticlePayload) => saveAdminArticle(payload),
onSuccess: (_, variables) => {
qc.invalidateQueries({ queryKey: ['admin-blogs'] })
setIsModalOpen(false)
toast.success(variables.id ? 'مقاله و ترجمه‌های آن با موفقیت به‌روزرسانی شد.' : 'مقاله جدید با موفقیت در دیتابیس ثبت گردید.')
},
onError: (err: any) => {
toast.error(err?.message || 'خطا در ثبت یا به‌روزرسانی مقاله')
},
})
const deleteMutation = useMutation({
mutationFn: (id: number) => deleteAdminArticle(id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin-blogs'] })
toast.success('مقاله و کلیه ترجمه‌های آن حذف شدند.')
},
onError: (err: any) => {
toast.error(err?.message || 'خطا در حذف مقاله')
},
})
const toggleStatusMutation = useMutation({
mutationFn: ({ id, status }: { id: number; status: boolean }) =>
toggleAdminArticleStatus(id, status),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin-blogs'] })
toast.success('وضعیت انتشار مقاله در کل زبان‌ها به‌روزرسانی شد.')
},
onError: (err: any) => {
toast.error(err?.message || 'خطا در تغییر وضعیت مقاله')
},
})
// Filtered articles
const filteredArticles = useMemo(() => {
let list = articles
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase().trim()
list = list.filter((a) => {
if (a.title?.toLowerCase().includes(q) || a.base_slug?.toLowerCase().includes(q)) {
return true
}
// Search across all translations
return Object.values(a.translations || {}).some(
(t) => t.title?.toLowerCase().includes(q) || t.summary?.toLowerCase().includes(q)
)
})
}
return list
}, [articles, searchQuery])
// Statistics
const totalCount = articles.length
const activeCount = articles.filter((a) => a.status).length
const draftCount = articles.filter((a) => !a.status).length
// Open Add Dialog
const handleOpenAdd = () => {
setEditingArticleId(null)
setFormGroupKey('')
setFormBaseSlug('')
setFormCover('')
setFormStatus(true)
setFormIsTopList(false)
setActiveFormLang('fa')
setPreviewMode(false)
setFormTranslations({
fa: { title: '', summary: '', content: '', tag_names: ['زیارت', 'عقیله'] },
ar: { title: '', summary: '', content: '', tag_names: ['زيارة', 'عقيلة'] },
en: { title: '', summary: '', content: '', tag_names: ['Pilgrimage', 'Aqila'] },
ru: { title: '', summary: '', content: '', tag_names: ['Паломничество', 'Акила'] },
id: { title: '', summary: '', content: '', tag_names: ['Ziarah', 'Aqila'] },
})
setIsModalOpen(true)
}
// Open Edit Dialog
const handleOpenEdit = (article: BlogArticleItem) => {
setEditingArticleId(article.id)
setFormGroupKey(article.group_key || article.base_slug)
setFormBaseSlug(article.base_slug || article.group_key)
setFormCover(article.cover || '')
setFormStatus(article.status)
setFormIsTopList(article.is_top_list)
setActiveFormLang(
(article.available_languages?.[0] as SupportedLang) || 'fa'
)
setPreviewMode(false)
// Populate translations
const newTrans: Record<SupportedLang, FormTranslationState> = {
fa: { title: '', summary: '', content: '', tag_names: [] },
ar: { title: '', summary: '', content: '', tag_names: [] },
en: { title: '', summary: '', content: '', tag_names: [] },
ru: { title: '', summary: '', content: '', tag_names: [] },
id: { title: '', summary: '', content: '', tag_names: [] },
}
LANGUAGE_CONFIG.forEach((cfg) => {
const existing = article.translations?.[cfg.code]
if (existing) {
newTrans[cfg.code] = {
title: existing.title || '',
summary: existing.summary || '',
content: existing.content || '',
tag_names:
existing.tag_names ||
existing.tags?.map((t) => t.name) ||
[],
}
}
})
setFormTranslations(newTrans)
setIsModalOpen(true)
}
// Save handler
const handleSaveArticle = () => {
// Validate that at least one language has a title
const hasAnyTranslation = Object.values(formTranslations).some(
(t) => t.title && t.title.trim().length > 0
)
if (!hasAnyTranslation) {
toast.error('لطفاً عنوان مقاله را حداقل برای یک زبان وارد نمایید.')
return
}
let finalSlug = formBaseSlug.trim()
if (!finalSlug) {
// Auto generate from first available title
const firstTitle = Object.values(formTranslations).find((t) => t.title.trim())?.title || 'article'
finalSlug = firstTitle
.toLowerCase()
.replace(/[^a-z0-9\u0600-\u06FF\s-]/g, '')
.replace(/\s+/g, '-')
.slice(0, 50)
}
const payload: SaveBlogArticlePayload = {
id: editingArticleId || undefined,
group_key: formGroupKey || finalSlug,
base_slug: finalSlug,
cover: formCover.trim() || undefined,
status: formStatus,
is_top_list: formIsTopList,
translations: formTranslations,
}
saveMutation.mutate(payload)
}
const currentLangConfig = LANGUAGE_CONFIG.find((l) => l.code === activeFormLang) || LANGUAGE_CONFIG[0]
const currentTranslation = formTranslations[activeFormLang]
return (
<div className="space-y-6">
{/* ۱. سربرگ یکپارچه بخش وبلاگ و آمار کلان */}
<div className="relative overflow-hidden rounded-2xl border border-border-soft bg-card/40 p-5 sm:p-6 shadow-xs backdrop-blur-xs">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2.5">
<div className="flex size-9 items-center justify-center rounded-xl bg-primary/10 text-primary">
<Ic name="file" className="size-5" />
</div>
<h1 className="text-xl font-black text-foreground">
وبلاگ و مقالات دانشنامه
</h1>
<Badge
variant="outline"
className="border-primary/30 bg-primary/10 text-primary font-bold font-mono text-xs"
>
Articles & Encyclopedia
</Badge>
</div>
<p className="text-xs text-grey-3">
بانک مقالات تخصصی، دانشنامه اماکن متبرکه، راهنمای جامع زیارت و سفر به عتبات عالیات با مدیریت یکپارچه ترجمه در ۵ زبان
</p>
</div>
<div className="flex items-center gap-2.5 shrink-0 flex-wrap">
<Badge
variant="outline"
className="border-border-soft bg-card/70 text-grey-2 text-xs py-1.5 px-3 rounded-xl font-medium gap-1.5 shadow-2xs"
>
<span className="size-2 rounded-full bg-emerald-500 animate-pulse" />
<span>محتوا و سئو چندزبانه</span>
</Badge>
<Button
onClick={handleOpenAdd}
className="gap-2 bg-primary text-primary-foreground hover:bg-primary/90 text-xs font-bold h-9 px-4 rounded-xl shadow-xs cursor-pointer"
>
<Ic name="plus" className="size-4" />
افزودن مقاله جدید
</Button>
</div>
</div>
{/* کارت‌های آماری */}
<div className="mt-6 grid grid-cols-2 gap-3 sm:grid-cols-4">
<Card className="border-border-soft bg-card/60 backdrop-blur">
<CardContent className="p-4 flex items-center justify-between">
<div>
<p className="text-[11px] text-grey-3">کل مقالات دانشنامه</p>
<p className="text-xl font-black text-foreground mt-1 tabular-nums">{toFa(totalCount)}</p>
</div>
<span className="p-2 rounded-xl bg-primary/10 text-primary">
<Ic name="file" className="size-4" />
</span>
</CardContent>
</Card>
<Card className="border-border-soft bg-card/60 backdrop-blur">
<CardContent className="p-4 flex items-center justify-between">
<div>
<p className="text-[11px] text-grey-3">مقالات منتشرشده (فعال)</p>
<p className="text-xl font-black text-emerald-400 mt-1 tabular-nums">{toFa(activeCount)}</p>
</div>
<span className="p-2 rounded-xl bg-emerald-500/10 text-emerald-400">
<Ic name="check" className="size-4" />
</span>
</CardContent>
</Card>
<Card className="border-border-soft bg-card/60 backdrop-blur">
<CardContent className="p-4 flex items-center justify-between">
<div>
<p className="text-[11px] text-grey-3">پیش‌نویس / غیرفعال</p>
<p className="text-xl font-black text-amber-400 mt-1 tabular-nums">{toFa(draftCount)}</p>
</div>
<span className="p-2 rounded-xl bg-amber-500/10 text-amber-400">
<Ic name="clock" className="size-4" />
</span>
</CardContent>
</Card>
<Card className="border-border-soft bg-card/60 backdrop-blur">
<CardContent className="p-4 flex items-center justify-between">
<div>
<p className="text-[11px] text-grey-3">پوشش زبان‌های عقیله</p>
<p className="text-xl font-black text-sky-400 mt-1">۵ زبان رسمی</p>
</div>
<span className="p-2 rounded-xl bg-sky-500/10 text-sky-400">
<Ic name="globe" className="size-4" />
</span>
</CardContent>
</Card>
</div>
</div>
{/* ۲. تب‌های فیلتر زبان و نوار جستجو */}
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
{/* فیلتر زبان */}
<div className="flex items-center gap-1.5 overflow-x-auto pb-1 scrollbar-none">
{FILTER_LANGUAGE_OPTIONS.map((lang) => (
<button
key={lang.code}
onClick={() => setSelectedFilterLang(lang.code)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-bold transition-all shrink-0 cursor-pointer ${
selectedFilterLang === lang.code
? 'bg-primary text-primary-foreground shadow-sm'
: 'bg-card/60 text-grey-2 hover:bg-card/90 border border-border-soft'
}`}
>
<span>{lang.flag}</span>
<span>{lang.label}</span>
</button>
))}
</div>
{/* جستجو، وضعیت و دکمه بروزرسانی */}
<div className="flex items-center gap-2.5">
<div className="relative w-full sm:w-64">
<Ic name="search" className="absolute start-3 top-1/2 -translate-y-1/2 size-3.5 text-grey-3" />
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="جستجو در عنوان یا موضوع مقاله..."
className="h-9 ps-9 text-xs bg-surface-base rounded-xl border-border-soft"
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="h-9 rounded-xl border border-border-soft bg-surface-base px-3 text-xs text-foreground focus:outline-none focus:border-primary/50 cursor-pointer"
>
<option value="ALL">همه وضعیت‌ها</option>
<option value="active">منتشرشده (فعال)</option>
<option value="inactive">پیش‌نویس / غیرفعال</option>
</select>
<Button
variant="outline"
size="sm"
onClick={() => refetch()}
className="h-9 px-3 gap-1.5 text-xs rounded-xl border-border-soft cursor-pointer"
>
<Ic name="refresh" className={`size-3.5 ${isLoading ? 'animate-spin' : ''}`} />
<span>بروزرسانی</span>
</Button>
</div>
</div>
{/* ۳. لیست و جدول مقالات یکپارچه */}
<div className="rounded-2xl border border-border-soft bg-card/40 backdrop-blur-xs overflow-hidden shadow-xs">
<div className="flex items-center justify-between border-b border-border-soft bg-card/60 px-5 py-3.5">
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-foreground">فهرست مقالات دانشنامه عقیله</span>
<Badge variant="secondary" className="bg-primary/10 text-primary border-0 text-[11px] font-mono">
{toFa(filteredArticles.length)} مقاله مستقل
</Badge>
</div>
<span className="text-[11px] text-grey-3">مدیریت ترجمه ۵ زبانه در یک ص</span>
</div>
{isLoading ? (
<div className="p-12 text-center text-xs text-grey-3 flex flex-col items-center justify-center gap-3">
<div className="size-8 border-2 border-primary/20 border-t-primary rounded-full animate-spin" />
<p>در حال بارگذاری بانک مقالات دانشنامه...</p>
</div>
) : filteredArticles.length === 0 ? (
<div className="p-12 text-center text-xs text-grey-3 flex flex-col items-center justify-center gap-3">
<div className="p-3 rounded-2xl bg-muted/40 text-grey-3">
<Ic name="file" className="size-8" />
</div>
<p className="font-bold text-foreground">هیچ مقاله‌ای با این فیلترها یافت نشد.</p>
<p className="text-[11px]">با کلیک بر روی «افزودن مقاله جدید» دانشنامه را کامل کنید.</p>
<Button
onClick={handleOpenAdd}
size="sm"
className="mt-2 text-xs bg-primary text-primary-foreground rounded-xl font-bold cursor-pointer"
>
افزودن مقاله جدید
</Button>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-start text-xs border-collapse">
<thead>
<tr className="border-b border-border-soft bg-muted/30 text-grey-2 text-[11px] font-bold">
<th className="py-3 px-4 text-start w-16">شناسه</th>
<th className="py-3 px-4 text-start">عنوان و تصویر شاخص</th>
<th className="py-3 px-4 text-center w-56">زبان‌های ترجمه‌شده</th>
<th className="py-3 px-4 text-center w-32">وضعیت انتشار</th>
<th className="py-3 px-4 text-center w-32">تاریخ ایجاد</th>
<th className="py-3 px-4 text-end w-24">عملیات</th>
</tr>
</thead>
<tbody className="divide-y divide-border-soft/60">
{filteredArticles.map((article) => {
const langs = article.available_languages || []
return (
<tr
key={article.id || article.group_key}
className="hover:bg-muted/20 transition-colors group"
>
{/* شناسه */}
<td className="py-3 px-4 font-mono text-grey-3 tabular-nums text-xs">
#{article.id}
</td>
{/* کاور + عنوان (+ نشان ویژه) + نامک سئو */}
<td className="py-3 px-4">
<div className="flex items-center gap-3">
{article.cover ? (
<img
src={article.cover}
alt={article.title}
className="size-11 rounded-xl object-cover border border-border-soft shrink-0 bg-muted/20 shadow-2xs"
onError={(e) => {
;(e.currentTarget as HTMLElement).style.display = 'none'
}}
/>
) : (
<div className="size-11 rounded-xl bg-primary/10 text-primary border border-border-soft flex items-center justify-center shrink-0">
<Ic name="file" className="size-5" />
</div>
)}
<div className="min-w-0 space-y-1">
<div className="flex items-center gap-2 flex-wrap">
<button
type="button"
onClick={() => handleOpenEdit(article)}
className="font-bold text-foreground hover:text-primary transition-colors text-start line-clamp-1 cursor-pointer text-sm"
title={article.title}
>
{article.title}
</button>
{article.is_top_list && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-md bg-amber-500/10 border border-amber-500/25 text-[10px] text-amber-500 font-medium shrink-0">
<Ic name="star" className="size-2.5 fill-amber-500" />
<span>ویژه</span>
</span>
)}
</div>
<p className="font-mono text-[11px] text-grey-3 truncate max-w-md" dir="ltr">
/blog/{article.base_slug || article.group_key}
</p>
</div>
</div>
</td>
{/* وضعیت زبان‌ها و ترجمه‌های مقاله */}
<td className="py-3 px-4 text-center">
<div className="inline-flex items-center gap-1.5 p-1 rounded-xl bg-surface-base/80 border border-border-soft">
{LANGUAGE_CONFIG.map((lang) => {
const hasTranslation = langs.includes(lang.code)
return (
<button
key={lang.code}
type="button"
onClick={() => {
handleOpenEdit(article)
setActiveFormLang(lang.code)
}}
title={
hasTranslation
? `دارای ترجمه به ${lang.label} (برای ویرایش کلیک کنید)`
: `فاقد ترجمه به ${lang.label} (برای افزودن کلیک کنید)`
}
className={`px-2 py-1 rounded-lg text-[10px] font-bold font-mono transition-all flex items-center gap-1 cursor-pointer ${
hasTranslation
? 'bg-emerald-500/15 text-emerald-400 border border-emerald-500/30 hover:bg-emerald-500/25'
: 'bg-muted/30 text-grey-3 border border-dashed border-border-soft hover:border-grey-3 hover:text-foreground opacity-50'
}`}
>
<span>{lang.flag}</span>
<span className="uppercase">{lang.code}</span>
{hasTranslation ? (
<span className="size-1 rounded-full bg-emerald-400" />
) : (
<span className="text-[9px] leading-none">+</span>
)}
</button>
)
})}
</div>
</td>
{/* وضعیت انتشار عمومی کل مقاله */}
<td className="py-3 px-4 text-center">
<button
onClick={() => toggleStatusMutation.mutate({ id: article.id, status: !article.status })}
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-bold transition-all cursor-pointer ${
article.status
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 hover:bg-emerald-500/20'
: 'bg-muted text-grey-3 border border-border-soft hover:bg-muted/80'
}`}
>
<span className={`size-1.5 rounded-full ${article.status ? 'bg-emerald-400' : 'bg-grey-3'}`} />
<span>{article.status ? 'منتشرشده' : 'پیش‌نویس'}</span>
</button>
</td>
{/* تاریخ ثبت */}
<td className="py-3 px-4 text-center font-mono text-[11px] text-grey-3">
{article.created_at ? formatDualDate(article.created_at, language as any) : '—'}
</td>
{/* عملیات */}
<td className="py-3 px-4 text-end">
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleOpenEdit(article)}
className="size-8 p-0 text-grey-2 hover:text-foreground hover:bg-muted rounded-lg cursor-pointer"
title="ویرایش مقاله و ترجمه‌ها"
>
<Ic name="pencil" className="size-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
if (confirm(`آیا از حذف مقاله «${article.title}» و کلیه ترجمه‌های ۵ زبانه آن اطمینان دارید؟`)) {
deleteMutation.mutate(article.id)
}
}}
className="size-8 p-0 text-grey-2 hover:text-red-400 hover:bg-red-500/10 rounded-lg cursor-pointer"
title="حذف کامل مقاله"
>
<Ic name="trash" className="size-3.5" />
</Button>
</div>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</div>
{/* ۴. دیالوگ یکپارچه مدیریت ۵ زبانه مقاله (همگی در یک ص هندل میشن) */}
<Dialog
open={isModalOpen}
onOpenChange={(open) => {
setIsModalOpen(open)
}}
>
<DialogContent className="sm:max-w-3xl md:max-w-4xl lg:max-w-5xl max-h-[92vh] overflow-hidden flex flex-col p-0 rounded-2xl">
{/* Header */}
<DialogHeader className="p-5 border-b border-border-soft bg-muted/20 shrink-0">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="size-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center shrink-0">
<Ic name="file" className="size-5" />
</div>
<div>
<DialogTitle className="text-base font-bold text-foreground">
{editingArticleId ? 'ویرایش مقاله و مدیریت ترجمه‌های ۵ زبانه' : 'افزودن مقاله دانشنامه جدید (۵ زبانه)'}
</DialogTitle>
<DialogDescription className="text-xs text-muted-foreground mt-0.5">
یک مقاله ساخته می‌شود؛ اگر در یک زبان ترجمه داشته باشد در وب‌سایت آن زبان نمایش داده می‌شود، در غیر این‌صورت نمایش داده نخواهد شد.
</DialogDescription>
</div>
</div>
{formBaseSlug && (
<Badge variant="outline" className="font-mono text-[11px] border-border-soft bg-surface-base px-2.5 py-1 text-grey-3 hidden sm:inline-flex" dir="ltr">
/blog/{formBaseSlug}
</Badge>
)}
</div>
</DialogHeader>
{/* Body */}
<DialogBody className="space-y-5 p-5 overflow-y-auto">
{/* ۱. بخش مشخصات مشترک بین همه زبان‌ها (Common settings) */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 rounded-xl border border-border-soft bg-surface-base/60">
{/* نامک اسلاگ پایه */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-foreground flex items-center justify-between">
<span>نامک URL یکتای مقاله (Base Slug):</span>
<span className="text-[10px] text-grey-3 font-normal">مشترک در تمام زبان‌ها</span>
</label>
<Input
value={formBaseSlug}
onChange={(e) => setFormBaseSlug(e.target.value)}
placeholder="najaf-holy-sites-guide"
dir="ltr"
className="bg-card border-border-soft rounded-xl text-xs font-mono h-9"
/>
</div>
{/* آدرس تصویر شاخص */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-foreground flex items-center justify-between">
<span>آدرس تصویر شاخص (Cover Image URL):</span>
<span className="text-[10px] text-grey-3 font-normal">مشترک در کارت‌ها</span>
</label>
<div className="flex gap-2 items-center">
<Input
value={formCover}
onChange={(e) => setFormCover(e.target.value)}
placeholder="https://... یا /media/blog/cover/..."
dir="ltr"
className="bg-card border-border-soft rounded-xl text-xs font-mono h-9 flex-1"
/>
{formCover && (
<img
src={formCover}
alt="کاور"
className="size-9 rounded-lg object-cover border border-border-soft shrink-0 bg-muted/20"
/>
)}
</div>
</div>
{/* وضعیت انتشار عمومی و مقاله ویژه */}
<div className="flex items-center justify-between p-3 rounded-xl border border-border-soft bg-card">
<div>
<p className="text-xs font-bold text-foreground">انتشار عمومی در وب‌سایت</p>
<p className="text-[10px] text-grey-3 mt-0.5">وضعیت فعال در کل زبان‌ها</p>
</div>
<Switch checked={formStatus} onCheckedChange={setFormStatus} />
</div>
<div className="flex items-center justify-between p-3 rounded-xl border border-border-soft bg-card">
<div>
<p className="text-xs font-bold text-foreground">نمایش در مقالات ویژه (Top List)</p>
<p className="text-[10px] text-grey-3 mt-0.5">برجسته کردن در بالای وبلاگ</p>
</div>
<Switch checked={formIsTopList} onCheckedChange={setFormIsTopList} />
</div>
</div>
{/* ۲. تب‌های انتخاب زبان ترجمه (۵ زبانه) */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-foreground flex items-center gap-1.5">
<Ic name="globe" className="size-4 text-primary" />
<span>انتخاب زبان محتوا و ترجمه:</span>
</label>
<div className="flex items-center gap-2 text-[11px] text-grey-3">
<span className="size-2 rounded-full bg-emerald-500 inline-block" />
<span>دارای محتوا</span>
<span className="size-2 rounded-full bg-grey-3 inline-block ms-2" />
<span>فاقد ترجمه (عدم نمایش در وب این زبان)</span>
</div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2 p-1.5 rounded-2xl bg-surface-base border border-border-soft">
{LANGUAGE_CONFIG.map((lang) => {
const isSelected = activeFormLang === lang.code
const hasData = Boolean(formTranslations[lang.code]?.title?.trim())
return (
<button
key={lang.code}
type="button"
onClick={() => setActiveFormLang(lang.code)}
className={`h-11 px-3 rounded-xl text-xs font-bold transition flex items-center justify-center gap-2 relative cursor-pointer ${
isSelected
? 'bg-primary text-primary-foreground shadow-xs'
: 'text-grey-2 hover:text-foreground hover:bg-card border border-transparent hover:border-border-soft'
}`}
>
<span className="text-base">{lang.flag}</span>
<span className="truncate">{lang.label}</span>
<span
className={`size-2 rounded-full shrink-0 ${
hasData
? isSelected
? 'bg-white'
: 'bg-emerald-500'
: 'bg-grey-3/40'
}`}
title={hasData ? 'دارای محتوا' : 'خالی'}
/>
</button>
)
})}
</div>
</div>
{/* ۳. فرم ورود محتوا برای زبان فعال */}
<div className="p-5 rounded-2xl border border-border-soft bg-card space-y-4">
<div className="flex items-center justify-between border-b border-border-soft pb-3">
<div className="flex items-center gap-2">
<span className="text-base">{currentLangConfig.flag}</span>
<span className="text-sm font-bold text-foreground">
محتوای مقاله به زبان {currentLangConfig.label}
</span>
<Badge variant="outline" className="text-[10px] uppercase font-mono border-border-soft">
{currentLangConfig.dir.toUpperCase()}
</Badge>
</div>
{!currentTranslation.title.trim() && (
<span className="text-[11px] text-amber-500 font-medium">
(در صورت خالی ماندن، مقاله در نسخه {currentLangConfig.label} نمایش داده نمی‌شود)
</span>
)}
</div>
{/* عنوان مقاله در این زبان */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-foreground">
عنوان سئو در زبان {currentLangConfig.label} (H1 Title):
</label>
<Input
value={currentTranslation.title}
onChange={(e) =>
setFormTranslations({
...formTranslations,
[activeFormLang]: { ...currentTranslation, title: e.target.value },
})
}
dir={currentLangConfig.dir}
placeholder={`عنوان مقاله به زبان ${currentLangConfig.label}...`}
className="bg-surface-base border-border-soft rounded-xl text-sm"
/>
</div>
{/* خلاصه متای سئو */}
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-foreground">
خلاصه متای سئو (Meta Description):
</label>
<span className="text-[10px] font-mono text-grey-3">
{toFa(currentTranslation.summary?.length || 0)} / ۱۶۰ کاراکتر
</span>
</div>
<Textarea
value={currentTranslation.summary}
onChange={(e) =>
setFormTranslations({
...formTranslations,
[activeFormLang]: { ...currentTranslation, summary: e.target.value },
})
}
dir={currentLangConfig.dir}
rows={2}
placeholder={`توضیحات کوتاه برای نمایش در کارت‌ها و نتایج جستجو به زبان ${currentLangConfig.label}...`}
className="w-full rounded-xl border border-border-soft bg-surface-base p-3 text-xs text-foreground focus:outline-none focus:border-primary/50"
/>
</div>
{/* برچسب‌ها */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-foreground">
برچسب‌ها در زبان {currentLangConfig.label} (با کاما جدا کنید):
</label>
<Input
value={currentTranslation.tag_names.join('، ')}
onChange={(e) =>
setFormTranslations({
...formTranslations,
[activeFormLang]: {
...currentTranslation,
tag_names: e.target.value.split(/[،,]+/).map((s) => s.trim()).filter(Boolean),
},
})
}
dir={currentLangConfig.dir}
placeholder="زیارت، نجف، کربلا، عقیله"
className="bg-surface-base border-border-soft rounded-xl text-xs"
/>
</div>
{/* متن کامل مقاله و سئو با قابلیت سوئیچ پیش‌نمایش */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-foreground">
محتوای کامل مقاله (Semantic HTML):
</label>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setPreviewMode(!previewMode)}
className="h-7 text-[11px] gap-1 border-border-soft rounded-lg cursor-pointer"
>
<Ic name={previewMode ? 'file' : 'eye'} className="size-3" />
<span>{previewMode ? 'حالت ویرایش HTML' : 'پیش‌نمایش زنده'}</span>
</Button>
</div>
{!previewMode ? (
<div className="space-y-2">
<div className="flex flex-wrap gap-1 p-2 rounded-xl bg-muted/20 border border-border-soft text-[11px]">
<button
type="button"
onClick={() =>
setFormTranslations({
...formTranslations,
[activeFormLang]: {
...currentTranslation,
content: (currentTranslation.content || '') + '\n<h2>عنوان بخش جدید</h2>\n<p>متن توضیحات...</p>',
},
})
}
className="px-2.5 py-1 rounded-lg bg-card hover:bg-card/80 border border-border-soft text-grey-2 text-xs font-medium cursor-pointer transition-colors"
>
+ تیتر H2
</button>
<button
type="button"
onClick={() =>
setFormTranslations({
...formTranslations,
[activeFormLang]: {
...currentTranslation,
content: (currentTranslation.content || '') + '\n<h3>زیرتیتر</h3>\n<p>متن بخش...</p>',
},
})
}
className="px-2.5 py-1 rounded-lg bg-card hover:bg-card/80 border border-border-soft text-grey-2 text-xs font-medium cursor-pointer transition-colors"
>
+ زیرتیتر H3
</button>
<button
type="button"
onClick={() =>
setFormTranslations({
...formTranslations,
[activeFormLang]: {
...currentTranslation,
content: (currentTranslation.content || '') + '\n<ul>\n <li>مورد اول</li>\n <li>مورد دوم</li>\n</ul>',
},
})
}
className="px-2.5 py-1 rounded-lg bg-card hover:bg-card/80 border border-border-soft text-grey-2 text-xs font-medium cursor-pointer transition-colors"
>
+ لیست آیتم‌ها
</button>
<button
type="button"
onClick={() =>
setFormTranslations({
...formTranslations,
[activeFormLang]: {
...currentTranslation,
content:
(currentTranslation.content || '') +
'\n<div class="callout-box bg-primary/10 border-r-4 border-primary p-4 rounded-xl my-4">\n <p>نکته زیارتی مهم برای زائران...</p>\n</div>',
},
})
}
className="px-2.5 py-1 rounded-lg bg-card hover:bg-card/80 border border-border-soft text-primary font-bold text-xs cursor-pointer transition-colors"
>
+ جعبه نکته کاربردی
</button>
</div>
<textarea
value={currentTranslation.content}
onChange={(e) =>
setFormTranslations({
...formTranslations,
[activeFormLang]: { ...currentTranslation, content: e.target.value },
})
}
rows={11}
dir={currentLangConfig.dir}
placeholder={`متن کامل مقاله به همراه تگ‌های استاندارد به زبان ${currentLangConfig.label}...`}
className="w-full rounded-xl border border-border-soft bg-surface-base p-4 text-xs font-mono text-foreground focus:outline-none focus:border-primary/50 leading-relaxed resize-y"
/>
</div>
) : (
<div
dir={currentLangConfig.dir}
className="max-h-96 min-h-[160px] overflow-y-auto rounded-xl border border-border-soft bg-card/60 p-5 prose dark:prose-invert prose-sm text-xs leading-relaxed"
dangerouslySetInnerHTML={{
__html: currentTranslation.content || '<p class="text-grey-3">محتوایی برای این زبان وارد نشده است.</p>',
}}
/>
)}
</div>
</div>
</DialogBody>
{/* Footer */}
<DialogFooter className="p-4 border-t border-border-soft bg-muted/20 shrink-0">
<Button
variant="outline"
size="sm"
onClick={() => setIsModalOpen(false)}
className="text-xs border-border-soft rounded-xl cursor-pointer"
>
انصراف
</Button>
<Button
size="sm"
onClick={handleSaveArticle}
disabled={saveMutation.isPending}
className="gap-1.5 bg-primary text-primary-foreground hover:bg-primary/90 text-xs font-bold px-5 rounded-xl cursor-pointer"
>
{saveMutation.isPending && <Ic name="refresh" className="size-3 animate-spin" />}
{editingArticleId ? 'ذخیره مقاله و ترجمه‌ها' : 'ثبت مقاله ۵ زبانه در دیتابیس'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}