From 9226cbadc301b94904a688f8ff108f93a4f0693a Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Thu, 21 Aug 2025 14:11:02 +0530 Subject: [PATCH] Update OTP Field with Resend OTP --- src/components/OtpField.astro | 489 +++++++++++++++++++--------------- src/pages/otp-form.astro | 22 +- 2 files changed, 286 insertions(+), 225 deletions(-) diff --git a/src/components/OtpField.astro b/src/components/OtpField.astro index 15955e8..3f78313 100644 --- a/src/components/OtpField.astro +++ b/src/components/OtpField.astro @@ -1,279 +1,340 @@ --- import type { HTMLAttributes } from "astro/types"; -import { cn } from "../utils/cn"; + +type Size = "sm" | "md" | "lg"; interface Props extends HTMLAttributes<"div"> { id?: string; - name: string; + name: string; // hidden input name (what Zod receives) label?: string; description?: string; - error?: string; // SSR error (optional) - required?: boolean; - disabled?: boolean; - value?: string; // initial value (e.g., from server) + error?: string; length?: number; // number of boxes - placeholder?: string | string[]; - onlyDigits?: boolean; // restrict to 0-9 - size?: "sm" | "md" | "lg"; // box sizes + placeholderChar?: string; // shown in each box autoFocus?: boolean; - class?: string; + disabled?: boolean; + size?: Size; + + // Resend (optional) + resendEnabled?: boolean; // show/hide resend UI + resendLabel?: string; // button label + resendCooldown?: number; // seconds + onResend?: string; // window function name to call (may be async) } const p = Astro.props as Props; const fieldId = p.id ?? p.name; -const descId = p.description ? `${fieldId}-desc` : undefined; const errId = `${fieldId}-err`; -const len = Math.max(1, p.length ?? 6); -const initial = (p.value ?? "").slice(0, len); -const boxesSSR = Array.from({ length: len }, (_, i) => initial[i] ?? ""); +const LEN = Math.max(4, Math.min(p.length ?? 6, 10)); +const SIZE = p.size ?? "md"; -const sizeCls = - p.size === "sm" +const boxCls = + SIZE === "sm" ? "wr:h-10 wr:w-10 wr:text-base" - : p.size === "lg" + : SIZE === "lg" ? "wr:h-14 wr:w-14 wr:text-xl" : "wr:h-12 wr:w-12 wr:text-lg"; -const boxBase = - "wr:rounded-lg wr:border wr:border-gray-200 wr:bg-background wr:text-center wr:tracking-widest " + - "wr:focus-visible:outline-none wr:focus-visible:ring-2 wr:focus-visible:ring-primary/40 " + - "wr:disabled:opacity-50 wr:disabled:pointer-events-none " + - "wr:dark:bg-neutral-900 wr:dark:border-neutral-700 wr:dark:text-neutral-100"; +const ringErr = "wr:border-danger wr:focus-visible:ring-danger/40"; +const ringOk = "wr:border-border wr:focus-visible:ring-primary/40"; -const groupCls = "wr:flex wr:items-center wr:gap-2 wr:select-none"; +const showResend = !!p.resendEnabled; +const resendLabel = p.resendLabel ?? "Resend code"; +const resendCooldown = Math.max(5, p.resendCooldown ?? 30); + +const containerId = `otp-${Math.random().toString(36).slice(2)}`; --- -
+
{ p.label && ( ) } - - - - +
+
+ { + Array.from({ length: LEN }).map((_, i) => ( + + )) + } +
+ { - boxesSSR.map((ch, i) => ( - - )) + p.resendEnabled && ( +
+ + +
+ ) }
+ + + { p.description && ( -

- {p.description} -

+

{p.description}

) } -
- diff --git a/src/pages/otp-form.astro b/src/pages/otp-form.astro index ccc5f12..98fb02f 100644 --- a/src/pages/otp-form.astro +++ b/src/pages/otp-form.astro @@ -10,19 +10,19 @@ import ComponentLayout from "../layouts/ComponentLayout.astro"; label="Verification code" description="Enter the 6-digit code we sent to your email." length={6} - placeholder="•" - onlyDigits={true} + placeholderChar="-" size="md" autoFocus={true} - required - /> - - + +