فرانتاند: ویرایش زنده درجا — دوقلوی دیجیتال سایت برای ادمین
لایه ویرایش بصری روی سایت واقعی (بدون داشبورد جدا): - ورود ادمین از مسیر پنهان /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:
@@ -6,16 +6,27 @@ import { ButtonLink } from '../Button'
|
||||
import { ProjectCard } from '../ProjectCard'
|
||||
import { SectionHeading } from '../SectionHeading'
|
||||
import { useSectionHeading, useSite, useStatusLabel } from '../../api'
|
||||
import { useFieldSaver } from '../../admin/AdminContext'
|
||||
import { InlineEdit } from '../admin/InlineEdit'
|
||||
import { AddItemButton, EditableItem } from '../../admin/Editable'
|
||||
import type { ProjectStatus } from '../../data/site'
|
||||
|
||||
export function FeaturedProjects() {
|
||||
const { projects } = useSite()
|
||||
const { projects, status_labels } = useSite()
|
||||
const heading = useSectionHeading('featured_projects')
|
||||
const doneLabel = useStatusLabel('done')
|
||||
const progressLabel = useStatusLabel('progress')
|
||||
const save = useFieldSaver()
|
||||
const saveStatusLabel = (key: string, value: string) => {
|
||||
const label = status_labels.find((s) => s.key === key)
|
||||
if (!label) return
|
||||
void save('status-labels', label.id, { label: value }, (d) => {
|
||||
const s = d.status_labels.find((x) => x.id === label.id)
|
||||
if (s) s.label = value
|
||||
})
|
||||
}
|
||||
|
||||
const FILTERS: { key: ProjectStatus; label: string }[] = [
|
||||
{ key: 'done', label: doneLabel },
|
||||
{ key: 'progress', label: progressLabel },
|
||||
{ key: 'done', label: useStatusLabel('done') },
|
||||
{ key: 'progress', label: useStatusLabel('progress') },
|
||||
]
|
||||
const [status, setStatus] = useState<ProjectStatus>('done')
|
||||
const list = projects.filter((p) => p.status === status).slice(0, 3)
|
||||
@@ -23,7 +34,7 @@ export function FeaturedProjects() {
|
||||
return (
|
||||
<section className="bg-ink py-24 lg:py-32" aria-labelledby="projects-title">
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
<SectionHeading title={heading.title} lead={heading.lead} />
|
||||
<SectionHeading title={heading.title} lead={heading.lead} headingKey="featured_projects" />
|
||||
|
||||
<div className="mt-10 flex flex-wrap items-center gap-3" role="tablist" aria-label="فیلتر وضعیت پروژه">
|
||||
{FILTERS.map((filter) => (
|
||||
@@ -46,7 +57,10 @@ export function FeaturedProjects() {
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-10 transition-colors duration-300 ease-out group-hover:text-char">
|
||||
{filter.label}
|
||||
<InlineEdit
|
||||
value={filter.label}
|
||||
onSave={(v) => saveStatusLabel(filter.key, v)}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
@@ -55,7 +69,7 @@ export function FeaturedProjects() {
|
||||
<div className="mt-10 min-h-[420px]">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={status}
|
||||
key={status}
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -15 }}
|
||||
@@ -64,13 +78,15 @@ export function FeaturedProjects() {
|
||||
>
|
||||
{list.map((project, i) => (
|
||||
<motion.div
|
||||
key={project.slug}
|
||||
key={project.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: i * 0.1, ease: [0.23, 1, 0.32, 1] }}
|
||||
className="h-full"
|
||||
>
|
||||
<ProjectCard project={project} />
|
||||
<EditableItem resource="projects" item={project} className="h-full">
|
||||
<ProjectCard project={project} />
|
||||
</EditableItem>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
@@ -81,7 +97,15 @@ export function FeaturedProjects() {
|
||||
<Reveal delay={0.1}>
|
||||
<div className="mt-12 flex justify-center">
|
||||
<ButtonLink to={heading.cta_href} variant="outline" className="font-light">
|
||||
{heading.cta_label}
|
||||
<InlineEdit
|
||||
value={heading.cta_label}
|
||||
onSave={(v) =>
|
||||
save('section-headings', heading.id, { cta_label: v }, (d) => {
|
||||
const h = d.section_headings.find((s) => s.id === heading.id)
|
||||
if (h) h.cta_label = v
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
</ButtonLink>
|
||||
</div>
|
||||
@@ -90,4 +114,4 @@ export function FeaturedProjects() {
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user