Skip to content

Commit

Permalink
feat(misc/password): create password generator
Browse files Browse the repository at this point in the history
  • Loading branch information
mateusfg7 committed Feb 1, 2024
1 parent 1768552 commit 451f104
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 8 deletions.
15 changes: 7 additions & 8 deletions src/app/_components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type Props = {
}
export function Navbar({ isOnDrawer = false }: Props) {
return (
<div className="text-lg p-7 md:p-0 max-h-dvh overflow-y-auto md:max-h-[calc(100vh-9rem)]">
<div className="text-lg p-7 md:p-0 max-h-dvh overflow-y-auto md:max-h-[calc(100vh-9rem)] w-max">
<Section title="Text">
<NavbarItem
isOnDrawer={isOnDrawer}
Expand Down Expand Up @@ -95,18 +95,17 @@ export function Navbar({ isOnDrawer = false }: Props) {
<Section title="Misc">
<NavbarItem
isOnDrawer={isOnDrawer}
disabled
Icon={ArrowLeftRight}
title="Converter"
path="/misc/converter"
Icon={KeyRound}
title="Password Generator"
path="/misc/password"
/>

<NavbarItem
isOnDrawer={isOnDrawer}
disabled
Icon={KeyRound}
title="Password Generator"
path="/misc/password"
Icon={ArrowLeftRight}
title="Converter"
path="/misc/converter"
/>
</Section>
</div>
Expand Down
38 changes: 38 additions & 0 deletions src/app/misc/password/_components/password-display.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
isLowerCasedLetter,
isNumber,
isSpecial,
isUpperCasedLetter
} from '../_lib/char-filters'

type Props = {
password: string
}
export function PasswordDisplay({ password }: Props) {
return (
<div className="flex justify-center flex-wrap border border-border w-full p-4 bg-muted/50 rounded-md">
{password.split('').map((char, i) => {
if (isUpperCasedLetter(char) || isLowerCasedLetter(char))
return (
<span key={i} className="text-neutral-950 dark:text-neutral-300">
{char}
</span>
)
if (isNumber(char))
return (
<span key={i} className="text-red-700 dark:text-red-400">
{char}
</span>
)
if (isSpecial(char))
return (
<span key={i} className="text-blue-700 dark:text-blue-400">
{char}
</span>
)

return <span key={i}>{char}</span>
})}
</div>
)
}
4 changes: 4 additions & 0 deletions src/app/misc/password/_lib/char-filters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const isUpperCasedLetter = (char: string) => /[A-Z]/.test(char)
export const isLowerCasedLetter = (char: string) => /[a-z]/.test(char)
export const isNumber = (char: string) => /[0-9]/.test(char)
export const isSpecial = (char: string) => /[!@#$%&*]/.test(char)
30 changes: 30 additions & 0 deletions src/app/misc/password/_lib/generate-password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getRandomPassword } from './get-random-password'
import { getAlphabet } from './get-alphabet'

export type Options = {
size: number
characters: {
upper: boolean
lower: boolean
numbers: boolean
special: boolean
}
}

export function generatePassword({
size,
characters: { lower, numbers, special, upper }
}: Options) {
const isAllDisabled = !(lower || upper || special || numbers)

const characters = isAllDisabled
? { lower: true, numbers, special, upper }
: { lower, numbers, special, upper }

const alphabet = getAlphabet(characters)

return getRandomPassword({
size,
alphabet
})
}
33 changes: 33 additions & 0 deletions src/app/misc/password/_lib/get-alphabet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { shuffleArray } from '~/shared/lib/shuffle-array'

import {
isLowerCasedLetter,
isNumber,
isSpecial,
isUpperCasedLetter
} from './char-filters'

export function getAlphabet({
lower,
numbers,
special,
upper
}: {
lower: boolean
numbers: boolean
special: boolean
upper: boolean
}) {
const alphabet =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&*'.split(
''
)

return shuffleArray(alphabet).filter(
char =>
(upper && isUpperCasedLetter(char)) ||
(lower && isLowerCasedLetter(char)) ||
(numbers && isNumber(char)) ||
(special && isSpecial(char))
)
}
15 changes: 15 additions & 0 deletions src/app/misc/password/_lib/get-random-password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
type Props = {
size: number
alphabet: string[]
}

export function getRandomPassword({ alphabet, size }: Props): string {
let newPasswordCharsArray: string[] = []

for (let index = 0; index < size; index++) {
const charIndex = Math.floor(Math.random() * alphabet.length)
newPasswordCharsArray.push(alphabet[charIndex])
}

return newPasswordCharsArray.join('')
}
10 changes: 10 additions & 0 deletions src/app/misc/password/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ReactNode } from 'react'
import type { Metadata } from 'next'

export const metadata: Metadata = {
title: 'Password'
}

export default function PasswordLayout({ children }: { children: ReactNode }) {
return children
}
143 changes: 143 additions & 0 deletions src/app/misc/password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
'use client'

import { ChangeEvent, useEffect, useState } from 'react'

import { Button } from '~/shared/components/button'
import { Input } from '~/shared/components/input'

import { Options, generatePassword } from './_lib/generate-password'
import { PasswordDisplay } from './_components/password-display'
import { Slider } from '~/shared/components/slider'
import { Label } from '~/shared/components/label'
import { Minus, Plus, RefreshCw } from 'lucide-react'
import { ToggleGroup, ToggleGroupItem } from '~/shared/components/toggle-group'
import { CopyButton } from '~/shared/components/copy-button'

export default function Page() {
const [passwordLength, setPasswordLength] = useState(20)
const [characters, setCharacters] = useState({
lower: true,
upper: true,
numbers: true,
special: true
})

const defaultOptions: Options = {
size: passwordLength,
characters
}

const [password, setPassword] = useState<string>(
generatePassword(defaultOptions)
)

const handleGeneratePassword = (options?: Options) =>
setPassword(generatePassword(options ?? defaultOptions))

function handleSetPasswordLength(value: number[]) {
if (value[0] < 8 || value[0] > 128) return

setPasswordLength(value[0])
handleGeneratePassword({ ...defaultOptions, size: value[0] })
}

function handleEnabledChars(enabledList: string[]) {
const characters = {
lower: enabledList.includes('lower'),
upper: enabledList.includes('upper'),
numbers: enabledList.includes('numbers'),
special: enabledList.includes('special')
}

const isAllDisabled = !(
characters.lower ||
characters.upper ||
characters.numbers ||
characters.special
)

const enabledCharacters = !isAllDisabled
? characters
: {
lower: true,
upper: false,
numbers: false,
special: false
}

setCharacters(enabledCharacters)
handleGeneratePassword({ ...defaultOptions, characters: enabledCharacters })
}

const enabledCharactersList = [
...(characters.lower ? ['lower'] : []),
...(characters.upper ? ['upper'] : []),
...(characters.numbers ? ['numbers'] : []),
...(characters.special ? ['special'] : [])
]

return (
<div className="space-y-14">
<PasswordDisplay password={password} />
<div className="space-y-7">
<div className="text-center space-y-2">
<Label htmlFor="password-length">
Password length: {passwordLength}
</Label>
<div className="flex gap-2 justify-between">
<Button
variant="ghost"
size="icon"
className="rounded-full"
onClick={() => handleSetPasswordLength([passwordLength - 1])}
>
<Minus size="1em" />
</Button>
<Slider
id="password-length"
className="max-w-md"
value={[passwordLength]}
onValueChange={handleSetPasswordLength}
step={1}
min={8}
max={128}
/>
<Button
variant="ghost"
size="icon"
className="rounded-full"
onClick={() => handleSetPasswordLength([passwordLength + 1])}
>
<Plus size="1em" />
</Button>
</div>
</div>
<div className="text-center space-y-2">
<span>Enabled characters</span>
<ToggleGroup
type="multiple"
variant="outline"
value={enabledCharactersList}
onValueChange={handleEnabledChars}
>
<ToggleGroupItem value="upper">ABC</ToggleGroupItem>
<ToggleGroupItem value="lower">abc</ToggleGroupItem>
<ToggleGroupItem value="numbers">123</ToggleGroupItem>
<ToggleGroupItem value="special">!@$</ToggleGroupItem>
</ToggleGroup>
</div>
</div>
<div className="flex justify-center flex-wrap gap-5">
<CopyButton
text={password}
toastMessage="Password copied to clipboard!"
variant="secondary"
/>
<Button className="space-x-2" onClick={() => handleGeneratePassword()}>
<RefreshCw size="1em" />
<span>New password</span>
</Button>
</div>
</div>
)
}
6 changes: 6 additions & 0 deletions src/shared/lib/shuffle-array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function shuffleArray<T>(array: T[]) {
return array
.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value)
}

0 comments on commit 451f104

Please sign in to comment.