Files
OCR/frontend/src/pages/LoginPage.tsx
egg d5bc311757 feat: simplify login page UX and add i18n English support
- Redesign LoginPage with minimal professional style
  - Remove animated gradient backgrounds and floating orbs
  - Remove marketing claims (99% accuracy, enterprise-grade)
  - Center login form with clean card design
- Add multi-language support (zh-TW, en-US)
  - Create LanguageSwitcher component in sidebar
  - Add en-US.json translation file
  - Persist language preference in localStorage
- Remove unused top header bar with search
- Move language switcher to sidebar user section

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 12:49:48 +08:00

162 lines
6.3 KiB
TypeScript

import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useAuthStore } from '@/store/authStore'
import { apiClientV2 } from '@/services/apiV2'
import { Lock, User, LayoutDashboard, AlertCircle, Loader2 } from 'lucide-react'
import LanguageSwitcher from '@/components/LanguageSwitcher'
export default function LoginPage() {
const { t } = useTranslation()
const navigate = useNavigate()
const setUser = useAuthStore((state) => state.setUser)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setLoading(true)
try {
const response = await apiClientV2.login({ username, password })
setUser({
id: response.user.id,
username: response.user.email,
email: response.user.email,
displayName: response.user.display_name
})
navigate('/upload')
} catch (err: any) {
const errorDetail = err.response?.data?.detail
if (Array.isArray(errorDetail)) {
setError(errorDetail.map((e: any) => e.msg || e.message || String(e)).join(', '))
} else if (typeof errorDetail === 'string') {
setError(errorDetail)
} else {
setError(t('auth.loginError'))
}
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-background flex flex-col">
{/* Top bar with language switcher */}
<div className="flex justify-end p-4">
<LanguageSwitcher />
</div>
{/* Centered login form */}
<div className="flex-1 flex items-center justify-center px-4 pb-16">
<div className="w-full max-w-md">
{/* Logo and title */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-xl bg-primary/10 mb-4">
<LayoutDashboard className="w-8 h-8 text-primary" />
</div>
<h1 className="text-2xl font-bold text-foreground">{t('app.title')}</h1>
<p className="text-sm text-muted-foreground mt-1">{t('app.subtitle')}</p>
</div>
{/* Login card */}
<div className="bg-card rounded-xl border border-border p-8 shadow-sm">
<div className="mb-6">
<h2 className="text-xl font-semibold text-foreground">{t('auth.welcomeBack')}</h2>
<p className="text-sm text-muted-foreground mt-1">{t('auth.loginPrompt')}</p>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
{/* Username */}
<div className="space-y-2">
<label htmlFor="username" className="block text-sm font-medium text-foreground">
{t('auth.username')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="h-5 w-5 text-muted-foreground" />
</div>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-background border border-border rounded-lg
text-foreground placeholder-muted-foreground
focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary
transition-colors"
placeholder={t('auth.usernamePlaceholder')}
required
/>
</div>
</div>
{/* Password */}
<div className="space-y-2">
<label htmlFor="password" className="block text-sm font-medium text-foreground">
{t('auth.password')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-muted-foreground" />
</div>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-background border border-border rounded-lg
text-foreground placeholder-muted-foreground
focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary
transition-colors"
placeholder={t('auth.passwordPlaceholder')}
required
/>
</div>
</div>
{/* Error message */}
{error && (
<div className="flex items-start gap-3 p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<AlertCircle className="h-5 w-5 text-destructive flex-shrink-0 mt-0.5" />
<p className="text-sm text-destructive">{error}</p>
</div>
)}
{/* Submit button */}
<button
type="submit"
disabled={loading}
className="w-full py-2.5 px-4 bg-primary text-primary-foreground rounded-lg font-medium
hover:bg-primary/90
focus:outline-none focus:ring-2 focus:ring-primary/50 focus:ring-offset-2
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" />
{t('auth.loggingIn')}
</span>
) : (
t('auth.loginButton')
)}
</button>
</form>
</div>
{/* Supported formats info */}
<p className="text-center text-xs text-muted-foreground mt-6">
{t('auth.supportedFormats')}
</p>
</div>
</div>
</div>
)
}