سبد و «بعداً میخرم»
پیشنمایش تمامصفحهسبد با سگمنتِ سبد/ذخیرهشده، جابهجاییِ کالا بینِ دو فهرست و یادآوریِ کاهشِ قیمت.
۱. نصب کامپوننتها
این بلاک از ۶ آیتم دیگ استفاده میکند: buttoncontrol-sizeiconspersiantoasttoggle-group
پکیجمنیجر پیشفرض 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/persian.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.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/persian.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.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/persian.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.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/persian.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
cart-saved.tsx
"use client"
import * as React from "react"
import { HeartIcon, ShoppingCartIcon, TrashIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
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; image: StockImageName; price: number; note?: string }
const CART: Item[] = [
{ id: "1", title: "گلدانِ سرامیکی دوقلو", image: "productVase", price: 1_290_000 },
{ id: "2", title: "دفترِ یادداشتِ جلدسخت", image: "coverNotebook", price: 180_000 },
]
const SAVED: Item[] = [
{ id: "3", title: "ظرفِ سفالی لعابمات", image: "coverMinimalRoom", price: 890_000, note: "قیمت ٪۱۰ کم شد" },
{ id: "4", title: "ست فنجانِ دستساز", image: "studioSunlit", price: 1_640_000 },
]
/**
* سبد با «بعداً میخرم»: سگمنتِ سبد/ذخیرهشده با شمار، جابهجایی هر کالا بینِ دو فهرست
* با یک دکمه، و یادآوری کاهشِ قیمتِ کالاهای ذخیرهشده؛ جمع فقط برای کالاهای سبد.
*/
export default function CartSavedBlock() {
const [cart, setCart] = React.useState(CART)
const [saved, setSaved] = React.useState(SAVED)
const [tab, setTab] = React.useState("cart")
const list = tab === "cart" ? cart : saved
const total = cart.reduce((sum, item) => sum + item.price, 0)
function move(item: Item) {
if (tab === "cart") {
setCart((items) => items.filter((entry) => entry.id !== item.id))
setSaved((items) => [item, ...items])
} else {
setSaved((items) => items.filter((entry) => entry.id !== item.id))
setCart((items) => [...items, item])
}
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-3xl flex-col gap-8 p-4 sm:p-6">
<header className="flex flex-wrap items-end justify-between gap-x-4 gap-y-6">
<h1 className="text-2xl font-bold">سبد خرید</h1>
<ToggleGroup type="single" variant="segmented" value={tab} onValueChange={(value) => value && setTab(value as string)} aria-label="فهرست">
<ToggleGroupItem value="cart" className="gap-2">
سبد
<span className="text-xs text-muted-foreground tabular-nums">{toFaDigits(cart.length)}</span>
</ToggleGroupItem>
<ToggleGroupItem value="saved" className="gap-2">
بعداً میخرم
<span className="text-xs text-muted-foreground tabular-nums">{toFaDigits(saved.length)}</span>
</ToggleGroupItem>
</ToggleGroup>
</header>
{list.length === 0 ? (
<p className="rounded-xl bg-subtle p-8 text-center text-muted-foreground">{tab === "cart" ? "سبد خالی است." : "چیزی برای بعد ذخیره نکردهای."}</p>
) : (
<ul className="flex flex-col gap-3">
{list.map((item) => (
<li key={item.id} className="flex items-center gap-4 rounded-xl border border-border bg-card p-4">
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img src={stockImages[item.image].src} alt="" className="size-20 shrink-0 rounded-lg object-cover" />
<span className="flex flex-1 flex-col gap-1">
<span className="font-medium">{item.title}</span>
<span className="font-bold tabular-nums">{formatCurrency(item.price)}</span>
{item.note && tab === "saved" && <span className="text-xs font-medium text-success">{item.note}</span>}
</span>
<div className="flex flex-col gap-2 sm:flex-row">
<Button variant="outline" onClick={() => move(item)}>
{tab === "cart" ? <HeartIcon /> : <ShoppingCartIcon />}
{tab === "cart" ? "بعداً" : "به سبد"}
</Button>
<Button variant="ghost" size="icon" aria-label={`حذفِ ${item.title}`} onClick={() => {
const set = tab === "cart" ? setCart : setSaved
const index = list.findIndex((row) => row.id === item.id)
set((rows) => rows.filter((row) => row.id !== item.id))
toast(`«${item.title}» حذف شد.`, { action: { label: "برگردان", onClick: () => set((rows) => (rows.some((row) => row.id === item.id) ? rows : [...rows.slice(0, index), item, ...rows.slice(index)])) } })
}}>
<TrashIcon />
</Button>
</div>
</li>
))}
</ul>
)}
{tab === "cart" && cart.length > 0 && (
<div className="flex flex-wrap items-center justify-between gap-4 border-t border-border pt-8">
<span className="flex flex-col">
<span className="text-sm text-muted-foreground">جمعِ سبد</span>
<span className="text-xl font-black tabular-nums">{formatCurrency(total)}</span>
</span>
<Button>ادامهٔ خرید</Button>
</div>
)}
</main>
</ControlSizeProvider>
)
}