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:
Jose Luis
2026-03-21 20:22:43 +01:00
parent e129fd3739
commit 3523111019
7 changed files with 399 additions and 5 deletions

View 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>
);
}