سبدِ کشویی
پیشنمایش تمامصفحهسبدِ خریدی که با «افزودن به سبد» از لبهٔ صفحه باز میشود: اقلام با عکس و تعداد، نوارِ تا ارسالِ رایگان، حذف با برگرداندن، جمع و دکمهٔ تسویهٔ چسبان در پایینِ کشو.
۱. نصب کامپوننتها
این بلاک از ۱۰ آیتم دیگ استفاده میکند: badgebuttoncontrol-sizeempty-stateiconsnumber-fieldpersianprogresssheettoast
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/sheet.json https://docs.digdesign.ir/r/toast.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/sheet.json https://docs.digdesign.ir/r/toast.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/sheet.json https://docs.digdesign.ir/r/toast.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/sheet.json https://docs.digdesign.ir/r/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
cart-drawer.tsx
"use client"
import * as React from "react"
import { ShoppingCartIcon, TrashIcon, TruckIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { EmptyState, EmptyStateDescription, EmptyStateTitle } from "@/components/ui/empty-state"
import { NumberField } from "@/components/ui/number-field"
import { Progress } from "@/components/ui/progress"
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import { toast } from "@/components/ui/toast"
import { formatCurrency, toFaDigits } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"
type Item = { id: string; title: string; variant: string; image: StockImageName; price: number; quantity: number }
const INITIAL: Item[] = [
{ id: "1", title: "گلدانِ سرامیکی دوقلو", variant: "کرم", image: "productVase", price: 1_290_000, quantity: 1 },
{ id: "2", title: "دفترِ یادداشتِ جلدسخت", variant: "A5", image: "coverNotebook", price: 180_000, quantity: 2 },
{ id: "3", title: "ست لوازمالتحریرِ رومیزی", variant: "طوسی", image: "coverOfficeSupplies", price: 640_000, quantity: 1 },
]
const FREE_SHIPPING = 3_000_000
/**
* سبدِ کشویی: با «افزودن به سبد» از لبهٔ پایانی باز میشود و کاربر از صفحه بیرون
* نمیرود. اقلام با عکس و تعداد، نوارِ «تا ارسالِ رایگان»، جمع و دکمهٔ تسویه که همیشه
* پایینِ کشو میماند؛ حذف با امکانِ برگرداندن.
*/
export default function CartDrawerBlock() {
const [open, setOpen] = React.useState(true)
const [items, setItems] = React.useState(INITIAL)
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
const count = items.reduce((sum, item) => sum + item.quantity, 0)
const remaining = Math.max(0, FREE_SHIPPING - subtotal)
function remove(item: Item) {
setItems((list) => list.filter((entry) => entry.id !== item.id))
toast(`«${item.title}» از سبد حذف شد.`, {
action: { label: "برگردان", onClick: () => setItems((list) => (list.some((entry) => entry.id === item.id) ? list : [...list, item])) },
})
}
return (
<ControlSizeProvider size="xl" asChild>
<div className="mx-auto flex min-h-svh w-full max-w-5xl flex-col gap-8 p-4 sm:p-6">
<header className="flex items-center justify-between gap-4">
<span className="flex items-center gap-2 text-lg font-black">
<span aria-hidden className="size-3 rounded-sm bg-[var(--dig-orange-500)]" />
خانهٔ سفال
</span>
<Button variant="outline" onClick={() => setOpen(true)} aria-label={`سبد خرید، ${toFaDigits(count)} کالا`}>
<ShoppingCartIcon />
سبد
{count > 0 && <Badge color="primary">{toFaDigits(count)}</Badge>}
</Button>
</header>
<main className="grid grid-cols-1 items-center gap-8 md:grid-cols-2">
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img src={stockImages.productVase.src} alt={stockImages.productVase.alt} className="aspect-square w-full rounded-xl object-cover" />
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-bold">گلدانِ سرامیکی دوقلو</h1>
<span className="text-2xl font-black tabular-nums">{formatCurrency(1_290_000)}</span>
</div>
<p className="leading-8 text-muted-foreground">دستسازِ کارگاهِ لالجین، لعابِ مات و مناسبِ گلِ طبیعی و خشک.</p>
<Button
className="mt-2 self-start"
onClick={() => {
setItems((list) => list.map((item) => (item.id === "1" ? { ...item, quantity: Math.min(item.quantity + 1, 10) } : item)))
setOpen(true)
}}
>
<ShoppingCartIcon />
افزودن به سبد
</Button>
</div>
</main>
<Sheet open={open} onOpenChange={setOpen}>
<ControlSizeProvider size="xl" asChild>
<SheetContent side="end" className="flex w-full max-w-md flex-col gap-0 p-0">
<SheetHeader className="gap-4 border-b border-border p-6">
<SheetTitle>سبد خرید ({toFaDigits(count)})</SheetTitle>
<SheetDescription className="sr-only">اقلامِ سبد و جمعِ مبلغ</SheetDescription>
{items.length > 0 && (
<div className="flex flex-col gap-2 rounded-lg bg-subtle p-4">
<span className="flex items-center gap-2 text-sm">
<TruckIcon aria-hidden className="size-4 text-brand-secondary" />
{remaining > 0 ? <>{formatCurrency(remaining)} تا ارسالِ رایگان</> : "ارسالِ این سفارش رایگان است"}
</span>
<Progress value={Math.min(100, (subtotal / FREE_SHIPPING) * 100)} aria-label="پیشرفت تا ارسالِ رایگان" />
</div>
)}
</SheetHeader>
{items.length === 0 ? (
<EmptyState className="flex-1">
<ShoppingCartIcon aria-hidden className="size-10 text-placeholder" />
<EmptyStateTitle>سبد خالی است</EmptyStateTitle>
<EmptyStateDescription>هر چه اضافه کنید اینجا میآید.</EmptyStateDescription>
</EmptyState>
) : (
<ul className="flex flex-1 flex-col divide-y divide-border overflow-y-auto px-6">
{items.map((item) => (
<li key={item.id} className="flex gap-4 py-5">
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img src={stockImages[item.image].src} alt={stockImages[item.image].alt} className="size-20 shrink-0 rounded-lg object-cover" />
<div className="flex flex-1 flex-col gap-3">
<div className="flex items-start justify-between gap-2">
<span className="flex flex-col gap-0.5">
<span className="font-medium">{item.title}</span>
<span className="text-sm text-muted-foreground">{item.variant}</span>
</span>
<Button variant="ghost" size="icon" aria-label={`حذفِ ${item.title}`} onClick={() => remove(item)}>
<TrashIcon />
</Button>
</div>
<div className="flex items-center justify-between gap-2">
<NumberField
aria-label={`تعداد ${item.title}`}
value={item.quantity}
onValueChange={(quantity) => setItems((list) => list.map((entry) => (entry.id === item.id ? { ...entry, quantity } : entry)))}
min={1}
max={10}
className="w-32"
/>
<span className="font-bold tabular-nums">{formatCurrency(item.price * item.quantity)}</span>
</div>
</div>
</li>
))}
</ul>
)}
{items.length > 0 && (
<SheetFooter className="gap-5 border-t border-border p-6">
<div className="flex items-baseline justify-between gap-2">
<span className="text-muted-foreground">جمعِ سبد</span>
<span className="text-xl font-black tabular-nums">{formatCurrency(subtotal)}</span>
</div>
<div className="flex flex-col gap-2">
<Button fullWidth>ادامهٔ خرید و تسویه</Button>
<Button variant="ghost" fullWidth onClick={() => setOpen(false)}>
ادامهٔ گشتوگذار
</Button>
</div>
</SheetFooter>
)}
</SheetContent>
</ControlSizeProvider>
</Sheet>
</div>
</ControlSizeProvider>
)
}