feat(ui): update UI v1

This commit is contained in:
fatemeh sane
2026-09-25 22:49:30 +03:30
parent 805f4594d4
commit 853ad67db5
82 changed files with 10311 additions and 505 deletions
+145 -109
View File
@@ -1,74 +1,98 @@
import React, { useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { ArrowLeftIcon, ChevronLeftIcon } from 'lucide-react'
import { ChevronLeftIcon } from 'lucide-react'
import { Reveal } from '../components/Reveal'
import { ButtonLink } from '../components/Button'
import { PremiumButton } from '../components/PremiumButton'
import { ProjectCard } from '../components/ProjectCard'
import { ContactSection } from '../components/ContactSection'
import { Lightbox } from '../components/Lightbox'
import { useEffect } from 'react';
import { LazyProjectsMapSingle } from '../map/LazyMaps';
import { createPlaceholderProject, findMapProjectByName } from '../data/mapProjects'
import { useSite, useStatusLabel } from '../api';
import { useLang } from '../i18n/lang';
import { applySeo } from '../seo';
import { projectsData } from '../data/projectsData';
export function ProjectDetail() {
const { slug } = useParams<{ slug: string }>()
const { projects, brand } = useSite()
const { t, href, lang } = useLang()
const project = projects.find((p) => p.slug === slug)
const statusLabel = useStatusLabel(project?.status ?? '')
const currentProject = projectsData.find((p) => p.slug === slug)
const [lightbox, setLightbox] = useState<number | null>(null)
// عنوان تب، توضیح متا و OG از خود پروژه
useEffect(() => {
if (!project) return;
applySeo({
title: `${project.title} | ${brand.name}`,
description: project.summary,
path: href(`/projects/${project.slug}`),
image: project.image,
lang,
});
}, [project?.id, lang, href]);
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>
)
if (!currentProject) {
return <div className="text-center py-20 text-ivory">Project not found</div>
}
const related = projects.filter((p) => p.slug !== project.slug).slice(0, 3)
const backendProject = projects.find((p) => p.slug === slug)
// 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
useEffect(() => {
applySeo({
title: `${displayTitle} | ${brand.name}`,
description: summary,
path: href(`/projects/${currentProject.slug}`),
image: image,
lang,
});
}, [currentProject.slug, lang, href, displayTitle, summary, image, brand.name]);
const related = projectsData.filter((p) => p.slug !== currentProject.slug).slice(0, 3)
const facts = [
{ 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 },
{ 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 },
]
return (
<>
<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">
<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">
<Link to={href('/')} className="transition-colors hover:text-copper">
{t('home')}
</Link>
@@ -77,75 +101,80 @@ export function ProjectDetail() {
{t('projects')}
</Link>
<ChevronLeftIcon className="h-3.5 w-3.5 ltr:rotate-180" />
<span className="text-copper">{project.title}</span>
<span className="text-copper">{displayTitle}</span>
</nav>
<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>
</section>
<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">
<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>
<div className="mb-8">
<h1 className="text-4xl lg:text-5xl font-bold text-ivory">{displayTitle}</h1>
</div>
{/* 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]">
{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
{/* Main large image */}
<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={project.gallery[0] || project.image}
alt=""
className="aspect-[4/3] w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105"
/>
<img src={mapImages[0]} alt="" className="h-full w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105" />
</button>
<div className="mt-4 grid grid-cols-3 gap-4">
{project.gallery.slice(1).map((img, i) => (
<button
key={img}
{/* 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) => (
<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="aspect-square w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105"
/>
<img src={img} alt="" className="h-full 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>
@@ -154,17 +183,24 @@ 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) => (
<Reveal key={item.slug} delay={0.06 * i} className="h-full">
<ProjectCard project={item} />
</Reveal>
))}
{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>
);
})}
</div>
</div>
</section>
<Lightbox
images={project.gallery}
images={gallery}
index={lightbox}
onClose={() => setLightbox(null)}
onChange={setLightbox}