From 7b4783bb3e4822d942d48f15576a6bc1f9fe61c7 Mon Sep 17 00:00:00 2001 From: aramesh-site deploy Date: Mon, 28 Sep 2026 08:36:30 +0000 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=D9=88=DB=8C=D8=B1=D8=A7=DB=8C?= =?UTF-8?q?=D8=B4=E2=80=8C=D9=BE=D8=B0=DB=8C=D8=B1=DB=8C=20=D9=87=DB=8C?= =?UTF-8?q?=D8=B1=D9=88=DB=8C=20=D8=B5=D9=81=D8=AD=D9=87=E2=80=8C=D9=87?= =?UTF-8?q?=D8=A7=20=D9=88=20=D9=BE=D8=B1=D9=88=DA=98=D9=87=E2=80=8C=D9=87?= =?UTF-8?q?=D8=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - page-heroes به فهرست موجودیت‌های پنل اضافه شد و PageHero یک دکمهٔ واضح «عنوان، سرآغاز و تصویر هیروی این صفحه» گرفت (پیش‌تر فقط ویرایش درجای نامرئی روی خود متن بود). - صفحهٔ پروژه‌ها و صفحهٔ جزئیات پروژه حالا از دیتابیس خوانده می‌شوند (فایل استاتیک projectsData فقط جانشین دیتابیس خالی/اسلاگ‌های قدیمی است) — پیش‌تر ویرایش پنل روی این دو صفحه اصلاً نمایش داده نمی‌شد. - روی لیست پروژه‌ها دکمهٔ ویرایش/جابه‌جایی/حذف و «افزودن پروژه» اضافه شد و روی صفحهٔ جزئیات هر پروژه دکمهٔ ویرایش همان پروژه. --- frontend/src/admin/entities.ts | 9 +++++ frontend/src/components/PageHero.tsx | 9 ++++- frontend/src/pages/ProjectDetail.tsx | 34 +++++++++++++++--- frontend/src/pages/Projects.tsx | 52 +++++++++++++++++++++------- 4 files changed, 85 insertions(+), 19 deletions(-) diff --git a/frontend/src/admin/entities.ts b/frontend/src/admin/entities.ts index b2690b8..ea13934 100644 --- a/frontend/src/admin/entities.ts +++ b/frontend/src/admin/entities.ts @@ -195,6 +195,15 @@ export const ENTITIES: Record = { fields: [F('title_highlight', 'واژه مسیِ تیتر'), F('badge_value', 'عدد کارت کنار'), F('badge_label', 'متن کارت کنار', 'textarea'), F('cta_primary_label', 'متن دکمه اصلی'), F('cta_primary_href', 'لینک دکمه اصلی', 'text', undefined, true), F('cta_secondary_label', 'متن دکمه دوم'), F('cta_secondary_href', 'لینک دکمه دوم', 'text', undefined, true), F('scroll_label', 'متن اسکرول'), F('kicker', 'متن بالای تیتر', 'textarea')], defaults: {}, }, + 'page-heroes': { + resource: 'page-heroes', dataKey: 'page_heroes', title: 'هیروی صفحه‌ها', kind: 'collection', + titleKey: 'title', + fields: [ + F('key', 'کلید صفحه', 'text', 'about / services / projects / investment / blog / faq / contact', true), + F('title', 'عنوان'), F('lead', 'سرآغاز', 'textarea'), F('image', 'تصویر', 'image', undefined, true), + ], + defaults: { key: 'about', title: 'عنوان صفحه', lead: '', image: '' }, + }, story: { resource: 'story', dataKey: 'story', title: 'تصاویر بخش درباره ما', kind: 'singleton', titleKey: 'title', diff --git a/frontend/src/components/PageHero.tsx b/frontend/src/components/PageHero.tsx index 94709bb..ee685c3 100644 --- a/frontend/src/components/PageHero.tsx +++ b/frontend/src/components/PageHero.tsx @@ -74,8 +74,15 @@ export function PageHero({ pageKey }: PageHeroProps) {
- {/* ویرایش تصویر پس‌زمینه و سئوی صفحه — فقط در حالت ویرایش ادمین */} + {/* ویرایش هیروی صفحه (عنوان/سرآغاز/تصویر)، تصویر پس‌زمینه و سئو — فقط در حالت ویرایش ادمین */}
+ d.page_heroes.find((h) => h.key === pageKey)} + // اگر ردیف هیروی این صفحه هنوز ساخته نشده باشد، همین‌جا ساخته می‌شود + createDefaults={() => ({ key: pageKey, title: '', lead: '', image: '' })} + /> () const { projects, brand } = useSite() const { t, href, lang } = useLang() - const currentProject = projectsData.find((p) => p.slug === slug) - + // اولویت با رکورد دیتابیس است تا ویرایش پنل روی صفحهٔ جزئیات هم دیده شود؛ + // فایل استاتیک فقط برای اسلاگ‌های قدیمی که در دیتابیس نیستند به کار می‌رود. + const dbProject = projects.find((p) => p.slug === slug) + const staticProject = projectsData.find((p) => p.slug === slug) + const currentProject = dbProject + ? { + id: String(dbProject.id), + slug: dbProject.slug, + name: { en: dbProject.title_en || dbProject.title, fa: dbProject.title }, + location: { en: dbProject.location_en || dbProject.location, fa: dbProject.location }, + area: { en: dbProject.area_en || dbProject.area, fa: dbProject.area }, + status: (dbProject.status === 'progress' ? 'progress' : 'done') as 'done' | 'progress', + } + : staticProject + const [lightbox, setLightbox] = useState(null) if (!currentProject) { return
Project not found
} - const backendProject = projects.find((p) => p.slug === slug) + const backendProject = dbProject // Dynamic bilingual text from projectsData const displayTitle = currentProject.name[lang as 'en' | 'fa'] || currentProject.name.en; @@ -76,7 +90,10 @@ export function ProjectDetail() { }); }, [currentProject.slug, lang, href, displayTitle, summary, image, brand.name]); - const related = projectsData.filter((p) => p.slug !== currentProject.slug).slice(0, 3) + // پروژه‌های مرتبط — از دیتابیس، و اگر خالی بود از فهرست استاتیک قدیمی + const related = (projects.length > 0 ? projects : projectsData) + .filter((p) => p.slug !== currentProject.slug) + .slice(0, 3) const facts = [ { label: t('specLocation'), value: displayLocation }, @@ -104,8 +121,15 @@ export function ProjectDetail() { {displayTitle} -
+

{displayTitle}

+ {dbProject && ( + d.projects.find((p) => p.slug === slug)} + /> + )}
{/* Gallery Desktop: Grid. Mobile: Grid */} diff --git a/frontend/src/pages/Projects.tsx b/frontend/src/pages/Projects.tsx index c5a7110..e7b1899 100644 --- a/frontend/src/pages/Projects.tsx +++ b/frontend/src/pages/Projects.tsx @@ -9,12 +9,13 @@ import { useSite, useStatusLabel } from '../api'; import { useLang } from '../i18n/lang'; import { useFieldSaver } from '../admin/AdminContext'; import { InlineEdit } from '../components/admin/InlineEdit'; +import { AddItemButton, EditableItem } from '../admin/Editable'; import { projectsData } from '../data/projectsData'; import { LazyProjectsMapAll } from '../map/LazyMaps'; import { MAP_PROJECTS, type MapProject } from '../data/mapProjects'; export function Projects() { - const { status_labels } = useSite(); + const { status_labels, projects: dbProjects } = useSite(); const { t } = useLang(); const save = useFieldSaver(); @@ -35,6 +36,14 @@ export function Projects() { ]; const [status, setStatus] = useState<'done' | 'progress'>('done'); + // پروژه‌ها از دیتابیس خوانده می‌شوند تا ویرایش پنل همان‌جا دیده شود؛ + // فایل استاتیک فقط وقتی دیتابیس خالی است جانشین می‌شود (محتوای قدیمی). + const fromDb = dbProjects.length > 0; + const cardProjects = useMemo(() => { + if (fromDb) return dbProjects; + return projectsData.map((p) => ({ ...p, id: Number(p.id) })); + }, [fromDb, dbProjects]); + // Filtering and Pagination State const [page, setPage] = useState(1); const [isLoading, setIsLoading] = useState(false); @@ -42,8 +51,8 @@ export function Projects() { // Filter projects by status const filteredProjects = useMemo(() => { - return projectsData.filter((p) => p.status === status); - }, [status]); + return cardProjects.filter((p) => p.status === status); + }, [cardProjects, status]); // Currently displayed projects const displayedProjects = useMemo(() => { @@ -68,10 +77,15 @@ export function Projects() { const BASE_IMG_URL = "/seed-media/2026/09/"; - // Resolve a project href from a MapProject entry by matching against projectsData slugs + // Resolve a project href from a MapProject entry by matching names — first in the + // database (so pins follow panel edits), then in the static legacy list. const resolveProjectHref = (mapProject: MapProject): string | undefined => { + const dbMatch = dbProjects.find( + (p) => p.title === mapProject.name || p.title_en === mapProject.name, + ); + if (dbMatch) return `/projects/${dbMatch.slug}`; const match = projectsData.find( - (p) => p.name.fa === mapProject.name || p.name.en === mapProject.name + (p) => p.name.fa === mapProject.name || p.name.en === mapProject.name, ); return match ? `/projects/${match.slug}` : undefined; }; @@ -131,19 +145,31 @@ export function Projects() {
{displayedProjects.map((project, index) => { - const dummyImageName = `${(index % 6) + 1}.png`; - const imageUrl = `${BASE_IMG_URL}${dummyImageName}`; - + // تصویر رکورد دیتابیس اولویت دارد؛ فقط ردیف‌های بدون تصویر از + // تصاویر جایگزین تکراری استفاده می‌کنند. + const hasOwnImage = 'image' in project && Boolean((project as { image?: string }).image); + const imageUrl = hasOwnImage ? undefined : `${BASE_IMG_URL}${(index % 6) + 1}.png`; + const card = ( + + ); + return ( - + {fromDb ? ( + + {card} + + ) : ( + card + )} ); })} + {fromDb && }
{/* Load More Button */}