Repository Public MIT
1import { useEffect, useState } from "react";2import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";3import { Loader2, RotateCw } from "lucide-react";4import { cn } from "@/lib/utils";56interface CodeInputProps {7 value: string;8 onChange: (v: string) => void;9 onComplete?: (v: string) => void;10 disabled?: boolean;11 error?: boolean;12 autoFocus?: boolean;13}1415/** Saisie d'un code à 6 chiffres (2 groupes de 3), collage pris en charge. */16export function CodeInput({ value, onChange, onComplete, disabled, error, autoFocus = true }: CodeInputProps) {17 return (18 <InputOTP19 maxLength={6}20 value={value}21 onChange={onChange}22 onComplete={onComplete}23 disabled={disabled}24 autoFocus={autoFocus}25 inputMode="numeric"26 pattern="^\d+$"27 containerClassName="justify-center gap-3"28 aria-label="Code à 6 chiffres"29 >30 <InputOTPGroup className="gap-1.5">31 {[0, 1, 2].map((i) => <Slot key={i} index={i} error={error} />)}32 </InputOTPGroup>33 <span className="h-1 w-3 rounded-full bg-border" aria-hidden />34 <InputOTPGroup className="gap-1.5">35 {[3, 4, 5].map((i) => <Slot key={i} index={i} error={error} />)}36 </InputOTPGroup>37 </InputOTP>38 );39}4041function Slot({ index, error }: { index: number; error?: boolean }) {42 return (43 <InputOTPSlot44 index={index}45 className={cn(46 "h-12 w-11 sm:h-14 sm:w-12 rounded-xl border bg-card font-mono text-xl font-semibold tnum first:rounded-l-xl last:rounded-r-xl border-l",47 "shadow-xs transition-all data-[active=true]:ring-4",48 error ? "border-destructive/60 ring-destructive/10" : "border-border ring-primary/15"49 )}50 />51 );52}5354interface ResendButtonProps {55 onResend: () => Promise<void> | void;56 cooldownSec?: number;57 disabled?: boolean;58 className?: string;59}6061/** Bouton « Renvoyer le code » avec compte à rebours. */62export function ResendButton({ onResend, cooldownSec = 45, disabled, className }: ResendButtonProps) {63 const [left, setLeft] = useState(cooldownSec);64 const [busy, setBusy] = useState(false);6566 useEffect(() => {67 if (left <= 0) return;68 const t = setInterval(() => setLeft((l) => Math.max(0, l - 1)), 1000);69 return () => clearInterval(t);70 }, [left]);7172 const click = async () => {73 if (left > 0 || busy) return;74 setBusy(true);75 try {76 await onResend();77 setLeft(cooldownSec);78 } finally {79 setBusy(false);80 }81 };8283 return (84 <button85 type="button"86 onClick={click}87 disabled={disabled || busy || left > 0}88 className={cn("inline-flex items-center gap-1.5 text-[13px] font-medium text-primary hover:underline disabled:text-muted-foreground disabled:no-underline disabled:cursor-default", className)}89 >90 {busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RotateCw className="h-3.5 w-3.5" />}91 {left > 0 ? `Renvoyer le code dans ${left} s` : "Renvoyer le code"}92 </button>93 );94}95