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

پنلِ حسابِ مشتری: انتخاب از نقشه و نشانیِ تازه، کارتِ هر نشانی با کدِ پستی و گیرنده، نشانیِ پیش‌فرضِ برجسته، و منوی پیش‌فرض‌کردن، ویرایش و حذفِ برگشت‌پذیر.

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

این بلاک از ۷ آیتم دیگ استفاده می‌کند: badgebuttoncontrol-sizedropdown-menuiconspersiantoast

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.json

۲. کد صفحه

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

account-addresses.tsx
"use client"

import * as React from "react"

import { ChatIcon, ChevronLeftIcon, DashboardIcon, EditIcon, HeartIcon, LocationIcon, LogoutIcon, MoreHorizontalIcon, PlusIcon, ShopIcon, TrashIcon, UserIcon, WalletIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { toast } from "@/components/ui/toast"
import { formatCurrency, toFaDigits } from "@/lib/persian"

const NAV = [
  { id: "overview", label: "خلاصهٔ فعالیت‌ها", icon: <DashboardIcon /> },
  { id: "orders", label: "سفارش‌ها", icon: <ShopIcon /> },
  { 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>
  )
}

type Address = { id: string; title: string; text: string; postal: string; receiver: string; phone: string }
const INITIAL: Address[] = [
  { id: "a1", title: "خانه", text: "اهواز، کیانپارس، خیابانِ ۱۴ غربی، کوچهٔ نرگس، پلاکِ ۳۲، واحدِ ۴", postal: "6155689134", receiver: "آرمیتا رستگار", phone: "09123456789" },
  { id: "a2", title: "محلِ کار", text: "تهران، خیابانِ ولیعصر، بالاتر از پارکِ ساعی، برجِ نگار، طبقهٔ ۷", postal: "1966734155", receiver: "آرمیتا رستگار", phone: "09123456789" },
  { id: "a3", title: "خانهٔ مادر", text: "شیراز، بلوارِ چمران، کوچهٔ ۲۱، پلاکِ ۹", postal: "7194813411", receiver: "مریم رستگار", phone: "09171234567" },
]

/**
 * نشانی‌ها در پنلِ حسابِ مشتری: «انتخاب از روی نقشه» و «نشانیِ تازه»، کارتِ هر نشانی با عنوان، متن،
 * کدِ پستی و گیرنده؛ نشانیِ پیش‌فرض با حاشیه و برچسب مشخص است و از منوی هر کارت می‌شود پیش‌فرضش کرد،
 * ویرایش کرد یا (با امکانِ برگرداندن) حذف کرد.
 */
export default function AccountAddressesBlock() {
  const [items, setItems] = React.useState(INITIAL)
  const [primary, setPrimary] = React.useState("a1")

  function remove(item: Address) {
    setItems((list) => list.filter((entry) => entry.id !== item.id))
    toast(`نشانیِ «${item.title}» حذف شد.`, { action: { label: "برگردان", onClick: () => setItems((list) => [...list, item]) } })
  }

  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="addresses" />
        </div>

        <section aria-labelledby="addresses-title" className="flex min-w-0 flex-col gap-6 rounded-xl border border-border bg-card p-5">
          <header className="flex flex-wrap items-center justify-between gap-3">
            <h1 id="addresses-title" className="text-xl font-black">
              نشانی‌ها
            </h1>
            <Button variant="ghost" className="text-brand-secondary">
              <PlusIcon />
              نشانیِ تازه
            </Button>
          </header>

          <Button variant="outline" className="h-auto justify-start gap-3 rounded-xl border-dashed py-5 bg-subtle text-base hover:bg-muted">
            <LocationIcon className="size-5" />
            انتخابِ موقعیت از روی نقشه
          </Button>

          <ul className="flex flex-col gap-3">
            {items.map((item) => {
              const isPrimary = item.id === primary
              return (
                <li key={item.id} className={`flex gap-3 rounded-xl border p-4 ${isPrimary ? "border-primary bg-primary-subtle" : "border-border"}`}>
                  <LocationIcon aria-hidden className={`mt-0.5 size-5 shrink-0 ${isPrimary ? "text-primary" : "text-muted-foreground"}`} />
                  <div className="flex min-w-0 flex-1 flex-col gap-1.5">
                    <span className="flex items-center gap-2">
                      <span className="font-bold">{item.title}</span>
                      {isPrimary && (
                        <Badge variant="soft" color="primary">
                          پیش‌فرض
                        </Badge>
                      )}
                    </span>
                    <span className="text-sm leading-6">{item.text}</span>
                    <span className="text-sm text-muted-foreground">
                      کدِ پستی <bdi className="tabular-nums">{toFaDigits(item.postal)}</bdi> · گیرنده {item.receiver} · <bdi dir="ltr" className="tabular-nums">{toFaDigits(item.phone)}</bdi>
                    </span>
                  </div>
                  <DropdownMenu>
                    <DropdownMenuTrigger asChild>
                      <Button variant="ghost" size="icon" aria-label={`گزینه‌های نشانیِ ${item.title}`} className="-me-2 -mt-2 shrink-0">
                        <MoreHorizontalIcon />
                      </Button>
                    </DropdownMenuTrigger>
                    <ControlSizeProvider size="xl" asChild>
                      <DropdownMenuContent align="end">
                        {!isPrimary && <DropdownMenuItem onSelect={() => setPrimary(item.id)}>نشانیِ پیش‌فرض شود</DropdownMenuItem>}
                        <DropdownMenuItem>
                          <EditIcon />
                          ویرایش
                        </DropdownMenuItem>
                        <DropdownMenuSeparator />
                        <DropdownMenuItem variant="destructive" disabled={isPrimary} onSelect={() => remove(item)}>
                          <TrashIcon />
                          {isPrimary ? "پیش‌فرض حذف نمی‌شود" : "حذف"}
                        </DropdownMenuItem>
                      </DropdownMenuContent>
                    </ControlSizeProvider>
                  </DropdownMenu>
                </li>
              )
            })}
          </ul>
        </section>
      </main>
    </ControlSizeProvider>
  )
}