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

گالریِ اسلایدی

پیش‌نمایش تمام‌صفحه

بخشِ گالری با اسلایدهای پهن و توضیحِ هر پروژه، دکمه‌های قبلی/بعدی کنارِ تیتر، اسلایدِ بعدی از لبه پیدا و نوارِ پیشرفت؛ عکس‌های غیرفعال سیاه‌وسفید.

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

این بلاک از ۳ آیتم دیگ استفاده می‌کند: carouselcontrol-sizepersian

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/carousel.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/persian.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/carousel.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/persian.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/carousel.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/persian.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/carousel.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/persian.json

۲. کد صفحه

کد را در app/<route>/page.tsx بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

gallery-carousel.tsx
"use client"

import * as React from "react"

import { ControlSizeProvider } from "@/components/ui/control-size"
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi } from "@/components/ui/carousel"
import { toFaDigits } from "@/lib/persian"
import { cn } from "@/lib/utils"
import { stockImages, type StockImageName } from "@/lib/stock-images"

const SLIDES: { photo: StockImageName; title: string; caption: string }[] = [
  { photo: "studioSunlit", title: "استودیوی نور", caption: "بازسازیِ یک انبارِ قدیمی به فضای کارِ اشتراکی، ۱۴۰۴" },
  { photo: "restaurant", title: "رستورانِ حیاط", caption: "طراحیِ داخلی و نورپردازیِ سالنِ ۸۰ نفره" },
  { photo: "coverMinimalRoom", title: "خانهٔ سپید", caption: "آپارتمانِ ۹۰ متری با مبلمانِ سفارشی" },
  { photo: "clinicRoom", title: "کلینیکِ مهر", caption: "فضای انتظارِ آرام با نورِ طبیعی" },
  { photo: "coverWorkspace", title: "دفترِ آوند", caption: "دفترِ ۴۰ نفره با اتاق‌های تمرکز" },
]

/**
 * گالریِ اسلایدی: عکس‌های پهن با اسم و توضیحِ پروژه، دکمه‌های قبلی/بعدی کنارِ تیتر،
 * نوارِ پیشرفتِ اسلایدها و شمارهٔ اسلاید؛ اسلایدِ بعدی از لبه پیداست تا کشیدن را القا کند.
 */
export default function GalleryCarouselBlock() {
  const [api, setApi] = React.useState<CarouselApi>()
  const [index, setIndex] = React.useState(0)

  React.useEffect(() => {
    if (!api) return
    const update = () => setIndex(api.selectedScrollSnap())
    update()
    api.on("select", update)
    return () => {
      api.off("select", update)
    }
  }, [api])

  return (
    <ControlSizeProvider size="xl" asChild>
      <section aria-labelledby="gc-title" className="flex w-full flex-col gap-10 overflow-hidden bg-subtle py-24">
        <Carousel setApi={setApi} opts={{ align: "start" }} className="mx-auto flex w-full max-w-6xl flex-col gap-10 px-4 sm:px-6">
          <div className="flex items-end justify-between gap-6">
            <div className="flex flex-col gap-3">
              <span className="text-sm font-semibold text-brand-secondary">نمونه‌کار</span>
              <h2 id="gc-title" className="text-3xl font-black sm:text-4xl">
                فضاهایی که ساختیم
              </h2>
            </div>
            <div className="flex shrink-0 gap-2">
              <CarouselPrevious className="static size-11 translate-y-0" />
              <CarouselNext className="static size-11 translate-y-0" />
            </div>
          </div>
          <CarouselContent className="[&>*]:basis-[88%] sm:[&>*]:basis-[70%] lg:[&>*]:basis-[58%]">
            {SLIDES.map((slide, slideIndex) => (
              <CarouselItem key={slide.title}>
                <figure className="flex flex-col gap-4">
                  {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
                  <img
                    src={stockImages[slide.photo].src}
                    alt={stockImages[slide.photo].alt}
                    className={cn("aspect-[16/10] w-full rounded-xl object-cover transition-[filter] duration-500", slideIndex !== index && "grayscale")}
                  />
                  <figcaption className="flex items-baseline justify-between gap-4">
                    <span className="flex flex-col gap-1">
                      <span className="text-lg font-bold">{slide.title}</span>
                      <span className="text-sm text-muted-foreground">{slide.caption}</span>
                    </span>
                    <span className="text-sm text-muted-foreground tabular-nums">{toFaDigits(String(slideIndex + 1).padStart(2, "0"))}</span>
                  </figcaption>
                </figure>
              </CarouselItem>
            ))}
          </CarouselContent>
          <div aria-hidden className="flex gap-1.5">
            {SLIDES.map((slide, slideIndex) => (
              <span key={slide.title} className={cn("h-1 rounded-full transition-all duration-300", slideIndex === index ? "w-10 bg-foreground" : "w-4 bg-border")} />
            ))}
          </div>
        </Carousel>
      </section>
    </ControlSizeProvider>
  )
}