diff --git a/src/components/javigaralva/components/button.tsx b/src/components/javigaralva/components/button.tsx new file mode 100644 index 000000000..46c5c346f --- /dev/null +++ b/src/components/javigaralva/components/button.tsx @@ -0,0 +1,10 @@ +export function Button({ children, className = '', onClick }) { + return ( + + ) +} diff --git a/src/components/javigaralva/components/colorized-letter.tsx b/src/components/javigaralva/components/colorized-character.tsx similarity index 71% rename from src/components/javigaralva/components/colorized-letter.tsx rename to src/components/javigaralva/components/colorized-character.tsx index 5348b69fa..3dfe76353 100644 --- a/src/components/javigaralva/components/colorized-letter.tsx +++ b/src/components/javigaralva/components/colorized-character.tsx @@ -8,34 +8,35 @@ import { isUpperCaseLetter } from '../utils/is-upper-case-letter' import { isSymbol } from '../utils/is-symbol' import { isNumber } from '../utils/is-number' -type ColorizedLetterStyles = { +type ColorizedCharacterStyles = { classNames: string style: CSSProperties } -interface ColorizedLetterProps { - letter: string +interface ColorizedCharacterProps { + character: string + onClick: () => void } -export function ColorizedLetter({ letter }: ColorizedLetterProps) { - const { classNames, style } = getStyles(letter) +export function ColorizedCharacter({ character, onClick }: ColorizedCharacterProps) { + const { classNames, style } = getStyles(character) return ( - - {letter} + + {character} ) } -function getStyles(letter: string): ColorizedLetterStyles { +function getStyles(character: string): ColorizedCharacterStyles { let classNames = 'text-white' let style: CSSProperties = {} - if (isNumber(letter)) { + if (isNumber(character)) { classNames = 'text-orange-600' style = getBigDotsStyle() - } else if (isSymbol(letter)) { + } else if (isSymbol(character)) { classNames = 'text-lime-600' style = getBigCircleDottedStyle() style.textShadow = 'rgb(116 255 77 / 60%) -1px -1px 6px, rgb(124 127 255 / 0%) 1px 1px 6px' - } else if (isUpperCaseLetter(letter)) { + } else if (isUpperCaseLetter(character)) { classNames = 'text-white/50' style = getSquareAndVerticalLinesStyle() } else { diff --git a/src/components/javigaralva/components/colorized-password.tsx b/src/components/javigaralva/components/colorized-password.tsx index 80e7396c6..353262c0f 100644 --- a/src/components/javigaralva/components/colorized-password.tsx +++ b/src/components/javigaralva/components/colorized-password.tsx @@ -1,13 +1,29 @@ -import { ColorizedLetter } from './colorized-letter' +import { ColorizedCharacter } from './colorized-character' + +type OnClickCharacterHandlerParams = { + character: string + index: number +} +export type OnClickCharacterHandler = ( + onClickCharacterHandlerParams: OnClickCharacterHandlerParams +) => void interface ColorizedPasswordProps { password: string + onClickCharacter?: OnClickCharacterHandler } -export function ColorizedPassword({ password }: ColorizedPasswordProps) { +export function ColorizedPassword({ password, onClickCharacter }: ColorizedPasswordProps) { + const handleOnClickCharacter: OnClickCharacterHandler = ({ character, index }) => { + onClickCharacter?.({ character, index }) + } return (
- {password.split('').map((letter, i) => ( - + {password.split('').map((character, i) => ( + handleOnClickCharacter({ character, index: i })} + /> ))}
) diff --git a/src/components/javigaralva/constants/constants.tsx b/src/components/javigaralva/constants/constants.tsx index 3201eaaba..dab6f6a04 100644 --- a/src/components/javigaralva/constants/constants.tsx +++ b/src/components/javigaralva/constants/constants.tsx @@ -1,3 +1,5 @@ +import { GeneratePasswordOptions } from '../utils/generate-password/generate-password' + const LETTERS = 'abcdefghijklmnopqrstuvwxyz' export const NUMBERS = '0123456789' export const SYMBOLS = '\'\\¡!"·$%&()=€@[]{}^/+-*' @@ -5,3 +7,13 @@ export const UPPER_CASE_LETTERS_LIST = LETTERS.toUpperCase().split('') export const LOWER_CASE_LETTERS_LIST = LETTERS.toLowerCase().split('') export const NUMBERS_LIST = NUMBERS.split('') export const SYMBOLS_LIST = SYMBOLS.split('') +export const MIN_PASSWORD_LENGTH = 1 +export const MAX_PASSWORD_LENGTH = 30 + +export const DEFAULT_PASSWORD_OPTIONS: GeneratePasswordOptions = { + length: 10, + hasUpperCaseLetters: true, + hasLowerCaseLetters: true, + hasNumbers: true, + hasSymbols: true +} diff --git a/src/components/javigaralva/hooks/use-password-generator.tsx b/src/components/javigaralva/hooks/use-password-generator.tsx new file mode 100644 index 000000000..3d12794aa --- /dev/null +++ b/src/components/javigaralva/hooks/use-password-generator.tsx @@ -0,0 +1,41 @@ +import { useState, useEffect } from 'react' +import { DEFAULT_PASSWORD_OPTIONS } from '../constants/constants' +import { + generatePassword, + replacePasswordCharacterAt +} from '../utils/generate-password/generate-password' + +export function usePasswordGenerator() { + const [passwordLength, setPasswordLength] = useState(DEFAULT_PASSWORD_OPTIONS.length) + const [password, setPassword] = useState('') + + const handleGeneratePassword = () => { + setPassword( + generatePassword({ + ...DEFAULT_PASSWORD_OPTIONS, + length: passwordLength + }) + ) + } + + const handleCharacterReplacement = ({ index }: { index: number }) => { + setPassword(replacePasswordCharacterAt({ password, index, options: DEFAULT_PASSWORD_OPTIONS })) + } + + useEffect(() => { + setPassword( + generatePassword({ + ...DEFAULT_PASSWORD_OPTIONS, + length: passwordLength + }) + ) + }, [passwordLength]) + + return { + passwordLength, + setPasswordLength, + password, + generatePassword: handleGeneratePassword, + replaceCharacterAt: handleCharacterReplacement + } +} diff --git a/src/components/javigaralva/password-generator-app.tsx b/src/components/javigaralva/password-generator-app.tsx index 9320128d3..ce6b673d4 100644 --- a/src/components/javigaralva/password-generator-app.tsx +++ b/src/components/javigaralva/password-generator-app.tsx @@ -1,113 +1,33 @@ -import { useState, useEffect, type ChangeEventHandler } from 'react' -import { sample } from './utils/sample' +import { Button } from './components/button' import { ColorizedPassword } from './components/colorized-password' -import { - UPPER_CASE_LETTERS_LIST, - LOWER_CASE_LETTERS_LIST, - NUMBERS_LIST, - SYMBOLS_LIST -} from './constants/constants' - -interface GeneratePasswordOptions { - length: number - hasUpperCaseLetters: boolean - hasLowerCaseLetters: boolean - hasNumbers: boolean - hasSymbols: boolean -} -const MIN_PASSWORD_LENGTH = 1 -const MAX_PASSWORD_LENGTH = 30 -const DEFAULT_PASSWORD_OPTIONS: GeneratePasswordOptions = { - length: 10, - hasUpperCaseLetters: true, - hasLowerCaseLetters: true, - hasNumbers: true, - hasSymbols: true -} - -function generatePassword(options: GeneratePasswordOptions) { - return Array.from({ length: options.length }).reduce( - (acc: string) => acc + getRandomCharacter(options), - '' - ) -} - -function getRandomCharacter(options: GeneratePasswordOptions) { - return getRandomCharacterPickerFunction(options)?.() ?? '' -} - -function getRandomCharacterPickerFunction(options: GeneratePasswordOptions) { - const choosableStrategy = getChoosableStrategies(options) - return sample(choosableStrategy) -} - -function getChoosableStrategies(options: GeneratePasswordOptions) { - return [ - options.hasUpperCaseLetters ? sampleUpperCaseLetter : undefined, - options.hasLowerCaseLetters ? sampleLowerCaseLetter : undefined, - options.hasNumbers ? sampleNumber : undefined, - options.hasSymbols ? sampleSymbol : undefined - ].filter(Boolean) -} - -const sampleUpperCaseLetter = makeSampleCharacterFunction(UPPER_CASE_LETTERS_LIST) -const sampleLowerCaseLetter = makeSampleCharacterFunction(LOWER_CASE_LETTERS_LIST) -const sampleNumber = makeSampleCharacterFunction(NUMBERS_LIST) -const sampleSymbol = makeSampleCharacterFunction(SYMBOLS_LIST) - -function makeSampleCharacterFunction(characters: string[]) { - return () => sample(characters) -} +import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH } from './constants/constants' +import { usePasswordGenerator } from './hooks/use-password-generator' export default function Main() { - const [passwordLength, setPasswordLength] = useState(DEFAULT_PASSWORD_OPTIONS.length) - const [password, setPassword] = useState('') + const { passwordLength, setPasswordLength, password, generatePassword, replaceCharacterAt } = + usePasswordGenerator() - const handleGeneratePassword = () => { - setPassword( - generatePassword({ - ...DEFAULT_PASSWORD_OPTIONS, - length: passwordLength - }) - ) + const handleCopy = () => { + navigator.clipboard.writeText(password) } - const handleOnChangeSlider: ChangeEventHandler = (e) => { - setPasswordLength(Number(e.target.value)) - } - - useEffect(() => { - setPassword( - generatePassword({ - ...DEFAULT_PASSWORD_OPTIONS, - length: passwordLength - }) - ) - }, [passwordLength]) - return ( -
+
- +
- - + + +
+ setPasswordLength(Number(e.target.value))} />
@@ -117,3 +37,39 @@ export default function Main() {
) } + +function PasswordLengthSlider({ passwordLength, onChange }) { + return ( + + ) +} +function GenerateButton({ onClick }) { + return ( + + ) +} + +function CopyButton({ onClick }) { + return ( + + ) +} diff --git a/src/components/javigaralva/utils/generate-password/generate-password.ts b/src/components/javigaralva/utils/generate-password/generate-password.ts new file mode 100644 index 000000000..a09df6ba0 --- /dev/null +++ b/src/components/javigaralva/utils/generate-password/generate-password.ts @@ -0,0 +1,62 @@ +import { sample } from '../sample' +import { + UPPER_CASE_LETTERS_LIST, + LOWER_CASE_LETTERS_LIST, + NUMBERS_LIST, + SYMBOLS_LIST +} from '../../constants/constants' + +export interface GeneratePasswordOptions { + length: number + hasUpperCaseLetters: boolean + hasLowerCaseLetters: boolean + hasNumbers: boolean + hasSymbols: boolean +} +export function generatePassword(options: GeneratePasswordOptions) { + return Array.from({ length: options.length }).reduce( + (acc: string) => acc + getRandomCharacter(options), + '' + ) +} +interface ReplacePasswordCharacterAtParams { + password: string + index: number + options: GeneratePasswordOptions +} +export function replacePasswordCharacterAt({ + password, + index, + options +}: ReplacePasswordCharacterAtParams): string { + const areParamsValid = index >= 0 && index <= password.length - 1 + if (!areParamsValid) { + return password + } + + const character = getRandomCharacter(options) + return password.substring(0, index) + character + password.substring(index + character.length) +} +function getRandomCharacter(options: GeneratePasswordOptions) { + return getRandomCharacterPickerFunction(options)?.() ?? '' +} +function getRandomCharacterPickerFunction(options: GeneratePasswordOptions) { + const choosableStrategy = getChoosableStrategies(options) + return sample(choosableStrategy) +} +function getChoosableStrategies(options: GeneratePasswordOptions) { + return [ + options.hasUpperCaseLetters ? sampleUpperCaseLetter : undefined, + options.hasLowerCaseLetters ? sampleLowerCaseLetter : undefined, + options.hasNumbers ? sampleNumber : undefined, + options.hasSymbols ? sampleSymbol : undefined + ].filter(Boolean) +} +const sampleUpperCaseLetter = makeSampleCharacterFunction(UPPER_CASE_LETTERS_LIST) +const sampleLowerCaseLetter = makeSampleCharacterFunction(LOWER_CASE_LETTERS_LIST) +const sampleNumber = makeSampleCharacterFunction(NUMBERS_LIST) +const sampleSymbol = makeSampleCharacterFunction(SYMBOLS_LIST) + +function makeSampleCharacterFunction(characters: string[]) { + return () => sample(characters) +}