From 5a77e7d1de20a04d9189f449b4c48d2b9ec09528 Mon Sep 17 00:00:00 2001 From: Laura Damian Date: Fri, 9 Jan 2026 13:37:02 +0100 Subject: [PATCH] add key fix --- src/apps/bridge-builder-math/App.tsx | 7 +- .../components/ControlPanel.tsx | 135 +++++++++++++++--- .../bridge-builder-math/package-lock.json | 6 + 3 files changed, 130 insertions(+), 18 deletions(-) diff --git a/src/apps/bridge-builder-math/App.tsx b/src/apps/bridge-builder-math/App.tsx index 1bfa9b5..32ffafc 100644 --- a/src/apps/bridge-builder-math/App.tsx +++ b/src/apps/bridge-builder-math/App.tsx @@ -46,6 +46,7 @@ const App: React.FC = () => { const [isTutorOpen, setIsTutorOpen] = useState(false); const [isMuted, setIsMuted] = useState(false); const [feedback, setFeedback] = useState<{ type: 'correct' | 'wrong', id: number } | null>(null); + const feedbackIdRef = useRef(0); // Responsive check for "Short Landscape" (Mobile Landscape) // We want side-by-side layout here because vertical space is scarce. @@ -183,7 +184,8 @@ const App: React.FC = () => { if (answer === currentProblem.answer) { // Correct! audioService.playCorrect(); - setFeedback({ type: 'correct', id: Date.now() }); + feedbackIdRef.current += 1; + setFeedback({ type: 'correct', id: feedbackIdRef.current }); setGameState(prev => { const newScore = prev.score + 10; @@ -222,7 +224,8 @@ const App: React.FC = () => { } else { // Incorrect audioService.playIncorrect(); - setFeedback({ type: 'wrong', id: Date.now() }); + feedbackIdRef.current += 1; + setFeedback({ type: 'wrong', id: feedbackIdRef.current }); setGameState(prev => ({ ...prev, streak: 0 })); // Visual feedback logic could go here (e.g. screen shake), handled by UI via streak reset } diff --git a/src/apps/bridge-builder-math/components/ControlPanel.tsx b/src/apps/bridge-builder-math/components/ControlPanel.tsx index 0938291..35c563b 100644 --- a/src/apps/bridge-builder-math/components/ControlPanel.tsx +++ b/src/apps/bridge-builder-math/components/ControlPanel.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Problem, GameStatus } from '../types'; import { Delete, ArrowRight } from 'lucide-react'; @@ -11,27 +11,130 @@ interface ControlPanelProps { export const ControlPanel: React.FC = ({ problem, status, onAnswer, forceVertical = false }) => { const [input, setInput] = useState(''); + const lastKeyPressRef = useRef<{ key: string; time: number } | null>(null); + const processingRef = useRef(false); + const onAnswerRef = useRef(onAnswer); + const listenerIdRef = useRef(Math.random()); + const lastClickRef = useRef<{ button: number; time: number } | null>(null); + + // Keep onAnswer ref up to date + useEffect(() => { + onAnswerRef.current = onAnswer; + }, [onAnswer]); // Clear input when problem changes useEffect(() => { setInput(''); }, [problem?.id]); - const handleNumClick = (num: number) => { - if (input.length < 3) { // Max 3 digits (answer < 200 usually) - setInput(prev => prev + num.toString()); + const handleNumClick = useCallback((num: number) => { + const clickTime = Date.now(); + const lastClick = lastClickRef.current; + + // Debounce: prevent clicks within 150ms + if (lastClick && lastClick.button === num && clickTime - lastClick.time < 150) { + console.log(`⛔ BLOCKED - too fast (${clickTime - lastClick.time}ms)`); + return; } - }; + + lastClickRef.current = { button: num, time: clickTime }; + setInput(prev => { + if (prev.length < 3) { + return prev + num.toString(); + } + return prev; + }); + }, []); - const handleDelete = () => { + const handleDelete = useCallback(() => { setInput(prev => prev.slice(0, -1)); - }; + }, []); - const handleSubmit = (e?: React.FormEvent) => { + const handleSubmit = useCallback((e?: React.FormEvent) => { e?.preventDefault(); - if (input === '') return; - onAnswer(parseInt(input, 10)); - }; + setInput(prev => { + if (prev !== '') { + onAnswerRef.current(parseInt(prev, 10)); + } + return prev; + }); + }, []); + + // Keyboard event handler + useEffect(() => { + if (status !== GameStatus.PLAYING || !problem) return; + + const handleKeyDown = (e: KeyboardEvent) => { + // Only handle if not repeating (key held down) + if (e.repeat) { + return; + } + + // Prevent double processing + if (processingRef.current) { + e.preventDefault(); + return; + } + + // Strict debounce check + const now = Date.now(); + const lastPress = lastKeyPressRef.current; + + if (lastPress && lastPress.key === e.key && now - lastPress.time < 200) { + e.preventDefault(); + return; + } + + // Number keys (0-9) + if (e.key >= '0' && e.key <= '9') { + e.preventDefault(); + processingRef.current = true; + lastKeyPressRef.current = { key: e.key, time: now }; + + const num = parseInt(e.key, 10); + setInput(prev => { + if (prev.length < 3) { + return prev + num.toString(); + } + return prev; + }); + + setTimeout(() => { + processingRef.current = false; + }, 50); + } + // Backspace/Delete + else if (e.key === 'Backspace' || e.key === 'Delete') { + e.preventDefault(); + processingRef.current = true; + lastKeyPressRef.current = { key: e.key, time: now }; + + setInput(prev => prev.slice(0, -1)); + + setTimeout(() => { processingRef.current = false; }, 50); + } + // Enter + else if (e.key === 'Enter') { + e.preventDefault(); + processingRef.current = true; + lastKeyPressRef.current = { key: e.key, time: now }; + + setInput(prev => { + if (prev !== '') { + onAnswerRef.current(parseInt(prev, 10)); + } + return prev; + }); + + setTimeout(() => { processingRef.current = false; }, 50); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [status, problem]); if (status !== GameStatus.PLAYING || !problem) { return
; @@ -61,21 +164,21 @@ export const ControlPanel: React.FC = ({ problem, status, onA
{/* Row 1: 1, 2, 3 */} {[1, 2, 3].map((num) => ( - ))} {/* Row 2: 4, 5, 6 */} {[4, 5, 6].map((num) => ( - ))} {/* Row 3: 7, 8, 9 */} {[7, 8, 9].map((num) => ( - ))} @@ -83,13 +186,13 @@ export const ControlPanel: React.FC = ({ problem, status, onA {/* Row 4: Del, 0, Enter */} - diff --git a/src/apps/bridge-builder-math/package-lock.json b/src/apps/bridge-builder-math/package-lock.json index 0f7bea9..7813bcb 100644 --- a/src/apps/bridge-builder-math/package-lock.json +++ b/src/apps/bridge-builder-math/package-lock.json @@ -51,6 +51,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1215,6 +1216,7 @@ "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1347,6 +1349,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1991,6 +1994,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2032,6 +2036,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2365,6 +2370,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4",