aubm / frontend /src /components /Login.tsx
cesjavi's picture
Feat: Complete Sign In / Sign Up flow with Aubix mascot (Phase 9)
89a451f
import React, { useState } from 'react';
import { supabase } from '../services/supabase';
import { LogIn, Mail, Lock, UserPlus, ArrowLeft, RefreshCw, CheckCircle2 } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import AubixIcon from './AubixIcon';
const Login: React.FC = () => {
const [isSignUp, setIsSignUp] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [fullName, setFullName] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) setError(error.message);
setLoading(false);
};
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
setSuccess(null);
const { error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
full_name: fullName,
}
}
});
if (error) {
setError(error.message);
} else {
setSuccess('Account created! You can now sign in.');
setIsSignUp(false);
setFullName('');
}
setLoading(false);
};
return (
<div className="login-screen" style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'radial-gradient(circle at center, #1a1a2e 0%, #0d0d14 100%)',
padding: 'var(--space-md)'
}}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="glass-panel login-panel"
style={{ width: '100%', maxWidth: '440px', padding: 'var(--space-xl)', textAlign: 'center' }}
>
<div style={{ marginBottom: 'var(--space-xl)' }}>
<AubixIcon size={120} />
<h1 style={{ fontSize: '2.5rem', marginBottom: 'var(--space-xs)', fontWeight: 800 }}>
{isSignUp ? 'Join Aubm' : 'Welcome'}
</h1>
<p style={{ color: 'var(--text-dim)' }}>
{isSignUp ? 'Create your agent operator profile' : 'Access the Aubm Orchestrator'}
</p>
</div>
{success && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
style={{ padding: 'var(--space-md)', background: 'rgba(16, 185, 129, 0.1)', border: '1px solid var(--success)', borderRadius: 'var(--radius-md)', color: 'var(--success)', marginBottom: 'var(--space-md)', display: 'flex', alignItems: 'center', gap: 'var(--space-sm)' }}
>
<CheckCircle2 size={18} />
{success}
</motion.div>
)}
<form onSubmit={isSignUp ? handleSignUp : handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-md)' }}>
<AnimatePresence mode="wait">
{isSignUp && (
<motion.div
key="signup-fields"
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
style={{ overflow: 'hidden' }}
>
<div style={{ position: 'relative', marginBottom: 'var(--space-md)' }}>
<UserPlus size={18} style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
<input
type="text"
placeholder="Full Name"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
required={isSignUp}
style={{
width: '100%',
padding: '0.8rem 1rem 0.8rem 2.5rem',
background: 'rgba(255,255,255,0.05)',
border: '1px solid var(--glass-border)',
borderRadius: 'var(--radius-md)',
color: 'white',
outline: 'none'
}}
/>
</div>
</motion.div>
)}
</AnimatePresence>
<div style={{ position: 'relative' }}>
<Mail size={18} style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
<input
type="email"
placeholder="Email Address"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
style={{
width: '100%',
padding: '0.8rem 1rem 0.8rem 2.5rem',
background: 'rgba(255,255,255,0.05)',
border: '1px solid var(--glass-border)',
borderRadius: 'var(--radius-md)',
color: 'white',
outline: 'none'
}}
/>
</div>
<div style={{ position: 'relative' }}>
<Lock size={18} style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
style={{
width: '100%',
padding: '0.8rem 1rem 0.8rem 2.5rem',
background: 'rgba(255,255,255,0.05)',
border: '1px solid var(--glass-border)',
borderRadius: 'var(--radius-md)',
color: 'white',
outline: 'none'
}}
/>
</div>
{error && <div style={{ color: 'var(--danger)', fontSize: '0.85rem' }}>{error}</div>}
<button
type="submit"
className="btn btn-primary"
disabled={loading}
style={{ padding: '0.85rem', marginTop: 'var(--space-sm)' }}
>
{loading ? <RefreshCw className="spin" size={18} /> : (isSignUp ? <UserPlus size={18} /> : <LogIn size={18} />)}
{loading ? (isSignUp ? 'Creating...' : 'Authenticating...') : (isSignUp ? 'Create Account' : 'Sign In')}
</button>
</form>
<div style={{ marginTop: 'var(--space-xl)', paddingTop: 'var(--space-lg)', borderTop: '1px solid var(--glass-border)' }}>
<button
type="button"
className="btn btn-glass"
onClick={() => {
setIsSignUp(!isSignUp);
setError(null);
setSuccess(null);
}}
style={{ width: '100%' }}
>
{isSignUp ? <ArrowLeft size={18} /> : <UserPlus size={18} />}
{isSignUp ? 'Back to Sign In' : 'Need an account? Sign Up'}
</button>
</div>
</motion.div>
</div>
);
};
export default Login;