Skip to content

Commit bc11276

Browse files
authored
Merge pull request #2 from webishdev/improve
Improve
2 parents e9ae2a9 + 2343212 commit bc11276

4 files changed

Lines changed: 141 additions & 101 deletions

File tree

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ dist
22
node_modules
33
docs
44
*.html
5+
coverage

eslint.config.js

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import js from '@eslint/js'
2-
import globals from 'globals'
3-
import reactHooks from 'eslint-plugin-react-hooks'
4-
import reactRefresh from 'eslint-plugin-react-refresh'
5-
import tseslint from 'typescript-eslint'
1+
import js from '@eslint/js';
2+
import globals from 'globals';
3+
import reactHooks from 'eslint-plugin-react-hooks';
4+
import reactRefresh from 'eslint-plugin-react-refresh';
5+
import tseslint from 'typescript-eslint';
66

77
export default tseslint.config(
8-
{ ignores: ['dist'] },
8+
{ ignores: ['dist', 'coverage'] },
99
{
1010
extends: [js.configs.recommended, ...tseslint.configs.recommended],
1111
files: ['**/*.{ts,tsx}'],
@@ -19,10 +19,7 @@ export default tseslint.config(
1919
},
2020
rules: {
2121
...reactHooks.configs.recommended.rules,
22-
'react-refresh/only-export-components': [
23-
'warn',
24-
{ allowConstantExport: true },
25-
],
22+
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
2623
},
27-
},
28-
)
24+
}
25+
);

src/App.tsx

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo, useState } from 'react';
1+
import { useCallback, useEffect, useMemo, useState } from 'react';
22
import {
33
Box,
44
Button,
@@ -21,12 +21,16 @@ interface Mark {
2121
label: string;
2222
}
2323

24+
const gcd = (a: number, b: number): number => {
25+
if (b === 0) return a;
26+
return gcd(b, a % b);
27+
};
28+
2429
function App() {
2530
const defaultLength = 16;
2631
const minLength = 4;
2732
const maxLength = 64;
2833

29-
const [toggle, setToggle] = useState<boolean>(false);
3034
const [copied, setCopied] = useState<boolean>(false);
3135

3236
const [length, setLength] = useState<number>(defaultLength);
@@ -36,9 +40,23 @@ function App() {
3640
const [special, setSpecial] = useState<boolean>(false);
3741
const [excludeAmbiguous, setExcludeAmbiguous] = useState<boolean>(true);
3842

39-
const password = useMemo<string>(() => {
40-
return genPassword(length, lowercase, uppercase, digits, special, excludeAmbiguous);
41-
}, [toggle, length, lowercase, uppercase, digits, special, excludeAmbiguous]);
43+
const enabledCharSetsCount =
44+
Number(lowercase) + Number(uppercase) + Number(digits) + Number(special);
45+
46+
const isLastEnabledCharSet = enabledCharSetsCount === 1;
47+
48+
const [password, setPassword] = useState<string>(() =>
49+
genPassword(defaultLength, true, true, true, false, true)
50+
);
51+
52+
const regeneratePassword = useCallback(() => {
53+
setPassword(genPassword(length, lowercase, uppercase, digits, special, excludeAmbiguous));
54+
}, [length, lowercase, uppercase, digits, special, excludeAmbiguous]);
55+
56+
// Regenerate whenever inputs change
57+
useEffect(() => {
58+
regeneratePassword();
59+
}, [regeneratePassword]);
4260

4361
const handleSliderChange = (_event: Event, newValue: number | number[]) => {
4462
if (Array.isArray(newValue)) {
@@ -74,11 +92,6 @@ function App() {
7492
});
7593
};
7694

77-
const gcd = (a: number, b: number): number => {
78-
if (b === 0) return a;
79-
return gcd(b, a % b);
80-
};
81-
8295
const marks = useMemo<Mark[]>(() => {
8396
const result: Mark[] = [];
8497

@@ -106,19 +119,43 @@ function App() {
106119
<Divider />
107120
<Stack>
108121
<FormControlLabel
109-
control={<Switch checked={lowercase} onChange={() => setLowercase(!lowercase)} />}
122+
control={
123+
<Switch
124+
checked={lowercase}
125+
onChange={() => setLowercase(!lowercase)}
126+
disabled={isLastEnabledCharSet && lowercase}
127+
/>
128+
}
110129
label="Lowercase"
111130
/>
112131
<FormControlLabel
113-
control={<Switch checked={uppercase} onChange={() => setUppercase(!uppercase)} />}
132+
control={
133+
<Switch
134+
checked={uppercase}
135+
onChange={() => setUppercase(!uppercase)}
136+
disabled={isLastEnabledCharSet && uppercase}
137+
/>
138+
}
114139
label="Uppercase"
115140
/>
116141
<FormControlLabel
117-
control={<Switch checked={digits} onChange={() => setDigits(!digits)} />}
142+
control={
143+
<Switch
144+
checked={digits}
145+
onChange={() => setDigits(!digits)}
146+
disabled={isLastEnabledCharSet && digits}
147+
/>
148+
}
118149
label="Digits"
119150
/>
120151
<FormControlLabel
121-
control={<Switch checked={special} onChange={() => setSpecial(!special)} />}
152+
control={
153+
<Switch
154+
checked={special}
155+
onChange={() => setSpecial(!special)}
156+
disabled={isLastEnabledCharSet && special}
157+
/>
158+
}
122159
label="Special"
123160
/>
124161
<FormControlLabel
@@ -175,7 +212,7 @@ function App() {
175212
<Button
176213
variant="contained"
177214
size="large"
178-
onClick={() => setToggle(!toggle)}
215+
onClick={regeneratePassword}
179216
endIcon={<RunCircleIcon />}
180217
>
181218
Generate

src/genPassword.ts

Lines changed: 79 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -14,87 +14,92 @@ const random = (min: number, max: number): number => {
1414
};
1515

1616
const genPassword = (
17-
length: number = 32,
18-
useLowercase: boolean = true,
19-
useUppercase: boolean = false,
20-
useDigits: boolean = false,
21-
useSpecial: boolean = false,
22-
excludeAmbiguous: boolean = false
17+
length: number = 32,
18+
useLowercase: boolean = true,
19+
useUppercase: boolean = false,
20+
useDigits: boolean = false,
21+
useSpecial: boolean = false,
22+
excludeAmbiguous: boolean = false
2323
): string => {
24-
// Define ambiguous characters to exclude
25-
// 0O (zero/O), 1lI (one/l/I), 5S (five/S), 8B (eight/B), 2Z (two/Z), 6b (six/b), 9q (nine/q)
26-
const ambiguousChars = '0O1lIo5S8B2Z6b9q';
27-
28-
// Filter character sets if excludeAmbiguous is enabled
29-
const filterAmbiguous = (chars: string): string => {
30-
if (!excludeAmbiguous) return chars;
31-
return chars.split('').filter(char => !ambiguousChars.includes(char)).join('');
32-
};
33-
34-
const charSets: string[] = [];
35-
let charPool = '';
36-
37-
if (useLowercase) {
38-
const filteredLowercase = filterAmbiguous(lowercase);
39-
if (filteredLowercase.length > 0) {
40-
charSets.push(filteredLowercase);
41-
charPool += filteredLowercase;
42-
}
24+
// Define ambiguous characters to exclude
25+
// 0O (zero/O), 1lI (one/l/I), 5S (five/S), 8B (eight/B), 2Z (two/Z), 6b (six/b), 9q (nine/q)
26+
const ambiguousChars = '0O1lIo5S8B2Z6b9q';
27+
28+
// Filter character sets if excludeAmbiguous is enabled
29+
const filterAmbiguous = (chars: string): string => {
30+
if (!excludeAmbiguous) return chars;
31+
return chars
32+
.split('')
33+
.filter((char) => !ambiguousChars.includes(char))
34+
.join('');
35+
};
36+
37+
const charSets: string[] = [];
38+
let charPool = '';
39+
40+
if (useLowercase) {
41+
const filteredLowercase = filterAmbiguous(lowercase);
42+
if (filteredLowercase.length > 0) {
43+
charSets.push(filteredLowercase);
44+
charPool += filteredLowercase;
4345
}
46+
}
4447

45-
if (useUppercase) {
46-
const filteredUppercase = filterAmbiguous(uppercase);
47-
if (filteredUppercase.length > 0) {
48-
charSets.push(filteredUppercase);
49-
charPool += filteredUppercase;
50-
}
48+
if (useUppercase) {
49+
const filteredUppercase = filterAmbiguous(uppercase);
50+
if (filteredUppercase.length > 0) {
51+
charSets.push(filteredUppercase);
52+
charPool += filteredUppercase;
5153
}
54+
}
5255

53-
if (useDigits) {
54-
const filteredDigits = filterAmbiguous(digits);
55-
if (filteredDigits.length > 0) {
56-
charSets.push(filteredDigits);
57-
charPool += filteredDigits;
58-
}
56+
if (useDigits) {
57+
const filteredDigits = filterAmbiguous(digits);
58+
if (filteredDigits.length > 0) {
59+
charSets.push(filteredDigits);
60+
charPool += filteredDigits;
5961
}
62+
}
6063

61-
if (useSpecial) {
62-
const filteredSpecial = filterAmbiguous(special);
63-
if (filteredSpecial.length > 0) {
64-
charSets.push(filteredSpecial);
65-
charPool += filteredSpecial;
66-
}
64+
if (useSpecial) {
65+
const filteredSpecial = filterAmbiguous(special);
66+
if (filteredSpecial.length > 0) {
67+
charSets.push(filteredSpecial);
68+
charPool += filteredSpecial;
6769
}
68-
69-
if (charPool.length === 0) {
70-
throw new Error('At least one character type must be selected');
71-
}
72-
73-
if (length < charSets.length) {
74-
throw new Error(`Password length must be at least ${charSets.length} to include all selected character types`);
75-
}
76-
77-
const result: string[] = [];
78-
79-
// Step 1: Guarantee at least one character from each selected set
80-
for (const set of charSets) {
81-
const randomIndex = random(0, set.length);
82-
result.push(set[randomIndex]);
83-
}
84-
85-
// Step 2: Fill remaining positions with random characters from the full pool
86-
for (let i = charSets.length; i < length; i++) {
87-
const randomIndex = random(0, charPool.length);
88-
result.push(charPool[randomIndex]);
89-
}
90-
91-
// Step 3: Shuffle to avoid predictable patterns (e.g., always lowercase first)
92-
for (let i = result.length - 1; i > 0; i--) {
93-
const j = random(0, i + 1);
94-
[result[i], result[j]] = [result[j], result[i]];
95-
}
96-
97-
return result.join('');
98-
}
70+
}
71+
72+
if (charPool.length === 0) {
73+
throw new Error('At least one character type must be selected');
74+
}
75+
76+
if (length < charSets.length) {
77+
throw new Error(
78+
`Password length must be at least ${charSets.length} to include all selected character types`
79+
);
80+
}
81+
82+
const result: string[] = [];
83+
84+
// Step 1: Guarantee at least one character from each selected set
85+
for (const set of charSets) {
86+
const randomIndex = random(0, set.length);
87+
result.push(set[randomIndex]);
88+
}
89+
90+
// Step 2: Fill remaining positions with random characters from the full pool
91+
for (let i = charSets.length; i < length; i++) {
92+
const randomIndex = random(0, charPool.length);
93+
result.push(charPool[randomIndex]);
94+
}
95+
96+
// Step 3: Shuffle to avoid predictable patterns (e.g., always lowercase first)
97+
for (let i = result.length - 1; i > 0; i--) {
98+
const j = random(0, i + 1);
99+
[result[i], result[j]] = [result[j], result[i]];
100+
}
101+
102+
return result.join('');
103+
};
99104

100105
export default genPassword;

0 commit comments

Comments
 (0)