پروفایلِ پزشک
پیشنمایش تمامصفحهپروفایلِ پزشک با عکس، تخصص، نظام پزشکیِ تأییدشده، امتیاز و آمار؛ زبانههای درباره، مطب روی نقشه با ساعتِ کاری، و نظرهای بیماران با توزیعِ امتیاز؛ نوعِ ویزیت (حضوری، تلفنی، متنی) با قیمت در ستونِ چسبان.
۱. نصب کامپوننتها
این بلاک از ۱۲ آیتم دیگ استفاده میکند: avatarbadgebuttoncommentcontrol-sizeiconsmap-viewpersianradio-cardradio-groupratingtabs
پکیجمنیجر پیشفرض 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/comment.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/radio-card.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/rating.json https://docs.digdesign.ir/r/tabs.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/comment.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/radio-card.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/rating.json https://docs.digdesign.ir/r/tabs.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/comment.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/radio-card.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/rating.json https://docs.digdesign.ir/r/tabs.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/comment.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/radio-card.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/rating.json https://docs.digdesign.ir/r/tabs.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
doctor-profile.tsx
"use client"
import * as React from "react"
import { AvatarGenderIcon, CalendarIcon, ClockIcon, LocationIcon, PhoneIcon, ShieldCheckIcon, StarIcon, ThumbsUpIcon } from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Comment, CommentList } from "@/components/ui/comment"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { MapMarker, MapView } from "@/components/ui/map-view"
import { RadioCard } from "@/components/ui/radio-card"
import { RadioGroup } from "@/components/ui/radio-group"
import { Rating } from "@/components/ui/rating"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { formatCurrency, formatRelativeTime, toFaDigits } from "@/lib/persian"
import { avatarPhoto, stockImages } from "@/lib/stock-images"
const VISITS = [
{ value: "clinic", title: "ویزیتِ حضوری در مطب", description: "نزدیکترین نوبت: امروز، ۱۶:۳۰", price: 850_000 },
{ value: "phone", title: "مشاورهٔ تلفنی", description: "۱۵ دقیقه · نزدیکترین نوبت: امروز، ۲۰:۰۰", price: 450_000 },
{ value: "chat", title: "مشاورهٔ متنی", description: "پاسخ تا ۶ ساعت، با امکانِ فرستادنِ آزمایش", price: 300_000 },
]
const STATS = [
{ value: "۱۸ سال", label: "سابقه" },
{ value: "۴٬۲۰۰+", label: "بیمار" },
{ value: "٪۹۶", label: "رضایت" },
]
const HOURS = [
{ day: "شنبه تا سهشنبه", time: "۱۶ تا ۲۰" },
{ day: "چهارشنبه", time: "۹ تا ۱۳" },
{ day: "پنجشنبه و جمعه", time: "تعطیل", closed: true },
]
const REVIEWS = [
{ id: 1, name: "نیلوفر صالحی", gender: "female" as const, rating: 5, text: "با حوصله توضیح دادند و نوار قلب همانجا گرفته شد. نوبت هم سرِ ساعت بود.", date: "2026-10-10T12:00:00", visit: "حضوری", likes: 14 },
{ id: 2, name: "امیر حسینی", gender: "male" as const, rating: 4, text: "مشاورهٔ تلفنی کوتاه ولی دقیق بود. کاش زمانش کمی بیشتر بود.", date: "2026-10-02T09:30:00", visit: "تلفنی", likes: 6 },
{ id: 3, name: "مهسا رحیمی", gender: "female" as const, rating: 5, text: "سه سال است پیششان میروم؛ آزمایشها را دقیق پیگیری میکنند.", date: "2026-09-21T18:10:00", visit: "حضوری", likes: 9 },
]
/**
* پروفایلِ پزشک: سرِ صفحه با عکس، نام، تخصص، شمارهٔ نظام پزشکی (تأییدشده)، امتیاز و سه آمار؛ زبانههای
* «درباره»، «مطب» (نشانی روی نقشه، ساعتِ کاری) و «نظرها» (توزیعِ امتیاز و نظرِ بیماران با نوعِ ویزیت). ستونِ
* چسبانِ نوبت: نوعِ ویزیت با RadioCard و قیمتِ هر کدام، نزدیکترین نوبت و دکمهٔ دریافت؛ در موبایل نوارِ پایین.
*/
export default function DoctorProfileBlock() {
const [visit, setVisit] = React.useState("clinic")
const [liked, setLiked] = React.useState<number[]>([])
const current = VISITS.find((item) => item.value === visit) ?? VISITS[0]
return (
<ControlSizeProvider size="xl" asChild>
<div className="mx-auto flex w-full max-w-6xl flex-col">
<main className="grid grid-cols-1 items-start gap-6 p-4 sm:p-6 lg:grid-cols-[minmax(0,1fr)_22rem] lg:gap-8">
<div className="flex min-w-0 flex-col gap-8">
<header className="flex flex-col gap-5 sm:flex-row sm:items-center">
<Avatar src={stockImages.doctorWomanCurly.src} alt="" name="سارا محمدی" className="size-24 shrink-0 sm:size-28" />
<div className="flex min-w-0 flex-col gap-2">
<h1 className="text-2xl leading-snug font-bold">دکتر سارا محمدی</h1>
<span className="text-muted-foreground">متخصصِ قلب و عروق · فلوشیپِ اکوکاردیوگرافی</span>
<span className="flex flex-wrap items-center gap-x-3 gap-y-2 text-sm">
<Badge variant="soft" color="success">
<ShieldCheckIcon aria-hidden />
ن.ن.پ ۱۲۳۴۵۶ · تأییدشده
</Badge>
<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">۴٫۸</span>
<span className="text-muted-foreground">(۴۱۲ نظر)</span>
</span>
</span>
</div>
</header>
<dl className="grid grid-cols-3 divide-x divide-border rounded-xl border border-border rtl:divide-x-reverse">
{STATS.map((stat) => (
<div key={stat.label} className="flex flex-col items-center gap-0.5 px-2 py-4 text-center">
<dt className="order-2 text-sm text-muted-foreground">{stat.label}</dt>
<dd className="order-1 text-lg font-bold tabular-nums">{stat.value}</dd>
</div>
))}
</dl>
<Tabs variant="line" defaultValue="about" className="gap-6">
<TabsList aria-label="اطلاعاتِ پزشک">
<TabsTrigger value="about">درباره</TabsTrigger>
<TabsTrigger value="clinic">مطب</TabsTrigger>
<TabsTrigger value="reviews">نظرها</TabsTrigger>
</TabsList>
<TabsContent value="about" className="flex flex-col gap-5 leading-8">
<p>
دکتر محمدی متخصصِ قلب و عروق از دانشگاهِ علوم پزشکیِ تهران است و فلوشیپِ اکوکاردیوگرافی را در بیمارستانِ شهید رجایی گذرانده. بیشترِ بیمارانشان برای فشارِ خون، تپشِ قلب
و چکاپِ پیش از ورزش مراجعه میکنند.
</p>
<div className="flex flex-col gap-3">
<h2 className="font-bold">خدمات</h2>
<ul className="flex flex-wrap gap-2">
{["اکوی قلب", "نوارِ قلب", "تستِ ورزش", "هولترِ فشار", "چکاپِ قلب"].map((service) => (
<li key={service}>
<Badge variant="soft" size="lg">
{service}
</Badge>
</li>
))}
</ul>
</div>
<div className="flex flex-col gap-2">
<h2 className="font-bold">بیمههای طرفِ قرارداد</h2>
<p className="text-muted-foreground">تأمین اجتماعی، سلامت، نیروهای مسلح، تکمیلیِ دی و آسیا</p>
</div>
</TabsContent>
<TabsContent value="clinic" className="flex flex-col gap-5">
<div className="flex flex-col gap-1">
<h2 className="font-bold">مطبِ ونک</h2>
<p className="text-muted-foreground">ونک، خیابانِ ملاصدرا، ساختمانِ پزشکانِ آرین، طبقهٔ ۴، واحدِ ۱۲</p>
</div>
<MapView label="مطبِ ونک روی نقشه" className="aspect-[16/8]">
<MapMarker x={52} y={56} label="مطب" tabIndex={-1} aria-hidden />
</MapView>
<div className="flex flex-wrap gap-2">
<ControlSizeProvider size="lg" mobileSize="default" asChild>
<div className="flex flex-wrap gap-2">
<Button variant="outline">
<LocationIcon />
مسیریابی
</Button>
<Button variant="outline" asChild>
<a href="tel:02188776655">
<PhoneIcon />
تماس با مطب
</a>
</Button>
</div>
</ControlSizeProvider>
</div>
<div className="flex flex-col gap-3">
<h2 className="flex items-center gap-2 font-bold">
<ClockIcon aria-hidden className="size-4 text-muted-foreground" />
ساعتِ کاری
</h2>
<dl className="flex flex-col divide-y divide-border rounded-xl border border-border text-sm">
{HOURS.map((row) => (
<div key={row.day} className="flex justify-between gap-3 px-4 py-3">
<dt>{row.day}</dt>
<dd className={row.closed ? "text-muted-foreground" : "font-medium tabular-nums"}>{row.time}</dd>
</div>
))}
</dl>
</div>
</TabsContent>
<TabsContent value="reviews" className="flex flex-col gap-6">
<div className="flex flex-col gap-5 rounded-xl bg-subtle p-5 sm:flex-row sm:items-center">
<div className="flex flex-col items-center gap-1 sm:px-4">
<span className="text-4xl font-black tabular-nums">۴٫۸</span>
<Rating value={4.8} readOnly size="sm" aria-label="امتیازِ ۴٫۸ از ۵" />
<span className="text-xs text-muted-foreground">از ۴۱۲ نظر</span>
</div>
<ul className="flex flex-1 flex-col gap-2 text-sm">
{[
{ label: "برخوردِ پزشک", value: 96 },
{ label: "وقتشناسی", value: 88 },
{ label: "توضیحِ کافی", value: 92 },
].map((row) => (
<li key={row.label} className="flex items-center gap-3">
<span className="w-24 shrink-0 text-muted-foreground">{row.label}</span>
<span className="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">
<span className="block h-full rounded-full bg-success" style={{ width: `${row.value}%` }} />
</span>
<span className="w-9 shrink-0 text-end font-medium tabular-nums">٪{toFaDigits(row.value)}</span>
</li>
))}
</ul>
</div>
<CommentList>
{REVIEWS.map((review) => {
const isLiked = liked.includes(review.id)
return (
<Comment
key={review.id}
avatar={<Avatar name={review.name} src={avatarPhoto(review.name, review.gender)} icon={<AvatarGenderIcon gender={review.gender} />} />}
author={review.name}
badge={<Rating value={review.rating} readOnly size="sm" aria-label={`${toFaDigits(review.rating)} از ۵`} />}
time={`${formatRelativeTime(review.date)} · ویزیتِ ${review.visit}`}
dateTime={review.date}
actions={
<Button
variant="ghost"
aria-pressed={isLiked}
aria-label={`مفید بود، نظرِ ${review.name}`}
className={isLiked ? "text-primary" : "text-muted-foreground"}
onClick={() => setLiked((list) => (isLiked ? list.filter((id) => id !== review.id) : [...list, review.id]))}
>
<ThumbsUpIcon filled={isLiked} />
مفید بود · {toFaDigits(review.likes + (isLiked ? 1 : 0))}
</Button>
}
>
{review.text}
</Comment>
)
})}
</CommentList>
</TabsContent>
</Tabs>
</div>
{/* ستونِ نوبت؛ در دسکتاپ کنارِ صفحه میچسبد. در موبایل خلاصهاش نوارِ پایین است. */}
<aside aria-labelledby="visit-title" className="flex flex-col gap-4 rounded-xl border border-border bg-card p-5 lg:sticky lg:top-6">
<h2 id="visit-title" className="font-bold">
نوعِ ویزیت
</h2>
<RadioGroup value={visit} onValueChange={setVisit} aria-labelledby="visit-title" className="gap-2">
{VISITS.map((item) => (
<RadioCard
key={item.value}
value={item.value}
title={item.title}
description={item.description}
aside={<span className="font-semibold tabular-nums">{formatCurrency(item.price)}</span>}
/>
))}
</RadioGroup>
<Button fullWidth className="max-lg:hidden">
<CalendarIcon />
انتخابِ زمانِ نوبت
</Button>
<p className="text-center text-xs text-muted-foreground max-lg:hidden">لغوِ رایگان تا ۲ ساعت پیش از نوبت</p>
</aside>
</main>
<div className="sticky bottom-0 flex items-center gap-3 border-t border-border bg-background px-4 py-3 lg:hidden">
<span className="flex min-w-0 flex-col">
<span className="truncate text-xs text-muted-foreground">{current.title}</span>
<span className="font-bold tabular-nums">{formatCurrency(current.price)}</span>
</span>
<Button className="flex-1">انتخابِ زمانِ نوبت</Button>
</div>
</div>
</ControlSizeProvider>
)
}