فرانتاند: ویرایش زنده درجا — دوقلوی دیجیتال سایت برای ادمین
لایه ویرایش بصری روی سایت واقعی (بدون داشبورد جدا): - ورود ادمین از مسیر پنهان /admin/login با توکن JWT (localStorage) و AdminContext شامل isEditMode و وضعیت ذخیره (همگام/در حال ذخیره/خطا) - AdminBar شناور پایین صفحه (فقط برای کاربر واردشده): کلید حالت ویرایش، وضعیت ذخیره، خروج — حالت ویرایش در sessionStorage میماند - InlineEdit: در حالت عادی دقیقاً همان HTML قبلی (بدون تغییر چیدمان)؛ در حالت ویرایش کادرخطچین + contentEditable با ذخیره خودکار در blur (Enter ذخیره، Esc لغو) — وایرایششده روی همه متنهای سایت: هیرو، تیتر بخشها، هیرو صفحات، داستان، فرم تماس، خبرنامه، فوتر، منو، برچسبهای وضعیت پروژه و... - کشوی عمومی EntityDrawer با پیکربندی متمرکز برای ۱۴ مجموعه (خدمات، پروژهها، مقالات، نظرات، سوالات، تیم، تایملاین، آمار، چرا آرامش، مدلهای سرمایهگذاری، مراحل کار، دفاتر، گالری، منو): ویرایش چندفیلدی، افزودن، حذف و جابهجایی ترتیب (▲▼) با ذخیره خودکار هر فیلد - دکمههای شناور ویرایش/حذف/ترتیب فقط در حالت ویرایش روی آیتمها - کشوهای یگانه: برند و لوگو، تنظیمات هیرو، فرم تماس، خبرنامه، فوتر، قبل/بعد و نقلقول داستان — آپلود تصویر به MinIO از همانجا - بهروزرسانی خوشبینانه SiteContext (updateSite) پس از هر PATCH؛ مسیرها در حالت ویرایش کلیک نمیشوند؛ breadcrumb محافظ beforeunload
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { CSSProperties, ElementType, KeyboardEvent } from 'react'
|
||||
import { useAdmin } from '../../admin/AdminContext'
|
||||
|
||||
type InlineEditProps = {
|
||||
value: string
|
||||
/** ذخیره async — در صورت خطا، متن به مقدار قبلی برمیگردد */
|
||||
onSave: (value: string) => Promise<boolean> | boolean | void
|
||||
as?: ElementType
|
||||
/** چندخطی: Enter خط جدید میگذارد و فقط blur ذخیره میکند */
|
||||
multiline?: boolean
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
'aria-label'?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* ویرایش درجا متن — در حالت عادی دقیقاً همان HTML قبلی را رندر میکند
|
||||
* (بدون هیچ تغییر چیدمان) و فقط در Edit Mode برای ادمین قابل ویرایش میشود.
|
||||
*/
|
||||
export function InlineEdit({
|
||||
value,
|
||||
onSave,
|
||||
as: Tag = 'span',
|
||||
multiline = false,
|
||||
className = '',
|
||||
style,
|
||||
...rest
|
||||
}: InlineEditProps) {
|
||||
const { isAuthenticated, isEditMode } = useAdmin()
|
||||
const editing = isAuthenticated && isEditMode
|
||||
const ref = useRef<HTMLElement>(null)
|
||||
const [focused, setFocused] = useState(false)
|
||||
|
||||
// همگامسازی متن فقط وقتی المان در حال ویرایش نیست (پرش نشانگر رخ ندهد)
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
if (el && !focused && el.innerText !== value) el.innerText = value
|
||||
}, [value, focused])
|
||||
|
||||
if (!editing) {
|
||||
return (
|
||||
<Tag className={className} style={style} {...rest}>
|
||||
{value}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
const commit = async () => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
setFocused(false)
|
||||
const next = el.innerText.replace(/\u00a0/g, ' ')
|
||||
if (next === value) return
|
||||
el.innerText = value // بازگشت فوری به مقدار فعلی؛ در صورت موفقیت، بهروزرسانی خوشبینانه متن تازه میآید
|
||||
await onSave(next)
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent<HTMLElement>) => {
|
||||
if (!multiline && e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
;(e.currentTarget as HTMLElement).blur()
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
if (ref.current) ref.current.innerText = value
|
||||
;(e.currentTarget as HTMLElement).blur()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag
|
||||
ref={ref as never}
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
spellCheck={false}
|
||||
data-inline-edit
|
||||
aria-label={rest['aria-label']}
|
||||
className={`${className} cursor-text rounded-sm transition-[outline-color,background-color] duration-150 outline-1 outline-dashed outline-transparent hover:bg-copper/5 hover:outline-copper/40 focus:bg-copper/5 focus:outline-copper focus:outline-solid`}
|
||||
style={style}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={commit}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
{value}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user