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

ریلِ عکس‌های هم‌پوشان

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

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

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

این بلاک از ۴ آیتم دیگ استفاده می‌کند: buttoncontrol-sizeiconsreveal

پکیج‌منیجر پیش‌فرض 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/icons.json https://docs.digdesign.ir/r/reveal.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/icons.json https://docs.digdesign.ir/r/reveal.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/icons.json https://docs.digdesign.ir/r/reveal.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/icons.json https://docs.digdesign.ir/r/reveal.json

۲. کد صفحه

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

gallery-reel.tsx
"use client"

import * as React from "react"

import { ArrowUpIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Reveal } from "@/components/ui/reveal"
import { cn } from "@/lib/utils"
import { stockImages, type StockImageName } from "@/lib/stock-images"

const STORIES: { photo: StockImageName; title: string; place: string }[] = [
  { photo: "studioSunlit", title: "کارگاهِ سفالِ نور", place: "اصفهان" },
  { photo: "personWoman1", title: "بوتیکِ لباسِ تابستانی", place: "شیراز" },
  { photo: "personWoman2", title: "کافه‌کتابِ سرو", place: "تهران" },
  { photo: "foodDish", title: "آشپزخانهٔ خانگیِ مهتاب", place: "رشت" },
  { photo: "personMan1", title: "استودیوی عکاسیِ آینه", place: "تبریز" },
  { photo: "productVase", title: "گالریِ دکورِ کاشی", place: "یزد" },
  { photo: "restaurant", title: "رستورانِ حیاط", place: "کاشان" },
]

/**
 * ریلِ عکس‌های هم‌پوشان: کارت‌ها روی هم می‌نشینند و کارتِ فعال باز می‌شود تا عکس و
 * اسمِ داستان کامل دیده شود. با هاور، فوکوسِ کیبورد یا لمس عوض می‌شود و هر چند ثانیه
 * خودش جلو می‌رود؛ در موبایل ریلِ افقیِ قابلِ کشیدن است.
 */
export default function GalleryReelBlock() {
  const [active, setActive] = React.useState(2)
  const [paused, setPaused] = React.useState(false)

  React.useEffect(() => {
    if (paused || window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return
    const timer = window.setInterval(() => setActive((current) => (current + 1) % STORIES.length), 3200)
    return () => window.clearInterval(timer)
  }, [paused])

  return (
    <ControlSizeProvider size="xl" asChild>
      <section aria-labelledby="reel-title" className="flex w-full flex-col gap-12 overflow-hidden py-24">
        <div className="mx-auto flex w-full max-w-6xl flex-col items-start justify-between gap-6 px-4 sm:px-6 md:flex-row md:items-end">
          <div className="flex max-w-xl flex-col gap-3">
            <span className="text-sm font-semibold text-brand-secondary">ساخته‌شده با پیشخوان</span>
            <h2 id="reel-title" className="text-3xl leading-snug font-black text-balance sm:text-4xl">
              کسب‌وکارهای کوچک، داستان‌های بزرگ
            </h2>
            <p className="leading-7 text-muted-foreground">بیش از سه هزار فروشگاهِ مستقل با پیشخوان فروش آنلاین را شروع کرده‌اند.</p>
          </div>
          <Button variant="outline">
            همهٔ داستان‌ها
            <ArrowUpIcon aria-hidden className="-rotate-45" />
          </Button>
        </div>

        <Reveal
          className="mx-auto flex w-full max-w-6xl snap-x snap-mandatory gap-3 overflow-x-auto px-4 pb-2 [scrollbar-width:none] sm:px-6 md:gap-0 md:overflow-visible md:pb-0 [&::-webkit-scrollbar]:hidden"
          onMouseEnter={() => setPaused(true)}
          onMouseLeave={() => setPaused(false)}
          onFocus={() => setPaused(true)}
          onBlur={() => setPaused(false)}
        >
          {STORIES.map((story, index) => {
            const open = index === active
            return (
              <a
                key={story.title}
                href="#"
                aria-current={open ? "true" : undefined}
                aria-label={`داستانِ ${story.title}، ${story.place}`}
                onMouseEnter={() => setActive(index)}
                onFocus={() => setActive(index)}
                style={{ zIndex: open ? STORIES.length + 1 : STORIES.length - Math.abs(index - active) }}
                className={cn(
                  "group relative h-96 shrink-0 snap-center overflow-hidden rounded-xl bg-muted text-start shadow-lg outline-none ring-4 ring-background transition-[flex-grow,flex-basis,transform,width] duration-500 ease-out focus-visible:ring-ring",
                  "w-64 md:w-auto md:basis-0",
                  index > 0 && "md:-ms-10",
                  open ? "md:grow-[3.2]" : "md:grow md:scale-[0.94]"
                )}
              >
                {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
                <img src={stockImages[story.photo].src} alt="" className="absolute inset-0 size-full object-cover transition-transform duration-700 group-hover:scale-105" />
                <span
                  aria-hidden
                  className={cn(
                    "absolute inset-x-0 bottom-0 flex flex-col gap-1 bg-linear-to-t from-[var(--dig-graphite-950)] to-transparent p-5 pt-20 text-[var(--dig-white)] transition-[translate] duration-500",
                    open ? "translate-y-0" : "md:translate-y-full"
                  )}
                >
                  <span className="text-lg font-bold">{story.title}</span>
                  <span className="text-sm text-[var(--dig-neutral-300)]">{story.place}</span>
                </span>
              </a>
            )
          })}
        </Reveal>
      </section>
    </ControlSizeProvider>
  )
}