mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 11:31:11 +00:00
23746ec61d
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>
96 lines
3.3 KiB
TypeScript
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>
|
|
)
|
|
}
|