0081f60b06
The sign-in page becomes a plain centred column with the brand mark and a segmented login/register switch, modals lose their last inner boxes, the red-days list collapses every year but the current one, and the retired components' CSS, an unused list component and 23 unreferenced translation keys are removed. ARCHITECTURE.md documents the Almanacka styling conventions and the per-year balance rule. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBRLomdLK9jF7wU7a8AA6H
134 lines
4.6 KiB
TypeScript
134 lines
4.6 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { useAuth } from '@/context/AuthContext'
|
|
import { useWorkspace } from '@/context/WorkspaceContext'
|
|
import { useTranslation } from '@/i18n'
|
|
import { LanguageToggle } from '@/components/layout/LanguageToggle'
|
|
import './auth.css'
|
|
|
|
export function Auth() {
|
|
const { login, register, isAuthenticated } = useAuth()
|
|
const { isLoading: wsLoading } = useWorkspace()
|
|
const { t } = useTranslation()
|
|
const navigate = useNavigate()
|
|
const [isRegister, setIsRegister] = useState(false)
|
|
const [email, setEmail] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
const [displayName, setDisplayName] = useState('')
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (isAuthenticated && !wsLoading) {
|
|
navigate('/', { replace: true })
|
|
}
|
|
}, [isAuthenticated, wsLoading, navigate])
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setError(null)
|
|
try {
|
|
if (isRegister) {
|
|
await register({ email, password, displayName })
|
|
navigate('/', { replace: true })
|
|
} else {
|
|
await login({ email, password })
|
|
navigate('/', { replace: true })
|
|
}
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : t('auth.genericError'))
|
|
}
|
|
}
|
|
|
|
// Switching tabs clears any error from the previous tab's attempt — a
|
|
// failed login's error text should not linger, confusingly, once the
|
|
// person has moved on to creating an account (or back).
|
|
const switchTab = (register: boolean) => {
|
|
setIsRegister(register)
|
|
setError(null)
|
|
}
|
|
|
|
return (
|
|
<main className="auth-page">
|
|
<div className="auth-page__lang">
|
|
<LanguageToggle />
|
|
</div>
|
|
<div className="auth-page__column">
|
|
<img className="auth-page__mark" src="/img/icon-64.png" alt="" width={32} height={32} />
|
|
<h1 className="auth-page__title">{isRegister ? t('auth.createAccountTitle') : t('auth.loginTitle')}</h1>
|
|
<div className="segmented auth-page__tabs" role="group">
|
|
<button
|
|
type="button"
|
|
className="segmented__option auth-page__tab"
|
|
aria-pressed={!isRegister}
|
|
onClick={() => switchTab(false)}
|
|
data-testid="auth-tab-login"
|
|
>
|
|
{t('auth.loginButton')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="segmented__option auth-page__tab"
|
|
aria-pressed={isRegister}
|
|
onClick={() => switchTab(true)}
|
|
data-testid="auth-tab-register"
|
|
>
|
|
{t('auth.createAccountButton')}
|
|
</button>
|
|
</div>
|
|
<form className="auth-page__form" onSubmit={(e) => void handleSubmit(e)}>
|
|
{isRegister && (
|
|
<div className="field">
|
|
<label htmlFor="auth-name" className="field__label">
|
|
{t('auth.nameLabel')}
|
|
</label>
|
|
<input
|
|
id="auth-name"
|
|
className="field__control"
|
|
placeholder={t('auth.namePlaceholder')}
|
|
value={displayName}
|
|
onChange={(e) => setDisplayName(e.target.value)}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="field">
|
|
<label htmlFor="auth-email" className="field__label">
|
|
{t('auth.emailLabel')}
|
|
</label>
|
|
<input
|
|
id="auth-email"
|
|
type="email"
|
|
data-testid="auth-email"
|
|
className="field__control"
|
|
placeholder={t('auth.emailPlaceholder')}
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="field">
|
|
<label htmlFor="auth-password" className="field__label">
|
|
{t('auth.passwordLabel')}
|
|
</label>
|
|
<input
|
|
id="auth-password"
|
|
type="password"
|
|
data-testid="auth-password"
|
|
className="field__control"
|
|
placeholder={t('auth.passwordPlaceholder')}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
/>
|
|
</div>
|
|
{error && (
|
|
<p role="alert" className="auth-page__error">
|
|
{error}
|
|
</p>
|
|
)}
|
|
<button type="submit" className="btn btn--primary btn--block" data-testid="auth-submit">
|
|
{isRegister ? t('auth.registerButton') : t('auth.loginButton')}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</main>
|
|
)
|
|
}
|