فرانتاند: ویرایش زنده درجا — دوقلوی دیجیتال سایت برای ادمین
لایه ویرایش بصری روی سایت واقعی (بدون داشبورد جدا): - ورود ادمین از مسیر پنهان /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:
@@ -5,11 +5,24 @@ import { ProjectCard } from '../components/ProjectCard';
|
||||
import { Gallery } from '../components/home/Gallery';
|
||||
import { ContactSection } from '../components/ContactSection';
|
||||
import type { ProjectStatus } from '../data/site';
|
||||
import { usePageHero, useSite, useStatusLabel } from '../api';
|
||||
import { useSite, useStatusLabel } from '../api';
|
||||
import { useFieldSaver } from '../admin/AdminContext';
|
||||
import { InlineEdit } from '../components/admin/InlineEdit';
|
||||
import { AddItemButton, EditableItem } from '../admin/Editable';
|
||||
|
||||
export function Projects() {
|
||||
const { projects } = useSite();
|
||||
const hero = usePageHero('projects');
|
||||
const { projects, status_labels } = useSite();
|
||||
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 doneLabel = useStatusLabel('done');
|
||||
const progressLabel = useStatusLabel('progress');
|
||||
const FILTERS: {key: ProjectStatus;label: string;}[] = [
|
||||
@@ -20,7 +33,7 @@ export function Projects() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHero title={hero.title} lead={hero.lead} image={hero.image} />
|
||||
<PageHero pageKey="projects" />
|
||||
|
||||
|
||||
<section className="bg-ink py-24 lg:py-32" aria-label="فهرست پروژهها">
|
||||
@@ -46,7 +59,10 @@ export function Projects() {
|
||||
|
||||
}
|
||||
<span className="relative z-10 transition-colors duration-300 ease-lux group-hover:text-char">
|
||||
{filter.label}
|
||||
<InlineEdit
|
||||
value={filter.label}
|
||||
onSave={(v) => saveStatusLabel(filter.key, v)}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -54,10 +70,13 @@ export function Projects() {
|
||||
|
||||
<div className="mt-12 grid gap-6 lg:grid-cols-3">
|
||||
{list.map((project, i) =>
|
||||
<Reveal key={project.slug} delay={0.05 * i} className="h-full">
|
||||
<ProjectCard project={project} imageClass="h-80" />
|
||||
<Reveal key={project.id} delay={0.05 * i} className="h-full">
|
||||
<EditableItem resource="projects" item={project} className="h-full">
|
||||
<ProjectCard project={project} imageClass="h-80" />
|
||||
</EditableItem>
|
||||
</Reveal>
|
||||
)}
|
||||
<AddItemButton resource="projects" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user