تماس با استودیو
پیشنمایش تمامصفحهتماسِ تیره: کلمهٔ درشتِ «تماس» پشتِ صفحه، سه قول و شبکههای اجتماعی، فرمِ شیشهای با اعتبارسنجی و پیامِ موفقیت، و سه کارتِ راهِ ارتباطی.
۱. نصب کامپوننتها
این بلاک از ۶ آیتم دیگ استفاده میکند: buttoncontrol-sizeiconspersiantext-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/icons.json https://docs.digdesign.ir/r/persian.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/icons.json https://docs.digdesign.ir/r/persian.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/icons.json https://docs.digdesign.ir/r/persian.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/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/textarea.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
contact-studio.tsx
"use client"
import * as React from "react"
import { ArrowUpIcon, CheckIcon, EnvelopeIcon, GlobeIcon, InstagramLogo, LinkedInLogo, PhoneIcon, TelegramLogo } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { TextField } from "@/components/ui/text-field"
import { Textarea } from "@/components/ui/textarea"
import { toFaDigits } from "@/lib/persian"
const PROMISES = ["پاسخ در یک روزِ کاری", "یک نفر از اول تا آخر کنارتان", "پشتیبانی تا مدتها پس از تحویل"]
const CHANNELS = [
{ icon: <PhoneIcon />, title: "تماس با استودیو", value: toFaDigits("021-88776655"), href: "tel:02188776655", ltr: true },
{ icon: <GlobeIcon />, title: "کجا هستیم", value: "تهران، خیابانِ ایرانشهر، کوچهٔ نوزدهم", href: "#" },
{ icon: <EnvelopeIcon />, title: "ایمیل", value: "hello@noor.studio", href: "mailto:hello@noor.studio", ltr: true },
]
/**
* تماس با استودیو، تیره: کلمهٔ درشتِ «تماس» کمرنگ پشتِ صفحه، سمتی «سلام کنید» با سه قول و
* شبکههای اجتماعی، سمتِ دیگر فرمِ شیشهای (نام، ایمیل، پیام) با اعتبارسنجی و پیامِ موفقیت؛
* زیرش سه کارتِ راهِ ارتباطی. همیشه تیره است.
*/
export default function ContactStudioBlock() {
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 (!/^\S+@\S+\.\S+$/.test(String(data.get("email")))) next.email = "ایمیل را کامل بنویسید."
if (String(data.get("message")).trim().length < 10) next.message = "کمی بیشتر از پروژه بگویید."
setErrors(next)
if (Object.keys(next).length === 0) setSent(true)
}
return (
<ControlSizeProvider size="xl" asChild>
<section
aria-labelledby="cs-title"
className="relative isolate w-full overflow-hidden bg-[var(--dig-graphite-950)] px-4 py-20 text-[var(--dig-white)] [--background:var(--dig-graphite-900)] [--border:var(--dig-graphite-700)] [--card:var(--dig-graphite-900)] [--foreground:var(--dig-white)] [--input:var(--dig-graphite-700)] [--muted-foreground:var(--dig-neutral-400)] [--primary-foreground:var(--dig-graphite-950)] [--primary:var(--dig-white)] sm:px-6 sm:py-28"
>
<span aria-hidden className="pointer-events-none absolute inset-x-0 top-10 -z-10 text-center text-[clamp(5rem,18vw,15rem)] leading-none font-black text-[var(--dig-graphite-800)] select-none">
تماس
</span>
<div className="mx-auto grid w-full max-w-6xl grid-cols-1 gap-12 lg:grid-cols-[1fr_1.3fr]">
<div className="flex flex-col items-start gap-6 pt-4">
<h2 id="cs-title" className="flex items-center gap-3 text-4xl font-black sm:text-5xl">
سلام کنید
<ArrowUpIcon aria-hidden className="size-9 -rotate-45" />
</h2>
<p className="max-w-sm leading-8 text-muted-foreground">پروژهای دارید یا فقط میخواهید سلام کنید؟ چند خط بنویسید؛ همهٔ پیامها را خودمان میخوانیم.</p>
<ul className="flex flex-col gap-3">
{PROMISES.map((item) => (
<li key={item} className="flex items-center gap-3">
<span aria-hidden className="flex size-7 items-center justify-center rounded-full border border-[var(--dig-graphite-700)] bg-[var(--dig-graphite-900)]">
<CheckIcon className="size-4" />
</span>
{item}
</li>
))}
</ul>
<div className="flex gap-2">
{[
{ icon: <InstagramLogo />, label: "اینستاگرام" },
{ icon: <LinkedInLogo />, label: "لینکدین" },
{ icon: <TelegramLogo />, label: "تلگرام" },
].map((social) => (
<Button key={social.label} variant="outline" size="icon" aria-label={social.label} asChild>
<a href="#">{social.icon}</a>
</Button>
))}
</div>
</div>
<div className="rounded-xl border border-[var(--dig-graphite-800)] bg-[color-mix(in_oklab,var(--dig-graphite-900)_80%,transparent)] p-5 shadow-2xl backdrop-blur-md sm:p-6">
{sent ? (
<div role="status" className="flex min-h-80 flex-col items-center justify-center gap-3 text-center">
<span aria-hidden className="flex size-14 items-center justify-center rounded-full bg-[var(--dig-white)] text-[var(--dig-graphite-950)]">
<CheckIcon className="size-7" />
</span>
<p className="text-xl font-bold">پیامتان رسید</p>
<p className="text-muted-foreground">تا پایانِ روزِ کاریِ بعد جواب میدهیم.</p>
</div>
) : (
<form noValidate onSubmit={submit} className="flex flex-col gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<TextField name="name" label="نام" variant="flat" invalid={!!errors.name} errorMessage={errors.name} />
<TextField name="email" type="email" dir="ltr" label="ایمیل" variant="flat" invalid={!!errors.email} errorMessage={errors.email} />
</div>
<div className="flex flex-col gap-2">
<label htmlFor="cs-message" className="text-sm font-medium">
پیام
</label>
<Textarea id="cs-message" name="message" variant="flat" rows={6} aria-invalid={!!errors.message} aria-describedby={errors.message ? "cs-message-error" : undefined} />
{errors.message && (
<p id="cs-message-error" className="text-sm text-[var(--dig-red-300)]">
{errors.message}
</p>
)}
</div>
<Button type="submit" radius="full" fullWidth>
ارسال
</Button>
</form>
)}
</div>
</div>
<ul className="mx-auto mt-14 grid w-full max-w-6xl grid-cols-1 gap-3 md:grid-cols-3">
{CHANNELS.map((channel) => (
<li key={channel.title}>
<a href={channel.href} className="group flex h-full flex-col gap-6 rounded-xl border border-[var(--dig-graphite-800)] bg-[var(--dig-graphite-900)] p-5 outline-none transition-colors hover:border-[var(--dig-graphite-600)] focus-visible:ring-2 focus-visible:ring-[var(--dig-white)]">
<span aria-hidden className="flex size-10 items-center justify-center rounded-lg bg-[var(--dig-graphite-800)] [&>svg]:size-5">
{channel.icon}
</span>
<span className="flex flex-col gap-1">
<span className="font-bold">{channel.title}</span>
{channel.ltr ? (
<bdi dir="ltr" className="text-start text-sm text-muted-foreground">
{channel.value}
</bdi>
) : (
<span className="text-sm text-muted-foreground">{channel.value}</span>
)}
</span>
</a>
</li>
))}
</ul>
</section>
</ControlSizeProvider>
)
}