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

پیشنهادِ شگفت‌انگیز

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

فروشِ ویژه: نوارِ قرمز با شمارشِ معکوس، کارت‌ها با درصدِ تخفیفِ درشت، قیمتِ قبل و بعد و نوارِ فروخته‌شده برای نشانِ کمیابی.

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

این بلاک از ۵ آیتم دیگ استفاده می‌کند: add-to-cartbadgecontrol-sizeiconspersian

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/add-to-cart.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/add-to-cart.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/add-to-cart.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/add-to-cart.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json

۲. کد صفحه

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

product-deals.tsx
"use client";

import * as React from "react";

import { BoltIcon } from "@/components/icons";
import { Badge } from "@/components/ui/badge";
import { AddToCart } from "@/components/ui/add-to-cart";
import { ControlSizeProvider } from "@/components/ui/control-size";
import { formatCurrency, formatNumber, toFaDigits } from "@/lib/persian";
import { stockImages, type StockImageName } from "@/lib/stock-images";

const ENDS_IN_MS = (5 * 60 + 42) * 60 * 1000;
const DEALS: {
  id: string;
  title: string;
  image: StockImageName;
  price: number;
  oldPrice: number;
  sold: number;
  stock: number;
}[] = [
  {
    id: "1",
    title: "گلدانِ سرامیکی دوقلو",
    image: "productVase",
    price: 990_000,
    oldPrice: 1_590_000,
    sold: 64,
    stock: 80,
  },
  {
    id: "2",
    title: "ست لوازم‌التحریرِ رومیزی",
    image: "coverOfficeSupplies",
    price: 450_000,
    oldPrice: 640_000,
    sold: 31,
    stock: 50,
  },
  {
    id: "3",
    title: "ماشین‌حسابِ رومیزی",
    image: "coverCalculator",
    price: 290_000,
    oldPrice: 420_000,
    sold: 47,
    stock: 60,
  },
  {
    id: "4",
    title: "دفترِ یادداشتِ جلدسخت",
    image: "coverNotebook",
    price: 120_000,
    oldPrice: 180_000,
    sold: 88,
    stock: 100,
  },
];

/** زمانِ باقی‌مانده بعد از mount خوانده می‌شود تا خروجی سرور و مرورگر یکی بماند. */
function useCountdown(ms: number) {
  const [left, setLeft] = React.useState<number | null>(null);
  React.useEffect(() => {
    const target = Date.now() + ms;
    const tick = () => setLeft(Math.max(0, target - Date.now()));
    const first = requestAnimationFrame(tick);
    const timer = setInterval(tick, 1000);
    return () => {
      cancelAnimationFrame(first);
      clearInterval(timer);
    };
  }, [ms]);
  const value = left ?? ms;
  const pad = (n: number) => toFaDigits(String(n).padStart(2, "0"));
  return `${pad(Math.floor(value / 3_600_000))}:${pad(Math.floor(value / 60_000) % 60)}:${pad(Math.floor(value / 1000) % 60)}`;
}

/**
 * پیشنهادِ شگفت‌انگیز: بنرِ گرادیانیِ طراحی‌شده با پترن و شمارشِ معکوس، کارتِ هر کالا با درصدِ
 * تخفیف، قیمتِ قبل و بعد، و شمارِ «باقی مانده» که کمیابی را نشان می‌دهد.
 */
export default function ProductDealsBlock() {
  const countdown = useCountdown(ENDS_IN_MS);

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto flex w-full max-w-6xl flex-col gap-8 p-4 sm:p-6">
        {/* بنرِ طراحی‌شده: گرادیانِ هم‌خانوادهٔ بنفش به نیلی با هایلایت‌های شعاعی، پترنِ نقطه‌ای که به سمتِ
            پایان محو می‌شود، و «٪۴۰»ِ درشتِ تزئینی؛ بی‌عکس تا در موبایل هم تمیز بماند. */}
        <header className="relative isolate flex flex-col gap-5 overflow-hidden rounded-xl bg-[linear-gradient(135deg,var(--dig-violet-700),var(--dig-indigo-700))] p-5 text-[var(--dig-white)] sm:flex-row sm:items-end sm:justify-between sm:p-10">
          <span
            aria-hidden
            className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_85%_0%,var(--dig-violet-400),transparent_50%),radial-gradient(circle_at_0%_100%,var(--dig-blue-500),transparent_45%)]"
          />
          <span
            aria-hidden
            className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(color-mix(in_oklab,var(--dig-white)_35%,transparent)_1.2px,transparent_1.3px)] [background-size:18px_18px] [mask-image:linear-gradient(to_left,black,transparent_70%)]"
          />
          <span
            aria-hidden
            className="pointer-events-none absolute top-4 end-5 -z-10 text-[5.5rem] leading-none font-black text-[color-mix(in_oklab,var(--dig-white)_14%,transparent)] sm:top-6 sm:end-8 sm:text-[8rem]"
          >
            ٪۴۰
          </span>

          <div className="flex flex-col gap-2 sm:gap-3">
            <span className="flex w-fit items-center gap-1.5 rounded-full bg-[var(--dig-white)] px-3 py-1 text-sm font-bold text-[var(--dig-violet-700)] [&>svg]:size-4">
              <BoltIcon aria-hidden />
              فقط امروز
            </span>
            <h1 className="text-[1.75rem] leading-snug font-black whitespace-nowrap sm:text-4xl lg:text-5xl">
              پیشنهادِ شگفت‌انگیز
            </h1>
            <p className="text-sm font-medium sm:text-lg">
              تا ٪۴۰ تخفیف روی پرفروش‌ها، تا پایانِ زمان.
            </p>
          </div>
          <div
            role="timer"
            aria-label={`زمانِ باقی‌مانده ${countdown}`}
            className="flex w-fit items-start gap-2"
            dir="ltr"
          >
            {countdown.split(":").map((part, index) => (
              <React.Fragment key={index}>
                {index > 0 && (
                  <span aria-hidden className="pt-3 text-xl font-black">
                    :
                  </span>
                )}
                <span
                  aria-hidden
                  className="flex flex-col items-center gap-1.5"
                >
                  <span className="flex size-12 items-center justify-center rounded-xl bg-[var(--dig-white)] text-xl sm:size-14 sm:text-2xl font-black text-[var(--dig-violet-800)] tabular-nums shadow-card">
                    {part}
                  </span>
                  <span className="text-xs font-medium">
                    {["ساعت", "دقیقه", "ثانیه"][index]}
                  </span>
                </span>
              </React.Fragment>
            ))}
          </div>
        </header>
        {/* کارت‌ها جمع‌وجورند (کنترلِ ۴۰ پیکسلی، قیمت در یک ردیف) تا در موبایل اسکرول کم شود. */}
        <ControlSizeProvider size="lg" asChild>
          <ul className="grid grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-4">
            {DEALS.map((deal) => {
              const off = Math.round((1 - deal.price / deal.oldPrice) * 100);
              return (
                <li
                  key={deal.id}
                  className="flex flex-col gap-2 rounded-xl border border-border bg-card p-2.5 sm:p-3"
                >
                  <div className="relative">
                    {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
                    <img
                      src={stockImages[deal.image].src}
                      alt={stockImages[deal.image].alt}
                      loading="lazy"
                      className="aspect-square w-full rounded-lg object-cover"
                    />
                    <Badge className="absolute start-2 top-2 bg-[var(--dig-violet-700)] text-[var(--dig-white)]">
                      ٪{toFaDigits(off)}
                    </Badge>
                  </div>
                  <span className="line-clamp-2 min-h-10 text-sm leading-5 font-medium">
                    {deal.title}
                  </span>
                  <span className="flex flex-col gap-0.5">
                    <span className="flex flex-wrap items-baseline gap-x-2">
                      <span className="font-black tabular-nums">
                        {formatCurrency(deal.price)}
                      </span>
                      <s className="text-xs text-muted-foreground tabular-nums">
                        {formatNumber(deal.oldPrice)}
                      </s>
                    </span>
                    <span className="text-xs text-muted-foreground">
                      {toFaDigits(deal.stock - deal.sold)} عدد باقی مانده
                    </span>
                  </span>
                  <AddToCart
                    fullWidth
                    label="افزودن"
                    itemName={deal.title}
                    max={deal.stock - deal.sold}
                  />
                </li>
              );
            })}
          </ul>
        </ControlSizeProvider>
      </main>
    </ControlSizeProvider>
  );
}