مقایسهٔ کالا
پیشنمایش تمامصفحههر ستون یک کالا با عکس، قیمت، امتیاز و افزودن به سبد؛ ردیفهای مشخصات با تیک و خط؛ «فقط تفاوتها» با سوییچ و حذفِ هر ستون تا حداقل دو کالا.
۱. نصب کامپوننتها
این بلاک از ۱۰ آیتم دیگ استفاده میکند: add-to-cartbadgebuttoncontrol-sizeiconslabelpersianratingswitchtable
پکیجمنیجر پیشفرض 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/button.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/persian.json https://docs.digdesign.ir/r/rating.json https://docs.digdesign.ir/r/switch.json https://docs.digdesign.ir/r/table.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/button.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/persian.json https://docs.digdesign.ir/r/rating.json https://docs.digdesign.ir/r/switch.json https://docs.digdesign.ir/r/table.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/button.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/persian.json https://docs.digdesign.ir/r/rating.json https://docs.digdesign.ir/r/switch.json https://docs.digdesign.ir/r/table.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/button.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/persian.json https://docs.digdesign.ir/r/rating.json https://docs.digdesign.ir/r/switch.json https://docs.digdesign.ir/r/table.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
product-compare.tsx
"use client";
import * as React from "react";
import { CheckIcon, MinusIcon, XIcon } from "@/components/icons";
import { Badge } from "@/components/ui/badge";
import { AddToCart } from "@/components/ui/add-to-cart";
import { Button } from "@/components/ui/button";
import { ControlSizeProvider } from "@/components/ui/control-size";
import { Label } from "@/components/ui/label";
import { Rating } from "@/components/ui/rating";
import { Switch } from "@/components/ui/switch";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { formatCurrency, toFaDigits } from "@/lib/persian";
import { stockImages, type StockImageName } from "@/lib/stock-images";
type Value = string | boolean;
const PRODUCTS: {
id: string;
title: string;
image: StockImageName;
price: number;
rating: number;
best?: boolean;
specs: Record<string, Value>;
}[] = [
{
id: "a",
title: "گلدانِ سرامیکی دوقلو",
image: "productVase",
price: 1_290_000,
rating: 4.8,
best: true,
specs: {
جنس: "سرامیک",
ارتفاع: "۳۲ سانتیمتر",
"مناسبِ گلِ طبیعی": true,
دستساز: true,
گارانتی: "۶ ماه",
},
},
{
id: "b",
title: "گلدانِ شیشهای مینیمال",
image: "coverMinimalRoom",
price: 890_000,
rating: 4.3,
specs: {
جنس: "شیشه",
ارتفاع: "۲۸ سانتیمتر",
"مناسبِ گلِ طبیعی": true,
دستساز: false,
گارانتی: "۶ ماه",
},
},
{
id: "c",
title: "گلدانِ سنگی کوچک",
image: "coverNotebook",
price: 640_000,
rating: 4.1,
specs: {
جنس: "سنگِ مصنوعی",
ارتفاع: "۱۸ سانتیمتر",
"مناسبِ گلِ طبیعی": false,
دستساز: false,
گارانتی: "۳ ماه",
},
},
];
function Mark({ value }: { value: Value }) {
if (typeof value === "string")
return <span className="text-sm">{value}</span>;
return value ? (
<CheckIcon aria-label="دارد" className="mx-auto size-5 text-success" />
) : (
<MinusIcon
aria-label="ندارد"
className="mx-auto size-5 text-muted-foreground"
/>
);
}
/**
* مقایسهٔ محصول: هر ستون یک کالا با عکس، قیمت، امتیاز و افزودن به سبد؛ ردیفهای
* مشخصات با تیک و خط؛ «فقط تفاوتها» ردیفهای یکسان را پنهان میکند و حذفِ هر ستون.
*/
export default function ProductCompareBlock() {
const [products, setProducts] = React.useState(PRODUCTS);
const [diffOnly, setDiffOnly] = React.useState(false);
const specs = Object.keys(PRODUCTS[0].specs).filter(
(spec) =>
!diffOnly ||
new Set(products.map((product) => String(product.specs[spec]))).size > 1,
);
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="flex flex-wrap items-end justify-between gap-x-4 gap-y-8">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold">مقایسهٔ کالاها</h1>
<p className="text-muted-foreground">
{toFaDigits(products.length)} کالا کنارِ هم
</p>
</div>
<div className="flex items-center gap-3">
<Label htmlFor="compare-diff">فقط تفاوتها</Label>
<Switch
id="compare-diff"
checked={diffOnly}
onCheckedChange={setDiffOnly}
/>
</div>
</header>
<div className="overflow-x-auto">
{/* در موبایل ستونِ نامِ مشخصه حذف و نامش ردیفی بالای مقدارها میشود؛ دو کالا کنارِ هم دیده میشوند. */}
<Table layout="fixed" className="min-w-[30rem] sm:min-w-[44rem]">
<TableHeader>
<TableRow className="hover:bg-card">
<TableHead className="hidden w-[22%] sm:table-cell">
<span className="sr-only">مشخصه</span>
</TableHead>
{products.map((product) => (
<TableHead key={product.id} className="h-auto py-5 align-top">
<div className="relative flex flex-col gap-3">
<Button
variant="outline"
size="icon"
aria-label={`حذفِ ${product.title} از مقایسه`}
onClick={() =>
setProducts((list) =>
list.filter((item) => item.id !== product.id),
)
}
disabled={products.length <= 2}
className="absolute end-2 top-2 z-10 bg-background"
>
<XIcon />
</Button>
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img
src={stockImages[product.image].src}
alt={stockImages[product.image].alt}
className="aspect-square w-full rounded-lg object-cover"
/>
<span className="flex flex-col items-start gap-1.5 text-sm font-bold text-foreground">
{product.title}
{product.best && (
<Badge color="violet">پیشنهادِ ما</Badge>
)}
</span>
<Rating
readOnly
defaultValue={product.rating}
size="sm"
aria-label={`امتیازِ ${toFaDigits(product.rating)}`}
/>
<span className="text-lg font-black text-foreground tabular-nums">
{formatCurrency(product.price)}
</span>
<AddToCart
fullWidth
variant={product.best ? "default" : "outline"}
itemName={product.title}
/>
</div>
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{specs.map((spec) => (
<React.Fragment key={spec}>
<TableRow className="hover:bg-card sm:hidden">
<TableCell
colSpan={products.length}
className="bg-subtle py-2 text-xs font-medium text-muted-foreground"
>
{spec}
</TableCell>
</TableRow>
<TableRow>
<TableCell className="hidden text-sm text-muted-foreground sm:table-cell">
{spec}
</TableCell>
{products.map((product) => (
<TableCell key={product.id} className="text-center">
<Mark value={product.specs[spec]} />
</TableCell>
))}
</TableRow>
</React.Fragment>
))}
{specs.length === 0 && (
<TableRow>
<TableCell
colSpan={products.length + 1}
className="py-8 text-center text-muted-foreground"
>
این کالاها در همهٔ مشخصات یکساناند.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</main>
</ControlSizeProvider>
);
}