ورودِ اپ با کد
پیشنمایش تمامصفحهورودِ تمامصفحهٔ اپ: شماره و کدِ ۶ رقمی در دو قدم، شمارشِ معکوسِ ارسالِ دوباره، خطای کد و دکمهٔ اصلیِ چسبیده به پایین بالای ناحیهٔ امن.
۱. نصب کامپوننتها
این بلاک از ۸ آیتم دیگ استفاده میکند: app-barbuttoncontrol-sizeiconsinput-otplinkpersiantext-field
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/app-bar.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/input-otp.json https://docs.digdesign.ir/r/link.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/app-bar.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/input-otp.json https://docs.digdesign.ir/r/link.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/app-bar.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/input-otp.json https://docs.digdesign.ir/r/link.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/app-bar.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/input-otp.json https://docs.digdesign.ir/r/link.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/text-field.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
mobile-login.tsx
"use client"
import * as React from "react"
import { ChevronRightIcon, SparklesIcon } from "@/components/icons"
import { AppBar, AppBarLeading } from "@/components/ui/app-bar"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"
import { Link } from "@/components/ui/link"
import { TextField } from "@/components/ui/text-field"
import { isValidMobile, toFaDigits } from "@/lib/persian"
const RESEND_SECONDS = 60
/**
* ورودِ اپ با کدِ پیامکی، تمامصفحه: نشانِ برند و تیتر بالا، فیلدِ موبایل یا کدِ ۶ رقمی وسط،
* و دکمهٔ اصلی چسبیده به پایین (بالای ناحیهٔ امن) تا با کیبورد هم در دسترس بماند.
*/
export default function MobileLoginBlock() {
const [step, setStep] = React.useState<"mobile" | "code">("mobile")
const [mobile, setMobile] = React.useState("")
const [code, setCode] = React.useState("")
const [seconds, setSeconds] = React.useState(RESEND_SECONDS)
const [loading, setLoading] = React.useState(false)
const [invalid, setInvalid] = React.useState(false)
React.useEffect(() => {
if (step !== "code" || seconds === 0) return
const timer = setTimeout(() => setSeconds((value) => value - 1), 1000)
return () => clearTimeout(timer)
}, [step, seconds])
async function submit(event: React.FormEvent) {
event.preventDefault()
setLoading(true)
await new Promise((resolve) => setTimeout(resolve, 700))
setLoading(false)
if (step === "mobile") {
setSeconds(RESEND_SECONDS)
setStep("code")
} else {
// جای بررسی واقعی کد؛ این نمونه فقط ۱۲۳۴۵۶ را میپذیرد.
setInvalid(code !== "123456")
}
}
const ready = step === "mobile" ? isValidMobile(mobile) : code.length === 6
return (
<ControlSizeProvider size="xl" asChild>
<form onSubmit={submit} className="mx-auto flex min-h-svh w-full max-w-md flex-col bg-background">
<AppBar>
<AppBarLeading>
{step === "code" && (
<Button type="button" variant="ghost" size="icon" aria-label="ویرایشِ شماره" onClick={() => setStep("mobile")}>
<ChevronRightIcon className="ltr:rotate-180" />
</Button>
)}
</AppBarLeading>
</AppBar>
<main className="flex flex-1 flex-col gap-8 px-6 pt-6">
<header className="flex flex-col gap-4">
<span aria-hidden className="flex size-14 items-center justify-center rounded-xl bg-[var(--dig-violet-700)] text-[var(--dig-white)] [&>svg]:size-7">
<SparklesIcon />
</span>
<h1 className="text-3xl font-black">{step === "mobile" ? "خوش آمدی" : "کد را وارد کن"}</h1>
<p className="leading-7 text-muted-foreground">
{step === "mobile" ? (
"شمارهات را بنویس؛ کدِ ورود پیامک میشود."
) : (
<>
کدِ ۶ رقمی به <bdi dir="ltr" className="font-medium text-foreground">{toFaDigits(mobile)}</bdi> فرستاده شد.
</>
)}
</p>
</header>
{step === "mobile" ? (
<TextField label="شماره موبایل" name="mobile" type="tel" inputMode="numeric" dir="ltr" maxLength={11} autoComplete="tel" placeholder="09121234567" value={mobile} onValueChange={setMobile} autoFocus />
) : (
<div className="flex flex-col gap-4">
<InputOTP
aria-label="کدِ تأیید"
value={code}
onValueChange={(value) => {
setCode(value)
setInvalid(false)
}}
aria-invalid={invalid || undefined}
>
<InputOTPGroup>
{[0, 1, 2, 3, 4, 5].map((index) => (
<InputOTPSlot key={index} index={index} />
))}
</InputOTPGroup>
</InputOTP>
{invalid && (
<p role="alert" className="text-sm text-destructive">
کد درست نیست؛ دوباره نگاه کن.
</p>
)}
{seconds > 0 ? (
<span className="text-sm text-muted-foreground">ارسالِ دوباره تا {toFaDigits(`${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`)}</span>
) : (
<Button type="button" variant="ghost" className="self-start" onClick={() => setSeconds(RESEND_SECONDS)}>
ارسالِ دوبارهٔ کد
</Button>
)}
</div>
)}
</main>
<footer className="sticky bottom-0 flex flex-col gap-4 bg-background p-6 pb-[max(1.5rem,env(safe-area-inset-bottom))]">
<Button type="submit" fullWidth loading={loading} disabled={!ready}>
{step === "mobile" ? "دریافتِ کد" : "ورود"}
</Button>
<p className="text-center text-xs leading-6 text-muted-foreground">
ورود یعنی پذیرفتنِ <Link href="#" size="sm">قوانین</Link> و <Link href="#" size="sm">حریمِ خصوصی</Link>.
</p>
</footer>
</form>
</ControlSizeProvider>
)
}