first commit
This commit is contained in:
91
apps/frontend/components/ui/otp-input.tsx
Normal file
91
apps/frontend/components/ui/otp-input.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface OtpInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onComplete?: (value: string) => void;
|
||||
length?: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function OtpInput({ value, onChange, onComplete, length = 6, disabled = false, className }: OtpInputProps) {
|
||||
const inputsRef = useRef<Array<HTMLInputElement | null>>([]);
|
||||
const digits = Array.from({ length }, (_, index) => value[index] ?? '');
|
||||
|
||||
function commit(next: string) {
|
||||
const clean = next.replace(/\D/g, '').slice(0, length);
|
||||
onChange(clean);
|
||||
if (clean.length === length) {
|
||||
onComplete?.(clean);
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(index: number, raw: string) {
|
||||
const char = raw.replace(/\D/g, '').slice(-1);
|
||||
if (!char) return;
|
||||
const chars = value.split('');
|
||||
chars[index] = char;
|
||||
const next = chars.join('').slice(0, length);
|
||||
commit(next);
|
||||
if (index < length - 1) {
|
||||
inputsRef.current[index + 1]?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(index: number, event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.key === 'Backspace') {
|
||||
event.preventDefault();
|
||||
const chars = value.split('');
|
||||
if (chars[index]) {
|
||||
chars[index] = '';
|
||||
onChange(chars.join(''));
|
||||
} else if (index > 0) {
|
||||
chars[index - 1] = '';
|
||||
onChange(chars.join(''));
|
||||
inputsRef.current[index - 1]?.focus();
|
||||
}
|
||||
}
|
||||
if (event.key === 'ArrowLeft' && index > 0) {
|
||||
inputsRef.current[index - 1]?.focus();
|
||||
}
|
||||
if (event.key === 'ArrowRight' && index < length - 1) {
|
||||
inputsRef.current[index + 1]?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function handlePaste(event: React.ClipboardEvent<HTMLInputElement>) {
|
||||
event.preventDefault();
|
||||
const pasted = event.clipboardData.getData('text').replace(/\D/g, '').slice(0, length);
|
||||
if (!pasted) return;
|
||||
commit(pasted);
|
||||
const focusIndex = Math.min(pasted.length, length - 1);
|
||||
inputsRef.current[focusIndex]?.focus();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex justify-between gap-2', className)}>
|
||||
{digits.map((digit, index) => (
|
||||
<input
|
||||
key={index}
|
||||
ref={(element) => {
|
||||
inputsRef.current[index] = element;
|
||||
}}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
maxLength={1}
|
||||
disabled={disabled}
|
||||
value={digit}
|
||||
onChange={(event) => handleChange(index, event.target.value)}
|
||||
onKeyDown={(event) => handleKeyDown(index, event)}
|
||||
onPaste={handlePaste}
|
||||
className="h-14 w-full rounded-2xl border border-[#555762] bg-transparent text-center text-2xl font-semibold text-white outline-none transition focus:border-[#7b7f8f] focus:ring-4 focus:ring-white/10 disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user