فرانت‌اند: ویرایش زنده درجا — دوقلوی دیجیتال سایت برای ادمین

لایه ویرایش بصری روی سایت واقعی (بدون داشبورد جدا):

- ورود ادمین از مسیر پنهان /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:
MMD
2026-09-06 21:57:46 +03:30
parent 6e422ec3dc
commit 5ed082631e
39 changed files with 1882 additions and 260 deletions
+37 -8
View File
@@ -1,29 +1,58 @@
import React from 'react'
import { Reveal } from './Reveal'
import { useSectionHeading } from '../api'
import { useFieldSaver } from '../admin/AdminContext'
import { InlineEdit } from './admin/InlineEdit'
type SectionHeadingProps = {
title: string
/** در صورت ارسال، تیتر و توضیح از دیتابیس خوانده و به‌صورت درجا ویرایش‌پذیر می‌شوند */
headingKey?: string
title?: string
lead?: string
align?: 'right' | 'center'
tone?: 'copper' | 'ivory'
}
export function SectionHeading({
headingKey,
title,
lead,
align = 'right',
tone = 'ivory',
}: SectionHeadingProps) {
const heading = useSectionHeading(headingKey ?? '')
const save = useFieldSaver()
const isDb = !!headingKey
const currentTitle = isDb ? heading.title : (title ?? '')
const currentLead = isDb ? heading.lead : (lead ?? '')
const isCenter = align === 'center'
const saveField = (field: 'title' | 'lead', value: string) =>
save('section-headings', heading.id, { [field]: value }, (d) => {
const h = d.section_headings.find((s) => s.id === heading.id)
if (h) (h as Record<string, unknown>)[field] = value
})
return (
<Reveal className={isCenter ? 'mx-auto max-w-2xl text-center' : 'max-w-2xl text-right'}>
<h2 className={`text-section font-bold ${tone === 'copper' ? 'text-copper' : 'text-ivory'}`}>
{title}
</h2>
{lead && <p className="mt-5 text-base font-light leading-9 text-ivory/60">{lead}</p>}
<InlineEdit
as="h2"
value={currentTitle}
onSave={(v) => saveField('title', v)}
className={`text-section font-bold ${tone === 'copper' ? 'text-copper' : 'text-ivory'}`}
/>
{currentLead && (
<InlineEdit
as="p"
multiline
value={currentLead}
onSave={(v) => saveField('lead', v)}
className="mt-5 text-base font-light leading-9 text-ivory/60"
/>
)}
<div className={`mt-6 h-px w-24 bg-copper/70 ${isCenter ? 'mx-auto' : ''}`} />
</Reveal>
)
}
}