diff --git a/backend/app/migrations.py b/backend/app/migrations.py index a50caf5..fbd9306 100644 --- a/backend/app/migrations.py +++ b/backend/app/migrations.py @@ -27,7 +27,11 @@ EN_COLUMNS: list[tuple[str, str, str]] = [ ("projects", "year_en", "VARCHAR(64)"), ("projects", "area_en", "VARCHAR(64)"), ("projects", "floors_en", "VARCHAR(64)"), - ("projects", "units_en", "VARCHAR(64)"), + ("projects", "land_area", "VARCHAR(64)"), + ("projects", "pilot_floors", "VARCHAR(32)"), + ("projects", "units_residential", "VARCHAR(32)"), + ("projects", "units_commercial", "VARCHAR(32)"), + ("projects", "units_office", "VARCHAR(32)"), ("projects", "client_en", "VARCHAR(128)"), ("projects", "project_type", "VARCHAR(128)"), ("projects", "project_type_en", "VARCHAR(128)"), diff --git a/backend/app/models.py b/backend/app/models.py index 9de3872..c1478cc 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -72,8 +72,11 @@ class Project(Base): area_en: Mapped[str] = mapped_column(String(64), default="") floors: Mapped[str] = mapped_column(String(64), default="") floors_en: Mapped[str] = mapped_column(String(64), default="") - units: Mapped[str] = mapped_column(String(64), default="") - units_en: Mapped[str] = mapped_column(String(64), default="") + land_area: Mapped[str] = mapped_column(String(64), default="") + pilot_floors: Mapped[str] = mapped_column(String(32), default="") + units_residential: Mapped[str] = mapped_column(String(32), default="") + units_commercial: Mapped[str] = mapped_column(String(32), default="") + units_office: Mapped[str] = mapped_column(String(32), default="") client: Mapped[str] = mapped_column(String(128), default="") client_en: Mapped[str] = mapped_column(String(128), default="") # مشخصات تکمیلی پروژه — به درخواست تیم: نوع پروژه و کاربری diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 5d55829..c939824 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -57,8 +57,11 @@ class ProjectOut(ORMModel): area_en: str = "" floors: str floors_en: str = "" - units: str - units_en: str = "" + land_area: str = "" + pilot_floors: str = "" + units_residential: str = "" + units_commercial: str = "" + units_office: str = "" client: str client_en: str = "" project_type: str = "" @@ -455,6 +458,11 @@ class ProjectIn(BaseModel): year: str = "" year_en: str = "" status: Literal["done", "progress"] = "done" + land_area: str = "" + pilot_floors: str = "" + units_residential: str = "" + units_commercial: str = "" + units_office: str = "" project_type: str = "" project_type_en: str = "" usage: str = "" @@ -465,8 +473,6 @@ class ProjectIn(BaseModel): area_en: str = "" floors: str = "" floors_en: str = "" - units: str = "" - units_en: str = "" client: str = "" client_en: str = "" summary: str = "" diff --git a/frontend/src/admin/entities.ts b/frontend/src/admin/entities.ts index bdef2d5..44ac0f8 100644 --- a/frontend/src/admin/entities.ts +++ b/frontend/src/admin/entities.ts @@ -69,8 +69,10 @@ export const ENTITIES: Record = { F('title', 'عنوان'), F('slug', 'نشانی (slug)', 'text', undefined, true), F('location', 'موقعیت'), F('year', 'سال'), F('status', 'وضعیت', 'text', 'done یا progress', true), F('image', 'تصویر شاخص', 'image', undefined, true), F('gallery', 'گالری (هر خط یک URL)', 'list', undefined, true), - F('area', 'زیربنا'), F('floors', 'طبقات'), F('units', 'واحدها'), - F('project_type', 'نوع پروژه'), F('usage', 'کاربری'), F('client', 'کارفرما'), + F('land_area', 'مساحت عرصه (زمین)'), F('area', 'مساحت زیربنا (اعیان)'), + F('usage', 'کاربری ساختمان'), F('pilot_floors', 'طبقات روی همکف (پیلوت)'), F('floors', 'تعداد طبقات'), + F('units_residential', 'تعداد واحد مسکونی'), F('units_commercial', 'تعداد واحد تجاری'), F('units_office', 'تعداد واحد اداری'), + F('project_type', 'نوع پروژه'), F('summary', 'خلاصه', 'textarea'), F('body', 'متن پروژه (هر پاراگراف یک خط)', 'list'), ], defaults: { title: 'پروژه جدید', slug: `project-${Date.now()}`, location: '', year: '', status: 'done', image: '', gallery: [], area: '', floors: '', units: '', client: '', summary: '', body: [] }, diff --git a/frontend/src/components/ProjectCard.tsx b/frontend/src/components/ProjectCard.tsx index 31999c1..9404a96 100644 --- a/frontend/src/components/ProjectCard.tsx +++ b/frontend/src/components/ProjectCard.tsx @@ -2,7 +2,14 @@ import { Link } from 'react-router-dom' import { useStatusLabel } from '../api' import { useLang } from '../i18n/lang' import { PremiumButton } from './PremiumButton' -import type { StaticProject } from '../data/projectsData' +export type StaticProject = { + id: string + slug: string + name: { en: string; fa: string } + location: { en: string; fa: string } + area: { en: string; fa: string } + status: 'done' | 'progress' +} import type { ProjectCardItem } from '../data/site' type ProjectCardProps = { diff --git a/frontend/src/components/home/FeaturedProjects.tsx b/frontend/src/components/home/FeaturedProjects.tsx index 61758b3..72adad0 100644 --- a/frontend/src/components/home/FeaturedProjects.tsx +++ b/frontend/src/components/home/FeaturedProjects.tsx @@ -10,10 +10,9 @@ import { useFieldSaver } from '../../admin/AdminContext' import { useLang } from '../../i18n/lang' import { InlineEdit } from '../admin/InlineEdit' import type { ProjectStatus } from '../../data/site' -import { projectsData } from '../../data/projectsData' export function FeaturedProjects() { - const { status_labels } = useSite() + const { status_labels, projects } = useSite() const { t, href } = useLang() const heading = useSectionHeading('featured_projects') const save = useFieldSaver() @@ -34,8 +33,8 @@ export function FeaturedProjects() { const [status, setStatus] = useState('done') - // Filter and slice static project data - const list = projectsData.filter((p) => p.status === status).slice(0, 3) + // سه پروژه از دیتابیس — ویرایش پنل همان‌جا اعمال می‌شود + const list = projects.filter((p) => p.status === status).slice(0, 3) const BASE_IMG_URL = "/seed-media/2026/09/"; return ( @@ -87,8 +86,7 @@ export function FeaturedProjects() { className="grid gap-6 lg:grid-cols-3" > {list.map((project, i) => { - const dummyImageName = `${(i % 6) + 1}.png`; - const imageUrl = `${BASE_IMG_URL}${dummyImageName}`; + const imageUrl = project.image || undefined; return ( String(f.value ?? '').trim() !== '') return ( diff --git a/frontend/src/pages/Projects.tsx b/frontend/src/pages/Projects.tsx index 1b89cc1..fdbcb95 100644 --- a/frontend/src/pages/Projects.tsx +++ b/frontend/src/pages/Projects.tsx @@ -10,7 +10,6 @@ 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'; @@ -38,11 +37,7 @@ export function Projects() { // پروژه‌ها از دیتابیس خوانده می‌شوند تا ویرایش پنل همان‌جا دیده شود؛ // فایل استاتیک فقط وقتی دیتابیس خالی است جانشین می‌شود (محتوای قدیمی). - const fromDb = dbProjects.length > 0; - const cardProjects = useMemo(() => { - if (fromDb) return dbProjects; - return projectsData.map((p) => ({ ...p, id: Number(p.id) })); - }, [fromDb, dbProjects]); + const cardProjects = useMemo(() => dbProjects, [dbProjects]); // Filtering and Pagination State const [page, setPage] = useState(1); @@ -83,11 +78,7 @@ export function Projects() { 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, - ); - return match ? `/projects/${match.slug}` : undefined; + return dbMatch ? `/projects/${dbMatch.slug}` : undefined; }; return ( @@ -147,29 +138,22 @@ export function Projects() { {displayedProjects.map((project, index) => { // تصویر رکورد دیتابیس اولویت دارد؛ فقط ردیف‌های بدون تصویر از // تصاویر جایگزین تکراری استفاده می‌کنند. - 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 - )} + + {card} + ); })} - {fromDb && } + {/* Load More Button */}