فرانتاند: ویرایش زنده درجا — دوقلوی دیجیتال سایت برای ادمین
لایه ویرایش بصری روی سایت واقعی (بدون داشبورد جدا): - ورود ادمین از مسیر پنهان /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:
@@ -2,41 +2,55 @@ import React from 'react'
|
||||
import { Reveal } from '../Reveal'
|
||||
import { ServiceIcon } from '../ServiceIcon'
|
||||
import { useSectionHeading, useSite } from '../../api'
|
||||
import { useFieldSaver } from '../../admin/AdminContext'
|
||||
import { InlineEdit } from '../admin/InlineEdit'
|
||||
import { AddItemButton, EditableItem } from '../../admin/Editable'
|
||||
|
||||
export function Services() {
|
||||
const { services } = useSite()
|
||||
const heading = useSectionHeading('services')
|
||||
const save = useFieldSaver()
|
||||
|
||||
const saveHeading = (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[field] = value
|
||||
})
|
||||
|
||||
return (
|
||||
<section className="bg-ink py-24 lg:py-32" aria-labelledby="services-title">
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
<Reveal className="max-w-2xl text-right">
|
||||
|
||||
<h2 id="services-title" className="text-section font-bold text-ivory">
|
||||
{heading.title}
|
||||
<InlineEdit value={heading.title} onSave={(v) => saveHeading('title', v)} />
|
||||
</h2>
|
||||
<p className="mt-5 text-base font-light leading-9 text-ivory/60">
|
||||
{heading.lead}
|
||||
<InlineEdit multiline value={heading.lead} onSave={(v) => saveHeading('lead', v)} />
|
||||
</p>
|
||||
<div className="mt-6 h-px w-24 bg-copper/70" />
|
||||
</Reveal>
|
||||
|
||||
<div className="mt-14 grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{services.map((service, i) => (
|
||||
<Reveal key={service.title} delay={0.04 * i} className="h-full">
|
||||
<article className="group flex h-full flex-col items-center border border-ivory/10 bg-char p-8 text-center transition-all duration-300 ease-out hover:-translate-y-2 hover:border-copper/40 hover:shadow-lift transform-gpu will-change-transform">
|
||||
<ServiceIcon
|
||||
name={service.icon}
|
||||
className="h-12 w-12 text-copper/85 transition-colors duration-300 ease-out group-hover:text-copper"
|
||||
/>
|
||||
<h3 className="mt-6 text-lg font-bold text-ivory transition-colors duration-300 ease-out group-hover:text-copper">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm font-light leading-8 text-ivory/55">{service.desc}</p>
|
||||
</article>
|
||||
<Reveal key={service.id} delay={0.04 * i} className="h-full">
|
||||
<EditableItem resource="services" item={service} className="h-full">
|
||||
<article className="group flex h-full flex-col items-center border border-ivory/10 bg-char p-8 text-center transition-all duration-300 ease-out hover:-translate-y-2 hover:border-copper/40 hover:shadow-lift transform-gpu will-change-transform">
|
||||
<ServiceIcon
|
||||
name={service.icon}
|
||||
className="h-12 w-12 text-copper/85 transition-colors duration-300 ease-out group-hover:text-copper"
|
||||
/>
|
||||
<h3 className="mt-6 text-lg font-bold text-ivory transition-colors duration-300 ease-out group-hover:text-copper">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm font-light leading-8 text-ivory/55">{service.desc}</p>
|
||||
</article>
|
||||
</EditableItem>
|
||||
</Reveal>
|
||||
))}
|
||||
<AddItemButton resource="services" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user