بلاک‌ها · فروشگاهی

پنلِ حسابِ مشتری: طلا یا نقره، قیمتِ لحظه‌ای، خرید و فروش با مبلغ یا مقدار، کارمزد و جمع، پیشنهادِ وام با وثیقهٔ طلا، کارتِ دارایی با پنهان‌کردنِ موجودی و نمودارِ ۲۴ ساعت.

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

این بلاک از ۸ آیتم دیگ استفاده می‌کند: buttonchart-presetscontrol-sizeiconspersiantext-fieldtoasttoggle-group

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

۲. کد صفحه

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

account-gold.tsx
"use client"

import * as React from "react"

import { ChatIcon, ChevronLeftIcon, ClockIcon, DashboardIcon, EditIcon, EyeIcon, EyeOffIcon, HeartIcon, InfoIcon, LocationIcon, LogoutIcon, ShopIcon, TrendingUpIcon, UserIcon, WalletIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { AreaChart } from "@/components/ui/chart-presets"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { TextField } from "@/components/ui/text-field"
import { toast } from "@/components/ui/toast"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatCurrency, formatNumber, toFaDigits } from "@/lib/persian"

const NAV = [
  { id: "overview", label: "خلاصهٔ فعالیت‌ها", icon: <DashboardIcon /> },
  { id: "orders", label: "سفارش‌ها", icon: <ShopIcon /> },
  { id: "gold", label: "طلای دیجیتال", icon: <TrendingUpIcon /> },
  { id: "lists", label: "لیست‌های من", icon: <HeartIcon /> },
  { id: "reviews", label: "دیدگاه‌ها و پرسش‌ها", icon: <ChatIcon /> },
  { id: "addresses", label: "نشانی‌ها", icon: <LocationIcon /> },
  { id: "account", label: "اطلاعاتِ حساب", icon: <UserIcon /> },
]

/** ستونِ کنارِ پنلِ حساب: نام و موبایل، کیف پول با افزایشِ موجودی، و منوی بخش‌ها. */
function AccountNav({ active }: { active: string }) {
  return (
    <aside className="flex flex-col gap-2 rounded-xl border border-border bg-card p-4 lg:sticky lg:top-6">
      <div className="flex items-start justify-between gap-2 px-2 pt-1 pb-4">
        <span className="flex flex-col gap-0.5">
          <span className="font-bold">آرمیتا رستگار</span>
          <bdi dir="ltr" className="text-sm text-muted-foreground tabular-nums">
            {toFaDigits("0912 345 6789")}
          </bdi>
        </span>
        <Button variant="ghost" size="icon" aria-label="ویرایشِ اطلاعات" className="-me-2 -mt-2">
          <EditIcon />
        </Button>
      </div>
      <div className="flex items-center justify-between gap-2 rounded-lg bg-subtle p-3">
        <span className="flex items-center gap-2 text-sm [&>svg]:size-4 [&>svg]:text-muted-foreground">
          <WalletIcon aria-hidden />
          کیف پول
        </span>
        <span className="font-bold tabular-nums">{formatCurrency(150_129)}</span>
      </div>
      <a href="#" className="flex items-center gap-1 self-end px-2 pb-2 text-sm font-medium text-brand-secondary hover:underline">
        افزایشِ موجودی
        <ChevronLeftIcon aria-hidden className="size-4" />
      </a>
      <nav aria-label="حسابِ کاربری" className="flex flex-col border-t border-border pt-2">
        {NAV.map((item) => (
          <a
            key={item.id}
            href="#"
            aria-current={item.id === active ? "page" : undefined}
            className={`flex min-h-12 items-center gap-3 rounded-lg px-3 text-sm [&>svg]:size-5 ${item.id === active ? "bg-primary-subtle font-bold" : "text-muted-foreground hover:bg-subtle hover:text-foreground"}`}
          >
            {item.icon}
            {item.label}
          </a>
        ))}
        <a href="#" className="flex min-h-12 items-center gap-3 rounded-lg px-3 text-sm text-destructive hover:bg-destructive-subtle [&>svg]:size-5">
          <LogoutIcon aria-hidden />
          خروج از حساب
        </a>
      </nav>
    </aside>
  )
}

const PRICE = { gold: 262_430, silver: 5_108 } // ریال برای هر میلی‌گرم
const FEE = 0.005
const CHART = [
  { label: "۱۲:۰۰", value: 255_088 },
  { label: "۱۵:۰۰", value: 256_120 },
  { label: "۱۸:۰۰", value: 257_064 },
  { label: "۲۱:۰۰", value: 258_900 },
  { label: "۰۰:۰۰", value: 259_041 },
  { label: "۰۳:۰۰", value: 259_800 },
  { label: "۰۶:۰۰", value: 261_018 },
  { label: "۰۹:۰۰", value: 262_430 },
]

/**
 * خرید و فروشِ طلای دیجیتال در پنلِ حساب: انتخابِ طلا/نقره، قیمتِ لحظه‌ایِ هر میلی‌گرم با نقطهٔ زنده،
 * خرید/فروش با سگمنت، مبلغ به ریال یا مقدار به میلی‌گرم (هر کدام را بنویسی دیگری حساب می‌شود)، کارمزد
 * و جمع، پیشنهادِ «وام با وثیقهٔ طلا» به‌جای فروش، و ستونِ دارایی با پنهان‌کردنِ موجودی و نمودارِ ۲۴ ساعت.
 */
export default function AccountGoldBlock() {
  const [metal, setMetal] = React.useState<"gold" | "silver">("gold")
  const [side, setSide] = React.useState("buy")
  const [rial, setRial] = React.useState("")
  const [mg, setMg] = React.useState("")
  const [hidden, setHidden] = React.useState(false)
  const [loanDismissed, setLoanDismissed] = React.useState(false)
  const price = PRICE[metal]
  const amount = Number(rial) || 0
  const fee = Math.round(amount * FEE)
  const name = metal === "gold" ? "طلا" : "نقره"

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto grid w-full max-w-6xl grid-cols-1 items-start gap-6 p-4 sm:p-6 lg:grid-cols-[17rem_minmax(0,1fr)]">
        <div className="hidden lg:block">
          <AccountNav active="gold" />
        </div>

        <div className="grid min-w-0 grid-cols-1 items-start gap-6 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
          <section aria-labelledby="trade-title" className="flex flex-col gap-5 rounded-xl border border-border bg-card p-5">
            <h1 id="trade-title" className="text-xl font-black">
              خرید و فروشِ طلای دیجیتال
            </h1>
            {/* انتخابِ فلز: دو کارتِ بزرگ با رنگِ هر فلز. */}
            <div role="radiogroup" aria-label="فلز" className="grid grid-cols-2 gap-3">
              {(["gold", "silver"] as const).map((item) => (
                <Button
                  key={item}
                  role="radio"
                  aria-checked={metal === item}
                  variant="outline"
                  onClick={() => setMetal(item)}
                  className={`h-auto gap-3 rounded-xl py-4 text-base ${
                    metal === item
                      ? item === "gold"
                        ? "border-[var(--dig-amber-500)] bg-[color-mix(in_oklab,var(--dig-amber-400)_14%,var(--card))] font-bold hover:bg-[color-mix(in_oklab,var(--dig-amber-400)_18%,var(--card))]"
                        : "border-[var(--dig-slate-400)] bg-subtle font-bold"
                      : "bg-card"
                  }`}
                >
                  <span aria-hidden className={`size-6 rotate-12 rounded-md ${item === "gold" ? "bg-[linear-gradient(135deg,var(--dig-amber-300),var(--dig-amber-600))]" : "bg-[linear-gradient(135deg,var(--dig-slate-200),var(--dig-slate-500))]"}`} />
                  {item === "gold" ? "طلا" : "نقره"}
                </Button>
              ))}
            </div>

            <div className="flex flex-col gap-4 rounded-xl border border-border p-4">
              <div className="flex items-center justify-between gap-3">
                <span className="flex items-center gap-2 text-sm">
                  <span aria-hidden className="relative flex size-2.5">
                    <span className="absolute inset-0 animate-ping rounded-full bg-destructive motion-reduce:hidden" />
                    <span className="relative size-2.5 rounded-full bg-destructive" />
                  </span>
                  قیمتِ لحظه‌ایِ ۱ میلی‌گرم {name}
                </span>
                <span className="font-black tabular-nums">{formatNumber(price)} ریال</span>
              </div>
              <ToggleGroup type="single" variant="segmented" value={side} onValueChange={(value) => value && setSide(value as string)} aria-label="خرید یا فروش" className="w-full">
                <ToggleGroupItem value="buy" className="flex-1">
                  خرید
                </ToggleGroupItem>
                <ToggleGroupItem value="sell" className="flex-1">
                  فروش
                </ToggleGroupItem>
              </ToggleGroup>
              <TextField
                label={side === "buy" ? "مبلغِ پرداختی" : "مبلغِ دریافتی"}
                inputMode="numeric"
                dir="rtl"
                groupDigits
                endContent="ریال"
                value={rial}
                onValueChange={(value) => {
                  setRial(value)
                  setMg(value ? String(Math.floor(Number(value) / price)) : "")
                }}
              />
              <TextField
                label={`مقدارِ ${name}`}
                inputMode="numeric"
                dir="rtl"
                groupDigits
                endContent="میلی‌گرم"
                value={mg}
                onValueChange={(value) => {
                  setMg(value)
                  setRial(value ? String(Number(value) * price) : "")
                }}
                description="مبلغ یا مقدار را بنویس؛ دیگری خودش حساب می‌شود."
              />
            </div>

            {side === "sell" && !loanDismissed && (
              <div className="flex flex-col gap-3 rounded-xl bg-subtle p-4">
                <span className="flex items-center gap-2 font-bold [&>svg]:size-5 [&>svg]:text-brand-secondary">
                  <InfoIcon aria-hidden />
                  پول نقد بدونِ فروشِ {name}
                </span>
                <p className="text-sm leading-6 text-muted-foreground">با وثیقه‌کردنِ {name}ِ دیجیتالت وام بگیر و دارایی‌ات را نگه دار؛ بازپرداخت در ۴ قسط.</p>
                <div className="flex gap-2">
                  <ControlSizeProvider size="lg" asChild>
                    <Button variant="outline" className="bg-card">
                      درخواستِ وام
                    </Button>
                  </ControlSizeProvider>
                  <ControlSizeProvider size="lg" asChild>
                    <Button variant="ghost" onClick={() => setLoanDismissed(true)}>
                      بعداً
                    </Button>
                  </ControlSizeProvider>
                </div>
              </div>
            )}

            <dl className="flex flex-col gap-2 text-sm">
              <div className="flex justify-between">
                <dt className="text-muted-foreground">کارمزدِ {side === "buy" ? "خرید" : "فروش"} (٪۰٫۵)</dt>
                <dd className="tabular-nums">{formatNumber(fee)} ریال</dd>
              </div>
              <div className="flex justify-between border-t border-border pt-2 text-base">
                <dt className="font-bold">{side === "buy" ? "جمعِ پرداخت" : "واریز به کیف پول"}</dt>
                <dd className="font-black tabular-nums">{formatNumber(side === "buy" ? amount + fee : Math.max(0, amount - fee))} ریال</dd>
              </div>
            </dl>
            <Button disabled={!amount} onClick={() => toast.success(side === "buy" ? `${toFaDigits(mg)} میلی‌گرم ${name} خریده شد.` : `${toFaDigits(mg)} میلی‌گرم ${name} فروخته شد.`)}>
              {amount ? `${side === "buy" ? "خرید" : "فروش"}ِ ${toFaDigits(formatNumber(Number(mg)))} میلی‌گرم ${name}` : "مبلغ یا مقدار را بنویس"}
            </Button>
          </section>

          <div className="flex flex-col gap-6">
            {/* داراییِ من: کارتِ رنگیِ طلایی با پنهان‌کردنِ موجودی. */}
            <section aria-labelledby="assets-title" className="relative isolate flex flex-col gap-4 overflow-hidden rounded-xl bg-[linear-gradient(135deg,var(--dig-amber-500),var(--dig-orange-600))] p-5 text-[var(--dig-white)]">
              <span aria-hidden className="pointer-events-none absolute -end-12 -top-12 -z-10 size-44 rounded-full border-[18px] border-[color-mix(in_oklab,var(--dig-white)_16%,transparent)]" />
              <div className="flex items-center justify-between">
                <h2 id="assets-title" className="font-bold">
                  داراییِ من
                </h2>
                <Button
                  variant="ghost"
                  size="icon"
                  aria-pressed={hidden}
                  aria-label={hidden ? "نمایشِ موجودی" : "پنهان‌کردنِ موجودی"}
                  onClick={() => setHidden((value) => !value)}
                  className="-me-2 text-[var(--dig-white)] hover:bg-[color-mix(in_oklab,var(--dig-white)_18%,var(--dig-orange-600))] hover:text-[var(--dig-white)]"
                >
                  {hidden ? <EyeIcon /> : <EyeOffIcon />}
                </Button>
              </div>
              <dl className="grid grid-cols-2 gap-3">
                {[
                  ["طلا", "۲٬۴۶۰ میلی‌گرم", formatCurrency(Math.round((2460 * PRICE.gold) / 10))],
                  ["نقره", "۱۸٬۰۰۰ میلی‌گرم", formatCurrency(Math.round((18000 * PRICE.silver) / 10))],
                ].map(([label, mgText, toman]) => (
                  <div key={label} className="flex flex-col gap-1 rounded-lg bg-[color-mix(in_oklab,var(--dig-white)_16%,var(--dig-orange-600))] p-3">
                    <dt className="text-sm">{label}</dt>
                    <dd className="flex flex-col">
                      <span className="text-lg font-black tabular-nums">{hidden ? "••••" : mgText}</span>
                      <span className="text-xs tabular-nums">{hidden ? "••••" : `≈ ${toman}`}</span>
                    </dd>
                  </div>
                ))}
              </dl>
            </section>

            <section aria-labelledby="chart-title" className="flex flex-col gap-4 rounded-xl border border-border bg-card p-5">
              <div className="flex items-center justify-between gap-3">
                <h2 id="chart-title" className="font-bold">
                  قیمتِ ۱ میلی‌گرم {name} در ۲۴ ساعت
                </h2>
                <span className="flex items-center gap-1 text-sm font-semibold text-success [&>svg]:size-4">
                  <TrendingUpIcon aria-hidden />
                  <bdi dir="ltr">+٪۲٫۹</bdi>
                </span>
              </div>
              <AreaChart className="h-48" data={CHART} categoryKey="label" smooth showPoints={false} valueAxisFromData series={[{ key: "value", label: "ریال", color: "var(--dig-amber-500)" }]} valueFormatter={(value) => formatNumber(value)} />
              <Button variant="outline">
                <ClockIcon />
                تاریخچهٔ خرید و فروش
              </Button>
            </section>
          </div>
        </div>
      </main>
    </ControlSizeProvider>
  )
}