تماس با ما
پیشنمایش تمامصفحهمعرفی، راههای ارتباطی با آیکون و نقشه، فرمِ کارتی با موضوعِ چیپی، نام و ایمیل با آیکون، اندازهٔ کسبوکار، موافقت با حریم خصوصی و پیامِ موفقیت.
۱. نصب کامپوننتها
این بلاک از ۱۱ آیتم دیگ استفاده میکند: badgebuttoncheckboxcontrol-sizeiconslabelpersianselecttext-fieldtextareatoggle-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/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.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 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/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.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 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/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.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 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/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.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 https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
contact.tsx
"use client"
import * as React from "react"
import { CheckIcon, ClockIcon, EnvelopeIcon, GlobeIcon, PhoneIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { TextField } from "@/components/ui/text-field"
import { Textarea } from "@/components/ui/textarea"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { isValidEmail, isValidMobile, toFaDigits } from "@/lib/persian"
const CHANNELS = [
{ icon: <PhoneIcon />, title: "تلفن", value: toFaDigits("021-91001234"), href: "tel:02191001234", dir: "ltr" as const },
{ icon: <EnvelopeIcon />, title: "ایمیل", value: "hello@hesabyar.example", href: "mailto:hello@hesabyar.example", dir: "ltr" as const },
{ icon: <GlobeIcon />, title: "دفتر", value: "تهران، خیابان ولیعصر، بالاتر از پارک ساعی، پلاک ۱۲", dir: "rtl" as const },
{ icon: <ClockIcon />, title: "ساعات پاسخگویی", value: "شنبه تا چهارشنبه، ۹ تا ۱۸", dir: "rtl" as const },
]
const TOPICS = ["پشتیبانی فنی", "فروش و پلنها", "همکاری", "رسانه", "سایر"]
/**
* صفحهٔ تماس: معرفی و راههای ارتباطی با نقشه در یک ستون، فرمِ کامل در کارتِ
* کنارش. موضوع با چیپهای تکانتخابی، نام و ایمیل با آیکون، موافقت با قوانین
* پیش از ارسال، و پیامِ موفقیت در همان جای فرم.
*/
export default function ContactBlock() {
const [topic, setTopic] = React.useState(TOPICS[0])
const [agreed, setAgreed] = React.useState(false)
const [sending, setSending] = React.useState(false)
const [sent, setSent] = React.useState(false)
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const form = event.currentTarget
setSending(true)
await new Promise((resolve) => setTimeout(resolve, 800))
setSending(false)
form.reset()
setAgreed(false)
setSent(true)
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto grid w-full grid-cols-1 max-w-6xl gap-12 px-4 py-16 sm:px-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.15fr)] lg:gap-16">
{/* ستونِ معرفی: تیتر، راههای ارتباطی و نقشه. */}
<section aria-labelledby="contact-title" className="flex flex-col gap-10">
<div className="flex flex-col gap-4">
<Badge variant="soft" className="self-start">
تماس با ما
</Badge>
<h1 id="contact-title" className="text-4xl leading-snug font-bold sm:text-5xl sm:leading-snug">
بیایید گفتوگو کنیم
</h1>
<p className="text-lg leading-8 text-muted-foreground">سؤال، پیشنهاد یا همکاری؛ فرم را پر کنید و تا یک روزِ کاری جواب میدهیم.</p>
</div>
<ul className="grid grid-cols-1 gap-5 sm:grid-cols-2">
{CHANNELS.map((channel) => (
<li key={channel.title} className="flex items-start gap-3">
<span aria-hidden className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-muted text-foreground [&>svg]:size-5">
{channel.icon}
</span>
<span className="flex min-w-0 flex-col gap-0.5">
<span className="text-sm text-muted-foreground">{channel.title}</span>
{channel.href ? (
<a href={channel.href} className="font-semibold underline-offset-4 hover:underline">
<bdi dir={channel.dir}>{channel.value}</bdi>
</a>
) : (
<span className="text-sm leading-6 font-medium">{channel.value}</span>
)}
</span>
</li>
))}
</ul>
<figure className="overflow-hidden rounded-xl border border-border">
{/* eslint-disable-next-line @next/next/no-img-element -- نقشهٔ تزئینی؛ در محصول، نقشهٔ واقعی جایگزین میشود */}
<img src="/blocks/contact-map.svg" alt="موقعیت دفتر روی نقشه" className="aspect-[16/10] w-full object-cover" />
<figcaption className="flex items-center justify-between gap-3 bg-card px-5 py-4 text-sm">
<span className="text-muted-foreground">خیابان ولیعصر، پلاک ۱۲</span>
<a href="#" className="font-semibold text-brand-secondary underline-offset-4 hover:underline">
مسیریابی
</a>
</figcaption>
</figure>
</section>
{/* فرم در کارت؛ نتیجه همانجا که کاربر نگاه میکند. */}
<section aria-label="فرم تماس" className="rounded-xl border border-border bg-card p-6 shadow-card sm:p-10">
{sent ? (
<div className="flex h-full flex-col items-center justify-center gap-4 py-16 text-center" role="status">
<span aria-hidden className="flex size-14 items-center justify-center rounded-xl bg-success-subtle text-success [&_svg]:size-7">
<CheckIcon />
</span>
<h2 className="text-xl font-bold">پیامتان رسید</h2>
<p className="max-w-sm text-sm leading-7 text-muted-foreground">تا یک روزِ کاری از همان راهی که گذاشتید جواب میدهیم. ممنون که وقت گذاشتید.</p>
<Button variant="outline" className="mt-4" onClick={() => setSent(false)}>
ارسالِ پیامی دیگر
</Button>
</div>
) : (
<form onSubmit={onSubmit} className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<h2 className="text-2xl font-bold">پیام بفرستید</h2>
<p className="text-sm text-muted-foreground">فیلدهای ستارهدار لازماند.</p>
</div>
<ControlSizeProvider size="xl" className="flex flex-col gap-5">
<div className="flex flex-col gap-2">
<span id="contact-topic" className="text-sm font-medium">
موضوع
</span>
<div className="max-w-full overflow-x-auto">
<ToggleGroup type="single" variant="segmented" value={topic} onValueChange={(value) => value && setTopic(value as string)} aria-labelledby="contact-topic">
{TOPICS.map((item) => (
<ToggleGroupItem key={item} value={item} className="whitespace-nowrap">
{item}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
</div>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<TextField label="نام" name="firstName" autoComplete="given-name" required />
<TextField label="نام خانوادگی" name="lastName" autoComplete="family-name" required />
</div>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<TextField
label="ایمیل"
name="email"
type="email"
dir="ltr"
autoComplete="email"
required
validate={(value) => (isValidEmail(value) ? undefined : "یک ایمیلِ درست وارد کنید.")}
/>
<TextField
label="موبایل"
name="mobile"
type="tel"
dir="ltr"
autoComplete="tel"
optional
validate={(value) => (!value || isValidMobile(value) ? undefined : "شمارهٔ موبایل درست نیست.")}
/>
</div>
<div className="flex flex-col gap-2">
<Label id="contact-size-label">اندازهٔ کسبوکار</Label>
<Select name="size" defaultValue="small">
<SelectTrigger aria-labelledby="contact-size-label" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="solo">تکنفره</SelectItem>
<SelectItem value="small">۲ تا ۱۰ نفر</SelectItem>
<SelectItem value="medium">۱۱ تا ۵۰ نفر</SelectItem>
<SelectItem value="large">بیش از ۵۰ نفر</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="contact-message">پیام</Label>
<Textarea id="contact-message" name="message" rows={5} required placeholder="دربارهٔ نیاز، زمانبندی یا سؤالتان بنویسید…" />
</div>
</ControlSizeProvider>
<div className="flex items-start gap-2">
<Checkbox id="contact-agree" name="agree" checked={agreed} onCheckedChange={(checked) => setAgreed(checked === true)} />
<Label htmlFor="contact-agree" className="text-sm leading-6 font-normal text-muted-foreground">
با{" "}
<a href="#" className="font-medium text-foreground underline underline-offset-4">
حریم خصوصی
</a>{" "}
موافقم.
</Label>
</div>
<ControlSizeProvider size="xl">
<Button type="submit" loading={sending} disabled={!agreed} fullWidth>
ارسالِ پیام
</Button>
</ControlSizeProvider>
</form>
)}
</section>
</main>
</ControlSizeProvider>
)
}