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

سبد با پیشنهاد

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

سبد با نوارِ تا ارسالِ رایگان، بسته‌بندیِ هدیه و «شاید این‌ها را هم بخواهی» با افزودنِ یک‌کلیکی.

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

این بلاک از ۹ آیتم دیگ استفاده می‌کند: buttoncheckboxcontrol-sizeiconslabelnumber-fieldpersianprogresstoast

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.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/progress.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/checkbox.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/progress.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/checkbox.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/progress.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/checkbox.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/progress.json https://docs.digdesign.ir/r/toast.json

۲. کد صفحه

کد را در app/<route>/page.tsx بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

cart-upsell.tsx
"use client"

import * as React from "react"

import { PlusIcon, TrashIcon, TruckIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Label } from "@/components/ui/label"
import { NumberField } from "@/components/ui/number-field"
import { Progress } from "@/components/ui/progress"
import { toast } from "@/components/ui/toast"
import { formatCurrency } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"

type Item = { id: string; title: string; image: StockImageName; price: number; quantity: number }
const INITIAL: Item[] = [
  { id: "1", title: "گلدانِ سرامیکی دوقلو", image: "productVase", price: 1_290_000, quantity: 1 },
  { id: "2", title: "دفترِ یادداشتِ جلدسخت", image: "coverNotebook", price: 180_000, quantity: 2 },
]
const SUGGESTED: Item[] = [
  { id: "s1", title: "ست لوازم‌التحریرِ رومیزی", image: "coverOfficeSupplies", price: 640_000, quantity: 1 },
  { id: "s2", title: "ماشین‌حسابِ رومیزی", image: "coverCalculator", price: 420_000, quantity: 1 },
]
const FREE_SHIPPING = 3_000_000
const GIFT = 60_000

/**
 * سبد با پیشنهادِ تکمیلی: نوارِ «تا ارسالِ رایگان» بالای سبد، اقلام با عکس و تعداد، بسته‌بندی
 * هدیه با چک‌باکس، و «شاید این‌ها را هم بخواهی» با افزودنِ یک‌کلیکی که نوار را جلو می‌برد.
 */
export default function CartUpsellBlock() {
  const [items, setItems] = React.useState(INITIAL)
  const [gift, setGift] = React.useState(false)
  const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  const total = subtotal + (gift ? GIFT : 0)
  const remaining = Math.max(0, FREE_SHIPPING - subtotal)
  const suggestions = SUGGESTED.filter((item) => !items.some((entry) => entry.id === item.id))

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto grid w-full max-w-5xl grid-cols-1 items-start gap-8 p-4 sm:p-6 lg:grid-cols-[minmax(0,1fr)_20rem]">
        <div className="flex flex-col gap-8">
          <header className="flex flex-col gap-4">
            <h1 className="text-2xl font-bold">سبد خرید</h1>
            <div className="flex flex-col gap-2 rounded-xl 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>
          </header>

          <ul className="flex flex-col divide-y divide-border">
            {items.map((item) => (
              <li key={item.id} className="flex gap-4 py-5 first:pt-0">
                {/* 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" />
                <div className="flex flex-1 flex-col gap-3">
                  <div className="flex items-start justify-between gap-2">
                    <span className="font-medium">{item.title}</span>
                    <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)])) } })
                    })}>
                      <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>

          {suggestions.length > 0 && (
            <section aria-labelledby="cu-suggest" className="flex flex-col gap-4 border-t border-border pt-8">
              <h2 id="cu-suggest" className="font-bold">
                شاید این‌ها را هم بخواهی
              </h2>
              <ul className="grid grid-cols-1 gap-3 sm:grid-cols-2">
                {suggestions.map((item) => (
                  <li key={item.id} className="flex items-center gap-3 rounded-xl border border-border p-3">
                    {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
                    <img src={stockImages[item.image].src} alt="" className="size-14 rounded-lg object-cover" />
                    <span className="flex flex-1 flex-col">
                      <span className="text-sm font-medium">{item.title}</span>
                      <span className="text-sm text-muted-foreground tabular-nums">{formatCurrency(item.price)}</span>
                    </span>
                    <Button
                      variant="outline"
                      size="icon"
                      aria-label={`افزودنِ ${item.title}`}
                      onClick={() => {
                        setItems((list) => [...list, item])
                        toast.success(`«${item.title}» اضافه شد.`)
                      }}
                    >
                      <PlusIcon />
                    </Button>
                  </li>
                ))}
              </ul>
            </section>
          )}
        </div>

        <aside className="flex flex-col gap-5 rounded-xl bg-subtle p-6 lg:sticky lg:top-6">
          <div className="flex items-center gap-3">
            <Checkbox id="cu-gift" checked={gift} onCheckedChange={(checked) => setGift(checked === true)} />
            <Label htmlFor="cu-gift" className="flex-1 font-normal">
              بسته‌بندی هدیه ({formatCurrency(GIFT)})
            </Label>
          </div>
          <div className="flex items-baseline justify-between gap-2 border-t border-border pt-5">
            <span className="text-muted-foreground">جمع</span>
            <span className="text-xl font-black tabular-nums">{formatCurrency(total)}</span>
          </div>
          <Button fullWidth disabled={items.length === 0} className="mt-3">
            ادامهٔ خرید
          </Button>
        </aside>
      </main>
    </ControlSizeProvider>
  )
}