باشگاهِ مشتریان
پیشنمایش تمامصفحهامتیاز و سطح با فاصله تا سطحِ بعد، جوایز با دلیلِ صریح برای غیرفعالها («۲٬۵۸۰ امتیاز کم داری»)، کدِ جایزه با کپی و تاریخچهٔ امتیاز.
۱. نصب کامپوننتها
این بلاک از ۶ آیتم دیگ استفاده میکند: buttoncontrol-sizeiconspersianprogresstoast
پکیجمنیجر پیشفرض 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/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/toast.jsonpnpm 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/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/toast.jsonyarn 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/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/toast.jsonbunx --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/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
wallet-rewards.tsx
"use client"
import * as React from "react"
import { BadgeStarIcon, CheckIcon, CopyIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Progress } from "@/components/ui/progress"
import { toast } from "@/components/ui/toast"
import { formatCurrency, formatNumber, toFaDigits } from "@/lib/persian"
const LEVEL = { name: "نقرهای", next: "طلایی", points: 3_420, nextAt: 5_000 }
const REWARDS = [
{ title: "۵۰ هزار تومان اعتبار", cost: 1_000, code: "AVND50" },
{ title: "ارسالِ رایگانِ یک ماهه", cost: 1_500, code: "SHIPFREE" },
{ title: "٪۱۰ تخفیفِ خانهٔ سفال", cost: 2_500, code: "SOFAL10" },
{ title: "اشتراکِ حرفهای، یک ماه", cost: 6_000, code: "PRO1M" },
]
const HISTORY = [
{ title: "خریدِ کافه رست", points: 185, date: "امروز" },
{ title: "دعوتِ سارا محمدی", points: 500, date: "دیروز" },
{ title: "خرید از فروشگاهِ بوم", points: 740, date: "۱۲ مهر" },
]
/**
* باشگاهِ مشتریان: کارتِ تیرهٔ امتیاز با سطحِ فعلی و فاصله تا سطحِ بعد، جوایزِ قابلِ دریافت
* (جایزهٔ گرانتر از موجودی غیرفعال با دلیلِ «۲٬۵۸۰ امتیاز کم داری»)، و تاریخچهٔ کسبِ امتیاز.
*/
export default function WalletRewardsBlock() {
const [points, setPoints] = React.useState(LEVEL.points)
const [claimed, setClaimed] = React.useState<string[]>([])
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-4xl flex-col gap-8 p-4 sm:p-6">
<h1 className="sr-only">باشگاهِ مشتریان</h1>
<section aria-label="امتیازِ من" className="flex flex-col gap-5 rounded-xl bg-inverse p-6 text-[var(--dig-white)] [--muted-foreground:var(--dig-neutral-400)] sm:p-8">
<div className="flex items-center justify-between gap-3">
<span className="flex items-center gap-2 font-semibold">
<BadgeStarIcon aria-hidden className="size-5 text-[var(--dig-orange-400)]" />
باشگاهِ آوند · سطحِ {LEVEL.name}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="text-sm text-muted-foreground">امتیازِ قابلِ خرج</span>
<span className="text-4xl font-black tabular-nums">{formatNumber(points)}</span>
</div>
<div className="flex flex-col gap-2">
<Progress value={(LEVEL.points / LEVEL.nextAt) * 100} aria-label={`پیشرفت تا سطحِ ${LEVEL.next}`} className="[--progress:var(--dig-orange-400)]" />
<span className="text-sm text-muted-foreground">
{formatNumber(LEVEL.nextAt - LEVEL.points)} امتیاز تا سطحِ {LEVEL.next} (ارسالِ رایگانِ همیشگی)
</span>
</div>
</section>
<section aria-labelledby="wr-rewards" className="flex flex-col gap-4">
<h2 id="wr-rewards" className="text-xl font-bold">
جوایز
</h2>
<ul className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{REWARDS.map((reward) => {
const missing = reward.cost - points
const done = claimed.includes(reward.code)
return (
<li key={reward.code} className="flex flex-col gap-4 rounded-xl border border-border bg-card p-4">
<div className="flex items-start justify-between gap-3">
<span className="font-semibold">{reward.title}</span>
<span className="shrink-0 text-sm text-muted-foreground tabular-nums">{formatNumber(reward.cost)} امتیاز</span>
</div>
{done ? (
<Button
variant="outline"
onClick={() => {
void navigator.clipboard?.writeText(reward.code)
toast.success("کد کپی شد.")
}}
>
<CopyIcon />
<bdi dir="ltr">{reward.code}</bdi>
</Button>
) : (
<Button
// در شبکهٔ گزینهها هیچکدام دکمهٔ اصلی نیست؛ چند دکمهٔ پررنگ کنارِ هم با هم رقابت میکنند.
variant="outline"
disabled={missing > 0}
onClick={() => {
setPoints((value) => value - reward.cost)
setClaimed((list) => [...list, reward.code])
toast.success("جایزه گرفتی؛ کدش آماده است.")
}}
>
{missing > 0 ? `${formatNumber(missing)} امتیاز کم داری` : "دریافت"}
</Button>
)}
</li>
)
})}
</ul>
</section>
<section aria-labelledby="wr-history" className="flex flex-col gap-3">
<h2 id="wr-history" className="text-xl font-bold">
امتیازهای اخیر
</h2>
<ul className="flex flex-col divide-y divide-border rounded-xl border border-border bg-card">
{HISTORY.map((item) => (
<li key={item.title} className="flex items-center gap-3 p-4">
<CheckIcon aria-hidden className="size-4 text-success" />
<span className="flex-1">{item.title}</span>
<span className="text-sm text-muted-foreground">{item.date}</span>
<span className="font-bold text-success tabular-nums">
<bdi dir="ltr">+{toFaDigits(item.points)}</bdi>
</span>
</li>
))}
</ul>
<p className="text-xs text-muted-foreground">هر {formatCurrency(1_000)} خرید یک امتیاز؛ امتیازها تا یک سال معتبرند.</p>
</section>
</main>
</ControlSizeProvider>
)
}