Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | import React from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useLoginViewModel } from '@/viewmodels/auth/useLoginViewModel'; import { useAuth } from '@/contexts/AuthContext'; import LanguageSwitcher from '@/components/LanguageSwitcher'; import { useTheme } from '@/contexts/ThemeContext'; import SemLogo from '@/components/SemLogo'; import '@/styles/auth.css'; export default function LoginView() { const { t } = useTranslation(); const vm = useLoginViewModel(); const { setSession } = useAuth(); const { theme } = useTheme(); const navigate = useNavigate(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const session = await vm.handleLogin(); if (session) { setSession(session.access_token, session.refresh_token, session.user.username, session.user.role); navigate('/discover', { replace: true }); } }; return ( <div className="auth-page"> <div className="auth-card"> <div className="auth-card-controls"> <LanguageSwitcher /> </div> <div className="auth-brand"> <SemLogo height={76} color={theme === 'dark' ? '#f9fafb' : '#111827'} /> </div> <h1 className="auth-title">{t('auth.login.title')}</h1> <p className="auth-subtitle">{t('auth.login.subtitle')}</p> {vm.apiError && ( <div className="error-banner" role="alert" aria-live="assertive"> {vm.apiError} </div> )} <form onSubmit={handleSubmit}> <div className="field-group"> <label className="field-label" htmlFor="username"> {t('auth.login.username')} </label> <input id="username" className={`field-input ${vm.errors.username ? 'has-error' : ''}`} type="text" placeholder={t('auth.login.username_placeholder')} value={vm.formData.username} onChange={(e) => vm.updateField('username', e.target.value)} autoComplete="username" disabled={vm.isLoading} aria-invalid={!!vm.errors.username} aria-describedby={vm.errors.username ? 'username-error' : undefined} /> {vm.errors.username && ( <p className="field-error" id="username-error" role="alert"> {vm.errors.username} </p> )} </div> <div className="field-group"> <label className="field-label" htmlFor="password"> {t('auth.login.password')} </label> <input id="password" className={`field-input ${vm.errors.password ? 'has-error' : ''}`} type="password" placeholder={t('auth.login.password_placeholder')} value={vm.formData.password} onChange={(e) => vm.updateField('password', e.target.value)} autoComplete="current-password" disabled={vm.isLoading} aria-invalid={!!vm.errors.password} aria-describedby={vm.errors.password ? 'password-error' : undefined} /> {vm.errors.password && ( <p className="field-error" id="password-error" role="alert"> {vm.errors.password} </p> )} <div style={{ textAlign: 'right', marginTop: '0.5rem', marginBottom: '1rem' }}> <Link to="/forgot-password" className="link" style={{ fontSize: '0.875rem' }}> {t('auth.login.forgot_password')} </Link> </div> </div> <button type="submit" className="btn-primary" disabled={vm.isLoading} > {vm.isLoading ? <span className="spinner" /> : t('auth.login.submit')} </button> </form> <div className="auth-footer"> <span>{t('auth.login.signup_prompt')}</span> <Link to="/register" className="link"> {t('auth.login.signup_link')} </Link> </div> </div> </div> ); } |