import { useEffect, useRef, useState, useCallback } from 'react' import X from 'lucide-react/dist/esm/icons/x' import { DEG_TO_RAD, isOperatorTail, tokenizeExpression, formatResult, evaluateExpression } from '../../utils/calculatorEngine' const Btn = ({ label, onClick, type = 'default', wide = false }) => ( ) export default function Calculator({ onClose }) { const dialogRef = useRef(null) const expressionRef = useRef('') const [display, setDisplay] = useState('0') const [expression, setExpression] = useState('') const [memory, setMemory] = useState(0) const [isDeg, setIsDeg] = useState(true) const [waitingForOperand, setWaitingForOperand] = useState(false) const getAngle = (val) => isDeg ? val * DEG_TO_RAD : val const setExpressionText = useCallback((next) => { const value = typeof next === 'function' ? next(expressionRef.current) : next expressionRef.current = value setExpression(value) }, []) useEffect(() => { const firstButton = dialogRef.current?.querySelector('button') firstButton?.focus() }, []) const inputDigit = (digit) => { if (waitingForOperand) { setDisplay(String(digit)) setWaitingForOperand(false) } else { setDisplay(display === '0' ? String(digit) : display + digit) } } const inputDecimal = () => { if (waitingForOperand) { setDisplay('0.'); setWaitingForOperand(false); return } if (!display.includes('.')) setDisplay(display + '.') } const handleOperator = (op) => { setExpressionText(prev => { const trimmed = prev.trim() if (!trimmed) return `${display} ${op}` if (isOperatorTail(trimmed)) return `${trimmed.slice(0, trimmed.lastIndexOf(' ')).trim()} ${op}` if (trimmed.endsWith(')')) return `${trimmed} ${op}` return `${trimmed} ${display} ${op}` }) setWaitingForOperand(true) } const inputOpenParen = () => { setExpressionText(prev => { const trimmed = prev.trim() if (!trimmed) return '(' if (isOperatorTail(trimmed) || trimmed.endsWith('(')) return `${trimmed} (` return `${trimmed} ${display} × (` }) setDisplay('0') setWaitingForOperand(true) } const inputCloseParen = () => { const tokens = tokenizeExpression(expressionRef.current) const openCount = tokens.filter(token => token === '(').length const closeCount = tokens.filter(token => token === ')').length if (openCount <= closeCount) return setExpressionText(prev => { const trimmed = prev.trim() if (!trimmed || trimmed.endsWith('(')) return trimmed if (trimmed.endsWith(')')) return `${trimmed} )` return `${trimmed} ${display} )` }) setWaitingForOperand(false) } const calculate = () => { if (!expressionRef.current) return try { const trimmed = expressionRef.current.trim() const finalExpression = trimmed.endsWith(')') ? trimmed : `${trimmed} ${display}` setDisplay(formatResult(evaluateExpression(finalExpression))) setExpressionText('') setWaitingForOperand(true) } catch { setDisplay('Error') } } const sciFunc = (fn) => { const val = parseFloat(display) let result try { switch (fn) { case 'sin': result = Math.sin(getAngle(val)); break case 'cos': result = Math.cos(getAngle(val)); break case 'tan': result = Math.tan(getAngle(val)); break case 'sin⁻¹': result = isDeg ? Math.asin(val) / DEG_TO_RAD : Math.asin(val); break case 'cos⁻¹': result = isDeg ? Math.acos(val) / DEG_TO_RAD : Math.acos(val); break case 'tan⁻¹': result = isDeg ? Math.atan(val) / DEG_TO_RAD : Math.atan(val); break case 'log': result = Math.log10(val); break case 'ln': result = Math.log(val); break case 'log₂': result = Math.log2(val); break case '√': result = Math.sqrt(val); break case 'x²': result = val * val; break case 'x³': result = val * val * val; break case '1/x': result = 1 / val; break case 'n!': result = factorial(val); break case 'eˣ': result = Math.exp(val); break case '10ˣ': result = Math.pow(10, val); break case '|x|': result = Math.abs(val); break case 'π': setDisplay(String(Math.PI)); return case 'e': setDisplay(String(Math.E)); return default: return } setDisplay(formatResult(result)) setWaitingForOperand(true) } catch { setDisplay('Error') } } const factorial = (n) => { if (n < 0 || !Number.isInteger(n)) return NaN if (n === 0 || n === 1) return 1 let r = 1; for (let i = 2; i <= n; i++) r *= i; return r } const clear = () => { setDisplay('0'); setExpressionText(''); setWaitingForOperand(false) } const backspace = () => { setDisplay(display.length > 1 ? display.slice(0, -1) : '0') } const toggleSign = () => setDisplay(String(parseFloat(display) * -1)) const handleDialogKeyDown = useCallback((event) => { if (event.key === 'Escape') { event.preventDefault() onClose() return } if (event.key !== 'Tab') return const focusable = Array.from( dialogRef.current?.querySelectorAll('button:not([disabled])') || [] ) if (focusable.length === 0) return const first = focusable[0] const last = focusable.at(-1) if (event.shiftKey && document.activeElement === first) { event.preventDefault() last.focus() } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault() first.focus() } }, [onClose]) return (
{/* Header */}
Scientific Calculator
{/* Display */}
{expression}
{display}
{/* Memory row */}
{[ { l: 'MC', fn: () => setMemory(0) }, { l: 'MR', fn: () => { setDisplay(String(memory)); setWaitingForOperand(false) } }, { l: 'MS', fn: () => setMemory(parseFloat(display)) }, { l: 'M+', fn: () => setMemory(m => m + parseFloat(display)) }, { l: 'M-', fn: () => setMemory(m => m - parseFloat(display)) }, ].map(({ l, fn }) => ( ))}
{/* Scientific functions */}
{['sin','cos','tan','log','ln', 'sin⁻¹','cos⁻¹','tan⁻¹','log₂','eˣ', 'x²','x³','√','10ˣ','1/x', 'n!','|x|','π','e','%'].map(fn => ( ))}
{/* Main keypad */}
handleOperator('xʸ')} type="op" /> handleOperator('÷')} type="op" /> inputDigit('7')} /> inputDigit('8')} /> inputDigit('9')} /> handleOperator('×')} type="op" /> inputDigit('4')} /> inputDigit('5')} /> inputDigit('6')} /> handleOperator('-')} type="op" /> inputDigit('1')} /> inputDigit('2')} /> inputDigit('3')} /> handleOperator('+')} type="op" /> handleOperator('%')} type="op" /> inputDigit('0')} wide />
) }