راهاندازی حساب
پیشنمایش تمامصفحهآنبوردینگِ سهقدمی: مراحلِ عمودی روی سطحِ 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.jsonpnpm 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.jsonyarn 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.jsonbunx --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>
)
}