Merge branch 'bilingual-en'

# Conflicts:
#	frontend/src/admin/ArticleWritePage.tsx
#	frontend/src/admin/EntityDrawer.tsx
#	frontend/src/admin/entities.ts
#	frontend/src/components/ArticleCard.tsx
#	frontend/src/index.css
#	frontend/src/pages/ArticleDetail.tsx
This commit is contained in:
MMD
2026-09-13 18:45:06 +03:30
52 changed files with 2731 additions and 321 deletions
+30 -4
View File
@@ -5,6 +5,7 @@ import Swal from 'sweetalert2'
import { useSiteData } from '../api'
import { useAdmin, useFieldSaver } from './AdminContext'
import { adminApi } from './adminClient'
import { editableFields } from './entities'
import type { EntityConfig, FieldConfig } from './entities'
import { ImageField } from '../components/admin/ImageField'
import { JalaliDatePicker } from '../components/admin/JalaliDatePicker'
@@ -22,11 +23,16 @@ type EntityDrawerProps = {
export function EntityDrawer({ config, item, onClose, deletable = false }: EntityDrawerProps) {
const save = useFieldSaver()
const { runSave } = useAdmin()
const { runSave, editLang } = useAdmin()
const { reload } = useSiteData()
const [deleting, setDeleting] = useState(false)
const flushRef = useRef<() => void>(() => {})
// در ویرایش انگلیسی فقط فیلدهای قابل ترجمه با نام انگلیسی رندر می‌شوند؛
// متن فارسی همان فیلد به‌عنوان جای‌نگهدار نشان داده می‌شود تا مترجم زمینه را ببیند
const fields = editableFields(config, editLang)
const faBase = (name: string) => name.replace(/_en$/, '')
const closeWithFlush = () => {
flushRef.current()
onClose()
@@ -113,7 +119,7 @@ export function EntityDrawer({ config, item, onClose, deletable = false }: Entit
<div className="flex-1 overflow-y-auto px-6 py-6">
<div className="grid gap-6">
{config.fields.map((field) => {
{fields.map((field) => {
// فیلد Alt تصویر: برای گالری همان ستون alt و برای بقیه <نام فیلد>_alt
const altKey =
field.type === 'image'
@@ -126,6 +132,7 @@ export function EntityDrawer({ config, item, onClose, deletable = false }: Entit
key={field.name}
field={field}
value={item[field.name]}
placeholder={editLang === 'en' ? item[faBase(field.name)] : undefined}
altValue={altKey ? String(item[altKey] ?? '') : undefined}
altFallback={String(item[config.titleKey] ?? '')}
onSave={(value) => saveField(field.name, value)}
@@ -166,6 +173,7 @@ function FieldControl({
value,
altValue,
altFallback,
placeholder,
onSave,
onSaveAlt,
registerFlush,
@@ -175,6 +183,8 @@ function FieldControl({
/** مقدار Alt تصویر — فقط برای فیلدهای image */
altValue?: string
altFallback?: string
/** مقدار فارسی همان فیلد — فقط در ویرایش انگلیسی به‌عنوان جای‌نگهدار نمایش داده می‌شود */
placeholder?: unknown
onSave: (value: unknown) => Promise<boolean>
onSaveAlt?: (alt: string) => Promise<boolean>
registerFlush?: (fn: () => void) => void
@@ -238,14 +248,22 @@ function FieldControl({
}
if (field.type === 'wysiwyg') {
return <WysiwygField field={field} value={value} onSave={onSave} registerFlush={registerFlush} />
return (
<WysiwygField
field={field}
value={value}
onSave={onSave}
registerFlush={registerFlush}
hint={placeholder ? 'خالی = بازدیدکننده متن فارسی را می‌بیند' : undefined}
/>
)
}
if (field.type === 'list') {
return field.ui === 'tags' ? (
<TagsField field={field} value={value} onSave={onSave} />
) : (
<ListField field={field} value={value} onSave={onSave} />
<ListField field={field} value={value} onSave={onSave} hint={placeholder ? 'خالی = بازدیدکننده فهرست فارسی را می‌بیند' : undefined} />
)
}
@@ -256,6 +274,7 @@ function FieldControl({
<textarea
dir="auto"
rows={4}
placeholder={placeholder ? String(placeholder) : undefined}
value={text}
onChange={(e) => setText(e.target.value)}
onBlur={commitText}
@@ -272,6 +291,7 @@ function FieldControl({
<input
dir={field.type === 'number' ? 'ltr' : 'auto'}
type={field.type === 'number' ? 'number' : 'text'}
placeholder={placeholder ? String(placeholder) : undefined}
value={text}
onChange={(e) => setText(e.target.value)}
onBlur={commitText}
@@ -314,10 +334,12 @@ function ListField({
field,
value,
onSave,
hint,
}: {
field: FieldConfig
value: unknown
onSave: (value: unknown) => Promise<boolean>
hint?: string
}) {
const [lines, setLines] = useState<string[]>(Array.isArray(value) ? (value as string[]) : [])
@@ -330,6 +352,7 @@ function ListField({
<div>
<p className="text-xs font-bold text-ivory/80">{field.label}</p>
{field.hint && <p className="mt-1 text-[10px] font-light text-ivory/40">{field.hint}</p>}
{hint && <p className="mt-1 text-[10px] font-light text-copper/70">{hint}</p>}
<div className="mt-2 grid gap-2">
{lines.map((line, i) => (
<div key={i} className="flex items-start gap-2">
@@ -377,11 +400,13 @@ function WysiwygField({
value,
onSave,
registerFlush,
hint,
}: {
field: FieldConfig
value: unknown
onSave: (value: unknown) => Promise<boolean>
registerFlush?: (fn: () => void) => void
hint?: string
}) {
const [doc, setDoc] = useState<Record<string, unknown>>(() =>
value && typeof value === 'object' && !Array.isArray(value)
@@ -427,6 +452,7 @@ function WysiwygField({
return (
<div>
<p className="text-xs font-bold text-ivory/80">{field.label}</p>
{hint && <p className="mt-1 text-[10px] font-light text-copper/70">{hint}</p>}
<p className="mt-1 text-[10px] font-light text-ivory/40">
ذخیره خودکار — تصویر را داخل متن بچسبانید یا بکشید
</p>