/
DnTyr
/
Password-Development
Обзор
Документация
Войти
/
DnTyr
/
Password-Development
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
frontend/src/components/Generator.js
319 строк
10 KB
DT
first_commit
03 июн 2026, 10:22
03 июн 2026, 10:22
357cb35
Код
Авторство
О чём код?
import { useGenerator, CLIPBOARD_CLEAR_SECONDS } from "../hooks/useGenerator"; import React, { useState, useRef, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { toastError } from "../utils/toast"; import { Eye, EyeOff, Sparkles, Lock, ChevronLeft } from 'lucide-react'; function Generator({ setResetSignal }) { const { t } = useTranslation(); const [visible, setVisible] = useState(true); const [manualPassphraseMode, setManualPassphraseMode] = useState(null); const { length, setLength, password, uppercase, setUppercase, numbers, setNumbers, symbols, setSymbols, loading, cooldown, generatePassword, copyPassword, clipboardSecondsLeft, } = useGenerator(); const isPassphrase = manualPassphraseMode !== null ? manualPassphraseMode : Number(length) > 32; const [modeSwitchPending, setModeSwitchPending] = useState(false); const lengthInputRef = useRef(null); const triggerGenerate = () => { if (cooldown || loading || modeSwitchPending) return; try { generatePassword(isPassphrase); if (typeof setResetSignal === "function") { setResetSignal(Date.now()); } } finally { const isTouchDevice = !window.matchMedia('(hover: hover) and (pointer: fine)').matches; if (!isTouchDevice && lengthInputRef.current) { lengthInputRef.current.focus(); } } }; const [showTip, setShowTip] = useState(false); // Реф (был ли переход между 32 и 33) const lastStableLengthRef = useRef(Number(length)); useEffect(() => { const num = Number(length); setModeSwitchPending(true); const timer = setTimeout(() => { const wasClassic = lastStableLengthRef.current <= 32; const isPhrase = num >= 33; // СЛУЧАЙ А: Переход вверх (авто-включение фраз) if (wasClassic && isPhrase) { // Только если режим фразы ВКЛЮЧЕН (автоматически или вручную) if (isPassphrase) { setUppercase(false); setNumbers(false); setSymbols(false); } } // СЛУЧАЙ Б: Переход вниз (возврат к короткой классике) else if (!wasClassic && !isPhrase && num > 0) { setUppercase(true); setNumbers(true); setSymbols(true); setManualPassphraseMode(null); } // СЛУЧАЙ В: Если мы ОСТАЕМСЯ на большой длине, но ВЫКЛЮЧИЛИ фразы вручную // Возвращаем чекбоксы в true, чтобы не было "голых" маленьких букв if (num > 32 && !isPassphrase) { setUppercase(true); setNumbers(true); setSymbols(true); } lastStableLengthRef.current = num; setModeSwitchPending(false); }, 1500); // задержка 1,5 секунды return () => clearTimeout(timer); }, [length, isPassphrase, setUppercase, setNumbers, setSymbols]); return ( <div className="generator-container"> <h2>{t("Генератор")}</h2> <label htmlFor="password-length-input"> <h5> {t("Укажите кол-во символов:")} <span className="length-range"> (12 - 128)</span> </h5> </label> <input id="password-length-input" ref={lengthInputRef} name="password-length" type="text" inputMode="numeric" pattern="[0-9]*" value={length} onChange={(e) => { const value = e.target.value; if (!/^\d*$/.test(value)) { toastError(t("Только цифры")); return; } if (value.length > 3) return; setLength(value); }} onPaste={(e) => { const paste = e.clipboardData.getData("text"); // Если вставляют слишком длинную строку или не цифры if (!/^\d+$/.test(paste) || paste.length > 3) { e.preventDefault(); toastError(t("Только цифры (макс. 3 знака)")); } }} placeholder={t("12-128")} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); e.target.blur(); if (e.repeat) return; if (cooldown || loading) return; triggerGenerate(); } }} onKeyUp={(e) => { if (e.key === "Enter") { } }} autoComplete="off" spellCheck="false" /> <div className="passphrase-row clickable" onClick={() => { setModeSwitchPending(true); const num = Number(length); if (num <= 32) { setLength(33); setManualPassphraseMode(true); } else { setManualPassphraseMode(!isPassphrase); } }} > <div className="passphrase-status-text"> <span key={isPassphrase} className={isPassphrase ? "animate-flash" : "status-emoji"}> {isPassphrase ? ( <Sparkles color="var(--color-sparkles)" size={26} /> ) : ( <Lock color="var(--color-lock)" size={18} /> )} </span> <div className="passphrase-label-group" style={{ display: 'flex', gap: '4px' }}> <span>{t("Парольная фраза:")}</span> <span className={isPassphrase ? "status-on" : "status-off"}> {isPassphrase ? t("вкл.") : t("выкл.")} </span> </div> </div> {/* Блок с иконкой информации */} <div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}> {/* 1. Стрелка ПЕРЕД вопросом */} {showTip && ( <span className="pointer-arrow-animate" style={{ position: 'absolute', right: '35px', }} > <ChevronLeft size={26} color="var(--color-sparkles)" /> </span> )} {/* 2. Знак вопроса */} <span className="info-icon" onMouseEnter={() => setShowTip(true)} onMouseLeave={() => setShowTip(false)} onClick={(e) => { e.stopPropagation(); setShowTip(!showTip); }} > ? </span> {/* 3. Облако подсказки */} {showTip && ( <div className="custom-tooltip"> {Number(length) <= 32 ? t("Режим фраз доступен от 33 символов. Нажмите на строку для быстрого перехода.") : isPassphrase ? t("Фраза активна. Кликните на строку, чтобы переключиться на обычный пароль.") : t("Обычный пароль активен. Кликните на строку, чтобы вернуть режим фраз.")} </div> )} </div> </div> <div className="toggle-group"> <div className="toggle"> <span>{t("Заглавные буквы")}</span> <label className="switch"> <input type="checkbox" checked={uppercase} onChange={() => setUppercase(!uppercase)} /> <span className="slider"></span> </label> </div> <div className="toggle"> <span>{t("Цифры")}</span> <label className="switch"> <input type="checkbox" checked={numbers} onChange={() => setNumbers(!numbers)} /> <span className="slider"></span> </label> </div> <div className="toggle"> <span>{t("Символы")}</span> <label className="switch"> <input type="checkbox" checked={symbols} onChange={() => setSymbols(!symbols)} /> <span className="slider"></span> </label> </div> </div> <button onClick={triggerGenerate} disabled={cooldown || loading || modeSwitchPending}> {loading ? t("Загрузка...") : t("Сгенерировать")} </button> {password && ( <div className="result"> <div className="password-display-wrapper"> <div className={`generated-password ${!visible ? "is-masked" : ""}`}> {password} </div> <button type="button" className="password-visibility-toggle" onClick={() => setVisible(!visible)} aria-label={visible ? "Скрыть пароль" : "Показать пароль"} > {visible ? <EyeOff size={20} /> : <Eye size={20} />} </button> </div> <button type="button" onClick={copyPassword}> {t("Копировать")} </button> {clipboardSecondsLeft != null && ( <div className="clipboard-secure-panel"> <div className="clipboard-secure-bar-wrap"> <div className="clipboard-secure-bar-fill" style={{ width: `${ (clipboardSecondsLeft / CLIPBOARD_CLEAR_SECONDS) * 100 }%`, }} /> </div> <p className="clipboard-secure-text"> {t("Буфер будет очищен через")}{" "} <strong>{clipboardSecondsLeft}</strong> {t("сек.")} </p> </div> )} </div> )} </div> ); } export default Generator;