diff --git a/.prettierignore b/.prettierignore index 17b03f7..c41b4e7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ dist node_modules docs *.html +coverage diff --git a/eslint.config.js b/eslint.config.js index 092408a..342511a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,11 +1,11 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; export default tseslint.config( - { ignores: ['dist'] }, + { ignores: ['dist', 'coverage'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], @@ -19,10 +19,7 @@ export default tseslint.config( }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], }, - }, -) + } +); diff --git a/src/App.tsx b/src/App.tsx index 8444d4a..2ca08c9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Box, Button, @@ -21,12 +21,16 @@ interface Mark { label: string; } +const gcd = (a: number, b: number): number => { + if (b === 0) return a; + return gcd(b, a % b); +}; + function App() { const defaultLength = 16; const minLength = 4; const maxLength = 64; - const [toggle, setToggle] = useState(false); const [copied, setCopied] = useState(false); const [length, setLength] = useState(defaultLength); @@ -36,9 +40,23 @@ function App() { const [special, setSpecial] = useState(false); const [excludeAmbiguous, setExcludeAmbiguous] = useState(true); - const password = useMemo(() => { - return genPassword(length, lowercase, uppercase, digits, special, excludeAmbiguous); - }, [toggle, length, lowercase, uppercase, digits, special, excludeAmbiguous]); + const enabledCharSetsCount = + Number(lowercase) + Number(uppercase) + Number(digits) + Number(special); + + const isLastEnabledCharSet = enabledCharSetsCount === 1; + + const [password, setPassword] = useState(() => + genPassword(defaultLength, true, true, true, false, true) + ); + + const regeneratePassword = useCallback(() => { + setPassword(genPassword(length, lowercase, uppercase, digits, special, excludeAmbiguous)); + }, [length, lowercase, uppercase, digits, special, excludeAmbiguous]); + + // Regenerate whenever inputs change + useEffect(() => { + regeneratePassword(); + }, [regeneratePassword]); const handleSliderChange = (_event: Event, newValue: number | number[]) => { if (Array.isArray(newValue)) { @@ -74,11 +92,6 @@ function App() { }); }; - const gcd = (a: number, b: number): number => { - if (b === 0) return a; - return gcd(b, a % b); - }; - const marks = useMemo(() => { const result: Mark[] = []; @@ -106,19 +119,43 @@ function App() { setLowercase(!lowercase)} />} + control={ + setLowercase(!lowercase)} + disabled={isLastEnabledCharSet && lowercase} + /> + } label="Lowercase" /> setUppercase(!uppercase)} />} + control={ + setUppercase(!uppercase)} + disabled={isLastEnabledCharSet && uppercase} + /> + } label="Uppercase" /> setDigits(!digits)} />} + control={ + setDigits(!digits)} + disabled={isLastEnabledCharSet && digits} + /> + } label="Digits" /> setSpecial(!special)} />} + control={ + setSpecial(!special)} + disabled={isLastEnabledCharSet && special} + /> + } label="Special" /> setToggle(!toggle)} + onClick={regeneratePassword} endIcon={} > Generate diff --git a/src/genPassword.ts b/src/genPassword.ts index e6cb3bd..e27f7d8 100644 --- a/src/genPassword.ts +++ b/src/genPassword.ts @@ -14,87 +14,92 @@ const random = (min: number, max: number): number => { }; const genPassword = ( - length: number = 32, - useLowercase: boolean = true, - useUppercase: boolean = false, - useDigits: boolean = false, - useSpecial: boolean = false, - excludeAmbiguous: boolean = false + length: number = 32, + useLowercase: boolean = true, + useUppercase: boolean = false, + useDigits: boolean = false, + useSpecial: boolean = false, + excludeAmbiguous: boolean = false ): string => { - // Define ambiguous characters to exclude - // 0O (zero/O), 1lI (one/l/I), 5S (five/S), 8B (eight/B), 2Z (two/Z), 6b (six/b), 9q (nine/q) - const ambiguousChars = '0O1lIo5S8B2Z6b9q'; - - // Filter character sets if excludeAmbiguous is enabled - const filterAmbiguous = (chars: string): string => { - if (!excludeAmbiguous) return chars; - return chars.split('').filter(char => !ambiguousChars.includes(char)).join(''); - }; - - const charSets: string[] = []; - let charPool = ''; - - if (useLowercase) { - const filteredLowercase = filterAmbiguous(lowercase); - if (filteredLowercase.length > 0) { - charSets.push(filteredLowercase); - charPool += filteredLowercase; - } + // Define ambiguous characters to exclude + // 0O (zero/O), 1lI (one/l/I), 5S (five/S), 8B (eight/B), 2Z (two/Z), 6b (six/b), 9q (nine/q) + const ambiguousChars = '0O1lIo5S8B2Z6b9q'; + + // Filter character sets if excludeAmbiguous is enabled + const filterAmbiguous = (chars: string): string => { + if (!excludeAmbiguous) return chars; + return chars + .split('') + .filter((char) => !ambiguousChars.includes(char)) + .join(''); + }; + + const charSets: string[] = []; + let charPool = ''; + + if (useLowercase) { + const filteredLowercase = filterAmbiguous(lowercase); + if (filteredLowercase.length > 0) { + charSets.push(filteredLowercase); + charPool += filteredLowercase; } + } - if (useUppercase) { - const filteredUppercase = filterAmbiguous(uppercase); - if (filteredUppercase.length > 0) { - charSets.push(filteredUppercase); - charPool += filteredUppercase; - } + if (useUppercase) { + const filteredUppercase = filterAmbiguous(uppercase); + if (filteredUppercase.length > 0) { + charSets.push(filteredUppercase); + charPool += filteredUppercase; } + } - if (useDigits) { - const filteredDigits = filterAmbiguous(digits); - if (filteredDigits.length > 0) { - charSets.push(filteredDigits); - charPool += filteredDigits; - } + if (useDigits) { + const filteredDigits = filterAmbiguous(digits); + if (filteredDigits.length > 0) { + charSets.push(filteredDigits); + charPool += filteredDigits; } + } - if (useSpecial) { - const filteredSpecial = filterAmbiguous(special); - if (filteredSpecial.length > 0) { - charSets.push(filteredSpecial); - charPool += filteredSpecial; - } + if (useSpecial) { + const filteredSpecial = filterAmbiguous(special); + if (filteredSpecial.length > 0) { + charSets.push(filteredSpecial); + charPool += filteredSpecial; } - - if (charPool.length === 0) { - throw new Error('At least one character type must be selected'); - } - - if (length < charSets.length) { - throw new Error(`Password length must be at least ${charSets.length} to include all selected character types`); - } - - const result: string[] = []; - - // Step 1: Guarantee at least one character from each selected set - for (const set of charSets) { - const randomIndex = random(0, set.length); - result.push(set[randomIndex]); - } - - // Step 2: Fill remaining positions with random characters from the full pool - for (let i = charSets.length; i < length; i++) { - const randomIndex = random(0, charPool.length); - result.push(charPool[randomIndex]); - } - - // Step 3: Shuffle to avoid predictable patterns (e.g., always lowercase first) - for (let i = result.length - 1; i > 0; i--) { - const j = random(0, i + 1); - [result[i], result[j]] = [result[j], result[i]]; - } - - return result.join(''); -} + } + + if (charPool.length === 0) { + throw new Error('At least one character type must be selected'); + } + + if (length < charSets.length) { + throw new Error( + `Password length must be at least ${charSets.length} to include all selected character types` + ); + } + + const result: string[] = []; + + // Step 1: Guarantee at least one character from each selected set + for (const set of charSets) { + const randomIndex = random(0, set.length); + result.push(set[randomIndex]); + } + + // Step 2: Fill remaining positions with random characters from the full pool + for (let i = charSets.length; i < length; i++) { + const randomIndex = random(0, charPool.length); + result.push(charPool[randomIndex]); + } + + // Step 3: Shuffle to avoid predictable patterns (e.g., always lowercase first) + for (let i = result.length - 1; i > 0; i--) { + const j = random(0, i + 1); + [result[i], result[j]] = [result[j], result[i]]; + } + + return result.join(''); +}; export default genPassword;