Initial commit - cleaned for CV

This commit is contained in:
Renato97
2026-03-31 01:28:28 -03:00
commit ce9f0d5180
203 changed files with 50950 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
import { useState } from 'react';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import {
Headphones,
Download,
Play,
Clock,
BookOpen,
Calendar,
ArrowLeft
} from 'lucide-react';
import { Link } from 'react-router-dom';
interface Clase {
id: number;
titulo: string;
modulo: string;
duracion: string;
fecha: string;
descripcion: string;
archivo: string;
}
const CLASES: Clase[] = [
{
id: 1,
titulo: 'Clase 1: Fundamentos de Economía',
modulo: 'Módulo 1',
duracion: '63 minutos',
fecha: '27 de Enero, 2025',
descripcion: 'Introducción a la economía, el problema económico fundamental, sistemas económicos, frontera de posibilidades de producción, agentes económicos y factores de producción.',
archivo: '/audios/clase1_completa.m4a'
},
{
id: 2,
titulo: 'Clase 2: Oferta, Demanda y Equilibrio',
modulo: 'Módulo 2',
duracion: '103 minutos',
fecha: '30 de Enero, 2025',
descripcion: 'Ley de la demanda, ley de la oferta, equilibrio de mercado, elasticidad de la demanda y controles de precio. Análisis completo del funcionamiento de los mercados.',
archivo: '/audios/clase2_completa.m4a'
},
{
id: 3,
titulo: 'Clase 3: Elasticidad y Teoría del Consumidor',
modulo: 'Módulo 3',
duracion: '52 minutos',
fecha: '3 de Febrero, 2025',
descripcion: 'Elasticidad precio, ingreso y cruzada. Utilidad total y marginal, restricción presupuestaria y maximización de la satisfacción del consumidor.',
archivo: '/audios/clase3_completa.m4a'
},
{
id: 4,
titulo: 'Clase 4: Teoría del Productor',
modulo: 'Módulo 4',
duracion: '46 minutos',
fecha: '6 de Febrero, 2025',
descripcion: 'Función de producción, ley de rendimientos decrecientes, costos a corto y largo plazo, ingresos y maximización de beneficios en competencia perfecta.',
archivo: '/audios/clase4_completa.m4a'
}
];
export function ClasesGrabadasPage() {
const [claseReproduciendo, setClaseReproduciendo] = useState<number | null>(null);
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4">
{/* Header */}
<div className="mb-8">
<Link
to="/"
className="inline-flex items-center text-gray-600 hover:text-blue-600 mb-4"
>
<ArrowLeft size={20} className="mr-2" />
Volver al Dashboard
</Link>
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl flex items-center justify-center shadow-lg">
<Headphones className="w-7 h-7 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900">Clases Grabadas</h1>
<p className="text-gray-600">
Escucha las clases completas en audio o descárgalas
</p>
</div>
</div>
</div>
{/* Info Banner */}
<Card className="mb-8 bg-gradient-to-r from-blue-50 to-purple-50 border-blue-200">
<div className="flex items-start gap-4">
<div className="p-3 bg-blue-100 rounded-xl">
<BookOpen className="w-6 h-6 text-blue-600" />
</div>
<div className="flex-1">
<h2 className="text-lg font-semibold text-blue-900 mb-2">
¿Cómo usar las clases grabadas?
</h2>
<ul className="space-y-2 text-blue-800 text-sm">
<li className="flex items-start gap-2">
<span className="text-blue-500 mt-0.5"></span>
<span>Cada clase corresponde a un módulo del curso</span>
</li>
<li className="flex items-start gap-2">
<span className="text-blue-500 mt-0.5"></span>
<span>Puedes escuchar directamente en la web o descargar para escuchar offline</span>
</li>
<li className="flex items-start gap-2">
<span className="text-blue-500 mt-0.5"></span>
<span>Te recomendamos escuchar la clase antes de hacer los ejercicios del módulo</span>
</li>
<li className="flex items-start gap-2">
<span className="text-blue-500 mt-0.5"></span>
<span>Total: {CLASES.length} clases · Duración total aproximada: 4.5 horas</span>
</li>
</ul>
</div>
</div>
</Card>
{/* Lista de Clases */}
<div className="space-y-6">
{CLASES.map((clase) => (
<Card key={clase.id} className="hover:shadow-lg transition-shadow">
<div className="flex flex-col lg:flex-row gap-6">
{/* Icono/Info */}
<div className="flex items-start gap-4 lg:w-1/3">
<div className="w-16 h-16 bg-gradient-to-br from-indigo-100 to-purple-100 rounded-2xl flex items-center justify-center flex-shrink-0">
<span className="text-2xl font-bold text-indigo-600">{clase.id}</span>
</div>
<div className="flex-1">
<span className="inline-block px-3 py-1 bg-gray-100 text-gray-600 text-xs font-medium rounded-full mb-2">
{clase.modulo}
</span>
<h3 className="text-lg font-bold text-gray-900 mb-1">
{clase.titulo}
</h3>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span className="flex items-center gap-1">
<Clock size={14} />
{clase.duracion}
</span>
<span className="flex items-center gap-1">
<Calendar size={14} />
{clase.fecha}
</span>
</div>
</div>
</div>
{/* Descripción */}
<div className="lg:w-1/3">
<p className="text-gray-600 text-sm leading-relaxed">
{clase.descripcion}
</p>
</div>
{/* Acciones */}
<div className="flex flex-col sm:flex-row gap-3 lg:w-1/3 lg:justify-end">
<Button
onClick={() => setClaseReproduciendo(claseReproduciendo === clase.id ? null : clase.id)}
className="flex-1 sm:flex-none"
>
<Play size={18} className="mr-2" />
{claseReproduciendo === clase.id ? 'Ocultar' : 'Escuchar'}
</Button>
<a
href={clase.archivo}
download
className="flex-1 sm:flex-none"
>
<Button variant="outline" className="w-full">
<Download size={18} className="mr-2" />
Descargar
</Button>
</a>
</div>
</div>
{/* Reproductor de Audio */}
{claseReproduciendo === clase.id && (
<div className="mt-6 pt-6 border-t border-gray-100">
<audio
controls
className="w-full"
autoPlay
>
<source src={clase.archivo} type="audio/mp4" />
Tu navegador no soporta el elemento de audio.
</audio>
<p className="text-sm text-gray-500 mt-3 text-center">
💡 Tip: Puedes descargar el audio para escucharlo sin conexión
</p>
</div>
)}
</Card>
))}
</div>
{/* Footer */}
<div className="mt-12 text-center">
<p className="text-gray-500 text-sm mb-4">
¿Ya escuchaste las clases? Pasa a los ejercicios interactivos para practicar.
</p>
<Link to="/modulos">
<Button>
Ir a los Ejercicios
</Button>
</Link>
</div>
</div>
</div>
);
}
export default ClasesGrabadasPage;

View File

@@ -0,0 +1,281 @@
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useAuthStore } from '../stores/authStore';
import { useProgressStore } from '../stores/progressStore';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { ProgressBar } from '../components/progress/ProgressBar';
import { ScoreDisplay } from '../components/progress/ScoreDisplay';
import { BadgesSection } from '../components/progress/Badges';
import { Loader } from '../components/ui/Loader';
import { BookOpen, User, LogOut, LayoutGrid, Award, Star, Target, CheckCircle, FileText, Headphones } from 'lucide-react';
import { SistemaAnuncios } from '../components/announcements/SistemaAnuncios';
const MODULOS_CONFIG = [
{ id: 'modulo1', numero: 1, titulo: 'Fundamentos de Economía', descripcion: 'Introducción a los conceptos básicos', totalEjercicios: 3 },
{ id: 'modulo2', numero: 2, titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Curvas de mercado', totalEjercicios: 3 },
{ id: 'modulo3', numero: 3, titulo: 'Utilidad y Elasticidad', descripcion: 'Teoría del consumidor', totalEjercicios: 3 },
{ id: 'modulo4', numero: 4, titulo: 'Teoría del Productor', descripcion: 'Costos y producción', totalEjercicios: 3 },
];
export function Dashboard() {
const { usuario, logout } = useAuthStore();
const {
puntuacionTotal,
nivel,
calcularPorcentajeModulo,
getBadgesDesbloqueados,
getBadgesBloqueados,
modulos,
loadProgreso,
isLoading,
error,
} = useProgressStore();
useEffect(() => {
loadProgreso();
}, [loadProgreso]);
const handleLogout = async () => {
await logout();
};
if (isLoading && Object.keys(modulos).length === 0) {
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 tu progreso...</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>
);
}
// Calcular progreso total
const totalProgreso = Math.round(
MODULOS_CONFIG.reduce((acc, mod) => {
return acc + calcularPorcentajeModulo(mod.id, mod.totalEjercicios);
}, 0) / MODULOS_CONFIG.length
);
const badgesDesbloqueados = getBadgesDesbloqueados();
const badgesBloqueados = getBadgesBloqueados();
// Calcular ejercicios completados por módulo
const getEjerciciosCompletados = (moduloId: string) => {
const modulo = modulos[moduloId];
if (!modulo) return 0;
return Object.values(modulo.ejercicios).filter(ej => ej.completado).length;
};
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 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-600 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 Interactiva</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 || 'Usuario'}</span>
<span className="px-2 py-0.5 bg-blue-100 text-blue-700 text-xs rounded-full font-semibold">
{nivel}
</span>
{usuario?.rol === 'admin' && (
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 text-xs rounded-full font-semibold">
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 y desbloquea nuevos logros</p>
</div>
{/* Sistema de Anuncios */}
<SistemaAnuncios />
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<Card className="bg-gradient-to-br from-blue-500 to-blue-600 text-white border-none">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-sm">Progreso total</p>
<p className="text-3xl font-bold mt-1">{totalProgreso}%</p>
</div>
<Target className="w-12 h-12 text-blue-100 opacity-80" />
</div>
<div className="mt-4 w-full bg-white/20 rounded-full h-2">
<div
className="bg-white h-2 rounded-full transition-all duration-500"
style={{ width: `${totalProgreso}%` }}
/>
</div>
<p className="mt-2 text-xs text-blue-100">
{totalProgreso === 100 ? '¡Has completado todos los módulos!' : 'Sigue así, vas por buen camino'}
</p>
</Card>
<Card className="bg-gradient-to-br from-amber-500 to-orange-500 text-white border-none">
<div className="flex items-center justify-between">
<div>
<p className="text-orange-100 text-sm">Puntuación total</p>
<p className="text-3xl font-bold mt-1">{puntuacionTotal.toLocaleString()}</p>
</div>
<Star className="w-12 h-12 text-orange-100 opacity-80" />
</div>
<p className="mt-4 text-sm text-orange-100">
Acumula puntos completando ejercicios para subir de nivel
</p>
</Card>
<Card className="bg-gradient-to-br from-purple-500 to-pink-500 text-white border-none">
<div className="flex items-center justify-between">
<div>
<p className="text-purple-100 text-sm">Logros</p>
<p className="text-3xl font-bold mt-1">
{badgesDesbloqueados.length}/{badgesDesbloqueados.length + badgesBloqueados.length}
</p>
</div>
<Award className="w-12 h-12 text-purple-100 opacity-80" />
</div>
<p className="mt-4 text-sm text-purple-100">
{badgesBloqueados.length === 0
? '¡Todos los logros desbloqueados!'
: `${badgesBloqueados.length} logros por desbloquear`}
</p>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Columna izquierda - Módulos */}
<div className="lg:col-span-2 space-y-6">
{/* Puntuación y Nivel */}
<ScoreDisplay
puntos={puntuacionTotal}
animar={false}
showNivel={true}
size="md"
/>
<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">
{MODULOS_CONFIG.map((modulo) => {
const porcentaje = calcularPorcentajeModulo(modulo.id, modulo.totalEjercicios);
const completados = getEjerciciosCompletados(modulo.id);
return (
<Link key={modulo.id} to={`/modulo/${modulo.numero}`}>
<Card className="hover:shadow-lg transition-shadow cursor-pointer h-full">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<span className="text-blue-600 font-bold text-lg">{modulo.numero}</span>
</div>
<div>
<h3 className="font-semibold text-gray-900">{modulo.titulo}</h3>
<p className="text-sm text-gray-500">
{completados}/{modulo.totalEjercicios} ejercicios
</p>
</div>
</div>
</div>
<ProgressBar
porcentaje={porcentaje}
moduloNumero={modulo.numero}
showLabel={false}
size="sm"
/>
<div className="flex items-center justify-between text-sm mt-3">
<span className="text-gray-500">{porcentaje}% completado</span>
{porcentaje === 100 && (
<span className="text-green-600 flex items-center gap-1 font-medium">
<CheckCircle className="w-4 h-4" />
Completado
</span>
)}
</div>
</Card>
</Link>
);
})}
</div>
<div className="mt-8 flex justify-center gap-4">
<Link to="/modulos">
<Button variant="outline" size="lg">
<LayoutGrid className="w-5 h-5 mr-2" />
Ver todos los módulos
</Button>
</Link>
<Link to="/recursos">
<Button variant="outline" size="lg">
<FileText className="w-5 h-5 mr-2" />
Material PDF
</Button>
</Link>
<Link to="/clases">
<Button variant="outline" size="lg" className="bg-gradient-to-r from-purple-50 to-pink-50 border-purple-200 hover:border-purple-300">
<Headphones className="w-5 h-5 mr-2 text-purple-600" />
<span className="text-purple-700">Clases Grabadas</span>
</Button>
</Link>
</div>
</div>
{/* Columna derecha - Logros */}
<div>
<BadgesSection
badgesDesbloqueados={badgesDesbloqueados}
badgesBloqueados={badgesBloqueados}
/>
</div>
</div>
</main>
</div>
);
}
export default Dashboard;

View File

@@ -0,0 +1,34 @@
import { Navigate } from 'react-router-dom';
import { useAuthStore } from '../stores/authStore';
import { LoginForm } from '../components/auth/LoginForm';
import { BookOpen } from 'lucide-react';
export function Login() {
const { isAuthenticated } = useAuthStore();
if (isAuthenticated) {
return <Navigate to="/" replace />;
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-primary rounded-2xl mb-4">
<BookOpen className="w-8 h-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-gray-900">Plataforma de Economía</h1>
<p className="text-gray-600 mt-2">Inicia sesión para continuar</p>
</div>
<div className="bg-white rounded-2xl shadow-xl p-8">
<LoginForm />
</div>
<p className="text-center text-sm text-gray-500 mt-6">
Sistema de aprendizaje interactivo
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,558 @@
// @ts-nocheck
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 { 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';
// 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 { DefinicionEconomiaQuiz } from '../components/exercises/modulo1/DefinicionEconomiaQuiz';
import { EscasezSimulator } from '../components/exercises/modulo1/EscasezSimulator';
import { ProblemaEconomicoFundamental } from '../components/exercises/modulo1/ProblemaEconomicoFundamental';
import { EconomiaPositivaVsNormativa } from '../components/exercises/modulo1/EconomiaPositivaVsNormativa';
import { RazonamientoEconomico } from '../components/exercises/modulo1/RazonamientoEconomico';
import { SistemasEconomicosQuiz } from '../components/exercises/modulo1/SistemasEconomicosQuiz';
import { ComparativaSistemas } from '../components/exercises/modulo1/ComparativaSistemas';
import { CasosPaises } from '../components/exercises/modulo1/CasosPaises';
import { VentajasDesventajasSistemas } from '../components/exercises/modulo1/VentajasDesventajasSistemas';
import { FPPConstructor } from '../components/exercises/modulo1/FPPConstructor';
import { FPPAnalizador } from '../components/exercises/modulo1/FPPAnalizador';
import { CostoOportunidadCalculator } from '../components/exercises/modulo1/CostoOportunidadCalculator';
import { CrecimientoEconomicoFPP } from '../components/exercises/modulo1/CrecimientoEconomicoFPP';
import { AgentesEconomicosQuiz } from '../components/exercises/modulo1/AgentesEconomicosQuiz';
import { RolesAgentesMatching } from '../components/exercises/modulo1/RolesAgentesMatching';
import { FlujoCircularBasico } from '../components/exercises/modulo1/FlujoCircularBasico';
import { FactoresProduccionQuiz } from '../components/exercises/modulo1/FactoresProduccionQuiz';
import { ProductividadCalculator } from '../components/exercises/modulo1/ProductividadCalculator';
import { CostoOportunidadCotidiano } from '../components/exercises/modulo1/CostoOportunidadCotidiano';
import { VentajaComparativaCalculator } from '../components/exercises/modulo1/VentajaComparativaCalculator';
// Imports Módulo 2 - Oferta, Demanda y Equilibrio
import { ConstructorCurvas } from '../components/exercises/modulo2/ConstructorCurvas';
import { IdentificarShocks } from '../components/exercises/modulo2/IdentificarShocks';
import { SimuladorPrecios } from '../components/exercises/modulo2/SimuladorPrecios';
import { LeyDemandaQuiz } from '../components/exercises/modulo2/LeyDemandaQuiz';
import { CurvaDemandaConstructor } from '../components/exercises/modulo2/CurvaDemandaConstructor';
import { TablaDemanda } from '../components/exercises/modulo2/TablaDemanda';
import { DemandaIndividualVsMercado } from '../components/exercises/modulo2/DemandaIndividualVsMercado';
import { DesplazamientoVsMovimiento } from '../components/exercises/modulo2/DesplazamientoVsMovimiento';
import { FactoresDesplazanDemanda } from '../components/exercises/modulo2/FactoresDesplazanDemanda';
import { LeyOfertaQuiz } from '../components/exercises/modulo2/LeyOfertaQuiz';
import { CurvaOfertaConstructor } from '../components/exercises/modulo2/CurvaOfertaConstructor';
import { TablaOferta } from '../components/exercises/modulo2/TablaOferta';
import { FactoresDesplazanOferta } from '../components/exercises/modulo2/FactoresDesplazanOferta';
import { OfertaCortoLargoPlazo } from '../components/exercises/modulo2/OfertaCortoLargoPlazo';
import { EquilibrioFinder } from '../components/exercises/modulo2/EquilibrioFinder';
import { EquilibrioGrafico } from '../components/exercises/modulo2/EquilibrioGrafico';
import { AjusteEquilibrio } from '../components/exercises/modulo2/AjusteEquilibrio';
import { ExcesoDemandaEscasez } from '../components/exercises/modulo2/ExcesoDemandaEscasez';
import { ExcesoOfertaSuperavit } from '../components/exercises/modulo2/ExcesoOfertaSuperavit';
import { CalculoElasticidadPrecio } from '../components/exercises/modulo2/CalculoElasticidadPrecio';
import { ElasticidadElasticaInelastica } from '../components/exercises/modulo2/ElasticidadElasticaInelastica';
import { FactoresElasticidad } from '../components/exercises/modulo2/FactoresElasticidad';
import { ElasticidadIngresoTotal } from '../components/exercises/modulo2/ElasticidadIngresoTotal';
import { PrecioMaximoTecho } from '../components/exercises/modulo2/PrecioMaximoTecho';
import { PrecioMinimoPiso } from '../components/exercises/modulo2/PrecioMinimoPiso';
import { SimuladorControles } from '../components/exercises/modulo2/SimuladorControles';
import { ControlesVidaReal } from '../components/exercises/modulo2/ControlesVidaReal';
import { CambiosEquilibrio } from '../components/exercises/modulo2/CambiosEquilibrio';
// Imports Módulo 3 - Utilidad y Elasticidad
import { ClasificadorBienes } from '../components/exercises/modulo3/ClasificadorBienes';
import { CalculadoraElasticidad } from '../components/exercises/modulo3/CalculadoraElasticidad';
import { EjerciciosExamen } from '../components/exercises/modulo3/EjerciciosExamen';
import { FormulaElasticidad } from '../components/exercises/modulo3/FormulaElasticidad';
import { MetodoPuntoMedio } from '../components/exercises/modulo3/MetodoPuntoMedio';
import { ElasticidadCurva } from '../components/exercises/modulo3/ElasticidadCurva';
import { ElasticidadRectas } from '../components/exercises/modulo3/ElasticidadRectas';
import { ClasificacionElasticidad } from '../components/exercises/modulo3/ClasificacionElasticidad';
import { FormulaElasticidadIngreso } from '../components/exercises/modulo3/FormulaElasticidadIngreso';
import { BienesNormalesInferiores } from '../components/exercises/modulo3/BienesNormalesInferiores';
import { BienesLujoNecesarios } from '../components/exercises/modulo3/BienesLujoNecesarios';
import { CurvaEngel } from '../components/exercises/modulo3/CurvaEngel';
import { FormulaElasticidadCruzada } from '../components/exercises/modulo3/FormulaElasticidadCruzada';
import { SustitutosComplementarios } from '../components/exercises/modulo3/SustitutosComplementarios';
import { GradoRelacion } from '../components/exercises/modulo3/GradoRelacion';
import { UtilidadTotalVsMarginal } from '../components/exercises/modulo3/UtilidadTotalVsMarginal';
import { LeyUtilidadMarginalDecreciente } from '../components/exercises/modulo3/LeyUtilidadMarginalDecreciente';
import { MaximizacionUtilidad } from '../components/exercises/modulo3/MaximizacionUtilidad';
import { CurvasIndiferencia } from '../components/exercises/modulo3/CurvasIndiferencia';
import { CanastaOptima } from '../components/exercises/modulo3/CanastaOptima';
import { DecisionesPrecios } from '../components/exercises/modulo3/DecisionesPrecios';
import { ParadojaAguaDiamantes } from '../components/exercises/modulo3/ParadojaAguaDiamantes';
// Imports Módulo 4 - Teoría del Productor
import { CalculadoraCostos } from '../components/exercises/modulo4/CalculadoraCostos';
import { SimuladorProduccion } from '../components/exercises/modulo4/SimuladorProduccion';
import { VisualizadorExcedentes } from '../components/exercises/modulo4/VisualizadorExcedentes';
import { FuncionProduccion } from '../components/exercises/modulo4/FuncionProduccion';
import { CortoVsLargoPlazo } from '../components/exercises/modulo4/CortoVsLargoPlazo';
import { ProductoTotal } from '../components/exercises/modulo4/ProductoTotal';
import { ProductoMedio } from '../components/exercises/modulo4/ProductoMedio';
import { ProductoMarginal } from '../components/exercises/modulo4/ProductoMarginal';
import { LeyRendimientosDecrecientes } from '../components/exercises/modulo4/LeyRendimientosDecrecientes';
import { EtapasProduccion } from '../components/exercises/modulo4/EtapasProduccion';
import { ProductorRacional } from '../components/exercises/modulo4/ProductorRacional';
import { CostosFijosVsVariables } from '../components/exercises/modulo4/CostosFijosVsVariables';
import { TablaCostos } from '../components/exercises/modulo4/TablaCostos';
import { CurvasCosto } from '../components/exercises/modulo4/CurvasCosto';
import { CostoTotalMedioMarginal } from '../components/exercises/modulo4/CostoTotalMedioMarginal';
import { CostosMedios } from '../components/exercises/modulo4/CostosMedios';
import { RelacionCMgCMe } from '../components/exercises/modulo4/RelacionCMgCMe';
import { CurvaCostoLargoPlazo } from '../components/exercises/modulo4/CurvaCostoLargoPlazo';
import { EconomiasEscala } from '../components/exercises/modulo4/EconomiasEscala';
import { DiseconomiasEscala } from '../components/exercises/modulo4/DiseconomiasEscala';
import { IngresoTotal } from '../components/exercises/modulo4/IngresoTotal';
import { IngresoMarginal } from '../components/exercises/modulo4/IngresoMarginal';
import { IngresoCompetenciaPerfecta } from '../components/exercises/modulo4/IngresoCompetenciaPerfecta';
import { ReglaImgCmg } from '../components/exercises/modulo4/ReglaImgCmg';
import { PuntoCierreEquilibrio } from '../components/exercises/modulo4/PuntoCierreEquilibrio';
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_POR_MODULO: Record<number, Array<{
id: string;
titulo: string;
descripcion: string;
componente: React.ComponentType<{ ejercicioId: string; onComplete?: (puntuacion: number) => void }>;
}>> = {
1: [
{ id: 'definicion-economia-quiz', titulo: 'Definición de Economía', descripcion: 'Aprende qué es la economía y sus objetivos principales', componente: DefinicionEconomiaQuiz },
{ id: 'escasez-simulator', titulo: 'Simulador de Escasez', descripcion: 'Comprende el concepto de escasez y sus implicaciones', componente: EscasezSimulator },
{ id: 'problema-economico-fundamental', titulo: 'Problema Económico Fundamental', descripcion: 'Explora las preguntas básicas de toda economía', componente: ProblemaEconomicoFundamental },
{ id: 'economia-positiva-vs-normativa', titulo: 'Economía Positiva vs Normativa', descripcion: 'Diferencia entre análisis descriptivo y prescriptivo', componente: EconomiaPositivaVsNormativa },
{ id: 'razonamiento-economico', titulo: 'Razonamiento Económico', descripcion: 'Desarrolla el pensamiento económico lógico', componente: RazonamientoEconomico },
{ id: 'sistemas-economicos-quiz', titulo: 'Sistemas Económicos', descripcion: 'Conoce los diferentes sistemas económicos', componente: SistemasEconomicosQuiz },
{ id: 'comparativa-sistemas', titulo: 'Comparativa de Sistemas', descripcion: 'Compara características de distintos sistemas', componente: ComparativaSistemas },
{ id: 'casos-paises', titulo: 'Casos de Países', descripcion: 'Analiza ejemplos reales de países', componente: CasosPaises },
{ id: 'ventajas-desventajas-sistemas', titulo: 'Ventajas y Desventajas', descripcion: 'Evalúa pros y contras de cada sistema', componente: VentajasDesventajasSistemas },
{ id: 'fpp-constructor', titulo: 'Constructor de FPP', descripcion: 'Construye la Frontera de Posibilidades de Producción', componente: FPPConstructor },
{ id: 'fpp-analizador', titulo: 'Analizador de FPP', descripcion: 'Analiza puntos en la frontera de posibilidades', componente: FPPAnalizador },
{ id: 'costo-oportunidad-calculator', titulo: 'Calculadora de Costo de Oportunidad', descripcion: 'Calcula costos de oportunidad en la FPP', componente: CostoOportunidadCalculator },
{ id: 'crecimiento-economico-fpp', titulo: 'Crecimiento Económico y FPP', descripcion: 'Observa cómo crece la FPP con el desarrollo', componente: CrecimientoEconomicoFPP },
{ id: 'agentes-economicos-quiz', titulo: 'Agentes Económicos', descripcion: 'Identifica hogares, empresas y gobierno', componente: AgentesEconomicosQuiz },
{ id: 'roles-agentes-matching', titulo: 'Roles de Agentes', descripcion: 'Relaciona agentes con sus funciones', componente: RolesAgentesMatching },
{ id: 'flujo-circular-basico', titulo: 'Flujo Circular Básico', descripcion: 'Comprende el flujo real y monetario', componente: FlujoCircularBasico },
{ id: 'factores-produccion-quiz', titulo: 'Factores de Producción', descripcion: 'Tierra, trabajo, capital y tecnología', componente: FactoresProduccionQuiz },
{ id: 'productividad-calculator', titulo: 'Calculadora de Productividad', descripcion: 'Calcula la eficiencia productiva', componente: ProductividadCalculator },
{ id: 'costo-oportunidad-cotidiano', titulo: 'Costo de Oportunidad Cotidiano', descripcion: 'Encuentra costos de oportunidad en tu vida', componente: CostoOportunidadCotidiano },
{ id: 'ventaja-comparativa-calculator', titulo: 'Ventaja Comparativa', descripcion: 'Calcula ventajas comparativas entre países', componente: VentajaComparativaCalculator },
{ 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: 'ley-demanda-quiz', titulo: 'Ley de la Demanda', descripcion: 'Comprende la relación inversa entre precio y cantidad demandada', componente: LeyDemandaQuiz },
{ id: 'curva-demanda-constructor', titulo: 'Constructor de Curva de Demanda', descripcion: 'Construye la curva de demanda paso a paso', componente: CurvaDemandaConstructor },
{ id: 'tabla-demanda', titulo: 'Tabla de Demanda', descripcion: 'Interpreta tablas de demanda y sus variaciones', componente: TablaDemanda },
{ id: 'demanda-individual-vs-mercado', titulo: 'Demanda Individual vs Mercado', descripcion: 'Diferencia entre demanda individual y agregada', componente: DemandaIndividualVsMercado },
{ id: 'desplazamiento-vs-movimiento', titulo: 'Desplazamiento vs Movimiento', descripcion: 'Distingue cambios en la demanda de variaciones en la cantidad', componente: DesplazamientoVsMovimiento },
{ id: 'factores-desplazan-demanda', titulo: 'Factores que Desplazan la Demanda', descripcion: 'Identifica los determinantes de la demanda', componente: FactoresDesplazanDemanda },
{ id: 'ley-oferta-quiz', titulo: 'Ley de la Oferta', descripcion: 'Comprende la relación directa entre precio y cantidad ofrecida', componente: LeyOfertaQuiz },
{ id: 'curva-oferta-constructor', titulo: 'Constructor de Curva de Oferta', descripcion: 'Construye la curva de oferta paso a paso', componente: CurvaOfertaConstructor },
{ id: 'tabla-oferta', titulo: 'Tabla de Oferta', descripcion: 'Interpreta tablas de oferta y sus variaciones', componente: TablaOferta },
{ id: 'factores-desplazan-oferta', titulo: 'Factores que Desplazan la Oferta', descripcion: 'Identifica los determinantes de la oferta', componente: FactoresDesplazanOferta },
{ id: 'oferta-corto-largo-plazo', titulo: 'Oferta a Corto vs Largo Plazo', descripcion: 'Diferencias en la elasticidad de la oferta según el tiempo', componente: OfertaCortoLargoPlazo },
{ id: 'equilibrio-finder', titulo: 'Buscador de Equilibrio', descripcion: 'Encuentra el punto de equilibrio de mercado', componente: EquilibrioFinder },
{ id: 'equilibrio-grafico', titulo: 'Equilibrio Gráfico', descripcion: 'Visualiza el equilibrio en el gráfico de oferta y demanda', componente: EquilibrioGrafico },
{ id: 'constructor-curvas', titulo: 'Constructor de Curvas', descripcion: 'Construye curvas de oferta y demanda', componente: ConstructorCurvas },
{ id: 'ajuste-equilibrio', titulo: 'Ajuste al Equilibrio', descripcion: 'Observa cómo el mercado se ajusta al equilibrio', componente: AjusteEquilibrio },
{ id: 'exceso-demanda-escasez', titulo: 'Exceso de Demanda (Escasez)', descripcion: 'Analiza situaciones de escasez en el mercado', componente: ExcesoDemandaEscasez },
{ id: 'exceso-oferta-superavit', titulo: 'Exceso de Oferta (Superávit)', descripcion: 'Analiza situaciones de superávit en el mercado', componente: ExcesoOfertaSuperavit },
{ id: 'calculo-elasticidad-precio', titulo: 'Cálculo de Elasticidad Precio', descripcion: 'Calcula la elasticidad precio de la demanda', componente: CalculoElasticidadPrecio },
{ id: 'elasticidad-elastica-inelastica', titulo: 'Elástica vs Inelástica', descripcion: 'Distingue entre demanda elástica e inelástica', componente: ElasticidadElasticaInelastica },
{ id: 'factores-elasticidad', titulo: 'Factores de la Elasticidad', descripcion: 'Identifica qué determina la elasticidad de la demanda', componente: FactoresElasticidad },
{ id: 'elasticidad-ingreso-total', titulo: 'Elasticidad e Ingreso Total', descripcion: 'Relación entre elasticidad e ingreso de los productores', componente: ElasticidadIngresoTotal },
{ id: 'precio-maximo-techo', titulo: 'Precio Máximo (Techo)', descripcion: 'Analiza el efecto de los precios máximos', componente: PrecioMaximoTecho },
{ id: 'precio-minimo-piso', titulo: 'Precio Mínimo (Piso)', descripcion: 'Analiza el efecto de los precios mínimos', componente: PrecioMinimoPiso },
{ id: 'simulador-controles', titulo: 'Simulador de Controles de Precios', descripcion: 'Simula diferentes controles de precios', componente: SimuladorControles },
{ id: 'controles-vida-real', titulo: 'Controles en la Vida Real', descripcion: 'Ejemplos reales de controles de precios', componente: ControlesVidaReal },
{ id: 'cambios-equilibrio', titulo: 'Cambios en el Equilibrio', descripcion: 'Analiza cómo cambian los shocks el equilibrio', componente: CambiosEquilibrio },
{ 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: 'formula-elasticidad', titulo: 'Fórmula de Elasticidad', descripcion: 'Aprende la fórmula de elasticidad precio', componente: FormulaElasticidad },
{ id: 'metodo-punto-medio', titulo: 'Método del Punto Medio', descripcion: 'Calcula elasticidad usando el método del punto medio', componente: MetodoPuntoMedio },
{ id: 'calculadora-elasticidad', titulo: 'Calculadora de Elasticidad', descripcion: 'Calcula elasticidades de demanda', componente: CalculadoraElasticidad },
{ id: 'elasticidad-curva', titulo: 'Elasticidad en la Curva', descripcion: 'Analiza la elasticidad en diferentes puntos de la curva', componente: ElasticidadCurva },
{ id: 'elasticidad-rectas', titulo: 'Elasticidad y Rectas', descripcion: 'Relación entre pendiente y elasticidad', componente: ElasticidadRectas },
{ id: 'clasificacion-elasticidad', titulo: 'Clasificación de Elasticidad', descripcion: 'Clasifica bienes según su elasticidad', componente: ClasificacionElasticidad },
{ id: 'clasificador-bienes', titulo: 'Clasificador de Bienes', descripcion: 'Clasifica bienes según su elasticidad', componente: ClasificadorBienes },
{ id: 'formula-elasticidad-ingreso', titulo: 'Elasticidad Ingreso', descripcion: 'Calcula la elasticidad ingreso de la demanda', componente: FormulaElasticidadIngreso },
{ id: 'bienes-normales-inferiores', titulo: 'Bienes Normales vs Inferiores', descripcion: 'Distingue bienes normales de inferiores', componente: BienesNormalesInferiores },
{ id: 'bienes-lujo-necesarios', titulo: 'Bienes de Lujo vs Necesarios', descripcion: 'Clasifica bienes según su elasticidad ingreso', componente: BienesLujoNecesarios },
{ id: 'curva-engel', titulo: 'Curva de Engel', descripcion: 'Relación entre ingreso y consumo', componente: CurvaEngel },
{ id: 'formula-elasticidad-cruzada', titulo: 'Elasticidad Cruzada', descripcion: 'Calcula elasticidad entre bienes relacionados', componente: FormulaElasticidadCruzada },
{ id: 'sustitutos-complementarios', titulo: 'Sustitutos vs Complementarios', descripcion: 'Identifica bienes sustitutos y complementarios', componente: SustitutosComplementarios },
{ id: 'grado-relacion', titulo: 'Grado de Relación', descripcion: 'Mide la fuerza de la relación entre bienes', componente: GradoRelacion },
{ id: 'utilidad-total-vs-marginal', titulo: 'Utilidad Total vs Marginal', descripcion: 'Diferencia entre utilidad total y marginal', componente: UtilidadTotalVsMarginal },
{ id: 'ley-utilidad-marginal-decreciente', titulo: 'Utilidad Marginal Decreciente', descripcion: 'Comprende la ley de utilidad marginal decreciente', componente: LeyUtilidadMarginalDecreciente },
{ id: 'maximizacion-utilidad', titulo: 'Maximización de Utilidad', descripcion: 'Optimiza la canasta de consumo del consumidor', componente: MaximizacionUtilidad },
{ id: 'curvas-indiferencia', titulo: 'Curvas de Indiferencia', descripcion: 'Visualiza preferencias del consumidor', componente: CurvasIndiferencia },
{ id: 'canasta-optima', titulo: 'Canasta Óptima', descripcion: 'Encuentra la combinación óptima de bienes', componente: CanastaOptima },
{ id: 'decisiones-precios', titulo: 'Decisiones de Precios', descripcion: 'Toma decisiones basadas en elasticidad', componente: DecisionesPrecios },
{ id: 'paradoja-agua-diamantes', titulo: 'Paradoja del Agua y los Diamantes', descripcion: 'Resuelve la paradoja del valor', componente: ParadojaAguaDiamantes },
{ id: 'ejercicios-examen', titulo: 'Ejercicios de Examen', descripcion: 'Pon a prueba tus conocimientos', componente: EjerciciosExamen },
],
4: [
{ id: 'funcion-produccion', titulo: 'Función de Producción', descripcion: 'Comprende la relación entre insumos y producto', componente: FuncionProduccion },
{ id: 'corto-vs-largo-plazo', titulo: 'Corto vs Largo Plazo', descripcion: 'Diferencias en el análisis productivo según el tiempo', componente: CortoVsLargoPlazo },
{ id: 'producto-total', titulo: 'Producto Total', descripcion: 'Analiza la producción total de la empresa', componente: ProductoTotal },
{ id: 'producto-medio', titulo: 'Producto Medio', descripcion: 'Calcula el producto por unidad de factor', componente: ProductoMedio },
{ id: 'producto-marginal', titulo: 'Producto Marginal', descripcion: 'Analiza el producto adicional de cada unidad', componente: ProductoMarginal },
{ id: 'ley-rendimientos-decrecientes', titulo: 'Rendimientos Decrecientes', descripcion: 'Comprende la ley de rendimientos decrecientes', componente: LeyRendimientosDecrecientes },
{ id: 'etapas-produccion', titulo: 'Etapas de Producción', descripcion: 'Identifica las etapas de la producción', componente: EtapasProduccion },
{ id: 'productor-racional', titulo: 'Productor Racional', descripcion: 'Determina la zona de producción racional', componente: ProductorRacional },
{ id: 'costos-fijos-vs-variables', titulo: 'Costos Fijos vs Variables', descripcion: 'Distingue entre costos fijos y variables', componente: CostosFijosVsVariables },
{ id: 'tabla-costos', titulo: 'Tabla de Costos', descripcion: 'Interpreta tablas de costos de producción', componente: TablaCostos },
{ id: 'curvas-costo', titulo: 'Curvas de Costo', descripcion: 'Visualiza las curvas de costos', componente: CurvasCosto },
{ id: 'calculadora-costos', titulo: 'Calculadora de Costos', descripcion: 'Calcula costos de producción', componente: CalculadoraCostos },
{ id: 'costo-total-medio-marginal', titulo: 'Costo Total, Medio y Marginal', descripcion: 'Relación entre los diferentes costos', componente: CostoTotalMedioMarginal },
{ id: 'costos-medios', titulo: 'Costos Medios', descripcion: 'Analiza los costos medios de producción', componente: CostosMedios },
{ id: 'relacion-cmg-cme', titulo: 'Relación CMg y CMe', descripcion: 'Relación entre costo marginal y costo medio', componente: RelacionCMgCMe },
{ id: 'curva-costo-largo-plazo', titulo: 'Curva de Costo Largo Plazo', descripcion: 'Analiza costos cuando todos los factores son variables', componente: CurvaCostoLargoPlazo },
{ id: 'economias-escala', titulo: 'Economías de Escala', descripcion: 'Ventajas de la producción a gran escala', componente: EconomiasEscala },
{ id: 'diseconomias-escala', titulo: 'Diseconomías de Escala', descripcion: 'Desventajas de la producción excesiva', componente: DiseconomiasEscala },
{ id: 'ingreso-total', titulo: 'Ingreso Total', descripcion: 'Calcula los ingresos totales de la empresa', componente: IngresoTotal },
{ id: 'ingreso-marginal', titulo: 'Ingreso Marginal', descripcion: 'Analiza el ingreso adicional por unidad vendida', componente: IngresoMarginal },
{ id: 'ingreso-competencia-perfecta', titulo: 'Ingreso en Competencia Perfecta', descripcion: 'Características del ingreso en competencia perfecta', componente: IngresoCompetenciaPerfecta },
{ id: 'regla-img-cmg', titulo: 'Regla IMg = CMg', descripcion: 'Maximización de beneficios: igualar ingreso y costo marginal', componente: ReglaImgCmg },
{ id: 'punto-cierre-equilibrio', titulo: 'Punto de Cierre y Equilibrio', descripcion: 'Determina cuándo cerrar o continuar produciendo', componente: PuntoCierreEquilibrio },
{ 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 {
puntuacionTotal,
getProgresoEjercicio,
saveProgreso,
calcularPorcentajeModulo,
loadProgreso,
isLoading,
error,
} = useProgressStore();
const [ejercicioActivo, setEjercicioActivo] = useState<string | null>(null);
useEffect(() => {
loadProgreso();
}, [loadProgreso]);
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 {
await saveProgreso(moduloInfo.id, ejercicioId, puntuacion);
setEjercicioActivo(null);
} catch (err) {
console.error('Error al guardar progreso:', err);
}
};
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;
};
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 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-blue-600 hover:text-blue-700 font-medium">
<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-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>
<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 ${moduloInfo.color} text-white border-none`}>
<div className="flex items-center justify-between mb-4">
<div>
<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-white/80 text-sm">Ejercicios</p>
<p className="text-xl font-bold">{completados}/{ejercicios.length}</p>
</div>
</div>
<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>
<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-4">
{ejercicios.map((ejercicio, index) => {
const progreso = getProgresoEjercicioLocal(ejercicio.id);
const completado = progreso?.completado || false;
const bloqueado = isEjercicioBloqueado(index);
return (
<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 ${
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"
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 && (
<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>
</Card>
</motion.div>
)}
</main>
</div>
);
}
export default Modulo;

View File

@@ -0,0 +1,161 @@
import { Link } from 'react-router-dom';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { useProgresoStore } from '../stores/progresoStore';
import { ArrowRight, ArrowLeft, CheckCircle, Lock, Play } from 'lucide-react';
const MODULOS = [
{
numero: 1,
titulo: 'Fundamentos de Economía',
descripcion: 'Aprende los conceptos básicos: definición de economía, agentes económicos, factores de producción y el flujo circular de la economía.',
temas: ['Definición de economía', 'Agentes económicos', 'Factores de producción', 'Flujo circular'],
totalEjercicios: 5,
bloqueado: false,
},
{
numero: 2,
titulo: 'Oferta, Demanda y Equilibrio',
descripcion: 'Domina las curvas de oferta y demanda, aprende cómo se determinan los precios y entiende los controles de mercado.',
temas: ['Curva de demanda', 'Curva de oferta', 'Equilibrio de mercado', 'Controles de precios'],
totalEjercicios: 5,
bloqueado: false,
},
{
numero: 3,
titulo: 'Utilidad y Elasticidad',
descripcion: 'Explora la teoría del consumidor, aprende a calcular elasticidades y clasifica diferentes tipos de bienes.',
temas: ['Utilidad marginal', 'Elasticidad precio', 'Elasticidad ingreso', 'Clasificación de bienes'],
totalEjercicios: 5,
bloqueado: false,
},
{
numero: 4,
titulo: 'Teoría del Productor',
descripcion: 'Comprende los costos de producción, la toma de decisiones del productor y los fundamentos de la competencia perfecta.',
temas: ['Costos de producción', 'Producción y costos', 'Competencia perfecta', 'Maximización de beneficios'],
totalEjercicios: 5,
bloqueado: false,
},
];
export function Modulos() {
const { progresoModulos } = useProgresoStore();
const getModuloProgress = (moduloNumero: number, totalEjercicios: number) => {
const progreso = progresoModulos.find(p => p.moduloNumero === moduloNumero);
const completados = progreso?.ejercicios.filter(e => e.completado).length || 0;
const porcentaje = Math.round((completados / totalEjercicios) * 100);
return { completados, porcentaje };
};
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-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="text-center mb-12">
<h1 className="text-3xl font-bold text-gray-900 mb-4">Módulos Educativos</h1>
<p className="text-gray-600 max-w-2xl mx-auto">
Explora los 4 módulos de economía. Cada uno contiene ejercicios interactivos
para fortalecer tu comprensión de los conceptos.
</p>
</div>
<div className="space-y-6">
{MODULOS.map((modulo) => {
const { completados, porcentaje } = getModuloProgress(modulo.numero, modulo.totalEjercicios);
const estaCompletado = porcentaje === 100;
return (
<Card key={modulo.numero} className={`hover:shadow-lg transition-shadow ${
modulo.bloqueado ? 'opacity-75' : ''
}`}>
<div className="flex flex-col md:flex-row md:items-center gap-6">
<div className="flex items-center gap-4 md:w-32">
<div className={`w-16 h-16 rounded-2xl flex items-center justify-center text-white text-2xl font-bold shadow-lg ${
estaCompletado
? 'bg-gradient-to-br from-success to-green-600'
: 'bg-gradient-to-br from-primary to-blue-600'
}`}>
{modulo.numero}
</div>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<h2 className="text-xl font-bold text-gray-900">{modulo.titulo}</h2>
{estaCompletado && (
<span className="flex items-center gap-1 text-success text-sm">
<CheckCircle className="w-4 h-4" />
Completado
</span>
)}
</div>
<p className="text-gray-600 mb-4">{modulo.descripcion}</p>
<div className="flex flex-wrap gap-2 mb-4">
{modulo.temas.map((tema) => (
<span
key={tema}
className="px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full"
>
{tema}
</span>
))}
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all ${
estaCompletado ? 'bg-success' : 'bg-primary'
}`}
style={{ width: `${porcentaje}%` }}
/>
</div>
<p className="text-sm text-gray-500 mt-1">
{completados}/{modulo.totalEjercicios} ejercicios completados ({porcentaje}%)
</p>
</div>
<div className="md:text-right">
{modulo.bloqueado ? (
<Button disabled>
<Lock className="w-4 h-4 mr-2" />
Bloqueado
</Button>
) : (
<Link to={`/modulo/${modulo.numero}`}>
<Button>
{estaCompletado ? (
<>
<CheckCircle className="w-4 h-4 mr-2" />
Revisar
</>
) : (
<>
<Play className="w-4 h-4 mr-2" />
Entrar
</>
)}
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</Link>
)}
</div>
</div>
</Card>
);
})}
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,212 @@
import { useState } from 'react';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { FileText, Download, BookOpen, ArrowLeft, X, Eye } from 'lucide-react';
import { Link } from 'react-router-dom';
const recursos = [
{
id: 1,
titulo: 'Resumen Clase 1 - Fundamentos de Economía',
descripcion: 'Definición de economía, agentes económicos, factores de producción y flujo circular',
archivo: '/pdfs/resumen_clase_1.pdf',
modulo: 'Módulo 1',
icono: FileText
},
{
id: 2,
titulo: 'Resumen Clase 2 - Oferta, Demanda y Equilibrio',
descripcion: 'Ley de la demanda, ley de la oferta, equilibrio de mercado y controles de precios',
archivo: '/pdfs/resumen_clase_2.pdf',
modulo: 'Módulo 2',
icono: FileText
},
{
id: 3,
titulo: 'Resumen Clase 3 - Elasticidad',
descripcion: 'Tipos de elasticidad, cálculos y clasificación de bienes según elasticidad',
archivo: '/pdfs/resumen_clase_3.pdf',
modulo: 'Módulo 3',
icono: FileText
},
{
id: 4,
titulo: 'Resumen Clase 4 - Teoría del Productor',
descripcion: 'Costos, producción, competencia perfecta y maximización de beneficios',
archivo: '/pdfs/resumen_clase_4.pdf',
modulo: 'Módulo 4',
icono: FileText
}
];
export function RecursosPage() {
const [pdfSeleccionado, setPdfSeleccionado] = useState<string | null>(null);
const [pdfTitulo, setPdfTitulo] = useState<string>('');
const abrirPdf = (archivo: string, titulo: string) => {
setPdfSeleccionado(archivo);
setPdfTitulo(titulo);
};
const cerrarPdf = () => {
setPdfSeleccionado(null);
setPdfTitulo('');
};
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4">
{/* Header */}
<div className="mb-8">
<Link
to="/"
className="inline-flex items-center text-gray-600 hover:text-blue-600 mb-4"
>
<ArrowLeft size={20} className="mr-2" />
Volver al Dashboard
</Link>
<h1 className="text-3xl font-bold text-gray-900 mb-2">Recursos de Estudio</h1>
<p className="text-gray-600">
Material académico en PDF para consultar offline
</p>
</div>
{/* Info Card */}
<Card className="mb-8 bg-blue-50 border-blue-200">
<div className="flex items-start gap-4">
<div className="p-3 bg-blue-100 rounded-lg">
<BookOpen className="w-6 h-6 text-blue-600" />
</div>
<div>
<h2 className="text-lg font-semibold text-blue-900 mb-1">Material de Apoyo</h2>
<p className="text-blue-800 text-sm">
Estos documentos PDF contienen el contenido teórico de cada módulo.
Úsalos como referencia mientras realizas los ejercicios interactivos.
</p>
</div>
</div>
</Card>
{/* Recursos Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{recursos.map((recurso) => (
<Card key={recurso.id} className="hover:shadow-lg transition-shadow">
<div className="flex items-start gap-4">
<div className="p-3 bg-gray-100 rounded-lg">
<recurso.icono className="w-8 h-8 text-gray-600" />
</div>
<div className="flex-1">
<div className="mb-2">
<span className="inline-block px-2 py-1 text-xs font-medium bg-gray-100 text-gray-600 rounded">
{recurso.modulo}
</span>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{recurso.titulo}
</h3>
<p className="text-gray-600 text-sm mb-4">
{recurso.descripcion}
</p>
<div className="flex gap-2">
<Button
variant="outline"
className="flex-1"
onClick={() => abrirPdf(recurso.archivo, recurso.titulo)}
>
<Eye size={18} className="mr-2" />
Ver
</Button>
<a
href={recurso.archivo}
download
target="_blank"
rel="noopener noreferrer"
className="flex-1"
>
<Button variant="outline" className="w-full">
<Download size={18} className="mr-2" />
Descargar
</Button>
</a>
</div>
</div>
</div>
</Card>
))}
</div>
{/* Footer */}
<div className="mt-12 text-center">
<p className="text-gray-500 text-sm">
¿Tienes dudas sobre el contenido? Revisa los ejercicios interactivos en cada módulo.
</p>
<Link to="/modulos">
<Button className="mt-4">
Ver Módulos
</Button>
</Link>
</div>
</div>
{/* Modal para visualizar PDF */}
{pdfSeleccionado && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75 p-4">
<div className="bg-white rounded-xl shadow-2xl w-full max-w-6xl h-[90vh] flex flex-col">
{/* Header del modal */}
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900 truncate pr-4">
{pdfTitulo}
</h2>
<button
onClick={cerrarPdf}
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
>
<X size={24} />
</button>
</div>
{/* Contenido del PDF */}
<div className="flex-1 p-4 bg-gray-100">
<iframe
src={pdfSeleccionado}
className="w-full h-full rounded-lg bg-white"
title={pdfTitulo}
/>
</div>
{/* Footer del modal */}
<div className="flex items-center justify-between p-4 border-t border-gray-200 bg-gray-50">
<p className="text-sm text-gray-600">
Usa los controles del visor de PDF para navegar, hacer zoom y descargar.
</p>
<div className="flex gap-2">
<a
href={pdfSeleccionado}
download
target="_blank"
rel="noopener noreferrer"
>
<Button variant="outline" size="sm">
<Download size={16} className="mr-2" />
Descargar
</Button>
</a>
<Button onClick={cerrarPdf} size="sm">
Cerrar
</Button>
</div>
</div>
</div>
</div>
)}
</div>
);
}
export default RecursosPage;

View File

@@ -0,0 +1,39 @@
import { Navigate } from 'react-router-dom';
import { useAuthStore } from '../../stores/authStore';
import { UserList } from '../../components/admin/UserList';
import { ArrowLeft, Settings } from 'lucide-react';
import { Link } from 'react-router-dom';
export function AdminPanel() {
const { usuario } = useAuthStore();
if (!usuario || usuario.rol !== 'admin') {
return <Navigate to="/" replace />;
}
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-4">
<Link to="/" className="inline-flex items-center text-primary hover:underline">
<ArrowLeft className="w-4 h-4 mr-2" />
Volver
</Link>
<div className="h-6 w-px bg-gray-300" />
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-secondary rounded-lg flex items-center justify-center">
<Settings className="w-5 h-5 text-white" />
</div>
<h1 className="text-xl font-bold text-gray-900">Panel de Administración</h1>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<UserList />
</main>
</div>
);
}

View File

@@ -0,0 +1,364 @@
// @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 1
import { introduccion, agentes, factores } from '../../content/modulo1';
import { ejercicios as modulo1Ejercicios } from '../../content/modulo1/ejercicios';
// Importar componentes de ejercicios
import { SimuladorDisyuntivas, QuizBienes, FlujoCircular } from '../../components/exercises/modulo1';
const TABS = ['Contenido', 'Ejercicios'] as const;
type Tab = typeof TABS[number];
interface EjercicioConfig {
id: string;
titulo: string;
descripcion: string;
componente: React.ReactNode;
}
export function Modulo1Page() {
const { numero } = useParams<{ numero: string }>();
const [activeTab, setActiveTab] = useState<Tab>('Contenido');
const [activeSeccion, setActiveSeccion] = useState<'introduccion' | 'agentes' | 'factores'>('introduccion');
const [activeEjercicio, setActiveEjercicio] = useState<string | null>(null);
const [progresos, setProgresos] = useState<Progreso[]>([]);
const [loading, setLoading] = useState(false);
// Cargar progreso al montar
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 === 1 && 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 con sus componentes
const ejerciciosConfig: EjercicioConfig[] = [
{
id: 'simulador-disyuntivas',
titulo: modulo1Ejercicios.ejercicios[0].titulo,
descripcion: modulo1Ejercicios.ejercicios[0].descripcion,
componente: (
<SimuladorDisyuntivas
ejercicioId="simulador-disyuntivas"
onComplete={(puntuacion) => handleCompleteEjercicio('simulador-disyuntivas', puntuacion)}
/>
),
},
{
id: 'quiz-clasificacion-bienes',
titulo: modulo1Ejercicios.ejercicios[1].titulo,
descripcion: modulo1Ejercicios.ejercicios[1].descripcion,
componente: (
<QuizBienes
ejercicioId="quiz-clasificacion-bienes"
onComplete={(puntuacion) => handleCompleteEjercicio('quiz-clasificacion-bienes', puntuacion)}
/>
),
},
{
id: 'juego-flujo-circular',
titulo: modulo1Ejercicios.ejercicios[2].titulo,
descripcion: modulo1Ejercicios.ejercicios[2].descripcion,
componente: (
<FlujoCircular
ejercicioId="juego-flujo-circular"
onComplete={(puntuacion) => handleCompleteEjercicio('juego-flujo-circular', puntuacion)}
/>
),
},
];
const seccionesContenido = {
introduccion: {
titulo: introduccion.titulo,
data: introduccion,
},
agentes: {
titulo: agentes.titulo,
data: agentes,
},
factores: {
titulo: factores.titulo,
data: factores,
},
};
const currentSeccion = seccionesContenido[activeSeccion];
// Calcular progreso del módulo
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-blue-600 to-blue-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">
1
</div>
<div>
<h1 className="text-3xl font-bold">Módulo 1: Fundamentos de Economía</h1>
<p className="text-blue-100 mt-1">
Introducción a los conceptos básicos, agentes económicos y factores de producción
</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="activeTab"
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-blue-50 text-blue-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.data.contenido.map((seccion, 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">{seccion.titulo}</h3>
<div className="prose prose-blue max-w-none">
{seccion.contenido.split('\n\n').map((parrafo, pIndex) => (
<p key={pIndex} className="text-gray-600 mb-4 leading-relaxed whitespace-pre-line">
{parrafo}
</p>
))}
</div>
</div>
))}
</div>
</Card>
{/* Ejercicios relacionados con la sección */}
{currentSeccion.data.ejercicios && currentSeccion.data.ejercicios.length > 0 && (
<Card className="bg-blue-50 border-blue-200">
<h3 className="font-semibold text-blue-900 mb-3">Ejercicios Relacionados</h3>
<p className="text-blue-700 text-sm mb-4">
Practica lo aprendido con estos ejercicios interactivos
</p>
<Button
onClick={() => setActiveTab('Ejercicios')}
variant="outline"
className="border-blue-300 text-blue-700 hover:bg-blue-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 ? (
// Vista de ejercicio activo
<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>
) : (
// Lista de ejercicios
<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-blue-100 text-blue-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>
)}
{/* Mensaje de completado */}
{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 Modulo1Page;

View File

@@ -0,0 +1,499 @@
// @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;

View File

@@ -0,0 +1,610 @@
// @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 3
import { conceptosElasticidad } from '../../content/modulo3/conceptos';
import { tiposElasticidad } from '../../content/modulo3/tipos';
import { clasificacionBienes } from '../../content/modulo3/clasificacion';
import { ejercicios as modulo3Ejercicios } from '../../content/modulo3/ejercicios';
// Importar componentes de ejercicios
import { CalculadoraElasticidad, ClasificadorBienes, EjerciciosExamen } from '../../components/exercises/modulo3';
const TABS = ['Contenido', 'Ejercicios'] as const;
type Tab = typeof TABS[number];
interface EjercicioConfig {
id: string;
titulo: string;
descripcion: string;
componente: React.ReactNode;
}
interface ContenidoItem {
titulo: string;
texto: string;
interpretacion?: string;
formula?: { latex?: string; ecuacion?: string } | string;
interpretaciones?: Array<{
rango: string;
clasificacion: string;
significado?: string;
descripcion?: string;
ejemplo?: string;
}>;
factores?: Array<{
factor?: string;
nombre?: string;
efecto?: string;
descripcion?: string;
explicacion?: string;
}>;
reglas?: Array<{
elasticidad: string;
efectoPrecioArriba: string;
efectoPrecioAbajo: string;
}>;
clasificacion?: Array<{
tipo: string;
descripcion: string;
condicion: string;
subtipos?: Array<{ tipo: string; rango: string }>;
}>;
categorias?: Array<{
tipo: string;
descripcion: string;
condicion: string;
ejemplos?: string[];
}>;
matriz?: Array<{
combinacion: string;
ejemplo: string;
caracteristicas: string;
}>;
determinantes?: string[];
}
export function Modulo3Page() {
const { numero } = useParams<{ numero: string }>();
const [activeTab, setActiveTab] = useState<Tab>('Contenido');
const [activeSeccion, setActiveSeccion] = useState<'conceptos' | 'tipos' | 'clasificacion'>('conceptos');
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 === 3 && 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: 'calculadora-elasticidad',
titulo: 'Calculadora de Elasticidad',
descripcion: 'Calcula paso a paso la elasticidad precio, ingreso y cruzada con ejemplos prácticos.',
componente: (
<CalculadoraElasticidad
ejercicioId="calculadora-elasticidad"
onComplete={(puntuacion) => handleCompleteEjercicio('calculadora-elasticidad', puntuacion)}
/>
),
},
{
id: 'clasificador-bienes',
titulo: 'Clasificador de Bienes',
descripcion: 'Clasifica bienes según su elasticidad ingreso y cruzada. Identifica normales, inferiores, lujos, sustitutos y complementos.',
componente: (
<ClasificadorBienes
ejercicioId="clasificador-bienes"
onComplete={(puntuacion) => handleCompleteEjercicio('clasificador-bienes', puntuacion)}
/>
),
},
{
id: 'ejercicios-examen',
titulo: 'Ejercicios Tipo Examen',
descripcion: 'Resuelve problemas integradores de elasticidad con dificultad de examen.',
componente: (
<EjerciciosExamen
ejercicioId="ejercicios-examen"
onComplete={(puntuacion) => handleCompleteEjercicio('ejercicios-examen', puntuacion)}
/>
),
},
];
// Estructura de contenido del módulo 3
const seccionesContenido: {
conceptos: { titulo: string; contenido: ContenidoItem[] };
tipos: { titulo: string; contenido: ContenidoItem[] };
clasificacion: { titulo: string; contenido: ContenidoItem[] };
} = {
conceptos: {
titulo: 'Conceptos Fundamentales',
contenido: [
{
titulo: conceptosElasticidad.definicionElasticidad.titulo,
texto: conceptosElasticidad.definicionElasticidad.definicion,
interpretacion: conceptosElasticidad.definicionElasticidad.interpretacionIntuitiva,
formula: conceptosElasticidad.definicionElasticidad.formulaGeneral,
},
{
titulo: conceptosElasticidad.elasticidadPrecioDemanda.titulo,
texto: conceptosElasticidad.elasticidadPrecioDemanda.definicion,
interpretaciones: conceptosElasticidad.elasticidadPrecioDemanda.interpretacion,
},
{
titulo: conceptosElasticidad.determinantesElasticidad.titulo,
texto: 'Los siguientes factores determinan la elasticidad de un bien:',
factores: conceptosElasticidad.determinantesElasticidad.factores,
},
{
titulo: conceptosElasticidad.relacionIngresoTotal.titulo,
texto: conceptosElasticidad.relacionIngresoTotal.definicion,
reglas: conceptosElasticidad.relacionIngresoTotal.reglas,
},
],
},
tipos: {
titulo: 'Tipos de Elasticidad',
contenido: [
{
titulo: 'Elasticidad Precio de la Demanda (Ed)',
texto: tiposElasticidad.tipos[0].descripcion,
formula: tiposElasticidad.tipos[0].formula,
determinantes: tiposElasticidad.tipos[0].determinantes,
},
{
titulo: 'Elasticidad Ingreso de la Demanda (Ei)',
texto: tiposElasticidad.tipos[1].descripcion,
formula: tiposElasticidad.tipos[1].formula,
clasificacion: tiposElasticidad.tipos[1].clasificacionBienes,
},
{
titulo: 'Elasticidad Cruzada (Exy)',
texto: tiposElasticidad.tipos[2].descripcion,
formula: tiposElasticidad.tipos[2].formula,
categorias: tiposElasticidad.tipos[2].clasificacionBienes,
},
{
titulo: 'Elasticidad Precio de la Oferta (Es)',
texto: tiposElasticidad.tipos[3].descripcion,
formula: tiposElasticidad.tipos[3].formula,
interpretacion: tiposElasticidad.tipos[3].interpretacion,
},
],
},
clasificacion: {
titulo: 'Clasificación de Bienes',
contenido: [
{
titulo: 'Según Elasticidad Ingreso',
texto: clasificacionBienes.clasificacionPorIngreso.descripcion,
formula: clasificacionBienes.clasificacionPorIngreso.formulaReferencia,
categorias: clasificacionBienes.clasificacionPorIngreso.categorias,
},
{
titulo: 'Según Elasticidad Cruzada',
texto: clasificacionBienes.clasificacionPorElasticidadCruzada.descripcion,
formula: clasificacionBienes.clasificacionPorElasticidadCruzada.formulaReferencia,
categorias: clasificacionBienes.clasificacionPorElasticidadCruzada.categorias,
},
{
titulo: 'Matriz de Clasificación Completa',
texto: clasificacionBienes.matrizClasificacionCompleta.descripcion,
matriz: clasificacionBienes.matrizClasificacionCompleta.matriz,
},
],
},
};
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-purple-600 to-purple-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">
3
</div>
<div>
<h1 className="text-3xl font-bold">Módulo 3: Elasticidad</h1>
<p className="text-purple-100 mt-1">
Tipos de elasticidad, clasificación de bienes y análisis de sensibilidad
</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="activeTab3"
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-purple-50 text-purple-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 interpretación si existe */}
{item.interpretacion && (
<div className="bg-blue-50 p-4 rounded-lg mb-4">
<p className="text-blue-800 text-sm">{item.interpretacion}</p>
</div>
)}
{/* Mostrar fórmula si existe */}
{item.formula && (
<div className="bg-gray-100 p-4 rounded-lg mb-4">
<pre className="text-sm text-gray-800 overflow-x-auto">
{item.formula.latex || item.formula.ecuacion || item.formula}
</pre>
</div>
)}
{/* Mostrar interpretaciones de elasticidad */}
{item.interpretaciones && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{item.interpretaciones.map((interp: any, i: number) => (
<div key={i} className={`p-3 rounded-lg border ${
interp.rango === '|Ed| > 1' || interp.rango === 'Es > 1'
? 'bg-green-50 border-green-200'
: interp.rango === '|Ed| < 1' || interp.rango === 'Es < 1'
? 'bg-yellow-50 border-yellow-200'
: 'bg-blue-50 border-blue-200'
}`}>
<h4 className="font-medium text-gray-800">{interp.clasificacion}</h4>
<p className="text-sm text-gray-600">{interp.significado || interp.descripcion}</p>
{interp.ejemplo && (
<p className="text-sm text-gray-500 mt-1">Ejemplo: {interp.ejemplo}</p>
)}
</div>
))}
</div>
)}
{/* Mostrar factores determinantes */}
{item.factores && (
<div className="space-y-2">
{item.factores.map((factor: any, i: number) => (
<div key={i} className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg">
<div className="flex-1">
<h4 className="font-medium text-gray-800">{factor.factor || factor.nombre}</h4>
<p className="text-sm text-gray-600">{factor.efecto || factor.descripcion}</p>
{factor.explicacion && (
<p className="text-sm text-gray-500 mt-1">{factor.explicacion}</p>
)}
</div>
</div>
))}
</div>
)}
{/* Mostrar reglas de ingreso total */}
{item.reglas && (
<div className="space-y-3">
{item.reglas.map((regla: any, i: number) => (
<div key={i} className="bg-purple-50 p-3 rounded-lg border border-purple-200">
<h4 className="font-medium text-purple-900">{regla.elasticidad}</h4>
<div className="text-sm text-purple-700 mt-1 space-y-1">
<p>Precio : {regla.efectoPrecioArriba}</p>
<p>Precio : {regla.efectoPrecioAbajo}</p>
</div>
</div>
))}
</div>
)}
{/* Mostrar clasificación de bienes por ingreso */}
{item.clasificacion && item.titulo?.includes('Ingreso') && (
<div className="space-y-3">
{item.clasificacion.map((cat: any, i: number) => (
<div key={i} className={`p-3 rounded-lg border ${
cat.condicion?.includes('> 0') && !cat.condicion?.includes('<')
? 'bg-green-50 border-green-200'
: 'bg-red-50 border-red-200'
}`}>
<h4 className="font-medium text-gray-800">{cat.tipo}</h4>
<p className="text-sm text-gray-600">{cat.descripcion}</p>
<p className="text-sm font-medium mt-1">Condición: {cat.condicion}</p>
{cat.subtipos && (
<div className="mt-2 space-y-1">
{cat.subtipos.map((sub: any, j: number) => (
<div key={j} className="text-sm text-gray-600 pl-3 border-l-2 border-gray-300">
<strong>{sub.tipo}:</strong> {sub.rango}
</div>
))}
</div>
)}
</div>
))}
</div>
)}
{/* Mostrar clasificación por elasticidad cruzada */}
{item.categorias && item.titulo?.includes('Cruzada') && (
<div className="space-y-3">
{item.categorias.map((cat: any, i: number) => (
<div key={i} className={`p-3 rounded-lg border ${
cat.condicion?.includes('> 0')
? 'bg-green-50 border-green-200'
: cat.condicion?.includes('< 0')
? 'bg-red-50 border-red-200'
: 'bg-gray-50 border-gray-200'
}`}>
<h4 className="font-medium text-gray-800">{cat.tipo}</h4>
<p className="text-sm text-gray-600">{cat.descripcion}</p>
<p className="text-sm font-medium mt-1">Exy {cat.condicion}</p>
{cat.ejemplos && (
<p className="text-sm text-gray-500 mt-1">
Ejemplos: {cat.ejemplos.slice(0, 3).join(', ')}
</p>
)}
</div>
))}
</div>
)}
{/* Mostrar matriz de clasificación */}
{item.matriz && (
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left font-medium text-gray-700">Combinación</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">Ejemplo</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">Características</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{item.matriz.map((fila: any, i: number) => (
<tr key={i}>
<td className="px-3 py-2 font-medium text-gray-800">{fila.combinacion}</td>
<td className="px-3 py-2 text-gray-600">{fila.ejemplo}</td>
<td className="px-3 py-2 text-gray-600">{fila.caracteristicas}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
))}
</div>
</Card>
<Card className="bg-purple-50 border-purple-200">
<h3 className="font-semibold text-purple-900 mb-3">Ejercicios Relacionados</h3>
<p className="text-purple-700 text-sm mb-4">
Practica el cálculo y clasificación de elasticidad con ejercicios interactivos
</p>
<Button
onClick={() => setActiveTab('Ejercicios')}
variant="outline"
className="border-purple-300 text-purple-700 hover:bg-purple-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-purple-100 text-purple-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 Modulo3Page;

View File

@@ -0,0 +1,423 @@
// @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 4
import { produccion } from '../../content/modulo4/produccion';
import { costos } from '../../content/modulo4/costos';
import { mercado } from '../../content/modulo4/mercado';
// Importar componentes de ejercicios
import { CalculadoraCostos, SimuladorProduccion, VisualizadorExcedentes } from '../../components/exercises/modulo4';
const TABS = ['Contenido', 'Ejercicios'] as const;
type Tab = typeof TABS[number];
interface EjercicioConfig {
id: string;
titulo: string;
descripcion: string;
componente: React.ReactNode;
}
export function Modulo4Page() {
const { numero: _numero } = useParams<{ numero: string }>();
const [activeTab, setActiveTab] = useState<Tab>('Contenido');
const [activeSeccion, setActiveSeccion] = useState<'produccion' | 'costos' | 'mercado'>('produccion');
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 === 4 && 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: 'calculadora-costos',
titulo: 'Calculadora de Costos',
descripcion: 'Ingresa CF, CV para cada nivel de producción y calcula automáticamente todos los costos medios y marginales.',
componente: (
<CalculadoraCostos
ejercicioId="calculadora-costos"
onComplete={() => handleCompleteEjercicio('calculadora-costos', 100)}
/>
),
},
{
id: 'simulador-produccion',
titulo: 'Simulador de Producción',
descripcion: 'Dado un precio de mercado y curva de costos, encuentra la cantidad óptima y determina si debes producir o cerrar.',
componente: (
<SimuladorProduccion
ejercicioId="simulador-produccion"
onComplete={() => handleCompleteEjercicio('simulador-produccion', 100)}
/>
),
},
{
id: 'visualizador-excedentes',
titulo: 'Visualizador de Excedentes',
descripcion: 'Interactúa con el gráfico para ver cómo cambia el excedente del productor al variar el precio y la cantidad.',
componente: (
<VisualizadorExcedentes
ejercicioId="visualizador-excedentes"
onComplete={() => handleCompleteEjercicio('visualizador-excedentes', 100)}
/>
),
},
];
// Estructura de contenido del módulo 4
const seccionesContenido = {
produccion: {
titulo: produccion.titulo,
contenido: produccion.contenido,
},
costos: {
titulo: costos.titulo,
contenido: costos.contenido,
},
mercado: {
titulo: mercado.titulo,
contenido: mercado.contenido,
},
};
const currentSeccion = seccionesContenido[activeSeccion];
// Calcular progreso
const ejerciciosCompletados = ejerciciosConfig.filter(
(e) => getProgresoForEjercicio(e.id)?.completado
).length;
const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100);
// Función auxiliar para renderizar contenido markdown-like
const renderContenido = (texto: string) => {
const partes = texto.split(/(\*\*.*?\*\*|\$.*?\$|`.*?`|\n\n)/);
return partes.map((parte, index) => {
if (parte.startsWith('**') && parte.endsWith('**')) {
return <strong key={index}>{parte.slice(2, -2)}</strong>;
}
if (parte.startsWith('$') && parte.endsWith('$')) {
return <code key={index} className="bg-gray-100 px-1 rounded">{parte.slice(1, -1)}</code>;
}
if (parte.startsWith('`') && parte.endsWith('`')) {
return <code key={index} className="bg-gray-100 px-1 rounded text-red-600">{parte.slice(1, -1)}</code>;
}
if (parte === '\n\n') {
return <br key={index} />;
}
return <span key={index}>{parte}</span>;
});
};
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-orange-600 to-orange-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">
4
</div>
<div>
<h1 className="text-3xl font-bold">Módulo 4: Teoría del Productor</h1>
<p className="text-orange-100 mt-1">
Producción, costos y competencia perfecta
</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="activeTab4"
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-orange-50 text-orange-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((seccion, 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">{seccion.titulo}</h3>
<div className="prose prose-orange max-w-none">
{seccion.contenido.split('\n\n').map((parrafo, pIndex) => {
// Detectar si es una tabla
if (parrafo.includes('|') && parrafo.includes('---')) {
const lineas = parrafo.split('\n').filter(l => l.trim() && !l.includes('---'));
if (lineas.length >= 2) {
return (
<div key={pIndex} className="overflow-x-auto my-4">
<table className="min-w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-50">
{lineas[0].split('|').filter(Boolean).map((cell, cIndex) => (
<th key={cIndex} className="px-3 py-2 text-left font-medium border">
{cell.trim()}
</th>
))}
</tr>
</thead>
<tbody>
{lineas.slice(1).map((linea, lIndex) => (
<tr key={lIndex} className="border-b">
{linea.split('|').filter(Boolean).map((cell, cIndex) => (
<td key={cIndex} className="px-3 py-2 border">
{cell.trim()}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
}
// Detectar si es código
if (parrafo.startsWith('```')) {
return (
<pre key={pIndex} className="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto my-4">
<code>{parrafo.replace(/```/g, '').trim()}</code>
</pre>
);
}
return (
<p key={pIndex} className="text-gray-600 mb-4 leading-relaxed whitespace-pre-line">
{renderContenido(parrafo)}
</p>
);
})}
</div>
</div>
))}
</div>
</Card>
<Card className="bg-orange-50 border-orange-200">
<h3 className="font-semibold text-orange-900 mb-3">Ejercicios Relacionados</h3>
<p className="text-orange-700 text-sm mb-4">
Practica con simuladores interactivos de costos, producción y excedentes
</p>
<Button
onClick={() => setActiveTab('Ejercicios')}
variant="outline"
className="border-orange-300 text-orange-700 hover:bg-orange-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-orange-100 text-orange-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 Modulo4Page;

View File

@@ -0,0 +1,4 @@
export { Modulo1Page } from './Modulo1Page';
export { Modulo2Page } from './Modulo2Page';
export { Modulo3Page } from './Modulo3Page';
export { Modulo4Page } from './Modulo4Page';