revert(ui): بازگردانی سه بخش به حالت پیش از UI v1 (کامیت 853ad67)
- دکمهٔ تغییر زبان: کپسول بزرگ دوزبانه → دکمهٔ کوچک آیکون+متن (طرح قبلی) - ساختار صفحهٔ جزئیات پروژه: بازگشت به چیدمان قبلی (دیتابیسمحور بود و میماند) - قالب پرسشوپاسخ: بازگشت به چیدمان دوستونه (تیتر کنار آکاردئون) توجه: بخشی از UI v1 (fatemeh) عمداً برگردانده شده به درخواست تیم UI؛ بقیهٔ تغییرات آن کامیت سر جایش است. ویرایشپذیری دیتابیسمحور پروژهها و دکمهٔ ویرایش، روی همین ساختار قدیمی دوباره اعمال شدهاند.
This commit is contained in:
@@ -1,18 +1,6 @@
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { LanguagesIcon } from 'lucide-react'
|
||||
import { counterpartPath, useLang, writeLangCookie } from '../i18n/lang'
|
||||
import { track } from '../analytics'
|
||||
|
||||
/**
|
||||
* تنظیمات ثابت هر زبان.
|
||||
* ترتیب اینجا = ترتیب نمایش در کپسول (فارسی سمت چپ، English سمت راست).
|
||||
* بجای مقادیر پیکسلی، از translate-x-full استفاده شده تا دقیقاً به اندازه عرض خودش (50%) جابجا شود.
|
||||
*/
|
||||
const LANGS = {
|
||||
fa: { label: 'فارسی', sliderTranslate: 'translate-x-0' },
|
||||
en: { label: 'English', sliderTranslate: 'translate-x-full' },
|
||||
} as const
|
||||
|
||||
type LangKey = keyof typeof LANGS
|
||||
|
||||
/** دکمه تغییر زبان — بین نسخه فارسی و /en انگلیسی همان صفحه جابهجا میشود */
|
||||
export function LangToggle({ className = '' }: { className?: string }) {
|
||||
@@ -22,9 +10,7 @@ export function LangToggle({ className = '' }: { className?: string }) {
|
||||
const target = counterpartPath(pathname, lang)
|
||||
|
||||
const switchLang = () => {
|
||||
const next = lang === 'fa' ? 'en' : 'fa'
|
||||
track('lang-switch', { to: next })
|
||||
writeLangCookie(next)
|
||||
writeLangCookie(lang === 'fa' ? 'en' : 'fa')
|
||||
// پرش خودکار بازدید بعدی با انتخاب دستی همینجا گره نمیخورد
|
||||
sessionStorage.setItem('langAutoRedirect', '1')
|
||||
navigate(target)
|
||||
@@ -36,61 +22,10 @@ export function LangToggle({ className = '' }: { className?: string }) {
|
||||
onClick={switchLang}
|
||||
aria-label={t('switchLangAria')}
|
||||
title={t('switchLangAria')}
|
||||
dir="ltr"
|
||||
className={[
|
||||
// ── Capsule shell ──────────────────────────────────────────────
|
||||
'relative flex items-center',
|
||||
'w-[152px] h-[42px] rounded-full p-1', // استفاده از p-1 (4px) به جای [4px]
|
||||
'bg-[#1a1715]',
|
||||
// deep inset shadow → recessed track feel
|
||||
'shadow-[inset_0_3px_7px_rgba(0,0,0,0.75),inset_0_-1px_2px_rgba(255,255,255,0.04)]',
|
||||
// interaction
|
||||
'cursor-pointer select-none',
|
||||
'active:scale-[0.975] transition-transform duration-150 ease-lux',
|
||||
className,
|
||||
].join(' ')}
|
||||
className={`group flex items-center gap-1.5 rounded-full border border-white/15 bg-white/5 px-3.5 py-2 text-xs font-bold text-ivory/80 backdrop-blur-lg shadow-[inset_0_1px_1px_rgba(255,255,255,0.15)] transition-all duration-300 ease-lux hover:border-copper/60 hover:bg-white/10 hover:text-copper active:scale-95 cursor-pointer ${className}`}
|
||||
>
|
||||
{/* ── Inner Track ───────────────────────────────────────────────
|
||||
این دیو داخلی باعث میشود پدینگ دکمه اصلی روی محاسبات 50 درصدی تاثیر نگذارد
|
||||
*/}
|
||||
<div className="relative w-full h-full flex items-center">
|
||||
|
||||
{/* ── Slider pill ─────────────────────────────────────────────── */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={[
|
||||
'absolute top-0 left-0', // چسبیده به لبههای Track داخلی
|
||||
'w-1/2 h-full rounded-full', // دقیقاً 50 درصد عرض و کل ارتفاع
|
||||
'bg-[#27221c] border border-[#d5874c]',
|
||||
// subtle raised shadow — tactile without being flashy
|
||||
'shadow-[0_2px_6px_rgba(0,0,0,0.55),0_0_0_1px_rgba(213,135,76,0.18),inset_0_1px_1px_rgba(255,255,255,0.06)]',
|
||||
'transition-transform duration-[380ms] ease-lux',
|
||||
LANGS[lang as LangKey].sliderTranslate,
|
||||
].join(' ')}
|
||||
/>
|
||||
|
||||
{/* ── Labels ──────────────────────────────────────────────────── */}
|
||||
<span className="relative z-10 flex w-full h-full">
|
||||
{(Object.keys(LANGS) as LangKey[]).map((key) => {
|
||||
const isActive = lang === key
|
||||
return (
|
||||
<span
|
||||
key={key}
|
||||
className={[
|
||||
// each label occupies exactly half the capsule
|
||||
'w-1/2 h-full flex justify-center items-center',
|
||||
'text-[11.5px] font-semibold tracking-wide leading-none',
|
||||
'transition-colors duration-[380ms] ease-lux',
|
||||
isActive ? 'text-[#d5874c]' : 'text-[#6a6662]',
|
||||
].join(' ')}
|
||||
>
|
||||
{LANGS[key].label}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<LanguagesIcon className="h-3.5 w-3.5 transition-transform duration-300 group-hover:rotate-12" />
|
||||
<span>{t('switchLang')}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -18,22 +18,20 @@ export function Faq({ hideHeading = false }: FaqProps) {
|
||||
|
||||
return (
|
||||
<section className="bg-ink py-24 lg:py-32" aria-labelledby="faq-title">
|
||||
<div className="mx-auto max-w-site px-4 md:px-8 lg:px-10">
|
||||
|
||||
{/* Header — centered, full width */}
|
||||
<div className={hideHeading ? "mx-auto max-w-4xl px-6 lg:px-10" : "mx-auto grid max-w-site gap-14 px-6 lg:grid-cols-12 lg:px-10"}>
|
||||
|
||||
{!hideHeading && (
|
||||
<div className="mb-8 md:mb-14 flex flex-col items-center w-full text-center">
|
||||
<div className="lg:col-span-4">
|
||||
<SectionHeading
|
||||
title={t('fallbackFaqTitle')}
|
||||
lead={t('fallbackFaqLead')}
|
||||
headingKey="faq"
|
||||
align="center"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Accordion list — constrained width, direction-aware */}
|
||||
<div className="w-full max-w-3xl mx-auto">
|
||||
<div className={hideHeading ? "" : "lg:col-span-8"}>
|
||||
<ul>
|
||||
{faqs.map((faq, i) => {
|
||||
const isOpen = open === i
|
||||
@@ -45,9 +43,9 @@ export function Faq({ hideHeading = false }: FaqProps) {
|
||||
type="button"
|
||||
onClick={() => setOpen(isOpen ? null : i)}
|
||||
aria-expanded={isOpen}
|
||||
className="flex w-full items-center justify-between gap-6 py-6 text-start transition-colors duration-200 ease-lux hover:text-copper"
|
||||
className="flex w-full items-center justify-between gap-6 py-6 text-right transition-colors duration-200 ease-lux hover:text-copper"
|
||||
>
|
||||
<span className={`text-base font-bold md:text-lg text-start ${isOpen ? 'text-copper' : 'text-ivory'}`}>
|
||||
<span className={`text-base font-bold md:text-lg ${isOpen ? 'text-copper' : 'text-ivory'}`}>
|
||||
{faq.question}
|
||||
</span>
|
||||
<PlusIcon
|
||||
@@ -65,7 +63,7 @@ export function Faq({ hideHeading = false }: FaqProps) {
|
||||
transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<p className="pb-7 ps-10 text-sm font-light leading-9 text-ivory/60 text-start">{faq.answer}</p>
|
||||
<p className="pb-7 pl-10 text-sm font-light leading-9 text-ivory/60">{faq.answer}</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -1,115 +1,75 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { ChevronLeftIcon } from 'lucide-react'
|
||||
import { ArrowLeftIcon, ChevronLeftIcon } from 'lucide-react'
|
||||
import { Reveal } from '../components/Reveal'
|
||||
import { PremiumButton } from '../components/PremiumButton'
|
||||
import { ButtonLink } from '../components/Button'
|
||||
import { ProjectCard } from '../components/ProjectCard'
|
||||
import { ContactSection } from '../components/ContactSection'
|
||||
import { Lightbox } from '../components/Lightbox'
|
||||
import { LazyProjectsMapSingle } from '../map/LazyMaps';
|
||||
import { createPlaceholderProject, findMapProjectByName } from '../data/mapProjects'
|
||||
import { useEffect } from 'react';
|
||||
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 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 project = projects.find((p) => p.slug === slug)
|
||||
const statusLabel = useStatusLabel(project?.status ?? '')
|
||||
const [lightbox, setLightbox] = useState<number | null>(null)
|
||||
|
||||
if (!currentProject) {
|
||||
return <div className="text-center py-20 text-ivory">Project not found</div>
|
||||
}
|
||||
|
||||
const backendProject = dbProject
|
||||
|
||||
// Dynamic bilingual text from projectsData
|
||||
const displayTitle = currentProject.name[lang as 'en' | 'fa'] || currentProject.name.en;
|
||||
const displayLocation = currentProject.location[lang as 'en' | 'fa'] || currentProject.location.en;
|
||||
const displayArea = currentProject.area[lang as 'en' | 'fa'] || currentProject.area.en;
|
||||
const displayStatus = currentProject.status;
|
||||
|
||||
const statusLabel = useStatusLabel(displayStatus)
|
||||
|
||||
// Fallbacks for layout structure fields missing in projectsData
|
||||
const summary = backendProject?.summary || (lang === 'en' ? 'A modern and luxurious architectural project built with high-quality materials and smart design.' : 'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ.');
|
||||
const body = backendProject?.body || (lang === 'en'
|
||||
? ['Located in the heart of the city, this project offers unparalleled access to major urban hubs.', 'It features state-of-the-art facilities, green spaces, and a sustainable approach to modern living.']
|
||||
: ['لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است.', 'چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است.']);
|
||||
const gallery = backendProject?.gallery?.length ? backendProject.gallery : [
|
||||
'/seed-media/2026/09/1.png',
|
||||
'/seed-media/2026/09/2.png',
|
||||
'/seed-media/2026/09/3.png',
|
||||
'/seed-media/2026/09/4.png'
|
||||
];
|
||||
const image = backendProject?.image || '/seed-media/2026/09/1.png';
|
||||
const year = backendProject?.year || (lang === 'en' ? '2024' : '1402');
|
||||
const floors = backendProject?.floors || '5';
|
||||
const units = backendProject?.units || '20';
|
||||
const client = backendProject?.client || (lang === 'en' ? 'Aramesh Group' : 'گروه آرامش');
|
||||
|
||||
// تصاویر پاپآپ نقشه
|
||||
const mapImages = useMemo(
|
||||
() => [image, ...gallery].filter(Boolean),
|
||||
[image, gallery]
|
||||
)
|
||||
|
||||
// مختصات همین پروژه
|
||||
const mapLocation = useMemo(
|
||||
() => findMapProjectByName(displayTitle) ?? createPlaceholderProject(displayTitle, displayLocation),
|
||||
[displayTitle, displayLocation]
|
||||
)
|
||||
|
||||
// SEO
|
||||
// عنوان تب، توضیح متا و OG از خود پروژه
|
||||
useEffect(() => {
|
||||
if (!project) return;
|
||||
applySeo({
|
||||
title: `${displayTitle} | ${brand.name}`,
|
||||
description: summary,
|
||||
path: href(`/projects/${currentProject.slug}`),
|
||||
image: image,
|
||||
title: `${project.title} | ${brand.name}`,
|
||||
description: project.summary,
|
||||
path: href(`/projects/${project.slug}`),
|
||||
image: project.image,
|
||||
lang,
|
||||
});
|
||||
}, [currentProject.slug, lang, href, displayTitle, summary, image, brand.name]);
|
||||
}, [project?.id, lang, href]);
|
||||
|
||||
// پروژههای مرتبط — از دیتابیس، و اگر خالی بود از فهرست استاتیک قدیمی
|
||||
const related = (projects.length > 0 ? projects : projectsData)
|
||||
.filter((p) => p.slug !== currentProject.slug)
|
||||
.slice(0, 3)
|
||||
if (!project) {
|
||||
return (
|
||||
<section className="flex min-h-[70vh] flex-col items-center justify-center px-6 text-center">
|
||||
<h1 className="text-section font-bold text-ivory">{t('projectNotFound')}</h1>
|
||||
<p className="mt-4 text-sm font-light text-ivory/60">
|
||||
{t('projectNotFoundText')}
|
||||
</p>
|
||||
<ButtonLink to={href('/projects')} variant="copper" className="mt-8">
|
||||
{t('backToProjects')}
|
||||
<ArrowLeftIcon className="h-4 w-4 ltr:-scale-x-100" />
|
||||
</ButtonLink>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const related = projects.filter((p) => p.slug !== project.slug).slice(0, 3)
|
||||
|
||||
const facts = [
|
||||
{ label: t('specLocation'), value: displayLocation },
|
||||
{ label: t('specYear'), value: year },
|
||||
{ label: t('specArea'), value: displayArea },
|
||||
{ label: t('specFloors'), value: floors },
|
||||
{ label: t('specUnits'), value: units },
|
||||
{ label: t('specClient'), value: client },
|
||||
{ label: t('specLocation'), value: project.location },
|
||||
{ label: t('specYear'), value: project.year },
|
||||
{ label: t('specArea'), value: project.area },
|
||||
{ label: t('specFloors'), value: project.floors },
|
||||
{ label: t('specUnits'), value: project.units },
|
||||
{ label: t('specClient'), value: project.client },
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="bg-char pt-24 pb-12 lg:pt-32 lg:pb-16" aria-label={t('projectDetailsAria')}>
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
|
||||
<nav aria-label={t('breadcrumbAria')} className="mb-6 flex items-center gap-2 text-xs font-light text-ivory/50">
|
||||
<section className="relative flex h-[62vh] min-h-[440px] items-end overflow-hidden">
|
||||
<img src={project.image} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover" />
|
||||
<div className="absolute inset-0 bg-ink/72" aria-hidden="true" />
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{ background: 'linear-gradient(to top, #27221c 6%, rgba(39,34,28,0.15) 70%)' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="relative mx-auto w-full max-w-site px-6 pb-16 lg:px-10">
|
||||
<nav aria-label={t('breadcrumbAria')} className="flex items-center gap-2 text-xs font-light text-ivory/50">
|
||||
<Link to={href('/')} className="transition-colors hover:text-copper">
|
||||
{t('home')}
|
||||
</Link>
|
||||
@@ -118,87 +78,83 @@ export function ProjectDetail() {
|
||||
{t('projects')}
|
||||
</Link>
|
||||
<ChevronLeftIcon className="h-3.5 w-3.5 ltr:rotate-180" />
|
||||
<span className="text-copper">{displayTitle}</span>
|
||||
<span className="text-copper">{project.title}</span>
|
||||
</nav>
|
||||
|
||||
<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)}
|
||||
/>
|
||||
)}
|
||||
<h1 className="mt-5 max-w-[700px] text-hero font-bold text-ivory">{project.title}</h1>
|
||||
<p className="mt-5 max-w-2xl text-base font-light leading-9 text-ivory/70">{project.summary}</p>
|
||||
{/* ویرایش این پروژه — فقط در حالت ویرایش ادمین */}
|
||||
<div className="mt-5 flex items-start gap-3 [&>div]:!mt-0">
|
||||
<SingletonPill
|
||||
resource="projects"
|
||||
label="ویرایش این پروژه (عنوان، مشخصات، گالری، متن)"
|
||||
select={(d) => d.projects.find((p) => p.slug === slug)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Gallery Desktop: Grid. Mobile: Grid */}
|
||||
<div className="grid grid-cols-2 gap-2 md:gap-3 rounded-xl lg:rounded-2xl overflow-hidden lg:h-[500px] xl:h-[600px]">
|
||||
<section className="bg-char py-20 lg:py-28" aria-label={t('projectDetailsAria')}>
|
||||
<div className="mx-auto grid max-w-site items-start gap-12 px-6 lg:grid-cols-2 lg:gap-16 lg:px-10">
|
||||
|
||||
{/* Main large image */}
|
||||
<Reveal className="order-1">
|
||||
<span className="flex items-center gap-3 text-sm font-light text-copper">
|
||||
<span aria-hidden="true" className="h-px w-8 bg-copper" />
|
||||
{statusLabel}
|
||||
</span>
|
||||
<h2 className="mt-4 text-3xl font-bold text-ivory">{t('aboutProject')}</h2>
|
||||
|
||||
{project.body.map((para) => (
|
||||
<p key={para} className="mt-5 text-base font-light leading-9 text-ivory/70">
|
||||
{para}
|
||||
</p>
|
||||
))}
|
||||
|
||||
<dl className="mt-10 grid grid-cols-2 gap-px border border-ivory/10 bg-ivory/10">
|
||||
{facts.map((f) => (
|
||||
<div key={f.label} className="bg-ink p-6 md:p-8 ltr:text-left rtl:text-right">
|
||||
<dt className="text-xs font-light text-ivory/50">{f.label}</dt>
|
||||
<dd className="mt-3 text-sm font-bold text-ivory">{f.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
|
||||
<div className="mt-10">
|
||||
<ButtonLink to={href('/contact')} variant="outline" className="font-light">
|
||||
{t('similarProjectCta')}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.1} className="order-2">
|
||||
<button
|
||||
type="button"
|
||||
className="col-span-2 lg:col-span-1 h-[250px] md:h-[400px] lg:h-full relative overflow-hidden cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-copper group block"
|
||||
onClick={() => setLightbox(0)}
|
||||
className="group block w-full overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-copper"
|
||||
>
|
||||
<img src={mapImages[0]} alt="" className="h-full w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105" />
|
||||
<img
|
||||
src={project.gallery[0] || project.image}
|
||||
alt=""
|
||||
className="aspect-[4/3] w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* 2x2 Small images */}
|
||||
<div className="col-span-2 lg:col-span-1 grid grid-cols-2 gap-2 md:gap-3 lg:grid-rows-2 lg:h-full">
|
||||
{mapImages.slice(1, 5).map((img, i) => (
|
||||
<div className="mt-4 grid grid-cols-3 gap-4">
|
||||
{project.gallery.slice(1).map((img, i) => (
|
||||
<button
|
||||
key={img}
|
||||
type="button"
|
||||
className={`relative overflow-hidden cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-copper group block h-[120px] md:h-[200px] lg:h-full ${i >= 2 ? 'hidden md:block' : ''}`}
|
||||
onClick={() => setLightbox(i + 1)}
|
||||
className="group block overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-copper"
|
||||
>
|
||||
<img src={img} alt="" className="h-full w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105" />
|
||||
<img
|
||||
src={img}
|
||||
alt=""
|
||||
className="aspect-square w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Smart Content Layout */}
|
||||
<div className="max-w-4xl mx-auto mt-16 lg:mt-24 flex flex-col gap-16 lg:gap-20">
|
||||
|
||||
{/* Description */}
|
||||
<Reveal>
|
||||
<span className="flex items-center gap-3 text-sm font-light text-copper mb-6">
|
||||
<span aria-hidden="true" className="h-px w-8 bg-copper" />
|
||||
{statusLabel}
|
||||
</span>
|
||||
<h2 className="text-3xl font-bold text-ivory">{t('aboutProject')}</h2>
|
||||
|
||||
<p className="mt-8 text-xl font-medium leading-9 text-ivory/90">{summary}</p>
|
||||
{body.map((para) => (
|
||||
<p key={para} className="mt-6 text-base font-light leading-9 text-ivory/70">
|
||||
{para}
|
||||
</p>
|
||||
))}
|
||||
</Reveal>
|
||||
|
||||
{/* Specifications Table - Perfectly center-aligned */}
|
||||
<Reveal>
|
||||
<dl className="grid grid-cols-2 md:grid-cols-3 gap-px border border-ivory/10 bg-ivory/10 rounded-xl overflow-hidden">
|
||||
{facts.map((f) => (
|
||||
<div key={f.label} className="bg-ink p-6 md:p-8 flex flex-col items-center justify-center text-center">
|
||||
<dt className="text-xs font-light text-ivory/50 mb-2">{f.label}</dt>
|
||||
<dd className="text-base font-bold text-ivory">{f.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</Reveal>
|
||||
|
||||
{/* Map (بدون Reveal) */}
|
||||
{mapLocation && (
|
||||
<div className="h-80 md:h-[400px] rounded-xl overflow-hidden border border-white/10 shadow-2xl">
|
||||
<LazyProjectsMapSingle location={mapLocation} images={mapImages} className="h-full w-full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -207,24 +163,17 @@ export function ProjectDetail() {
|
||||
<h2 className="text-section font-bold text-ivory">{t('otherProjects')}</h2>
|
||||
<div className="mt-6 h-px w-24 bg-copper/70" />
|
||||
<div className="mt-12 grid gap-6 lg:grid-cols-3">
|
||||
{related.map((item, i) => {
|
||||
const dummyImageName = `${(i % 6) + 1}.png`;
|
||||
return (
|
||||
<Reveal key={item.slug} delay={0.06 * i} className="h-full">
|
||||
<ProjectCard
|
||||
project={item}
|
||||
imageUrl={`/seed-media/2026/09/${dummyImageName}`}
|
||||
detailHref={href(`/projects/${item.slug}`)}
|
||||
/>
|
||||
</Reveal>
|
||||
);
|
||||
})}
|
||||
{related.map((item, i) => (
|
||||
<Reveal key={item.slug} delay={0.06 * i} className="h-full">
|
||||
<ProjectCard project={item} />
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Lightbox
|
||||
images={gallery}
|
||||
images={project.gallery}
|
||||
index={lightbox}
|
||||
onClose={() => setLightbox(null)}
|
||||
onChange={setLightbox}
|
||||
|
||||
Reference in New Issue
Block a user