بلاک‌ها · موبایل

آن‌بوردینگ موبایل

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

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

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

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