| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- import { useState, useCallback, useEffect, useRef } from 'react';
- import { getMessages } from '../messages/index.js';
- import { Keyboard } from './Keyboard.js';
- const COLOR_MAP: Record<string, string> = {
- 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<CellState[][]>(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<CellState>) => {
- 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 (
- <div>
- <style>{`
- @keyframes wl-solver-blink {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0; }
- }
- `}</style>
- {/* Grid */}
- <div style={{ display: 'flex', flexDirection: 'column', gap: '4px', alignItems: 'center' }}>
- {board.map((row, rowIdx) => (
- <div key={rowIdx} style={{ display: 'flex', gap: '4px' }}>
- {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 (
- <div
- key={colIdx}
- onClick={() => {
- 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 && (
- <div
- style={{
- position: 'absolute',
- bottom: '4px',
- left: '8px',
- right: '8px',
- height: '3px',
- backgroundColor: CURSOR_COLOR,
- borderRadius: '1px',
- animation: 'wl-solver-blink 1s step-end infinite',
- }}
- />
- )}
- </div>
- );
- })}
- </div>
- ))}
- </div>
- {/* Hint */}
- <div
- style={{
- textAlign: 'center',
- marginTop: '10px',
- fontSize: '0.75rem',
- color: '#787c7e',
- fontFamily: 'sans-serif',
- }}
- >
- {m.solver.tapHint}
- </div>
- {/* On-screen keyboard */}
- <Keyboard
- letterColors={{}}
- onKeyPress={handleKey}
- enterDisabled={updateDisabled}
- />
- </div>
- );
- }
|