سبد خرید
پیشنمایش تمامصفحهاقلام سبد با تغییر تعداد و حذف، کد تخفیف و خلاصهٔ مبلغ.
۱. نصب کامپوننتها
این بلاک از ۱۰ آیتم دیگ استفاده میکند: buttoncontrol-sizeempty-stateiconsnumber-fieldpersianproduct-cardprogresstext-fieldtoast
پکیجمنیجر پیشفرض 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/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/product-card.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/text-field.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/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/product-card.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/text-field.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/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/product-card.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/text-field.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/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/product-card.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
cart.tsx
"use client"
import * as React from "react"
import { CheckCircleIcon, ShieldCheckIcon, ShoppingCartIcon, TrashIcon, TruckIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import {
EmptyState,
EmptyStateActions,
EmptyStateDescription,
EmptyStateTitle,
} from "@/components/ui/empty-state"
import { NumberField } from "@/components/ui/number-field"
import {
ProductCard,
ProductCardBody,
ProductCardFooter,
ProductCardMedia,
ProductCardPrice,
ProductCardTitle,
} from "@/components/ui/product-card"
import { Progress } from "@/components/ui/progress"
import { TextField } from "@/components/ui/text-field"
import { toast } from "@/components/ui/toast"
import { formatCurrency, formatNumber, toFaDigits } from "@/lib/persian"
type Item = { id: string; title: string; variant: string; swatch?: string; price: number; oldPrice?: number; quantity: number }
const INITIAL: Item[] = [
{ id: "1", title: "هدفون بیسیم دیگ پرو", variant: "مشکی", swatch: "var(--dig-graphite-900)", price: 3_490_000, oldPrice: 4_100_000, quantity: 1 },
{ id: "2", title: "کابل شارژ تایپسی بافتهشده", variant: "۱ متری", price: 180_000, quantity: 2 },
{ id: "3", title: "کیف محافظ هدفون", variant: "سرمهای", swatch: "var(--dig-blue-900)", price: 420_000, quantity: 1 },
]
const SHIPPING = 45_000
const FREE_SHIPPING = 5_000_000
export default function CartBlock() {
const [items, setItems] = React.useState(INITIAL)
const [discount, setDiscount] = React.useState(0)
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
const count = items.reduce((sum, item) => sum + item.quantity, 0)
const shipping = subtotal >= FREE_SHIPPING ? 0 : SHIPPING
const remaining = Math.max(0, FREE_SHIPPING - subtotal)
const update = (id: string, quantity: number) =>
setItems((current) => current.map((item) => (item.id === id ? { ...item, quantity } : item)))
if (items.length === 0) {
return (
<main className="mx-auto w-full max-w-3xl p-4 sm:p-6">
<EmptyState variant="bordered" size="lg">
<ShoppingCartIcon aria-hidden className="size-10 text-placeholder" />
<EmptyStateTitle>سبد خرید شما خالی است</EmptyStateTitle>
<EmptyStateDescription>محصولی که به سبد اضافه کنید اینجا نمایش داده میشود.</EmptyStateDescription>
<EmptyStateActions>
<Button onClick={() => setItems(INITIAL)}>مشاهدهٔ محصولات</Button>
</EmptyStateActions>
</EmptyState>
</main>
)
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-5xl flex-col gap-6 p-4 sm:p-6">
<header className="flex flex-col gap-1">
<h1 className="text-2xl font-bold">سبد خرید</h1>
<p className="text-sm text-muted-foreground">{toFaDigits(count)} کالا در سبد شما</p>
</header>
<ControlSizeProvider size="xl">
<div className="grid grid-cols-1 items-start gap-6 lg:grid-cols-[minmax(0,1fr)_22rem]">
<section aria-label="کالاهای سبد" className="flex flex-col gap-3">
{/* پیشرفت تا ارسالِ رایگان: انگیزهٔ افزودنِ کالا، همانجا که کاربر تصمیم میگیرد. */}
<div className="flex flex-col gap-2.5 rounded-xl border border-border bg-card p-4">
<div className="flex items-center gap-2 text-sm">
{remaining > 0 ? (
<TruckIcon aria-hidden className="size-5 text-muted-foreground" />
) : (
<CheckCircleIcon aria-hidden className="size-5 text-success" />
)}
{remaining > 0 ? (
<span>
<strong className="font-semibold">{formatCurrency(remaining)}</strong> تا ارسالِ رایگان
</span>
) : (
<span className="font-medium text-success-subtle-foreground">ارسالِ این سفارش رایگان است</span>
)}
</div>
<Progress
value={Math.min(100, (subtotal / FREE_SHIPPING) * 100)}
color={remaining > 0 ? "primary" : "success"}
size="sm"
aria-label="پیشرفت تا ارسال رایگان"
/>
</div>
{items.map((item) => (
<ProductCard key={item.id} orientation="horizontal">
<ProductCardMedia />
<ProductCardBody>
<ProductCardTitle>{item.title}</ProductCardTitle>
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
{item.swatch && (
<span aria-hidden className="size-3 rounded-full border border-border" style={{ background: item.swatch }} />
)}
{item.variant}
</span>
<ProductCardPrice
amount={formatCurrency(item.price * item.quantity)}
originalAmount={item.oldPrice ? formatCurrency(item.oldPrice * item.quantity) : undefined}
discount={item.oldPrice ? `٪${toFaDigits(Math.round((1 - item.price / item.oldPrice) * 100))}` : undefined}
/>
</ProductCardBody>
<ProductCardFooter>
<NumberField
aria-label={`تعداد ${item.title}`}
value={item.quantity}
onValueChange={(quantity) => update(item.id, quantity)}
min={1}
max={10}
className="w-28"
/>
<Button
variant="ghost"
size="icon"
aria-label={`حذف ${item.title}`}
onClick={(() => {
const index = items.findIndex((row) => row.id === item.id)
setItems((current) => current.filter((row) => row.id !== item.id))
toast(`«${item.title}» از سبد حذف شد.`, { action: { label: "برگردان", onClick: () => setItems((current) => (current.some((row) => row.id === item.id) ? current : [...current.slice(0, index), item, ...current.slice(index)])) } })
})}
className="text-muted-foreground hover:text-destructive"
>
<TrashIcon />
</Button>
</ProductCardFooter>
</ProductCard>
))}
</section>
<aside aria-labelledby="summary-title" className="flex flex-col gap-5 rounded-xl border border-border bg-card p-5 lg:sticky lg:top-6">
<h2 id="summary-title" className="text-lg font-semibold">
خلاصهٔ سفارش
</h2>
<form
className="flex items-end gap-2"
onSubmit={(event) => {
event.preventDefault()
setDiscount(Math.round(subtotal * 0.1))
toast.success("کد تخفیف اعمال شد.")
}}
>
<TextField label="کد تخفیف" name="coupon" dir="ltr" autoComplete="off" className="flex-1" />
<Button type="submit" variant="outline">
اعمال
</Button>
</form>
<dl className="mt-2 flex flex-col gap-4 text-sm">
<div className="flex justify-between gap-2">
<dt className="text-muted-foreground">جمع کالاها ({toFaDigits(count)})</dt>
<dd>{formatCurrency(subtotal)}</dd>
</div>
{discount > 0 && (
<div className="flex justify-between gap-2 text-success-subtle-foreground">
<dt>تخفیف</dt>
<dd>
<bdi dir="ltr">−{formatNumber(discount)}</bdi> تومان
</dd>
</div>
)}
<div className="flex justify-between gap-2">
<dt className="text-muted-foreground">هزینهٔ ارسال</dt>
<dd>{shipping === 0 ? <span className="font-medium text-success-subtle-foreground">رایگان</span> : formatCurrency(shipping)}</dd>
</div>
<div className="mt-5 flex items-baseline justify-between gap-2 border-t border-border pt-8">
<dt className="font-semibold">مبلغ قابل پرداخت</dt>
<dd className="text-xl font-bold">{formatCurrency(subtotal - discount + shipping)}</dd>
</div>
</dl>
<div className="mt-3 flex flex-col gap-3">
<Button fullWidth>
ادامه و پرداخت
</Button>
<p className="flex items-center justify-center gap-1.5 text-xs text-muted-foreground">
<ShieldCheckIcon aria-hidden className="size-4" />
پرداختِ امن از درگاهِ شاپرک
</p>
</div>
</aside>
</div>
</ControlSizeProvider>
</main>
</ControlSizeProvider>
)
}