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

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

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

صفحهٔ اصلیِ اپِ داروخانه: نشانیِ ارسال و سبد، جست‌وجوی دارو، بنرِ سفارش با عکسِ نسخه، دسته‌ها، پیشنهادهای ویژه با افزودن به سبد و داروخانه‌های نزدیک روی نقشه.

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

این بلاک از ۱۰ آیتم دیگ استفاده می‌کند: 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.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/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
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/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
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/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>
  )
}