Compare commits
No commits in common. "fix/improve-keys" and "master" have entirely different histories.
fix/improv
...
master
@ -46,7 +46,6 @@ const App: React.FC = () => {
|
|||||||
const [isTutorOpen, setIsTutorOpen] = useState(false);
|
const [isTutorOpen, setIsTutorOpen] = useState(false);
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
const [feedback, setFeedback] = useState<{ type: 'correct' | 'wrong', id: number } | null>(null);
|
const [feedback, setFeedback] = useState<{ type: 'correct' | 'wrong', id: number } | null>(null);
|
||||||
const feedbackIdRef = useRef(0);
|
|
||||||
|
|
||||||
// Responsive check for "Short Landscape" (Mobile Landscape)
|
// Responsive check for "Short Landscape" (Mobile Landscape)
|
||||||
// We want side-by-side layout here because vertical space is scarce.
|
// We want side-by-side layout here because vertical space is scarce.
|
||||||
@ -184,8 +183,7 @@ const App: React.FC = () => {
|
|||||||
if (answer === currentProblem.answer) {
|
if (answer === currentProblem.answer) {
|
||||||
// Correct!
|
// Correct!
|
||||||
audioService.playCorrect();
|
audioService.playCorrect();
|
||||||
feedbackIdRef.current += 1;
|
setFeedback({ type: 'correct', id: Date.now() });
|
||||||
setFeedback({ type: 'correct', id: feedbackIdRef.current });
|
|
||||||
|
|
||||||
setGameState(prev => {
|
setGameState(prev => {
|
||||||
const newScore = prev.score + 10;
|
const newScore = prev.score + 10;
|
||||||
@ -224,8 +222,7 @@ const App: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
// Incorrect
|
// Incorrect
|
||||||
audioService.playIncorrect();
|
audioService.playIncorrect();
|
||||||
feedbackIdRef.current += 1;
|
setFeedback({ type: 'wrong', id: Date.now() });
|
||||||
setFeedback({ type: 'wrong', id: feedbackIdRef.current });
|
|
||||||
setGameState(prev => ({ ...prev, streak: 0 }));
|
setGameState(prev => ({ ...prev, streak: 0 }));
|
||||||
// Visual feedback logic could go here (e.g. screen shake), handled by UI via streak reset
|
// Visual feedback logic could go here (e.g. screen shake), handled by UI via streak reset
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Problem, GameStatus } from '../types';
|
import { Problem, GameStatus } from '../types';
|
||||||
import { Delete, ArrowRight } from 'lucide-react';
|
import { Delete, ArrowRight } from 'lucide-react';
|
||||||
|
|
||||||
@ -11,130 +11,27 @@ interface ControlPanelProps {
|
|||||||
|
|
||||||
export const ControlPanel: React.FC<ControlPanelProps> = ({ problem, status, onAnswer, forceVertical = false }) => {
|
export const ControlPanel: React.FC<ControlPanelProps> = ({ problem, status, onAnswer, forceVertical = false }) => {
|
||||||
const [input, setInput] = useState('');
|
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
|
// Clear input when problem changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInput('');
|
setInput('');
|
||||||
}, [problem?.id]);
|
}, [problem?.id]);
|
||||||
|
|
||||||
const handleNumClick = useCallback((num: number) => {
|
const handleNumClick = (num: number) => {
|
||||||
const clickTime = Date.now();
|
if (input.length < 3) { // Max 3 digits (answer < 200 usually)
|
||||||
const lastClick = lastClickRef.current;
|
setInput(prev => prev + num.toString());
|
||||||
|
|
||||||
// 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 };
|
const handleDelete = () => {
|
||||||
setInput(prev => {
|
|
||||||
if (prev.length < 3) {
|
|
||||||
return prev + num.toString();
|
|
||||||
}
|
|
||||||
return prev;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
|
||||||
setInput(prev => prev.slice(0, -1));
|
setInput(prev => prev.slice(0, -1));
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const handleSubmit = useCallback((e?: React.FormEvent) => {
|
const handleSubmit = (e?: React.FormEvent) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
setInput(prev => {
|
if (input === '') return;
|
||||||
if (prev !== '') {
|
onAnswer(parseInt(input, 10));
|
||||||
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) {
|
if (status !== GameStatus.PLAYING || !problem) {
|
||||||
return <div className="h-full bg-slate-100 rounded-t-xl animate-pulse"></div>;
|
return <div className="h-full bg-slate-100 rounded-t-xl animate-pulse"></div>;
|
||||||
@ -164,21 +61,21 @@ export const ControlPanel: React.FC<ControlPanelProps> = ({ problem, status, onA
|
|||||||
<form onSubmit={handleSubmit} className="grid grid-cols-3 gap-2 h-full">
|
<form onSubmit={handleSubmit} className="grid grid-cols-3 gap-2 h-full">
|
||||||
{/* Row 1: 1, 2, 3 */}
|
{/* Row 1: 1, 2, 3 */}
|
||||||
{[1, 2, 3].map((num) => (
|
{[1, 2, 3].map((num) => (
|
||||||
<button key={num} type="button" onPointerDown={(e) => { e.preventDefault(); handleNumClick(num); }} className={btnClass}>
|
<button key={num} type="button" onClick={() => handleNumClick(num)} className={btnClass}>
|
||||||
{num}
|
{num}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Row 2: 4, 5, 6 */}
|
{/* Row 2: 4, 5, 6 */}
|
||||||
{[4, 5, 6].map((num) => (
|
{[4, 5, 6].map((num) => (
|
||||||
<button key={num} type="button" onPointerDown={(e) => { e.preventDefault(); handleNumClick(num); }} className={btnClass}>
|
<button key={num} type="button" onClick={() => handleNumClick(num)} className={btnClass}>
|
||||||
{num}
|
{num}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Row 3: 7, 8, 9 */}
|
{/* Row 3: 7, 8, 9 */}
|
||||||
{[7, 8, 9].map((num) => (
|
{[7, 8, 9].map((num) => (
|
||||||
<button key={num} type="button" onPointerDown={(e) => { e.preventDefault(); handleNumClick(num); }} className={btnClass}>
|
<button key={num} type="button" onClick={() => handleNumClick(num)} className={btnClass}>
|
||||||
{num}
|
{num}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@ -186,13 +83,13 @@ export const ControlPanel: React.FC<ControlPanelProps> = ({ problem, status, onA
|
|||||||
{/* Row 4: Del, 0, Enter */}
|
{/* Row 4: Del, 0, Enter */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onPointerDown={(e) => { e.preventDefault(); handleDelete(); }}
|
onClick={handleDelete}
|
||||||
className="bg-red-100 hover:bg-red-200 text-red-600 font-bold rounded-lg shadow-sm border-b-4 border-red-200 active:border-b-0 active:translate-y-1 transition-all h-full flex items-center justify-center touch-manipulation select-none"
|
className="bg-red-100 hover:bg-red-200 text-red-600 font-bold rounded-lg shadow-sm border-b-4 border-red-200 active:border-b-0 active:translate-y-1 transition-all h-full flex items-center justify-center touch-manipulation select-none"
|
||||||
>
|
>
|
||||||
<Delete />
|
<Delete />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button" onPointerDown={(e) => { e.preventDefault(); handleNumClick(0); }} className={btnClass}>
|
<button type="button" onClick={() => handleNumClick(0)} className={btnClass}>
|
||||||
0
|
0
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
6
src/apps/bridge-builder-math/package-lock.json
generated
6
src/apps/bridge-builder-math/package-lock.json
generated
@ -51,7 +51,6 @@
|
|||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@ -1216,7 +1215,6 @@
|
|||||||
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
|
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@ -1349,7 +1347,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@ -1994,7 +1991,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -2036,7 +2032,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -2370,7 +2365,6 @@
|
|||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user