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

صفحهٔ محصولِ داروخانه

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

صفحهٔ محصولِ داروخانه با مقایسهٔ داروخانه‌های نزدیک (زمانِ رسیدن، قیمت، هزینهٔ ارسال) با کارتِ رادیویی، هشدارِ مصرف، توضیحاتِ تاشونده و نوارِ خریدِ پایین در موبایل.

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

این بلاک از ۱۱ آیتم دیگ استفاده می‌کند: accordionadd-to-cartalertbadgebuttoncontrol-sizeiconspersianradio-cardradio-grouptoast

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

۲. کد صفحه

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

pharmacy-product.tsx
"use client"

import * as React from "react"

import { ExpressIcon, HeartIcon, ShieldCheckIcon, StarIcon, TruckIcon } from "@/components/icons"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { AddToCart } from "@/components/ui/add-to-cart"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { RadioCard } from "@/components/ui/radio-card"
import { RadioGroup } from "@/components/ui/radio-group"
import { toast } from "@/components/ui/toast"
import { formatCurrency, toFaDigits } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"

const OFFERS = [
  { id: "azadi", name: "داروخانهٔ شبانه‌روزیِ آزادی", minutes: 15, price: 186_000, fee: 0 },
  { id: "salehi", name: "داروخانهٔ دکتر صالحی", minutes: 25, price: 179_000, fee: 25_000 },
  { id: "sina", name: "داروخانهٔ سینا", minutes: 40, price: 192_000, fee: 0 },
]
const INFO = [
  { id: "use", title: "موارد مصرف", body: "تقویتِ سیستمِ ایمنی و کاهشِ خستگی در دوره‌هایی که بدن ویتامین C بیشتری لازم دارد. روزی یک قرص را در یک لیوان آب حل کنید." },
  { id: "side", title: "عوارض و هشدارها", body: "در مصرفِ بیش از حد ممکن است ناراحتیِ معده یا اسهال پیش بیاید. در سنگِ کلیه یا رژیمِ کم‌سدیم پیش از مصرف با پزشک مشورت کنید؛ هر قرص سدیم دارد." },
  { id: "keep", title: "نگهداری", body: "دور از نور و رطوبت، در دمای زیرِ ۲۵ درجه و دور از دسترسِ کودکان." },
]

/**
 * صفحهٔ محصولِ داروخانه: تصویرِ محصول، نام، شکل و مقدار، امتیاز، و مقایسهٔ داروخانه‌های نزدیک (زمانِ رسیدن، قیمت،
 * هزینهٔ ارسال) با RadioCard تا کاربر سریع‌ترین یا ارزان‌ترین را خودش انتخاب کند. پیش‌فرض سریع‌ترین
 * با ارسالِ رایگان است. توضیحات تاشونده‌اند و هشدارِ مصرف دیده می‌شود. در موبایل نوارِ پایین با مبلغِ
 * کل و افزودن، در دسکتاپ ستونِ کنار.
 */
export default function PharmacyProductBlock() {
  const [pharmacy, setPharmacy] = React.useState("azadi")
  const [quantity, setQuantity] = React.useState(0)
  const [saved, setSaved] = React.useState(false)
  const offer = OFFERS.find((item) => item.id === pharmacy) ?? OFFERS[0]
  const total = offer.price * Math.max(1, quantity) + offer.fee

  const addToCart = (
    <AddToCart
      fullWidth
      variant="default"
      label="افزودن به سبد"
      itemName="قرصِ جوشانِ ویتامین C"
      max={6}
      quantity={quantity}
      onQuantityChange={(next) => {
        if (quantity === 0 && next === 1) toast.success(`از ${offer.name} به سبد اضافه شد.`)
        setQuantity(next)
      }}
    />
  )

  return (
    <ControlSizeProvider size="xl" asChild>
      <div className="mx-auto flex w-full max-w-5xl flex-col">
        <main className="grid grid-cols-1 items-start gap-6 p-4 sm:p-6 lg:grid-cols-[minmax(0,1fr)_22rem] lg:gap-8">
          <div className="flex min-w-0 flex-col gap-6">
            <div className="relative aspect-[16/9] overflow-hidden rounded-xl bg-muted">
              {/* eslint-disable @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
              {/* عکسِ محصول کامل دیده می‌شود (contain)؛ پشتش همان عکس محو و بزرگ‌شده تا دو طرفِ قاب نوارِ خالی نداشته باشد. */}
              <img src={stockImages.vitaminCEffervescent.src} alt="" aria-hidden className="absolute inset-0 size-full scale-125 object-cover blur-2xl" />
              <img src={stockImages.vitaminCEffervescent.src} alt={stockImages.vitaminCEffervescent.alt} className="relative size-full object-contain" />
              <ControlSizeProvider size="default" mobileSize="sm" asChild>
                <Button
                  variant="outline"
                  size="icon"
                  aria-label={saved ? "برداشتن از ذخیره‌ها" : "ذخیره"}
                  aria-pressed={saved}
                  onClick={() => setSaved((value) => !value)}
                  className={`absolute end-3 top-3 bg-card ${saved ? "text-destructive hover:text-destructive" : ""}`}
                >
                  <HeartIcon filled={saved} />
                </Button>
              </ControlSizeProvider>
            </div>

            <header className="flex flex-col gap-2">
              <span className="text-sm text-muted-foreground">هانسال · مکمل و ویتامین</span>
              <h1 className="text-2xl leading-snug font-bold">قرصِ جوشانِ ویتامین C هزار میلی‌گرم</h1>
              <div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-sm">
                <span className="text-muted-foreground">قرصِ جوشان · طعمِ پرتقال · ۲۰ عدد</span>
                <span aria-hidden className="text-muted-foreground">·</span>
                <span className="flex items-center gap-1">
                  <StarIcon aria-hidden className="size-4 fill-[var(--dig-amber-400)] text-[var(--dig-amber-400)]" />
                  {toFaDigits("۴٫۷")}
                  <a href="#reviews" className="text-muted-foreground hover:underline">
                    ({toFaDigits(128)} نظر)
                  </a>
                </span>
                <Badge variant="soft" color="success">
                  بدونِ نسخه
                </Badge>
              </div>
            </header>

            <section aria-labelledby="offers-title" className="flex flex-col gap-3">
              <div className="flex items-baseline justify-between gap-3">
                <h2 id="offers-title" className="text-lg leading-snug font-bold">
                  از کدام داروخانه؟
                </h2>
                <span className="text-xs text-muted-foreground">{toFaDigits(OFFERS.length)} داروخانهٔ نزدیک موجود دارند</span>
              </div>
              <RadioGroup value={pharmacy} onValueChange={setPharmacy} aria-label="داروخانه" className="gap-2">
                {OFFERS.map((item) => (
                  <RadioCard
                    key={item.id}
                    value={item.id}
                    title={item.name}
                    description={
                      <span className="flex flex-wrap items-center gap-x-3">
                        <span className="flex items-center gap-1">
                          <ExpressIcon aria-hidden className="size-4" />
                          رسیدن در {toFaDigits(item.minutes)} دقیقه
                        </span>
                        <span className={item.fee ? "" : "font-medium text-success"}>
                          {item.fee ? `ارسال ${formatCurrency(item.fee)}` : "ارسالِ رایگان"}
                        </span>
                      </span>
                    }
                    aside={<span className="font-bold tabular-nums">{formatCurrency(item.price)}</span>}
                  />
                ))}
              </RadioGroup>
            </section>

            <Alert color="warning">
              <AlertDescription>مکمل جای داروی تجویزی را نمی‌گیرد؛ اگر داروی دیگری مصرف می‌کنی، پیش از خرید با داروساز مشورت کن.</AlertDescription>
            </Alert>

            <Accordion type="single" collapsible variant="soft" indicator="plus" defaultValue="use">
              {INFO.map((item) => (
                <AccordionItem key={item.id} value={item.id}>
                  <AccordionTrigger className="text-base font-semibold">{item.title}</AccordionTrigger>
                  <AccordionContent className="leading-8 text-muted-foreground">{item.body}</AccordionContent>
                </AccordionItem>
              ))}
            </Accordion>
          </div>

          {/* ستونِ خرید در دسکتاپ؛ در موبایل همین اطلاعات در نوارِ پایین است. */}
          <aside aria-label="خرید" className="hidden flex-col gap-4 rounded-xl border border-border bg-card p-5 lg:sticky lg:top-6 lg:flex">
            <span className="text-sm text-muted-foreground">{offer.name}</span>
            <dl className="flex flex-col gap-2 text-sm">
              <div className="flex justify-between gap-2">
                <dt className="text-muted-foreground">قیمت</dt>
                <dd className="tabular-nums">{formatCurrency(offer.price)}</dd>
              </div>
              <div className="flex justify-between gap-2">
                <dt className="text-muted-foreground">ارسال</dt>
                <dd className={offer.fee ? "tabular-nums" : "font-medium text-success"}>{offer.fee ? formatCurrency(offer.fee) : "رایگان"}</dd>
              </div>
              <div className="mt-1 flex items-baseline justify-between gap-2 border-t border-border pt-3">
                <dt className="font-semibold">جمع</dt>
                <dd className="text-lg font-bold tabular-nums">{formatCurrency(total)}</dd>
              </div>
            </dl>
            {addToCart}
            <p className="flex items-center justify-center gap-1.5 text-xs text-muted-foreground">
              <ShieldCheckIcon aria-hidden className="size-4" />
              اصالتِ کالا با کدِ رهگیریِ سازمانِ غذا و دارو
            </p>
          </aside>
        </main>

        <div className="sticky bottom-0 flex items-center gap-3 border-t border-border bg-background px-4 py-3 lg:hidden">
          <span className="flex flex-col">
            <span className="flex items-center gap-1 text-xs text-muted-foreground">
              <TruckIcon aria-hidden className="size-3.5" />
              {offer.fee ? `با ارسال` : "ارسال رایگان"}
            </span>
            <span className="font-bold tabular-nums">{formatCurrency(total)}</span>
          </span>
          <div className="flex-1">{addToCart}</div>
        </div>
      </div>
    </ControlSizeProvider>
  )
}