بلاک‌ها · ورود و ثبت‌نام

ورود با کد پیامکی

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

ورود و ثبت‌نامِ یکجا با شمارهٔ موبایل: کارتِ وسط‌چین روی پترنِ نقطه‌ای، دو قدمِ شماره و کدِ ۶ رقمی در همان کارت، خطای کدِ نادرست و شمارشِ معکوسِ ارسالِ دوباره.

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

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