/** * Manual Entry Modal for Ticket Scanner * Optimized for gate staff with gloves and stylus input */ import { useState, useEffect, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { X, Delete, Check, Loader2, AlertCircle, Hash, Eye, EyeOff } from 'lucide-react'; import { Button } from '../../components/ui/Button'; import { Card } from '../../components/ui/Card'; import { formatBackupCode, QRValidator } from '../../lib/qr-validator'; export interface ManualEntryProps { isOpen: boolean; onClose: () => void; onSubmit: (code: string) => Promise; isLoading: boolean; lastError?: string; } const KEYPAD_BUTTONS = [ ['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['clear', '0', 'delete'] ] as const; const LETTER_BUTTONS = [ ['A', 'B', 'C'], ['D', 'E', 'F'] ] as const; export function ManualEntryModal({ isOpen, onClose, onSubmit, isLoading, lastError }: ManualEntryProps): JSX.Element { const [code, setCode] = useState(''); const [showLetters, setShowLetters] = useState(false); const [validator] = useState(() => new QRValidator()); // Reset state when modal opens/closes useEffect(() => { if (isOpen) { setCode(''); setShowLetters(false); } }, [isOpen]); const handleKeyPress = useCallback((key: string) => { if (isLoading) {return;} switch (key) { case 'clear': setCode(''); break; case 'delete': setCode(prev => prev.slice(0, -1)); break; default: if (code.length < 8) { setCode(prev => prev + key); } break; } }, [code.length, isLoading]); const handleSubmit = useCallback(async () => { if (code.length === 0 || isLoading) {return;} const validation = validator.validateBackupCode(code); if (!validation.valid) { // Don't submit invalid codes - let user fix them return; } try { await onSubmit(validation.normalizedCode!); } catch (error) { // Error handling is done in parent component console.error('Manual entry submission error:', error); } }, [code, isLoading, onSubmit, validator]); const isCodeValid = validator.validateBackupCode(code).valid; const formattedCode = formatBackupCode(code.padEnd(8, '_')); // Keyboard event handling useEffect(() => { if (!isOpen) {return;} const handleKeyDown = (event: KeyboardEvent) => { if (isLoading) {return;} const {key} = event; // Numbers if (/^[0-9]$/.test(key)) { event.preventDefault(); handleKeyPress(key); } // Letters (A-F for hex codes) if (/^[A-Fa-f]$/.test(key)) { event.preventDefault(); handleKeyPress(key.toUpperCase()); } // Backspace if (key === 'Backspace') { event.preventDefault(); handleKeyPress('delete'); } // Escape if (key === 'Escape') { event.preventDefault(); onClose(); } // Enter if (key === 'Enter' && isCodeValid) { event.preventDefault(); handleSubmit(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isOpen, handleKeyPress, handleSubmit, isCodeValid, isLoading, onClose]); if (!isOpen) {return <>;} return ( e.stopPropagation()} className="w-full max-w-md mx-auto" > {/* Header */}

Manual Entry

{/* Instructions */}

Enter the last 8 characters from the ticket code

• Found at bottom of physical tickets

• Use when QR code is damaged or unreadable

• Numbers and letters A-F only

{/* Code Display */}
0 && !isCodeValid ? 'border-warning-500 bg-warning-50' : ''} `}> {formattedCode}
{/* Validation indicator */}
{code.length === 8 && isCodeValid && ( )} {code.length > 0 && code.length < 8 && ( {8 - code.length} more )}
{/* Error Display */} {lastError && (

{lastError}

)}
{/* Letter/Number Toggle */}
{/* Keypad */}
{/* Number Keys */} {KEYPAD_BUTTONS.map((row, rowIndex) => (
{row.map((key) => { let buttonContent: React.ReactNode; let variant: 'primary' | 'outline' | 'secondary' = 'outline'; let disabled = isLoading; switch (key) { case 'clear': buttonContent = 'Clear'; variant = 'secondary'; disabled = disabled || code.length === 0; break; case 'delete': buttonContent = ; variant = 'secondary'; disabled = disabled || code.length === 0; break; default: buttonContent = key; disabled = disabled || code.length >= 8; break; } return ( ); })}
))} {/* Letter Keys (A-F) */} {showLetters && (
{LETTER_BUTTONS.map((row, rowIndex) => (
{row.map((key) => ( ))}
))}
)}
{/* Submit Button */}
{/* Footer Help */}

Press Enter to submit • Esc to cancel

); }