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

هر ستون یک کالا با عکس، قیمت، امتیاز و افزودن به سبد؛ ردیف‌های مشخصات با تیک و خط؛ «فقط تفاوت‌ها» با سوییچ و حذفِ هر ستون تا حداقل دو کالا.

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

این بلاک از ۱۰ آیتم دیگ استفاده می‌کند: 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.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/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
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/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
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/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>
  );
}