ثبتنامِ چندمرحلهای
پیشنمایش تمامصفحهثبتنام در سه قدم با ستونِ تیرهٔ مراحل: حساب (نام، ایمیل، رمز با قدرت)، تیم (اندازه با سگمنت، نقش) و انتخابِ پلن با کارتهای رادیویی؛ برگشت دادهها را نگه میدارد و پیامِ موفقیت.
۱. نصب کامپوننتها
این بلاک از ۱۱ آیتم دیگ استفاده میکند: badgebuttoncontrol-sizeiconslabellinkpassword-fieldpersianradio-grouptext-fieldtoggle-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/control-size.json https://docs.digdesign.ir/r/icons.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/radio-group.json https://docs.digdesign.ir/r/text-field.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/control-size.json https://docs.digdesign.ir/r/icons.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/radio-group.json https://docs.digdesign.ir/r/text-field.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/control-size.json https://docs.digdesign.ir/r/icons.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/radio-group.json https://docs.digdesign.ir/r/text-field.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/control-size.json https://docs.digdesign.ir/r/icons.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/radio-group.json https://docs.digdesign.ir/r/text-field.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
signup-steps.tsx
"use client"
import * as React from "react"
import { CheckIcon } from "@/components/icons"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Label } from "@/components/ui/label"
import { Link } from "@/components/ui/link"
import { PasswordField } from "@/components/ui/password-field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { TextField } from "@/components/ui/text-field"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatCurrency, isValidEmail, toFaDigits } from "@/lib/persian"
const STEPS = [
{ title: "حساب", text: "نام، ایمیل و رمز" },
{ title: "تیم", text: "اندازه و نقشِ شما" },
{ title: "پلن", text: "بعداً هم عوض میشود" },
]
const PLANS = [
{ value: "free", title: "رایگان", price: 0, note: "تا ۳ عضو، برای شروع" },
{ value: "pro", title: "حرفهای", price: 290_000, note: "اعضای نامحدود، گزارشها", best: true },
{ value: "business", title: "سازمانی", price: 890_000, note: "ورودِ یکپارچه و پشتیبانی اختصاصی" },
]
const optionCard =
"flex items-start gap-3 rounded-xl border border-border p-4 transition-colors has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary-subtle"
/**
* ثبتنامِ چندمرحلهای: ستونِ برندِ کنار با مراحلِ عمودی (انجامشده تیک میخورد)، و فرمِ
* هر مرحله جدا: حساب، تیم (اندازه با سگمنت) و انتخابِ پلن با کارتهای رادیویی. برگشت
* به مرحلهٔ قبل دادهها را نگه میدارد.
*/
export default function SignupStepsBlock() {
const [step, setStep] = React.useState(0)
const [size, setSize] = React.useState("2-10")
const [plan, setPlan] = React.useState("pro")
const [done, setDone] = React.useState(false)
const last = step === STEPS.length - 1
return (
<ControlSizeProvider size="xl" asChild>
<main className="grid grid-cols-1 min-h-svh grid-rows-[auto_1fr] lg:grid-cols-[22rem_minmax(0,1fr)] lg:grid-rows-none">
<aside className="flex flex-col gap-6 bg-[var(--dig-violet-800)] p-6 text-[var(--dig-white)] [--muted-foreground:var(--dig-violet-200)] lg:gap-10 lg:p-10">
<span className="flex items-center gap-2 text-lg font-black">
<span aria-hidden className="size-3 rounded-sm bg-[var(--dig-violet-300)]" />
آوند
</span>
<ol className="flex flex-row gap-6 lg:flex-col lg:gap-0">
{STEPS.map((item, index) => {
const state = done || index < step ? "done" : index === step ? "current" : "next"
return (
<li key={item.title} aria-current={state === "current" ? "step" : undefined} className="relative flex gap-4 lg:pb-10 lg:last:pb-0">
{index < STEPS.length - 1 && <span aria-hidden className="absolute start-4 top-10 bottom-0 hidden w-px bg-[var(--dig-violet-600)] lg:block" />}
<span
className={`flex size-8 shrink-0 items-center justify-center rounded-full text-sm font-bold [&>svg]:size-4 ${
state === "done" ? "bg-[var(--dig-green-500)] text-[var(--dig-white)]" : state === "current" ? "bg-[var(--dig-white)] text-[var(--dig-violet-800)]" : "border border-[var(--dig-violet-500)] text-muted-foreground"
}`}
>
{state === "done" ? <CheckIcon aria-label="انجام شد" /> : toFaDigits(index + 1)}
</span>
<span className="hidden flex-col gap-0.5 sm:flex">
<span className={state === "next" ? "font-medium text-muted-foreground" : "font-semibold"}>{item.title}</span>
<span className="text-sm text-muted-foreground">{item.text}</span>
</span>
</li>
)
})}
</ol>
<p className="mt-auto hidden text-sm leading-6 text-muted-foreground lg:block">بیش از ۱۲ هزار تیم کارهایشان را با آوند جلو میبرند.</p>
</aside>
<section className="flex items-start justify-center p-6 sm:p-10 lg:items-center">
{done ? (
<div className="flex w-full max-w-md flex-col items-center gap-4 text-center" role="status">
<span aria-hidden className="flex size-16 items-center justify-center rounded-full bg-success-subtle text-success [&>svg]:size-8">
<CheckIcon />
</span>
<h1 className="text-2xl font-bold">حسابت آماده است</h1>
<p className="leading-7 text-muted-foreground">لینکِ تأییدِ ایمیل را فرستادیم. تا آن موقع میتوانی فضای کارت را بچینی.</p>
<Button className="mt-4">ورود به آوند</Button>
</div>
) : (
<form
className="flex w-full max-w-md flex-col gap-8"
onSubmit={(event) => {
event.preventDefault()
if (last) setDone(true)
else setStep((value) => value + 1)
}}
>
<header className="flex flex-col gap-1">
<span className="text-sm text-muted-foreground">
مرحلهٔ {toFaDigits(step + 1)} از {toFaDigits(STEPS.length)}
</span>
<h1 className="text-2xl font-bold">{["حسابت را بساز", "دربارهٔ تیمت بگو", "پلنت را انتخاب کن"][step]}</h1>
</header>
{/* هر مرحله پنهان میشود نه حذف، تا با برگشت دادهها بمانند. */}
<div hidden={step !== 0} className="flex flex-col gap-5">
<TextField label="نام و نام خانوادگی" name="name" autoComplete="name" required={step === 0} />
<TextField label="ایمیل کاری" name="email" type="email" dir="ltr" autoComplete="email" required={step === 0} validate={(value) => (isValidEmail(value) ? undefined : "ایمیل معتبر نیست؛ مثل name@example.com")} />
<PasswordField label="رمز عبور" name="password" autoComplete="new-password" strength required={step === 0} />
</div>
<div hidden={step !== 1} className="flex flex-col gap-5">
<TextField label="نامِ تیم یا شرکت" name="team" required={step === 1} />
<div className="flex flex-col gap-2">
<span id="team-size" className="text-sm font-medium">
چند نفرید؟
</span>
<ToggleGroup type="single" variant="segmented" value={size} onValueChange={(value) => value && setSize(value as string)} aria-labelledby="team-size" className="w-full">
{["فقط من", "2-10", "11-50", "+50"].map((item) => (
<ToggleGroupItem key={item} value={item} className="flex-1">
<bdi>{toFaDigits(item)}</bdi>
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<TextField label="نقشِ شما" name="role" placeholder="مثلاً مدیرِ محصول" />
</div>
<RadioGroup hidden={step !== 2} name="plan" value={plan} onValueChange={setPlan} aria-label="پلن" className="gap-3">
{PLANS.map((item) => (
<div key={item.value} className={optionCard}>
<RadioGroupItem id={`plan-${item.value}`} value={item.value} className="mt-1" />
<Label htmlFor={`plan-${item.value}`} className="flex flex-1 flex-col items-start gap-1">
<span className="flex items-center gap-2 font-semibold">
{item.title}
{item.best && <Badge color="violet">پیشنهادِ ما</Badge>}
</span>
<span className="text-sm font-normal text-muted-foreground">{item.note}</span>
</Label>
<span className="font-bold tabular-nums">{item.price ? formatCurrency(item.price) : "رایگان"}</span>
</div>
))}
</RadioGroup>
<div className="flex flex-col gap-4 pt-4">
<div className="flex gap-3">
{step > 0 && (
<Button type="button" variant="outline" onClick={() => setStep((value) => value - 1)}>
قبلی
</Button>
)}
<Button type="submit" className="flex-1">
{last ? "ساختِ حساب" : "ادامه"}
</Button>
</div>
{step === 0 && (
<p className="text-center text-sm text-muted-foreground">
حساب داری؟ <Link href="#">وارد شو</Link>
</p>
)}
</div>
</form>
)}
</section>
</main>
</ControlSizeProvider>
)
}