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

داروخانه‌های نزدیک

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

داروخانه‌های نزدیک روی نقشه و فهرست با انتخابِ مشترک، فیلترِ الان باز، شبانه‌روزی و طرفِ قرارداد بیمه، فاصله، وضعیتِ باز یا بسته با ساعت، مسیریابی و تماس، و حالتِ بدونِ نتیجه.

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

این بلاک از ۹ آیتم دیگ استفاده می‌کند: badgebuttoncontrol-sizeempty-stateiconsmap-viewpersiansearch-fieldtoggle-group

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

۲. کد صفحه

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

pharmacy-nearby.tsx
"use client"

import * as React from "react"

import { ClockIcon, LocationIcon, PhoneIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { EmptyState, EmptyStateActions, EmptyStateDescription, EmptyStateTitle } from "@/components/ui/empty-state"
import { MapMarker, MapView, MapViewOverlay } from "@/components/ui/map-view"
import { SearchField } from "@/components/ui/search-field"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { toFaDigits, toSearchKey } from "@/lib/persian"

type Pharmacy = { id: string; name: string; address: string; km: number; open: boolean; hours: string; allDay?: boolean; insurance: boolean; x: number; y: number }
const PHARMACIES: Pharmacy[] = [
  { id: "1", name: "داروخانهٔ شبانه‌روزیِ آزادی", address: "خیابانِ آزادی، نبشِ بهبودی، پلاکِ ۱۲۰", km: 0.8, open: true, hours: "شبانه‌روزی", allDay: true, insurance: true, x: 34, y: 42 },
  { id: "2", name: "داروخانهٔ دکتر صالحی", address: "میدانِ ونک، کوچهٔ سوم، پلاکِ ۴", km: 1.4, open: true, hours: "باز تا ۲۳", insurance: true, x: 64, y: 30 },
  { id: "3", name: "داروخانهٔ سینا", address: "بلوارِ کشاورز، نبشِ فلسطین", km: 2.1, open: false, hours: "از ۸ صبح باز می‌شود", insurance: false, x: 78, y: 62 },
  { id: "4", name: "داروخانهٔ دکتر نیک‌نام", address: "خیابانِ ستارخان، روبه‌روی پارک", km: 2.9, open: true, hours: "باز تا ۲۱", insurance: true, x: 18, y: 70 },
]
const FILTERS = [
  { value: "all", label: "همه" },
  { value: "open", label: "الان باز" },
  { value: "allday", label: "شبانه‌روزی" },
  { value: "insurance", label: "بیمه" },
]
const km = (value: number) => `${toFaDigits(String(value).replace(".", "٫"))} کیلومتر`

/**
 * داروخانه‌های نزدیک: جست‌وجو، فیلترهای پرتکرار (الان باز، شبانه‌روزی، بیمه)، نقشه و فهرست با یک انتخابِ
 * مشترک، و برای هر داروخانه فاصله، وضعیتِ باز یا بسته با ساعت (رنگ و متن با هم)، مسیریابی و تماس. نتیجهٔ
 * خالی راهِ برگشت دارد. در موبایل نقشه بالای فهرست، در دسکتاپ کنارِ آن و چسبان.
 */
export default function PharmacyNearbyBlock() {
  const [query, setQuery] = React.useState("")
  const [filter, setFilter] = React.useState("open")
  const [active, setActive] = React.useState("1")
  const key = toSearchKey(query)
  const visible = PHARMACIES.filter(
    (item) =>
      (!key || toSearchKey(item.name + item.address).includes(key)) &&
      (filter === "all" || (filter === "open" ? item.open : filter === "allday" ? item.allDay : item.insurance))
  )
  const selected = visible.find((item) => item.id === active) ?? visible[0]

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6">
        <header className="flex flex-col gap-1">
          <h1 className="text-2xl leading-snug font-bold">داروخانه‌های نزدیک</h1>
          <p className="text-sm text-muted-foreground">
            {toFaDigits(visible.length)} داروخانه در شعاعِ ۳ کیلومتریِ <span className="font-medium text-foreground">خیابانِ آزادی</span>
          </p>
        </header>

        <div className="flex flex-col gap-3">
          <SearchField aria-label="جست‌وجوی داروخانه" placeholder="نامِ داروخانه یا خیابان" value={query} onValueChange={setQuery} />
          {/* چهار گزینهٔ کوتاه که بی‌اسکرول در عرضِ گوشی جا می‌شوند؛ گزینهٔ بریده‌شده در لبه دیده نمی‌شد. */}
          <div>
            <ControlSizeProvider size="lg" mobileSize="default" asChild>
              <ToggleGroup type="single" variant="segmented" fullWidth value={filter} onValueChange={(value) => value && setFilter(value)} aria-label="فیلترِ داروخانه‌ها" className="sm:w-max">
                {FILTERS.map((item) => (
                  <ToggleGroupItem key={item.value} value={item.value}>
                    {item.label}
                  </ToggleGroupItem>
                ))}
              </ToggleGroup>
            </ControlSizeProvider>
          </div>
        </div>

        {visible.length === 0 ? (
          <EmptyState variant="bordered">
            <EmptyStateTitle>داروخانه‌ای با این شرایط پیدا نشد</EmptyStateTitle>
            <EmptyStateDescription>فیلتر را بردار یا نامِ خیابانِ دیگری را جست‌وجو کن؛ داروخانه‌های شبانه‌روزی همیشه در فهرستِ «همه» هستند.</EmptyStateDescription>
            <EmptyStateActions>
              <Button
                variant="outline"
                onClick={() => {
                  setFilter("all")
                  setQuery("")
                }}
              >
                نمایشِ همهٔ داروخانه‌ها
              </Button>
            </EmptyStateActions>
          </EmptyState>
        ) : (
          <div className="grid grid-cols-1 items-start gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)]">
            <MapView label="نقشهٔ داروخانه‌های نزدیک" className="aspect-[16/10] lg:sticky lg:top-6 lg:order-last lg:aspect-[4/3]">
              <MapMarker variant="you" x={48} y={54} label="موقعیتِ شما" />
              {visible.map((item) => (
                <MapMarker key={item.id} x={item.x} y={item.y} label={item.name} active={selected?.id === item.id} onClick={() => setActive(item.id)} />
              ))}
              {selected && (
                <MapViewOverlay className="flex items-center justify-between gap-3 rounded-lg border border-border bg-card px-3 py-2 shadow-card lg:hidden">
                  <span className="min-w-0 truncate text-sm font-semibold">{selected.name}</span>
                  <span className="shrink-0 text-xs text-muted-foreground tabular-nums">{km(selected.km)}</span>
                </MapViewOverlay>
              )}
            </MapView>

            <ul className="flex flex-col gap-3" aria-label="فهرستِ داروخانه‌ها">
              {visible.map((item) => {
                const isActive = selected?.id === item.id
                return (
                  <li
                    key={item.id}
                    data-active={isActive}
                    onClick={() => setActive(item.id)}
                    className="flex flex-col gap-3 rounded-xl border border-border bg-card p-4 transition-colors data-[active=true]:border-primary data-[active=true]:bg-primary-subtle"
                  >
                    <div className="flex items-start justify-between gap-3">
                      <span className="flex min-w-0 flex-col gap-1">
                        <a href="#" onFocus={() => setActive(item.id)} className="leading-snug font-bold hover:underline">
                          {item.name}
                        </a>
                        <span className="text-sm text-muted-foreground">{item.address}</span>
                      </span>
                      <span className="shrink-0 text-sm font-semibold tabular-nums">{km(item.km)}</span>
                    </div>
                    <div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-sm">
                      {/* وضعیت هم با رنگ و هم با متن؛ رنگ تنها حاملِ معنا نیست. */}
                      <span className={`flex items-center gap-1 font-medium ${item.open ? "text-success" : "text-destructive"}`}>
                        <ClockIcon aria-hidden className="size-4" />
                        {item.open ? "باز" : "بسته"}
                        <span className="font-normal text-muted-foreground">· {item.hours}</span>
                      </span>
                      {item.insurance && (
                        <Badge variant="soft" color="info">
                          طرفِ قرارداد بیمه
                        </Badge>
                      )}
                    </div>
                    <ControlSizeProvider size="lg" mobileSize="default" asChild>
                      <div className="mt-1 grid grid-cols-2 gap-2">
                        <Button variant="outline">
                          <LocationIcon />
                          مسیریابی
                        </Button>
                        <Button variant="outline" asChild>
                          <a href="tel:02188776655" aria-label={`تماس با ${item.name}`}>
                            <PhoneIcon />
                            تماس
                          </a>
                        </Button>
                      </div>
                    </ControlSizeProvider>
                  </li>
                )
              })}
            </ul>
          </div>
        )}
      </main>
    </ControlSizeProvider>
  )
}