خطا در نتایجِ بلیت
پیشنمایش تمامصفحهوقتی جستوجوی بلیت خطا میدهد یا برای آن روز چیزی نیست: خلاصهٔ جستوجو، نوارِ روزها، حالتِ خطا با «تلاشِ دوباره» و اسکلتِ بارگذاری، پیشنهادِ روزها و مسیرهای نزدیک، و پرسشهای پرتکرار.
۱. نصب کامپوننتها
این بلاک از ۹ آیتم دیگ استفاده میکند: 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.jsonpnpm 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.jsonyarn 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.jsonbunx --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>
)
}