feat(admin): ویرایش‌پذیری هیروی صفحه‌ها و پروژه‌ها

- page-heroes به فهرست موجودیت‌های پنل اضافه شد و PageHero یک دکمهٔ واضح
  «عنوان، سرآغاز و تصویر هیروی این صفحه» گرفت (پیش‌تر فقط ویرایش درجای
  نامرئی روی خود متن بود).
- صفحهٔ پروژه‌ها و صفحهٔ جزئیات پروژه حالا از دیتابیس خوانده می‌شوند
  (فایل استاتیک projectsData فقط جانشین دیتابیس خالی/اسلاگ‌های قدیمی است) —
  پیش‌تر ویرایش پنل روی این دو صفحه اصلاً نمایش داده نمی‌شد.
- روی لیست پروژه‌ها دکمهٔ ویرایش/جابه‌جایی/حذف و «افزودن پروژه» اضافه شد و
  روی صفحهٔ جزئیات هر پروژه دکمهٔ ویرایش همان پروژه.
This commit is contained in:
aramesh-site deploy
2026-09-28 08:36:30 +00:00
parent 98c7977a19
commit 7b4783bb3e
4 changed files with 85 additions and 19 deletions
+9
View File
@@ -195,6 +195,15 @@ export const ENTITIES: Record<string, EntityConfig> = {
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',
+8 -1
View File
@@ -74,8 +74,15 @@ export function PageHero({ pageKey }: PageHeroProps) {
</motion.p>
<div className="mt-8 h-px w-28 bg-copper" />
{/* ویرایش تصویر پس‌زمینه و سئوی صفحه — فقط در حالت ویرایش ادمین */}
{/* ویرایش هیروی صفحه (عنوان/سرآغاز/تصویر)، تصویر پس‌زمینه و سئو — فقط در حالت ویرایش ادمین */}
<div className="mt-6 flex items-start gap-3 [&>div]:!mt-0">
<SingletonPill
resource="page-heroes"
label="عنوان، سرآغاز و تصویر هیروی این صفحه"
select={(d) => d.page_heroes.find((h) => h.key === pageKey)}
// اگر ردیف هیروی این صفحه هنوز ساخته نشده باشد، همین‌جا ساخته می‌شود
createDefaults={() => ({ key: pageKey, title: '', lead: '', image: '' })}
/>
<SingletonPill
resource="seo-meta"
label="عنوان و توضیح سئوی این صفحه"
+29 -5
View File
@@ -12,21 +12,35 @@ import { useSite, useStatusLabel } from '../api';
import { useLang } from '../i18n/lang';
import { applySeo } from '../seo';
import { projectsData } from '../data/projectsData';
import { SingletonPill } from '../admin/Editable';
export function ProjectDetail() {
const { slug } = useParams<{ slug: string }>()
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<number | null>(null)
if (!currentProject) {
return <div className="text-center py-20 text-ivory">Project not found</div>
}
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() {
<span className="text-copper">{displayTitle}</span>
</nav>
<div className="mb-8">
<div className="mb-8 flex flex-col items-start gap-4 sm:flex-row sm:items-center sm:justify-between">
<h1 className="text-4xl lg:text-5xl font-bold text-ivory">{displayTitle}</h1>
{dbProject && (
<SingletonPill
resource="projects"
label="ویرایش این پروژه (عنوان، مشخصات، گالری، متن)"
select={(d) => d.projects.find((p) => p.slug === slug)}
/>
)}
</div>
{/* Gallery Desktop: Grid. Mobile: Grid */}
+39 -13
View File
@@ -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() {
<div className="mt-12 grid gap-6 lg:grid-cols-3">
{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 = (
<ProjectCard
project={project}
imageUrl={imageUrl}
detailHref={`/projects/${project.slug}`}
/>
);
return (
<Reveal key={project.id} delay={0.05 * (index % itemsPerPage)} className="h-full">
<ProjectCard
project={project}
imageUrl={imageUrl}
detailHref={`/projects/${project.slug}`}
/>
{fromDb ? (
<EditableItem resource="projects" item={project} className="h-full">
{card}
</EditableItem>
) : (
card
)}
</Reveal>
);
})}
{fromDb && <AddItemButton resource="projects" />}
</div>
{/* Load More Button */}