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

محصولِ دیجیتال

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

دوره، کتابِ الکترونیک یا اشتراک: کاور با پیش‌نمایش، مدرس، آمار، فهرستِ شامل‌ها و کارتِ خریدِ چسبان با انتخابِ نوعِ دسترسی.

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

این بلاک از ۷ آیتم دیگ استفاده می‌کند: avatarbuttoncontrol-sizeiconslabelpersianradio-group

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/avatar.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/radio-group.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.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/radio-group.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.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/radio-group.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/avatar.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/radio-group.json

۲. کد صفحه

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

product-detail-digital.tsx
"use client"

import * as React from "react"

import { CheckIcon, ClockIcon, PlayIcon, StarIcon, UsersIcon } from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { formatCurrency } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"

const OPTIONS = [
  { value: "course", title: "دوره", note: "دسترسی همیشگی به ۲۴ درس", price: 1_900_000 },
  { value: "mentor", title: "دوره + منتور", note: "به‌همراهِ ۴ جلسهٔ رفعِ اشکال", price: 3_400_000, best: true },
]
const INCLUDES = ["۲۴ درسِ ویدیویی، ۸ ساعت", "فایلِ تمرین و قالبِ اکسل", "گواهی پایانِ دوره", "به‌روزرسانی رایگان"]

const optionCard =
  "flex items-center gap-3 rounded-xl border border-border p-4 transition-colors has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary-subtle"

/**
 * جزئیاتِ محصولِ دیجیتال (دوره، کتابِ الکترونیک، اشتراک): کاور با پیش‌نمایشِ ویدیو، مدرس،
 * آمار، «شاملِ چه چیزهایی»، و کارتِ خریدِ چسبان با انتخابِ نوعِ دسترسی و قیمت.
 */
export default function ProductDetailDigitalBlock() {
  const [option, setOption] = React.useState("mentor")
  const active = OPTIONS.find((item) => item.value === option) ?? OPTIONS[0]

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto grid w-full max-w-6xl grid-cols-1 items-start gap-10 p-4 sm:p-6 lg:grid-cols-[minmax(0,1fr)_22rem]">
        <div className="flex flex-col gap-8">
          <div className="relative overflow-hidden rounded-xl bg-inverse">
            {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
            <img src={stockImages.studentStudy.src} alt="" className="aspect-video w-full object-cover" />
            <div className="absolute inset-0 flex items-center justify-center">
              <Button className="bg-[var(--dig-white)] text-[var(--dig-graphite-900)] hover:bg-[var(--dig-neutral-200)]">
                <PlayIcon />
                پیش‌نمایشِ رایگان
              </Button>
            </div>
          </div>
          <div className="flex flex-col gap-4">
            <h1 className="text-3xl font-bold text-balance">حسابداری برای صاحبانِ کسب‌وکار</h1>
            <p className="text-lg leading-8 text-muted-foreground">بدونِ پیش‌نیاز؛ از سند تا سود و زیان، با مثال‌های واقعی فروشگاه و کافه.</p>
            <ul className="flex flex-wrap gap-x-6 gap-y-2 text-sm text-muted-foreground">
              <li className="flex items-center gap-1.5">
                <StarIcon aria-hidden className="size-4 fill-[var(--dig-orange-400)] text-[var(--dig-orange-400)]" />
                ۴٫۹ (۶۴۰ نظر)
              </li>
              <li className="flex items-center gap-1.5">
                <UsersIcon aria-hidden className="size-4" />
                ۳٬۴۰۰ دانشجو
              </li>
              <li className="flex items-center gap-1.5">
                <ClockIcon aria-hidden className="size-4" />
                ۸ ساعت
              </li>
            </ul>
          </div>
          <div className="flex items-center gap-4 rounded-xl bg-subtle p-4">
            <Avatar size="lg" alt="" src={stockImages.personWoman3.src} />
            <span className="flex flex-col">
              <span className="font-semibold">نگار احمدی</span>
              <span className="text-sm text-muted-foreground">حسابدارِ رسمی، ۱۲ سال تدریس</span>
            </span>
          </div>
          <section aria-labelledby="pdd-includes" className="flex flex-col gap-4">
            <h2 id="pdd-includes" className="text-xl font-bold">
              شاملِ این‌هاست
            </h2>
            <ul className="grid grid-cols-1 gap-3 sm:grid-cols-2">
              {INCLUDES.map((item) => (
                <li key={item} className="flex items-center gap-3">
                  <CheckIcon aria-hidden className="size-5 text-success" />
                  {item}
                </li>
              ))}
            </ul>
          </section>
        </div>
        <aside className="flex flex-col gap-5 rounded-xl border border-border bg-card p-5 lg:sticky lg:top-6">
          <RadioGroup value={option} onValueChange={setOption} aria-label="نوعِ دسترسی" className="gap-3">
            {OPTIONS.map((item) => (
              <div key={item.value} className={optionCard}>
                <RadioGroupItem id={`pdd-${item.value}`} value={item.value} />
                <Label htmlFor={`pdd-${item.value}`} className="flex flex-1 flex-col items-start gap-1">
                  <span className="font-semibold">{item.title}</span>
                  <span className="text-xs font-normal text-muted-foreground">{item.note}</span>
                </Label>
                <span className="text-sm font-bold tabular-nums">{formatCurrency(item.price)}</span>
              </div>
            ))}
          </RadioGroup>
          <div className="flex items-baseline justify-between border-t border-border pt-5">
            <span className="text-muted-foreground">قیمت</span>
            <span className="text-2xl font-black tabular-nums">{formatCurrency(active.price)}</span>
          </div>
          <Button fullWidth className="mt-2">
            خرید و شروعِ دوره
          </Button>
          <p className="text-center text-xs text-muted-foreground">تا ۷ روز اگر راضی نبودی، کامل برمی‌گردانیم.</p>
        </aside>
      </main>
    </ControlSizeProvider>
  )
}