feat: add identification page
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useLanguage } from '../LanguageContext';
|
||||
import { apiService } from '../services/api';
|
||||
import { Lock, User, Loader2, Languages, ArrowRight, ArrowLeftRight } from 'lucide-react';
|
||||
import type { LoginCredentials } from '../types';
|
||||
|
||||
interface LoginScreenProps {
|
||||
onLoginSuccess: (token: string, username: string) => void;
|
||||
onLoginError: (msg: string) => void;
|
||||
}
|
||||
|
||||
const LoginScreen: React.FC<LoginScreenProps> = ({ onLoginSuccess, onLoginError }) => {
|
||||
const { t, language, setLanguage } = useLanguage();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
const [isLangMenuOpen, setIsLangMenuOpen] = useState(false);
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setLocalError(null);
|
||||
|
||||
try {
|
||||
const creds: LoginCredentials = { username, password };
|
||||
const response = await apiService.login(creds);
|
||||
|
||||
if (response.status === 'success') {
|
||||
onLoginSuccess(response.data.token, response.data.username);
|
||||
} else {
|
||||
const errorMsg = response.message || t('auth.invalidCredentials');
|
||||
setLocalError(errorMsg);
|
||||
onLoginError(errorMsg);
|
||||
}
|
||||
} catch {
|
||||
setLocalError(t('auth.invalidCredentials'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const currentLangLabel =
|
||||
language === 'en' ? 'English'
|
||||
: language === 'es' ? 'Español'
|
||||
: language === 'chs' ? '简体中文'
|
||||
: '繁體中文';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-gray-800 via-gray-900 to-black p-4">
|
||||
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none z-0">
|
||||
<div className="absolute top-0 left-1/4 w-96 h-96 bg-plex-orange/10 rounded-full blur-[100px] opacity-20"></div>
|
||||
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-blue-500/10 rounded-full blur-[100px] opacity-20"></div>
|
||||
</div>
|
||||
|
||||
{/* Language Switcher (Top Right) */}
|
||||
<div className="absolute top-6 right-6 z-20">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsLangMenuOpen(!isLangMenuOpen)}
|
||||
className="flex items-center space-x-2 px-3 py-2 rounded-lg bg-gray-800/50 hover:bg-gray-700/50 border border-gray-700 hover:border-gray-600 text-gray-300 transition-all backdrop-blur-sm"
|
||||
>
|
||||
<Languages size={16} />
|
||||
<span className="text-sm font-medium">{currentLangLabel}</span>
|
||||
</button>
|
||||
|
||||
{isLangMenuOpen && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-10" onClick={() => setIsLangMenuOpen(false)}></div>
|
||||
<div className="absolute right-0 top-full mt-2 w-32 bg-gray-800 border border-gray-700 rounded-lg shadow-xl z-20 overflow-hidden">
|
||||
<button
|
||||
onClick={() => { setLanguage('en'); setIsLangMenuOpen(false); }}
|
||||
className={`w-full px-4 py-2 text-sm text-left hover:bg-gray-700 transition-colors ${language === 'en' ? 'text-plex-orange font-bold' : 'text-gray-300'}`}
|
||||
>
|
||||
English
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setLanguage('es'); setIsLangMenuOpen(false); }}
|
||||
className={`w-full px-4 py-2 text-sm text-left hover:bg-gray-700 transition-colors ${language === 'es' ? 'text-plex-orange font-bold' : 'text-gray-300'}`}
|
||||
>
|
||||
Español
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setLanguage('chs'); setIsLangMenuOpen(false); }}
|
||||
className={`w-full px-4 py-2 text-sm text-left hover:bg-gray-700 transition-colors ${language === 'chs' ? 'text-plex-orange font-bold' : 'text-gray-300'}`}
|
||||
>
|
||||
简体中文
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setLanguage('cht'); setIsLangMenuOpen(false); }}
|
||||
className={`w-full px-4 py-2 text-sm text-left hover:bg-gray-700 transition-colors ${language === 'cht' ? 'text-plex-orange font-bold' : 'text-gray-300'}`}
|
||||
>
|
||||
繁體中文
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Card */}
|
||||
<div className="w-full max-w-md bg-gray-900/60 backdrop-blur-xl border border-gray-700/50 rounded-2xl shadow-2xl p-8 z-10">
|
||||
<div className="text-center mb-8 flex flex-col items-center">
|
||||
<div className="inline-flex items-center justify-center p-3 rounded-xl bg-gradient-to-br from-plex-orange to-yellow-600 shadow-lg shadow-plex-orange/20 mb-4">
|
||||
<ArrowLeftRight size={32} strokeWidth={2.5} className="text-gray-900" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-white">
|
||||
<span className="text-plex-orange">PMS</span> Playlist Sync
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
{localError && (
|
||||
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-xs flex items-center justify-center">
|
||||
{localError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">{t('auth.username')}</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<User size={16} className="text-gray-500 group-focus-within:text-plex-orange transition-colors" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full h-11 pl-10 pr-4 bg-gray-800/50 border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:border-plex-orange focus:ring-1 focus:ring-plex-orange focus:outline-none transition-all"
|
||||
placeholder="username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">{t('auth.password')}</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Lock size={16} className="text-gray-500 group-focus-within:text-plex-orange transition-colors" />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full h-11 pl-10 pr-4 bg-gray-800/50 border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:border-plex-orange focus:ring-1 focus:ring-plex-orange focus:outline-none transition-all"
|
||||
placeholder="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !username || !password}
|
||||
className={`w-full h-12 mt-6 rounded-lg text-sm font-bold flex items-center justify-center gap-2 transition-all shadow-lg
|
||||
${isLoading
|
||||
? 'bg-gray-700 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-plex-orange text-gray-900 hover:bg-yellow-500 active:scale-[0.98]'
|
||||
}`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 size={18} className="animate-spin" />
|
||||
<span>{t('auth.loggingIn')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{t('auth.loginBtn')}</span>
|
||||
<ArrowRight size={18} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-gray-700/50 text-center">
|
||||
<p className="text-[10px] text-gray-600">© PMS Playlist Sync</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginScreen;
|
||||
Reference in New Issue
Block a user