آدرس ارسال
پیشنمایش تمامصفحهفرم آدرس با استان و شهر، نشانی، پلاک و واحد، کد پستی و مشخصات گیرنده.
۱. نصب کامپوننتها
این بلاک از ۱۴ آیتم دیگ استفاده میکند: autocompletebadgebuttoncardcheckboxcontrol-sizeiconsiran-locationslabelpersianradio-grouptext-fieldtextareatoast
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/autocomplete.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/iran-locations.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/textarea.json https://docs.digdesign.ir/r/toast.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/autocomplete.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/iran-locations.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/textarea.json https://docs.digdesign.ir/r/toast.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/autocomplete.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/iran-locations.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/textarea.json https://docs.digdesign.ir/r/toast.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/autocomplete.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/iran-locations.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/textarea.json https://docs.digdesign.ir/r/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
shipping-address.tsx
"use client"
import * as React from "react"
import { EditIcon, PlusIcon } from "@/components/icons"
import { Autocomplete } from "@/components/ui/autocomplete"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { TextField } from "@/components/ui/text-field"
import { Textarea } from "@/components/ui/textarea"
import { toast } from "@/components/ui/toast"
import { getCities, IRAN_PROVINCES } from "@/lib/iran-locations"
import { isValidMobile, toFaDigits } from "@/lib/persian"
// همهٔ استانها و شهرها؛ Autocomplete جستوجو را خودش انجام میدهد.
const PROVINCE_ITEMS = IRAN_PROVINCES.map((province) => ({ value: province.name, label: province.name }))
const SAVED = [
{ id: "home", title: "خانه", address: "تهران، خیابان ولیعصر، کوچهٔ نسترن، پلاک ۱۲، واحد ۴", receiver: "مهسا کریمی", mobile: "09121234567", isDefault: true },
{ id: "work", title: "محلِ کار", address: "تهران، بزرگراه مدرس، خیابان قبادیان، پلاک ۸، طبقهٔ ۳", receiver: "مهسا کریمی", mobile: "09121234567" },
]
const optionCard =
"flex items-start gap-3 rounded-xl border border-border p-4 transition-colors has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary-subtle"
/**
* نشانی تحویل: انتخاب از نشانیهای ذخیرهشده با کارتهای رادیویی (ویرایشِ هر کدام)،
* و فرمِ نشانی تازه با تأییدِ موقعیت روی نقشه. فرمِ تازه با «افزودن» باز میشود.
*/
export default function ShippingAddressBlock() {
const [selected, setSelected] = React.useState("home")
const [adding, setAdding] = React.useState(false)
const [province, setProvince] = React.useState("تهران")
const [city, setCity] = React.useState("تهران")
const cityItems = React.useMemo(() => getCities(province).map((name) => ({ value: name, label: name })), [province])
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-3xl flex-col gap-8 p-4 sm:p-6">
<header className="flex flex-wrap items-end justify-between gap-x-4 gap-y-8">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold">نشانی تحویل</h1>
<p className="text-muted-foreground">سفارش به نشانی انتخابشده فرستاده میشود.</p>
</div>
{!adding && (
<Button variant="outline" onClick={() => setAdding(true)}>
<PlusIcon />
افزودنِ نشانی
</Button>
)}
</header>
<RadioGroup value={selected} onValueChange={setSelected} aria-label="نشانیهای ذخیرهشده" className="gap-3">
{SAVED.map((item) => (
<div key={item.id} className={optionCard}>
<RadioGroupItem id={`addr-${item.id}`} value={item.id} className="mt-1" />
<Label htmlFor={`addr-${item.id}`} className="flex flex-1 flex-col items-start gap-1.5">
<span className="flex items-center gap-2 font-semibold">
{item.title}
{item.isDefault && <Badge variant="soft">پیشفرض</Badge>}
</span>
<span className="text-sm leading-6 font-normal text-muted-foreground">{item.address}</span>
<span className="text-sm font-normal text-muted-foreground">
{item.receiver} · <bdi dir="ltr">{toFaDigits(item.mobile)}</bdi>
</span>
</Label>
<Button variant="ghost" size="icon" aria-label={`ویرایشِ نشانی ${item.title}`}>
<EditIcon />
</Button>
</div>
))}
</RadioGroup>
{adding && (
<form
onSubmit={(event) => {
event.preventDefault()
setAdding(false)
toast.success("نشانی ذخیره شد.")
}}
>
<Card>
<CardHeader>
<CardTitle className="text-base">نشانی تازه</CardTitle>
<CardDescription>اول موقعیت را روی نقشه تأیید کنید؛ بعد جزئیات.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-6">
<div className="relative overflow-hidden rounded-lg border border-border">
{/* eslint-disable-next-line @next/next/no-img-element -- نقشهٔ تزئینی؛ در محصول نقشهٔ واقعی میآید */}
<img src="/blocks/contact-map.svg" alt="نقشهٔ محدودهٔ نشانی" className="aspect-[21/9] w-full object-cover" />
<Button type="button" variant="secondary" className="absolute start-3 bottom-3" onClick={() => toast.success("موقعیت تأیید شد.")}>
تأییدِ موقعیت
</Button>
</div>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div className="flex flex-col gap-2">
<Label htmlFor="province">استان</Label>
<Autocomplete
id="province"
items={PROVINCE_ITEMS}
value={province}
onValueChange={(value) => {
setProvince(value)
// شهرِ قبلی مالِ استانِ دیگری است؛ کاربر باید از نو انتخاب کند.
setCity("")
}}
placeholder="انتخاب استان"
emptyText="استانی پیدا نشد"
className="w-full"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="city">شهر</Label>
<Autocomplete
id="city"
items={cityItems}
value={city}
onValueChange={setCity}
placeholder={province ? "انتخاب شهر" : "اول استان را انتخاب کنید"}
emptyText="شهری پیدا نشد"
disabled={!province}
className="w-full"
/>
</div>
<div className="flex flex-col gap-2 sm:col-span-2">
<Label htmlFor="address">نشانی</Label>
<Textarea id="address" name="address" rows={3} autoComplete="street-address" placeholder="خیابان، کوچه" required />
</div>
<TextField label="پلاک" name="plaque" inputMode="numeric" dir="ltr" required />
<TextField label="واحد" name="unit" inputMode="numeric" dir="ltr" optional />
<TextField label="کد پستی" name="postalCode" inputMode="numeric" dir="ltr" maxLength={10} description="۱۰ رقم، بدون خط تیره" required className="sm:col-span-2" />
</div>
<div className="grid grid-cols-1 gap-5 border-t border-border pt-6 sm:grid-cols-2">
<TextField label="نام گیرنده" name="receiver" autoComplete="name" required />
<TextField
label="شماره موبایل گیرنده"
name="receiverMobile"
type="tel"
dir="ltr"
maxLength={11}
autoComplete="tel"
required
validate={(value) => (isValidMobile(value) ? undefined : "شماره موبایل باید با ۰۹ شروع شود و ۱۱ رقم باشد.")}
/>
<div className="flex items-center gap-2 sm:col-span-2">
<Checkbox id="default-address" name="default" />
<Label htmlFor="default-address">نشانی پیشفرض من باشد</Label>
</div>
</div>
<div className="flex justify-end gap-3 pt-4">
<Button type="button" variant="outline" onClick={() => setAdding(false)}>
انصراف
</Button>
<Button type="submit">ذخیرهٔ نشانی</Button>
</div>
</CardContent>
</Card>
</form>
)}
{!adding && (
<div className="flex justify-end border-t border-border pt-8">
<Button onClick={() => toast.success("نشانی برای این سفارش ثبت شد.")}>ارسال به این نشانی</Button>
</div>
)}
</main>
</ControlSizeProvider>
)
}