import React, { useState, useEffect, memo, useCallback } from 'react'; import html2canvas from 'html2canvas'; // Мемоизированный компонент для лучшей производительности const App = memo(() => { const [currentNumbers, setCurrentNumbers] = useState([0]); const [minRange, setMinRange] = useState(1); const [maxRange, setMaxRange] = useState(99999); const [quantity, setQuantity] = useState(1); const [targetNumber, setTargetNumber] = useState(348); const [clickCount, setClickCount] = useState(0); const [usedNumbers, setUsedNumbers] = useState>(new Set()); const [isAnimating, setIsAnimating] = useState(false); const [excludeRepeats, setExcludeRepeats] = useState(false); const [noRepeatsUntilLast, setNoRepeatsUntilLast] = useState(false); // Загрузка состояния из localStorage при инициализации useEffect(() => { const savedState = localStorage.getItem('randomGeneratorState'); if (savedState) { try { const state = JSON.parse(savedState); setCurrentNumbers(state.currentNumbers || [0]); setClickCount(state.clickCount || 0); setMinRange(state.minRange || 1); setMaxRange(state.maxRange || 99999); setQuantity(state.quantity || 1); setTargetNumber(state.targetNumber || 348); setUsedNumbers(new Set(state.usedNumbers || [])); setExcludeRepeats(state.excludeRepeats || false); setNoRepeatsUntilLast(state.noRepeatsUntilLast || false); } catch (error) { console.error('Ошибка загрузки состояния:', error); } } }, []); // Сохранение состояния в localStorage при изменении useEffect(() => { const state = { currentNumbers, clickCount, minRange, maxRange, quantity, targetNumber, usedNumbers: Array.from(usedNumbers), excludeRepeats, noRepeatsUntilLast }; localStorage.setItem('randomGeneratorState', JSON.stringify(state)); }, [currentNumbers, clickCount, minRange, maxRange, quantity, targetNumber, usedNumbers, excludeRepeats, noRepeatsUntilLast]); // Мемоизированная функция генерации чисел const generateRandomNumbers = useCallback(() => { const newClickCount = clickCount + 1; setClickCount(newClickCount); setIsAnimating(true); // На 11-й клик возвращаем заданное число как первое, если оно в диапазоне const shouldIncludeTarget = newClickCount === 11 && targetNumber >= minRange && targetNumber <= maxRange; const generateNumbers = () => { const results: number[] = []; let currentUsedNumbers = new Set(usedNumbers); // Для исключения повторов в текущей генерации const currentGenerationNumbers = new Set(); // Если нужно включить целевое число if (shouldIncludeTarget) { results.push(targetNumber); if (excludeRepeats) { currentGenerationNumbers.add(targetNumber); } if (noRepeatsUntilLast) { currentUsedNumbers.add(targetNumber); } } // Генерируем остальные числа const remainingQuantity = shouldIncludeTarget ? quantity - 1 : quantity; for (let i = 0; i < remainingQuantity; i++) { let availableNumbers = []; // Собираем доступные числа в зависимости от настроек for (let num = minRange; num <= maxRange; num++) { let isAvailable = true; // Исключаем целевое число if (num === targetNumber) { isAvailable = false; } // Проверяем "Исключить повторы" (в текущей генерации) if (excludeRepeats && currentGenerationNumbers.has(num)) { isAvailable = false; } // Проверяем "Без повторений, до последнего числа" (между всеми генерациями) if (noRepeatsUntilLast && currentUsedNumbers.has(num)) { isAvailable = false; } if (isAvailable) { availableNumbers.push(num); } } // Если все числа использованы для "Без повторений, до последнего числа" if (noRepeatsUntilLast && availableNumbers.length === 0) { // Сбрасываем использованные числа между генерациями currentUsedNumbers.clear(); if (shouldIncludeTarget) { currentUsedNumbers.add(targetNumber); } // Пересобираем доступные числа for (let num = minRange; num <= maxRange; num++) { let isAvailable = true; if (num === targetNumber) { isAvailable = false; } if (excludeRepeats && currentGenerationNumbers.has(num)) { isAvailable = false; } if (isAvailable) { availableNumbers.push(num); } } } if (availableNumbers.length > 0) { const randomIndex = Math.floor(Math.random() * availableNumbers.length); const randomNum = availableNumbers[randomIndex]; results.push(randomNum); // Отмечаем использование if (excludeRepeats) { currentGenerationNumbers.add(randomNum); } if (noRepeatsUntilLast) { currentUsedNumbers.add(randomNum); } } } return { results, updatedUsedNumbers: currentUsedNumbers }; }; setTimeout(() => { const { results, updatedUsedNumbers } = generateNumbers(); setCurrentNumbers(results); // Обновляем использованные числа только если включена соответствующая опция if (noRepeatsUntilLast) { setUsedNumbers(updatedUsedNumbers); } setIsAnimating(false); }, 800); }, [clickCount, minRange, maxRange, quantity, targetNumber, usedNumbers, excludeRepeats, noRepeatsUntilLast]); const handleRangeChange = useCallback((newMin?: number, newMax?: number) => { if (newMin !== undefined) setMinRange(newMin); if (newMax !== undefined) setMaxRange(newMax); // При изменении диапазона сбрасываем использованные числа, но сохраняем клики setUsedNumbers(new Set()); if (clickCount === 0) { setCurrentNumbers([0]); } }, [clickCount]); const handleQuantityChange = useCallback((newQuantity: number) => { const validQuantity = Math.max(1, Math.min(100, newQuantity)); // Ограничиваем от 1 до 100 setQuantity(validQuantity); if (clickCount === 0) { setCurrentNumbers(Array(validQuantity).fill(0)); } }, [clickCount]); // Функция копирования результата const copyResult = useCallback(async () => { const numberText = currentNumbers.join(', '); try { await navigator.clipboard.writeText(numberText); alert('Результат скопирован!'); } catch (err) { console.error('Ошибка копирования:', err); // Fallback для старых браузеров const textArea = document.createElement('textarea'); textArea.value = numberText; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); alert('Результат скопирован!'); } }, [currentNumbers]); // Простая функция скачивания скриншота const downloadScreenshot = useCallback(async () => { try { console.log('Начинаю создание скриншота...'); const element = document.body; if (!element) { console.error('Элемент не найден'); return; } console.log('Элемент найден, создаю скриншот...'); const canvas = await html2canvas(element, { allowTaint: true, useCORS: true, scale: 1, backgroundColor: '#ffffff', width: window.innerWidth, height: window.innerHeight, scrollX: 0, scrollY: 0, logging: true, onclone: (clonedDoc) => { console.log('Документ клонирован'); } }); console.log('Canvas создан:', canvas); // Создаем ссылку для скачивания const link = document.createElement('a'); const date = new Date().toISOString().split('T')[0]; link.download = `генератор-чисел-${date}.png`; link.href = canvas.toDataURL('image/png'); console.log('Ссылка создана, начинаю скачивание...'); // Программное скачивание document.body.appendChild(link); link.click(); document.body.removeChild(link); console.log('Скачивание завершено'); } catch (err) { console.error('Ошибка создания скриншота:', err); alert('Ошибка при создании скриншота: ' + err.message); } }, []); // Мемоизированная функция для даты/времени const getCurrentDateTime = useCallback(() => { const now = new Date(); const day = now.getDate().toString().padStart(2, '0'); const month = (now.getMonth() + 1).toString().padStart(2, '0'); const year = now.getFullYear(); const hours = now.getHours().toString().padStart(2, '0'); const minutes = now.getMinutes().toString().padStart(2, '0'); const seconds = now.getSeconds().toString().padStart(2, '0'); return `${day}.${month}.${year} в ${hours}:${minutes}:${seconds} GMT +3`; }, []); return (
{/* Top Header */}

Самый простой, точный и понятный

ГЕНЕРАТОР СЛУЧАЙНЫХ ЧИСЕЛ

{/* Numbers Display with Top-Down Reveal Animation */}
{currentNumbers.length === 1 ? (
{currentNumbers[0]}
) : (
{currentNumbers.map((num, index) => (
{num}
))}
)}
{/* Generate Button - БЕЗ анимации */}
{/* Date/Time */}
Сейчас: {getCurrentDateTime()}

{/* Settings */}
{/* Range */}

В ДИАПАЗОНЕ ОТ:

handleRangeChange(Number(e.target.value), undefined)} className="w-32 sm:w-40 px-4 py-3 border-2 border-gray-400 text-center text-lg sm:text-xl text-gray-500 focus:border-black focus:outline-none transition-colors bg-white" /> ДО handleRangeChange(undefined, Number(e.target.value))} className="w-32 sm:w-40 px-4 py-3 border-2 border-gray-400 text-center text-lg sm:text-xl text-gray-500 focus:border-black focus:outline-none transition-colors bg-white" />
{/* Quantity */}

КОЛИЧЕСТВО ЧИСЕЛ:

handleQuantityChange(Number(e.target.value))} min="1" max="100" className="w-32 sm:w-40 px-4 py-3 border-2 border-gray-400 text-center text-lg sm:text-xl text-gray-500 focus:border-black focus:outline-none transition-colors bg-white" />
{/* Options */}
Исключить повторы:
NEW! Без повторений, до последнего числа:
{/* Copy and Download Buttons */}
); }); App.displayName = 'App'; export default App;