ثبتنام (عکس)
پیشنمایش تمامصفحهثبتنام، مدلِ عکس: فرم در یک ستون و عکسِ واقعیِ تمامقد با جملهٔ برند در ستونِ دیگر. همان فرم، اعتبارسنجی و حالتهای صفحهٔ اصلی.
۱. نصب کامپوننتها
این بلاک از ۱۰ آیتم دیگ استفاده میکند: buttoncheckboxcontrol-sizeiconsinput-otplabellinkpassword-fieldpersiantext-field
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add 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/input-otp.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/link.json https://docs.digdesign.ir/r/password-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.jsonpnpm dlx digdesign@latest add 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/input-otp.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/link.json https://docs.digdesign.ir/r/password-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.jsonyarn dlx digdesign@latest add 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/input-otp.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/link.json https://docs.digdesign.ir/r/password-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.jsonbunx --bun digdesign@latest add 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/input-otp.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/link.json https://docs.digdesign.ir/r/password-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
signup-image.tsx
"use client"
import * as React from "react"
import NextLink from "next/link"
import { CheckIcon, SparklesIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"
import { Label } from "@/components/ui/label"
import { Link } from "@/components/ui/link"
import { PasswordField } from "@/components/ui/password-field"
import { TextField } from "@/components/ui/text-field"
import { isValidMobile, toFaDigits } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"
/** نشانِ برند بالای فرم. */
function BrandMark() {
return (
<span aria-hidden className="flex size-12 items-center justify-center rounded-xl bg-[var(--dig-violet-700)] text-[var(--dig-white)] [&>svg]:size-6">
<SparklesIcon />
</span>
)
}
/**
* ثبتنام با همان ساختارِ صفحهٔ ورود: فرم در ستونِ شروع، پنلِ برند در ستونِ
* پایان. بعد از فرم، تأییدِ شماره و پیامِ موفقیت در همان ستون میآیند.
*/
/** مدلِ عکس: فرم در یک ستون و عکسِ واقعی تمامقد با جملهٔ برند در ستونِ دیگر. */
export default function SignupImageBlock() {
const [password, setPassword] = React.useState("")
const [confirm, setConfirm] = React.useState("")
const [accepted, setAccepted] = React.useState(false)
const [sending, setSending] = React.useState(false)
const [mobile, setMobile] = React.useState("")
// بعد از ثبتنام کاربر باید بداند قدمِ بعد چیست: تأییدِ شماره، بعد ورود.
const [step, setStep] = React.useState<"form" | "verify" | "done">("form")
const mismatch = confirm.length > 0 && confirm !== password
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
if (mismatch || !accepted || !isValidMobile(mobile)) return
setSending(true)
// جای درخواستِ واقعی ثبتنام و ارسالِ پیامک.
await new Promise((resolve) => setTimeout(resolve, 900))
setSending(false)
setStep("verify")
}
async function verify() {
setSending(true)
await new Promise((resolve) => setTimeout(resolve, 700))
setSending(false)
setStep("done")
}
return (
<main className="grid grid-cols-1 min-h-svh gap-4 bg-background p-4 lg:grid-cols-2">
<div className="flex flex-col items-center px-4 py-10">
<ControlSizeProvider size="xl" className="flex w-full max-w-sm flex-1 flex-col justify-center gap-8">
{step === "form" && (
<>
<header className="flex flex-col items-center gap-4 text-center">
<BrandMark />
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold">ساخت حساب</h1>
<p className="text-sm text-muted-foreground">۱۴ روز رایگان، بدونِ کارتِ بانکی.</p>
</div>
</header>
<form onSubmit={onSubmit} className="flex flex-col gap-4">
<TextField label="نام و نام خانوادگی" name="fullName" autoComplete="name" required />
<TextField
label="شماره موبایل"
name="mobile"
type="tel"
dir="ltr"
maxLength={11}
autoComplete="tel"
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
value={mobile}
onValueChange={setMobile}
description="کد تأیید به همین شماره پیامک میشود."
required
validate={(value) => (isValidMobile(value) ? undefined : "شماره موبایل باید با ۰۹ شروع شود و ۱۱ رقم باشد.")}
/>
<PasswordField label="رمز عبور" name="password" autoComplete="new-password" value={password} onValueChange={setPassword} strength requirements required />
<PasswordField
label="تکرار رمز عبور"
name="confirm"
autoComplete="new-password"
value={confirm}
onValueChange={setConfirm}
invalid={mismatch}
errorMessage="با رمز بالا یکی نیست."
required
/>
<div className="flex items-start gap-2">
<Checkbox id="terms" name="terms" checked={accepted} onCheckedChange={(checked) => setAccepted(checked === true)} required />
<Label htmlFor="terms" className="leading-6 font-normal">
<span>
<Link href="#">قوانین و مقررات</Link> را خواندهام و میپذیرم.
</span>
</Label>
</div>
<Button type="submit" fullWidth loading={sending} disabled={!accepted || mismatch} className="mt-4">
ساخت حساب
</Button>
</form>
<p className="text-center text-sm text-muted-foreground">
حساب دارید؟ <Link href="/login">وارد شوید</Link>
</p>
</>
)}
{step === "verify" && (
<>
<header className="flex flex-col items-center gap-4 text-center">
<BrandMark />
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold">تأیید شماره موبایل</h1>
<p className="text-sm leading-6 text-muted-foreground">
کدِ ۶ رقمی به <bdi dir="ltr" className="font-medium text-foreground">{toFaDigits(mobile)}</bdi> پیامک شد.
</p>
</div>
</header>
<InputOTP aria-label="کد تأیید" onComplete={verify} disabled={sending} className="mx-auto">
<InputOTPGroup>
{[0, 1, 2, 3, 4, 5].map((index) => (
<InputOTPSlot key={index} index={index} />
))}
</InputOTPGroup>
</InputOTP>
<p className="text-center text-sm text-muted-foreground">
شماره اشتباه است؟{" "}
<Button variant="link" className="h-auto p-0" onClick={() => setStep("form")}>
ویرایش شماره
</Button>
</p>
</>
)}
{step === "done" && (
<div className="flex flex-col items-center gap-4 text-center" role="status">
<span aria-hidden className="flex size-12 items-center justify-center rounded-xl bg-success-subtle text-success [&_svg]:size-6">
<CheckIcon />
</span>
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold">حساب شما ساخته شد</h1>
<p className="text-sm text-muted-foreground">خوش آمدید! حالا میتوانید از همهٔ امکانات استفاده کنید.</p>
</div>
<Button asChild fullWidth className="mt-4">
<NextLink href="/">رفتن به داشبورد</NextLink>
</Button>
</div>
)}
</ControlSizeProvider>
<p className="pt-10 text-xs text-muted-foreground">
<Link href="#" size="sm">
قوانین
</Link>{" "}
و{" "}
<Link href="#" size="sm">
حریم خصوصی
</Link>
</p>
</div>
<aside className="relative hidden overflow-hidden rounded-xl bg-muted lg:block">
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img src={stockImages.studioSunlit.src} alt="" className="absolute inset-0 size-full object-cover" />
<div className="absolute inset-x-0 bottom-0 flex flex-col gap-2 bg-linear-to-t from-[var(--dig-graphite-950)] to-transparent p-8 pt-24 text-[var(--dig-white)] sm:p-12 sm:pt-32">
<p className="max-w-md text-2xl leading-relaxed font-bold sm:text-3xl sm:leading-relaxed">تیمت را بیاور؛ بقیهاش با ما.</p>
<p className="text-sm text-[var(--dig-neutral-300)]">راهاندازی در کمتر از دو دقیقه، بدونِ کارتِ بانکی.</p>
</div>
</aside>
</main>
)
}