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

گالری تصاویر، امتیاز، قیمت، انتخاب رنگ و تعداد، افزودن به سبد، مشخصات و معرفی.

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

این بلاک از ۱۲ آیتم دیگ استفاده می‌کند: add-to-cartbadgebuttonchipcolor-swatch-pickercontrol-sizeiconslabelpersianproduct-cardtabstoggle-group

پکیج‌منیجر پیش‌فرض 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/chip.json https://docs.digdesign.ir/r/color-swatch-picker.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/product-card.json https://docs.digdesign.ir/r/tabs.json https://docs.digdesign.ir/r/toggle-group.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/chip.json https://docs.digdesign.ir/r/color-swatch-picker.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/product-card.json https://docs.digdesign.ir/r/tabs.json https://docs.digdesign.ir/r/toggle-group.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/chip.json https://docs.digdesign.ir/r/color-swatch-picker.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/product-card.json https://docs.digdesign.ir/r/tabs.json https://docs.digdesign.ir/r/toggle-group.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/chip.json https://docs.digdesign.ir/r/color-swatch-picker.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/product-card.json https://docs.digdesign.ir/r/tabs.json https://docs.digdesign.ir/r/toggle-group.json

۲. کد صفحه

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

product-detail.tsx
"use client"

import * as React from "react"

import { HeartIcon, ImageIcon, RefreshIcon, ShieldCheckIcon, StarIcon, TruckIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { AddToCart } from "@/components/ui/add-to-cart"
import { Button } from "@/components/ui/button"
import { Chip } from "@/components/ui/chip"
import { ColorSwatchPicker, ColorSwatchPickerItem } from "@/components/ui/color-swatch-picker"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Label } from "@/components/ui/label"
import { ProductCard, ProductCardBody, ProductCardMedia, ProductCardPrice, ProductCardTitle } from "@/components/ui/product-card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatCurrency } from "@/lib/persian"

const VIEWS = ["نمای روبه‌رو", "نمای کنار", "جزئیات بند", "در جعبه"]
// رنگِ واقعی کالا داده است، نه توکنِ رابط؛ از پالتِ پایهٔ دیگ می‌آید تا hex خام نباشد.
const COLORS = [
  { value: "black", label: "مشکی", swatch: "var(--dig-graphite-900)" },
  { value: "white", label: "سفید", swatch: "var(--dig-neutral-50)" },
  { value: "blue", label: "سرمه‌ای", swatch: "var(--dig-blue-900)" },
  { value: "sage", label: "سبزِ مریمی", swatch: "var(--dig-green-300)" },
]
const FEATURES = ["حذفِ نویزِ فعال", "۳۰ ساعت شارژ", "بلوتوث ۵٫۳", "ضدِ تعریق"]
const SPECS = [
  ["اتصال", "بلوتوث ۵٫۳"],
  ["شارژدهی", "تا ۳۰ ساعت"],
  ["وزن", "۲۵۰ گرم"],
  ["گارانتی", "۱۸ ماه"],
]
const PROMISES = [
  { icon: <TruckIcon />, title: "ارسالِ رایگان", detail: "تحویلِ ۲ روزه" },
  { icon: <ShieldCheckIcon />, title: "۱۸ ماه گارانتی", detail: "اصالت و سلامت" },
  { icon: <RefreshIcon />, title: "۷ روز بازگشت", detail: "بدونِ پرسش" },
]
const RELATED = [
  { id: "r1", title: "کیف محافظ هدفون", price: 420_000 },
  { id: "r2", title: "کابل شارژ تایپ‌سی بافته‌شده", price: 180_000 },
  { id: "r3", title: "پایهٔ رومیزی هدفون", price: 690_000 },
  { id: "r4", title: "ایرپد یدکی نرم", price: 240_000 },
]

export default function ProductDetailBlock() {
  const [view, setView] = React.useState(VIEWS[0])
  const [color, setColor] = React.useState("black")
  const [liked, setLiked] = React.useState(false)

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto flex w-full max-w-6xl flex-col gap-12 p-4 sm:p-6">
        <div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:gap-12">
          {/* گالری: تصویرِ اصلی با برچسب‌های روی تصویر، زیرش بندانگشتی‌ها. */}
          <section aria-label="تصاویر محصول" className="flex flex-col gap-3 self-start">
            <div
              role="img"
              aria-label={view}
              className="relative flex aspect-square items-center justify-center rounded-xl bg-[linear-gradient(145deg,var(--muted),color-mix(in_oklab,var(--brand-secondary)_10%,var(--muted)))] text-placeholder"
            >
              <ImageIcon className="size-16" />
              <span className="absolute start-4 top-4 rounded-full bg-background px-3 py-1 text-xs font-semibold">تازه رسید</span>
              <span className="absolute end-4 top-4 flex items-center gap-1 rounded-full bg-background px-2.5 py-1 text-xs font-semibold">
                <StarIcon className="size-3.5 text-warning" />
                ۴٫۴
              </span>
            </div>
            <ToggleGroup
              type="single"
              value={view}
              onValueChange={(next) => next && setView(next as string)}
              aria-label="انتخاب تصویر"
              className="grid w-full grid-cols-4 gap-3"
            >
              {VIEWS.map((item) => (
                <ToggleGroupItem
                  key={item}
                  value={item}
                  aria-label={item}
                  className="aspect-square h-auto w-full rounded-lg! bg-muted text-placeholder data-[state=on]:bg-muted data-[state=on]:ring-2 data-[state=on]:ring-foreground [&_svg]:size-6"
                >
                  <ImageIcon />
                </ToggleGroupItem>
              ))}
            </ToggleGroup>
          </section>

          {/* سلسله‌مراتب: معرفی، قیمت، ویژگی‌ها، انتخاب‌ها، خرید، تعهدها؛ هر گروه با فاصلهٔ بیشتر از قبلی. */}
          <section aria-label="جزئیات محصول" className="flex flex-col">
            <div className="flex flex-col gap-3">
              <span className="text-sm font-medium text-brand-secondary">صوتی / هدفون بی‌سیم</span>
              <h1 className="text-3xl leading-snug font-bold">هدفون بی‌سیم دیگ پرو</h1>
              <p className="leading-7 text-muted-foreground">
                حذفِ نویزِ فعال و باتری سی‌ساعته برای رفت‌وآمدِ روزانه و کار در فضای شلوغ؛ سبک، تاشو و همراهِ هر روز.
              </p>
            </div>

            <div className="mt-6 flex flex-col gap-1.5">
              <div className="flex flex-wrap items-center gap-3">
                <span className="text-3xl font-bold">{formatCurrency(3_490_000)}</span>
                <Badge variant="soft" color="success">
                  ارسالِ رایگان
                </Badge>
              </div>
              <div className="flex items-center gap-2 text-sm text-muted-foreground">
                <s>{formatCurrency(4_100_000)}</s>
                <Badge color="destructive" size="sm">
                  ٪۱۵
                </Badge>
                <span>· قیمت با مالیات</span>
              </div>
            </div>

            <ul aria-label="ویژگی‌ها" className="mt-6 flex flex-wrap gap-2">
              {FEATURES.map((feature) => (
                <li key={feature}>
                  <Chip variant="soft">{feature}</Chip>
                </li>
              ))}
            </ul>

            <ControlSizeProvider size="xl">
              <div className="mt-8 flex flex-col gap-3">
                <Label id="color-label" className="gap-1">
                  رنگ:
                  <span className="font-normal text-muted-foreground">{COLORS.find((item) => item.value === color)?.label}</span>
                </Label>
                <ColorSwatchPicker
                  value={color}
                  onValueChange={setColor}
                  aria-labelledby="color-label"
                  size="md"
                  radius="full"
                  indicator="ring"
                  className="gap-3 p-1"
                >
                  {COLORS.map((item) => (
                    <ColorSwatchPickerItem key={item.value} value={item.value} color={item.swatch} label={item.label} />
                  ))}
                </ColorSwatchPicker>
              </div>

              {/* تعداد بعد از افزودن روی همین دکمه انتخاب می‌شود؛ فیلدِ جدا لازم نیست. */}
              <div className="mt-8 flex gap-3">
                <AddToCart variant="default" fullWidth label="افزودن به سبد خرید" itemName="هدفون" max={7} className="flex-1" />
                <Button
                  variant="outline"
                  size="icon"
                  aria-pressed={liked}
                  aria-label={liked ? "حذف از علاقه‌مندی‌ها" : "افزودن به علاقه‌مندی‌ها"}
                  onClick={() => setLiked((value) => !value)}
                  className={liked ? "text-destructive hover:text-destructive" : undefined}
                >
                  <HeartIcon filled={liked} />
                </Button>
              </div>
            </ControlSizeProvider>

            <ul aria-label="تعهدهای فروشگاه" className="mt-8 grid grid-cols-3 gap-3">
              {PROMISES.map((item) => (
                <li key={item.title} className="flex flex-col items-center gap-2 rounded-xl border border-border p-3 text-center">
                  <span aria-hidden className="text-muted-foreground [&_svg]:size-5">
                    {item.icon}
                  </span>
                  <span className="flex flex-col gap-0.5">
                    <span className="text-sm font-semibold">{item.title}</span>
                    <span className="text-xs text-muted-foreground">{item.detail}</span>
                  </span>
                </li>
              ))}
            </ul>
          </section>
        </div>

        <Tabs defaultValue="specs" className="gap-4">
          <TabsList>
            <TabsTrigger value="specs">مشخصات</TabsTrigger>
            <TabsTrigger value="about">معرفی</TabsTrigger>
          </TabsList>
          <TabsContent value="specs">
            <dl className="grid grid-cols-1 gap-x-8 text-sm sm:grid-cols-2">
              {SPECS.map(([key, value]) => (
                <div key={key} className="flex justify-between gap-4 border-b border-border py-3">
                  <dt className="text-muted-foreground">{key}</dt>
                  <dd className="font-medium">{value}</dd>
                </div>
              ))}
            </dl>
          </TabsContent>
          <TabsContent value="about">
            <p className="max-w-3xl text-sm leading-7 text-muted-foreground">
              هدفون دیگ پرو با حذف نویز فعال و باتری سی‌ساعته برای رفت‌وآمد روزانه و کار در فضای شلوغ ساخته شده است.
            </p>
          </TabsContent>
        </Tabs>

        <section aria-labelledby="related-title" className="flex flex-col gap-4">
          <h2 id="related-title" className="text-xl font-bold">
            معمولاً با هم خریده می‌شوند
          </h2>
          <div className="flex gap-3 overflow-x-auto pb-2">
            {RELATED.map((item) => (
              <ProductCard key={item.id} size="sm">
                <ProductCardMedia />
                <ProductCardBody>
                  <ProductCardTitle>{item.title}</ProductCardTitle>
                  <ProductCardPrice amount={formatCurrency(item.price)} />
                </ProductCardBody>
              </ProductCard>
            ))}
          </div>
        </section>
      </main>
    </ControlSizeProvider>
  )
}