بلاک‌ها · پشتیبانی و محتوا

فرمِ رزرو و درخواست

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

تماس با فرمِ رزرو: نام، ایمیل، موبایل، مسیر، تاریخِ شمسی، تعداد و توضیح با اعتبارسنجی؛ عکسِ مسیر کنارش و سه ستونِ راهِ تماس و ساعتِ کار.

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

این بلاک از ۹ آیتم دیگ استفاده می‌کند: buttoncontrol-sizedate-pickericonsnumber-fieldpersianselecttext-fieldtextarea

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

۲. کد صفحه

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

contact-booking.tsx
"use client"

import * as React from "react"

import { CalendarIcon, CheckCircleIcon, ClockIcon, EnvelopeIcon, PhoneIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { DatePicker } from "@/components/ui/date-picker"
import { NumberField } from "@/components/ui/number-field"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { TextField } from "@/components/ui/text-field"
import { Textarea } from "@/components/ui/textarea"
import { toFaDigits } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"

const ROUTES = [
  { value: "kavir", label: "کویرِ مرنجاب، دو روز" },
  { value: "qeshm", label: "جزیرهٔ قشم، سه روز" },
  { value: "alamut", label: "درهٔ الموت، یک روز" },
  { value: "kaluts", label: "کلوت‌های شهداد، سه روز" },
]

/**
 * رزرو با فرمِ درخواست: برچسبِ «رزرو» و تیترِ درشت، فرمِ دوستونه (نام، ایمیل، تلفن، مسیر،
 * تاریخِ شمسی، تعدادِ نفرات، توضیح) روی سطحِ subtle و عکسِ مسیر کنارش؛ پیامِ ثبت در همان
 * کارت، و سه ستونِ راهِ تماس و ساعتِ کار پایین.
 */
export default function ContactBookingBlock() {
  const [route, setRoute] = React.useState("")
  const [errors, setErrors] = React.useState<Record<string, string>>({})
  const [sent, setSent] = React.useState(false)

  const submit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault()
    const data = new FormData(event.currentTarget)
    const next: Record<string, string> = {}
    if (!String(data.get("name")).trim()) next.name = "نامِ کامل را بنویسید."
    if (!/^09\d{9}$/.test(String(data.get("phone")))) next.phone = "شمارهٔ موبایل ۱۱ رقم است و با ۰۹ شروع می‌شود."
    if (!route) next.route = "یک مسیر انتخاب کنید."
    setErrors(next)
    if (Object.keys(next).length === 0) setSent(true)
  }

  return (
    <ControlSizeProvider size="xl" asChild>
      <section aria-labelledby="cb-title" className="mx-auto flex w-full max-w-6xl flex-col gap-10 px-4 py-20 sm:px-6">
        <div className="flex flex-col items-start justify-between gap-6 md:flex-row md:items-end">
          <div className="flex flex-col items-start gap-3">
            <span className="rounded-full bg-muted px-3 py-1 text-sm font-semibold">رزرو</span>
            <h2 id="cb-title" className="text-4xl font-black sm:text-6xl">
              مسیرت را رزرو کن
            </h2>
          </div>
          <p className="max-w-xs leading-7 text-muted-foreground">تاریخ و مسیرِ دلخواه را بگویید؛ همان روز ظرفیتِ خالی را خبر می‌دهیم.</p>
        </div>

        <div className="grid grid-cols-1 gap-4 lg:grid-cols-[1.6fr_1fr]">
          <div className="rounded-xl bg-subtle p-5 sm:p-6">
            {sent ? (
              <div role="status" className="flex min-h-96 flex-col items-center justify-center gap-3 text-center">
                <CheckCircleIcon aria-hidden className="size-12 text-success" />
                <p className="text-xl font-bold">درخواستتان ثبت شد</p>
                <p className="max-w-sm leading-7 text-muted-foreground">کارشناسِ سفر تا چند ساعتِ دیگر برای هماهنگیِ ظرفیت تماس می‌گیرد.</p>
                <Button variant="outline" onClick={() => setSent(false)}>
                  درخواستِ دیگر
                </Button>
              </div>
            ) : (
              <form noValidate onSubmit={submit} className="grid grid-cols-1 gap-4 sm:grid-cols-2">
                <TextField name="name" label="نامِ کامل" placeholder="نام و نام خانوادگی" invalid={!!errors.name} errorMessage={errors.name} />
                <TextField name="email" type="email" dir="ltr" label="ایمیل" placeholder="name@mail.com" optional />
                <TextField name="phone" type="tel" dir="ltr" label="موبایل" placeholder="۰۹۱۲ ۰۰۰ ۰۰۰۰" faDigits invalid={!!errors.phone} errorMessage={errors.phone} />
                <div className="flex flex-col gap-2">
                  <span id="cb-route" className="text-sm font-medium">
                    مسیر
                  </span>
                  <Select value={route} onValueChange={(value) => setRoute(value as string)}>
                    <SelectTrigger aria-labelledby="cb-route" aria-invalid={!!errors.route}>
                      <SelectValue placeholder="انتخابِ مسیر" />
                    </SelectTrigger>
                    <SelectContent>
                      {ROUTES.map((item) => (
                        <SelectItem key={item.value} value={item.value}>
                          {item.label}
                        </SelectItem>
                      ))}
                    </SelectContent>
                  </Select>
                  {errors.route && <p className="text-sm text-destructive">{errors.route}</p>}
                </div>
                <DatePicker label="تاریخِ رفت" calendar="jalali" placeholder="انتخابِ روز" />
                <div className="flex flex-col gap-2">
                  <span id="cb-people" className="text-sm font-medium">
                    چند نفر
                  </span>
                  <NumberField aria-labelledby="cb-people" defaultValue={2} min={1} max={12} />
                </div>
                <div className="flex flex-col gap-2 sm:col-span-2">
                  <label htmlFor="cb-notes" className="text-sm font-medium">
                    چیزی که باید بدانیم
                  </label>
                  <Textarea id="cb-notes" name="notes" rows={3} placeholder="رژیمِ غذایی، همراهِ کودک، سؤال درباره مسیر…" />
                </div>
                <div className="sm:col-span-2">
                  <Button type="submit">
                    <CalendarIcon />
                    بررسیِ ظرفیت
                  </Button>
                </div>
              </form>
            )}
          </div>
          <div className="relative min-h-72 overflow-hidden rounded-xl">
            {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
            <img src={stockImages.planeWindow.src} alt={stockImages.planeWindow.alt} className="absolute inset-0 size-full object-cover" />
            <span className="absolute end-4 top-4 rounded-full bg-card px-3 py-1.5 text-sm font-semibold shadow-sm">سفرِ شما</span>
          </div>
        </div>

        <ul className="grid grid-cols-1 gap-8 pt-4 text-center sm:grid-cols-3">
          {[
            { icon: <EnvelopeIcon />, title: "برایمان بنویسید", lines: ["hello@masir.travel"], ltr: true },
            { icon: <PhoneIcon />, title: "تماس یا پیامک", lines: [toFaDigits("0912 345 6789"), toFaDigits("021 4433 2211")], ltr: true },
            { icon: <ClockIcon />, title: "ساعتِ کار", lines: ["شنبه تا چهارشنبه، ۹ تا ۱۸", "پنجشنبه تا ۱۴"] },
          ].map((item) => (
            <li key={item.title} className="flex flex-col items-center gap-2">
              <span aria-hidden className="mb-1 flex size-10 items-center justify-center rounded-full bg-muted [&>svg]:size-5">
                {item.icon}
              </span>
              <span className="font-bold">{item.title}</span>
              {item.lines.map((line) =>
                item.ltr ? (
                  <bdi key={line} dir="ltr" className="text-sm text-muted-foreground">
                    {line}
                  </bdi>
                ) : (
                  <span key={line} className="text-sm text-muted-foreground">
                    {line}
                  </span>
                )
              )}
            </li>
          ))}
        </ul>
      </section>
    </ControlSizeProvider>
  )
}