Frontend: - replace the i18next stack with a small typed i18n module - hero clouds removed, llms.txt, HUD widget ARIA fixes and tests Mod: - TargetESP constellation mode; tracker and colors extracted - JumpCircles: 7 new shapes, shared animation and color pipeline - KillEffect blood mode with Burst/Fountain/Ring/Spray styles - main menu: menuLayout setting (orbs/hero) and hero screen - Simple Voice Chat soft integration: VoiceChat module and HUD panel - renderer/quick: static Draw facade with glass outline - vanilla chat: Baritone-style autocomplete popup for the % command prefix
57 lines
1.8 KiB
TypeScript
57 lines
1.8 KiB
TypeScript
import { useState, type FormEvent } from 'react'
|
|
import { useTranslation } from '@shared/i18n/react'
|
|
import { useLocation, useNavigate } from 'react-router'
|
|
import { describeError } from '@shared/api/errors'
|
|
import { Button } from '@shared/ui/Button'
|
|
import { Notice } from '@shared/ui/Notice'
|
|
import { TextField } from '@shared/ui/text-field/TextField'
|
|
import { useSession } from '../session'
|
|
|
|
export function LoginForm() {
|
|
const { t } = useTranslation('auth')
|
|
const { t: tc } = useTranslation('common')
|
|
const { signIn } = useSession()
|
|
const navigate = useNavigate()
|
|
const from = (useLocation().state as { from?: string } | null)?.from ?? '/configs'
|
|
const [email, setEmail] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
const [error, setError] = useState<string>()
|
|
const [busy, setBusy] = useState(false)
|
|
|
|
async function submit(e: FormEvent) {
|
|
e.preventDefault()
|
|
setBusy(true)
|
|
setError(undefined)
|
|
try {
|
|
await signIn(email.trim(), password)
|
|
navigate(from, { replace: true })
|
|
} catch (err) {
|
|
setError(describeError(tc, err, { 401: t('login.wrongCredentials') }))
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={submit} className="flex w-full max-w-sm flex-col gap-4" noValidate>
|
|
<TextField
|
|
label={t('login.emailLabel')}
|
|
type="email"
|
|
autoComplete="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
/>
|
|
<TextField
|
|
label={t('login.passwordLabel')}
|
|
type="password"
|
|
autoComplete="current-password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
/>
|
|
{error ? <Notice tone="error">{error}</Notice> : null}
|
|
<Button type="submit" busy={busy}>
|
|
{t('login.submit')}
|
|
</Button>
|
|
</form>
|
|
)
|
|
}
|