Files
moraworld/apps/web/src/app/registro/page.tsx
T
Lizandro Guarnizo 4872053fd1 feat: Fase 1 — Auth JWT+MFA, portales CRUD, UI completa
API:
- AuthModule: register, login, refresh, logout, MFA/TOTP setup+verify
- JwtStrategy + JwtAuthGuard + RolesGuard + CurrentUser decorator
- PackagesModule: CRUD paquetes + historial de estados
- PreAlertsModule: pre-alertas por usuario
- UsersModule: gestión de usuarios + roles + activación
- B2BModule: solicitudes de carga pesada/cotización
- ValidationPipe global + CORS configurado

Web (Next.js 15):
- globals.css completo (design system + utility classes)
- Layout raíz con WhatsApp flotante
- /login + /registro funcionales con JWT y redirección por rol
- /portal: dashboard, mi-casillero, mis-paquetes, pre-alerta, calculadora, perfil
- /admin: dashboard, usuarios (gestión roles/activación), tarifas, reportes, auditoría
- /bodega: dashboard, paquetes (crear+actualizar estado), verificación, despacho
- /tracking: tracking real con progreso visual + historial
- /calculadora: calculadora interactiva real (API SENAE §15)
- /como-funciona, /tarifas, /quienes-somos, /casillero
- /carga-pesada + /carga-pesada/cotizacion (formulario B2B)
- lib/api.ts: cliente HTTP con auto-refresh de token

Roles sincronizados con schema: SUPER_ADMIN, ADMIN_EMPRESA,
OPERADOR_BODEGA, AGENTE_ADUANERO, CLIENTE, SOPORTE
2026-06-01 09:42:38 -05:00

110 lines
5.0 KiB
TypeScript

"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { api, setToken, setRefresh, setUser } from "@/lib/api";
export default function RegistroPage() {
const router = useRouter();
const [form, setForm] = useState({ email: "", password: "", confirmPassword: "", firstName: "", lastName: "", phone: "" });
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (form.password !== form.confirmPassword) { setError("Las contraseñas no coinciden."); return; }
if (form.password.length < 8) { setError("La contraseña debe tener al menos 8 caracteres."); return; }
setLoading(true);
try {
const { confirmPassword, ...body } = form;
const data = await api.auth.register(body);
setToken(data.accessToken);
setRefresh(data.refreshToken);
setUser(data.user);
router.push("/portal/mi-casillero");
} catch (err: any) {
setError(err.message ?? "Error al registrarse");
} finally {
setLoading(false);
}
};
const set = (k: string) => (e: React.ChangeEvent<HTMLInputElement>) =>
setForm(f => ({ ...f, [k]: e.target.value }));
return (
<div style={{ minHeight: "100vh", background: "linear-gradient(135deg, #0d1117 0%, #0f2050 100%)", display: "flex", alignItems: "center", justifyContent: "center", padding: "1.5rem" }}>
<div style={{ width: "100%", maxWidth: 480 }}>
<div className="text-center mb-8">
<Link href="/" style={{ fontSize: "1.5rem", fontWeight: 800, color: "white" }}>
Moraworld<span style={{ color: "var(--accent)" }}>.</span>
</Link>
<p style={{ color: "rgba(255,255,255,.6)", marginTop: ".5rem", fontSize: ".9rem" }}>
Crea tu casillero gratis en 1 minuto
</p>
</div>
<div className="card" style={{ padding: "2rem" }}>
{error && <div className="alert alert-error mb-6">{error}</div>}
<form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "1.125rem" }}>
<div className="grid-2" style={{ gap: "1rem" }}>
<div className="form-group">
<label className="label">Nombre</label>
<input type="text" className="input" placeholder="Juan" value={form.firstName} onChange={set("firstName")} required minLength={2} />
</div>
<div className="form-group">
<label className="label">Apellido</label>
<input type="text" className="input" placeholder="Pérez" value={form.lastName} onChange={set("lastName")} required minLength={2} />
</div>
</div>
<div className="form-group">
<label className="label">Correo electrónico</label>
<input type="email" className="input" placeholder="juan@email.com" value={form.email} onChange={set("email")} required />
</div>
<div className="form-group">
<label className="label">Teléfono (opcional)</label>
<input type="tel" className="input" placeholder="+593 99 123 4567" value={form.phone} onChange={set("phone")} />
</div>
<div className="form-group">
<label className="label">Contraseña</label>
<input type="password" className="input" placeholder="Mínimo 8 caracteres" value={form.password} onChange={set("password")} required minLength={8} />
</div>
<div className="form-group">
<label className="label">Confirmar contraseña</label>
<input type="password" className="input" placeholder="Repite tu contraseña" value={form.confirmPassword} onChange={set("confirmPassword")} required />
</div>
<button type="submit" className="btn btn-primary" style={{ width: "100%", padding: ".875rem", marginTop: ".25rem" }} disabled={loading}>
{loading ? "Creando cuenta..." : "Crear mi casillero gratis"}
</button>
</form>
<p style={{ fontSize: ".75rem", color: "var(--gray-500)", textAlign: "center", marginTop: "1rem", lineHeight: 1.5 }}>
Al registrarte aceptas nuestros{" "}
<Link href="/terminos" style={{ color: "var(--primary)" }}>Términos de Servicio</Link>{" "}
y{" "}
<Link href="/privacidad" style={{ color: "var(--primary)" }}>Política de Privacidad</Link>.
</p>
<div className="text-center mt-4">
<p style={{ fontSize: ".875rem", color: "var(--gray-500)" }}>
¿Ya tienes cuenta?{" "}
<Link href="/login" style={{ color: "var(--primary)", fontWeight: 600 }}>Inicia sesión</Link>
</p>
</div>
</div>
<div className="text-center mt-6">
<Link href="/" style={{ fontSize: ".8rem", color: "rgba(255,255,255,.4)" }}> Volver al sitio</Link>
</div>
</div>
</div>
);
}