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

سفارشِ داروی بدونِ نسخه

پیش‌نمایش تمام‌صفحه

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

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

این بلاک از ۱۱ آیتم دیگ استفاده می‌کند: buttoncontrol-sizeiconslabelnumber-fieldpersianselectsteppertext-fieldtextareatoast

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/stepper.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/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/stepper.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/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/stepper.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/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/stepper.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 بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

pharmacy-order.tsx
"use client"

import * as React from "react"

import { PlusIcon, TrashIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Label } from "@/components/ui/label"
import { NumberField } from "@/components/ui/number-field"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Stepper, StepperItem, StepperTitle } from "@/components/ui/stepper"
import { TextField } from "@/components/ui/text-field"
import { Textarea } from "@/components/ui/textarea"
import { toast } from "@/components/ui/toast"
import { toFaDigits } from "@/lib/persian"

const FORMS = [
  { value: "tablet-strip", label: "قرص · ورق", unit: "ورق" },
  { value: "tablet-box", label: "قرص · بسته", unit: "بسته" },
  { value: "capsule", label: "کپسول", unit: "ورق" },
  { value: "syrup", label: "شربت", unit: "شیشه" },
  { value: "ointment", label: "پماد و کرم", unit: "عدد" },
  { value: "drops", label: "قطره", unit: "عدد" },
]
type Drug = { id: number; name: string; form: string; quantity: number }

/**
 * فرمِ سفارشِ داروی بدونِ نسخه: مراحلِ سفارش بالای صفحه، برای هر دارو کارتی با نام، شکلِ دارو و تعداد
 * (واحدِ تعداد از شکلِ دارو می‌آید: ورق، شیشه، عدد)، حذفِ برگشت‌پذیر، «افزودنِ داروی دیگر» و یادداشت برای
 * داروخانه. خطای «نامِ دارو» بعد از ترکِ فیلد یا ارسال کنارِ همان فیلد است. دکمهٔ ادامه در موبایل پایینِ صفحه.
 */
export default function PharmacyOrderBlock() {
  const nextId = React.useRef(2)
  const [drugs, setDrugs] = React.useState<Drug[]>([{ id: 1, name: "استامینوفن ۵۰۰", form: "tablet-strip", quantity: 2 }])
  const [touched, setTouched] = React.useState<number[]>([])
  const [submitted, setSubmitted] = React.useState(false)

  const update = (id: number, patch: Partial<Drug>) => setDrugs((list) => list.map((item) => (item.id === id ? { ...item, ...patch } : item)))
  const remove = (drug: Drug) => {
    const index = drugs.findIndex((item) => item.id === drug.id)
    setDrugs((list) => list.filter((item) => item.id !== drug.id))
    toast(`«${drug.name || "دارو"}» برداشته شد.`, {
      action: { label: "برگردان", onClick: () => setDrugs((list) => [...list.slice(0, index), drug, ...list.slice(index)]) },
    })
  }
  const add = () => {
    const id = nextId.current++
    setDrugs((list) => [...list, { id, name: "", form: "tablet-strip", quantity: 1 }])
    focusNext.current = id
  }
  // فوکوس روی نامِ داروی تازه، بعد از این‌که کارتش رندر شد، تا کاربر بی‌کلیک ادامه دهد.
  const focusNext = React.useRef<number | null>(null)
  React.useEffect(() => {
    if (focusNext.current === null) return
    document.getElementById(`drug-name-${focusNext.current}`)?.focus()
    focusNext.current = null
  }, [drugs])
  const invalid = (drug: Drug) => !drug.name.trim() && (submitted || touched.includes(drug.id))

  return (
    <ControlSizeProvider size="xl" asChild>
      <form
        className="mx-auto flex w-full max-w-2xl flex-col"
        noValidate
        onSubmit={(event) => {
          event.preventDefault()
          setSubmitted(true)
          const empty = drugs.find((item) => !item.name.trim())
          if (empty) {
            document.getElementById(`drug-name-${empty.id}`)?.focus()
            return
          }
          toast.success("سفارش ثبت شد؛ داروخانه تا ۱۵ دقیقهٔ دیگر قیمت را اعلام می‌کند.")
        }}
      >
        <div className="flex flex-col gap-6 p-4 sm:p-6">
          <header className="flex flex-col gap-5">
            <h1 className="text-2xl leading-snug font-bold">سفارش از داروخانه</h1>
            <Stepper value={0}>
              <StepperItem>
                <StepperTitle>ثبتِ سفارش</StepperTitle>
              </StepperItem>
              <StepperItem>
                <StepperTitle>اعلامِ قیمت</StepperTitle>
              </StepperItem>
              <StepperItem>
                <StepperTitle>پرداخت</StepperTitle>
              </StepperItem>
              <StepperItem>
                <StepperTitle>تحویل</StepperTitle>
              </StepperItem>
            </Stepper>
          </header>

          <section aria-labelledby="drugs-title" className="flex flex-col gap-3">
            <div className="flex items-baseline justify-between gap-3">
              <h2 id="drugs-title" className="text-lg leading-snug font-bold">
                داروهای بدونِ نسخه
              </h2>
              <span className="text-sm text-muted-foreground">{toFaDigits(drugs.length)} قلم</span>
            </div>

            <ol className="flex flex-col gap-3">
              {drugs.map((drug, index) => {
                const form = FORMS.find((item) => item.value === drug.form) ?? FORMS[0]
                return (
                  <li key={drug.id} className="flex flex-col gap-4 rounded-xl border border-border bg-card p-4">
                    <div className="flex items-center justify-between gap-3">
                      <span className="text-sm font-semibold text-muted-foreground">داروی {toFaDigits(index + 1)}</span>
                      {drugs.length > 1 && (
                        <ControlSizeProvider size="default" asChild>
                          <Button type="button" variant="ghost" size="icon" aria-label={`حذفِ ${drug.name || `داروی ${toFaDigits(index + 1)}`}`} onClick={() => remove(drug)} className="-me-2 text-muted-foreground hover:text-destructive">
                            <TrashIcon />
                          </Button>
                        </ControlSizeProvider>
                      )}
                    </div>
                    <TextField
                      id={`drug-name-${drug.id}`}
                      label="نامِ دارو"
                      placeholder="مثلاً استامینوفن ۵۰۰"
                      value={drug.name}
                      onValueChange={(name) => update(drug.id, { name })}
                      onBlur={() => setTouched((list) => (list.includes(drug.id) ? list : [...list, drug.id]))}
                      invalid={invalid(drug)}
                      errorMessage="نامِ دارو را بنویس؛ اگر مطمئن نیستی، عکسِ جعبه را در یادداشت توضیح بده."
                      autoComplete="off"
                    />
                    <div className="grid grid-cols-1 gap-3 sm:grid-cols-[minmax(0,1fr)_11rem]">
                      <div className="flex flex-col gap-2">
                        <Label id={`drug-form-${drug.id}`}>شکلِ دارو</Label>
                        <Select value={drug.form} onValueChange={(value) => update(drug.id, { form: value })}>
                          <SelectTrigger aria-labelledby={`drug-form-${drug.id}`} className="w-full">
                            <SelectValue />
                          </SelectTrigger>
                          <ControlSizeProvider size="xl" asChild>
                            <SelectContent>
                              {FORMS.map((item) => (
                                <SelectItem key={item.value} value={item.value}>
                                  {item.label}
                                </SelectItem>
                              ))}
                            </SelectContent>
                          </ControlSizeProvider>
                        </Select>
                      </div>
                      <div className="flex flex-col gap-2">
                        <Label id={`drug-qty-${drug.id}`}>تعداد ({form.unit})</Label>
                        <NumberField aria-labelledby={`drug-qty-${drug.id}`} value={drug.quantity} onValueChange={(quantity) => update(drug.id, { quantity })} min={1} max={10} className="w-full" />
                      </div>
                    </div>
                  </li>
                )
              })}
            </ol>

            <Button type="button" variant="outline" onClick={add} className="border-dashed">
              <PlusIcon />
              افزودنِ داروی دیگر
            </Button>
          </section>

          <section className="flex flex-col gap-2">
            <Label htmlFor="pharmacy-note">یادداشت برای داروخانه</Label>
            <Textarea id="pharmacy-note" rows={3} placeholder="مثلاً اگر این برند نبود، مشابهِ ایرانی بفرستید." />
            <p className="text-xs text-muted-foreground">داروساز پیش از اعلامِ قیمت می‌خواندش.</p>
          </section>
        </div>

        {/* دکمهٔ اصلی در دسترسِ شست؛ در دسکتاپ زیرِ فرم و هم‌عرضِ آن. */}
        <div className="sticky bottom-0 border-t border-border bg-background p-4 sm:static sm:border-0 sm:px-6 sm:pt-2 sm:pb-6">
          <Button type="submit" fullWidth>
            تأیید و ادامهٔ سفارش
          </Button>
        </div>
      </form>
    </ControlSizeProvider>
  )
}