- All frontend pages, labels, notices and errors; html lang=uk, uk-UA money format - Brand "Assistant System" in the top bar and page title - Backend error details returned to the UI (auth, orders, receipts, cash registers, Checkbox/CRM/NP errors) and CLI output - Tests updated for the new messages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
4.4 KiB
TypeScript
126 lines
4.4 KiB
TypeScript
import { useId, useState } from 'react'
|
||
import type { FormEvent } from 'react'
|
||
import { Navigate, useLocation } from 'react-router-dom'
|
||
|
||
import { ApiError } from '@/api/client'
|
||
import '@/features/auth/LoginPage.css'
|
||
import { useAuth } from '@/features/auth/useAuth'
|
||
|
||
interface LocationState {
|
||
from?: { pathname: string }
|
||
}
|
||
|
||
export function LoginPage() {
|
||
const { status, login } = useAuth()
|
||
const location = useLocation()
|
||
|
||
const emailId = useId()
|
||
const passwordId = useId()
|
||
|
||
const [email, setEmail] = useState('')
|
||
const [password, setPassword] = useState('')
|
||
const [showPassword, setShowPassword] = useState(false)
|
||
const [submitting, setSubmitting] = useState(false)
|
||
const [formError, setFormError] = useState<string | null>(null)
|
||
|
||
// Уже вошли — на странице входа делать нечего.
|
||
if (status === 'authenticated') {
|
||
const state = location.state as LocationState | null
|
||
const redirectTo = state?.from?.pathname ?? '/'
|
||
return <Navigate to={redirectTo} replace />
|
||
}
|
||
|
||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||
event.preventDefault()
|
||
setFormError(null)
|
||
setSubmitting(true)
|
||
try {
|
||
await login(email, password)
|
||
} catch (err) {
|
||
const message = err instanceof ApiError ? err.message : 'Не вдалося підключитися до сервера'
|
||
setFormError(message)
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="login-page">
|
||
<div className="login-backdrop" aria-hidden="true">
|
||
<span className="login-blob login-blob--a" />
|
||
<span className="login-blob login-blob--b" />
|
||
<span className="login-blob login-blob--c" />
|
||
</div>
|
||
|
||
<div className="login-card">
|
||
<div className="login-header">
|
||
<div className="login-logo" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<path d="M12 3l7 4v5c0 4.5-3 8-7 9-4-1-7-4.5-7-9V7l7-4z" />
|
||
<path d="M9 12l2 2 4-4" />
|
||
</svg>
|
||
</div>
|
||
<h1>Assistant System</h1>
|
||
<p>Вхід у систему</p>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} noValidate>
|
||
{formError && (
|
||
<div className="login-error" role="alert">
|
||
{formError}
|
||
</div>
|
||
)}
|
||
|
||
<div className="login-field">
|
||
<label htmlFor={emailId}>Email</label>
|
||
<input
|
||
id={emailId}
|
||
type="email"
|
||
autoComplete="username"
|
||
required
|
||
value={email}
|
||
disabled={submitting}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
placeholder="you@example.com"
|
||
/>
|
||
</div>
|
||
|
||
<div className="login-field">
|
||
<label htmlFor={passwordId}>Пароль</label>
|
||
<div className="login-password">
|
||
<input
|
||
id={passwordId}
|
||
type={showPassword ? 'text' : 'password'}
|
||
autoComplete="current-password"
|
||
required
|
||
value={password}
|
||
disabled={submitting}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
placeholder="••••••••"
|
||
/>
|
||
<button
|
||
type="button"
|
||
className="login-toggle"
|
||
onClick={() => setShowPassword((v) => !v)}
|
||
aria-label={showPassword ? 'Приховати пароль' : 'Показати пароль'}
|
||
aria-pressed={showPassword}
|
||
>
|
||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z" />
|
||
<circle cx="12" cy="12" r="3" />
|
||
{showPassword && <path d="M3 3l18 18" />}
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" className="login-submit" disabled={submitting}>
|
||
{submitting && <span className="spinner" aria-hidden="true" />}
|
||
{submitting ? 'Входимо…' : 'Увійти'}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|