پیشنهادِ شگفتانگیز
پیشنمایش تمامصفحهفروشِ ویژه: نوارِ قرمز با شمارشِ معکوس، کارتها با درصدِ تخفیفِ درشت، قیمتِ قبل و بعد و نوارِ فروختهشده برای نشانِ کمیابی.
۱. نصب کامپوننتها
این بلاک از ۵ آیتم دیگ استفاده میکند: 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.jsonpnpm 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.jsonyarn 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.jsonbunx --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>
);
}