جستوجوی بلیط
پیشنمایش تمامصفحهمبدأ و مقصد، یکطرفه یا رفت و برگشت، تاریخ شمسی و تعداد مسافر.
۱. نصب کامپوننتها
این بلاک از ۱۱ آیتم دیگ استفاده میکند: autocompletebuttoncontrol-sizedate-pickerdate-range-pickerlabelnumber-fieldpersianpopovertoasttoggle-group
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/autocomplete.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/date-picker.json https://docs.digdesign.ir/r/date-range-picker.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/popover.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/autocomplete.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/date-picker.json https://docs.digdesign.ir/r/date-range-picker.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/popover.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/autocomplete.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/date-picker.json https://docs.digdesign.ir/r/date-range-picker.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/popover.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/autocomplete.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/date-picker.json https://docs.digdesign.ir/r/date-range-picker.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/number-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/popover.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
travel-search.tsx
"use client"
import * as React from "react"
import type { DateRange } from "react-day-picker"
import { Autocomplete } from "@/components/ui/autocomplete"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { DatePicker } from "@/components/ui/date-picker"
import { DateRangePicker } from "@/components/ui/date-range-picker"
import { Label } from "@/components/ui/label"
import { NumberField } from "@/components/ui/number-field"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { toast } from "@/components/ui/toast"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { toFaDigits } from "@/lib/persian"
const CITIES = [
{ value: "THR", label: "تهران" },
{ value: "MHD", label: "مشهد" },
{ value: "IFN", label: "اصفهان" },
{ value: "SYZ", label: "شیراز" },
{ value: "TBZ", label: "تبریز" },
{ value: "KIH", label: "کیش" },
]
const POPULAR = [
{ from: "THR", to: "MHD" },
{ from: "THR", to: "KIH" },
{ from: "IFN", to: "MHD" },
{ from: "THR", to: "SYZ" },
]
const cityName = (code: string) => CITIES.find((city) => city.value === code)?.label ?? ""
function SwapIcon({ className }: { className?: string }) {
return (
<svg aria-hidden viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" className={className}>
<path d="M7 7h11l-3-3M17 17H6l3 3" />
</svg>
)
}
/** مسیرِ پروازِ تزئینی با هواپیما در بنر. */
function FlightPath() {
return (
<svg aria-hidden viewBox="0 0 320 120" className="pointer-events-none absolute end-4 top-4 -z-10 hidden w-80 text-[color-mix(in_oklab,var(--dig-white)_45%,transparent)] sm:block" fill="none">
<path d="M8 104 C 90 20, 210 10, 300 40" stroke="currentColor" strokeWidth="2" strokeDasharray="2 8" strokeLinecap="round" />
<circle cx="8" cy="104" r="5" fill="currentColor" />
<g transform="translate(296 38) rotate(200)" fill="var(--dig-white)">
<path d="M0 0 L-6 -3 L-18 -3 L-22 -10 L-26 -10 L-23 -3 L-32 -3 L-35 -7 L-38 -7 L-36 0 L-38 7 L-35 7 L-32 3 L-23 3 L-26 10 L-22 10 L-18 3 L-6 3 Z" />
</g>
</svg>
)
}
/**
* جستوجوی پرواز: بنرِ گرادیانی با مسیرِ پرواز و تیترِ پرسشی، و کارتِ جستوجو که روی لبهٔ
* بنر مینشیند. نوعِ سفر بالای فیلدها، دکمهٔ جابهجاییِ مبدأ و مقصد، مسافران در پاپاوور،
* و مسیرهای پرطرفدار برای پرکردنِ یکلمسی. دکمهٔ جستوجو غیرفعال نمیشود؛ اگر مقصد خالی
* باشد خطا کنارِ همان فیلد میآید.
*/
export default function TravelSearchBlock() {
const [trip, setTrip] = React.useState("oneway")
const [origin, setOrigin] = React.useState("THR")
const [destination, setDestination] = React.useState("")
const [departure, setDeparture] = React.useState<Date>()
const [range, setRange] = React.useState<DateRange>()
const [adults, setAdults] = React.useState(1)
const [children, setChildren] = React.useState(0)
const [missingDestination, setMissingDestination] = React.useState(false)
const passengers = adults + children
function swap() {
setOrigin(destination)
setDestination(origin)
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-5xl flex-col gap-8 p-4 sm:p-6">
<div className="flex flex-col">
{/* بنر: گرادیانِ بنفش به نیلی با پترن و مسیرِ پرواز. */}
<header className="relative isolate overflow-hidden rounded-xl bg-[linear-gradient(135deg,var(--dig-violet-700),var(--dig-indigo-800))] px-5 pt-6 pb-20 text-[var(--dig-white)] sm:px-10 sm:pt-10 sm:pb-24">
<span aria-hidden className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_90%_0%,var(--dig-violet-400),transparent_45%),radial-gradient(circle_at_0%_100%,var(--dig-blue-500),transparent_40%)]" />
<span aria-hidden className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(color-mix(in_oklab,var(--dig-white)_30%,transparent)_1.2px,transparent_1.3px)] [background-size:18px_18px] [mask-image:linear-gradient(to_left,black,transparent_60%)]" />
<FlightPath />
<div className="flex max-w-md flex-col gap-2">
<h1 className="text-2xl font-black sm:text-4xl">کجا پرواز کنیم؟</h1>
<p className="text-sm text-[var(--dig-violet-100)] sm:text-base">پروازهای داخلی از ۱۲ ایرلاین، با قیمتِ نهایی.</p>
</div>
</header>
{/* کارتِ جستوجو روی لبهٔ بنر. */}
<form
noValidate
className="relative mx-3 -mt-14 flex flex-col gap-5 rounded-xl border border-border bg-card p-4 shadow-card sm:mx-6 sm:p-6"
onSubmit={(event) => {
event.preventDefault()
if (!destination) {
setMissingDestination(true)
return
}
toast.success(`پروازهای ${cityName(origin)} به ${cityName(destination)}…`)
}}
>
<ToggleGroup type="single" variant="segmented" value={trip} onValueChange={(value) => value && setTrip(value as string)} aria-label="نوع سفر" className="self-start">
<ToggleGroupItem value="oneway">یکطرفه</ToggleGroupItem>
<ToggleGroupItem value="round">رفت و برگشت</ToggleGroupItem>
</ToggleGroup>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)_minmax(0,0.8fr)_auto] lg:items-start">
{/* مبدأ و مقصد؛ دکمهٔ جابهجایی در دسکتاپ ستونی میانِ دو فیلد است و در موبایل کنارِ برچسبِ مقصد. */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] sm:items-start sm:gap-2">
<div className="flex flex-col gap-2">
<Label htmlFor="origin">مبدأ</Label>
<Autocomplete id="origin" items={CITIES.filter((city) => city.value !== destination)} value={origin} onValueChange={setOrigin} placeholder="شهرِ مبدأ" className="w-full" />
</div>
<Button type="button" variant="ghost" size="icon" aria-label="جابهجاییِ مبدأ و مقصد" disabled={!destination} onClick={swap} className="hidden rounded-full sm:mt-7 sm:flex">
<SwapIcon />
</Button>
<div className="flex flex-col gap-2">
<div className="flex h-5 items-center justify-between gap-2">
<Label htmlFor="destination">مقصد</Label>
<ControlSizeProvider size="lg" asChild>
<Button type="button" variant="ghost" aria-label="جابهجاییِ مبدأ و مقصد" disabled={!destination} onClick={swap} className="-me-2 gap-1.5 px-2 sm:hidden">
<SwapIcon className="rotate-90" />
جابهجایی
</Button>
</ControlSizeProvider>
</div>
<Autocomplete
id="destination"
items={CITIES.filter((city) => city.value !== origin)}
value={destination}
onValueChange={(value) => {
setDestination(value)
setMissingDestination(false)
}}
placeholder="شهرِ مقصد"
emptyText="شهری پیدا نشد"
aria-invalid={missingDestination || undefined}
aria-describedby={missingDestination ? "destination-error" : undefined}
className="w-full"
/>
{missingDestination && (
<p id="destination-error" className="text-sm text-destructive">
مقصد را انتخاب کن.
</p>
)}
</div>
</div>
{trip === "round" ? (
<div className="flex flex-col gap-2">
<Label>تاریخ رفت و برگشت</Label>
<DateRangePicker calendar="jalali" value={range} onChange={setRange} numberOfMonths={2} className="w-full" />
</div>
) : (
<DatePicker id="departure" label="تاریخ رفت" calendar="jalali" value={departure} onChange={setDeparture} minDate={new Date()} className="w-full" />
)}
<div className="flex flex-col gap-2">
<Label id="passengers-label">مسافران</Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" aria-labelledby="passengers-label" className="justify-between">
{toFaDigits(passengers)} مسافر
</Button>
</PopoverTrigger>
<ControlSizeProvider size="xl" asChild>
<PopoverContent align="end" className="flex w-64 flex-col gap-3">
<div className="flex items-center justify-between gap-2">
<Label id="adults">بزرگسال</Label>
<NumberField aria-labelledby="adults" value={adults} onValueChange={setAdults} min={1} max={9 - children} className="w-28" />
</div>
<div className="flex items-center justify-between gap-2">
<Label id="children">کودک (۲ تا ۱۲ سال)</Label>
<NumberField aria-labelledby="children" value={children} onValueChange={setChildren} min={0} max={9 - adults} className="w-28" />
</div>
</PopoverContent>
</ControlSizeProvider>
</Popover>
</div>
<Button type="submit" className="lg:mt-7 lg:min-w-32">
جستوجو
</Button>
</div>
</form>
</div>
{/* مسیرهای پرطرفدار: یک لمس، مبدأ و مقصد پر میشود. */}
<section aria-labelledby="popular-title" className="flex flex-col gap-3">
<h2 id="popular-title" className="font-bold">
مسیرهای پرطرفدار
</h2>
<div className="flex gap-2 overflow-x-auto pb-1 sm:flex-wrap">
{POPULAR.map((route) => (
<Button
key={`${route.from}-${route.to}`}
type="button"
variant="outline"
className="shrink-0"
onClick={() => {
setOrigin(route.from)
setDestination(route.to)
setMissingDestination(false)
}}
>
{cityName(route.from)}
<span aria-label="به" className="text-muted-foreground">
←
</span>
{cityName(route.to)}
</Button>
))}
</div>
</section>
</main>
</ControlSizeProvider>
)
}