195 lines
6.8 KiB
TypeScript
195 lines
6.8 KiB
TypeScript
"use client";
|
|
|
|
import { AnimatePresence, motion } from "framer-motion";
|
|
import { useState } from "react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface Projects7Type {
|
|
id: number;
|
|
title: string;
|
|
category: string;
|
|
description: string;
|
|
image: string;
|
|
tags: string[];
|
|
}
|
|
|
|
const categoryLabels: Record<string, string> = {
|
|
MINIMAL: "مینیمال",
|
|
ABSTRACT: "انتزاعی",
|
|
EDITORIAL: "ادیتوریال",
|
|
"STUDIO SHOT": "عکاسی استودیو",
|
|
};
|
|
|
|
const filterCategories = [
|
|
{ key: "ALL", label: "همه" },
|
|
{ key: "MINIMAL", label: "مینیمال" },
|
|
{ key: "ABSTRACT", label: "انتزاعی" },
|
|
{ key: "EDITORIAL", label: "ادیتوریال" },
|
|
{ key: "STUDIO SHOT", label: "عکاسی استودیو" },
|
|
] as const;
|
|
|
|
const Projects7Data: Projects7Type[] = [
|
|
{
|
|
id: 1,
|
|
title: "هندسه مینیمال",
|
|
category: "MINIMAL",
|
|
description:
|
|
"ترکیبی تمیز با تمرکز بر شکلهای ساده و فضای منفی که آرامش و وضوح را برمیانگیزد.",
|
|
image: "/block-media/shadcnblocks/block/lummi/random1.jpeg",
|
|
tags: ["minimal", "geometry"],
|
|
},
|
|
{
|
|
id: 2,
|
|
title: "جریان رنگ انتزاعی",
|
|
category: "ABSTRACT",
|
|
description:
|
|
"رنگهای زنده در الگویی انتزاعی میآمیزند و حس حرکت و انرژی میآفرینند.",
|
|
image: "/block-media/shadcnblocks/block/lummi/random2.jpeg",
|
|
tags: ["abstract", "color"],
|
|
},
|
|
{
|
|
id: 3,
|
|
title: "پرتره ادیتوریال",
|
|
category: "EDITORIAL",
|
|
description:
|
|
"پرترهای ادیتوریال چشمگیر با نورپردازی دراماتیک و تمرکز بر بیان و حس.",
|
|
image: "/block-media/shadcnblocks/block/lummi/random3.jpeg",
|
|
tags: ["editorial", "portrait"],
|
|
},
|
|
{
|
|
id: 4,
|
|
title: "طبیعت بیجان استودیو",
|
|
category: "STUDIO SHOT",
|
|
description:
|
|
"عکاسی استودیویی با چیدمانی دقیق از اشیای روزمره که بافت و فرم را برجسته میکند.",
|
|
image: "/block-media/shadcnblocks/block/lummi/random4.jpeg",
|
|
tags: ["studio", "still-life"],
|
|
},
|
|
{
|
|
id: 5,
|
|
title: "سایههای مینیمال",
|
|
category: "MINIMAL",
|
|
description:
|
|
"سایههای نرم و گرادیانهای ظریف، صحنهای آرام و مینیمال با حس مدرن میسازند.",
|
|
image: "/block-media/shadcnblocks/block/lummi/random5.jpeg",
|
|
tags: ["minimal", "shadows"],
|
|
},
|
|
{
|
|
id: 6,
|
|
title: "استودیوی انتزاعی",
|
|
category: "ABSTRACT",
|
|
description:
|
|
"نور استودیو با فرمهای انتزاعی در این ترکیببندی آزمایشی میآمیزد و هنر و عکاسی را در هم میکشد.",
|
|
image: "/block-media/shadcnblocks/block/lummi/random6.jpeg",
|
|
tags: ["abstract", "studio"],
|
|
},
|
|
];
|
|
|
|
interface Projects7Props {
|
|
className?: string;
|
|
}
|
|
|
|
const Projects7 = ({ className }: Projects7Props) => {
|
|
const [activeFilter, setActiveFilter] = useState<string>("ALL");
|
|
const [filteredItems, setFilteredItems] = useState(Projects7Data);
|
|
|
|
const handleFilterChange = (category: string) => {
|
|
setActiveFilter(category);
|
|
|
|
if (category === "ALL") {
|
|
setFilteredItems(Projects7Data);
|
|
} else {
|
|
const filtered = Projects7Data.filter(
|
|
(item) =>
|
|
item.category === category ||
|
|
item.tags.includes(category.toLowerCase().replace(" ", "-")),
|
|
);
|
|
setFilteredItems(filtered);
|
|
}
|
|
};
|
|
|
|
const activeFilterLabel =
|
|
filterCategories.find((item) => item.key === activeFilter)?.label ??
|
|
activeFilter;
|
|
|
|
return (
|
|
<section dir="rtl" lang="fa" className={cn("py-32", className)}>
|
|
<div className="container">
|
|
<div className="mx-auto max-w-7xl">
|
|
<div className="mb-16 flex flex-wrap justify-center gap-8">
|
|
{filterCategories.map(({ key, label }) => (
|
|
<button
|
|
key={key}
|
|
onClick={() => handleFilterChange(key)}
|
|
className={`text-sm font-medium tracking-wider transition-colors duration-300 hover:text-foreground ${
|
|
activeFilter === key
|
|
? "border-b-2 border-border pb-1 text-foreground"
|
|
: "text-gray-500"
|
|
}`}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<motion.div layout className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
|
<AnimatePresence mode="wait">
|
|
{filteredItems.map((item) => (
|
|
<motion.div
|
|
key={item.id}
|
|
layout
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.9 }}
|
|
transition={{
|
|
duration: 0.2,
|
|
ease: "easeInOut",
|
|
}}
|
|
className="group relative cursor-pointer overflow-hidden rounded-lg"
|
|
>
|
|
<div className="relative aspect-[4/3] h-96 w-full">
|
|
<img
|
|
src={item.image}
|
|
alt={item.title}
|
|
className="object-cover transition-transform duration-300"
|
|
/>
|
|
|
|
<motion.div className="absolute inset-0 flex flex-col items-center justify-center bg-black/60 p-8 text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
|
<motion.div className="translate-y-5 text-center opacity-0 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100">
|
|
<p className="mb-4 text-xs font-medium tracking-widest text-gray-300">
|
|
{categoryLabels[item.category] ?? item.category}
|
|
</p>
|
|
<h3 className="mb-4 text-2xl font-light tracking-wide">
|
|
{item.title}
|
|
</h3>
|
|
<p className="max-w-xs text-sm leading-relaxed text-gray-200">
|
|
{item.description}
|
|
</p>
|
|
</motion.div>
|
|
</motion.div>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
</motion.div>
|
|
|
|
{filteredItems.length === 0 && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
className="py-16 text-center"
|
|
>
|
|
<p className="text-lg text-gray-500">
|
|
موردی در دسته «{activeFilterLabel}» یافت نشد.
|
|
</p>
|
|
</motion.div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export { Projects7 };
|