نظرِ مشتری، دستهٔ کارت
پیشنمایش تمامصفحهبخشِ نظرِ مشتری: عکسِ گوینده روی دستهای از کارتهای کجشده و با «بعدی» کارتِ رو به ته میرود؛ تیترِ نتیجه، نقلقول، گوینده و شمارهٔ داستان کنارش.
۱. نصب کامپوننتها
این بلاک از ۵ آیتم دیگ استفاده میکند: 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.jsonpnpm 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.jsonyarn 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.jsonbunx --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>
)
}