آنبوردینگ موبایل
پیشنمایش تمامصفحهآشنایی در سه قدم با نوارِ پیشرفت: معرفی روی عکسِ تمامصفحه با تیترِ درشت، انتخابِ علاقهها با چیپهای بزرگ (حداقل سه، شمارنده روی دکمه)، و پیشنهادِ دنبالکردن با دکمهٔ دنبال کن/دنبال شد؛ «رد کردن» همیشه و دکمهٔ اصلیِ چسبان.
۱. نصب کامپوننتها
این بلاک از ۶ آیتم دیگ استفاده میکند: avatarbuttoncontrol-sizeiconspersiantoggle-group
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/avatar.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/persian.json https://docs.digdesign.ir/r/toggle-group.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.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/persian.json https://docs.digdesign.ir/r/toggle-group.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.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/persian.json https://docs.digdesign.ir/r/toggle-group.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/avatar.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/persian.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
mobile-onboarding.tsx
"use client"
import * as React from "react"
import { ArrowUpIcon, CheckIcon, ChevronRightIcon } from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { toFaDigits } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"
const TOPICS = ["بودجهبندی", "سرمایهگذاری", "طلا و سکه", "بورس", "پسانداز", "وام و قسط", "مالیات", "رمزارز", "کسبوکار", "بیمه", "املاک", "بازنشستگی"]
const MIN_TOPICS = 3
const EXPERTS = [
{ id: "1", name: "نگار احمدی", role: "مشاورِ مالیِ خانواده", photo: stockImages.personWoman3.src },
{ id: "2", name: "امیر رضایی", role: "تحلیلگرِ بازارِ سرمایه", photo: stockImages.personMan1.src },
{ id: "3", name: "سارا محمدی", role: "نویسندهٔ «پولِ ساده»", photo: stockImages.personWoman2.src },
{ id: "4", name: "علی کاظمی", role: "کارشناسِ مالیات و بیمه", photo: stockImages.personMan2.src },
]
const STEPS = 3
/**
* آنبوردینگِ اپِ موبایل در سه قدم با نوارِ پیشرفتِ بالا: ۱) معرفی روی عکسِ تمامصفحه با
* تیترِ درشت و دکمهٔ گردِ «بعدی»، ۲) انتخابِ علاقهها با چیپهای بزرگ (حداقل سه، با شمارنده
* روی دکمه)، ۳) پیشنهادِ دنبالکردن با دکمهٔ «دنبال کن» که درجا «دنبال شد» میشود. «رد کردن»
* همیشه هست و دکمهٔ اصلی پایینِ صفحه چسبیده است.
*/
export default function MobileOnboardingBlock() {
const [step, setStep] = React.useState(0)
const [topics, setTopics] = React.useState<string[]>(["بودجهبندی", "پسانداز"])
const [following, setFollowing] = React.useState<string[]>(["1"])
const enoughTopics = topics.length >= MIN_TOPICS
const progress = (
<div className="flex gap-1.5" role="progressbar" aria-label="پیشرفتِ آشنایی" aria-valuemin={1} aria-valuemax={STEPS} aria-valuenow={step + 1}>
{Array.from({ length: STEPS }, (_, index) => (
<span key={index} className={`h-1 flex-1 rounded-full ${index <= step ? (step === 0 ? "bg-[var(--dig-white)]" : "bg-foreground") : step === 0 ? "bg-[color-mix(in_oklab,var(--dig-white)_35%,transparent)]" : "bg-muted"}`} />
))}
</div>
)
if (step === 0) {
return (
<ControlSizeProvider size="xl" asChild>
<div className="relative mx-auto flex min-h-svh w-full max-w-md flex-col overflow-hidden bg-[var(--dig-slate-900)] text-[var(--dig-white)]">
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img src={stockImages.coverFinanceDesk.src} alt="" className="absolute inset-0 size-full object-cover" />
{/* سایهروشن از پایین تا متنِ سفید روی هر عکسی خوانا بماند. */}
<span aria-hidden className="absolute inset-0 bg-linear-to-t from-[var(--dig-slate-950)] via-[color-mix(in_oklab,var(--dig-slate-950)_35%,transparent)] via-40% to-transparent" />
<header className="relative flex items-center gap-4 p-4">
<div className="flex-1">{progress}</div>
<Button variant="ghost" onClick={() => setStep(2)} className="text-[var(--dig-white)] hover:bg-[color-mix(in_oklab,var(--dig-white)_15%,var(--dig-slate-900))]">
رد کردن
</Button>
</header>
<section className="relative mt-auto flex flex-col gap-8 p-6 pb-10">
<div className="flex flex-col gap-3">
<h1 className="text-3xl leading-snug font-black text-balance">پولت را بشناس، آرامتر خرج کن</h1>
<p className="leading-7 text-[var(--dig-slate-200)]">همهٔ کارتها و حسابها یکجا؛ با پیشنهادهایی که برای خودت ساخته میشود.</p>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[var(--dig-slate-300)] tabular-nums">
{toFaDigits(1)} از {toFaDigits(STEPS)}
</span>
<Button size="icon" aria-label="بعدی" onClick={() => setStep(1)} className="size-14 rounded-full bg-[var(--dig-white)] text-[var(--dig-slate-900)] hover:bg-[var(--dig-slate-100)]">
<ChevronRightIcon className="rotate-180" />
</Button>
</div>
</section>
</div>
</ControlSizeProvider>
)
}
return (
<ControlSizeProvider size="xl" asChild>
<div className="mx-auto flex min-h-svh w-full max-w-md flex-col bg-background">
<header className="flex items-center gap-2 p-4">
<Button variant="ghost" size="icon" aria-label="قبلی" onClick={() => setStep((value) => value - 1)} className="-ms-2">
<ChevronRightIcon />
</Button>
<div className="flex-1">{progress}</div>
<Button variant="ghost" onClick={() => setStep(STEPS - 1)} className={step === STEPS - 1 ? "invisible" : undefined}>
رد کردن
</Button>
</header>
{step === 1 ? (
<section aria-labelledby="topics-title" className="flex flex-1 flex-col gap-8 px-4 pt-4">
<div className="flex flex-col gap-2">
<h1 id="topics-title" className="text-3xl leading-snug font-black">
به چه چیزهایی
<br />
علاقه داری؟
</h1>
<p className="text-muted-foreground">حداقل {toFaDigits(MIN_TOPICS)} موضوع؛ بعداً هم عوض میشود.</p>
</div>
<ToggleGroup type="multiple" variant="outline" separated value={topics} onValueChange={(value) => setTopics(value as string[])} aria-labelledby="topics-title" className="flex-wrap justify-start gap-2">
{TOPICS.map((topic) => (
<ToggleGroupItem
key={topic}
value={topic}
className="rounded-full! border-transparent bg-muted px-5 text-base font-bold shadow-none data-[state=on]:border-[var(--dig-violet-600)] data-[state=on]:bg-[var(--dig-violet-600)] data-[state=on]:text-[var(--dig-white)]"
>
{topic}
</ToggleGroupItem>
))}
</ToggleGroup>
</section>
) : (
<section aria-labelledby="follow-title" className="flex flex-1 flex-col gap-8 px-4 pt-4">
<div className="flex flex-col gap-2">
<h1 id="follow-title" className="text-3xl leading-snug font-black">
پیشنهاد
<br />
برای تو
</h1>
<p className="text-muted-foreground">کارشناسهایی که دربارهٔ علاقههایت مینویسند.</p>
</div>
<ul className="flex flex-col gap-5">
{EXPERTS.map((expert) => {
const followed = following.includes(expert.id)
return (
<li key={expert.id} className="flex items-center gap-3">
<Avatar size="lg" alt={expert.name} src={expert.photo} className="size-11" />
<span className="flex min-w-0 flex-1 flex-col">
<span className="text-[0.9375rem] leading-6 font-bold">{expert.name}</span>
<span className="truncate text-sm leading-5 text-muted-foreground">{expert.role}</span>
</span>
<ControlSizeProvider size="lg" asChild>
<Button
variant={followed ? "default" : "secondary"}
aria-pressed={followed}
onClick={() => setFollowing((list) => (followed ? list.filter((id) => id !== expert.id) : [...list, expert.id]))}
className={`min-w-24 shrink-0 rounded-full px-4 text-sm ${followed ? "bg-[var(--dig-violet-600)] text-[var(--dig-white)] hover:bg-[var(--dig-violet-700)]" : ""}`}
>
{followed && <CheckIcon />}
{followed ? "دنبال شد" : "دنبال کن"}
</Button>
</ControlSizeProvider>
</li>
)
})}
</ul>
</section>
)}
<footer className="sticky bottom-0 bg-background p-4 pb-[max(1.5rem,env(safe-area-inset-bottom))]">
{step === 1 ? (
<Button fullWidth disabled={!enoughTopics} onClick={() => setStep(2)} className="rounded-full">
{enoughTopics ? `ادامه با ${toFaDigits(topics.length)} موضوع` : `${toFaDigits(MIN_TOPICS - topics.length)} موضوعِ دیگر انتخاب کن`}
</Button>
) : (
<Button fullWidth className="rounded-full">
شروع کن
<ArrowUpIcon className="-rotate-90" />
</Button>
)}
</footer>
</div>
</ControlSizeProvider>
)
}