import { useState, useCallback, useEffect, useRef } from 'react'; import { getMessages } from '../messages/index.js'; import { Keyboard } from './Keyboard.js'; const COLOR_MAP: Record = { g: '#6aaa64', y: '#c9b458', x: '#787c7e', }; const CURSOR_COLOR = '#6aaa64'; const COLOR_CYCLE: Array<'x' | 'y' | 'g'> = ['x', 'y', 'g']; type CellColor = 'x' | 'y' | 'g'; interface CellState { letter: string; color: CellColor; } interface SolverBoardProps { onUpdate: (rows: Array<{ guess: string; result: string }>) => void; loading?: boolean; } function emptyBoard(): CellState[][] { return Array.from({ length: 6 }, () => Array.from({ length: 5 }, () => ({ letter: '', color: 'x' as CellColor })), ); } function buildRows(board: CellState[][]): Array<{ guess: string; result: string }> { const rows: Array<{ guess: string; result: string }> = []; for (const row of board) { if (row.every((c) => c.letter !== '')) { rows.push({ guess: row.map((c) => c.letter).join(''), result: row.map((c) => c.color).join(''), }); } } return rows; } function hasPartialRow(board: CellState[][]): boolean { for (const row of board) { const filled = row.filter((c) => c.letter !== '').length; if (filled > 0 && filled < 5) return true; } return false; } export function SolverBoard({ onUpdate, loading }: SolverBoardProps) { const [board, setBoard] = useState(emptyBoard); const [cursor, setCursor] = useState<{ row: number; col: number }>({ row: 0, col: 0 }); // Refs for latest values used in keyboard handler (stable closure) const cursorRef = useRef(cursor); cursorRef.current = cursor; const boardRef = useRef(board); boardRef.current = board; const loadingRef = useRef(loading); loadingRef.current = loading; const partial = hasPartialRow(board); const updateDisabled = loading || partial; const triggerUpdate = useCallback(() => { const b = boardRef.current; const r = buildRows(b); if (hasPartialRow(b) || loadingRef.current) return; onUpdate(r); }, [onUpdate]); // ── Cell mutation helpers ── const setCell = useCallback((row: number, col: number, update: Partial) => { setBoard((prev) => { const next = prev.map((r) => r.map((c) => ({ ...c }))); if (update.letter !== undefined) next[row][col].letter = update.letter; if (update.color !== undefined) next[row][col].color = update.color; return next; }); }, []); const cycleColor = useCallback((row: number, col: number) => { setBoard((prev) => { const next = prev.map((r) => r.map((c) => ({ ...c }))); const current = next[row][col].color; const idx = COLOR_CYCLE.indexOf(current); next[row][col].color = COLOR_CYCLE[(idx + 1) % COLOR_CYCLE.length]; return next; }); }, []); const moveCursor = useCallback((row: number, col: number) => { setCursor({ row: Math.max(0, Math.min(5, row)), col: Math.max(0, Math.min(4, col)) }); }, []); // ── Shared key handler (on-screen keyboard + physical keyboard) ── const handleKey = useCallback( (key: string) => { const { row, col } = cursorRef.current; if (key === 'Enter') { triggerUpdate(); return; } if (key === 'Backspace') { setCell(row, col, { letter: '', color: 'x' }); if (col > 0) { moveCursor(row, col - 1); } else if (row > 0) { moveCursor(row - 1, 4); } return; } if (key === 'ArrowLeft') { moveCursor(row, col - 1); return; } if (key === 'ArrowRight') { moveCursor(row, col + 1); return; } if (key === 'ArrowUp') { moveCursor(row - 1, col); return; } if (key === 'ArrowDown') { moveCursor(row + 1, col); return; } // Letter key (EN + RU, no ё) if (key.length === 1 && /^[a-zA-Zа-яА-Я]$/.test(key) && key.toLowerCase() !== 'ё') { const letter = key.toLowerCase(); setCell(row, col, { letter, color: 'x' }); if (col < 4) { moveCursor(row, col + 1); } } }, [triggerUpdate, setCell, moveCursor], ); // ── Physical keyboard ── useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Ignore if user is typing in an input field if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; const key = e.key; // Only handle single letters and special keys if ( key === 'Enter' || key === 'Backspace' || key.startsWith('Arrow') || (key.length === 1 && /^[a-zA-Zа-яА-Я]$/.test(key) && key.toLowerCase() !== 'ё') ) { e.preventDefault(); handleKey(key); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [handleKey]); // ── Render ── const m = getMessages(); return (
{/* Grid */}
{board.map((row, rowIdx) => (
{row.map((cell, colIdx) => { const hasLetter = cell.letter !== ''; const bg = hasLetter ? COLOR_MAP[cell.color] : 'transparent'; const isFocused = cursor.row === rowIdx && cursor.col === colIdx; return (
{ if (isFocused && hasLetter) { cycleColor(rowIdx, colIdx); } else { moveCursor(rowIdx, colIdx); } }} style={{ width: '52px', height: '52px', border: `2px solid ${hasLetter ? 'transparent' : '#d3d6da'}`, backgroundColor: bg, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '1.5rem', fontWeight: 'bold', color: hasLetter ? '#fff' : '#000', textTransform: 'uppercase', fontFamily: 'monospace', position: 'relative', cursor: 'pointer', userSelect: 'none', }} > {cell.letter} {isFocused && (
)}
); })}
))}
{/* Hint */}
{m.solver.tapHint}
{/* On-screen keyboard */}
); }