اطلاعات مسافران
پیشنمایش تمامصفحهگامِ دومِ خریدِ بلیت: مراحل، یادآوریِ «مثل کارت ملی»، فرمِ هر مسافر (نام، کد ملی، تاریخ تولد شمسی، جنسیت)، اطلاعاتِ تماس و خلاصهٔ چسبانِ پرواز با ریزِ مبلغ.
۱. نصب کامپوننتها
این بلاک از ۱۱ آیتم دیگ استفاده میکند: badgebuttoncardcontrol-sizedate-pickericonspersiansteppertext-fieldtoasttoggle-group
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/badge.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/date-picker.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.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/badge.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/date-picker.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.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/badge.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/date-picker.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.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/badge.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/date-picker.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
passenger-info.tsx
"use client"
import * as React from "react"
import { InfoIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { DatePicker } from "@/components/ui/date-picker"
import { Stepper, StepperItem, StepperTitle } from "@/components/ui/stepper"
import { TextField } from "@/components/ui/text-field"
import { toast } from "@/components/ui/toast"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatCurrency, isValidEmail, isValidMobile, isValidNationalId, toFaDigits } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"
const PASSENGERS = [
{ id: 1, type: "بزرگسال" },
{ id: 2, type: "بزرگسال" },
]
const FARE = 2_450_000
const TAX = 120_000
const DISCOUNT = 200_000
/** یک مسافر: شماره و نوع در سر، نام و کد ملی و جنسیت و تولد در دو ستون. */
function PassengerFields({ index, type }: { index: number; type: string }) {
const [gender, setGender] = React.useState("female")
const [birth, setBirth] = React.useState<Date | undefined>()
const prefix = `p${index}`
return (
<fieldset className="flex flex-col gap-5">
<legend className="mb-5 flex w-full items-center gap-3 border-b border-border pb-4">
<span aria-hidden className="flex size-7 items-center justify-center rounded-full bg-primary text-sm font-bold text-primary-foreground tabular-nums">
{toFaDigits(index)}
</span>
<span className="font-semibold">مسافر {toFaDigits(index)}</span>
<Badge variant="soft">
{type}
</Badge>
</legend>
{/* در موبایل هم نام و نام خانوادگی کنارِ هماند؛ فرمِ چندمسافره کوتاهتر میشود. */}
<div className="grid grid-cols-2 gap-x-3 gap-y-5 sm:gap-x-5">
<TextField label="نام" name={`${prefix}-first`} autoComplete="given-name" required />
<TextField label="نام خانوادگی" name={`${prefix}-last`} autoComplete="family-name" required />
<TextField
label="کد ملی"
className="col-span-2 sm:col-span-1"
name={`${prefix}-nid`}
inputMode="numeric"
dir="ltr"
maxLength={10}
required
validate={(value) => (isValidNationalId(value) ? undefined : value.length === 10 ? "کد ملی معتبر نیست." : "کد ملی باید ۱۰ رقم باشد.")}
/>
<div className="col-span-2 sm:col-span-1">
<DatePicker id={`${prefix}-birth`} label="تاریخ تولد" calendar="jalali" value={birth} onChange={setBirth} maxDate={new Date()} required className="w-full" />
</div>
<div className="col-span-2 flex flex-col gap-2">
<span id={`${prefix}-gender`} className="text-sm font-medium">
جنسیت
</span>
<ToggleGroup type="single" variant="segmented" value={gender} onValueChange={(value) => value && setGender(value as string)} aria-labelledby={`${prefix}-gender`} className="self-start">
<ToggleGroupItem value="female">زن</ToggleGroupItem>
<ToggleGroupItem value="male">مرد</ToggleGroupItem>
</ToggleGroup>
</div>
</div>
</fieldset>
)
}
/**
* اطلاعاتِ مسافران در خریدِ بلیت (گامِ دوم): هر مسافر یک گروه، اطلاعاتِ تماس
* جدا، و خلاصهٔ پرواز و مبلغ در ستونِ کناری. یادآوری «مثل کارت ملی» بالای فرم
* است، همانجا که کاربر شروع به نوشتن میکند.
*/
export default function PassengerInfoBlock() {
const subtotal = FARE * PASSENGERS.length + TAX
const total = subtotal - DISCOUNT
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-6xl flex-col gap-8 p-4 pb-28 sm:p-6 sm:pb-28 lg:pb-6">
<Stepper value={1} className="mx-auto w-full max-w-2xl">
<StepperItem>
<StepperTitle>انتخاب پرواز</StepperTitle>
</StepperItem>
<StepperItem>
<StepperTitle>اطلاعات مسافران</StepperTitle>
</StepperItem>
<StepperItem>
<StepperTitle>پرداخت</StepperTitle>
</StepperItem>
<StepperItem>
<StepperTitle>صدور بلیت</StepperTitle>
</StepperItem>
</Stepper>
<form
onSubmit={(event) => {
event.preventDefault()
toast.success("اطلاعات مسافران ثبت شد؛ به صفحهٔ پرداخت میروید.")
}}
className="grid grid-cols-1 items-start gap-6 lg:grid-cols-[minmax(0,1fr)_22rem]"
>
<div className="flex flex-col gap-6">
<header className="flex flex-col gap-3">
<h1 className="text-2xl font-bold">اطلاعات مسافران</h1>
<p className="flex items-start gap-2 rounded-xl bg-[color-mix(in_oklab,var(--brand-secondary)_8%,var(--background))] p-4 text-sm leading-6">
<InfoIcon aria-hidden className="mt-0.5 size-4 shrink-0 text-brand-secondary" />
نام و نام خانوادگی را دقیقاً مثلِ کارتِ ملی بنویسید؛ بلیت با نامِ متفاوت قابلِ استفاده نیست.
</p>
</header>
<Card>
<CardContent className="flex flex-col gap-10">
{PASSENGERS.map((passenger) => (
<PassengerFields key={passenger.id} index={passenger.id} type={passenger.type} />
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">اطلاعات تماس</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<TextField
label="موبایل"
name="mobile"
type="tel"
dir="ltr"
maxLength={11}
autoComplete="tel"
description="بلیت و تغییراتِ پرواز پیامک میشود."
required
validate={(value) => (isValidMobile(value) ? undefined : "شماره موبایل باید با ۰۹ شروع شود و ۱۱ رقم باشد.")}
/>
<TextField
label="ایمیل"
name="email"
type="email"
dir="ltr"
autoComplete="email"
optional
validate={(value) => (!value || isValidEmail(value) ? undefined : "یک ایمیلِ درست وارد کنید.")}
/>
</CardContent>
</Card>
</div>
{/* خلاصه: مسیر و زمانِ پرواز، ریزِ مبلغ و جمعِ قابلِ پرداخت؛ دکمه با فاصلهٔ بیشتر. */}
<Card className="lg:sticky lg:top-6">
<CardHeader>
<CardTitle className="text-base">خلاصهٔ پرواز</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-6">
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img src={stockImages.planeWindow.src} alt="" className="aspect-[16/9] w-full rounded-lg object-cover" />
<div className="flex items-center justify-between gap-3 rounded-xl bg-muted p-4">
<div className="flex flex-col">
<span className="text-xl font-bold">تهران</span>
<span className="text-xs text-muted-foreground tabular-nums">{toFaDigits("08:30")}</span>
</div>
<div className="flex flex-1 flex-col items-center gap-1 text-xs text-muted-foreground">
<span>۱ ساعت و ۲۰ دقیقه</span>
<span aria-hidden className="h-px w-full border-t border-dashed border-border" />
<span>مستقیم</span>
</div>
<div className="flex flex-col items-end">
<span className="text-xl font-bold">مشهد</span>
<span className="text-xs text-muted-foreground tabular-nums">{toFaDigits("09:50")}</span>
</div>
</div>
<dl className="flex flex-col gap-3 text-sm">
<div className="flex justify-between gap-3">
<dt className="text-muted-foreground">بلیت بزرگسال × {toFaDigits(PASSENGERS.length)}</dt>
<dd className="tabular-nums">{formatCurrency(FARE * PASSENGERS.length)}</dd>
</div>
<div className="flex justify-between gap-3">
<dt className="text-muted-foreground">عوارض فرودگاه</dt>
<dd className="tabular-nums">{formatCurrency(TAX)}</dd>
</div>
<div className="flex justify-between gap-3">
<dt className="text-muted-foreground">کد تخفیف</dt>
<dd className="text-success tabular-nums">−{formatCurrency(DISCOUNT)}</dd>
</div>
<div className="mt-3 flex items-baseline justify-between gap-3 border-t border-border pt-6">
<dt className="font-semibold">قابل پرداخت</dt>
<dd className="text-xl font-bold tabular-nums">{formatCurrency(total)}</dd>
</div>
</dl>
<Button type="submit" fullWidth className="mt-2 hidden lg:flex">
ادامه و پرداخت
</Button>
</CardContent>
</Card>
{/* در موبایل مبلغ و دکمهٔ پرداخت پایینِ صفحه چسبیدهاند، نه بعد از فرمِ بلند و خلاصه. */}
<div className="fixed inset-x-0 bottom-0 z-20 flex items-center gap-4 border-t border-border bg-background p-4 pb-[max(1rem,env(safe-area-inset-bottom))] lg:hidden">
<span className="flex flex-1 flex-col">
<span className="text-xs text-muted-foreground">قابل پرداخت</span>
<span className="font-bold tabular-nums">{formatCurrency(total)}</span>
</span>
<Button type="submit" className="shrink-0">
ادامه و پرداخت
</Button>
</div>
</form>
</main>
</ControlSizeProvider>
)
}