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