🎓 Initial commit: Math2 Platform - Plataforma de Álgebra Lineal PRO
Some checks failed
Test Suite / test-backend (push) Has been cancelled
Test Suite / test-frontend (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / coverage-check (push) Has been cancelled

 Características:
- 45 ejercicios universitarios (Basic → Advanced)
- Renderizado LaTeX profesional
- IA generativa (Z.ai/DashScope)
- Docker 9 servicios
- Tests 123/123 pasando
- Seguridad enterprise (JWT, XSS, Rate limiting)

🐳 Infraestructura:
- Next.js 14 + Node.js 20
- PostgreSQL 15 + Redis 7
- Docker Compose completo
- Nginx + SSL ready

📚 Documentación:
- 5 informes técnicos completos
- README profesional
- Scripts de deployment automatizados

Estado: Producción lista 
This commit is contained in:
Renato
2026-03-31 11:27:11 -03:00
commit bc43c9e772
309 changed files with 84845 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Loader2, CheckCircle2 } from 'lucide-react';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useAuthStore } from '@/store/useAuthStore';
import { api, apiEndpoints, ApiError } from '@/lib/api';
import { registerSchema, type RegisterFormData } from '@/lib/validators';
import { useToast } from '@/hooks/use-toast';
export default function RegisterPage() {
const router = useRouter();
const { toast } = useToast();
const { login } = useAuthStore();
const [isLoading, setIsLoading] = useState(false);
const {
register,
handleSubmit,
watch,
setError,
formState: { errors },
} = useForm<RegisterFormData>({
resolver: zodResolver(registerSchema),
});
const password = watch('password', '');
const getPasswordStrength = () => {
if (!password) return { level: 0, label: '', color: '' };
let strength = 0;
if (password.length >= 8) strength++;
if (/[a-z]/.test(password)) strength++;
if (/[A-Z]/.test(password)) strength++;
if (/\d/.test(password)) strength++;
if (/[^a-zA-Z\d]/.test(password)) strength++;
const levels = [
{ level: 1, label: 'Muy débil', color: 'bg-red-500' },
{ level: 2, label: 'Débil', color: 'bg-orange-500' },
{ level: 3, label: 'Aceptable', color: 'bg-yellow-500' },
{ level: 4, label: 'Fuerte', color: 'bg-green-500' },
{ level: 5, label: 'Muy fuerte', color: 'bg-green-600' },
];
return levels[strength - 1] ?? levels[0];
};
const passwordStrength = getPasswordStrength();
const onSubmit = async (data: RegisterFormData) => {
setIsLoading(true);
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { confirmPassword: _confirmPassword, ...registerData } = data;
const response = await api.post<{
user: {
id: string;
email: string;
username: string;
createdAt: string;
lastLoginAt: string;
};
token: string;
refreshToken: string;
}>(apiEndpoints.auth.register, registerData);
login(response.user, response.token, response.refreshToken);
toast({
title: '¡Cuenta creada!',
description: 'Bienvenido a Math Platform.',
});
router.push('/dashboard');
} catch (error) {
// Handle field-specific validation errors from backend
if (error instanceof ApiError && error.response) {
const response = error.response as { error?: { details?: Record<string, string> } };
if (response.error?.details) {
// Set field-specific errors using react-hook-form's setError
Object.entries(response.error.details).forEach(([field, message]) => {
setError(field as keyof RegisterFormData, {
type: 'server',
message: message,
});
});
} else {
toast({
title: 'Error al crear cuenta',
description: error.message,
variant: 'destructive',
});
}
} else {
toast({
title: 'Error al crear cuenta',
description: error instanceof Error ? error.message : 'No se pudo crear la cuenta',
variant: 'destructive',
});
}
} finally {
setIsLoading(false);
}
};
const onSubmitHandler = (e: React.FormEvent) => {
e.preventDefault();
void handleSubmit(onSubmit)(e);
};
return (
<Card className="w-full">
<CardHeader className="space-y-1 text-center">
<CardTitle className="text-2xl">Crear cuenta</CardTitle>
<CardDescription>
Regístrate para comenzar a aprender Álgebra Lineal
</CardDescription>
</CardHeader>
<form onSubmit={onSubmitHandler}>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">Nombre de usuario</Label>
<Input
id="username"
type="text"
placeholder="matematico2024"
disabled={isLoading}
{...register('username')}
/>
{errors.username && (
<p className="text-sm text-destructive">{errors.username.message}</p>
)}
<p className="text-xs text-muted-foreground">
3-20 caracteres, debe empezar con letra. Solo letras, números y guiones bajos
</p>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="tu@email.com"
disabled={isLoading}
{...register('email')}
/>
{errors.email && (
<p className="text-sm text-destructive">{errors.email.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="password">Contraseña</Label>
<Input
id="password"
type="password"
placeholder="••••••••"
disabled={isLoading}
{...register('password')}
/>
{errors.password && (
<p className="text-sm text-destructive">{errors.password.message}</p>
)}
{password && passwordStrength && (
<div className="space-y-1">
<div className="flex h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div
className={`h-full transition-all ${passwordStrength.color}`}
style={{ width: `${(passwordStrength.level / 5) * 100}%` }}
/>
</div>
<p className="text-xs text-muted-foreground">
Fortaleza: <span className="font-medium">{passwordStrength.label}</span>
</p>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirmar contraseña</Label>
<Input
id="confirmPassword"
type="password"
placeholder="••••••••"
disabled={isLoading}
{...register('confirmPassword')}
/>
{errors.confirmPassword && (
<p className="text-sm text-destructive">{errors.confirmPassword.message}</p>
)}
{password && watch('confirmPassword') && !errors.confirmPassword && (
<p className="flex items-center gap-1 text-sm text-green-600">
<CheckCircle2 className="h-4 w-4" />
Las contraseñas coinciden
</p>
)}
</div>
<div className="flex items-start gap-2">
<input
type="checkbox"
id="terms"
required
className="mt-1 h-4 w-4 rounded border-input"
/>
<label
htmlFor="terms"
className="text-sm leading-tight text-muted-foreground"
>
Acepto los{' '}
<Link href="/terms" className="text-primary hover:underline">
términos y condiciones
</Link>{' '}
y la{' '}
<Link href="/privacy" className="text-primary hover:underline">
política de privacidad
</Link>
</label>
</div>
</CardContent>
<CardFooter className="flex flex-col space-y-4">
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creando cuenta...
</>
) : (
'Crear cuenta'
)}
</Button>
<p className="text-sm text-center text-muted-foreground">
¿Ya tienes cuenta?{' '}
<Link href="/login" className="text-primary hover:underline">
Inicia sesión
</Link>
</p>
</CardFooter>
</form>
</Card>
);
}