بلاک‌ها · بخش‌های صفحه

بخشِ گالری با عکس‌های هم‌عرض و ارتفاعِ متفاوت در ستون‌ها، فیلترِ سگمنتِ دسته و نمای بزرگ در دیالوگ با قبلی/بعدی، کلیدهای جهت و شمارهٔ عکس.

۱. نصب کامپوننت‌ها

این بلاک از ۷ آیتم دیگ استفاده می‌کند: 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.json
pnpm 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.json
yarn 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.json
bunx --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>
  );
}