- 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
500 lines
21 KiB
TypeScript
500 lines
21 KiB
TypeScript
// @ts-nocheck
|
|
import { useState, useEffect } from 'react';
|
|
import { useParams, Link } from 'react-router-dom';
|
|
import { motion, AnimatePresence } 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, BookOpen, Trophy, ChevronRight } from 'lucide-react';
|
|
|
|
// Importar contenido del módulo 2
|
|
import { default as demandaContent } from '../../content/modulo2/demanda';
|
|
import { default as ofertaContent } from '../../content/modulo2/oferta';
|
|
import { default as equilibrioContent } from '../../content/modulo2/equilibrio';
|
|
|
|
// Importar componentes de ejercicios
|
|
import { ConstructorCurvas, SimuladorPrecios, IdentificarShocks } from '../../components/exercises/modulo2';
|
|
|
|
const TABS = ['Contenido', 'Ejercicios'] as const;
|
|
type Tab = typeof TABS[number];
|
|
|
|
interface EjercicioConfig {
|
|
id: string;
|
|
titulo: string;
|
|
descripcion: string;
|
|
componente: React.ReactNode;
|
|
}
|
|
|
|
export function Modulo2Page() {
|
|
const { numero } = useParams<{ numero: string }>();
|
|
const [activeTab, setActiveTab] = useState<Tab>('Contenido');
|
|
const [activeSeccion, setActiveSeccion] = useState<'demanda' | 'oferta' | 'equilibrio'>('demanda');
|
|
const [activeEjercicio, setActiveEjercicio] = useState<string | null>(null);
|
|
const [progresos, setProgresos] = useState<Progreso[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
loadProgreso();
|
|
}, []);
|
|
|
|
const loadProgreso = async () => {
|
|
try {
|
|
const data = await progresoService.getProgreso();
|
|
setProgresos(data);
|
|
} catch {
|
|
// Silenciar error
|
|
}
|
|
};
|
|
|
|
const getProgresoForEjercicio = (ejercicioId: string) => {
|
|
return progresos.find(
|
|
(p) => p.modulo_numero === 2 && p.ejercicio_id === ejercicioId
|
|
);
|
|
};
|
|
|
|
const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => {
|
|
setLoading(true);
|
|
try {
|
|
await progresoService.saveProgreso(ejercicioId, puntuacion);
|
|
await loadProgreso();
|
|
} catch {
|
|
// Silenciar error
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Configuración de ejercicios
|
|
const ejerciciosConfig: EjercicioConfig[] = [
|
|
{
|
|
id: 'constructor-curvas',
|
|
titulo: 'Constructor de Curvas de Oferta y Demanda',
|
|
descripcion: 'Construye curvas de oferta y demanda arrastrando puntos para entender sus pendientes y movimientos.',
|
|
componente: (
|
|
<ConstructorCurvas
|
|
ejercicioId="constructor-curvas"
|
|
onComplete={(puntuacion) => handleCompleteEjercicio('constructor-curvas', puntuacion)}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
id: 'simulador-precios',
|
|
titulo: 'Simulador de Precios Intervenidos',
|
|
descripcion: 'Ajusta precios máximos y mínimos para observar sus efectos en el mercado: escasez, superávit, y pérdida de bienestar.',
|
|
componente: (
|
|
<SimuladorPrecios
|
|
ejercicioId="simulador-precios"
|
|
onComplete={(puntuacion) => handleCompleteEjercicio('simulador-precios', puntuacion)}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
id: 'identificar-shocks',
|
|
titulo: 'Identificador de Shocks del Mercado',
|
|
descripcion: 'Analiza escenarios económicos reales e identifica si afectan la oferta, la demanda, ambas, y cómo cambian precio y cantidad de equilibrio.',
|
|
componente: (
|
|
<IdentificarShocks
|
|
ejercicioId="identificar-shocks"
|
|
onComplete={(puntuacion) => handleCompleteEjercicio('identificar-shocks', puntuacion)}
|
|
/>
|
|
),
|
|
},
|
|
];
|
|
|
|
// Estructura de contenido del módulo 2
|
|
const seccionesContenido = {
|
|
demanda: {
|
|
titulo: 'Ley de la Demanda',
|
|
contenido: [
|
|
{
|
|
titulo: demandaContent.definicion.titulo,
|
|
texto: demandaContent.definicion.definicion,
|
|
elementos: demandaContent.definicion.elementosClave,
|
|
},
|
|
{
|
|
titulo: demandaContent.ley.titulo,
|
|
texto: demandaContent.ley.enunciado,
|
|
efectos: demandaContent.ley.efectos,
|
|
},
|
|
{
|
|
titulo: 'Factores que Desplazan la Demanda',
|
|
texto: 'Los siguientes factores causan desplazamientos de la curva de demanda:',
|
|
factores: demandaContent.factores,
|
|
},
|
|
],
|
|
},
|
|
oferta: {
|
|
titulo: 'Ley de la Oferta',
|
|
contenido: [
|
|
{
|
|
titulo: ofertaContent.definicion.titulo,
|
|
texto: ofertaContent.definicion.definicion,
|
|
elementos: ofertaContent.definicion.elementosClave,
|
|
},
|
|
{
|
|
titulo: ofertaContent.ley.titulo,
|
|
texto: ofertaContent.ley.enunciado,
|
|
razones: ofertaContent.ley.razones,
|
|
},
|
|
{
|
|
titulo: 'Factores que Desplazan la Oferta',
|
|
texto: 'Los siguientes factores causan desplazamientos de la curva de oferta:',
|
|
factores: ofertaContent.factores,
|
|
},
|
|
],
|
|
},
|
|
equilibrio: {
|
|
titulo: 'Equilibrio de Mercado',
|
|
contenido: [
|
|
{
|
|
titulo: equilibrioContent.definicion.titulo,
|
|
texto: equilibrioContent.definicion.definicion,
|
|
caracteristicas: equilibrioContent.definicion.caracteristicas,
|
|
},
|
|
{
|
|
titulo: 'Excedentes del Mercado',
|
|
texto: 'En el equilibrio se generan beneficios para consumidores y productores:',
|
|
excedentes: [
|
|
equilibrioContent.excedentes.excedenteConsumidor,
|
|
equilibrioContent.excedentes.excedenteProductor,
|
|
],
|
|
},
|
|
{
|
|
titulo: 'Controles de Precio',
|
|
texto: 'Los gobiernos pueden intervenir el mercado estableciendo precios máximos o mínimos:',
|
|
controles: [
|
|
equilibrioContent.controles.precioMaximo,
|
|
equilibrioContent.controles.precioMinimo,
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const currentSeccion = seccionesContenido[activeSeccion];
|
|
|
|
// Calcular progreso
|
|
const ejerciciosCompletados = ejerciciosConfig.filter(
|
|
(e) => getProgresoForEjercicio(e.id)?.completado
|
|
).length;
|
|
const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
{/* Header */}
|
|
<header className="bg-white shadow-sm sticky top-0 z-10">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<Link to="/modulos" className="inline-flex items-center text-blue-600 hover:underline">
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
Volver a Módulos
|
|
</Link>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-gray-600">Progreso:</span>
|
|
<div className="w-32 bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className="bg-green-500 h-2 rounded-full transition-all"
|
|
style={{ width: `${porcentajeProgreso}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-sm font-medium text-gray-700">{porcentajeProgreso}%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Título del módulo */}
|
|
<div className="bg-gradient-to-r from-green-600 to-green-800 text-white">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-16 h-16 bg-white/20 rounded-xl flex items-center justify-center text-3xl font-bold">
|
|
2
|
|
</div>
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Módulo 2: Oferta, Demanda y Equilibrio</h1>
|
|
<p className="text-green-100 mt-1">
|
|
Curvas de oferta y demanda, equilibrio de mercado y controles de precio
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
<div className="flex gap-2 border-b border-gray-200">
|
|
{TABS.map((tab) => (
|
|
<button
|
|
key={tab}
|
|
onClick={() => {
|
|
setActiveTab(tab);
|
|
setActiveEjercicio(null);
|
|
}}
|
|
className={`px-6 py-3 font-medium text-sm transition-colors relative ${
|
|
activeTab === tab
|
|
? 'text-blue-600'
|
|
: 'text-gray-500 hover:text-gray-700'
|
|
}`}
|
|
>
|
|
{tab === 'Contenido' && <BookOpen className="w-4 h-4 inline mr-2" />}
|
|
{tab === 'Ejercicios' && <Trophy className="w-4 h-4 inline mr-2" />}
|
|
{tab}
|
|
{activeTab === tab && (
|
|
<motion.div
|
|
layoutId="activeTab2"
|
|
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"
|
|
/>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Contenido según tab activo */}
|
|
<AnimatePresence mode="wait">
|
|
{activeTab === 'Contenido' ? (
|
|
<motion.div
|
|
key="contenido"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
className="mt-6 grid grid-cols-1 lg:grid-cols-4 gap-6"
|
|
>
|
|
{/* Navegación de secciones */}
|
|
<div className="lg:col-span-1">
|
|
<Card className="sticky top-24">
|
|
<h3 className="font-semibold text-gray-900 mb-4">Secciones</h3>
|
|
<nav className="space-y-2">
|
|
{(Object.keys(seccionesContenido) as Array<keyof typeof seccionesContenido>).map((key) => (
|
|
<button
|
|
key={key}
|
|
onClick={() => setActiveSeccion(key)}
|
|
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${
|
|
activeSeccion === key
|
|
? 'bg-green-50 text-green-700 font-medium'
|
|
: 'text-gray-600 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
{seccionesContenido[key].titulo}
|
|
<ChevronRight className="w-4 h-4" />
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Contenido de la sección */}
|
|
<div className="lg:col-span-3 space-y-6">
|
|
<Card>
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">{currentSeccion.titulo}</h2>
|
|
<div className="space-y-6">
|
|
{currentSeccion.contenido.map((item, index) => (
|
|
<div key={index} className="border-b border-gray-100 last:border-0 pb-6 last:pb-0">
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-3">{item.titulo}</h3>
|
|
<p className="text-gray-600 mb-4 leading-relaxed">{item.texto}</p>
|
|
|
|
{/* Mostrar elementos clave si existen */}
|
|
{item.elementos && (
|
|
<ul className="list-disc list-inside space-y-2 text-gray-600">
|
|
{item.elementos.map((el: any, i: number) => (
|
|
<li key={i}>
|
|
<strong>{el.elemento}:</strong> {el.descripcion}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
|
|
{/* Mostrar efectos/razones si existen */}
|
|
{item.efectos && (
|
|
<div className="space-y-3">
|
|
{item.efectos.map((efecto: any, i: number) => (
|
|
<div key={i} className="bg-gray-50 p-3 rounded-lg">
|
|
<h4 className="font-medium text-gray-800">{efecto.nombre}</h4>
|
|
<p className="text-sm text-gray-600">{efecto.descripcion}</p>
|
|
<p className="text-sm text-blue-600 mt-1">Ejemplo: {efecto.ejemplo}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Mostrar razones si existen */}
|
|
{item.razones && (
|
|
<div className="space-y-3">
|
|
{item.razones.map((razon: any, i: number) => (
|
|
<div key={i} className="bg-gray-50 p-3 rounded-lg">
|
|
<h4 className="font-medium text-gray-800">{razon.nombre}</h4>
|
|
<p className="text-sm text-gray-600">{razon.descripcion}</p>
|
|
<p className="text-sm text-green-600 mt-1">Ejemplo: {razon.ejemplo}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Mostrar factores si existen */}
|
|
{item.factores && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{item.factores.map((factor: any, i: number) => (
|
|
<div key={i} className="bg-blue-50 p-3 rounded-lg border border-blue-100">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="text-lg">{factor.icono}</span>
|
|
<h4 className="font-medium text-blue-900">{factor.nombre}</h4>
|
|
</div>
|
|
<p className="text-sm text-blue-700">{factor.descripcion}</p>
|
|
<p className="text-sm text-blue-600 mt-1 italic">{factor.ejemplo}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Mostrar características si existen */}
|
|
{item.caracteristicas && (
|
|
<ul className="list-disc list-inside space-y-2 text-gray-600">
|
|
{item.caracteristicas.map((car: any, i: number) => (
|
|
<li key={i}>
|
|
<strong>{car.caracteristica}:</strong> {car.explicacion}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
|
|
{/* Mostrar excedentes si existen */}
|
|
{item.excedentes && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{item.excedentes.map((exc: any, i: number) => (
|
|
<div key={i} className="bg-purple-50 p-3 rounded-lg border border-purple-100">
|
|
<h4 className="font-medium text-purple-900">{exc.nombre}</h4>
|
|
<p className="text-sm text-purple-700">{exc.definicion}</p>
|
|
<p className="text-sm text-purple-600 mt-1">Fórmula: {exc.formula}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Mostrar controles si existen */}
|
|
{item.controles && (
|
|
<div className="space-y-3">
|
|
{item.controles.map((control: any, i: number) => (
|
|
<div key={i} className="bg-orange-50 p-3 rounded-lg border border-orange-100">
|
|
<h4 className="font-medium text-orange-900">{control.nombre}</h4>
|
|
<p className="text-sm text-orange-700">{control.definicion}</p>
|
|
<p className="text-sm text-orange-600 mt-1">
|
|
Condición: {control.condicionEfectivo}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
|
|
<Card className="bg-green-50 border-green-200">
|
|
<h3 className="font-semibold text-green-900 mb-3">Ejercicios Relacionados</h3>
|
|
<p className="text-green-700 text-sm mb-4">
|
|
Pon a prueba tus conocimientos con ejercicios interactivos sobre oferta, demanda y equilibrio
|
|
</p>
|
|
<Button
|
|
onClick={() => setActiveTab('Ejercicios')}
|
|
variant="outline"
|
|
className="border-green-300 text-green-700 hover:bg-green-100"
|
|
>
|
|
<Play className="w-4 h-4 mr-2" />
|
|
Ir a Ejercicios
|
|
</Button>
|
|
</Card>
|
|
</div>
|
|
</motion.div>
|
|
) : (
|
|
<motion.div
|
|
key="ejercicios"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
className="mt-6"
|
|
>
|
|
{activeEjercicio ? (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<Button variant="ghost" onClick={() => setActiveEjercicio(null)}>
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
Volver a ejercicios
|
|
</Button>
|
|
{loading && <span className="text-sm text-gray-500">Guardando progreso...</span>}
|
|
</div>
|
|
{ejerciciosConfig.find((e) => e.id === activeEjercicio)?.componente}
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{ejerciciosConfig.map((ejercicio, index) => {
|
|
const progreso = getProgresoForEjercicio(ejercicio.id);
|
|
const completado = progreso?.completado || false;
|
|
|
|
return (
|
|
<Card
|
|
key={ejercicio.id}
|
|
className="hover:shadow-lg transition-shadow cursor-pointer"
|
|
onClick={() => setActiveEjercicio(ejercicio.id)}
|
|
>
|
|
<div className="flex items-start gap-4">
|
|
<div
|
|
className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
|
|
completado ? 'bg-green-100 text-green-600' : 'bg-green-100 text-green-600'
|
|
}`}
|
|
>
|
|
{completado ? (
|
|
<CheckCircle className="w-6 h-6" />
|
|
) : (
|
|
<span className="text-xl 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 mt-1">{ejercicio.descripcion}</p>
|
|
{completado && progreso && (
|
|
<div className="mt-3 flex items-center gap-2">
|
|
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full">
|
|
Completado
|
|
</span>
|
|
<span className="text-xs text-gray-500">
|
|
{progreso.puntuacion} pts
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Button className="w-full mt-4" size="sm">
|
|
<Play className="w-4 h-4 mr-2" />
|
|
{completado ? 'Repetir' : 'Comenzar'}
|
|
</Button>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{ejerciciosCompletados === ejerciciosConfig.length && ejerciciosConfig.length > 0 && (
|
|
<Card className="mt-6 bg-green-50 border-green-200">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
|
|
<Trophy className="w-6 h-6 text-green-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-green-900">¡Felicitaciones!</h3>
|
|
<p className="text-green-700 text-sm">
|
|
Has completado todos los ejercicios de este módulo.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default Modulo2Page;
|