بلاک‌ها · حساب کاربری

راه‌اندازی حساب

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

آن‌بوردینگِ سه‌قدمی: مراحلِ عمودی روی سطحِ subtle (موبایل: افقی)، نام و نشانیِ فضای کار، هدف با کارت‌های رادیویی، دعوتِ تیم با چیپِ ایمیل و «فعلاً نه»، و صفحهٔ پایانی با عکس و رفتن به داشبورد.

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

این بلاک از ۱۰ آیتم دیگ استفاده می‌کند: buttonchipcontrol-sizeiconsinputlabelpersianradio-groupsteppertext-field

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chip.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chip.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chip.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/chip.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.json

۲. کد صفحه

کد را در app/<route>/page.tsx بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

onboarding.tsx
"use client"

import * as React from "react"

import { BarChartIcon, CheckIcon, ReceiptIcon, SparklesIcon, UsersIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Chip, ChipCloseButton, ChipLabel } from "@/components/ui/chip"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Stepper, StepperDescription, StepperItem, StepperTitle } from "@/components/ui/stepper"
import { TextField } from "@/components/ui/text-field"
import { isValidEmail } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"

const STEPS = [
  { title: "فضای کار", text: "نام و نشانی" },
  { title: "هدفِ شما", text: "برای پیشنهادِ درست" },
  { title: "دعوتِ تیم", text: "بعداً هم می‌شود" },
]

const GOALS = [
  { value: "invoices", icon: <ReceiptIcon />, title: "فاکتور و دریافتِ پول", text: "صدورِ فاکتور و پیگیری بدهکاران" },
  { value: "reports", icon: <BarChartIcon />, title: "گزارشِ مالی", text: "سود و زیان و جریانِ نقدی" },
  { value: "team", icon: <UsersIcon />, title: "حسابداری تیمی", text: "دسترسی چندنفره با نقش" },
]

/**
 * راه‌اندازی حساب در سه قدم: مراحلِ عمودی در ستونِ کنار (در موبایل افقی بالا)،
 * یک فرم در هر قدم، دکمه‌های قبلی و بعدی با فاصلهٔ بیشتر در پایین، و پیامِ
 * پایانی با گام‌های بعدی. دعوتِ تیم لازم نیست و «فعلاً نه» دارد.
 */
export default function OnboardingBlock() {
  const [step, setStep] = React.useState(0)
  const [workspace, setWorkspace] = React.useState("")
  const [goal, setGoal] = React.useState("invoices")
  const [emails, setEmails] = React.useState<string[]>(["arash@example.com"])
  const [draft, setDraft] = React.useState("")
  const [draftError, setDraftError] = React.useState("")
  const [slug, setSlug] = React.useState("")

  const addEmail = () => {
    const email = draft.trim()
    if (!isValidEmail(email)) {
      setDraftError("یک ایمیلِ درست وارد کنید.")
      return
    }
    if (!emails.includes(email)) setEmails((list) => [...list, email])
    setDraft("")
    setDraftError("")
  }

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="grid grid-cols-1 min-h-svh bg-background lg:grid-cols-[22rem_minmax(0,1fr)]">
        {/* ستونِ مراحل روی سطحِ subtle؛ نشانِ برند بالا و پیامِ کمک پایین. */}
        <aside className="flex flex-col gap-10 border-b border-border bg-subtle p-6 sm:p-10 lg:border-e lg:border-b-0">
          <span className="flex items-center gap-2 font-black">
            <span aria-hidden className="flex size-9 items-center justify-center rounded-lg bg-[var(--dig-violet-700)] text-[var(--dig-white)] [&>svg]:size-4">
              <SparklesIcon />
            </span>
            حساب‌یار
          </span>
          <Stepper value={step} orientation="vertical" className="hidden lg:flex">
            {STEPS.map((item) => (
              <StepperItem key={item.title}>
                <StepperTitle>{item.title}</StepperTitle>
                <StepperDescription>{item.text}</StepperDescription>
              </StepperItem>
            ))}
          </Stepper>
          <Stepper value={step} className="lg:hidden">
            {STEPS.map((item) => (
              <StepperItem key={item.title}>
                <StepperTitle>{item.title}</StepperTitle>
              </StepperItem>
            ))}
          </Stepper>
          <p className="mt-auto hidden text-sm leading-7 text-muted-foreground lg:block">
            سؤالی دارید؟ پشتیبانی هر روز از ۸ تا ۲۲ در گفت‌وگوی آنلاین پاسخ‌گوست.
          </p>
        </aside>

        <section className="flex flex-col px-4 py-10 sm:px-10 lg:py-16">
          <div className="mx-auto flex w-full max-w-xl flex-1 flex-col gap-10">
            {step < 3 && (
              <header className="flex flex-col gap-2">
                <span className="text-sm font-medium text-brand-secondary">
                  قدمِ {(step + 1).toLocaleString("fa-IR")} از ۳
                </span>
                <h1 className="text-3xl font-bold">
                  {step === 0 && "فضای کارتان را بسازید"}
                  {step === 1 && "بیشتر برای چه کاری؟"}
                  {step === 2 && "همکارانتان را دعوت کنید"}
                </h1>
                <p className="text-muted-foreground">
                  {step === 0 && "همه‌چیزِ حساب‌وکتابِ کسب‌وکار یک‌جا؛ اسمش را بعداً هم می‌شود عوض کرد."}
                  {step === 1 && "صفحهٔ شروع و پیشنهادها را بر همین اساس می‌چینیم."}
                  {step === 2 && "هر نفر با ایمیلِ خودش وارد می‌شود و نقشش را بعداً تعیین می‌کنید."}
                </p>
              </header>
            )}

            {step === 0 && (
              <div className="flex flex-col gap-5">
                <TextField label="نامِ کسب‌وکار" value={workspace} onValueChange={setWorkspace} placeholder="مثلاً کافه رست" required />
                <TextField
                  label="نشانی فضای کار"
                  dir="ltr"
                  value={slug}
                  onValueChange={(value) => setSlug(value.toLowerCase().replace(/[^a-z0-9-]/g, ""))}
                  placeholder="kafe-rast"
                  description={slug ? `${slug}.hesabyar.ir` : "حروفِ انگلیسی، عدد و خطِ تیره."}
                />
              </div>
            )}

            {step === 1 && (
              <RadioGroup value={goal} onValueChange={setGoal} aria-label="هدفِ اصلی" className="gap-3">
                {GOALS.map((item) => (
                  <div
                    key={item.value}
                    className="flex items-center gap-4 rounded-xl border border-border p-4 transition-colors has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary-subtle"
                  >
                    <RadioGroupItem id={`goal-${item.value}`} value={item.value} />
                    <span aria-hidden className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-[color-mix(in_oklab,var(--brand-secondary)_12%,var(--card))] text-brand-secondary [&>svg]:size-5">
                      {item.icon}
                    </span>
                    <Label htmlFor={`goal-${item.value}`} className="flex flex-1 flex-col items-start gap-1">
                      <span className="font-semibold">{item.title}</span>
                      <span className="text-sm font-normal text-muted-foreground">{item.text}</span>
                    </Label>
                  </div>
                ))}
              </RadioGroup>
            )}

            {step === 2 && (
              <div className="flex flex-col gap-4">
                <div className="flex flex-col gap-2">
                  <Label htmlFor="onboarding-invite">ایمیلِ همکار</Label>
                  <div className="flex gap-2">
                    <Input
                      id="onboarding-invite"
                      dir="ltr"
                      type="email"
                      placeholder="name@example.com"
                      value={draft}
                      onChange={(event) => setDraft(event.target.value)}
                      onKeyDown={(event) => {
                        if (event.key === "Enter") {
                          event.preventDefault()
                          addEmail()
                        }
                      }}
                      aria-invalid={draftError ? true : undefined}
                      aria-describedby={draftError ? "onboarding-invite-error" : undefined}
                      className="flex-1"
                    />
                    <Button variant="outline" onClick={addEmail} disabled={!draft.trim()}>
                      افزودن
                    </Button>
                  </div>
                  {draftError && (
                    <p id="onboarding-invite-error" role="alert" className="text-xs text-destructive">
                      {draftError}
                    </p>
                  )}
                </div>
                {emails.length > 0 && (
                  <ul className="flex flex-wrap gap-2" aria-label="دعوت‌شده‌ها">
                    {emails.map((email) => (
                      <li key={email}>
                        <Chip>
                          <ChipLabel>
                            <bdi dir="ltr">{email}</bdi>
                          </ChipLabel>
                          <ChipCloseButton aria-label={`حذفِ ${email}`} onClick={() => setEmails((list) => list.filter((item) => item !== email))} />
                        </Chip>
                      </li>
                    ))}
                  </ul>
                )}
              </div>
            )}

            {step === 3 && (
              <div className="flex flex-1 flex-col items-center justify-center gap-6 text-center" role="status">
                <span aria-hidden className="flex size-16 items-center justify-center rounded-xl bg-success-subtle text-success [&>svg]:size-8">
                  <CheckIcon />
                </span>
                <div className="flex flex-col gap-2">
                  <h1 className="text-3xl font-bold">همه‌چیز آماده است!</h1>
                  <p className="text-muted-foreground">
                    فضای کارِ «{workspace || "شما"}» ساخته شد{emails.length > 0 && ` و دعوت‌نامه برای ${emails.length.toLocaleString("fa-IR")} نفر رفت`}.
                  </p>
                </div>
                {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
                <img src={stockImages.coverMinimalRoom.src} alt="" className="aspect-[16/9] w-full max-w-md rounded-xl object-cover" />
                <Button>رفتن به داشبورد</Button>
              </div>
            )}

            {step < 3 && (
              // دکمه‌ها با فاصلهٔ بیشتر از فرم؛ «قبلی» فقط از قدمِ دوم، «فعلاً نه» فقط در دعوت.
              <div className="mt-auto flex items-center justify-between gap-3 border-t border-border pt-8">
                <Button variant="ghost" onClick={() => setStep((value) => value - 1)} disabled={step === 0}>
                  قبلی
                </Button>
                <div className="flex gap-3">
                  {step === 2 && (
                    <Button variant="outline" onClick={() => setStep(3)}>
                      فعلاً نه
                    </Button>
                  )}
                  <Button onClick={() => setStep((value) => value + 1)} disabled={step === 0 && !workspace.trim()}>
                    {step === 2 ? "ارسالِ دعوت و پایان" : "ادامه"}
                  </Button>
                </div>
              </div>
            )}
          </div>
        </section>
      </main>
    </ControlSizeProvider>
  )
}