بلاک‌ها · رزرو و خدمات

پروفایلِ پزشک با عکس، تخصص، نظام پزشکیِ تأییدشده، امتیاز و آمار؛ زبانه‌های درباره، مطب روی نقشه با ساعتِ کاری، و نظرهای بیماران با توزیعِ امتیاز؛ نوعِ ویزیت (حضوری، تلفنی، متنی) با قیمت در ستونِ چسبان.

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

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