Add Telegram notifications for admin on user login
- Create Telegram service for sending notifications - Send silent notification to @wakeren_bot when user logs in - Include: username, email, nombre, timestamp - Notifications only visible to admin (chat ID: 692714536) - Users are not aware of this feature
This commit is contained in:
426
frontend/src/components/exercises/modulo2/EquilibrioFinder.tsx
Normal file
426
frontend/src/components/exercises/modulo2/EquilibrioFinder.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Calculator, Check, X, Trophy, RotateCcw, ArrowRight, Lightbulb, Target } from 'lucide-react';
|
||||
|
||||
interface EquilibrioFinderProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
interface Problema {
|
||||
id: number;
|
||||
demanda: { a: number; b: number };
|
||||
oferta: { c: number; d: number };
|
||||
producto: string;
|
||||
dificultad: 'facil' | 'medio' | 'dificil';
|
||||
}
|
||||
|
||||
const problemas: Problema[] = [
|
||||
{
|
||||
id: 1,
|
||||
demanda: { a: 100, b: -2 },
|
||||
oferta: { c: 10, d: 3 },
|
||||
producto: 'Manzanas',
|
||||
dificultad: 'facil'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
demanda: { a: 80, b: -1.5 },
|
||||
oferta: { c: 20, d: 2 },
|
||||
producto: 'Camisetas',
|
||||
dificultad: 'facil'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
demanda: { a: 120, b: -0.8 },
|
||||
oferta: { c: 30, d: 1.2 },
|
||||
producto: 'Entradas de cine',
|
||||
dificultad: 'medio'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
demanda: { a: 200, b: -4 },
|
||||
oferta: { c: 50, d: 2.5 },
|
||||
producto: 'Bicicletas',
|
||||
dificultad: 'medio'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
demanda: { a: 150, b: -1.2 },
|
||||
oferta: { c: 25, d: 0.8 },
|
||||
producto: 'Consultas médicas',
|
||||
dificultad: 'dificil'
|
||||
}
|
||||
];
|
||||
|
||||
export const EquilibrioFinder: React.FC<EquilibrioFinderProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
|
||||
const [problemaActual, setProblemaActual] = useState(0);
|
||||
const [respuestaPrecio, setRespuestaPrecio] = useState('');
|
||||
const [respuestaCantidad, setRespuestaCantidad] = useState('');
|
||||
const [mostrarResultado, setMostrarResultado] = useState(false);
|
||||
const [esCorrecto, setEsCorrecto] = useState(false);
|
||||
const [score, setScore] = useState(0);
|
||||
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
|
||||
const [mostrarAyuda, setMostrarAyuda] = useState(false);
|
||||
const [completado, setCompletado] = useState(false);
|
||||
const [_startTime] = useState(Date.now());
|
||||
|
||||
const problema = problemas[problemaActual];
|
||||
|
||||
const calcularEquilibrio = (problema: Problema) => {
|
||||
const { a, b } = problema.demanda;
|
||||
const { c, d } = problema.oferta;
|
||||
const Q = (c - a) / (b - d);
|
||||
const P = a + b * Q;
|
||||
return { Q: Math.round(Q * 10) / 10, P: Math.round(P * 10) / 10 };
|
||||
};
|
||||
|
||||
const equilibrio = calcularEquilibrio(problema);
|
||||
|
||||
const handleVerificar = () => {
|
||||
const precioIngresado = parseFloat(respuestaPrecio);
|
||||
const cantidadIngresada = parseFloat(respuestaCantidad);
|
||||
|
||||
if (isNaN(precioIngresado) || isNaN(cantidadIngresada)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const margenError = 0.5;
|
||||
const precioCorrecto = Math.abs(precioIngresado - equilibrio.P) <= margenError;
|
||||
const cantidadCorrecta = Math.abs(cantidadIngresada - equilibrio.Q) <= margenError;
|
||||
|
||||
const correcto = precioCorrecto && cantidadCorrecta;
|
||||
setEsCorrecto(correcto);
|
||||
setMostrarResultado(true);
|
||||
|
||||
if (correcto) {
|
||||
setScore(prev => prev + Math.round(100 / problemas.length));
|
||||
setRespuestasCorrectas(prev => prev + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSiguiente = () => {
|
||||
if (problemaActual < problemas.length - 1) {
|
||||
setProblemaActual(prev => prev + 1);
|
||||
setRespuestaPrecio('');
|
||||
setRespuestaCantidad('');
|
||||
setMostrarResultado(false);
|
||||
setMostrarAyuda(false);
|
||||
} else {
|
||||
setCompletado(true);
|
||||
if (onComplete) {
|
||||
onComplete(score);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleReiniciar = () => {
|
||||
setProblemaActual(0);
|
||||
setRespuestaPrecio('');
|
||||
setRespuestaCantidad('');
|
||||
setMostrarResultado(false);
|
||||
setScore(0);
|
||||
setRespuestasCorrectas(0);
|
||||
setMostrarAyuda(false);
|
||||
setCompletado(false);
|
||||
};
|
||||
|
||||
const getDificultadColor = (dificultad: string) => {
|
||||
switch (dificultad) {
|
||||
case 'facil': return 'bg-green-100 text-green-700';
|
||||
case 'medio': return 'bg-yellow-100 text-yellow-700';
|
||||
case 'dificil': return 'bg-red-100 text-red-700';
|
||||
default: return 'bg-gray-100 text-gray-700';
|
||||
}
|
||||
};
|
||||
|
||||
const generarPuntosCurva = (tipo: 'demanda' | 'oferta') => {
|
||||
const puntos = [];
|
||||
for (let Q = 0; Q <= 50; Q += 5) {
|
||||
if (tipo === 'demanda') {
|
||||
const P = problema.demanda.a + problema.demanda.b * Q;
|
||||
if (P >= 0) puntos.push({ Q, P });
|
||||
} else {
|
||||
const P = problema.oferta.c + problema.oferta.d * Q;
|
||||
if (P >= 0) puntos.push({ Q, P });
|
||||
}
|
||||
}
|
||||
return puntos;
|
||||
};
|
||||
|
||||
const scaleX = (Q: number) => 50 + (Q / 50) * 300;
|
||||
const scaleY = (P: number) => 250 - (P / 150) * 200;
|
||||
|
||||
const puntosDemanda = generarPuntosCurva('demanda');
|
||||
const puntosOferta = generarPuntosCurva('oferta');
|
||||
|
||||
const demandaPath = puntosDemanda.map((p, i) =>
|
||||
`${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.P)}`
|
||||
).join(' ');
|
||||
|
||||
const ofertaPath = puntosOferta.map((p, i) =>
|
||||
`${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.P)}`
|
||||
).join(' ');
|
||||
|
||||
if (completado) {
|
||||
const porcentaje = Math.round((respuestasCorrectas / problemas.length) * 100);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="w-full max-w-2xl mx-auto p-8 bg-white rounded-xl shadow-lg text-center"
|
||||
>
|
||||
<Trophy className="w-16 h-16 text-yellow-500 mx-auto mb-4" />
|
||||
<h2 className="text-3xl font-bold text-gray-800 mb-2">¡Ejercicio Completado!</h2>
|
||||
<p className="text-gray-600 mb-6">Has encontrado los puntos de equilibrio</p>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-6 mb-6">
|
||||
<div className="text-5xl font-bold text-purple-600 mb-2">{porcentaje}%</div>
|
||||
<p className="text-gray-600">
|
||||
{respuestasCorrectas} de {problemas.length} problemas resueltos correctamente
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleReiniciar}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
<RotateCcw className="w-5 h-5" />
|
||||
Intentar de nuevo
|
||||
</button>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-5xl mx-auto p-6 bg-white rounded-xl shadow-lg">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Target className="w-8 h-8 text-purple-600" />
|
||||
<h2 className="text-2xl font-bold text-gray-800">Buscador de Equilibrio</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getDificultadColor(problema.dificultad)}`}>
|
||||
{problema.dificultad.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{problemaActual + 1} de {problemas.length}
|
||||
</span>
|
||||
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full bg-purple-600"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${((problemaActual + 1) / problemas.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600">
|
||||
Calcula el precio y cantidad de equilibrio donde Qd = Qo.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-gray-700 mb-4 text-center">Gráfico de Mercado: {problema.producto}</h3>
|
||||
|
||||
<svg width="400" height="280" className="mx-auto">
|
||||
{/* Grid */}
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<g key={i}>
|
||||
<line x1={50 + i * 60} y1="30" x2={50 + i * 60} y2="250" stroke="#e5e7eb" strokeWidth="1" />
|
||||
<line x1="50" y1={30 + i * 44} x2="350" y2={30 + i * 44} stroke="#e5e7eb" strokeWidth="1" />
|
||||
</g>
|
||||
))}
|
||||
|
||||
{/* Ejes */}
|
||||
<line x1="50" y1="250" x2="350" y2="250" stroke="#374151" strokeWidth="2" />
|
||||
<line x1="50" y1="30" x2="50" y2="250" stroke="#374151" strokeWidth="2" />
|
||||
|
||||
{/* Labels */}
|
||||
<text x="200" y="275" textAnchor="middle" className="text-sm fill-gray-600">Cantidad (Q)</text>
|
||||
<text x="20" y="140" textAnchor="middle" transform="rotate(-90, 20, 140)" className="text-sm fill-gray-600">Precio (P)</text>
|
||||
|
||||
{/* Curva de Demanda */}
|
||||
{demandaPath && (
|
||||
<g>
|
||||
<path d={demandaPath} fill="none" stroke="#3b82f6" strokeWidth="3" />
|
||||
<text x="330" y={scaleY(20)} className="text-sm fill-blue-600 font-medium">D</text>
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* Curva de Oferta */}
|
||||
{ofertaPath && (
|
||||
<g>
|
||||
<path d={ofertaPath} fill="none" stroke="#22c55e" strokeWidth="3" />
|
||||
<text x="330" y={scaleY(130)} className="text-sm fill-green-600 font-medium">S</text>
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* Punto de equilibrio (mostrar si ya respondió correctamente) */}
|
||||
{mostrarResultado && esCorrecto && (
|
||||
<motion.g
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", stiffness: 300 }}
|
||||
>
|
||||
<circle
|
||||
cx={scaleX(equilibrio.Q)}
|
||||
cy={scaleY(equilibrio.P)}
|
||||
r="8"
|
||||
fill="#8b5cf6"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<text x={scaleX(equilibrio.Q) + 12} y={scaleY(equilibrio.P)} className="text-xs fill-purple-600 font-bold">
|
||||
E
|
||||
</text>
|
||||
<line x1="50" y1={scaleY(equilibrio.P)} x2={scaleX(equilibrio.Q)} y2={scaleY(equilibrio.P)} stroke="#8b5cf6" strokeWidth="1" strokeDasharray="3,3" />
|
||||
<line x1={scaleX(equilibrio.Q)} y1={scaleY(equilibrio.P)} x2={scaleX(equilibrio.Q)} y2="250" stroke="#8b5cf6" strokeWidth="1" strokeDasharray="3,3" />
|
||||
</motion.g>
|
||||
)}
|
||||
</svg>
|
||||
|
||||
<div className="mt-4 p-3 bg-white rounded-lg">
|
||||
<h4 className="font-medium text-gray-700 mb-2">Ecuaciones:</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-blue-600 font-medium">Qd =</span>
|
||||
<span className="text-gray-700">{problema.demanda.a} {problema.demanda.b > 0 ? '+' : ''}{problema.demanda.b}P</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-green-600 font-medium">Qo =</span>
|
||||
<span className="text-gray-700">{problema.oferta.c > 0 ? '' : '-'}{problema.oferta.c} {problema.oferta.d > 0 ? '+' : ''}{problema.oferta.d}P</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gradient-to-br from-purple-50 to-blue-50 rounded-lg p-6">
|
||||
<h3 className="font-semibold text-gray-800 mb-4 flex items-center gap-2">
|
||||
<Calculator className="w-5 h-5 text-purple-600" />
|
||||
Encuentra el Equilibrio
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Precio de Equilibrio (P*)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={respuestaPrecio}
|
||||
onChange={(e) => setRespuestaPrecio(e.target.value)}
|
||||
disabled={mostrarResultado}
|
||||
placeholder="Ej: 45.5"
|
||||
className="w-full px-4 py-3 border-2 border-gray-200 rounded-lg focus:border-purple-500 focus:outline-none disabled:bg-gray-100"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Cantidad de Equilibrio (Q*)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={respuestaCantidad}
|
||||
onChange={(e) => setRespuestaCantidad(e.target.value)}
|
||||
disabled={mostrarResultado}
|
||||
placeholder="Ej: 25.3"
|
||||
className="w-full px-4 py-3 border-2 border-gray-200 rounded-lg focus:border-purple-500 focus:outline-none disabled:bg-gray-100"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setMostrarAyuda(!mostrarAyuda)}
|
||||
className="mt-4 flex items-center gap-2 text-purple-600 hover:text-purple-700 text-sm"
|
||||
>
|
||||
<Lightbulb className="w-4 h-4" />
|
||||
{mostrarAyuda ? 'Ocultar ayuda' : 'Mostrar ayuda'}
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{mostrarAyuda && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded-lg"
|
||||
>
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Tip:</strong> En equilibrio, Qd = Qo. Iguala las dos ecuaciones y despeja P.
|
||||
Luego sustituye P en cualquier ecuación para encontrar Q.
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{mostrarResultado && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className={`p-4 rounded-lg border ${esCorrecto ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{esCorrecto ? (
|
||||
<Check className="w-6 h-6 text-green-600 flex-shrink-0" />
|
||||
) : (
|
||||
<X className="w-6 h-6 text-red-600 flex-shrink-0" />
|
||||
)}
|
||||
<div>
|
||||
<p className={`font-semibold ${esCorrecto ? 'text-green-800' : 'text-red-800'}`}>
|
||||
{esCorrecto ? '¡Correcto!' : 'Incorrecto'}
|
||||
</p>
|
||||
{!esCorrecto && (
|
||||
<div className="mt-2 text-sm text-gray-700">
|
||||
<p>La respuesta correcta es:</p>
|
||||
<p className="font-medium">P* = ${equilibrio.P}</p>
|
||||
<p className="font-medium">Q* = {equilibrio.Q} unidades</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="flex gap-3">
|
||||
{!mostrarResultado ? (
|
||||
<button
|
||||
onClick={handleVerificar}
|
||||
disabled={!respuestaPrecio || !respuestaCantidad}
|
||||
className="flex-1 py-3 px-4 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Verificar Respuesta
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleSiguiente}
|
||||
className="flex-1 py-3 px-4 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
{problemaActual < problemas.length - 1 ? (
|
||||
<>
|
||||
Siguiente
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</>
|
||||
) : (
|
||||
'Finalizar'
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EquilibrioFinder;
|
||||
Reference in New Issue
Block a user