Feat: Complete Sign In / Sign Up flow with Aubix mascot (Phase 9)
Browse files- frontend/src/components/Login.tsx +111 -37
frontend/src/components/Login.tsx
CHANGED
|
@@ -1,13 +1,17 @@
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
import { supabase } from '../services/supabase';
|
| 3 |
-
import { LogIn, Mail, Lock,
|
| 4 |
-
import { motion } from 'framer-motion';
|
|
|
|
| 5 |
|
| 6 |
const Login: React.FC = () => {
|
|
|
|
| 7 |
const [email, setEmail] = useState('');
|
| 8 |
const [password, setPassword] = useState('');
|
|
|
|
| 9 |
const [loading, setLoading] = useState(false);
|
| 10 |
const [error, setError] = useState<string | null>(null);
|
|
|
|
| 11 |
|
| 12 |
const handleLogin = async (e: React.FormEvent) => {
|
| 13 |
e.preventDefault();
|
|
@@ -19,20 +23,101 @@ const Login: React.FC = () => {
|
|
| 19 |
setLoading(false);
|
| 20 |
};
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
return (
|
| 23 |
-
<div className="login-screen"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
<motion.div
|
| 25 |
-
initial={{ opacity: 0,
|
| 26 |
-
animate={{ opacity: 1,
|
| 27 |
className="glass-panel login-panel"
|
|
|
|
| 28 |
>
|
| 29 |
-
<div style={{ marginBottom: 'var(--space-
|
| 30 |
-
<
|
| 31 |
-
<h1 style={{ fontSize: '
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
</div>
|
| 34 |
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
<div style={{ position: 'relative' }}>
|
| 37 |
<Mail size={18} style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
|
| 38 |
<input
|
|
@@ -79,39 +164,28 @@ const Login: React.FC = () => {
|
|
| 79 |
type="submit"
|
| 80 |
className="btn btn-primary"
|
| 81 |
disabled={loading}
|
| 82 |
-
style={{ padding: '0.
|
| 83 |
>
|
| 84 |
-
{loading ?
|
| 85 |
-
|
| 86 |
-
<LogIn size={18} />
|
| 87 |
-
Sign In
|
| 88 |
-
</>
|
| 89 |
-
)}
|
| 90 |
</button>
|
| 91 |
</form>
|
| 92 |
|
| 93 |
-
{
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
<GitBranch size={18} />
|
| 107 |
-
GitHub
|
| 108 |
</button>
|
| 109 |
</div>
|
| 110 |
-
*/}
|
| 111 |
-
|
| 112 |
-
<div style={{ marginTop: 'var(--space-lg)', fontSize: '0.85rem', color: 'var(--text-dim)' }}>
|
| 113 |
-
Enterprise authentication enabled.
|
| 114 |
-
</div>
|
| 115 |
</motion.div>
|
| 116 |
</div>
|
| 117 |
);
|
|
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
import { supabase } from '../services/supabase';
|
| 3 |
+
import { LogIn, Mail, Lock, UserPlus, ArrowLeft, RefreshCw, CheckCircle2 } from 'lucide-react';
|
| 4 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 5 |
+
import AubixIcon from './AubixIcon';
|
| 6 |
|
| 7 |
const Login: React.FC = () => {
|
| 8 |
+
const [isSignUp, setIsSignUp] = useState(false);
|
| 9 |
const [email, setEmail] = useState('');
|
| 10 |
const [password, setPassword] = useState('');
|
| 11 |
+
const [fullName, setFullName] = useState('');
|
| 12 |
const [loading, setLoading] = useState(false);
|
| 13 |
const [error, setError] = useState<string | null>(null);
|
| 14 |
+
const [success, setSuccess] = useState<string | null>(null);
|
| 15 |
|
| 16 |
const handleLogin = async (e: React.FormEvent) => {
|
| 17 |
e.preventDefault();
|
|
|
|
| 23 |
setLoading(false);
|
| 24 |
};
|
| 25 |
|
| 26 |
+
const handleSignUp = async (e: React.FormEvent) => {
|
| 27 |
+
e.preventDefault();
|
| 28 |
+
setLoading(true);
|
| 29 |
+
setError(null);
|
| 30 |
+
setSuccess(null);
|
| 31 |
+
|
| 32 |
+
const { error } = await supabase.auth.signUp({
|
| 33 |
+
email,
|
| 34 |
+
password,
|
| 35 |
+
options: {
|
| 36 |
+
data: {
|
| 37 |
+
full_name: fullName,
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
if (error) {
|
| 43 |
+
setError(error.message);
|
| 44 |
+
} else {
|
| 45 |
+
setSuccess('Account created! You can now sign in.');
|
| 46 |
+
setIsSignUp(false);
|
| 47 |
+
setFullName('');
|
| 48 |
+
}
|
| 49 |
+
setLoading(false);
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
return (
|
| 53 |
+
<div className="login-screen" style={{
|
| 54 |
+
minHeight: '100vh',
|
| 55 |
+
display: 'flex',
|
| 56 |
+
alignItems: 'center',
|
| 57 |
+
justifyContent: 'center',
|
| 58 |
+
background: 'radial-gradient(circle at center, #1a1a2e 0%, #0d0d14 100%)',
|
| 59 |
+
padding: 'var(--space-md)'
|
| 60 |
+
}}>
|
| 61 |
<motion.div
|
| 62 |
+
initial={{ opacity: 0, y: 20 }}
|
| 63 |
+
animate={{ opacity: 1, y: 0 }}
|
| 64 |
className="glass-panel login-panel"
|
| 65 |
+
style={{ width: '100%', maxWidth: '440px', padding: 'var(--space-xl)', textAlign: 'center' }}
|
| 66 |
>
|
| 67 |
+
<div style={{ marginBottom: 'var(--space-xl)' }}>
|
| 68 |
+
<AubixIcon size={120} />
|
| 69 |
+
<h1 style={{ fontSize: '2.5rem', marginBottom: 'var(--space-xs)', fontWeight: 800 }}>
|
| 70 |
+
{isSignUp ? 'Join Aubm' : 'Welcome'}
|
| 71 |
+
</h1>
|
| 72 |
+
<p style={{ color: 'var(--text-dim)' }}>
|
| 73 |
+
{isSignUp ? 'Create your agent operator profile' : 'Access the Aubm Orchestrator'}
|
| 74 |
+
</p>
|
| 75 |
</div>
|
| 76 |
|
| 77 |
+
{success && (
|
| 78 |
+
<motion.div
|
| 79 |
+
initial={{ opacity: 0, scale: 0.9 }}
|
| 80 |
+
animate={{ opacity: 1, scale: 1 }}
|
| 81 |
+
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)' }}
|
| 82 |
+
>
|
| 83 |
+
<CheckCircle2 size={18} />
|
| 84 |
+
{success}
|
| 85 |
+
</motion.div>
|
| 86 |
+
)}
|
| 87 |
+
|
| 88 |
+
<form onSubmit={isSignUp ? handleSignUp : handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-md)' }}>
|
| 89 |
+
<AnimatePresence mode="wait">
|
| 90 |
+
{isSignUp && (
|
| 91 |
+
<motion.div
|
| 92 |
+
key="signup-fields"
|
| 93 |
+
initial={{ height: 0, opacity: 0 }}
|
| 94 |
+
animate={{ height: 'auto', opacity: 1 }}
|
| 95 |
+
exit={{ height: 0, opacity: 0 }}
|
| 96 |
+
style={{ overflow: 'hidden' }}
|
| 97 |
+
>
|
| 98 |
+
<div style={{ position: 'relative', marginBottom: 'var(--space-md)' }}>
|
| 99 |
+
<UserPlus size={18} style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
|
| 100 |
+
<input
|
| 101 |
+
type="text"
|
| 102 |
+
placeholder="Full Name"
|
| 103 |
+
value={fullName}
|
| 104 |
+
onChange={(e) => setFullName(e.target.value)}
|
| 105 |
+
required={isSignUp}
|
| 106 |
+
style={{
|
| 107 |
+
width: '100%',
|
| 108 |
+
padding: '0.8rem 1rem 0.8rem 2.5rem',
|
| 109 |
+
background: 'rgba(255,255,255,0.05)',
|
| 110 |
+
border: '1px solid var(--glass-border)',
|
| 111 |
+
borderRadius: 'var(--radius-md)',
|
| 112 |
+
color: 'white',
|
| 113 |
+
outline: 'none'
|
| 114 |
+
}}
|
| 115 |
+
/>
|
| 116 |
+
</div>
|
| 117 |
+
</motion.div>
|
| 118 |
+
)}
|
| 119 |
+
</AnimatePresence>
|
| 120 |
+
|
| 121 |
<div style={{ position: 'relative' }}>
|
| 122 |
<Mail size={18} style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-muted)' }} />
|
| 123 |
<input
|
|
|
|
| 164 |
type="submit"
|
| 165 |
className="btn btn-primary"
|
| 166 |
disabled={loading}
|
| 167 |
+
style={{ padding: '0.85rem', marginTop: 'var(--space-sm)' }}
|
| 168 |
>
|
| 169 |
+
{loading ? <RefreshCw className="spin" size={18} /> : (isSignUp ? <UserPlus size={18} /> : <LogIn size={18} />)}
|
| 170 |
+
{loading ? (isSignUp ? 'Creating...' : 'Authenticating...') : (isSignUp ? 'Create Account' : 'Sign In')}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
</button>
|
| 172 |
</form>
|
| 173 |
|
| 174 |
+
<div style={{ marginTop: 'var(--space-xl)', paddingTop: 'var(--space-lg)', borderTop: '1px solid var(--glass-border)' }}>
|
| 175 |
+
<button
|
| 176 |
+
type="button"
|
| 177 |
+
className="btn btn-glass"
|
| 178 |
+
onClick={() => {
|
| 179 |
+
setIsSignUp(!isSignUp);
|
| 180 |
+
setError(null);
|
| 181 |
+
setSuccess(null);
|
| 182 |
+
}}
|
| 183 |
+
style={{ width: '100%' }}
|
| 184 |
+
>
|
| 185 |
+
{isSignUp ? <ArrowLeft size={18} /> : <UserPlus size={18} />}
|
| 186 |
+
{isSignUp ? 'Back to Sign In' : 'Need an account? Sign Up'}
|
|
|
|
|
|
|
| 187 |
</button>
|
| 188 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
</motion.div>
|
| 190 |
</div>
|
| 191 |
);
|