feat: add red flash on invalid movement (Issue 3)
When user attempts to move out of bounds, show visual feedback: CSS: - Added .invalid-move class with red outline and background - Animation: invalid-flash (500ms ease-out) - Starts with bright red glow, fades out - Red outline (3px solid) matches cursor style Logic: - flashInvalidMove() function in useCursor - Triggers when move would go out of bounds (< 0 or > 8) - Adds invalid-move class to current cell - Removes class after 500ms - Passes containerRef to useCursor for DOM access Cleanup: - Removed all console.log debugging statements - Clean code ready for production UX Flow: - Press h at left edge → Red flash at current position - Press j at bottom edge → Red flash at current position - Press k at top edge → Red flash at current position - Press l at right edge → Red flash at current position Issue 3 complete!
This commit is contained in:
@@ -263,3 +263,22 @@
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Invalid move flash */
|
||||||
|
.sudoku-cell.invalid-move {
|
||||||
|
background-color: rgba(239, 68, 68, 0.4);
|
||||||
|
outline: 3px solid rgb(239, 68, 68);
|
||||||
|
outline-offset: -3px;
|
||||||
|
animation: invalid-flash 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes invalid-flash {
|
||||||
|
0% {
|
||||||
|
background-color: rgba(239, 68, 68, 0.6);
|
||||||
|
box-shadow: 0 0 20px rgba(239, 68, 68, 0.8);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: rgba(239, 68, 68, 0.4);
|
||||||
|
box-shadow: 0 0 0 rgba(239, 68, 68, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+20
-21
@@ -2,23 +2,36 @@ import { useState, useEffect } from 'react';
|
|||||||
import type { Coord, CursorManager } from './types';
|
import type { Coord, CursorManager } from './types';
|
||||||
import type { GameStatusManager } from '../useGameStatus/types';
|
import type { GameStatusManager } from '../useGameStatus/types';
|
||||||
|
|
||||||
export function useCursor(gameStatusManager: GameStatusManager) {
|
export function useCursor(gameStatusManager: GameStatusManager, containerRef: React.RefObject<HTMLDivElement | null>) {
|
||||||
const [position, setPosition] = useState<Coord>({ row: 0, col: 0 });
|
const [position, setPosition] = useState<Coord>({ row: 0, col: 0 });
|
||||||
const { gameStatus } = gameStatusManager;
|
const { gameStatus } = gameStatusManager;
|
||||||
|
|
||||||
|
function flashInvalidMove(): void {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const cells = container.querySelectorAll('.sudoku-cell');
|
||||||
|
const index = position.row * 9 + position.col;
|
||||||
|
const targetCell = cells[index];
|
||||||
|
|
||||||
|
if (targetCell) {
|
||||||
|
targetCell.classList.add('invalid-move');
|
||||||
|
setTimeout(() => {
|
||||||
|
targetCell.classList.remove('invalid-move');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function move(dRows: number, dCols: number, count: number = 1): void {
|
function move(dRows: number, dCols: number, count: number = 1): void {
|
||||||
setPosition(currentPos => {
|
setPosition(currentPos => {
|
||||||
const newRow = currentPos.row + (dRows * count);
|
const newRow = currentPos.row + (dRows * count);
|
||||||
const newCol = currentPos.col + (dCols * count);
|
const newCol = currentPos.col + (dCols * count);
|
||||||
|
|
||||||
console.log('🚀 Move attempt:', { currentPos, dRows, dCols, count, newRow, newCol });
|
|
||||||
|
|
||||||
if (newRow < 0 || newRow > 8 || newCol < 0 || newCol > 8) {
|
if (newRow < 0 || newRow > 8 || newCol < 0 || newCol > 8) {
|
||||||
console.log('⛔ Out of bounds, movement blocked');
|
flashInvalidMove();
|
||||||
return currentPos;
|
return currentPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Position updated to:', { row: newRow, col: newCol });
|
|
||||||
return { row: newRow, col: newCol };
|
return { row: newRow, col: newCol };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -40,38 +53,24 @@ export function useCursor(gameStatusManager: GameStatusManager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('🎮 useCursor effect - gameStatus:', gameStatus);
|
if (gameStatus !== 'started') return;
|
||||||
|
|
||||||
if (gameStatus !== 'started') {
|
|
||||||
console.log('⏸️ Not started, skipping key listener');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
const key = e.key;
|
const key = e.key;
|
||||||
console.log('⌨️ Key pressed:', key);
|
|
||||||
|
|
||||||
if (key === 'h') {
|
if (key === 'h') {
|
||||||
console.log('← Move left');
|
|
||||||
moveLeft();
|
moveLeft();
|
||||||
} else if (key === 'j') {
|
} else if (key === 'j') {
|
||||||
console.log('↓ Move down');
|
|
||||||
moveDown();
|
moveDown();
|
||||||
} else if (key === 'k') {
|
} else if (key === 'k') {
|
||||||
console.log('↑ Move up');
|
|
||||||
moveUp();
|
moveUp();
|
||||||
} else if (key === 'l') {
|
} else if (key === 'l') {
|
||||||
console.log('→ Move right');
|
|
||||||
moveRight();
|
moveRight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Adding keydown listener');
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => {
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
console.log('🗑️ Removing keydown listener');
|
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
};
|
|
||||||
}, [gameStatus]);
|
}, [gameStatus]);
|
||||||
|
|
||||||
const cursor: CursorManager = {
|
const cursor: CursorManager = {
|
||||||
|
|||||||
@@ -12,16 +12,10 @@ export function useCursorRender(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gameStatus !== 'started') return;
|
if (gameStatus !== 'started') return;
|
||||||
|
|
||||||
console.log('🎯 Cursor render triggered - position:', position, 'boardRendered:', boardRendered);
|
|
||||||
|
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) {
|
if (!container) return;
|
||||||
console.log('❌ No container ref');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cells = container.querySelectorAll('.sudoku-cell');
|
const cells = container.querySelectorAll('.sudoku-cell');
|
||||||
console.log('📊 Found cells:', cells.length);
|
|
||||||
|
|
||||||
cells.forEach(cell => {
|
cells.forEach(cell => {
|
||||||
cell.classList.remove('cursor');
|
cell.classList.remove('cursor');
|
||||||
@@ -29,13 +23,9 @@ export function useCursorRender(
|
|||||||
|
|
||||||
const index = position.row * 9 + position.col;
|
const index = position.row * 9 + position.col;
|
||||||
const targetCell = cells[index];
|
const targetCell = cells[index];
|
||||||
console.log('🎯 Target cell index:', index, 'cell:', targetCell);
|
|
||||||
|
|
||||||
if (targetCell) {
|
if (targetCell) {
|
||||||
targetCell.classList.add('cursor');
|
targetCell.classList.add('cursor');
|
||||||
console.log('✅ Added cursor class to cell');
|
|
||||||
} else {
|
|
||||||
console.log('❌ Target cell not found');
|
|
||||||
}
|
}
|
||||||
}, [position, containerRef, gameStatus, boardRendered]);
|
}, [position, containerRef, gameStatus, boardRendered]);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ export function useGame(options: GameOptions = {}, platformHook?: unknown): Game
|
|||||||
const [keyLog, setKeyLog] = useState<{ key: string; timestamp: number }[]>([]);
|
const [keyLog, setKeyLog] = useState<{ key: string; timestamp: number }[]>([]);
|
||||||
const [boardRendered, setBoardRendered] = useState(0);
|
const [boardRendered, setBoardRendered] = useState(0);
|
||||||
|
|
||||||
const cursor = useCursor(gameStatusManager);
|
const cursor = useCursor(gameStatusManager, containerRef);
|
||||||
const cursorPosition = cursor.position();
|
const cursorPosition = cursor.position();
|
||||||
|
|
||||||
useCursorRender(containerRef, cursorPosition, gameStatus, boardRendered);
|
useCursorRender(containerRef, cursorPosition, gameStatus, boardRendered);
|
||||||
|
|||||||
Reference in New Issue
Block a user