fix(admin): resolve Tiptap editor bugs, dynamic RTL/LTR layout, sticky TOC, and equalize Bidi typography
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useId, useRef, useState } from 'react'
|
||||
import { useId, useRef, useState } from 'react'
|
||||
import { ImageIcon, ImagePlusIcon, Loader2Icon } from 'lucide-react'
|
||||
import { DEFAULT_IMAGE_TITLE } from '../../data/constants'
|
||||
import { adminApi } from '../../admin/adminClient'
|
||||
import { useAdmin } from '../../admin/AdminContext'
|
||||
import { useSmartTextField } from './useSmartTextField'
|
||||
|
||||
type ImageFieldProps = {
|
||||
label: string
|
||||
@@ -30,6 +31,7 @@ const META_FIELD_CLASS =
|
||||
* انتخاب و آپلود تصویر به MinIO با پیشنمایش بزرگ.
|
||||
* ترتیب فیلدها: ۱) عنوان تصویر (پیشفرض «Photo 1») ۲) متن جایگزین (پیشفرض «{عنوان} - تصویر شاخص»).
|
||||
* هر دو «دستی یا پیشفرض» هستند و Alt هرگز از URL ساخته نمیشود.
|
||||
* پیشفرض خودکار با فوکوس پاک میشود تا کاربر بلافاصله تایپ کند و با بلورِ خالی برمیگردد.
|
||||
*/
|
||||
export function ImageField({
|
||||
label,
|
||||
@@ -47,40 +49,25 @@ export function ImageField({
|
||||
const titleId = useId()
|
||||
const altId = useId()
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [altText, setAltText] = useState(alt ?? '')
|
||||
const [titleText, setTitleText] = useState(title ?? '')
|
||||
|
||||
const showTitle = title !== undefined || !!onSaveTitle
|
||||
const showAlt = alt !== undefined || !!onSaveAlt
|
||||
|
||||
// همگامسازی با مقادیر ذخیرهشده (پس از بهروزرسانی خوشبینانه در درخت داده)
|
||||
useEffect(() => {
|
||||
setAltText(alt ?? '')
|
||||
}, [alt])
|
||||
useEffect(() => {
|
||||
setTitleText(title ?? '')
|
||||
}, [title])
|
||||
const titleField = useSmartTextField({
|
||||
value: title ?? '',
|
||||
fallback: titleFallback,
|
||||
hardFallback: DEFAULT_IMAGE_TITLE,
|
||||
onCommit: onSaveTitle,
|
||||
})
|
||||
const altField = useSmartTextField({
|
||||
value: alt ?? '',
|
||||
fallback: altFallback,
|
||||
hardFallback: label,
|
||||
onCommit: onSaveAlt,
|
||||
})
|
||||
|
||||
if (!isAuthenticated || !isEditMode) return null
|
||||
|
||||
/** عنوان نمایشی: ورودی کاربر → پیشنهاد → «Photo 1» (با تغییر عنوان مقاله زنده بهروز میشود) */
|
||||
const titleDisplayed = titleText.trim() || titleFallback.trim() || DEFAULT_IMAGE_TITLE
|
||||
|
||||
/** Alt نمایشی: ورودی کاربر → پیشنهاد عنوانمحور (هرگز URL) */
|
||||
const altDisplayed = altText.trim() || altFallback.trim()
|
||||
|
||||
const resolveTitle = () => titleDisplayed
|
||||
const resolveAlt = () => altDisplayed || label
|
||||
|
||||
const persistMeta = async () => {
|
||||
const nextTitle = resolveTitle()
|
||||
const nextAlt = resolveAlt()
|
||||
setTitleText(nextTitle)
|
||||
setAltText(nextAlt)
|
||||
if (showTitle && nextTitle !== (title ?? '').trim() && onSaveTitle) await onSaveTitle(nextTitle)
|
||||
if (showAlt && nextAlt !== (alt ?? '').trim() && onSaveAlt) await onSaveAlt(nextAlt)
|
||||
}
|
||||
|
||||
const pick = async (file: File | undefined) => {
|
||||
if (!file) return
|
||||
setUploading(true)
|
||||
@@ -89,18 +76,15 @@ export function ImageField({
|
||||
const { url } = await adminApi.upload(file)
|
||||
const ok = await onSave(url)
|
||||
if (!ok) throw new Error('ذخیره نشد')
|
||||
await persistMeta()
|
||||
// پیشفرضهای Alt/Title همان لحظه ثبت میشوند تا تصویر بدون متادیتا نماند
|
||||
if (showTitle) await titleField.commit()
|
||||
if (showAlt) await altField.commit()
|
||||
})
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const commitMeta = () => {
|
||||
if (!onSaveTitle && !onSaveAlt) return
|
||||
void persistMeta()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="text-xs font-bold text-ivory/80">{label}</p>
|
||||
@@ -110,8 +94,8 @@ export function ImageField({
|
||||
{value ? (
|
||||
<img
|
||||
src={value}
|
||||
alt={altText || label}
|
||||
title={titleText || undefined}
|
||||
alt={altField.display || label}
|
||||
title={titleField.display || undefined}
|
||||
className="h-[27rem] w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
@@ -149,10 +133,11 @@ export function ImageField({
|
||||
id={titleId}
|
||||
type="text"
|
||||
dir="auto"
|
||||
value={titleDisplayed}
|
||||
onChange={(event) => setTitleText(event.target.value)}
|
||||
onBlur={commitMeta}
|
||||
placeholder={titleFallback || DEFAULT_IMAGE_TITLE}
|
||||
value={titleField.display}
|
||||
onChange={(event) => titleField.onChange(event.target.value)}
|
||||
onFocus={titleField.onFocus}
|
||||
onBlur={titleField.onBlur}
|
||||
placeholder={titleFallback.trim() || DEFAULT_IMAGE_TITLE}
|
||||
className={META_FIELD_CLASS}
|
||||
/>
|
||||
</div>
|
||||
@@ -167,11 +152,12 @@ export function ImageField({
|
||||
id={altId}
|
||||
type="text"
|
||||
dir="auto"
|
||||
value={altDisplayed}
|
||||
onChange={(event) => setAltText(event.target.value)}
|
||||
onBlur={commitMeta}
|
||||
value={altField.display}
|
||||
onChange={(event) => altField.onChange(event.target.value)}
|
||||
onFocus={altField.onFocus}
|
||||
onBlur={altField.onBlur}
|
||||
placeholder={
|
||||
altFallback || 'توضیح کوتاه تصویر برای سئو و صفحهخوانها'
|
||||
altFallback.trim() || 'توضیح کوتاه تصویر برای سئو و صفحهخوانها'
|
||||
}
|
||||
className={META_FIELD_CLASS}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user