All files / src/pages LoginPage.tsx

97.14% Statements 34/35
93.33% Branches 28/30
100% Functions 8/8
96.96% Lines 32/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118                        28x 28x 28x 28x 28x 28x 28x 28x 28x   28x 2x 2x 2x 1x 1x   1x 1x 1x 1x     1x   1x       28x 28x 67x     28x   27x                   54x             1x 1x                             10x                   8x                     3x                              
import { useState } from 'react'
import type { SubmitEventHandler } from 'react'
import { Navigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useAuth } from '../auth'
import { Button } from '../components/Button'
import { PasswordInput } from '../components/PasswordInput'
import { usePressPulse } from '../usePressPulse'
 
type Tab = 'login' | 'register'
 
export function LoginPage() {
	const { t } = useTranslation()
	const [tab, setTab] = useState<Tab>('login')
	const [username, setUsername] = useState('')
	const [password, setPassword] = useState('')
	const [repeatPassword, setRepeatPassword] = useState('')
	const [error, setError] = useState('')
	const [loading, setLoading] = useState(false)
	const { token, signIn, register } = useAuth()
	const [submitRef, pulseSubmit] = usePressPulse<HTMLButtonElement>()
 
	const handleSubmit: SubmitEventHandler<HTMLFormElement> = async (e) => {
		e.preventDefault()
		pulseSubmit()
		if (tab === 'register' && password !== repeatPassword) {
			setError(t('login.passwordsNoMatch'))
			return
		}
		setError('')
		setLoading(true)
		try {
			if (tab === 'login') await signIn(username, password)
			else Eawait register(username, password)
		} catch (e) {
			setError(e instanceof Error ? e.message : String(e))
		} finally {
			setLoading(false)
		}
	}
 
	const passwordMismatch = error === t('login.passwordsNoMatch')
	const inputClass = (invalid: boolean) =>
		`w-full border rounded p-2 ${invalid ? 'border-red-500' : 'border-gray-300 dark:border-neutral-600'}`
 
	// already signed in -> skip the login page
	if (token) return <Navigate to="/generate" replace />
 
	return (
		<main className="mx-auto flex max-w-2xl flex-col gap-4 p-6 animate-fade-in">
			<h1 className="text-2xl font-bold">{t('layout.generate')}</h1>
			<div className="relative inline-flex self-start rounded-lg bg-gray-100 dark:bg-neutral-700 p-1">
				<span
					className={`pointer-events-none absolute inset-y-1 left-1 w-24 rounded-md bg-white dark:bg-neutral-800 shadow-sm transition-transform duration-200 ease-out ${
						tab === 'register' ? 'translate-x-full' : 'translate-x-0'
					}`}
				/>
				{(['login', 'register'] as const).map((value) => (
					<button
						key={value}
						type="button"
						className={`relative z-10 w-24 rounded-md py-1 text-sm font-medium transition-colors ${
							tab === value ? 'text-orange-600 dark:text-orange-400' : 'text-gray-500 dark:text-neutral-400'
						}`}
						onClick={() => {
							setTab(value)
							setError('')
						}}
					>
						{value === 'login' ? t('login.login') : t('login.register')}
					</button>
				))}
			</div>
 
			<form className="flex flex-col gap-4 max-w-sm" onSubmit={handleSubmit}>
				<label className="flex flex-col gap-1">
					<span className="font-medium">{t('login.username')}</span>
					<input
						type="text"
						className={inputClass(!!error && !passwordMismatch)}
						value={username}
						onChange={(e) => setUsername(e.target.value)}
						autoComplete="username"
					/>
				</label>
 
				<label className="flex flex-col gap-1">
					<span className="font-medium">{t('login.password')}</span>
					<PasswordInput
						className={inputClass(!!error)}
						value={password}
						onChange={(e) => setPassword(e.target.value)}
						autoComplete={tab === 'login' ? 'current-password' : 'new-password'}
					/>
				</label>
 
				{tab === 'register' && (
					<label className="flex flex-col gap-1">
						<span className="font-medium">{t('login.repeatPassword')}</span>
						<PasswordInput
							className={inputClass(passwordMismatch)}
							value={repeatPassword}
							onChange={(e) => setRepeatPassword(e.target.value)}
							autoComplete="new-password"
						/>
					</label>
				)}
 
				<Button ref={submitRef} type="submit" className="self-start" disabled={loading}>
					{loading ? t('login.pleaseWait') : tab === 'login' ? t('login.login') : t('login.signup')}
				</Button>
 
				{error && <p className="text-red-600 dark:text-red-400">{error}</p>}
			</form>
		</main>
	)
}