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