بلاک‌ها · پشتیبانی و محتوا

نمای کارشناس: کنش‌های تیکت، پیامِ درخواست‌دهنده با پیوست‌های نوع‌دار، پاسخ‌نویس، و پنلِ جزئیات، درخواست‌دهنده و ویژگی‌های قابل‌ویرایش با دکمهٔ به‌روزرسانی.

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

این بلاک از ۱۴ آیتم دیگ استفاده می‌کند: avatarbadgebreadcrumbbuttoncardcontrol-sizedropdown-menufile-uploadiconslabelpersianselecttextareatoast

پکیج‌منیجر پیش‌فرض 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/breadcrumb.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/textarea.json https://docs.digdesign.ir/r/toast.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/breadcrumb.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/textarea.json https://docs.digdesign.ir/r/toast.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/breadcrumb.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/textarea.json https://docs.digdesign.ir/r/toast.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/breadcrumb.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/textarea.json https://docs.digdesign.ir/r/toast.json

۲. کد صفحه

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

ticket-detail.tsx
"use client"

import * as React from "react"

import { AvatarGenderIcon, ClockIcon, DownloadIcon, MoreHorizontalIcon, PaperclipIcon, TrashIcon, UserIcon } from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { FileTypeIcon } from "@/components/ui/file-upload"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { toast } from "@/components/ui/toast"
import { formatJalali, toFaDigits } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"

const ATTACHMENTS = [
  { name: "رسید-پرداخت.pdf", kind: "PDF", size: "۵۷۰ کیلوبایت" },
  { name: "تصاویر-خطا.zip", kind: "ZIP", size: "۲٫۴ مگابایت" },
]

const DETAILS = [
  { label: "شمارهٔ تیکت", value: <Badge color="destructive" variant="soft">#{toFaDigits(1042)}</Badge> },
  { label: "موضوع", value: "افزودن روشِ پرداخت" },
  { label: "مهلتِ اولین پاسخ", value: formatJalali(new Date("2026-10-06T13:48:00"), "long") },
  { label: "مهلتِ حل", value: formatJalali(new Date("2026-10-07T20:22:00"), "long") },
]

/**
 * جزئیاتِ تیکت از نگاهِ کارشناس: کنش‌ها بالای گفت‌وگو، پیامِ درخواست‌دهنده با
 * پیوست‌ها، پاسخ‌نویس پایینِ همان ستون، و پنلِ جزئیات و ویژگی‌ها کنارش. تغییرِ
 * ویژگی‌ها با دکمهٔ «به‌روزرسانی» ذخیره می‌شود تا تصادفی عوض نشود.
 */
export default function TicketDetailBlock() {
  const [reply, setReply] = React.useState("")
  const [dirty, setDirty] = React.useState(false)

  const send = () => {
    setReply("")
    toast.success("پاسخ برای مهسا کریمی فرستاده شد.")
  }

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto flex w-full max-w-6xl flex-col gap-6 p-4 sm:p-6">
        <Breadcrumb>
          <BreadcrumbList>
            <BreadcrumbItem>
              <BreadcrumbLink href="#">پشتیبانی</BreadcrumbLink>
            </BreadcrumbItem>
            <BreadcrumbSeparator />
            <BreadcrumbItem>
              <BreadcrumbPage>جزئیات تیکت</BreadcrumbPage>
            </BreadcrumbItem>
          </BreadcrumbList>
        </Breadcrumb>

        <div className="grid grid-cols-1 items-start gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]">
          <Card className="gap-0 py-0">
            {/* کنش‌های تیکت؛ کم‌کاربردها در منوی «بیشتر». */}
            <div className="flex flex-wrap items-center gap-2 border-b border-border p-4">
              <Button variant="outline">
                <UserIcon />
                ارجاع
              </Button>
              <Button variant="outline">بایگانی</Button>
              <Button variant="outline">علامتِ خوانده‌نشده</Button>
              <DropdownMenu>
                <DropdownMenuTrigger asChild>
                  <Button variant="outline" size="icon" aria-label="کنش‌های بیشتر">
                    <MoreHorizontalIcon />
                  </Button>
                </DropdownMenuTrigger>
                <DropdownMenuContent align="end">
                  <DropdownMenuItem>افزودنِ کار</DropdownMenuItem>
                  <DropdownMenuItem>ادغام با تیکتِ دیگر</DropdownMenuItem>
                  <DropdownMenuItem variant="destructive">
                    <TrashIcon />
                    هرزنامه
                  </DropdownMenuItem>
                </DropdownMenuContent>
              </DropdownMenu>
            </div>

            <div className="flex flex-col gap-8 p-6">
              {/* عنوان و شماره در شروع، زمانِ پیام در پایانِ همان ردیف. */}
              <div className="flex flex-wrap items-center justify-between gap-3">
                <h1 className="flex flex-wrap items-center gap-3 text-xl font-bold">
                  افزودن روشِ پرداخت
                  <Badge color="destructive" variant="soft">
                    #{toFaDigits(1042)}
                  </Badge>
                </h1>
                <span className="flex items-center gap-1.5 text-sm text-muted-foreground">
                  <ClockIcon aria-hidden className="size-4" />
                  {toFaDigits("10:34")} (۵ دقیقه پیش)
                </span>
              </div>

              {/* پیام: فرستنده و زمان یک ردیف، متن و پیوست‌ها زیرش. */}
              <article className="flex flex-col gap-6">
                <header className="flex items-start gap-3">
                  <Avatar size="lg" alt="مهسا کریمی" src={stockImages.personWoman1.src} icon={<AvatarGenderIcon gender="female" />} />
                  <div className="flex min-w-0 flex-1 flex-col gap-0.5 text-sm">
                    <span>
                      <span className="font-semibold">مهسا کریمی</span>{" "}
                      <bdi dir="ltr" className="text-muted-foreground">
                        mahsa.k@example.com
                      </bdi>
                    </span>
                    <span className="text-muted-foreground">به: تیمِ پشتیبانی</span>
                  </div>
                </header>
                <div className="flex flex-col gap-4 leading-8">
                  <p>سلام وقت بخیر،</p>
                  <p>
                    می‌خواهم کارتِ بانکی دومم را به‌عنوانِ روشِ پرداخت اضافه کنم، ولی بعد از واردکردنِ رمزِ پویا پیامِ «خطای
                    ناشناخته» می‌گیرم. رسید و تصویرِ خطا را پیوست کرده‌ام.
                  </p>
                  <p>
                    ممنون،
                    <br />
                    مهسا کریمی
                  </p>
                </div>
                <ul className="grid grid-cols-1 gap-3 sm:grid-cols-2">
                  {ATTACHMENTS.map((file) => (
                    <li key={file.name} className="flex items-center gap-3 rounded-xl border border-border p-3">
                      <FileTypeIcon kind={file.kind} className="size-10" />
                      <span className="flex min-w-0 flex-1 flex-col">
                        <bdi dir="auto" className="truncate text-sm font-medium">
                          {file.name}
                        </bdi>
                        <span className="text-xs text-muted-foreground">{file.size}</span>
                      </span>
                      <Button variant="ghost" size="icon" aria-label={`دانلودِ ${file.name}`}>
                        <DownloadIcon />
                      </Button>
                    </li>
                  ))}
                </ul>
              </article>

              {/* پاسخ‌نویس: برچسب، فیلدِ متن و کنش‌ها؛ ارسال تا متنی نباشد غیرفعال است. */}
              <div className="flex flex-col gap-2 border-t border-border pt-8">
                <Label htmlFor="ticket-reply" className="font-normal text-muted-foreground">
                  پاسخ به <span className="font-medium text-foreground">مهسا کریمی</span>
                </Label>
                <Textarea id="ticket-reply" rows={4} value={reply} onChange={(event) => setReply(event.target.value)} placeholder="پاسخ را بنویسید…" />
                <div className="flex items-center gap-2 pt-2">
                  <Button onClick={send} disabled={!reply.trim()}>
                    ارسال پاسخ
                  </Button>
                  <Button variant="ghost" size="icon" aria-label="پیوستِ فایل">
                    <PaperclipIcon />
                  </Button>
                  <Button variant="ghost" size="icon" aria-label="پاک کردنِ پیش‌نویس" className="ms-auto" disabled={!reply} onClick={() => setReply("")}>
                    <TrashIcon />
                  </Button>
                </div>
              </div>
            </div>
          </Card>

          {/* پنلِ کناری: جزئیات، درخواست‌دهنده و ویژگی‌های قابل‌ویرایش. */}
          <Card className="gap-0 p-6">
            <section aria-labelledby="ticket-details" className="flex flex-col gap-4">
              <h2 id="ticket-details" className="font-bold">
                جزئیات
              </h2>
              <dl className="flex flex-col gap-3 text-sm">
                {DETAILS.map((item) => (
                  <div key={item.label} className="flex items-center justify-between gap-3">
                    <dt className="text-muted-foreground">{item.label}</dt>
                    <dd className="text-end font-medium">{item.value}</dd>
                  </div>
                ))}
              </dl>
            </section>

            <section aria-labelledby="ticket-requester" className="mt-8 flex flex-col gap-4 border-t border-border pt-8">
              <h2 id="ticket-requester" className="font-bold">
                درخواست‌دهنده
              </h2>
              <dl className="flex flex-col gap-3 text-sm">
                <div className="flex items-center justify-between gap-3">
                  <dt className="text-muted-foreground">نام</dt>
                  <dd className="font-medium">مهسا کریمی</dd>
                </div>
                <div className="flex items-center justify-between gap-3">
                  <dt className="text-muted-foreground">ایمیل</dt>
                  <dd className="font-medium">
                    <bdi dir="ltr">mahsa.k@example.com</bdi>
                  </dd>
                </div>
              </dl>
            </section>

            <section aria-labelledby="ticket-props" className="mt-8 flex flex-col gap-4 border-t border-border pt-8">
              <h2 id="ticket-props" className="font-bold">
                ویژگی‌ها
              </h2>
              {[
                { id: "type", label: "نوع", value: "question", options: [["question", "پرسش"], ["bug", "خطا"], ["request", "درخواست"]] },
                { id: "status", label: "وضعیت", value: "open", options: [["open", "باز"], ["pending", "در انتظار مشتری"], ["solved", "حل‌شده"]] },
                { id: "priority", label: "اولویت", value: "low", options: [["low", "کم"], ["medium", "متوسط"], ["high", "زیاد"]] },
                { id: "assignee", label: "کارشناس", value: "nima", options: [["nima", "نیما رضایی"], ["leila", "لیلا احمدی"], ["arash", "آرش نوری"]] },
              ].map((field) => (
                <div key={field.id} className="flex flex-col gap-2">
                  <Label id={`ticket-${field.id}`}>{field.label}</Label>
                  <Select defaultValue={field.value} onValueChange={() => setDirty(true)}>
                    <SelectTrigger aria-labelledby={`ticket-${field.id}`} className="w-full">
                      <SelectValue />
                    </SelectTrigger>
                    <SelectContent>
                      {field.options.map(([value, text]) => (
                        <SelectItem key={value} value={value}>
                          {text}
                        </SelectItem>
                      ))}
                    </SelectContent>
                  </Select>
                </div>
              ))}
              <Button
                fullWidth
                className="mt-4"
                disabled={!dirty}
                onClick={() => {
                  setDirty(false)
                  toast.success("ویژگی‌های تیکت به‌روز شد.")
                }}
              >
                به‌روزرسانی
              </Button>
            </section>
          </Card>
        </div>
      </main>
    </ControlSizeProvider>
  )
}