صفحهٔ محصولِ داروخانه
پیشنمایش تمامصفحهصفحهٔ محصولِ داروخانه با مقایسهٔ داروخانههای نزدیک (زمانِ رسیدن، قیمت، هزینهٔ ارسال) با کارتِ رادیویی، هشدارِ مصرف، توضیحاتِ تاشونده و نوارِ خریدِ پایین در موبایل.
۱. نصب کامپوننتها
این بلاک از ۱۱ آیتم دیگ استفاده میکند: 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.jsonpnpm 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.jsonyarn 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.jsonbunx --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>
)
}