فرانتاند: ویرایش زنده درجا — دوقلوی دیجیتال سایت برای ادمین
لایه ویرایش بصری روی سایت واقعی (بدون داشبورد جدا): - ورود ادمین از مسیر پنهان /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:
@@ -3,6 +3,9 @@ import { ArrowLeftIcon } from 'lucide-react'
|
||||
import { Reveal } from '../Reveal'
|
||||
import { ButtonLink } from '../Button'
|
||||
import { useSite } from '../../api'
|
||||
import { useFieldSaver } from '../../admin/AdminContext'
|
||||
import { InlineEdit } from '../admin/InlineEdit'
|
||||
import { SingletonPill } from '../../admin/Editable'
|
||||
|
||||
type StoryProps = {
|
||||
showLink?: boolean;
|
||||
@@ -14,6 +17,19 @@ type StoryProps = {
|
||||
|
||||
export function Story({ showLink = true, showLabel = true, showQuote = false, alignTop = false, children }: StoryProps) {
|
||||
const { story } = useSite()
|
||||
const save = useFieldSaver()
|
||||
|
||||
const saveStory = (field: string, value: unknown) =>
|
||||
save('story', story.id, { [field]: value }, (d) => {
|
||||
;(d.story as unknown as Record<string, unknown>)[field] = value
|
||||
})
|
||||
|
||||
const saveParagraph = (index: number, value: string) => {
|
||||
const paragraphs = [...story.paragraphs]
|
||||
paragraphs[index] = value
|
||||
void saveStory('paragraphs', paragraphs)
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="story" className="bg-ink py-24 lg:py-32" aria-labelledby="story-title">
|
||||
<div className={`mx-auto grid max-w-site gap-14 px-6 lg:grid-cols-2 lg:px-10 ${alignTop ? 'items-start' : 'items-center'}`}>
|
||||
@@ -21,7 +37,7 @@ export function Story({ showLink = true, showLabel = true, showQuote = false, al
|
||||
{showLabel && (
|
||||
<Reveal>
|
||||
<h2 id="story-title" className="text-section font-bold text-ivory">
|
||||
{story.title}
|
||||
<InlineEdit value={story.title} onSave={(v) => saveStory('title', v)} />
|
||||
</h2>
|
||||
<div className="mt-6 h-px w-24 bg-copper/70" />
|
||||
</Reveal>
|
||||
@@ -30,7 +46,7 @@ export function Story({ showLink = true, showLabel = true, showQuote = false, al
|
||||
<Reveal delay={0.08}>
|
||||
{story.paragraphs.map((paragraph, i) => (
|
||||
<p key={i} className={`text-base font-light leading-9 text-ivory/65 ${i === 0 ? 'mt-8' : 'mt-5'}`}>
|
||||
{paragraph}
|
||||
<InlineEdit multiline value={paragraph} onSave={(v) => saveParagraph(i, v)} />
|
||||
</p>
|
||||
))}
|
||||
</Reveal>
|
||||
@@ -38,12 +54,20 @@ export function Story({ showLink = true, showLabel = true, showQuote = false, al
|
||||
<Reveal delay={0.16}>
|
||||
<div className="mt-10 grid grid-cols-2 gap-8 border-t border-ivory/10 pt-8">
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-copper">{story.stat1_value}</p>
|
||||
<p className="mt-2 text-sm font-light text-ivory/55">{story.stat1_label}</p>
|
||||
<p className="text-2xl font-bold text-copper">
|
||||
<InlineEdit value={story.stat1_value} onSave={(v) => saveStory('stat1_value', v)} />
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-light text-ivory/55">
|
||||
<InlineEdit value={story.stat1_label} onSave={(v) => saveStory('stat1_label', v)} />
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-copper">{story.stat2_value}</p>
|
||||
<p className="mt-2 text-sm font-light text-ivory/55">{story.stat2_label}</p>
|
||||
<p className="text-2xl font-bold text-copper">
|
||||
<InlineEdit value={story.stat2_value} onSave={(v) => saveStory('stat2_value', v)} />
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-light text-ivory/55">
|
||||
<InlineEdit value={story.stat2_label} onSave={(v) => saveStory('stat2_label', v)} />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
@@ -51,8 +75,12 @@ export function Story({ showLink = true, showLabel = true, showQuote = false, al
|
||||
{showQuote && story.quote && (
|
||||
<Reveal delay={0.2}>
|
||||
<div className="mt-12 border-r-2 border-copper bg-char/40 p-6">
|
||||
<p className="text-sm font-light leading-8 text-ivory/80">{story.quote}</p>
|
||||
<p className="mt-5 text-sm font-bold text-copper">{story.quote_author}</p>
|
||||
<p className="text-sm font-light leading-8 text-ivory/80">
|
||||
<InlineEdit multiline value={story.quote} onSave={(v) => saveStory('quote', v)} />
|
||||
</p>
|
||||
<p className="mt-5 text-sm font-bold text-copper">
|
||||
<InlineEdit value={story.quote_author} onSave={(v) => saveStory('quote_author', v)} />
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
)}
|
||||
@@ -60,7 +88,7 @@ export function Story({ showLink = true, showLabel = true, showQuote = false, al
|
||||
{showLink && (
|
||||
<Reveal delay={0.22}>
|
||||
<ButtonLink to={story.link_href} variant="outline" className="mt-10 font-light">
|
||||
{story.link_label}
|
||||
<InlineEdit value={story.link_label} onSave={(v) => saveStory('link_label', v)} />
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
</ButtonLink>
|
||||
</Reveal>
|
||||
@@ -73,7 +101,8 @@ export function Story({ showLink = true, showLabel = true, showQuote = false, al
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="relative grid grid-cols-2 gap-4">
|
||||
<SingletonPill resource="story" label="تصاویر و نقلقول بخش درباره ما" className="absolute -top-10 right-0" />
|
||||
{story.bento.map((item, i) => (
|
||||
<Reveal key={item.src + i} delay={0.06 * i}>
|
||||
<div className="overflow-hidden">
|
||||
|
||||
Reference in New Issue
Block a user