import { useState, useRef, useCallback, useEffect, memo, useMemo } from 'react'; import { colors, radius, shadows } from '../styles/theme'; interface CellProps { letter: string; size: number; fontSize: number; bg: string; border: string; textColor: string; boxShadow: string; animation: string | undefined; } const Cell = memo(({ letter, size, fontSize, bg, border, textColor, boxShadow, animation }) => { return (
{letter}
); }); Cell.displayName = 'Cell'; interface LetterGridProps { letters: string[][]; gridSize: number; onWordSelect: (cells: [number, number][]) => void; foundWords: string[]; firstLetterCell: [number, number] | null; wordPaths: Record; newFoundWord?: string | null; } const LetterGrid: React.FC = ({ letters, gridSize, onWordSelect, foundWords, firstLetterCell, wordPaths, newFoundWord, }) => { const [selectedCells, setSelectedCells] = useState<[number, number][]>([]); const [isSelecting, setIsSelecting] = useState(false); const [windowWidth, setWindowWidth] = useState(window.innerWidth); const gridRef = useRef(null); const processingRef = useRef(false); const touchTimeoutRef = useRef | null>(null); const lastProcessedCellsRef = useRef(''); // Адаптивный размер клетки const maxGridWidth = Math.min(windowWidth - 32, 400); const gapSize = gridSize <= 4 ? 5 : gridSize <= 6 ? 4 : 3; const totalGap = gapSize * (gridSize - 1); const cellSize = Math.floor((maxGridWidth - totalGap) / gridSize); useEffect(() => { const handleResize = () => { setWindowWidth(window.innerWidth); }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); if (touchTimeoutRef.current) { clearTimeout(touchTimeoutRef.current); } }; }, []); useEffect(() => { processingRef.current = false; lastProcessedCellsRef.current = ''; if (touchTimeoutRef.current) { clearTimeout(touchTimeoutRef.current); touchTimeoutRef.current = null; } }, [letters]); // useMemo для мгновенного обновления (без задержки useEffect) const foundCells = useMemo(() => { const fc = new Set(); foundWords.forEach(word => { const path = wordPaths[word]; if (path) { path.forEach(([r, c]) => { fc.add(`${r},${c}`); }); } }); return fc; }, [foundWords, wordPaths]); const newFoundCells = useMemo(() => { const nfc = new Set(); if (newFoundWord) { const path = wordPaths[newFoundWord]; if (path) { path.forEach(([r, c]) => { nfc.add(`${r},${c}`); }); } } return nfc; }, [newFoundWord, wordPaths]); const selectedSet = useMemo(() => { const ss = new Set(); selectedCells.forEach(([r, c]) => ss.add(`${r},${c}`)); return ss; }, [selectedCells]); const isAdjacent = (cell1: [number, number], cell2: [number, number]): boolean => { const [r1, c1] = cell1; const [r2, c2] = cell2; return (Math.abs(r1 - r2) === 1 && c1 === c2) || (Math.abs(c1 - c2) === 1 && r1 === r2); }; const getCellFromTouch = useCallback((clientX: number, clientY: number): [number, number] | null => { if (!gridRef.current) return null; const rect = gridRef.current.getBoundingClientRect(); const x = clientX - rect.left; const y = clientY - rect.top; const col = Math.floor(x / (cellSize + gapSize)); const row = Math.floor(y / (cellSize + gapSize)); if (row >= 0 && row < gridSize && col >= 0 && col < gridSize) { return [row, col]; } return null; }, [gridSize, cellSize, gapSize]); const handleTouchStart = useCallback((e: React.TouchEvent) => { e.preventDefault(); if (processingRef.current) return; const touch = e.touches[0]; const cell = getCellFromTouch(touch.clientX, touch.clientY); if (cell) { const [row, col] = cell; const key = `${row},${col}`; if (!foundCells.has(key)) { setIsSelecting(true); setSelectedCells([cell]); if (window.Telegram?.WebApp?.HapticFeedback) { window.Telegram.WebApp.HapticFeedback.impactOccurred('light'); } } } }, [foundCells, getCellFromTouch]); const handleTouchMove = useCallback((e: React.TouchEvent) => { e.preventDefault(); if (!isSelecting || processingRef.current) return; const touch = e.touches[0]; const cell = getCellFromTouch(touch.clientX, touch.clientY); if (!cell) return; const [row, col] = cell; const key = `${row},${col}`; if (foundCells.has(key)) return; setSelectedCells(prev => { const lastCell = prev[prev.length - 1]; if (!lastCell) return prev; if (lastCell[0] === row && lastCell[1] === col) return prev; if (!isAdjacent(lastCell, [row, col])) return prev; const alreadySelected = prev.some(([r, c]) => r === row && c === col); if (alreadySelected) { const prevCell = prev[prev.length - 2]; if (prevCell && prevCell[0] === row && prevCell[1] === col) { return prev.slice(0, -1); } return prev; } return [...prev, [row, col]]; }); }, [isSelecting, foundCells, getCellFromTouch]); const handleTouchEnd = useCallback((e: React.TouchEvent) => { e.preventDefault(); if (!isSelecting || processingRef.current) { setIsSelecting(false); setSelectedCells([]); return; } if (selectedCells.length > 1) { const cellsKey = selectedCells.map(([r, c]) => `${r},${c}`).join('|'); if (cellsKey !== lastProcessedCellsRef.current) { processingRef.current = true; lastProcessedCellsRef.current = cellsKey; onWordSelect(selectedCells); if (touchTimeoutRef.current) { clearTimeout(touchTimeoutRef.current); } touchTimeoutRef.current = setTimeout(() => { processingRef.current = false; touchTimeoutRef.current = null; }, 300); } } setIsSelecting(false); setSelectedCells([]); }, [isSelecting, selectedCells, onWordSelect]); const flatLetters = Array.isArray(letters) ? letters.flat() : []; if (flatLetters.length === 0 || flatLetters.length !== gridSize * gridSize) { return (
Loading grid...
); } const fontSize = gridSize <= 4 ? Math.max(cellSize * 0.42, 18) : gridSize <= 6 ? Math.max(cellSize * 0.4, 15) : gridSize <= 8 ? Math.max(cellSize * 0.38, 13) : Math.max(cellSize * 0.35, 11); return (
{flatLetters.map((letter, index) => { const row = Math.floor(index / gridSize); const col = index % gridSize; const key = `${row},${col}`; const selected = selectedSet.has(key); const first = firstLetterCell ? firstLetterCell[0] === row && firstLetterCell[1] === col : false; const found = foundCells.has(key); const newFound = newFoundCells.has(key); let bg = colors.cellDefault; let border = colors.border; let textColor = colors.text; let animation = ''; let boxShadow = shadows.cell; if (found) { bg = colors.cellFound; border = colors.success; textColor = '#1a7a3a'; if (newFound) { animation = 'cellPopIn 0.4s ease, cellGlow 1s ease 0.4s'; } } else if (first) { bg = colors.cellFirst; border = colors.gold; textColor = '#b8860b'; animation = 'firstLetterPulse 1s ease infinite'; boxShadow = '0 0 10px rgba(255, 215, 0, 0.5)'; } else if (selected) { bg = colors.cellSelected; border = colors.accent; textColor = colors.accent; boxShadow = `0 0 8px ${colors.accent}40`; } return ( ); })}
); }; export default LetterGrid;