بلاک‌ها · ورود و ثبت‌نام

ثبت‌نامِ چندمرحله‌ای

پیش‌نمایش تمام‌صفحه

ثبت‌نام در سه قدم با ستونِ تیرهٔ مراحل: حساب (نام، ایمیل، رمز با قدرت)، تیم (اندازه با سگمنت، نقش) و انتخابِ پلن با کارت‌های رادیویی؛ برگشت داده‌ها را نگه می‌دارد و پیامِ موفقیت.

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

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