File size: 6,944 Bytes
8d3471e | 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 117 118 119 120 121 122 123 124 125 126 127 128 | import { useState } from 'react'
import { Key, ArrowRight, ShieldCheck, Lock, Check } from 'lucide-react'
import clsx from 'clsx'
import { useI18n } from '../i18n'
import LanguageToggle from './LanguageToggle'
export default function Login({ onLogin, onMessage }) {
const { t } = useI18n()
const [adminKey, setAdminKey] = useState('')
const [loading, setLoading] = useState(false)
const [remember, setRemember] = useState(true)
const handleLogin = async (e) => {
e.preventDefault()
if (!adminKey.trim()) return
setLoading(true)
try {
const res = await fetch('/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ admin_key: adminKey }),
})
const data = await res.json()
if (res.ok && data.success) {
const storage = remember ? localStorage : sessionStorage
storage.setItem('ds2api_token', data.token)
storage.setItem('ds2api_token_expires', Date.now() + data.expires_in * 1000)
onLogin(data.token)
if (data.message) {
onMessage('warning', data.message)
}
} else {
onMessage('error', data.detail || t('login.signInFailed'))
}
} catch (e) {
onMessage('error', t('login.networkError', { error: e.message }))
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen w-full flex flex-col items-center justify-center p-4 bg-background text-foreground">
<div className="absolute top-6 right-6">
<LanguageToggle />
</div>
<div className="w-full max-w-[400px] relative z-10 animate-in fade-in zoom-in-95 duration-200">
<div className="w-full bg-card border border-border rounded-xl p-8 shadow-sm">
<div className="text-center space-y-2 mb-8 animate-in fade-in slide-in-from-top-4 duration-500">
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mb-2">
<Lock className="w-6 h-6" />
</div>
<h1 className="text-3xl font-bold tracking-tight text-foreground">{t('login.welcome')}</h1>
<p className="text-sm text-muted-foreground/80">{t('login.subtitle')}</p>
</div>
<form onSubmit={handleLogin} className="space-y-5 animate-in fade-in slide-in-from-bottom-4 duration-700 delay-150">
<div className="space-y-2">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-widest ml-1">{t('login.adminKeyLabel')}</label>
<div className="relative group">
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-muted-foreground group-focus-within:text-primary transition-colors">
<Key className="w-4 h-4" />
</div>
<input
type="password"
className="w-full bg-[#09090b] border border-border rounded-xl pl-10 pr-4 py-3 text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all placeholder:text-muted-foreground/30 text-foreground"
placeholder={t('login.adminKeyPlaceholder')}
value={adminKey}
onChange={e => setAdminKey(e.target.value)}
autoFocus
/>
</div>
</div>
<div className="flex items-center justify-between px-1">
<label className="flex items-center gap-2.5 cursor-pointer group">
<div className="relative flex items-center">
<input
type="checkbox"
className="peer sr-only"
checked={remember}
onChange={e => setRemember(e.target.checked)}
/>
<div className="w-[18px] h-[18px] bg-secondary border border-border rounded-md peer-checked:bg-primary peer-checked:border-primary transition-all shadow-sm"></div>
<Check className="absolute inset-0 m-auto w-3 h-3 text-primary-foreground opacity-0 peer-checked:opacity-100 transition-opacity stroke-[3]" />
</div>
<span className="text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors">{t('login.rememberSession')}</span>
</label>
</div>
<button
type="submit"
disabled={loading}
className="w-full h-12 flex items-center justify-center gap-2 bg-primary text-primary-foreground rounded-xl hover:bg-primary/90 transition-all font-semibold text-sm shadow-lg shadow-primary/20 hover:shadow-primary/30 disabled:opacity-50 disabled:shadow-none"
>
{loading ? (
<div className="w-5 h-5 border-2 border-primary-foreground/30 border-t-primary-foreground rounded-full animate-spin" />
) : (
<div className="flex items-center gap-2">
<span>{t('login.signIn')}</span>
<ArrowRight className="w-4 h-4" />
</div>
)}
</button>
</form>
<div className="mt-6 pt-6 border-t border-border flex justify-center">
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground/60 font-medium tracking-wide uppercase">
<ShieldCheck className="w-3 h-3" />
<span>{t('login.secureConnection')}</span>
</div>
</div>
</div>
<div className="mt-8 text-center">
<p className="text-[10px] text-muted-foreground/30 font-mono text-center">{t('login.adminPortal')}</p>
</div>
</div>
</div>
)
}
|