Fix login blank screen and progress persistence
- Fix authStore to persist user data, not just isAuthenticated - Fix progressStore handling of undefined API responses - Remove minimax.md documentation file - All progress now properly saves to PostgreSQL - Login flow working correctly
This commit is contained in:
@@ -1,63 +1,213 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
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';
|
||||
import { Loader } from '../components/ui/Loader';
|
||||
import { useProgressStore } from '../stores/progressStore';
|
||||
import { ScoreDisplay } from '../components/progress/ScoreDisplay';
|
||||
import {
|
||||
ArrowLeft,
|
||||
CheckCircle,
|
||||
Play,
|
||||
Lock,
|
||||
Trophy,
|
||||
TrendingUp,
|
||||
RotateCcw
|
||||
} from 'lucide-react';
|
||||
import type { EjercicioProgreso } from '../stores/progressStore';
|
||||
|
||||
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' },
|
||||
// Importar ejercicios reales
|
||||
import { FlujoCircular } from '../components/exercises/modulo1/FlujoCircular';
|
||||
import { QuizBienes } from '../components/exercises/modulo1/QuizBienes';
|
||||
import { SimuladorDisyuntivas } from '../components/exercises/modulo1/SimuladorDisyuntivas';
|
||||
import { ConstructorCurvas } from '../components/exercises/modulo2/ConstructorCurvas';
|
||||
import { IdentificarShocks } from '../components/exercises/modulo2/IdentificarShocks';
|
||||
import { SimuladorPrecios } from '../components/exercises/modulo2/SimuladorPrecios';
|
||||
import { ClasificadorBienes } from '../components/exercises/modulo3/ClasificadorBienes';
|
||||
import { CalculadoraElasticidad } from '../components/exercises/modulo3/CalculadoraElasticidad';
|
||||
import { EjerciciosExamen } from '../components/exercises/modulo3/EjerciciosExamen';
|
||||
import { CalculadoraCostos } from '../components/exercises/modulo4/CalculadoraCostos';
|
||||
import { SimuladorProduccion } from '../components/exercises/modulo4/SimuladorProduccion';
|
||||
import { VisualizadorExcedentes } from '../components/exercises/modulo4/VisualizadorExcedentes';
|
||||
|
||||
const MODULOS_INFO: Record<number, {
|
||||
id: string;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
color: string;
|
||||
}> = {
|
||||
1: {
|
||||
id: 'modulo1',
|
||||
titulo: 'Fundamentos de Economía',
|
||||
descripcion: 'Introducción a los conceptos básicos de economía',
|
||||
color: 'from-blue-500 to-blue-600'
|
||||
},
|
||||
2: {
|
||||
id: 'modulo2',
|
||||
titulo: 'Oferta, Demanda y Equilibrio',
|
||||
descripcion: 'Curvas de oferta y demanda en el mercado',
|
||||
color: 'from-green-500 to-green-600'
|
||||
},
|
||||
3: {
|
||||
id: 'modulo3',
|
||||
titulo: 'Utilidad y Elasticidad',
|
||||
descripcion: 'Teoría del consumidor y elasticidades',
|
||||
color: 'from-purple-500 to-purple-600'
|
||||
},
|
||||
4: {
|
||||
id: 'modulo4',
|
||||
titulo: 'Teoría del Productor',
|
||||
descripcion: 'Costos de producción y competencia perfecta',
|
||||
color: 'from-orange-500 to-orange-600'
|
||||
},
|
||||
};
|
||||
|
||||
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' },
|
||||
];
|
||||
const EJERCICIOS_POR_MODULO: Record<number, Array<{
|
||||
id: string;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
componente: React.ComponentType<{ ejercicioId: string; onComplete?: (puntuacion: number) => void }>;
|
||||
}>> = {
|
||||
1: [
|
||||
{ id: 'simulador-disyuntivas', titulo: 'Simulador de Disyuntivas', descripcion: 'Explora las decisiones económicas fundamentales', componente: SimuladorDisyuntivas },
|
||||
{ id: 'quiz-bienes', titulo: 'Quiz de Bienes', descripcion: 'Identifica diferentes tipos de bienes', componente: QuizBienes },
|
||||
{ id: 'flujo-circular', titulo: 'Flujo Circular', descripcion: 'Comprende el flujo de bienes y dinero en la economía', componente: FlujoCircular },
|
||||
],
|
||||
2: [
|
||||
{ id: 'constructor-curvas', titulo: 'Constructor de Curvas', descripcion: 'Construye curvas de oferta y demanda', componente: ConstructorCurvas },
|
||||
{ id: 'identificar-shocks', titulo: 'Identificar Shocks', descripcion: 'Reconoce cambios en el mercado', componente: IdentificarShocks },
|
||||
{ id: 'simulador-precios', titulo: 'Simulador de Precios', descripcion: 'Simula el equilibrio de precios', componente: SimuladorPrecios },
|
||||
],
|
||||
3: [
|
||||
{ id: 'clasificador-bienes', titulo: 'Clasificador de Bienes', descripcion: 'Clasifica bienes según su elasticidad', componente: ClasificadorBienes },
|
||||
{ id: 'calculadora-elasticidad', titulo: 'Calculadora de Elasticidad', descripcion: 'Calcula elasticidades de demanda', componente: CalculadoraElasticidad },
|
||||
{ id: 'ejercicios-examen', titulo: 'Ejercicios de Examen', descripcion: 'Pon a prueba tus conocimientos', componente: EjerciciosExamen },
|
||||
],
|
||||
4: [
|
||||
{ id: 'calculadora-costos', titulo: 'Calculadora de Costos', descripcion: 'Calcula costos de producción', componente: CalculadoraCostos },
|
||||
{ id: 'simulador-produccion', titulo: 'Simulador de Producción', descripcion: 'Simula la producción óptima', componente: SimuladorProduccion },
|
||||
{ id: 'visualizador-excedentes', titulo: 'Visualizador de Excedentes', descripcion: 'Visualiza excedentes del consumidor y productor', componente: VisualizadorExcedentes },
|
||||
],
|
||||
};
|
||||
|
||||
export function Modulo() {
|
||||
const { numero } = useParams<{ numero: string }>();
|
||||
const num = parseInt(numero || '1', 10);
|
||||
const [progresos, setProgresos] = useState<Progreso[]>([]);
|
||||
|
||||
const {
|
||||
puntuacionTotal,
|
||||
getProgresoEjercicio,
|
||||
saveProgreso,
|
||||
calcularPorcentajeModulo,
|
||||
loadProgreso,
|
||||
isLoading,
|
||||
error,
|
||||
} = useProgressStore();
|
||||
|
||||
const moduloInfo = MODULOS_INFO[num] || MODULOS_INFO[1];
|
||||
const ejercicios = EJERCICIOS_MOCK;
|
||||
const [ejercicioActivo, setEjercicioActivo] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadProgreso();
|
||||
}, [num]);
|
||||
}, [loadProgreso]);
|
||||
|
||||
const loadProgreso = async () => {
|
||||
const moduloInfo = MODULOS_INFO[num] || MODULOS_INFO[1];
|
||||
const ejercicios = EJERCICIOS_POR_MODULO[num] || [];
|
||||
const porcentaje = calcularPorcentajeModulo(moduloInfo.id, ejercicios.length);
|
||||
|
||||
const getProgresoEjercicioLocal = (ejercicioId: string): EjercicioProgreso | undefined => {
|
||||
return getProgresoEjercicio(moduloInfo.id, ejercicioId);
|
||||
};
|
||||
|
||||
const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => {
|
||||
try {
|
||||
const data = await progresoService.getProgreso();
|
||||
setProgresos(data);
|
||||
} catch {
|
||||
// Silencio
|
||||
await saveProgreso(moduloInfo.id, ejercicioId, puntuacion);
|
||||
setEjercicioActivo(null);
|
||||
} catch (err) {
|
||||
console.error('Error al guardar progreso:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const getProgresoForEjercicio = (ejercicioId: string) => {
|
||||
return progresos.find(
|
||||
(p) => p.modulo_numero === num && p.ejercicio_id === ejercicioId
|
||||
);
|
||||
const completados = ejercicios.filter(
|
||||
(e) => getProgresoEjercicioLocal(e.id)?.completado
|
||||
).length;
|
||||
|
||||
// Determinar si un ejercicio está bloqueado (el primero siempre desbloqueado)
|
||||
const isEjercicioBloqueado = (index: number): boolean => {
|
||||
if (index === 0) return false;
|
||||
// Ejercicio anterior completado?
|
||||
const ejercicioAnterior = ejercicios[index - 1];
|
||||
return !getProgresoEjercicioLocal(ejercicioAnterior.id)?.completado;
|
||||
};
|
||||
|
||||
const completados = ejercicios.filter(
|
||||
(e) => getProgresoForEjercicio(e.id)?.completado
|
||||
).length;
|
||||
const porcentaje = Math.round((completados / ejercicios.length) * 100);
|
||||
if (ejercicioActivo) {
|
||||
const ejercicio = ejercicios.find(e => e.id === ejercicioActivo);
|
||||
if (!ejercicio) return null;
|
||||
|
||||
const EjercicioComponent = ejercicio.componente;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<header className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<button
|
||||
onClick={() => setEjercicioActivo(null)}
|
||||
className="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver al módulo
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">{ejercicio.titulo}</h1>
|
||||
<p className="text-gray-600">{ejercicio.descripcion}</p>
|
||||
</div>
|
||||
|
||||
<EjercicioComponent
|
||||
ejercicioId={ejercicio.id}
|
||||
onComplete={(puntuacion: number) => handleCompleteEjercicio(ejercicio.id, puntuacion)}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Loader size="lg" className="mx-auto mb-4" />
|
||||
<p className="text-gray-600">Cargando ejercicios...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center max-w-md mx-auto px-4">
|
||||
<div className="text-red-500 mb-4">
|
||||
<svg className="w-16 h-16 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-2">Error al cargar el progreso</h2>
|
||||
<p className="text-gray-600 mb-4">{error}</p>
|
||||
<Button onClick={loadProgreso}>Reintentar</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<header className="bg-white shadow-sm">
|
||||
<header className="bg-white shadow-sm border-b border-gray-200">
|
||||
<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">
|
||||
<Link to="/" className="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver al Dashboard
|
||||
</Link>
|
||||
@@ -67,7 +217,7 @@ export function Modulo() {
|
||||
<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">
|
||||
<div className={`w-16 h-16 bg-gradient-to-br ${moduloInfo.color} rounded-xl flex items-center justify-center text-white text-2xl font-bold shadow-lg`}>
|
||||
{num}
|
||||
</div>
|
||||
<div>
|
||||
@@ -76,76 +226,153 @@ export function Modulo() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="bg-gradient-to-r from-primary to-blue-600 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<Card className={`bg-gradient-to-r ${moduloInfo.color} text-white border-none`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<p className="text-blue-100">Tu progreso en este módulo</p>
|
||||
<p className="text-white/80 text-sm">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>
|
||||
<p className="text-white/80 text-sm">Ejercicios</p>
|
||||
<p className="text-xl font-bold">{completados}/{ejercicios.length}</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 className="w-full bg-white/20 rounded-full h-3 overflow-hidden">
|
||||
<motion.div
|
||||
className="bg-white h-full rounded-full"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${porcentaje}%` }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4">Ejercicios</h2>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-gray-900">Ejercicios</h2>
|
||||
<ScoreDisplay puntos={puntuacionTotal} animar={false} showNivel={false} size="sm" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
{ejercicios.map((ejercicio, index) => {
|
||||
const progreso = getProgresoForEjercicio(ejercicio.id);
|
||||
const progreso = getProgresoEjercicioLocal(ejercicio.id);
|
||||
const completado = progreso?.completado || false;
|
||||
const bloqueado = isEjercicioBloqueado(index);
|
||||
|
||||
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>
|
||||
<motion.div
|
||||
key={ejercicio.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
>
|
||||
<Card className={`transition-all ${
|
||||
bloqueado
|
||||
? 'opacity-60 bg-gray-50'
|
||||
: 'hover:shadow-md cursor-pointer'
|
||||
}`}
|
||||
onClick={() => !bloqueado && setEjercicioActivo(ejercicio.id)}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-12 h-12 rounded-full flex items-center justify-center ${
|
||||
completado
|
||||
? 'bg-green-500 text-white'
|
||||
: bloqueado
|
||||
? 'bg-gray-200 text-gray-400'
|
||||
: 'bg-blue-100 text-blue-600'
|
||||
}`}>
|
||||
{completado ? (
|
||||
<CheckCircle className="w-6 h-6" />
|
||||
) : bloqueado ? (
|
||||
<Lock className="w-5 h-5" />
|
||||
) : (
|
||||
<span className="font-bold">{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>
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold ${
|
||||
completado ? 'text-gray-900' : bloqueado ? 'text-gray-500' : 'text-gray-900'
|
||||
}`}>
|
||||
{ejercicio.titulo}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">{ejercicio.descripcion}</p>
|
||||
{completado && progreso && progreso.puntuacion > 0 && (
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs font-medium text-green-600">
|
||||
Mejor puntuación: {progreso.puntuacion} pts
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
({progreso.intentos} {progreso.intentos === 1 ? 'intento' : 'intentos'})
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button size="sm">
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{completado ? 'Repetir' : 'Comenzar'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={bloqueado}
|
||||
variant={completado ? 'outline' : 'primary'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
!bloqueado && setEjercicioActivo(ejercicio.id);
|
||||
}}
|
||||
>
|
||||
{completado ? (
|
||||
<>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Repetir
|
||||
</>
|
||||
) : bloqueado ? (
|
||||
'Bloqueado'
|
||||
) : (
|
||||
<>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Comenzar
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</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" />
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
<Card className="mt-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white border-none">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<Trophy className="w-7 h-7" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-bold text-lg">¡Felicitaciones!</h3>
|
||||
<p className="text-green-100">
|
||||
Has completado todos los ejercicios de este módulo.
|
||||
{num < 4 ? ' ¡Continúa con el siguiente módulo!' : ' ¡Has completado todos los módulos!'}
|
||||
</p>
|
||||
</div>
|
||||
{num < 4 && (
|
||||
<Link to={`/modulo/${num + 1}`}>
|
||||
<Button variant="primary" className="bg-white text-green-600 hover:bg-green-50">
|
||||
Siguiente módulo
|
||||
<TrendingUp className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</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>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Modulo;
|
||||
|
||||
Reference in New Issue
Block a user