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