بلاک‌ها · بانکی و پرداخت

باشگاهِ مشتریان

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

امتیاز و سطح با فاصله تا سطحِ بعد، جوایز با دلیلِ صریح برای غیرفعال‌ها («۲٬۵۸۰ امتیاز کم داری»)، کدِ جایزه با کپی و تاریخچهٔ امتیاز.

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

این بلاک از ۶ آیتم دیگ استفاده می‌کند: 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.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/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/toast.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/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/toast.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/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>
  )
}