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

خطا در نتایجِ بلیت

پیش‌نمایش تمام‌صفحه

وقتی جست‌وجوی بلیت خطا می‌دهد یا برای آن روز چیزی نیست: خلاصهٔ جست‌وجو، نوارِ روزها، حالتِ خطا با «تلاشِ دوباره» و اسکلتِ بارگذاری، پیشنهادِ روزها و مسیرهای نزدیک، و پرسش‌های پرتکرار.

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

این بلاک از ۹ آیتم دیگ استفاده می‌کند: accordionbuttoncontrol-sizeempty-statefare-date-stripiconspersianskeletontoast

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/accordion.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/fare-date-strip.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/skeleton.json https://docs.digdesign.ir/r/toast.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/accordion.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/fare-date-strip.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/skeleton.json https://docs.digdesign.ir/r/toast.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/accordion.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/fare-date-strip.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/skeleton.json https://docs.digdesign.ir/r/toast.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/accordion.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/fare-date-strip.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/skeleton.json https://docs.digdesign.ir/r/toast.json

۲. کد صفحه

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

ticket-results-error.tsx
"use client"

import * as React from "react"

import { AlertTriangleIcon, EditIcon, PlaneIcon, RefreshIcon, SearchIcon } from "@/components/icons"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { EmptyState, EmptyStateActions, EmptyStateDescription, EmptyStateIcon, EmptyStateTitle } from "@/components/ui/empty-state"
import { FareDateStrip, type FareDay } from "@/components/ui/fare-date-strip"
import { Skeleton } from "@/components/ui/skeleton"
import { toast } from "@/components/ui/toast"
import { formatCurrency } from "@/lib/persian"

type Status = "error" | "loading" | "empty"

/** سه‌شنبه ۲۸ مهر ۱۴۰۵؛ ثابت تا عکسِ گالری هر بار یکی باشد. */
const BASE = new Date(2026, 9, 20)
/** تقویمِ قیمت هم از همان سرویس می‌آید؛ وقتی خطا داده، روزها بی‌قیمت‌اند. */
const DAYS: FareDay[] = [-3, -2, -1, 0, 1, 2, 3].map((offset) => ({ date: new Date(BASE.getFullYear(), BASE.getMonth(), BASE.getDate() + offset), price: null }))
/** پیشنهادهای جایگزین وقتی این روز پروازی ندارد. */
const NEARBY = [
  { label: "پنجشنبه ۳۰ مهر", hint: "۶ پرواز", price: 2480000 },
  { label: "شنبه ۲ آبان", hint: "۹ پرواز", price: 2350000 },
]
const FAQ = [
  { q: "چرا نتیجه‌ای نمایش داده نمی‌شود؟", a: "گاهی سامانهٔ ایرلاین‌ها پاسخ نمی‌دهد یا برای آن روز هنوز پروازی ثبت نشده. چند دقیقهٔ بعد دوباره جست‌وجو کنید یا روزِ نزدیک را ببینید." },
  { q: "اگر پول کم شد و بلیت صادر نشد چه کنم؟", a: "مبلغ حداکثر تا ۷۲ ساعت خودکار به کارتتان برمی‌گردد. اگر برنگشت، با شمارهٔ پیگیری به پشتیبانی پیام بدهید." },
  { q: "قیمتِ بلیتِ هواپیمای تهران به مشهد چقدر است؟", a: "در روزهای عادی از حدودِ ۲ میلیون و ۳۵۰ هزار تومان شروع می‌شود و در تعطیلات بیشتر است." },
  { q: "استردادِ بلیت چطور است؟", a: "از «سفرهای من» لغو کنید؛ جریمه طبقِ قانونِ ایرلاین و زمانِ باقی‌مانده تا پرواز کم می‌شود." },
]
const dayLabel = new Intl.DateTimeFormat("fa-IR-u-ca-persian", { weekday: "long", day: "numeric", month: "long" })

/**
 * وقتی جست‌وجوی بلیت به نتیجه نمی‌رسد: خلاصهٔ جست‌وجو و نوارِ روزها می‌مانند تا کاربر گم نشود، جای فهرست
 * حالتِ خطا با «تلاشِ دوباره» (که اسکلتِ بارگذاری نشان می‌دهد و بعد به «پروازی نیست» می‌رسد) و برای
 * بی‌نتیجه بودن، روزهای نزدیک با قیمت پیشنهاد می‌شوند. پرسش‌های پرتکرار زیرِ همه.
 */
export default function TicketResultsErrorBlock() {
  const [status, setStatus] = React.useState<Status>("error")
  const [date, setDate] = React.useState(BASE)
  const timer = React.useRef<number | undefined>(undefined)
  React.useEffect(() => () => window.clearTimeout(timer.current), [])

  const retry = (next: Status) => {
    setStatus("loading")
    window.clearTimeout(timer.current)
    timer.current = window.setTimeout(() => setStatus(next), 900)
  }

  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-4 rounded-xl border bg-card p-4 shadow-card sm:flex-row sm:items-center sm:p-5">
          <span aria-hidden className="hidden size-12 shrink-0 items-center justify-center rounded-lg bg-primary-subtle text-primary-subtle-foreground sm:flex [&>svg]:size-6">
            <PlaneIcon />
          </span>
          <div className="flex min-w-0 flex-1 flex-col gap-1">
            <h1 className="text-lg leading-snug font-black sm:text-xl">بلیتِ هواپیمای تهران به مشهد</h1>
            <p className="text-sm text-muted-foreground">{dayLabel.format(date)} · ۱ بزرگسال · اکونومی</p>
          </div>
          <ControlSizeProvider size="lg" mobileSize="default" asChild>
            <div className="flex">
              <Button variant="outline" onClick={() => toast("فرمِ جست‌وجو با همین سفر باز می‌شود.")} className="w-full sm:w-auto">
                <EditIcon />
                ویرایشِ جست‌وجو
              </Button>
            </div>
          </ControlSizeProvider>
        </header>

        <FareDateStrip
          days={DAYS}
          value={date}
          onValueChange={(next) => {
            setDate(next)
            retry("empty")
          }}
          aria-label="روزِ پرواز"
          className="-m-1"
        />

        <section aria-live="polite" aria-busy={status === "loading"} aria-label="نتیجهٔ جست‌وجو">
          {status === "loading" ? (
            <div className="flex flex-col gap-4">
              <span className="sr-only">در حالِ جست‌وجوی دوباره…</span>
              {[0, 1, 2].map((key) => (
                <Skeleton key={key} className="h-40 rounded-xl" />
              ))}
            </div>
          ) : status === "error" ? (
            <EmptyState variant="bordered" size="lg">
              <EmptyStateIcon className="bg-destructive-subtle text-destructive-subtle-foreground">
                <AlertTriangleIcon />
              </EmptyStateIcon>
              <EmptyStateTitle>نتیجه‌ها به دستمان نرسید</EmptyStateTitle>
              <EmptyStateDescription>سامانهٔ ایرلاین‌ها پاسخ نداد. دوباره جست‌وجو کنید؛ اگر باز هم تکرار شد چند دقیقهٔ بعد سر بزنید.</EmptyStateDescription>
              <EmptyStateActions>
                <Button onClick={() => retry("empty")}>
                  <RefreshIcon />
                  تلاشِ دوباره
                </Button>
              </EmptyStateActions>
              <p className="text-xs text-muted-foreground">
                کدِ پیگیری: <span dir="ltr">SR-48213</span>
              </p>
            </EmptyState>
          ) : (
            <div className="flex flex-col gap-6">
              <EmptyState variant="bordered">
                <EmptyStateIcon>
                  <SearchIcon />
                </EmptyStateIcon>
                <EmptyStateTitle>برای {dayLabel.format(date)} پروازی نیست</EmptyStateTitle>
                <EmptyStateDescription>همهٔ پروازهای این روز پر شده یا هنوز برنامه‌ای ثبت نشده. روزهای نزدیک را ببینید.</EmptyStateDescription>
              </EmptyState>
              <div className="flex flex-col gap-3">
                <h2 className="font-bold">نزدیک‌ترین روزها با پرواز</h2>
                <ul className="grid gap-3 sm:grid-cols-2">
                  {NEARBY.map((item) => (
                    <li key={item.label} className="flex items-center justify-between gap-3 rounded-xl border bg-card p-4">
                      <span className="flex flex-col gap-1">
                        <span className="font-bold">{item.label}</span>
                        <span className="text-sm text-muted-foreground">
                          {item.hint}، از {formatCurrency(item.price)}
                        </span>
                      </span>
                      <ControlSizeProvider size="lg" mobileSize="default" asChild>
                        <Button variant="outline" onClick={() => toast.success(`نتایجِ ${item.label} باز می‌شود.`)} aria-label={`دیدنِ پروازهای ${item.label}`}>
                          دیدنِ پروازها
                        </Button>
                      </ControlSizeProvider>
                    </li>
                  ))}
                </ul>
              </div>
            </div>
          )}
        </section>

        <section aria-labelledby="error-faq" className="flex flex-col gap-3 pt-4">
          <h2 id="error-faq" className="text-lg leading-snug font-black">
            پرسش‌های پرتکرار
          </h2>
          <Accordion type="single" collapsible variant="bordered">
            {FAQ.map((item) => (
              <AccordionItem key={item.q} value={item.q}>
                <AccordionTrigger className="text-base font-semibold">{item.q}</AccordionTrigger>
                <AccordionContent>{item.a}</AccordionContent>
              </AccordionItem>
            ))}
          </Accordion>
        </section>
      </main>
    </ControlSizeProvider>
  )
}