Compare commits
1 Commits
master
...
fix/improv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a77e7d1de |
@ -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
|
||||
}
|
||||
|
||||
@ -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<ControlPanelProps> = ({ 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 = useCallback(() => {
|
||||
setInput(prev => prev.slice(0, -1));
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback((e?: React.FormEvent) => {
|
||||
e?.preventDefault();
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setInput(prev => prev.slice(0, -1));
|
||||
};
|
||||
|
||||
const handleSubmit = (e?: React.FormEvent) => {
|
||||
e?.preventDefault();
|
||||
if (input === '') return;
|
||||
onAnswer(parseInt(input, 10));
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [status, problem]);
|
||||
|
||||
if (status !== GameStatus.PLAYING || !problem) {
|
||||
return <div className="h-full bg-slate-100 rounded-t-xl animate-pulse"></div>;
|
||||
@ -61,21 +164,21 @@ export const ControlPanel: React.FC<ControlPanelProps> = ({ problem, status, onA
|
||||
<form onSubmit={handleSubmit} className="grid grid-cols-3 gap-2 h-full">
|
||||
{/* Row 1: 1, 2, 3 */}
|
||||
{[1, 2, 3].map((num) => (
|
||||
<button key={num} type="button" onClick={() => handleNumClick(num)} className={btnClass}>
|
||||
<button key={num} type="button" onPointerDown={(e) => { e.preventDefault(); handleNumClick(num); }} className={btnClass}>
|
||||
{num}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Row 2: 4, 5, 6 */}
|
||||
{[4, 5, 6].map((num) => (
|
||||
<button key={num} type="button" onClick={() => handleNumClick(num)} className={btnClass}>
|
||||
<button key={num} type="button" onPointerDown={(e) => { e.preventDefault(); handleNumClick(num); }} className={btnClass}>
|
||||
{num}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Row 3: 7, 8, 9 */}
|
||||
{[7, 8, 9].map((num) => (
|
||||
<button key={num} type="button" onClick={() => handleNumClick(num)} className={btnClass}>
|
||||
<button key={num} type="button" onPointerDown={(e) => { e.preventDefault(); handleNumClick(num); }} className={btnClass}>
|
||||
{num}
|
||||
</button>
|
||||
))}
|
||||
@ -83,13 +186,13 @@ export const ControlPanel: React.FC<ControlPanelProps> = ({ problem, status, onA
|
||||
{/* Row 4: Del, 0, Enter */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
onPointerDown={(e) => { e.preventDefault(); 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"
|
||||
>
|
||||
<Delete />
|
||||
</button>
|
||||
|
||||
<button type="button" onClick={() => handleNumClick(0)} className={btnClass}>
|
||||
<button type="button" onPointerDown={(e) => { e.preventDefault(); handleNumClick(0); }} className={btnClass}>
|
||||
0
|
||||
</button>
|
||||
|
||||
|
||||
6
src/apps/bridge-builder-math/package-lock.json
generated
6
src/apps/bridge-builder-math/package-lock.json
generated
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user