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

فرم آدرس با استان و شهر، نشانی، پلاک و واحد، کد پستی و مشخصات گیرنده.

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

این بلاک از ۱۴ آیتم دیگ استفاده می‌کند: 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.json
pnpm 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.json
yarn 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.json
bunx --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>
  )
}