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

اطلاعات مسافران

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

گامِ دومِ خریدِ بلیت: مراحل، یادآوریِ «مثل کارت ملی»، فرمِ هر مسافر (نام، کد ملی، تاریخ تولد شمسی، جنسیت)، اطلاعاتِ تماس و خلاصهٔ چسبانِ پرواز با ریزِ مبلغ.

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

این بلاک از ۱۱ آیتم دیگ استفاده می‌کند: 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.json
pnpm 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.json
yarn 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.json
bunx --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>
  )
}