صفحهٔ اصلیِ داروخانه
پیشنمایش تمامصفحهصفحهٔ اصلیِ اپِ داروخانه: نشانیِ ارسال و سبد، جستوجوی دارو، بنرِ سفارش با عکسِ نسخه، دستهها، پیشنهادهای ویژه با افزودن به سبد و داروخانههای نزدیک روی نقشه.
۱. نصب کامپوننتها
این بلاک از ۱۰ آیتم دیگ استفاده میکند: add-to-cartbadgebottom-navigationbuttoncontrol-sizeiconsmap-viewpersianproduct-cardsearch-field
پکیجمنیجر پیشفرض 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/bottom-navigation.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/map-view.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/product-card.json https://docs.digdesign.ir/r/search-field.jsonpnpm 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/bottom-navigation.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/map-view.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/product-card.json https://docs.digdesign.ir/r/search-field.jsonyarn 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/bottom-navigation.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/map-view.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/product-card.json https://docs.digdesign.ir/r/search-field.jsonbunx --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/bottom-navigation.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/map-view.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/product-card.json https://docs.digdesign.ir/r/search-field.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
pharmacy-home.tsx
"use client"
import * as React from "react"
import {
BandageIcon,
ChevronDownIcon,
ChevronLeftIcon,
CloudUploadIcon,
DashboardIcon,
FaceMaskIcon,
FirstAidKitIcon,
GridIcon,
HandSoapIcon,
HeartIcon,
LocationIcon,
PillIcon,
ShoppingCartIcon,
StethoscopeIcon,
TestTubeIcon,
UserIcon,
} from "@/components/icons"
import { AddToCart } from "@/components/ui/add-to-cart"
import { Badge } from "@/components/ui/badge"
import { BottomNavigation, BottomNavigationItem } from "@/components/ui/bottom-navigation"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { MapMarker, MapView } from "@/components/ui/map-view"
import { ProductCard, ProductCardBody, ProductCardCategory, ProductCardFooter, ProductCardMedia, ProductCardPrice, ProductCardTitle } from "@/components/ui/product-card"
import { SearchField } from "@/components/ui/search-field"
import { formatCurrency, toFaDigits } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"
/** هر دسته یک رنگِ ملایم از خانوادهٔ خودش؛ کاشی و آیکون همرنگ، بیopacity. */
const CATEGORIES = [
{ id: "otc", label: "داروی بدونِ نسخه", icon: <PillIcon />, hue: "--dig-blue-500" },
{ id: "supplement", label: "مکمل و ویتامین", icon: <TestTubeIcon />, hue: "--dig-amber-500" },
{ id: "beauty", label: "آرایشی و بهداشتی", icon: <HandSoapIcon />, hue: "--dig-pink-500" },
{ id: "devices", label: "تجهیزاتِ پزشکی", icon: <StethoscopeIcon />, hue: "--dig-violet-500" },
{ id: "firstaid", label: "کمکهای اولیه", icon: <BandageIcon />, hue: "--dig-red-500" },
{ id: "hygiene", label: "ماسک و ضدعفونی", icon: <FaceMaskIcon />, hue: "--dig-green-500" },
]
type Product = { id: string; title: string; brand: string; price: number; oldPrice?: number; rx?: boolean; image: StockImageName }
const OFFERS: Product[] = [
{ id: "1", title: "قرصِ جوشانِ ویتامین C هزار، ۲۰ عدد", brand: "هانسال", price: 186_000, oldPrice: 248_000, image: "vitaminCEffervescent" },
{ id: "2", title: "ژلِ ضدعفونیِ دست ۵۰۰ میلیلیتر", brand: "درمانگستر", price: 94_000, image: "handSanitizer" },
{ id: "3", title: "قطرهٔ چشمیِ لاتانوپروست ۲٫۵ میلیلیتر", brand: "سینا دارو", price: 412_000, rx: true, image: "eyeDrops" },
{ id: "4", title: "چسبِ زخمِ ضدآب، ۲۰ عدد", brand: "پارسطب", price: 58_000, oldPrice: 72_000, image: "bandAid" },
]
const NEARBY = [
{ id: "1", name: "داروخانهٔ شبانهروزیِ آزادی", distance: "۰٫۸ کیلومتر", open: true, hours: "شبانهروزی", x: 34, y: 46 },
{ id: "2", name: "داروخانهٔ دکتر صالحی", distance: "۱٫۴ کیلومتر", open: true, hours: "تا ۲۳", x: 66, y: 34 },
{ id: "3", name: "داروخانهٔ سینا", distance: "۲٫۱ کیلومتر", open: false, hours: "از ۸ صبح", x: 78, y: 70 },
]
const tint = (hue: string) => ({ background: `color-mix(in oklab, var(${hue}) 14%, var(--card))`, color: `color-mix(in oklab, var(${hue}) 75%, var(--foreground))` })
/** عنوانِ هر بخش با «همه» در انتهای ردیف؛ یک الگو برای همهٔ بخشها. */
function SectionHeader({ id, title, href = "#" }: { id: string; title: string; href?: string }) {
return (
<div className="flex items-center justify-between gap-3">
<h2 id={id} className="text-lg leading-snug font-bold">
{title}
</h2>
<a href={href} className="flex items-center gap-0.5 text-sm font-medium text-brand-secondary hover:underline">
همه
<ChevronLeftIcon aria-hidden className="size-4" />
</a>
</div>
)
}
/**
* صفحهٔ اصلیِ اپِ داروخانه: نشانیِ ارسال و سبد در سرصفحه، جستوجوی دارو، بنرِ «سفارش با عکسِ نسخه» (راهِ اصلیِ
* خریدِ داروی نسخهای)، دستهها، پیشنهادهای ویژه با AddToCart (داروی نسخهای بهجای افزودن «ثبت با نسخه»
* دارد) و داروخانههای نزدیک روی نقشه با وضعیتِ باز یا بسته. در موبایل نوارِ پایین، در دسکتاپ گریدِ پهن.
*/
export default function PharmacyHomeBlock() {
const [cart, setCart] = React.useState<Record<string, number>>({})
const [tab, setTab] = React.useState("home")
const [active, setActive] = React.useState("1")
const cartCount = Object.values(cart).reduce((sum, qty) => sum + qty, 0)
return (
<ControlSizeProvider size="xl" asChild>
<div className="mx-auto flex min-h-svh w-full max-w-6xl flex-col bg-background">
<main className="flex flex-1 flex-col gap-8 p-4 sm:p-6">
<header className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-3">
<Button variant="ghost" aria-label="نشانیِ ارسال: خانه، خیابانِ آزادی؛ تغییر" className="-ms-1 h-auto min-w-0 justify-start gap-2 px-1 py-1 text-start font-normal">
<span aria-hidden className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary-subtle text-primary-subtle-foreground [&>svg]:size-5">
<LocationIcon />
</span>
<span className="flex min-w-0 flex-col">
<span className="text-xs text-muted-foreground">ارسال به</span>
<span className="flex items-center gap-1 truncate text-sm font-semibold">
خانه، خیابانِ آزادی
<ChevronDownIcon aria-hidden className="size-4 shrink-0" />
</span>
</span>
</Button>
<ControlSizeProvider size="lg" mobileSize="default" asChild>
<Button variant="outline" size="icon" aria-label={cartCount ? `سبدِ خرید، ${toFaDigits(cartCount)} کالا` : "سبدِ خرید"} className="relative shrink-0">
<ShoppingCartIcon />
{cartCount > 0 && (
<Badge color="destructive" className="absolute -end-1.5 -top-1.5 min-w-5 justify-center px-1 tabular-nums">
{toFaDigits(cartCount)}
</Badge>
)}
</Button>
</ControlSizeProvider>
</div>
<SearchField aria-label="جستوجوی دارو" placeholder="نامِ دارو، برند یا علامت" />
</header>
{/* بنرِ طراحیشده: گرادیانِ همخانواده، الگوی نقطهای و کاشیِ کج بهعنوانِ عنصرِ تزئینی. */}
<section aria-labelledby="rx-title" className="relative isolate overflow-hidden rounded-xl bg-[linear-gradient(135deg,var(--dig-green-600),var(--dig-green-800))] p-5 text-[var(--dig-white)] sm:p-8">
<span
aria-hidden
className="absolute inset-0 -z-10 bg-[radial-gradient(color-mix(in_oklab,var(--dig-white)_30%,var(--dig-green-800))_1.2px,transparent_1.3px)] [background-size:18px_18px] [mask-image:linear-gradient(to_left,black,transparent_70%)]"
/>
<span
aria-hidden
className="absolute -end-6 -bottom-8 flex size-36 rotate-[-12deg] items-center justify-center rounded-3xl bg-[color-mix(in_oklab,var(--dig-white)_16%,var(--dig-green-700))] text-[var(--dig-white)] sm:-end-2 sm:size-48 [&>svg]:size-16 sm:[&>svg]:size-24"
>
<FirstAidKitIcon />
</span>
<div className="flex max-w-sm flex-col items-start gap-3 pe-24 sm:pe-0">
<h1 id="rx-title" className="text-xl leading-snug font-black sm:text-3xl">
عکسِ نسخه را بفرست، داروخانه آماده میکند
</h1>
<p className="text-sm leading-6 text-[color-mix(in_oklab,var(--dig-white)_85%,var(--dig-green-800))]">قیمت در ۱۵ دقیقه اعلام میشود؛ بعد از تأییدِ تو پیک میآورد.</p>
<ControlSizeProvider size="lg" asChild>
<Button className="mt-1 bg-[var(--dig-white)] text-[var(--dig-green-900)] hover:bg-[color-mix(in_oklab,var(--dig-white)_90%,var(--dig-green-800))]">
<CloudUploadIcon />
بارگذاریِ نسخه
</Button>
</ControlSizeProvider>
</div>
</section>
<section aria-labelledby="cat-title" className="flex flex-col gap-4">
<SectionHeader id="cat-title" title="دستهها" />
<ul className="grid grid-cols-3 gap-2 sm:grid-cols-6 sm:gap-3">
{CATEGORIES.map((item) => (
<li key={item.id}>
<a
href="#"
className="flex h-full flex-col items-center gap-2 rounded-xl border border-border bg-card px-2 py-3 text-center text-xs leading-5 font-medium outline-none hover:border-[color-mix(in_oklab,var(--foreground)_30%,var(--border))] focus-visible:ring-[3px] focus-visible:ring-ring/50 sm:text-sm"
>
<span aria-hidden className="flex size-11 items-center justify-center rounded-lg [&>svg]:size-6" style={tint(item.hue)}>
{item.icon}
</span>
{item.label}
</a>
</li>
))}
</ul>
</section>
<section aria-labelledby="offers-title" className="flex flex-col gap-4">
<SectionHeader id="offers-title" title="پیشنهادهای ویژه" />
{/* موبایل: ردیفِ پیمایشی با کارتِ ۱۶۰ پیکسلی؛ دسکتاپ: گریدِ چهارستونه. */}
<ul className="-mx-4 flex snap-x gap-3 overflow-x-auto px-4 pb-1 [scrollbar-width:none] sm:mx-0 sm:grid sm:grid-cols-4 sm:overflow-visible sm:px-0">
{OFFERS.map((item) => (
<li key={item.id} className="w-40 shrink-0 snap-start sm:w-auto">
<ProductCard size="sm" className="h-full w-full">
<ProductCardMedia>
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img src={stockImages[item.image].src} alt={stockImages[item.image].alt} loading="lazy" />
</ProductCardMedia>
<ProductCardBody>
<ProductCardCategory>{item.brand}</ProductCardCategory>
<ProductCardTitle>{item.title}</ProductCardTitle>
{item.rx && (
<Badge variant="soft" color="warning" className="w-fit">
نیاز به نسخه
</Badge>
)}
<ProductCardPrice
amount={formatCurrency(item.price)}
originalAmount={item.oldPrice ? formatCurrency(item.oldPrice) : undefined}
discount={item.oldPrice ? `٪${toFaDigits(Math.round((1 - item.price / item.oldPrice) * 100))}` : undefined}
/>
</ProductCardBody>
<ProductCardFooter>
{item.rx ? (
<Button variant="outline" fullWidth>
ثبت با نسخه
</Button>
) : (
<AddToCart
fullWidth
label="افزودن"
itemName={item.title}
quantity={cart[item.id] ?? 0}
onQuantityChange={(quantity) => setCart((current) => ({ ...current, [item.id]: quantity }))}
/>
)}
</ProductCardFooter>
</ProductCard>
</li>
))}
</ul>
</section>
<section aria-labelledby="near-title" className="flex flex-col gap-4">
<SectionHeader id="near-title" title="داروخانههای نزدیک" />
<div className="grid grid-cols-1 gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<MapView label="نقشهٔ داروخانههای نزدیک" className="aspect-[2/1] lg:aspect-auto lg:h-full">
<MapMarker variant="you" x={50} y={60} label="موقعیتِ شما" />
{NEARBY.map((item) => (
<MapMarker key={item.id} x={item.x} y={item.y} label={item.name} active={active === item.id} onClick={() => setActive(item.id)} />
))}
</MapView>
<ul className="flex flex-col divide-y divide-border rounded-xl border border-border bg-card">
{NEARBY.map((item) => (
<li key={item.id}>
<a
href="#"
aria-current={active === item.id ? "true" : undefined}
onFocus={() => setActive(item.id)}
onMouseEnter={() => setActive(item.id)}
className="flex items-center gap-3 p-4 outline-none hover:bg-subtle focus-visible:bg-subtle aria-[current=true]:bg-primary-subtle"
>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate font-semibold">{item.name}</span>
<span className="flex flex-wrap items-center gap-x-2 text-sm text-muted-foreground">
{item.distance}
<span aria-hidden>·</span>
<span className={item.open ? "font-medium text-success" : "font-medium text-destructive"}>{item.open ? "باز" : "بسته"}</span>
<span>{item.hours}</span>
</span>
</span>
<ChevronLeftIcon aria-hidden className="size-5 shrink-0 text-muted-foreground" />
</a>
</li>
))}
</ul>
</div>
</section>
</main>
<BottomNavigation value={tab} onValueChange={setTab} indicator="pill" fixed={false} className="sticky bottom-0 sm:hidden">
<BottomNavigationItem value="home" icon={<DashboardIcon />} label="خانه" />
<BottomNavigationItem value="catalog" icon={<GridIcon />} label="دستهها" />
<BottomNavigationItem
value="cart"
icon={<ShoppingCartIcon />}
label="سبد"
badge={cartCount ? <Badge color="destructive">{toFaDigits(cartCount)}</Badge> : undefined}
/>
<BottomNavigationItem value="saved" icon={<HeartIcon />} label="ذخیرهها" />
<BottomNavigationItem value="profile" icon={<UserIcon />} label="حساب" />
</BottomNavigation>
</div>
</ControlSizeProvider>
)
}