پروژه کامل: فرانت متصل به 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
+119
View File
@@ -0,0 +1,119 @@
import React, { useCallback, useEffect, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { ChevronLeftIcon, ChevronRightIcon, XIcon } from 'lucide-react'
import { Reveal } from '../Reveal'
import { SectionHeading } from '../SectionHeading'
import { IMAGES } from '../../data/site'
const GRID = [IMAGES[0], IMAGES[1], IMAGES[2], IMAGES[3], IMAGES[4], IMAGES[1]]
export function Gallery() {
const [active, setActive] = useState<number | null>(null)
const close = useCallback(() => setActive(null), [])
const step = useCallback((dir: number) => {
setActive((prev) => (prev === null ? prev : (prev + dir + GRID.length) % GRID.length))
}, [])
useEffect(() => {
if (active === null) return
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') close()
if (e.key === 'ArrowRight') step(-1)
if (e.key === 'ArrowLeft') step(1)
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [active, close, step])
return (
<section className="bg-ink py-24 lg:py-32" aria-labelledby="gallery-title">
<div className="mx-auto max-w-site px-6 lg:px-10">
<SectionHeading
title="گالری تصاویر"
lead="قاب‌هایی از کارگاه‌ها و پروژه‌های تحویل‌شده آرامش پلازا."
align="center"
/>
<div className="mt-14 grid grid-cols-2 gap-4 lg:grid-cols-3">
{GRID.map((src, i) => (
<Reveal key={`${src}-${i}`} delay={0.04 * i}>
<button
type="button"
onClick={() => setActive(i)}
className="group block w-full overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-copper"
aria-label={`بزرگ‌نمایی تصویر شماره ${(i + 1).toLocaleString('fa-IR')}`}
>
<img
src={src}
alt={`تصویری از پروژه‌های آرامش پلازا، شماره ${(i + 1).toLocaleString('fa-IR')}`}
className="aspect-[4/3] w-full object-cover transition-transform duration-500 ease-lux group-hover:scale-105"
/>
</button>
</Reveal>
))}
</div>
</div>
<AnimatePresence>
{active !== null && (
<motion.div
className="fixed inset-0 z-[80] flex items-center justify-center bg-char/80 backdrop-blur-lg p-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5, ease: [0.23, 1, 0.32, 1] }}
role="dialog"
aria-modal="true"
aria-label="نمایش تصویر"
onClick={close}
>
<button
type="button"
onClick={close}
className="absolute left-8 top-8 flex h-11 w-11 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper"
aria-label="بستن"
>
<XIcon className="h-5 w-5" />
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
step(-1)
}}
className="absolute right-6 flex h-12 w-12 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper md:right-10"
aria-label="تصویر قبلی"
>
<ChevronRightIcon className="h-6 w-6" />
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
step(1)
}}
className="absolute left-6 flex h-12 w-12 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper md:left-10"
aria-label="تصویر بعدی"
>
<ChevronLeftIcon className="h-6 w-6" />
</button>
<motion.img
key={active}
src={GRID[active]}
alt=""
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.5, ease: [0.23, 1, 0.32, 1] }}
className="max-h-[85vh] w-auto max-w-[85vw] object-contain shadow-[0_30px_80px_-20px_rgba(0,0,0,0.9)]"
onClick={(e) => e.stopPropagation()}
/>
</motion.div>
)}
</AnimatePresence>
</section>
)
}