ورود با کد پیامکی
پیشنمایش تمامصفحهورود و ثبتنامِ یکجا با شمارهٔ موبایل: کارتِ وسطچین روی پترنِ نقطهای، دو قدمِ شماره و کدِ ۶ رقمی در همان کارت، خطای کدِ نادرست و شمارشِ معکوسِ ارسالِ دوباره.
۱. نصب کامپوننتها
این بلاک از ۸ آیتم دیگ استفاده میکند: background-patternbuttoncontrol-sizeiconsinput-otplinkpersiantext-field
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/background-pattern.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/background-pattern.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/background-pattern.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/background-pattern.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 بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
login-otp.tsx
"use client"
import * as React from "react"
import { SparklesIcon } from "@/components/icons"
import { DotPattern } from "@/components/ui/background-pattern"
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 LoginOtpBlock() {
const [step, setStep] = React.useState<"mobile" | "code">("mobile")
const [mobile, setMobile] = React.useState("")
const [sending, setSending] = React.useState(false)
const [seconds, setSeconds] = React.useState(RESEND_SECONDS)
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 sendCode(event: React.FormEvent) {
event.preventDefault()
if (!isValidMobile(mobile)) return
setSending(true)
await new Promise((resolve) => setTimeout(resolve, 700))
setSending(false)
setSeconds(RESEND_SECONDS)
setStep("code")
}
async function verify(code: string) {
setSending(true)
// جای بررسی واقعی کد؛ این نمونه فقط ۱۲۳۴۵۶ را میپذیرد.
await new Promise((resolve) => setTimeout(resolve, 700))
setSending(false)
setInvalid(code !== "123456")
}
const countdown = toFaDigits(`${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`)
return (
<ControlSizeProvider size="xl" asChild>
<main className="relative flex min-h-svh items-center justify-center overflow-hidden bg-muted p-4">
<DotPattern size={22} />
<div className="relative flex w-full max-w-md flex-col gap-8 rounded-xl border border-border bg-card p-8 shadow-card sm:p-10">
<header className="flex flex-col items-center gap-4 text-center">
<span aria-hidden className="flex size-12 items-center justify-center rounded-xl bg-[var(--dig-violet-700)] text-[var(--dig-white)] [&>svg]:size-6">
<SparklesIcon />
</span>
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold">{step === "mobile" ? "ورود یا ثبتنام" : "کد تأیید را وارد کنید"}</h1>
<p className="text-sm leading-6 text-muted-foreground">
{step === "mobile" ? (
"شمارهٔ موبایلتان را بنویسید؛ کدِ ورود پیامک میشود."
) : (
<>
کدِ ۶ رقمی به <bdi dir="ltr" className="font-medium text-foreground">{toFaDigits(mobile)}</bdi> پیامک شد.
</>
)}
</p>
</div>
</header>
{step === "mobile" ? (
<form onSubmit={sendCode} className="flex flex-col gap-4">
<TextField
label="شماره موبایل"
type="tel"
dir="ltr"
maxLength={11}
autoComplete="tel"
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
value={mobile}
onValueChange={setMobile}
required
validate={(value) => (isValidMobile(value) ? undefined : "شماره موبایل باید با ۰۹ شروع شود و ۱۱ رقم باشد.")}
/>
<Button type="submit" fullWidth loading={sending} disabled={!isValidMobile(mobile)} className="mt-4">
دریافت کد
</Button>
</form>
) : (
<div className="flex flex-col items-center gap-4">
<InputOTP
aria-label="کد تأیید"
onComplete={verify}
onValueChange={() => setInvalid(false)}
disabled={sending}
invalid={invalid}
aria-describedby="login-otp-status"
>
<InputOTPGroup>
{[0, 1, 2, 3, 4, 5].map((index) => (
<InputOTPSlot key={index} index={index} />
))}
</InputOTPGroup>
</InputOTP>
<p id="login-otp-status" className="min-h-5 text-sm text-destructive" aria-live="polite">
{invalid ? "کد واردشده درست نیست." : ""}
</p>
<p className="text-sm text-muted-foreground">
{seconds > 0 ? (
<>
ارسالِ دوباره تا <span className="font-medium text-foreground tabular-nums">{countdown}</span>
</>
) : (
<Button variant="link" className="h-auto p-0" onClick={() => setSeconds(RESEND_SECONDS)}>
ارسالِ دوبارهٔ کد
</Button>
)}
</p>
<Button variant="ghost" fullWidth onClick={() => setStep("mobile")} className="mt-2">
ویرایش شماره
</Button>
</div>
)}
<p className="text-center text-xs leading-6 text-muted-foreground">
ورود یعنی پذیرشِ{" "}
<Link href="#" size="sm">
قوانین
</Link>{" "}
و{" "}
<Link href="#" size="sm">
حریم خصوصی
</Link>
.
</p>
</div>
</main>
</ControlSizeProvider>
)
}