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

نظرِ مشتری، دستهٔ کارت

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

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

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

این بلاک از ۵ آیتم دیگ استفاده می‌کند: avatarbuttoncontrol-sizeiconspersian

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

۲. کد صفحه

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

testimonial-deck.tsx
"use client"

import * as React from "react"

import { ChevronLeftIcon, ChevronRightIcon } from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { toFaDigits } from "@/lib/persian"
import { cn } from "@/lib/utils"
import { stockImages, type StockImageName } from "@/lib/stock-images"

const STORIES: { photo: StockImageName; title: string; quote: string; name: string; role: string }[] = [
  {
    photo: "personWoman3",
    title: "شیفت‌ها تا ساعتِ نه بسته می‌شود",
    quote: "شنبه‌ها صبح را صرفِ جابه‌جاکردنِ شیفت‌ها می‌کردیم. حالا برنامه پیش از اولین چای آماده است.",
    name: "نسرین کاظمی",
    role: "مدیرِ عملیات، کلینیکِ مهر",
  },
  {
    photo: "personMan1",
    title: "صورت‌حساب‌ها دیگر گم نمی‌شوند",
    quote: "هر فاکتور خودش یادآوری می‌فرستد. وصولِ مطالباتمان در سه ماه دو برابر سریع‌تر شد.",
    name: "بهرام توکلی",
    role: "مدیرِ مالی، پخشِ البرز",
  },
  {
    photo: "personWoman1",
    title: "مشتری خودش نوبت می‌گیرد",
    quote: "تلفن کمتر زنگ می‌زند و صندلی‌ها پرتر است. لینکِ رزرو را در اینستاگرام گذاشتیم و تمام.",
    name: "مریم صالحی",
    role: "مؤسسِ سالنِ آینه",
  },
  {
    photo: "personMan2",
    title: "تیمِ ده‌نفره، یک تقویم",
    quote: "اولین ابزاری است که همه واقعاً بازش می‌کنند. جلسه‌های تکراری نصف شد.",
    name: "کیوان رستمی",
    role: "مدیرِ محصول، آوند",
  },
]

/**
 * نظرِ مشتری به‌شکلِ دستهٔ کارت: عکسِ گوینده روی دسته‌ای از کارت‌های کج‌شده می‌نشیند و با
 * «بعدی» کارتِ رو به ته می‌رود؛ تیترِ نتیجه، نقل‌قول، گوینده و شمارهٔ داستان کنارش عوض می‌شود.
 */
export default function TestimonialDeckBlock() {
  const [index, setIndex] = React.useState(0)
  const story = STORIES[index]
  const go = (delta: number) => setIndex((current) => (current + delta + STORIES.length) % STORIES.length)

  return (
    <ControlSizeProvider size="xl" asChild>
      <section aria-labelledby="deck-title" className="w-full overflow-hidden bg-[var(--dig-amber-300)] px-4 py-16 sm:px-6 sm:py-24 dark:bg-[var(--dig-amber-900)]">
        <div className="mx-auto grid w-full max-w-6xl grid-cols-1 items-center gap-12 rounded-xl bg-card p-6 shadow-xl sm:p-10 lg:grid-cols-[1fr_24rem] lg:p-14">
          <div aria-live="polite" className="flex flex-col gap-6">
            <span className="text-sm font-semibold text-muted-foreground">داستانِ مشتری</span>
            <h2 id="deck-title" key={story.title} className="animate-in fade-in slide-in-from-bottom-2 text-3xl leading-snug font-black text-balance duration-500 sm:text-4xl">
              {story.title}
            </h2>
            <blockquote key={story.quote} className="animate-in fade-in max-w-xl text-lg leading-9 duration-700">
              «{story.quote}»
            </blockquote>
            <div className="flex items-center gap-3">
              <Avatar src={stockImages[story.photo].src} alt="" size="lg" />
              <span className="flex flex-col">
                <span className="font-bold">{story.name}</span>
                <span className="text-sm text-muted-foreground">{story.role}</span>
              </span>
            </div>
            <div className="mt-4 flex items-center justify-between gap-4">
              <div className="flex gap-2">
                <Button variant="outline" size="icon" radius="full" aria-label="داستانِ قبلی" onClick={() => go(-1)}>
                  <ChevronRightIcon />
                </Button>
                <Button size="icon" radius="full" aria-label="داستانِ بعدی" onClick={() => go(1)}>
                  <ChevronLeftIcon />
                </Button>
              </div>
              <span className="text-sm text-muted-foreground tabular-nums">
                {toFaDigits(index + 1)} از {toFaDigits(STORIES.length)}
              </span>
            </div>
          </div>

          <div aria-hidden className="relative mx-auto aspect-[4/5] w-full max-w-80">
            {STORIES.map((item, itemIndex) => {
              const depth = (itemIndex - index + STORIES.length) % STORIES.length
              return (
                <div
                  key={item.name}
                  style={{
                    zIndex: STORIES.length - depth,
                    transform: `translateX(${depth * -14}px) translateY(${depth * 6}px) rotate(${depth === 0 ? 0 : depth * -3}deg) scale(${1 - depth * 0.04})`,
                  }}
                  className="absolute inset-0 overflow-hidden rounded-xl border-4 border-card bg-muted shadow-lg transition-transform duration-500 ease-out"
                >
                  {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
                  <img src={stockImages[item.photo].src} alt="" className={cn("size-full object-cover", depth > 0 && "grayscale")} />
                </div>
              )
            })}
          </div>
        </div>
      </section>
    </ControlSizeProvider>
  )
}