'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import { useLocale } from '@/lib/i18n/locale-context'; import { SectionHeader } from '@/components/ui/SectionHeader'; import type { Dict } from '@/lib/i18n/dictionaries'; import { cn } from '@/lib/utils'; type Item = Dict['portfolio']['items'][number]; type Accent = 'electric' | 'violet' | 'magenta' | 'emerald' | 'cyan'; const ACCENT_TEXT: Record = { electric: 'text-electric', violet: 'text-violet', magenta: 'text-magenta', emerald: 'text-emerald', cyan: 'text-cyan', }; const ACCENT_BORDER: Record = { electric: 'border-electric/30 bg-electric/5 text-electric', violet: 'border-violet/30 bg-violet/5 text-violet', magenta: 'border-magenta/30 bg-magenta/5 text-magenta', emerald: 'border-emerald/30 bg-emerald/5 text-emerald', cyan: 'border-cyan/30 bg-cyan/5 text-cyan', }; const ACCENT_RING: Record = { electric: 'hover:ring-electric/40', violet: 'hover:ring-violet/40', magenta: 'hover:ring-magenta/40', emerald: 'hover:ring-emerald/40', cyan: 'hover:ring-cyan/40', }; // Full literal classes so Tailwind's JIT scanner picks them up — runtime // string concatenation (`group-hover:${...}`) would never be detected. const ACCENT_GROUP_HOVER: Record = { electric: 'group-hover:text-electric', violet: 'group-hover:text-violet', magenta: 'group-hover:text-magenta', emerald: 'group-hover:text-emerald', cyan: 'group-hover:text-cyan', }; export function Portfolio() { const { t, locale } = useLocale(); const items = t.portfolio.items as readonly Item[]; const [openId, setOpenId] = useState(null); const active = useMemo( () => items.find((p) => p.id === openId) ?? null, [items, openId], ); return (
{items.map((item, i) => { const accent = item.accent as Accent; return ( setOpenId(item.id)} initial={{ opacity: 0, y: 24 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: '-60px' }} transition={{ duration: 0.55, ease: [0.22, 1, 0.36, 1], delay: 0.04 * i, }} className={cn( 'group relative flex flex-col overflow-hidden rounded-2xl border border-white/8 bg-white/[0.02] text-start ring-1 ring-transparent transition-all duration-300 hover:-translate-y-1', ACCENT_RING[accent], )} > {/* Cover */}
{/* eslint-disable-next-line @next/next/no-img-element */} {item.title}
{item.role} {item.year}
{/* Body */}

{item.title}

{item.summary}

{item.tags.slice(0, 4).map((tag) => ( {tag} ))}
{t.portfolio.labels.view}
); })}
{active && ( setOpenId(null)} /> )}
); } function Lightbox({ item, labels, locale, onClose, }: { item: Item; labels: Dict['portfolio']['labels']; locale: 'fa' | 'en'; onClose: () => void; }) { const accent = item.accent as Accent; const images = useMemo(() => [item.cover, ...item.gallery], [item]); const [idx, setIdx] = useState(0); const go = useCallback( (dir: number) => setIdx((i) => (i + dir + images.length) % images.length), [images.length], ); // Keyboard navigation + scroll lock while the lightbox is open. useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); else if (e.key === 'ArrowRight') go(locale === 'fa' ? -1 : 1); else if (e.key === 'ArrowLeft') go(locale === 'fa' ? 1 : -1); }; document.addEventListener('keydown', onKey); const prevOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = prevOverflow; }; }, [go, locale, onClose]); return ( e.stopPropagation()} className="grid max-h-full w-full max-w-5xl grid-rows-[auto] overflow-hidden rounded-3xl border border-white/10 bg-base-900/95 shadow-2xl md:grid-cols-[1.4fr_1fr]" > {/* Gallery viewer */}
{/* eslint-disable-next-line @next/next/no-img-element */} {images.length > 1 && ( <> go(locale === 'fa' ? 1 : -1)} label={labels.prev} /> go(locale === 'fa' ? -1 : 1)} label={labels.next} /> )}
{/* Thumbnails */}
{images.map((src, i) => ( ))}
{/* Meta panel */}
{item.client}

{item.title}

{item.summary}

{/* Metrics */}
{item.metrics.map((mt) => (
{mt.value}
{mt.label}
))}
{labels.stack}
{item.tags.map((tag) => ( {tag} ))}
); } function Field({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); } function NavButton({ side, locale, onClick, label, }: { side: 'prev' | 'next'; locale: 'fa' | 'en'; onClick: () => void; label: string; }) { // Visually pin to the left/right edge regardless of text direction. const edge = side === 'prev' ? 'left-3' : 'right-3'; const pointLeft = side === 'prev'; return ( ); } function Arrow({ locale }: { locale: 'fa' | 'en' }) { return ( ); }