تأیید کد پیامکی
پیشنمایش تمامصفحههمان ساختارِ ورود و ثبتنام: کدِ ۶ رقمی با خطای کدِ نادرست، شمارشِ معکوسِ ارسالِ دوباره و ویرایشِ شماره؛ پنلِ برند کنارش.
۱. نصب کامپوننتها
این بلاک از ۷ آیتم دیگ استفاده میکند: avatarbuttoncontrol-sizeiconsinput-otplinkpersian
پکیجمنیجر پیشفرض 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/input-otp.json https://docs.digdesign.ir/r/link.json https://docs.digdesign.ir/r/persian.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/input-otp.json https://docs.digdesign.ir/r/link.json https://docs.digdesign.ir/r/persian.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/input-otp.json https://docs.digdesign.ir/r/link.json https://docs.digdesign.ir/r/persian.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/input-otp.json https://docs.digdesign.ir/r/link.json https://docs.digdesign.ir/r/persian.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
otp-verify.tsx
"use client"
import * as React from "react"
import { AvatarGenderIcon, SparklesIcon } from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
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 { toFaDigits } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"
const RESEND_SECONDS = 60
/**
* پنلِ برند کنارِ فرم (در موبایل پنهان تا فرم بالای صفحه بماند): پیشنمایشِ تزئینی محصول و نظرِ یک
* مشتری. رنگها با بازتعریفِ متغیرهای پایه روی خودِ پنل برعکس میشوند.
*/
function BrandPanel() {
return (
<aside
aria-label="دربارهٔ حسابیار"
className="relative hidden flex-col justify-end overflow-hidden rounded-xl bg-[var(--dig-violet-800)] p-10 text-[var(--dig-white)] lg:flex [--muted-foreground:var(--dig-violet-200)]"
>
{/* پیشنمایشِ محصول: سه پنجرهٔ پشتِ هم با خطهای اسکلتی. */}
<div aria-hidden className="absolute -end-16 top-12 hidden h-80 w-[115%] lg:block">
{[2, 1, 0].map((layer) => (
<div
key={layer}
className="absolute inset-y-0 rounded-xl border border-[var(--dig-violet-600)] bg-[var(--dig-violet-700)] p-5"
style={{ insetInlineStart: `${layer * 2.5}rem`, top: `${layer * 1.5}rem`, transform: "rotate(-6deg)" }}
>
{layer === 0 && (
<div className="flex h-full gap-4">
<div className="flex w-40 flex-col gap-3">
<span className="flex size-9 items-center justify-center rounded-lg bg-[var(--dig-violet-500)] [&>svg]:size-4">
<SparklesIcon />
</span>
<span className="flex h-9 items-center gap-2 rounded-lg bg-[var(--dig-violet-600)] px-3 text-sm">داشبورد</span>
{[70, 55, 80, 60].map((width) => (
<span key={width} className="h-2.5 rounded-full bg-[var(--dig-violet-600)]" style={{ width: `${width}%` }} />
))}
</div>
<div className="flex flex-1 flex-col gap-3">
<span className="h-16 rounded-lg bg-[var(--dig-violet-600)]" />
<span className="h-2.5 w-2/3 rounded-full bg-[var(--dig-violet-600)]" />
<span className="h-2.5 w-1/2 rounded-full bg-[var(--dig-violet-600)]" />
</div>
</div>
)}
</div>
))}
</div>
<span aria-hidden className="pointer-events-none absolute inset-x-0 bottom-0 h-2/3 bg-linear-to-t from-[var(--dig-violet-800)] from-40% to-transparent" />
<figure className="relative flex max-w-md flex-col gap-4 lg:gap-6">
<Avatar size="lg" alt="سارا محمدی" src={stockImages.personWoman2.src} icon={<AvatarGenderIcon gender="female" />} className="hidden ring-2 ring-[var(--dig-violet-500)] lg:flex" />
<blockquote className="text-lg leading-relaxed font-bold lg:text-2xl">
«حسابوکتابِ ماهانهمان از سه روز به یک ساعت رسید.{" "}
<span className="text-muted-foreground">حالا وقتمان صرفِ فروش میشود، نه اکسل.»</span>
</blockquote>
<figcaption className="flex flex-col gap-0.5">
<span className="font-semibold">سارا محمدی</span>
<span className="text-sm text-muted-foreground">مدیرِ فروشگاهِ آنلاین</span>
</figcaption>
</figure>
</aside>
)
}
/** نشانِ برند بالای فرم. */
function BrandMark() {
return (
<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>
)
}
/**
* تأییدِ شماره با همان ساختارِ ورود و ثبتنام: فرم در ستونِ شروع، پنلِ برند در
* ستونِ پایان. خطای کدِ نادرست زیرِ خانهها و شمارشِ معکوسِ ارسالِ دوباره.
*/
export default function OtpVerifyBlock() {
const [code, setCode] = React.useState("")
const [seconds, setSeconds] = React.useState(RESEND_SECONDS)
const [checking, setChecking] = React.useState(false)
const [invalid, setInvalid] = React.useState(false)
React.useEffect(() => {
if (seconds === 0) return
const timer = setTimeout(() => setSeconds((value) => value - 1), 1000)
return () => clearTimeout(timer)
}, [seconds])
async function verify(value: string) {
setChecking(true)
// جای بررسی واقعی کد؛ این نمونه فقط ۱۲۳۴۵۶ را میپذیرد.
await new Promise((resolve) => setTimeout(resolve, 700))
setChecking(false)
setInvalid(value !== "123456")
}
const countdown = toFaDigits(`${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`)
return (
<main className="grid grid-cols-1 min-h-svh gap-4 bg-background p-4 lg:grid-cols-2">
<div className="flex flex-col items-center px-4 py-10">
<ControlSizeProvider size="xl" className="flex w-full max-w-sm flex-1 flex-col justify-center gap-8">
<header className="flex flex-col items-center gap-4 text-center">
<BrandMark />
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold">تأیید شمارهٔ موبایل</h1>
<p className="text-sm leading-6 text-muted-foreground">
کدِ ۶ رقمی پیامکشده به <bdi dir="ltr" className="font-medium text-foreground">{toFaDigits("0912 *** 4567")}</bdi> را وارد کنید.
</p>
</div>
</header>
<div className="flex flex-col items-center gap-3">
<InputOTP
aria-label="کد تأیید"
value={code}
onValueChange={(value) => {
setCode(value)
setInvalid(false)
}}
onComplete={verify}
disabled={checking}
invalid={invalid}
aria-describedby="otp-status"
>
<InputOTPGroup>
{[0, 1, 2, 3, 4, 5].map((index) => (
<InputOTPSlot key={index} index={index} />
))}
</InputOTPGroup>
</InputOTP>
<p id="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>
</div>
<Button fullWidth loading={checking} disabled={code.length < 6} onClick={() => verify(code)}>
تأیید و ادامه
</Button>
<p className="text-center text-sm text-muted-foreground">
شماره اشتباه است؟ <Link href="/signup">ویرایش شماره</Link>
</p>
</ControlSizeProvider>
<p className="text-xs text-muted-foreground">
<Link href="#" size="sm">
قوانین
</Link>{" "}
و{" "}
<Link href="#" size="sm">
حریم خصوصی
</Link>
</p>
</div>
<BrandPanel />
</main>
)
}