Files
ui/src/features/login/LoginPage.tsx
T
Oleksandr Bezdieniezhnykh 23746ec61d [AZ-485] Add Public API barrels + STC-ARCH-01 (F4 close)
Closes architecture baseline finding F4. Every component now exposes
its Public API through `src/<component>/index.ts`; cross-component
imports go through the barrel. `scripts/check-arch-imports.mjs` plus
`STC-ARCH-01` in the static profile enforce the rule; tests in
`tests/architecture_imports.test.ts` cover AC-4/AC-5 + 2 exemption
cases. One F3-pending exemption (`classColors`) is documented in 5
places (barrel, consumer, script, doc, test) to avoid a circular
import.

Phase B cycle 1 batch 1 of 2 (epic AZ-447). Batch 2 is AZ-486
(endpoint builders) — blocked on this commit landing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 10:33:30 +03:00

96 lines
3.3 KiB
TypeScript

import { useState, type FormEvent } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useAuth } from '../../auth'
type UnlockStep = 'idle' | 'authenticating' | 'downloadingKey' | 'decrypting' | 'startingServices' | 'ready'
const STEP_KEYS: Record<UnlockStep, string> = {
idle: '',
authenticating: 'login.authenticating',
downloadingKey: 'login.downloadingKey',
decrypting: 'login.decrypting',
startingServices: 'login.startingServices',
ready: 'login.ready',
}
export default function LoginPage() {
const { t } = useTranslation()
const { login } = useAuth()
const navigate = useNavigate()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [step, setStep] = useState<UnlockStep>('idle')
const runUnlockSequence = async () => {
const steps: UnlockStep[] = ['downloadingKey', 'decrypting', 'startingServices', 'ready']
for (const s of steps) {
setStep(s)
await new Promise(r => setTimeout(r, 600))
}
navigate('/flights')
}
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
setError('')
setStep('authenticating')
try {
await login(email, password)
await runUnlockSequence()
} catch {
setStep('idle')
setError(t('login.error'))
}
}
return (
<div className="flex items-center justify-center h-screen bg-az-bg">
<form onSubmit={handleSubmit} className="bg-az-panel border border-az-border rounded-lg p-6 w-[400px] shadow-2xl">
<h1 className="text-2xl font-bold text-az-orange text-center mb-6 tracking-widest">{t('login.title')}</h1>
{step !== 'idle' && (
<div className="mb-4 text-center">
<div className="inline-block animate-spin rounded-full h-6 w-6 border-2 border-az-orange border-t-transparent mb-2" />
<div className="text-sm text-az-text">{t(STEP_KEYS[step])}</div>
</div>
)}
{step === 'idle' && (
<>
<div className="mb-3">
<label className="block text-xs text-az-muted mb-1">{t('login.email')}</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
className="w-full bg-az-bg border border-az-border rounded px-3 py-2 text-az-text outline-none focus:border-az-orange"
required
autoFocus
/>
</div>
<div className="mb-4">
<label className="block text-xs text-az-muted mb-1">{t('login.password')}</label>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
className="w-full bg-az-bg border border-az-border rounded px-3 py-2 text-az-text outline-none focus:border-az-orange"
required
/>
</div>
{error && <div className="text-az-red text-sm mb-3">{error}</div>}
<button
type="submit"
className="w-full bg-az-orange text-white font-semibold py-2 rounded hover:bg-orange-600 transition"
>
{t('login.submit')}
</button>
</>
)}
</form>
</div>
)
}