- 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>
162 lines
6.3 KiB
TypeScript
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>
|
|
)
|
|
}
|