e3d3df9ecb
feat: Implement user authentication and login screen Merge commit 'a14210c458d5f6c6a4875ca8228db63c0b73cf75'
171 lines
7.5 KiB
TypeScript
171 lines
7.5 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
|
import { useLanguage } from '../LanguageContext';
|
|
import { apiService } from '../services/api';
|
|
import { Lock, User, Loader2, Languages, ArrowRight, ArrowLeftRight } from 'lucide-react';
|
|
|
|
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 {
|
|
// Mock credentials: admin / password
|
|
const response = await apiService.login({ username, password });
|
|
|
|
if (response.status === 'success') {
|
|
onLoginSuccess(response.data.token, response.data.username);
|
|
} else {
|
|
const errorMsg = response.message || t('auth.invalidCredentials');
|
|
setLocalError(errorMsg);
|
|
onLoginError(errorMsg);
|
|
}
|
|
} catch (err) {
|
|
setLocalError(t('auth.invalidCredentials'));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
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">
|
|
|
|
{/* Background decoration */}
|
|
<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">{language === 'en' ? 'English' : 'Español'}</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 animate-in fade-in slide-in-from-top-2">
|
|
<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>
|
|
</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 animate-in zoom-in-95 duration-300">
|
|
|
|
<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="admin"
|
|
/>
|
|
</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 hover:shadow-plex-orange/30 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;
|