feat: frontend auth — login/register modal + user badge
- API service (api.js): fetch wrapper with JWT, auto-refresh on 401 - AuthContext: user state, login/register/logout, loading, roles - AuthModal: tabbed login/register form matching .pen design - User badge in toolbar (Sandbox + WorldMap) with initial avatar - "Entrar" button when not logged in - CSS: auth overlay, card, tabs, inputs, error state, user badge - Auth is opt-in: app works fully without login Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
114
packages/client/src/components/AuthModal.jsx
Normal file
114
packages/client/src/components/AuthModal.jsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../services/AuthContext.jsx';
|
||||
|
||||
export default function AuthModal() {
|
||||
const { showAuth, closeAuth, login, register } = useAuth();
|
||||
const [tab, setTab] = useState('login'); // 'login' | 'register'
|
||||
const [email, setEmail] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
if (!showAuth) return null;
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
if (tab === 'login') {
|
||||
await login(email, password);
|
||||
} else {
|
||||
await register(email, username, password);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message || 'Error');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setError('');
|
||||
setEmail('');
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="auth-overlay" onClick={closeAuth}>
|
||||
<div className="auth-card" onClick={e => e.stopPropagation()}>
|
||||
<div className="auth-logo">
|
||||
<div className="auth-logo-box">~</div>
|
||||
<span className="auth-logo-name">Reaktor</span>
|
||||
</div>
|
||||
|
||||
<div className="auth-tabs">
|
||||
<button
|
||||
className={`auth-tab ${tab === 'login' ? 'active' : ''}`}
|
||||
onClick={() => { setTab('login'); reset(); }}
|
||||
>
|
||||
Iniciar Sesion
|
||||
</button>
|
||||
<button
|
||||
className={`auth-tab ${tab === 'register' ? 'active' : ''}`}
|
||||
onClick={() => { setTab('register'); reset(); }}
|
||||
>
|
||||
Registrarse
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="auth-form">
|
||||
<label className="auth-label">EMAIL</label>
|
||||
<input
|
||||
type="email"
|
||||
className="auth-input"
|
||||
placeholder="tu@email.com"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
{tab === 'register' && (
|
||||
<>
|
||||
<label className="auth-label">USUARIO</label>
|
||||
<input
|
||||
type="text"
|
||||
className="auth-input"
|
||||
placeholder="username"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
minLength={3}
|
||||
maxLength={50}
|
||||
required
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<label className="auth-label">CONTRASEÑA</label>
|
||||
<input
|
||||
type="password"
|
||||
className="auth-input"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
minLength={6}
|
||||
required
|
||||
/>
|
||||
|
||||
{error && <div className="auth-error">{error}</div>}
|
||||
|
||||
<button type="submit" className="auth-submit" disabled={loading}>
|
||||
{loading ? '...' : tab === 'login' ? 'Entrar' : 'Crear Cuenta'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<button className="auth-skip" onClick={closeAuth}>
|
||||
Continuar sin cuenta
|
||||
</button>
|
||||
|
||||
<button className="auth-close" onClick={closeAuth}>✕</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user