فهرستِ پزشکان
پیشنمایش تمامصفحهجستوجوی پزشک با نام، تخصص یا بیماری، چیپِ تخصص، نوبتِ امروز و ویزیتِ آنلاین، مرتبسازی؛ کارتِ پزشک با عکس، شمارهٔ نظام پزشکی، امتیاز، مطب و فاصله و نزدیکترین نوبت، و «خبرم کن» برای پزشکِ بینوبت.
۱. نصب کامپوننتها
این بلاک از ۱۱ آیتم دیگ استفاده میکند: avatarbadgebuttoncontrol-sizeempty-stateiconspersiansearch-fieldselectswitchtoggle-group
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/avatar.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/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/switch.json https://docs.digdesign.ir/r/toggle-group.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.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/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/switch.json https://docs.digdesign.ir/r/toggle-group.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.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/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/switch.json https://docs.digdesign.ir/r/toggle-group.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/avatar.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/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/switch.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
doctor-list.tsx
"use client"
import * as React from "react"
import { CalendarIcon, ChatIcon, LocationIcon, StarIcon } from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
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 { SearchField } from "@/components/ui/search-field"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { toFaDigits, toSearchKey } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"
type Doctor = {
id: string
name: string
specialty: string
group: string
photo: StockImageName
council: string
rating: number
reviews: number
clinic: string
km: number
online: boolean
/** نزدیکترین نوبت؛ dayOffset صفر یعنی امروز. */
next: { dayOffset: number; time: string } | null
}
const DOCTORS: Doctor[] = [
{ id: "1", name: "دکتر سارا محمدی", specialty: "متخصصِ قلب و عروق", group: "heart", photo: "doctorWomanCurly", council: "۱۲۳۴۵۶", rating: 4.8, reviews: 412, clinic: "ونک، ساختمانِ پزشکانِ آرین", km: 1.2, online: true, next: { dayOffset: 0, time: "۱۶:۳۰" } },
{ id: "2", name: "دکتر علی رضایی", specialty: "پزشکِ عمومی", group: "general", photo: "doctorManGlasses", council: "۲۳۴۵۶۷", rating: 4.6, reviews: 289, clinic: "یوسفآباد، کلینیکِ سینا", km: 2.4, online: true, next: { dayOffset: 1, time: "۱۰:۰۰" } },
{ id: "3", name: "دکتر مریم کاظمی", specialty: "متخصصِ پوست و مو", group: "skin", photo: "doctorWomanRed", council: "۳۴۵۶۷۸", rating: 4.9, reviews: 530, clinic: "سعادتآباد، کلینیکِ پوستِ نیکان", km: 4.1, online: false, next: { dayOffset: 3, time: "۱۱:۳۰" } },
{ id: "4", name: "دکتر رضا احمدی", specialty: "متخصصِ کودکان", group: "kids", photo: "doctorManScrubs", council: "۴۵۶۷۸۹", rating: 4.7, reviews: 198, clinic: "پاسداران، کلینیکِ آوا", km: 3.0, online: true, next: { dayOffset: 0, time: "۱۸:۰۰" } },
{ id: "5", name: "دکتر کامران نوری", specialty: "متخصصِ چشم", group: "eye", photo: "doctorManOffice", council: "۵۶۷۸۹۰", rating: 4.5, reviews: 147, clinic: "ولیعصر، بیمارستانِ نور", km: 5.6, online: false, next: null },
]
const GROUPS = [
{ value: "all", label: "همه" },
{ value: "general", label: "عمومی" },
{ value: "heart", label: "قلب" },
{ value: "skin", label: "پوست" },
{ value: "kids", label: "کودکان" },
{ value: "eye", label: "چشم" },
]
const dayLabel = (offset: number) => (offset === 0 ? "امروز" : offset === 1 ? "فردا" : `${toFaDigits(offset)} روزِ دیگر`)
/**
* فهرستِ پزشکان برای نوبتدهیِ آنلاین: جستوجو با نام، تخصص یا بیماری، چیپِ تخصص، «فقط نوبتِ امروز» و
* «ویزیتِ آنلاین»، و مرتبسازی. کارتِ هر پزشک عکس، تخصص و شمارهٔ نظام پزشکی (برای اعتماد)، امتیاز با تعدادِ
* نظر، مطب و فاصله، و نزدیکترین نوبت را دارد؛ نوبتِ امروز سبز است. پزشکِ بینوبت دکمهٔ «خبرم کن» دارد، نه
* دکمهٔ غیرفعال. نتیجهٔ خالی راهِ برگشت دارد.
*/
export default function DoctorListBlock() {
const [query, setQuery] = React.useState("")
const [group, setGroup] = React.useState("all")
const [today, setToday] = React.useState(false)
const [online, setOnline] = React.useState(false)
const [sort, setSort] = React.useState("soonest")
const key = toSearchKey(query)
const visible = DOCTORS.filter(
(doctor) =>
(!key || toSearchKey(doctor.name + doctor.specialty).includes(key)) &&
(group === "all" || doctor.group === group) &&
(!today || doctor.next?.dayOffset === 0) &&
(!online || doctor.online)
).sort((a, b) => (sort === "rating" ? b.rating - a.rating : sort === "nearest" ? a.km - b.km : (a.next?.dayOffset ?? 99) - (b.next?.dayOffset ?? 99)))
const reset = () => {
setQuery("")
setGroup("all")
setToday(false)
setOnline(false)
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-5xl flex-col gap-6 p-4 sm:p-6">
<header className="flex flex-col gap-1">
<h1 className="text-2xl leading-snug font-bold sm:text-3xl">نوبتِ پزشک</h1>
<p className="text-muted-foreground">از بیش از ۴۰۰ پزشکِ تهران، بیتماس و بیصف.</p>
</header>
<div className="flex flex-col gap-4">
<SearchField aria-label="جستوجوی پزشک" placeholder="نامِ پزشک، تخصص یا بیماری" value={query} onValueChange={setQuery} />
{/* تخصصها چیپاند (۳۶، در گوشی ۳۲) و میشکنند؛ گزینهای در لبه بریده نمیشود. */}
<ControlSizeProvider size="default" mobileSize="sm" asChild>
<ToggleGroup type="single" variant="outline" separated value={group} onValueChange={(value) => value && setGroup(value as string)} aria-label="تخصص" className="flex-wrap gap-2">
{GROUPS.map((item) => (
<ToggleGroupItem key={item.value} value={item.value} className="rounded-full! px-3.5 text-sm">
{item.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</ControlSizeProvider>
<div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-3">
<div className="flex flex-wrap items-center gap-x-6 gap-y-3 text-sm">
<label className="flex cursor-pointer items-center gap-2">
<Switch checked={today} onCheckedChange={setToday} />
فقط نوبتِ امروز
</label>
<label className="flex cursor-pointer items-center gap-2">
<Switch checked={online} onCheckedChange={setOnline} />
ویزیتِ آنلاین
</label>
</div>
<ControlSizeProvider size="lg" mobileSize="default" asChild>
<div className="flex items-center gap-2 text-sm">
<span id="doctor-sort" className="text-muted-foreground">
مرتبسازی
</span>
<Select value={sort} onValueChange={setSort}>
<SelectTrigger aria-labelledby="doctor-sort" className="w-auto min-w-36">
<SelectValue />
</SelectTrigger>
<ControlSizeProvider size="xl" asChild>
<SelectContent>
<SelectItem value="soonest">نزدیکترین نوبت</SelectItem>
<SelectItem value="rating">بیشترین امتیاز</SelectItem>
<SelectItem value="nearest">نزدیکترین مطب</SelectItem>
</SelectContent>
</ControlSizeProvider>
</Select>
</div>
</ControlSizeProvider>
</div>
</div>
<p className="text-sm text-muted-foreground" aria-live="polite">
{toFaDigits(visible.length)} پزشک
</p>
{visible.length === 0 ? (
<EmptyState variant="bordered">
<EmptyStateTitle>پزشکی با این شرایط پیدا نشد</EmptyStateTitle>
<EmptyStateDescription>فیلترها را کم کن یا نامِ تخصص را بنویس؛ مثلاً «قلب» بهجای نامِ بیماری.</EmptyStateDescription>
<EmptyStateActions>
<Button variant="outline" onClick={reset}>
پاککردنِ فیلترها
</Button>
</EmptyStateActions>
</EmptyState>
) : (
<ul className="flex flex-col gap-3">
{visible.map((doctor) => (
<li key={doctor.id} className="flex flex-col gap-4 rounded-xl border border-border bg-card p-4 sm:flex-row sm:items-center sm:gap-5 sm:p-5">
<div className="flex min-w-0 flex-1 gap-4">
<Avatar src={stockImages[doctor.photo].src} alt="" name={doctor.name} className="size-16 shrink-0 sm:size-20" />
<div className="flex min-w-0 flex-col gap-1.5">
<a href="#" className="leading-snug font-bold hover:underline">
{doctor.name}
</a>
<span className="text-sm text-muted-foreground">
{doctor.specialty} · ن.ن.پ {doctor.council}
</span>
<span className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
<span className="flex items-center gap-1">
<StarIcon aria-hidden className="size-4 fill-[var(--dig-amber-400)] text-[var(--dig-amber-400)]" />
<span className="font-medium">{toFaDigits(String(doctor.rating).replace(".", "٫"))}</span>
<span className="text-muted-foreground">({toFaDigits(doctor.reviews)} نظر)</span>
</span>
{doctor.online && (
<Badge variant="soft" color="info">
<ChatIcon aria-hidden />
ویزیتِ آنلاین
</Badge>
)}
</span>
<span className="flex items-center gap-1 text-sm text-muted-foreground">
<LocationIcon aria-hidden className="size-4 shrink-0" />
<span className="truncate">{doctor.clinic}</span>
<span className="shrink-0">· {toFaDigits(String(doctor.km).replace(".", "٫"))} کیلومتر</span>
</span>
</div>
</div>
{/* ستونِ نوبت: در موبایل زیرِ اطلاعات با خطِ جداکننده، در دسکتاپ کنارش. */}
<div className="flex items-center justify-between gap-3 border-t border-border pt-4 sm:w-52 sm:flex-col sm:items-stretch sm:border-t-0 sm:border-s sm:ps-5 sm:pt-0">
{doctor.next ? (
<span className="flex flex-col gap-0.5 text-sm">
<span className="text-muted-foreground">نزدیکترین نوبت</span>
<span className={`flex items-center gap-1.5 font-semibold ${doctor.next.dayOffset === 0 ? "text-success" : ""}`}>
<CalendarIcon aria-hidden className="size-4" />
{dayLabel(doctor.next.dayOffset)}، {doctor.next.time}
</span>
</span>
) : (
<span className="text-sm text-muted-foreground">نوبتِ خالی در این هفته نیست</span>
)}
<ControlSizeProvider size="lg" mobileSize="default" asChild>
{doctor.next ? <Button>دریافتِ نوبت</Button> : <Button variant="outline">نوبت باز شد خبرم کن</Button>}
</ControlSizeProvider>
</div>
</li>
))}
</ul>
)}
</main>
</ControlSizeProvider>
)
}