Initial commit: Plataforma de Economía

Features:
- React 18 + TypeScript frontend with Vite
- Go + Gin backend API
- PostgreSQL database
- JWT authentication with refresh tokens
- User management (admin panel)
- Docker containerization
- Progress tracking system
- 4 economic modules structure

Fixed:
- Login with username or email
- User creation without required email
- Database nullable timestamps
- API response field naming
This commit is contained in:
Renato
2026-02-12 01:30:57 +01:00
commit d31575a143
57 changed files with 7017 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { progresoService } from '../services/api';
import type { Progreso } from '../types';
import { ArrowLeft, CheckCircle, Play } from 'lucide-react';
const MODULOS_INFO: Record<number, { titulo: string; descripcion: string }> = {
1: { titulo: 'Fundamentos de Economía', descripcion: 'Introducción a los conceptos básicos de economía' },
2: { titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Curvas de oferta y demanda en el mercado' },
3: { titulo: 'Utilidad y Elasticidad', descripcion: 'Teoría del consumidor y elasticidades' },
4: { titulo: 'Teoría del Productor', descripcion: 'Costos de producción y competencia perfecta' },
};
const EJERCICIOS_MOCK = [
{ id: 'e1', titulo: 'Conceptos básicos', descripcion: 'Repasa los fundamentos de la economía' },
{ id: 'e2', titulo: 'Agentes económicos', descripcion: 'Identifica los diferentes agentes en la economía' },
{ id: 'e3', titulo: 'Factores de producción', descripcion: 'Aprende sobre tierra, trabajo y capital' },
{ id: 'e4', titulo: 'Flujo circular', descripcion: 'Comprende el flujo de bienes y dinero' },
{ id: 'e5', titulo: 'Evaluación final', descripcion: 'Pon a prueba todo lo aprendido' },
];
export function Modulo() {
const { numero } = useParams<{ numero: string }>();
const num = parseInt(numero || '1', 10);
const [progresos, setProgresos] = useState<Progreso[]>([]);
const moduloInfo = MODULOS_INFO[num] || MODULOS_INFO[1];
const ejercicios = EJERCICIOS_MOCK;
useEffect(() => {
loadProgreso();
}, [num]);
const loadProgreso = async () => {
try {
const data = await progresoService.getProgreso();
setProgresos(data);
} catch {
// Silencio
}
};
const getProgresoForEjercicio = (ejercicioId: string) => {
return progresos.find(
(p) => p.modulo_numero === num && p.ejercicio_id === ejercicioId
);
};
const completados = ejercicios.filter(
(e) => getProgresoForEjercicio(e.id)?.completado
).length;
const porcentaje = Math.round((completados / ejercicios.length) * 100);
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<Link to="/" className="inline-flex items-center text-primary hover:underline">
<ArrowLeft className="w-4 h-4 mr-2" />
Volver al Dashboard
</Link>
</div>
</header>
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<div className="flex items-center gap-4 mb-4">
<div className="w-14 h-14 bg-gradient-to-br from-primary to-blue-600 rounded-xl flex items-center justify-center text-white text-2xl font-bold shadow-lg">
{num}
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">{moduloInfo.titulo}</h1>
<p className="text-gray-600">{moduloInfo.descripcion}</p>
</div>
</div>
<Card className="bg-gradient-to-r from-primary to-blue-600 text-white">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100">Tu progreso en este módulo</p>
<p className="text-3xl font-bold mt-1">{porcentaje}%</p>
</div>
<div className="text-right">
<p className="text-blue-100">{completados}/{ejercicios.length} ejercicios</p>
</div>
</div>
<div className="mt-4 w-full bg-white/20 rounded-full h-2">
<div
className="bg-white h-2 rounded-full transition-all"
style={{ width: `${porcentaje}%` }}
/>
</div>
</Card>
</div>
<h2 className="text-xl font-bold text-gray-900 mb-4">Ejercicios</h2>
<div className="space-y-3">
{ejercicios.map((ejercicio, index) => {
const progreso = getProgresoForEjercicio(ejercicio.id);
const completado = progreso?.completado || false;
return (
<Card key={ejercicio.id} className="hover:shadow-md transition-shadow">
<div className="flex items-center gap-4">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
completado ? 'bg-success text-white' : 'bg-gray-100 text-gray-500'
}`}>
{completado ? (
<CheckCircle className="w-5 h-5" />
) : (
<span className="font-medium">{index + 1}</span>
)}
</div>
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{ejercicio.titulo}</h3>
<p className="text-sm text-gray-500">{ejercicio.descripcion}</p>
</div>
<Button size="sm">
<Play className="w-4 h-4 mr-2" />
{completado ? 'Repetir' : 'Comenzar'}
</Button>
</div>
</Card>
);
})}
</div>
{porcentaje === 100 && (
<Card className="mt-6 bg-success/10 border border-success">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-success rounded-full flex items-center justify-center">
<CheckCircle className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="font-semibold text-success">¡Felicitaciones!</h3>
<p className="text-sm text-gray-600">
Has completado todos los ejercicios de este módulo.
</p>
</div>
</div>
</Card>
)}
</main>
</div>
);
}