SPB Git

Repository Public MIT

code-input.tsx 2.9 KB 95 lines tsx

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
Files main
Menu

Theme