گالریِ آجری
پیشنمایش تمامصفحهبخشِ گالری با عکسهای همعرض و ارتفاعِ متفاوت در ستونها، فیلترِ سگمنتِ دسته و نمای بزرگ در دیالوگ با قبلی/بعدی، کلیدهای جهت و شمارهٔ عکس.
۱. نصب کامپوننتها
این بلاک از ۷ آیتم دیگ استفاده میکند: buttoncontrol-sizedialogiconspersianrevealtoggle-group
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dialog.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/reveal.json https://docs.digdesign.ir/r/toggle-group.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dialog.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/reveal.json https://docs.digdesign.ir/r/toggle-group.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dialog.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/reveal.json https://docs.digdesign.ir/r/toggle-group.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dialog.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/reveal.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
gallery-masonry.tsx
"use client";
import * as React from "react";
import { ChevronLeftIcon, ChevronRightIcon } from "@/components/icons";
import { Button } from "@/components/ui/button";
import { ControlSizeProvider } from "@/components/ui/control-size";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Reveal } from "@/components/ui/reveal";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { toFaDigits } from "@/lib/persian";
import { stockImages, type StockImageName } from "@/lib/stock-images";
type Shot = {
photo: StockImageName;
title: string;
tag: "space" | "food" | "people";
tall?: boolean;
};
const SHOTS: Shot[] = [
{
photo: "restaurant",
title: "سالنِ اصلی، عصرِ پنجشنبه",
tag: "space",
tall: true,
},
{ photo: "foodDish", title: "خورشِ فصل با برنجِ دمپخت", tag: "food" },
{ photo: "personWoman2", title: "سرآشپز، پیش از سرویس", tag: "people" },
{ photo: "studioSunlit", title: "نورِ صبح روی میزِ کار", tag: "space" },
{
photo: "productVase",
title: "ظرفهای سفالیِ دستساز",
tag: "food",
tall: true,
},
{ photo: "personMan2", title: "میزبانِ شب", tag: "people", tall: true },
{ photo: "coverMinimalRoom", title: "گوشهٔ آرامِ طبقهٔ دوم", tag: "space" },
{ photo: "projectCafeBrand", title: "منوی تازهٔ پاییز", tag: "food" },
];
const FILTERS = [
{ value: "all", label: "همه" },
{ value: "space", label: "فضا" },
{ value: "food", label: "غذا" },
{ value: "people", label: "آدمها" },
];
/**
* گالریِ آجری با فیلترِ سگمنت: عکسها با ارتفاعِ متفاوت در ستونها مینشینند و با کلیک در
* نمای بزرگ باز میشوند؛ قبلی/بعدی با دکمه یا کلیدهای جهت، و شمارهٔ عکس.
*/
export default function GalleryMasonryBlock() {
const [filter, setFilter] = React.useState("all");
const [open, setOpen] = React.useState<number | null>(null);
const shots = SHOTS.filter((shot) => filter === "all" || shot.tag === filter);
const current = open === null ? null : shots[open];
const step = (delta: number) =>
setOpen((index) =>
index === null ? index : (index + delta + shots.length) % shots.length,
);
return (
<ControlSizeProvider size="xl" asChild>
<section
aria-labelledby="masonry-title"
className="mx-auto flex w-full max-w-6xl flex-col gap-10 px-4 py-24 sm:px-6"
>
<div className="flex flex-col items-start justify-between gap-6 md:flex-row md:items-end">
<div className="flex flex-col gap-3">
<span className="text-sm font-semibold text-brand-secondary">
گالری
</span>
<h2 id="masonry-title" className="text-3xl font-black sm:text-4xl">
یک شب در حیاط
</h2>
</div>
<ToggleGroup
type="single"
variant="segmented"
value={filter}
onValueChange={(value) => value && setFilter(value as string)}
aria-label="دستهٔ عکس"
>
{FILTERS.map((item) => (
<ToggleGroupItem key={item.value} value={item.value}>
{item.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<Dialog
open={current !== null}
onOpenChange={(value) => !value && setOpen(null)}
>
<ul className="columns-1 gap-4 sm:columns-2 lg:columns-3">
{shots.map((shot, index) => (
<Reveal asChild key={shot.photo} delay={(index % 3) * 80}>
<li className="mb-4 break-inside-avoid">
<DialogTrigger asChild>
<button
type="button"
onClick={() => setOpen(index)}
className="group relative block w-full overflow-hidden rounded-xl outline-none focus-visible:ring-4 focus-visible:ring-ring"
>
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img
src={stockImages[shot.photo].src}
alt={shot.title}
className={`w-full object-cover transition-transform duration-500 group-hover:scale-105 ${shot.tall ? "aspect-[3/4]" : "aspect-[4/3]"}`}
/>
<span
aria-hidden
className="absolute inset-x-0 bottom-0 translate-y-full bg-linear-to-t from-[var(--dig-graphite-950)] to-transparent p-4 pt-12 text-start text-sm font-semibold text-[var(--dig-white)] transition-transform duration-300 group-hover:translate-y-0 group-focus-visible:translate-y-0"
>
{shot.title}
</span>
</button>
</DialogTrigger>
</li>
</Reveal>
))}
</ul>
<DialogContent
className="max-w-4xl gap-4 p-3 sm:p-4"
onKeyDown={(event) => {
if (event.key === "ArrowLeft") step(1);
if (event.key === "ArrowRight") step(-1);
}}
>
{current && (
<>
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img
src={stockImages[current.photo].src}
alt={current.title}
className="max-h-[70vh] w-full rounded-lg object-cover"
/>
<div className="flex items-center justify-between gap-4 px-1">
<div className="flex flex-col gap-0.5">
<DialogTitle className="text-base">
{current.title}
</DialogTitle>
<DialogDescription>
عکسِ {toFaDigits((open ?? 0) + 1)} از{" "}
{toFaDigits(shots.length)}
</DialogDescription>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="icon"
aria-label="عکسِ قبلی"
onClick={() => step(-1)}
>
<ChevronRightIcon />
</Button>
<Button
variant="outline"
size="icon"
aria-label="عکسِ بعدی"
onClick={() => step(1)}
>
<ChevronLeftIcon />
</Button>
</div>
</div>
</>
)}
</DialogContent>
</Dialog>
</section>
</ControlSizeProvider>
);
}