fix(blog): زبان ویرایش بهازای هر مقاله بهخاطر سپرده میشود، شمارندهٔ متا روی زبان فعال، جهت محتوای ادیتور از زبان ویرایش
This commit is contained in:
@@ -134,7 +134,7 @@ const STATUS_TEXT: Record<string, string> = {
|
|||||||
* مسیرها: /admin/write (مقاله جدید) و /admin/write/:id (ویرایش).
|
* مسیرها: /admin/write (مقاله جدید) و /admin/write/:id (ویرایش).
|
||||||
*/
|
*/
|
||||||
export function ArticleWritePage() {
|
export function ArticleWritePage() {
|
||||||
const { isAuthenticated, saveStatus, runSave, editLang, toggleEditLang, role } = useAdmin()
|
const { isAuthenticated, saveStatus, runSave, editLang, toggleEditLang, syncEditLang, role } = useAdmin()
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { data, updateSite } = useSiteData()
|
const { data, updateSite } = useSiteData()
|
||||||
@@ -219,6 +219,23 @@ export function ArticleWritePage() {
|
|||||||
|
|
||||||
const article = existing ?? fetched
|
const article = existing ?? fetched
|
||||||
|
|
||||||
|
// زبان ویرایش این مقاله بهازای خودش بهخاطر سپرده میشود — رفرش تب را عوض نکند
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id) return
|
||||||
|
const saved = localStorage.getItem(`aramesh_write_lang_${id}`)
|
||||||
|
if (saved === 'en' || saved === 'fa') {
|
||||||
|
syncEditLang(saved)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const art = existing ?? null
|
||||||
|
if (art) {
|
||||||
|
const hasEn = Boolean((art as Draft).title_en?.trim()) || Boolean((art as Draft).body_en as unknown)
|
||||||
|
const hasFa = Boolean((art as Draft).title?.trim())
|
||||||
|
if (hasEn && !hasFa) syncEditLang('en')
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [id, Boolean(article)])
|
||||||
|
|
||||||
// وقتی مقاله موجود شناخته شد، فرم و شناسه ساختهشده یکبار مقداردهی میشود
|
// وقتی مقاله موجود شناخته شد، فرم و شناسه ساختهشده یکبار مقداردهی میشود
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!article || createdRef.current) return
|
if (!article || createdRef.current) return
|
||||||
@@ -338,7 +355,6 @@ export function ArticleWritePage() {
|
|||||||
queueSave({ [name]: e.target.value }),
|
queueSave({ [name]: e.target.value }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const metaLength = (draft.meta_description ?? '').trim().length
|
|
||||||
// در زبان انگلیسی، فیلدهای متنی به ستونهای _en میروند؛ اسلاگ/تصویر/وضعیت مشترکاند
|
// در زبان انگلیسی، فیلدهای متنی به ستونهای _en میروند؛ اسلاگ/تصویر/وضعیت مشترکاند
|
||||||
const suf = <N extends string>(name: N) => `${name}${editLang === 'en' ? '_en' : ''}` as keyof Draft
|
const suf = <N extends string>(name: N) => `${name}${editLang === 'en' ? '_en' : ''}` as keyof Draft
|
||||||
// مقدار فعال فیلد دوزبانه — برای value ورودیها و چکلیست سئو
|
// مقدار فعال فیلد دوزبانه — برای value ورودیها و چکلیست سئو
|
||||||
@@ -353,6 +369,7 @@ export function ArticleWritePage() {
|
|||||||
|
|
||||||
const activeVal = (name: 'title' | 'slug' | 'author' | 'seo_keyphrase' | 'meta_description' | 'excerpt'): string =>
|
const activeVal = (name: 'title' | 'slug' | 'author' | 'seo_keyphrase' | 'meta_description' | 'excerpt'): string =>
|
||||||
String((editLang === 'en' ? draft[`${name}_en` as keyof Draft] : draft[name]) ?? '')
|
String((editLang === 'en' ? draft[`${name}_en` as keyof Draft] : draft[name]) ?? '')
|
||||||
|
const metaLength = activeVal('meta_description').trim().length
|
||||||
|
|
||||||
// متن جایگزین تصویر شاخص — با watch('title') بهصورت زنده ساخته میشود
|
// متن جایگزین تصویر شاخص — با watch('title') بهصورت زنده ساخته میشود
|
||||||
const imageAltFallback = [watchedTitle.trim(), FEATURED_IMAGE_ALT_SUFFIX]
|
const imageAltFallback = [watchedTitle.trim(), FEATURED_IMAGE_ALT_SUFFIX]
|
||||||
@@ -376,7 +393,11 @@ export function ArticleWritePage() {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleEditLang}
|
onClick={() => {
|
||||||
|
const next = editLang === 'en' ? 'fa' : 'en'
|
||||||
|
toggleEditLang()
|
||||||
|
if (createdId) localStorage.setItem(`aramesh_write_lang_${createdId}`, next)
|
||||||
|
}}
|
||||||
title="تعیین میکند متن مقاله روی کدام زبان ذخیره شود"
|
title="تعیین میکند متن مقاله روی کدام زبان ذخیره شود"
|
||||||
className={`px-4 py-2 text-xs font-bold transition-colors duration-200 ${
|
className={`px-4 py-2 text-xs font-bold transition-colors duration-200 ${
|
||||||
editLang === 'en'
|
editLang === 'en'
|
||||||
@@ -505,6 +526,7 @@ export function ArticleWritePage() {
|
|||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<ArticleEditor
|
<ArticleEditor
|
||||||
contentKey={loadedContentKey}
|
contentKey={loadedContentKey}
|
||||||
|
contentLang={editLang}
|
||||||
doc={(editLang === 'en' ? draft.body_en : draft.body) as unknown as Record<string, unknown>}
|
doc={(editLang === 'en' ? draft.body_en : draft.body) as unknown as Record<string, unknown>}
|
||||||
onChange={(json) => {
|
onChange={(json) => {
|
||||||
// زمان مطالعه بهصورت پویا از متن سند محاسبه میشود — به زبان جاری
|
// زمان مطالعه بهصورت پویا از متن سند محاسبه میشود — به زبان جاری
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ type ArticleEditorProps = {
|
|||||||
* (مثلاً وقتی مقاله از سرور میرسد). بدون این prop محتوا فقط یکبار مقداردهی میشود.
|
* (مثلاً وقتی مقاله از سرور میرسد). بدون این prop محتوا فقط یکبار مقداردهی میشود.
|
||||||
*/
|
*/
|
||||||
contentKey?: string | number
|
contentKey?: string | number
|
||||||
|
/** زبان محتوای ویرایشگر — جهت و زبان عنصر ویرایش با آن همگام میشود */
|
||||||
|
contentLang?: 'fa' | 'en'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** نمونه ویرایشگر TipTap — همان چیزی که useEditor برمیگرداند */
|
/** نمونه ویرایشگر TipTap — همان چیزی که useEditor برمیگرداند */
|
||||||
@@ -186,7 +188,7 @@ const Divider = () => <span className="mx-1 h-5 w-px bg-ivory/10" aria-hidden="t
|
|||||||
* ویرایشگر WYSIWYG مقاله (TipTap) — نود تصویر کپشندار، مودال لینک سئو-محور،
|
* ویرایشگر WYSIWYG مقاله (TipTap) — نود تصویر کپشندار، مودال لینک سئو-محور،
|
||||||
* کلید راستچین/چپچین (فقط روی متن اثر دارد، نه نوار ابزار) و آپلود تصویر به MinIO.
|
* کلید راستچین/چپچین (فقط روی متن اثر دارد، نه نوار ابزار) و آپلود تصویر به MinIO.
|
||||||
*/
|
*/
|
||||||
export function ArticleEditor({ doc, onChange, contentKey }: ArticleEditorProps) {
|
export function ArticleEditor({ doc, onChange, contentKey, contentLang = 'fa' }: ArticleEditorProps) {
|
||||||
const { runSave } = useAdmin()
|
const { runSave } = useAdmin()
|
||||||
const fileRef = useRef<HTMLInputElement>(null)
|
const fileRef = useRef<HTMLInputElement>(null)
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
@@ -294,6 +296,11 @@ export function ArticleEditor({ doc, onChange, contentKey }: ArticleEditorProps)
|
|||||||
onUpdate: ({ editor: instance }) => onChange(instance.getJSON()),
|
onUpdate: ({ editor: instance }) => onChange(instance.getJSON()),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// جهت متن از زبان محتوای در حال ویرایش پیروی میکند — انگلیسی LTR، فارسی RTL
|
||||||
|
useEffect(() => {
|
||||||
|
setDirection(contentLang === 'en' ? 'ltr' : 'rtl')
|
||||||
|
}, [contentLang])
|
||||||
|
|
||||||
// جهت متن فقط روی ناحیه ویرایش اعمال میشود؛ نوار ابزار همیشه راستچین میماند
|
// جهت متن فقط روی ناحیه ویرایش اعمال میشود؛ نوار ابزار همیشه راستچین میماند
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
|
|||||||
Reference in New Issue
Block a user