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,176 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useAuthStore } from '../stores/authStore';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { progresoService } from '../services/api';
import type { ModuloProgreso } from '../types';
import { BookOpen, TrendingUp, User, LogOut, LayoutGrid } from 'lucide-react';
const MODULOS_DEFAULT = [
{ numero: 1, titulo: 'Fundamentos de Economía', descripcion: 'Introducción a los conceptos básicos' },
{ numero: 2, titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Curvas de mercado' },
{ numero: 3, titulo: 'Utilidad y Elasticidad', descripcion: 'Teoría del consumidor' },
{ numero: 4, titulo: 'Teoría del Productor', descripcion: 'Costos y producción' },
];
export function Dashboard() {
const { usuario, logout } = useAuthStore();
const [modulosProgreso, setModulosProgreso] = useState<ModuloProgreso[]>([]);
useEffect(() => {
loadProgreso();
}, []);
const loadProgreso = async () => {
try {
const progresos = await progresoService.getProgreso();
const modulos = MODULOS_DEFAULT.map((mod) => {
const modProgresos = progresos.filter((p) => p.modulo_numero === mod.numero);
const completados = modProgresos.filter((p) => p.completado).length;
const total = 5; // Asumiendo 5 ejercicios por módulo
return {
numero: mod.numero,
titulo: mod.titulo,
porcentaje: Math.round((completados / total) * 100),
ejerciciosCompletados: completados,
totalEjercicios: total,
};
});
setModulosProgreso(modulos);
} catch {
// Si hay error, mostrar progreso vacío
setModulosProgreso(
MODULOS_DEFAULT.map((mod) => ({
numero: mod.numero,
titulo: mod.titulo,
porcentaje: 0,
ejerciciosCompletados: 0,
totalEjercicios: 5,
}))
);
}
};
const handleLogout = async () => {
await logout();
};
const totalProgreso = Math.round(
modulosProgreso.reduce((acc, mod) => acc + mod.porcentaje, 0) / modulosProgreso.length
);
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 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
<BookOpen className="w-5 h-5 text-white" />
</div>
<h1 className="text-xl font-bold text-gray-900">Economía</h1>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-gray-600">
<User className="w-5 h-5" />
<span className="font-medium">{usuario?.nombre}</span>
{usuario?.rol === 'admin' && (
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 text-xs rounded-full">
Admin
</span>
)}
</div>
<Button variant="ghost" size="sm" onClick={handleLogout}>
<LogOut className="w-4 h-4" />
</Button>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<h2 className="text-2xl font-bold text-gray-900">Tu progreso</h2>
<p className="text-gray-600">Continúa donde lo dejaste</p>
</div>
<Card className="mb-8">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="font-semibold text-gray-900">Progreso total</h3>
<p className="text-sm text-gray-500">{totalProgreso}% completado</p>
</div>
<div className="text-3xl font-bold text-primary">{totalProgreso}%</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className="bg-primary h-3 rounded-full transition-all duration-500"
style={{ width: `${totalProgreso}%` }}
/>
</div>
</Card>
<div className="mb-6 flex items-center justify-between">
<h2 className="text-xl font-bold text-gray-900">Módulos</h2>
{usuario?.rol === 'admin' && (
<Link to="/admin">
<Button variant="outline" size="sm">
Panel de Admin
</Button>
</Link>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{modulosProgreso.map((modulo) => (
<Link key={modulo.numero} to={`/modulo/${modulo.numero}`}>
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<span className="text-primary font-bold">{modulo.numero}</span>
</div>
<div>
<h3 className="font-semibold text-gray-900">{modulo.titulo}</h3>
<p className="text-sm text-gray-500">
{modulo.ejerciciosCompletados}/{modulo.totalEjercicios} ejercicios
</p>
</div>
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2 mb-2">
<div
className={`h-2 rounded-full transition-all ${
modulo.porcentaje === 100 ? 'bg-success' : 'bg-primary'
}`}
style={{ width: `${modulo.porcentaje}%` }}
/>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500">{modulo.porcentaje}% completado</span>
{modulo.porcentaje === 100 && (
<span className="text-success flex items-center gap-1">
<TrendingUp className="w-4 h-4" />
Completado
</span>
)}
</div>
</Card>
</Link>
))}
</div>
<div className="mt-8 text-center">
<Link to="/modulos">
<Button variant="outline" size="lg">
<LayoutGrid className="w-5 h-5 mr-2" />
Ver todos los módulos
</Button>
</Link>
</div>
</main>
</div>
);
}