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

معرفی، راه‌های ارتباطی با آیکون و نقشه، فرمِ کارتی با موضوعِ چیپی، نام و ایمیل با آیکون، اندازهٔ کسب‌وکار، موافقت با حریم خصوصی و پیامِ موفقیت.

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

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