پروژه کامل: فرانت متصل به API + داکرایز

- اتصال کامل فرانت به API: همه محتوای سایت (پروژه‌ها، مقالات، خدمات، برند و...) از بک‌اند خوانده می‌شود (SiteProvider + useSite)
- فرم تماس و خبرنامه متصل به بک‌اند و تست‌شده در مرورگر
- ساختار مونوریپو: frontend/ + backend/
- Dockerfile برای هر دو بخش + docker-compose (PostgreSQL + FastAPI + nginx)
- nginx با پروکسی /api — بدون نیاز به CORS در حالت داکر
- README کامل فارسی
This commit is contained in:
Behrad
2026-08-29 13:26:58 +03:30
parent ed14435485
commit ad75f7a7bd
78 changed files with 8242 additions and 133 deletions
+160
View File
@@ -0,0 +1,160 @@
import React, { useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { ArrowLeftIcon, ChevronLeftIcon } from 'lucide-react'
import { Reveal } from '../components/Reveal'
import { ButtonLink } from '../components/Button'
import { ProjectCard } from '../components/ProjectCard'
import { ContactSection } from '../components/ContactSection'
import { Lightbox } from '../components/Lightbox'
import { STATUS_LABEL } from '../data/site'
import { useSite } from '../api'
export function ProjectDetail() {
const { slug } = useParams<{ slug: string }>()
const { projects } = useSite()
const project = projects.find((p) => p.slug === slug)
const [lightbox, setLightbox] = useState<number | null>(null)
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">این پروژه پیدا نشد</h1>
<p className="mt-4 text-sm font-light text-ivory/60">
ممکن است نشانی تغییر کرده باشد. فهرست کامل پروژه‌ها را ببینید.
</p>
<ButtonLink to="/projects" variant="copper" className="mt-8">
بازگشت به پروژه‌ها
<ArrowLeftIcon className="h-4 w-4" />
</ButtonLink>
</section>
)
}
const related = projects.filter((p) => p.slug !== project.slug).slice(0, 3)
const facts = [
{ label: 'موقعیت', value: project.location },
{ label: 'سال', value: project.year },
{ label: 'زیربنا', value: project.area },
{ label: 'تعداد طبقات', value: project.floors },
{ label: 'واحدها', value: project.units },
{ label: 'کارفرما', value: project.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="مسیر صفحه" className="flex items-center gap-2 text-xs font-light text-ivory/50">
<Link to="/" className="transition-colors hover:text-copper">
صفحه اصلی
</Link>
<ChevronLeftIcon className="h-3.5 w-3.5" />
<Link to="/projects" className="transition-colors hover:text-copper">
پروژه‌ها
</Link>
<ChevronLeftIcon className="h-3.5 w-3.5" />
<span className="text-copper">{project.title}</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="جزئیات پروژه">
<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" />
{STATUS_LABEL[project.status]}
</span>
<h2 className="mt-4 text-3xl font-bold text-ivory">درباره پروژه</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 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="/contact" variant="outline" className="font-light">
پروژه‌ای مشابه دارید؟
</ButtonLink>
</div>
</Reveal>
<Reveal delay={0.1} className="order-2">
<button
type="button"
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"
/>
</button>
<div className="mt-4 grid grid-cols-3 gap-4">
{project.gallery.slice(1).map((img, i) => (
<button
key={img}
type="button"
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"
/>
</button>
))}
</div>
</Reveal>
</div>
</section>
<section className="bg-ink py-20 lg:py-28" aria-label="پروژه‌های مرتبط">
<div className="mx-auto max-w-site px-6 lg:px-10">
<h2 className="text-section font-bold text-ivory">پروژه‌های دیگر</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>
))}
</div>
</div>
</section>
<Lightbox
images={project.gallery}
index={lightbox}
onClose={() => setLightbox(null)}
onChange={setLightbox}
/>
<ContactSection />
</>
)
}