/** * NumPad Component * * On-screen number pad for tablets with large, touch-friendly buttons. * Ideal for cash tender entry and custom tip amounts. * * Features: * - Large 60x60px buttons minimum * - Visual feedback on press * - Decimal point support * - Backspace and clear * - Keyboard input support * - Format as currency ($) */ import React, { useState, useEffect, useRef } from 'react'; import { Delete, X } from 'lucide-react'; interface NumPadProps { /** Current value in cents */ value: number; /** Callback when value changes (in cents) */ onChange: (cents: number) => void; /** Optional label/title above the display */ label?: string; /** Show currency symbol ($) */ showCurrency?: boolean; /** Maximum value allowed in cents */ maxCents?: number; /** Callback when Enter/OK is pressed */ onSubmit?: () => void; /** Show submit button */ showSubmit?: boolean; /** Submit button text */ submitText?: string; /** Additional CSS classes */ className?: string; } export const NumPad: React.FC = ({ value, onChange, label, showCurrency = true, maxCents, onSubmit, showSubmit = false, submitText = 'OK', className = '', }) => { const [displayValue, setDisplayValue] = useState(''); // Track if user is actively inputting (to prevent value prop from overwriting) const isInputtingRef = useRef(false); // Convert cents to display value (dollars) only when: // 1. Value prop changes externally (not from our own onChange) // 2. Not currently inputting useEffect(() => { if (!isInputtingRef.current) { const dollars = (value / 100).toFixed(2); setDisplayValue(dollars); } }, [value]); /** * Handle number button click */ const handleNumber = (num: string) => { isInputtingRef.current = true; // Remove all non-digit characters except decimal const cleanValue = displayValue.replace(/[^\d.]/g, ''); // Special case: if value is "0.00" (initial zero state), start fresh with the new number if (cleanValue === '0.00') { setDisplayValue(num); updateValue(num); return; } // Split into dollars and cents const parts = cleanValue.split('.'); if (parts.length === 1) { // No decimal yet const newValue = cleanValue + num; setDisplayValue(newValue); updateValue(newValue); } else if (parts.length === 2) { // Already has decimal if (parts[1].length < 2) { // Can still add cents const newValue = parts[0] + '.' + parts[1] + num; setDisplayValue(newValue); updateValue(newValue); } } }; /** * Handle decimal point */ const handleDecimal = () => { isInputtingRef.current = true; const cleanValue = displayValue.replace(/[^\d.]/g, ''); if (!cleanValue.includes('.')) { const newValue = (cleanValue || '0') + '.'; setDisplayValue(newValue); updateValue(newValue); } }; /** * Handle backspace */ const handleBackspace = () => { isInputtingRef.current = true; const cleanValue = displayValue.replace(/[^\d.]/g, ''); if (cleanValue.length > 0) { const newValue = cleanValue.slice(0, -1) || '0'; setDisplayValue(newValue); updateValue(newValue); } }; /** * Handle clear */ const handleClear = () => { isInputtingRef.current = false; // Reset to allow external value sync setDisplayValue('0.00'); updateValue('0'); }; /** * Update the value and call onChange with cents * Returns the actual cents value used (may be capped) */ const updateValue = (newValue: string): number => { let cents = Math.round(parseFloat(newValue || '0') * 100); // Cap at max value if specified if (maxCents !== undefined && cents > maxCents) { cents = maxCents; // Update display to show capped value const cappedDisplay = (cents / 100).toFixed(2); setDisplayValue(cappedDisplay); } onChange(cents); return cents; }; /** * Keyboard support */ useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key >= '0' && e.key <= '9') { e.preventDefault(); handleNumber(e.key); } else if (e.key === '.' || e.key === ',') { e.preventDefault(); handleDecimal(); } else if (e.key === 'Backspace') { e.preventDefault(); handleBackspace(); } else if (e.key === 'Escape') { e.preventDefault(); handleClear(); } else if (e.key === 'Enter' && onSubmit) { e.preventDefault(); onSubmit(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [displayValue]); /** * Format display value with currency symbol */ const formattedDisplay = showCurrency ? `$${displayValue}` : displayValue; return (
{/* Label */} {label && (
{label}
)} {/* Display */}
{formattedDisplay}
{/* Number pad grid */}
{/* Numbers 1-9 */} {[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => ( ))} {/* Bottom row: Clear, 0, Decimal */} {/* Backspace button - spans column */} {/* Optional submit button */} {showSubmit && onSubmit && ( )}
{/* Keyboard hint */}
Keyboard: 0-9, . (decimal), Backspace, Esc (clear) {onSubmit && ', Enter (submit)'}
); }; export default NumPad;