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

مرجوعی در سه قدم: انتخابِ اقلام با عکس، دلیل (و عکسِ آسیب در صورتِ نیاز) و روشِ برگشت؛ مبلغِ بازگشتی در ستونِ کنار و پایان با کدِ پیگیری.

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

این بلاک از ۱۱ آیتم دیگ استفاده می‌کند: buttoncheckboxcontrol-sizefile-uploadiconslabelpersianradio-groupselectsteppertextarea

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.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/select.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/textarea.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.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/select.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/textarea.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.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/select.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/textarea.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.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/select.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/textarea.json

۲. کد صفحه

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

returns.tsx
"use client"

import * as React from "react"

import { CheckIcon, TruckIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { FileUpload } from "@/components/ui/file-upload"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Stepper, StepperItem, StepperTitle } from "@/components/ui/stepper"
import { Textarea } from "@/components/ui/textarea"
import { formatCurrency, toFaDigits } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"

const ITEMS: { id: string; title: string; variant: string; image: StockImageName; price: number }[] = [
  { id: "1", title: "گلدانِ سرامیکی دوقلو", variant: "کرم", image: "productVase", price: 1_290_000 },
  { id: "2", title: "دفترِ یادداشتِ جلدسخت", variant: "A5", image: "coverNotebook", price: 180_000 },
]
const REASONS = ["آسیب‌دیده رسید", "با توضیحات فرق داشت", "اندازه یا رنگ اشتباه", "دیگر لازم ندارم"]
const METHODS = [
  { value: "courier", title: "پیک از درِ خانه", note: "فردا ۹ تا ۱۳، رایگان", icon: <TruckIcon /> },
  { value: "post", title: "تحویل به پست", note: "کدِ مرسوله پیامک می‌شود", icon: <CheckIcon /> },
]
const STEPS = ["انتخابِ کالا", "دلیل", "روشِ برگشت"]

const optionCard =
  "flex items-center 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 ReturnsBlock() {
  const [step, setStep] = React.useState(0)
  const [selected, setSelected] = React.useState<string[]>(["1"])
  const [reason, setReason] = React.useState(REASONS[0])
  const [method, setMethod] = React.useState("courier")
  const [done, setDone] = React.useState(false)
  const refund = ITEMS.filter((item) => selected.includes(item.id)).reduce((sum, item) => sum + item.price, 0)

  if (done) {
    return (
      <ControlSizeProvider size="xl" asChild>
        <main className="mx-auto flex w-full max-w-md flex-col items-center gap-4 p-6 py-16 text-center" role="status">
          <span aria-hidden className="flex size-16 items-center justify-center rounded-full bg-success-subtle text-success [&>svg]:size-8">
            <CheckIcon />
          </span>
          <h1 className="text-2xl font-bold">درخواستِ مرجوعی ثبت شد</h1>
          <p className="leading-7 text-muted-foreground">
            کدِ پیگیری <bdi dir="ltr" className="font-semibold text-foreground">{toFaDigits("R-48120")}</bdi>. بعد از بررسی کالا، {formatCurrency(refund)} به کیف پولت برمی‌گردد.
          </p>
          <Button variant="outline" className="mt-4" onClick={() => setDone(false)}>
            بازگشت به سفارش‌ها
          </Button>
        </main>
      </ControlSizeProvider>
    )
  }

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto grid grid-cols-1 w-full max-w-5xl items-start gap-8 p-4 sm:p-6 lg:grid-cols-[minmax(0,1fr)_18rem]">
        <form
          className="flex flex-col gap-8 rounded-xl border border-border bg-card p-6 sm:p-8"
          onSubmit={(event) => {
            event.preventDefault()
            if (step < STEPS.length - 1) setStep((value) => value + 1)
            else setDone(true)
          }}
        >
          <div className="flex flex-col gap-1">
            <h1 className="text-2xl font-bold">مرجوعی کالا</h1>
            <p className="text-muted-foreground">سفارشِ {toFaDigits("8821042")} · تا ۷ روز بعد از تحویل</p>
          </div>
          <Stepper value={step}>
            {STEPS.map((title) => (
              <StepperItem key={title}>
                <StepperTitle>{title}</StepperTitle>
              </StepperItem>
            ))}
          </Stepper>

          {step === 0 && (
            <ul className="flex flex-col gap-3" aria-label="اقلامِ سفارش">
              {ITEMS.map((item) => (
                <li key={item.id} className={optionCard}>
                  <Checkbox
                    id={`ret-${item.id}`}
                    checked={selected.includes(item.id)}
                    onCheckedChange={(checked) => setSelected((list) => (checked ? [...list, item.id] : list.filter((id) => id !== item.id)))}
                  />
                  {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
                  <img src={stockImages[item.image].src} alt="" className="size-16 rounded-lg object-cover" />
                  {/* در موبایل قیمت زیرِ نام می‌آید تا نام در سه خط نشکند. */}
                  <Label htmlFor={`ret-${item.id}`} className="flex min-w-0 flex-1 flex-col items-start gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
                    <span className="flex flex-col gap-1">
                      <span className="font-semibold">{item.title}</span>
                      <span className="text-sm font-normal text-muted-foreground">{item.variant}</span>
                    </span>
                    <span className="font-bold tabular-nums">{formatCurrency(item.price)}</span>
                  </Label>
                </li>
              ))}
            </ul>
          )}

          {step === 1 && (
            <div className="flex flex-col gap-5">
              <div className="flex flex-col gap-2">
                <Label id="ret-reason">دلیلِ مرجوعی</Label>
                <Select value={reason} onValueChange={(value) => setReason(value as string)}>
                  <SelectTrigger aria-labelledby="ret-reason" className="w-full">
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    {REASONS.map((item) => (
                      <SelectItem key={item} value={item}>
                        {item}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>
              <div className="flex flex-col gap-2">
                <Label htmlFor="ret-note">توضیح</Label>
                <Textarea id="ret-note" rows={3} placeholder="هر چه دقیق‌تر بنویسید، سریع‌تر بررسی می‌شود." />
              </div>
              {reason === REASONS[0] && <FileUpload name="photos" multiple maxFiles={3} accept="image/*" title="عکسِ آسیب" hint="تا ۳ عکس، هر کدام تا ۵ مگابایت" />}
            </div>
          )}

          {step === 2 && (
            <RadioGroup value={method} onValueChange={setMethod} aria-label="روشِ برگشت" className="gap-3">
              {METHODS.map((item) => (
                <div key={item.value} className={optionCard}>
                  <RadioGroupItem id={`method-${item.value}`} value={item.value} />
                  <span aria-hidden className="flex size-10 items-center justify-center rounded-lg bg-subtle [&>svg]:size-5">
                    {item.icon}
                  </span>
                  <Label htmlFor={`method-${item.value}`} className="flex flex-1 flex-col items-start gap-1">
                    <span className="font-semibold">{item.title}</span>
                    <span className="text-sm font-normal text-muted-foreground">{item.note}</span>
                  </Label>
                </div>
              ))}
            </RadioGroup>
          )}

          <div className="flex justify-between gap-3 border-t border-border pt-8">
            <Button type="button" variant="outline" disabled={step === 0} onClick={() => setStep((value) => value - 1)}>
              قبلی
            </Button>
            <Button type="submit" disabled={selected.length === 0}>
              {step === STEPS.length - 1 ? "ثبتِ درخواست" : "ادامه"}
            </Button>
          </div>
        </form>

        <aside className="flex flex-col gap-3 rounded-xl bg-subtle p-6 lg:sticky lg:top-6">
          <span className="text-sm text-muted-foreground">مبلغِ بازگشتی</span>
          <span className="text-3xl font-black tabular-nums">{formatCurrency(refund)}</span>
          <span className="text-sm leading-6 text-muted-foreground">
            {toFaDigits(selected.length)} کالا · بعد از بررسی، ظرفِ ۲۴ ساعت به کیف پول برمی‌گردد.
          </span>
        </aside>
      </main>
    </ControlSizeProvider>
  )
}