This commit is contained in:
amirhosein.ashourloo
2026-08-09 12:05:39 +03:30
commit 6207bb8431
76 changed files with 5127 additions and 0 deletions
@@ -0,0 +1,131 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useMemo, useState } from "react";
import { Reveal } from "@/components/Reveal";
import type { DisplayProject, PortfolioCategory } from "@/lib/api";
const ALL_ID = "__all__";
/**
* Category tabs above a grid — no search, click a tab to filter in place.
*
* Tabs only render when there is more than one real category on the fetched
* projects. The client's portfolio items currently share a single category,
* so the bar stays hidden rather than show one meaningless tab; the moment
* a second category exists in the CMS, this switches on with no code change.
*/
export function PortfolioBrowser({
projects,
categories,
}: {
projects: DisplayProject[];
categories: PortfolioCategory[];
}) {
const [activeId, setActiveId] = useState(ALL_ID);
const showTabs = categories.length > 1;
const filtered = useMemo(
() => (activeId === ALL_ID ? projects : projects.filter((p) => p.categoryId === activeId)),
[projects, activeId],
);
return (
<div>
{showTabs && (
<div className="no-scrollbar mx-auto mb-14 max-w-[1400px] overflow-x-auto px-6 lg:px-12">
<ul className="flex w-max items-center gap-10 border-b border-line">
<li>
<button
type="button"
onClick={() => setActiveId(ALL_ID)}
aria-current={activeId === ALL_ID}
className={`relative whitespace-nowrap pb-4 text-sm transition-colors duration-300 ${
activeId === ALL_ID ? "text-ink" : "text-muted hover:text-ink"
}`}
>
همه پروژهها
<span
aria-hidden="true"
className={`absolute inset-x-0 -bottom-px h-[2px] bg-gold transition-transform duration-300 ease-out-soft ${
activeId === ALL_ID ? "scale-x-100" : "scale-x-0"
}`}
/>
</button>
</li>
{categories.map((cat) => (
<li key={cat.id}>
<button
type="button"
onClick={() => setActiveId(cat.id)}
aria-current={activeId === cat.id}
className={`relative whitespace-nowrap pb-4 text-sm transition-colors duration-300 ${
activeId === cat.id ? "text-ink" : "text-muted hover:text-ink"
}`}
>
{cat.name}
<span
aria-hidden="true"
className={`absolute inset-x-0 -bottom-px h-[2px] bg-gold transition-transform duration-300 ease-out-soft ${
activeId === cat.id ? "scale-x-100" : "scale-x-0"
}`}
/>
</button>
</li>
))}
</ul>
</div>
)}
{filtered.length === 0 ? (
<p className="mx-auto max-w-[1400px] px-6 py-16 text-center text-muted lg:px-12">
پروژهای در این دسته یافت نشد.
</p>
) : (
<ul className="mx-auto grid max-w-[1400px] gap-6 px-6 pb-24 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 lg:px-12 lg:pb-32">
{filtered.map((project, i) => (
<Reveal as="li" key={project.slug} delay={(i % 6) * 70}>
<Link href={`/portfolios/${project.slug}`} className="group block">
<div className="relative aspect-[3/4] overflow-hidden bg-ink">
<Image
src={project.image}
alt={`${project.type} ${project.name}`}
fill
quality={80}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
loading={i < 3 ? "eager" : "lazy"}
className="object-cover transition-transform duration-[1100ms] ease-out-soft group-hover:scale-[1.06]"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/85 via-black/15 to-transparent" />
<div
aria-hidden="true"
className="pointer-events-none absolute inset-4 border border-gold opacity-0 transition-opacity duration-500 group-hover:opacity-100"
/>
<div className="absolute inset-x-0 bottom-0 p-6">
<span className="latin block text-[0.66rem] tracking-[0.28em] text-gold-soft">
{project.nameLatin}
</span>
<h3 className="mt-2.5 text-xl font-bold text-white">{project.name}</h3>
<span className="mt-1.5 block text-sm text-white/65">{project.type}</span>
<span className="mt-4 flex h-0 items-center gap-2 overflow-hidden text-sm text-white opacity-0 transition-all duration-500 ease-out-soft group-hover:h-6 group-hover:opacity-100">
مشاهده پروژه
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-3.5 w-3.5">
<path d="M19 12H5m0 0 6-6m-6 6 6 6" stroke="currentColor" strokeWidth="1.8" />
</svg>
</span>
</div>
</div>
</Link>
</Reveal>
))}
</ul>
)}
</div>
);
}