فراموشی رمز
پیشنمایش تمامصفحههمان ساختارِ ورود و ثبتنام: سه قدمِ شماره، کدِ پیامکی و رمزِ تازه با مراحل، در ستونِ فرم؛ پنلِ برند کنارش و پیامِ موفقیت.
۱. نصب کامپوننتها
این بلاک از ۱۰ آیتم دیگ استفاده میکند: avatarbuttoncontrol-sizeiconsinput-otplinkpassword-fieldpersiansteppertext-field
پکیجمنیجر پیشفرض 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/password-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.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/password-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.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/password-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.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/password-field.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/stepper.json https://docs.digdesign.ir/r/text-field.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
forgot-password.tsx
"use client"
import * as React from "react"
import NextLink from "next/link"
import { AvatarGenderIcon, CheckIcon, 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 { PasswordField } from "@/components/ui/password-field"
import { Stepper, StepperItem, StepperTitle } from "@/components/ui/stepper"
import { TextField } from "@/components/ui/text-field"
import { isValidMobile, toFaDigits } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"
/**
* پنلِ برند کنارِ فرم (در موبایل پنهان تا فرم بالای صفحه بماند): پیشنمایشِ تزئینی محصول و نظرِ یک
* مشتری. رنگها با بازتعریفِ متغیرهای پایه روی خودِ پنل برعکس میشوند.
*/
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 ForgotPasswordBlock() {
const [step, setStep] = React.useState(0)
const [mobile, setMobile] = React.useState("")
const [password, setPassword] = React.useState("")
const [confirm, setConfirm] = React.useState("")
const [loading, setLoading] = React.useState(false)
const mismatch = confirm.length > 0 && confirm !== password
const next = async () => {
setLoading(true)
await new Promise((resolve) => setTimeout(resolve, 600))
setLoading(false)
setStep((value) => value + 1)
}
const titles = [
{ title: "فراموشی رمز", text: "شمارهٔ موبایلِ حساب را بنویسید تا کدِ بازیابی بفرستیم." },
{ title: "کد تأیید", text: "" },
{ title: "رمز تازه", text: "رمزی بسازید که جای دیگری استفاده نکردهاید." },
]
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">
{step < 3 ? (
<>
<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">{titles[step].title}</h1>
<p className="text-sm leading-6 text-muted-foreground">
{step === 1 ? (
<>
کدِ ۶ رقمی به <bdi dir="ltr" className="font-medium text-foreground">{toFaDigits(mobile)}</bdi> پیامک شد.
</>
) : (
titles[step].text
)}
</p>
</div>
</header>
<Stepper value={step} className="w-full">
<StepperItem>
<StepperTitle>شماره</StepperTitle>
</StepperItem>
<StepperItem>
<StepperTitle>کد تأیید</StepperTitle>
</StepperItem>
<StepperItem>
<StepperTitle>رمز تازه</StepperTitle>
</StepperItem>
</Stepper>
{step === 0 && (
<form
onSubmit={(event) => {
event.preventDefault()
if (isValidMobile(mobile)) void next()
}}
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={loading} className="mt-4">
ارسال کد
</Button>
</form>
)}
{step === 1 && (
<div className="flex flex-col items-center gap-6">
<InputOTP aria-label="کد تأیید" onComplete={next} disabled={loading}>
<InputOTPGroup>
{[0, 1, 2, 3, 4, 5].map((index) => (
<InputOTPSlot key={index} index={index} />
))}
</InputOTPGroup>
</InputOTP>
<p className="text-sm text-muted-foreground">
شماره اشتباه است؟{" "}
<Button variant="link" className="h-auto p-0" onClick={() => setStep(0)}>
تغییر شماره
</Button>
</p>
</div>
)}
{step === 2 && (
<form
onSubmit={(event) => {
event.preventDefault()
if (password && !mismatch) void next()
}}
className="flex flex-col gap-4"
>
<PasswordField label="رمز تازه" autoComplete="new-password" value={password} onValueChange={setPassword} strength requirements required />
<PasswordField
label="تکرار رمز تازه"
autoComplete="new-password"
value={confirm}
onValueChange={setConfirm}
invalid={mismatch}
errorMessage="با رمز بالا یکی نیست."
required
/>
<Button type="submit" fullWidth loading={loading} disabled={!password || mismatch} className="mt-4">
ذخیرهٔ رمز
</Button>
</form>
)}
<p className="text-center text-sm text-muted-foreground">
رمز یادتان آمد؟ <Link href="/login">وارد شوید</Link>
</p>
</>
) : (
<div className="flex flex-col items-center gap-4 text-center" role="status">
<span aria-hidden className="flex size-12 items-center justify-center rounded-xl bg-success-subtle text-success [&_svg]:size-6">
<CheckIcon />
</span>
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold">رمز عبور تغییر کرد</h1>
<p className="text-sm text-muted-foreground">از این به بعد با رمز تازه وارد شوید.</p>
</div>
<Button asChild fullWidth className="mt-4">
<NextLink href="/login">ورود به حساب</NextLink>
</Button>
</div>
)}
</ControlSizeProvider>
<p className="text-xs text-muted-foreground">
<Link href="#" size="sm">
قوانین
</Link>{" "}
و{" "}
<Link href="#" size="sm">
حریم خصوصی
</Link>
</p>
</div>
<BrandPanel />
</main>
)
}