fpl-solver / frontend /src /components /LoginModal.jsx
AnayShukla's picture
updates
418395e
import React, { useState, useContext } from 'react';
import { X, Mail, Lock, Loader2, Shield, Eye, EyeOff } from 'lucide-react';
import { PlayerContext } from '../PlayerContext';
import { GoogleLogin } from '@react-oauth/google';
export default function LoginModal({ isOpen, onClose }) {
const { setIsLoggedIn, setUserProfile, setHasGuestMadeEdits } = useContext(PlayerContext);
const [isSignUp, setIsSignUp] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [isSuccess, setIsSuccess] = useState(false);
if (!isOpen) return null;
const toggleMode = () => {
setIsSignUp(!isSignUp);
setError('');
setPassword('');
setConfirmPassword('');
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
setError('');
// Reconfirmation Validation for Sign Up
if (isSignUp && password !== confirmPassword) {
setError('Passwords do not match!');
setIsLoading(false);
return;
}
const endpoint = isSignUp ? '/api/auth/register' : '/api/auth/login';
try {
const res = await fetch(`https://anayshukla-fpl-solver.hf.space${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.detail || 'Authentication failed');
}
// Success! Save token and update Global Context
localStorage.setItem('fpl_token', data.access_token);
setUserProfile({
username: data.email.split('@')[0],
defaultTeamId: null,
isAdmin: data.is_admin
});
setIsLoggedIn(true);
setHasGuestMadeEdits(false);
setIsSuccess(true);
setTimeout(() => onClose(), 100);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
return (
<div className="fixed inset-0 z-modal flex items-center justify-center bg-black/80 backdrop-blur-sm p-3 sm:p-4">
<div className="relative bg-slate-950 border border-slate-800 w-full max-w-md rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200 max-h-[90vh] overflow-y-auto custom-scrollbar">
{/* Header */}
<div className="bg-slate-900 p-5 flex justify-between items-center border-b border-slate-800">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-luigi-500/20 text-luigi-400 rounded-lg flex items-center justify-center">
<Shield size={18} />
</div>
<h2 className="text-xl font-black text-slate-100">
{isSignUp ? 'Create Account' : 'Welcome Back'}
</h2>
</div>
<button onClick={onClose} className="text-slate-500 hover:text-white transition-colors bg-slate-950 p-1.5 rounded-full border border-slate-800">
<X size={18} />
</button>
</div>
{/* Body */}
<div className="p-6">
{error && (
<div className="mb-4 p-3 bg-red-950/30 border border-red-900/50 text-red-400 text-sm rounded-lg text-center font-bold">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
{/* Email Field */}
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
type="email"
required
placeholder="Email Address"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-xl py-3 pl-10 pr-4 text-sm text-slate-200 focus:outline-none focus:border-luigi-400 transition-colors"
/>
</div>
{/* Password Field */}
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
type={showPassword ? "text" : "password"}
required
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-xl py-3 pl-10 pr-10 text-sm text-slate-200 focus:outline-none focus:border-luigi-400 transition-colors"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-500 hover:text-slate-300 transition-colors"
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
{/* Confirm Password Field (Only for Sign Up) */}
{isSignUp && (
<div className="relative animate-in slide-in-from-top-2 fade-in duration-200">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" size={16} />
<input
type={showConfirmPassword ? "text" : "password"}
required
placeholder="Confirm Password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className={`w-full bg-slate-900 border rounded-xl py-3 pl-10 pr-10 text-sm text-slate-200 focus:outline-none transition-colors ${
confirmPassword && password !== confirmPassword
? "border-red-500/50 focus:border-red-500"
: "border-slate-700 focus:border-luigi-400"
}`}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-500 hover:text-slate-300 transition-colors"
>
{showConfirmPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
)}
<button
type="submit"
disabled={isLoading}
className="w-full bg-luigi-500 hover:bg-luigi-400 text-slate-950 py-3 rounded-xl font-bold text-sm transition-colors shadow-lg shadow-luigi-500/20 flex justify-center items-center mt-2"
>
{isLoading ? <Loader2 size={18} className="animate-spin" /> : (isSignUp ? 'Create Account' : 'Log In')}
</button>
</form>
{/* Toggle Login/Signup Mode */}
<div className="mt-6 flex items-center justify-between text-sm text-slate-500">
<span>{isSignUp ? 'Already have an account?' : "Don't have an account?"}</span>
<button
onClick={toggleMode}
className="text-luigi-400 font-bold hover:underline"
>
{isSignUp ? 'Log In' : 'Sign Up'}
</button>
</div>
{/* Elegant "OR" Divider */}
<div className="mt-6 mb-2 relative flex items-center justify-center">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-800"></div>
</div>
<div className="relative px-4 bg-slate-950 text-xs font-bold text-slate-500 uppercase tracking-widest">
OR
</div>
</div>
{/* Google Login Block */}
<div className="mt-4 flex justify-center">
<GoogleLogin
text={isSignUp ? "signup_with" : "signin_with"}
onSuccess={async (credentialResponse) => {
try {
const res = await fetch('https://anayshukla-fpl-solver.hf.space/api/auth/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: credentialResponse.credential })
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || "Google Auth Failed");
localStorage.setItem('fpl_token', data.access_token);
setUserProfile({
username: data.email.split('@')[0],
defaultTeamId: null,
isAdmin: data.is_admin
});
setIsLoggedIn(true);
setHasGuestMadeEdits(false);
setIsSuccess(true);
setTimeout(() => onClose(), 100);
} catch (err) {
setError(err.message);
}
}}
onError={() => {
setError('Google Login window closed or failed.');
}}
theme="filled_black"
shape="pill"
/>
</div>
</div>
</div>
{isSuccess && (
<div className="fixed inset-0 bg-slate-950 flex flex-col items-center justify-center gap-3 z-critical">
<div className="w-10 h-10 border-4 border-slate-800 border-t-luigi-500 rounded-full animate-spin shadow-[0_0_15px_rgba(16,185,129,0.5)]" />
<p className="text-luigi-400 font-bold tracking-widest uppercase text-xs animate-pulse">
Entering Mansion...
</p>
</div>
)}
</div>
);
}