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