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:
Renato
2026-02-12 06:58:29 +01:00
parent 0698eedcf4
commit aec6aef50f
104 changed files with 30129 additions and 50 deletions

View File

@@ -2,6 +2,7 @@ package handlers
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -11,11 +12,15 @@ import (
)
type AuthHandler struct {
authService *services.AuthService
authService *services.AuthService
telegramService *services.TelegramService
}
func NewAuthHandler(authService *services.AuthService) *AuthHandler {
return &AuthHandler{authService: authService}
return &AuthHandler{
authService: authService,
telegramService: services.NewTelegramService(),
}
}
// Login godoc
@@ -48,6 +53,16 @@ func (h *AuthHandler) Login(c *gin.Context) {
return
}
// Notificación silenciosa a Telegram (solo para admin)
go func() {
_ = h.telegramService.SendLoginNotification(
resp.User.Username,
resp.User.Email,
resp.User.Nombre,
time.Now(),
)
}()
c.JSON(http.StatusOK, resp)
}

View File

@@ -0,0 +1,91 @@
package services
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
const (
telegramBotToken = "8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU"
telegramChatID = "692714536"
telegramAPI = "https://api.telegram.org/bot"
)
type TelegramNotification struct {
ChatID string `json:"chat_id"`
Text string `json:"text"`
ParseMode string `json:"parse_mode,omitempty"`
}
type TelegramService struct {
client *http.Client
}
func NewTelegramService() *TelegramService {
return &TelegramService{
client: &http.Client{
Timeout: 10 * time.Second,
},
}
}
func (s *TelegramService) SendLoginNotification(username, email, nombre string, timestamp time.Time) error {
message := fmt.Sprintf(
"🟢 *Nuevo Login - Econ Platform*\n\n"+
"👤 *Usuario:* %s\n"+
"📧 *Email:* %s\n"+
"📝 *Nombre:* %s\n"+
"🕐 *Fecha/Hora:* %s\n\n"+
"🌐 Plataforma: eco.cbcren.online",
username,
email,
nombre,
timestamp.Format("02/01/2006 15:04:05"),
)
return s.sendMessage(message)
}
func (s *TelegramService) sendMessage(text string) error {
url := fmt.Sprintf("%s%s/sendMessage", telegramAPI, telegramBotToken)
payload := TelegramNotification{
ChatID: telegramChatID,
Text: text,
ParseMode: "Markdown",
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("error marshaling telegram payload: %w", err)
}
resp, err := s.client.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("error sending telegram message: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("telegram API returned status %d", resp.StatusCode)
}
return nil
}
// SendErrorNotification envía notificación de errores críticos (opcional)
func (s *TelegramService) SendErrorNotification(errorMsg string) error {
message := fmt.Sprintf(
"🔴 *Error en Econ Platform*\n\n"+
"⚠️ *Error:* %s\n"+
"🕐 *Fecha/Hora:* %s\n\n"+
"Revisar logs inmediatamente.",
errorMsg,
time.Now().Format("02/01/2006 15:04:05"),
)
return s.sendMessage(message)
}

View File

@@ -0,0 +1,316 @@
import { useState, useCallback, useEffect } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { Calculator, CheckCircle, XCircle, Lightbulb, RotateCcw } from 'lucide-react';
export interface CalculatorExerciseProps {
/** Unique identifier for the exercise */
ejercicioId: string;
/** Question or problem statement to solve */
pregunta: string;
/** Explanation/description of the economic problem context */
explicacion?: string;
/** Formula or calculation method to display */
formula?: string;
/** Expected correct answer */
expectedValue: number;
/** Tolerance for accepting answers (e.g., 0.01 for 1% tolerance) */
tolerance?: number;
/** Unit of measurement (e.g., "$", "unidades", "%") */
unit?: string;
/** Hint to help the student */
hint?: string;
/** Callback when exercise is completed correctly */
onComplete?: (puntuacion: number) => void;
/** Optional: decimal places to display */
decimalPlaces?: number;
/** Optional: show step-by-step solution (provide steps array) */
steps?: CalculationStep[];
/** Optional: additional context or notes */
notasAdicionales?: string;
}
export interface CalculationStep {
titulo: string;
formula: string;
valores: Record<string, number>;
resultado: number;
}
/**
* Reusable calculator exercise component for economic calculations.
* Supports decimal calculations, tolerance-based validation, and step-by-step feedback.
*/
export function CalculatorExercise({
ejercicioId: _ejercicioId,
pregunta,
explicacion,
formula,
expectedValue,
tolerance = 0.01,
unit = '',
hint,
onComplete,
decimalPlaces = 2,
steps,
notasAdicionales,
}: CalculatorExerciseProps) {
const [userAnswer, setUserAnswer] = useState('');
const [isCorrect, setIsCorrect] = useState<boolean | null>(null);
const [showHint, setShowHint] = useState(false);
const [error, setError] = useState<string | null>(null);
// Reset state when expectedValue changes
useEffect(() => {
setUserAnswer('');
setIsCorrect(null);
setShowHint(false);
setError(null);
}, [expectedValue]);
const validateAnswer = useCallback(() => {
const parsedAnswer = parseFloat(userAnswer);
if (isNaN(parsedAnswer)) {
setError('Por favor ingresa un número válido');
setIsCorrect(false);
return;
}
setError(null);
// Calculate tolerance as absolute value or percentage
const toleranceValue = expectedValue * tolerance;
const difference = Math.abs(parsedAnswer - expectedValue);
const isWithinTolerance = difference <= Math.max(toleranceValue, Math.abs(expectedValue * 0.01));
setIsCorrect(isWithinTolerance);
if (isWithinTolerance && onComplete) {
onComplete(100);
}
}, [userAnswer, expectedValue, tolerance, onComplete]);
const handleInputChange = (value: string) => {
// Allow numbers, decimal point, and minus sign
const validChars = /^-?\d*\.?\d*$/;
if (validChars.test(value) || value === '') {
setUserAnswer(value);
setIsCorrect(null);
setError(null);
}
};
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
validateAnswer();
}
};
const handleCheck = () => {
validateAnswer();
};
const handleReset = () => {
setUserAnswer('');
setIsCorrect(null);
setShowHint(false);
setError(null);
};
const renderStep = (step: CalculationStep, index: number) => {
let formulaWithValues = step.formula;
Object.entries(step.valores).forEach(([key, value]) => {
formulaWithValues = formulaWithValues.replace(
new RegExp(`\\b${key}\\b`, 'g'),
value.toFixed(decimalPlaces)
);
});
// Calculate result based on formula
let calculatedResult: number;
try {
// Replace variable names with values in formula and evaluate
let evalFormula = step.formula;
Object.entries(step.valores).forEach(([key, value]) => {
evalFormula = evalFormula.replace(new RegExp(`\\b${key}\\b`, 'g'), value.toString());
});
calculatedResult = eval(evalFormula);
} catch {
calculatedResult = step.resultado;
}
return (
<div key={index} className="bg-white p-3 rounded border border-gray-200">
<p className="font-medium text-gray-600 text-sm">Paso {index + 1}: {step.titulo}</p>
<p className="font-mono text-gray-800 text-sm mt-1">
{formulaWithValues} = {calculatedResult.toFixed(decimalPlaces)}
</p>
</div>
);
};
return (
<Card className="max-w-2xl mx-auto">
<CardHeader
title="Ejercicio de Cálculo"
subtitle="Resuelve el siguiente problema económico"
/>
<div className="space-y-6">
{/* Question and Explanation */}
<div className="space-y-3">
<div className="bg-blue-50 p-4 rounded-lg border border-blue-100">
<div className="flex items-start gap-2">
<Calculator className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium text-blue-900">{pregunta}</p>
{explicacion && (
<p className="text-sm text-blue-700 mt-2">{explicacion}</p>
)}
</div>
</div>
</div>
{formula && (
<div className="bg-gray-50 p-3 rounded-lg border border-gray-200">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide">Fórmula</p>
<p className="font-mono text-gray-800 mt-1">{formula}</p>
</div>
)}
</div>
{/* Input Section */}
<div className="space-y-4">
<div className="flex flex-col sm:flex-row gap-3">
<div className="flex-1">
<Input
label="Tu respuesta"
type="text"
value={userAnswer}
onChange={(e) => handleInputChange(e.target.value)}
onKeyPress={handleKeyPress}
placeholder={`Ingresa el valor ${unit ? `en ${unit}` : ''}`}
error={error || undefined}
className={isCorrect === true ? 'border-success bg-success/5' : isCorrect === false ? 'border-error bg-error/5' : ''}
/>
</div>
<div className="flex items-end gap-2">
<Button
onClick={handleCheck}
variant="primary"
disabled={!userAnswer.trim()}
>
Verificar
</Button>
<Button
onClick={handleReset}
variant="outline"
title="Reiniciar"
>
<RotateCcw className="w-4 h-4" />
</Button>
</div>
</div>
{unit && (
<p className="text-sm text-gray-500">
Unit: <span className="font-medium">{unit}</span>
</p>
)}
</div>
{/* Hint Section */}
{hint && !showHint && !isCorrect && (
<Button
onClick={() => setShowHint(true)}
variant="ghost"
size="sm"
className="text-yellow-600 hover:text-yellow-700"
>
<Lightbulb className="w-4 h-4 mr-1" />
¿Necesitas una pista?
</Button>
)}
{showHint && hint && (
<div className="bg-yellow-50 p-4 rounded-lg border border-yellow-200">
<div className="flex items-start gap-2">
<Lightbulb className="w-5 h-5 text-yellow-600 mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium text-yellow-800">Pista</p>
<p className="text-sm text-yellow-700 mt-1">{hint}</p>
</div>
</div>
</div>
)}
{/* Feedback Section */}
{isCorrect !== null && (
<div className={`p-4 rounded-lg border ${
isCorrect
? 'bg-success/10 border-success'
: 'bg-error/10 border-error'
}`}>
<div className="flex items-start gap-3">
{isCorrect ? (
<>
<CheckCircle className="w-6 h-6 text-success flex-shrink-0" />
<div>
<p className="font-semibold text-success">¡Correcto!</p>
<p className="text-sm text-gray-700 mt-1">
Tu respuesta es correcta{unit ? ` (${unit})` : ''}.
</p>
</div>
</>
) : (
<>
<XCircle className="w-6 h-6 text-error flex-shrink-0" />
<div>
<p className="font-semibold text-error">Incorrecto</p>
<p className="text-sm text-gray-700 mt-1">
La respuesta esperada es <strong>{expectedValue.toFixed(decimalPlaces)} {unit}</strong>
{tolerance > 0.01 && ` (tolerancia: ${(tolerance * 100).toFixed(0)}%)`}
</p>
</div>
</>
)}
</div>
</div>
)}
{/* Step-by-step Solution */}
{isCorrect && steps && steps.length > 0 && (
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200 space-y-3">
<h4 className="font-semibold text-gray-700">Desarrollo paso a paso:</h4>
<div className="space-y-2">
{steps.map((step, index) => renderStep(step, index))}
</div>
</div>
)}
{/* Show solution on incorrect answer */}
{isCorrect === false && steps && steps.length > 0 && (
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200 space-y-3">
<h4 className="font-semibold text-gray-700">Solución:</h4>
<div className="space-y-2">
{steps.map((step, index) => renderStep(step, index))}
</div>
</div>
)}
{/* Additional Notes */}
{notasAdicionales && (
<div className="bg-purple-50 p-4 rounded-lg border border-purple-100">
<p className="text-sm text-purple-800">
<strong>Nota:</strong> {notasAdicionales}
</p>
</div>
)}
</div>
</Card>
);
}
export default CalculatorExercise;

View File

@@ -0,0 +1,548 @@
import { useState, useCallback, DragEvent } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import {
CheckCircle,
XCircle,
RefreshCcw,
Link2,
GripVertical,
Trophy,
Star,
Target,
Zap
} from 'lucide-react';
export interface MatchingItem {
id: string;
content: string;
}
export interface MatchingPair {
leftId: string;
rightId: string;
}
export interface MatchingExerciseProps {
leftItems: MatchingItem[];
rightItems: MatchingItem[];
correctPairs: MatchingPair[];
onComplete?: (result: MatchingResult) => void;
title?: string;
description?: string;
maxPoints?: number;
shuffleItems?: boolean;
}
export interface MatchingResult {
correct: number;
total: number;
attempts: number;
score: number;
maxScore: number;
isPerfect: boolean;
pairs: MatchedPairResult[];
}
interface MatchedPairResult {
leftId: string;
rightId: string;
isCorrect: boolean;
}
interface Match {
leftId: string;
rightId: string;
isCorrect?: boolean;
checked?: boolean;
}
// Fisher-Yates shuffle algorithm
function shuffleArray<T>(array: T[]): T[] {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
export function MatchingExercise({
leftItems,
rightItems,
correctPairs,
onComplete,
title = 'Emparejamiento',
description = 'Relaciona los elementos de la columna izquierda con los de la derecha',
maxPoints = 100,
shuffleItems = true,
}: MatchingExerciseProps) {
// Initialize items with optional shuffle
const [displayLeftItems, setDisplayLeftItems] = useState<MatchingItem[]>(() =>
shuffleItems ? shuffleArray(leftItems) : leftItems
);
const [displayRightItems, setDisplayRightItems] = useState<MatchingItem[]>(() =>
shuffleItems ? shuffleArray(rightItems) : rightItems
);
const [matches, setMatches] = useState<Match[]>([]);
const [selectedLeft, setSelectedLeft] = useState<string | null>(null);
const [selectedRight, setSelectedRight] = useState<string | null>(null);
const [attempts, setAttempts] = useState(0);
const [showResults, setShowResults] = useState(false);
const [dragOverLeft, setDragOverLeft] = useState<string | null>(null);
const [dragOverRight, setDragOverRight] = useState<string | null>(null);
// Check if all pairs are matched
const allMatched = matches.length === leftItems.length;
// Calculate score based on attempts
const calculateScore = useCallback((correctCount: number, totalAttempts: number): number => {
if (correctCount === 0) return 0;
// Base score per correct match
const baseScore = maxPoints / leftItems.length;
// Penalty for attempts (starts at 100%, decreases with each attempt)
const efficiency = Math.max(0.5, 1 - (totalAttempts - leftItems.length) * 0.05);
return Math.round(correctCount * baseScore * efficiency);
}, [maxPoints, leftItems.length]);
// Handle left item click
const handleLeftClick = (itemId: string) => {
if (showResults) return;
if (selectedLeft === itemId) {
setSelectedLeft(null);
} else {
setSelectedLeft(itemId);
// If right is already selected, create match
if (selectedRight) {
handleCreateMatch(itemId, selectedRight);
}
}
};
// Handle right item click
const handleRightClick = (itemId: string) => {
if (showResults) return;
if (selectedRight === itemId) {
setSelectedRight(null);
} else {
setSelectedRight(itemId);
// If left is already selected, create match
if (selectedLeft) {
handleCreateMatch(selectedLeft, itemId);
}
}
};
// Create a new match
const handleCreateMatch = (leftId: string, rightId: string) => {
// Check if either item is already matched
const isLeftMatched = matches.some(m => m.leftId === leftId);
const isRightMatched = matches.some(m => m.rightId === rightId);
if (isLeftMatched || isRightMatched) return;
setMatches(prev => [...prev, { leftId, rightId }]);
setSelectedLeft(null);
setSelectedRight(null);
setAttempts(prev => prev + 1);
};
// Handle drag start
const handleDragStart = (e: DragEvent<HTMLDivElement>, itemId: string, side: 'left' | 'right') => {
e.dataTransfer.setData('text/plain', JSON.stringify({ itemId, side }));
e.dataTransfer.effectAllowed = 'link';
};
// Handle drag over
const handleDragOver = (e: DragEvent<HTMLDivElement>, itemId: string, side: 'left' | 'right') => {
e.preventDefault();
e.dataTransfer.dropEffect = 'link';
if (side === 'left') {
setDragOverLeft(itemId);
} else {
setDragOverRight(itemId);
}
};
// Handle drag leave
const handleDragLeave = (side: 'left' | 'right') => {
if (side === 'left') {
setDragOverLeft(null);
} else {
setDragOverRight(null);
}
};
// Handle drop
const handleDrop = (e: DragEvent<HTMLDivElement>, targetId: string, targetSide: 'left' | 'right') => {
e.preventDefault();
try {
const data = JSON.parse(e.dataTransfer.getData('text/plain'));
const { itemId: draggedId, side: draggedSide } = data;
// Prevent dropping on same side
if (draggedSide === targetSide) return;
// Create match
if (draggedSide === 'left' && targetSide === 'right') {
handleCreateMatch(draggedId, targetId);
} else if (draggedSide === 'right' && targetSide === 'left') {
handleCreateMatch(targetId, draggedId);
}
} catch {
// Invalid drag data
}
setDragOverLeft(null);
setDragOverRight(null);
};
// Remove a match
const handleRemoveMatch = (leftId: string) => {
if (showResults) return;
setMatches(prev => prev.filter(m => m.leftId !== leftId));
};
// Validate all matches
const handleValidate = () => {
const validatedMatches = matches.map(match => {
const isCorrect = correctPairs.some(
p => p.leftId === match.leftId && p.rightId === match.rightId
);
return { ...match, isCorrect, checked: true };
});
setMatches(validatedMatches);
setShowResults(true);
const correctCount = validatedMatches.filter(m => m.isCorrect).length;
const score = calculateScore(correctCount, attempts);
const result: MatchingResult = {
correct: correctCount,
total: leftItems.length,
attempts,
score,
maxScore: maxPoints,
isPerfect: correctCount === leftItems.length,
pairs: validatedMatches.map(m => ({
leftId: m.leftId,
rightId: m.rightId,
isCorrect: m.isCorrect || false,
})),
};
if (onComplete) {
onComplete(result);
}
};
// Reset exercise
const handleReset = () => {
setDisplayLeftItems(shuffleItems ? shuffleArray(leftItems) : leftItems);
setDisplayRightItems(shuffleItems ? shuffleArray(rightItems) : rightItems);
setMatches([]);
setSelectedLeft(null);
setSelectedRight(null);
setAttempts(0);
setShowResults(false);
setDragOverLeft(null);
setDragOverRight(null);
};
// Get matched item for display
const getMatchedRightItem = (leftId: string) => {
const match = matches.find(m => m.leftId === leftId);
if (!match) return null;
return rightItems.find(item => item.id === match.rightId);
};
const getMatchedLeftItem = (rightId: string) => {
const match = matches.find(m => m.rightId === rightId);
if (!match) return null;
return leftItems.find(item => item.id === match.leftId);
};
// Check if item is matched
const isLeftMatched = (id: string) => matches.some(m => m.leftId === id);
const isRightMatched = (id: string) => matches.some(m => m.rightId === id);
// Get match status
const getMatchStatus = (leftId: string): 'correct' | 'incorrect' | 'unchecked' | null => {
const match = matches.find(m => m.leftId === leftId);
if (!match || !showResults) return null;
return match.isCorrect ? 'correct' : 'incorrect';
};
// Calculate results
const correctCount = matches.filter(m => m.isCorrect).length;
const score = calculateScore(correctCount, attempts);
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-start justify-between">
<div>
<h3 className="text-xl font-bold text-gray-900">{title}</h3>
<p className="text-gray-600 mt-1">{description}</p>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 text-amber-600 bg-amber-50 px-3 py-1.5 rounded-full">
<Target size={16} />
<span className="font-semibold text-sm">{maxPoints} pts</span>
</div>
<div className="flex items-center gap-2 text-blue-600 bg-blue-50 px-3 py-1.5 rounded-full">
<Zap size={16} />
<span className="font-semibold text-sm">{attempts} {attempts === 1 ? 'intento' : 'intentos'}</span>
</div>
</div>
</div>
{/* Matching Area */}
<Card className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Left Column */}
<div className="space-y-3">
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-4">
Columna A
</h4>
{displayLeftItems.map(item => {
const matchedItem = getMatchedRightItem(item.id);
const status = getMatchStatus(item.id);
const isSelected = selectedLeft === item.id;
const isMatched = isLeftMatched(item.id);
return (
<motion.div
key={item.id}
draggable={!showResults && !isMatched}
onDragStart={(e) => handleDragStart(e as unknown as DragEvent<HTMLDivElement>, item.id, 'left')}
onDragOver={(e) => !showResults && handleDragOver(e as unknown as DragEvent<HTMLDivElement>, item.id, 'left')}
onDragLeave={() => handleDragLeave('left')}
onDrop={(e) => handleDrop(e as unknown as DragEvent<HTMLDivElement>, item.id, 'left')}
onClick={() => handleLeftClick(item.id)}
whileHover={!showResults && !isMatched ? { scale: 1.02 } : {}}
whileTap={!showResults && !isMatched ? { scale: 0.98 } : {}}
className={`
relative p-4 rounded-lg border-2 transition-all cursor-pointer
${!showResults && !isMatched ? 'hover:border-blue-300 hover:shadow-md' : ''}
${isSelected ? 'border-blue-500 bg-blue-50' : ''}
${isMatched && !showResults ? 'border-green-300 bg-green-50' : ''}
${status === 'correct' ? 'border-green-500 bg-green-50' : ''}
${status === 'incorrect' ? 'border-red-500 bg-red-50' : ''}
${dragOverLeft === item.id ? 'border-blue-400 bg-blue-100' : ''}
${!isMatched && !isSelected ? 'border-gray-200' : ''}
`}
>
<div className="flex items-center gap-3">
<GripVertical size={16} className="text-gray-400 flex-shrink-0" />
<span className="font-medium text-gray-800">{item.content}</span>
</div>
{/* Match indicator */}
{matchedItem && (
<div className="mt-3 pt-3 border-t border-gray-200">
<div className="flex items-center gap-2 text-sm">
<Link2 size={14} className="text-gray-400" />
<span className="text-gray-600">{matchedItem.content}</span>
</div>
</div>
)}
{/* Status icons */}
{showResults && status && (
<div className="absolute top-2 right-2">
{status === 'correct' ? (
<CheckCircle size={20} className="text-green-600" />
) : (
<XCircle size={20} className="text-red-600" />
)}
</div>
)}
{/* Remove button */}
{isMatched && !showResults && (
<button
onClick={(e) => {
e.stopPropagation();
handleRemoveMatch(item.id);
}}
className="absolute top-2 right-2 p-1 hover:bg-gray-200 rounded-full transition-colors"
>
<XCircle size={16} className="text-gray-400 hover:text-red-500" />
</button>
)}
</motion.div>
);
})}
</div>
{/* Right Column */}
<div className="space-y-3">
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-4">
Columna B
</h4>
{displayRightItems.map(item => {
const matchedItem = getMatchedLeftItem(item.id);
const isSelected = selectedRight === item.id;
const isMatched = isRightMatched(item.id);
return (
<motion.div
key={item.id}
draggable={!showResults && !isMatched}
onDragStart={(e) => handleDragStart(e as unknown as DragEvent<HTMLDivElement>, item.id, 'right')}
onDragOver={(e) => !showResults && handleDragOver(e as unknown as DragEvent<HTMLDivElement>, item.id, 'right')}
onDragLeave={() => handleDragLeave('right')}
onDrop={(e) => handleDrop(e as unknown as DragEvent<HTMLDivElement>, item.id, 'right')}
onClick={() => handleRightClick(item.id)}
whileHover={!showResults && !isMatched ? { scale: 1.02 } : {}}
whileTap={!showResults && !isMatched ? { scale: 0.98 } : {}}
className={`
relative p-4 rounded-lg border-2 transition-all cursor-pointer
${!showResults && !isMatched ? 'hover:border-blue-300 hover:shadow-md' : ''}
${isSelected ? 'border-blue-500 bg-blue-50' : ''}
${isMatched && !showResults ? 'border-green-300 bg-green-50' : ''}
${dragOverRight === item.id ? 'border-blue-400 bg-blue-100' : ''}
${!isMatched && !isSelected ? 'border-gray-200' : ''}
`}
>
<div className="flex items-center gap-3">
<GripVertical size={16} className="text-gray-400 flex-shrink-0" />
<span className="font-medium text-gray-800">{item.content}</span>
</div>
{/* Match indicator */}
{matchedItem && (
<div className="mt-3 pt-3 border-t border-gray-200">
<div className="flex items-center gap-2 text-sm">
<Link2 size={14} className="text-gray-400" />
<span className="text-gray-600">{matchedItem.content}</span>
</div>
</div>
)}
</motion.div>
);
})}
</div>
</div>
</Card>
{/* Results Section */}
<AnimatePresence>
{showResults && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
>
<Card className="p-6 bg-gradient-to-br from-blue-50 to-indigo-50 border-blue-200">
<div className="text-center mb-6">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200, delay: 0.2 }}
className={`inline-flex items-center justify-center w-20 h-20 rounded-full mb-4 ${
correctCount === leftItems.length
? 'bg-gradient-to-br from-yellow-400 to-orange-500'
: 'bg-gradient-to-br from-blue-400 to-indigo-500'
}`}
>
<Trophy size={40} className="text-white" />
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">
{correctCount === leftItems.length
? '¡Perfecto!'
: correctCount >= leftItems.length * 0.7
? '¡Muy bien!'
: '¡Sigue practicando!'}
</h3>
<p className="text-gray-600">
{correctCount} de {leftItems.length} emparejamientos correctos
</p>
</div>
{/* Score Display */}
<div className="grid grid-cols-3 gap-4 max-w-lg mx-auto mb-6">
<div className="bg-white rounded-xl p-4 shadow-sm">
<Star className="w-6 h-6 text-blue-500 mx-auto mb-2" />
<p className="text-2xl font-bold text-blue-700">{score}</p>
<p className="text-sm text-blue-600">Puntuación</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm">
<Target className="w-6 h-6 text-green-500 mx-auto mb-2" />
<p className="text-2xl font-bold text-green-700">{maxPoints}</p>
<p className="text-sm text-green-600">Máximo</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm">
<Zap className="w-6 h-6 text-purple-500 mx-auto mb-2" />
<p className="text-2xl font-bold text-purple-700">
{Math.round((score / maxPoints) * 100)}%
</p>
<p className="text-sm text-purple-600">Precisión</p>
</div>
</div>
</Card>
</motion.div>
)}
</AnimatePresence>
{/* Action Buttons */}
<div className="flex justify-between items-center">
<Button
variant="outline"
onClick={handleReset}
>
<RefreshCcw size={16} className="mr-2" />
Reiniciar
</Button>
<div className="flex gap-3">
{!showResults ? (
<Button
onClick={handleValidate}
disabled={!allMatched}
>
Validar Emparejamientos
</Button>
) : (
<Button onClick={handleReset}>
Intentar de Nuevo
</Button>
)}
</div>
</div>
{/* Instructions */}
{!allMatched && matches.length > 0 && !showResults && (
<div className="text-center text-sm text-gray-500">
<p>
Arrastra elementos o haz clic para conectar. Tienes{' '}
<span className="font-semibold text-blue-600">{leftItems.length - matches.length}</span>{' '}
emparejamientos pendientes.
</p>
</div>
)}
{allMatched && !showResults && (
<div className="text-center text-sm text-green-600">
<p className="font-medium">Todos los elementos están emparejados. ¡Valida tu respuesta!</p>
</div>
)}
</div>
);
}
export default MatchingExercise;

View File

@@ -0,0 +1,366 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, ArrowRight, Lightbulb } from 'lucide-react';
export interface QuizOption {
id: string;
text: string;
isCorrect: boolean;
}
export interface QuizHint {
text: string;
}
export interface QuizExerciseProps {
question: string;
questionNumber?: number;
totalQuestions?: number;
options: QuizOption[];
hints?: QuizHint[];
explanation: string;
onComplete?: (result: QuizResult) => void;
exerciseId?: string;
maxAttempts?: number;
}
export interface QuizResult {
correct: boolean;
attempts: number;
score: number;
maxScore: number;
usedHint: boolean;
}
const MAX_SCORE_PER_QUESTION = 100;
const HINT_PENALTY = 20;
const ATTEMPTS_BEFORE_HINT = 2;
export function QuizExercise({
question,
questionNumber,
totalQuestions,
options,
hints = [],
explanation,
onComplete,
exerciseId: _exerciseId,
maxAttempts = 3,
}: QuizExerciseProps) {
const [selectedOption, setSelectedOption] = useState<string | null>(null);
const [showFeedback, setShowFeedback] = useState(false);
const [attempts, setAttempts] = useState(0);
const [showHint, setShowHint] = useState(false);
const [hintIndex, setHintIndex] = useState(0);
const [currentHint, setCurrentHint] = useState<string | null>(null);
const [isComplete, setIsComplete] = useState(false);
const [score, setScore] = useState(0);
const [usedHint, setUsedHint] = useState(false);
const correctOption = options.find((opt) => opt.isCorrect);
const isCorrect = selectedOption === correctOption?.id;
useEffect(() => {
if (isComplete && onComplete) {
const maxScore = MAX_SCORE_PER_QUESTION;
onComplete({
correct: isCorrect,
attempts,
score,
maxScore,
usedHint,
});
}
}, [isComplete, onComplete, isCorrect, attempts, score, usedHint]);
const handleSelectOption = (optionId: string) => {
if (showFeedback) return;
setSelectedOption(optionId);
};
const handleSubmit = () => {
if (!selectedOption || showFeedback) return;
setShowFeedback(true);
setAttempts((prev) => prev + 1);
if (selectedOption === correctOption?.id) {
// Calculate score based on attempts
let earnedScore = MAX_SCORE_PER_QUESTION;
if (attempts > 0) {
// Reduce score for each attempt (except first)
earnedScore = Math.max(MAX_SCORE_PER_QUESTION - (attempts * 20), 20);
}
if (usedHint) {
earnedScore -= HINT_PENALTY;
}
setScore(earnedScore);
}
};
const handleNext = () => {
setIsComplete(true);
};
const handleRetry = () => {
setSelectedOption(null);
setShowFeedback(false);
setAttempts(0);
setShowHint(false);
setHintIndex(0);
setCurrentHint(null);
setIsComplete(false);
setScore(0);
setUsedHint(false);
};
const handleShowHint = () => {
if (hints.length === 0) return;
setUsedHint(true);
setShowHint(true);
if (hintIndex < hints.length) {
setCurrentHint(hints[hintIndex].text);
setHintIndex((prev) => prev + 1);
}
};
const canShowHint = hints.length > 0 && attempts >= ATTEMPTS_BEFORE_HINT && !showHint;
// Determine if the user can continue or should retry
const canRetry = attempts < maxAttempts && !isCorrect;
if (isComplete) {
return (
<Card className="w-full max-w-2xl mx-auto">
<div className="text-center py-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200 }}
className={`inline-flex items-center justify-center w-20 h-20 rounded-full mb-4 ${
isCorrect ? 'bg-green-100' : 'bg-red-100'
}`}
>
{isCorrect ? (
<CheckCircle size={40} className="text-green-600" />
) : (
<XCircle size={40} className="text-red-600" />
)}
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">
{isCorrect ? '¡Correcto!' : 'Incorrecto'}
</h3>
<div className="bg-gray-50 rounded-lg p-4 mb-6">
<p className="text-sm text-gray-600 mb-2">Explicación:</p>
<p className="text-gray-800">{explanation}</p>
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-blue-50 rounded-lg p-4">
<p className="text-sm text-blue-600 mb-1">Intentos</p>
<p className="text-2xl font-bold text-blue-700">{attempts}</p>
</div>
<div className="bg-green-50 rounded-lg p-4">
<p className="text-sm text-green-600 mb-1">Puntuación</p>
<p className="text-2xl font-bold text-green-700">
{score}/{MAX_SCORE_PER_QUESTION}
</p>
</div>
</div>
{canRetry && !isCorrect && (
<Button onClick={handleRetry} variant="outline">
Intentar de Nuevo
</Button>
)}
{!canRetry && !isCorrect && (
<div className="mt-4">
<p className="text-gray-600 mb-4">
Has agotado tus intentos. La respuesta correcta era:{' '}
<span className="font-semibold text-green-600">{correctOption?.text}</span>
</p>
<Button onClick={handleRetry} variant="outline">
Reiniciar Ejercicio
</Button>
</div>
)}
</div>
</Card>
);
}
return (
<Card className="w-full max-w-2xl mx-auto">
<CardHeader
title={question}
subtitle={
questionNumber && totalQuestions
? `Pregunta ${questionNumber} de ${totalQuestions}`
: undefined
}
/>
{/* Progress bar for multi-question exercises */}
{questionNumber && totalQuestions && (
<div className="mb-6">
<div className="flex justify-between text-sm text-gray-600 mb-2">
<span>Progreso</span>
<span>{Math.round((questionNumber / totalQuestions) * 100)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<motion.div
className="bg-blue-600 h-2.5 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${(questionNumber / totalQuestions) * 100}%` }}
transition={{ duration: 0.3 }}
/>
</div>
</div>
)}
<div className="space-y-3 mb-6">
{options.map((option, index) => {
const isSelected = selectedOption === option.id;
const showCorrect = showFeedback && option.isCorrect;
const showIncorrect = showFeedback && isSelected && !option.isCorrect;
return (
<motion.button
key={option.id}
onClick={() => handleSelectOption(option.id)}
disabled={showFeedback}
whileHover={!showFeedback ? { scale: 1.02 } : {}}
whileTap={!showFeedback ? { scale: 0.98 } : {}}
className={`w-full p-4 rounded-lg border-2 text-left transition-all ${
showCorrect
? 'border-green-500 bg-green-50'
: showIncorrect
? 'border-red-500 bg-red-50'
: isSelected
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 text-gray-700 font-semibold text-sm">
{String.fromCharCode(65 + index)}
</span>
<span className="font-medium">{option.text}</span>
</div>
{showCorrect && <CheckCircle size={20} className="text-green-600" />}
{showIncorrect && <XCircle size={20} className="text-red-600" />}
</div>
</motion.button>
);
})}
</div>
{/* Hint section */}
<AnimatePresence>
{showHint && currentHint && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden mb-6"
>
<div className="p-4 rounded-lg border bg-yellow-50 border-yellow-200">
<div className="flex items-center gap-2 mb-2">
<Lightbulb size={20} className="text-yellow-600" />
<span className="font-semibold text-yellow-800">Pista</span>
</div>
<p className="text-sm text-yellow-700">{currentHint}</p>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Feedback section */}
<AnimatePresence>
{showFeedback && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden mb-6"
>
<div
className={`p-4 rounded-lg border ${
isCorrect ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'
}`}
>
<div className="flex items-center gap-2 mb-2">
{isCorrect ? (
<CheckCircle size={20} className="text-green-600" />
) : (
<XCircle size={20} className="text-red-600" />
)}
<span
className={`font-semibold ${
isCorrect ? 'text-green-800' : 'text-red-800'
}`}
>
{isCorrect ? '¡Correcto!' : 'Incorrecto'}
</span>
</div>
<p className={`text-sm ${isCorrect ? 'text-green-700' : 'text-red-700'}`}>
{explanation}
</p>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="flex justify-between items-center">
<div className="text-sm text-gray-600">
<span>
Intentos: <span className="font-bold">{attempts}</span>/{maxAttempts}
</span>
{canShowHint && (
<Button
variant="ghost"
size="sm"
onClick={handleShowHint}
className="ml-2 text-yellow-600 hover:text-yellow-700"
>
<Lightbulb size={16} className="mr-1" />
Ver pista
</Button>
)}
</div>
{!showFeedback ? (
<Button onClick={handleSubmit} disabled={!selectedOption}>
Validar Respuesta
</Button>
) : (
<Button onClick={handleNext}>
{isCorrect ? (
<>
Continuar
<ArrowRight size={16} className="ml-2" />
</>
) : canRetry ? (
<>
Intentar de Nuevo
<ArrowRight size={16} className="ml-2" />
</>
) : (
'Ver Resultado'
)}
</Button>
)}
</div>
</Card>
);
}
export default QuizExercise;

View File

@@ -0,0 +1,3 @@
export { QuizExercise, type QuizExerciseProps, type QuizOption, type QuizHint, type QuizResult } from './QuizExercise';
export { CalculatorExercise, type CalculatorExerciseProps, type CalculationStep } from './CalculatorExercise';
export { MatchingExercise, type MatchingExerciseProps, type MatchingItem, type MatchingPair, type MatchingResult } from './MatchingExercise';

View File

@@ -1 +1,3 @@
export { EjercicioWrapper } from './EjercicioWrapper';
export { QuizExercise } from './common/QuizExercise';
export type { QuizExerciseProps, QuizOption, QuizHint, QuizResult } from './common/QuizExercise';

View File

@@ -0,0 +1,304 @@
import React, { useState } from 'react';
interface Pregunta {
id: number;
pregunta: string;
opciones: { letra: string; texto: string; correcta: boolean }[];
explicacion: string;
categoria: string;
}
const preguntas: Pregunta[] = [
{
id: 1,
pregunta: "¿Cuál es la función principal de las familias como agente económico?",
opciones: [
{ letra: "A", texto: "Producir bienes y servicios para el mercado", correcta: false },
{ letra: "B", texto: "Ofrecer factores productivos (trabajo, capital) y consumir", correcta: true },
{ letra: "C", texto: "Regular la economía y recaudar impuestos", correcta: false },
{ letra: "D", texto: "Importar y exportar productos", correcta: false },
],
explicacion: "Las familias son agentes económicos que ofrecen factores de producción (especialmente trabajo) a las empresas y utilizan sus ingresos para consumir bienes y servicios.",
categoria: "Familias"
},
{
id: 2,
pregunta: "¿Qué tipo de empresas son las que buscan maximizar beneficios?",
opciones: [
{ letra: "A", texto: "Empresas públicas", correcta: false },
{ letra: "B", texto: "Empresas privadas", correcta: true },
{ letra: "C", texto: "ONGs", correcta: false },
{ letra: "D", texto: "Cooperativas", correcta: false },
],
explicacion: "Las empresas privadas tienen como objetivo principal la maximización de beneficios o ganancias, a diferencia de las empresas públicas que persiguen objetivos de bienestar social.",
categoria: "Empresas"
},
{
id: 3,
pregunta: "¿Cuál de las siguientes NO es una función del Estado como agente económico?",
opciones: [
{ letra: "A", texto: "Recaudar impuestos", correcta: false },
{ letra: "B", texto: "Regular la actividad económica", correcta: false },
{ letra: "C", texto: "Maximizar utilidades privadas", correcta: true },
{ letra: "D", texto: "Proporcionar bienes públicos", correcta: false },
],
explicacion: "El Estado no busca maximizar utilidades privadas; esa es la función de las empresas privadas. El Estado persigue el bienestar social y el funcionamiento ordenado de la economía.",
categoria: "Estado"
},
{
id: 4,
pregunta: "¿Qué flujo representa el pago de salarios en el circuito económico?",
opciones: [
{ letra: "A", texto: "Flujo real de bienes y servicios", correcta: false },
{ letra: "B", texto: "Flujo monetario del sector empresas a familias", correcta: true },
{ letra: "C", texto: "Flujo de impuestos al Estado", correcta: false },
{ letra: "D", texto: "Flujo de subsidios", correcta: false },
],
explicacion: "Los salarios representan un flujo monetario que va desde las empresas (que pagan) hacia las familias (que reciben el pago por su trabajo).",
categoria: "Circuito Económico"
},
{
id: 5,
pregunta: "¿Qué son los bienes públicos según la economía?",
opciones: [
{ letra: "A", texto: "Productos que solo pueden usar las familias ricas", correcta: false },
{ letra: "B", texto: "Bienes no rivales y no excluibles proporcionados por el Estado", correcta: true },
{ letra: "C", texto: "Productos importados de otros países", correcta: false },
{ letra: "D", texto: "Bienes de lujo que produce el sector privado", correcta: false },
],
explicacion: "Los bienes públicos son aquellos que son no rivales (el uso por una persona no impide el uso por otra) y no excluibles (no se puede impedir que alguien los use), como la defensa nacional o los parques públicos.",
categoria: "Bienes Públicos"
},
{
id: 6,
pregunta: "¿Cuál es la relación entre empresas y familias en el mercado de factores?",
opciones: [
{ letra: "A", texto: "Las empresas ofrecen trabajo y las familias lo demandan", correcta: false },
{ letra: "B", texto: "Las familias ofrecen factores productivos y las empresas los demandan", correcta: true },
{ letra: "C", texto: "El Estado controla ambos lados del mercado", correcta: false },
{ letra: "D", texto: "No hay relación entre ellos", correcta: false },
],
explicacion: "En el mercado de factores, las familias son los oferentes (proveen trabajo, tierra, capital) y las empresas son los demandantes de estos factores productivos.",
categoria: "Mercado de Factores"
},
{
id: 7,
pregunta: "¿Qué papel juega el Estado en la redistribución del ingreso?",
opciones: [
{ letra: "A", texto: "No interviene en la distribución del ingreso", correcta: false },
{ letra: "B", texto: "Recauda impuestos y proporciona transferencias y servicios sociales", correcta: true },
{ letra: "C", texto: "Solo cobra impuestos a las empresas", correcta: false },
{ letra: "D", texto: "Fija los salarios de todos los trabajadores", correcta: false },
],
explicacion: "El Estado redistribuye el ingreso mediante el cobro de impuestos (generalmente progresivos) y el gasto en transferencias, subsidios, educación, salud y otros servicios públicos.",
categoria: "Redistribución"
},
{
id: 8,
pregunta: "¿Cuál es un ejemplo de empresa estatal?",
opciones: [
{ letra: "A", texto: "Una tienda de ropa privada", correcta: false },
{ letra: "B", texto: "Una empresa petrolera nacional", correcta: true },
{ letra: "C", texto: "Un restaurante familiar", correcta: false },
{ letra: "D", texto: "Una empresa tecnológica multinacional", correcta: false },
],
explicacion: "Las empresas petroleras nacionales (como PEMEX, Petrobras, PDVSA) son ejemplos clásicos de empresas estatales, propiedad del gobierno.",
categoria: "Empresas Estatales"
}
];
export const AgentesEconomicosQuiz: React.FC = () => {
const [preguntaActual, setPreguntaActual] = useState<number>(0);
const [respuestas, setRespuestas] = useState<{ [key: number]: string }>({});
const [mostrarResultado, setMostrarResultado] = useState<boolean>(false);
const [quizTerminado, setQuizTerminado] = useState<boolean>(false);
const seleccionarRespuesta = (letra: string) => {
setRespuestas({ ...respuestas, [preguntas[preguntaActual].id]: letra });
setMostrarResultado(true);
};
const siguientePregunta = () => {
if (preguntaActual < preguntas.length - 1) {
setPreguntaActual(preguntaActual + 1);
setMostrarResultado(false);
} else {
setQuizTerminado(true);
}
};
const anteriorPregunta = () => {
if (preguntaActual > 0) {
setPreguntaActual(preguntaActual - 1);
setMostrarResultado(true);
}
};
const reiniciarQuiz = () => {
setPreguntaActual(0);
setRespuestas({});
setMostrarResultado(false);
setQuizTerminado(false);
};
const calcularPuntuacion = () => {
let correctas = 0;
preguntas.forEach(pregunta => {
const opcionCorrecta = pregunta.opciones.find(o => o.correcta);
if (opcionCorrecta && respuestas[pregunta.id] === opcionCorrecta.letra) {
correctas++;
}
});
return correctas;
};
if (quizTerminado) {
const puntuacion = calcularPuntuacion();
const porcentaje = (puntuacion / preguntas.length) * 100;
return (
<div className="max-w-4xl mx-auto p-6">
<h2 className="text-2xl font-bold mb-6">Resultados del Quiz</h2>
<div className="bg-blue-50 p-6 rounded-lg text-center mb-6">
<p className="text-4xl font-bold text-blue-600 mb-2">
{puntuacion} / {preguntas.length}
</p>
<p className="text-xl">{porcentaje.toFixed(0)}% de aciertos</p>
<p className="mt-4">
{porcentaje >= 80
? '🎉 ¡Excelente! Dominas los agentes económicos'
: porcentaje >= 60
? '👍 ¡Bien! Puedes mejorar un poco más'
: '📚 Sigue estudiando los agentes económicos'}
</p>
</div>
<div className="space-y-4 mb-6">
{preguntas.map((pregunta, index) => {
const respuestaUsuario = respuestas[pregunta.id];
const opcionCorrecta = pregunta.opciones.find(o => o.correcta);
const esCorrecta = respuestaUsuario === opcionCorrecta?.letra;
return (
<div key={pregunta.id} className={`p-4 rounded-lg ${esCorrecta ? 'bg-green-100' : 'bg-red-100'}`}>
<p className="font-semibold">{index + 1}. {pregunta.pregunta}</p>
<p className="text-sm mt-2">
Tu respuesta: <span className={esCorrecta ? 'text-green-700 font-medium' : 'text-red-700'}>
{respuestaUsuario || 'Sin respuesta'}
</span>
{!esCorrecta && (
<span className="text-green-700 font-medium ml-4">
Correcta: {opcionCorrecta?.letra}
</span>
)}
</p>
</div>
);
})}
</div>
<button
onClick={reiniciarQuiz}
className="w-full bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600"
>
Intentar de Nuevo
</button>
</div>
);
}
const pregunta = preguntas[preguntaActual];
const respuestaSeleccionada = respuestas[pregunta.id];
return (
<div className="max-w-4xl mx-auto p-6">
<h2 className="text-2xl font-bold mb-2">Quiz: Agentes Económicos</h2>
<div className="flex justify-between items-center mb-6">
<span className="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm">
Pregunta {preguntaActual + 1} de {preguntas.length}
</span>
<span className="bg-gray-100 text-gray-800 px-3 py-1 rounded-full text-sm">
Categoría: {pregunta.categoria}
</span>
</div>
<div className="bg-white border rounded-lg p-6 mb-6">
<h3 className="text-lg font-semibold mb-4">{pregunta.pregunta}</h3>
<div className="space-y-3">
{pregunta.opciones.map((opcion) => {
const estaSeleccionada = respuestaSeleccionada === opcion.letra;
const mostrarCorrecta = mostrarResultado && opcion.correcta;
const mostrarIncorrecta = mostrarResultado && estaSeleccionada && !opcion.correcta;
return (
<button
key={opcion.letra}
onClick={() => !mostrarResultado && seleccionarRespuesta(opcion.letra)}
disabled={mostrarResultado}
className={`w-full text-left p-4 rounded-lg border-2 transition-all ${
mostrarCorrecta
? 'border-green-500 bg-green-50'
: mostrarIncorrecta
? 'border-red-500 bg-red-50'
: estaSeleccionada
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
>
<span className="font-bold mr-2">{opcion.letra})</span>
{opcion.texto}
{mostrarCorrecta && <span className="ml-2 text-green-600"></span>}
{mostrarIncorrecta && <span className="ml-2 text-red-600"></span>}
</button>
);
})}
</div>
{mostrarResultado && (
<div className="mt-6 p-4 bg-yellow-50 rounded-lg">
<p className="font-semibold mb-2">Explicación:</p>
<p className="text-gray-700">{pregunta.explicacion}</p>
</div>
)}
</div>
<div className="flex justify-between">
<button
onClick={anteriorPregunta}
disabled={preguntaActual === 0}
className="bg-gray-500 text-white px-6 py-2 rounded hover:bg-gray-600 disabled:bg-gray-300"
>
Anterior
</button>
<button
onClick={siguientePregunta}
disabled={!respuestaSeleccionada}
className="bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600 disabled:bg-gray-300"
>
{preguntaActual === preguntas.length - 1 ? 'Ver Resultados' : 'Siguiente'}
</button>
</div>
<div className="mt-6 flex justify-center gap-2">
{preguntas.map((_, index) => (
<div
key={index}
className={`w-3 h-3 rounded-full ${
index === preguntaActual
? 'bg-blue-500'
: respuestas[preguntas[index].id]
? 'bg-green-400'
: 'bg-gray-300'
}`}
/>
))}
</div>
</div>
);
};
export default AgentesEconomicosQuiz;

View File

@@ -0,0 +1,305 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, Globe, TrendingUp, Building2, Scale } from 'lucide-react';
interface EjercicioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
type SistemaTipo = 'mercado' | 'planificado' | 'mixto';
interface Pais {
id: string;
nombre: string;
emoji: string;
descripcion: string;
caracteristicas: string[];
sistemaCorrecto: SistemaTipo;
explicacion: string;
}
const SISTEMAS: Record<SistemaTipo, { nombre: string; color: string; icono: React.ReactNode }> = {
mercado: {
nombre: 'Economía de Mercado',
color: 'bg-blue-500',
icono: <TrendingUp className="w-5 h-5" />
},
planificado: {
nombre: 'Economía Planificada',
color: 'bg-red-500',
icono: <Building2 className="w-5 h-5" />
},
mixto: {
nombre: 'Economía Mixta',
color: 'bg-green-500',
icono: <Scale className="w-5 h-5" />
}
};
const PAISES: Pais[] = [
{
id: 'singapur',
nombre: 'Singapur',
emoji: '🇸🇬',
descripcion: 'Centro financiero asiático con uno de los índices de libertad económica más altos del mundo.',
caracteristicas: [
'Libre comercio y bajos aranceles',
'Impuestos corporativos bajos',
'Mínima intervención estatal en negocios',
'Sector privado altamente competitivo'
],
sistemaCorrecto: 'mercado',
explicacion: 'Singapur es un ejemplo clásico de economía de mercado, con mínima regulación, bajos impuestos y gran libertad para la empresa privada.'
},
{
id: 'noruega',
nombre: 'Noruega',
emoji: '🇳🇴',
descripcion: 'País escandinavo con altos estándares de vida y fuerte sector petrolero estatal.',
caracteristicas: [
'Servicios públicos universales gratuitos',
'Fuerte sistema de bienestar social',
'Empresas privadas con regulación estatal',
'Fondo soberano de petróleo gestionado por el Estado'
],
sistemaCorrecto: 'mixto',
explicacion: 'Noruega combina economía de mercado con fuerte intervención estatal en bienestar social y sectores estratégicos.'
},
{
id: 'cuba',
nombre: 'Cuba',
emoji: '🇨🇺',
descripcion: 'Isla caribeña con sistema económico único en el hemisferio occidental.',
caracteristicas: [
'Mayoría de empresas son estatales',
'Planificación centralizada',
'Racionamiento de bienes básicos',
'Recientemente ha permitido pequeñas empresas privadas'
],
sistemaCorrecto: 'planificado',
explicacion: 'Cuba mantiene principalmente una economía planificada donde el Estado controla la mayoría de los medios de producción.'
},
{
id: 'suiza',
nombre: 'Suiza',
emoji: '🇨🇭',
descripcion: 'País alpino conocido por su estabilidad económica y sistema bancario.',
caracteristicas: [
'Política fiscal conservadora',
'Fuerte protección de la propiedad privada',
'Mercado laboral flexible',
'Alta competitividad internacional'
],
sistemaCorrecto: 'mercado',
explicacion: 'Suiza opera principalmente como economía de mercado con fuerte protección a la propiedad privada y libre empresa.'
},
{
id: 'francia',
nombre: 'Francia',
emoji: '🇫🇷',
descripcion: 'Potencia europea con tradición de intervención estatal en la economía.',
caracteristicas: [
'Altos impuestos para financiar servicios públicos',
'Regulación extensa del mercado laboral',
'Empresas privadas dominantes pero reguladas',
'Sistema de salud público universal'
],
sistemaCorrecto: 'mixto',
explicacion: 'Francia representa una economía mixta europea donde coexisten empresas privadas con fuerte regulación y servicios públicos extensos.'
},
{
id: 'corea-norte',
nombre: 'Corea del Norte',
emoji: '🇰🇵',
descripcion: 'País asiático con uno de los sistemas económicos más cerrados del mundo.',
caracteristicas: [
'Planificación económica centralizada (Juche)',
'Propiedad estatal total de medios de producción',
'Comercio internacional severamente restringido',
'Distribución de bienes por el Estado'
],
sistemaCorrecto: 'planificado',
explicacion: 'Corea del Norte mantiene una economía altamente centralizada y planificada con mínima actividad de mercado permitida.'
},
{
id: 'hong-kong',
nombre: 'Hong Kong',
emoji: '🇭🇰',
descripcion: 'Región administrativa especial de China con sistema económico único.',
caracteristicas: [
'Política de "un país, dos sistemas"',
'Libertad económica y financiera',
'Bajos impuestos y aranceles',
'Mínima intervención gubernamental'
],
sistemaCorrecto: 'mercado',
explicacion: 'Hong Kong históricamente ha operado como economía de mercado con mínima regulación y máxima libertad comercial.'
},
{
id: 'alemania',
nombre: 'Alemania',
emoji: '🇩🇪',
descripcion: 'Mayor economía de Europa con modelo social de mercado.',
caracteristicas: [
'Economía social de mercado',
'Codeterminación (trabajadores en consejos)',
'Fuerte sector industrial privado',
'Extensas redes de protección social'
],
sistemaCorrecto: 'mixto',
explicacion: 'Alemania practica el modelo de "economía social de mercado", combinando mercado libre con fuerte estado de bienestar.'
}
];
export function CasosPaises({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) {
const [paisActual, setPaisActual] = useState(0);
const [respuestas, setRespuestas] = useState<Record<string, SistemaTipo>>({});
const [mostrarResultado, setMostrarResultado] = useState(false);
const [completado, setCompletado] = useState(false);
const pais = PAISES[paisActual];
const esUltima = paisActual === PAISES.length - 1;
const handleRespuesta = (sistema: SistemaTipo) => {
setRespuestas(prev => ({ ...prev, [pais.id]: sistema }));
setMostrarResultado(true);
};
const handleSiguiente = () => {
if (esUltima) {
const correctas = PAISES.filter(p => respuestas[p.id] === p.sistemaCorrecto).length;
const puntuacion = Math.round((correctas / PAISES.length) * 100);
setCompletado(true);
onComplete?.(puntuacion);
} else {
setPaisActual(prev => prev + 1);
setMostrarResultado(false);
}
};
const esCorrecta = respuestas[pais.id] === pais.sistemaCorrecto;
if (completado) {
const correctas = PAISES.filter(p => respuestas[p.id] === p.sistemaCorrecto).length;
const puntuacion = Math.round((correctas / PAISES.length) * 100);
return (
<Card className="w-full max-w-2xl mx-auto text-center p-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="mb-6"
>
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto">
<Globe className="w-10 h-10 text-green-600" />
</div>
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">¡Ejercicio Completado!</h3>
<p className="text-gray-600 mb-6">
Identificaste correctamente {correctas} de {PAISES.length} países
</p>
<div className="text-5xl font-bold text-blue-600 mb-2">{puntuacion}</div>
<p className="text-gray-500">puntos</p>
</Card>
);
}
return (
<Card className="w-full max-w-2xl mx-auto">
<div className="p-6">
<div className="mb-6">
<div className="flex justify-between text-sm text-gray-500 mb-2">
<span>País {paisActual + 1} de {PAISES.length}</span>
<span>{Math.round((paisActual / PAISES.length) * 100)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<motion.div
className="bg-blue-500 h-2 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${(paisActual / PAISES.length) * 100}%` }}
/>
</div>
</div>
<AnimatePresence mode="wait">
<motion.div
key={pais.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
>
<div className="text-center mb-6">
<span className="text-6xl mb-2 block">{pais.emoji}</span>
<h3 className="text-2xl font-bold text-gray-900">{pais.nombre}</h3>
<p className="text-gray-600 mt-2">{pais.descripcion}</p>
</div>
<div className="bg-gray-50 rounded-xl p-4 mb-6">
<h4 className="font-semibold text-gray-700 mb-3">Características económicas:</h4>
<ul className="space-y-2">
{pais.caracteristicas.map((caracteristica, index) => (
<li key={index} className="flex items-start gap-2 text-gray-600">
<span className="text-blue-500 mt-1"></span>
<span>{caracteristica}</span>
</li>
))}
</ul>
</div>
{!mostrarResultado ? (
<div className="space-y-3">
<h4 className="font-semibold text-gray-700 mb-4">¿Qué sistema económico predomina?</h4>
{(Object.keys(SISTEMAS) as SistemaTipo[]).map((sistema) => (
<motion.button
key={sistema}
onClick={() => handleRespuesta(sistema)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="w-full p-4 flex items-center gap-4 border-2 border-gray-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all"
>
<div className={`${SISTEMAS[sistema].color} text-white p-2 rounded-lg`}>
{SISTEMAS[sistema].icono}
</div>
<span className="font-medium text-gray-700">{SISTEMAS[sistema].nombre}</span>
</motion.button>
))}
</div>
) : (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`p-6 rounded-xl mb-6 ${esCorrecta ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}
>
<div className="flex items-center gap-3 mb-3">
{esCorrecta ? (
<CheckCircle className="w-6 h-6 text-green-600" />
) : (
<XCircle className="w-6 h-6 text-red-600" />
)}
<span className={`font-bold ${esCorrecta ? 'text-green-800' : 'text-red-800'}`}>
{esCorrecta ? '¡Correcto!' : 'Incorrecto'}
</span>
</div>
<p className="text-gray-700 mb-2">
<strong>Respuesta correcta:</strong> {SISTEMAS[pais.sistemaCorrecto].nombre}
</p>
<p className="text-gray-600">{pais.explicacion}</p>
<Button onClick={handleSiguiente} className="mt-4 w-full">
{esUltima ? 'Finalizar' : 'Siguiente país'}
</Button>
</motion.div>
)}
</motion.div>
</AnimatePresence>
</div>
</Card>
);
}
export default CasosPaises;

View File

@@ -0,0 +1,334 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, ArrowRight, ArrowLeft } from 'lucide-react';
interface EjercicioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Categoria {
id: string;
nombre: string;
color: string;
}
interface Casilla {
id: string;
categoria: string;
sistema: 'mercado' | 'planificado' | 'mixto' | null;
opciones: string[];
correcta: string;
}
const CATEGORIAS: Categoria[] = [
{ id: 'propiedad', nombre: 'Propiedad de medios de producción', color: 'bg-blue-100' },
{ id: 'precios', nombre: 'Fijación de precios', color: 'bg-green-100' },
{ id: 'competencia', nombre: 'Competencia', color: 'bg-purple-100' },
{ id: 'objetivo', nombre: 'Objetivo principal', color: 'bg-orange-100' },
{ id: 'planificacion', nombre: 'Planificación económica', color: 'bg-pink-100' },
{ id: 'bienestar', nombre: 'Bienestar social', color: 'bg-teal-100' }
];
const SISTEMAS = [
{ id: 'mercado', nombre: 'Economía de Mercado', color: 'bg-blue-500' },
{ id: 'planificado', nombre: 'Economía Planificada', color: 'bg-red-500' },
{ id: 'mixto', nombre: 'Economía Mixta', color: 'bg-green-500' }
];
const CASILLAS: Casilla[] = [
{
id: 'propiedad-mercado',
categoria: 'propiedad',
sistema: 'mercado',
opciones: ['Privada', 'Estatal', 'Mixta'],
correcta: 'Privada'
},
{
id: 'propiedad-planificado',
categoria: 'propiedad',
sistema: 'planificado',
opciones: ['Privada', 'Estatal', 'Mixta'],
correcta: 'Estatal'
},
{
id: 'propiedad-mixto',
categoria: 'propiedad',
sistema: 'mixto',
opciones: ['Privada', 'Estatal', 'Mixta'],
correcta: 'Mixta'
},
{
id: 'precios-mercado',
categoria: 'precios',
sistema: 'mercado',
opciones: ['Oferta y demanda', 'Estado', 'Combinación'],
correcta: 'Oferta y demanda'
},
{
id: 'precios-planificado',
categoria: 'precios',
sistema: 'planificado',
opciones: ['Oferta y demanda', 'Estado', 'Combinación'],
correcta: 'Estado'
},
{
id: 'precios-mixto',
categoria: 'precios',
sistema: 'mixto',
opciones: ['Oferta y demanda', 'Estado', 'Combinación'],
correcta: 'Combinación'
},
{
id: 'competencia-mercado',
categoria: 'competencia',
sistema: 'mercado',
opciones: ['Libre', 'No existe', 'Regulada'],
correcta: 'Libre'
},
{
id: 'competencia-planificado',
categoria: 'competencia',
sistema: 'planificado',
opciones: ['Libre', 'No existe', 'Regulada'],
correcta: 'No existe'
},
{
id: 'competencia-mixto',
categoria: 'competencia',
sistema: 'mixto',
opciones: ['Libre', 'No existe', 'Regulada'],
correcta: 'Regulada'
},
{
id: 'objetivo-mercado',
categoria: 'objetivo',
sistema: 'mercado',
opciones: ['Beneficio', 'Igualdad', 'Equilibrio'],
correcta: 'Beneficio'
},
{
id: 'objetivo-planificado',
categoria: 'objetivo',
sistema: 'planificado',
opciones: ['Beneficio', 'Igualdad', 'Equilibrio'],
correcta: 'Igualdad'
},
{
id: 'objetivo-mixto',
categoria: 'objetivo',
sistema: 'mixto',
opciones: ['Beneficio', 'Igualdad', 'Equilibrio'],
correcta: 'Equilibrio'
},
{
id: 'planificacion-mercado',
categoria: 'planificacion',
sistema: 'mercado',
opciones: ['Descentralizada', 'Centralizada', 'Mixta'],
correcta: 'Descentralizada'
},
{
id: 'planificacion-planificado',
categoria: 'planificacion',
sistema: 'planificado',
opciones: ['Descentralizada', 'Centralizada', 'Mixta'],
correcta: 'Centralizada'
},
{
id: 'planificacion-mixto',
categoria: 'planificacion',
sistema: 'mixto',
opciones: ['Descentralizada', 'Centralizada', 'Mixta'],
correcta: 'Mixta'
},
{
id: 'bienestar-mercado',
categoria: 'bienestar',
sistema: 'mercado',
opciones: ['Privado', 'Estatal', 'Combinado'],
correcta: 'Privado'
},
{
id: 'bienestar-planificado',
categoria: 'bienestar',
sistema: 'planificado',
opciones: ['Privado', 'Estatal', 'Combinado'],
correcta: 'Estatal'
},
{
id: 'bienestar-mixto',
categoria: 'bienestar',
sistema: 'mixto',
opciones: ['Privado', 'Estatal', 'Combinado'],
correcta: 'Combinado'
}
];
export function ComparativaSistemas({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) {
const [respuestas, setRespuestas] = useState<Record<string, string>>({});
const [casillaActual, setCasillaActual] = useState(0);
const [completado, setCompletado] = useState(false);
const [mostrarResultado, setMostrarResultado] = useState(false);
const casilla = CASILLAS[casillaActual];
const esUltima = casillaActual === CASILLAS.length - 1;
const handleRespuesta = (respuesta: string) => {
setRespuestas(prev => ({ ...prev, [casilla.id]: respuesta }));
setMostrarResultado(true);
};
const handleSiguiente = () => {
if (esUltima) {
const correctas = CASILLAS.filter(c => respuestas[c.id] === c.correcta).length;
const puntuacion = Math.round((correctas / CASILLAS.length) * 100);
setCompletado(true);
onComplete?.(puntuacion);
} else {
setCasillaActual(prev => prev + 1);
setMostrarResultado(false);
}
};
const handleAnterior = () => {
if (casillaActual > 0) {
setCasillaActual(prev => prev - 1);
setMostrarResultado(false);
}
};
const esCorrecta = respuestas[casilla.id] === casilla.correcta;
const categoria = CATEGORIAS.find(c => c.id === casilla.categoria);
const sistema = SISTEMAS.find(s => s.id === casilla.sistema);
const yaRespondida = respuestas[casilla.id] !== undefined;
if (completado) {
const correctas = CASILLAS.filter(c => respuestas[c.id] === c.correcta).length;
const puntuacion = Math.round((correctas / CASILLAS.length) * 100);
return (
<Card className="w-full max-w-2xl mx-auto text-center p-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="mb-6"
>
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto">
<CheckCircle className="w-10 h-10 text-green-600" />
</div>
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">¡Ejercicio Completado!</h3>
<p className="text-gray-600 mb-6">
Completaste {correctas} de {CASILLAS.length} casillas correctamente
</p>
<div className="text-5xl font-bold text-blue-600 mb-2">{puntuacion}</div>
<p className="text-gray-500">puntos</p>
</Card>
);
}
return (
<Card className="w-full max-w-3xl mx-auto">
<div className="p-6">
<div className="mb-6">
<div className="flex justify-between text-sm text-gray-500 mb-2">
<span>Casilla {casillaActual + 1} de {CASILLAS.length}</span>
<span>{Math.round((Object.keys(respuestas).length / CASILLAS.length) * 100)}% completado</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<motion.div
className="bg-blue-500 h-2 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${(Object.keys(respuestas).length / CASILLAS.length) * 100}%` }}
/>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<div className={`${categoria?.color} p-4 rounded-xl`}>
<p className="text-sm font-semibold text-gray-600 mb-1">Categoría</p>
<p className="font-bold text-gray-900">{categoria?.nombre}</p>
</div>
<div className={`${sistema?.color} text-white p-4 rounded-xl`}>
<p className="text-sm font-semibold text-white/80 mb-1">Sistema Económico</p>
<p className="font-bold">{sistema?.nombre}</p>
</div>
<div className="bg-gray-100 p-4 rounded-xl">
<p className="text-sm font-semibold text-gray-600 mb-1">Progreso</p>
<p className="font-bold text-gray-900">{Object.keys(respuestas).length}/{CASILLAS.length}</p>
</div>
</div>
<AnimatePresence mode="wait">
{!mostrarResultado ? (
<motion.div
key="pregunta"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-4"
>
<h3 className="text-lg font-bold text-gray-900">
¿Cómo se caracteriza esta dimensión en {sistema?.nombre}?
</h3>
<div className="grid grid-cols-1 gap-3">
{casilla.opciones.map((opcion) => (
<motion.button
key={opcion}
onClick={() => handleRespuesta(opcion)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="p-4 text-left border-2 border-gray-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all"
>
<span className="font-medium text-gray-700">{opcion}</span>
</motion.button>
))}
</div>
</motion.div>
) : (
<motion.div
key="resultado"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`p-6 rounded-xl ${esCorrecta ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}
>
<div className="flex items-center gap-3 mb-3">
{esCorrecta ? (
<CheckCircle className="w-6 h-6 text-green-600" />
) : (
<XCircle className="w-6 h-6 text-red-600" />
)}
<span className={`font-bold ${esCorrecta ? 'text-green-800' : 'text-red-800'}`}>
{esCorrecta ? '¡Correcto!' : 'Incorrecto'}
</span>
</div>
<p className="text-gray-700 mb-4">
{esCorrecta
? `Correcto. En ${sistema?.nombre}, la ${categoria?.nombre.toLowerCase()} es ${casilla.correcta.toLowerCase()}.`
: `La respuesta correcta es: ${casilla.correcta}. En ${sistema?.nombre}, la ${categoria?.nombre.toLowerCase()} se caracteriza por ser ${casilla.correcta.toLowerCase()}.`
}
</p>
<div className="flex gap-3">
<Button onClick={handleAnterior} variant="outline" disabled={casillaActual === 0}>
<ArrowLeft className="w-4 h-4 mr-2" />
Anterior
</Button>
<Button onClick={handleSiguiente}>
{esUltima ? 'Finalizar' : 'Siguiente'}
{!esUltima && <ArrowRight className="w-4 h-4 ml-2" />}
</Button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</Card>
);
}
export default ComparativaSistemas;

View File

@@ -0,0 +1,157 @@
import React, { useState } from 'react';
interface OpcionProduccion {
bienesA: number;
bienesB: number;
}
const datosFPP: OpcionProduccion[] = [
{ bienesA: 0, bienesB: 100 },
{ bienesA: 20, bienesB: 90 },
{ bienesA: 40, bienesB: 75 },
{ bienesA: 60, bienesB: 55 },
{ bienesA: 80, bienesB: 30 },
{ bienesA: 100, bienesB: 0 },
];
export const CostoOportunidadCalculator: React.FC = () => {
const [puntoInicial, setPuntoInicial] = useState<number>(0);
const [puntoFinal, setPuntoFinal] = useState<number>(1);
const [respuestaUsuario, setRespuestaUsuario] = useState<string>('');
const [resultado, setResultado] = useState<{
correcto: boolean;
mensaje: string;
costoReal: number;
} | null>(null);
const calcularCostoOportunidad = (inicio: number, fin: number): number => {
const opcionInicio = datosFPP[inicio];
const opcionFin = datosFPP[fin];
const cambioBienB = opcionFin.bienesB - opcionInicio.bienesB;
const cambioBienA = opcionFin.bienesA - opcionInicio.bienesA;
if (cambioBienA === 0) return 0;
return Math.abs(cambioBienB / cambioBienA);
};
const verificarRespuesta = () => {
const costoReal = calcularCostoOportunidad(puntoInicial, puntoFinal);
const respuestaNum = parseFloat(respuestaUsuario);
if (isNaN(respuestaNum)) {
setResultado({
correcto: false,
mensaje: 'Por favor ingresa un número válido',
costoReal: costoReal
});
return;
}
const margenError = 0.5;
const correcto = Math.abs(respuestaNum - costoReal) <= margenError;
setResultado({
correcto,
mensaje: correcto
? '¡Correcto! Has calculado bien el costo de oportunidad.'
: 'Incorrecto. Revisa tu cálculo.',
costoReal: costoReal
});
};
const generarNuevoEjercicio = () => {
const nuevoInicio = Math.floor(Math.random() * (datosFPP.length - 1));
const nuevoFin = nuevoInicio + 1 + Math.floor(Math.random() * (datosFPP.length - nuevoInicio - 1));
setPuntoInicial(nuevoInicio);
setPuntoFinal(nuevoFin);
setRespuestaUsuario('');
setResultado(null);
};
return (
<div className="max-w-4xl mx-auto p-6">
<h2 className="text-2xl font-bold mb-4">Calculadora de Costo de Oportunidad</h2>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<h3 className="font-semibold mb-2">Tabla de Posibilidades de Producción:</h3>
<table className="w-full text-center">
<thead>
<tr className="bg-blue-100">
<th className="p-2">Opción</th>
<th className="p-2">Bien A</th>
<th className="p-2">Bien B</th>
</tr>
</thead>
<tbody>
{datosFPP.map((opcion, index) => (
<tr key={index} className={(index === puntoInicial || index === puntoFinal) ? 'bg-yellow-200' : ''}>
<td className="p-2">{index + 1}</td>
<td className="p-2">{opcion.bienesA}</td>
<td className="p-2">{opcion.bienesB}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gray-50 p-4 rounded-lg mb-6">
<h3 className="font-semibold mb-2">Ejercicio:</h3>
<p className="mb-4">
Si la economía se mueve de la <strong>Opción {puntoInicial + 1}</strong> a la
<strong> Opción {puntoFinal + 1}</strong>, ¿cuál es el costo de oportunidad
de producir una unidad adicional del Bien A?
</p>
<div className="flex items-center gap-4 mb-4">
<label className="font-medium">Costo de oportunidad:</label>
<input
type="number"
step="0.1"
value={respuestaUsuario}
onChange={(e) => setRespuestaUsuario(e.target.value)}
className="border p-2 rounded w-32"
placeholder="Ej: 0.75"
/>
<span>unidades del Bien B</span>
</div>
<div className="flex gap-2">
<button
onClick={verificarRespuesta}
className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
>
Verificar
</button>
<button
onClick={generarNuevoEjercicio}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Nuevo Ejercicio
</button>
</div>
</div>
{resultado && (
<div className={`p-4 rounded-lg ${resultado.correcto ? 'bg-green-100' : 'bg-red-100'}`}>
<p className="font-medium">{resultado.mensaje}</p>
{!resultado.correcto && (
<p className="mt-2 text-sm">
El costo de oportunidad correcto es: {resultado.costoReal.toFixed(2)} unidades del Bien B
</p>
)}
</div>
)}
<div className="mt-6 bg-yellow-50 p-4 rounded-lg">
<h4 className="font-semibold mb-2">Fórmula:</h4>
<p className="text-sm">
Costo de Oportunidad = |Cambio en Bien B| / |Cambio en Bien A|
</p>
</div>
</div>
);
};
export default CostoOportunidadCalculator;

View File

@@ -0,0 +1,246 @@
import { useState } from 'react';
interface CostoOportunidadCotidianoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Situacion {
id: number;
titulo: string;
descripcion: string;
decision: string;
opciones: string[];
costoOportunidadCorrecto: string;
explicacion: string;
}
const situaciones: Situacion[] = [
{
id: 1,
titulo: "Tiempo libre",
descripcion: "Tienes 3 horas libres un sábado por la tarde.",
decision: "Decides estudiar para un examen importante.",
opciones: [
"El tiempo que podrías haber pasado con amigos",
"Las calificaciones del examen",
"El dinero ahorrado",
"La comida que no comiste"
],
costoOportunidadCorrecto: "El tiempo que podrías haber pasado con amigos",
explicacion: "El costo de oportunidad es lo que sacrificas: el tiempo con amigos que elegiste no hacer."
},
{
id: 2,
titulo: "Compra de tecnología",
descripcion: "Tienes $1,000 ahorrados.",
decision: "Compras una laptop nueva para trabajar.",
opciones: [
"El dinero que gastaste",
"El dinero que podrías haber invertido",
"La laptop misma",
"Las especificaciones técnicas"
],
costoOportunidadCorrecto: "El dinero que podrías haber invertido",
explicacion: "Al gastar en la laptop, sacrificas la oportunidad de invertir ese dinero y obtener rendimientos."
},
{
id: 3,
titulo: "Carrera profesional",
descripcion: "Terminas la universidad con dos ofertas de trabajo.",
decision: "Aceptas el trabajo en una startup con menor salario inicial.",
opciones: [
"El salario más alto de la otra oferta",
"La experiencia en la startup",
"Tu título universitario",
"El tiempo de búsqueda"
],
costoOportunidadCorrecto: "El salario más alto de la otra oferta",
explicacion: "Al elegir la startup, renuncias al salario más alto que ofrecía la otra empresa."
},
{
id: 4,
titulo: "Vacaciones",
descripcion: "Tienes dos semanas de vacaciones este verano.",
decision: "Viajas a Europa en lugar de quedarte trabajando.",
opciones: [
"Las fotos que tomarás",
"El dinero que gastarás en el viaje",
"El dinero que podrías haber ganado trabajando",
"La experiencia cultural"
],
costoOportunidadCorrecto: "El dinero que podrías haber ganado trabajando",
explicacion: "El costo de oportunidad incluye los ingresos que sacrificas al no trabajar esas semanas."
}
];
export function CostoOportunidadCotidiano({ ejercicioId: _ejercicioId, onComplete }: CostoOportunidadCotidianoProps) {
const [respuestas, setRespuestas] = useState<{[key: number]: string}>({});
const [mostrarExplicacion, setMostrarExplicacion] = useState<{[key: number]: boolean}>({});
const [completado, setCompletado] = useState(false);
const handleSeleccion = (situacionId: number, opcion: string) => {
setRespuestas(prev => ({ ...prev, [situacionId]: opcion }));
};
const handleValidar = () => {
const nuevasExplicaciones: {[key: number]: boolean} = {};
let correctas = 0;
situaciones.forEach(situacion => {
nuevasExplicaciones[situacion.id] = true;
if (respuestas[situacion.id] === situacion.costoOportunidadCorrecto) {
correctas++;
}
});
setMostrarExplicacion(nuevasExplicaciones);
if (correctas === situaciones.length && !completado) {
setCompletado(true);
if (onComplete) {
onComplete(100);
}
}
};
const handleReset = () => {
setRespuestas({});
setMostrarExplicacion({});
setCompletado(false);
};
const correctas = situaciones.filter(s => respuestas[s.id] === s.costoOportunidadCorrecto).length;
return (
<div className="w-full max-w-4xl mx-auto bg-white rounded-lg shadow-lg p-6">
<div className="mb-6">
<h3 className="text-xl font-semibold text-gray-900">Costo de Oportunidad en Decisiones Cotidianas</h3>
<p className="text-sm text-gray-500 mt-1">
Identifica el costo de oportunidad en cada situación de la vida real.
</p>
</div>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
<p className="text-amber-800 text-sm">
<strong>Recuerda:</strong> El costo de oportunidad es el valor de la mejor alternativa a la que renuncias
al tomar una decisión. No es lo que gastas, sino lo que sacrificas.
</p>
</div>
<div className="space-y-6">
{situaciones.map((situacion, index) => (
<div
key={situacion.id}
className="border border-gray-200 rounded-lg p-5 hover:shadow-md transition-shadow"
>
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 font-bold">
{index + 1}
</div>
<div className="flex-grow">
<h4 className="font-semibold text-gray-900 text-lg">{situacion.titulo}</h4>
<p className="text-gray-600 mt-1">{situacion.descripcion}</p>
<p className="text-blue-700 font-medium mt-2">Decisión: {situacion.decision}</p>
<div className="mt-4">
<p className="text-sm font-medium text-gray-700 mb-3">
¿Cuál es el costo de oportunidad?
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{situacion.opciones.map((opcion) => {
const isSelected = respuestas[situacion.id] === opcion;
const isCorrect = opcion === situacion.costoOportunidadCorrecto;
const showResult = mostrarExplicacion[situacion.id];
let buttonClass = 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50';
if (showResult) {
if (isCorrect) {
buttonClass = 'border-green-500 bg-green-50 text-green-800';
} else if (isSelected && !isCorrect) {
buttonClass = 'border-red-500 bg-red-50 text-red-800';
} else {
buttonClass = 'border-gray-200 bg-gray-50 text-gray-400';
}
} else if (isSelected) {
buttonClass = 'border-blue-500 bg-blue-50 text-blue-800';
}
return (
<button
key={opcion}
onClick={() => !showResult && handleSeleccion(situacion.id, opcion)}
disabled={showResult}
className={`p-3 rounded-lg text-left text-sm transition-all ${buttonClass}`}
>
{opcion}
{showResult && isCorrect && (
<span className="ml-2 text-green-600 font-bold"></span>
)}
</button>
);
})}
</div>
</div>
{mostrarExplicacion[situacion.id] && (
<div className={`mt-4 p-4 rounded-lg ${
respuestas[situacion.id] === situacion.costoOportunidadCorrecto
? 'bg-green-50 border border-green-200'
: 'bg-amber-50 border border-amber-200'
}`}>
<p className={`text-sm font-medium ${
respuestas[situacion.id] === situacion.costoOportunidadCorrecto
? 'text-green-800'
: 'text-amber-800'
}`}>
{respuestas[situacion.id] === situacion.costoOportunidadCorrecto
? '¡Correcto!'
: 'Respuesta correcta:'}
</p>
<p className={`text-sm mt-1 ${
respuestas[situacion.id] === situacion.costoOportunidadCorrecto
? 'text-green-700'
: 'text-amber-700'
}`}>
{situacion.explicacion}
</p>
</div>
)}
</div>
</div>
</div>
))}
</div>
<div className="mt-6 flex gap-3">
<button
onClick={handleValidar}
disabled={completado || Object.keys(respuestas).length < situaciones.length}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition-colors"
>
Validar Respuestas
</button>
<button
onClick={handleReset}
className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Reiniciar
</button>
</div>
{completado && (
<div className="mt-6 bg-green-100 border border-green-300 rounded-lg p-4 text-center">
<p className="text-green-800 font-semibold">¡Excelente comprensión!</p>
<p className="text-green-700 text-2xl font-bold mt-1">100 puntos</p>
<p className="text-green-700 text-sm mt-2">
Has identificado correctamente todos los costos de oportunidad.
</p>
</div>
)}
</div>
);
}
export default CostoOportunidadCotidiano;

View File

@@ -0,0 +1,205 @@
import React, { useState } from 'react';
interface PuntoFPP {
x: number;
y: number;
}
export const CrecimientoEconomicoFPP: React.FC = () => {
const [tipoCambio, setTipoCambio] = useState<string>('');
const [factorSeleccionado, setFactorSeleccionado] = useState<string>('');
const [respuestasCorrectas, setRespuestasCorrectas] = useState<boolean[]>([false, false]);
const [mostrarResultado, setMostrarResultado] = useState(false);
const puntosFPPOriginal: PuntoFPP[] = [
{ x: 0, y: 100 },
{ x: 50, y: 90 },
{ x: 100, y: 70 },
{ x: 150, y: 40 },
{ x: 200, y: 0 },
];
const puntosFPPDesplazada: PuntoFPP[] = [
{ x: 0, y: 120 },
{ x: 60, y: 108 },
{ x: 120, y: 84 },
{ x: 180, y: 48 },
{ x: 240, y: 0 },
];
const opcionesCambio = [
{ valor: 'crecimiento', label: 'Crecimiento económico (desplazamiento hacia afuera)' },
{ valor: 'recesion', label: 'Recesión económica (desplazamiento hacia adentro)' },
{ valor: 'mejoraA', label: 'Mejora tecnológica solo en Bien A' },
{ valor: 'mejoraB', label: 'Mejora tecnológica solo en Bien B' },
];
const opcionesFactores = [
{ valor: 'tecnologia', label: 'Avance tecnológico', tipo: 'crecimiento' },
{ valor: 'capital', label: 'Aumento del capital físico', tipo: 'crecimiento' },
{ valor: 'trabajo', label: 'Aumento de la fuerza laboral', tipo: 'crecimiento' },
{ valor: 'recursos', label: 'Descubrimiento de nuevos recursos', tipo: 'crecimiento' },
{ valor: 'guerra', label: 'Conflicto bélico', tipo: 'recesion' },
{ valor: 'desastre', label: 'Desastre natural', tipo: 'recesion' },
{ valor: 'emigracion', label: 'Emigración masiva', tipo: 'recesion' },
{ valor: 'destruccion', label: 'Destrucción de capital', tipo: 'recesion' },
];
const verificarRespuestas = () => {
const esCrecimiento = tipoCambio === 'crecimiento';
const factorEsCrecimiento = ['tecnologia', 'capital', 'trabajo', 'recursos'].includes(factorSeleccionado);
setRespuestasCorrectas([esCrecimiento, factorEsCrecimiento]);
setMostrarResultado(true);
};
const reiniciarEjercicio = () => {
setTipoCambio('');
setFactorSeleccionado('');
setRespuestasCorrectas([false, false]);
setMostrarResultado(false);
};
const SVG_HEIGHT = 300;
const SVG_WIDTH = 400;
const PADDING = 40;
const escalarX = (x: number) => PADDING + (x / 250) * (SVG_WIDTH - 2 * PADDING);
const escalarY = (y: number) => SVG_HEIGHT - PADDING - (y / 130) * (SVG_HEIGHT - 2 * PADDING);
const crearPath = (puntos: PuntoFPP[]) => {
return puntos.map((p, i) =>
`${i === 0 ? 'M' : 'L'} ${escalarX(p.x)} ${escalarY(p.y)}`
).join(' ');
};
return (
<div className="max-w-4xl mx-auto p-6">
<h2 className="text-2xl font-bold mb-4">Crecimiento Económico y Curva FPP</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="font-semibold mb-4">Gráfico de la Frontera de Posibilidades de Producción</h3>
<svg viewBox={`0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`} className="w-full border rounded">
{/* Ejes */}
<line x1={PADDING} y1={SVG_HEIGHT - PADDING} x2={SVG_WIDTH - PADDING} y2={SVG_HEIGHT - PADDING} stroke="black" strokeWidth="2" />
<line x1={PADDING} y1={SVG_HEIGHT - PADDING} x2={PADDING} y2={PADDING} stroke="black" strokeWidth="2" />
{/* Etiquetas de ejes */}
<text x={SVG_WIDTH / 2} y={SVG_HEIGHT - 10} textAnchor="middle" fontSize="12">Bien A</text>
<text x={15} y={SVG_HEIGHT / 2} textAnchor="middle" fontSize="12" transform={`rotate(-90, 15, ${SVG_HEIGHT / 2})`}>Bien B</text>
{/* FPP Original */}
<path d={crearPath(puntosFPPOriginal)} fill="none" stroke="#3b82f6" strokeWidth="3" strokeDasharray="5,5" />
{/* FPP Desplazada */}
<path d={crearPath(puntosFPPDesplazada)} fill="none" stroke="#22c55e" strokeWidth="3" />
{/* Leyenda */}
<rect x={PADDING + 10} y={PADDING} width="15" height="15" fill="none" stroke="#3b82f6" strokeDasharray="5,5" strokeWidth="2" />
<text x={PADDING + 35} y={PADDING + 12} fontSize="12">FPP Original</text>
<rect x={PADDING + 10} y={PADDING + 25} width="15" height="15" fill="none" stroke="#22c55e" strokeWidth="2" />
<text x={PADDING + 35} y={PADDING + 37} fontSize="12">FPP Nueva</text>
</svg>
</div>
<div className="space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-semibold mb-3">Pregunta 1: ¿Qué tipo de cambio observas en el gráfico?</h3>
<div className="space-y-2">
{opcionesCambio.map((opcion) => (
<label key={opcion.valor} className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="tipoCambio"
value={opcion.valor}
checked={tipoCambio === opcion.valor}
onChange={(e) => setTipoCambio(e.target.value)}
className="w-4 h-4"
/>
<span className="text-sm">{opcion.label}</span>
</label>
))}
</div>
</div>
<div className="bg-green-50 p-4 rounded-lg">
<h3 className="font-semibold mb-3">Pregunta 2: ¿Qué factor podría causar este cambio?</h3>
<select
value={factorSeleccionado}
onChange={(e) => setFactorSeleccionado(e.target.value)}
className="w-full p-2 border rounded"
>
<option value="">Selecciona un factor...</option>
<optgroup label="Factores de Crecimiento">
{opcionesFactores.filter(f => f.tipo === 'crecimiento').map(f => (
<option key={f.valor} value={f.valor}>{f.label}</option>
))}
</optgroup>
<optgroup label="Factores de Recesión">
{opcionesFactores.filter(f => f.tipo === 'recesion').map(f => (
<option key={f.valor} value={f.valor}>{f.label}</option>
))}
</optgroup>
</select>
</div>
</div>
</div>
<div className="flex gap-4 mb-6">
<button
onClick={verificarRespuestas}
disabled={!tipoCambio || !factorSeleccionado}
className="bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600 disabled:bg-gray-300"
>
Verificar Respuestas
</button>
<button
onClick={reiniciarEjercicio}
className="bg-gray-500 text-white px-6 py-2 rounded hover:bg-gray-600"
>
Reiniciar
</button>
</div>
{mostrarResultado && (
<div className="space-y-4">
<div className={`p-4 rounded-lg ${respuestasCorrectas[0] ? 'bg-green-100' : 'bg-red-100'}`}>
<p className="font-medium">
Pregunta 1: {respuestasCorrectas[0] ? '¡Correcto!' : 'Incorrecto.'}
</p>
<p className="text-sm mt-1">
{respuestasCorrectas[0]
? 'El gráfico muestra un crecimiento económico, representado por el desplazamiento hacia afuera de la curva FPP.'
: 'El gráfico muestra crecimiento económico (desplazamiento hacia afuera de la FPP).'}
</p>
</div>
<div className={`p-4 rounded-lg ${respuestasCorrectas[1] ? 'bg-green-100' : 'bg-red-100'}`}>
<p className="font-medium">
Pregunta 2: {respuestasCorrectas[1] ? '¡Correcto!' : 'Incorrecto.'}
</p>
<p className="text-sm mt-1">
{respuestasCorrectas[1]
? 'Excelente selección. Este factor contribuye al crecimiento económico.'
: 'Revisa tu selección. Considera qué factores aumentan la capacidad productiva de la economía.'}
</p>
</div>
</div>
)}
<div className="mt-6 bg-yellow-50 p-4 rounded-lg">
<h4 className="font-semibold mb-2">Conceptos clave:</h4>
<ul className="list-disc list-inside text-sm space-y-1">
<li><strong>Crecimiento económico:</strong> Desplazamiento de la FPP hacia afuera, permite producir más de ambos bienes</li>
<li><strong>Recesión:</strong> Desplazamiento de la FPP hacia adentro, reduce la capacidad productiva</li>
<li><strong>Factores del crecimiento:</strong> Tecnología, capital, trabajo, recursos naturales</li>
</ul>
</div>
</div>
);
};
export default CrecimientoEconomicoFPP;

View File

@@ -0,0 +1,197 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle } from 'lucide-react';
interface EjercicioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
const PREGUNTAS = [
{
id: 1,
pregunta: "¿Qué es la economía?",
opciones: [
"Ciencia social que estudia cómo se asignan recursos escasos para satisfacer necesidades ilimitadas",
"Estudio exclusivo del dinero y los bancos",
"Análisis únicamente de empresas grandes",
"Gestión de presupuestos familiares"
],
correcta: 0,
explicacion: "La economía es una ciencia social que estudia la asignación de recursos escasos para satisfacer necesidades ilimitadas."
},
{
id: 2,
pregunta: "¿Cuál es la diferencia entre microeconomía y macroeconomía?",
opciones: [
"La micro estudia individuos y empresas; la macro estudia la economía como un todo",
"La micro es más difícil que la macro",
"La micro estudia solo bancos; la macro estudia gobiernos",
"No hay diferencia, son lo mismo"
],
correcta: 0,
explicacion: "La microeconomía estudia el comportamiento de individuos y empresas, mientras que la macroeconomía analiza la economía en su conjunto (PIB, inflación, desempleo)."
},
{
id: 3,
pregunta: "¿Qué es el problema económico fundamental?",
opciones: [
"La escasez de recursos frente a necesidades ilimitadas",
"La falta de dinero en los bancos",
"El desempleo elevado",
"La inflación alta"
],
correcta: 0,
explicacion: "El problema económico fundamental es la escasez: los recursos son limitados pero las necesidades humanas son ilimitadas."
},
{
id: 4,
pregunta: "¿Qué estudia la economía positiva?",
opciones: [
"Lo que es (hechos y descripciones)",
"Lo que debería ser (valores y juicios)",
"Solo matemáticas económicas",
"Únicamente historia económica"
],
correcta: 0,
explicacion: "La economía positiva describe y explica hechos objetivos ('lo que es'), sin hacer juicios de valor."
},
{
id: 5,
pregunta: "Complete: La economía normativa se refiere a...",
opciones: [
"Juicios de valor sobre lo que debería ser",
"Datos estadísticos objetivos",
"Teorías matemáticas puras",
"Hechos históricos verificables"
],
correcta: 0,
explicacion: "La economía normativa hace juicios de valor y prescripciones sobre lo que debería ser ('deberíamos aumentar los impuestos')."
}
];
export function DefinicionEconomiaQuiz({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) {
const [preguntaActual, setPreguntaActual] = useState(0);
const [respuestas, setRespuestas] = useState<number[]>([]);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [completado, setCompletado] = useState(false);
const pregunta = PREGUNTAS[preguntaActual];
const esUltima = preguntaActual === PREGUNTAS.length - 1;
const handleRespuesta = (index: number) => {
const nuevasRespuestas = [...respuestas, index];
setRespuestas(nuevasRespuestas);
if (esUltima) {
// Calcular puntuación
const correctas = nuevasRespuestas.filter((r, i) => r === PREGUNTAS[i].correcta).length;
const puntuacion = Math.round((correctas / PREGUNTAS.length) * 100);
setCompletado(true);
onComplete?.(puntuacion);
} else {
setMostrarResultado(true);
}
};
const handleSiguiente = () => {
setPreguntaActual(prev => prev + 1);
setMostrarResultado(false);
};
const esCorrecta = respuestas[preguntaActual] === pregunta.correcta;
if (completado) {
const correctas = respuestas.filter((r, i) => r === PREGUNTAS[i].correcta).length;
const puntuacion = Math.round((correctas / PREGUNTAS.length) * 100);
return (
<Card className="w-full max-w-2xl mx-auto text-center p-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="mb-6"
>
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto">
<CheckCircle className="w-10 h-10 text-green-600" />
</div>
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">¡Quiz Completado!</h3>
<p className="text-gray-600 mb-6">
Respondiste {correctas} de {PREGUNTAS.length} preguntas correctamente
</p>
<div className="text-5xl font-bold text-blue-600 mb-2">{puntuacion}</div>
<p className="text-gray-500">puntos</p>
</Card>
);
}
return (
<Card className="w-full max-w-2xl mx-auto">
<div className="p-6">
{/* Progress */}
<div className="mb-6">
<div className="flex justify-between text-sm text-gray-500 mb-2">
<span>Pregunta {preguntaActual + 1} de {PREGUNTAS.length}</span>
<span>{Math.round(((preguntaActual) / PREGUNTAS.length) * 100)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<motion.div
className="bg-blue-500 h-2 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${((preguntaActual) / PREGUNTAS.length) * 100}%` }}
/>
</div>
</div>
{/* Pregunta */}
<h3 className="text-xl font-bold text-gray-900 mb-6">{pregunta.pregunta}</h3>
{/* Opciones */}
{!mostrarResultado ? (
<div className="space-y-3">
{pregunta.opciones.map((opcion, index) => (
<motion.button
key={index}
onClick={() => handleRespuesta(index)}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className="w-full p-4 text-left border-2 border-gray-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all"
>
<span className="font-medium text-gray-700">{String.fromCharCode(65 + index)}. {opcion}</span>
</motion.button>
))}
</div>
) : (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`p-6 rounded-xl mb-6 ${esCorrecta ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}
>
<div className="flex items-center gap-3 mb-3">
{esCorrecta ? (
<CheckCircle className="w-6 h-6 text-green-600" />
) : (
<XCircle className="w-6 h-6 text-red-600" />
)}
<span className={`font-bold ${esCorrecta ? 'text-green-800' : 'text-red-800'}`}>
{esCorrecta ? '¡Correcto!' : 'Incorrecto'}
</span>
</div>
<p className="text-gray-700">{pregunta.explicacion}</p>
<Button onClick={handleSiguiente} className="mt-4">
{esUltima ? 'Finalizar' : 'Siguiente'}
</Button>
</motion.div>
)}
</div>
</Card>
);
}
export default DefinicionEconomiaQuiz;

View File

@@ -0,0 +1,275 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, RefreshCw, ArrowRight } from 'lucide-react';
interface EjercicioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Enunciado {
id: number;
texto: string;
tipo: 'positiva' | 'normativa';
explicacion: string;
}
const ENUNCIADOS: Enunciado[] = [
{
id: 1,
texto: "La inflación en el país alcanzó el 5% el año pasado.",
tipo: 'positiva',
explicacion: "Este es un enunciado positivo porque describe un hecho objetivo y verificable."
},
{
id: 2,
texto: "El gobierno debería reducir los impuestos para estimular la economía.",
tipo: 'normativa',
explicacion: "Este es un enunciado normativo porque expresa una opinión sobre lo que debería hacerse."
},
{
id: 3,
texto: "La tasa de desempleo juvenil es del 15%.",
tipo: 'positiva',
explicacion: "Es positivo porque presenta un dato estadístico verificable."
},
{
id: 4,
texto: "Es injusto que existan grandes diferencias de ingreso entre ricos y pobres.",
tipo: 'normativa',
explicacion: "Es normativo porque contiene un juicio de valor sobre la justicia."
},
{
id: 5,
texto: "El PIB del país creció un 3% durante el último trimestre.",
tipo: 'positiva',
explicacion: "Es positivo porque es una afirmación factual basada en datos."
},
{
id: 6,
texto: "Se debería aumentar el salario mínimo para mejorar la calidad de vida.",
tipo: 'normativa',
explicacion: "Es normativo porque prescribe una acción basada en valores."
},
{
id: 7,
texto: "El costo de vida en la capital es 20% más alto que en el interior.",
tipo: 'positiva',
explicacion: "Es positivo porque compara datos observables y mensurables."
},
{
id: 8,
texto: "Las empresas multinacionales tienen la obligación ética de pagar impuestos justos.",
tipo: 'normativa',
explicacion: "Es normativo porque habla de obligaciones éticas y valores."
}
];
export function EconomiaPositivaVsNormativa({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) {
const [enunciadosRestantes, setEnunciadosRestantes] = useState<Enunciado[]>([...ENUNCIADOS]);
const [clasificaciones, setClasificaciones] = useState<{id: number, correcta: boolean}[]>([]);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [completado, setCompletado] = useState(false);
const [ultimaRespuesta, setUltimaRespuesta] = useState<'positiva' | 'normativa' | null>(null);
const enunciadoActual = enunciadosRestantes[0];
const progreso = ((ENUNCIADOS.length - enunciadosRestantes.length) / ENUNCIADOS.length) * 100;
const handleClasificacion = (tipo: 'positiva' | 'normativa') => {
if (!enunciadoActual) return;
const esCorrecta = tipo === enunciadoActual.tipo;
setUltimaRespuesta(tipo);
setClasificaciones(prev => [...prev, { id: enunciadoActual.id, correcta: esCorrecta }]);
setMostrarResultado(true);
if (enunciadosRestantes.length === 1) {
const nuevasClasificaciones = [...clasificaciones, { id: enunciadoActual.id, correcta: esCorrecta }];
const correctas = nuevasClasificaciones.filter(c => c.correcta).length;
const puntuacion = Math.round((correctas / ENUNCIADOS.length) * 100);
setCompletado(true);
onComplete?.(puntuacion);
}
};
const handleSiguiente = () => {
setEnunciadosRestantes(prev => prev.slice(1));
setMostrarResultado(false);
setUltimaRespuesta(null);
};
const handleReiniciar = () => {
setEnunciadosRestantes([...ENUNCIADOS]);
setClasificaciones([]);
setMostrarResultado(false);
setCompletado(false);
setUltimaRespuesta(null);
};
if (completado) {
const correctas = clasificaciones.filter(c => c.correcta).length;
const puntuacion = Math.round((correctas / ENUNCIADOS.length) * 100);
return (
<Card className="w-full max-w-2xl mx-auto text-center p-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="mb-6"
>
<div className={`w-20 h-20 rounded-full flex items-center justify-center mx-auto ${puntuacion >= 70 ? 'bg-green-100' : 'bg-yellow-100'}`}>
{puntuacion >= 70 ? (
<CheckCircle className="w-10 h-10 text-green-600" />
) : (
<RefreshCw className="w-10 h-10 text-yellow-600" />
)}
</div>
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">
{puntuacion >= 70 ? '¡Excelente trabajo!' : '¡Sigue practicando!'}
</h3>
<p className="text-gray-600 mb-6">
Clasificaste correctamente {correctas} de {ENUNCIADOS.length} enunciados
</p>
<div className="text-5xl font-bold text-blue-600 mb-2">{puntuacion}</div>
<p className="text-gray-500 mb-6">puntos</p>
{puntuacion < 70 && (
<Button onClick={handleReiniciar} variant="outline" className="flex items-center gap-2 mx-auto">
<RefreshCw className="w-4 h-4" />
Intentar de nuevo
</Button>
)}
</Card>
);
}
if (!enunciadoActual) return null;
const esCorrecta = ultimaRespuesta === enunciadoActual.tipo;
return (
<Card className="w-full max-w-2xl mx-auto">
<div className="p-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-bold text-gray-900">Clasifica el enunciado</h3>
<p className="text-sm text-gray-500">¿Es una afirmación positiva o normativa?</p>
</div>
<span className="text-sm text-gray-500">
{ENUNCIADOS.length - enunciadosRestantes.length + 1} / {ENUNCIADOS.length}
</span>
</div>
{/* Progress */}
<div className="mb-6">
<div className="w-full bg-gray-200 rounded-full h-2">
<motion.div
className="bg-blue-500 h-2 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${progreso}%` }}
transition={{ duration: 0.3 }}
/>
</div>
</div>
{/* Enunciado */}
<AnimatePresence mode="wait">
{!mostrarResultado ? (
<motion.div
key="pregunta"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
>
<div className="bg-gray-50 rounded-xl p-6 mb-6">
<p className="text-lg text-gray-800 italic">"{enunciadoActual.texto}"</p>
</div>
<div className="grid grid-cols-2 gap-4">
<motion.button
onClick={() => handleClasificacion('positiva')}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="p-4 border-2 border-blue-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all text-center"
>
<div className="font-semibold text-blue-800 mb-1">Economía Positiva</div>
<p className="text-xs text-gray-600">Describe hechos objetivos</p>
</motion.button>
<motion.button
onClick={() => handleClasificacion('normativa')}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="p-4 border-2 border-purple-200 rounded-xl hover:border-purple-400 hover:bg-purple-50 transition-all text-center"
>
<div className="font-semibold text-purple-800 mb-1">Economía Normativa</div>
<p className="text-xs text-gray-600">Expresa juicios de valor</p>
</motion.button>
</div>
</motion.div>
) : (
<motion.div
key="resultado"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`p-6 rounded-xl ${esCorrecta ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}
>
<div className="flex items-center gap-3 mb-3">
{esCorrecta ? (
<CheckCircle className="w-6 h-6 text-green-600" />
) : (
<XCircle className="w-6 h-6 text-red-600" />
)}
<span className={`font-bold ${esCorrecta ? 'text-green-800' : 'text-red-800'}`}>
{esCorrecta ? '¡Correcto!' : 'Incorrecto'}
</span>
</div>
<div className="mb-4">
<p className="text-sm text-gray-600 mb-1">Respuesta correcta:</p>
<p className={`font-medium ${enunciadoActual.tipo === 'positiva' ? 'text-blue-700' : 'text-purple-700'}`}>
Economía {enunciadoActual.tipo === 'positiva' ? 'Positiva' : 'Normativa'}
</p>
</div>
<p className="text-gray-700 mb-4">{enunciadoActual.explicacion}</p>
<Button onClick={handleSiguiente} className="flex items-center gap-2">
{enunciadosRestantes.length === 1 ? 'Finalizar' : 'Siguiente'}
<ArrowRight className="w-4 h-4" />
</Button>
</motion.div>
)}
</AnimatePresence>
{/* Legend */}
<div className="mt-6 pt-6 border-t border-gray-200">
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex items-start gap-2">
<div className="w-3 h-3 rounded-full bg-blue-400 mt-1 flex-shrink-0"></div>
<div>
<p className="font-medium text-gray-800">Positiva</p>
<p className="text-gray-500 text-xs">Lo que es (hechos)</p>
</div>
</div>
<div className="flex items-start gap-2">
<div className="w-3 h-3 rounded-full bg-purple-400 mt-1 flex-shrink-0"></div>
<div>
<p className="font-medium text-gray-800">Normativa</p>
<p className="text-gray-500 text-xs">Lo que debería ser (valores)</p>
</div>
</div>
</div>
</div>
</div>
</Card>
);
}
export default EconomiaPositivaVsNormativa;

View File

@@ -0,0 +1,189 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, AlertCircle } from 'lucide-react';
interface EjercicioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
const NECESIDADES = [
{ id: 'alimentacion', nombre: 'Alimentación', icono: '🍽️' },
{ id: 'vivienda', nombre: 'Vivienda', icono: '🏠' },
{ id: 'educacion', nombre: 'Educación', icono: '📚' },
{ id: 'salud', nombre: 'Salud', icono: '🏥' }
];
export function EscasezSimulator({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) {
const [asignaciones, setAsignaciones] = useState<Record<string, number>>({
alimentacion: 25,
vivienda: 25,
educacion: 25,
salud: 25
});
const [validado, setValidado] = useState(false);
const [completado, setCompletado] = useState(false);
const total = Object.values(asignaciones).reduce((sum, val) => sum + val, 0);
const restante = 100 - total;
const excedido = total > 100;
const handleSliderChange = (id: string, value: number) => {
setAsignaciones(prev => ({ ...prev, [id]: value }));
setValidado(false);
};
const handleValidar = () => {
if (excedido) return;
setValidado(true);
// Calcular puntuación basada en equilibrio
// Ideal: todas las necesidades tienen al menos 15 puntos y no se excede
const valores = Object.values(asignaciones);
const todasConMinimo = valores.every(v => v >= 15);
const sumaExacta = total === 100;
let puntuacion = 0;
if (sumaExacta) {
puntuacion = 60; // Base por usar exactamente 100
if (todasConMinimo) puntuacion += 40; // Bonus por equilibrio
} else if (total <= 100) {
puntuacion = Math.round((total / 100) * 50); // Proporcional si no usa todo
}
setTimeout(() => {
setCompletado(true);
onComplete?.(puntuacion);
}, 1500);
};
if (completado) {
return (
<Card className="w-full max-w-2xl mx-auto text-center p-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="mb-6"
>
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto">
<CheckCircle className="w-10 h-10 text-green-600" />
</div>
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">¡Simulación Completada!</h3>
<p className="text-gray-600 mb-6">
Has distribuido los recursos disponibles.
</p>
<div className="bg-gray-50 rounded-xl p-4 mb-6">
<p className="text-sm text-gray-600 mb-2">Distribución final:</p>
<div className="grid grid-cols-2 gap-2 text-sm">
{NECESIDADES.map(nec => (
<div key={nec.id} className="flex justify-between">
<span>{nec.icono} {nec.nombre}:</span>
<span className="font-bold">{asignaciones[nec.id]} pts</span>
</div>
))}
</div>
</div>
</Card>
);
}
return (
<Card className="w-full max-w-2xl mx-auto">
<div className="p-6">
<div className="text-center mb-6">
<h3 className="text-xl font-bold text-gray-900 mb-2">Simulador de Escasez</h3>
<p className="text-gray-600">
Tienes <span className="font-bold text-blue-600">100 puntos</span> para distribuir entre 4 necesidades básicas.
</p>
</div>
{/* Indicador de recursos */}
<div className="mb-6">
<div className="flex justify-between text-sm mb-2">
<span className={excedido ? 'text-red-600 font-bold' : 'text-gray-600'}>
{excedido ? '¡Excedido!' : `Restante: ${restante} pts`}
</span>
<span className={`font-bold ${excedido ? 'text-red-600' : total === 100 ? 'text-green-600' : 'text-blue-600'}`}>
{total} / 100
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<motion.div
className={`h-3 rounded-full transition-all ${
excedido ? 'bg-red-500' : total === 100 ? 'bg-green-500' : 'bg-blue-500'
}`}
animate={{ width: `${Math.min(total, 100)}%` }}
/>
</div>
{excedido && (
<p className="text-red-600 text-sm mt-2 flex items-center gap-1">
<AlertCircle className="w-4 h-4" />
Has excedido los 100 puntos disponibles. Reduce alguna asignación.
</p>
)}
</div>
{/* Sliders */}
<div className="space-y-6 mb-6">
{NECESIDADES.map(necesidad => (
<div key={necesidad.id} className="bg-gray-50 p-4 rounded-xl">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="text-2xl">{necesidad.icono}</span>
<span className="font-medium text-gray-700">{necesidad.nombre}</span>
</div>
<span className="text-lg font-bold text-blue-600">
{asignaciones[necesidad.id]} pts
</span>
</div>
<input
type="range"
min="0"
max="50"
value={asignaciones[necesidad.id]}
onChange={(e) => handleSliderChange(necesidad.id, parseInt(e.target.value))}
className="w-full h-2 bg-gray-300 rounded-lg appearance-none cursor-pointer accent-blue-500"
/>
<div className="flex justify-between text-xs text-gray-400 mt-1">
<span>0</span>
<span>50</span>
</div>
</div>
))}
</div>
{/* Botón validar */}
<Button
onClick={handleValidar}
disabled={excedido}
className="w-full"
variant={excedido ? 'outline' : 'primary'}
>
{excedido ? 'Ajusta las asignaciones' : 'Validar Distribución'}
</Button>
{validado && !excedido && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-xl text-center"
>
<p className="text-blue-800">
{total === 100
? '¡Excelente! Has utilizado todos los recursos disponibles.'
: `Has utilizado ${total} de 100 puntos. ¿Quieres ajustar o continuar?`}
</p>
</motion.div>
)}
</div>
</Card>
);
}
export default EscasezSimulator;

View File

@@ -0,0 +1,478 @@
import { useState, useCallback, DragEvent } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import {
CheckCircle,
XCircle,
RefreshCcw,
Link2,
GripVertical,
Trophy,
Scale,
Target,
Zap,
BookOpen
} from 'lucide-react';
interface MatchingItem {
id: string;
content: string;
}
interface Match {
leftId: string;
rightId: string;
isCorrect?: boolean;
checked?: boolean;
}
interface FPPAnalizadorProps {
onComplete?: (score: number, total: number) => void;
}
const PUNTOS_INICIALES = [
{ x: 15, y: 85, tipo: 'ineficiente', label: 'A' },
{ x: 45, y: 55, tipo: 'eficiente', label: 'B' },
{ x: 75, y: 25, tipo: 'inalcanzable', label: 'C' },
{ x: 30, y: 70, tipo: 'ineficiente', label: 'D' },
{ x: 60, y: 40, tipo: 'eficiente', label: 'E' },
{ x: 90, y: 10, tipo: 'inalcanzable', label: 'F' },
];
const TIPOS_OPCIONES = [
{ id: 'eficiente', label: 'Eficiente', color: 'green', icon: CheckCircle, descripcion: 'En la FPP - máxima producción' },
{ id: 'ineficiente', label: 'Ineficiente', color: 'orange', icon: Zap, descripcion: 'Dentro de la FPP - recursos subutilizados' },
{ id: 'inalcanzable', label: 'Inalcanzable', color: 'red', icon: XCircle, descripcion: 'Fuera de la FPP - no hay recursos suficientes' },
];
export function FPPAnalizador({ onComplete }: FPPAnalizadorProps) {
const [asignaciones, setAsignaciones] = useState<Record<string, string | null>>(() =>
PUNTOS_INICIALES.reduce((acc, punto) => ({ ...acc, [punto.label]: null }), {})
);
const [mostrarResultados, setMostrarResultados] = useState(false);
const [draggedTipo, setDraggedTipo] = useState<string | null>(null);
const handleDragStart = (e: DragEvent<HTMLDivElement>, tipoId: string) => {
e.dataTransfer.setData('text/plain', tipoId);
setDraggedTipo(tipoId);
};
const handleDragEnd = () => {
setDraggedTipo(null);
};
const handleDrop = (e: DragEvent<SVGGElement>, puntoLabel: string) => {
e.preventDefault();
const tipoId = e.dataTransfer.getData('text/plain');
if (tipoId) {
setAsignaciones(prev => ({ ...prev, [puntoLabel]: tipoId }));
}
setDraggedTipo(null);
};
const handleDragOver = (e: DragEvent<SVGGElement>) => {
e.preventDefault();
};
const handleAsignar = (puntoLabel: string, tipoId: string) => {
setAsignaciones(prev => ({ ...prev, [puntoLabel]: tipoId }));
};
const handleVerificar = () => {
setMostrarResultados(true);
const correctas = PUNTOS_INICIALES.filter(
punto => asignaciones[punto.label] === punto.tipo
).length;
if (onComplete) {
onComplete(correctas, PUNTOS_INICIALES.length);
}
};
const handleReiniciar = () => {
setAsignaciones(PUNTOS_INICIALES.reduce((acc, punto) => ({ ...acc, [punto.label]: null }), {}));
setMostrarResultados(false);
};
const todasAsignadas = Object.values(asignaciones).every(a => a !== null);
const correctas = PUNTOS_INICIALES.filter(
punto => asignaciones[punto.label] === punto.tipo
).length;
// Generar curva FPP
const generateFPPPath = () => {
const puntos = [
{ x: 10, y: 90 },
{ x: 20, y: 85 },
{ x: 30, y: 78 },
{ x: 40, y: 70 },
{ x: 50, y: 60 },
{ x: 60, y: 50 },
{ x: 70, y: 40 },
{ x: 80, y: 30 },
{ x: 90, y: 20 },
];
let path = `M ${puntos[0].x} ${puntos[0].y}`;
for (let i = 1; i < puntos.length; i++) {
const cp1x = puntos[i - 1].x + (puntos[i].x - puntos[i - 1].x) * 0.5;
const cp1y = puntos[i - 1].y;
const cp2x = puntos[i - 1].x + (puntos[i].x - puntos[i - 1].x) * 0.5;
const cp2y = puntos[i].y;
path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${puntos[i].x} ${puntos[i].y}`;
}
return path;
};
const getEstiloPunto = (punto: typeof PUNTOS_INICIALES[0]) => {
const asignacion = asignaciones[punto.label];
if (!mostrarResultados) {
return {
fill: asignacion ? {
eficiente: '#22c55e',
ineficiente: '#f97316',
inalcanzable: '#ef4444',
}[asignacion] : '#3b82f6',
stroke: '#1e40af',
strokeWidth: 2,
};
}
const esCorrecto = asignacion === punto.tipo;
return {
fill: esCorrecto ? '#22c55e' : '#ef4444',
stroke: esCorrecto ? '#166534' : '#991b1b',
strokeWidth: 3,
};
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-start justify-between">
<div>
<h3 className="text-xl font-bold text-gray-900">Analizador de la FPP</h3>
<p className="text-gray-600 mt-1">
Identifica si cada punto es Eficiente, Ineficiente o Inalcanzable
</p>
</div>
<div className="flex items-center gap-2 text-amber-600 bg-amber-50 px-3 py-1.5 rounded-full">
<Target size={16} />
<span className="font-semibold text-sm">100 pts</span>
</div>
</div>
{/* Leyenda */}
<Card className="p-4">
<div className="flex items-center gap-2 mb-3">
<BookOpen size={18} className="text-blue-600" />
<h4 className="font-semibold text-gray-800">Arrastra el tipo a cada punto:</h4>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{TIPOS_OPCIONES.map((tipo) => {
const Icon = tipo.icon;
return (
<motion.div
key={tipo.id}
draggable={!mostrarResultados}
onDragStart={(e) => handleDragStart(e as unknown as DragEvent<HTMLDivElement>, tipo.id)}
onDragEnd={handleDragEnd}
whileHover={!mostrarResultados ? { scale: 1.02 } : {}}
whileTap={!mostrarResultados ? { scale: 0.98 } : {}}
className={`p-3 rounded-lg border-2 cursor-grab active:cursor-grabbing ${
tipo.color === 'green' ? 'border-green-200 bg-green-50' :
tipo.color === 'orange' ? 'border-orange-200 bg-orange-50' :
'border-red-200 bg-red-50'
} ${draggedTipo === tipo.id ? 'opacity-50' : ''}`}
>
<div className="flex items-center gap-2">
<Icon size={18} className={
tipo.color === 'green' ? 'text-green-600' :
tipo.color === 'orange' ? 'text-orange-600' :
'text-red-600'
} />
<div>
<span className={`font-semibold text-sm ${
tipo.color === 'green' ? 'text-green-800' :
tipo.color === 'orange' ? 'text-orange-800' :
'text-red-800'
}`}>
{tipo.label}
</span>
<p className="text-xs text-gray-500 mt-0.5">{tipo.descripcion}</p>
</div>
</div>
</motion.div>
);
})}
</div>
</Card>
{/* SVG con FPP */}
<Card className="p-4 overflow-hidden">
<div className="relative">
<svg viewBox="0 0 100 100" className="w-full h-auto aspect-square max-h-[500px]">
{/* Fondo con gradientes */}
<defs>
<linearGradient id="areaFactible" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#dcfce7" stopOpacity="0.8" />
<stop offset="100%" stopColor="#dcfce7" stopOpacity="0.4" />
</linearGradient>
<linearGradient id="areaInalcanzable" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#fee2e2" stopOpacity="0.3" />
<stop offset="100%" stopColor="#fee2e2" stopOpacity="0.1" />
</linearGradient>
</defs>
{/* Áreas */}
<path
d={`${generateFPPPath()} L 90 90 L 10 90 Z`}
fill="url(#areaFactible)"
stroke="none"
/>
<path
d={`M 10 10 L 90 10 L 90 20 ${generateFPPPath().split('L').slice(1).join('L')} Z`}
fill="url(#areaInalcanzable)"
stroke="none"
/>
{/* Ejes */}
<line x1="10" y1="90" x2="95" y2="90" stroke="#374151" strokeWidth="0.5" strokeLinecap="round" />
<line x1="10" y1="90" x2="10" y2="5" stroke="#374151" strokeWidth="0.5" strokeLinecap="round" />
{/* Flechas de ejes */}
<polygon points="95,90 92,88.5 92,91.5" fill="#374151" />
<polygon points="10,5 8.5,8 11.5,8" fill="#374151" />
{/* Etiquetas de ejes */}
<text x="50" y="97" textAnchor="middle" className="text-[3px] fill-gray-700 font-medium">
Bien de Consumo (Y)
</text>
<text x="3" y="50" textAnchor="middle" transform="rotate(-90, 3, 50)" className="text-[3px] fill-gray-700 font-medium">
Bien de Capital (X)
</text>
{/* Curva FPP */}
<path
d={generateFPPPath()}
fill="none"
stroke="#2563eb"
strokeWidth="1.5"
strokeLinecap="round"
/>
{/* Puntos interactivos */}
{PUNTOS_INICIALES.map((punto) => {
const estilo = getEstiloPunto(punto);
const asignacion = asignaciones[punto.label];
return (
<g
key={punto.label}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e as unknown as DragEvent<SVGGElement>, punto.label)}
className="cursor-pointer"
>
{/* Círculo del punto */}
<motion.circle
cx={punto.x}
cy={punto.y}
r="3"
fill={estilo.fill}
stroke={estilo.stroke}
strokeWidth={estilo.strokeWidth}
whileHover={{ r: 4 }}
transition={{ duration: 0.2 }}
/>
{/* Label del punto */}
<text
x={punto.x}
y={punto.y - 5}
textAnchor="middle"
className="text-[3.5px] fill-gray-800 font-bold"
>
{punto.label}
</text>
{/* Indicador de asignación */}
{asignacion && !mostrarResultados && (
<text
x={punto.x}
y={punto.y + 6}
textAnchor="middle"
className={`text-[2.5px] font-medium ${
asignacion === 'eficiente' ? 'fill-green-600' :
asignacion === 'ineficiente' ? 'fill-orange-600' :
'fill-red-600'
}`}
>
{TIPOS_OPCIONES.find(t => t.id === asignacion)?.label}
</text>
)}
{/* Checkmark o X si hay resultado */}
{mostrarResultados && (
<g transform={`translate(${punto.x - 4}, ${punto.y - 4})`}>
{asignacion === punto.tipo ? (
<circle cx="0" cy="0" r="2" fill="#22c55e" />
) : (
<circle cx="0" cy="0" r="2" fill="#ef4444" />
)}
</g>
)}
</g>
);
})}
{/* Leyenda en el SVG */}
<g transform="translate(75, 15)">
<rect x="-8" y="-3" width="22" height="14" rx="1" fill="white" stroke="#e5e7eb" strokeWidth="0.3" />
<text x="0" y="0" className="text-[2px] fill-gray-700 font-semibold">Leyenda:</text>
<line x1="0" y1="3" x2="6" y2="3" stroke="#2563eb" strokeWidth="0.5" />
<text x="8" y="4" className="text-[1.8px] fill-gray-600">FPP</text>
<rect x="0" y="6" width="4" height="3" fill="#dcfce7" stroke="none" />
<text x="6" y="8.5" className="text-[1.8px] fill-gray-600">Factible</text>
<rect x="0" y="10" width="4" height="3" fill="#fee2e2" stroke="none" />
<text x="6" y="12.5" className="text-[1.8px] fill-gray-600">Inalcanzable</text>
</g>
</svg>
</div>
{/* Botones de asignación alternativos (para móvil) */}
<div className="mt-4 grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-2">
{PUNTOS_INICIALES.map((punto) => (
<div key={punto.label} className="border rounded-lg p-2 bg-gray-50">
<div className="flex items-center gap-2 mb-2">
<div
className={`w-4 h-4 rounded-full ${
asignaciones[punto.label] === 'eficiente' ? 'bg-green-500' :
asignaciones[punto.label] === 'ineficiente' ? 'bg-orange-500' :
asignaciones[punto.label] === 'inalcanzable' ? 'bg-red-500' :
'bg-blue-500'
}`}
/>
<span className="font-bold text-gray-800">Punto {punto.label}</span>
</div>
{!mostrarResultados && (
<select
value={asignaciones[punto.label] || ''}
onChange={(e) => handleAsignar(punto.label, e.target.value)}
className="w-full text-sm border rounded px-2 py-1"
>
<option value="">Seleccionar...</option>
{TIPOS_OPCIONES.map(tipo => (
<option key={tipo.id} value={tipo.id}>{tipo.label}</option>
))}
</select>
)}
{mostrarResultados && (
<div className={`text-xs font-medium ${
asignaciones[punto.label] === punto.tipo ? 'text-green-600' : 'text-red-600'
}`}>
{asignaciones[punto.label] === punto.tipo ? '✓ Correcto' : '✗ Incorrecto'}
</div>
)}
</div>
))}
</div>
</Card>
{/* Resultados */}
<AnimatePresence>
{mostrarResultados && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
>
<Card className={`p-6 ${
correctas === PUNTOS_INICIALES.length
? 'bg-gradient-to-br from-green-50 to-emerald-50 border-green-200'
: 'bg-gradient-to-br from-blue-50 to-indigo-50 border-blue-200'
}`}>
<div className="text-center">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200, delay: 0.2 }}
className={`inline-flex items-center justify-center w-16 h-16 rounded-full mb-4 ${
correctas === PUNTOS_INICIALES.length
? 'bg-gradient-to-br from-yellow-400 to-orange-500'
: 'bg-gradient-to-br from-blue-400 to-indigo-500'
}`}
>
<Trophy size={32} className="text-white" />
</motion.div>
<h3 className="text-xl font-bold text-gray-900 mb-2">
{correctas === PUNTOS_INICIALES.length
? '¡Perfecto!'
: correctas >= PUNTOS_INICIALES.length * 0.7
? '¡Muy bien!'
: '¡Sigue practicando!'}
</h3>
<p className="text-gray-600 mb-4">
{correctas} de {PUNTOS_INICIALES.length} puntos clasificados correctamente
</p>
<div className="grid grid-cols-3 gap-4 max-w-md mx-auto">
<div className="bg-white rounded-xl p-4 shadow-sm">
<CheckCircle className="w-5 h-5 text-green-500 mx-auto mb-2" />
<p className="text-xl font-bold text-green-700">{correctas}</p>
<p className="text-xs text-green-600">Correctos</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm">
<XCircle className="w-5 h-5 text-red-500 mx-auto mb-2" />
<p className="text-xl font-bold text-red-700">{PUNTOS_INICIALES.length - correctas}</p>
<p className="text-xs text-red-600">Incorrectos</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm">
<Scale className="w-5 h-5 text-blue-500 mx-auto mb-2" />
<p className="text-xl font-bold text-blue-700">
{Math.round((correctas / PUNTOS_INICIALES.length) * 100)}%
</p>
<p className="text-xs text-blue-600">Precisión</p>
</div>
</div>
</div>
</Card>
</motion.div>
)}
</AnimatePresence>
{/* Botones de acción */}
<div className="flex justify-between items-center">
<Button variant="outline" onClick={handleReiniciar}>
<RefreshCcw size={16} className="mr-2" />
Reiniciar
</Button>
{!mostrarResultados ? (
<Button onClick={handleVerificar} disabled={!todasAsignadas}>
Verificar Respuestas
</Button>
) : (
<Button onClick={handleReiniciar}>
Intentar de Nuevo
</Button>
)}
</div>
{/* Instrucciones */}
{!todasAsignadas && !mostrarResultados && (
<div className="text-center text-sm text-gray-500">
<p>
Arrastra los tipos hacia los puntos en el gráfico o usa los selectores debajo.
Faltan <span className="font-semibold text-blue-600">
{PUNTOS_INICIALES.length - Object.values(asignaciones).filter(Boolean).length}
</span> puntos por clasificar.
</p>
</div>
)}
</div>
);
}
export default FPPAnalizador;

View File

@@ -0,0 +1,554 @@
import { useState, useRef, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import {
CheckCircle,
XCircle,
RefreshCcw,
Trophy,
Move,
Info,
Target,
TrendingUp,
Trash2
} from 'lucide-react';
interface PuntoFPP {
id: string;
x: number;
y: number;
}
interface FPPConstructorProps {
onComplete?: (score: number, total: number) => void;
}
const ESCENARIOS = [
{
id: 1,
titulo: 'Economía de Bienes',
descripcion: 'Un país produce solo dos bienes: Alimentos y Tecnología.',
objetivo: 'Construye una FPP convexa que muestre la creciente escasez de recursos.',
puntosRequeridos: 5,
tipo: 'convexa',
maxX: 100,
maxY: 100,
},
{
id: 2,
titulo: 'Especialización Laboral',
descripcion: 'Dos trabajadores pueden producir manzanas o naranjas con costos de oportunidad constantes.',
objetivo: 'Construye una FPP lineal que refleje costos de oportunidad constantes.',
puntosRequeridos: 4,
tipo: 'lineal',
maxX: 100,
maxY: 100,
},
{
id: 3,
titulo: 'Economía con Recursos Especializados',
descripcion: 'Algunos recursos son mejores para producir un bien que otro.',
objetivo: 'Construye una FPP cóncava que muestre ventajas de especialización.',
puntosRequeridos: 5,
tipo: 'concava',
maxX: 100,
maxY: 100,
},
];
export function FPPConstructor({ onComplete }: FPPConstructorProps) {
const [escenarioActual, setEscenarioActual] = useState(0);
const [puntos, setPuntos] = useState<PuntoFPP[]>([]);
const [puntoArrastrado, setPuntoArrastrado] = useState<string | null>(null);
const [mostrarResultados, setMostrarResultados] = useState(false);
const svgRef = useRef<SVGSVGElement>(null);
const escenario = ESCENARIOS[escenarioActual];
const handleSvgClick = (e: React.MouseEvent<SVGSVGElement>) => {
if (mostrarResultados || puntoArrastrado) return;
const svg = svgRef.current;
if (!svg) return;
const rect = svg.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
// Limitar dentro del área del gráfico
const limitedX = Math.max(5, Math.min(95, x));
const limitedY = Math.max(5, Math.min(95, y));
const nuevoPunto: PuntoFPP = {
id: `punto-${Date.now()}`,
x: limitedX,
y: limitedY,
};
setPuntos(prev => [...prev, nuevoPunto]);
};
const handleMouseDown = (e: React.MouseEvent, puntoId: string) => {
e.stopPropagation();
if (mostrarResultados) return;
setPuntoArrastrado(puntoId);
};
const handleMouseMove = useCallback((e: React.MouseEvent<SVGSVGElement>) => {
if (!puntoArrastrado || !svgRef.current) return;
const svg = svgRef.current;
const rect = svg.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
const limitedX = Math.max(5, Math.min(95, x));
const limitedY = Math.max(5, Math.min(95, y));
setPuntos(prev =>
prev.map(p =>
p.id === puntoArrastrado ? { ...p, x: limitedX, y: limitedY } : p
)
);
}, [puntoArrastrado]);
const handleMouseUp = () => {
setPuntoArrastrado(null);
};
const handleEliminarPunto = (puntoId: string) => {
if (mostrarResultados) return;
setPuntos(prev => prev.filter(p => p.id !== puntoId));
};
const handleReiniciar = () => {
setPuntos([]);
setMostrarResultados(false);
setPuntoArrastrado(null);
};
const handleVerificar = () => {
setMostrarResultados(true);
// Calcular puntuación basada en el tipo de FPP
let correctas = 0;
const puntosOrdenados = [...puntos].sort((a, b) => a.x - b.x);
if (escenario.tipo === 'lineal') {
// Para FPP lineal, verificar que los puntos formen aproximadamente una línea recta
if (puntosOrdenados.length >= 2) {
const pendiente = (puntosOrdenados[0].y - puntosOrdenados[puntosOrdenados.length - 1].y) /
(puntosOrdenados[puntosOrdenados.length - 1].x - puntosOrdenados[0].x);
correctas = puntosOrdenados.every((p, i) => {
if (i === 0) return true;
const xExpected = puntosOrdenados[0].x + (i / (puntosOrdenados.length - 1)) *
(puntosOrdenados[puntosOrdenados.length - 1].x - puntosOrdenados[0].x);
const yExpected = puntosOrdenados[0].y - pendiente * (xExpected - puntosOrdenados[0].x);
return Math.abs(p.x - xExpected) < 15 && Math.abs(p.y - yExpected) < 15;
}) ? escenario.puntosRequeridos : Math.floor(escenario.puntosRequeridos * 0.6);
}
} else if (escenario.tipo === 'convexa') {
// Para FPP convexa (creciente escasez), verificar curvatura hacia arriba
if (puntosOrdenados.length >= 3) {
let esConvexa = true;
for (let i = 1; i < puntosOrdenados.length - 1; i++) {
const pendiente1 = (puntosOrdenados[i].y - puntosOrdenados[i-1].y) /
(puntosOrdenados[i].x - puntosOrdenados[i-1].x || 0.1);
const pendiente2 = (puntosOrdenados[i+1].y - puntosOrdenados[i].y) /
(puntosOrdenados[i+1].x - puntosOrdenados[i].x || 0.1);
if (pendiente2 < pendiente1 * 0.5) esConvexa = false;
}
correctas = esConvexa && puntosOrdenados.length >= escenario.puntosRequeridos
? escenario.puntosRequeridos
: Math.max(0, puntosOrdenados.length - 1);
}
} else if (escenario.tipo === 'concava') {
// Para FPP cóncava, verificar curvatura hacia abajo
if (puntosOrdenados.length >= 3) {
let esConcava = true;
for (let i = 1; i < puntosOrdenados.length - 1; i++) {
const pendiente1 = (puntosOrdenados[i].y - puntosOrdenados[i-1].y) /
(puntosOrdenados[i].x - puntosOrdenados[i-1].x || 0.1);
const pendiente2 = (puntosOrdenados[i+1].y - puntosOrdenados[i].y) /
(puntosOrdenados[i+1].x - puntosOrdenados[i].x || 0.1);
if (pendiente2 > pendiente1 * 1.5) esConcava = false;
}
correctas = esConcava && puntosOrdenados.length >= escenario.puntosRequeridos
? escenario.puntosRequeridos
: Math.max(0, puntosOrdenados.length - 1);
}
}
const puntosFinales = Math.min(correctas, escenario.puntosRequeridos);
if (onComplete) {
onComplete(puntosFinales, escenario.puntosRequeridos);
}
};
const handleSiguienteEscenario = () => {
if (escenarioActual < ESCENARIOS.length - 1) {
setEscenarioActual(prev => prev + 1);
setPuntos([]);
setMostrarResultados(false);
}
};
const handleAnteriorEscenario = () => {
if (escenarioActual > 0) {
setEscenarioActual(prev => prev - 1);
setPuntos([]);
setMostrarResultados(false);
}
};
// Generar path para la línea FPP
const generateFPPPath = () => {
if (puntos.length < 2) return '';
const sorted = [...puntos].sort((a, b) => a.x - b.x);
// Si es lineal, usar líneas rectas
if (escenario.tipo === 'lineal') {
return sorted.reduce((path, p, i) =>
i === 0 ? `M ${p.x} ${p.y}` : `${path} L ${p.x} ${p.y}`, ''
);
}
// Para convexa/cóncava, usar curvas suaves
let path = `M ${sorted[0].x} ${sorted[0].y}`;
for (let i = 1; i < sorted.length; i++) {
const prev = sorted[i - 1];
const curr = sorted[i];
const cpX1 = prev.x + (curr.x - prev.x) * 0.3;
const cpX2 = prev.x + (curr.x - prev.x) * 0.7;
path += ` C ${cpX1} ${prev.y}, ${cpX2} ${curr.y}, ${curr.x} ${curr.y}`;
}
return path;
};
const esCompletado = puntos.length >= escenario.puntosRequeridos;
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-start justify-between">
<div>
<h3 className="text-xl font-bold text-gray-900">Constructor de FPP</h3>
<p className="text-gray-600 mt-1">
Escenario {escenarioActual + 1} de {ESCENARIOS.length}: {escenario.titulo}
</p>
</div>
<div className="flex items-center gap-2 text-amber-600 bg-amber-50 px-3 py-1.5 rounded-full">
<Target size={16} />
<span className="font-semibold text-sm">
{escenario.puntosRequeridos} pts mín.
</span>
</div>
</div>
{/* Descripción del escenario */}
<Card className="p-4 bg-blue-50 border-blue-200">
<div className="flex items-start gap-3">
<Info size={20} className="text-blue-600 flex-shrink-0 mt-0.5" />
<div>
<h4 className="font-semibold text-blue-900">{escenario.descripcion}</h4>
<p className="text-blue-700 text-sm mt-1">
<strong>Objetivo:</strong> {escenario.objetivo}
</p>
</div>
</div>
</Card>
{/* Área de trabajo SVG */}
<Card className="p-4 overflow-hidden">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Move size={18} className="text-gray-500" />
<span className="text-sm text-gray-600">
Haz clic para agregar puntos Arrastra para mover
<span className="text-blue-600 font-medium"> {puntos.length} puntos</span>
</span>
</div>
{puntos.length > 0 && (
<Button
variant="outline"
size="sm"
onClick={handleReiniciar}
disabled={mostrarResultados}
>
<RefreshCcw size={14} className="mr-1" />
Limpiar
</Button>
)}
</div>
<div className="relative">
<svg
ref={svgRef}
viewBox="0 0 100 100"
className="w-full h-auto aspect-square max-h-[500px] cursor-crosshair touch-none"
onClick={handleSvgClick}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
{/* Grid */}
<defs>
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="#e5e7eb" strokeWidth="0.3" />
</pattern>
</defs>
<rect width="100" height="100" fill="url(#grid)" />
{/* Ejes */}
<line x1="5" y1="95" x2="95" y2="95" stroke="#374151" strokeWidth="0.8" />
<line x1="5" y1="95" x2="5" y2="5" stroke="#374151" strokeWidth="0.8" />
{/* Flechas */}
<polygon points="95,95 92,93 92,97" fill="#374151" />
<polygon points="5,5 3,8 7,8" fill="#374151" />
{/* Etiquetas */}
<text x="50" y="99" textAnchor="middle" className="text-[3.5px] fill-gray-700 font-semibold">
Bien X
</text>
<text x="1" y="50" textAnchor="middle" transform="rotate(-90, 1, 50)" className="text-[3.5px] fill-gray-700 font-semibold">
Bien Y
</text>
{/* Marcas de escala */}
{[0, 25, 50, 75, 100].map(val => (
<g key={val}>
<line x1={5 + val * 0.9} y1="95" x2={5 + val * 0.9} y2="96" stroke="#374151" strokeWidth="0.5" />
<text x={5 + val * 0.9} y="98.5" textAnchor="middle" className="text-[2.5px] fill-gray-500">{val}</text>
<line x1="5" y1={95 - val * 0.9} x2="4" y2={95 - val * 0.9} stroke="#374151" strokeWidth="0.5" />
<text x="3" y={95 - val * 0.9 + 1} textAnchor="end" className="text-[2.5px] fill-gray-500">{val}</text>
</g>
))}
{/* Línea FPP */}
{puntos.length >= 2 && (
<motion.path
d={generateFPPPath()}
fill="none"
stroke="#2563eb"
strokeWidth="1.5"
strokeLinecap="round"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.5 }}
/>
)}
{/* Puntos */}
{puntos.map((punto, index) => (
<g key={punto.id}>
<motion.circle
cx={punto.x}
cy={punto.y}
r="3"
fill={mostrarResultados ? '#22c55e' : '#3b82f6'}
stroke="white"
strokeWidth="1"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
whileHover={{ scale: 1.2 }}
className="cursor-move"
onMouseDown={(e) => handleMouseDown(e, punto.id)}
/>
<text
x={punto.x}
y={punto.y - 5}
textAnchor="middle"
className="text-[3px] fill-gray-700 font-bold pointer-events-none"
>
P{index + 1}
</text>
{/* Botón eliminar */}
{!mostrarResultados && (
<g
transform={`translate(${punto.x + 4}, ${punto.y - 4})`}
className="cursor-pointer"
onClick={(e) => {
e.stopPropagation();
handleEliminarPunto(punto.id);
}}
>
<circle r="2.5" fill="#ef4444" />
<text x="0" y="1" textAnchor="middle" className="text-[3px] fill-white font-bold">×</text>
</g>
)}
</g>
))}
{/* Indicador de tipo de FPP */}
<g transform="translate(70, 10)">
<rect x="0" y="0" width="25" height="8" rx="1" fill="white" stroke="#e5e7eb" strokeWidth="0.3" />
<text x="2" y="3" className="text-[2px] fill-gray-500">Tipo FPP:</text>
<text x="2" y="6.5" className={`text-[2.5px] font-semibold ${
escenario.tipo === 'lineal' ? 'fill-blue-600' :
escenario.tipo === 'convexa' ? 'fill-purple-600' :
'fill-orange-600'
}`}>
{escenario.tipo === 'lineal' ? 'Lineal (CCO constante)' :
escenario.tipo === 'convexa' ? 'Convexa (escasez creciente)' :
'Cóncava (especialización)'}
</text>
</g>
</svg>
</div>
{/* Lista de puntos */}
{puntos.length > 0 && (
<div className="mt-4 border-t pt-4">
<h4 className="text-sm font-semibold text-gray-700 mb-2">Coordenadas:</h4>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-2">
{[...puntos].sort((a, b) => a.x - b.x).map((punto, index) => (
<div key={punto.id} className="bg-gray-50 rounded-lg p-2 text-xs">
<div className="font-semibold text-gray-800">P{index + 1}</div>
<div className="text-gray-600">X: {punto.x.toFixed(1)}</div>
<div className="text-gray-600">Y: {punto.y.toFixed(1)}</div>
</div>
))}
</div>
</div>
)}
</Card>
{/* Resultados */}
<AnimatePresence>
{mostrarResultados && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
>
<Card className={`p-6 ${
puntos.length >= escenario.puntosRequeridos
? 'bg-gradient-to-br from-green-50 to-emerald-50 border-green-200'
: 'bg-gradient-to-br from-yellow-50 to-orange-50 border-yellow-200'
}`}>
<div className="text-center">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200, delay: 0.2 }}
className={`inline-flex items-center justify-center w-16 h-16 rounded-full mb-4 ${
puntos.length >= escenario.puntosRequeridos
? 'bg-gradient-to-br from-yellow-400 to-orange-500'
: 'bg-gradient-to-br from-yellow-400 to-orange-400'
}`}
>
{puntos.length >= escenario.puntosRequeridos ? (
<Trophy size={32} className="text-white" />
) : (
<TrendingUp size={32} className="text-white" />
)}
</motion.div>
<h3 className="text-xl font-bold text-gray-900 mb-2">
{puntos.length >= escenario.puntosRequeridos
? '¡Excelente trabajo!'
: '¡Necesitas más puntos!'}
</h3>
<p className="text-gray-600 mb-4">
{puntos.length >= escenario.puntosRequeridos
? `Has construido una FPP ${escenario.tipo} correctamente con ${puntos.length} puntos.`
: `Agrega al menos ${escenario.puntosRequeridos - puntos.length} punto(s) más para completar el escenario.`}
</p>
<div className="flex justify-center gap-8">
<div className="text-center">
<p className="text-3xl font-bold text-blue-700">{puntos.length}</p>
<p className="text-sm text-blue-600">Puntos agregados</p>
</div>
<div className="text-center">
<p className="text-3xl font-bold text-green-700">{escenario.puntosRequeridos}</p>
<p className="text-sm text-green-600">Requeridos</p>
</div>
</div>
</div>
</Card>
</motion.div>
)}
</AnimatePresence>
{/* Navegación y acciones */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex gap-2">
<Button
variant="outline"
onClick={handleAnteriorEscenario}
disabled={escenarioActual === 0}
>
Anterior
</Button>
<Button
variant="outline"
onClick={handleSiguienteEscenario}
disabled={escenarioActual === ESCENARIOS.length - 1}
>
Siguiente
</Button>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleReiniciar}>
<RefreshCcw size={16} className="mr-2" />
Reiniciar
</Button>
{!mostrarResultados ? (
<Button onClick={handleVerificar} disabled={puntos.length < 2}>
<CheckCircle size={16} className="mr-2" />
Verificar
</Button>
) : (
<Button onClick={handleReiniciar}>
Intentar de Nuevo
</Button>
)}
</div>
</div>
{/* Instrucciones adicionales */}
{!esCompletado && !mostrarResultados && (
<div className="text-center text-sm text-gray-500">
<p>
Faltan <span className="font-semibold text-blue-600">
{Math.max(0, escenario.puntosRequeridos - puntos.length)}
</span> puntos para completar este escenario.
</p>
</div>
)}
{/* Guía de tipos de FPP */}
<Card className="p-4 bg-gray-50">
<h4 className="font-semibold text-gray-800 mb-3 flex items-center gap-2">
<TrendingUp size={18} className="text-gray-600" />
Tipos de Frontera de Posibilidades
</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div className="border rounded-lg p-3 bg-white">
<h5 className="font-semibold text-blue-700 mb-1">Lineal</h5>
<p className="text-gray-600">Costos de oportunidad constantes. Los recursos son perfectamente sustituibles entre bienes.</p>
</div>
<div className="border rounded-lg p-3 bg-white">
<h5 className="font-semibold text-purple-700 mb-1">Convexa (hacia afuera)</h5>
<p className="text-gray-600">Costos de oportunidad crecientes. Los recursos no son perfectamente adaptables.</p>
</div>
<div className="border rounded-lg p-3 bg-white">
<h5 className="font-semibold text-orange-700 mb-1">Cóncava (hacia adentro)</h5>
<p className="text-gray-600">Costos de oportunidad decrecientes. Especialización en bienes específicos.</p>
</div>
</div>
</Card>
</div>
);
}
export default FPPConstructor;

View File

@@ -0,0 +1,414 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, Trophy, RotateCcw, ArrowRight, Mountain, Users, Factory, Lightbulb } from 'lucide-react';
interface FactoresProduccionQuizProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Pregunta {
id: number;
pregunta: string;
tipo: 'tierra' | 'trabajo' | 'capital' | 'emprendimiento';
opciones: string[];
respuestaCorrecta: number;
explicacion: string;
}
const PREGUNTAS: Pregunta[] = [
{
id: 1,
pregunta: '¿Cuál de los siguientes es un ejemplo de TIERRA como factor de producción?',
tipo: 'tierra',
opciones: [
'El trabajo de un obrero',
'Un terreno agrícola',
'Una máquina industrial',
'La habilidad de un gerente'
],
respuestaCorrecta: 1,
explicacion: 'La tierra incluye todos los recursos naturales: terrenos, minerales, agua, petróleo, etc. Es todo lo que nos proporciona la naturaleza sin transformar.'
},
{
id: 2,
pregunta: 'El TRABAJO como factor de producción se refiere a:',
tipo: 'trabajo',
opciones: [
'Solo el esfuerzo físico',
'Solo el esfuerzo mental',
'El esfuerzo físico y mental que aportan las personas',
'Las máquinas que reemplazan a los humanos'
],
respuestaCorrecta: 2,
explicacion: 'El trabajo incluye tanto el esfuerzo físico (como el de un albañil) como el mental (como el de un ingeniero). Es el factor humano en la producción.'
},
{
id: 3,
pregunta: '¿Qué se considera CAPITAL como factor de producción?',
tipo: 'capital',
opciones: [
'Dinero en una cuenta bancaria',
'Acciones de una empresa',
'Maquinaria, herramientas y equipos utilizados para producir',
'Terrenos y edificios'
],
respuestaCorrecta: 2,
explicacion: 'En economía, el capital físico (o capital real) son los bienes manufacturados utilizados para producir otros bienes: máquinas, herramientas, fábricas, etc. No es dinero.'
},
{
id: 4,
pregunta: '¿Cuál es la recompensa que reciben los propietarios del factor TIERRA?',
tipo: 'tierra',
opciones: [
'Salarios',
'Rentas o alquileres',
'Intereses',
'Beneficios'
],
respuestaCorrecta: 1,
explicacion: 'Los propietarios de tierra reciben RENTAS (o alquileres) como pago por el uso de sus recursos naturales.'
},
{
id: 5,
pregunta: 'Los trabajadores reciben _____ como recompensa por su factor de producción.',
tipo: 'trabajo',
opciones: [
'Intereses',
'Rentas',
'Salarios',
'Dividendos'
],
respuestaCorrecta: 2,
explicacion: 'El trabajo recibe SALARIOS (o sueldos) como compensación por el esfuerzo físico y mental aportado a la producción.'
},
{
id: 6,
pregunta: '¿Qué reciben los propietarios de CAPITAL como recompensa?',
tipo: 'capital',
opciones: [
'Salarios',
'Rentas',
'Intereses',
'Bonificaciones'
],
respuestaCorrecta: 2,
explicacion: 'El capital recibe INTERESES como recompensa. Si prestas tu capital (maquinaria o dinero para comprarla), recibes intereses a cambio.'
},
{
id: 7,
pregunta: 'El EMPRENDIMIENTO (o empresa) es el factor que:',
tipo: 'emprendimiento',
opciones: [
'Solo invierte dinero',
'Combina los otros factores de producción asumiendo riesgos',
'Trabaja en la fábrica',
'Solo vende los productos'
],
respuestaCorrecta: 1,
explicacion: 'El emprendimiento es el factor que organiza y combina tierra, trabajo y capital para producir bienes y servicios, asumiendo el riesgo del negocio.'
},
{
id: 8,
pregunta: '¿Cuál es la recompensa del EMPRENDIMIENTO?',
tipo: 'emprendimiento',
opciones: [
'Salario fijo',
'Intereses garantizados',
'Beneficios (o pérdidas)',
'Renta del terreno'
],
respuestaCorrecta: 2,
explicacion: 'El emprendimiento recibe BENEFICIOS cuando la empresa tiene éxito, pero también puede sufrir PÉRDIDAS. Es el factor con mayor riesgo y potencial de ganancia.'
}
];
export function FactoresProduccionQuiz({ ejercicioId: _ejercicioId, onComplete }: FactoresProduccionQuizProps) {
const [preguntaActual, setPreguntaActual] = useState(0);
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<number | null>(null);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [puntuacion, setPuntuacion] = useState(0);
const [completado, setCompletado] = useState(false);
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
const pregunta = PREGUNTAS[preguntaActual];
const getTipoIcon = (tipo: string) => {
switch (tipo) {
case 'tierra':
return <Mountain size={20} />;
case 'trabajo':
return <Users size={20} />;
case 'capital':
return <Factory size={20} />;
case 'emprendimiento':
return <Lightbulb size={20} />;
default:
return null;
}
};
const getTipoLabel = (tipo: string) => {
switch (tipo) {
case 'tierra':
return 'Tierra';
case 'trabajo':
return 'Trabajo';
case 'capital':
return 'Capital';
case 'emprendimiento':
return 'Emprendimiento';
default:
return '';
}
};
const getTipoColor = (tipo: string) => {
switch (tipo) {
case 'tierra':
return 'bg-green-100 text-green-700 border-green-200';
case 'trabajo':
return 'bg-blue-100 text-blue-700 border-blue-200';
case 'capital':
return 'bg-amber-100 text-amber-700 border-amber-200';
case 'emprendimiento':
return 'bg-purple-100 text-purple-700 border-purple-200';
default:
return 'bg-gray-100 text-gray-700 border-gray-200';
}
};
const handleSeleccionar = (index: number) => {
if (mostrarResultado) return;
setRespuestaSeleccionada(index);
};
const handleVerificar = () => {
if (respuestaSeleccionada === null) return;
const esCorrecta = respuestaSeleccionada === pregunta.respuestaCorrecta;
setMostrarResultado(true);
if (esCorrecta) {
setPuntuacion(prev => prev + Math.round(100 / PREGUNTAS.length));
setRespuestasCorrectas(prev => prev + 1);
}
if (preguntaActual === PREGUNTAS.length - 1) {
setTimeout(() => {
setCompletado(true);
const puntuacionFinal = puntuacion + (esCorrecta ? Math.round(100 / PREGUNTAS.length) : 0);
if (onComplete) {
onComplete(puntuacionFinal);
}
}, 2000);
}
};
const handleSiguiente = () => {
setPreguntaActual(prev => prev + 1);
setRespuestaSeleccionada(null);
setMostrarResultado(false);
};
const handleReiniciar = () => {
setPreguntaActual(0);
setRespuestaSeleccionada(null);
setMostrarResultado(false);
setPuntuacion(0);
setCompletado(false);
setRespuestasCorrectas(0);
};
if (completado) {
return (
<Card className="w-full max-w-2xl mx-auto">
<div className="text-center py-8 px-4">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-full mb-6"
>
<Trophy size={40} className="text-white" />
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">
¡Quiz Completado!
</h3>
<p className="text-gray-600 mb-6">
Has respondido {respuestasCorrectas} de {PREGUNTAS.length} preguntas correctamente
</p>
<div className="grid grid-cols-2 gap-4 max-w-md mx-auto mb-6">
<div className={`p-4 rounded-xl border ${getTipoColor('tierra')}`}>
<Mountain className="mx-auto mb-2" size={24} />
<p className="font-semibold text-sm">Tierra</p>
<p className="text-xs opacity-75">Rentas</p>
</div>
<div className={`p-4 rounded-xl border ${getTipoColor('trabajo')}`}>
<Users className="mx-auto mb-2" size={24} />
<p className="font-semibold text-sm">Trabajo</p>
<p className="text-xs opacity-75">Salarios</p>
</div>
<div className={`p-4 rounded-xl border ${getTipoColor('capital')}`}>
<Factory className="mx-auto mb-2" size={24} />
<p className="font-semibold text-sm">Capital</p>
<p className="text-xs opacity-75">Intereses</p>
</div>
<div className={`p-4 rounded-xl border ${getTipoColor('emprendimiento')}`}>
<Lightbulb className="mx-auto mb-2" size={24} />
<p className="font-semibold text-sm">Emprendimiento</p>
<p className="text-xs opacity-75">Beneficios</p>
</div>
</div>
<div className="bg-blue-50 rounded-xl p-6 mb-6">
<p className="text-sm text-blue-600 mb-1">Puntuación Total</p>
<p className="text-4xl font-bold text-blue-700">{puntuacion}</p>
<p className="text-sm text-blue-500">puntos</p>
</div>
<Button onClick={handleReiniciar} variant="outline">
<RotateCcw size={16} className="mr-2" />
Intentar de Nuevo
</Button>
</div>
</Card>
);
}
return (
<Card className="w-full max-w-3xl mx-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-xl font-bold text-gray-900">Factores de Producción</h3>
<p className="text-sm text-gray-500">Pregunta {preguntaActual + 1} de {PREGUNTAS.length}</p>
</div>
<div className="text-right">
<p className="text-xs text-gray-500">Puntos</p>
<p className="text-xl font-bold text-blue-600">{puntuacion}</p>
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2 mb-6">
<motion.div
className="h-2 bg-blue-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${((preguntaActual + 1) / PREGUNTAS.length) * 100}%` }}
/>
</div>
<div className="mb-6">
<div className={`inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-semibold mb-3 border ${getTipoColor(pregunta.tipo)}`}>
{getTipoIcon(pregunta.tipo)}
<span>{getTipoLabel(pregunta.tipo)}</span>
</div>
<h4 className="text-lg font-medium text-gray-800">
{pregunta.pregunta}
</h4>
</div>
<div className="space-y-3 mb-6">
{pregunta.opciones.map((opcion, index) => {
const estaSeleccionada = respuestaSeleccionada === index;
const esCorrecta = index === pregunta.respuestaCorrecta;
const mostrarCorrecta = mostrarResultado && esCorrecta;
const mostrarIncorrecta = mostrarResultado && estaSeleccionada && !esCorrecta;
return (
<motion.button
key={index}
onClick={() => handleSeleccionar(index)}
disabled={mostrarResultado}
whileHover={!mostrarResultado ? { scale: 1.01 } : {}}
whileTap={!mostrarResultado ? { scale: 0.99 } : {}}
className={`w-full p-4 rounded-xl border-2 text-left transition-all ${
mostrarCorrecta
? 'border-green-500 bg-green-50'
: mostrarIncorrecta
? 'border-red-500 bg-red-50'
: estaSeleccionada
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 bg-white hover:border-blue-300'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-6 h-6 rounded-full border-2 flex items-center justify-center ${
mostrarCorrecta
? 'border-green-500 bg-green-500 text-white'
: mostrarIncorrecta
? 'border-red-500 bg-red-500 text-white'
: estaSeleccionada
? 'border-blue-500 bg-blue-500 text-white'
: 'border-gray-300'
}`}>
{mostrarCorrecta && <CheckCircle size={14} />}
{mostrarIncorrecta && <XCircle size={14} />}
{!mostrarResultado && estaSeleccionada && (
<div className="w-2 h-2 bg-white rounded-full" />
)}
</div>
<span className={`font-medium ${
mostrarCorrecta ? 'text-green-800' :
mostrarIncorrecta ? 'text-red-800' :
'text-gray-700'
}`}>
{opcion}
</span>
</div>
</motion.button>
);
})}
</div>
<AnimatePresence>
{mostrarResultado && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className={`p-4 rounded-xl mb-6 ${
respuestaSeleccionada === pregunta.respuestaCorrecta
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}
>
<p className={`font-medium mb-2 ${
respuestaSeleccionada === pregunta.respuestaCorrecta
? 'text-green-800'
: 'text-red-800'
}`}>
{respuestaSeleccionada === pregunta.respuestaCorrecta
? '¡Correcto!'
: 'Incorrecto'}
</p>
<p className="text-sm text-gray-700">{pregunta.explicacion}</p>
</motion.div>
)}
</AnimatePresence>
<div className="flex justify-end">
{!mostrarResultado ? (
<Button
onClick={handleVerificar}
disabled={respuestaSeleccionada === null}
>
Verificar Respuesta
</Button>
) : preguntaActual < PREGUNTAS.length - 1 ? (
<Button onClick={handleSiguiente}>
Siguiente
<ArrowRight size={16} className="ml-2" />
</Button>
) : null}
</div>
</div>
</Card>
);
}
export default FactoresProduccionQuiz;

View File

@@ -0,0 +1,361 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, Trophy, RotateCcw, ArrowRight, Users, Building2 } from 'lucide-react';
interface FlujoCircularBasicoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Pregunta {
id: number;
pregunta: string;
tipo: 'mercado-bienes' | 'mercado-factores' | 'flujo-real' | 'flujo-monetario';
opciones: string[];
respuestaCorrecta: number;
explicacion: string;
}
const PREGUNTAS: Pregunta[] = [
{
id: 1,
pregunta: 'En el mercado de bienes y servicios, ¿quiénes son los demandantes?',
tipo: 'mercado-bienes',
opciones: [
'Las empresas',
'Las familias',
'El gobierno',
'Los bancos'
],
respuestaCorrecta: 1,
explicacion: 'En el mercado de bienes y servicios, las familias son los demandantes (compran productos) y las empresas son los oferentes (venden productos).'
},
{
id: 2,
pregunta: 'En el mercado de factores de producción, ¿quiénes ofrecen el trabajo, la tierra y el capital?',
tipo: 'mercado-factores',
opciones: [
'Las familias',
'Las empresas',
'El Estado',
'Los inversores extranjeros'
],
respuestaCorrecta: 0,
explicacion: 'Las familias son propietarias de los factores de producción (trabajo, tierra, capital) y los ofrecen a las empresas a cambio de ingresos.'
},
{
id: 3,
pregunta: '¿Qué representa el FLUJO REAL en el modelo de 2 sectores?',
tipo: 'flujo-real',
opciones: [
'El movimiento de dinero entre familias y empresas',
'El movimiento de bienes, servicios y factores de producción',
'Los impuestos pagados al gobierno',
'Las transacciones bancarias'
],
respuestaCorrecta: 1,
explicacion: 'El flujo real representa el movimiento físico de bienes y servicios (de empresas a familias) y factores de producción (de familias a empresas).'
},
{
id: 4,
pregunta: '¿Qué reciben las familias a cambio de ofrecer sus factores de producción?',
tipo: 'flujo-monetario',
opciones: [
'Productos terminados',
'Ingresos (salarios, rentas, intereses, beneficios)',
'Servicios públicos',
'Acciones de empresas'
],
respuestaCorrecta: 1,
explicacion: 'Las familias reciben ingresos monetarios: salarios (por trabajo), rentas (por tierra), intereses (por capital) y beneficios (por empresa).'
},
{
id: 5,
pregunta: 'En el FLUJO MONETARIO, el dinero fluye de las empresas a las familias como:',
tipo: 'flujo-monetario',
opciones: [
'Pagos por compra de bienes',
'Pagos por factores de producción (costes)',
'Impuestos',
'Subvenciones'
],
respuestaCorrecta: 1,
explicacion: 'Las empresas pagan a las familias por el uso de sus factores: salarios (trabajo), alquileres (tierra), intereses (capital) y beneficios (emprendimiento).'
},
{
id: 6,
pregunta: '¿Por qué se llama "flujo circular"?',
tipo: 'flujo-real',
opciones: [
'Porque el dinero siempre aumenta',
'Porque hay un flujo continuo en ambas direcciones entre familias y empresas',
'Porque las empresas siempre ganan',
'Porque el gobierno interviene'
],
respuestaCorrecta: 1,
explicacion: 'Se llama flujo circular porque hay un movimiento continuo en ambas direcciones: factores de producción van de familias a empresas, y bienes/servicios van de empresas a familias.'
}
];
export function FlujoCircularBasico({ ejercicioId: _ejercicioId, onComplete }: FlujoCircularBasicoProps) {
const [preguntaActual, setPreguntaActual] = useState(0);
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<number | null>(null);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [puntuacion, setPuntuacion] = useState(0);
const [completado, setCompletado] = useState(false);
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
const pregunta = PREGUNTAS[preguntaActual];
const getTipoLabel = (tipo: string) => {
switch (tipo) {
case 'mercado-bienes':
return 'Mercado de Bienes';
case 'mercado-factores':
return 'Mercado de Factores';
case 'flujo-real':
return 'Flujo Real';
case 'flujo-monetario':
return 'Flujo Monetario';
default:
return '';
}
};
const getTipoColor = (tipo: string) => {
switch (tipo) {
case 'mercado-bienes':
return 'bg-blue-100 text-blue-700';
case 'mercado-factores':
return 'bg-green-100 text-green-700';
case 'flujo-real':
return 'bg-purple-100 text-purple-700';
case 'flujo-monetario':
return 'bg-amber-100 text-amber-700';
default:
return 'bg-gray-100 text-gray-700';
}
};
const handleSeleccionar = (index: number) => {
if (mostrarResultado) return;
setRespuestaSeleccionada(index);
};
const handleVerificar = () => {
if (respuestaSeleccionada === null) return;
const esCorrecta = respuestaSeleccionada === pregunta.respuestaCorrecta;
setMostrarResultado(true);
if (esCorrecta) {
setPuntuacion(prev => prev + Math.round(100 / PREGUNTAS.length));
setRespuestasCorrectas(prev => prev + 1);
}
if (preguntaActual === PREGUNTAS.length - 1) {
setTimeout(() => {
setCompletado(true);
const puntuacionFinal = puntuacion + (esCorrecta ? Math.round(100 / PREGUNTAS.length) : 0);
if (onComplete) {
onComplete(puntuacionFinal);
}
}, 2000);
}
};
const handleSiguiente = () => {
setPreguntaActual(prev => prev + 1);
setRespuestaSeleccionada(null);
setMostrarResultado(false);
};
const handleReiniciar = () => {
setPreguntaActual(0);
setRespuestaSeleccionada(null);
setMostrarResultado(false);
setPuntuacion(0);
setCompletado(false);
setRespuestasCorrectas(0);
};
if (completado) {
return (
<Card className="w-full max-w-2xl mx-auto">
<div className="text-center py-8 px-4">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-full mb-6"
>
<Trophy size={40} className="text-white" />
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">
¡Ejercicio Completado!
</h3>
<p className="text-gray-600 mb-4">
Has respondido {respuestasCorrectas} de {PREGUNTAS.length} preguntas correctamente
</p>
<div className="flex justify-center gap-8 mb-6">
<div className="flex items-center gap-2 text-blue-600">
<Users size={24} />
<span className="font-medium">Familias</span>
</div>
<div className="text-2xl text-gray-300"></div>
<div className="flex items-center gap-2 text-green-600">
<Building2 size={24} />
<span className="font-medium">Empresas</span>
</div>
</div>
<div className="bg-blue-50 rounded-xl p-6 mb-6">
<p className="text-sm text-blue-600 mb-1">Puntuación</p>
<p className="text-4xl font-bold text-blue-700">{puntuacion}</p>
<p className="text-sm text-blue-500">puntos</p>
</div>
<Button onClick={handleReiniciar} variant="outline">
<RotateCcw size={16} className="mr-2" />
Intentar de Nuevo
</Button>
</div>
</Card>
);
}
return (
<Card className="w-full max-w-3xl mx-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-xl font-bold text-gray-900">Flujo Circular: 2 Sectores</h3>
<p className="text-sm text-gray-500">Pregunta {preguntaActual + 1} de {PREGUNTAS.length}</p>
</div>
<div className="text-right">
<p className="text-xs text-gray-500">Puntos</p>
<p className="text-xl font-bold text-blue-600">{puntuacion}</p>
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2 mb-6">
<motion.div
className="h-2 bg-blue-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${((preguntaActual + 1) / PREGUNTAS.length) * 100}%` }}
/>
</div>
<div className="mb-6">
<div className={`inline-block px-3 py-1 rounded-full text-xs font-semibold mb-3 ${getTipoColor(pregunta.tipo)}`}>
{getTipoLabel(pregunta.tipo)}
</div>
<h4 className="text-lg font-medium text-gray-800">
{pregunta.pregunta}
</h4>
</div>
<div className="space-y-3 mb-6">
{pregunta.opciones.map((opcion, index) => {
const estaSeleccionada = respuestaSeleccionada === index;
const esCorrecta = index === pregunta.respuestaCorrecta;
const mostrarCorrecta = mostrarResultado && esCorrecta;
const mostrarIncorrecta = mostrarResultado && estaSeleccionada && !esCorrecta;
return (
<motion.button
key={index}
onClick={() => handleSeleccionar(index)}
disabled={mostrarResultado}
whileHover={!mostrarResultado ? { scale: 1.01 } : {}}
whileTap={!mostrarResultado ? { scale: 0.99 } : {}}
className={`w-full p-4 rounded-xl border-2 text-left transition-all ${
mostrarCorrecta
? 'border-green-500 bg-green-50'
: mostrarIncorrecta
? 'border-red-500 bg-red-50'
: estaSeleccionada
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 bg-white hover:border-blue-300'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-6 h-6 rounded-full border-2 flex items-center justify-center ${
mostrarCorrecta
? 'border-green-500 bg-green-500 text-white'
: mostrarIncorrecta
? 'border-red-500 bg-red-500 text-white'
: estaSeleccionada
? 'border-blue-500 bg-blue-500 text-white'
: 'border-gray-300'
}`}>
{mostrarCorrecta && <CheckCircle size={14} />}
{mostrarIncorrecta && <XCircle size={14} />}
{!mostrarResultado && estaSeleccionada && (
<div className="w-2 h-2 bg-white rounded-full" />
)}
</div>
<span className={`font-medium ${
mostrarCorrecta ? 'text-green-800' :
mostrarIncorrecta ? 'text-red-800' :
'text-gray-700'
}`}>
{opcion}
</span>
</div>
</motion.button>
);
})}
</div>
<AnimatePresence>
{mostrarResultado && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className={`p-4 rounded-xl mb-6 ${
respuestaSeleccionada === pregunta.respuestaCorrecta
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}
>
<p className={`font-medium mb-2 ${
respuestaSeleccionada === pregunta.respuestaCorrecta
? 'text-green-800'
: 'text-red-800'
}`}>
{respuestaSeleccionada === pregunta.respuestaCorrecta
? '¡Correcto!'
: 'Incorrecto'}
</p>
<p className="text-sm text-gray-700">{pregunta.explicacion}</p>
</motion.div>
)}
</AnimatePresence>
<div className="flex justify-end">
{!mostrarResultado ? (
<Button
onClick={handleVerificar}
disabled={respuestaSeleccionada === null}
>
Verificar Respuesta
</Button>
) : preguntaActual < PREGUNTAS.length - 1 ? (
<Button onClick={handleSiguiente}>
Siguiente
<ArrowRight size={16} className="ml-2" />
</Button>
) : null}
</div>
</div>
</Card>
);
}
export default FlujoCircularBasico;

View File

@@ -0,0 +1,258 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, ArrowRight } from 'lucide-react';
interface EjercicioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Pregunta {
id: number;
pregunta: string;
opciones: string[];
correcta: number;
explicacion: string;
categoria: 'que' | 'como' | 'para_quien';
}
const PREGUNTAS: Pregunta[] = [
{
id: 1,
pregunta: "¿Qué decisión responde a la pregunta 'QUÉ producir'?",
opciones: [
"Elegir entre producir computadoras o smartphones",
"Decidir si usar mano de obra o maquinaria",
"Determinar si los productos van a ricos o pobres",
"Establecer el precio de venta de los productos"
],
correcta: 0,
explicacion: "La pregunta 'QUÉ producir' se refiere a la elección de qué bienes y servicios se van a fabricar con los recursos disponibles.",
categoria: 'que'
},
{
id: 2,
pregunta: "¿Qué decisión responde a la pregunta 'CÓMO producir'?",
opciones: [
"Elegir entre producir autos o camiones",
"Decidir entre usar tecnología o mano de obra intensiva",
"Determinar quién consumirá los productos",
"Calcular cuánto invertir en publicidad"
],
correcta: 1,
explicacion: "La pregunta 'CÓMO producir' se refiere a la elección de la técnica o método de producción a utilizar.",
categoria: 'como'
},
{
id: 3,
pregunta: "¿Qué decisión responde a la pregunta 'PARA QUIÉN producir'?",
opciones: [
"Seleccionar los materiales a utilizar",
"Elegir la ubicación de la fábrica",
"Distribuir los productos entre diferentes grupos de la sociedad",
"Determinar la cantidad a producir"
],
correcta: 2,
explicacion: "La pregunta 'PARA QUIÉN producir' se refiere a la distribución de los bienes y servicios entre los miembros de la sociedad.",
categoria: 'para_quien'
},
{
id: 4,
pregunta: "Un país debe decidir entre destinar sus recursos a hospitales o a escuelas. ¿Qué pregunta del problema económico resuelve?",
opciones: [
"¿Qué producir?",
"¿Cómo producir?",
"¿Para quién producir?",
"¿Cuánto producir?"
],
correcta: 0,
explicacion: "Elegir entre hospitales y escuelas es una decisión sobre QUÉ bienes y servicios públicos producir.",
categoria: 'que'
},
{
id: 5,
pregunta: "Una empresa textil decide reemplazar trabajadores por máquinas automáticas. ¿Qué pregunta responde?",
opciones: [
"¿Qué producir?",
"¿Cómo producir?",
"¿Para quién producir?",
"¿Dónde producir?"
],
correcta: 1,
explicacion: "La decisión de usar máquinas vs. trabajadores es una decisión sobre CÓMO producir.",
categoria: 'como'
},
{
id: 6,
pregunta: "El gobierno implementa subsidios para que los medicamentos sean accesibles a personas de bajos recursos. ¿Qué pregunta resuelve?",
opciones: [
"¿Qué producir?",
"¿Cómo producir?",
"¿Para quién producir?",
"¿Cuándo producir?"
],
correcta: 2,
explicacion: "Los subsidios para acceso equitativo responden a la pregunta PARA QUIÉN producir o distribuir.",
categoria: 'para_quien'
}
];
export function ProblemaEconomicoFundamental({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) {
const [preguntaActual, setPreguntaActual] = useState(0);
const [respuestas, setRespuestas] = useState<number[]>([]);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [completado, setCompletado] = useState(false);
const pregunta = PREGUNTAS[preguntaActual];
const esUltima = preguntaActual === PREGUNTAS.length - 1;
const progreso = ((preguntaActual) / PREGUNTAS.length) * 100;
const handleRespuesta = (index: number) => {
const nuevasRespuestas = [...respuestas, index];
setRespuestas(nuevasRespuestas);
if (esUltima) {
const correctas = nuevasRespuestas.filter((r, i) => r === PREGUNTAS[i].correcta).length;
const puntuacion = Math.round((correctas / PREGUNTAS.length) * 100);
setCompletado(true);
onComplete?.(puntuacion);
} else {
setMostrarResultado(true);
}
};
const handleSiguiente = () => {
setPreguntaActual(prev => prev + 1);
setMostrarResultado(false);
};
const esCorrecta = respuestas[preguntaActual] === pregunta.correcta;
const getCategoriaLabel = (cat: string) => {
switch (cat) {
case 'que': return '¿Qué producir?';
case 'como': return '¿Cómo producir?';
case 'para_quien': return '¿Para quién producir?';
default: return '';
}
};
const getCategoriaColor = (cat: string) => {
switch (cat) {
case 'que': return 'bg-blue-100 text-blue-800';
case 'como': return 'bg-green-100 text-green-800';
case 'para_quien': return 'bg-purple-100 text-purple-800';
default: return 'bg-gray-100 text-gray-800';
}
};
if (completado) {
const correctas = respuestas.filter((r, i) => r === PREGUNTAS[i].correcta).length;
const puntuacion = Math.round((correctas / PREGUNTAS.length) * 100);
return (
<Card className="w-full max-w-2xl mx-auto text-center p-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="mb-6"
>
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto">
<CheckCircle className="w-10 h-10 text-green-600" />
</div>
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">¡Ejercicio Completado!</h3>
<p className="text-gray-600 mb-6">
Dominaste las tres preguntas fundamentales de la economía
</p>
<div className="text-5xl font-bold text-blue-600 mb-2">{puntuacion}</div>
<p className="text-gray-500 mb-6">puntos</p>
<div className="bg-gray-50 rounded-xl p-4 text-left">
<p className="text-sm text-gray-600 mb-2">Resumen:</p>
<p className="text-lg font-medium text-gray-900">{correctas} de {PREGUNTAS.length} correctas</p>
</div>
</Card>
);
}
return (
<Card className="w-full max-w-2xl mx-auto">
<div className="p-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getCategoriaColor(pregunta.categoria)}`}>
{getCategoriaLabel(pregunta.categoria)}
</span>
<span className="text-sm text-gray-500">
{preguntaActual + 1} / {PREGUNTAS.length}
</span>
</div>
{/* Progress */}
<div className="mb-6">
<div className="w-full bg-gray-200 rounded-full h-2">
<motion.div
className="bg-blue-500 h-2 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${progreso}%` }}
transition={{ duration: 0.3 }}
/>
</div>
</div>
{/* Pregunta */}
<h3 className="text-xl font-bold text-gray-900 mb-6">{pregunta.pregunta}</h3>
{/* Opciones */}
{!mostrarResultado ? (
<div className="space-y-3">
{pregunta.opciones.map((opcion, index) => (
<motion.button
key={index}
onClick={() => handleRespuesta(index)}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className="w-full p-4 text-left border-2 border-gray-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all flex items-center gap-3"
>
<span className="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center text-sm font-medium text-gray-600">
{String.fromCharCode(65 + index)}
</span>
<span className="font-medium text-gray-700">{opcion}</span>
</motion.button>
))}
</div>
) : (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`p-6 rounded-xl mb-6 ${esCorrecta ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}
>
<div className="flex items-center gap-3 mb-3">
{esCorrecta ? (
<CheckCircle className="w-6 h-6 text-green-600" />
) : (
<XCircle className="w-6 h-6 text-red-600" />
)}
<span className={`font-bold ${esCorrecta ? 'text-green-800' : 'text-red-800'}`}>
{esCorrecta ? '¡Correcto!' : 'Incorrecto'}
</span>
</div>
<p className="text-gray-700 mb-4">{pregunta.explicacion}</p>
<Button onClick={handleSiguiente} className="flex items-center gap-2">
{esUltima ? 'Finalizar' : 'Siguiente'}
<ArrowRight className="w-4 h-4" />
</Button>
</motion.div>
)}
</div>
</Card>
);
}
export default ProblemaEconomicoFundamental;

View File

@@ -0,0 +1,220 @@
import { useState } from 'react';
interface ProductividadCalculatorProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Escenario {
id: number;
nombre: string;
trabajadores: number;
output: number;
productividadMarginal?: number;
}
const escenariosIniciales: Escenario[] = [
{ id: 1, nombre: 'Fábrica A', trabajadores: 10, output: 500 },
{ id: 2, nombre: 'Fábrica B', trabajadores: 20, output: 900 },
{ id: 3, nombre: 'Fábrica C', trabajadores: 30, output: 1200 },
];
export function ProductividadCalculator({ ejercicioId: _ejercicioId, onComplete }: ProductividadCalculatorProps) {
const [escenarios, setEscenarios] = useState<Escenario[]>(escenariosIniciales);
const [respuestas, setRespuestas] = useState<{[key: number]: {media: string; marginal: string}}>({});
const [validados, setValidados] = useState<{[key: number]: boolean}>({});
const [completado, setCompletado] = useState(false);
const calcularProductividadMedia = (trabajadores: number, output: number): number => {
return Number((output / trabajadores).toFixed(2));
};
const calcularRespuestasCorrectas = (): number => {
let correctas = 0;
escenarios.forEach((escenario, index) => {
if (index === 0) return;
const prodMediaCorrecta = calcularProductividadMedia(escenario.trabajadores, escenario.output);
const prodMarginalCorrecta = (escenario.output - escenarios[index - 1].output) /
(escenario.trabajadores - escenarios[index - 1].trabajadores);
const respuesta = respuestas[escenario.id];
if (respuesta) {
if (Math.abs(Number(respuesta.media) - prodMediaCorrecta) < 0.5) correctas++;
if (Math.abs(Number(respuesta.marginal) - prodMarginalCorrecta) < 0.5) correctas++;
}
});
return correctas;
};
const handleValidar = () => {
const nuevosValidados: {[key: number]: boolean} = {};
let todasCorrectas = true;
escenarios.forEach((escenario, index) => {
if (index === 0) {
nuevosValidados[escenario.id] = true;
return;
}
const respuesta = respuestas[escenario.id];
if (!respuesta || !respuesta.media || !respuesta.marginal) {
todasCorrectas = false;
nuevosValidados[escenario.id] = false;
return;
}
const prodMediaCorrecta = calcularProductividadMedia(escenario.trabajadores, escenario.output);
const prodMarginalCorrecta = (escenario.output - escenarios[index - 1].output) /
(escenario.trabajadores - escenarios[index - 1].trabajadores);
const mediaCorrecta = Math.abs(Number(respuesta.media) - prodMediaCorrecta) < 0.5;
const marginalCorrecta = Math.abs(Number(respuesta.marginal) - prodMarginalCorrecta) < 0.5;
nuevosValidados[escenario.id] = mediaCorrecta && marginalCorrecta;
if (!mediaCorrecta || !marginalCorrecta) todasCorrectas = false;
});
setValidados(nuevosValidados);
if (todasCorrectas && !completado) {
setCompletado(true);
if (onComplete) {
onComplete(100);
}
}
};
const handleReset = () => {
setRespuestas({});
setValidados({});
setCompletado(false);
};
return (
<div className="w-full max-w-4xl mx-auto bg-white rounded-lg shadow-lg p-6">
<div className="mb-6">
<h3 className="text-xl font-semibold text-gray-900">Calculadora de Productividad</h3>
<p className="text-sm text-gray-500 mt-1">
Calcula la productividad media y marginal para cada escenario.
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h4 className="font-semibold text-blue-900 mb-2">Fórmulas:</h4>
<ul className="text-sm text-blue-800 space-y-1">
<li><strong>Productividad Media:</strong> Output ÷ Número de trabajadores</li>
<li><strong>Productividad Marginal:</strong> ΔOutput ÷ ΔTrabajadores</li>
</ul>
</div>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-300 px-4 py-3 text-left">Escenario</th>
<th className="border border-gray-300 px-4 py-3 text-center">Trabajadores</th>
<th className="border border-gray-300 px-4 py-3 text-center">Output (unidades)</th>
<th className="border border-gray-300 px-4 py-3 text-center">Productividad Media</th>
<th className="border border-gray-300 px-4 py-3 text-center">Productividad Marginal</th>
<th className="border border-gray-300 px-4 py-3 text-center">Estado</th>
</tr>
</thead>
<tbody>
{escenarios.map((escenario, index) => (
<tr key={escenario.id} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className="border border-gray-300 px-4 py-3 font-medium">{escenario.nombre}</td>
<td className="border border-gray-300 px-4 py-3 text-center">{escenario.trabajadores}</td>
<td className="border border-gray-300 px-4 py-3 text-center">{escenario.output}</td>
<td className="border border-gray-300 px-4 py-3 text-center">
{index === 0 ? (
<span className="font-semibold text-gray-700">
{calcularProductividadMedia(escenario.trabajadores, escenario.output)}
</span>
) : (
<input
type="number"
step="0.1"
value={respuestas[escenario.id]?.media || ''}
onChange={(e) => setRespuestas(prev => ({
...prev,
[escenario.id]: { ...prev[escenario.id], media: e.target.value }
}))}
className={`w-24 px-2 py-1 text-center border rounded ${
validados[escenario.id] === true
? 'border-green-500 bg-green-50'
: validados[escenario.id] === false
? 'border-red-500 bg-red-50'
: 'border-gray-300'
}`}
placeholder="?"
/>
)}
</td>
<td className="border border-gray-300 px-4 py-3 text-center">
{index === 0 ? (
<span className="text-gray-400">-</span>
) : (
<input
type="number"
step="0.1"
value={respuestas[escenario.id]?.marginal || ''}
onChange={(e) => setRespuestas(prev => ({
...prev,
[escenario.id]: { ...prev[escenario.id], marginal: e.target.value }
}))}
className={`w-24 px-2 py-1 text-center border rounded ${
validados[escenario.id] === true
? 'border-green-500 bg-green-50'
: validados[escenario.id] === false
? 'border-red-500 bg-red-50'
: 'border-gray-300'
}`}
placeholder="?"
/>
)}
</td>
<td className="border border-gray-300 px-4 py-3 text-center">
{validados[escenario.id] === true && (
<span className="text-green-600 font-semibold"> Correcto</span>
)}
{validados[escenario.id] === false && (
<span className="text-red-600 font-semibold"> Revisar</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-6 flex gap-3">
<button
onClick={handleValidar}
disabled={completado}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition-colors"
>
Validar Respuestas
</button>
<button
onClick={handleReset}
className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Reiniciar
</button>
</div>
{completado && (
<div className="mt-6 bg-green-100 border border-green-300 rounded-lg p-4 text-center">
<p className="text-green-800 font-semibold">¡Excelente trabajo!</p>
<p className="text-green-700 text-2xl font-bold mt-1">100 puntos</p>
<p className="text-green-700 text-sm mt-2">
Has calculado correctamente todas las productividades.
</p>
</div>
)}
</div>
);
}
export default ProductividadCalculator;

View File

@@ -0,0 +1,356 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, Lightbulb, ArrowRight, TrendingUp, Users, DollarSign, Scale } from 'lucide-react';
interface EjercicioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Escenario {
id: number;
titulo: string;
descripcion: string;
pregunta: string;
opciones: {
texto: string;
correcta: boolean;
explicacion: string;
}[];
icono: 'trending' | 'users' | 'dollar' | 'scale';
}
const ESCENARIOS: Escenario[] = [
{
id: 1,
titulo: "Costo de Oportunidad",
descripcion: "María tiene $100 y está decidiendo entre comprar un libro de economía o ir al cine con amigos.",
pregunta: "¿Cuál es el costo de oportunidad de elegir el libro?",
opciones: [
{
texto: "Los $100 que gasta en el libro",
correcta: false,
explicacion: "El dinero gastado es el costo explícito, no el costo de oportunidad."
},
{
texto: "El disfrute y experiencia de ir al cine con amigos",
correcta: true,
explicacion: "¡Correcto! El costo de oportunidad es lo que sacrificas: la experiencia del cine que dejas de tener."
},
{
texto: "El tiempo que le toma leer el libro",
correcta: false,
explicacion: "Aunque el tiempo es un recurso, el costo de oportunidad específico es la alternativa forgada (el cine)."
},
{
texto: "El valor del libro mismo",
correcta: false,
explicacion: "El valor del libro es el beneficio de la elección, no el costo de la alternativa."
}
],
icono: 'scale'
},
{
id: 2,
titulo: "Incentivos",
descripcion: "Un gobierno aumenta los impuestos a los cigarrillos para reducir el consumo tabáquico.",
pregunta: "¿Qué principio económico se está aplicando?",
opciones: [
{
texto: "Las personas enfrentan disyuntivas",
correcta: false,
explicacion: "Aunque las disyuntivas existen, no es el principio principal aquí."
},
{
texto: "El costo de algo es lo que sacrificas",
correcta: false,
explicacion: "No es el principio más relevante en este caso."
},
{
texto: "Los incentivos afectan el comportamiento",
correcta: true,
explicacion: "¡Correcto! Al aumentar el costo (precio), se crea un incentivo para fumar menos."
},
{
texto: "El comercio puede mejorar el bienestar",
correcta: false,
explicacion: "Este principio no aplica directamente a esta situación."
}
],
icono: 'trending'
},
{
id: 3,
titulo: "Racionalidad Económica",
descripcion: "Una empresa decide invertir en maquinaria nueva que aumentará la producción en 50%.",
pregunta: "¿Qué supuesto sobre la racionalidad económica se está haciendo?",
opciones: [
{
texto: "Que la empresa busca maximizar beneficios",
correcta: true,
explicacion: "¡Correcto! Se asume que la empresa actúa racionalmente para maximizar sus ganancias."
},
{
texto: "Que la empresa quiere ayudar a la comunidad",
correcta: false,
explicacion: "Aunque podría ser cierto, la racionalidad económica supone maximización de beneficios."
},
{
texto: "Que la empresa no tiene otras opciones",
correcta: false,
explicacion: "La racionalidad económica implica elegir la mejor opción entre alternativas."
},
{
texto: "Que la empresa actúa por emociones",
correcta: false,
explicacion: "La racionalidad económica asume decisiones basadas en cálculo, no emociones."
}
],
icono: 'dollar'
},
{
id: 4,
titulo: "Marginalismo",
descripcion: "Un estudiante está estudiando para un examen. Ya lleva 6 horas estudiando.",
pregunta: "¿Qué análisis debería hacer para decidir si estudia una hora más?",
opciones: [
{
texto: "Calcular el promedio de todas sus calificaciones",
correcta: false,
explicacion: "El análisis promedio no ayuda en decisiones de una hora adicional."
},
{
texto: "Comparar el beneficio adicional vs el costo de una hora más",
correcta: true,
explicacion: "¡Correcto! El análisis marginal compara beneficios y costos adicionales."
},
{
texto: "Preguntarle a sus compañeros cuánto estudiaron",
correcta: false,
explicacion: "Las decisiones de otros no determinan tu análisis marginal óptimo."
},
{
texto: "Ver cuánto tiempo ha estudiado en total",
correcta: false,
explicacion: "El tiempo acumulado no es relevante para la decisión marginal."
}
],
icono: 'users'
},
{
id: 5,
titulo: "Eficiencia vs. Equidad",
descripcion: "Un país puede distribuir la riqueza de forma igualitaria (todos ganan lo mismo) o por productividad (los más productivos ganan más).",
pregunta: "¿Qué principio económico ilustra esta disyuntiva?",
opciones: [
{
texto: "La especialización mejora la productividad",
correcta: false,
explicacion: "La especialización es otro principio diferente."
},
{
texto: "Los mercados son generalmente eficientes",
correcta: false,
explicacion: "Aunque relacionado, no captura la tensión entre eficiencia y equidad."
},
{
texto: "Existe una disyuntiva entre eficiencia y equidad",
correcta: true,
explicacion: "¡Correcto! La distribución igualitaria (equidad) puede reducir incentivos (eficiencia)."
},
{
texto: "El comercio internacional beneficia a todos",
correcta: false,
explicacion: "Este principio no aplica a la distribución interna de riqueza."
}
],
icono: 'scale'
}
];
export function RazonamientoEconomico({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) {
const [escenarioActual, setEscenarioActual] = useState(0);
const [respuestas, setRespuestas] = useState<{escenarioId: number, correcta: boolean}[]>([]);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [completado, setCompletado] = useState(false);
const escenario = ESCENARIOS[escenarioActual];
const esUltima = escenarioActual === ESCENARIOS.length - 1;
const progreso = (escenarioActual / ESCENARIOS.length) * 100;
const getIcono = (tipo: string) => {
switch (tipo) {
case 'trending': return <TrendingUp className="w-6 h-6" />;
case 'users': return <Users className="w-6 h-6" />;
case 'dollar': return <DollarSign className="w-6 h-6" />;
case 'scale': return <Scale className="w-6 h-6" />;
default: return <Lightbulb className="w-6 h-6" />;
}
};
const handleRespuesta = (index: number) => {
const esCorrecta = escenario.opciones[index].correcta;
const nuevasRespuestas = [...respuestas, { escenarioId: escenario.id, correcta: esCorrecta }];
setRespuestas(nuevasRespuestas);
if (esUltima) {
const correctas = nuevasRespuestas.filter(r => r.correcta).length;
const puntuacion = Math.round((correctas / ESCENARIOS.length) * 100);
setCompletado(true);
onComplete?.(puntuacion);
} else {
setMostrarResultado(true);
}
};
const handleSiguiente = () => {
setEscenarioActual(prev => prev + 1);
setMostrarResultado(false);
};
const respuestaActual = respuestas[respuestas.length - 1];
if (completado) {
const correctas = respuestas.filter(r => r.correcta).length;
const puntuacion = Math.round((correctas / ESCENARIOS.length) * 100);
return (
<Card className="w-full max-w-2xl mx-auto text-center p-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="mb-6"
>
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto">
<Lightbulb className="w-10 h-10 text-green-600" />
</div>
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">¡Razonamiento Completado!</h3>
<p className="text-gray-600 mb-6">
Has aplicado principios clave del pensamiento económico
</p>
<div className="text-5xl font-bold text-blue-600 mb-2">{puntuacion}</div>
<p className="text-gray-500 mb-6">puntos</p>
<div className="bg-gray-50 rounded-xl p-4 text-left">
<p className="text-sm text-gray-600 mb-2">Conceptos evaluados:</p>
<div className="flex flex-wrap gap-2">
{ESCENARIOS.map((e, i) => (
<span
key={e.id}
className={`px-2 py-1 rounded text-xs ${respuestas[i]?.correcta ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}
>
{e.titulo}
</span>
))}
</div>
</div>
</Card>
);
}
return (
<Card className="w-full max-w-2xl mx-auto">
<div className="p-6">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center text-blue-600">
{getIcono(escenario.icono)}
</div>
<div>
<h3 className="font-bold text-gray-900">{escenario.titulo}</h3>
<p className="text-xs text-gray-500">Caso {escenarioActual + 1} de {ESCENARIOS.length}</p>
</div>
</div>
</div>
{/* Progress */}
<div className="mb-6">
<div className="w-full bg-gray-200 rounded-full h-2">
<motion.div
className="bg-blue-500 h-2 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${progreso}%` }}
transition={{ duration: 0.3 }}
/>
</div>
</div>
{!mostrarResultado ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
{/* Escenario */}
<div className="bg-blue-50 rounded-xl p-5 mb-6">
<p className="text-gray-800 leading-relaxed">{escenario.descripcion}</p>
</div>
{/* Pregunta */}
<h4 className="text-lg font-semibold text-gray-900 mb-4">{escenario.pregunta}</h4>
{/* Opciones */}
<div className="space-y-3">
{escenario.opciones.map((opcion, index) => (
<motion.button
key={index}
onClick={() => handleRespuesta(index)}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className="w-full p-4 text-left border-2 border-gray-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all"
>
<div className="flex items-start gap-3">
<span className="w-6 h-6 rounded-full bg-gray-100 flex items-center justify-center text-xs font-medium text-gray-600 flex-shrink-0 mt-0.5">
{String.fromCharCode(65 + index)}
</span>
<span className="text-gray-700">{opcion.texto}</span>
</div>
</motion.button>
))}
</div>
</motion.div>
) : (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`p-6 rounded-xl ${respuestaActual?.correcta ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}
>
<div className="flex items-center gap-3 mb-4">
{respuestaActual?.correcta ? (
<CheckCircle className="w-6 h-6 text-green-600" />
) : (
<XCircle className="w-6 h-6 text-red-600" />
)}
<span className={`font-bold ${respuestaActual?.correcta ? 'text-green-800' : 'text-red-800'}`}>
{respuestaActual?.correcta ? '¡Excelente razonamiento!' : 'No es correcto'}
</span>
</div>
<div className="mb-4">
<p className="text-sm text-gray-600 mb-1">Respuesta correcta:</p>
<p className="font-medium text-gray-900">
{escenario.opciones.find(o => o.correcta)?.texto}
</p>
</div>
<p className="text-gray-700 mb-4">
{escenario.opciones.find(o => o.correcta)?.explicacion}
</p>
<Button onClick={handleSiguiente} className="flex items-center gap-2">
{esUltima ? 'Finalizar' : 'Siguiente caso'}
<ArrowRight className="w-4 h-4" />
</Button>
</motion.div>
)}
</div>
</Card>
);
}
export default RazonamientoEconomico;

View File

@@ -0,0 +1,63 @@
import { MatchingExercise } from '../common/MatchingExercise';
const ROLES_AGENTES = {
leftItems: [
{ id: 'consumidor', content: 'Consumidor' },
{ id: 'productor', content: 'Productor' },
{ id: 'propietario-factores', content: 'Propietario de factores' },
{ id: 'demandante-bienes', content: 'Demandante de bienes y servicios' },
{ id: 'ofertante-factores', content: 'Ofertante de factores de producción' },
{ id: 'generador-ingresos', content: 'Generador de rentas y salarios' },
],
rightItems: [
{ id: 'familia', content: 'Familias' },
{ id: 'empresa', content: 'Empresas' },
{ id: 'ambos', content: 'Ambos agentes' },
],
correctPairs: [
{ leftId: 'consumidor', rightId: 'familia' },
{ leftId: 'productor', rightId: 'empresa' },
{ leftId: 'propietario-factores', rightId: 'familia' },
{ leftId: 'demandante-bienes', rightId: 'familia' },
{ leftId: 'ofertante-factores', rightId: 'familia' },
{ leftId: 'generador-ingresos', rightId: 'ambos' },
],
};
interface RolesAgentesMatchingProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function RolesAgentesMatching({
ejercicioId: _ejercicioId,
onComplete,
}: RolesAgentesMatchingProps) {
const handleComplete = (result: {
correct: number;
total: number;
attempts: number;
score: number;
maxScore: number;
isPerfect: boolean;
}) => {
if (onComplete) {
onComplete(result.score);
}
};
return (
<MatchingExercise
title="Roles y Agentes Económicos"
description="Relaciona cada rol económico con el agente correspondiente. Las familias y empresas desempeñan diferentes funciones en la economía."
leftItems={ROLES_AGENTES.leftItems}
rightItems={ROLES_AGENTES.rightItems}
correctPairs={ROLES_AGENTES.correctPairs}
maxPoints={100}
shuffleItems={true}
onComplete={handleComplete}
/>
);
}
export default RolesAgentesMatching;

View File

@@ -0,0 +1,229 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle } from 'lucide-react';
interface EjercicioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
const PREGUNTAS = [
{
id: 1,
pregunta: "¿En qué sistema económico el Estado controla los medios de producción y la distribución de bienes?",
opciones: [
"Economía de mercado",
"Economía planificada o centralizada",
"Economía mixta",
"Economía tradicional"
],
correcta: 1,
explicacion: "En la economía planificada o centralizada, el Estado o gobierno controla todos los medios de producción y decide qué producir, cómo producirlo y para quién."
},
{
id: 2,
pregunta: "¿Cuál es la característica principal de una economía de mercado?",
opciones: [
"El gobierno decide todos los precios",
"Las decisiones económicas se toman por la oferta y la demanda",
"No existe propiedad privada",
"La producción se basa en costumbres ancestrales"
],
correcta: 1,
explicacion: "En la economía de mercado, las decisiones económicas se determinan por la libre interacción de oferta y demanda, sin intervención estatal directa."
},
{
id: 3,
pregunta: "¿Qué sistema económico combina elementos del mercado con intervención estatal?",
opciones: [
"Economía de mercado pura",
"Economía planificada",
"Economía mixta",
"Economía cerrada"
],
correcta: 2,
explicacion: "La economía mixta combina el funcionamiento del mercado con intervención estatal en sectores clave para corregir fallos de mercado y garantizar el bienestar social."
},
{
id: 4,
pregunta: "En una economía planificada, ¿quién decide qué bienes se producen?",
opciones: [
"Los consumidores mediante sus compras",
"Las empresas privadas",
"El gobierno o planificadores centrales",
"Los sindicatos"
],
correcta: 2,
explicacion: "En la economía planificada, son los planificadores gubernamentales quienes determinan qué producir, en qué cantidad y a qué precio."
},
{
id: 5,
pregunta: "¿Qué ventaja principal tiene la economía de mercado sobre la planificada?",
opciones: [
"Mayor equidad en la distribución",
"Mayor eficiencia y respuesta a las preferencias de los consumidores",
"Eliminación de la competencia",
"Control total de la inflación"
],
correcta: 1,
explicacion: "La economía de mercado tiende a ser más eficiente asignando recursos y responde mejor a las preferencias de los consumidores a través del mecanismo de precios."
},
{
id: 6,
pregunta: "¿Qué problema suele presentar la economía planificada?",
opciones: [
"Exceso de bienes de lujo",
"Ineficiencia y escasez por falta de incentivos",
"Alta desigualdad entre ricos y pobres",
"Inestabilidad cambiaria"
],
correcta: 1,
explicacion: "La economía planificada suele sufrir de ineficiencias porque carece de los incentivos del mercado y la información descentralizada que guía la economía de mercado."
},
{
id: 7,
pregunta: "¿Cuál es el rol del Estado en una economía mixta?",
opciones: [
"No interviene en absoluto",
"Controla toda la producción",
"Regula, corrige fallos de mercado y provee bienes públicos",
"Solo recauda impuestos"
],
correcta: 2,
explicacion: "En la economía mixta, el Estado regula el mercado, corrige fallos de mercado, proporciona bienes públicos y protege a los consumidores."
},
{
id: 8,
pregunta: "La propiedad privada de los medios de producción es característica de:",
opciones: [
"Solo economía de mercado",
"Solo economía mixta",
"Economía de mercado y economía mixta",
"Economía planificada"
],
correcta: 2,
explicacion: "Tanto la economía de mercado como la mixta permiten la propiedad privada, a diferencia de la economía planificada donde los medios de producción son estatales."
}
];
export function SistemasEconomicosQuiz({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) {
const [preguntaActual, setPreguntaActual] = useState(0);
const [respuestas, setRespuestas] = useState<number[]>([]);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [completado, setCompletado] = useState(false);
const pregunta = PREGUNTAS[preguntaActual];
const esUltima = preguntaActual === PREGUNTAS.length - 1;
const handleRespuesta = (index: number) => {
const nuevasRespuestas = [...respuestas, index];
setRespuestas(nuevasRespuestas);
if (esUltima) {
const correctas = nuevasRespuestas.filter((r, i) => r === PREGUNTAS[i].correcta).length;
const puntuacion = Math.round((correctas / PREGUNTAS.length) * 100);
setCompletado(true);
onComplete?.(puntuacion);
} else {
setMostrarResultado(true);
}
};
const handleSiguiente = () => {
setPreguntaActual(prev => prev + 1);
setMostrarResultado(false);
};
const esCorrecta = respuestas[preguntaActual] === pregunta.correcta;
if (completado) {
const correctas = respuestas.filter((r, i) => r === PREGUNTAS[i].correcta).length;
const puntuacion = Math.round((correctas / PREGUNTAS.length) * 100);
return (
<Card className="w-full max-w-2xl mx-auto text-center p-8">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="mb-6"
>
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto">
<CheckCircle className="w-10 h-10 text-green-600" />
</div>
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">¡Quiz Completado!</h3>
<p className="text-gray-600 mb-6">
Respondiste {correctas} de {PREGUNTAS.length} preguntas correctamente
</p>
<div className="text-5xl font-bold text-blue-600 mb-2">{puntuacion}</div>
<p className="text-gray-500">puntos</p>
</Card>
);
}
return (
<Card className="w-full max-w-2xl mx-auto">
<div className="p-6">
<div className="mb-6">
<div className="flex justify-between text-sm text-gray-500 mb-2">
<span>Pregunta {preguntaActual + 1} de {PREGUNTAS.length}</span>
<span>{Math.round(((preguntaActual) / PREGUNTAS.length) * 100)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<motion.div
className="bg-blue-500 h-2 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${((preguntaActual) / PREGUNTAS.length) * 100}%` }}
/>
</div>
</div>
<h3 className="text-xl font-bold text-gray-900 mb-6">{pregunta.pregunta}</h3>
{!mostrarResultado ? (
<div className="space-y-3">
{pregunta.opciones.map((opcion, index) => (
<motion.button
key={index}
onClick={() => handleRespuesta(index)}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className="w-full p-4 text-left border-2 border-gray-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all"
>
<span className="font-medium text-gray-700">{String.fromCharCode(65 + index)}. {opcion}</span>
</motion.button>
))}
</div>
) : (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`p-6 rounded-xl mb-6 ${esCorrecta ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}
>
<div className="flex items-center gap-3 mb-3">
{esCorrecta ? (
<CheckCircle className="w-6 h-6 text-green-600" />
) : (
<XCircle className="w-6 h-6 text-red-600" />
)}
<span className={`font-bold ${esCorrecta ? 'text-green-800' : 'text-red-800'}`}>
{esCorrecta ? '¡Correcto!' : 'Incorrecto'}
</span>
</div>
<p className="text-gray-700">{pregunta.explicacion}</p>
<Button onClick={handleSiguiente} className="mt-4">
{esUltima ? 'Finalizar' : 'Siguiente'}
</Button>
</motion.div>
)}
</div>
</Card>
);
}
export default SistemasEconomicosQuiz;

View File

@@ -0,0 +1,359 @@
import { useState } from 'react';
interface VentajaComparativaCalculatorProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Pais {
nombre: string;
vino: number;
queso: number;
}
interface Respuestas {
ventajaAbsolutaVino: string;
ventajaAbsolutaQueso: string;
costoOportunidadPaisA: string;
costoOportunidadPaisB: string;
ventajaComparativaVino: string;
ventajaComparativaQueso: string;
}
const paises: Pais[] = [
{ nombre: 'País A', vino: 100, queso: 200 },
{ nombre: 'País B', vino: 80, queso: 120 },
];
const opcionesPais = ['País A', 'País B', 'Ninguno (igual producción)'];
export function VentajaComparativaCalculator({ ejercicioId: _ejercicioId, onComplete }: VentajaComparativaCalculatorProps) {
const [respuestas, setRespuestas] = useState<Partial<Respuestas>>({});
const [validados, setValidados] = useState<{[key: string]: boolean | null}>({});
const [completado, setCompletado] = useState(false);
const calcularCostoOportunidad = (pais: Pais, bien: 'vino' | 'queso'): number => {
if (bien === 'vino') {
return pais.queso / pais.vino;
}
return pais.vino / pais.queso;
};
const handleRespuestaChange = (campo: keyof Respuestas, valor: string) => {
setRespuestas(prev => ({ ...prev, [campo]: valor }));
setValidados(prev => ({ ...prev, [campo]: null }));
};
const handleValidar = () => {
const nuevosValidados: {[key: string]: boolean | null} = {};
const correctas: {[key: string]: string} = {
ventajaAbsolutaVino: paises[0].vino > paises[1].vino ? 'País A' : 'País B',
ventajaAbsolutaQueso: paises[0].queso > paises[1].queso ? 'País A' : 'País B',
costoOportunidadPaisA: `${calcularCostoOportunidad(paises[0], 'vino').toFixed(2)}`,
costoOportunidadPaisB: `${calcularCostoOportunidad(paises[1], 'vino').toFixed(2)}`,
};
const costoA = calcularCostoOportunidad(paises[0], 'vino');
const costoB = calcularCostoOportunidad(paises[1], 'vino');
correctas.ventajaComparativaVino = costoA < costoB ? 'País A' : 'País B';
correctas.ventajaComparativaQueso = costoA < costoB ? 'País B' : 'País A';
let todasCorrectas = true;
Object.keys(correctas).forEach(key => {
const esCorrecta = respuestas[key as keyof Respuestas] === correctas[key];
nuevosValidados[key] = esCorrecta;
if (!esCorrecta) todasCorrectas = false;
});
setValidados(nuevosValidados);
if (todasCorrectas && !completado) {
setCompletado(true);
if (onComplete) {
onComplete(100);
}
}
};
const handleReset = () => {
setRespuestas({});
setValidados({});
setCompletado(false);
};
return (
<div className="w-full max-w-4xl mx-auto bg-white rounded-lg shadow-lg p-6">
<div className="mb-6">
<h3 className="text-xl font-semibold text-gray-900">Calculadora de Ventaja Comparativa</h3>
<p className="text-sm text-gray-500 mt-1">
Analiza la producción de dos países para determinar ventajas absolutas y comparativas.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="font-semibold text-gray-900 mb-3">Tabla de Producción</h4>
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-200">
<th className="border border-gray-300 px-3 py-2 text-left">País</th>
<th className="border border-gray-300 px-3 py-2 text-center">Vino (barriles)</th>
<th className="border border-gray-300 px-3 py-2 text-center">Queso (kg)</th>
</tr>
</thead>
<tbody>
{paises.map((pais, idx) => (
<tr key={pais.nombre} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className="border border-gray-300 px-3 py-2 font-medium">{pais.nombre}</td>
<td className="border border-gray-300 px-3 py-2 text-center">{pais.vino}</td>
<td className="border border-gray-300 px-3 py-2 text-center">{pais.queso}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-semibold text-blue-900 mb-2">Guía:</h4>
<ul className="text-sm text-blue-800 space-y-2">
<li>
<strong>Ventaja Absoluta:</strong> Quien produce más de un bien con los mismos recursos.
</li>
<li>
<strong>Costo de Oportunidad:</strong>
<br/> Vino: Queso sacrificado ÷ Vino producido
<br/> Queso: Vino sacrificado ÷ Queso producido
</li>
<li>
<strong>Ventaja Comparativa:</strong> Quien tiene el menor costo de oportunidad.
</li>
</ul>
</div>
</div>
<div className="space-y-6">
<div className="border border-gray-200 rounded-lg p-5">
<h4 className="font-semibold text-gray-900 mb-4">1. Ventaja Absoluta</h4>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
¿Quién tiene ventaja absoluta en la producción de vino?
</label>
<div className="flex flex-wrap gap-2">
{opcionesPais.map(opcion => {
const isSelected = respuestas.ventajaAbsolutaVino === opcion;
const estado = validados.ventajaAbsolutaVino;
let className = 'px-4 py-2 rounded-lg text-sm border transition-all ';
if (estado === true) {
className += isSelected ? 'bg-green-100 border-green-500 text-green-800' : 'bg-gray-100 border-gray-200 text-gray-400';
} else if (estado === false && isSelected) {
className += 'bg-red-100 border-red-500 text-red-800';
} else {
className += isSelected ? 'bg-blue-100 border-blue-500 text-blue-800' : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50';
}
return (
<button
key={opcion}
onClick={() => handleRespuestaChange('ventajaAbsolutaVino', opcion)}
className={className}
>
{opcion}
{estado === true && isSelected && ' ✓'}
</button>
);
})}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
¿Quién tiene ventaja absoluta en la producción de queso?
</label>
<div className="flex flex-wrap gap-2">
{opcionesPais.map(opcion => {
const isSelected = respuestas.ventajaAbsolutaQueso === opcion;
const estado = validados.ventajaAbsolutaQueso;
let className = 'px-4 py-2 rounded-lg text-sm border transition-all ';
if (estado === true) {
className += isSelected ? 'bg-green-100 border-green-500 text-green-800' : 'bg-gray-100 border-gray-200 text-gray-400';
} else if (estado === false && isSelected) {
className += 'bg-red-100 border-red-500 text-red-800';
} else {
className += isSelected ? 'bg-blue-100 border-blue-500 text-blue-800' : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50';
}
return (
<button
key={opcion}
onClick={() => handleRespuestaChange('ventajaAbsolutaQueso', opcion)}
className={className}
>
{opcion}
{estado === true && isSelected && ' ✓'}
</button>
);
})}
</div>
</div>
</div>
</div>
<div className="border border-gray-200 rounded-lg p-5">
<h4 className="font-semibold text-gray-900 mb-4">2. Costo de Oportunidad del Vino</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
País A: 1 barril de vino =
</label>
<div className="flex items-center gap-2">
<input
type="number"
step="0.01"
value={respuestas.costoOportunidadPaisA || ''}
onChange={(e) => handleRespuestaChange('costoOportunidadPaisA', e.target.value)}
className={`w-24 px-3 py-2 border rounded-lg text-center ${
validados.costoOportunidadPaisA === true
? 'border-green-500 bg-green-50'
: validados.costoOportunidadPaisA === false
? 'border-red-500 bg-red-50'
: 'border-gray-300'
}`}
placeholder="?"
/>
<span className="text-gray-600">kg de queso</span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
País B: 1 barril de vino =
</label>
<div className="flex items-center gap-2">
<input
type="number"
step="0.01"
value={respuestas.costoOportunidadPaisB || ''}
onChange={(e) => handleRespuestaChange('costoOportunidadPaisB', e.target.value)}
className={`w-24 px-3 py-2 border rounded-lg text-center ${
validados.costoOportunidadPaisB === true
? 'border-green-500 bg-green-50'
: validados.costoOportunidadPaisB === false
? 'border-red-500 bg-red-50'
: 'border-gray-300'
}`}
placeholder="?"
/>
<span className="text-gray-600">kg de queso</span>
</div>
</div>
</div>
</div>
<div className="border border-gray-200 rounded-lg p-5">
<h4 className="font-semibold text-gray-900 mb-4">3. Ventaja Comparativa</h4>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
¿Quién tiene ventaja comparativa en vino? (menor costo de oportunidad)
</label>
<div className="flex flex-wrap gap-2">
{opcionesPais.map(opcion => {
const isSelected = respuestas.ventajaComparativaVino === opcion;
const estado = validados.ventajaComparativaVino;
let className = 'px-4 py-2 rounded-lg text-sm border transition-all ';
if (estado === true) {
className += isSelected ? 'bg-green-100 border-green-500 text-green-800' : 'bg-gray-100 border-gray-200 text-gray-400';
} else if (estado === false && isSelected) {
className += 'bg-red-100 border-red-500 text-red-800';
} else {
className += isSelected ? 'bg-blue-100 border-blue-500 text-blue-800' : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50';
}
return (
<button
key={opcion}
onClick={() => handleRespuestaChange('ventajaComparativaVino', opcion)}
className={className}
>
{opcion}
{estado === true && isSelected && ' ✓'}
</button>
);
})}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
¿Quién tiene ventaja comparativa en queso? (menor costo de oportunidad)
</label>
<div className="flex flex-wrap gap-2">
{opcionesPais.map(opcion => {
const isSelected = respuestas.ventajaComparativaQueso === opcion;
const estado = validados.ventajaComparativaQueso;
let className = 'px-4 py-2 rounded-lg text-sm border transition-all ';
if (estado === true) {
className += isSelected ? 'bg-green-100 border-green-500 text-green-800' : 'bg-gray-100 border-gray-200 text-gray-400';
} else if (estado === false && isSelected) {
className += 'bg-red-100 border-red-500 text-red-800';
} else {
className += isSelected ? 'bg-blue-100 border-blue-500 text-blue-800' : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50';
}
return (
<button
key={opcion}
onClick={() => handleRespuestaChange('ventajaComparativaQueso', opcion)}
className={className}
>
{opcion}
{estado === true && isSelected && ' ✓'}
</button>
);
})}
</div>
</div>
</div>
</div>
</div>
<div className="mt-6 flex gap-3">
<button
onClick={handleValidar}
disabled={completado}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition-colors"
>
Validar Respuestas
</button>
<button
onClick={handleReset}
className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Reiniciar
</button>
</div>
{completado && (
<div className="mt-6 bg-green-100 border border-green-300 rounded-lg p-4 text-center">
<p className="text-green-800 font-semibold">¡Excelente análisis económico!</p>
<p className="text-green-700 text-2xl font-bold mt-1">100 puntos</p>
<p className="text-green-700 text-sm mt-2">
Has identificado correctamente las ventajas absolutas y comparativas.
</p>
</div>
)}
</div>
);
}
export default VentajaComparativaCalculator;

View File

@@ -0,0 +1,546 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../../ui/Card';
import { Button } from '../../ui/Button';
import {
CheckCircle,
XCircle,
RefreshCcw,
Link2,
Trophy,
Scale,
Target,
Zap,
ArrowRight,
Building2,
Users,
Globe,
Landmark,
Coins
} from 'lucide-react';
interface MatchingItem {
id: string;
content: string;
icon?: React.ReactNode;
}
interface MatchingPair {
leftId: string;
rightId: string;
}
interface VentajasDesventajasSistemasProps {
onComplete?: (result: {
correct: number;
total: number;
score: number;
isPerfect: boolean;
}) => void;
}
const SISTEMAS_ECONOMICOS: MatchingItem[] = [
{
id: 'mercado',
content: 'Economía de Mercado',
icon: <Coins size={18} />,
},
{
id: 'planificada',
content: 'Economía Planificada',
icon: <Landmark size={18} />,
},
{
id: 'mixta',
content: 'Economía Mixta',
icon: <Scale size={18} />,
},
];
const CARACTERISTICAS: MatchingItem[] = [
{
id: 'eficiencia',
content: 'Alta eficiencia en la asignación de recursos',
icon: <Zap size={18} />,
},
{
id: 'desigualdad',
content: 'Puede generar grandes desigualdades de ingreso',
icon: <Users size={18} />,
},
{
id: 'planificacion',
content: 'El gobierno controla la producción y distribución',
icon: <Landmark size={18} />,
},
{
id: 'flexibilidad',
content: 'Respuesta rápida a cambios en la demanda',
icon: <ArrowRight size={18} />,
},
{
id: 'equidad',
content: 'Mayor equidad en la distribución de bienes',
icon: <Scale size={18} />,
},
{
id: 'burocracia',
content: 'Alta burocracia y lentitud en decisiones',
icon: <Building2 size={18} />,
},
{
id: 'equilibrio',
content: 'Combina eficiencia con justicia social',
icon: <Globe size={18} />,
},
{
id: 'intervencion',
content: 'El Estado regula y corrige fallas del mercado',
icon: <Target size={18} />,
},
];
const PAREJAS_CORRECTAS: MatchingPair[] = [
{ leftId: 'mercado', rightId: 'eficiencia' },
{ leftId: 'mercado', rightId: 'desigualdad' },
{ leftId: 'mercado', rightId: 'flexibilidad' },
{ leftId: 'planificada', rightId: 'planificacion' },
{ leftId: 'planificada', rightId: 'equidad' },
{ leftId: 'planificada', rightId: 'burocracia' },
{ leftId: 'mixta', rightId: 'equilibrio' },
{ leftId: 'mixta', rightId: 'intervencion' },
];
interface Match {
leftId: string;
rightId: string;
isCorrect?: boolean;
}
export function VentajasDesventajasSistemas({ onComplete }: VentajasDesventajasSistemasProps) {
const [matches, setMatches] = useState<Match[]>([]);
const [selectedLeft, setSelectedLeft] = useState<string | null>(null);
const [selectedRight, setSelectedRight] = useState<string | null>(null);
const [showResults, setShowResults] = useState(false);
const [attempts, setAttempts] = useState(0);
const handleLeftClick = (itemId: string) => {
if (showResults) return;
if (selectedLeft === itemId) {
setSelectedLeft(null);
} else {
setSelectedLeft(itemId);
if (selectedRight) {
handleCreateMatch(itemId, selectedRight);
}
}
};
const handleRightClick = (itemId: string) => {
if (showResults) return;
if (selectedRight === itemId) {
setSelectedRight(null);
} else {
setSelectedRight(itemId);
if (selectedLeft) {
handleCreateMatch(selectedLeft, itemId);
}
}
};
const handleCreateMatch = (leftId: string, rightId: string) => {
const isLeftMatched = matches.some(m => m.leftId === leftId);
const isRightMatched = matches.some(m => m.rightId === rightId);
if (isLeftMatched || isRightMatched) return;
setMatches(prev => [...prev, { leftId, rightId }]);
setSelectedLeft(null);
setSelectedRight(null);
setAttempts(prev => prev + 1);
};
const handleRemoveMatch = (leftId: string) => {
if (showResults) return;
setMatches(prev => prev.filter(m => m.leftId !== leftId));
};
const handleValidate = () => {
const validatedMatches = matches.map(match => {
const isCorrect = PAREJAS_CORRECTAS.some(
p => p.leftId === match.leftId && p.rightId === match.rightId
);
return { ...match, isCorrect };
});
setMatches(validatedMatches);
setShowResults(true);
const correctCount = validatedMatches.filter(m => m.isCorrect).length;
const score = Math.round((correctCount / PAREJAS_CORRECTAS.length) * 100);
if (onComplete) {
onComplete({
correct: correctCount,
total: PAREJAS_CORRECTAS.length,
score,
isPerfect: correctCount === PAREJAS_CORRECTAS.length,
});
}
};
const handleReset = () => {
setMatches([]);
setSelectedLeft(null);
setSelectedRight(null);
setShowResults(false);
setAttempts(0);
};
const isLeftMatched = (id: string) => matches.some(m => m.leftId === id);
const isRightMatched = (id: string) => matches.some(m => m.rightId === id);
const getMatchStatus = (leftId: string): 'correct' | 'incorrect' | null => {
const match = matches.find(m => m.leftId === leftId);
if (!match || !showResults) return null;
return match.isCorrect ? 'correct' : 'incorrect';
};
const getMatchedRightItem = (leftId: string) => {
const match = matches.find(m => m.leftId === leftId);
if (!match) return null;
return CARACTERISTICAS.find(item => item.id === match.rightId);
};
const getMatchedLeftItem = (rightId: string) => {
const match = matches.find(m => m.rightId === rightId);
if (!match) return null;
return SISTEMAS_ECONOMICOS.find(item => item.id === match.leftId);
};
const correctCount = matches.filter(m => m.isCorrect).length;
const allMatched = matches.length === PAREJAS_CORRECTAS.length;
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-start justify-between">
<div>
<h3 className="text-xl font-bold text-gray-900">Sistemas Económicos</h3>
<p className="text-gray-600 mt-1">
Relaciona cada sistema económico con sus características correspondientes
</p>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 text-amber-600 bg-amber-50 px-3 py-1.5 rounded-full">
<Target size={16} />
<span className="font-semibold text-sm">100 pts</span>
</div>
<div className="flex items-center gap-2 text-blue-600 bg-blue-50 px-3 py-1.5 rounded-full">
<Zap size={16} />
<span className="font-semibold text-sm">{attempts} intentos</span>
</div>
</div>
</div>
{/* Instrucciones */}
<Card className="p-4 bg-blue-50 border-blue-200">
<div className="flex items-start gap-3">
<div className="bg-blue-100 p-2 rounded-lg">
<Link2 size={18} className="text-blue-600" />
</div>
<div>
<h4 className="font-semibold text-blue-900 mb-1">¿Cómo jugar?</h4>
<p className="text-sm text-blue-700">
Haz clic en un sistema económico y luego en sus características para emparejarlos.
Cada sistema debe emparejarse con sus ventajas y desventajas específicas.
</p>
</div>
</div>
</Card>
{/* Matching Area */}
<Card className="p-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Sistemas Económicos */}
<div className="space-y-4">
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wider">
Sistemas Económicos
</h4>
<div className="space-y-3">
{SISTEMAS_ECONOMICOS.map(item => {
const matchedItem = getMatchedRightItem(item.id);
const status = getMatchStatus(item.id);
const isSelected = selectedLeft === item.id;
const isMatched = isLeftMatched(item.id);
return (
<motion.div
key={item.id}
onClick={() => handleLeftClick(item.id)}
whileHover={!showResults && !isMatched ? { scale: 1.02 } : {}}
whileTap={!showResults && !isMatched ? { scale: 0.98 } : {}}
className={`
relative p-4 rounded-xl border-2 transition-all cursor-pointer
${!showResults && !isMatched ? 'hover:border-blue-300 hover:shadow-md' : ''}
${isSelected ? 'border-blue-500 bg-blue-50 ring-2 ring-blue-200' : ''}
${isMatched && !showResults ? 'border-indigo-300 bg-indigo-50' : ''}
${status === 'correct' ? 'border-green-500 bg-green-50' : ''}
${status === 'incorrect' ? 'border-red-500 bg-red-50' : ''}
${!isMatched && !isSelected ? 'border-gray-200 bg-white' : ''}
`}
>
<div className="flex items-center gap-3">
<div className={`
p-2 rounded-lg flex-shrink-0
${isSelected ? 'bg-blue-500 text-white' :
status === 'correct' ? 'bg-green-500 text-white' :
status === 'incorrect' ? 'bg-red-500 text-white' :
isMatched ? 'bg-indigo-500 text-white' :
'bg-gray-100 text-gray-600'}
`}>
{item.icon}
</div>
<span className="font-semibold text-gray-800">{item.content}</span>
</div>
{/* Match indicator */}
{matchedItem && (
<div className="mt-3 pt-3 border-t border-gray-200/60">
<div className="flex items-center gap-2 text-sm">
<Link2 size={14} className="text-gray-400" />
<span className="text-gray-600">{matchedItem.content}</span>
</div>
</div>
)}
{/* Status icons */}
{showResults && status && (
<div className="absolute top-3 right-3">
{status === 'correct' ? (
<CheckCircle size={22} className="text-green-600" />
) : (
<XCircle size={22} className="text-red-600" />
)}
</div>
)}
{/* Remove button */}
{isMatched && !showResults && (
<button
onClick={(e) => {
e.stopPropagation();
handleRemoveMatch(item.id);
}}
className="absolute top-3 right-3 p-1.5 hover:bg-gray-200 rounded-full transition-colors"
>
<XCircle size={18} className="text-gray-400 hover:text-red-500" />
</button>
)}
{/* Counter badge */}
{!showResults && (
<div className="absolute -top-2 -right-2">
<span className={`
inline-flex items-center justify-center w-6 h-6 text-xs font-bold rounded-full
${isMatched ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-600'}
`}>
{matches.filter(m => m.leftId === item.id).length}
</span>
</div>
)}
</motion.div>
);
})}
</div>
</div>
{/* Características */}
<div className="space-y-4">
<h4 className="text-sm font-semibold text-gray-500 uppercase tracking-wider">
Características
</h4>
<div className="space-y-2">
{CARACTERISTICAS.map(item => {
const matchedItem = getMatchedLeftItem(item.id);
const isSelected = selectedRight === item.id;
const isMatched = isRightMatched(item.id);
return (
<motion.div
key={item.id}
onClick={() => handleRightClick(item.id)}
whileHover={!showResults && !isMatched ? { scale: 1.02 } : {}}
whileTap={!showResults && !isMatched ? { scale: 0.98 } : {}}
className={`
relative p-3 rounded-xl border-2 transition-all cursor-pointer
${!showResults && !isMatched ? 'hover:border-blue-300 hover:shadow-md' : ''}
${isSelected ? 'border-blue-500 bg-blue-50 ring-2 ring-blue-200' : ''}
${isMatched ? 'border-gray-300 bg-gray-100 opacity-60' : ''}
${!isMatched && !isSelected ? 'border-gray-200 bg-white' : ''}
`}
>
<div className="flex items-center gap-3">
<div className={`
p-1.5 rounded-lg flex-shrink-0
${isSelected ? 'bg-blue-500 text-white' :
isMatched ? 'bg-gray-400 text-white' :
'bg-gray-100 text-gray-500'}
`}>
{item.icon}
</div>
<span className={`font-medium ${isMatched ? 'text-gray-500' : 'text-gray-700'}`}>
{item.content}
</span>
</div>
{/* Match indicator */}
{matchedItem && (
<div className="mt-2 pt-2 border-t border-gray-200/60">
<div className="flex items-center gap-2 text-xs">
<Link2 size={12} className="text-gray-400" />
<span className="text-gray-500">Emparejado con: {matchedItem.content}</span>
</div>
</div>
)}
</motion.div>
);
})}
</div>
</div>
</div>
</Card>
{/* Results Section */}
<AnimatePresence>
{showResults && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
>
<Card className={`
p-6 border-2
${correctCount === PAREJAS_CORRECTAS.length
? 'bg-gradient-to-br from-green-50 to-emerald-50 border-green-200'
: 'bg-gradient-to-br from-blue-50 to-indigo-50 border-blue-200'}
`}>
<div className="text-center">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200, delay: 0.2 }}
className={`
inline-flex items-center justify-center w-20 h-20 rounded-full mb-4
${correctCount === PAREJAS_CORRECTAS.length
? 'bg-gradient-to-br from-yellow-400 to-orange-500'
: 'bg-gradient-to-br from-blue-400 to-indigo-500'}
`}
>
<Trophy size={40} className="text-white" />
</motion.div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">
{correctCount === PAREJAS_CORRECTAS.length
? '¡Excelente!'
: correctCount >= PAREJAS_CORRECTAS.length * 0.7
? '¡Muy bien!'
: '¡Sigue practicando!'}
</h3>
<p className="text-gray-600 mb-6">
{correctCount} de {PAREJAS_CORRECTAS.length} emparejamientos correctos
</p>
{/* Score Display */}
<div className="grid grid-cols-3 gap-4 max-w-lg mx-auto">
<div className="bg-white rounded-xl p-4 shadow-sm">
<Target className="w-6 h-6 text-blue-500 mx-auto mb-2" />
<p className="text-2xl font-bold text-blue-700">
{Math.round((correctCount / PAREJAS_CORRECTAS.length) * 100)}
</p>
<p className="text-sm text-blue-600">Puntuación</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm">
<CheckCircle className="w-6 h-6 text-green-500 mx-auto mb-2" />
<p className="text-2xl font-bold text-green-700">{correctCount}</p>
<p className="text-sm text-green-600">Correctos</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm">
<Zap className="w-6 h-6 text-purple-500 mx-auto mb-2" />
<p className="text-2xl font-bold text-purple-700">{attempts}</p>
<p className="text-sm text-purple-600">Intentos</p>
</div>
</div>
{/* Explicación de respuestas */}
{correctCount < PAREJAS_CORRECTAS.length && (
<div className="mt-6 text-left bg-white rounded-xl p-4">
<h4 className="font-semibold text-gray-800 mb-3">Respuestas correctas:</h4>
<div className="space-y-2 text-sm">
{SISTEMAS_ECONOMICOS.map(sistema => {
const caracteristicas = PAREJAS_CORRECTAS
.filter(p => p.leftId === sistema.id)
.map(p => CARACTERISTICAS.find(c => c.id === p.rightId)?.content);
return (
<div key={sistema.id} className="flex items-start gap-2">
<span className="font-semibold text-gray-700 min-w-[140px]">
{sistema.content}:
</span>
<span className="text-gray-600">
{caracteristicas.join(', ')}
</span>
</div>
);
})}
</div>
</div>
)}
</div>
</Card>
</motion.div>
)}
</AnimatePresence>
{/* Action Buttons */}
<div className="flex justify-between items-center">
<Button variant="outline" onClick={handleReset}>
<RefreshCcw size={16} className="mr-2" />
Reiniciar
</Button>
{!showResults ? (
<Button onClick={handleValidate} disabled={matches.length === 0}>
Verificar Emparejamientos
</Button>
) : (
<Button onClick={handleReset}>
Intentar de Nuevo
</Button>
)}
</div>
{/* Progress indicator */}
<div className="text-center text-sm text-gray-500">
<p>
Progreso: <span className="font-semibold text-blue-600">{matches.length}</span> de{' '}
<span className="font-semibold">{PAREJAS_CORRECTAS.length}</span> emparejamientos
</p>
<div className="w-full max-w-md mx-auto mt-2 bg-gray-200 rounded-full h-2">
<motion.div
className="h-2 bg-blue-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${(matches.length / PAREJAS_CORRECTAS.length) * 100}%` }}
/>
</div>
</div>
</div>
);
}
export default VentajasDesventajasSistemas;

View File

@@ -1,3 +1,23 @@
export { SimuladorDisyuntivas } from './SimuladorDisyuntivas';
export { QuizBienes } from './QuizBienes';
export { FlujoCircular } from './FlujoCircular';
export { DefinicionEconomiaQuiz } from './DefinicionEconomiaQuiz';
export { EscasezSimulator } from './EscasezSimulator';
export { ProblemaEconomicoFundamental } from './ProblemaEconomicoFundamental';
export { EconomiaPositivaVsNormativa } from './EconomiaPositivaVsNormativa';
export { RazonamientoEconomico } from './RazonamientoEconomico';
export { SistemasEconomicosQuiz } from './SistemasEconomicosQuiz';
export { ComparativaSistemas } from './ComparativaSistemas';
export { CasosPaises } from './CasosPaises';
export { VentajasDesventajasSistemas } from './VentajasDesventajasSistemas';
export { FPPConstructor } from './FPPConstructor';
export { FPPAnalizador } from './FPPAnalizador';
export { CostoOportunidadCalculator } from './CostoOportunidadCalculator';
export { CrecimientoEconomicoFPP } from './CrecimientoEconomicoFPP';
export { AgentesEconomicosQuiz } from './AgentesEconomicosQuiz';
export { RolesAgentesMatching } from './RolesAgentesMatching';
export { FlujoCircularBasico } from './FlujoCircularBasico';
export { FactoresProduccionQuiz } from './FactoresProduccionQuiz';
export { ProductividadCalculator } from './ProductividadCalculator';
export { CostoOportunidadCotidiano } from './CostoOportunidadCotidiano';
export { VentajaComparativaCalculator } from './VentajaComparativaCalculator';

View File

@@ -0,0 +1,473 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { RefreshCw, Play, Pause, ArrowRight, CheckCircle2, Trophy, RotateCcw, Info } from 'lucide-react';
interface AjusteEquilibrioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
type EscenarioTipo = 'exceso_demanda' | 'exceso_oferta';
interface Escenario {
id: number;
tipo: EscenarioTipo;
titulo: string;
descripcion: string;
precioInicial: number;
precioEquilibrio: number;
cantidadEquilibrio: number;
mensajeAjuste: string;
}
const escenarios: Escenario[] = [
{
id: 1,
tipo: 'exceso_demanda',
titulo: 'Escasez de Vivienda',
descripcion: 'El precio actual de $600 está por debajo del equilibrio. Hay más personas buscando vivienda que apartamentos disponibles.',
precioInicial: 600,
precioEquilibrio: 900,
cantidadEquilibrio: 300,
mensajeAjuste: 'La escasez presiona al alza: los compradores compiten ofreciendo más, los vendedores suben precios.'
},
{
id: 2,
tipo: 'exceso_oferta',
titulo: 'Superávit de Manzanas',
descripcion: 'La cosecha fue abundante y el precio actual de $80 está por encima del equilibrio. Hay más manzanas de las que la gente quiere comprar.',
precioInicial: 80,
precioEquilibrio: 50,
cantidadEquilibrio: 100,
mensajeAjuste: 'El superávit presiona a la baja: los vendedores compiten bajando precios para liquidar inventario.'
}
];
export const AjusteEquilibrio: React.FC<AjusteEquilibrioProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [escenarioActual, setEscenarioActual] = useState(0);
const [precioActual, setPrecioActual] = useState(escenarios[0].precioInicial);
const [estaAnimando, setEstaAnimando] = useState(false);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [faseAjuste, setFaseAjuste] = useState<'inicio' | 'ajustando' | 'completado'>('inicio');
const [score, setScore] = useState(0);
const [completado, setCompletado] = useState(false);
const [_startTime] = useState(Date.now());
const escenario = escenarios[escenarioActual];
useEffect(() => {
let interval: ReturnType<typeof setInterval>;
if (estaAnimando) {
setFaseAjuste('ajustando');
interval = setInterval(() => {
setPrecioActual(prev => {
const diferencia = escenario.precioEquilibrio - prev;
const cambio = diferencia * 0.05;
if (Math.abs(diferencia) < 2) {
setEstaAnimando(false);
setFaseAjuste('completado');
setMostrarResultado(true);
return escenario.precioEquilibrio;
}
return prev + cambio;
});
}, 100);
}
return () => clearInterval(interval);
}, [estaAnimando, escenario]);
const handleIniciar = () => {
setEstaAnimando(true);
};
const handlePausar = () => {
setEstaAnimando(false);
};
const handleSiguiente = () => {
if (escenarioActual < escenarios.length - 1) {
const nextEscenario = escenarios[escenarioActual + 1];
setEscenarioActual(prev => prev + 1);
setPrecioActual(nextEscenario.precioInicial);
setEstaAnimando(false);
setMostrarResultado(false);
setFaseAjuste('inicio');
setScore(prev => prev + 50);
} else {
setCompletado(true);
if (onComplete) {
onComplete(100);
}
}
};
const handleReiniciar = () => {
setEscenarioActual(0);
setPrecioActual(escenarios[0].precioInicial);
setEstaAnimando(false);
setMostrarResultado(false);
setFaseAjuste('inicio');
setScore(0);
setCompletado(false);
};
const generarCurvas = () => {
const demanda = [];
const oferta = [];
for (let Q = 0; Q <= 400; Q += 20) {
const Pd = 1200 - 1 * Q;
const Po = 0 + 3 * Q;
if (Pd >= 0) demanda.push({ Q, P: Pd });
if (Po >= 0) oferta.push({ Q, P: Po });
}
return { demanda, oferta };
};
const { demanda, oferta } = generarCurvas();
const scaleX = (Q: number) => 60 + (Q / 400) * 320;
const scaleY = (P: number) => 280 - (P / 1200) * 240;
const demandaPath = demanda.map((p, i) =>
`${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.P)}`
).join(' ');
const ofertaPath = oferta.map((p, i) =>
`${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.P)}`
).join(' ');
const calcularCantidades = (precio: number) => {
const Qd = Math.max(0, 1200 - precio);
const Qo = Math.max(0, precio / 3);
return { Qd, Qo };
};
const cantidades = calcularCantidades(precioActual);
const esExcesoDemanda = cantidades.Qd > cantidades.Qo;
const diferencia = Math.abs(cantidades.Qd - cantidades.Qo);
if (completado) {
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 observado el ajuste hacia el equilibrio</p>
<div className="bg-gray-50 rounded-lg p-6 mb-6">
<div className="text-5xl font-bold text-blue-600 mb-2">100%</div>
<p className="text-gray-600">
Has completado todos los escenarios
</p>
</div>
<button
onClick={handleReiniciar}
className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-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">
<RefreshCw className="w-8 h-8 text-blue-600" />
<h2 className="text-2xl font-bold text-gray-800">Ajuste al Equilibrio</h2>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-500">
{escenarioActual + 1} de {escenarios.length}
</span>
<button
onClick={handleReiniciar}
className="p-2 text-gray-500 hover:text-blue-600 transition-colors"
>
<RotateCcw className="w-5 h-5" />
</button>
</div>
</div>
<p className="text-gray-600">
Observa cómo el mercado se autocorrige hacia el equilibrio.
</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">{escenario.titulo}</h3>
<svg width="400" height="300" className="mx-auto">
{/* Grid */}
{Array.from({ length: 6 }).map((_, i) => (
<g key={i}>
<line x1={60 + i * 64} y1="30" x2={60 + i * 64} y2="280" stroke="#e5e7eb" strokeWidth="1" />
<line x1="60" y1={30 + i * 50} x2="380" y2={30 + i * 50} stroke="#e5e7eb" strokeWidth="1" />
</g>
))}
{/* Ejes */}
<line x1="60" y1="280" x2="380" y2="280" stroke="#374151" strokeWidth="2" />
<line x1="60" y1="30" x2="60" y2="280" stroke="#374151" strokeWidth="2" />
{/* Labels */}
<text x="220" y="300" textAnchor="middle" className="text-sm fill-gray-600">Cantidad (Q)</text>
<text x="30" y="155" textAnchor="middle" transform="rotate(-90, 30, 155)" className="text-sm fill-gray-600">Precio (P)</text>
{/* Curva de Demanda */}
<path d={demandaPath} fill="none" stroke="#3b82f6" strokeWidth="3" />
<text x="360" y={scaleY(50)} className="text-sm fill-blue-600 font-medium">D</text>
{/* Curva de Oferta */}
<path d={ofertaPath} fill="none" stroke="#22c55e" strokeWidth="3" />
<text x="360" y={scaleY(1100)} className="text-sm fill-green-600 font-medium">S</text>
{/* Punto de equilibrio (E) */}
<circle
cx={scaleX(300)}
cy={scaleY(900)}
r="6"
fill="#8b5cf6"
stroke="white"
strokeWidth="2"
/>
<text x={scaleX(300) + 10} y={scaleY(900) - 5} className="text-xs fill-purple-600 font-medium">
E
</text>
{/* Línea de precio actual */}
<motion.line
x1="60"
y1={scaleY(precioActual)}
x2="380"
y2={scaleY(precioActual)}
stroke={esExcesoDemanda ? "#ef4444" : "#f59e0b"}
strokeWidth="3"
strokeDasharray="5,5"
initial={false}
animate={{ y1: scaleY(precioActual), y2: scaleY(precioActual) }}
transition={{ duration: 0.1 }}
/>
<text x="385" y={scaleY(precioActual)} className={`text-xs font-medium ${esExcesoDemanda ? 'fill-red-500' : 'fill-amber-500'}`}>
P=${Math.round(precioActual)}
</text>
{/* Cantidad demandada */}
<motion.circle
cx={scaleX(cantidades.Qd)}
cy={scaleY(precioActual)}
r="6"
fill="#3b82f6"
stroke="white"
strokeWidth="2"
initial={false}
animate={{ cx: scaleX(cantidades.Qd), cy: scaleY(precioActual) }}
transition={{ duration: 0.1 }}
/>
<text x={scaleX(cantidades.Qd)} y={scaleY(precioActual) + 20} textAnchor="middle" className="text-xs fill-blue-600 font-medium">
Qd
</text>
{/* Cantidad ofrecida */}
<motion.circle
cx={scaleX(cantidades.Qo)}
cy={scaleY(precioActual)}
r="6"
fill="#22c55e"
stroke="white"
strokeWidth="2"
initial={false}
animate={{ cx: scaleX(cantidades.Qo), cy: scaleY(precioActual) }}
transition={{ duration: 0.1 }}
/>
<text x={scaleX(cantidades.Qo)} y={scaleY(precioActual) + 20} textAnchor="middle" className="text-xs fill-green-600 font-medium">
Qo
</text>
{/* Flecha de dirección del ajuste */}
{faseAjuste === 'ajustando' && (
<motion.g
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<path
d={escenario.tipo === 'exceso_demanda'
? `M 390 ${scaleY(precioActual) - 20} L 390 ${scaleY(precioActual) - 60}`
: `M 390 ${scaleY(precioActual) + 20} L 390 ${scaleY(precioActual) + 60}`
}
stroke={escenario.tipo === 'exceso_demanda' ? "#ef4444" : "#f59e0b"}
strokeWidth="3"
markerEnd={`url(#arrow-${escenario.tipo})`}
fill="none"
/>
<defs>
<marker id="arrow-exceso_demanda" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 7, 10 3.5, 0 0" fill="#ef4444" />
</marker>
<marker id="arrow-exceso_oferta" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#f59e0b" />
</marker>
</defs>
</motion.g>
)}
</svg>
<div className="mt-4 grid grid-cols-3 gap-2 text-sm">
<div className="bg-white p-2 rounded border text-center">
<span className="text-gray-500 text-xs block">Precio</span>
<span className="text-gray-800 font-bold">${Math.round(precioActual)}</span>
</div>
<div className="bg-blue-50 p-2 rounded border text-center">
<span className="text-blue-600 text-xs block">Q Demanda</span>
<span className="text-blue-800 font-bold">{Math.round(cantidades.Qd)}</span>
</div>
<div className="bg-green-50 p-2 rounded border text-center">
<span className="text-green-600 text-xs block">Q Oferta</span>
<span className="text-green-800 font-bold">{Math.round(cantidades.Qo)}</span>
</div>
</div>
</div>
<div className="space-y-4">
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg p-6">
<div className="flex items-start gap-3">
<Info className="w-6 h-6 text-blue-600 flex-shrink-0 mt-1" />
<div>
<h3 className="font-semibold text-gray-800 mb-2">Situación Actual</h3>
<p className="text-gray-700">{escenario.descripcion}</p>
</div>
</div>
</div>
<AnimatePresence mode="wait">
{faseAjuste === 'inicio' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="bg-white border-2 border-gray-200 rounded-lg p-6"
>
<h3 className="font-semibold text-gray-800 mb-4">¿Qué está pasando?</h3>
<div className="space-y-3">
<div className={`p-3 rounded-lg ${esExcesoDemanda ? 'bg-red-50 border border-red-200' : 'bg-amber-50 border border-amber-200'}`}>
<p className={`font-medium ${esExcesoDemanda ? 'text-red-800' : 'text-amber-800'}`}>
{esExcesoDemanda ? '🔥 Exceso de Demanda (Escasez)' : '📦 Exceso de Oferta (Superávit)'}
</p>
<p className="text-sm mt-1 text-gray-700">
Diferencia: <strong>{Math.round(diferencia)} unidades</strong>
</p>
</div>
</div>
<button
onClick={handleIniciar}
className="mt-4 w-full py-3 px-4 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center justify-center gap-2"
>
<Play className="w-5 h-5" />
Ver Ajuste al Equilibrio
</button>
</motion.div>
)}
{faseAjuste === 'ajustando' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="bg-blue-50 border-2 border-blue-200 rounded-lg p-6"
>
<h3 className="font-semibold text-blue-800 mb-3 flex items-center gap-2">
<RefreshCw className="w-5 h-5 animate-spin" />
Ajustando...
</h3>
<p className="text-blue-700">{escenario.mensajeAjuste}</p>
<div className="mt-4 flex items-center gap-2">
<div className="flex-1 h-2 bg-blue-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-blue-600"
initial={{ width: '0%' }}
animate={{ width: `${((precioActual - escenario.precioInicial) / (escenario.precioEquilibrio - escenario.precioInicial)) * 100}%` }}
/>
</div>
<button
onClick={handlePausar}
className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg"
>
<Pause className="w-5 h-5" />
</button>
</div>
</motion.div>
)}
{faseAjuste === 'completado' && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-green-50 border-2 border-green-200 rounded-lg p-6"
>
<div className="flex items-center gap-3 mb-3">
<CheckCircle2 className="w-8 h-8 text-green-600" />
<h3 className="font-bold text-green-800 text-lg">¡Equilibrio Alcanzado!</h3>
</div>
<div className="space-y-2 text-green-700">
<p><strong>Precio de equilibrio:</strong> ${escenario.precioEquilibrio}</p>
<p><strong>Cantidad de equilibrio:</strong> {escenario.cantidadEquilibrio} unidades</p>
<p className="text-sm mt-3">
En equilibrio, la cantidad demandada es igual a la cantidad ofrecida.
No hay presión para que el precio cambie.
</p>
</div>
<button
onClick={handleSiguiente}
className="mt-4 w-full 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"
>
{escenarioActual < escenarios.length - 1 ? (
<>
Siguiente Escenario
<ArrowRight className="w-5 h-5" />
</>
) : (
'Finalizar'
)}
</button>
</motion.div>
)}
</AnimatePresence>
<div className="p-4 bg-gray-100 rounded-lg">
<h4 className="font-semibold text-gray-700 mb-2">Principio del Ajuste:</h4>
<ul className="text-sm text-gray-600 space-y-1">
<li className="flex items-start gap-2">
<span className="text-red-500"></span>
<span><strong>Escasez (P &lt; Pe):</strong> Los compradores ofrecen más sube el precio</span>
</li>
<li className="flex items-start gap-2">
<span className="text-amber-500"></span>
<span><strong>Superávit (P &gt; Pe):</strong> Los vendedores bajan precios baja el precio</span>
</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default AjusteEquilibrio;

View File

@@ -0,0 +1,310 @@
import React, { useState, useEffect } from 'react';
interface DatosPrecioCantidad {
precioInicial: number;
cantidadInicial: number;
precioFinal: number;
cantidadFinal: number;
}
const generarDatosAleatorios = (): DatosPrecioCantidad => {
const precioInicial = Math.round((Math.random() * 50 + 10) * 100) / 100;
const cantidadInicial = Math.round(Math.random() * 800 + 200);
const cambioPrecio = (Math.random() > 0.5 ? 1 : -1) * (Math.random() * 20 + 5);
const elasticidad = Math.random() * 2 + 0.3;
const precioFinal = Math.round((precioInicial + cambioPrecio) * 100) / 100;
const cambioCantidad = -elasticidad * (cambioPrecio / ((precioInicial + precioFinal) / 2)) * cantidadInicial;
const cantidadFinal = Math.round(cantidadInicial + cambioCantidad);
return {
precioInicial: Math.max(1, precioInicial),
cantidadInicial: Math.max(10, cantidadInicial),
precioFinal: Math.max(1, precioFinal),
cantidadFinal: Math.max(10, cantidadFinal)
};
};
export const CalculoElasticidadPrecio: React.FC = () => {
const [datos, setDatos] = useState<DatosPrecioCantidad>(generarDatosAleatorios());
const [respuestaUsuario, setRespuestaUsuario] = useState<string>('');
const [resultado, setResultado] = useState<{
correcto: boolean;
mensaje: string;
valorReal: number;
} | null>(null);
const [mostrarFormula, setMostrarFormula] = useState<boolean>(true);
const calcularElasticidadPuntoMedio = (d: DatosPrecioCantidad): number => {
const cambioCantidad = d.cantidadFinal - d.cantidadInicial;
const cambioPrecio = d.precioFinal - d.precioInicial;
const cantidadPromedio = (d.cantidadInicial + d.cantidadFinal) / 2;
const precioPromedio = (d.precioInicial + d.precioFinal) / 2;
if (cantidadPromedio === 0 || precioPromedio === 0) return 0;
const elasticidad = (cambioCantidad / cantidadPromedio) / (cambioPrecio / precioPromedio);
return Math.abs(elasticidad);
};
const verificarRespuesta = () => {
const elasticidadReal = calcularElasticidadPuntoMedio(datos);
const respuestaNum = parseFloat(respuestaUsuario);
if (isNaN(respuestaNum) || respuestaNum < 0) {
setResultado({
correcto: false,
mensaje: 'Por favor ingresa un número válido mayor o igual a 0',
valorReal: elasticidadReal
});
return;
}
const margenError = 0.15;
const correcto = Math.abs(respuestaNum - elasticidadReal) <= margenError;
setResultado({
correcto,
mensaje: correcto
? '¡Correcto! Has calculado la elasticidad correctamente.'
: `Incorrecto. El valor correcto es ${elasticidadReal.toFixed(2)}`,
valorReal: elasticidadReal
});
};
const generarNuevoEjercicio = () => {
setDatos(generarDatosAleatorios());
setRespuestaUsuario('');
setResultado(null);
};
const precioPromedio = (datos.precioInicial + datos.precioFinal) / 2;
const cantidadPromedio = (datos.cantidadInicial + datos.cantidadFinal) / 2;
const cambioPrecio = datos.precioFinal - datos.precioInicial;
const cambioCantidad = datos.cantidadFinal - datos.cantidadInicial;
return (
<div className="max-w-4xl mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-2 text-blue-800">Cálculo de Elasticidad Precio de la Demanda</h2>
<p className="text-gray-600 mb-6">Utiliza la fórmula del punto medio para calcular la elasticidad.</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-5 rounded-xl border border-blue-100">
<h3 className="font-bold text-lg mb-4 text-blue-800 flex items-center gap-2">
<span className="w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center text-sm">1</span>
Datos Iniciales
</h3>
<div className="space-y-3">
<div className="flex justify-between items-center bg-white p-3 rounded-lg shadow-sm">
<span className="text-gray-600">Precio inicial (P):</span>
<span className="font-bold text-lg text-blue-700">${datos.precioInicial.toFixed(2)}</span>
</div>
<div className="flex justify-between items-center bg-white p-3 rounded-lg shadow-sm">
<span className="text-gray-600">Cantidad inicial (Q):</span>
<span className="font-bold text-lg text-blue-700">{datos.cantidadInicial.toLocaleString()} unidades</span>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-green-50 to-emerald-50 p-5 rounded-xl border border-green-100">
<h3 className="font-bold text-lg mb-4 text-green-800 flex items-center gap-2">
<span className="w-8 h-8 bg-green-500 text-white rounded-full flex items-center justify-center text-sm">2</span>
Datos Finales
</h3>
<div className="space-y-3">
<div className="flex justify-between items-center bg-white p-3 rounded-lg shadow-sm">
<span className="text-gray-600">Precio final (P):</span>
<span className="font-bold text-lg text-green-700">${datos.precioFinal.toFixed(2)}</span>
</div>
<div className="flex justify-between items-center bg-white p-3 rounded-lg shadow-sm">
<span className="text-gray-600">Cantidad final (Q):</span>
<span className="font-bold text-lg text-green-700">{datos.cantidadFinal.toLocaleString()} unidades</span>
</div>
</div>
</div>
</div>
{mostrarFormula && (
<div className="bg-gradient-to-r from-amber-50 to-yellow-50 p-5 rounded-xl border border-amber-200 mb-6">
<h3 className="font-bold text-amber-800 mb-3 flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
Fórmula del Punto Medio (Arco)
</h3>
<div className="bg-white p-4 rounded-lg shadow-inner text-center">
<p className="text-lg font-mono text-gray-800">
E<sub>d</sub> = |<span className="text-blue-600">(Q - Q) / ((Q + Q) / 2)</span>|
<span className="mx-2">÷</span>
<span className="text-green-600">(P - P) / ((P + P) / 2)</span>
</p>
</div>
<p className="text-sm text-amber-700 mt-3">
Donde: Q = Cantidad, P = Precio, y usamos valores absolutos para obtener la elasticidad como número positivo.
</p>
</div>
)}
<div className="bg-gray-50 p-5 rounded-xl border border-gray-200 mb-6">
<h3 className="font-bold text-gray-800 mb-4">Paso a paso (valores calculados):</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="bg-blue-50 p-3 rounded-lg">
<p className="text-blue-800 font-medium">Cambio en cantidad:</p>
<p className="font-mono">({datos.cantidadFinal} - {datos.cantidadInicial}) = {cambioCantidad}</p>
</div>
<div className="bg-green-50 p-3 rounded-lg">
<p className="text-green-800 font-medium">Cantidad promedio:</p>
<p className="font-mono">({datos.cantidadFinal} + {datos.cantidadInicial}) / 2 = {cantidadPromedio}</p>
</div>
<div className="bg-blue-50 p-3 rounded-lg">
<p className="text-blue-800 font-medium">Cambio en precio:</p>
<p className="font-mono">(${datos.precioFinal} - ${datos.precioInicial}) = ${cambioPrecio.toFixed(2)}</p>
</div>
<div className="bg-green-50 p-3 rounded-lg">
<p className="text-green-800 font-medium">Precio promedio:</p>
<p className="font-mono">(${datos.precioFinal} + ${datos.precioInicial}) / 2 = ${precioPromedio.toFixed(2)}</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-xl border-2 border-indigo-100 shadow-lg mb-6">
<h3 className="font-bold text-xl mb-4 text-indigo-800">Tu Respuesta</h3>
<div className="flex flex-col sm:flex-row items-center gap-4">
<div className="flex items-center gap-3 flex-1">
<label className="font-medium text-gray-700 whitespace-nowrap">Elasticidad (E<sub>d</sub>):</label>
<input
type="number"
step="0.01"
min="0"
value={respuestaUsuario}
onChange={(e) => setRespuestaUsuario(e.target.value)}
className="border-2 border-indigo-200 p-3 rounded-lg w-full sm:w-40 text-center text-lg font-mono focus:border-indigo-500 focus:outline-none"
placeholder="Ej: 1.25"
/>
</div>
<div className="flex gap-3">
<button
onClick={verificarRespuesta}
className="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Verificar
</button>
<button
onClick={generarNuevoEjercicio}
className="bg-gray-600 hover:bg-gray-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Nuevo
</button>
</div>
</div>
<button
onClick={() => setMostrarFormula(!mostrarFormula)}
className="mt-4 text-indigo-600 hover:text-indigo-800 text-sm font-medium flex items-center gap-1"
>
{mostrarFormula ? 'Ocultar' : 'Mostrar'} fórmula y pasos
<svg className={`w-4 h-4 transform transition-transform ${mostrarFormula ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
{resultado && (
<div className={`p-6 rounded-xl border-2 ${resultado.correcto ? 'bg-green-50 border-green-300' : 'bg-red-50 border-red-300'}`}>
<div className="flex items-start gap-4">
<div className={`p-3 rounded-full ${resultado.correcto ? 'bg-green-100' : 'bg-red-100'}`}>
{resultado.correcto ? (
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
</div>
<div className="flex-1">
<h4 className={`font-bold text-lg ${resultado.correcto ? 'text-green-800' : 'text-red-800'}`}>
{resultado.correcto ? '¡Respuesta Correcta!' : 'Respuesta Incorrecta'}
</h4>
<p className={`mt-1 ${resultado.correcto ? 'text-green-700' : 'text-red-700'}`}>
{resultado.mensaje}
</p>
{!resultado.correcto && (
<div className="mt-4 bg-white p-4 rounded-lg border border-red-200">
<p className="font-medium text-gray-800 mb-2">Desglose del cálculo:</p>
<div className="font-mono text-sm space-y-1 text-gray-600">
<p>% Cambio en Q = {cambioCantidad} / {cantidadPromedio} = {((cambioCantidad / cantidadPromedio) * 100).toFixed(2)}%</p>
<p>% Cambio en P = {cambioPrecio.toFixed(2)} / {precioPromedio.toFixed(2)} = {((cambioPrecio / precioPromedio) * 100).toFixed(2)}%</p>
<p className="text-indigo-600 font-bold mt-2">
E<sub>d</sub> = |{((cambioCantidad / cantidadPromedio) / (cambioPrecio / precioPromedio)).toFixed(3)}| = {resultado.valorReal.toFixed(2)}
</p>
</div>
</div>
)}
{resultado.correcto && resultado.valorReal > 0 && (
<div className="mt-4 bg-white p-4 rounded-lg border border-green-200">
<p className="font-medium text-gray-800">
Clasificación: {' '}
{resultado.valorReal > 1 ? (
<span className="text-green-600 font-bold">Elástica (E<sub>d</sub> &gt; 1)</span>
) : resultado.valorReal < 1 ? (
<span className="text-amber-600 font-bold">Inelástica (E<sub>d</sub> &lt; 1)</span>
) : (
<span className="text-blue-600 font-bold">Unitaria (E<sub>d</sub> = 1)</span>
)}
</p>
<p className="text-sm text-gray-600 mt-2">
{resultado.valorReal > 1
? 'La demanda responde proporcionalmente más que el cambio en precio.'
: resultado.valorReal < 1
? 'La demanda responde proporcionalmente menos que el cambio en precio.'
: 'La demanda responde exactamente en la misma proporción que el precio.'}
</p>
</div>
)}
</div>
</div>
</div>
)}
<div className="mt-6 bg-indigo-50 p-4 rounded-xl border border-indigo-200">
<h4 className="font-bold text-indigo-800 mb-2 flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Interpretación de Resultados
</h4>
<div className="grid grid-cols-3 gap-4 text-sm">
<div className="bg-white p-3 rounded-lg text-center border border-green-200">
<p className="font-bold text-green-700">E<sub>d</sub> &gt; 1</p>
<p className="text-gray-600">Elástica</p>
<p className="text-xs text-gray-500">%ΔQ &gt; %ΔP</p>
</div>
<div className="bg-white p-3 rounded-lg text-center border border-blue-200">
<p className="font-bold text-blue-700">E<sub>d</sub> = 1</p>
<p className="text-gray-600">Unitaria</p>
<p className="text-xs text-gray-500">%ΔQ = %ΔP</p>
</div>
<div className="bg-white p-3 rounded-lg text-center border border-amber-200">
<p className="font-bold text-amber-700">E<sub>d</sub> &lt; 1</p>
<p className="text-gray-600">Inelástica</p>
<p className="text-xs text-gray-500">%ΔQ &lt; %ΔP</p>
</div>
</div>
</div>
</div>
);
};
export default CalculoElasticidadPrecio;

View File

@@ -0,0 +1,577 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { GitBranch, ArrowRight, ArrowLeft, CheckCircle2, XCircle, Trophy, RotateCcw, BookOpen, TrendingUp, TrendingDown } from 'lucide-react';
interface CambiosEquilibrioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
type DireccionShock = 'oferta-aumenta' | 'oferta-disminuye' | 'demanda-aumenta' | 'demanda-disminuye';
interface Escenario {
id: number;
descripcion: string;
shock: DireccionShock;
curva: 'oferta' | 'demanda';
direccion: 'aumenta' | 'disminuye';
cambioPrecio: 'sube' | 'baja';
cambioCantidad: 'sube' | 'baja';
explicacion: string;
dificultad: 'facil' | 'medio' | 'dificil';
}
const escenarios: Escenario[] = [
{
id: 1,
descripcion: 'Una nueva tecnología reduce los costos de producción de teléfonos inteligentes.',
shock: 'oferta-aumenta',
curva: 'oferta',
direccion: 'aumenta',
cambioPrecio: 'baja',
cambioCantidad: 'sube',
explicacion: 'La tecnología mejora la productividad, aumentando la oferta. La curva se desplaza a la derecha: el precio baja y la cantidad sube.',
dificultad: 'facil'
},
{
id: 2,
descripcion: 'Un informe de salud afirma que el café aumenta la longevidad.',
shock: 'demanda-aumenta',
curva: 'demanda',
direccion: 'aumenta',
cambioPrecio: 'sube',
cambioCantidad: 'sube',
explicacion: 'Las preferencias positivas aumentan la demanda. La curva se desplaza a la derecha: el precio y la cantidad suben.',
dificultad: 'facil'
},
{
id: 3,
descripcion: 'Una plaga de langostas destruye el 30% de la cosecha de granos.',
shock: 'oferta-disminuye',
curva: 'oferta',
direccion: 'disminuye',
cambioPrecio: 'sube',
cambioCantidad: 'baja',
explicacion: 'La plaga reduce la cantidad disponible, disminuyendo la oferta. La curva se desplaza a la izquierda: el precio sube y la cantidad baja.',
dificultad: 'facil'
},
{
id: 4,
descripcion: 'La economía entra en recesión y el ingreso promedio cae 20% (bien normal).',
shock: 'demanda-disminuye',
curva: 'demanda',
direccion: 'disminuye',
cambioPrecio: 'baja',
cambioCantidad: 'baja',
explicacion: 'Para bienes normales, al bajar el ingreso, disminuye la demanda. La curva se desplaza a la izquierda: el precio y la cantidad bajan.',
dificultad: 'medio'
},
{
id: 5,
descripcion: 'El gobierno subsidia la compra de autos eléctricos con $10,000.',
shock: 'demanda-aumenta',
curva: 'demanda',
direccion: 'aumenta',
cambioPrecio: 'sube',
cambioCantidad: 'sube',
explicacion: 'El subsidio reduce el precio efectivo para consumidores, aumentando la demanda. El equilibrio se mueve hacia mayor precio y cantidad.',
dificultad: 'medio'
},
{
id: 6,
descripcion: 'El precio del petróleo (insumo importante) sube un 50%.',
shock: 'oferta-disminuye',
curva: 'oferta',
direccion: 'disminuye',
cambioPrecio: 'sube',
cambioCantidad: 'baja',
explicacion: 'Al subir los costos de insumos, producir es más caro, disminuyendo la oferta. El equilibrio resulta en mayor precio y menor cantidad.',
dificultad: 'dificil'
}
];
interface OpcionShock {
value: DireccionShock;
label: string;
descripcion: string;
icon: React.ReactNode;
}
const opcionesShock: OpcionShock[] = [
{ value: 'oferta-aumenta', label: 'Oferta ↑', descripcion: 'Aumenta', icon: <TrendingUp className="w-5 h-5" /> },
{ value: 'oferta-disminuye', label: 'Oferta ↓', descripcion: 'Disminuye', icon: <TrendingDown className="w-5 h-5" /> },
{ value: 'demanda-aumenta', label: 'Demanda ↑', descripcion: 'Aumenta', icon: <TrendingUp className="w-5 h-5" /> },
{ value: 'demanda-disminuye', label: 'Demanda ↓', descripcion: 'Disminuye', icon: <TrendingDown className="w-5 h-5" /> },
];
interface OpcionCambio {
value: 'sube' | 'baja';
label: string;
icon: React.ReactNode;
}
const opcionesCambio: OpcionCambio[] = [
{ value: 'sube', label: 'Sube', icon: <TrendingUp className="w-5 h-5" /> },
{ value: 'baja', label: 'Baja', icon: <TrendingDown className="w-5 h-5" /> },
];
export const CambiosEquilibrio: React.FC<CambiosEquilibrioProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [escenarioActual, setEscenarioActual] = useState(0);
const [shockSeleccionado, setShockSeleccionado] = useState<DireccionShock | null>(null);
const [cambioPrecio, setCambioPrecio] = useState<'sube' | 'baja' | null>(null);
const [cambioCantidad, setCambioCantidad] = useState<'sube' | 'baja' | null>(null);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [esCorrecto, setEsCorrecto] = useState(false);
const [score, setScore] = useState(0);
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
const [completado, setCompletado] = useState(false);
const [_startTime] = useState(Date.now());
const escenario = escenarios[escenarioActual];
const handleVerificar = () => {
if (!shockSeleccionado || !cambioPrecio || !cambioCantidad) return;
const shockCorrecto = shockSeleccionado === escenario.shock;
const precioCorrecto = cambioPrecio === escenario.cambioPrecio;
const cantidadCorrecta = cambioCantidad === escenario.cambioCantidad;
const todoCorrecto = shockCorrecto && precioCorrecto && cantidadCorrecta;
setEsCorrecto(todoCorrecto);
setMostrarResultado(true);
if (todoCorrecto) {
setScore(prev => prev + Math.round(100 / escenarios.length));
setRespuestasCorrectas(prev => prev + 1);
}
};
const handleSiguiente = () => {
if (escenarioActual < escenarios.length - 1) {
setEscenarioActual(prev => prev + 1);
setShockSeleccionado(null);
setCambioPrecio(null);
setCambioCantidad(null);
setMostrarResultado(false);
} else {
setCompletado(true);
if (onComplete) {
onComplete(score);
}
}
};
const handleReiniciar = () => {
setEscenarioActual(0);
setShockSeleccionado(null);
setCambioPrecio(null);
setCambioCantidad(null);
setMostrarResultado(false);
setScore(0);
setRespuestasCorrectas(0);
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 getShockColor = (value: DireccionShock) => {
if (value.includes('oferta')) return value.includes('aumenta') ? 'green' : 'red';
return value.includes('aumenta') ? 'blue' : 'orange';
};
const renderGrafico = () => {
const isOferta = escenario.curva === 'oferta';
const isAumenta = escenario.direccion === 'aumenta';
return (
<svg width="300" height="250" className="mx-auto">
{/* Grid */}
{Array.from({ length: 6 }).map((_, i) => (
<g key={i}>
<line x1={50 + i * 40} y1="30" x2={50 + i * 40} y2="210" stroke="#e5e7eb" strokeWidth="1" />
<line x1="50" y1={30 + i * 36} x2="250" y2={30 + i * 36} stroke="#e5e7eb" strokeWidth="1" />
</g>
))}
{/* Ejes */}
<line x1="50" y1="210" x2="250" y2="210" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="30" x2="50" y2="210" stroke="#374151" strokeWidth="2" />
{/* Labels */}
<text x="150" y="235" textAnchor="middle" className="text-sm fill-gray-600">Q</text>
<text x="25" y="120" textAnchor="middle" transform="rotate(-90, 25, 120)" className="text-sm fill-gray-600">P</text>
{/* Curva original */}
{isOferta ? (
<line x1="80" y1="180" x2="200" y2="80" stroke="#22c55e" strokeWidth="3" />
) : (
<line x1="80" y1="80" x2="200" y2="180" stroke="#3b82f6" strokeWidth="3" />
)}
<text x={isOferta ? 210 : 210} y={isOferta ? 75 : 190} className={`text-sm ${isOferta ? 'fill-green-600' : 'fill-blue-600'}`}>
{isOferta ? 'S₁' : 'D₁'}
</text>
{/* Curva desplazada */}
{mostrarResultado && (
<motion.g
initial={{ opacity: 0, x: isAumenta ? 30 : -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
{isOferta ? (
<line x1={isAumenta ? 110 : 50} y1="180" x2={isAumenta ? 230 : 170} y2="80" stroke="#22c55e" strokeWidth="3" strokeDasharray="5,5" />
) : (
<line x1={isAumenta ? 110 : 50} y1="80" x2={isAumenta ? 230 : 170} y2="180" stroke="#3b82f6" strokeWidth="3" strokeDasharray="5,5" />
)}
<text x={isAumenta ? 240 : 180} y={isOferta ? 75 : 190} className={`text-sm ${isOferta ? 'fill-green-600' : 'fill-blue-600'}`}>
{isOferta ? 'S₂' : 'D₂'}
</text>
</motion.g>
)}
{/* Punto de equilibrio original */}
<circle cx="140" cy="130" r="5" fill="#8b5cf6" stroke="white" strokeWidth="2" />
<text x="150" y="125" className="text-xs fill-purple-600">E</text>
{/* Nuevo equilibrio (si se muestra resultado) */}
{mostrarResultado && (
<motion.g
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.3, type: "spring" }}
>
<circle
cx={isAumenta ? 170 : 110}
cy={escenario.cambioPrecio === 'sube' ? 110 : 150}
r="5"
fill="#8b5cf6"
stroke="white"
strokeWidth="2"
/>
<text x={isAumenta ? 180 : 120} y={escenario.cambioPrecio === 'sube' ? 105 : 145} className="text-xs fill-purple-600 font-bold">
E
</text>
</motion.g>
)}
</svg>
);
};
if (completado) {
const porcentaje = Math.round((respuestasCorrectas / escenarios.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 analizado cambios en el 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 {escenarios.length} respuestas correctas
</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">
<GitBranch className="w-8 h-8 text-purple-600" />
<h2 className="text-2xl font-bold text-gray-800">Cambios en el Equilibrio</h2>
</div>
<div className="flex items-center gap-4">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getDificultadColor(escenario.dificultad)}`}>
{escenario.dificultad.toUpperCase()}
</span>
<span className="text-sm text-gray-500">
{escenarioActual + 1} de {escenarios.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: `${((escenarioActual + 1) / escenarios.length) * 100}%` }}
/>
</div>
</div>
</div>
<p className="text-gray-600">
Analiza cómo los shocks del mercado afectan el precio y cantidad de equilibrio.
</p>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-4">
<div className="bg-gradient-to-br from-purple-50 to-blue-50 rounded-lg p-6">
<div className="flex items-start gap-3">
<BookOpen className="w-6 h-6 text-purple-600 flex-shrink-0 mt-1" />
<div>
<h3 className="font-semibold text-gray-800 mb-2">Escenario {escenario.id}</h3>
<p className="text-gray-700 text-lg">{escenario.descripcion}</p>
</div>
</div>
</div>
<div className="bg-white border-2 border-gray-200 rounded-lg p-6">
<h3 className="font-semibold text-gray-800 mb-4">1. ¿Qué curva se desplaza y en qué dirección?</h3>
<div className="grid grid-cols-2 gap-3">
{opcionesShock.map((opcion) => {
const isSelected = shockSeleccionado === opcion.value;
const isCorrect = mostrarResultado && opcion.value === escenario.shock;
const color = getShockColor(opcion.value);
return (
<motion.button
key={opcion.value}
onClick={() => !mostrarResultado && setShockSeleccionado(opcion.value)}
disabled={mostrarResultado}
whileHover={!mostrarResultado ? { scale: 1.02 } : {}}
whileTap={!mostrarResultado ? { scale: 0.98 } : {}}
className={`p-4 rounded-lg border-2 transition-all flex flex-col items-center gap-2 ${
isCorrect
? 'border-green-500 bg-green-50'
: isSelected && mostrarResultado && opcion.value !== escenario.shock
? 'border-red-500 bg-red-50'
: isSelected
? `border-${color}-500 bg-${color}-50`
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
}`}
>
{opcion.icon}
<span className={`font-semibold ${
isCorrect ? 'text-green-700' :
isSelected && mostrarResultado && opcion.value !== escenario.shock ? 'text-red-700' :
isSelected ? `text-${color}-700` : 'text-gray-700'
}`}>
{opcion.label}
</span>
</motion.button>
);
})}
</div>
</div>
<div className="bg-white border-2 border-gray-200 rounded-lg p-6">
<h3 className="font-semibold text-gray-800 mb-4">2. ¿Cómo cambian el precio y la cantidad de equilibrio?</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Precio (P*)</label>
<div className="flex gap-2">
{opcionesCambio.map((opcion) => {
const isSelected = cambioPrecio === opcion.value;
const isCorrect = mostrarResultado && opcion.value === escenario.cambioPrecio;
return (
<button
key={opcion.value}
onClick={() => !mostrarResultado && setCambioPrecio(opcion.value)}
disabled={mostrarResultado}
className={`flex-1 py-2 px-3 rounded-lg border-2 flex items-center justify-center gap-2 transition-all ${
isCorrect
? 'border-green-500 bg-green-50 text-green-700'
: isSelected && mostrarResultado && opcion.value !== escenario.cambioPrecio
? 'border-red-500 bg-red-50 text-red-700'
: isSelected
? 'border-purple-500 bg-purple-50 text-purple-700'
: 'border-gray-200 hover:border-gray-300 text-gray-700'
}`}
>
{opcion.icon}
<span className="font-medium">{opcion.label}</span>
</button>
);
})}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Cantidad (Q*)</label>
<div className="flex gap-2">
{opcionesCambio.map((opcion) => {
const isSelected = cambioCantidad === opcion.value;
const isCorrect = mostrarResultado && opcion.value === escenario.cambioCantidad;
return (
<button
key={opcion.value}
onClick={() => !mostrarResultado && setCambioCantidad(opcion.value)}
disabled={mostrarResultado}
className={`flex-1 py-2 px-3 rounded-lg border-2 flex items-center justify-center gap-2 transition-all ${
isCorrect
? 'border-green-500 bg-green-50 text-green-700'
: isSelected && mostrarResultado && opcion.value !== escenario.cambioCantidad
? 'border-red-500 bg-red-50 text-red-700'
: isSelected
? 'border-purple-500 bg-purple-50 text-purple-700'
: 'border-gray-200 hover:border-gray-300 text-gray-700'
}`}
>
{opcion.icon}
<span className="font-medium">{opcion.label}</span>
</button>
);
})}
</div>
</div>
</div>
</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 ? (
<CheckCircle2 className="w-6 h-6 text-green-600 flex-shrink-0" />
) : (
<XCircle 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!' : 'Algunas respuestas son incorrectas'}
</p>
<p className="text-sm mt-1 text-gray-700">{escenario.explicacion}</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="flex gap-3">
{!mostrarResultado ? (
<button
onClick={handleVerificar}
disabled={!shockSeleccionado || !cambioPrecio || !cambioCantidad}
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"
>
{escenarioActual < escenarios.length - 1 ? (
<>
Siguiente
<ArrowRight className="w-5 h-5" />
</>
) : (
'Finalizar'
)}
</button>
)}
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-700 mb-4 text-center">Visualización del Cambio</h3>
{renderGrafico()}
<div className="mt-4 p-3 bg-white rounded-lg">
<h4 className="font-medium text-gray-700 mb-2">Resumen de efectos:</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between items-center p-2 bg-gray-50 rounded">
<span className="text-gray-600">Curva de {escenario.curva}:</span>
<span className={`font-medium ${escenario.direccion === 'aumenta' ? 'text-green-600' : 'text-red-600'}`}>
Se {escenario.direccion === 'aumenta' ? 'desplaza a la derecha' : 'desplaza a la izquierda'}
</span>
</div>
<div className="flex justify-between items-center p-2 bg-gray-50 rounded">
<span className="text-gray-600">Precio de equilibrio:</span>
<span className={`font-medium ${escenario.cambioPrecio === 'sube' ? 'text-green-600' : 'text-red-600'}`}>
{escenario.cambioPrecio === 'sube' ? '↑ Sube' : '↓ Baja'}
</span>
</div>
<div className="flex justify-between items-center p-2 bg-gray-50 rounded">
<span className="text-gray-600">Cantidad de equilibrio:</span>
<span className={`font-medium ${escenario.cambioCantidad === 'sube' ? 'text-green-600' : 'text-red-600'}`}>
{escenario.cambioCantidad === 'sube' ? '↑ Sube' : '↓ Baja'}
</span>
</div>
</div>
</div>
<div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
<strong>Recordatorio:</strong>
</p>
<ul className="text-sm text-yellow-700 mt-1 space-y-1">
<li> Oferta P, Q</li>
<li> Oferta P, Q</li>
<li> Demanda P, Q</li>
<li> Demanda P, Q</li>
</ul>
</div>
</div>
</div>
<div className="mt-6 flex justify-between items-center">
<button
onClick={() => setEscenarioActual(Math.max(0, escenarioActual - 1))}
disabled={escenarioActual === 0}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Anterior
</button>
<div className="flex items-center gap-1">
{escenarios.map((_, index) => (
<div
key={index}
className={`w-2 h-2 rounded-full ${
index === escenarioActual
? 'bg-purple-600'
: index < escenarioActual
? 'bg-green-500'
: 'bg-gray-300'
}`}
/>
))}
</div>
<button
onClick={() => setEscenarioActual(Math.min(escenarios.length - 1, escenarioActual + 1))}
disabled={escenarioActual === escenarios.length - 1}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Siguiente
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
);
};
export default CambiosEquilibrio;

View File

@@ -0,0 +1,509 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Building2, Wallet, AlertCircle, CheckCircle2, BookOpen, TrendingUp, Users, MapPin } from 'lucide-react';
interface ControlesVidaRealProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface CasoEstudio {
id: string;
titulo: string;
categoria: 'vivienda' | 'laboral' | 'agricola';
ubicacion: string;
anio: string;
contexto: string;
intervencion: string;
resultados: string[];
lecciones: string[];
datos: {
antes: { precio: number; cantidad: number };
despues: { precio: number; cantidad: number };
};
icono: React.ReactNode;
color: string;
}
const casosEstudio: CasoEstudio[] = [
{
id: 'nyc-rent',
titulo: "Rent Control en Nueva York",
categoria: 'vivienda',
ubicacion: "Nueva York, USA",
anio: "1947-presente",
contexto: "Nueva York implementó controles de alquiler después de la Segunda Guerra Mundial para proteger a los inquilinos. Actualmente afecta a aproximadamente 1 millón de apartamentos.",
intervencion: "Los alquileres de apartamentos antiguos están regulados y no pueden aumentar más allá de ciertos límites establecidos por la Junta de Alquileres.",
resultados: [
"Reducción de la oferta de vivienda a largo plazo",
"Deterioro de la calidad de edificios regulados",
"Mercado paralelo de 'pagos clave'",
"Beneficios para inquilinos antiguos, no para nuevos"
],
lecciones: [
"Los controles benefician a quienes ya tienen vivienda",
"Desincentivan la construcción de nueva vivienda",
"Crean ineficiencias en la asignación de recursos",
"Difícil de eliminar una vez implementado"
],
datos: {
antes: { precio: 1000, cantidad: 100 },
despues: { precio: 800, cantidad: 85 }
},
icono: <Building2 className="w-6 h-6" />,
color: "blue"
},
{
id: 'venezuela-gasolina',
titulo: "Gasolina Subsidiada en Venezuela",
categoria: 'agricola',
ubicacion: "Venezuela",
anio: "1976-2019",
contexto: "Venezuela mantuvo durante décadas el precio de la gasolina casi gratis (menos de $0.01 por litro) debido a subsidios gubernamentales masivos.",
intervencion: "Precio máximo artificial mantenido por subsidios estatales, sin relación con costos reales de producción.",
resultados: [
"Contrabando masivo a países vecinos",
"Colapso de la infraestructura de refinación",
"Desabastecimiento crónico en 2019",
"Pérdida fiscal insostenible para el Estado"
],
lecciones: [
"Los precios deben reflejar costos reales",
"Subsidios masivos son fiscalmente insostenibles",
"Crearán mercados negros inevitablemente",
"La transición es extremadamente difícil"
],
datos: {
antes: { precio: 0.50, cantidad: 100 },
despues: { precio: 0.01, cantidad: 60 }
},
icono: <AlertCircle className="w-6 h-6" />,
color: "red"
},
{
id: 'seattle-wage',
titulo: "Salario Mínimo en Seattle",
categoria: 'laboral',
ubicacion: "Seattle, USA",
anio: "2014-2019",
contexto: "Seattle aumentó gradualmente el salario mínimo de $9.47 a $15/hora entre 2014 y 2017, siendo pionera en Estados Unidos.",
intervencion: "Incremento progresivo del salario mínimo municipal hasta $15/hora, con ritmo diferenciado por tamaño de empresa.",
resultados: [
"Reducción en horas trabajadas por empleados de bajos ingresos",
"Pérdida neta de ingresos para algunos trabajadores",
"Beneficio para trabajadores que mantuvieron empleo",
"Aumento de precios en restaurantes"
],
lecciones: [
"Efectos no son uniformes en todos los trabajadores",
"El ajuste puede ocurrir por horas, no solo empleos",
"La elasticidad de la demanda laboral importa",
"Estudios rigurosos muestran efectos mixtos"
],
datos: {
antes: { precio: 9.47, cantidad: 100 },
despues: { precio: 15.00, cantidad: 92 }
},
icono: <Wallet className="w-6 h-6" />,
color: "amber"
},
{
id: 'eu-agricultura',
titulo: "Política Agrícola de la UE",
categoria: 'agricola',
ubicacion: "Unión Europea",
anio: "1962-presente",
contexto: "La Política Agrícola Común (PAC) estableció precios de intervención para garantizar ingresos a los agricultores europeos.",
intervencion: "Precios mínimos garantizados para productos agrícolas clave, con compras gubernamentales del excedente.",
resultados: [
"Superávits masivos de productos lácteos y cereales",
"Montañas de mantequilla y lagos de vino",
"Gasto fiscal considerable",
"Reformas parciales desde los 90"
],
lecciones: [
"Los precios de soporte generan excedentes",
"El gobierno termina comprando producción no deseada",
"Crean distorsiones en el comercio internacional",
"Las reformas son políticamente difíciles"
],
datos: {
antes: { precio: 100, cantidad: 80 },
despues: { precio: 130, cantidad: 110 }
},
icono: <TrendingUp className="w-6 h-6" />,
color: "green"
}
];
export const ControlesVidaReal: React.FC<ControlesVidaRealProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [casoActivo, setCasoActivo] = useState<CasoEstudio | null>(null);
const [respuestas, setRespuestas] = useState<Record<string, string>>({});
const [mostrarResultado, setMostrarResultado] = useState(false);
const [casosCompletados, setCasosCompletados] = useState<Set<string>>(new Set());
const [puntuacion, setPuntuacion] = useState(0);
const seleccionarCaso = (caso: CasoEstudio) => {
setCasoActivo(caso);
setMostrarResultado(false);
};
const responderPregunta = (respuesta: string) => {
if (!casoActivo) return;
setRespuestas(prev => ({ ...prev, [casoActivo.id]: respuesta }));
setMostrarResultado(true);
if (!casosCompletados.has(casoActivo.id)) {
setCasosCompletados(prev => new Set([...prev, casoActivo.id]));
setPuntuacion(prev => prev + 25);
if (casosCompletados.size + 1 >= 4) {
setTimeout(() => {
onComplete?.(100);
}, 2000);
}
}
};
const getColorClass = (color: string) => {
const colors: Record<string, { bg: string; border: string; text: string; light: string }> = {
blue: { bg: 'bg-blue-600', border: 'border-blue-400', text: 'text-blue-800', light: 'bg-blue-50' },
red: { bg: 'bg-red-600', border: 'border-red-400', text: 'text-red-800', light: 'bg-red-50' },
amber: { bg: 'bg-amber-600', border: 'border-amber-400', text: 'text-amber-800', light: 'bg-amber-50' },
green: { bg: 'bg-green-600', border: 'border-green-400', text: 'text-green-800', light: 'bg-green-50' }
};
return colors[color] || colors.blue;
};
return (
<div className="w-full max-w-6xl mx-auto p-6 bg-white rounded-xl shadow-lg">
{/* Header */}
<div className="mb-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-indigo-100 rounded-lg">
<BookOpen className="w-6 h-6 text-indigo-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-800">Controles de Precio en la Vida Real</h2>
<p className="text-gray-600">Estudia casos históricos y sus consecuencias reales</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-500">Progreso: {casosCompletados.size}/4</span>
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-indigo-600"
initial={{ width: 0 }}
animate={{ width: `${puntuacion}%` }}
/>
</div>
</div>
</div>
</div>
{!casoActivo ? (
/* Grid de casos de estudio */
<div className="grid md:grid-cols-2 gap-4">
{casosEstudio.map((caso) => {
const colors = getColorClass(caso.color);
const completado = casosCompletados.has(caso.id);
return (
<motion.button
key={caso.id}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => seleccionarCaso(caso)}
className={`p-5 text-left rounded-xl border-2 transition-all ${
completado
? `${colors.light} ${colors.border}`
: 'bg-white border-gray-200 hover:border-indigo-300'
}`}
>
<div className="flex items-start justify-between mb-3">
<div className={`p-2 ${colors.light} rounded-lg`}>
{caso.icono}
</div>
{completado && (
<CheckCircle2 className="w-5 h-5 text-green-600" />
)}
</div>
<h3 className="font-bold text-gray-800 mb-2">{caso.titulo}</h3>
<div className="flex items-center gap-4 text-sm text-gray-500 mb-3">
<span className="flex items-center gap-1">
<MapPin className="w-3 h-3" />
{caso.ubicacion}
</span>
<span className="flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
{caso.anio}
</span>
</div>
<p className="text-sm text-gray-600 line-clamp-2">{caso.contexto}</p>
<div className="mt-3">
<span className={`inline-block text-xs px-2 py-1 rounded ${colors.light} ${colors.text}`}>
{caso.categoria === 'vivienda' && '🏠 Vivienda'}
{caso.categoria === 'laboral' && '💼 Laboral'}
{caso.categoria === 'agricola' && '🌾 Agrícola'}
</span>
</div>
</motion.button>
);
})}
</div>
) : (
/* Vista detallada del caso */
<div className="space-y-6">
{/* Navegación */}
<button
onClick={() => setCasoActivo(null)}
className="text-indigo-600 hover:text-indigo-800 font-medium flex items-center gap-2"
>
Volver a casos de estudio
</button>
{(() => {
const colors = getColorClass(casoActivo.color);
return (
<>
{/* Header del caso */}
<div className={`p-6 rounded-xl ${colors.light} border ${colors.border}`}>
<div className="flex items-center gap-4 mb-4">
<div className="p-3 bg-white rounded-lg shadow-sm">
{casoActivo.icono}
</div>
<div>
<h2 className="text-2xl font-bold text-gray-800">{casoActivo.titulo}</h2>
<div className="flex items-center gap-4 text-sm text-gray-600 mt-1">
<span className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
{casoActivo.ubicacion}
</span>
<span className="flex items-center gap-1">
<AlertCircle className="w-4 h-4" />
{casoActivo.anio}
</span>
</div>
</div>
</div>
<p className="text-gray-700">{casoActivo.contexto}</p>
</div>
<div className="grid md:grid-cols-2 gap-6">
{/* Columna izquierda: Información */}
<div className="space-y-4">
{/* Intervención */}
<div className="p-4 bg-gray-50 rounded-lg">
<h3 className="font-semibold text-gray-800 mb-2 flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-indigo-600" />
Intervención
</h3>
<p className="text-gray-700">{casoActivo.intervencion}</p>
</div>
{/* Resultados */}
<div className="p-4 bg-gray-50 rounded-lg">
<h3 className="font-semibold text-gray-800 mb-2 flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-indigo-600" />
Resultados Observados
</h3>
<ul className="space-y-2">
{casoActivo.resultados.map((resultado, idx) => (
<motion.li
key={idx}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.1 }}
className="flex items-start gap-2 text-sm text-gray-700"
>
<span className="text-indigo-600 mt-0.5"></span>
{resultado}
</motion.li>
))}
</ul>
</div>
{/* Lecciones */}
<div className="p-4 bg-indigo-50 rounded-lg border border-indigo-200">
<h3 className="font-semibold text-indigo-800 mb-2 flex items-center gap-2">
<BookOpen className="w-4 h-4" />
Lecciones Aprendidas
</h3>
<ul className="space-y-2">
{casoActivo.lecciones.map((leccion, idx) => (
<motion.li
key={idx}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.1 }}
className="flex items-start gap-2 text-sm text-indigo-900"
>
<span className="text-indigo-600 mt-0.5">💡</span>
{leccion}
</motion.li>
))}
</ul>
</div>
</div>
{/* Columna derecha: Visualización y pregunta */}
<div className="space-y-4">
{/* Visualización simple */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-800 mb-3">Evolución del Mercado</h3>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-white rounded-lg">
<span className="text-sm text-gray-600">Antes</span>
<div className="text-right">
<div className="font-bold text-gray-800">${casoActivo.datos.antes.precio}</div>
<div className="text-xs text-gray-500">{casoActivo.datos.antes.cantidad} unidades</div>
</div>
</div>
<div className="flex justify-center">
<div className="text-2xl text-gray-400"></div>
</div>
<div className={`flex items-center justify-between p-3 rounded-lg ${colors.light}`}>
<span className="text-sm text-gray-600">Después</span>
<div className="text-right">
<div className={`font-bold ${colors.text}`}>${casoActivo.datos.despues.precio}</div>
<div className="text-xs text-gray-600">{casoActivo.datos.despues.cantidad} unidades</div>
</div>
</div>
</div>
</div>
{/* Pregunta de comprensión */}
<AnimatePresence mode="wait">
{!mostrarResultado ? (
<motion.div
key="pregunta"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="p-5 bg-white border-2 border-gray-200 rounded-lg"
>
<h3 className="font-bold text-gray-800 mb-4">
¿Cuál es la principal consecuencia económica observada?
</h3>
<div className="space-y-2">
<button
onClick={() => responderPregunta('desajuste')}
className="w-full p-3 text-left bg-gray-50 hover:bg-gray-100 border border-gray-200 rounded-lg transition-colors"
>
<span className="font-medium text-gray-800">Desajuste entre oferta y demanda</span>
<p className="text-sm text-gray-600">Escasez o superávit según el tipo de control</p>
</button>
<button
onClick={() => responderPregunta('equilibrio')}
className="w-full p-3 text-left bg-gray-50 hover:bg-gray-100 border border-gray-200 rounded-lg transition-colors"
>
<span className="font-medium text-gray-800">El mercado alcanzó equilibrio</span>
<p className="text-sm text-gray-600">Los controles no afectaron las cantidades</p>
</button>
<button
onClick={() => responderPregunta('eficiencia')}
className="w-full p-3 text-left bg-gray-50 hover:bg-gray-100 border border-gray-200 rounded-lg transition-colors"
>
<span className="font-medium text-gray-800">Mayor eficiencia económica</span>
<p className="text-sm text-gray-600">Mejor asignación de recursos</p>
</button>
</div>
</motion.div>
) : (
<motion.div
key="resultado"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`p-5 rounded-lg border-2 ${
respuestas[casoActivo.id] === 'desajuste'
? 'bg-green-50 border-green-200'
: 'bg-amber-50 border-amber-200'
}`}
>
<div className="flex items-center gap-3 mb-3">
{respuestas[casoActivo.id] === 'desajuste' ? (
<CheckCircle2 className="w-6 h-6 text-green-600" />
) : (
<AlertCircle className="w-6 h-6 text-amber-600" />
)}
<h4 className={`font-bold ${
respuestas[casoActivo.id] === 'desajuste' ? 'text-green-800' : 'text-amber-800'
}`}>
{respuestas[casoActivo.id] === 'desajuste' ? '¡Correcto!' : 'Revisa la respuesta'}
</h4>
</div>
<p className="text-gray-700 mb-4">
{respuestas[casoActivo.id] === 'desajuste'
? 'Los controles de precio siempre crean desajustes: precios máximos generan escasez, precios mínimos generan superávits.'
: 'Recuerda: los controles de precio fijados fuera del equilibrio siempre crean desajustes entre oferta y demanda, generando ineficiencias.'
}
</p>
<button
onClick={() => setCasoActivo(null)}
className="w-full py-2 px-4 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition-colors"
>
Explorar otro caso
</button>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</>
);
})()}
</div>
)}
{/* Barra de progreso */}
<div className="mt-8 pt-6 border-t border-gray-200">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">Tu progreso</span>
<span className="text-sm text-gray-500">{casosCompletados.size} de 4 casos completados</span>
</div>
<div className="w-full h-3 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-indigo-500 to-purple-600"
initial={{ width: 0 }}
animate={{ width: `${(casosCompletados.size / 4) * 100}%` }}
transition={{ duration: 0.5 }}
/>
</div>
{casosCompletados.size >= 4 && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg text-center"
>
<div className="flex items-center justify-center gap-2 mb-2">
<Users className="w-5 h-5 text-green-600" />
<span className="font-bold text-green-800">¡Felicidades! Has completado todos los casos</span>
</div>
<p className="text-sm text-green-700">
Ahora comprendes mejor las consecuencias reales de los controles de precio en diferentes contextos.
</p>
</motion.div>
)}
</div>
</div>
);
};
export default ControlesVidaReal;

View File

@@ -0,0 +1,368 @@
import React, { useState, useRef, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { LineChart, Check, X, RotateCcw, Trophy, HelpCircle } from 'lucide-react';
interface Punto {
x: number;
y: number;
id: string;
}
interface CurvaDemandaConstructorProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
const GRID_SIZE = 350;
const PADDING = 50;
const MAX_PRECIO = 100;
const MAX_CANTIDAD = 100;
export const CurvaDemandaConstructor: React.FC<CurvaDemandaConstructorProps> = ({
ejercicioId: _ejercicioId,
onComplete
}) => {
const [puntos, setPuntos] = useState<Punto[]>([]);
const [mensaje, setMensaje] = useState<string>('');
const [showSuccess, setShowSuccess] = useState(false);
const [score, setScore] = useState(0);
const [intentos, setIntentos] = useState(0);
const svgRef = useRef<SVGSVGElement>(null);
const cartesianToSvg = useCallback((x: number, y: number) => {
const svgX = PADDING + (x / MAX_CANTIDAD) * GRID_SIZE;
const svgY = PADDING + GRID_SIZE - (y / MAX_PRECIO) * GRID_SIZE;
return { x: svgX, y: svgY };
}, []);
const svgToCartesian = useCallback((svgX: number, svgY: number) => {
const x = ((svgX - PADDING) / GRID_SIZE) * MAX_CANTIDAD;
const y = ((PADDING + GRID_SIZE - svgY) / GRID_SIZE) * MAX_PRECIO;
return {
x: Math.max(0, Math.min(MAX_CANTIDAD, Math.round(x))),
y: Math.max(0, Math.min(MAX_PRECIO, Math.round(y)))
};
}, []);
const handleSvgClick = (e: React.MouseEvent<SVGSVGElement>) => {
if (showSuccess) return;
const rect = svgRef.current?.getBoundingClientRect();
if (!rect) return;
const svgX = e.clientX - rect.left;
const svgY = e.clientY - rect.top;
const cartesian = svgToCartesian(svgX, svgY);
if (puntos.length >= 5) {
setMensaje('Máximo 5 puntos permitidos');
return;
}
const newPoint: Punto = {
x: cartesian.x,
y: cartesian.y,
id: `point-${Date.now()}-${Math.random()}`
};
setPuntos(prev => [...prev, newPoint]);
setMensaje('');
};
const calcularPendiente = (puntos: Punto[]): number | null => {
if (puntos.length < 2) return null;
const sorted = [...puntos].sort((a, b) => a.x - b.x);
const first = sorted[0];
const last = sorted[sorted.length - 1];
if (last.x === first.x) return 0;
return (last.y - first.y) / (last.x - first.x);
};
const validarCurva = () => {
setIntentos(prev => prev + 1);
if (puntos.length < 2) {
setMensaje('Necesitas al menos 2 puntos para trazar una curva de demanda');
return;
}
const pendiente = calcularPendiente(puntos);
if (pendiente === null) return;
if (pendiente >= 0) {
setMensaje('¡Incorrecto! La curva de demanda debe tener pendiente NEGATIVA (bajar de izquierda a derecha)');
return;
}
// Calcular puntuación basada en intentos
let puntuacion = 100;
if (intentos >= 1) puntuacion -= 20;
if (intentos >= 2) puntuacion -= 20;
puntuacion = Math.max(puntuacion, 40);
setScore(puntuacion);
setMensaje('');
setShowSuccess(true);
setTimeout(() => {
if (onComplete) {
onComplete(puntuacion);
}
}, 2000);
};
const reiniciar = () => {
setPuntos([]);
setMensaje('');
setShowSuccess(false);
setScore(0);
setIntentos(0);
};
const eliminarPunto = (id: string) => {
setPuntos(prev => prev.filter(p => p.id !== id));
};
const renderLineaCurva = () => {
if (puntos.length < 2) return null;
const sorted = [...puntos].sort((a, b) => a.x - b.x);
const points = sorted.map(p => {
const svg = cartesianToSvg(p.x, p.y);
return `${svg.x},${svg.y}`;
}).join(' ');
return (
<polyline
points={points}
fill="none"
stroke="#3b82f6"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
/>
);
};
return (
<div className="w-full max-w-4xl 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">
<LineChart className="w-8 h-8 text-blue-600" />
<h2 className="text-2xl font-bold text-gray-800">Constructor de Curva de Demanda</h2>
</div>
<button
onClick={reiniciar}
className="p-2 text-gray-500 hover:text-blue-600 transition-colors"
>
<RotateCcw className="w-5 h-5" />
</button>
</div>
<p className="text-gray-600">
Haz clic en el gráfico para colocar puntos que formen una curva de demanda con pendiente negativa.
La demanda debe descender de izquierda a derecha.
</p>
</div>
<div className="mb-4 p-3 bg-blue-50 rounded-lg flex items-center gap-2">
<HelpCircle className="w-5 h-5 text-blue-600" />
<span className="text-sm text-blue-700">
<strong>Instrucción:</strong> Coloca al menos 2 puntos formando una línea descendente.
Haz clic en un punto para eliminarlo.
</span>
</div>
<div className="flex flex-col md:flex-row gap-6">
<div className="flex-1 flex justify-center">
<svg
ref={svgRef}
width={GRID_SIZE + 2 * PADDING}
height={GRID_SIZE + 2 * PADDING}
className="border-2 border-gray-300 rounded-lg bg-white cursor-crosshair"
onClick={handleSvgClick}
>
{/* Grid */}
{Array.from({ length: 11 }).map((_, i) => (
<g key={i}>
<line
x1={PADDING + (i * GRID_SIZE) / 10}
y1={PADDING}
x2={PADDING + (i * GRID_SIZE) / 10}
y2={PADDING + GRID_SIZE}
stroke="#e5e7eb"
strokeWidth="1"
/>
<line
x1={PADDING}
y1={PADDING + (i * GRID_SIZE) / 10}
x2={PADDING + GRID_SIZE}
y2={PADDING + (i * GRID_SIZE) / 10}
stroke="#e5e7eb"
strokeWidth="1"
/>
</g>
))}
{/* Ejes */}
<line
x1={PADDING}
y1={PADDING + GRID_SIZE}
x2={PADDING + GRID_SIZE}
y2={PADDING + GRID_SIZE}
stroke="#374151"
strokeWidth="2"
/>
<line
x1={PADDING}
y1={PADDING}
x2={PADDING}
y2={PADDING + GRID_SIZE}
stroke="#374151"
strokeWidth="2"
/>
{/* Labels ejes */}
<text x={PADDING + GRID_SIZE / 2} y={PADDING + GRID_SIZE + 30} textAnchor="middle" className="text-sm fill-gray-600 font-medium">
Cantidad (Q)
</text>
<text x={20} y={PADDING + GRID_SIZE / 2} textAnchor="middle" transform={`rotate(-90, 20, ${PADDING + GRID_SIZE / 2})`} className="text-sm fill-gray-600 font-medium">
Precio (P)
</text>
{/* Marcas de ejes */}
{Array.from({ length: 6 }).map((_, i) => (
<g key={`marks-${i}`}>
<text x={PADDING + (i * GRID_SIZE) / 5} y={PADDING + GRID_SIZE + 18} textAnchor="middle" className="text-xs fill-gray-400">
{i * 20}
</text>
<text x={PADDING - 10} y={PADDING + GRID_SIZE - (i * GRID_SIZE) / 5 + 4} textAnchor="end" className="text-xs fill-gray-400">
{i * 20}
</text>
</g>
))}
{/* Curva */}
{renderLineaCurva()}
{/* Puntos */}
{puntos.map(punto => {
const svg = cartesianToSvg(punto.x, punto.y);
return (
<motion.g key={punto.id} initial={{ scale: 0 }} animate={{ scale: 1 }}>
<circle
cx={svg.x}
cy={svg.y}
r="8"
fill="#3b82f6"
stroke="white"
strokeWidth="2"
className="cursor-pointer hover:r-10"
onClick={(e) => {
e.stopPropagation();
eliminarPunto(punto.id);
}}
/>
<text x={svg.x} y={svg.y - 12} textAnchor="middle" className="text-xs fill-gray-500">
({punto.x}, {punto.y})
</text>
</motion.g>
);
})}
{/* Flecha indicando pendiente descendente */}
{puntos.length >= 2 && (
<g>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#6b7280" />
</marker>
</defs>
<text x={PADDING + GRID_SIZE - 80} y={PADDING + 40} className="text-xs fill-gray-400">
Pendiente -
</text>
</g>
)}
</svg>
</div>
<div className="w-full md:w-72 space-y-4">
<div className="p-4 bg-gray-50 rounded-lg">
<h3 className="font-semibold text-gray-700 mb-2">Progreso</h3>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Puntos colocados:</span>
<span className="font-medium text-blue-600">{puntos.length}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Intentos:</span>
<span className="font-medium text-gray-800">{intentos}</span>
</div>
{showSuccess && (
<div className="flex justify-between text-sm">
<span className="text-gray-600">Puntuación:</span>
<span className="font-medium text-green-600">{score}/100</span>
</div>
)}
</div>
</div>
{mensaje && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className={`p-3 border rounded-lg flex items-start gap-2 ${
mensaje.includes('Correcto') || mensaje.includes('Excelente')
? 'bg-green-50 border-green-200'
: 'bg-red-50 border-red-200'
}`}
>
{mensaje.includes('Correcto') || mensaje.includes('Excelente') ? (
<Check className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
) : (
<X className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
)}
<p className={`text-sm ${
mensaje.includes('Correcto') || mensaje.includes('Excelente')
? 'text-green-700'
: 'text-red-700'
}`}>{mensaje}</p>
</motion.div>
)}
{!showSuccess && (
<button
onClick={validarCurva}
disabled={puntos.length < 2}
className="w-full py-3 px-4 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2"
>
<Check className="w-5 h-5" />
Validar Curva
</button>
)}
<AnimatePresence>
{showSuccess && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="p-4 bg-green-50 border border-green-200 rounded-lg text-center"
>
<Trophy className="w-8 h-8 text-green-500 mx-auto mb-2" />
<p className="font-semibold text-green-700">¡Excelente!</p>
<p className="text-sm text-green-600 mt-1">
Has trazado correctamente una curva de demanda con pendiente negativa.
</p>
<div className="mt-3 text-2xl font-bold text-green-700">
{score}/100
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
);
};
export default CurvaDemandaConstructor;

View File

@@ -0,0 +1,451 @@
import React, { useState, useRef, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { TrendingUp, Check, X, RotateCcw, Trophy, HelpCircle } from 'lucide-react';
interface Punto {
x: number;
y: number;
id: string;
}
interface CurvaOfertaConstructorProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
const GRID_SIZE = 350;
const PADDING = 50;
const MAX_PRECIO = 100;
const MAX_CANTIDAD = 100;
export const CurvaOfertaConstructor: React.FC<CurvaOfertaConstructorProps> = ({
onComplete,
ejercicioId: _ejercicioId
}) => {
const [puntos, setPuntos] = useState<Punto[]>([]);
const [mensaje, setMensaje] = useState<string>('');
const [showSuccess, setShowSuccess] = useState(false);
const [score, setScore] = useState(0);
const [intentos, setIntentos] = useState(0);
const [completado, setCompletado] = useState(false);
const svgRef = useRef<SVGSVGElement>(null);
const [draggedPoint, setDraggedPoint] = useState<string | null>(null);
const cartesianToSvg = useCallback((x: number, y: number) => {
const svgX = PADDING + (x / MAX_CANTIDAD) * GRID_SIZE;
const svgY = PADDING + GRID_SIZE - (y / MAX_PRECIO) * GRID_SIZE;
return { x: svgX, y: svgY };
}, []);
const svgToCartesian = useCallback((svgX: number, svgY: number) => {
const x = ((svgX - PADDING) / GRID_SIZE) * MAX_CANTIDAD;
const y = ((PADDING + GRID_SIZE - svgY) / GRID_SIZE) * MAX_PRECIO;
return {
x: Math.max(0, Math.min(MAX_CANTIDAD, Math.round(x))),
y: Math.max(0, Math.min(MAX_PRECIO, Math.round(y)))
};
}, []);
const handleSvgClick = (e: React.MouseEvent<SVGSVGElement>) => {
if (draggedPoint || completado) return;
const rect = svgRef.current?.getBoundingClientRect();
if (!rect) return;
const svgX = e.clientX - rect.left;
const svgY = e.clientY - rect.top;
const cartesian = svgToCartesian(svgX, svgY);
if (puntos.length >= 5) {
setMensaje('Máximo 5 puntos permitidos');
return;
}
const newPoint: Punto = {
x: cartesian.x,
y: cartesian.y,
id: `point-${Date.now()}-${Math.random()}`
};
setPuntos(prev => [...prev, newPoint]);
setMensaje('');
};
const handlePointDrag = (pointId: string) => {
setDraggedPoint(pointId);
};
const handlePointMove = (e: React.MouseEvent<SVGSVGElement>) => {
if (!draggedPoint) return;
const rect = svgRef.current?.getBoundingClientRect();
if (!rect) return;
const svgX = e.clientX - rect.left;
const svgY = e.clientY - rect.top;
const cartesian = svgToCartesian(svgX, svgY);
setPuntos(prev =>
prev.map(p => p.id === draggedPoint ? { ...p, x: cartesian.x, y: cartesian.y } : p)
);
};
const handlePointUp = () => {
setDraggedPoint(null);
};
const calcularPendiente = (): number | null => {
if (puntos.length < 2) return null;
const sorted = [...puntos].sort((a, b) => a.x - b.x);
const first = sorted[0];
const last = sorted[sorted.length - 1];
if (last.x === first.x) return 0;
return (last.y - first.y) / (last.x - first.x);
};
const validarCurva = () => {
if (puntos.length < 2) {
setMensaje('Necesitas al menos 2 puntos para trazar la curva de oferta');
return;
}
const pendiente = calcularPendiente();
if (pendiente === null) return;
setIntentos(prev => prev + 1);
if (pendiente <= 0) {
setMensaje('¡Incorrecto! La curva de oferta debe tener pendiente POSITIVA (subir de izquierda a derecha)');
// Penalización por intentos
if (intentos >= 2) {
setScore(Math.max(0, 60 - (intentos - 2) * 10));
}
return;
}
// Curva correcta
const puntosBonus = puntos.length >= 3 ? 10 : 0;
const intentosBonus = intentos === 0 ? 30 : intentos === 1 ? 20 : 10;
const puntajeFinal = Math.min(100, 60 + puntosBonus + intentosBonus);
setScore(puntajeFinal);
setMensaje('');
setShowSuccess(true);
setCompletado(true);
setTimeout(() => {
if (onComplete) {
onComplete(puntajeFinal);
}
}, 2000);
};
const reiniciar = () => {
setPuntos([]);
setMensaje('');
setShowSuccess(false);
setScore(0);
setIntentos(0);
setCompletado(false);
};
const eliminarPunto = (id: string) => {
setPuntos(prev => prev.filter(p => p.id !== id));
};
const renderLineaCurva = () => {
if (puntos.length < 2) return null;
const sorted = [...puntos].sort((a, b) => a.x - b.x);
const points = sorted.map(p => {
const svg = cartesianToSvg(p.x, p.y);
return `${svg.x},${svg.y}`;
}).join(' ');
return (
<motion.polyline
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.5 }}
points={points}
fill="none"
stroke="#22c55e"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
/>
);
};
// Puntos guía esperados para la curva de oferta
const puntosGuia = [
{ x: 20, y: 20 },
{ x: 50, y: 40 },
{ x: 80, y: 70 }
];
return (
<div className="w-full max-w-4xl 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">
<TrendingUp className="w-8 h-8 text-green-600" />
<h2 className="text-2xl font-bold text-gray-800">Constructor de Curva de Oferta</h2>
</div>
<div className="flex items-center gap-4">
{completado && (
<span className="text-2xl font-bold text-green-600">{score} pts</span>
)}
<button
onClick={reiniciar}
className="p-2 text-gray-500 hover:text-green-600 transition-colors"
>
<RotateCcw className="w-5 h-5" />
</button>
</div>
</div>
<p className="text-gray-600">
Coloca puntos en el gráfico para trazar una curva de oferta con pendiente POSITIVA.
Recuerda: a mayor precio, mayor cantidad ofrecida.
</p>
</div>
<div className="mb-4 p-3 bg-green-50 rounded-lg flex items-center gap-2">
<HelpCircle className="w-5 h-5 text-green-600" />
<span className="text-sm text-green-700">
<strong>Instrucciones:</strong> Haz clic en el gráfico para colocar puntos.
La curva debe subir de izquierda a derecha (pendiente positiva).
Arrastra los puntos para ajustar su posición. Haz clic en un punto para eliminarlo.
</span>
</div>
<div className="flex flex-col md:flex-row gap-6">
<div className="flex-1">
<svg
ref={svgRef}
width={GRID_SIZE + 2 * PADDING}
height={GRID_SIZE + 2 * PADDING}
className="border-2 border-gray-300 rounded-lg bg-white cursor-crosshair mx-auto"
onClick={handleSvgClick}
onMouseMove={handlePointMove}
onMouseUp={handlePointUp}
onMouseLeave={handlePointUp}
>
{/* Grid */}
{Array.from({ length: 11 }).map((_, i) => (
<g key={i}>
<line
x1={PADDING + (i * GRID_SIZE) / 10}
y1={PADDING}
x2={PADDING + (i * GRID_SIZE) / 10}
y2={PADDING + GRID_SIZE}
stroke="#e5e7eb"
strokeWidth="1"
/>
<line
x1={PADDING}
y1={PADDING + (i * GRID_SIZE) / 10}
x2={PADDING + GRID_SIZE}
y2={PADDING + (i * GRID_SIZE) / 10}
stroke="#e5e7eb"
strokeWidth="1"
/>
</g>
))}
{/* Ejes */}
<line
x1={PADDING}
y1={PADDING + GRID_SIZE}
x2={PADDING + GRID_SIZE}
y2={PADDING + GRID_SIZE}
stroke="#374151"
strokeWidth="2"
/>
<line
x1={PADDING}
y1={PADDING}
x2={PADDING}
y2={PADDING + GRID_SIZE}
stroke="#374151"
strokeWidth="2"
/>
{/* Labels ejes */}
<text x={PADDING + GRID_SIZE / 2} y={PADDING + GRID_SIZE + 30} textAnchor="middle" className="text-sm fill-gray-600 font-medium">
Cantidad (Q)
</text>
<text x={20} y={PADDING + GRID_SIZE / 2} textAnchor="middle" transform={`rotate(-90, 20, ${PADDING + GRID_SIZE / 2})`} className="text-sm fill-gray-600 font-medium">
Precio (P)
</text>
{/* Valores en ejes */}
{[0, 25, 50, 75, 100].map((val) => (
<g key={val}>
<text x={PADDING + (val / 100) * GRID_SIZE} y={PADDING + GRID_SIZE + 15} textAnchor="middle" className="text-xs fill-gray-500">
{val}
</text>
<text x={PADDING - 10} y={PADDING + GRID_SIZE - (val / 100) * GRID_SIZE + 4} textAnchor="end" className="text-xs fill-gray-500">
{val}
</text>
</g>
))}
{/* Línea de tendencia esperada (dotted, muy sutil) */}
{!completado && (
<line
x1={PADDING + (20 / 100) * GRID_SIZE}
y1={PADDING + GRID_SIZE - (20 / 100) * GRID_SIZE}
x2={PADDING + (80 / 100) * GRID_SIZE}
y2={PADDING + GRID_SIZE - (80 / 100) * GRID_SIZE}
stroke="#dcfce7"
strokeWidth="2"
strokeDasharray="5,5"
opacity="0.5"
/>
)}
{/* Curva */}
{renderLineaCurva()}
{/* Puntos */}
{puntos.map((punto, index) => {
const svg = cartesianToSvg(punto.x, punto.y);
return (
<motion.g
key={punto.id}
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
>
<circle
cx={svg.x}
cy={svg.y}
r="10"
fill="#22c55e"
stroke="white"
strokeWidth="3"
className="cursor-move hover:r-12"
onMouseDown={() => handlePointDrag(punto.id)}
onClick={(e) => {
e.stopPropagation();
eliminarPunto(punto.id);
}}
/>
<text x={svg.x} y={svg.y - 15} textAnchor="middle" className="text-xs fill-gray-600 font-medium">
P{index + 1}({punto.x}, {punto.y})
</text>
</motion.g>
);
})}
{/* Etiqueta S */}
{puntos.length >= 2 && (
<text
x={PADDING + GRID_SIZE + 10}
y={PADDING + 30}
className="text-lg fill-green-600 font-bold"
>
S
</text>
)}
</svg>
</div>
<div className="w-full md:w-72 space-y-4">
<div className="p-4 bg-gray-50 rounded-lg">
<h3 className="font-semibold text-gray-700 mb-3">Progreso</h3>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Puntos colocados:</span>
<span className="font-semibold text-green-600">{puntos.length}/5</span>
</div>
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-green-600"
initial={{ width: 0 }}
animate={{ width: `${(puntos.length / 5) * 100}%` }}
/>
</div>
{intentos > 0 && (
<div className="text-sm text-gray-600">
Intentos: <span className="font-semibold">{intentos}</span>
</div>
)}
</div>
</div>
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<h3 className="font-semibold text-blue-800 mb-2">Recuerda:</h3>
<ul className="text-sm text-blue-700 space-y-1">
<li className="flex items-start gap-2">
<span className="text-blue-500"></span>
La oferta tiene pendiente POSITIVA
</li>
<li className="flex items-start gap-2">
<span className="text-blue-500"></span>
Subir de izquierda a derecha
</li>
<li className="flex items-start gap-2">
<span className="text-blue-500"></span>
Precio Cantidad
</li>
</ul>
</div>
{mensaje && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="p-3 bg-red-50 border border-red-200 rounded-lg flex items-start gap-2"
>
<X className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-700">{mensaje}</p>
</motion.div>
)}
<button
onClick={validarCurva}
disabled={completado || puntos.length < 2}
className="w-full py-3 px-4 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2"
>
<Check className="w-5 h-5" />
Validar Curva
</button>
<AnimatePresence>
{showSuccess && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="p-4 bg-green-50 border border-green-200 rounded-lg text-center"
>
<Trophy className="w-8 h-8 text-green-500 mx-auto mb-2" />
<p className="font-semibold text-green-700">¡Excelente!</p>
<p className="text-sm text-green-600">
Has trazado correctamente la curva de oferta
</p>
<p className="text-lg font-bold text-green-700 mt-2">{score} puntos</p>
</motion.div>
)}
</AnimatePresence>
{completado && (
<div className="p-3 bg-gray-100 rounded-lg text-sm text-gray-600">
<strong>Puntuación:</strong>
<ul className="mt-1 space-y-1">
<li> Base: 60 puntos</li>
<li> +3 puntos: 10 pts</li>
<li> Primer intento: 30 pts</li>
</ul>
</div>
)}
</div>
</div>
</div>
);
};
export default CurvaOfertaConstructor;

View File

@@ -0,0 +1,260 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Users, Plus, Check, X, Trophy, RotateCcw, ArrowRight } from 'lucide-react';
interface DemandaIndividualVsMercadoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Consumidor {
id: string;
nombre: string;
cantidad: number;
}
const consumidores: Consumidor[] = [
{ id: 'ana', nombre: 'Ana', cantidad: 5 },
{ id: 'beto', nombre: 'Beto', cantidad: 3 },
{ id: 'carlos', nombre: 'Carlos', cantidad: 7 },
{ id: 'diana', nombre: 'Diana', cantidad: 4 },
];
export const DemandaIndividualVsMercado: React.FC<DemandaIndividualVsMercadoProps> = ({
ejercicioId: _ejercicioId,
onComplete
}) => {
const [respuestaUsuario, setRespuestaUsuario] = useState('');
const [mostrarResultado, setMostrarResultado] = useState(false);
const [score, setScore] = useState(0);
const [intentos, setIntentos] = useState(0);
const [completado, setCompletado] = useState(false);
const [mostrarExplicacion, setMostrarExplicacion] = useState(false);
const demandaTotal = consumidores.reduce((sum, c) => sum + c.cantidad, 0);
const validarRespuesta = () => {
if (respuestaUsuario === '') {
alert('Por favor ingresa tu respuesta');
return;
}
setIntentos(prev => prev + 1);
setMostrarResultado(true);
const respuestaNum = parseInt(respuestaUsuario);
const esCorrecta = respuestaNum === demandaTotal;
let puntuacion = esCorrecta ? 100 : 0;
// Si está cerca (±2), dar puntuación parcial
if (!esCorrecta && Math.abs(respuestaNum - demandaTotal) <= 2) {
puntuacion = 50;
}
// Penalización por intentos
if (intentos >= 1) puntuacion -= 10;
if (intentos >= 2) puntuacion -= 10;
puntuacion = Math.max(puntuacion, 10);
setScore(puntuacion);
if (esCorrecta) {
setCompletado(true);
}
};
const handleFinalizar = () => {
if (onComplete) {
onComplete(score);
}
};
const reiniciar = () => {
setRespuestaUsuario('');
setMostrarResultado(false);
setMostrarExplicacion(false);
setScore(0);
setCompletado(false);
};
const handleSiguienteIntento = () => {
setRespuestaUsuario('');
setMostrarResultado(false);
};
return (
<div className="w-full max-w-3xl 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">
<Users className="w-8 h-8 text-blue-600" />
<h2 className="text-2xl font-bold text-gray-800">Demanda Individual vs. Mercado</h2>
</div>
<button
onClick={reiniciar}
className="p-2 text-gray-500 hover:text-blue-600 transition-colors"
>
<RotateCcw className="w-5 h-5" />
</button>
</div>
<p className="text-gray-600">
La demanda de mercado es la suma horizontal de todas las demandas individuales.
Calcula la cantidad total demandada sumando las cantidades de todos los consumidores.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
{/* Tarjetas de consumidores */}
<div className="space-y-3">
<h3 className="font-semibold text-gray-700 mb-3">Demandas Individuales</h3>
{consumidores.map((consumidor, index) => (
<motion.div
key={consumidor.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className="flex items-center justify-between p-4 bg-blue-50 rounded-lg border-2 border-blue-100"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold">
{consumidor.nombre[0]}
</div>
<span className="font-medium text-gray-800">{consumidor.nombre}</span>
</div>
<div className="text-right">
<span className="text-sm text-gray-500">Demanda:</span>
<span className="ml-2 font-bold text-blue-600">{consumidor.cantidad} unidades</span>
</div>
</motion.div>
))}
</div>
{/* Visualización de la suma */}
<div className="flex flex-col items-center justify-center p-6 bg-gray-50 rounded-lg">
<h3 className="font-semibold text-gray-700 mb-4">Suma de Demandas</h3>
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
{consumidores.map((consumidor, index) => (
<React.Fragment key={consumidor.id}>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: index * 0.2 }}
className="px-3 py-2 bg-blue-100 text-blue-700 rounded-lg font-bold"
>
{consumidor.cantidad}
</motion.div>
{index < consumidores.length - 1 && (
<Plus className="w-5 h-5 text-gray-400" />
)}
</React.Fragment>
))}
</div>
<ArrowRight className="w-6 h-6 text-gray-400 mb-4 rotate-90 md:rotate-0" />
<div className="w-full max-w-xs">
<label className="block text-sm font-medium text-gray-700 mb-2">
Demanda Total del Mercado:
</label>
<div className="flex items-center gap-2">
<input
type="number"
value={respuestaUsuario}
onChange={(e) => setRespuestaUsuario(e.target.value)}
disabled={mostrarResultado}
placeholder="¿Cuánto suma?"
className={`flex-1 px-4 py-3 border-2 rounded-lg text-center text-lg font-bold focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all ${
mostrarResultado
? parseInt(respuestaUsuario) === demandaTotal
? 'border-green-500 bg-green-50 text-green-700'
: 'border-red-500 bg-red-50 text-red-700'
: 'border-gray-300 focus:border-blue-500'
}`}
/>
<span className="text-gray-600 font-medium">unidades</span>
</div>
</div>
</div>
</div>
<AnimatePresence>
{mostrarResultado && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mb-6 overflow-hidden"
>
<div className={`p-4 rounded-lg border ${
parseInt(respuestaUsuario) === demandaTotal
? 'bg-green-50 border-green-200'
: 'bg-red-50 border-red-200'
}`}>
<div className="flex items-start gap-3 mb-3">
{parseInt(respuestaUsuario) === demandaTotal ? (
<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 ${
parseInt(respuestaUsuario) === demandaTotal ? 'text-green-800' : 'text-red-800'
}`}>
{parseInt(respuestaUsuario) === demandaTotal
? '¡Correcto! Has calculado correctamente la demanda de mercado'
: 'Respuesta incorrecta'}
</p>
<p className="text-sm mt-1 text-gray-700">
{parseInt(respuestaUsuario) === demandaTotal
? `La demanda total es ${demandaTotal} unidades (${consumidores.map(c => c.cantidad).join(' + ')}).`
: `La respuesta correcta es ${demandaTotal} unidades. Sumaste ${respuestaUsuario}.`}
</p>
</div>
</div>
<div className="flex items-center gap-4 mt-3 pt-3 border-t border-gray-200">
<Trophy className="w-5 h-5 text-yellow-500" />
<span className="font-bold text-gray-800">Puntuación: {score}/100</span>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="flex flex-wrap gap-4">
{!mostrarResultado ? (
<button
onClick={validarRespuesta}
disabled={respuestaUsuario === ''}
className="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
>
<Check className="w-5 h-5" />
Validar Respuesta
</button>
) : (
<>
{!completado && (
<button
onClick={handleSiguienteIntento}
className="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center gap-2"
>
<RotateCcw className="w-5 h-5" />
Intentar de Nuevo
</button>
)}
<button
onClick={handleFinalizar}
className="px-6 py-3 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 transition-colors flex items-center gap-2"
>
Finalizar Ejercicio
</button>
</>
)}
</div>
{intentos > 0 && (
<div className="mt-4 text-sm text-gray-500">
Intentos realizados: {intentos}
</div>
)}
</div>
);
};
export default DemandaIndividualVsMercado;

View File

@@ -0,0 +1,336 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ArrowRightLeft, MoveHorizontal, DollarSign, Check, X, Trophy, RotateCcw } from 'lucide-react';
interface DesplazamientoVsMovimientoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Escenario {
id: number;
situacion: string;
tipo: 'movimiento' | 'desplazamiento';
explicacion: string;
pista: string;
}
const escenarios: Escenario[] = [
{
id: 1,
situacion: "El precio de las manzanas sube de $2 a $4 por kilo. ¿Qué ocurre con la cantidad demandada de manzanas?",
tipo: 'movimiento',
explicacion: "Cambio en el precio del propio bien = MOVIMIENTO a lo largo de la curva. La cantidad demandada disminuye.",
pista: "¿Cambió el precio del propio bien o un factor externo?"
},
{
id: 2,
situacion: "Los ingresos de los consumidores aumentan. ¿Qué ocurre con la demanda de restaurantes?",
tipo: 'desplazamiento',
explicacion: "Cambio en ingresos = DESPLAZAMIENTO de la curva. La demanda aumenta (la curva se mueve a la derecha).",
pista: "El ingreso es un factor externo que desplaza toda la curva."
},
{
id: 3,
situacion: "Una heladería baja sus precios en verano. ¿Qué ocurre con la cantidad demandada de helados?",
tipo: 'movimiento',
explicacion: "Cambio en el precio del bien = MOVIMIENTO a lo largo de la curva. La cantidad demandada aumenta.",
pista: "La heladería cambió sus precios, no un factor externo."
},
{
id: 4,
situacion: "Se espera que el precio de los autos suba el próximo mes. ¿Qué ocurre con la demanda de autos hoy?",
tipo: 'desplazamiento',
explicacion: "Expectativas futuras = DESPLAZAMIENTO de la curva. La gente compra antes, aumentando la demanda actual.",
pista: "Las expectativas son un factor que desplaza la curva, no el precio actual."
},
{
id: 5,
situacion: "Una campaña publicitaria exitosa promueve el consumo de aguacates. ¿Qué ocurre con la demanda?",
tipo: 'desplazamiento',
explicacion: "Cambio en gustos/preferencias = DESPLAZAMIENTO de la curva. La demanda aumenta.",
pista: "La publicidad afecta los gustos, desplazando la curva."
},
{
id: 6,
situacion: "El precio de las entradas al cine baja un 50%. ¿Qué ocurre con la cantidad demandada de entradas?",
tipo: 'movimiento',
explicacion: "Cambio en el precio del propio bien = MOVIMIENTO a lo largo de la curva. Más personas van al cine.",
pista: "¿El precio del bien mismo cambió? Entonces es un movimiento."
}
];
export const DesplazamientoVsMovimiento: React.FC<DesplazamientoVsMovimientoProps> = ({
ejercicioId: _ejercicioId,
onComplete
}) => {
const [escenarioActual, setEscenarioActual] = useState(0);
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<'movimiento' | 'desplazamiento' | null>(null);
const [mostrarFeedback, setMostrarFeedback] = useState(false);
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
const [completado, setCompletado] = useState(false);
const [mostrarPista, setMostrarPista] = useState(false);
const [usoPistas, setUsoPistas] = useState(0);
const escenario = escenarios[escenarioActual];
const esCorrecta = respuestaSeleccionada === escenario.tipo;
const handleSeleccionar = (tipo: 'movimiento' | 'desplazamiento') => {
if (mostrarFeedback) return;
setRespuestaSeleccionada(tipo);
};
const handleValidar = () => {
if (respuestaSeleccionada === null) return;
setMostrarFeedback(true);
if (esCorrecta) {
setRespuestasCorrectas(prev => prev + 1);
}
};
const handleSiguiente = () => {
if (escenarioActual < escenarios.length - 1) {
setEscenarioActual(prev => prev + 1);
setRespuestaSeleccionada(null);
setMostrarFeedback(false);
setMostrarPista(false);
} else {
setCompletado(true);
// Calcular puntuación
let puntuacion = Math.round((respuestasCorrectas + (esCorrecta ? 1 : 0)) / escenarios.length * 100);
// Penalización por uso de pistas
puntuacion -= usoPistas * 10;
puntuacion = Math.max(puntuacion, 0);
setTimeout(() => {
if (onComplete) {
onComplete(puntuacion);
}
}, 2000);
}
};
const handleMostrarPista = () => {
setMostrarPista(true);
setUsoPistas(prev => prev + 1);
};
const reiniciar = () => {
setEscenarioActual(0);
setRespuestaSeleccionada(null);
setMostrarFeedback(false);
setRespuestasCorrectas(0);
setCompletado(false);
setMostrarPista(false);
setUsoPistas(0);
};
if (completado) {
const puntuacionFinal = Math.max(0, Math.round((respuestasCorrectas / escenarios.length) * 100) - usoPistas * 10);
return (
<div className="w-full max-w-2xl mx-auto p-6 bg-white rounded-xl shadow-lg text-center">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="inline-flex items-center justify-center w-20 h-20 bg-green-100 rounded-full mb-4"
>
<Trophy className="w-10 h-10 text-green-600" />
</motion.div>
<h2 className="text-2xl font-bold text-gray-800 mb-2">¡Ejercicio Completado!</h2>
<p className="text-gray-600 mb-2">
Has identificado correctamente {respuestasCorrectas} de {escenarios.length} situaciones
</p>
{usoPistas > 0 && (
<p className="text-sm text-yellow-600 mb-4">Pistas utilizadas: {usoPistas} (-{usoPistas * 10} pts)</p>
)}
<div className="text-4xl font-bold text-blue-600 mb-2">{puntuacionFinal}/100</div>
<p className="text-sm text-gray-500">Puntuación final</p>
<button
onClick={reiniciar}
className="mt-6 px-6 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors flex items-center gap-2 mx-auto"
>
<RotateCcw className="w-4 h-4" />
Intentar de nuevo
</button>
</div>
);
}
return (
<div className="w-full max-w-3xl mx-auto p-6 bg-white rounded-xl shadow-lg">
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-800">Desplazamiento vs. Movimiento</h2>
<span className="text-sm text-gray-500">Situación {escenarioActual + 1} de {escenarios.length}</span>
</div>
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-blue-600"
initial={{ width: 0 }}
animate={{ width: `${((escenarioActual + 1) / escenarios.length) * 100}%` }}
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{/* Panel izquierdo: Situación */}
<div className="p-5 bg-blue-50 rounded-xl border-2 border-blue-100">
<h3 className="font-semibold text-blue-800 mb-3 flex items-center gap-2">
<DollarSign className="w-5 h-5" />
Situación
</h3>
<p className="text-gray-800 leading-relaxed">{escenario.situacion}</p>
{!mostrarPista && !mostrarFeedback && (
<button
onClick={handleMostrarPista}
className="mt-4 text-sm text-blue-600 hover:text-blue-800 underline"
>
¿Necesitas una pista? (-10 pts)
</button>
)}
{mostrarPista && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg"
>
<p className="text-sm text-yellow-800">
<strong>Pista:</strong> {escenario.pista}
</p>
</motion.div>
)}
</div>
{/* Panel derecho: Opciones */}
<div className="space-y-4">
<h3 className="font-semibold text-gray-700 mb-3">¿Qué tipo de cambio ocurre?</h3>
<motion.button
onClick={() => handleSeleccionar('movimiento')}
disabled={mostrarFeedback}
whileHover={!mostrarFeedback ? { scale: 1.02 } : {}}
whileTap={!mostrarFeedback ? { scale: 0.98 } : {}}
className={`w-full p-4 rounded-xl border-2 text-left transition-all ${
respuestaSeleccionada === 'movimiento'
? mostrarFeedback
? escenario.tipo === 'movimiento'
? 'border-green-500 bg-green-50'
: 'border-red-500 bg-red-50'
: 'border-blue-500 bg-blue-50'
: mostrarFeedback && escenario.tipo === 'movimiento'
? 'border-green-500 bg-green-50'
: 'border-gray-200 hover:border-blue-300 bg-white'
}`}
>
<div className="flex items-start gap-3">
<div className={`p-2 rounded-lg ${
mostrarFeedback && escenario.tipo === 'movimiento'
? 'bg-green-100 text-green-600'
: respuestaSeleccionada === 'movimiento' && mostrarFeedback
? 'bg-red-100 text-red-600'
: 'bg-blue-100 text-blue-600'
}`}>
<ArrowRightLeft className="w-6 h-6" />
</div>
<div>
<h4 className="font-semibold text-gray-800">Movimiento a lo largo de la curva</h4>
<p className="text-sm text-gray-600 mt-1">
Cambio en la cantidad demandada debido a un cambio en el precio del propio bien
</p>
</div>
</div>
</motion.button>
<motion.button
onClick={() => handleSeleccionar('desplazamiento')}
disabled={mostrarFeedback}
whileHover={!mostrarFeedback ? { scale: 1.02 } : {}}
whileTap={!mostrarFeedback ? { scale: 0.98 } : {}}
className={`w-full p-4 rounded-xl border-2 text-left transition-all ${
respuestaSeleccionada === 'desplazamiento'
? mostrarFeedback
? escenario.tipo === 'desplazamiento'
? 'border-green-500 bg-green-50'
: 'border-red-500 bg-red-50'
: 'border-blue-500 bg-blue-50'
: mostrarFeedback && escenario.tipo === 'desplazamiento'
? 'border-green-500 bg-green-50'
: 'border-gray-200 hover:border-blue-300 bg-white'
}`}
>
<div className="flex items-start gap-3">
<div className={`p-2 rounded-lg ${
mostrarFeedback && escenario.tipo === 'desplazamiento'
? 'bg-green-100 text-green-600'
: respuestaSeleccionada === 'desplazamiento' && mostrarFeedback
? 'bg-red-100 text-red-600'
: 'bg-purple-100 text-purple-600'
}`}>
<MoveHorizontal className="w-6 h-6" />
</div>
<div>
<h4 className="font-semibold text-gray-800">Desplazamiento de la curva</h4>
<p className="text-sm text-gray-600 mt-1">
Cambio en la demanda debido a factores externos (ingresos, gustos, expectativas, etc.)
</p>
</div>
</div>
</motion.button>
</div>
</div>
<AnimatePresence>
{mostrarFeedback && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mb-6 overflow-hidden"
>
<div className={`p-4 rounded-lg border ${
esCorrecta ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'
}`}>
<div className="flex items-start gap-3">
{esCorrecta ? (
<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 ${esCorrecta ? 'text-green-800' : 'text-red-800'}`}>
{esCorrecta ? '¡Correcto!' : 'Incorrecto'}
</p>
<p className="text-sm mt-1 text-gray-700">{escenario.explicacion}</p>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="flex justify-end">
{!mostrarFeedback ? (
<button
onClick={handleValidar}
disabled={respuestaSeleccionada === null}
className="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Validar Respuesta
</button>
) : (
<button
onClick={handleSiguiente}
className="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center gap-2"
>
{escenarioActual < escenarios.length - 1 ? 'Siguiente Situación' : 'Finalizar'}
</button>
)}
</div>
</div>
);
};
export default DesplazamientoVsMovimiento;

View File

@@ -0,0 +1,385 @@
import React, { useState } from 'react';
interface EscenarioElasticidad {
id: number;
producto: string;
descripcion: string;
precioInicial: number;
precioFinal: number;
cantidadInicial: number;
cantidadFinal: number;
categoriaCorrecta: 'elastica' | 'inelastica' | 'unitaria';
explicacion: string;
}
const escenarios: EscenarioElasticidad[] = [
{
id: 1,
producto: 'Medicamentos esenciales',
descripcion: 'El precio de antibióticos aumenta un 20% debido a escasez.',
precioInicial: 100,
precioFinal: 120,
cantidadInicial: 10000,
cantidadFinal: 9500,
categoriaCorrecta: 'inelastica',
explicacion: 'Los medicamentos esenciales tienen demanda inelástica porque son necesarios para la salud y no tienen sustitutos cercanos. La cantidad demandada disminuye muy poco (5%) a pesar del gran aumento de precio (20%).'
},
{
id: 2,
producto: 'Boletos de cine de lujo',
descripcion: 'Los cines VIP aumentan sus precios un 15%.',
precioInicial: 200,
precioFinal: 230,
cantidadInicial: 5000,
cantidadFinal: 3000,
categoriaCorrecta: 'elastica',
explicacion: 'El entretenimiento de lujo es elástico porque es un bien discrecional con muchos sustitutos (streaming, cines regulares, otras actividades). La cantidad demandada cae drásticamente (40%) ante un aumento moderado de precio.'
},
{
id: 3,
producto: 'Gasolina',
descripcion: 'El precio de la gasolina sube un 10% por impuestos.',
precioInicial: 50,
precioFinal: 55,
cantidadInicial: 100000,
cantidadFinal: 95000,
categoriaCorrecta: 'inelastica',
explicacion: 'La gasolina tiene demanda inelástica a corto plazo porque es necesaria para el transporte y muchos no pueden cambiar sus hábitos inmediatamente. La cantidad solo baja 5% pese al aumento de 10%.'
},
{
id: 4,
producto: 'Marca específica de cereal',
descripcion: 'Una marca de cereal aumenta su precio un 8% mientras las competidoras mantienen precios.',
precioInicial: 50,
precioFinal: 54,
cantidadInicial: 8000,
cantidadFinal: 4000,
categoriaCorrecta: 'elastica',
explicacion: 'Una marca específica de cereal tiene demanda muy elástica porque hay muchos sustitutos perfectos (otras marcas). Los consumidores cambian fácilmente de marca cuando sube el precio.'
},
{
id: 5,
producto: 'Sal marina gourmet',
descripcion: 'El precio de sal marina artesanal baja un 25% en promoción.',
precioInicial: 40,
precioFinal: 30,
cantidadInicial: 2000,
cantidadFinal: 2100,
categoriaCorrecta: 'inelastica',
explicacion: 'La sal es un bien básico con demanda muy inelástica. Aunque baje el precio, la cantidad demandada no aumenta mucho porque la gente solo consume la cantidad que necesita.'
}
];
export const ElasticidadElasticaInelastica: React.FC = () => {
const [escenarioActual, setEscenarioActual] = useState<number>(0);
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<string | null>(null);
const [resultado, setResultado] = useState<{
correcto: boolean;
mensaje: string;
mostrarExplicacion: boolean;
} | null>(null);
const [puntuacion, setPuntuacion] = useState<number>(0);
const [ejerciciosCompletados, setEjerciciosCompletados] = useState<number>(0);
const escenario = escenarios[escenarioActual];
const calcularElasticidad = (e: EscenarioElasticidad): number => {
const cambioCantidad = e.cantidadFinal - e.cantidadInicial;
const cambioPrecio = e.precioFinal - e.precioInicial;
const cantidadPromedio = (e.cantidadInicial + e.cantidadFinal) / 2;
const precioPromedio = (e.precioInicial + e.precioFinal) / 2;
if (cantidadPromedio === 0 || precioPromedio === 0) return 0;
return Math.abs((cambioCantidad / cantidadPromedio) / (cambioPrecio / precioPromedio));
};
const verificarRespuesta = (categoria: string) => {
if (resultado) return;
setRespuestaSeleccionada(categoria);
const correcto = categoria === escenario.categoriaCorrecta;
if (correcto) {
setPuntuacion(prev => prev + 1);
}
setResultado({
correcto,
mensaje: correcto
? '¡Correcto! Has identificado la elasticidad correctamente.'
: 'Incorrecto. Revisa el valor calculado de la elasticidad.',
mostrarExplicacion: true
});
setEjerciciosCompletados(prev => prev + 1);
};
const siguienteEjercicio = () => {
const siguiente = (escenarioActual + 1) % escenarios.length;
setEscenarioActual(siguiente);
setRespuestaSeleccionada(null);
setResultado(null);
};
const reiniciarEjercicios = () => {
setEscenarioActual(0);
setRespuestaSeleccionada(null);
setResultado(null);
setPuntuacion(0);
setEjerciciosCompletados(0);
};
const elasticidadCalculada = calcularElasticidad(escenario);
const cambioPrecioPorcentaje = ((escenario.precioFinal - escenario.precioInicial) / ((escenario.precioInicial + escenario.precioFinal) / 2) * 100);
const cambioCantidadPorcentaje = ((escenario.cantidadFinal - escenario.cantidadInicial) / ((escenario.cantidadInicial + escenario.cantidadFinal) / 2) * 100);
const getCategoriaColor = (cat: string) => {
switch (cat) {
case 'elastica': return 'bg-green-100 border-green-300 text-green-800';
case 'inelastica': return 'bg-amber-100 border-amber-300 text-amber-800';
case 'unitaria': return 'bg-blue-100 border-blue-300 text-blue-800';
default: return 'bg-gray-100 border-gray-300';
}
};
return (
<div className="max-w-4xl mx-auto p-6 bg-white rounded-lg shadow-md">
<div className="flex justify-between items-center mb-6">
<div>
<h2 className="text-2xl font-bold text-blue-800">Clasificación de Elasticidad</h2>
<p className="text-gray-600">Analiza cada escenario y clasifica la elasticidad de la demanda.</p>
</div>
<div className="text-right">
<div className="bg-blue-50 px-4 py-2 rounded-lg">
<p className="text-sm text-gray-600">Puntuación</p>
<p className="text-2xl font-bold text-blue-700">{puntuacion}/{ejerciciosCompletados}</p>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-indigo-50 to-purple-50 p-6 rounded-xl border border-indigo-200 mb-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-indigo-500 rounded-xl flex items-center justify-center text-white text-xl font-bold flex-shrink-0">
{escenario.id}
</div>
<div className="flex-1">
<h3 className="text-xl font-bold text-gray-800 mb-2">{escenario.producto}</h3>
<p className="text-gray-600 mb-4">{escenario.descripcion}</p>
<div className="grid grid-cols-2 gap-4 mb-4">
<div className="bg-white p-4 rounded-lg shadow-sm">
<p className="text-sm text-gray-500 mb-1">Precio Inicial</p>
<p className="text-xl font-bold text-blue-600">${escenario.precioInicial}</p>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm">
<p className="text-sm text-gray-500 mb-1">Precio Final</p>
<p className="text-xl font-bold text-blue-600">${escenario.precioFinal}</p>
<p className="text-xs text-gray-500 mt-1">
Cambio: {cambioPrecioPorcentaje > 0 ? '+' : ''}{cambioPrecioPorcentaje.toFixed(1)}%
</p>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm">
<p className="text-sm text-gray-500 mb-1">Cantidad Inicial</p>
<p className="text-xl font-bold text-green-600">{escenario.cantidadInicial.toLocaleString()}</p>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm">
<p className="text-sm text-gray-500 mb-1">Cantidad Final</p>
<p className="text-xl font-bold text-green-600">{escenario.cantidadFinal.toLocaleString()}</p>
<p className="text-xs text-gray-500 mt-1">
Cambio: {cambioCantidadPorcentaje > 0 ? '+' : ''}{cambioCantidadPorcentaje.toFixed(1)}%
</p>
</div>
</div>
<div className="bg-amber-50 p-4 rounded-lg border border-amber-200">
<p className="text-sm font-medium text-amber-800 mb-2">Datos calculados para ti:</p>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600">Elasticidad calculada:</span>
<span className="ml-2 font-bold text-indigo-600 text-lg">{elasticidadCalculada.toFixed(2)}</span>
</div>
<div>
<span className="text-gray-600">Ratio %Q / %P:</span>
<span className="ml-2 font-bold text-indigo-600">
{Math.abs(cambioCantidadPorcentaje / cambioPrecioPorcentaje).toFixed(2)}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="mb-6">
<h3 className="font-bold text-gray-800 mb-4 text-lg">¿Cómo clasificarías la elasticidad de la demanda?</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<button
onClick={() => verificarRespuesta('elastica')}
disabled={!!resultado}
className={`p-5 rounded-xl border-2 text-left transition-all ${
respuestaSeleccionada === 'elastica'
? escenario.categoriaCorrecta === 'elastica'
? 'bg-green-100 border-green-500 ring-2 ring-green-300'
: 'bg-red-100 border-red-500 ring-2 ring-red-300'
: 'bg-white border-gray-200 hover:border-green-300 hover:bg-green-50'
} ${!!resultado && escenario.categoriaCorrecta !== 'elastica' && respuestaSeleccionada !== 'elastica' ? 'opacity-50' : ''}`}
>
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
<span className="font-bold text-lg">Elástica</span>
</div>
<p className="text-sm text-gray-600">E<sub>d</sub> &gt; 1</p>
<p className="text-xs text-gray-500 mt-2">La cantidad cambia más que el precio</p>
</button>
<button
onClick={() => verificarRespuesta('unitaria')}
disabled={!!resultado}
className={`p-5 rounded-xl border-2 text-left transition-all ${
respuestaSeleccionada === 'unitaria'
? escenario.categoriaCorrecta === 'unitaria'
? 'bg-green-100 border-green-500 ring-2 ring-green-300'
: 'bg-red-100 border-red-500 ring-2 ring-red-300'
: 'bg-white border-gray-200 hover:border-blue-300 hover:bg-blue-50'
} ${!!resultado && escenario.categoriaCorrecta !== 'unitaria' && respuestaSeleccionada !== 'unitaria' ? 'opacity-50' : ''}`}
>
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14" />
</svg>
</div>
<span className="font-bold text-lg">Unitaria</span>
</div>
<p className="text-sm text-gray-600">E<sub>d</sub> = 1</p>
<p className="text-xs text-gray-500 mt-2">La cantidad cambia igual que el precio</p>
</button>
<button
onClick={() => verificarRespuesta('inelastica')}
disabled={!!resultado}
className={`p-5 rounded-xl border-2 text-left transition-all ${
respuestaSeleccionada === 'inelastica'
? escenario.categoriaCorrecta === 'inelastica'
? 'bg-green-100 border-green-500 ring-2 ring-green-300'
: 'bg-red-100 border-red-500 ring-2 ring-red-300'
: 'bg-white border-gray-200 hover:border-amber-300 hover:bg-amber-50'
} ${!!resultado && escenario.categoriaCorrecta !== 'inelastica' && respuestaSeleccionada !== 'inelastica' ? 'opacity-50' : ''}`}
>
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 bg-amber-100 rounded-full flex items-center justify-center">
<svg className="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6" />
</svg>
</div>
<span className="font-bold text-lg">Inelástica</span>
</div>
<p className="text-sm text-gray-600">E<sub>d</sub> &lt; 1</p>
<p className="text-xs text-gray-500 mt-2">La cantidad cambia menos que el precio</p>
</button>
</div>
</div>
{resultado && (
<div className={`p-6 rounded-xl border-2 mb-6 ${resultado.correcto ? 'bg-green-50 border-green-300' : 'bg-red-50 border-red-300'}`}>
<div className="flex items-start gap-4">
<div className={`p-3 rounded-full ${resultado.correcto ? 'bg-green-100' : 'bg-red-100'}`}>
{resultado.correcto ? (
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
</div>
<div className="flex-1">
<h4 className={`font-bold text-lg ${resultado.correcto ? 'text-green-800' : 'text-red-800'}`}>
{resultado.correcto ? '¡Respuesta Correcta!' : 'Respuesta Incorrecta'}
</h4>
<p className={`mt-1 ${resultado.correcto ? 'text-green-700' : 'text-red-700'}`}>
{resultado.mensaje}
</p>
<div className={`mt-4 p-4 rounded-lg border ${getCategoriaColor(escenario.categoriaCorrecta)}`}>
<p className="font-bold mb-2">
Respuesta correcta: {escenario.categoriaCorrecta === 'elastica' ? 'Elástica' : escenario.categoriaCorrecta === 'inelastica' ? 'Inelástica' : 'Unitaria'} (E<sub>d</sub> = {elasticidadCalculada.toFixed(2)})
</p>
<p className="text-sm">{escenario.explicacion}</p>
</div>
</div>
</div>
</div>
)}
{resultado && (
<div className="flex gap-4">
<button
onClick={siguienteEjercicio}
className="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors flex items-center justify-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
Siguiente Ejercicio
</button>
<button
onClick={reiniciarEjercicios}
className="bg-gray-600 hover:bg-gray-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Reiniciar
</button>
</div>
)}
<div className="mt-8 bg-gray-50 p-5 rounded-xl border border-gray-200">
<h4 className="font-bold text-gray-800 mb-4 flex items-center gap-2">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
Guía de Clasificación
</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white p-4 rounded-lg border-l-4 border-green-500">
<p className="font-bold text-green-700 mb-2">Elástica (E<sub>d</sub> &gt; 1)</p>
<ul className="text-sm text-gray-600 space-y-1">
<li> Lujos y bienes discrecionales</li>
<li> Muchos sustitutos disponibles</li>
<li> Consumo puede posponerse</li>
<li> Representa % grande del ingreso</li>
</ul>
</div>
<div className="bg-white p-4 rounded-lg border-l-4 border-blue-500">
<p className="font-bold text-blue-700 mb-2">Unitaria (E<sub>d</sub> = 1)</p>
<ul className="text-sm text-gray-600 space-y-1">
<li> Cambio proporcional exacto</li>
<li> Caso teórico ideal</li>
<li> Ingreso total constante</li>
</ul>
</div>
<div className="bg-white p-4 rounded-lg border-l-4 border-amber-500">
<p className="font-bold text-amber-700 mb-2">Inelástica (E<sub>d</sub> &lt; 1)</p>
<ul className="text-sm text-gray-600 space-y-1">
<li> Necesidades básicas</li>
<li> Pocos o ningún sustituto</li>
<li> Consumo indispensable</li>
<li> Representa % pequeño del ingreso</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default ElasticidadElasticaInelastica;

View File

@@ -0,0 +1,437 @@
import React, { useState } from 'react';
interface ProductoEscenario {
id: number;
nombre: string;
elasticidad: number;
precioInicial: number;
cantidadInicial: number;
precioActual: number;
cantidadActual: number;
descripcion: string;
}
const generarEscenario = (): ProductoEscenario => {
const elasticidades = [0.3, 0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0];
const elasticidad = elasticidades[Math.floor(Math.random() * elasticidades.length)];
const nombresProductos = [
'Medicamentos esenciales',
'Gasolina',
'Pan de caja',
'Leche',
'Cereal de marca',
'Entradas de cine',
'Restaurantes de lujo',
'Viajes internacionales',
'Yates',
'Diamantes'
];
const nombre = nombresProductos[Math.floor(Math.random() * nombresProductos.length)];
const precioInicial = Math.round((Math.random() * 200 + 20) * 100) / 100;
const cantidadInicial = Math.round(Math.random() * 5000 + 500);
const cambioPrecioPorcentaje = (Math.random() > 0.5 ? 1 : -1) * (Math.random() * 30 + 10);
const cambioCantidadPorcentaje = -elasticidad * cambioPrecioPorcentaje;
const precioActual = Math.round(precioInicial * (1 + cambioPrecioPorcentaje / 100) * 100) / 100;
const cantidadActual = Math.round(cantidadInicial * (1 + cambioCantidadPorcentaje / 100));
const descripciones: Record<string, string> = {
'Medicamentos esenciales': 'Bien de necesidad sin sustitutos. La demanda es extremadamente inelástica.',
'Gasolina': 'Necesidad a corto plazo con pocos sustitutos inmediatos.',
'Pan de caja': 'Bien básico con algunos sustitutos (pan artesanal, tortillas).',
'Leche': 'Necesidad básica aunque existen sustitutos (leche de almendra, soya).',
'Cereal de marca': 'Bien con muchos sustitutos de otras marcas.',
'Entradas de cine': 'Entretenimiento discrecional con alternativas (streaming).',
'Restaurantes de lujo': 'Bien de lujo altamente discrecional.',
'Viajes internacionales': 'Lujo con muchas alternativas de entretenimiento.',
'Yates': 'Bien de super lujo, demanda muy elástica.',
'Diamantes': 'Bien de lujo con demanda altamente sensible al precio.'
};
return {
id: Date.now(),
nombre,
elasticidad,
precioInicial: Math.max(1, precioInicial),
cantidadInicial: Math.max(10, cantidadInicial),
precioActual: Math.max(1, precioActual),
cantidadActual: Math.max(10, cantidadActual),
descripcion: descripciones[nombre] || 'Producto con características estándar.'
};
};
export const ElasticidadIngresoTotal: React.FC = () => {
const [escenario, setEscenario] = useState<ProductoEscenario>(generarEscenario());
const [decision, setDecision] = useState<'subir' | 'bajar' | null>(null);
const [resultado, setResultado] = useState<{
correcto: boolean;
mensaje: string;
ingresoInicial: number;
ingresoNuevo: number;
mostrarAnalisis: boolean;
} | null>(null);
const [puntuacion, setPuntuacion] = useState<number>(0);
const [intentos, setIntentos] = useState<number>(0);
const ingresoInicial = escenario.precioInicial * escenario.cantidadInicial;
const ingresoActual = escenario.precioActual * escenario.cantidadActual;
const cambioPrecio = ((escenario.precioActual - escenario.precioInicial) / escenario.precioInicial) * 100;
const elasticidadCalculada = Math.abs(
((escenario.cantidadActual - escenario.cantidadInicial) / ((escenario.cantidadInicial + escenario.cantidadActual) / 2)) /
((escenario.precioActual - escenario.precioInicial) / ((escenario.precioInicial + escenario.precioActual) / 2))
);
const verificarDecision = (dec: 'subir' | 'bajar') => {
if (resultado) return;
setDecision(dec);
setIntentos(prev => prev + 1);
const demandaElastica = elasticidadCalculada > 1;
const precioSubio = cambioPrecio > 0;
let correcto = false;
let mensaje = '';
if (demandaElastica) {
// Demanda elástica: subir precio reduce ingreso, bajar precio aumenta ingreso
if (dec === 'subir') {
correcto = false;
mensaje = 'Incorrecto. Con demanda elástica, subir el precio reduce el ingreso total porque la cantidad cae proporcionalmente más.';
} else {
correcto = true;
mensaje = '¡Correcto! Con demanda elástica, bajar el precio aumenta el ingreso total porque la cantidad vendida aumenta proporcionalmente más.';
setPuntuacion(prev => prev + 1);
}
} else if (elasticidadCalculada < 1) {
// Demanda inelástica: subir precio aumenta ingreso, bajar precio reduce ingreso
if (dec === 'subir') {
correcto = true;
mensaje = '¡Correcto! Con demanda inelástica, subir el precio aumenta el ingreso total porque la cantidad cae proporcionalmente menos.';
setPuntuacion(prev => prev + 1);
} else {
correcto = false;
mensaje = 'Incorrecto. Con demanda inelástica, bajar el precio reduce el ingreso total porque la cantidad no aumenta lo suficiente para compensar.';
}
} else {
// Demanda unitaria
correcto = true;
mensaje = 'La demanda es unitaria, por lo que cualquier cambio de precio mantendrá el ingreso constante. Ambas opciones son igualmente válidas.';
setPuntuacion(prev => prev + 1);
}
setResultado({
correcto,
mensaje,
ingresoInicial,
ingresoNuevo: ingresoActual,
mostrarAnalisis: true
});
};
const generarNuevoEscenario = () => {
setEscenario(generarEscenario());
setDecision(null);
setResultado(null);
};
const formatearDinero = (cantidad: number) => {
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN',
minimumFractionDigits: 2
}).format(cantidad);
};
return (
<div className="max-w-4xl mx-auto p-6 bg-white rounded-lg shadow-md">
<div className="flex justify-between items-center mb-6">
<div>
<h2 className="text-2xl font-bold text-blue-800">Elasticidad e Ingreso Total</h2>
<p className="text-gray-600">Maximiza el ingreso total tomando la decisión correcta sobre precios.</p>
</div>
<div className="text-right">
<div className="bg-blue-50 px-4 py-2 rounded-lg">
<p className="text-sm text-gray-600">Puntuación</p>
<p className="text-2xl font-bold text-blue-700">{puntuacion}/{intentos}</p>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-indigo-50 to-purple-50 p-6 rounded-xl border border-indigo-200 mb-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-indigo-500 rounded-xl flex items-center justify-center text-white flex-shrink-0">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
</div>
<div className="flex-1">
<h3 className="text-xl font-bold text-gray-800 mb-1">{escenario.nombre}</h3>
<p className="text-gray-600 mb-4">{escenario.descripcion}</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="bg-white p-3 rounded-lg shadow-sm text-center">
<p className="text-xs text-gray-500 mb-1">Precio Inicial</p>
<p className="text-lg font-bold text-blue-600">{formatearDinero(escenario.precioInicial)}</p>
</div>
<div className="bg-white p-3 rounded-lg shadow-sm text-center">
<p className="text-xs text-gray-500 mb-1">Cantidad Inicial</p>
<p className="text-lg font-bold text-blue-600">{escenario.cantidadInicial.toLocaleString()}</p>
</div>
<div className="bg-white p-3 rounded-lg shadow-sm text-center">
<p className="text-xs text-gray-500 mb-1">Precio Actual</p>
<p className={`text-lg font-bold ${cambioPrecio > 0 ? 'text-red-600' : 'text-green-600'}`}>
{formatearDinero(escenario.precioActual)}
</p>
<p className={`text-xs ${cambioPrecio > 0 ? 'text-red-500' : 'text-green-500'}`}>
{cambioPrecio > 0 ? '+' : ''}{cambioPrecio.toFixed(1)}%
</p>
</div>
<div className="bg-white p-3 rounded-lg shadow-sm text-center">
<p className="text-xs text-gray-500 mb-1">Cantidad Actual</p>
<p className={`text-lg font-bold ${escenario.cantidadActual > escenario.cantidadInicial ? 'text-green-600' : 'text-red-600'}`}>
{escenario.cantidadActual.toLocaleString()}
</p>
<p className={`text-xs ${escenario.cantidadActual > escenario.cantidadInicial ? 'text-green-500' : 'text-red-500'}`}>
{((escenario.cantidadActual - escenario.cantidadInicial) / escenario.cantidadInicial * 100) > 0 ? '+' : ''}
{((escenario.cantidadActual - escenario.cantidadInicial) / escenario.cantidadInicial * 100).toFixed(1)}%
</p>
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="bg-gradient-to-br from-green-50 to-emerald-50 p-5 rounded-xl border border-green-200">
<h4 className="font-bold text-green-800 mb-2 flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Ingreso Inicial
</h4>
<p className="text-3xl font-bold text-green-700">{formatearDinero(ingresoInicial)}</p>
<p className="text-sm text-green-600 mt-1">
{escenario.precioInicial} × {escenario.cantidadInicial.toLocaleString()}
</p>
</div>
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-5 rounded-xl border border-blue-200">
<h4 className="font-bold text-blue-800 mb-2 flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Ingreso Actual
</h4>
<p className="text-3xl font-bold text-blue-700">{formatearDinero(ingresoActual)}</p>
<p className="text-sm text-blue-600 mt-1">
{escenario.precioActual} × {escenario.cantidadActual.toLocaleString()}
</p>
</div>
<div className={`bg-gradient-to-br p-5 rounded-xl border ${
ingresoActual > ingresoInicial
? 'from-green-50 to-emerald-50 border-green-200'
: 'from-red-50 to-pink-50 border-red-200'
}`}>
<h4 className={`font-bold mb-2 flex items-center gap-2 ${
ingresoActual > ingresoInicial ? 'text-green-800' : 'text-red-800'
}`}>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
Cambio en Ingreso
</h4>
<p className={`text-3xl font-bold ${ingresoActual > ingresoInicial ? 'text-green-700' : 'text-red-700'}`}>
{ingresoActual > ingresoInicial ? '+' : ''}
{formatearDinero(ingresoActual - ingresoInicial)}
</p>
<p className={`text-sm mt-1 ${ingresoActual > ingresoInicial ? 'text-green-600' : 'text-red-600'}`}>
{((ingresoActual - ingresoInicial) / ingresoInicial * 100) > 0 ? '+' : ''}
{((ingresoActual - ingresoInicial) / ingresoInicial * 100).toFixed(1)}%
</p>
</div>
</div>
<div className="bg-amber-50 p-5 rounded-xl border border-amber-200 mb-6">
<h4 className="font-bold text-amber-800 mb-3 flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
Tu Decisión
</h4>
<p className="text-gray-700 mb-4">
Basándote en los datos anteriores, ¿qué decisión de precio maximizaría el ingreso total?
</p>
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => verificarDecision('subir')}
disabled={!!resultado}
className={`p-5 rounded-xl border-2 text-left transition-all ${
decision === 'subir'
? elasticidadCalculada < 1
? 'bg-green-100 border-green-500 ring-2 ring-green-300'
: 'bg-red-100 border-red-500 ring-2 ring-red-300'
: 'bg-white border-gray-200 hover:border-red-300 hover:bg-red-50'
} ${!!resultado && decision !== 'subir' ? 'opacity-50' : ''}`}
>
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center">
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
<span className="font-bold text-lg">Subir Precio</span>
</div>
<p className="text-sm text-gray-600">Aumentar el precio actual</p>
</button>
<button
onClick={() => verificarDecision('bajar')}
disabled={!!resultado}
className={`p-5 rounded-xl border-2 text-left transition-all ${
decision === 'bajar'
? elasticidadCalculada > 1
? 'bg-green-100 border-green-500 ring-2 ring-green-300'
: 'bg-red-100 border-red-500 ring-2 ring-red-300'
: 'bg-white border-gray-200 hover:border-green-300 hover:bg-green-50'
} ${!!resultado && decision !== 'bajar' ? 'opacity-50' : ''}`}
>
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6" />
</svg>
</div>
<span className="font-bold text-lg">Bajar Precio</span>
</div>
<p className="text-sm text-gray-600">Reducir el precio actual</p>
</button>
</div>
</div>
{resultado && (
<div className={`p-6 rounded-xl border-2 mb-6 ${resultado.correcto ? 'bg-green-50 border-green-300' : 'bg-red-50 border-red-300'}`}>
<div className="flex items-start gap-4">
<div className={`p-3 rounded-full ${resultado.correcto ? 'bg-green-100' : 'bg-red-100'}`}>
{resultado.correcto ? (
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
</div>
<div className="flex-1">
<h4 className={`font-bold text-lg ${resultado.correcto ? 'text-green-800' : 'text-red-800'}`}>
{resultado.correcto ? '¡Decisión Correcta!' : 'Decisión Incorrecta'}
</h4>
<p className={`mt-1 ${resultado.correcto ? 'text-green-700' : 'text-red-700'}`}>
{resultado.mensaje}
</p>
<div className="mt-4 bg-white p-4 rounded-lg border border-gray-200">
<div className="grid grid-cols-2 gap-4 mb-3">
<div>
<span className="text-gray-600">Elasticidad calculada:</span>
<span className="ml-2 font-bold text-indigo-600 text-lg">{elasticidadCalculada.toFixed(2)}</span>
</div>
<div>
<span className="text-gray-600">Clasificación:</span>
<span className={`ml-2 font-bold ${
elasticidadCalculada > 1 ? 'text-green-600' :
elasticidadCalculada < 1 ? 'text-amber-600' : 'text-blue-600'
}`}>
{elasticidadCalculada > 1 ? 'Elástica' : elasticidadCalculada < 1 ? 'Inelástica' : 'Unitaria'}
</span>
</div>
</div>
<div className="bg-gray-50 p-3 rounded-lg">
<p className="font-medium text-gray-800 mb-2">Análisis del Ingreso Total:</p>
<ul className="text-sm text-gray-600 space-y-1">
<li> Ingreso inicial: {formatearDinero(resultado.ingresoInicial)}</li>
<li> Ingreso después del cambio: {formatearDinero(resultado.ingresoNuevo)}</li>
<li> Diferencia: {formatearDinero(resultado.ingresoNuevo - resultado.ingresoInicial)}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
)}
{resultado && (
<div className="flex gap-4">
<button
onClick={generarNuevoEscenario}
className="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors flex items-center justify-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Nuevo Escenario
</button>
</div>
)}
<div className="mt-8 bg-gradient-to-r from-indigo-50 to-blue-50 p-5 rounded-xl border border-indigo-200">
<h4 className="font-bold text-indigo-800 mb-4 flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Regla para Maximizar Ingreso Total
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white p-4 rounded-lg border border-green-200">
<div className="flex items-center gap-2 mb-2">
<span className="px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm font-bold">E<sub>d</sub> &gt; 1</span>
<span className="font-bold text-gray-800">Demanda Elástica</span>
</div>
<p className="text-sm text-gray-600 mb-2">Para maximizar ingreso:</p>
<p className="text-green-700 font-semibold flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
BAJAR el precio
</p>
<p className="text-xs text-gray-500 mt-2">
La cantidad aumenta más que proporcionalmente al precio.
</p>
</div>
<div className="bg-white p-4 rounded-lg border border-amber-200">
<div className="flex items-center gap-2 mb-2">
<span className="px-3 py-1 bg-amber-100 text-amber-700 rounded-full text-sm font-bold">E<sub>d</sub> &lt; 1</span>
<span className="font-bold text-gray-800">Demanda Inelástica</span>
</div>
<p className="text-sm text-gray-600 mb-2">Para maximizar ingreso:</p>
<p className="text-amber-700 font-semibold flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
SUBIR el precio
</p>
<p className="text-xs text-gray-500 mt-2">
La cantidad cae menos que proporcionalmente al precio.
</p>
</div>
</div>
<div className="mt-4 bg-blue-100 p-3 rounded-lg border border-blue-200 text-center">
<p className="text-blue-800 font-medium">
<span className="font-bold">E<sub>d</sub> = 1 (Unitaria):</span> El ingreso total ya está maximizado. Cualquier cambio de precio mantendrá el ingreso constante.
</p>
</div>
</div>
</div>
);
};
export default ElasticidadIngresoTotal;

View 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;

View File

@@ -0,0 +1,543 @@
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { LineChart, Check, X, RotateCcw, Trophy, Info, MousePointer2 } from 'lucide-react';
interface EquilibrioGraficoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Punto {
x: number;
y: number;
}
const GRID_SIZE = 350;
const PADDING = 50;
const MAX_PRECIO = 100;
const MAX_CANTIDAD = 100;
export const EquilibrioGrafico: React.FC<EquilibrioGraficoProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [demandaPoints, setDemandaPoints] = useState<Punto[]>([]);
const [ofertaPoints, setOfertaPoints] = useState<Punto[]>([]);
const [modoActivo, setModoActivo] = useState<'demanda' | 'oferta'>('demanda');
const [equilibrioEncontrado, setEquilibrioEncontrado] = useState(false);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [score, setScore] = useState(0);
const [mensaje, setMensaje] = useState('');
const [showTutorial, setShowTutorial] = useState(true);
const [_startTime] = useState(Date.now());
const svgRef = useRef<SVGSVGElement>(null);
const cartesianToSvg = useCallback((x: number, y: number) => {
const svgX = PADDING + (x / MAX_CANTIDAD) * GRID_SIZE;
const svgY = PADDING + GRID_SIZE - (y / MAX_PRECIO) * GRID_SIZE;
return { x: svgX, y: svgY };
}, []);
const svgToCartesian = useCallback((svgX: number, svgY: number) => {
const x = ((svgX - PADDING) / GRID_SIZE) * MAX_CANTIDAD;
const y = ((PADDING + GRID_SIZE - svgY) / GRID_SIZE) * MAX_PRECIO;
return {
x: Math.max(0, Math.min(MAX_CANTIDAD, Math.round(x))),
y: Math.max(0, Math.min(MAX_PRECIO, Math.round(y)))
};
}, []);
const handleSvgClick = (e: React.MouseEvent<SVGSVGElement>) => {
if (equilibrioEncontrado) return;
const rect = svgRef.current?.getBoundingClientRect();
if (!rect) return;
const svgX = e.clientX - rect.left;
const svgY = e.clientY - rect.top;
const cartesian = svgToCartesian(svgX, svgY);
const newPoint: Punto = { x: cartesian.x, y: cartesian.y };
if (modoActivo === 'demanda') {
if (demandaPoints.length >= 2) {
setDemandaPoints([newPoint]);
} else {
setDemandaPoints(prev => [...prev, newPoint]);
}
} else {
if (ofertaPoints.length >= 2) {
setOfertaPoints([newPoint]);
} else {
setOfertaPoints(prev => [...prev, newPoint]);
}
}
setMensaje('');
};
const calcularInterseccion = () => {
if (demandaPoints.length < 2 || ofertaPoints.length < 2) return null;
const d1 = demandaPoints[0];
const d2 = demandaPoints[1];
const s1 = ofertaPoints[0];
const s2 = ofertaPoints[1];
const m1 = (d2.y - d1.y) / (d2.x - d1.x);
const m2 = (s2.y - s1.y) / (s2.x - s1.x);
if (Math.abs(m1 - m2) < 0.01) return null;
const b1 = d1.y - m1 * d1.x;
const b2 = s1.y - m2 * s1.x;
const x = (b2 - b1) / (m1 - m2);
const y = m1 * x + b1;
return { x: Math.round(x), y: Math.round(y) };
};
const validarEquilibrio = () => {
if (demandaPoints.length < 2) {
setMensaje('Necesitas 2 puntos para trazar la curva de demanda');
return;
}
if (ofertaPoints.length < 2) {
setMensaje('Necesitas 2 puntos para trazar la curva de oferta');
return;
}
const d1 = demandaPoints[0];
const d2 = demandaPoints[1];
const s1 = ofertaPoints[0];
const s2 = ofertaPoints[1];
const pendienteDemanda = (d2.y - d1.y) / (d2.x - d1.x);
const pendienteOferta = (s2.y - s1.y) / (s2.x - s1.x);
if (pendienteDemanda >= 0) {
setMensaje('La demanda debe tener pendiente negativa (bajar de izquierda a derecha)');
return;
}
if (pendienteOferta <= 0) {
setMensaje('La oferta debe tener pendiente positiva (subir de izquierda a derecha)');
return;
}
const interseccion = calcularInterseccion();
if (!interseccion) {
setMensaje('Las curvas no se intersectan dentro del rango válido');
return;
}
setEquilibrioEncontrado(true);
setMostrarResultado(true);
setScore(100);
setMensaje('');
setTimeout(() => {
if (onComplete) {
onComplete(100);
}
}, 3000);
};
const reiniciar = () => {
setDemandaPoints([]);
setOfertaPoints([]);
setModoActivo('demanda');
setEquilibrioEncontrado(false);
setMostrarResultado(false);
setScore(0);
setMensaje('');
setShowTutorial(true);
};
const renderLineaCurva = (puntos: Punto[], color: string, esPunteada: boolean = false) => {
if (puntos.length < 2) return null;
const sorted = [...puntos].sort((a, b) => a.x - b.x);
const start = sorted[0];
const end = sorted[sorted.length - 1];
const startSvg = cartesianToSvg(start.x, start.y);
const endSvg = cartesianToSvg(end.x, end.y);
const m = (end.y - start.y) / (end.x - start.x);
const b = start.y - m * start.x;
const yAtX0 = b;
const yAtXMax = m * MAX_CANTIDAD + b;
const p0 = cartesianToSvg(0, Math.max(0, Math.min(MAX_PRECIO, yAtX0)));
const pMax = cartesianToSvg(MAX_CANTIDAD, Math.max(0, Math.min(MAX_PRECIO, yAtXMax)));
return (
<line
x1={p0.x}
y1={p0.y}
x2={pMax.x}
y2={pMax.y}
stroke={color}
strokeWidth="3"
strokeLinecap="round"
strokeDasharray={esPunteada ? "5,5" : "0"}
/>
);
};
const interseccion = calcularInterseccion();
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">
<LineChart className="w-8 h-8 text-purple-600" />
<h2 className="text-2xl font-bold text-gray-800">Gráfico de Equilibrio</h2>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-500">Encuentra la intersección</span>
<button
onClick={reiniciar}
className="p-2 text-gray-500 hover:text-purple-600 transition-colors"
>
<RotateCcw className="w-5 h-5" />
</button>
</div>
</div>
<p className="text-gray-600">
Traza las curvas de demanda y oferta para encontrar el punto de equilibrio donde se cruzan.
</p>
</div>
{showTutorial && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg flex items-start gap-3"
>
<Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h4 className="font-semibold text-blue-800 mb-1">Cómo jugar:</h4>
<ul className="text-sm text-blue-700 space-y-1">
<li> Selecciona el modo (Demanda u Oferta) con los botones</li>
<li> Haz clic en el gráfico para colocar 2 puntos de cada curva</li>
<li> La demanda debe tener pendiente negativa (baja)</li>
<li> La oferta debe tener pendiente positiva (sube)</li>
<li> Presiona "Validar Equilibrio" cuando estén ambas curvas</li>
</ul>
</div>
<button
onClick={() => setShowTutorial(false)}
className="text-blue-600 hover:text-blue-800"
>
×
</button>
</motion.div>
)}
<div className="grid lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<div className="bg-gray-50 rounded-lg p-4">
<svg
ref={svgRef}
width={GRID_SIZE + 2 * PADDING}
height={GRID_SIZE + 2 * PADDING}
className="w-full border-2 border-gray-300 rounded-lg bg-white cursor-crosshair"
onClick={handleSvgClick}
>
{/* Grid */}
{Array.from({ length: 11 }).map((_, i) => (
<g key={i}>
<line
x1={PADDING + (i * GRID_SIZE) / 10}
y1={PADDING}
x2={PADDING + (i * GRID_SIZE) / 10}
y2={PADDING + GRID_SIZE}
stroke="#e5e7eb"
strokeWidth="1"
/>
<line
x1={PADDING}
y1={PADDING + (i * GRID_SIZE) / 10}
x2={PADDING + GRID_SIZE}
y2={PADDING + (i * GRID_SIZE) / 10}
stroke="#e5e7eb"
strokeWidth="1"
/>
</g>
))}
{/* Ejes */}
<line
x1={PADDING}
y1={PADDING + GRID_SIZE}
x2={PADDING + GRID_SIZE}
y2={PADDING + GRID_SIZE}
stroke="#374151"
strokeWidth="2"
/>
<line
x1={PADDING}
y1={PADDING}
x2={PADDING}
y2={PADDING + GRID_SIZE}
stroke="#374151"
strokeWidth="2"
/>
{/* Labels ejes */}
<text x={PADDING + GRID_SIZE / 2} y={PADDING + GRID_SIZE + 30} textAnchor="middle" className="text-sm fill-gray-600">
Cantidad (Q)
</text>
<text x={20} y={PADDING + GRID_SIZE / 2} textAnchor="middle" transform={`rotate(-90, 20, ${PADDING + GRID_SIZE / 2})`} className="text-sm fill-gray-600">
Precio (P)
</text>
{/* Curvas */}
{renderLineaCurva(demandaPoints, '#3b82f6')}
{renderLineaCurva(ofertaPoints, '#22c55e')}
{/* Labels de curvas */}
{demandaPoints.length >= 2 && (
<text x={PADDING + GRID_SIZE - 20} y={PADDING + 30} className="text-sm fill-blue-600 font-bold">D</text>
)}
{ofertaPoints.length >= 2 && (
<text x={PADDING + GRID_SIZE - 20} y={PADDING + GRID_SIZE - 20} className="text-sm fill-green-600 font-bold">S</text>
)}
{/* Puntos Demanda */}
{demandaPoints.map((punto, index) => {
const svg = cartesianToSvg(punto.x, punto.y);
return (
<motion.g
key={`demanda-${index}`}
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 300 }}
>
<circle
cx={svg.x}
cy={svg.y}
r="8"
fill="#3b82f6"
stroke="white"
strokeWidth="2"
/>
<text x={svg.x} y={svg.y - 12} textAnchor="middle" className="text-xs fill-blue-600 font-medium">
D{index + 1}
</text>
</motion.g>
);
})}
{/* Puntos Oferta */}
{ofertaPoints.map((punto, index) => {
const svg = cartesianToSvg(punto.x, punto.y);
return (
<motion.g
key={`oferta-${index}`}
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 300 }}
>
<circle
cx={svg.x}
cy={svg.y}
r="8"
fill="#22c55e"
stroke="white"
strokeWidth="2"
/>
<text x={svg.x} y={svg.y - 12} textAnchor="middle" className="text-xs fill-green-600 font-medium">
S{index + 1}
</text>
</motion.g>
);
})}
{/* Punto de Equilibrio */}
{equilibrioEncontrado && interseccion && (
<motion.g
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 200, delay: 0.3 }}
>
<circle
cx={cartesianToSvg(interseccion.x, interseccion.y).x}
cy={cartesianToSvg(interseccion.x, interseccion.y).y}
r="12"
fill="#8b5cf6"
stroke="white"
strokeWidth="3"
/>
<text
x={cartesianToSvg(interseccion.x, interseccion.y).x + 15}
y={cartesianToSvg(interseccion.x, interseccion.y).y - 10}
className="text-sm fill-purple-600 font-bold"
>
E ({interseccion.x}, {interseccion.y})
</text>
{/* Líneas guía */}
<line
x1={PADDING}
y1={cartesianToSvg(interseccion.x, interseccion.y).y}
x2={cartesianToSvg(interseccion.x, interseccion.y).x}
y2={cartesianToSvg(interseccion.x, interseccion.y).y}
stroke="#8b5cf6"
strokeWidth="1"
strokeDasharray="4,4"
opacity="0.5"
/>
<line
x1={cartesianToSvg(interseccion.x, interseccion.y).x}
y1={cartesianToSvg(interseccion.x, interseccion.y).y}
x2={cartesianToSvg(interseccion.x, interseccion.y).x}
y2={PADDING + GRID_SIZE}
stroke="#8b5cf6"
strokeWidth="1"
strokeDasharray="4,4"
opacity="0.5"
/>
</motion.g>
)}
</svg>
</div>
</div>
<div className="space-y-4">
<div className="flex gap-2 p-3 bg-gray-100 rounded-lg">
<button
onClick={() => setModoActivo('demanda')}
disabled={equilibrioEncontrado}
className={`flex-1 py-3 px-3 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2 ${
modoActivo === 'demanda'
? 'bg-blue-600 text-white shadow-lg'
: 'bg-white text-gray-700 hover:bg-gray-50'
}`}
>
<div className="w-3 h-3 bg-blue-400 rounded-full" />
Demanda
</button>
<button
onClick={() => setModoActivo('oferta')}
disabled={equilibrioEncontrado}
className={`flex-1 py-3 px-3 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2 ${
modoActivo === 'oferta'
? 'bg-green-600 text-white shadow-lg'
: 'bg-white text-gray-700 hover:bg-gray-50'
}`}
>
<div className="w-3 h-3 bg-green-400 rounded-full" />
Oferta
</button>
</div>
<div className="p-4 bg-gray-50 rounded-lg">
<h3 className="font-semibold text-gray-700 mb-3 flex items-center gap-2">
<MousePointer2 className="w-4 h-4" />
Estado
</h3>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Puntos Demanda:</span>
<span className={`text-sm font-medium ${demandaPoints.length >= 2 ? 'text-blue-600' : 'text-gray-400'}`}>
{demandaPoints.length}/2
</span>
</div>
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-blue-500"
initial={{ width: 0 }}
animate={{ width: `${(demandaPoints.length / 2) * 100}%` }}
/>
</div>
<div className="flex items-center justify-between mt-3">
<span className="text-sm text-gray-600">Puntos Oferta:</span>
<span className={`text-sm font-medium ${ofertaPoints.length >= 2 ? 'text-green-600' : 'text-gray-400'}`}>
{ofertaPoints.length}/2
</span>
</div>
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-green-500"
initial={{ width: 0 }}
animate={{ width: `${(ofertaPoints.length / 2) * 100}%` }}
/>
</div>
</div>
</div>
{interseccion && !equilibrioEncontrado && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="p-3 bg-purple-50 border border-purple-200 rounded-lg"
>
<p className="text-sm text-purple-800">
<strong>Intersección detectada:</strong>
</p>
<p className="text-sm text-purple-700">
Q* = {interseccion.x}, P* = {interseccion.y}
</p>
</motion.div>
)}
{mensaje && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="p-3 bg-red-50 border border-red-200 rounded-lg flex items-start gap-2"
>
<X className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-700">{mensaje}</p>
</motion.div>
)}
<button
onClick={validarEquilibrio}
disabled={equilibrioEncontrado}
className="w-full 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 flex items-center justify-center gap-2"
>
<Check className="w-5 h-5" />
Validar Equilibrio
</button>
<AnimatePresence>
{mostrarResultado && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="p-4 bg-green-50 border border-green-200 rounded-lg text-center"
>
<Trophy className="w-12 h-12 text-green-500 mx-auto mb-2" />
<p className="font-bold text-green-800 text-lg">¡Equilibrio Encontrado!</p>
{interseccion && (
<div className="mt-2 text-green-700">
<p>Precio de equilibrio: <strong>${interseccion.y}</strong></p>
<p>Cantidad de equilibrio: <strong>{interseccion.x} unidades</strong></p>
</div>
)}
</motion.div>
)}
</AnimatePresence>
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
<strong>Recuerda:</strong>
</p>
<ul className="text-sm text-yellow-700 mt-1 space-y-1">
<li> Demanda: pendiente negativa </li>
<li> Oferta: pendiente positiva </li>
<li> El equilibrio es donde se cruzan</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default EquilibrioGrafico;

View File

@@ -0,0 +1,454 @@
import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { AlertTriangle, TrendingDown, ArrowRight, CheckCircle2, XCircle, Trophy, RotateCcw, BookOpen } from 'lucide-react';
interface ExcesoDemandaEscasezProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Escenario {
id: number;
producto: string;
precioEquilibrio: number;
cantidadEquilibrio: number;
precioControl: number;
demanda: { a: number; b: number };
oferta: { c: number; d: number };
contexto: string;
dificultad: 'facil' | 'medio' | 'dificil';
}
const escenarios: Escenario[] = [
{
id: 1,
producto: 'Pan',
precioEquilibrio: 50,
cantidadEquilibrio: 100,
precioControl: 30,
demanda: { a: 150, b: -1 },
oferta: { c: 0, d: 2 },
contexto: 'El gobierno fija un precio máximo de $30 para el pan para proteger a los consumidores.',
dificultad: 'facil'
},
{
id: 2,
producto: 'Gasolina',
precioEquilibrio: 80,
cantidadEquilibrio: 60,
precioControl: 50,
demanda: { a: 140, b: -1 },
oferta: { c: 20, d: 0.75 },
contexto: 'Se impone un precio máximo de $50 por galón ante el alza de precios internacionales.',
dificultad: 'medio'
},
{
id: 3,
producto: 'Vivienda',
precioEquilibrio: 1200,
cantidadEquilibrio: 500,
precioControl: 800,
demanda: { a: 2200, b: -2 },
oferta: { c: 200, d: 2 },
contexto: 'Control de rentas fija el precio máximo en $800 para hacer la vivienda accesible.',
dificultad: 'dificil'
}
];
export const ExcesoDemandaEscasez: React.FC<ExcesoDemandaEscasezProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [escenarioActual, setEscenarioActual] = useState(0);
const [respuestaExceso, setRespuestaExceso] = useState('');
const [mostrarResultado, setMostrarResultado] = useState(false);
const [esCorrecto, setEsCorrecto] = useState(false);
const [score, setScore] = useState(0);
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
const [completado, setCompletado] = useState(false);
const [_startTime] = useState(Date.now());
const escenario = escenarios[escenarioActual];
const calcularCantidades = (precio: number, escenario: Escenario) => {
const Qd = escenario.demanda.a + escenario.demanda.b * precio;
const Qo = escenario.oferta.c + escenario.oferta.d * precio;
return { Qd: Math.max(0, Qd), Qo: Math.max(0, Qo) };
};
const equilibrio = calcularCantidades(escenario.precioEquilibrio, escenario);
const conControl = calcularCantidades(escenario.precioControl, escenario);
const excesoDemandaReal = conControl.Qd - conControl.Qo;
const handleVerificar = () => {
const respuesta = parseFloat(respuestaExceso);
if (isNaN(respuesta)) return;
const margenError = escenario.precioEquilibrio * 0.1;
const correcto = Math.abs(respuesta - excesoDemandaReal) <= margenError;
setEsCorrecto(correcto);
setMostrarResultado(true);
if (correcto) {
setScore(prev => prev + Math.round(100 / escenarios.length));
setRespuestasCorrectas(prev => prev + 1);
}
};
const handleSiguiente = () => {
if (escenarioActual < escenarios.length - 1) {
setEscenarioActual(prev => prev + 1);
setRespuestaExceso('');
setMostrarResultado(false);
} else {
setCompletado(true);
if (onComplete) {
onComplete(score);
}
}
};
const handleReiniciar = () => {
setEscenarioActual(0);
setRespuestaExceso('');
setMostrarResultado(false);
setScore(0);
setRespuestasCorrectas(0);
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', escenario: Escenario) => {
const puntos = [];
const maxP = Math.max(escenario.precioEquilibrio * 1.5, escenario.precioControl * 1.2);
for (let P = 0; P <= maxP; P += maxP / 20) {
if (tipo === 'demanda') {
const Q = escenario.demanda.a + escenario.demanda.b * P;
if (Q >= 0) puntos.push({ Q, P });
} else {
const Q = escenario.oferta.c + escenario.oferta.d * P;
if (Q >= 0) puntos.push({ Q, P });
}
}
return puntos;
};
const maxQ = Math.max(equilibrio.Qd, conControl.Qd) * 1.3;
const maxP = escenario.precioEquilibrio * 1.5;
const scaleX = (Q: number) => 50 + (Q / maxQ) * 350;
const scaleY = (P: number) => 300 - (P / maxP) * 250;
const puntosDemanda = generarPuntosCurva('demanda', escenario);
const puntosOferta = generarPuntosCurva('oferta', escenario);
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 / escenarios.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 analizado escenarios de escasez</p>
<div className="bg-gray-50 rounded-lg p-6 mb-6">
<div className="text-5xl font-bold text-red-600 mb-2">{porcentaje}%</div>
<p className="text-gray-600">
{respuestasCorrectas} de {escenarios.length} respuestas correctas
</p>
</div>
<button
onClick={handleReiniciar}
className="inline-flex items-center gap-2 px-6 py-3 bg-red-600 text-white rounded-lg font-medium hover:bg-red-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">
<AlertTriangle className="w-8 h-8 text-red-600" />
<h2 className="text-2xl font-bold text-gray-800">Exceso de Demanda (Escasez)</h2>
</div>
<div className="flex items-center gap-4">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getDificultadColor(escenario.dificultad)}`}>
{escenario.dificultad.toUpperCase()}
</span>
<span className="text-sm text-gray-500">
{escenarioActual + 1} de {escenarios.length}
</span>
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-red-600"
initial={{ width: 0 }}
animate={{ width: `${((escenarioActual + 1) / escenarios.length) * 100}%` }}
/>
</div>
</div>
</div>
<p className="text-gray-600">
Analiza qué sucede cuando el precio está por debajo del equilibrio.
</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-2 text-center">Mercado de {escenario.producto}</h3>
<svg width="450" height="320" className="mx-auto">
{/* Grid */}
{Array.from({ length: 6 }).map((_, i) => (
<g key={i}>
<line x1={50 + i * 70} y1="30" x2={50 + i * 70} y2="300" stroke="#e5e7eb" strokeWidth="1" />
<line x1="50" y1={30 + i * 54} x2="400" y2={30 + i * 54} stroke="#e5e7eb" strokeWidth="1" />
</g>
))}
{/* Ejes */}
<line x1="50" y1="300" x2="400" y2="300" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="30" x2="50" y2="300" stroke="#374151" strokeWidth="2" />
{/* Labels */}
<text x="225" y="320" textAnchor="middle" className="text-sm fill-gray-600">Cantidad (Q)</text>
<text x="25" y="165" textAnchor="middle" transform="rotate(-90, 25, 165)" className="text-sm fill-gray-600">Precio (P)</text>
{/* Curva de Demanda */}
<path d={demandaPath} fill="none" stroke="#3b82f6" strokeWidth="3" />
<text x="380" y={scaleY(20)} className="text-sm fill-blue-600 font-medium">D</text>
{/* Curva de Oferta */}
<path d={ofertaPath} fill="none" stroke="#22c55e" strokeWidth="3" />
<text x="380" y={scaleY(maxP * 0.9)} className="text-sm fill-green-600 font-medium">S</text>
{/* Punto de equilibrio */}
<circle
cx={scaleX(equilibrio.Qd)}
cy={scaleY(escenario.precioEquilibrio)}
r="6"
fill="#8b5cf6"
stroke="white"
strokeWidth="2"
/>
<text x={scaleX(equilibrio.Qd) + 10} y={scaleY(escenario.precioEquilibrio)} className="text-xs fill-purple-600">
E
</text>
{/* Línea de precio de control */}
<line
x1="50"
y1={scaleY(escenario.precioControl)}
x2="400"
y2={scaleY(escenario.precioControl)}
stroke="#ef4444"
strokeWidth="2"
strokeDasharray="5,5"
/>
<text x="410" y={scaleY(escenario.precioControl)} className="text-xs fill-red-500 font-medium">
Pmax=${escenario.precioControl}
</text>
{/* Zona de exceso de demanda */}
<motion.g
initial={{ opacity: 0 }}
animate={{ opacity: mostrarResultado && esCorrecto ? 1 : 0.3 }}
>
<rect
x={scaleX(conControl.Qo)}
y={scaleY(escenario.precioControl) - 5}
width={scaleX(conControl.Qd) - scaleX(conControl.Qo)}
height="10"
fill="#fef3c7"
stroke="#f59e0b"
strokeWidth="2"
opacity="0.7"
/>
<text
x={scaleX((conControl.Qd + conControl.Qo) / 2)}
y={scaleY(escenario.precioControl) - 15}
textAnchor="middle"
className="text-xs fill-amber-700 font-medium"
>
Escasez
</text>
</motion.g>
{/* Puntos de cantidad */}
<circle
cx={scaleX(conControl.Qd)}
cy={scaleY(escenario.precioControl)}
r="5"
fill="#3b82f6"
stroke="white"
strokeWidth="2"
/>
<text x={scaleX(conControl.Qd)} y={scaleY(escenario.precioControl) + 20} textAnchor="middle" className="text-xs fill-blue-600">
Qd
</text>
<circle
cx={scaleX(conControl.Qo)}
cy={scaleY(escenario.precioControl)}
r="5"
fill="#22c55e"
stroke="white"
strokeWidth="2"
/>
<text x={scaleX(conControl.Qo)} y={scaleY(escenario.precioControl) + 20} textAnchor="middle" className="text-xs fill-green-600">
Qo
</text>
</svg>
<div className="mt-4 grid grid-cols-2 gap-3 text-sm">
<div className="bg-white p-2 rounded border">
<span className="text-purple-600 font-medium">Equilibrio:</span>
<p className="text-gray-700">P=${escenario.precioEquilibrio}, Q={Math.round(equilibrio.Qd)}</p>
</div>
<div className="bg-red-50 p-2 rounded border border-red-200">
<span className="text-red-600 font-medium">Con Pmax:</span>
<p className="text-gray-700">Qd={Math.round(conControl.Qd)}, Qo={Math.round(conControl.Qo)}</p>
</div>
</div>
</div>
<div className="space-y-4">
<div className="bg-gradient-to-br from-red-50 to-orange-50 rounded-lg p-6">
<div className="flex items-start gap-3">
<BookOpen className="w-6 h-6 text-red-600 flex-shrink-0 mt-1" />
<div>
<h3 className="font-semibold text-gray-800 mb-2">Escenario</h3>
<p className="text-gray-700">{escenario.contexto}</p>
</div>
</div>
</div>
<div className="bg-white border-2 border-red-200 rounded-lg p-6">
<h3 className="font-semibold text-gray-800 mb-4 flex items-center gap-2">
<TrendingDown className="w-5 h-5 text-red-600" />
Calcula el Exceso de Demanda
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
¿Cuántas unidades de escasez se generan?
</label>
<input
type="number"
value={respuestaExceso}
onChange={(e) => setRespuestaExceso(e.target.value)}
disabled={mostrarResultado}
placeholder="Ingresa el valor numérico"
className="w-full px-4 py-3 border-2 border-red-200 rounded-lg focus:border-red-500 focus:outline-none disabled:bg-gray-100"
/>
<p className="text-xs text-gray-500 mt-1">
Fórmula: Exceso de Demanda = Qd - Qo (al precio de control)
</p>
</div>
</div>
</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 ? (
<CheckCircle2 className="w-6 h-6 text-green-600 flex-shrink-0" />
) : (
<XCircle 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>
<div className="mt-2 text-sm text-gray-700">
<p>Al precio de ${escenario.precioControl}:</p>
<ul className="mt-1 space-y-1">
<li> Cantidad demandada: <strong>{Math.round(conControl.Qd)} unidades</strong></li>
<li> Cantidad ofrecida: <strong>{Math.round(conControl.Qo)} unidades</strong></li>
<li> Exceso de demanda: <strong>{Math.round(excesoDemandaReal)} unidades</strong></li>
</ul>
{!esCorrecto && (
<p className="mt-2 text-red-700">
La respuesta correcta es: <strong>{Math.round(excesoDemandaReal)} unidades</strong>
</p>
)}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="flex gap-3">
{!mostrarResultado ? (
<button
onClick={handleVerificar}
disabled={!respuestaExceso}
className="flex-1 py-3 px-4 bg-red-600 text-white rounded-lg font-medium hover:bg-red-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"
>
{escenarioActual < escenarios.length - 1 ? (
<>
Siguiente
<ArrowRight className="w-5 h-5" />
</>
) : (
'Finalizar'
)}
</button>
)}
</div>
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg">
<p className="text-sm text-amber-800">
<strong>Consecuencias del exceso de demanda:</strong>
</p>
<ul className="text-sm text-amber-700 mt-1 space-y-1">
<li> Largas filas y esperas</li>
<li> Racionamiento del producto</li>
<li> Mercados negros</li>
<li> Pérdida de peso muerto</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default ExcesoDemandaEscasez;

View File

@@ -0,0 +1,454 @@
import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Package, TrendingUp, ArrowRight, CheckCircle2, XCircle, Trophy, RotateCcw, BookOpen } from 'lucide-react';
interface ExcesoOfertaSuperavitProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Escenario {
id: number;
producto: string;
precioEquilibrio: number;
cantidadEquilibrio: number;
precioMinimo: number;
demanda: { a: number; b: number };
oferta: { c: number; d: number };
contexto: string;
dificultad: 'facil' | 'medio' | 'dificil';
}
const escenarios: Escenario[] = [
{
id: 1,
producto: 'Leche',
precioEquilibrio: 40,
cantidadEquilibrio: 80,
precioMinimo: 60,
demanda: { a: 120, b: -1 },
oferta: { c: 0, d: 2 },
contexto: 'El gobierno establece un precio mínimo de $60 para proteger a los productores lecheros.',
dificultad: 'facil'
},
{
id: 2,
producto: 'Trigo',
precioEquilibrio: 100,
cantidadEquilibrio: 200,
precioMinimo: 140,
demanda: { a: 300, b: -1 },
oferta: { c: -100, d: 2 },
contexto: 'Se fija un precio de sustento de $140 para garantizar ingresos a los agricultores.',
dificultad: 'medio'
},
{
id: 3,
producto: 'Trabajo no calificado',
precioEquilibrio: 50,
cantidadEquilibrio: 1000,
precioMinimo: 80,
demanda: { a: 150, b: -0.1 },
oferta: { c: 0, d: 20 },
contexto: 'El salario mínimo se fija en $80, por encima del salario de equilibrio del mercado.',
dificultad: 'dificil'
}
];
export const ExcesoOfertaSuperavit: React.FC<ExcesoOfertaSuperavitProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [escenarioActual, setEscenarioActual] = useState(0);
const [respuestaExceso, setRespuestaExceso] = useState('');
const [mostrarResultado, setMostrarResultado] = useState(false);
const [esCorrecto, setEsCorrecto] = useState(false);
const [score, setScore] = useState(0);
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
const [completado, setCompletado] = useState(false);
const [_startTime] = useState(Date.now());
const escenario = escenarios[escenarioActual];
const calcularCantidades = (precio: number, escenario: Escenario) => {
const Qd = escenario.demanda.a + escenario.demanda.b * precio;
const Qo = escenario.oferta.c + escenario.oferta.d * precio;
return { Qd: Math.max(0, Qd), Qo: Math.max(0, Qo) };
};
const equilibrio = calcularCantidades(escenario.precioEquilibrio, escenario);
const conMinimo = calcularCantidades(escenario.precioMinimo, escenario);
const excesoOfertaReal = conMinimo.Qo - conMinimo.Qd;
const handleVerificar = () => {
const respuesta = parseFloat(respuestaExceso);
if (isNaN(respuesta)) return;
const margenError = escenario.precioEquilibrio * 0.15;
const correcto = Math.abs(respuesta - excesoOfertaReal) <= margenError;
setEsCorrecto(correcto);
setMostrarResultado(true);
if (correcto) {
setScore(prev => prev + Math.round(100 / escenarios.length));
setRespuestasCorrectas(prev => prev + 1);
}
};
const handleSiguiente = () => {
if (escenarioActual < escenarios.length - 1) {
setEscenarioActual(prev => prev + 1);
setRespuestaExceso('');
setMostrarResultado(false);
} else {
setCompletado(true);
if (onComplete) {
onComplete(score);
}
}
};
const handleReiniciar = () => {
setEscenarioActual(0);
setRespuestaExceso('');
setMostrarResultado(false);
setScore(0);
setRespuestasCorrectas(0);
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', escenario: Escenario) => {
const puntos = [];
const maxP = Math.max(escenario.precioMinimo * 1.2, escenario.precioEquilibrio * 1.5);
for (let P = 0; P <= maxP; P += maxP / 20) {
if (tipo === 'demanda') {
const Q = escenario.demanda.a + escenario.demanda.b * P;
if (Q >= 0) puntos.push({ Q, P });
} else {
const Q = escenario.oferta.c + escenario.oferta.d * P;
if (Q >= 0) puntos.push({ Q, P });
}
}
return puntos;
};
const maxQ = Math.max(conMinimo.Qo, equilibrio.Qd) * 1.3;
const maxP = Math.max(escenario.precioMinimo, escenario.precioEquilibrio) * 1.3;
const scaleX = (Q: number) => 50 + (Q / maxQ) * 350;
const scaleY = (P: number) => 300 - (P / maxP) * 250;
const puntosDemanda = generarPuntosCurva('demanda', escenario);
const puntosOferta = generarPuntosCurva('oferta', escenario);
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 / escenarios.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 analizado escenarios de superávit</p>
<div className="bg-gray-50 rounded-lg p-6 mb-6">
<div className="text-5xl font-bold text-amber-600 mb-2">{porcentaje}%</div>
<p className="text-gray-600">
{respuestasCorrectas} de {escenarios.length} respuestas correctas
</p>
</div>
<button
onClick={handleReiniciar}
className="inline-flex items-center gap-2 px-6 py-3 bg-amber-600 text-white rounded-lg font-medium hover:bg-amber-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">
<Package className="w-8 h-8 text-amber-600" />
<h2 className="text-2xl font-bold text-gray-800">Exceso de Oferta (Superávit)</h2>
</div>
<div className="flex items-center gap-4">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getDificultadColor(escenario.dificultad)}`}>
{escenario.dificultad.toUpperCase()}
</span>
<span className="text-sm text-gray-500">
{escenarioActual + 1} de {escenarios.length}
</span>
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-amber-600"
initial={{ width: 0 }}
animate={{ width: `${((escenarioActual + 1) / escenarios.length) * 100}%` }}
/>
</div>
</div>
</div>
<p className="text-gray-600">
Analiza qué sucede cuando el precio está por encima del equilibrio.
</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-2 text-center">Mercado de {escenario.producto}</h3>
<svg width="450" height="320" className="mx-auto">
{/* Grid */}
{Array.from({ length: 6 }).map((_, i) => (
<g key={i}>
<line x1={50 + i * 70} y1="30" x2={50 + i * 70} y2="300" stroke="#e5e7eb" strokeWidth="1" />
<line x1="50" y1={30 + i * 54} x2="400" y2={30 + i * 54} stroke="#e5e7eb" strokeWidth="1" />
</g>
))}
{/* Ejes */}
<line x1="50" y1="300" x2="400" y2="300" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="30" x2="50" y2="300" stroke="#374151" strokeWidth="2" />
{/* Labels */}
<text x="225" y="320" textAnchor="middle" className="text-sm fill-gray-600">Cantidad (Q)</text>
<text x="25" y="165" textAnchor="middle" transform="rotate(-90, 25, 165)" className="text-sm fill-gray-600">Precio (P)</text>
{/* Curva de Demanda */}
<path d={demandaPath} fill="none" stroke="#3b82f6" strokeWidth="3" />
<text x="380" y={scaleY(20)} className="text-sm fill-blue-600 font-medium">D</text>
{/* Curva de Oferta */}
<path d={ofertaPath} fill="none" stroke="#22c55e" strokeWidth="3" />
<text x="380" y={scaleY(maxP * 0.9)} className="text-sm fill-green-600 font-medium">S</text>
{/* Punto de equilibrio */}
<circle
cx={scaleX(equilibrio.Qd)}
cy={scaleY(escenario.precioEquilibrio)}
r="6"
fill="#8b5cf6"
stroke="white"
strokeWidth="2"
/>
<text x={scaleX(equilibrio.Qd) + 10} y={scaleY(escenario.precioEquilibrio)} className="text-xs fill-purple-600">
E
</text>
{/* Línea de precio mínimo */}
<line
x1="50"
y1={scaleY(escenario.precioMinimo)}
x2="400"
y2={scaleY(escenario.precioMinimo)}
stroke="#f59e0b"
strokeWidth="2"
strokeDasharray="5,5"
/>
<text x="410" y={scaleY(escenario.precioMinimo)} className="text-xs fill-amber-500 font-medium">
Pmin=${escenario.precioMinimo}
</text>
{/* Zona de exceso de oferta */}
<motion.g
initial={{ opacity: 0 }}
animate={{ opacity: mostrarResultado && esCorrecto ? 1 : 0.3 }}
>
<rect
x={scaleX(conMinimo.Qd)}
y={scaleY(escenario.precioMinimo) - 5}
width={scaleX(conMinimo.Qo) - scaleX(conMinimo.Qd)}
height="10"
fill="#fef3c7"
stroke="#f59e0b"
strokeWidth="2"
opacity="0.7"
/>
<text
x={scaleX((conMinimo.Qd + conMinimo.Qo) / 2)}
y={scaleY(escenario.precioMinimo) - 15}
textAnchor="middle"
className="text-xs fill-amber-700 font-medium"
>
Superávit
</text>
</motion.g>
{/* Puntos de cantidad */}
<circle
cx={scaleX(conMinimo.Qd)}
cy={scaleY(escenario.precioMinimo)}
r="5"
fill="#3b82f6"
stroke="white"
strokeWidth="2"
/>
<text x={scaleX(conMinimo.Qd)} y={scaleY(escenario.precioMinimo) + 20} textAnchor="middle" className="text-xs fill-blue-600">
Qd
</text>
<circle
cx={scaleX(conMinimo.Qo)}
cy={scaleY(escenario.precioMinimo)}
r="5"
fill="#22c55e"
stroke="white"
strokeWidth="2"
/>
<text x={scaleX(conMinimo.Qo)} y={scaleY(escenario.precioMinimo) + 20} textAnchor="middle" className="text-xs fill-green-600">
Qo
</text>
</svg>
<div className="mt-4 grid grid-cols-2 gap-3 text-sm">
<div className="bg-white p-2 rounded border">
<span className="text-purple-600 font-medium">Equilibrio:</span>
<p className="text-gray-700">P=${escenario.precioEquilibrio}, Q={Math.round(equilibrio.Qd)}</p>
</div>
<div className="bg-amber-50 p-2 rounded border border-amber-200">
<span className="text-amber-600 font-medium">Con Pmin:</span>
<p className="text-gray-700">Qd={Math.round(conMinimo.Qd)}, Qo={Math.round(conMinimo.Qo)}</p>
</div>
</div>
</div>
<div className="space-y-4">
<div className="bg-gradient-to-br from-amber-50 to-yellow-50 rounded-lg p-6">
<div className="flex items-start gap-3">
<BookOpen className="w-6 h-6 text-amber-600 flex-shrink-0 mt-1" />
<div>
<h3 className="font-semibold text-gray-800 mb-2">Escenario</h3>
<p className="text-gray-700">{escenario.contexto}</p>
</div>
</div>
</div>
<div className="bg-white border-2 border-amber-200 rounded-lg p-6">
<h3 className="font-semibold text-gray-800 mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-amber-600" />
Calcula el Exceso de Oferta
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
¿Cuántas unidades de superávit se generan?
</label>
<input
type="number"
value={respuestaExceso}
onChange={(e) => setRespuestaExceso(e.target.value)}
disabled={mostrarResultado}
placeholder="Ingresa el valor numérico"
className="w-full px-4 py-3 border-2 border-amber-200 rounded-lg focus:border-amber-500 focus:outline-none disabled:bg-gray-100"
/>
<p className="text-xs text-gray-500 mt-1">
Fórmula: Exceso de Oferta = Qo - Qd (al precio mínimo)
</p>
</div>
</div>
</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 ? (
<CheckCircle2 className="w-6 h-6 text-green-600 flex-shrink-0" />
) : (
<XCircle 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>
<div className="mt-2 text-sm text-gray-700">
<p>Al precio de ${escenario.precioMinimo}:</p>
<ul className="mt-1 space-y-1">
<li> Cantidad ofrecida: <strong>{Math.round(conMinimo.Qo)} unidades</strong></li>
<li> Cantidad demandada: <strong>{Math.round(conMinimo.Qd)} unidades</strong></li>
<li> Exceso de oferta: <strong>{Math.round(excesoOfertaReal)} unidades</strong></li>
</ul>
{!esCorrecto && (
<p className="mt-2 text-red-700">
La respuesta correcta es: <strong>{Math.round(excesoOfertaReal)} unidades</strong>
</p>
)}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="flex gap-3">
{!mostrarResultado ? (
<button
onClick={handleVerificar}
disabled={!respuestaExceso}
className="flex-1 py-3 px-4 bg-amber-600 text-white rounded-lg font-medium hover:bg-amber-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"
>
{escenarioActual < escenarios.length - 1 ? (
<>
Siguiente
<ArrowRight className="w-5 h-5" />
</>
) : (
'Finalizar'
)}
</button>
)}
</div>
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
<strong>Consecuencias del exceso de oferta:</strong>
</p>
<ul className="text-sm text-yellow-700 mt-1 space-y-1">
<li> Acumulación de inventarios</li>
<li> Presión para bajar precios</li>
<li> Necesidad de compras gubernamentales</li>
<li> Desperdicio de recursos</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default ExcesoOfertaSuperavit;

View File

@@ -0,0 +1,339 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { TrendingUp, TrendingDown, Users, DollarSign, Heart, Briefcase, Check, X, Trophy, RotateCcw } from 'lucide-react';
interface FactoresDesplazanDemandaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Factor {
id: string;
nombre: string;
icono: React.ReactNode;
descripcion: string;
efecto: 'aumenta' | 'disminuye';
explicacion: string;
ejemplo: string;
}
const factores: Factor[] = [
{
id: 'ingreso',
nombre: 'Ingreso de los consumidores',
icono: <DollarSign className="w-6 h-6" />,
descripcion: 'Cuando los ingresos cambian, la demanda de bienes normales se desplaza',
efecto: 'aumenta',
explicacion: 'Si los ingresos suben, los consumidores pueden comprar más de casi todo (bienes normales).',
ejemplo: 'Un aumento de sueldo permite comprar más ropa, comer fuera más seguido, etc.'
},
{
id: 'precios_relacionados',
nombre: 'Precios de bienes relacionados',
icono: <TrendingUp className="w-6 h-6" />,
descripcion: 'El precio de sustitutos y complementos afecta la demanda',
efecto: 'aumenta',
explicacion: 'Si el precio del café sube, la demanda de té (sustituto) aumenta.',
ejemplo: 'Si la gasolina sube, más personas quieren autos eléctricos (sustitutos del consumo de gasolina).'
},
{
id: 'gustos',
nombre: 'Gustos y preferencias',
icono: <Heart className="w-6 h-6" />,
descripcion: 'Las modas, publicidad y cambios culturales desplazan la demanda',
efecto: 'aumenta',
explicacion: 'Si un producto se vuelve popular, más personas lo quieren independientemente del precio.',
ejemplo: 'La moda de los celulares inteligentes desplazó la demanda de cámaras fotográficas tradicionales.'
},
{
id: 'expectativas',
nombre: 'Expectativas de precios futuros',
icono: <TrendingDown className="w-6 h-6" />,
descripcion: 'Lo que esperamos que pase con los precios afecta la demanda actual',
efecto: 'aumenta',
explicacion: 'Si esperamos que suban los precios, compramos más ahora (demanda aumenta).',
ejemplo: 'Antes de un aumento de impuestos a autos, la gente compra vehículos anticipadamente.'
},
{
id: 'poblacion',
nombre: 'Número de compradores',
icono: <Users className="w-6 h-6" />,
descripcion: 'Más consumidores en el mercado aumentan la demanda total',
efecto: 'aumenta',
explicacion: 'El crecimiento poblacional o la apertura de nuevos mercados aumenta la demanda.',
ejemplo: 'Una nueva colonia residencial aumenta la demanda de supermercados cercanos.'
},
{
id: 'demografia',
nombre: 'Cambios demográficos',
icono: <Briefcase className="w-6 h-6" />,
descripcion: 'La edad, género, ocupación y educación de la población afectan la demanda',
efecto: 'aumenta',
explicacion: 'Diferentes grupos demográficos tienen diferentes necesidades y preferencias.',
ejemplo: 'El envejecimiento poblacional aumenta la demanda de servicios de salud y centros de jubilados.'
}
];
interface PreguntaFactor {
factorId: string;
pregunta: string;
respuestaCorrecta: boolean;
escenario: string;
}
const preguntas: PreguntaFactor[] = [
{
factorId: 'ingreso',
pregunta: '¿Qué sucede con la demanda de bienes normales si los ingresos de los consumidores aumentan?',
respuestaCorrecta: true,
escenario: 'La economía está creciendo y los salarios suben un 10%'
},
{
factorId: 'precios_relacionados',
pregunta: 'Si el precio de la mantequilla sube mucho, ¿qué pasa con la demanda de margarina?',
respuestaCorrecta: true,
escenario: 'La mantequilla cuesta $10 y la margarina $5'
},
{
factorId: 'gustos',
pregunta: 'Un influencer popular recomienda un nuevo celular. ¿Qué sucede con su demanda?',
respuestaCorrecta: true,
escenario: 'El video del influencer tiene 10 millones de views'
},
{
factorId: 'expectativas',
pregunta: 'Se anuncia que el precio de la gasolina subirá mañana. ¿Qué pasa hoy con la demanda?',
respuestaCorrecta: true,
escenario: 'Todos los noticieros anuncian el aumento de precios'
},
{
factorId: 'poblacion',
pregunta: 'Una nueva fábrica trae 5,000 trabajadores a una ciudad pequeña. ¿Qué pasa con la demanda de vivienda?',
respuestaCorrecta: true,
escenario: 'La población de la ciudad se duplica en un año'
},
{
factorId: 'demografia',
pregunta: 'En un país, el 30% de la población tiene más de 60 años. ¿Qué servicios verán aumentada demanda?',
respuestaCorrecta: true,
escenario: 'La población está envejeciendo rápidamente'
}
];
export const FactoresDesplazanDemanda: React.FC<FactoresDesplazanDemandaProps> = ({
ejercicioId: _ejercicioId,
onComplete
}) => {
const [modo, setModo] = useState<'aprender' | 'practicar'>('aprender');
const [preguntaActual, setPreguntaActual] = useState(0);
const [respuestas, setRespuestas] = useState<boolean[]>([]);
const [mostrarFeedback, setMostrarFeedback] = useState(false);
const [score, setScore] = useState(0);
const [completado, setCompletado] = useState(false);
const handleRespuesta = (respuesta: boolean) => {
const esCorrecta = respuesta === preguntas[preguntaActual].respuestaCorrecta;
setRespuestas(prev => [...prev, esCorrecta]);
setMostrarFeedback(true);
};
const handleSiguiente = () => {
if (preguntaActual < preguntas.length - 1) {
setPreguntaActual(prev => prev + 1);
setMostrarFeedback(false);
} else {
const correctas = respuestas.filter(r => r).length + (mostrarFeedback && respuestas.length === preguntaActual ? 1 : 0);
const puntuacion = Math.round((correctas / preguntas.length) * 100);
setScore(puntuacion);
setCompletado(true);
setTimeout(() => {
if (onComplete) {
onComplete(puntuacion);
}
}, 3000);
}
};
const reiniciar = () => {
setModo('aprender');
setPreguntaActual(0);
setRespuestas([]);
setMostrarFeedback(false);
setScore(0);
setCompletado(false);
};
const pregunta = preguntas[preguntaActual];
const factorActual = factores.find(f => f.id === pregunta.factorId);
const ultimaRespuestaCorrecta = respuestas[respuestas.length - 1];
if (completado) {
return (
<div className="w-full max-w-3xl mx-auto p-6 bg-white rounded-xl shadow-lg text-center">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="inline-flex items-center justify-center w-20 h-20 bg-green-100 rounded-full mb-4"
>
<Trophy className="w-10 h-10 text-green-600" />
</motion.div>
<h2 className="text-2xl font-bold text-gray-800 mb-2">¡Ejercicio Completado!</h2>
<p className="text-gray-600 mb-4">Has identificado correctamente {respuestas.filter(r => r).length} de {preguntas.length} factores</p>
<div className="text-4xl font-bold text-blue-600 mb-2">{score}/100</div>
<p className="text-sm text-gray-500">Puntuación final</p>
</div>
);
}
if (modo === 'aprender') {
return (
<div className="w-full max-w-4xl mx-auto p-6 bg-white rounded-xl shadow-lg">
<div className="mb-6">
<h2 className="text-2xl font-bold text-gray-800 mb-2">Factores que Desplazan la Curva de Demanda</h2>
<p className="text-gray-600">
Estos 6 factores hacen que la curva de demanda se desplace (aumente o disminuya),
a diferencia de un movimiento a lo largo de la curva que solo el precio puede causar.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
{factores.map((factor, index) => (
<motion.div
key={factor.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="p-4 bg-blue-50 rounded-lg border-2 border-blue-100 hover:border-blue-300 transition-colors"
>
<div className="flex items-start gap-3">
<div className="p-2 bg-blue-600 text-white rounded-lg">
{factor.icono}
</div>
<div className="flex-1">
<h3 className="font-semibold text-gray-800 mb-1">{factor.nombre}</h3>
<p className="text-sm text-gray-600 mb-2">{factor.descripcion}</p>
<div className="text-xs bg-white p-2 rounded border border-blue-200">
<span className="font-medium text-blue-700">Ejemplo:</span> {factor.ejemplo}
</div>
</div>
</div>
</motion.div>
))}
</div>
<div className="flex justify-center">
<button
onClick={() => setModo('practicar')}
className="px-8 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center gap-2"
>
<Check className="w-5 h-5" />
Practicar con Ejercicios
</button>
</div>
</div>
);
}
return (
<div className="w-full max-w-3xl mx-auto p-6 bg-white rounded-xl shadow-lg">
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-800">Identifica el Factor</h2>
<span className="text-sm text-gray-500">Pregunta {preguntaActual + 1} de {preguntas.length}</span>
</div>
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-blue-600"
initial={{ width: 0 }}
animate={{ width: `${((preguntaActual + 1) / preguntas.length) * 100}%` }}
/>
</div>
</div>
{factorActual && (
<div className="mb-6 p-4 bg-blue-50 rounded-lg flex items-center gap-3">
<div className="p-2 bg-blue-600 text-white rounded-lg">
{factorActual.icono}
</div>
<span className="font-medium text-blue-800">Factor: {factorActual.nombre}</span>
</div>
)}
<div className="mb-6">
<div className="p-4 bg-gray-100 rounded-lg mb-4">
<span className="text-sm font-medium text-gray-600">Escenario:</span>
<p className="text-gray-800 mt-1">{pregunta.escenario}</p>
</div>
<h3 className="text-lg font-semibold text-gray-800">{pregunta.pregunta}</h3>
</div>
<AnimatePresence>
{mostrarFeedback && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mb-6 overflow-hidden"
>
<div className={`p-4 rounded-lg border ${
ultimaRespuestaCorrecta ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'
}`}>
<div className="flex items-start gap-3">
{ultimaRespuestaCorrecta ? (
<Check className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
) : (
<X className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
)}
<div>
<p className={`font-semibold ${ultimaRespuestaCorrecta ? 'text-green-800' : 'text-red-800'}`}>
{ultimaRespuestaCorrecta ? '¡Correcto!' : 'Incorrecto'}
</p>
{factorActual && (
<p className="text-sm mt-1 text-gray-700">
{factorActual.explicacion}
</p>
)}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="flex gap-4">
{!mostrarFeedback ? (
<>
<button
onClick={() => handleRespuesta(true)}
className="flex-1 py-3 px-4 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 transition-colors"
>
La demanda AUMENTA (se desplaza a la derecha)
</button>
<button
onClick={() => handleRespuesta(false)}
className="flex-1 py-3 px-4 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition-colors"
>
La demanda DISMINUYE (se desplaza a la izquierda)
</button>
</>
) : (
<button
onClick={handleSiguiente}
className="w-full py-3 px-4 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center justify-center gap-2"
>
{preguntaActual < preguntas.length - 1 ? 'Siguiente Pregunta' : 'Ver Resultados'}
</button>
)}
</div>
<button
onClick={() => setModo('aprender')}
className="mt-4 text-sm text-gray-500 hover:text-blue-600 transition-colors"
>
Volver a repasar los factores
</button>
</div>
);
};
export default FactoresDesplazanDemanda;

View File

@@ -0,0 +1,440 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ArrowRightLeft, CheckCircle2, XCircle, Trophy, RotateCcw, Factory, DollarSign, Users, Zap, Truck, AlertTriangle } from 'lucide-react';
interface FactoresDesplazanOfertaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
type Direccion = 'izquierda' | 'derecha' | 'ninguna';
type Categoria = 'tecnologia' | 'insumos' | 'competidores' | 'expectativas' | 'impuestos';
interface Factor {
id: number;
nombre: string;
icono: React.ReactNode;
descripcion: string;
efecto: Direccion;
explicacion: string;
categoria: Categoria;
color: string;
}
const factores: Factor[] = [
{
id: 1,
nombre: 'Tecnología',
icono: <Zap className="w-6 h-6" />,
descripcion: 'Nueva maquinaria reduce el tiempo de producción a la mitad',
efecto: 'derecha',
explicacion: 'La tecnología mejora la productividad, permitiendo producir más al mismo costo. La oferta aumenta (se desplaza a la derecha).',
categoria: 'tecnologia',
color: 'blue'
},
{
id: 2,
nombre: 'Costo de Insumos',
icono: <DollarSign className="w-6 h-6" />,
descripcion: 'El precio del petróleo (materia prima) sube un 50%',
efecto: 'izquierda',
explicacion: 'Al subir los costos de producción, es menos rentable fabricar. La oferta disminuye (se desplaza a la izquierda).',
categoria: 'insumos',
color: 'red'
},
{
id: 3,
nombre: 'Número de Vendedores',
icono: <Users className="w-6 h-6" />,
descripcion: 'Muchas nuevas empresas entran al mercado',
efecto: 'derecha',
explicacion: 'Más vendedores en el mercado significa más producción total. La oferta aumenta (se desplaza a la derecha).',
categoria: 'competidores',
color: 'blue'
},
{
id: 4,
nombre: 'Expectativas',
icono: <AlertTriangle className="w-6 h-6" />,
descripcion: 'Los productores esperan que el precio suba el próximo mes',
efecto: 'izquierda',
explicacion: 'Si esperan precios más altos mañana, retienen producción hoy. La oferta actual disminuye (se desplaza a la izquierda).',
categoria: 'expectativas',
color: 'orange'
},
{
id: 5,
nombre: 'Impuestos',
icono: <Factory className="w-6 h-6" />,
descripcion: 'El gobierno elimina un impuesto a la producción',
efecto: 'derecha',
explicacion: 'Sin el impuesto, los costos de producción bajan. La oferta aumenta (se desplaza a la derecha).',
categoria: 'impuestos',
color: 'blue'
},
{
id: 6,
nombre: 'Subsidios',
icono: <Truck className="w-6 h-6" />,
descripcion: 'El gobierno cancela un subsidio a los agricultores',
efecto: 'izquierda',
explicacion: 'Sin el subsidio, los costos de producción suben. La oferta disminuye (se desplaza a la izquierda).',
categoria: 'impuestos',
color: 'red'
}
];
export const FactoresDesplazanOferta: React.FC<FactoresDesplazanOfertaProps> = ({
onComplete,
ejercicioId: _ejercicioId
}) => {
const [factorActual, setFactorActual] = useState(0);
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<Direccion | null>(null);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [score, setScore] = useState(0);
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
const [completado, setCompletado] = useState(false);
const factor = factores[factorActual];
const handleSeleccionar = (direccion: Direccion) => {
if (mostrarResultado) return;
setRespuestaSeleccionada(direccion);
};
const handleVerificar = () => {
if (!respuestaSeleccionada) return;
const esCorrecta = respuestaSeleccionada === factor.efecto;
setMostrarResultado(true);
if (esCorrecta) {
setScore(prev => prev + Math.round(100 / factores.length));
setRespuestasCorrectas(prev => prev + 1);
}
};
const handleSiguiente = () => {
if (factorActual < factores.length - 1) {
setFactorActual(prev => prev + 1);
setRespuestaSeleccionada(null);
setMostrarResultado(false);
} else {
setCompletado(true);
if (onComplete) {
onComplete(score);
}
}
};
const handleReiniciar = () => {
setFactorActual(0);
setRespuestaSeleccionada(null);
setMostrarResultado(false);
setScore(0);
setRespuestasCorrectas(0);
setCompletado(false);
};
const renderGrafico = () => {
const isRight = factor.efecto === 'derecha';
return (
<svg width="300" height="250" className="mx-auto">
{/* Grid */}
{Array.from({ length: 6 }).map((_, i) => (
<g key={i}>
<line x1={50 + i * 40} y1="30" x2={50 + i * 40} y2="210" stroke="#e5e7eb" strokeWidth="1" />
<line x1="50" y1={30 + i * 36} x2="250" y2={30 + i * 36} stroke="#e5e7eb" strokeWidth="1" />
</g>
))}
{/* Ejes */}
<line x1="50" y1="210" x2="250" y2="210" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="30" x2="50" y2="210" stroke="#374151" strokeWidth="2" />
{/* Curva original S1 */}
<line x1="80" y1="180" x2="200" y2="80" stroke="#22c55e" strokeWidth="3" />
<text x="210" y="75" className="text-sm fill-green-600 font-medium">S</text>
{/* Curva desplazada S2 */}
<motion.g
initial={{ opacity: 0, x: isRight ? 30 : -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
<line
x1={isRight ? 110 : 50}
y1="180"
x2={isRight ? 230 : 170}
y2="80"
stroke="#22c55e"
strokeWidth="3"
strokeDasharray="5,5"
/>
<text x={isRight ? 240 : 180} y="75" className="text-sm fill-green-600 font-medium">S</text>
</motion.g>
{/* Flecha de dirección */}
{mostrarResultado && (
<motion.path
d={isRight ? 'M 260 130 L 280 130' : 'M 280 130 L 260 130'}
stroke="#22c55e"
strokeWidth="3"
markerEnd="url(#arrowhead)"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.5, delay: 0.3 }}
/>
)}
{/* Defs para flecha */}
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#22c55e" />
</marker>
</defs>
{/* Labels */}
<text x="150" y="235" textAnchor="middle" className="text-xs fill-gray-600">Cantidad</text>
<text x="25" y="120" textAnchor="middle" transform="rotate(-90, 25, 120)" className="text-xs fill-gray-600">Precio</text>
</svg>
);
};
if (completado) {
const porcentaje = Math.round((respuestasCorrectas / factores.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 identificado los factores que desplazan la oferta</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 {factores.length} respuestas correctas
</p>
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="p-4 bg-blue-50 rounded-lg">
<p className="text-2xl font-bold text-blue-600">
{factores.filter(f => f.efecto === 'derecha').length}
</p>
<p className="text-sm text-blue-700">Aumentan oferta </p>
</div>
<div className="p-4 bg-red-50 rounded-lg">
<p className="text-2xl font-bold text-red-600">
{factores.filter(f => f.efecto === 'izquierda').length}
</p>
<p className="text-sm text-red-700">Disminuyen oferta </p>
</div>
</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-4xl 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">
<ArrowRightLeft className="w-8 h-8 text-green-600" />
<h2 className="text-2xl font-bold text-gray-800">Factores que Desplazan la Oferta</h2>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-500">
{factorActual + 1} de {factores.length}
</span>
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-green-600"
initial={{ width: 0 }}
animate={{ width: `${((factorActual + 1) / factores.length) * 100}%` }}
/>
</div>
</div>
</div>
<p className="text-gray-600">
Identifica en qué dirección se desplaza la curva de oferta ante cada situación.
</p>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div>
<motion.div
className={`p-6 rounded-lg mb-4 bg-${factor.color}-50 border border-${factor.color}-200`}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
key={factor.id}
>
<div className="flex items-center gap-3 mb-3">
<div className={`p-3 rounded-full bg-${factor.color}-100 text-${factor.color}-600`}>
{factor.icono}
</div>
<h3 className="font-bold text-gray-800 text-lg">{factor.nombre}</h3>
</div>
<p className="text-gray-700">{factor.descripcion}</p>
</motion.div>
<div className="space-y-3">
<p className="text-sm font-medium text-gray-700">¿Qué ocurre con la oferta?</p>
{(['izquierda', 'derecha'] as Direccion[]).map((direccion) => {
const isSelected = respuestaSeleccionada === direccion;
const isCorrect = mostrarResultado && direccion === factor.efecto;
const isWrong = mostrarResultado && isSelected && direccion !== factor.efecto;
let buttonClass = 'w-full p-4 rounded-lg border-2 transition-all flex items-center gap-3 ';
if (isCorrect) {
buttonClass += 'border-green-500 bg-green-50';
} else if (isWrong) {
buttonClass += 'border-red-500 bg-red-50';
} else if (isSelected) {
buttonClass += 'border-green-500 bg-green-50';
} else {
buttonClass += 'border-gray-200 hover:border-gray-300 hover:bg-gray-50';
}
return (
<motion.button
key={direccion}
onClick={() => handleSeleccionar(direccion)}
disabled={mostrarResultado}
whileHover={!mostrarResultado ? { scale: 1.02 } : {}}
whileTap={!mostrarResultado ? { scale: 0.98 } : {}}
className={buttonClass}
>
<span className={`text-2xl ${
direccion === 'izquierda' ? 'transform rotate-180' : ''
}`}>
</span>
<span className={`flex-1 font-semibold ${
isCorrect ? 'text-green-700' :
isWrong ? 'text-red-700' :
isSelected ? 'text-green-700' : 'text-gray-700'
}`}>
{direccion === 'derecha' ? 'Aumenta (derecha)' : 'Disminuye (izquierda)'}
</span>
{isCorrect && <CheckCircle2 className="w-5 h-5 text-green-600" />}
{isWrong && <XCircle className="w-5 h-5 text-red-600" />}
</motion.button>
);
})}
</div>
<AnimatePresence>
{mostrarResultado && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className={`mt-4 p-4 rounded-lg ${
respuestaSeleccionada === factor.efecto
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}
>
<div className="flex items-start gap-2">
{respuestaSeleccionada === factor.efecto ? (
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
) : (
<XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
)}
<div>
<p className={`font-semibold ${
respuestaSeleccionada === factor.efecto ? 'text-green-800' : 'text-red-800'
}`}>
{respuestaSeleccionada === factor.efecto ? '¡Correcto!' : 'Incorrecto'}
</p>
<p className="text-sm mt-1 text-gray-700">{factor.explicacion}</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="mt-4 flex gap-3">
{!mostrarResultado ? (
<button
onClick={handleVerificar}
disabled={!respuestaSeleccionada}
className="flex-1 py-3 px-4 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Verificar respuesta
</button>
) : (
<button
onClick={handleSiguiente}
className="flex-1 py-3 px-4 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 transition-colors flex items-center justify-center gap-2"
>
{factorActual < factores.length - 1 ? 'Siguiente' : 'Finalizar'}
</button>
)}
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-700 mb-4 text-center">Visualización del Desplazamiento</h3>
{renderGrafico()}
<div className="mt-4 space-y-2 text-sm">
<div className="flex items-center gap-2">
<div className="w-8 h-0.5 bg-green-600"></div>
<span className="text-gray-600">S: Oferta original</span>
</div>
<div className="flex items-center gap-2">
<div className="w-8 h-0.5 border-t-2 border-dashed border-green-600"></div>
<span className="text-gray-600">S: Nueva oferta</span>
</div>
</div>
<div className="mt-4 p-3 bg-yellow-50 rounded-lg border border-yellow-200">
<p className="text-sm text-yellow-800">
<strong>Recuerda:</strong>
</p>
<ul className="text-sm text-yellow-700 mt-1 space-y-1">
<li> <strong> Derecha:</strong> Oferta aumenta</li>
<li> <strong> Izquierda:</strong> Oferta disminuye</li>
<li> El precio del bien NO desplaza la curva</li>
</ul>
</div>
</div>
</div>
{/* Indicadores de progreso */}
<div className="mt-6 flex justify-center items-center gap-1">
{factores.map((_, index) => (
<div
key={index}
className={`w-2 h-2 rounded-full ${
index === factorActual
? 'bg-green-600'
: index < factorActual
? 'bg-green-500'
: 'bg-gray-300'
}`}
/>
))}
</div>
</div>
);
};
export default FactoresDesplazanOferta;

View File

@@ -0,0 +1,448 @@
import React, { useState } from 'react';
interface PreguntaFactor {
id: number;
pregunta: string;
opciones: string[];
respuestaCorrecta: number;
explicacion: string;
categoria: 'sustitutos' | 'necesidad' | 'porcion' | 'tiempo' | 'definicion';
}
const preguntas: PreguntaFactor[] = [
{
id: 1,
pregunta: "¿Qué sucede con la elasticidad de la demanda cuando aumenta la disponibilidad de bienes sustitutos?",
opciones: [
"La elasticidad disminuye (se vuelve más inelástica)",
"La elasticidad aumenta (se vuelve más elástica)",
"La elasticidad no se ve afectada",
"La elasticidad se vuelve unitaria"
],
respuestaCorrecta: 1,
explicacion: "Cuanto más sustitutos disponibles tenga un bien, más elástica será su demanda. Los consumidores pueden cambiar fácilmente a alternativas cuando el precio sube, haciendo que la cantidad demandada responda más al cambio de precio.",
categoria: 'sustitutos'
},
{
id: 2,
pregunta: "¿Cuál de los siguientes bienes probablemente tenga la demanda más inelástica?",
opciones: [
"Un yate de lujo",
"Agua embotellada en un día normal",
"Entradas para un concierto de una banda específica",
"Una marca particular de cereal"
],
respuestaCorrecta: 1,
explicacion: "El agua es una necesidad básica sin sustitutos cercanos en la mayoría de situaciones. La demanda de necesidades es inelástica porque los consumidores la necesitan independientemente del precio, dentro de rangos razonables.",
categoria: 'necesidad'
},
{
id: 3,
pregunta: "Si el precio de la sal aumenta un 50%, ¿por qué la cantidad demandada probablemente no cambie significativamente?",
opciones: [
"Porque la sal es un lujo que todos quieren",
"Porque representa una pequeña porción del presupuesto y es una necesidad",
"Porque hay muchos sustitutos para la sal",
"Porque la ley prohíbe cambiar el consumo de sal"
],
respuestaCorrecta: 1,
explicacion: "La sal representa una porción muy pequeña del ingreso de los consumidores y es una necesidad básica. Incluso si el precio sube mucho, el impacto económico es mínimo y no hay sustitutos directos para su función en la alimentación.",
categoria: 'porcion'
},
{
id: 4,
pregunta: "¿Por qué la demanda de gasolina es más elástica a largo plazo que a corto plazo?",
opciones: [
"Porque la gasolina es más barata a largo plazo",
"Porque los consumidores pueden ajustar su comportamiento (comprar autos eficientes, mudarse, etc.)",
"Porque hay más estaciones de gasolina a largo plazo",
"Porque el gobierno regula los precios a largo plazo"
],
respuestaCorrecta: 1,
explicacion: "A corto plazo, los consumidores están 'atrapados' con sus vehículos y rutas actuales. A largo plazo, pueden hacer cambios significativos como comprar autos más eficientes, usar transporte público, mudarse más cerca del trabajo, etc., haciendo la demanda más sensible al precio.",
categoria: 'tiempo'
},
{
id: 5,
pregunta: "¿Qué relación existe entre el lujo/necesidad y la elasticidad de la demanda?",
opciones: [
"Los lujos tienen demanda inelástica; las necesidades tienen demanda elástica",
"Los lujos tienen demanda elástica; las necesidades tienen demanda inelástica",
"Ambos tienen la misma elasticidad",
"La elasticidad depende únicamente del precio, no del tipo de bien"
],
respuestaCorrecta: 1,
explicacion: "Los bienes de lujo tienen demanda elástica porque son discrecionales - los consumidores pueden reducir su consumo o eliminarlo si el precio sube. Las necesidades tienen demanda inelástica porque se requieren independientemente del precio.",
categoria: 'definicion'
},
{
id: 6,
pregunta: "Un bien representa el 30% del presupuesto mensual de una familia. ¿Qué podemos esperar sobre su elasticidad?",
opciones: [
"Será inelástica porque representa una porción grande del presupuesto",
"Será elástica porque los cambios de precio tendrán impacto significativo",
"La elasticidad solo depende de si es necesidad o lujo",
"No se puede determinar sin más información"
],
respuestaCorrecta: 1,
explicacion: "Cuando un bien representa una porción significativa del presupuesto, los consumidores son más sensibles a los cambios de precio. Un aumento de precio significaría un impacto sustancial en sus finanzas, por lo que buscarán alternativas o reducirán consumo, haciendo la demanda más elástica.",
categoria: 'porcion'
},
{
id: 7,
pregunta: "¿Cuál factor NO es determinante de la elasticidad precio de la demanda?",
opciones: [
"Disponibilidad de sustitutos",
"Naturaleza del bien (necesidad vs lujo)",
"Porción del ingreso que representa",
"El color del empaque del producto"
],
respuestaCorrecta: 3,
explicacion: "El color del empaque puede afectar las preferencias pero no determina la elasticidad precio de la demanda. Los factores clave son: disponibilidad de sustitutos, naturaleza del bien (necesidad/lujo), porción del ingreso, y horizonte temporal (corto vs largo plazo).",
categoria: 'definicion'
},
{
id: 8,
pregunta: "¿Por qué la demanda de medicamentos específicos para enfermedades crónicas es extremadamente inelástica?",
opciones: [
"Porque son muy baratos",
"Porque no tienen sustitutos y son necesarios para la salud",
"Porque hay muchas marcas competidoras",
"Porque representan una pequeña porción del ingreso"
],
respuestaCorrecta: 1,
explicacion: "Los medicamentos para enfermedades crónicas combinan dos factores de inelasticidad: son necesidades absolutas (sin ellos la salud se deteriora) y frecuentemente no tienen sustitutos terapéuticos equivalentes. Los pacientes deben comprarlos independientemente del precio.",
categoria: 'sustitutos'
}
];
export const FactoresElasticidad: React.FC = () => {
const [preguntaActual, setPreguntaActual] = useState<number>(0);
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<number | null>(null);
const [resultado, setResultado] = useState<{
correcto: boolean;
mostrarResultado: boolean;
} | null>(null);
const [puntuacion, setPuntuacion] = useState<number>(0);
const [respondidas, setRespondidas] = useState<number>(0);
const [mostrarResumen, setMostrarResumen] = useState<boolean>(false);
const pregunta = preguntas[preguntaActual];
const verificarRespuesta = (indice: number) => {
if (resultado?.mostrarResultado) return;
setRespuestaSeleccionada(indice);
const correcto = indice === pregunta.respuestaCorrecta;
if (correcto) {
setPuntuacion(prev => prev + 1);
}
setResultado({
correcto,
mostrarResultado: true
});
setRespondidas(prev => prev + 1);
};
const siguientePregunta = () => {
if (preguntaActual < preguntas.length - 1) {
setPreguntaActual(prev => prev + 1);
setRespuestaSeleccionada(null);
setResultado(null);
} else {
setMostrarResumen(true);
}
};
const reiniciarQuiz = () => {
setPreguntaActual(0);
setRespuestaSeleccionada(null);
setResultado(null);
setPuntuacion(0);
setRespondidas(0);
setMostrarResumen(false);
};
const getCategoriaIcon = (categoria: string) => {
switch (categoria) {
case 'sustitutos':
return (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
);
case 'necesidad':
return (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
);
case 'porcion':
return (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
case 'tiempo':
return (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
default:
return (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
);
}
};
const getCategoriaNombre = (categoria: string) => {
switch (categoria) {
case 'sustitutos': return 'Sustitutos';
case 'necesidad': return 'Lujo vs Necesidad';
case 'porcion': return 'Porción del Ingreso';
case 'tiempo': return 'Horizonte Temporal';
default: return 'Definiciones';
}
};
const getCategoriaColor = (categoria: string) => {
switch (categoria) {
case 'sustitutos': return 'bg-purple-100 text-purple-700 border-purple-200';
case 'necesidad': return 'bg-pink-100 text-pink-700 border-pink-200';
case 'porcion': return 'bg-green-100 text-green-700 border-green-200';
case 'tiempo': return 'bg-blue-100 text-blue-700 border-blue-200';
default: return 'bg-gray-100 text-gray-700 border-gray-200';
}
};
if (mostrarResumen) {
const porcentaje = Math.round((puntuacion / preguntas.length) * 100);
return (
<div className="max-w-4xl mx-auto p-6 bg-white rounded-lg shadow-md">
<div className="text-center py-8">
<div className="w-24 h-24 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-full mx-auto mb-6 flex items-center justify-center">
<span className="text-4xl font-bold text-white">{porcentaje}%</span>
</div>
<h2 className="text-3xl font-bold text-gray-800 mb-2">¡Quiz Completado!</h2>
<p className="text-gray-600 mb-6">
Has respondido {puntuacion} de {preguntas.length} preguntas correctamente
</p>
<div className="max-w-md mx-auto bg-gray-50 p-6 rounded-xl border border-gray-200 mb-8">
<div className="flex justify-between items-center mb-4">
<span className="text-gray-600">Respuestas correctas:</span>
<span className="font-bold text-green-600">{puntuacion}</span>
</div>
<div className="flex justify-between items-center mb-4">
<span className="text-gray-600">Respuestas incorrectas:</span>
<span className="font-bold text-red-600">{preguntas.length - puntuacion}</span>
</div>
<div className="h-4 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-green-500 to-green-600 transition-all duration-500"
style={{ width: `${porcentaje}%` }}
/>
</div>
</div>
<button
onClick={reiniciarQuiz}
className="bg-indigo-600 hover:bg-indigo-700 text-white px-8 py-3 rounded-lg font-semibold transition-colors inline-flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Intentar de Nuevo
</button>
</div>
</div>
);
}
return (
<div className="max-w-4xl mx-auto p-6 bg-white rounded-lg shadow-md">
<div className="flex justify-between items-center mb-6">
<div>
<h2 className="text-2xl font-bold text-blue-800">Factores de la Elasticidad</h2>
<p className="text-gray-600">Identifica cómo diferentes factores afectan la elasticidad de la demanda.</p>
</div>
<div className="text-right">
<div className="bg-blue-50 px-4 py-2 rounded-lg">
<p className="text-sm text-gray-600">Pregunta {preguntaActual + 1} de {preguntas.length}</p>
<p className="text-xl font-bold text-blue-700">{puntuacion}/{respondidas}</p>
</div>
</div>
</div>
<div className="mb-4">
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-indigo-500 transition-all duration-300"
style={{ width: `${((preguntaActual + 1) / preguntas.length) * 100}%` }}
/>
</div>
</div>
<div className="bg-gradient-to-br from-gray-50 to-indigo-50 p-6 rounded-xl border border-indigo-100 mb-6">
<div className="flex items-start gap-4">
<div className={`p-3 rounded-xl ${getCategoriaColor(pregunta.categoria)}`}>
{getCategoriaIcon(pregunta.categoria)}
</div>
<div className="flex-1">
<span className={`inline-block px-3 py-1 rounded-full text-xs font-semibold mb-3 ${getCategoriaColor(pregunta.categoria)}`}>
{getCategoriaNombre(pregunta.categoria)}
</span>
<h3 className="text-xl font-bold text-gray-800">{pregunta.pregunta}</h3>
</div>
</div>
</div>
<div className="space-y-3 mb-6">
{pregunta.opciones.map((opcion, indice) => (
<button
key={indice}
onClick={() => verificarRespuesta(indice)}
disabled={resultado?.mostrarResultado}
className={`w-full p-4 rounded-xl border-2 text-left transition-all ${
respuestaSeleccionada === indice
? indice === pregunta.respuestaCorrecta
? 'bg-green-100 border-green-500 ring-2 ring-green-200'
: 'bg-red-100 border-red-500 ring-2 ring-red-200'
: resultado?.mostrarResultado && indice === pregunta.respuestaCorrecta
? 'bg-green-100 border-green-500'
: 'bg-white border-gray-200 hover:border-indigo-300 hover:bg-indigo-50'
}`}
>
<div className="flex items-center gap-3">
<span className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm ${
respuestaSeleccionada === indice
? indice === pregunta.respuestaCorrecta
? 'bg-green-500 text-white'
: 'bg-red-500 text-white'
: resultado?.mostrarResultado && indice === pregunta.respuestaCorrecta
? 'bg-green-500 text-white'
: 'bg-gray-200 text-gray-600'
}`}>
{String.fromCharCode(65 + indice)}
</span>
<span className="flex-1">{opcion}</span>
{respuestaSeleccionada === indice && indice === pregunta.respuestaCorrecta && (
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
{respuestaSeleccionada === indice && indice !== pregunta.respuestaCorrecta && (
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
</div>
</button>
))}
</div>
{resultado?.mostrarResultado && (
<div className={`p-6 rounded-xl border-2 mb-6 ${resultado.correcto ? 'bg-green-50 border-green-300' : 'bg-red-50 border-red-300'}`}>
<div className="flex items-start gap-4">
<div className={`p-3 rounded-full ${resultado.correcto ? 'bg-green-100' : 'bg-red-100'}`}>
{resultado.correcto ? (
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
</div>
<div className="flex-1">
<h4 className={`font-bold text-lg ${resultado.correcto ? 'text-green-800' : 'text-red-800'}`}>
{resultado.correcto ? '¡Respuesta Correcta!' : 'Respuesta Incorrecta'}
</h4>
<div className="mt-4 bg-white p-4 rounded-lg border border-gray-200">
<p className="font-medium text-gray-800 mb-2">Explicación:</p>
<p className="text-gray-600">{pregunta.explicacion}</p>
</div>
{!resultado.correcto && (
<p className="mt-3 text-green-700 font-medium">
La respuesta correcta es: {pregunta.opciones[pregunta.respuestaCorrecta]}
</p>
)}
</div>
</div>
</div>
)}
{resultado?.mostrarResultado && (
<button
onClick={siguientePregunta}
className="w-full bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-4 rounded-lg font-semibold transition-colors flex items-center justify-center gap-2"
>
{preguntaActual < preguntas.length - 1 ? 'Siguiente Pregunta' : 'Ver Resultados'}
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</button>
)}
<div className="mt-8 bg-gradient-to-r from-indigo-50 to-purple-50 p-5 rounded-xl border border-indigo-200">
<h4 className="font-bold text-indigo-800 mb-4 flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
Factores Clave de la Elasticidad
</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="bg-white p-3 rounded-lg border border-purple-200">
<div className="text-purple-600 mb-1">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
</div>
<p className="font-medium text-sm text-gray-800">Sustitutos</p>
<p className="text-xs text-gray-500">Más sustitutos = más elástica</p>
</div>
<div className="bg-white p-3 rounded-lg border border-pink-200">
<div className="text-pink-600 mb-1">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
</div>
<p className="font-medium text-sm text-gray-800">Necesidad</p>
<p className="text-xs text-gray-500">Necesidades = inelástica</p>
</div>
<div className="bg-white p-3 rounded-lg border border-green-200">
<div className="text-green-600 mb-1">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="font-medium text-sm text-gray-800">Porción</p>
<p className="text-xs text-gray-500">% mayor del ingreso = más elástica</p>
</div>
<div className="bg-white p-3 rounded-lg border border-blue-200">
<div className="text-blue-600 mb-1">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="font-medium text-sm text-gray-800">Tiempo</p>
<p className="text-xs text-gray-500">Largo plazo = más elástica</p>
</div>
</div>
</div>
</div>
);
};
export default FactoresElasticidad;

View File

@@ -0,0 +1,244 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { HelpCircle, Check, X, ArrowRight, Trophy } from 'lucide-react';
interface LeyDemandaQuizProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Pregunta {
id: number;
pregunta: string;
opciones: string[];
correcta: number;
explicacion: string;
}
const preguntas: Pregunta[] = [
{
id: 1,
pregunta: "¿Qué sucede con la cantidad demandada cuando el precio de un bien aumenta?",
opciones: [
"Aumenta proporcionalmente",
"Disminuye (ley de la demanda)",
"Se mantiene constante",
"Depende del tipo de bien"
],
correcta: 1,
explicacion: "Según la Ley de la Demanda, existe una relación inversa entre precio y cantidad demandada: cuando el precio sube, la cantidad demandada baja."
},
{
id: 2,
pregunta: "¿Cuál es la forma típica de la curva de demanda?",
opciones: [
"Línea horizontal",
"Línea vertical",
"Pendiente descendente (de izquierda a derecha)",
"Pendiente ascendente"
],
correcta: 2,
explicacion: "La curva de demanda tiene pendiente descendente porque a precios más bajos, los consumidores están dispuestos a comprar más cantidad."
},
{
id: 3,
pregunta: "Si el precio del helado baja de $5 a $3, ¿qué esperamos que ocurra?",
opciones: [
"La gente comprará menos helado",
"No cambiará la cantidad demandada",
"La gente comprará más helado",
"Solo los ricos comprarán helado"
],
correcta: 2,
explicacion: "Una disminución en el precio genera un aumento en la cantidad demandada (movimiento a lo largo de la curva)."
},
{
id: 4,
pregunta: "¿Qué representa el eje vertical (Y) en un gráfico de demanda?",
opciones: [
"Cantidad demandada",
"Precio del bien",
"Ingreso de los consumidores",
"Tiempo"
],
correcta: 1,
explicacion: "En un gráfico de demanda estándar, el eje Y representa el Precio y el eje X representa la Cantidad."
},
{
id: 5,
pregunta: "Complete: 'A mayor precio, ______ cantidad demandada'",
opciones: [
"Mayor",
"Menor",
"Igual",
"No hay relación"
],
correcta: 1,
explicacion: "La ley de la demanda establece que a mayor precio, menor cantidad demandada (relación inversa)."
}
];
export const LeyDemandaQuiz: React.FC<LeyDemandaQuizProps> = ({ ejercicioId: _ejercicioId, onComplete }) => {
const [preguntaActual, setPreguntaActual] = useState(0);
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<number | null>(null);
const [mostrarFeedback, setMostrarFeedback] = useState(false);
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
const [completado, setCompletado] = useState(false);
const pregunta = preguntas[preguntaActual];
const esCorrecta = respuestaSeleccionada === pregunta.correcta;
const handleSeleccionar = (index: number) => {
if (mostrarFeedback) return;
setRespuestaSeleccionada(index);
};
const handleValidar = () => {
if (respuestaSeleccionada === null) return;
setMostrarFeedback(true);
if (esCorrecta) {
setRespuestasCorrectas(prev => prev + 1);
}
};
const handleSiguiente = () => {
if (preguntaActual < preguntas.length - 1) {
setPreguntaActual(prev => prev + 1);
setRespuestaSeleccionada(null);
setMostrarFeedback(false);
} else {
const puntuacion = Math.round((respuestasCorrectas + (esCorrecta ? 1 : 0)) / preguntas.length * 100);
setCompletado(true);
if (onComplete) {
onComplete(puntuacion);
}
}
};
const calcularProgreso = () => ((preguntaActual + 1) / preguntas.length) * 100;
if (completado) {
const puntuacionFinal = Math.round(respuestasCorrectas / preguntas.length * 100);
return (
<div className="w-full max-w-2xl mx-auto p-6 bg-white rounded-xl shadow-lg text-center">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="inline-flex items-center justify-center w-20 h-20 bg-green-100 rounded-full mb-4"
>
<Trophy className="w-10 h-10 text-green-600" />
</motion.div>
<h2 className="text-2xl font-bold text-gray-800 mb-2">¡Quiz Completado!</h2>
<p className="text-gray-600 mb-4">Has respondido {respuestasCorrectas} de {preguntas.length} preguntas correctamente</p>
<div className="text-4xl font-bold text-blue-600 mb-2">{puntuacionFinal}/100</div>
<p className="text-sm text-gray-500">Puntuación final</p>
</div>
);
}
return (
<div className="w-full max-w-2xl mx-auto p-6 bg-white rounded-xl shadow-lg">
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-800">Quiz: Ley de la Demanda</h2>
<span className="text-sm text-gray-500">Pregunta {preguntaActual + 1} de {preguntas.length}</span>
</div>
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-blue-600"
initial={{ width: 0 }}
animate={{ width: `${calcularProgreso()}%` }}
/>
</div>
</div>
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">{pregunta.pregunta}</h3>
<div className="space-y-3">
{pregunta.opciones.map((opcion, index) => (
<motion.button
key={index}
onClick={() => handleSeleccionar(index)}
disabled={mostrarFeedback}
whileHover={!mostrarFeedback ? { scale: 1.02 } : {}}
whileTap={!mostrarFeedback ? { scale: 0.98 } : {}}
className={`w-full p-4 rounded-lg border-2 text-left transition-all ${
respuestaSeleccionada === index
? mostrarFeedback
? index === pregunta.correcta
? 'border-green-500 bg-green-50'
: 'border-red-500 bg-red-50'
: 'border-blue-500 bg-blue-50'
: mostrarFeedback && index === pregunta.correcta
? 'border-green-500 bg-green-50'
: 'border-gray-200 hover:border-blue-300'
}`}
>
<div className="flex items-center justify-between">
<span className="font-medium">{opcion}</span>
{mostrarFeedback && index === pregunta.correcta && (
<Check className="w-5 h-5 text-green-600" />
)}
{mostrarFeedback && respuestaSeleccionada === index && index !== pregunta.correcta && (
<X className="w-5 h-5 text-red-600" />
)}
</div>
</motion.button>
))}
</div>
</div>
<AnimatePresence>
{mostrarFeedback && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mb-6 overflow-hidden"
>
<div className={`p-4 rounded-lg border ${esCorrecta ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}>
<div className="flex items-start gap-3">
{esCorrecta ? (
<Check className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
) : (
<X className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
)}
<div>
<p className={`font-semibold ${esCorrecta ? 'text-green-800' : 'text-red-800'}`}>
{esCorrecta ? '¡Correcto!' : 'Incorrecto'}
</p>
<p className={`text-sm mt-1 ${esCorrecta ? 'text-green-700' : 'text-red-700'}`}>
{pregunta.explicacion}
</p>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="flex justify-end">
{!mostrarFeedback ? (
<button
onClick={handleValidar}
disabled={respuestaSeleccionada === null}
className="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Validar Respuesta
</button>
) : (
<button
onClick={handleSiguiente}
className="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center gap-2"
>
{preguntaActual < preguntas.length - 1 ? 'Siguiente' : 'Finalizar'}
<ArrowRight className="w-4 h-4" />
</button>
)}
</div>
</div>
);
};
export default LeyDemandaQuiz;

View File

@@ -0,0 +1,340 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { TrendingUp, CheckCircle2, XCircle, Trophy, RotateCcw, BookOpen, ArrowRight, ArrowLeft } from 'lucide-react';
interface LeyOfertaQuizProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Pregunta {
id: number;
pregunta: string;
opciones: string[];
respuestaCorrecta: number;
explicacion: string;
}
const preguntas: Pregunta[] = [
{
id: 1,
pregunta: 'Según la Ley de la Oferta, ¿qué ocurre cuando el precio de un bien aumenta?',
opciones: [
'Los productores ofrecen menos cantidad',
'Los productores ofrecen más cantidad',
'La cantidad ofrecida no cambia',
'La demanda aumenta'
],
respuestaCorrecta: 1,
explicacion: 'La Ley de la Oferta establece que existe una relación directa entre precio y cantidad ofrecida: cuando sube el precio, los productores quieren vender más.'
},
{
id: 2,
pregunta: '¿Por qué la curva de oferta tiene pendiente positiva?',
opciones: [
'Porque a mayor precio, mayor es el costo de producción',
'Porque los consumidores compran más cuando bajan los precios',
'Porque a mayor precio, más rentable es producir y vender',
'Porque el gobierno lo establece así'
],
respuestaCorrecta: 2,
explicacion: 'La pendiente positiva refleja que a precios más altos, la producción es más rentable, incentivando a los productores a ofrecer más cantidad.'
},
{
id: 3,
pregunta: 'Un agricultor vende manzanas. Si el precio pasa de $2 a $4 por kg, ¿qué esperamos?',
opciones: [
'Venderá la misma cantidad de siempre',
'Querrá vender menos porque es más caro',
'Querrá vender más manzanas al mercado',
'Dejará de vender manzanas'
],
respuestaCorrecta: 2,
explicacion: 'Al duplicar el precio, el agricultor tiene más incentivo para llevar más manzanas al mercado, aumentando su oferta.'
},
{
id: 4,
pregunta: '¿Cuál de los siguientes es un movimiento A LO LARGO de la curva de oferta?',
opciones: [
'Mejora tecnológica que reduce costos',
'Aumento del precio del petróleo (insumo)',
'Subida del precio del bien, aumentando cantidad ofrecida',
'Entrada de nuevos competidores al mercado'
],
respuestaCorrecta: 2,
explicacion: 'El movimiento a lo largo de la curva ocurre solo cuando cambia el precio del propio bien. Los otros factores desplazan toda la curva.'
},
{
id: 5,
pregunta: 'La relación precio-cantidad ofrecida es:',
opciones: [
'Inversa (negativa)',
'Directa (positiva)',
'No existe relación',
'Depende del tipo de bien'
],
respuestaCorrecta: 1,
explicacion: 'La relación es directa o positiva: a mayor precio, mayor cantidad ofrecida. Esto es lo opuesto a la demanda, que tiene relación inversa.'
}
];
export const LeyOfertaQuiz: React.FC<LeyOfertaQuizProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [preguntaActual, setPreguntaActual] = useState(0);
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<number | null>(null);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [score, setScore] = useState(0);
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
const [completado, setCompletado] = useState(false);
const pregunta = preguntas[preguntaActual];
const handleSeleccionar = (index: number) => {
if (mostrarResultado) return;
setRespuestaSeleccionada(index);
};
const handleVerificar = () => {
if (respuestaSeleccionada === null) return;
const esCorrecta = respuestaSeleccionada === pregunta.respuestaCorrecta;
setMostrarResultado(true);
if (esCorrecta) {
setScore(prev => prev + Math.round(100 / preguntas.length));
setRespuestasCorrectas(prev => prev + 1);
}
};
const handleSiguiente = () => {
if (preguntaActual < preguntas.length - 1) {
setPreguntaActual(prev => prev + 1);
setRespuestaSeleccionada(null);
setMostrarResultado(false);
} else {
setCompletado(true);
if (onComplete) {
onComplete(score);
}
}
};
const handleReiniciar = () => {
setPreguntaActual(0);
setRespuestaSeleccionada(null);
setMostrarResultado(false);
setScore(0);
setRespuestasCorrectas(0);
setCompletado(false);
};
if (completado) {
const porcentaje = Math.round((respuestasCorrectas / preguntas.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">¡Quiz Completado!</h2>
<p className="text-gray-600 mb-6">Has demostrado tu comprensión de la Ley de la Oferta</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 {preguntas.length} respuestas correctas
</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-3xl 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">
<TrendingUp className="w-8 h-8 text-green-600" />
<h2 className="text-2xl font-bold text-gray-800">Ley de la Oferta</h2>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-500">
{preguntaActual + 1} de {preguntas.length}
</span>
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-green-600"
initial={{ width: 0 }}
animate={{ width: `${((preguntaActual + 1) / preguntas.length) * 100}%` }}
/>
</div>
</div>
</div>
<p className="text-gray-600">
Responde las preguntas sobre la relación entre precio y cantidad ofrecida.
</p>
</div>
<div className="bg-gradient-to-br from-green-50 to-emerald-50 rounded-lg p-6 mb-6">
<div className="flex items-start gap-3">
<BookOpen className="w-6 h-6 text-green-600 flex-shrink-0 mt-1" />
<div>
<h3 className="font-semibold text-gray-800 mb-2">Pregunta {pregunta.id}</h3>
<p className="text-gray-700 text-lg">{pregunta.pregunta}</p>
</div>
</div>
</div>
<div className="space-y-3 mb-6">
{pregunta.opciones.map((opcion, index) => {
const isSelected = respuestaSeleccionada === index;
const isCorrect = mostrarResultado && index === pregunta.respuestaCorrecta;
const isWrong = mostrarResultado && isSelected && index !== pregunta.respuestaCorrecta;
let buttonClass = 'w-full p-4 rounded-lg border-2 text-left transition-all flex items-center gap-3 ';
if (isCorrect) {
buttonClass += 'border-green-500 bg-green-50';
} else if (isWrong) {
buttonClass += 'border-red-500 bg-red-50';
} else if (isSelected) {
buttonClass += 'border-green-500 bg-green-50';
} else {
buttonClass += 'border-gray-200 hover:border-gray-300 hover:bg-gray-50';
}
return (
<motion.button
key={index}
onClick={() => handleSeleccionar(index)}
disabled={mostrarResultado}
whileHover={!mostrarResultado ? { scale: 1.01 } : {}}
whileTap={!mostrarResultado ? { scale: 0.99 } : {}}
className={buttonClass}
>
<span className={`w-8 h-8 rounded-full flex items-center justify-center font-semibold ${
isCorrect ? 'bg-green-500 text-white' :
isWrong ? 'bg-red-500 text-white' :
isSelected ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-700'
}`}>
{String.fromCharCode(65 + index)}
</span>
<span className={`flex-1 ${
isCorrect ? 'text-green-700 font-semibold' :
isWrong ? 'text-red-700' :
isSelected ? 'text-green-700' : 'text-gray-700'
}`}>
{opcion}
</span>
{isCorrect && <CheckCircle2 className="w-5 h-5 text-green-600" />}
{isWrong && <XCircle className="w-5 h-5 text-red-600" />}
</motion.button>
);
})}
</div>
<AnimatePresence>
{mostrarResultado && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className={`mb-6 p-4 rounded-lg ${
respuestaSeleccionada === pregunta.respuestaCorrecta
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}
>
<div className="flex items-start gap-2">
{respuestaSeleccionada === pregunta.respuestaCorrecta ? (
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
) : (
<XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
)}
<div>
<p className={`font-semibold ${
respuestaSeleccionada === pregunta.respuestaCorrecta ? 'text-green-800' : 'text-red-800'
}`}>
{respuestaSeleccionada === pregunta.respuestaCorrecta ? '¡Correcto!' : 'Incorrecto'}
</p>
<p className="text-sm mt-1 text-gray-700">{pregunta.explicacion}</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="flex gap-3">
{!mostrarResultado ? (
<button
onClick={handleVerificar}
disabled={respuestaSeleccionada === null}
className="flex-1 py-3 px-4 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Verificar respuesta
</button>
) : (
<button
onClick={handleSiguiente}
className="flex-1 py-3 px-4 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 transition-colors flex items-center justify-center gap-2"
>
{preguntaActual < preguntas.length - 1 ? (
<>
Siguiente
<ArrowRight className="w-5 h-5" />
</>
) : (
'Finalizar'
)}
</button>
)}
</div>
<div className="mt-6 flex justify-between items-center">
<button
onClick={() => setPreguntaActual(Math.max(0, preguntaActual - 1))}
disabled={preguntaActual === 0}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Anterior
</button>
<div className="flex items-center gap-1">
{preguntas.map((_, index) => (
<div
key={index}
className={`w-2 h-2 rounded-full ${
index === preguntaActual
? 'bg-green-600'
: index < preguntaActual
? 'bg-green-500'
: 'bg-gray-300'
}`}
/>
))}
</div>
<button
onClick={() => setPreguntaActual(Math.min(preguntas.length - 1, preguntaActual + 1))}
disabled={preguntaActual === preguntas.length - 1}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Siguiente
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
);
};
export default LeyOfertaQuiz;

View File

@@ -0,0 +1,443 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Clock, CheckCircle2, XCircle, Trophy, RotateCcw, TrendingUp, AlertCircle, BookOpen } from 'lucide-react';
interface OfertaCortoLargoPlazoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Escenario {
id: number;
tipo: 'corto' | 'largo';
descripcion: string;
tiempo: string;
opciones: string[];
respuestaCorrecta: number;
explicacion: string;
}
const escenarios: Escenario[] = [
{
id: 1,
tipo: 'corto',
descripcion: 'El precio del café sube de $5 a $10 por libra. Los agricultores tienen 1 mes para reaccionar.',
tiempo: 'Plazo: 1 mes',
opciones: [
'Pueden plantar más árboles de café y aumentar significativamente la producción',
'Solo pueden cosechar más del cultivo existente, aumento limitado de oferta',
'La oferta no cambia en absoluto',
'Pueden contratar más trabajadores inmediatamente y duplicar la producción'
],
respuestaCorrecta: 1,
explicacion: 'En el corto plazo, los agricultores no pueden plantar nuevos árboles (toman 3-4 años en producir). Solo pueden cosechar más del cultivo existente, por lo que el aumento de oferta es limitado.'
},
{
id: 2,
tipo: 'largo',
descripcion: 'El precio del café se mantiene alto a $10 por libra durante 5 años. Los agricultores pueden planificar a futuro.',
tiempo: 'Plazo: 5 años',
opciones: [
'La oferta permanece igual que al inicio',
'Solo pueden vender lo que ya tenían almacenado',
'Pueden plantar nuevos árboles, expandir fincas y aumentar significativamente la oferta',
'El gobierno controla cuánto pueden producir'
],
respuestaCorrecta: 2,
explicacion: 'En el largo plazo, los agricultores pueden hacer todo: plantar nuevos árboles, comprar más tierra, invertir en tecnología. La oferta es mucho más elástica.'
},
{
id: 3,
tipo: 'corto',
descripcion: 'Una fábrica de autos recibe un pedido urgente. Necesita aumentar la producción esta semana.',
tiempo: 'Plazo: 1 semana',
opciones: [
'Puede construir una nueva planta de producción rápidamente',
'Solo puede aumentar turnos existentes y usar inventarios, aumento limitado',
'Puede contratar y entrenar a 500 nuevos trabajadores en 2 días',
'La producción se duplica automáticamente'
],
respuestaCorrecta: 1,
explicacion: 'En el corto plazo, la fábrica no puede construir nuevas instalaciones. Solo puede aumentar turnos, usar inventarios o pedir horas extras. La capacidad de aumentar oferta es limitada.'
},
{
id: 4,
tipo: 'largo',
descripcion: 'La demanda de software crece constantemente durante 3 años. Las empresas tecnológicas responden.',
tiempo: 'Plazo: 3 años',
opciones: [
'No pueden hacer nada, la oferta de programadores es fija',
'Pueden contratar algunos freelancers temporalmente',
'Pueden contratar y formar programadores, expandir oficinas, adaptar toda su capacidad',
'El precio sube pero la cantidad ofrecida no cambia'
],
respuestaCorrecta: 2,
explicacion: 'En el largo plazo, las empresas pueden formar nuevos programadores (universidades, bootcamps), abrir oficinas en nuevas ciudades, adaptar completamente su capacidad productiva.'
},
{
id: 5,
tipo: 'corto',
descripcion: 'Un huracán destruye refinerías de petróleo. El precio sube. ¿Qué pueden hacer otras refinerías?',
tiempo: 'Plazo: Inmediato',
opciones: [
'Construir nuevas refinerías en un mes',
'Operar al máximo de su capacidad existente, aumento muy limitado',
'Descubrir petróleo nuevo en semanas',
'La oferta de petróleo es infinitamente elástica'
],
respuestaCorrecta: 1,
explicacion: 'Las refinerías existentes ya operan cerca de su capacidad máxima. En el corto plazo no pueden construir nuevas instalaciones (toma años). Solo pueden intentar operar al máximo.'
}
];
export const OfertaCortoLargoPlazo: React.FC<OfertaCortoLargoPlazoProps> = ({
onComplete,
ejercicioId: _ejercicioId
}) => {
const [escenarioActual, setEscenarioActual] = useState(0);
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<number | null>(null);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [score, setScore] = useState(0);
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
const [completado, setCompletado] = useState(false);
const escenario = escenarios[escenarioActual];
const handleSeleccionar = (index: number) => {
if (mostrarResultado) return;
setRespuestaSeleccionada(index);
};
const handleVerificar = () => {
if (respuestaSeleccionada === null) return;
const esCorrecta = respuestaSeleccionada === escenario.respuestaCorrecta;
setMostrarResultado(true);
if (esCorrecta) {
setScore(prev => prev + Math.round(100 / escenarios.length));
setRespuestasCorrectas(prev => prev + 1);
}
};
const handleSiguiente = () => {
if (escenarioActual < escenarios.length - 1) {
setEscenarioActual(prev => prev + 1);
setRespuestaSeleccionada(null);
setMostrarResultado(false);
} else {
setCompletado(true);
if (onComplete) {
onComplete(score);
}
}
};
const handleReiniciar = () => {
setEscenarioActual(0);
setRespuestaSeleccionada(null);
setMostrarResultado(false);
setScore(0);
setRespuestasCorrectas(0);
setCompletado(false);
};
if (completado) {
const porcentaje = Math.round((respuestasCorrectas / escenarios.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 comprendido la elasticidad temporal de la oferta</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 {escenarios.length} respuestas correctas
</p>
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="p-4 bg-orange-50 rounded-lg border border-orange-200">
<Clock className="w-8 h-8 text-orange-600 mx-auto mb-2" />
<h3 className="font-semibold text-orange-800">Corto Plazo</h3>
<p className="text-sm text-orange-700">Oferta inelástica</p>
</div>
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<TrendingUp className="w-8 h-8 text-blue-600 mx-auto mb-2" />
<h3 className="font-semibold text-blue-800">Largo Plazo</h3>
<p className="text-sm text-blue-700">Oferta más elástica</p>
</div>
</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-4xl 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">
<Clock className="w-8 h-8 text-green-600" />
<h2 className="text-2xl font-bold text-gray-800">Oferta: Corto vs Largo Plazo</h2>
</div>
<div className="flex items-center gap-4">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
escenario.tipo === 'corto'
? 'bg-orange-100 text-orange-700'
: 'bg-blue-100 text-blue-700'
}`}>
{escenario.tipo === 'corto' ? 'CORTO PLAZO' : 'LARGO PLAZO'}
</span>
<span className="text-sm text-gray-500">
{escenarioActual + 1} de {escenarios.length}
</span>
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-green-600"
initial={{ width: 0 }}
animate={{ width: `${((escenarioActual + 1) / escenarios.length) * 100}%` }}
/>
</div>
</div>
</div>
<p className="text-gray-600">
Determina si el escenario describe el corto plazo (oferta inelástica) o largo plazo (oferta elástica).
</p>
</div>
<div className="grid md:grid-cols-5 gap-6">
<div className="md:col-span-3">
<motion.div
className={`p-6 rounded-lg mb-4 ${
escenario.tipo === 'corto'
? 'bg-orange-50 border border-orange-200'
: 'bg-blue-50 border border-blue-200'
}`}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
key={escenario.id}
>
<div className="flex items-start gap-3 mb-3">
<BookOpen className={`w-6 h-6 flex-shrink-0 mt-0.5 ${
escenario.tipo === 'corto' ? 'text-orange-600' : 'text-blue-600'
}`} />
<div>
<h3 className="font-bold text-gray-800 text-lg mb-1">
Escenario {escenario.id}
</h3>
<p className={`inline-block px-2 py-1 rounded text-xs font-medium mb-3 ${
escenario.tipo === 'corto'
? 'bg-orange-200 text-orange-800'
: 'bg-blue-200 text-blue-800'
}`}>
{escenario.tiempo}
</p>
<p className="text-gray-700">{escenario.descripcion}</p>
</div>
</div>
</motion.div>
<div className="space-y-3">
{escenario.opciones.map((opcion, index) => {
const isSelected = respuestaSeleccionada === index;
const isCorrect = mostrarResultado && index === escenario.respuestaCorrecta;
const isWrong = mostrarResultado && isSelected && index !== escenario.respuestaCorrecta;
let buttonClass = 'w-full p-4 rounded-lg border-2 text-left transition-all ';
if (isCorrect) {
buttonClass += 'border-green-500 bg-green-50';
} else if (isWrong) {
buttonClass += 'border-red-500 bg-red-50';
} else if (isSelected) {
buttonClass += 'border-green-500 bg-green-50';
} else {
buttonClass += 'border-gray-200 hover:border-gray-300 hover:bg-gray-50';
}
return (
<motion.button
key={index}
onClick={() => handleSeleccionar(index)}
disabled={mostrarResultado}
whileHover={!mostrarResultado ? { scale: 1.01 } : {}}
whileTap={!mostrarResultado ? { scale: 0.99 } : {}}
className={buttonClass}
>
<div className="flex items-start gap-3">
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-sm font-semibold flex-shrink-0 ${
isCorrect ? 'bg-green-500 text-white' :
isWrong ? 'bg-red-500 text-white' :
isSelected ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-700'
}`}>
{String.fromCharCode(65 + index)}
</span>
<span className={`flex-1 ${
isCorrect ? 'text-green-700 font-medium' :
isWrong ? 'text-red-700' :
isSelected ? 'text-green-700' : 'text-gray-700'
}`}>
{opcion}
</span>
{isCorrect && <CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0" />}
{isWrong && <XCircle className="w-5 h-5 text-red-600 flex-shrink-0" />}
</div>
</motion.button>
);
})}
</div>
<AnimatePresence>
{mostrarResultado && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className={`mt-4 p-4 rounded-lg ${
respuestaSeleccionada === escenario.respuestaCorrecta
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}
>
<div className="flex items-start gap-2">
{respuestaSeleccionada === escenario.respuestaCorrecta ? (
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
) : (
<XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
)}
<div>
<p className={`font-semibold ${
respuestaSeleccionada === escenario.respuestaCorrecta ? 'text-green-800' : 'text-red-800'
}`}>
{respuestaSeleccionada === escenario.respuestaCorrecta ? '¡Correcto!' : 'Incorrecto'}
</p>
<p className="text-sm mt-1 text-gray-700">{escenario.explicacion}</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<div className="mt-4 flex gap-3">
{!mostrarResultado ? (
<button
onClick={handleVerificar}
disabled={respuestaSeleccionada === null}
className="flex-1 py-3 px-4 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Verificar respuesta
</button>
) : (
<button
onClick={handleSiguiente}
className="flex-1 py-3 px-4 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 transition-colors"
>
{escenarioActual < escenarios.length - 1 ? 'Siguiente escenario' : 'Finalizar'}
</button>
)}
</div>
</div>
<div className="md:col-span-2 space-y-4">
{/* Gráfico de elasticidad */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-700 mb-4 text-center">Elasticidad de la Oferta</h3>
<svg width="250" height="200" className="mx-auto">
{/* Grid */}
{Array.from({ length: 6 }).map((_, i) => (
<g key={i}>
<line x1={40 + i * 35} y1="20" x2={40 + i * 35} y2="170" stroke="#e5e7eb" strokeWidth="1" />
<line x1="40" y1={20 + i * 30} x2="210" y2={20 + i * 30} stroke="#e5e7eb" strokeWidth="1" />
</g>
))}
{/* Ejes */}
<line x1="40" y1="170" x2="210" y2="170" stroke="#374151" strokeWidth="2" />
<line x1="40" y1="20" x2="40" y2="170" stroke="#374151" strokeWidth="2" />
{/* Curva corto plazo (más vertical) */}
<line x1="70" y1="150" x2="100" y2="50" stroke="#f97316" strokeWidth="3" />
<text x="105" y="45" className="text-xs fill-orange-600 font-medium">CP</text>
{/* Curva largo plazo (más horizontal) */}
<line x1="70" y1="140" x2="180" y2="80" stroke="#3b82f6" strokeWidth="3" />
<text x="185" y="75" className="text-xs fill-blue-600 font-medium">LP</text>
{/* Labels */}
<text x="125" y="190" textAnchor="middle" className="text-xs fill-gray-600">Cantidad</text>
<text x="20" y="95" textAnchor="middle" transform="rotate(-90, 20, 95)" className="text-xs fill-gray-600">Precio</text>
</svg>
<div className="mt-3 space-y-2 text-sm">
<div className="flex items-center gap-2">
<div className="w-6 h-0.5 bg-orange-500"></div>
<span className="text-gray-600">Corto plazo: inelástica</span>
</div>
<div className="flex items-center gap-2">
<div className="w-6 h-0.5 bg-blue-500"></div>
<span className="text-gray-600">Largo plazo: elástica</span>
</div>
</div>
</div>
{/* Info boxes */}
<div className="p-4 bg-orange-50 rounded-lg border border-orange-200">
<div className="flex items-center gap-2 mb-2">
<AlertCircle className="w-5 h-5 text-orange-600" />
<h4 className="font-semibold text-orange-800">Corto Plazo</h4>
</div>
<ul className="text-sm text-orange-700 space-y-1">
<li> Algunos factores son fijos</li>
<li> Difícil cambiar capacidad</li>
<li> Oferta poco sensible a precios</li>
</ul>
</div>
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="w-5 h-5 text-blue-600" />
<h4 className="font-semibold text-blue-800">Largo Plazo</h4>
</div>
<ul className="text-sm text-blue-700 space-y-1">
<li> Todos los factores son variables</li>
<li> Pueden expandir capacidad</li>
<li> Oferta muy sensible a precios</li>
</ul>
</div>
</div>
</div>
{/* Indicadores de progreso */}
<div className="mt-6 flex justify-center items-center gap-1">
{escenarios.map((_, index) => (
<div
key={index}
className={`w-2 h-2 rounded-full ${
index === escenarioActual
? 'bg-green-600'
: index < escenarioActual
? 'bg-green-500'
: 'bg-gray-300'
}`}
/>
))}
</div>
</div>
);
};
export default OfertaCortoLargoPlazo;

View File

@@ -0,0 +1,412 @@
import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ArrowDown, AlertTriangle, Home, Scale, Info, CheckCircle2, XCircle } from 'lucide-react';
interface PrecioMaximoTechoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Escenario {
id: number;
nombre: string;
descripcion: string;
pe: number;
qe: number;
pmax: number;
contexto: string;
consecuencias: string[];
icono: React.ReactNode;
}
const escenarios: Escenario[] = [
{
id: 1,
nombre: "Control de Alquileres",
descripcion: "El gobierno establece un precio máximo de $600 para apartamentos cuando el equilibrio está en $800.",
pe: 800,
qe: 50,
pmax: 600,
contexto: "Mercado de vivienda en alquiler",
consecuencias: [
"Escasez de apartamentos disponibles",
"Listas de espera cada vez más largas",
"Deterioro de la calidad de las viviendas",
"Mercado negro de alquileres"
],
icono: <Home className="w-6 h-6" />
},
{
id: 2,
nombre: "Gasolina Subsidiada",
descripcion: "El precio de la gasolina se congela en $3/galón cuando el precio de mercado es $5/galón.",
pe: 5,
qe: 100,
pmax: 3,
contexto: "Mercado de combustibles",
consecuencias: [
"Largas filas en gasolineras",
"Desabastecimiento periódico",
"Contrabando de combustible",
"Inversión insuficiente en refinación"
],
icono: <AlertTriangle className="w-6 h-6" />
},
{
id: 3,
nombre: "Medicamentos Esenciales",
descripcion: "Precio máximo en medicamentos básicos: $20 cuando cuestan $35 producirlos.",
pe: 35,
qe: 80,
pmax: 20,
contexto: "Mercado farmacéutico",
consecuencias: [
"Desaparición de medicamentos del mercado",
"Reducción de la investigación",
"Mercado negro de medicinas",
"Importación irregular"
],
icono: <Scale className="w-6 h-6" />
}
];
export const PrecioMaximoTecho: React.FC<PrecioMaximoTechoProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [escenarioActual, setEscenarioActual] = useState(0);
const [respuestas, setRespuestas] = useState<Record<number, boolean>>({});
const [mostrarExplicacion, setMostrarExplicacion] = useState(false);
const [score, setScore] = useState(0);
const escenario = escenarios[escenarioActual];
// Cálculos para el gráfico
const calcularInterseccion = (precio: number) => {
const qd = Math.max(0, escenario.qe + (escenario.pe - precio) * 2);
const qo = Math.max(0, escenario.qe - (escenario.pe - precio) * 1.5);
return { qd, qo };
};
const datosGrafico = useMemo(() => {
const { qd, qo } = calcularInterseccion(escenario.pmax);
const excesoDemanda = Math.max(0, qd - qo);
return {
qd,
qo,
excesoDemanda,
cantidadTransada: Math.min(qd, qo)
};
}, [escenario]);
const verificarRespuesta = (hayEscasez: boolean) => {
const correcto = hayEscasez === true;
setRespuestas(prev => ({ ...prev, [escenario.id]: correcto }));
setMostrarExplicacion(true);
if (correcto) {
setScore(prev => prev + 33);
}
if (escenarioActual === escenarios.length - 1 && correcto) {
setTimeout(() => {
onComplete?.(Math.min(100, score + 33));
}, 2000);
}
};
const siguienteEscenario = () => {
if (escenarioActual < escenarios.length - 1) {
setEscenarioActual(prev => prev + 1);
setMostrarExplicacion(false);
}
};
// Configuración del gráfico SVG
const width = 400;
const height = 300;
const padding = 50;
const graphWidth = width - 2 * padding;
const graphHeight = height - 2 * padding;
const maxP = Math.max(escenario.pe, escenario.pmax) * 1.2;
const maxQ = escenario.qe * 1.5;
const scaleX = (q: number) => padding + (q / maxQ) * graphWidth;
const scaleY = (p: number) => padding + graphHeight - (p / maxP) * graphHeight;
// Generar puntos de curvas
const puntosDemanda = [];
const puntosOferta = [];
for (let q = 0; q <= maxQ; q += 2) {
const pd = escenario.pe + (escenario.pe / escenario.qe) * (escenario.qe - q);
const po = escenario.pe * 0.3 + (escenario.pe / escenario.qe) * 0.7 * q;
if (pd > 0 && pd <= maxP) puntosDemanda.push({ q, p: pd });
if (po > 0 && po <= maxP) puntosOferta.push({ q, p: po });
}
const pathDemanda = puntosDemanda.map((p, i) =>
`${i === 0 ? 'M' : 'L'} ${scaleX(p.q)} ${scaleY(p.p)}`
).join(' ');
const pathOferta = puntosOferta.map((p, i) =>
`${i === 0 ? 'M' : 'L'} ${scaleX(p.q)} ${scaleY(p.p)}`
).join(' ');
return (
<div className="w-full max-w-5xl mx-auto p-6 bg-white rounded-xl shadow-lg">
{/* Header */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-red-100 rounded-lg">
<ArrowDown className="w-6 h-6 text-red-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-800">Precio Máximo (Techo)</h2>
<p className="text-gray-600">Analiza los efectos de los controles de precios máximos</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-500">Ejercicio {escenarioActual + 1} de {escenarios.length}</span>
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-red-600"
initial={{ width: 0 }}
animate={{ width: `${score}%` }}
/>
</div>
</div>
</div>
</div>
{/* Contenido principal */}
<div className="grid md:grid-cols-2 gap-6">
{/* Panel izquierdo: Escenario y gráfico */}
<div className="space-y-4">
{/* Tarjeta del escenario */}
<motion.div
key={escenario.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="p-4 bg-gradient-to-br from-red-50 to-orange-50 rounded-lg border border-red-200"
>
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-white rounded-lg shadow-sm">
{escenario.icono}
</div>
<div>
<h3 className="font-bold text-gray-800">{escenario.nombre}</h3>
<span className="text-sm text-red-600">{escenario.contexto}</span>
</div>
</div>
<p className="text-gray-700">{escenario.descripcion}</p>
</motion.div>
{/* Gráfico interactivo */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="font-semibold text-gray-700 mb-3 flex items-center gap-2">
<Info className="w-4 h-4" />
Análisis Gráfico
</h4>
<svg width={width} height={height} className="w-full">
{/* Grid */}
{Array.from({ length: 6 }).map((_, i) => (
<g key={i}>
<line
x1={padding}
y1={padding + (i * graphHeight) / 5}
x2={padding + graphWidth}
y2={padding + (i * graphHeight) / 5}
stroke="#e5e7eb"
strokeWidth="1"
/>
<line
x1={padding + (i * graphWidth) / 5}
y1={padding}
x2={padding + (i * graphWidth) / 5}
y2={padding + graphHeight}
stroke="#e5e7eb"
strokeWidth="1"
/>
</g>
))}
{/* Ejes */}
<line x1={padding} y1={padding + graphHeight} x2={padding + graphWidth} y2={padding + graphHeight} stroke="#374151" strokeWidth="2" />
<line x1={padding} y1={padding} x2={padding} y2={padding + graphHeight} stroke="#374151" strokeWidth="2" />
{/* Etiquetas */}
<text x={padding + graphWidth / 2} y={height - 10} textAnchor="middle" className="text-xs fill-gray-600">Cantidad</text>
<text x={15} y={padding + graphHeight / 2} textAnchor="middle" transform={`rotate(-90, 15, ${padding + graphHeight / 2})`} className="text-xs fill-gray-600">Precio</text>
{/* Curva de demanda */}
<path d={pathDemanda} fill="none" stroke="#3b82f6" strokeWidth="3" />
<text x={padding + graphWidth - 30} y={padding + 20} className="text-sm fill-blue-600 font-bold">D</text>
{/* Curva de oferta */}
<path d={pathOferta} fill="none" stroke="#22c55e" strokeWidth="3" />
<text x={padding + graphWidth - 30} y={padding + graphHeight - 30} className="text-sm fill-green-600 font-bold">S</text>
{/* Punto de equilibrio */}
<circle cx={scaleX(escenario.qe)} cy={scaleY(escenario.pe)} r="6" fill="#8b5cf6" stroke="white" strokeWidth="2" />
<text x={scaleX(escenario.qe) + 10} y={scaleY(escenario.pe) - 10} className="text-xs fill-purple-600 font-bold">E</text>
{/* Línea de precio máximo */}
<line
x1={padding}
y1={scaleY(escenario.pmax)}
x2={padding + graphWidth}
y2={scaleY(escenario.pmax)}
stroke="#ef4444"
strokeWidth="2"
strokeDasharray="5,5"
/>
<text x={padding + graphWidth - 40} y={scaleY(escenario.pmax) - 5} className="text-xs fill-red-600 font-bold">Pmax</text>
{/* Puntos de intersección con Pmax */}
<circle cx={scaleX(datosGrafico.qd)} cy={scaleY(escenario.pmax)} r="5" fill="#3b82f6" stroke="white" strokeWidth="2" />
<circle cx={scaleX(datosGrafico.qo)} cy={scaleY(escenario.pmax)} r="5" fill="#22c55e" stroke="white" strokeWidth="2" />
{/* Zona de escasez */}
{datosGrafico.excesoDemanda > 0 && (
<g>
<rect
x={scaleX(datosGrafico.qo)}
y={scaleY(escenario.pmax) - 20}
width={scaleX(datosGrafico.qd) - scaleX(datosGrafico.qo)}
height="20"
fill="#fef3c7"
opacity="0.8"
/>
<text
x={scaleX((datosGrafico.qo + datosGrafico.qd) / 2)}
y={scaleY(escenario.pmax) - 25}
textAnchor="middle"
className="text-xs fill-amber-700 font-bold"
>
Escasez: {datosGrafico.excesoDemanda.toFixed(1)}
</text>
</g>
)}
</svg>
</div>
</div>
{/* Panel derecho: Pregunta y consecuencias */}
<div className="space-y-4">
{/* Pregunta */}
<AnimatePresence mode="wait">
{!mostrarExplicacion ? (
<motion.div
key="pregunta"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="p-6 bg-white border-2 border-gray-200 rounded-lg"
>
<h3 className="text-lg font-bold text-gray-800 mb-4">
¿Qué ocurrirá en este mercado con el precio máximo establecido?
</h3>
<div className="space-y-3">
<button
onClick={() => verificarRespuesta(true)}
className="w-full p-4 text-left bg-red-50 hover:bg-red-100 border border-red-200 rounded-lg transition-colors flex items-center gap-3"
>
<ArrowDown className="w-5 h-5 text-red-600" />
<div>
<span className="font-semibold text-red-800">Habrá escasez</span>
<p className="text-sm text-red-600">La demanda excederá a la oferta</p>
</div>
</button>
<button
onClick={() => verificarRespuesta(false)}
className="w-full p-4 text-left bg-green-50 hover:bg-green-100 border border-green-200 rounded-lg transition-colors flex items-center gap-3"
>
<Scale className="w-5 h-5 text-green-600" />
<div>
<span className="font-semibold text-green-800">El mercado se equilibrará</span>
<p className="text-sm text-green-600">La cantidad demandada igualará a la ofrecida</p>
</div>
</button>
</div>
</motion.div>
) : (
<motion.div
key="explicacion"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={`p-6 rounded-lg border-2 ${respuestas[escenario.id] ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}
>
<div className="flex items-center gap-3 mb-4">
{respuestas[escenario.id] ? (
<CheckCircle2 className="w-8 h-8 text-green-600" />
) : (
<XCircle className="w-8 h-8 text-red-600" />
)}
<h3 className={`text-lg font-bold ${respuestas[escenario.id] ? 'text-green-800' : 'text-red-800'}`}>
{respuestas[escenario.id] ? '¡Correcto!' : 'Incorrecto'}
</h3>
</div>
<p className="text-gray-700 mb-4">
Al fijar un precio máximo <strong>por debajo</strong> del precio de equilibrio (${escenario.pe}),
se crea una escasez porque:
</p>
<ul className="space-y-2 mb-6">
<li className="flex items-start gap-2 text-gray-700">
<span className="text-blue-600 font-bold"></span>
<span>Los productores reducen la cantidad ofrecida a {datosGrafico.qo.toFixed(1)} unidades</span>
</li>
<li className="flex items-start gap-2 text-gray-700">
<span className="text-green-600 font-bold"></span>
<span>Los consumidores aumentan la cantidad demandada a {datosGrafico.qd.toFixed(1)} unidades</span>
</li>
<li className="flex items-start gap-2 text-gray-700">
<span className="text-amber-600 font-bold">!</span>
<span>Resultado: Exceso de demanda de {datosGrafico.excesoDemanda.toFixed(1)} unidades</span>
</li>
</ul>
{escenarioActual < escenarios.length - 1 ? (
<button
onClick={siguienteEscenario}
className="w-full py-3 px-4 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
>
Siguiente Escenario
</button>
) : (
<div className="text-center p-4 bg-green-100 rounded-lg">
<p className="font-semibold text-green-800">¡Ejercicio completado!</p>
<p className="text-sm text-green-600">Has analizado todos los escenarios</p>
</div>
)}
</motion.div>
)}
</AnimatePresence>
{/* Consecuencias */}
<div className="p-4 bg-amber-50 rounded-lg border border-amber-200">
<h4 className="font-semibold text-amber-800 mb-3 flex items-center gap-2">
<AlertTriangle className="w-5 h-5" />
Consecuencias Típicas
</h4>
<ul className="space-y-2">
{escenario.consecuencias.map((consecuencia, idx) => (
<motion.li
key={idx}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.1 }}
className="flex items-start gap-2 text-sm text-amber-900"
>
<span className="text-amber-600 font-bold mt-0.5"></span>
{consecuencia}
</motion.li>
))}
</ul>
</div>
</div>
</div>
</div>
);
};
export default PrecioMaximoTecho;

View File

@@ -0,0 +1,432 @@
import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ArrowUp, AlertTriangle, Briefcase, Wheat, Info, CheckCircle2, XCircle } from 'lucide-react';
interface PrecioMinimoPisoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Escenario {
id: number;
nombre: string;
descripcion: string;
pe: number;
qe: number;
pmin: number;
contexto: string;
consecuencias: string[];
icono: React.ReactNode;
}
const escenarios: Escenario[] = [
{
id: 1,
nombre: "Salario Mínimo",
descripcion: "El salario mínimo se fija en $15/hora cuando el equilibrio del mercado laboral está en $10/hora.",
pe: 10,
qe: 1000,
pmin: 15,
contexto: "Mercado laboral",
consecuencias: [
"Reducción de la demanda de trabajadores",
"Aumento del desempleo",
"Beneficio para trabajadores que conservan empleo",
"Posible mercado laboral informal"
],
icono: <Briefcase className="w-6 h-6" />
},
{
id: 2,
nombre: "Precio de Soporte Agrícola",
descripcion: "El gobierno garantiza $5/bushel de trigo cuando el precio de mercado es $3/bushel.",
pe: 3,
qe: 500,
pmin: 5,
contexto: "Mercado agrícola",
consecuencias: [
"Superávit de producción agrícola",
"El gobierno debe comprar el exceso",
"Costos fiscales significativos",
"Posible despilfarro de recursos"
],
icono: <Wheat className="w-6 h-6" />
},
{
id: 3,
nombre: "Tarifa Mínima de Taxis",
descripcion: "La tarifa mínima se establece en $25 cuando el precio de equilibrio es $15 por viaje.",
pe: 15,
qe: 200,
pmin: 25,
contexto: "Mercado de transporte",
consecuencias: [
"Menor demanda de servicios de taxi",
"Exceso de oferta (taxis vacíos)",
"Aparición de competencia informal",
"Beneficio para conductores con clientes"
],
icono: <AlertTriangle className="w-6 h-6" />
}
];
export const PrecioMinimoPiso: React.FC<PrecioMinimoPisoProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [escenarioActual, setEscenarioActual] = useState(0);
const [respuestas, setRespuestas] = useState<Record<number, boolean>>({});
const [mostrarExplicacion, setMostrarExplicacion] = useState(false);
const [score, setScore] = useState(0);
const escenario = escenarios[escenarioActual];
// Cálculos para el gráfico
const calcularInterseccion = (precio: number) => {
const qd = Math.max(0, escenario.qe - (precio - escenario.pe) * 1.5);
const qo = Math.max(0, escenario.qe + (precio - escenario.pe) * 2);
return { qd, qo };
};
const datosGrafico = useMemo(() => {
const { qd, qo } = calcularInterseccion(escenario.pmin);
const excesoOferta = Math.max(0, qo - qd);
return {
qd,
qo,
excesoOferta,
cantidadTransada: Math.min(qd, qo)
};
}, [escenario]);
const verificarRespuesta = (haySuperavit: boolean) => {
const correcto = haySuperavit === true;
setRespuestas(prev => ({ ...prev, [escenario.id]: correcto }));
setMostrarExplicacion(true);
if (correcto) {
setScore(prev => prev + 33);
}
if (escenarioActual === escenarios.length - 1 && correcto) {
setTimeout(() => {
onComplete?.(Math.min(100, score + 33));
}, 2000);
}
};
const siguienteEscenario = () => {
if (escenarioActual < escenarios.length - 1) {
setEscenarioActual(prev => prev + 1);
setMostrarExplicacion(false);
}
};
// Configuración del gráfico SVG
const width = 400;
const height = 300;
const padding = 50;
const graphWidth = width - 2 * padding;
const graphHeight = height - 2 * padding;
const maxP = escenario.pmin * 1.2;
const maxQ = Math.max(datosGrafico.qo, escenario.qe) * 1.3;
const scaleX = (q: number) => padding + (q / maxQ) * graphWidth;
const scaleY = (p: number) => padding + graphHeight - (p / maxP) * graphHeight;
// Generar puntos de curvas
const puntosDemanda = [];
const puntosOferta = [];
for (let q = 0; q <= maxQ; q += 5) {
const pd = escenario.pe + (escenario.pe / escenario.qe) * (escenario.qe - q);
const po = escenario.pe * 0.5 + (escenario.pe / escenario.qe) * q;
if (pd > 0 && pd <= maxP) puntosDemanda.push({ q, p: pd });
if (po > 0 && po <= maxP) puntosOferta.push({ q, p: po });
}
const pathDemanda = puntosDemanda.map((p, i) =>
`${i === 0 ? 'M' : 'L'} ${scaleX(p.q)} ${scaleY(p.p)}`
).join(' ');
const pathOferta = puntosOferta.map((p, i) =>
`${i === 0 ? 'M' : 'L'} ${scaleX(p.q)} ${scaleY(p.p)}`
).join(' ');
return (
<div className="w-full max-w-5xl mx-auto p-6 bg-white rounded-xl shadow-lg">
{/* Header */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-amber-100 rounded-lg">
<ArrowUp className="w-6 h-6 text-amber-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-800">Precio Mínimo (Piso)</h2>
<p className="text-gray-600">Analiza los efectos de los precios mínimos o precios de soporte</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-500">Ejercicio {escenarioActual + 1} de {escenarios.length}</span>
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
<motion.div
className="h-full bg-amber-600"
initial={{ width: 0 }}
animate={{ width: `${score}%` }}
/>
</div>
</div>
</div>
</div>
{/* Contenido principal */}
<div className="grid md:grid-cols-2 gap-6">
{/* Panel izquierdo: Escenario y gráfico */}
<div className="space-y-4">
{/* Tarjeta del escenario */}
<motion.div
key={escenario.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="p-4 bg-gradient-to-br from-amber-50 to-yellow-50 rounded-lg border border-amber-200"
>
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-white rounded-lg shadow-sm">
{escenario.icono}
</div>
<div>
<h3 className="font-bold text-gray-800">{escenario.nombre}</h3>
<span className="text-sm text-amber-600">{escenario.contexto}</span>
</div>
</div>
<p className="text-gray-700">{escenario.descripcion}</p>
</motion.div>
{/* Gráfico interactivo */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="font-semibold text-gray-700 mb-3 flex items-center gap-2">
<Info className="w-4 h-4" />
Análisis Gráfico
</h4>
<svg width={width} height={height} className="w-full">
{/* Grid */}
{Array.from({ length: 6 }).map((_, i) => (
<g key={i}>
<line
x1={padding}
y1={padding + (i * graphHeight) / 5}
x2={padding + graphWidth}
y2={padding + (i * graphHeight) / 5}
stroke="#e5e7eb"
strokeWidth="1"
/>
<line
x1={padding + (i * graphWidth) / 5}
y1={padding}
x2={padding + (i * graphWidth) / 5}
y2={padding + graphHeight}
stroke="#e5e7eb"
strokeWidth="1"
/>
</g>
))}
{/* Ejes */}
<line x1={padding} y1={padding + graphHeight} x2={padding + graphWidth} y2={padding + graphHeight} stroke="#374151" strokeWidth="2" />
<line x1={padding} y1={padding} x2={padding} y2={padding + graphHeight} stroke="#374151" strokeWidth="2" />
{/* Etiquetas */}
<text x={padding + graphWidth / 2} y={height - 10} textAnchor="middle" className="text-xs fill-gray-600">Cantidad</text>
<text x={15} y={padding + graphHeight / 2} textAnchor="middle" transform={`rotate(-90, 15, ${padding + graphHeight / 2})`} className="text-xs fill-gray-600">Precio</text>
{/* Curva de demanda */}
<path d={pathDemanda} fill="none" stroke="#3b82f6" strokeWidth="3" />
<text x={padding + graphWidth - 30} y={padding + 20} className="text-sm fill-blue-600 font-bold">D</text>
{/* Curva de oferta */}
<path d={pathOferta} fill="none" stroke="#22c55e" strokeWidth="3" />
<text x={padding + graphWidth - 30} y={padding + graphHeight - 30} className="text-sm fill-green-600 font-bold">S</text>
{/* Punto de equilibrio */}
<circle cx={scaleX(escenario.qe)} cy={scaleY(escenario.pe)} r="6" fill="#8b5cf6" stroke="white" strokeWidth="2" />
<text x={scaleX(escenario.qe) + 10} y={scaleY(escenario.pe) + 15} className="text-xs fill-purple-600 font-bold">E</text>
{/* Línea de precio mínimo */}
<line
x1={padding}
y1={scaleY(escenario.pmin)}
x2={padding + graphWidth}
y2={scaleY(escenario.pmin)}
stroke="#f59e0b"
strokeWidth="2"
strokeDasharray="5,5"
/>
<text x={padding + graphWidth - 40} y={scaleY(escenario.pmin) - 5} className="text-xs fill-amber-600 font-bold">Pmin</text>
{/* Puntos de intersección con Pmin */}
<circle cx={scaleX(datosGrafico.qd)} cy={scaleY(escenario.pmin)} r="5" fill="#3b82f6" stroke="white" strokeWidth="2" />
<circle cx={scaleX(datosGrafico.qo)} cy={scaleY(escenario.pmin)} r="5" fill="#22c55e" stroke="white" strokeWidth="2" />
{/* Zona de superávit */}
{datosGrafico.excesoOferta > 0 && (
<g>
<rect
x={scaleX(datosGrafico.qd)}
y={scaleY(escenario.pmin)}
width={scaleX(datosGrafico.qo) - scaleX(datosGrafico.qd)}
height="20"
fill="#fef3c7"
opacity="0.8"
/>
<text
x={scaleX((datosGrafico.qd + datosGrafico.qo) / 2)}
y={scaleY(escenario.pmin) + 35}
textAnchor="middle"
className="text-xs fill-amber-700 font-bold"
>
Superávit: {datosGrafico.excesoOferta.toFixed(0)}
</text>
</g>
)}
{/* Flechas indicadoras */}
{datosGrafico.excesoOferta > 0 && (
<g>
<line
x1={scaleX(datosGrafico.qd)}
y1={scaleY(escenario.pmin) + 30}
x2={scaleX(datosGrafico.qo)}
y2={scaleY(escenario.pmin) + 30}
stroke="#f59e0b"
strokeWidth="2"
markerEnd="url(#arrowhead)"
/>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#f59e0b" />
</marker>
</defs>
</g>
)}
</svg>
</div>
</div>
{/* Panel derecho: Pregunta y consecuencias */}
<div className="space-y-4">
{/* Pregunta */}
<AnimatePresence mode="wait">
{!mostrarExplicacion ? (
<motion.div
key="pregunta"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="p-6 bg-white border-2 border-gray-200 rounded-lg"
>
<h3 className="text-lg font-bold text-gray-800 mb-4">
¿Qué ocurrirá en este mercado con el precio mínimo establecido?
</h3>
<div className="space-y-3">
<button
onClick={() => verificarRespuesta(true)}
className="w-full p-4 text-left bg-amber-50 hover:bg-amber-100 border border-amber-200 rounded-lg transition-colors flex items-center gap-3"
>
<ArrowUp className="w-5 h-5 text-amber-600" />
<div>
<span className="font-semibold text-amber-800">Habrá superávit</span>
<p className="text-sm text-amber-600">La oferta excederá a la demanda</p>
</div>
</button>
<button
onClick={() => verificarRespuesta(false)}
className="w-full p-4 text-left bg-green-50 hover:bg-green-100 border border-green-200 rounded-lg transition-colors flex items-center gap-3"
>
<AlertTriangle className="w-5 h-5 text-green-600" />
<div>
<span className="font-semibold text-green-800">El mercado se equilibrará</span>
<p className="text-sm text-green-600">La cantidad demandada igualará a la ofrecida</p>
</div>
</button>
</div>
</motion.div>
) : (
<motion.div
key="explicacion"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={`p-6 rounded-lg border-2 ${respuestas[escenario.id] ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}
>
<div className="flex items-center gap-3 mb-4">
{respuestas[escenario.id] ? (
<CheckCircle2 className="w-8 h-8 text-green-600" />
) : (
<XCircle className="w-8 h-8 text-red-600" />
)}
<h3 className={`text-lg font-bold ${respuestas[escenario.id] ? 'text-green-800' : 'text-red-800'}`}>
{respuestas[escenario.id] ? '¡Correcto!' : 'Incorrecto'}
</h3>
</div>
<p className="text-gray-700 mb-4">
Al fijar un precio mínimo <strong>por encima</strong> del precio de equilibrio (${escenario.pe}),
se crea un superávit porque:
</p>
<ul className="space-y-2 mb-6">
<li className="flex items-start gap-2 text-gray-700">
<span className="text-green-600 font-bold"></span>
<span>Los productores aumentan la cantidad ofrecida a {datosGrafico.qo.toFixed(0)} unidades</span>
</li>
<li className="flex items-start gap-2 text-gray-700">
<span className="text-blue-600 font-bold"></span>
<span>Los consumidores reducen la cantidad demandada a {datosGrafico.qd.toFixed(0)} unidades</span>
</li>
<li className="flex items-start gap-2 text-gray-700">
<span className="text-amber-600 font-bold">!</span>
<span>Resultado: Exceso de oferta de {datosGrafico.excesoOferta.toFixed(0)} unidades</span>
</li>
</ul>
{escenarioActual < escenarios.length - 1 ? (
<button
onClick={siguienteEscenario}
className="w-full py-3 px-4 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
>
Siguiente Escenario
</button>
) : (
<div className="text-center p-4 bg-green-100 rounded-lg">
<p className="font-semibold text-green-800">¡Ejercicio completado!</p>
<p className="text-sm text-green-600">Has analizado todos los escenarios</p>
</div>
)}
</motion.div>
)}
</AnimatePresence>
{/* Consecuencias */}
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<h4 className="font-semibold text-blue-800 mb-3 flex items-center gap-2">
<AlertTriangle className="w-5 h-5" />
Consecuencias Típicas
</h4>
<ul className="space-y-2">
{escenario.consecuencias.map((consecuencia, idx) => (
<motion.li
key={idx}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.1 }}
className="flex items-start gap-2 text-sm text-blue-900"
>
<span className="text-blue-600 font-bold mt-0.5"></span>
{consecuencia}
</motion.li>
))}
</ul>
</div>
</div>
</div>
</div>
);
};
export default PrecioMinimoPiso;

View File

@@ -0,0 +1,600 @@
import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Play, RotateCcw, TrendingDown, TrendingUp, Scale, AlertTriangle, Calculator, BarChart3 } from 'lucide-react';
interface SimuladorControlesProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface ResultadoSimulacion {
tipo: 'equilibrio' | 'precio-maximo' | 'precio-minimo';
precio: number;
cantidad: number;
excesoDemanda: number;
excesoOferta: number;
pesoMuerto: number;
mensaje: string;
}
interface EscenarioPredefinido {
id: string;
nombre: string;
descripcion: string;
tipo: 'maximo' | 'minimo' | 'libre';
precio: number;
pe: number;
qe: number;
}
const escenariosPredefinidos: EscenarioPredefinido[] = [
{
id: 'libre',
nombre: "Mercado Libre",
descripcion: "Sin intervención gubernamental",
tipo: 'libre',
precio: 0,
pe: 50,
qe: 100
},
{
id: 'rent-control',
nombre: "Control de Alquileres",
descripcion: "Precio máximo de $35 (equilibrio: $50)",
tipo: 'maximo',
precio: 35,
pe: 50,
qe: 100
},
{
id: 'salario-minimo',
nombre: "Salario Mínimo",
descripcion: "Precio mínimo de $65 (equilibrio: $50)",
tipo: 'minimo',
precio: 65,
pe: 50,
qe: 100
}
];
export const SimuladorControles: React.FC<SimuladorControlesProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [escenarioActivo, setEscenarioActivo] = useState<string | null>(null);
const [precioControl, setPrecioControl] = useState(50);
const [tipoControl, setTipoControl] = useState<'maximo' | 'minimo' | null>(null);
const [historial, setHistorial] = useState<ResultadoSimulacion[]>([]);
const [score, setScore] = useState(0);
const pe = 50; // Precio de equilibrio
const qe = 100; // Cantidad de equilibrio
// Funciones de demanda y oferta lineales
const calcularCantidades = (p: number) => {
// Demanda: Qd = 150 - 1*P (pendiente negativa)
const qd = Math.max(0, 150 - p);
// Oferta: Qo = 0 + 2*P (pendiente positiva)
const qo = Math.max(0, 2 * p);
return { qd, qo };
};
const resultado = useMemo((): ResultadoSimulacion => {
if (!tipoControl) {
return {
tipo: 'equilibrio',
precio: pe,
cantidad: qe,
excesoDemanda: 0,
excesoOferta: 0,
pesoMuerto: 0,
mensaje: 'Mercado en equilibrio libre'
};
}
const { qd, qo } = calcularCantidades(precioControl);
const cantidadTransada = Math.min(qd, qo);
const excesoDemanda = Math.max(0, qd - qo);
const excesoOferta = Math.max(0, qo - qd);
// Calcular pérdida de peso muerto (triángulo)
const base = qe - cantidadTransada;
const altura = tipoControl === 'maximo'
? pe - precioControl
: precioControl - pe;
const pesoMuerto = 0.5 * base * altura;
if (tipoControl === 'maximo' && precioControl < pe) {
return {
tipo: 'precio-maximo',
precio: precioControl,
cantidad: cantidadTransada,
excesoDemanda,
excesoOferta: 0,
pesoMuerto: Math.max(0, pesoMuerto),
mensaje: `Precio máximo crea escasez de ${excesoDemanda.toFixed(1)} unidades`
};
}
if (tipoControl === 'minimo' && precioControl > pe) {
return {
tipo: 'precio-minimo',
precio: precioControl,
cantidad: cantidadTransada,
excesoDemanda: 0,
excesoOferta,
pesoMuerto: Math.max(0, pesoMuerto),
mensaje: `Precio mínimo crea superávit de ${excesoOferta.toFixed(1)} unidades`
};
}
return {
tipo: 'equilibrio',
precio: precioControl,
cantidad: Math.min(qd, qo),
excesoDemanda: 0,
excesoOferta: 0,
pesoMuerto: 0,
mensaje: tipoControl === 'maximo'
? 'El precio máximo no es restrictivo (está por encima del equilibrio)'
: 'El precio mínimo no es restrictivo (está por debajo del equilibrio)'
};
}, [precioControl, tipoControl]);
const aplicarEscenario = (escenario: EscenarioPredefinido) => {
setEscenarioActivo(escenario.id);
if (escenario.tipo === 'libre') {
setTipoControl(null);
setPrecioControl(escenario.pe);
} else {
setTipoControl(escenario.tipo);
setPrecioControl(escenario.precio);
}
// Agregar al historial
const nuevoResultado: ResultadoSimulacion = {
tipo: escenario.tipo === 'libre' ? 'equilibrio' : escenario.tipo === 'maximo' ? 'precio-maximo' : 'precio-minimo',
precio: escenario.tipo === 'libre' ? escenario.pe : escenario.precio,
cantidad: escenario.qe,
excesoDemanda: escenario.tipo === 'maximo' ? 30 : 0,
excesoOferta: escenario.tipo === 'minimo' ? 30 : 0,
pesoMuerto: escenario.tipo === 'libre' ? 0 : 225,
mensaje: escenario.nombre
};
setHistorial(prev => [...prev.slice(-2), nuevoResultado]);
if (score < 100) {
setScore(prev => Math.min(100, prev + 25));
}
if (historial.length >= 2) {
setTimeout(() => {
onComplete?.(100);
}, 2000);
}
};
const reset = () => {
setEscenarioActivo(null);
setTipoControl(null);
setPrecioControl(50);
setHistorial([]);
setScore(0);
};
// Configuración del gráfico SVG
const width = 450;
const height = 350;
const padding = 50;
const graphWidth = width - 2 * padding;
const graphHeight = height - 2 * padding;
const maxP = 80;
const maxQ = 150;
const scaleX = (q: number) => padding + (q / maxQ) * graphWidth;
const scaleY = (p: number) => padding + graphHeight - (p / maxP) * graphHeight;
// Puntos para curvas
const puntosDemanda = [];
const puntosOferta = [];
for (let p = 0; p <= maxP; p += 2) {
const { qd, qo } = calcularCantidades(p);
puntosDemanda.push({ p, q: qd });
puntosOferta.push({ p, q: qo });
}
const pathDemanda = puntosDemanda
.filter(p => p.q >= 0 && p.q <= maxQ)
.map((p, i) => `${i === 0 ? 'M' : 'L'} ${scaleX(p.q)} ${scaleY(p.p)}`)
.join(' ');
const pathOferta = puntosOferta
.filter(p => p.q >= 0 && p.q <= maxQ)
.map((p, i) => `${i === 0 ? 'M' : 'L'} ${scaleX(p.q)} ${scaleY(p.p)}`)
.join(' ');
return (
<div className="w-full max-w-6xl mx-auto p-6 bg-white rounded-xl shadow-lg">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-100 rounded-lg">
<Calculator className="w-6 h-6 text-purple-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-800">Simulador de Controles</h2>
<p className="text-gray-600">Experimenta con diferentes controles de precio y observa las consecuencias</p>
</div>
</div>
<div className="flex items-center gap-4">
<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: `${score}%` }}
/>
</div>
<button
onClick={reset}
className="p-2 text-gray-500 hover:text-purple-600 transition-colors"
>
<RotateCcw className="w-5 h-5" />
</button>
</div>
</div>
<div className="grid lg:grid-cols-3 gap-6">
{/* Panel izquierdo: Escenarios */}
<div className="space-y-4">
<h3 className="font-semibold text-gray-700 flex items-center gap-2">
<Play className="w-4 h-4" />
Escenarios Predefinidos
</h3>
{escenariosPredefinidos.map((escenario) => (
<button
key={escenario.id}
onClick={() => aplicarEscenario(escenario)}
className={`w-full p-4 text-left rounded-lg border-2 transition-all ${
escenarioActivo === escenario.id
? 'bg-purple-50 border-purple-400 shadow-md'
: 'bg-white border-gray-200 hover:border-purple-300'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-gray-800">{escenario.nombre}</span>
{escenarioActivo === escenario.id && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="w-3 h-3 bg-purple-500 rounded-full"
/>
)}
</div>
<p className="text-sm text-gray-600">{escenario.descripcion}</p>
<div className="mt-2 flex items-center gap-2">
<span className={`text-xs px-2 py-1 rounded ${
escenario.tipo === 'maximo' ? 'bg-red-100 text-red-700' :
escenario.tipo === 'minimo' ? 'bg-amber-100 text-amber-700' :
'bg-green-100 text-green-700'
}`}>
{escenario.tipo === 'maximo' ? 'Precio Máximo' :
escenario.tipo === 'minimo' ? 'Precio Mínimo' :
'Libre'}
</span>
</div>
</button>
))}
{/* Control manual */}
<div className="p-4 bg-gray-50 rounded-lg border border-gray-200">
<h4 className="font-semibold text-gray-700 mb-3">Control Manual</h4>
<div className="space-y-3">
<div className="flex gap-2">
<button
onClick={() => setTipoControl('maximo')}
className={`flex-1 py-2 px-3 rounded text-sm font-medium transition-colors ${
tipoControl === 'maximo'
? 'bg-red-600 text-white'
: 'bg-white border border-gray-300 text-gray-700 hover:bg-red-50'
}`}
>
Precio Máx
</button>
<button
onClick={() => setTipoControl('minimo')}
className={`flex-1 py-2 px-3 rounded text-sm font-medium transition-colors ${
tipoControl === 'minimo'
? 'bg-amber-600 text-white'
: 'bg-white border border-gray-300 text-gray-700 hover:bg-amber-50'
}`}
>
Precio Mín
</button>
<button
onClick={() => setTipoControl(null)}
className={`flex-1 py-2 px-3 rounded text-sm font-medium transition-colors ${
tipoControl === null
? 'bg-green-600 text-white'
: 'bg-white border border-gray-300 text-gray-700 hover:bg-green-50'
}`}
>
Libre
</button>
</div>
{tipoControl && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Precio: ${precioControl}
</label>
<input
type="range"
min="10"
max="75"
value={precioControl}
onChange={(e) => setPrecioControl(Number(e.target.value))}
className={`w-full ${
tipoControl === 'maximo' ? 'accent-red-500' : 'accent-amber-500'
}`}
/>
<div className="flex justify-between text-xs text-gray-500">
<span>$10</span>
<span className="text-gray-400">Equilibrio: $50</span>
<span>$75</span>
</div>
</div>
)}
</div>
</div>
</div>
{/* Panel central: Gráfico */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-700 mb-3 flex items-center gap-2">
<BarChart3 className="w-4 h-4" />
Gráfico de Mercado
</h3>
<svg width={width} height={height} className="w-full">
{/* Grid */}
{Array.from({ length: 6 }).map((_, i) => (
<g key={i}>
<line
x1={padding}
y1={padding + (i * graphHeight) / 5}
x2={padding + graphWidth}
y2={padding + (i * graphHeight) / 5}
stroke="#e5e7eb"
strokeWidth="1"
/>
<line
x1={padding + (i * graphWidth) / 5}
y1={padding}
x2={padding + (i * graphWidth) / 5}
y2={padding + graphHeight}
stroke="#e5e7eb"
strokeWidth="1"
/>
</g>
))}
{/* Ejes */}
<line x1={padding} y1={padding + graphHeight} x2={padding + graphWidth} y2={padding + graphHeight} stroke="#374151" strokeWidth="2" />
<line x1={padding} y1={padding} x2={padding} y2={padding + graphHeight} stroke="#374151" strokeWidth="2" />
{/* Etiquetas */}
<text x={padding + graphWidth / 2} y={height - 10} textAnchor="middle" className="text-xs fill-gray-600">Cantidad</text>
<text x={15} y={padding + graphHeight / 2} textAnchor="middle" transform={`rotate(-90, 15, ${padding + graphHeight / 2})`} className="text-xs fill-gray-600">Precio ($)</text>
{/* Curvas */}
<path d={pathDemanda} fill="none" stroke="#3b82f6" strokeWidth="3" />
<text x={padding + 10} y={padding + 15} className="text-sm fill-blue-600 font-bold">D</text>
<path d={pathOferta} fill="none" stroke="#22c55e" strokeWidth="3" />
<text x={padding + graphWidth - 30} y={padding + graphHeight - 10} className="text-sm fill-green-600 font-bold">S</text>
{/* Punto de equilibrio */}
<circle cx={scaleX(qe)} cy={scaleY(pe)} r="6" fill="#8b5cf6" stroke="white" strokeWidth="2" />
<text x={scaleX(qe) + 10} y={scaleY(pe) - 10} className="text-xs fill-purple-600 font-bold">E ($50)</text>
{/* Línea de control */}
<AnimatePresence>
{tipoControl && (
<motion.g
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<line
x1={padding}
y1={scaleY(precioControl)}
x2={padding + graphWidth}
y2={scaleY(precioControl)}
stroke={tipoControl === 'maximo' ? '#ef4444' : '#f59e0b'}
strokeWidth="2"
strokeDasharray="5,5"
/>
<text
x={padding + 5}
y={scaleY(precioControl) - 5}
className={`text-xs font-bold ${tipoControl === 'maximo' ? 'fill-red-600' : 'fill-amber-600'}`}
>
{tipoControl === 'maximo' ? 'Pmax' : 'Pmin'}: ${precioControl}
</text>
</motion.g>
)}
</AnimatePresence>
{/* Indicadores de desequilibrio */}
<AnimatePresence>
{resultado.excesoDemanda > 0 && (
<motion.g
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<rect
x={scaleX(resultado.cantidad)}
y={scaleY(precioControl) - 25}
width={scaleX(resultado.excesoDemanda) - padding}
height="20"
fill="#fef3c7"
opacity="0.8"
/>
<text
x={scaleX(resultado.cantidad + resultado.excesoDemanda / 2)}
y={scaleY(precioControl) - 30}
textAnchor="middle"
className="text-xs fill-red-700 font-bold"
>
Escasez
</text>
</motion.g>
)}
</AnimatePresence>
<AnimatePresence>
{resultado.excesoOferta > 0 && (
<motion.g
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<rect
x={scaleX(resultado.cantidad)}
y={scaleY(precioControl)}
width={scaleX(resultado.excesoOferta) - padding}
height="20"
fill="#dbeafe"
opacity="0.8"
/>
<text
x={scaleX(resultado.cantidad + resultado.excesoOferta / 2)}
y={scaleY(precioControl) + 35}
textAnchor="middle"
className="text-xs fill-amber-700 font-bold"
>
Superávit
</text>
</motion.g>
)}
</AnimatePresence>
</svg>
</div>
{/* Panel derecho: Resultados */}
<div className="space-y-4">
{/* Resultado actual */}
<AnimatePresence mode="wait">
<motion.div
key={resultado.tipo}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`p-4 rounded-lg border-2 ${
resultado.tipo === 'equilibrio' ? 'bg-green-50 border-green-200' :
resultado.tipo === 'precio-maximo' ? 'bg-red-50 border-red-200' :
'bg-amber-50 border-amber-200'
}`}
>
<div className="flex items-center gap-2 mb-3">
{resultado.tipo === 'equilibrio' ? (
<Scale className="w-5 h-5 text-green-600" />
) : resultado.tipo === 'precio-maximo' ? (
<TrendingDown className="w-5 h-5 text-red-600" />
) : (
<TrendingUp className="w-5 h-5 text-amber-600" />
)}
<span className={`font-bold ${
resultado.tipo === 'equilibrio' ? 'text-green-800' :
resultado.tipo === 'precio-maximo' ? 'text-red-800' :
'text-amber-800'
}`}>
{resultado.mensaje}
</span>
</div>
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="bg-white rounded p-2">
<span className="text-gray-500">Precio:</span>
<p className="font-bold text-gray-800">${resultado.precio.toFixed(0)}</p>
</div>
<div className="bg-white rounded p-2">
<span className="text-gray-500">Cantidad:</span>
<p className="font-bold text-gray-800">{resultado.cantidad.toFixed(0)} un</p>
</div>
{resultado.excesoDemanda > 0 && (
<div className="bg-red-100 rounded p-2 col-span-2">
<span className="text-red-600">Exceso de demanda:</span>
<p className="font-bold text-red-800">{resultado.excesoDemanda.toFixed(1)} unidades</p>
</div>
)}
{resultado.excesoOferta > 0 && (
<div className="bg-amber-100 rounded p-2 col-span-2">
<span className="text-amber-600">Exceso de oferta:</span>
<p className="font-bold text-amber-800">{resultado.excesoOferta.toFixed(1)} unidades</p>
</div>
)}
{resultado.pesoMuerto > 0 && (
<div className="bg-gray-100 rounded p-2 col-span-2">
<span className="text-gray-600 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" />
Pérdida de peso muerto:
</span>
<p className="font-bold text-gray-800">${resultado.pesoMuerto.toFixed(0)}</p>
</div>
)}
</div>
</motion.div>
</AnimatePresence>
{/* Historial */}
<div className="p-4 bg-gray-50 rounded-lg">
<h4 className="font-semibold text-gray-700 mb-3">Historial</h4>
<div className="space-y-2 max-h-48 overflow-y-auto">
{historial.length === 0 ? (
<p className="text-sm text-gray-500 italic">Selecciona un escenario para comenzar</p>
) : (
historial.map((h, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className={`p-2 rounded text-sm ${
h.tipo === 'equilibrio' ? 'bg-green-100 text-green-800' :
h.tipo === 'precio-maximo' ? 'bg-red-100 text-red-800' :
'bg-amber-100 text-amber-800'
}`}
>
<div className="font-medium">{h.mensaje}</div>
<div className="text-xs opacity-75">
P: ${h.precio} | Q: {h.cantidad}
{h.excesoDemanda > 0 && ` | Esc: ${h.excesoDemanda.toFixed(0)}`}
{h.excesoOferta > 0 && ` | Sup: ${h.excesoOferta.toFixed(0)}`}
</div>
</motion.div>
))
)}
</div>
</div>
{/* Instrucciones */}
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<h4 className="font-semibold text-blue-800 mb-2">💡 Consejos</h4>
<ul className="text-sm text-blue-700 space-y-1">
<li> Prueba los 3 escenarios predefinidos</li>
<li> Observa cómo cambian las cantidades</li>
<li> Identifica escasez vs superávit</li>
<li> La pérdida de peso muerto es ineficiencia</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default SimuladorControles;

View File

@@ -0,0 +1,262 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Table, Check, X, Trophy, RotateCcw, Calculator } from 'lucide-react';
interface TablaDemandaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FilaDemanda {
precio: number;
cantidadCorrecta: number;
cantidadUsuario: string;
}
const datosDemanda: FilaDemanda[] = [
{ precio: 10, cantidadCorrecta: 100, cantidadUsuario: '' },
{ precio: 20, cantidadCorrecta: 80, cantidadUsuario: '' },
{ precio: 30, cantidadCorrecta: 60, cantidadUsuario: '' },
{ precio: 40, cantidadCorrecta: 40, cantidadUsuario: '' },
{ precio: 50, cantidadCorrecta: 20, cantidadUsuario: '' },
];
export const TablaDemanda: React.FC<TablaDemandaProps> = ({
ejercicioId: _ejercicioId,
onComplete
}) => {
const [filas, setFilas] = useState<FilaDemanda[]>(datosDemanda);
const [mostrarResultados, setMostrarResultados] = useState(false);
const [score, setScore] = useState(0);
const [intentos, setIntentos] = useState(0);
const [completado, setCompletado] = useState(false);
const handleCantidadChange = (index: number, valor: string) => {
if (mostrarResultados) return;
const nuevasFilas = [...filas];
nuevasFilas[index].cantidadUsuario = valor;
setFilas(nuevasFilas);
};
const validarRespuestas = () => {
setIntentos(prev => prev + 1);
// Verificar que todos los campos estén llenos
const camposVacios = filas.some(fila => fila.cantidadUsuario === '');
if (camposVacios) {
alert('Por favor completa todas las cantidades antes de validar');
return;
}
setMostrarResultados(true);
// Calcular puntuación
let correctas = 0;
filas.forEach(fila => {
if (parseInt(fila.cantidadUsuario) === fila.cantidadCorrecta) {
correctas++;
}
});
let puntuacion = Math.round((correctas / filas.length) * 100);
// Penalización por intentos
if (intentos >= 1) puntuacion -= 10;
if (intentos >= 2) puntuacion -= 10;
puntuacion = Math.max(puntuacion, 20);
setScore(puntuacion);
if (correctas === filas.length) {
setCompletado(true);
setTimeout(() => {
if (onComplete) {
onComplete(puntuacion);
}
}, 2000);
}
};
const reiniciar = () => {
setFilas(datosDemanda.map(f => ({ ...f, cantidadUsuario: '' })));
setMostrarResultados(false);
setScore(0);
setCompletado(false);
};
const handleFinalizar = () => {
if (onComplete) {
onComplete(score);
}
};
const tablaCompletada = filas.every(fila => fila.cantidadUsuario !== '');
return (
<div className="w-full max-w-3xl 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">
<Table className="w-8 h-8 text-blue-600" />
<h2 className="text-2xl font-bold text-gray-800">Tabla de Demanda</h2>
</div>
<button
onClick={reiniciar}
className="p-2 text-gray-500 hover:text-blue-600 transition-colors"
>
<RotateCcw className="w-5 h-5" />
</button>
</div>
<p className="text-gray-600">
Completa la tabla de demanda siguiendo la ley de la demanda: a mayor precio, menor cantidad demandada.
<br />
<span className="text-sm text-blue-600">
<strong>Pista:</strong> Por cada $10 que sube el precio, la cantidad baja 20 unidades.
</span>
</p>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full border-collapse">
<thead>
<tr className="bg-blue-50">
<th className="p-4 text-left font-semibold text-gray-700 border-b-2 border-blue-200">
Precio ($)
</th>
<th className="p-4 text-left font-semibold text-gray-700 border-b-2 border-blue-200">
Cantidad Demandada
</th>
{mostrarResultados && (
<th className="p-4 text-center font-semibold text-gray-700 border-b-2 border-blue-200 w-24">
Resultado
</th>
)}
</tr>
</thead>
<tbody>
{filas.map((fila, index) => (
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className="p-4 border-b border-gray-200 font-medium">
${fila.precio}
</td>
<td className="p-4 border-b border-gray-200">
<input
type="number"
value={fila.cantidadUsuario}
onChange={(e) => handleCantidadChange(index, e.target.value)}
disabled={mostrarResultados}
placeholder="¿Cuántas unidades?"
className={`w-full max-w-xs px-4 py-2 border-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all ${
mostrarResultados
? parseInt(fila.cantidadUsuario) === fila.cantidadCorrecta
? 'border-green-500 bg-green-50'
: 'border-red-500 bg-red-50'
: 'border-gray-300 focus:border-blue-500'
}`}
/>
</td>
{mostrarResultados && (
<td className="p-4 border-b border-gray-200 text-center">
{parseInt(fila.cantidadUsuario) === fila.cantidadCorrecta ? (
<Check className="w-6 h-6 text-green-500 mx-auto" />
) : (
<div className="flex flex-col items-center gap-1">
<X className="w-6 h-6 text-red-500" />
<span className="text-xs text-green-600 font-medium">
{fila.cantidadCorrecta}
</span>
</div>
)}
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
<AnimatePresence>
{mostrarResultados && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mb-6 overflow-hidden"
>
<div className={`p-4 rounded-lg border ${
filas.every(f => parseInt(f.cantidadUsuario) === f.cantidadCorrecta)
? 'bg-green-50 border-green-200'
: 'bg-yellow-50 border-yellow-200'
}`}>
<div className="flex items-center gap-3 mb-3">
{filas.every(f => parseInt(f.cantidadUsuario) === f.cantidadCorrecta) ? (
<Trophy className="w-6 h-6 text-green-600" />
) : (
<Calculator className="w-6 h-6 text-yellow-600" />
)}
<span className={`font-semibold ${
filas.every(f => parseInt(f.cantidadUsuario) === f.cantidadCorrecta)
? 'text-green-800'
: 'text-yellow-800'
}`}>
{filas.every(f => parseInt(f.cantidadUsuario) === f.cantidadCorrecta)
? '¡Perfecto! Todas las respuestas son correctas'
: 'Algunas respuestas necesitan revisión'}
</span>
</div>
<p className="text-sm text-gray-700 mb-2">
Puntuación: <span className="font-bold text-blue-600">{score}/100</span>
</p>
{!filas.every(f => parseInt(f.cantidadUsuario) === f.cantidadCorrecta) && (
<p className="text-sm text-gray-600">
Las cantidades correctas se muestran en verde debajo de las respuestas incorrectas.
Observa el patrón: la cantidad disminuye 20 unidades por cada aumento de $10 en el precio.
</p>
)}
</div>
</motion.div>
)}
</AnimatePresence>
<div className="flex flex-wrap gap-4">
{!mostrarResultados ? (
<button
onClick={validarRespuestas}
disabled={!tablaCompletada}
className="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
>
<Check className="w-5 h-5" />
Validar Respuestas
</button>
) : (
<>
{!completado && (
<button
onClick={() => {
setMostrarResultados(false);
}}
className="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors flex items-center gap-2"
>
<RotateCcw className="w-5 h-5" />
Intentar de Nuevo
</button>
)}
<button
onClick={handleFinalizar}
className="px-6 py-3 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 transition-colors flex items-center gap-2"
>
Finalizar Ejercicio
</button>
</>
)}
</div>
{intentos > 0 && (
<div className="mt-4 text-sm text-gray-500">
Intentos realizados: {intentos}
</div>
)}
</div>
);
};
export default TablaDemanda;

View File

@@ -0,0 +1,289 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Table, Check, X, RotateCcw, Trophy, Calculator, HelpCircle } from 'lucide-react';
interface TablaOfertaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FilaTabla {
precio: number;
cantidad: number | null;
respuestaCorrecta: number;
}
const datosIniciales: FilaTabla[] = [
{ precio: 10, cantidad: null, respuestaCorrecta: 20 },
{ precio: 20, cantidad: null, respuestaCorrecta: 40 },
{ precio: 30, cantidad: null, respuestaCorrecta: 60 },
{ precio: 40, cantidad: null, respuestaCorrecta: 80 },
{ precio: 50, cantidad: null, respuestaCorrecta: 100 },
];
export const TablaOferta: React.FC<TablaOfertaProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
const [filas, setFilas] = useState<FilaTabla[]>(datosIniciales);
const [respuestasUsuario, setRespuestasUsuario] = useState<{[key: number]: string}>({});
const [mostrarResultados, setMostrarResultados] = useState(false);
const [score, setScore] = useState(0);
const [intentos, setIntentos] = useState(0);
const [completado, setCompletado] = useState(false);
const handleInputChange = (precio: number, valor: string) => {
if (mostrarResultados) return;
setRespuestasUsuario(prev => ({
...prev,
[precio]: valor
}));
};
const verificarRespuestas = () => {
let correctas = 0;
const nuevasFilas = filas.map(fila => {
const respuestaUsuario = parseInt(respuestasUsuario[fila.precio] || '0');
const esCorrecta = respuestaUsuario === fila.respuestaCorrecta;
if (esCorrecta) correctas++;
return {
...fila,
cantidad: respuestaUsuario
};
});
setFilas(nuevasFilas);
setMostrarResultados(true);
setIntentos(prev => prev + 1);
// Calcular puntuación
const porcentajeCorrectas = correctas / filas.length;
const bonusIntentos = intentos === 0 ? 20 : intentos === 1 ? 10 : 0;
const puntajeFinal = Math.round((porcentajeCorrectas * 80) + bonusIntentos);
setScore(puntajeFinal);
if (correctas === filas.length) {
setCompletado(true);
setTimeout(() => {
if (onComplete) {
onComplete(puntajeFinal);
}
}, 1500);
}
};
const reiniciar = () => {
setFilas(datosIniciales);
setRespuestasUsuario({});
setMostrarResultados(false);
setScore(0);
setIntentos(0);
setCompletado(false);
};
const todasRespondidas = filas.every(fila => respuestasUsuario[fila.precio] !== undefined && respuestasUsuario[fila.precio] !== '');
return (
<div className="w-full max-w-4xl 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">
<Table className="w-8 h-8 text-green-600" />
<h2 className="text-2xl font-bold text-gray-800">Completar Tabla de Oferta</h2>
</div>
<div className="flex items-center gap-4">
{completado && (
<span className="text-2xl font-bold text-green-600">{score} pts</span>
)}
<button
onClick={reiniciar}
className="p-2 text-gray-500 hover:text-green-600 transition-colors"
>
<RotateCcw className="w-5 h-5" />
</button>
</div>
</div>
<p className="text-gray-600">
Completa la tabla de oferta siguiendo la relación directa entre precio y cantidad.
Cuando el precio se duplica, la cantidad ofrecida también se duplica.
</p>
</div>
<div className="mb-6 p-4 bg-green-50 rounded-lg border border-green-200">
<div className="flex items-start gap-3">
<Calculator className="w-6 h-6 text-green-600 flex-shrink-0 mt-0.5" />
<div>
<h3 className="font-semibold text-green-800 mb-1">Datos del problema:</h3>
<p className="text-green-700 text-sm">
Un productor de camisetas está dispuesto a vender 20 unidades a $10 cada una.
La función de oferta es lineal: <strong>Q = 2 × P</strong>
</p>
</div>
</div>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="border-2 border-gray-300 p-4 text-left font-semibold text-gray-700">
<div className="flex items-center gap-2">
<span>Precio ($)</span>
</div>
</th>
<th className="border-2 border-gray-300 p-4 text-left font-semibold text-gray-700">
<div className="flex items-center gap-2">
<span>Cantidad Ofrecida (unidades)</span>
<HelpCircle className="w-4 h-4 text-gray-400" />
</div>
</th>
<th className="border-2 border-gray-300 p-4 text-center font-semibold text-gray-700 w-24">
Estado
</th>
</tr>
</thead>
<tbody>
{filas.map((fila, index) => {
const respuestaUsuario = respuestasUsuario[fila.precio] || '';
const esCorrecta = mostrarResultados && parseInt(respuestaUsuario) === fila.respuestaCorrecta;
const esIncorrecta = mostrarResultados && parseInt(respuestaUsuario) !== fila.respuestaCorrecta;
return (
<motion.tr
key={fila.precio}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className={`
${esCorrecta ? 'bg-green-50' : ''}
${esIncorrecta ? 'bg-red-50' : ''}
`}
>
<td className="border-2 border-gray-300 p-4">
<span className="text-xl font-semibold text-gray-800">
${fila.precio}
</span>
</td>
<td className="border-2 border-gray-300 p-4">
<div className="flex items-center gap-3">
<input
type="number"
value={respuestaUsuario}
onChange={(e) => handleInputChange(fila.precio, e.target.value)}
disabled={mostrarResultados}
placeholder="¿Cuántas unidades?"
className={`
w-full max-w-xs px-4 py-2 border-2 rounded-lg text-lg font-medium
focus:outline-none focus:ring-2 focus:ring-green-500
${esCorrecta ? 'border-green-500 bg-green-50 text-green-700' : ''}
${esIncorrecta ? 'border-red-500 bg-red-50 text-red-700' : ''}
${!mostrarResultados ? 'border-gray-300 hover:border-green-400' : ''}
`}
/>
{mostrarResultados && esIncorrecta && (
<span className="text-sm text-red-600">
Correcto: {fila.respuestaCorrecta}
</span>
)}
</div>
</td>
<td className="border-2 border-gray-300 p-4 text-center">
<AnimatePresence>
{mostrarResultados && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
>
{esCorrecta ? (
<Check className="w-6 h-6 text-green-600 mx-auto" />
) : (
<X className="w-6 h-6 text-red-600 mx-auto" />
)}
</motion.div>
)}
</AnimatePresence>
</td>
</motion.tr>
);
})}
</tbody>
</table>
</div>
{/* Visualización de la relación */}
<div className="mb-6 p-4 bg-blue-50 rounded-lg">
<h3 className="font-semibold text-blue-800 mb-3">Patrón a seguir:</h3>
<div className="flex flex-wrap items-center gap-4">
<div className="flex items-center gap-2 text-blue-700">
<span className="font-semibold">P = $10</span>
<span></span>
<span>Q = 20</span>
</div>
<span className="text-blue-400">|</span>
<div className="flex items-center gap-2 text-blue-700">
<span className="font-semibold">P = $20</span>
<span></span>
<span>Q = 40</span>
</div>
<span className="text-blue-400">|</span>
<div className="flex items-center gap-2 text-blue-700">
<span className="font-semibold">P = $30</span>
<span></span>
<span>Q = 60</span>
</div>
<span className="text-blue-400">|</span>
<span className="text-blue-600 italic">¿Sigues el patrón?</span>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-4 items-center justify-between">
<div className="text-sm text-gray-600">
{mostrarResultados && (
<span>
Correctas: <span className="font-semibold text-green-600">
{filas.filter(f => f.cantidad === f.respuestaCorrecta).length}
</span> de {filas.length}
</span>
)}
</div>
{!completado ? (
<button
onClick={verificarRespuestas}
disabled={!todasRespondidas}
className="w-full sm:w-auto px-8 py-3 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2"
>
<Check className="w-5 h-5" />
{mostrarResultados ? 'Intentar de nuevo' : 'Verificar respuestas'}
</button>
) : (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="flex items-center gap-3 px-6 py-3 bg-green-50 border border-green-200 rounded-lg"
>
<Trophy className="w-6 h-6 text-green-600" />
<div>
<p className="font-semibold text-green-700">¡Completado!</p>
<p className="text-sm text-green-600">Puntuación: {score}/100</p>
</div>
</motion.div>
)}
</div>
{mostrarResultados && !completado && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg"
>
<p className="text-yellow-800">
<strong>Consejo:</strong> La cantidad ofrecida siempre es el doble del precio.
Por ejemplo: si P = $40, entonces Q = 2 × 40 = 80 unidades.
</p>
</motion.div>
)}
</div>
);
};
export default TablaOferta;

View File

@@ -1,3 +1,28 @@
export { LeyDemandaQuiz } from './LeyDemandaQuiz';
export { LeyOfertaQuiz } from './LeyOfertaQuiz';
export { TablaDemanda } from './TablaDemanda';
export { TablaOferta } from './TablaOferta';
export { CurvaDemandaConstructor } from './CurvaDemandaConstructor';
export { CurvaOfertaConstructor } from './CurvaOfertaConstructor';
export { EquilibrioFinder } from './EquilibrioFinder';
export { EquilibrioGrafico } from './EquilibrioGrafico';
export { FactoresDesplazanDemanda } from './FactoresDesplazanDemanda';
export { FactoresDesplazanOferta } from './FactoresDesplazanOferta';
export { DesplazamientoVsMovimiento } from './DesplazamientoVsMovimiento';
export { ExcesoDemandaEscasez } from './ExcesoDemandaEscasez';
export { ExcesoOfertaSuperavit } from './ExcesoOfertaSuperavit';
export { DemandaIndividualVsMercado } from './DemandaIndividualVsMercado';
export { OfertaCortoLargoPlazo } from './OfertaCortoLargoPlazo';
export { AjusteEquilibrio } from './AjusteEquilibrio';
export { CambiosEquilibrio } from './CambiosEquilibrio';
export { CalculoElasticidadPrecio } from './CalculoElasticidadPrecio';
export { ElasticidadElasticaInelastica } from './ElasticidadElasticaInelastica';
export { FactoresElasticidad } from './FactoresElasticidad';
export { ElasticidadIngresoTotal } from './ElasticidadIngresoTotal';
export { PrecioMaximoTecho } from './PrecioMaximoTecho';
export { PrecioMinimoPiso } from './PrecioMinimoPiso';
export { SimuladorControles } from './SimuladorControles';
export { ControlesVidaReal } from './ControlesVidaReal';
export { ConstructorCurvas } from './ConstructorCurvas';
export { SimuladorPrecios } from './SimuladorPrecios';
export { IdentificarShocks } from './IdentificarShocks';

View File

@@ -0,0 +1,286 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Card, CardHeader } from '../../ui/Card';
interface Ejercicio {
id: number;
bien: string;
descripcion: string;
escenario: string;
elasticidad: number;
respuestaCorrecta: 'lujo' | 'necesario';
explicacion: string;
}
const ejercicios: Ejercicio[] = [
{
id: 1,
bien: "Caviar",
descripcion: "Alimento de lujo",
escenario: "Cuando el ingreso aumenta 10%, el consumo de caviar aumenta 35%",
elasticidad: 3.5,
respuestaCorrecta: 'lujo',
explicacion: "Ei = 3.5 > 1, por lo que es un bien de lujo. El consumo aumenta más que proporcionalmente al ingreso."
},
{
id: 2,
bien: "Pan",
descripcion: "Alimento básico",
escenario: "Cuando el ingreso aumenta 20%, el consumo de pan aumenta solo 6%",
elasticidad: 0.3,
respuestaCorrecta: 'necesario',
explicacion: "Ei = 0.3 (entre 0 y 1), por lo que es un bien necesario. El consumo aumenta menos que proporcionalmente al ingreso."
},
{
id: 3,
bien: "Yates privados",
descripcion: "Embarcaciones recreativas",
escenario: "Un aumento del 15% en ingreso produce un aumento del 60% en la demanda de yates",
elasticidad: 4.0,
respuestaCorrecta: 'lujo',
explicacion: "Ei = 4.0 > 1, claramente un bien de lujo. Los bienes de lujo tienen elasticidad ingreso mayor a 1."
},
{
id: 4,
bien: "Sal",
descripcion: "Condimento esencial",
escenario: "Aunque el ingreso aumente 50%, el consumo de sal apenas varía un 2%",
elasticidad: 0.04,
respuestaCorrecta: 'necesario',
explicacion: "Ei = 0.04 ≈ 0, típico de bienes necesarios básicos. El consumo es relativamente independiente del ingreso."
},
{
id: 5,
bien: "Viajes en primera clase",
descripcion: "Transporte aéreo de lujo",
escenario: "El ingreso aumenta 25% y los viajes en primera clase aumentan 70%",
elasticidad: 2.8,
respuestaCorrecta: 'lujo',
explicacion: "Ei = 2.8 > 1, por lo que es un bien de lujo. Solo personas con alto ingreso pueden acceder a él."
},
{
id: 6,
bien: "Medicinas genéricas",
descripcion: "Productos farmacéuticos básicos",
escenario: "El ingreso sube 30% pero el consumo solo aumenta 6%",
elasticidad: 0.2,
respuestaCorrecta: 'necesario',
explicacion: "Ei = 0.2 (entre 0 y 1), por lo que es un bien necesario. La salud es prioritaria independiente del ingreso."
}
];
interface Respuesta {
tipo: 'lujo' | 'necesario' | null;
esCorrecta: boolean | null;
}
interface BienesLujoNecesariosProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function BienesLujoNecesarios({ ejercicioId: _ejercicioId, onComplete }: BienesLujoNecesariosProps) {
const [respuestas, setRespuestas] = useState<Record<number, Respuesta>>({});
const [mostrarResultados, setMostrarResultados] = useState(false);
const seleccionarRespuesta = (ejercicioId: number, tipo: 'lujo' | 'necesario') => {
if (mostrarResultados) return;
setRespuestas(prev => ({
...prev,
[ejercicioId]: { tipo, esCorrecta: null }
}));
};
const verificarTodo = () => {
const nuevasRespuestas: Record<number, Respuesta> = {};
ejercicios.forEach(ej => {
const respuesta = respuestas[ej.id];
if (respuesta?.tipo) {
nuevasRespuestas[ej.id] = {
tipo: respuesta.tipo,
esCorrecta: respuesta.tipo === ej.respuestaCorrecta
};
}
});
setRespuestas(nuevasRespuestas);
setMostrarResultados(true);
const correctas = Object.values(nuevasRespuestas).filter(r => r.esCorrecta).length;
if (onComplete) {
onComplete(Math.round((correctas / ejercicios.length) * 100));
}
};
const reiniciar = () => {
setRespuestas({});
setMostrarResultados(false);
};
const getCardStyle = (ejercicioId: number) => {
const respuesta = respuestas[ejercicioId];
if (!mostrarResultados || !respuesta?.tipo) {
return 'bg-white border-gray-200';
}
return respuesta.esCorrecta
? 'bg-green-50 border-green-400'
: 'bg-red-50 border-red-400';
};
const correctas = Object.values(respuestas).filter(r => r.esCorrecta).length;
const totalRespondidas = Object.keys(respuestas).length;
return (
<Card className="max-w-4xl mx-auto">
<CardHeader
title="Bienes de Lujo vs. Bienes Necesarios"
subtitle="Clasifica cada bien según su elasticidad ingreso"
/>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div className="bg-purple-50 p-4 rounded-lg border-2 border-purple-200">
<h4 className="font-bold text-lg text-purple-800">Bien de Lujo</h4>
<p className="text-sm font-mono text-purple-600 mt-1">E<sub>i</sub> &gt; 1</p>
<p className="text-sm text-purple-700 mt-2">
El gasto aumenta más que proporcionalmente al ingreso. Son bienes que se consumen más a medida que las personas tienen más dinero disponible.
</p>
<p className="text-xs text-purple-600 mt-2 italic">
Ejemplos: joyería, yates, viajes de lujo, arte
</p>
</div>
<div className="bg-yellow-50 p-4 rounded-lg border-2 border-yellow-200">
<h4 className="font-bold text-lg text-yellow-800">Bien Necesario</h4>
<p className="text-sm font-mono text-yellow-600 mt-1">0 &lt; E<sub>i</sub> &lt; 1</p>
<p className="text-sm text-yellow-700 mt-2">
El gasto aumenta menos que proporcionalmente al ingreso. Son bienes esenciales cuyo consumo no varía mucho con el ingreso.
</p>
<p className="text-xs text-yellow-600 mt-2 italic">
Ejemplos: alimentos básicos, medicinas, servicios públicos
</p>
</div>
</div>
<div className="space-y-4">
{ejercicios.map((ejercicio) => {
const respuesta = respuestas[ejercicio.id];
return (
<div
key={ejercicio.id}
className={`p-4 rounded-lg border-2 transition-all ${getCardStyle(ejercicio.id)}`}
>
<div className="flex flex-col md:flex-row md:items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2">
<h4 className="font-semibold text-gray-800">{ejercicio.bien}</h4>
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
{ejercicio.descripcion}
</span>
</div>
<p className="text-sm text-gray-600 mt-2">{ejercicio.escenario}</p>
{mostrarResultados && (
<div className="mt-3 space-y-2">
<p className="font-mono text-sm">
E<sub>i</sub> = {ejercicio.elasticidad}
</p>
<p className={`text-sm ${
ejercicio.respuestaCorrecta === 'lujo'
? 'text-purple-600'
: 'text-yellow-600'
}`}>
<strong>
{ejercicio.respuestaCorrecta === 'lujo'
? 'Bien de Lujo'
: 'Bien Necesario'}
</strong>
</p>
<p className="text-xs text-gray-500">{ejercicio.explicacion}</p>
</div>
)}
</div>
<div className="flex gap-2">
<button
onClick={() => seleccionarRespuesta(ejercicio.id, 'lujo')}
disabled={mostrarResultados}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
respuesta?.tipo === 'lujo'
? 'bg-purple-500 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
} ${mostrarResultados ? 'cursor-default opacity-50' : 'cursor-pointer'}`}
>
Lujo
</button>
<button
onClick={() => seleccionarRespuesta(ejercicio.id, 'necesario')}
disabled={mostrarResultados}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
respuesta?.tipo === 'necesario'
? 'bg-yellow-500 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
} ${mostrarResultados ? 'cursor-default opacity-50' : 'cursor-pointer'}`}
>
Necesario
</button>
</div>
</div>
</div>
);
})}
</div>
<div className="flex items-center justify-between pt-4 border-t">
<div className="text-sm text-gray-600">
Progreso: {totalRespondidas} / {ejercicios.length}
</div>
{!mostrarResultados ? (
<Button
onClick={verificarTodo}
disabled={totalRespondidas < ejercicios.length}
>
Verificar Respuestas
</Button>
) : (
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-sm text-gray-600">Puntuación</p>
<p className="text-2xl font-bold text-gray-800">
{correctas} / {ejercicios.length}
</p>
</div>
<Button variant="outline" onClick={reiniciar}>
Reiniciar
</Button>
</div>
)}
</div>
{mostrarResultados && (
<div className={`p-4 rounded-lg ${
correctas === ejercicios.length
? 'bg-green-100 border border-green-300'
: correctas >= ejercicios.length / 2
? 'bg-yellow-100 border border-yellow-300'
: 'bg-red-100 border border-red-300'
}`}>
<p className="font-semibold text-gray-800">
{correctas === ejercicios.length
? '¡Excelente! Has clasificado todos los bienes correctamente.'
: correctas >= ejercicios.length / 2
? '¡Buen trabajo! Algunos bienes necesitan más atención.'
: 'Necesitas repasar la diferencia entre bienes de lujo y necesarios.'}
</p>
</div>
)}
</div>
</Card>
);
}
export default BienesLujoNecesarios;

View File

@@ -0,0 +1,292 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Card, CardHeader } from '../../ui/Card';
interface Caso {
id: number;
bien: string;
descripcion: string;
i1: number;
i2: number;
q1: number;
q2: number;
respuestaCorrecta: 'normal' | 'inferior';
}
const casos: Caso[] = [
{
id: 1,
bien: "Arroz",
descripcion: "Alimento básico de consumo diario",
i1: 1000,
i2: 1500,
q1: 20,
q2: 22,
respuestaCorrecta: 'normal'
},
{
id: 2,
bien: "Autobuses urbanos",
descripcion: "Transporte público económico",
i1: 2000,
i2: 3000,
q1: 50,
q2: 30,
respuestaCorrecta: 'inferior'
},
{
id: 3,
bien: "Pan blanco común",
descripcion: "Pan básico de bajo costo",
i1: 1500,
i2: 2500,
q1: 30,
q2: 15,
respuestaCorrecta: 'inferior'
},
{
id: 4,
bien: "Leche",
descripcion: "Producto lácteo básico",
i1: 2000,
i2: 3500,
q1: 12,
q2: 18,
respuestaCorrecta: 'normal'
},
{
id: 5,
bien: "Frijoles enlatados",
descripcion: "Versión económica vs. frescos",
i1: 3000,
i2: 5000,
q1: 20,
q2: 8,
respuestaCorrecta: 'inferior'
}
];
interface Respuesta {
tipo: 'normal' | 'inferior' | null;
esCorrecta: boolean | null;
}
interface BienesNormalesInferioresProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function BienesNormalesInferiores({ ejercicioId: _ejercicioId, onComplete }: BienesNormalesInferioresProps) {
const [respuestas, setRespuestas] = useState<Record<number, Respuesta>>({});
const [mostrarResultados, setMostrarResultados] = useState(false);
const calcularElasticidad = (caso: Caso) => {
const deltaQ = caso.q2 - caso.q1;
const deltaI = caso.i2 - caso.i1;
const qPromedio = (caso.q1 + caso.q2) / 2;
const iPromedio = (caso.i1 + caso.i2) / 2;
const porcentajeQ = (deltaQ / qPromedio) * 100;
const porcentajeI = (deltaI / iPromedio) * 100;
return porcentajeQ / porcentajeI;
};
const seleccionarRespuesta = (casoId: number, tipo: 'normal' | 'inferior') => {
if (mostrarResultados) return;
setRespuestas(prev => ({
...prev,
[casoId]: { tipo, esCorrecta: null }
}));
};
const verificarTodo = () => {
const nuevasRespuestas: Record<number, Respuesta> = {};
casos.forEach(caso => {
const respuesta = respuestas[caso.id];
if (respuesta?.tipo) {
nuevasRespuestas[caso.id] = {
tipo: respuesta.tipo,
esCorrecta: respuesta.tipo === caso.respuestaCorrecta
};
}
});
setRespuestas(nuevasRespuestas);
setMostrarResultados(true);
const correctas = Object.values(nuevasRespuestas).filter(r => r.esCorrecta).length;
if (onComplete) {
onComplete(Math.round((correctas / casos.length) * 100));
}
};
const reiniciar = () => {
setRespuestas({});
setMostrarResultados(false);
};
const getCardStyle = (caso: Caso) => {
const respuesta = respuestas[caso.id];
if (!mostrarResultados || !respuesta?.tipo) {
return 'bg-white border-gray-200';
}
return respuesta.esCorrecta
? 'bg-green-50 border-green-400'
: 'bg-red-50 border-red-400';
};
const correctas = Object.values(respuestas).filter(r => r.esCorrecta).length;
const totalRespondidas = Object.keys(respuestas).length;
return (
<Card className="max-w-4xl mx-auto">
<CardHeader
title="Bienes Normales vs. Bienes Inferiores"
subtitle="Identifica el tipo de bien según su comportamiento ante cambios de ingreso"
/>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div className="bg-blue-50 p-4 rounded-lg border-2 border-blue-200">
<h4 className="font-bold text-lg text-blue-800">Bien Normal</h4>
<p className="text-sm font-mono text-blue-600 mt-1">E<sub>i</sub> &gt; 0</p>
<p className="text-sm text-blue-700 mt-2">
La demanda aumenta cuando sube el ingreso
</p>
</div>
<div className="bg-red-50 p-4 rounded-lg border-2 border-red-200">
<h4 className="font-bold text-lg text-red-800">Bien Inferior</h4>
<p className="text-sm font-mono text-red-600 mt-1">E<sub>i</sub> &lt; 0</p>
<p className="text-sm text-red-700 mt-2">
La demanda disminuye cuando sube el ingreso
</p>
</div>
</div>
<div className="space-y-4">
{casos.map((caso) => {
const elasticidad = calcularElasticidad(caso);
const respuesta = respuestas[caso.id];
return (
<div
key={caso.id}
className={`p-4 rounded-lg border-2 transition-all ${getCardStyle(caso)}`}
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex-1">
<h4 className="font-semibold text-gray-800">{caso.bien}</h4>
<p className="text-sm text-gray-500">{caso.descripcion}</p>
<div className="grid grid-cols-4 gap-2 mt-3 text-sm">
<div className="bg-gray-50 p-2 rounded text-center">
<span className="font-mono text-xs">I</span>
<p className="font-semibold">${caso.i1}</p>
</div>
<div className="bg-gray-50 p-2 rounded text-center">
<span className="font-mono text-xs">I</span>
<p className="font-semibold">${caso.i2}</p>
</div>
<div className="bg-gray-50 p-2 rounded text-center">
<span className="font-mono text-xs">Q</span>
<p className="font-semibold">{caso.q1}</p>
</div>
<div className="bg-gray-50 p-2 rounded text-center">
<span className="font-mono text-xs">Q</span>
<p className="font-semibold">{caso.q2}</p>
</div>
</div>
{mostrarResultados && respuesta?.tipo && (
<div className="mt-3 text-sm">
<p className="font-mono">
E<sub>i</sub> = {elasticidad.toFixed(2)} {' '}
<span className={elasticidad > 0 ? 'text-blue-600' : 'text-red-600'}>
{elasticidad > 0 ? 'Bien Normal' : 'Bien Inferior'}
</span>
</p>
</div>
)}
</div>
<div className="flex gap-2">
<button
onClick={() => seleccionarRespuesta(caso.id, 'normal')}
disabled={mostrarResultados}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
respuesta?.tipo === 'normal'
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
} ${mostrarResultados ? 'cursor-default opacity-50' : 'cursor-pointer'}`}
>
Normal
</button>
<button
onClick={() => seleccionarRespuesta(caso.id, 'inferior')}
disabled={mostrarResultados}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
respuesta?.tipo === 'inferior'
? 'bg-red-500 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
} ${mostrarResultados ? 'cursor-default opacity-50' : 'cursor-pointer'}`}
>
Inferior
</button>
</div>
</div>
</div>
);
})}
</div>
<div className="flex items-center justify-between pt-4 border-t">
<div className="text-sm text-gray-600">
Progreso: {totalRespondidas} / {casos.length}
</div>
{!mostrarResultados ? (
<Button
onClick={verificarTodo}
disabled={totalRespondidas < casos.length}
>
Verificar Respuestas
</Button>
) : (
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-sm text-gray-600">Puntuación</p>
<p className="text-2xl font-bold text-gray-800">
{correctas} / {casos.length}
</p>
</div>
<Button variant="outline" onClick={reiniciar}>
Reiniciar
</Button>
</div>
)}
</div>
{mostrarResultados && (
<div className={`p-4 rounded-lg ${
correctas === casos.length
? 'bg-green-100 border border-green-300'
: correctas >= casos.length / 2
? 'bg-yellow-100 border border-yellow-300'
: 'bg-red-100 border border-red-300'
}`}>
<p className="font-semibold text-gray-800">
{correctas === casos.length
? '¡Excelente! Has identificado todos los bienes correctamente.'
: correctas >= casos.length / 2
? '¡Buen trabajo! Algunos bienes necesitan más atención.'
: 'Necesitas repasar la diferencia entre bienes normales e inferiores.'}
</p>
</div>
)}
</div>
</Card>
);
}
export default BienesNormalesInferiores;

View File

@@ -0,0 +1,334 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Card, CardHeader } from '../../ui/Card';
interface CanastaOptimaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Canasta {
id: string;
nombre: string;
descripcion: string;
items: { nombre: string; cantidad: number; precio: number }[];
utTotal: number;
esOptima: boolean;
razonamiento: string;
}
const canastas: Canasta[] = [
{
id: 'a',
nombre: 'Canasta A',
descripcion: 'Mayor cantidad de bienes baratos',
items: [
{ nombre: 'Manzanas 🍎', cantidad: 8, precio: 2 },
{ nombre: 'Naranjas 🍊', cantidad: 3, precio: 3 },
],
utTotal: 68,
esOptima: false,
razonamiento: 'Aunque tiene muchas manzanas, la UMg/P de las naranjas es mayor al inicio. No maximiza la utilidad.'
},
{
id: 'b',
nombre: 'Canasta B',
descripcion: 'Combinación balanceada',
items: [
{ nombre: 'Manzanas 🍎', cantidad: 5, precio: 2 },
{ nombre: 'Naranjas 🍊', cantidad: 5, precio: 3 },
],
utTotal: 85,
esOptima: true,
razonamiento: '¡Óptima! En esta combinación, UMg/P de manzanas ≈ UMg/P de naranjas. Maximiza la utilidad dado el presupuesto de $25.'
},
{
id: 'c',
nombre: 'Canasta C',
descripcion: 'Muchas naranjas',
items: [
{ nombre: 'Manzanas 🍎', cantidad: 2, precio: 2 },
{ nombre: 'Naranjas 🍊', cantidad: 7, precio: 3 },
],
utTotal: 78,
esOptima: false,
razonamiento: 'Demasiadas naranjas. La UMg de las últimas unidades es muy baja comparada con las manzanas que podría haber comprado.'
},
{
id: 'd',
nombre: 'Canasta D',
descripcion: 'Excede el presupuesto',
items: [
{ nombre: 'Manzanas 🍎', cantidad: 6, precio: 2 },
{ nombre: 'Naranjas 🍊', cantidad: 6, precio: 3 },
],
utTotal: 90,
esOptima: false,
razonamiento: 'Costo total: $30. ¡Excede el presupuesto de $25! No es factible.'
}
];
const datosUtilidad = {
manzanas: {
um: [12, 10, 8, 6, 4, 2, 0, -2],
precio: 2
},
naranjas: {
um: [15, 12, 9, 6, 3, 0, -3],
precio: 3
}
};
export function CanastaOptima({ ejercicioId: _ejercicioId, onComplete }: CanastaOptimaProps) {
const [canastaSeleccionada, setCanastaSeleccionada] = useState<string | null>(null);
const [mostrarResultado, setMostrarResultado] = useState(false);
const [etapa, setEtapa] = useState(0);
const presupuesto = 25;
const handleSeleccion = (id: string) => {
setCanastaSeleccionada(id);
setMostrarResultado(false);
};
const verificar = () => {
setMostrarResultado(true);
const canasta = canastas.find(c => c.id === canastaSeleccionada);
if (canasta?.esOptima && onComplete) {
onComplete(100);
}
};
const calcularCosto = (canasta: Canasta) => {
return canasta.items.reduce((total, item) => total + item.cantidad * item.precio, 0);
};
return (
<Card className="max-w-4xl mx-auto">
<CardHeader
title="Canasta Óptima de Consumo"
subtitle="Identifica la combinación de bienes que maximiza la utilidad dado un presupuesto limitado"
/>
<div className="space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-bold text-blue-900 mb-2">Problema</h3>
<p className="text-sm text-blue-800 mb-2">
Tienes un presupuesto de <strong>${presupuesto}</strong> para gastar en manzanas y naranjas.
</p>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="bg-white p-2 rounded">
<p className="font-semibold">🍎 Manzanas: $2 cada una</p>
<p className="text-xs text-gray-600">UMg: 12, 10, 8, 6, 4, 2, 0, -2</p>
</div>
<div className="bg-white p-2 rounded">
<p className="font-semibold">🍊 Naranjas: $3 cada una</p>
<p className="text-xs text-gray-600">UMg: 15, 12, 9, 6, 3, 0, -3</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{canastas.map((canasta) => {
const costo = calcularCosto(canasta);
const dentroPresupuesto = costo <= presupuesto;
return (
<div
key={canasta.id}
onClick={() => handleSeleccion(canasta.id)}
className={`p-4 rounded-lg border-2 cursor-pointer transition-all ${
canastaSeleccionada === canasta.id
? 'border-primary bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
} ${mostrarResultado && canasta.esOptima ? 'ring-2 ring-green-500' : ''}`}
>
<div className="flex justify-between items-start mb-2">
<h4 className="font-bold">{canasta.nombre}</h4>
{mostrarResultado && canasta.esOptima && (
<span className="bg-green-500 text-white text-xs px-2 py-1 rounded">ÓPTIMA </span>
)}
</div>
<p className="text-sm text-gray-600 mb-2">{canasta.descripcion}</p>
<div className="space-y-1 text-sm mb-3">
{canasta.items.map((item) => (
<div key={item.nombre} className="flex justify-between">
<span>{item.nombre}</span>
<span className="font-mono">{item.cantidad} × ${item.precio} = ${item.cantidad * item.precio}</span>
</div>
))}
</div>
<div className="border-t pt-2">
<div className="flex justify-between text-sm">
<span>Costo Total:</span>
<span className={`font-mono font-bold ${dentroPresupuesto ? 'text-green-600' : 'text-red-600'}`}>
${costo} {dentroPresupuesto ? '✓' : '✗'}
</span>
</div>
<div className="flex justify-between text-sm mt-1">
<span>Utilidad Total:</span>
<span className="font-mono font-bold text-blue-600">{canasta.utTotal}</span>
</div>
</div>
{mostrarResultado && (
<div className={`mt-3 p-2 rounded text-xs ${
canasta.esOptima ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-700'
}`}>
{canasta.razonamiento}
</div>
)}
</div>
);
})}
</div>
<div className="flex justify-between items-center">
<div className="text-sm text-gray-600">
{canastaSeleccionada && (
<>Seleccionado: <strong>{canastas.find(c => c.id === canastaSeleccionada)?.nombre}</strong></>
)}
</div>
<Button
onClick={verificar}
disabled={!canastaSeleccionada || mostrarResultado}
>
Verificar Respuesta
</Button>
</div>
{mostrarResultado && (
<div className={`p-4 rounded-lg ${
canastas.find(c => c.id === canastaSeleccionada)?.esOptima
? 'bg-green-100 border border-green-300'
: 'bg-red-100 border border-red-300'
}`}>
<p className="font-semibold">
{canastas.find(c => c.id === canastaSeleccionada)?.esOptima
? '¡Correcto! Has identificado la canasta óptima.'
: 'Incorrecto. Revisa el razonamiento de cada canasta y encuentra la que maximiza la utilidad sin exceder el presupuesto.'
}
</p>
</div>
)}
<div className="border-t pt-4">
<div className="flex gap-2 mb-4">
{['Datos', 'Proceso', 'Análisis'].map((etapaNombre, idx) => (
<Button
key={etapaNombre}
variant={etapa === idx ? 'primary' : 'outline'}
size="sm"
onClick={() => setEtapa(idx)}
>
{idx + 1}. {etapaNombre}
</Button>
))}
</div>
{etapa === 0 && (
<div className="bg-gray-50 p-4 rounded-lg">
<h4 className="font-bold mb-3">Tabla de Utilidad Marginal por Precio (UMg/P)</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-200">
<th className="border p-2">Unidad</th>
<th className="border p-2">UMg Manzana</th>
<th className="border p-2">UMg/P Manzana</th>
<th className="border p-2">UMg Naranja</th>
<th className="border p-2">UMg/P Naranja</th>
</tr>
</thead>
<tbody>
{[1, 2, 3, 4, 5, 6, 7].map((i) => (
<tr key={i}>
<td className="border p-2 text-center">{i}</td>
<td className="border p-2 text-center">{datosUtilidad.manzanas.um[i-1] || '-'}</td>
<td className="border p-2 text-center">
{datosUtilidad.manzanas.um[i-1] ? (datosUtilidad.manzanas.um[i-1] / 2).toFixed(1) : '-'}
</td>
<td className="border p-2 text-center">{datosUtilidad.naranjas.um[i-1] || '-'}</td>
<td className="border p-2 text-center">
{datosUtilidad.naranjas.um[i-1] ? (datosUtilidad.naranjas.um[i-1] / 3).toFixed(1) : '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{etapa === 1 && (
<div className="bg-yellow-50 p-4 rounded-lg">
<h4 className="font-bold mb-3 text-yellow-900">Proceso de Optimización</h4>
<div className="space-y-2 text-sm text-yellow-800">
<p><strong>Paso 1:</strong> Ordenar todas las unidades por UMg/P (de mayor a menor):</p>
<div className="bg-white p-2 rounded font-mono text-xs">
1. Naranja 1: 15/3 = 5.0<br/>
2. Manzana 1: 12/2 = 6.0 <br/>
3. Naranja 2: 12/3 = 4.0<br/>
4. Manzana 2: 10/2 = 5.0<br/>
5. Naranja 3: 9/3 = 3.0<br/>
6. Manzana 3: 8/2 = 4.0<br/>
7. Naranja 4: 6/3 = 2.0<br/>
8. Manzana 4: 6/2 = 3.0<br/>
9. Manzana 5: 4/2 = 2.0<br/>
10. Naranja 5: 3/3 = 1.0<br/>
...
</div>
<p className="mt-2"><strong>Paso 2:</strong> Seleccionar unidades hasta agotar el presupuesto de $25:</p>
<div className="bg-white p-2 rounded font-mono text-xs">
Manzana 1: $2 (Total: $2)<br/>
Naranja 1: $3 (Total: $5)<br/>
Manzana 2: $2 (Total: $7)<br/>
Naranja 2: $3 (Total: $10)<br/>
Manzana 3: $2 (Total: $12)<br/>
Naranja 3: $3 (Total: $15)<br/>
Manzana 4: $2 (Total: $17)<br/>
Naranja 4: $3 (Total: $20)<br/>
Manzana 5: $2 (Total: $22)<br/>
Naranja 5: $3 (Total: $25) <br/>
<strong>Resultado: 5 manzanas + 5 naranjas = $25</strong>
</div>
</div>
</div>
)}
{etapa === 2 && (
<div className="bg-green-50 p-4 rounded-lg">
<h4 className="font-bold mb-3 text-green-900">Análisis de la Canasta Óptima</h4>
<div className="space-y-3 text-sm text-green-800">
<p>La canasta óptima debe cumplir dos condiciones:</p>
<ol className="list-decimal ml-5 space-y-1">
<li><strong>Agotar el presupuesto:</strong> Gastar exactamente $25</li>
<li><strong>Igualar UMg/P:</strong> La utilidad marginal por peso debe ser similar para ambos bienes</li>
</ol>
<div className="bg-white p-3 rounded mt-3">
<p className="font-semibold">En la Canasta B (Óptima):</p>
<ul className="mt-2 space-y-1">
<li>5 manzanas × $2 = $10</li>
<li>5 naranjas × $3 = $15</li>
<li><strong>Total: $25</strong> </li>
<li className="mt-2">UMg/P de última manzana: 4/2 = 2.0</li>
<li>UMg/P de última naranja: 3/3 = 1.0</li>
</ul>
<p className="mt-2 text-xs">
Nota: En el óptimo, las UMg/P son aproximadamente iguales (diferencias pequeñas se deben a que no podemos comprar fracciones de unidades).
</p>
</div>
</div>
</div>
)}
</div>
</div>
</Card>
);
}
export default CanastaOptima;

View File

@@ -0,0 +1,236 @@
import { useState, useCallback } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, RotateCcw, HelpCircle, AlertCircle } from 'lucide-react';
interface ClasificacionElasticidadProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
type TipoElasticidad = 'elastica' | 'unitaria' | 'inelastica';
interface EjercicioData {
ep: number;
descripcion: string;
explicacion: string;
}
const ejercicios: EjercicioData[] = [
{
ep: -2.5,
descripcion: 'Un producto tiene una elasticidad precio de -2.5. ¿Cómo se clasifica?',
explicacion: '|Ep| = 2.5 > 1 → Demanda ELÁSTICA. El % de cambio en cantidad es mayor que el % de cambio en precio.',
},
{
ep: -0.3,
descripcion: 'La elasticidad precio de un bien es -0.3. ¿Qué tipo de demanda tiene?',
explicacion: '|Ep| = 0.3 < 1 → Demanda INELÁSTICA. El % de cambio en cantidad es menor que el % de cambio en precio.',
},
{
ep: -1.0,
descripcion: 'Un artículo tiene elasticidad precio igual a -1. ¿Cómo se clasifica?',
explicacion: '|Ep| = 1 → Demanda UNITARIA. El % de cambio en cantidad es igual al % de cambio en precio.',
},
{
ep: -0.8,
descripcion: 'La elasticidad de un medicamento es de -0.8. ¿Qué tipo de elasticidad tiene?',
explicacion: '|Ep| = 0.8 < 1 → Demanda INELÁSTICA. Los medicamentos suelen ser inelásticos porque son necesidades básicas.',
},
{
ep: -4.2,
descripcion: 'Un restaurante de lujo tiene elasticidad de -4.2. ¿Cómo se clasifica?',
explicacion: '|Ep| = 4.2 > 1 → Demanda ELÁSTICA. Los lujos suelen tener demanda muy elástica porque son opcionales.',
},
];
export function ClasificacionElasticidad({ ejercicioId: _ejercicioId, onComplete }: ClasificacionElasticidadProps) {
const [ejercicioIndex, setEjercicioIndex] = useState(0);
const [respuesta, setRespuesta] = useState<TipoElasticidad | null>(null);
const [validado, setValidado] = useState(false);
const [aciertos, setAciertos] = useState(0);
const [completado, setCompletado] = useState(false);
const ejercicio = ejercicios[ejercicioIndex];
const obtenerRespuestaCorrecta = useCallback((ep: number): TipoElasticidad => {
const valorAbs = Math.abs(ep);
if (valorAbs > 1) return 'elastica';
if (valorAbs < 1) return 'inelastica';
return 'unitaria';
}, []);
const validarRespuesta = () => {
if (!respuesta) return;
const correcta = obtenerRespuestaCorrecta(ejercicio.ep);
const esCorrecto = respuesta === correcta;
setValidado(true);
if (esCorrecto) {
setAciertos((prev) => prev + 1);
}
if (ejercicioIndex === ejercicios.length - 1) {
setCompletado(true);
if (onComplete) {
const puntuacion = Math.round((aciertos + (esCorrecto ? 1 : 0)) / ejercicios.length * 100);
onComplete(puntuacion);
}
}
};
const siguienteEjercicio = () => {
if (ejercicioIndex < ejercicios.length - 1) {
setEjercicioIndex((prev) => prev + 1);
setRespuesta(null);
setValidado(false);
}
};
const reiniciar = () => {
setEjercicioIndex(0);
setRespuesta(null);
setValidado(false);
setAciertos(0);
setCompletado(false);
};
const respuestaCorrecta = obtenerRespuestaCorrecta(ejercicio.ep);
const opciones: { value: TipoElasticidad; label: string; color: string }[] = [
{ value: 'elastica', label: 'Elástica (|Ep| > 1)', color: 'bg-green-100 border-green-300 text-green-800' },
{ value: 'unitaria', label: 'Unitaria (|Ep| = 1)', color: 'bg-yellow-100 border-yellow-300 text-yellow-800' },
{ value: 'inelastica', label: 'Inelástica (|Ep| < 1)', color: 'bg-blue-100 border-blue-300 text-blue-800' },
];
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Clasificación de Elasticidad"
subtitle="Identifica si la demanda es elástica, unitaria o inelástica"
/>
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-green-50 border-2 border-green-200 rounded-lg p-4 text-center">
<h4 className="font-bold text-green-800 mb-1">ELÁSTICA</h4>
<p className="text-2xl font-bold text-green-600">|Ep| &gt; 1</p>
<p className="text-xs text-green-700 mt-1">%ΔQ &gt; %ΔP</p>
</div>
<div className="bg-yellow-50 border-2 border-yellow-200 rounded-lg p-4 text-center">
<h4 className="font-bold text-yellow-800 mb-1">UNITARIA</h4>
<p className="text-2xl font-bold text-yellow-600">|Ep| = 1</p>
<p className="text-xs text-yellow-700 mt-1">%ΔQ = %ΔP</p>
</div>
<div className="bg-blue-50 border-2 border-blue-200 rounded-lg p-4 text-center">
<h4 className="font-bold text-blue-800 mb-1">INELÁSTICA</h4>
<p className="text-2xl font-bold text-blue-600">|Ep| &lt; 1</p>
<p className="text-xs text-blue-700 mt-1">%ΔQ &lt; %ΔP</p>
</div>
</div>
<div className="mb-6">
<div className="flex items-center gap-2 mb-3">
<span className="bg-primary text-white text-xs font-bold px-2 py-1 rounded">
{ejercicioIndex + 1}/{ejercicios.length}
</span>
<h4 className="font-medium text-gray-700">Pregunta:</h4>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-gray-800 mb-3">{ejercicio.descripcion}</p>
<p className="text-lg font-bold text-primary">
E<sub>p</sub> = {ejercicio.ep}
</p>
</div>
</div>
<div className="space-y-3 mb-6">
{opciones.map((opcion) => (
<button
key={opcion.value}
onClick={() => {
setRespuesta(opcion.value);
setValidado(false);
}}
disabled={validado}
className={`w-full p-4 rounded-lg border-2 text-left transition-all ${
respuesta === opcion.value
? opcion.color
: 'bg-white border-gray-200 hover:border-gray-300'
} ${validado && respuestaCorrecta === opcion.value ? 'ring-2 ring-success' : ''}`}
>
<div className="flex items-center justify-between">
<span className="font-medium">{opcion.label}</span>
{validado && respuestaCorrecta === opcion.value && (
<CheckCircle className="w-5 h-5 text-success" />
)}
{validado && respuesta === opcion.value && respuesta !== respuestaCorrecta && (
<AlertCircle className="w-5 h-5 text-error" />
)}
</div>
</button>
))}
</div>
{validado && (
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 mb-6">
<h4 className="font-medium text-blue-900 mb-1 flex items-center gap-2">
<HelpCircle className="w-4 h-4" />
Explicación:
</h4>
<p className="text-blue-800">{ejercicio.explicacion}</p>
</div>
)}
<div className="flex gap-3">
{!validado ? (
<Button onClick={validarRespuesta} variant="primary" disabled={!respuesta}>
<CheckCircle className="w-4 h-4 mr-2" />
Validar Respuesta
</Button>
) : ejercicioIndex < ejercicios.length - 1 ? (
<Button onClick={siguienteEjercicio} variant="primary">
Siguiente Ejercicio
</Button>
) : (
<Button onClick={reiniciar} variant="primary">
<RotateCcw className="w-4 h-4 mr-2" />
Reiniciar
</Button>
)}
</div>
{completado && (
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
<p className="text-success font-medium text-center">
¡Completado! Has acertado {aciertos + (respuesta === respuestaCorrecta ? 1 : 0)} de{' '}
{ejercicios.length} ejercicios
</p>
</div>
)}
</Card>
<Card className="bg-purple-50 border-purple-200">
<h4 className="font-semibold text-purple-900 mb-2">Interpretación Económica:</h4>
<ul className="space-y-2 text-sm text-purple-800">
<li>
<strong>Elástica (|Ep| &gt; 1):</strong> Los consumidores son muy sensibles al precio.
Un cambio de precio genera un cambio proporcionalmente mayor en cantidad demandada.
</li>
<li>
<strong>Unitaria (|Ep| = 1):</strong> Sensibilidad proporcional.
El gasto total de los consumidores se mantiene constante ante cambios de precio.
</li>
<li>
<strong>Inelástica (|Ep| &lt; 1):</strong> Los consumidores son poco sensibles al precio.
La cantidad demandada cambia menos que proporcionalmente al precio.
</li>
</ul>
</Card>
</div>
);
}
export default ClasificacionElasticidad;

View File

@@ -0,0 +1,326 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { Card, CardHeader } from '../../ui/Card';
interface PuntoCurva {
ingreso: number;
cantidad: number;
}
interface Ejercicio {
id: number;
titulo: string;
descripcion: string;
bien: string;
puntos: PuntoCurva[];
tipoBien: 'lujo' | 'necesario' | 'inferior';
}
const ejercicios: Ejercicio[] = [
{
id: 1,
titulo: "Curva de Engel - Bien de Lujo",
descripcion: "La siguiente tabla muestra cómo varía el consumo de restaurantes de lujo ante diferentes niveles de ingreso mensual.",
bien: "Restaurantes de lujo",
puntos: [
{ ingreso: 1000, cantidad: 0 },
{ ingreso: 2000, cantidad: 2 },
{ ingreso: 3000, cantidad: 6 },
{ ingreso: 4000, cantidad: 12 },
{ ingreso: 5000, cantidad: 20 }
],
tipoBien: 'lujo'
},
{
id: 2,
titulo: "Curva de Engel - Bien Necesario",
descripcion: "La siguiente tabla muestra cómo varía el consumo de leche ante diferentes niveles de ingreso mensual.",
bien: "Leche (litros)",
puntos: [
{ ingreso: 1000, cantidad: 8 },
{ ingreso: 2000, cantidad: 10 },
{ ingreso: 3000, cantidad: 11 },
{ ingreso: 4000, cantidad: 12 },
{ ingreso: 5000, cantidad: 12.5 }
],
tipoBien: 'necesario'
},
{
id: 3,
titulo: "Curva de Engel - Bien Inferior",
descripcion: "La siguiente tabla muestra cómo varía el consumo de pan de bagazo ante diferentes niveles de ingreso mensual.",
bien: "Pan de bagazo (kg)",
puntos: [
{ ingreso: 1000, cantidad: 15 },
{ ingreso: 2000, cantidad: 12 },
{ ingreso: 3000, cantidad: 8 },
{ ingreso: 4000, cantidad: 4 },
{ ingreso: 5000, cantidad: 1 }
],
tipoBien: 'inferior'
}
];
interface Respuesta {
tipo: string | null;
esCorrecta: boolean | null;
}
interface CurvaEngelProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function CurvaEngel({ ejercicioId: _ejercicioId, onComplete }: CurvaEngelProps) {
const [ejercicioActual, setEjercicioActual] = useState(0);
const [respuestas, setRespuestas] = useState<Record<number, Respuesta>>({});
const [mostrarResultados, setMostrarResultados] = useState(false);
const [calculosElasticidad, setCalculosElasticidad] = useState<Record<string, string>>({});
const ejercicio = ejercicios[ejercicioActual];
const calcularElasticidadIntervalo = (p1: PuntoCurva, p2: PuntoCurva) => {
const deltaQ = p2.cantidad - p1.cantidad;
const deltaI = p2.ingreso - p1.ingreso;
const qPromedio = (p1.cantidad + p2.cantidad) / 2;
const iPromedio = (p1.ingreso + p2.ingreso) / 2;
if (qPromedio === 0 || iPromedio === 0) return 0;
const porcentajeQ = (deltaQ / qPromedio) * 100;
const porcentajeI = (deltaI / iPromedio) * 100;
return porcentajeQ / porcentajeI;
};
const handleCalculoChange = (intervalo: number, valor: string) => {
setCalculosElasticidad(prev => ({
...prev,
[`${ejercicio.id}_${intervalo}`]: valor
}));
};
const seleccionarTipo = (tipo: string) => {
if (mostrarResultados) return;
setRespuestas(prev => ({
...prev,
[ejercicio.id]: { tipo, esCorrecta: null }
}));
};
const verificar = () => {
const respuesta = respuestas[ejercicio.id];
if (!respuesta?.tipo) return;
const esCorrecta = respuesta.tipo === ejercicio.tipoBien;
setRespuestas(prev => ({
...prev,
[ejercicio.id]: { ...respuesta, esCorrecta }
}));
setMostrarResultados(true);
if (esCorrecta && ejercicioActual === ejercicios.length - 1 && onComplete) {
const totalCorrectas = Object.values(respuestas).filter(r => r.esCorrecta).length + 1;
onComplete(Math.round((totalCorrectas / ejercicios.length) * 100));
}
};
const siguienteEjercicio = () => {
if (ejercicioActual < ejercicios.length - 1) {
setEjercicioActual(prev => prev + 1);
setMostrarResultados(false);
}
};
const reiniciar = () => {
setEjercicioActual(0);
setRespuestas({});
setMostrarResultados(false);
setCalculosElasticidad({});
};
const respuestaActual = respuestas[ejercicio.id];
const getTipoColor = (tipo: string) => {
switch (tipo) {
case 'lujo': return 'bg-purple-100 border-purple-300 text-purple-800';
case 'necesario': return 'bg-yellow-100 border-yellow-300 text-yellow-800';
case 'inferior': return 'bg-red-100 border-red-300 text-red-800';
default: return 'bg-gray-100 border-gray-300 text-gray-800';
}
};
return (
<Card className="max-w-4xl mx-auto">
<CardHeader
title="Curvas de Engel"
subtitle={`Ejercicio ${ejercicioActual + 1} de ${ejercicios.length}`}
/>
<div className="mb-4">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${((ejercicioActual + 1) / ejercicios.length) * 100}%` }}
/>
</div>
</div>
<div className="space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-bold text-lg text-blue-900">{ejercicio.titulo}</h3>
<p className="text-gray-700 mt-2">{ejercicio.descripcion}</p>
<p className="text-sm text-gray-600 mt-1">
<strong>Bien analizado:</strong> {ejercicio.bien}
</p>
</div>
<div className="bg-white border rounded-lg overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">Ingreso Mensual ($)</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">Cantidad Consumida</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">Elasticidad del Intervalo</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{ejercicio.puntos.map((punto, index) => (
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className="px-4 py-3 font-mono">${punto.ingreso.toLocaleString()}</td>
<td className="px-4 py-3 font-mono">{punto.cantidad}</td>
<td className="px-4 py-3">
{index < ejercicio.puntos.length - 1 ? (
<div className="flex items-center gap-2">
<Input
type="number"
step="0.01"
className="w-24 text-sm"
placeholder="Ei"
value={calculosElasticidad[`${ejercicio.id}_${index}`] || ''}
onChange={(e) => handleCalculoChange(index, e.target.value)}
/>
{mostrarResultados && (
<span className="text-sm font-mono text-gray-600">
= {calcularElasticidadIntervalo(punto, ejercicio.puntos[index + 1]).toFixed(2)}
</span>
)}
</div>
) : (
<span className="text-gray-400 text-sm">-</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<p className="font-medium text-gray-700 mb-3">
Según el comportamiento de la curva, ¿qué tipo de bien es "{ejercicio.bien}"?
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{['lujo', 'necesario', 'inferior'].map((tipo) => (
<button
key={tipo}
onClick={() => seleccionarTipo(tipo)}
disabled={mostrarResultados}
className={`p-3 rounded-lg border-2 text-center transition-all ${
respuestaActual?.tipo === tipo
? getTipoColor(tipo)
: 'bg-white border-gray-200 hover:border-gray-300'
} ${mostrarResultados ? 'cursor-default' : 'cursor-pointer'}`}
>
<p className="font-semibold capitalize">
Bien {tipo === 'lujo' ? 'de Lujo' : tipo}
</p>
<p className="text-xs mt-1 opacity-80">
{tipo === 'lujo' && 'Ei > 1'}
{tipo === 'necesario' && '0 < Ei < 1'}
{tipo === 'inferior' && 'Ei < 0'}
</p>
</button>
))}
</div>
{!mostrarResultados ? (
<Button
onClick={verificar}
disabled={!respuestaActual?.tipo}
className="mt-4"
>
Verificar Respuesta
</Button>
) : (
<div className={`mt-4 p-4 rounded-lg ${
respuestaActual?.esCorrecta
? 'bg-green-100 border border-green-300'
: 'bg-red-100 border border-red-300'
}`}>
<p className="font-semibold">
{respuestaActual?.esCorrecta
? '¡Correcto!'
: 'Incorrecto. El tipo de bien es: '}
</p>
{!respuestaActual?.esCorrecta && (
<p className={`mt-2 inline-block px-3 py-1 rounded ${getTipoColor(ejercicio.tipoBien)}`}>
Bien {ejercicio.tipoBien === 'lujo' ? 'de Lujo' : ejercicio.tipoBien}
</p>
)}
<div className="mt-3 text-sm text-gray-600">
<p className="font-medium">Análisis:</p>
<ul className="list-disc list-inside mt-1 space-y-1">
{ejercicio.puntos.slice(0, -1).map((punto, idx) => {
const ei = calcularElasticidadIntervalo(punto, ejercicio.puntos[idx + 1]);
return (
<li key={idx}>
Intervalo ${punto.ingreso}-${ejercicio.puntos[idx + 1].ingreso}:
E<sub>i</sub> = {ei.toFixed(2)}
</li>
);
})}
</ul>
</div>
</div>
)}
</div>
<div className="flex justify-between pt-4 border-t">
{ejercicioActual > 0 && (
<Button
variant="outline"
onClick={() => {
setEjercicioActual(prev => prev - 1);
setMostrarResultados(false);
}}
>
Anterior
</Button>
)}
<div className="flex-1" />
{ejercicioActual < ejercicios.length - 1 ? (
<Button
onClick={siguienteEjercicio}
disabled={!mostrarResultados || !respuestaActual?.esCorrecta}
>
Siguiente Ejercicio
</Button>
) : (
<Button variant="outline" onClick={reiniciar}>
Reiniciar Ejercicios
</Button>
)}
</div>
</div>
</Card>
);
}
export default CurvaEngel;

View File

@@ -0,0 +1,336 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Card, CardHeader } from '../../ui/Card';
interface CurvasIndiferenciaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Punto {
x: number;
y: number;
utilidad: number;
}
const curvas: Punto[][] = [
// Curva U=10: 2x + 3y = 10
[
{ x: 0, y: 3.33, utilidad: 10 },
{ x: 2, y: 2, utilidad: 10 },
{ x: 5, y: 0, utilidad: 10 },
],
// Curva U=20: 2x + 3y = 20
[
{ x: 1, y: 6, utilidad: 20 },
{ x: 4, y: 4, utilidad: 20 },
{ x: 7, y: 2, utilidad: 20 },
{ x: 10, y: 0, utilidad: 20 },
],
// Curva U=30: 2x + 3y = 30
[
{ x: 0, y: 10, utilidad: 30 },
{ x: 3, y: 8, utilidad: 30 },
{ x: 6, y: 6, utilidad: 30 },
{ x: 9, y: 4, utilidad: 30 },
{ x: 12, y: 2, utilidad: 30 },
{ x: 15, y: 0, utilidad: 30 },
],
];
const puntosEjemplo = [
{ x: 2, y: 2, label: 'A', utilidad: 10 },
{ x: 4, y: 4, label: 'B', utilidad: 20 },
{ x: 6, y: 6, label: 'C', utilidad: 30 },
{ x: 3, y: 5, label: 'D', utilidad: 21 },
{ x: 8, y: 2, label: 'E', utilidad: 22 },
];
export function CurvasIndiferencia({ ejercicioId: _ejercicioId, onComplete }: CurvasIndiferenciaProps) {
const [puntoSeleccionado, setPuntoSeleccionado] = useState<string | null>(null);
const [mostrarPropiedades, setMostrarPropiedades] = useState(true);
const [preguntaRespuesta, setPreguntaRespuesta] = useState<Record<string, string>>({});
const [verificado, setVerificado] = useState(false);
const handleSeleccionPunto = (label: string) => {
setPuntoSeleccionado(label);
setVerificado(false);
};
const verificarRespuesta = (pregunta: string, respuestaCorrecta: string) => {
const esCorrecta = preguntaRespuesta[pregunta] === respuestaCorrecta;
setVerificado(true);
if (esCorrecta && onComplete) {
onComplete(100);
}
return esCorrecta;
};
return (
<Card className="max-w-4xl mx-auto">
<CardHeader
title="Curvas de Indiferencia"
subtitle="Mapa de preferencias que muestra combinaciones de bienes que proporcionan la misma utilidad"
/>
<div className="space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-bold text-blue-900 mb-2">Definición</h3>
<p className="text-sm text-blue-800">
Una <strong>curva de indiferencia</strong> muestra todas las combinaciones de dos bienes
que proporcionan al consumidor el mismo nivel de utilidad o satisfacción.
El consumidor es "indiferente" entre cualquiera de estas combinaciones.
</p>
</div>
<div className="border rounded-lg p-4">
<h4 className="font-bold mb-3">Mapa de Curvas de Indiferencia</h4>
<div className="bg-white border rounded p-4">
<div className="relative h-80">
<svg viewBox="0 0 500 300" className="w-full h-full">
<line x1="50" y1="280" x2="480" y2="280" stroke="#333" strokeWidth="2" />
<line x1="50" y1="280" x2="50" y2="20" stroke="#333" strokeWidth="2" />
<text x="265" y="298" textAnchor="middle" className="text-sm fill-gray-700 font-bold">Bien X (Unidades)</text>
<text x="15" y="150" textAnchor="middle" className="text-sm fill-gray-700 font-bold" transform="rotate(-90, 15, 150)">Bien Y (Unidades)</text>
{[0, 3, 6, 9, 12, 15].map((val) => (
<g key={`x-${val}`}>
<line x1={50 + val * 26} y1="280" x2={50 + val * 26} y2="285" stroke="#333" />
<text x={50 + val * 26} y="298" textAnchor="middle" className="text-xs fill-gray-600">{val}</text>
</g>
))}
{[0, 2, 4, 6, 8, 10].map((val) => (
<g key={`y-${val}`}>
<line x1="45" y1={280 - val * 24} x2="50" y2={280 - val * 24} stroke="#333" />
<text x="40" y={284 - val * 24} textAnchor="end" className="text-xs fill-gray-600">{val}</text>
</g>
))}
{curvas.map((curva, idx) => (
<g key={idx}>
<polyline
points={curva.map(p => `${50 + p.x * 26},${280 - p.y * 24}`).join(' ')}
fill="none"
stroke={['#3b82f6', '#10b981', '#f59e0b'][idx]}
strokeWidth="2"
/>
<text
x={50 + curva[Math.floor(curva.length / 2)].x * 26 + 10}
y={280 - curva[Math.floor(curva.length / 2)].y * 24}
className="text-xs fill-gray-500"
>
U={curva[0].utilidad}
</text>
</g>
))}
{puntosEjemplo.map((punto) => (
<g key={punto.label}>
<circle
cx={50 + punto.x * 26}
cy={280 - punto.y * 24}
r={puntoSeleccionado === punto.label ? 8 : 6}
fill={puntoSeleccionado === punto.label ? '#ef4444' : '#6366f1'}
className="cursor-pointer"
onClick={() => handleSeleccionPunto(punto.label)}
/>
<text
x={50 + punto.x * 26 + 12}
y={280 - punto.y * 24 - 5}
className="text-sm font-bold fill-gray-700"
>
{punto.label}
</text>
</g>
))}
</svg>
</div>
<p className="text-sm text-gray-600 mt-2">
Haz clic en los puntos (A, B, C, D, E) para ver sus características.
Observa cómo las curvas más alejadas del origen representan mayor utilidad.
</p>
</div>
{puntoSeleccionado && (
<div className="mt-4 bg-blue-50 p-4 rounded-lg">
<h5 className="font-bold text-blue-900 mb-2">
Punto {puntoSeleccionado}
</h5>
{(() => {
const punto = puntosEjemplo.find(p => p.label === puntoSeleccionado);
if (!punto) return null;
return (
<div className="space-y-1 text-sm text-blue-800">
<p><strong>Bien X:</strong> {punto.x} unidades</p>
<p><strong>Bien Y:</strong> {punto.y} unidades</p>
<p><strong>Utilidad:</strong> {punto.utilidad} utils</p>
<p className="text-xs mt-2">
Este punto se encuentra en la curva de indiferencia U={punto.utilidad}.
Cualquier otro punto en esta misma curva proporciona exactamente la misma satisfacción.
</p>
</div>
);
})()}
</div>
)}
</div>
<div className="bg-yellow-50 border border-yellow-300 p-4 rounded-lg">
<h4 className="font-bold text-yellow-900 mb-3">Propiedades de las Curvas de Indiferencia</h4>
<div className="space-y-3">
<div className="flex gap-3">
<div className="bg-white p-2 rounded w-8 h-8 flex items-center justify-center font-bold text-yellow-800 flex-shrink-0">1</div>
<div>
<p className="font-semibold text-sm">No se cortan</p>
<p className="text-xs text-yellow-800">Dos curvas de indiferencia nunca pueden intersectarse. Si lo hicieran, implicaría que una misma combinación tiene dos niveles de utilidad diferentes.</p>
</div>
</div>
<div className="flex gap-3">
<div className="bg-white p-2 rounded w-8 h-8 flex items-center justify-center font-bold text-yellow-800 flex-shrink-0">2</div>
<div>
<p className="font-semibold text-sm">Tienen pendiente negativa</p>
<p className="text-xs text-yellow-800">Para mantener el mismo nivel de utilidad, si consumes más de un bien debes consumir menos del otro (sustitución).</p>
</div>
</div>
<div className="flex gap-3">
<div className="bg-white p-2 rounded w-8 h-8 flex items-center justify-center font-bold text-yellow-800 flex-shrink-0">3</div>
<div>
<p className="font-semibold text-sm">Son convexas al origen</p>
<p className="text-xs text-yellow-800">La TMS (Tasa Marginal de Sustitución) disminuye a medida que te mueves hacia abajo a lo largo de la curva.</p>
</div>
</div>
<div className="flex gap-3">
<div className="bg-white p-2 rounded w-8 h-8 flex items-center justify-center font-bold text-yellow-800 flex-shrink-0">4</div>
<div>
<p className="font-semibold text-sm">Curvas más alejadas = Mayor utilidad</p>
<p className="text-xs text-yellow-800">Las curvas más alejadas del origen representan niveles de utilidad más altos (U=30 {'>'} U=20 {'>'} U=10).</p>
</div>
</div>
</div>
</div>
<div className="border rounded-lg p-4">
<h4 className="font-bold mb-3">Tasa Marginal de Sustitución (TMS)</h4>
<p className="text-sm text-gray-600 mb-3">
La TMS mide cuántas unidades de Y estás dispuesto a sacrificar por una unidad adicional de X,
manteniendo constante la utilidad.
</p>
<div className="bg-gray-50 p-3 rounded font-mono text-center text-sm mb-3">
TMS = -ΔY/ΔX = UMgX / UMgY
</div>
<div className="bg-white border rounded p-3">
<p className="font-semibold text-sm mb-2">Ejemplo en el punto A (2,2):</p>
<div className="space-y-1 text-sm">
<p> Para aumentar X de 2 a 4 (ΔX = +2), debes reducir Y de 2 a... ¿cuánto?</p>
<p> En U=10: Si X=4, entonces 2(4) + 3Y = 10 Y 0.67</p>
<p> TMS = -(0.67 - 2)/(4 - 2) = -(-1.33)/2 = 0.67</p>
<p className="text-xs text-gray-600 mt-2">
Estás dispuesto a dar up aproximadamente 0.67 unidades de Y por cada unidad adicional de X.
</p> </div> </div> </div>
<div className="border-t pt-4">
<h4 className="font-bold mb-4">Ejercicios de Comprensión</h4>
<div className="space-y-4">
<div className="border rounded-lg p-4">
<p className="font-medium mb-3">1. ¿Qué significa que dos puntos estén en la misma curva de indiferencia?</p>
<div className="space-y-2">
{[
{ id: '1a', texto: 'Tienen los mismos precios', correcta: false },
{ id: '1b', texto: 'Proporcionan la misma utilidad', correcta: true },
{ id: '1c', texto: 'Son igualmente caros', correcta: false },
{ id: '1d', texto: 'Son bienes sustitutos perfectos', correcta: false },
].map((opcion) => (
<label key={opcion.id} className="flex items-center gap-2 p-2 hover:bg-gray-50 rounded cursor-pointer">
<input
type="radio"
name="p1"
onChange={() => setPreguntaRespuesta(prev => ({ ...prev, p1: opcion.id }))}
className="text-primary"
/>
<span className="text-sm">{opcion.texto}</span>
</label>
))}
</div> </div>
<div className="border rounded-lg p-4">
<p className="font-medium mb-3">2. Según el mapa de curvas, ¿qué punto tiene mayor utilidad?</p>
<div className="space-y-2">
{[
{ id: '2a', texto: 'Punto A (2,2)', correcta: false },
{ id: '2b', texto: 'Punto B (4,4)', correcta: false },
{ id: '2c', texto: 'Punto C (6,6)', correcta: true },
{ id: '2d', texto: 'Punto D (3,5)', correcta: false },
].map((opcion) => (
<label key={opcion.id} className="flex items-center gap-2 p-2 hover:bg-gray-50 rounded cursor-pointer">
<input
type="radio"
name="p2"
onChange={() => setPreguntaRespuesta(prev => ({ ...prev, p2: opcion.id }))}
className="text-primary"
/>
<span className="text-sm">{opcion.texto}</span>
</label>
))}
</div> </div>
<div className="border rounded-lg p-4">
<p className="font-medium mb-3">3. ¿Por qué las curvas de indiferencia tienen pendiente negativa?</p>
<div className="space-y-2">
{[
{ id: '3a', texto: 'Porque los bienes son complementarios', correcta: false },
{ id: '3b', texto: 'Para mantener la utilidad constante, más de X implica menos de Y', correcta: true },
{ id: '3c', texto: 'Porque los precios son inversos', correcta: false },
{ id: '3d', texto: 'Porque la utilidad marginal es negativa', correcta: false },
].map((opcion) => (
<label key={opcion.id} className="flex items-center gap-2 p-2 hover:bg-gray-50 rounded cursor-pointer">
<input
type="radio"
name="p3"
onChange={() => setPreguntaRespuesta(prev => ({ ...prev, p3: opcion.id }))}
className="text-primary"
/>
<span className="text-sm">{opcion.texto}</span>
</label>
))}
</div> </div>
</div>
<div className="mt-4">
<Button
onClick={() => {
const correctas = [
preguntaRespuesta.p1 === '1b',
preguntaRespuesta.p2 === '2c',
preguntaRespuesta.p3 === '3b'
].filter(Boolean).length;
setVerificado(true);
if (correctas === 3 && onComplete) {
onComplete(100);
}
}}
disabled={!preguntaRespuesta.p1 || !preguntaRespuesta.p2 || !preguntaRespuesta.p3}
>
Verificar Respuestas
</Button> </div>
{verificado && (
<div className="mt-4 p-3 bg-green-100 border border-green-300 rounded-lg">
<p className="text-green-800">
¡Respuestas verificadas! Revisa cuáles fueron correctas.
</p> </div>
)}
</div>
</div>
</Card>
);
}
export default CurvasIndiferencia;

View File

@@ -0,0 +1,358 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { DollarSign, TrendingUp, TrendingDown, CheckCircle, RotateCcw } from 'lucide-react';
interface DecisionesPreciosProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Escenario {
id: number;
producto: string;
ep: number;
situacion: string;
pregunta: string;
opciones: {
respuesta: 'subir' | 'bajar' | 'mantener';
label: string;
explicacionCorrecta: string;
explicacionIncorrecta: string;
}[];
}
const escenarios: Escenario[] = [
{
id: 1,
producto: 'Medicamentos esenciales',
ep: -0.3,
situacion: 'Tu farmacia vende medicamentos esenciales con elasticidad de -0.3. Las ventas han disminuido y necesitas aumentar tus ingresos.',
pregunta: '¿Qué decisión de precios deberías tomar?',
opciones: [
{
respuesta: 'subir',
label: 'Subir el precio',
explicacionCorrecta: 'Correcto. Con Ep = -0.3 (inelástico), al subir el precio la cantidad cae menos que proporcionalmente, aumentando los ingresos totales.',
explicacionIncorrecta: '',
},
{
respuesta: 'bajar',
label: 'Bajar el precio',
explicacionCorrecta: '',
explicacionIncorrecta: 'Incorrecto. Con demanda inelástica, bajar el precio aumenta la cantidad menos que proporcionalmente, reduciendo los ingresos.',
},
{
respuesta: 'mantener',
label: 'Mantener el precio',
explicacionCorrecta: '',
explicacionIncorrecta: 'Incorrecto. Con demanda inelástica, subir precios aumentaría los ingresos totales.',
},
],
},
{
id: 2,
producto: 'Restaurante de lujo',
ep: -3.5,
situacion: 'Tu restaurante de alta cocina tiene una elasticidad de -3.5. La competencia está fuerte y necesitas atraer más clientes.',
pregunta: '¿Qué estrategia de precios recomiendas?',
opciones: [
{
respuesta: 'subir',
label: 'Subir el precio',
explicacionCorrecta: '',
explicacionIncorrecta: 'Incorrecto. Con Ep = -3.5 (muy elástico), subir precios haría que muchos clientes dejen de venir, reduciendo ingresos drásticamente.',
},
{
respuesta: 'bajar',
label: 'Bajar el precio',
explicacionCorrecta: 'Correcto. Con Ep = -3.5 (elástico), bajar el precio aumenta la cantidad más que proporcionalmente, incrementando los ingresos totales.',
explicacionIncorrecta: '',
},
{
respuesta: 'mantener',
label: 'Mantener el precio',
explicacionCorrecta: '',
explicacionIncorrecta: 'Incorrecto. Con demanda elástica y competencia fuerte, bajar precios atraería más clientes y aumentaría ingresos.',
},
],
},
{
id: 3,
producto: 'Gasolina',
ep: -0.8,
situacion: 'Tu gasolinera tiene una elasticidad de -0.8. Los costos han subido y necesitas cubrirlos.',
pregunta: '¿Deberías subir los precios de la gasolina?',
opciones: [
{
respuesta: 'subir',
label: 'Sí, subir el precio',
explicacionCorrecta: 'Correcto. Con Ep = -0.8 (inelástico), subir el precio aumenta los ingresos totales porque la cantidad demandada cae menos que proporcionalmente.',
explicacionIncorrecta: '',
},
{
respuesta: 'bajar',
label: 'No, bajar el precio',
explicacionCorrecta: '',
explicacionIncorrecta: 'Incorrecto. Bajar precios con demanda inelástica reduciría los ingresos totales, no ayudando a cubrir los costos mayores.',
},
{
respuesta: 'mantener',
label: 'Mantener igual',
explicacionCorrecta: '',
explicacionIncorrecta: 'Incorrecto. Manteniendo precios no cubrirías los mayores costos. Subir precios aumentaría ingresos con demanda inelástica.',
},
],
},
{
id: 4,
producto: 'Cine (entradas)',
ep: -1.8,
situacion: 'Tu cine tiene una elasticidad de -1.8. Es temporada baja y quieres llenar las salas.',
pregunta: '¿Qué decisión de precios tomarías?',
opciones: [
{
respuesta: 'subir',
label: 'Subir el precio',
explicacionCorrecta: '',
explicacionIncorrecta: 'Incorrecto. Con Ep = -1.8 (elástico), subir precios reduciría significativamente la asistencia y los ingresos.',
},
{
respuesta: 'bajar',
label: 'Bajar el precio',
explicacionCorrecta: 'Correcto. Con Ep = -1.8 (elástico), bajar precios aumentaría la asistencia más que proporcionalmente, llenando las salas y aumentando ingresos.',
explicacionIncorrecta: '',
},
{
respuesta: 'mantener',
label: 'Mantener el precio',
explicacionCorrecta: '',
explicacionIncorrecta: 'Incorrecto. Mantener precios no ayudaría a llenar las salas en temporada baja. Bajar precios sería más efectivo.',
},
],
},
];
export function DecisionesPrecios({ ejercicioId: _ejercicioId, onComplete }: DecisionesPreciosProps) {
const [escenarioIndex, setEscenarioIndex] = useState(0);
const [respuesta, setRespuesta] = useState<'subir' | 'bajar' | 'mantener' | null>(null);
const [validado, setValidado] = useState(false);
const [aciertos, setAciertos] = useState(0);
const [completado, setCompletado] = useState(false);
const escenario = escenarios[escenarioIndex];
const respuestaCorrecta = escenario.opciones.find((o) => o.explicacionCorrecta)?.respuesta;
const validarRespuesta = () => {
if (!respuesta) return;
const esCorrecto = respuesta === respuestaCorrecta;
setValidado(true);
if (esCorrecto) {
setAciertos((prev) => prev + 1);
}
};
const siguienteEscenario = () => {
if (escenarioIndex < escenarios.length - 1) {
setEscenarioIndex((prev) => prev + 1);
setRespuesta(null);
setValidado(false);
} else {
setCompletado(true);
if (onComplete) {
onComplete(Math.round((aciertos + (respuesta === respuestaCorrecta ? 1 : 0)) / escenarios.length * 100));
}
}
};
const reiniciar = () => {
setEscenarioIndex(0);
setRespuesta(null);
setValidado(false);
setAciertos(0);
setCompletado(false);
};
const obtenerClasificacion = (ep: number): string => {
const valorAbs = Math.abs(ep);
if (valorAbs > 1) return 'Elástica';
if (valorAbs < 1) return 'Inelástica';
return 'Unitaria';
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Decisiones de Precios según Elasticidad"
subtitle="Aprende a tomar decisiones estratégicas basadas en la elasticidad"
/>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-green-50 border-2 border-green-200 rounded-lg p-4">
<h4 className="font-bold text-green-800 mb-2 flex items-center gap-2">
<TrendingUp className="w-5 h-5" />
Demanda ELÁSTICA (|Ep| &gt; 1)
</h4>
<ul className="text-sm text-green-700 space-y-1">
<li> Bajar precio Aumentan ingresos</li>
<li> Subir precio Disminuyen ingresos</li>
<li> Los consumidores son muy sensibles</li>
</ul>
</div>
<div className="bg-blue-50 border-2 border-blue-200 rounded-lg p-4">
<h4 className="font-bold text-blue-800 mb-2 flex items-center gap-2">
<TrendingDown className="w-5 h-5" />
Demanda INELÁSTICA (|Ep| &lt; 1)
</h4>
<ul className="text-sm text-blue-700 space-y-1">
<li> Subir precio Aumentan ingresos</li>
<li> Bajar precio Disminuyen ingresos</li>
<li> Los consumidores son poco sensibles</li>
</ul>
</div>
</div>
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-gray-700 flex items-center gap-2">
<DollarSign className="w-4 h-4 text-primary" />
Caso {escenarioIndex + 1} de {escenarios.length}: {escenario.producto}
</h4>
<span className="bg-primary text-white text-xs font-bold px-2 py-1 rounded">
Ep = {escenario.ep}
</span>
</div>
<div className="bg-gray-50 rounded-lg p-4 mb-4">
<p className="text-gray-700 mb-2">
<strong>Situación:</strong> {escenario.situacion}
</p>
<p className="text-gray-800 font-medium">{escenario.pregunta}</p>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-3 mb-4">
<p className="text-sm text-purple-800">
<strong>Análisis:</strong> Este producto tiene demanda{' '}
<span className="font-bold">{obtenerClasificacion(escenario.ep).toUpperCase()}</span>{' '}
(|Ep| = {Math.abs(escenario.ep).toFixed(1)})
</p>
</div>
<div className="space-y-3">
{escenario.opciones.map((opcion) => (
<button
key={opcion.respuesta}
onClick={() => {
setRespuesta(opcion.respuesta);
setValidado(false);
}}
disabled={validado}
className={`w-full p-4 rounded-lg border-2 text-left transition-all ${
respuesta === opcion.respuesta
? 'border-primary bg-primary/5'
: 'border-gray-200 hover:border-gray-300'
} ${
validado && opcion.respuesta === respuestaCorrecta
? 'border-success bg-success/10'
: ''
} ${
validado && respuesta === opcion.respuesta && opcion.respuesta !== respuestaCorrecta
? 'border-error bg-error/10'
: ''
}`}
>
<div className="flex items-center justify-between">
<span className="font-medium">{opcion.label}</span>
{validado && opcion.respuesta === respuestaCorrecta && (
<CheckCircle className="w-5 h-5 text-success" />
)}
</div>
</button>
))}
</div>
</div>
{validado && (
<div className="mb-6">
{escenario.opciones.map(
(opcion) =>
(respuesta === opcion.respuesta || opcion.respuesta === respuestaCorrecta) && (
<div
key={`feedback-${opcion.respuesta}`}
className={`p-4 rounded-lg mb-2 ${
opcion.respuesta === respuestaCorrecta
? 'bg-success/10 border border-success'
: 'bg-error/10 border border-error'
}`}
>
<p
className={`font-medium ${
opcion.respuesta === respuestaCorrecta ? 'text-success' : 'text-error'
}`}
>
{opcion.explicacionCorrecta || opcion.explicacionIncorrecta}
</p>
</div>
)
)}
</div>
)}
<div className="flex gap-3">
{!validado ? (
<Button onClick={validarRespuesta} variant="primary" disabled={!respuesta}>
<CheckCircle className="w-4 h-4 mr-2" />
Validar Decisión
</Button>
) : !completado ? (
<Button onClick={siguienteEscenario} variant="primary">
Siguiente Caso
</Button>
) : (
<Button onClick={reiniciar} variant="primary">
<RotateCcw className="w-4 h-4 mr-2" />
Reiniciar
</Button>
)}
</div>
{completado && (
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
<p className="text-success font-medium text-center">
¡Ejercicio completado! Has acertado {aciertos + (respuesta === respuestaCorrecta ? 1 : 0)} de{' '}
{escenarios.length} casos
</p>
</div>
)}
</Card>
<Card className="bg-yellow-50 border-yellow-200">
<h4 className="font-semibold text-yellow-900 mb-2">Regla de Oro para Decisiones de Precios:</h4>
<div className="space-y-2 text-sm text-yellow-800">
<p>
<strong>Ingreso Total (IT)</strong> = Precio × Cantidad
</p>
<ul className="space-y-1 ml-4">
<li>
Si |Ep| &gt; 1 (Elástica):{' '}
<span className="text-green-700 font-medium">IT y P se mueven en direcciones opuestas</span>
</li>
<li>
Si |Ep| &lt; 1 (Inelástica):{' '}
<span className="text-green-700 font-medium">IT y P se mueven en la misma dirección</span>
</li>
<li>
Si |Ep| = 1 (Unitaria):{' '}
<span className="text-green-700 font-medium">IT es máximo, cambios en P no afectan IT</span>
</li>
</ul>
</div>
</Card>
</div>
);
}
export default DecisionesPrecios;

View File

@@ -0,0 +1,369 @@
import { useState, useCallback, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { MousePointer, RotateCcw, Info } from 'lucide-react';
interface ElasticidadCurvaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Punto {
q: number;
p: number;
ep: number;
}
export function ElasticidadCurva({ ejercicioId: _ejercicioId, onComplete }: ElasticidadCurvaProps) {
const [puntoSeleccionado, setPuntoSeleccionado] = useState<number>(50);
const [validado, setValidado] = useState(false);
const [completado, setCompletado] = useState(false);
// Curva de demanda lineal: P = 20 - 0.2Q
const generarPuntos = useCallback((): Punto[] => {
const puntos: Punto[] = [];
for (let q = 0; q <= 100; q += 5) {
const p = 20 - 0.2 * q;
// Elasticidad en curva lineal: Ep = -b * (P/Q) donde b es la pendiente
const ep = -0.2 * (p / q);
puntos.push({ q, p: Math.max(0, p), ep: q > 0 ? ep : 0 });
}
return puntos;
}, []);
const puntos = useMemo(() => generarPuntos(), [generarPuntos]);
const puntoActual = useMemo(() => {
const q = puntoSeleccionado;
const p = 20 - 0.2 * q;
const ep = q > 0 ? -0.2 * (p / q) : 0;
return { q, p: Math.max(0, p), ep };
}, [puntoSeleccionado]);
const obtenerClasificacion = useCallback((ep: number): string => {
const valorAbs = Math.abs(ep);
if (valorAbs > 1) return 'Elástica';
if (valorAbs < 1) return 'Inelástica';
return 'Unitaria';
}, []);
const validar = () => {
setValidado(true);
setCompletado(true);
if (onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setPuntoSeleccionado(50);
setValidado(false);
setCompletado(false);
};
// SVG config
const svgWidth = 400;
const svgHeight = 300;
const margin = { top: 20, right: 30, bottom: 50, left: 60 };
const chartWidth = svgWidth - margin.left - margin.right;
const chartHeight = svgHeight - margin.top - margin.bottom;
const scaleX = (q: number) => margin.left + (q / 100) * chartWidth;
const scaleY = (p: number) => margin.top + chartHeight - (p / 20) * chartHeight;
// Puntos para la curva
const pathData = puntos
.filter((p) => p.p >= 0)
.map((p, i) => `${i === 0 ? 'M' : 'L'} ${scaleX(p.q)},${scaleY(p.p)}`)
.join(' ');
// Punto unitario (donde Ep = -1)
const qUnitaria = 50;
const pUnitaria = 10;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Elasticidad en Diferentes Puntos de la Curva"
subtitle="Explora cómo cambia la elasticidad a lo largo de una curva de demanda lineal"
/>
<div className="bg-yellow-50 border-l-4 border-yellow-500 p-4 mb-6">
<p className="text-yellow-900 font-medium flex items-center gap-2">
<Info className="w-4 h-4" />
Concepto Clave:
</p>
<p className="text-yellow-800 mt-1">
En una curva de demanda <strong>lineal</strong>, la elasticidad NO es constante.
Va desde elástica (parte alta) a inelástica (parte baja), pasando por unitaria en el punto medio.
</p>
</div>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2 flex items-center gap-2">
<MousePointer className="w-4 h-4" />
Selecciona un punto en la curva:
</label>
<input
type="range"
min="5"
max="95"
value={puntoSeleccionado}
onChange={(e) => {
setPuntoSeleccionado(Number(e.target.value));
setValidado(false);
}}
className="w-full h-2 bg-gray-200 rounded-lg cursor-pointer"
style={{ accentColor: '#2563eb' }}
/>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>Q = 5</span>
<span>Q = 50</span>
<span>Q = 95</span>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4 mb-6">
<h4 className="font-medium text-gray-700 mb-3">Valores en el punto seleccionado:</h4>
<div className="grid grid-cols-3 gap-4">
<div className="text-center p-3 bg-white rounded-lg shadow-sm">
<p className="text-xs text-gray-500">Cantidad (Q)</p>
<p className="text-xl font-bold text-primary">{puntoActual.q.toFixed(0)}</p>
</div>
<div className="text-center p-3 bg-white rounded-lg shadow-sm">
<p className="text-xs text-gray-500">Precio (P)</p>
<p className="text-xl font-bold text-secondary">${puntoActual.p.toFixed(1)}</p>
</div>
<div className="text-center p-3 bg-white rounded-lg shadow-sm">
<p className="text-xs text-gray-500">Elasticidad (Ep)</p>
<p className="text-xl font-bold text-purple-600">{puntoActual.ep.toFixed(2)}</p>
</div>
</div>
</div>
<div className="bg-white border-2 border-gray-200 rounded-lg p-4 mb-6">
<h4 className="font-medium text-gray-700 mb-3 text-center">Gráfico de Demanda</h4>
<svg viewBox={`0 0 ${svgWidth} ${svgHeight}`} className="w-full" style={{ maxHeight: '350px' }}>
{/* Grid */}
<defs>
<pattern id="grid3" width="40" height="30" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 30" fill="none" stroke="#e5e7eb" strokeWidth="0.5" />
</pattern>
</defs>
<rect x={margin.left} y={margin.top} width={chartWidth} height={chartHeight} fill="url(#grid3)" />
{/* Ejes */}
<line
x1={margin.left}
y1={margin.top + chartHeight}
x2={margin.left + chartWidth}
y2={margin.top + chartHeight}
stroke="#374151"
strokeWidth="2"
/>
<line
x1={margin.left}
y1={margin.top}
x2={margin.left}
y2={margin.top + chartHeight}
stroke="#374151"
strokeWidth="2"
/>
{/* Flechas */}
<polygon
points={`${margin.left + chartWidth},${margin.top + chartHeight} ${margin.left + chartWidth - 5},${margin.top + chartHeight - 3} ${margin.left + chartWidth - 5},${margin.top + chartHeight + 3}`}
fill="#374151"
/>
<polygon
points={`${margin.left},${margin.top} ${margin.left - 3},${margin.top + 5} ${margin.left + 3},${margin.top + 5}`}
fill="#374151"
/>
{/* Etiquetas */}
<text
x={margin.left + chartWidth / 2}
y={svgHeight - 10}
textAnchor="middle"
fill="#6b7280"
fontSize="12"
>
Cantidad (Q)
</text>
<text
x={15}
y={margin.top + chartHeight / 2}
textAnchor="middle"
fill="#6b7280"
fontSize="12"
transform={`rotate(-90, 15, ${margin.top + chartHeight / 2})`}
>
Precio (P)
</text>
{/* Marcas X */}
{[0, 25, 50, 75, 100].map((val, i) => (
<g key={`x-${val}`}>
<line
x1={scaleX(val)}
y1={margin.top + chartHeight}
x2={scaleX(val)}
y2={margin.top + chartHeight + 5}
stroke="#374151"
/>
<text
x={scaleX(val)}
y={margin.top + chartHeight + 18}
textAnchor="middle"
fill="#6b7280"
fontSize="10"
>
{val}
</text>
</g>
))}
{/* Marcas Y */}
{[0, 5, 10, 15, 20].map((val, i) => (
<g key={`y-${val}`}>
<line
x1={margin.left - 5}
y1={scaleY(val)}
x2={margin.left}
y2={scaleY(val)}
stroke="#374151"
/>
<text
x={margin.left - 8}
y={scaleY(val) + 3}
textAnchor="end"
fill="#6b7280"
fontSize="10"
>
${val}
</text>
</g>
))}
{/* Curva de demanda */}
<path d={pathData} fill="none" stroke="#2563eb" strokeWidth="3" />
{/* Punto unitario marcado */}
<circle cx={scaleX(qUnitaria)} cy={scaleY(pUnitaria)} r="6" fill="#fbbf24" stroke="#f59e0b" strokeWidth="2" />
<text x={scaleX(qUnitaria)} y={scaleY(pUnitaria) - 12} textAnchor="middle" fill="#f59e0b" fontSize="10" fontWeight="bold">
Unitario
</text>
{/* Punto seleccionado */}
<circle
cx={scaleX(puntoActual.q)}
cy={scaleY(puntoActual.p)}
r="8"
fill={Math.abs(puntoActual.ep) > 1 ? '#10b981' : Math.abs(puntoActual.ep) < 1 ? '#3b82f6' : '#fbbf24'}
stroke="white"
strokeWidth="3"
/>
{/* Líneas punteadas al punto */}
<line
x1={scaleX(puntoActual.q)}
y1={scaleY(puntoActual.p)}
x2={scaleX(puntoActual.q)}
y2={margin.top + chartHeight}
stroke="#6b7280"
strokeWidth="1"
strokeDasharray="4"
/>
<line
x1={scaleX(puntoActual.q)}
y1={scaleY(puntoActual.p)}
x2={margin.left}
y2={scaleY(puntoActual.p)}
stroke="#6b7280"
strokeWidth="1"
strokeDasharray="4"
/>
{/* Leyenda */}
<g transform={`translate(${margin.left + chartWidth - 120}, ${margin.top + 20})`}>
<rect x="0" y="0" width="110" height="80" fill="white" stroke="#e5e7eb" strokeWidth="1" rx="4" />
<circle cx="15" cy="20" r="5" fill="#10b981" />
<text x="25" y="24" fontSize="10" fill="#374151">
Elástica (|Ep|&gt;1)
</text>
<circle cx="15" cy="40" r="5" fill="#fbbf24" />
<text x="25" y="44" fontSize="10" fill="#374151">
Unitaria (|Ep|=1)
</text>
<circle cx="15" cy="60" r="5" fill="#3b82f6" />
<text x="25" y="64" fontSize="10" fill="#374151">
Inelástica (|Ep|&lt;1)
</text>
</g>
</svg>
</div>
<div className="bg-gray-50 rounded-lg p-4 mb-6">
<h4 className="font-medium text-gray-700 mb-2">Clasificación del punto actual:</h4>
<p className="text-lg">
En Q = {puntoActual.q.toFixed(0)}, P = ${puntoActual.p.toFixed(1)}:
</p>
<p className="text-2xl font-bold mt-2">
Demanda{' '}
<span
className={
Math.abs(puntoActual.ep) > 1
? 'text-green-600'
: Math.abs(puntoActual.ep) < 1
? 'text-blue-600'
: 'text-yellow-600'
}
>
{obtenerClasificacion(puntoActual.ep).toUpperCase()}
</span>
</p>
<p className="text-sm text-gray-600 mt-2">
(|Ep| = {Math.abs(puntoActual.ep).toFixed(2)})
</p>
</div>
<div className="flex gap-3">
<Button onClick={validar} variant="primary">
Confirmar Exploración
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Reiniciar
</Button>
</div>
{completado && (
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
<p className="text-success font-medium text-center">
¡Excelente! Has explorado cómo la elasticidad varía a lo largo de la curva.
</p>
</div>
)}
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Fórmula para curva lineal:</h4>
<p className="text-sm text-blue-800 mb-2">
Para una curva de demanda lineal P = a - bQ, la elasticidad en cualquier punto es:
</p>
<p className="text-lg text-blue-900 font-mono mb-2">
E<sub>p</sub> = -b × (P/Q)
</p>
<p className="text-sm text-blue-800">
<strong>En este ejemplo:</strong> P = 20 - 0.2Q, por lo que b = 0.2
</p>
<p className="text-sm text-blue-800 mt-1">
<strong>Punto unitario:</strong> Ocurre donde P/Q = 1/b = 5, es decir, en Q = 50, P = 10
</p>
</Card>
</div>
);
}
export default ElasticidadCurva;

View File

@@ -0,0 +1,426 @@
import { useState, useCallback } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { RotateCcw, LineChart, AlertTriangle } from 'lucide-react';
interface ElasticidadRectasProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface CurvaData {
id: number;
pendiente: number;
intercepto: number;
color: string;
nombre: string;
descripcion: string;
}
const curvas: CurvaData[] = [
{
id: 1,
pendiente: 0.1,
intercepto: 15,
color: '#10b981',
nombre: 'Curva A (Plana)',
descripcion: 'Pendiente pequena → Mayor elasticidad',
},
{
id: 2,
pendiente: 0.2,
intercepto: 20,
color: '#3b82f6',
nombre: 'Curva B (Media)',
descripcion: 'Pendiente media → Elasticidad media',
},
{
id: 3,
pendiente: 0.4,
intercepto: 30,
color: '#ef4444',
nombre: 'Curva C (Empinada)',
descripcion: 'Pendiente grande → Menor elasticidad',
},
];
export function ElasticidadRectas({ ejercicioId: _ejercicioId, onComplete }: ElasticidadRectasProps) {
const [puntoQ, setPuntoQ] = useState<number>(40);
const [curvaSeleccionada, setCurvaSeleccionada] = useState<number>(2);
const [respuestaEp, setRespuestaEp] = useState<string>('');
const [validado, setValidado] = useState(false);
const [completado, setCompletado] = useState(false);
const curvaActual = curvas.find((c) => c.id === curvaSeleccionada) || curvas[1];
const calcularElasticidad = useCallback(
(q: number, curva: CurvaData): number => {
const p = curva.intercepto - curva.pendiente * q;
if (q <= 0 || p <= 0) return 0;
return -curva.pendiente * (p / q);
},
[]
);
const epCorrecto = calcularElasticidad(puntoQ, curvaActual);
const precioActual = curvaActual.intercepto - curvaActual.pendiente * puntoQ;
const validarRespuesta = () => {
const respuestaNum = parseFloat(respuestaEp);
const tolerancia = 0.2;
setValidado(true);
if (Math.abs(respuestaNum - epCorrecto) <= tolerancia) {
setCompletado(true);
if (onComplete) {
onComplete(100);
}
}
};
const reiniciar = () => {
setPuntoQ(40);
setCurvaSeleccionada(2);
setRespuestaEp('');
setValidado(false);
setCompletado(false);
};
const svgWidth = 450;
const svgHeight = 350;
const margin = { top: 20, right: 40, bottom: 50, left: 60 };
const chartWidth = svgWidth - margin.left - margin.right;
const chartHeight = svgHeight - margin.top - margin.bottom;
const maxQ = 150;
const maxP = 35;
const scaleX = (q: number) => margin.left + (q / maxQ) * chartWidth;
const scaleY = (p: number) => margin.top + chartHeight - (p / maxP) * chartHeight;
const generarPath = (curva: CurvaData): string => {
const qMax = Math.min(maxQ, curva.intercepto / curva.pendiente);
const puntos: string[] = [];
for (let q = 0; q <= qMax; q += 5) {
const p = curva.intercepto - curva.pendiente * q;
if (p >= 0) {
puntos.push(`${scaleX(q)},${scaleY(p)}`);
}
}
return puntos.length > 0 ? `M ${puntos.join(' L ')}` : '';
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Elasticidad y Pendiente de Rectas"
subtitle="Comprende la relacion entre la pendiente y la elasticidad"
/>
<div className="bg-yellow-50 border-l-4 border-yellow-500 p-4 mb-6">
<p className="text-yellow-900 font-medium flex items-center gap-2">
<AlertTriangle className="w-4 h-4" />
Concepto Importante:
</p>
<p className="text-yellow-800 mt-1">
<strong>NO confundir pendiente con elasticidad.</strong> Aunque estan relacionadas,
son conceptos distintos. Una curva mas plana (menor pendiente) tiende a ser mas elastica,
pero la elasticidad tambien depende del punto (P/Q).
</p>
</div>
<div className="grid grid-cols-3 gap-4 mb-6">
{curvas.map((curva) => (
<button
key={curva.id}
onClick={() => {
setCurvaSeleccionada(curva.id);
setValidado(false);
}}
className={`p-4 rounded-lg border-2 text-left transition-all ${
curvaSeleccionada === curva.id
? 'border-primary bg-primary/5'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<div className="flex items-center gap-2 mb-2">
<div className="w-4 h-1 rounded" style={{ backgroundColor: curva.color }} />
<span className="font-medium text-sm">{curva.nombre}</span>
</div>
<p className="text-xs text-gray-600">{curva.descripcion}</p>
<p className="text-xs text-gray-500 mt-1">
P = {curva.intercepto} - {curva.pendiente}Q
</p>
</button>
))}
</div>
<div className="bg-white border-2 border-gray-200 rounded-lg p-4 mb-6">
<h4 className="font-medium text-gray-700 mb-3 text-center flex items-center justify-center gap-2">
<LineChart className="w-4 h-4" />
Comparacion de Curvas
</h4>
<svg viewBox={`0 0 ${svgWidth} ${svgHeight}`} className="w-full" style={{ maxHeight: '400px' }}>
<defs>
<pattern id="gridRectas" width="30" height="28" patternUnits="userSpaceOnUse">
<path d="M 30 0 L 0 0 0 28" fill="none" stroke="#e5e7eb" strokeWidth="0.5" />
</pattern>
</defs>
<rect x={margin.left} y={margin.top} width={chartWidth} height={chartHeight} fill="url(#gridRectas)" />
<line
x1={margin.left}
y1={margin.top + chartHeight}
x2={margin.left + chartWidth}
y2={margin.top + chartHeight}
stroke="#374151"
strokeWidth="2"
/>
<line
x1={margin.left}
y1={margin.top}
x2={margin.left}
y2={margin.top + chartHeight}
stroke="#374151"
strokeWidth="2"
/>
<text
x={margin.left + chartWidth / 2}
y={svgHeight - 10}
textAnchor="middle"
fill="#6b7280"
fontSize="12"
>
Cantidad (Q)
</text>
<text
x={15}
y={margin.top + chartHeight / 2}
textAnchor="middle"
fill="#6b7280"
fontSize="12"
transform={`rotate(-90, 15, ${margin.top + chartHeight / 2})`}
>
Precio (P)
</text>
{[0, 50, 100, 150].map((val) => (
<g key={`x-${val}`}>
<line
x1={scaleX(val)}
y1={margin.top + chartHeight}
x2={scaleX(val)}
y2={margin.top + chartHeight + 5}
stroke="#374151"
/>
<text
x={scaleX(val)}
y={margin.top + chartHeight + 18}
textAnchor="middle"
fill="#6b7280"
fontSize="10"
>
{val}
</text>
</g>
))}
{[0, 10, 20, 30].map((val) => (
<g key={`y-${val}`}>
<line
x1={margin.left - 5}
y1={scaleY(val)}
x2={margin.left}
y2={scaleY(val)}
stroke="#374151"
/>
<text
x={margin.left - 8}
y={scaleY(val) + 3}
textAnchor="end"
fill="#6b7280"
fontSize="10"
>
${val}
</text>
</g>
))}
{curvas.map((curva) => (
<path
key={curva.id}
d={generarPath(curva)}
fill="none"
stroke={curva.color}
strokeWidth={curvaSeleccionada === curva.id ? 4 : 2}
opacity={curvaSeleccionada === curva.id ? 1 : 0.4}
/>
))}
<circle
cx={scaleX(puntoQ)}
cy={scaleY(precioActual)}
r="8"
fill={curvaActual.color}
stroke="white"
strokeWidth="3"
/>
<line
x1={scaleX(puntoQ)}
y1={scaleY(precioActual)}
x2={scaleX(puntoQ)}
y2={margin.top + chartHeight}
stroke="#6b7280"
strokeWidth="1"
strokeDasharray="4"
/>
<line
x1={scaleX(puntoQ)}
y1={scaleY(precioActual)}
x2={margin.left}
y2={scaleY(precioActual)}
stroke="#6b7280"
strokeWidth="1"
strokeDasharray="4"
/>
{curvas.map((curva) => {
const qLabel = 20;
const pLabel = curva.intercepto - curva.pendiente * qLabel;
return (
<text
key={`label-${curva.id}`}
x={scaleX(qLabel)}
y={scaleY(pLabel) - 8}
fill={curva.color}
fontSize="10"
fontWeight="bold"
>
{curva.nombre}
</text>
);
})}
</svg>
</div>
<div className="space-y-4 mb-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Selecciona cantidad (Q) para calcular elasticidad:
</label>
<input
type="range"
min="10"
max="100"
value={puntoQ}
onChange={(e) => {
setPuntoQ(Number(e.target.value));
setValidado(false);
}}
className="w-full h-2 bg-gray-200 rounded-lg cursor-pointer"
style={{ accentColor: curvaActual.color }}
/>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>Q = 10</span>
<span>Q = {puntoQ}</span>
<span>Q = 100</span>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="font-medium text-gray-700 mb-2">Datos en el punto seleccionado:</h4>
<div className="grid grid-cols-3 gap-4">
<div>
<p className="text-xs text-gray-500">Cantidad (Q)</p>
<p className="text-lg font-bold">{puntoQ}</p>
</div>
<div>
<p className="text-xs text-gray-500">Precio (P)</p>
<p className="text-lg font-bold">${precioActual.toFixed(2)}</p>
</div>
<div>
<p className="text-xs text-gray-500">Pendiente (b)</p>
<p className="text-lg font-bold">{curvaActual.pendiente}</p>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Calcula la Elasticidad (Ep):
</label>
<p className="text-sm text-gray-500 mb-2">
Usa la formula: Ep = -b x (P/Q)
</p>
<input
type="number"
step="0.01"
value={respuestaEp}
onChange={(e) => {
setRespuestaEp(e.target.value);
setValidado(false);
}}
placeholder="Ej: -0.75"
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
/>
</div>
</div>
<div className="flex gap-3">
<Button onClick={validarRespuesta} variant="primary">
Validar Calculo
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Reiniciar
</Button>
</div>
{validado && (
<div
className={`mt-4 p-4 rounded-lg ${
completado
? 'bg-success/10 border border-success'
: 'bg-error/10 border border-error'
}`}
>
<p className={`font-medium ${completado ? 'text-success' : 'text-error'}`}>
{completado
? `Correcto! La elasticidad es ${epCorrecto.toFixed(2)}`
: `Incorrecto. La respuesta correcta es ${epCorrecto.toFixed(2)}`}
</p>
{!completado && (
<p className="text-sm text-error mt-1">
Recuerda: Ep = -{curvaActual.pendiente} x ({precioActual.toFixed(2)} / {puntoQ}) = {epCorrecto.toFixed(2)}
</p>
)}
</div>
)}
</Card>
<Card className="bg-purple-50 border-purple-200">
<h4 className="font-semibold text-purple-900 mb-2">Relacion Pendiente vs Elasticidad:</h4>
<ul className="space-y-2 text-sm text-purple-800">
<li>
<strong>Pendiente (b):</strong> Indica cuanto cambia P por cada unidad de Q.
Es la inclinacion geometrica de la recta.
</li>
<li>
<strong>Elasticidad (Ep):</strong> Indica cuanto cambia Q (%) por cada cambio de P (%).
Depende de la pendiente Y de la relacion P/Q en ese punto.
</li>
<li>
<strong>Conclusion:</strong> Una curva mas plana (menor b) NO siempre es mas elastica,
porque tambien depende de donde estes en la curva (el valor P/Q).
</li>
</ul>
</Card>
</div>
);
}
export default ElasticidadRectas;

View File

@@ -0,0 +1,236 @@
import { useState, useCallback } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Calculator, RotateCcw, TrendingUp, TrendingDown } from 'lucide-react';
interface FormulaElasticidadProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function FormulaElasticidad({ ejercicioId: _ejercicioId, onComplete }: FormulaElasticidadProps) {
const [qInicial, setQInicial] = useState(100);
const [qFinal, setQFinal] = useState(80);
const [pInicial, setPInicial] = useState(10);
const [pFinal, setPFinal] = useState(12);
const [respuesta, setRespuesta] = useState<string>('');
const [validado, setValidado] = useState(false);
const [completado, setCompletado] = useState(false);
const porcentajeCambioQ = useCallback(() => {
return ((qFinal - qInicial) / qInicial) * 100;
}, [qInicial, qFinal]);
const porcentajeCambioP = useCallback(() => {
return ((pFinal - pInicial) / pInicial) * 100;
}, [pInicial, pFinal]);
const elasticidadCorrecta = useCallback(() => {
return porcentajeCambioQ() / porcentajeCambioP();
}, [porcentajeCambioQ, porcentajeCambioP]);
const validarRespuesta = () => {
const respuestaNum = parseFloat(respuesta);
const correcta = elasticidadCorrecta();
const tolerancia = 0.05;
setValidado(true);
if (Math.abs(respuestaNum - correcta) <= tolerancia) {
setCompletado(true);
if (onComplete) {
onComplete(100);
}
}
};
const reiniciar = () => {
setQInicial(100);
setQFinal(80);
setPInicial(10);
setPFinal(12);
setRespuesta('');
setValidado(false);
setCompletado(false);
};
const pctQ = porcentajeCambioQ();
const pctP = porcentajeCambioP();
const correcta = elasticidadCorrecta();
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Fórmula Básica de Elasticidad Precio"
subtitle="Calcula la elasticidad usando la fórmula: Ep = (%ΔQ) / (%ΔP)"
/>
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 mb-6">
<p className="text-blue-900 font-medium">Fórmula:</p>
<p className="text-2xl text-blue-800 font-bold mt-2">
E<sub>p</sub> = %ΔQ / %ΔP
</p>
<p className="text-sm text-blue-700 mt-1">
Donde: %ΔQ = (Q<sub>f</sub> - Q<sub>i</sub>) / Q<sub>i</sub> × 100
</p>
<p className="text-sm text-blue-700">
%ΔP = (P<sub>f</sub> - P<sub>i</sub>) / P<sub>i</sub> × 100
</p>
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="space-y-4">
<h4 className="font-medium text-gray-700 flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
Datos Iniciales
</h4>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Cantidad Inicial (Q<sub>i</sub>)
</label>
<input
type="number"
value={qInicial}
onChange={(e) => {
setQInicial(Number(e.target.value));
setValidado(false);
}}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Precio Inicial (P<sub>i</sub>)
</label>
<input
type="number"
value={pInicial}
onChange={(e) => {
setPInicial(Number(e.target.value));
setValidado(false);
}}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
/>
</div>
</div>
<div className="space-y-4">
<h4 className="font-medium text-gray-700 flex items-center gap-2">
<TrendingDown className="w-4 h-4" />
Datos Finales
</h4>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Cantidad Final (Q<sub>f</sub>)
</label>
<input
type="number"
value={qFinal}
onChange={(e) => {
setQFinal(Number(e.target.value));
setValidado(false);
}}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Precio Final (P<sub>f</sub>)
</label>
<input
type="number"
value={pFinal}
onChange={(e) => {
setPFinal(Number(e.target.value));
setValidado(false);
}}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
/>
</div>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4 mb-6">
<h4 className="font-medium text-gray-700 mb-3">Cálculo Paso a Paso:</h4>
<div className="space-y-2 text-sm font-mono">
<p>
%ΔQ = ({qFinal} - {qInicial}) / {qInicial} × 100 = <span className="text-blue-600 font-bold">{pctQ.toFixed(2)}%</span>
</p>
<p>
%ΔP = ({pFinal} - {pInicial}) / {pInicial} × 100 = <span className="text-green-600 font-bold">{pctP.toFixed(2)}%</span>
</p>
<p className="pt-2 border-t">
E<sub>p</sub> = {pctQ.toFixed(2)} / {pctP.toFixed(2)} = <span className="text-purple-600 font-bold text-lg">{correcta.toFixed(2)}</span>
</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tu Respuesta (Coeficiente de Elasticidad):
</label>
<input
type="number"
step="0.01"
value={respuesta}
onChange={(e) => {
setRespuesta(e.target.value);
setValidado(false);
}}
placeholder="Ej: -1.25"
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
/>
</div>
<div className="flex gap-3">
<Button onClick={validarRespuesta} variant="primary">
<Calculator className="w-4 h-4 mr-2" />
Validar
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Reiniciar
</Button>
</div>
</div>
{validado && (
<div
className={`mt-4 p-4 rounded-lg ${
completado
? 'bg-success/10 border border-success'
: 'bg-error/10 border border-error'
}`}
>
<p
className={`font-medium ${
completado ? 'text-success' : 'text-error'
}`}
>
{completado
? '¡Correcto! La elasticidad es ' + correcta.toFixed(2)
: 'Incorrecto. La respuesta correcta es ' + correcta.toFixed(2)}
</p>
</div>
)}
</Card>
<Card className="bg-yellow-50 border-yellow-200">
<h4 className="font-semibold text-yellow-900 mb-2">Importante:</h4>
<ul className="space-y-1 text-sm text-yellow-800">
<li> La elasticidad precio suele ser negativa (ley de demanda)</li>
<li>
Usamos valor absoluto para clasificar: |E<sub>p</sub>| &gt; 1 = Elástica
</li>
<li>
|E<sub>p</sub>| = 1 = Unitaria, |E<sub>p</sub>| &lt; 1 = Inelástica
</li>
</ul>
</Card>
</div>
);
}
export default FormulaElasticidad;

View File

@@ -0,0 +1,286 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { Card, CardHeader } from '../../ui/Card';
interface Ejercicio {
id: number;
titulo: string;
descripcion: string;
bienX: string;
bienY: string;
pY1: number;
pY2: number;
qX1: number;
qX2: number;
unidadP: string;
unidadQ: string;
}
const ejercicios: Ejercicio[] = [
{
id: 1,
titulo: "Elasticidad Cruzada - Sustitutos",
descripcion: "Cuando el precio del café (bien Y) aumenta, observamos cambios en la demanda de té (bien X).",
bienX: "Té",
bienY: "Café",
pY1: 5,
pY2: 7,
qX1: 100,
qX2: 140,
unidadP: "$/libra",
unidadQ: "libras mensuales"
},
{
id: 2,
titulo: "Elasticidad Cruzada - Complementarios",
descripcion: "Cuando el precio de las impresoras (bien Y) aumenta, observamos cambios en la demanda de tinta (bien X).",
bienX: "Cartuchos de tinta",
bienY: "Impresoras",
pY1: 80,
pY2: 120,
qX1: 500,
qX2: 350,
unidadP: "$",
unidadQ: "unidades mensuales"
},
{
id: 3,
titulo: "Elasticidad Cruzada - Bienes Independientes",
descripcion: "Analiza la relación entre helado (bien X) y gasolina (bien Y).",
bienX: "Helado",
bienY: "Gasolina",
pY1: 3,
pY2: 4.5,
qX1: 200,
qX2: 205,
unidadP: "$/galón",
unidadQ: "litros mensuales"
}
];
interface Respuesta {
valor: string;
esCorrecta: boolean | null;
}
interface FormulaElasticidadCruzadaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function FormulaElasticidadCruzada({ ejercicioId: _ejercicioId, onComplete }: FormulaElasticidadCruzadaProps) {
const [ejercicioActual, setEjercicioActual] = useState(0);
const [respuestas, setRespuestas] = useState<Record<number, Respuesta>>({});
const [mostrarSolucion, setMostrarSolucion] = useState<Record<number, boolean>>({});
const [mostrarFormula, setMostrarFormula] = useState(false);
const ejercicio = ejercicios[ejercicioActual];
const calcularElasticidad = (ej: Ejercicio) => {
const deltaQX = ej.qX2 - ej.qX1;
const deltaPY = ej.pY2 - ej.pY1;
const qXPromedio = (ej.qX1 + ej.qX2) / 2;
const pYPromedio = (ej.pY1 + ej.pY2) / 2;
const porcentajeQX = (deltaQX / qXPromedio) * 100;
const porcentajePY = (deltaPY / pYPromedio) * 100;
return porcentajeQX / porcentajePY;
};
const verificarRespuesta = () => {
const respuesta = respuestas[ejercicio.id];
if (!respuesta) return;
const valorCorrecto = calcularElasticidad(ejercicio);
const valorIngresado = parseFloat(respuesta.valor);
const esCorrecta = Math.abs(valorIngresado - valorCorrecto) <= 0.05;
setRespuestas(prev => ({
...prev,
[ejercicio.id]: { ...respuesta, esCorrecta }
}));
if (esCorrecta && ejercicioActual === ejercicios.length - 1 && onComplete) {
onComplete(100);
}
};
const handleRespuesta = (valor: string) => {
setRespuestas(prev => ({
...prev,
[ejercicio.id]: { valor, esCorrecta: null }
}));
};
const toggleSolucion = () => {
setMostrarSolucion(prev => ({
...prev,
[ejercicio.id]: !prev[ejercicio.id]
}));
};
const siguienteEjercicio = () => {
if (ejercicioActual < ejercicios.length - 1) {
setEjercicioActual(prev => prev + 1);
}
};
const resultado = calcularElasticidad(ejercicio);
const respuestaActual = respuestas[ejercicio.id];
return (
<Card className="max-w-3xl mx-auto">
<CardHeader
title="Fórmula de Elasticidad Cruzada"
subtitle={`Ejercicio ${ejercicioActual + 1} de ${ejercicios.length}`}
/>
<div className="mb-4">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${((ejercicioActual + 1) / ejercicios.length) * 100}%` }}
/>
</div>
</div>
<div className="space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-bold text-lg text-blue-900">{ejercicio.titulo}</h3>
<p className="text-gray-700 mt-2">{ejercicio.descripcion}</p>
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="bg-white p-3 rounded border">
<p className="text-xs text-gray-500 mb-2">Bien X: {ejercicio.bienX}</p>
<div className="grid grid-cols-2 gap-2">
<div className="text-center">
<span className="font-mono text-sm font-bold">Q<sub>X1</sub></span>
<p className="text-lg font-semibold">{ejercicio.qX1}</p>
</div>
<div className="text-center">
<span className="font-mono text-sm font-bold">Q<sub>X2</sub></span>
<p className="text-lg font-semibold">{ejercicio.qX2}</p>
</div>
</div>
<p className="text-xs text-gray-500 text-center mt-1">{ejercicio.unidadQ}</p>
</div>
<div className="bg-white p-3 rounded border">
<p className="text-xs text-gray-500 mb-2">Bien Y: {ejercicio.bienY}</p>
<div className="grid grid-cols-2 gap-2">
<div className="text-center">
<span className="font-mono text-sm font-bold">P<sub>Y1</sub></span>
<p className="text-lg font-semibold">${ejercicio.pY1}</p>
</div>
<div className="text-center">
<span className="font-mono text-sm font-bold">P<sub>Y2</sub></span>
<p className="text-lg font-semibold">${ejercicio.pY2}</p>
</div>
</div>
<p className="text-xs text-gray-500 text-center mt-1">{ejercicio.unidadP}</p>
</div>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center justify-between mb-3">
<h4 className="font-semibold text-gray-700">Fórmula de Elasticidad Cruzada:</h4>
<Button variant="ghost" size="sm" onClick={() => setMostrarFormula(!mostrarFormula)}>
{mostrarFormula ? 'Ocultar' : 'Mostrar'} fórmula
</Button>
</div>
{mostrarFormula && (
<div className="bg-white p-4 rounded border space-y-3">
<p className="font-mono text-center text-lg">
E<sub>cr</sub> = (%ΔQ<sub>X</sub>) / (%ΔP<sub>Y</sub>)
</p>
<div className="text-sm text-gray-600 space-y-1">
<p>Donde:</p>
<p> %ΔQ<sub>X</sub> = [(Q<sub>X2</sub> - Q<sub>X1</sub>) / ((Q<sub>X1</sub> + Q<sub>X2</sub>) / 2)] × 100</p>
<p> %ΔP<sub>Y</sub> = [(P<sub>Y2</sub> - P<sub>Y1</sub>) / ((P<sub>Y1</sub> + P<sub>Y2</sub>) / 2)] × 100</p>
</div>
</div>
)}
</div>
<div className="border rounded-lg p-4">
<p className="text-gray-800 font-medium mb-3">
Calcule la elasticidad cruzada (E<sub>cr</sub>) entre {ejercicio.bienX} y {ejercicio.bienY}:
</p>
<div className="flex gap-2">
<Input
type="number"
step="0.01"
value={respuestaActual?.valor || ''}
onChange={(e) => handleRespuesta(e.target.value)}
className="w-48"
placeholder="Respuesta"
/>
<Button
variant="outline"
onClick={verificarRespuesta}
disabled={!respuestaActual?.valor}
>
Verificar
</Button>
</div>
{respuestaActual?.esCorrecta !== null && (
<div className={`mt-3 p-3 rounded ${
respuestaActual.esCorrecta
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{respuestaActual.esCorrecta
? '¡Correcto!'
: 'Incorrecto. Revisa tus cálculos.'}
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={toggleSolucion}
className="mt-2"
>
{mostrarSolucion[ejercicio.id] ? 'Ocultar' : 'Ver'} solución paso a paso
</Button>
{mostrarSolucion[ejercicio.id] && (
<div className="mt-2 bg-gray-50 p-4 rounded text-sm space-y-2">
<p className="font-semibold">Desarrollo:</p>
<p className="font-mono">ΔQ<sub>X</sub> = {ejercicio.qX2} - {ejercicio.qX1} = {ejercicio.qX2 - ejercicio.qX1}</p>
<p className="font-mono">ΔP<sub>Y</sub> = {ejercicio.pY2} - {ejercicio.pY1} = {ejercicio.pY2 - ejercicio.pY1}</p>
<p className="font-mono"><sub>X</sub> = ({ejercicio.qX1} + {ejercicio.qX2}) / 2 = {((ejercicio.qX1 + ejercicio.qX2) / 2).toFixed(1)}</p>
<p className="font-mono"><sub>Y</sub> = ({ejercicio.pY1} + {ejercicio.pY2}) / 2 = {((ejercicio.pY1 + ejercicio.pY2) / 2).toFixed(1)}</p>
<p className="font-mono">%ΔQ<sub>X</sub> = ({ejercicio.qX2 - ejercicio.qX1} / {((ejercicio.qX1 + ejercicio.qX2) / 2).toFixed(1)}) × 100 = {(((ejercicio.qX2 - ejercicio.qX1) / ((ejercicio.qX1 + ejercicio.qX2) / 2)) * 100).toFixed(2)}%</p>
<p className="font-mono">%ΔP<sub>Y</sub> = ({ejercicio.pY2 - ejercicio.pY1} / {((ejercicio.pY1 + ejercicio.pY2) / 2).toFixed(1)}) × 100 = {(((ejercicio.pY2 - ejercicio.pY1) / ((ejercicio.pY1 + ejercicio.pY2) / 2)) * 100).toFixed(2)}%</p>
<p className="font-mono font-bold text-blue-800">
E<sub>cr</sub> = {(((ejercicio.qX2 - ejercicio.qX1) / ((ejercicio.qX1 + ejercicio.qX2) / 2)) * 100).toFixed(2)} / {(((ejercicio.pY2 - ejercicio.pY1) / ((ejercicio.pY1 + ejercicio.pY2) / 2)) * 100).toFixed(2)} = {resultado.toFixed(2)}
</p>
</div>
)}
</div>
<div className="flex justify-end">
{ejercicioActual < ejercicios.length - 1 ? (
<Button
onClick={siguienteEjercicio}
disabled={respuestaActual?.esCorrecta !== true}
>
Siguiente Ejercicio
</Button>
) : (
<div className="text-green-600 font-semibold">
{respuestaActual?.esCorrecta ? '¡Ejercicios completados!' : ''}
</div>
)}
</div>
</div>
</Card>
);
}
export default FormulaElasticidadCruzada;

View File

@@ -0,0 +1,265 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { Card, CardHeader } from '../../ui/Card';
interface Ejercicio {
id: number;
titulo: string;
descripcion: string;
i1: number;
i2: number;
q1: number;
q2: number;
unidadI: string;
unidadQ: string;
}
const ejercicios: Ejercicio[] = [
{
id: 1,
titulo: "Cálculo de Elasticidad Ingreso",
descripcion: "Cuando el ingreso mensual de una familia aumenta de $2,000 a $2,500, su consumo de carne aumenta de 8 kg a 12 kg mensuales.",
i1: 2000,
i2: 2500,
q1: 8,
q2: 12,
unidadI: "$/mes",
unidadQ: "kg"
},
{
id: 2,
titulo: "Elasticidad Ingreso - Producto Tecnológico",
descripcion: "El ingreso promedio de consumidores sube de $1,500 a $1,800 mensuales, y las ventas de smartphones premium aumentan de 50 a 80 unidades.",
i1: 1500,
i2: 1800,
q1: 50,
q2: 80,
unidadI: "$/mes",
unidadQ: "unidades"
},
{
id: 3,
titulo: "Elasticidad Ingreso - Transporte",
descripcion: "Cuando el ingreso familiar aumenta de $3,000 a $4,000 mensuales, el uso de transporte público disminuye de 40 a 25 viajes mensuales.",
i1: 3000,
i2: 4000,
q1: 40,
q2: 25,
unidadI: "$/mes",
unidadQ: "viajes"
}
];
interface Respuesta {
valor: string;
esCorrecta: boolean | null;
}
interface FormulaElasticidadIngresoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function FormulaElasticidadIngreso({ ejercicioId: _ejercicioId, onComplete }: FormulaElasticidadIngresoProps) {
const [ejercicioActual, setEjercicioActual] = useState(0);
const [respuestas, setRespuestas] = useState<Record<number, Respuesta>>({});
const [mostrarSolucion, setMostrarSolucion] = useState<Record<number, boolean>>({});
const [mostrarFormula, setMostrarFormula] = useState(false);
const ejercicio = ejercicios[ejercicioActual];
const calcularElasticidad = (ej: Ejercicio) => {
const deltaQ = ej.q2 - ej.q1;
const deltaI = ej.i2 - ej.i1;
const qPromedio = (ej.q1 + ej.q2) / 2;
const iPromedio = (ej.i1 + ej.i2) / 2;
const porcentajeQ = (deltaQ / qPromedio) * 100;
const porcentajeI = (deltaI / iPromedio) * 100;
return porcentajeQ / porcentajeI;
};
const verificarRespuesta = () => {
const respuesta = respuestas[ejercicio.id];
if (!respuesta) return;
const valorCorrecto = calcularElasticidad(ejercicio);
const valorIngresado = parseFloat(respuesta.valor);
const esCorrecta = Math.abs(valorIngresado - valorCorrecto) <= 0.05;
setRespuestas(prev => ({
...prev,
[ejercicio.id]: { ...respuesta, esCorrecta }
}));
if (esCorrecta && ejercicioActual === ejercicios.length - 1 && onComplete) {
onComplete(100);
}
};
const handleRespuesta = (valor: string) => {
setRespuestas(prev => ({
...prev,
[ejercicio.id]: { valor, esCorrecta: null }
}));
};
const toggleSolucion = () => {
setMostrarSolucion(prev => ({
...prev,
[ejercicio.id]: !prev[ejercicio.id]
}));
};
const siguienteEjercicio = () => {
if (ejercicioActual < ejercicios.length - 1) {
setEjercicioActual(prev => prev + 1);
}
};
const resultado = calcularElasticidad(ejercicio);
const respuestaActual = respuestas[ejercicio.id];
return (
<Card className="max-w-3xl mx-auto">
<CardHeader
title="Fórmula de Elasticidad Ingreso"
subtitle={`Ejercicio ${ejercicioActual + 1} de ${ejercicios.length}`}
/>
<div className="mb-4">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${((ejercicioActual + 1) / ejercicios.length) * 100}%` }}
/>
</div>
</div>
<div className="space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-bold text-lg text-blue-900">{ejercicio.titulo}</h3>
<p className="text-gray-700 mt-2">{ejercicio.descripcion}</p>
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="bg-white p-3 rounded text-center border">
<span className="font-mono text-sm font-bold">I</span>
<p className="text-lg font-semibold">{ejercicio.i1.toLocaleString()} {ejercicio.unidadI}</p>
</div>
<div className="bg-white p-3 rounded text-center border">
<span className="font-mono text-sm font-bold">I</span>
<p className="text-lg font-semibold">{ejercicio.i2.toLocaleString()} {ejercicio.unidadI}</p>
</div>
<div className="bg-white p-3 rounded text-center border">
<span className="font-mono text-sm font-bold">Q</span>
<p className="text-lg font-semibold">{ejercicio.q1} {ejercicio.unidadQ}</p>
</div>
<div className="bg-white p-3 rounded text-center border">
<span className="font-mono text-sm font-bold">Q</span>
<p className="text-lg font-semibold">{ejercicio.q2} {ejercicio.unidadQ}</p>
</div>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center justify-between mb-3">
<h4 className="font-semibold text-gray-700">Fórmula del método del punto medio:</h4>
<Button variant="ghost" size="sm" onClick={() => setMostrarFormula(!mostrarFormula)}>
{mostrarFormula ? 'Ocultar' : 'Mostrar'} fórmula
</Button>
</div>
{mostrarFormula && (
<div className="bg-white p-4 rounded border space-y-3">
<p className="font-mono text-center text-lg">
E<sub>i</sub> = (%ΔQ) / (%ΔI)
</p>
<div className="text-sm text-gray-600 space-y-1">
<p>Donde:</p>
<p> %ΔQ = [(Q - Q) / ((Q + Q) / 2)] × 100</p>
<p> %ΔI = [(I - I) / ((I + I) / 2)] × 100</p>
</div>
</div>
)}
</div>
<div className="border rounded-lg p-4">
<p className="text-gray-800 font-medium mb-3">
Calcule la elasticidad ingreso (E<sub>i</sub>):
</p>
<div className="flex gap-2">
<Input
type="number"
step="0.01"
value={respuestaActual?.valor || ''}
onChange={(e) => handleRespuesta(e.target.value)}
className="w-48"
placeholder="Respuesta"
/>
<Button
variant="outline"
onClick={verificarRespuesta}
disabled={!respuestaActual?.valor}
>
Verificar
</Button>
</div>
{respuestaActual?.esCorrecta !== null && (
<div className={`mt-3 p-3 rounded ${
respuestaActual.esCorrecta
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{respuestaActual.esCorrecta
? '¡Correcto!'
: 'Incorrecto. Revisa tus cálculos.'}
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={toggleSolucion}
className="mt-2"
>
{mostrarSolucion[ejercicio.id] ? 'Ocultar' : 'Ver'} solución paso a paso
</Button>
{mostrarSolucion[ejercicio.id] && (
<div className="mt-2 bg-gray-50 p-4 rounded text-sm space-y-2">
<p className="font-semibold">Desarrollo:</p>
<p className="font-mono">ΔQ = {ejercicio.q2} - {ejercicio.q1} = {ejercicio.q2 - ejercicio.q1}</p>
<p className="font-mono">ΔI = {ejercicio.i2} - {ejercicio.i1} = {ejercicio.i2 - ejercicio.i1}</p>
<p className="font-mono"> = ({ejercicio.q1} + {ejercicio.q2}) / 2 = {((ejercicio.q1 + ejercicio.q2) / 2).toFixed(1)}</p>
<p className="font-mono">Ī = ({ejercicio.i1} + {ejercicio.i2}) / 2 = {((ejercicio.i1 + ejercicio.i2) / 2).toFixed(1)}</p>
<p className="font-mono">%ΔQ = ({ejercicio.q2 - ejercicio.q1} / {((ejercicio.q1 + ejercicio.q2) / 2).toFixed(1)}) × 100 = {(((ejercicio.q2 - ejercicio.q1) / ((ejercicio.q1 + ejercicio.q2) / 2)) * 100).toFixed(2)}%</p>
<p className="font-mono">%ΔI = ({ejercicio.i2 - ejercicio.i1} / {((ejercicio.i1 + ejercicio.i2) / 2).toFixed(1)}) × 100 = {(((ejercicio.i2 - ejercicio.i1) / ((ejercicio.i1 + ejercicio.i2) / 2)) * 100).toFixed(2)}%</p>
<p className="font-mono font-bold text-blue-800">
E<sub>i</sub> = {(((ejercicio.q2 - ejercicio.q1) / ((ejercicio.q1 + ejercicio.q2) / 2)) * 100).toFixed(2)} / {(((ejercicio.i2 - ejercicio.i1) / ((ejercicio.i1 + ejercicio.i2) / 2)) * 100).toFixed(2)} = {resultado.toFixed(2)}
</p>
</div>
)}
</div>
<div className="flex justify-end">
{ejercicioActual < ejercicios.length - 1 ? (
<Button
onClick={siguienteEjercicio}
disabled={respuestaActual?.esCorrecta !== true}
>
Siguiente Ejercicio
</Button>
) : (
<div className="text-green-600 font-semibold">
{respuestaActual?.esCorrecta ? '¡Ejercicios completados!' : ''}
</div>
)}
</div>
</div>
</Card>
);
}
export default FormulaElasticidadIngreso;

View File

@@ -0,0 +1,334 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Card, CardHeader } from '../../ui/Card';
interface ParBienes {
id: number;
bienX: string;
bienY: string;
categoria: string;
elasticidad: number;
tipoRelacion: 'sustitutos' | 'complementarios';
interpretacion: string;
}
const paresBienes: ParBienes[] = [
{
id: 1,
bienX: "Coca-Cola",
bienY: "Pepsi",
categoria: "Refrescos",
elasticidad: 2.5,
tipoRelacion: 'sustitutos',
interpretacion: "Sustitutos cercanos. Elasticidad alta indica que los consumidores los ven como casi perfectamente intercambiables."
},
{
id: 2,
bienX: "Café",
bienY: "Té",
categoria: "Bebidas calientes",
elasticidad: 0.8,
tipoRelacion: 'sustitutos',
interpretacion: "Sustitutos moderados. Elasticidad positiva pero menor indica cierta diferenciación entre los productos."
},
{
id: 3,
bienX: "Automóviles",
bienY: "Gasolina",
categoria: "Transporte",
elasticidad: -0.3,
tipoRelacion: 'complementarios',
interpretacion: "Complementarios débiles. A corto plazo, los dueños de autos no pueden cambiar fácilmente su consumo de gasolina."
},
{
id: 4,
bienX: "Computadoras",
bienY: "Software",
categoria: "Tecnología",
elasticidad: -2.0,
tipoRelacion: 'complementarios',
interpretacion: "Complementarios fuertes. Elasticidad negativa alta indica que se usan estrictamente juntos."
},
{
id: 5,
bienX: "Mantequilla",
bienY: "Margarina",
categoria: "Grasas",
elasticidad: 1.8,
tipoRelacion: 'sustitutos',
interpretacion: "Sustitutos cercanos. Son productos similares que los consumidores intercambian fácilmente según el precio."
},
{
id: 6,
bienX: "CDs de música",
bienY: "Conciertos",
categoria: "Entretenimiento",
elasticidad: 0.4,
tipoRelacion: 'sustitutos',
interpretacion: "Sustitutos débiles. Aunque ambos son música, satisfacen necesidades diferentes (hogar vs. experiencia)."
},
{
id: 7,
bienX: "Cámaras",
bienY: "Película fotográfica",
categoria: "Fotografía",
elasticidad: -1.5,
tipoRelacion: 'complementarios',
interpretacion: "Complementarios moderados. Cámaras tradicionales requieren película para funcionar."
},
{
id: 8,
bienX: "Hamburguesas",
bienY: "Papas fritas",
categoria: "Comida rápida",
elasticidad: -0.7,
tipoRelacion: 'complementarios',
interpretacion: "Complementarios moderados. Se consumen frecuentemente juntos en restaurantes de comida rápida."
}
];
type NivelRelacion = 'muy-fuerte' | 'fuerte' | 'moderado' | 'debil' | null;
interface Respuesta {
nivel: NivelRelacion;
esCorrecta: boolean | null;
}
interface GradoRelacionProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function GradoRelacion({ ejercicioId: _ejercicioId, onComplete }: GradoRelacionProps) {
const [respuestas, setRespuestas] = useState<Record<number, Respuesta>>({});
const [mostrarResultados, setMostrarResultados] = useState(false);
const getNivelEsperado = (elasticidad: number): NivelRelacion => {
const absE = Math.abs(elasticidad);
if (absE > 2) return 'muy-fuerte';
if (absE > 1) return 'fuerte';
if (absE > 0.5) return 'moderado';
return 'debil';
};
const seleccionarNivel = (parId: number, nivel: NivelRelacion) => {
if (mostrarResultados) return;
setRespuestas(prev => ({
...prev,
[parId]: { nivel, esCorrecta: null }
}));
};
const verificarTodo = () => {
const nuevasRespuestas: Record<number, Respuesta> = {};
paresBienes.forEach(par => {
const respuesta = respuestas[par.id];
if (respuesta?.nivel) {
const nivelEsperado = getNivelEsperado(par.elasticidad);
nuevasRespuestas[par.id] = {
nivel: respuesta.nivel,
esCorrecta: respuesta.nivel === nivelEsperado
};
}
});
setRespuestas(nuevasRespuestas);
setMostrarResultados(true);
const correctas = Object.values(nuevasRespuestas).filter(r => r.esCorrecta).length;
if (onComplete) {
onComplete(Math.round((correctas / paresBienes.length) * 100));
}
};
const reiniciar = () => {
setRespuestas({});
setMostrarResultados(false);
};
const getCardStyle = (parId: number) => {
const respuesta = respuestas[parId];
if (!mostrarResultados || !respuesta?.nivel) {
return 'bg-white border-gray-200';
}
return respuesta.esCorrecta
? 'bg-green-50 border-green-400'
: 'bg-red-50 border-red-400';
};
const getNivelColor = (nivel: string) => {
switch (nivel) {
case 'muy-fuerte': return 'bg-purple-100 border-purple-300 text-purple-800';
case 'fuerte': return 'bg-blue-100 border-blue-300 text-blue-800';
case 'moderado': return 'bg-yellow-100 border-yellow-300 text-yellow-800';
case 'debil': return 'bg-gray-100 border-gray-300 text-gray-800';
default: return 'bg-white border-gray-200';
}
};
const getNivelLabel = (nivel: string) => {
switch (nivel) {
case 'muy-fuerte': return 'Muy Fuerte';
case 'fuerte': return 'Fuerte';
case 'moderado': return 'Moderado';
case 'debil': return 'Débil';
default: return nivel;
}
};
const correctas = Object.values(respuestas).filter(r => r.esCorrecta).length;
const totalRespondidas = Object.keys(respuestas).length;
return (
<Card className="max-w-4xl mx-auto">
<CardHeader
title="Grado de Relación entre Bienes"
subtitle="Evalúa la intensidad de la relación según el valor absoluto de la elasticidad cruzada"
/>
<div className="space-y-6">
<div className="bg-gray-50 p-4 rounded-lg mb-6">
<h4 className="font-semibold text-gray-700 mb-3">Criterios de clasificación:</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="bg-purple-50 p-3 rounded border border-purple-200">
<p className="font-bold text-purple-800 text-sm">Muy Fuerte</p>
<p className="text-xs text-purple-600 font-mono">|E<sub>cr</sub>| &gt; 2</p>
</div>
<div className="bg-blue-50 p-3 rounded border border-blue-200">
<p className="font-bold text-blue-800 text-sm">Fuerte</p>
<p className="text-xs text-blue-600 font-mono">1 &lt; |E<sub>cr</sub>| 2</p>
</div>
<div className="bg-yellow-50 p-3 rounded border border-yellow-200">
<p className="font-bold text-yellow-800 text-sm">Moderado</p>
<p className="text-xs text-yellow-600 font-mono">0.5 &lt; |E<sub>cr</sub>| 1</p>
</div>
<div className="bg-gray-100 p-3 rounded border border-gray-300">
<p className="font-bold text-gray-800 text-sm">Débil</p>
<p className="text-xs text-gray-600 font-mono">|E<sub>cr</sub>| 0.5</p>
</div>
</div>
</div>
<div className="space-y-4">
{paresBienes.map((par) => {
const respuesta = respuestas[par.id];
const nivelEsperado = getNivelEsperado(par.elasticidad);
return (
<div
key={par.id}
className={`p-4 rounded-lg border-2 transition-all ${getCardStyle(par.id)}`}
>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
{par.categoria}
</span>
<div className="flex items-center gap-2">
<span className="font-semibold">{par.bienX}</span>
<span className="text-gray-400">vs</span>
<span className="font-semibold">{par.bienY}</span>
</div>
</div>
<div className="text-right">
<p className="font-mono text-lg font-bold text-gray-800">
E<sub>cr</sub> = {par.elasticidad}
</p>
<p className={`text-xs ${
par.tipoRelacion === 'sustitutos' ? 'text-green-600' : 'text-red-600'
}`}>
{par.tipoRelacion === 'sustitutos' ? 'Sustitutos' : 'Complementarios'}
</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
{['muy-fuerte', 'fuerte', 'moderado', 'debil'].map((nivel) => (
<button
key={nivel}
onClick={() => seleccionarNivel(par.id, nivel as NivelRelacion)}
disabled={mostrarResultados}
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all ${
respuesta?.nivel === nivel
? getNivelColor(nivel)
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 border border-transparent'
} ${mostrarResultados ? 'cursor-default opacity-50' : 'cursor-pointer'}`}
>
{getNivelLabel(nivel)}
</button>
))}
</div>
{mostrarResultados && (
<div className={`mt-2 p-3 rounded ${
respuesta?.esCorrecta ? 'bg-green-100' : 'bg-red-100'
}`}>
<p className="text-sm">
<strong>Grado de relación:</strong>{' '}
<span className={`inline-block px-2 py-1 rounded text-xs ${getNivelColor(nivelEsperado!)}`}>
{getNivelLabel(nivelEsperado!)}
</span>
</p>
<p className="text-xs text-gray-600 mt-2">{par.interpretacion}</p>
</div>
)}
</div>
</div>
);
})}
</div>
<div className="flex items-center justify-between pt-4 border-t">
<div className="text-sm text-gray-600">
Progreso: {totalRespondidas} / {paresBienes.length}
</div>
{!mostrarResultados ? (
<Button
onClick={verificarTodo}
disabled={totalRespondidas < paresBienes.length}
>
Verificar Respuestas
</Button>
) : (
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-sm text-gray-600">Puntuación</p>
<p className="text-2xl font-bold text-gray-800">
{correctas} / {paresBienes.length}
</p>
</div>
<Button variant="outline" onClick={reiniciar}>
Reiniciar
</Button>
</div>
)}
</div>
{mostrarResultados && (
<div className={`p-4 rounded-lg ${
correctas === paresBienes.length
? 'bg-green-100 border border-green-300'
: correctas >= paresBienes.length / 2
? 'bg-yellow-100 border border-yellow-300'
: 'bg-red-100 border border-red-300'
}`}>
<p className="font-semibold text-gray-800">
{correctas === paresBienes.length
? '¡Excelente! Has evaluado correctamente todos los grados de relación.'
: correctas >= paresBienes.length / 2
? '¡Buen trabajo! Algunos grados de relación necesitan más práctica.'
: 'Necesitas repasar cómo interpretar la magnitud de la elasticidad cruzada.'}
</p>
</div>
)}
</div>
</Card>
);
}
export default GradoRelacion;

View File

@@ -0,0 +1,270 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Card, CardHeader } from '../../ui/Card';
interface LeyUtilidadMarginalDecrecienteProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Escenario {
id: string;
nombre: string;
descripcion: string;
datos: { unidad: number; um: number; ejemplo: string }[];
explicacion: string;
}
const escenarios: Escenario[] = [
{
id: 'pizza',
nombre: 'Pizza 🍕',
descripcion: 'Utilidad marginal de comer rebanadas de pizza',
datos: [
{ unidad: 1, um: 20, ejemplo: '¡Deliciosa! Gran satisfacción' },
{ unidad: 2, um: 15, ejemplo: 'Muy buena, sigue siendo placentera' },
{ unidad: 3, um: 10, ejemplo: 'Aún rica, pero menos emocionante' },
{ unidad: 4, um: 5, ejemplo: 'Estoy llenándome...' },
{ unidad: 5, um: 0, ejemplo: 'No puedo más, estoy satisfecho' },
{ unidad: 6, um: -5, ejemplo: '¡Me siento mal! Demasiado' },
],
explicacion: 'Cada rebanada adicional aporta menos utilidad que la anterior. Después de la quinta, la utilidad se vuelve negativa (malestar).'
},
{
id: 'cafe',
nombre: 'Café ☕',
descripcion: 'Utilidad marginal de tomar tazas de café',
datos: [
{ unidad: 1, um: 15, ejemplo: '¡Perfecto para empezar el día!' },
{ unidad: 2, um: 12, ejemplo: 'Aún disfruto mucho el sabor' },
{ unidad: 3, um: 8, ejemplo: 'Está bien, me mantiene despierto' },
{ unidad: 4, um: 3, ejemplo: 'Ya no sabe igual de bien' },
{ unidad: 5, um: -2, ejemplo: 'Me pone nervioso/a' },
],
explicacion: 'La primera taza da la mayor satisfacción. Después de la cuarta, la cafeína excesiva genera malestar.'
},
{
id: 'netflix',
nombre: 'Series de Netflix 📺',
descripcion: 'Utilidad marginal de ver episodios seguidos',
datos: [
{ unidad: 1, um: 25, ejemplo: '¡Emocionante! Quiero saber qué pasa' },
{ unidad: 2, um: 22, ejemplo: 'La trama se pone mejor' },
{ unidad: 3, um: 18, ejemplo: 'Bien, sigue interesante' },
{ unidad: 4, um: 12, ejemplo: 'Me estoy cansando un poco' },
{ unidad: 5, um: 6, ejemplo: 'Ya quiero dormir...' },
{ unidad: 6, um: 0, ejemplo: 'Me duermo viendo la pantalla' },
],
explicacion: 'Aunque disfrutamos la serie, el cansancio hace que cada episodio adicional aporte menos utilidad.'
}
];
export function LeyUtilidadMarginalDecreciente({ ejercicioId: _ejercicioId, onComplete }: LeyUtilidadMarginalDecrecienteProps) {
const [escenarioActivo, setEscenarioActivo] = useState<Escenario>(escenarios[0]);
const [respuestas, setRespuestas] = useState<Record<string, boolean>>({});
const [mostrarResultados, setMostrarResultados] = useState(false);
const [preguntaActiva, setPreguntaActiva] = useState(0);
const preguntas = [
{
id: 'p1',
texto: '¿Qué sucede con la utilidad marginal a medida que consumes más unidades de un bien?',
opciones: [
{ id: 'a', texto: 'Aumenta constantemente', correcta: false },
{ id: 'b', texto: 'Permanece igual', correcta: false },
{ id: 'c', texto: 'Disminuye (Ley de Utilidad Marginal Decreciente)', correcta: true },
{ id: 'd', texto: 'Se vuelve negativa inmediatamente', correcta: false },
]
},
{
id: 'p2',
texto: 'En el ejemplo de la pizza, ¿en qué rebanada la utilidad marginal se vuelve negativa?',
opciones: [
{ id: 'a', texto: 'Segunda rebanada', correcta: false },
{ id: 'b', texto: 'Cuarta rebanada', correcta: false },
{ id: 'c', texto: 'Quinta rebanada', correcta: false },
{ id: 'd', texto: 'Sexta rebanada', correcta: true },
]
},
{
id: 'p3',
texto: '¿Por qué la utilidad marginal disminuye?',
opciones: [
{ id: 'a', texto: 'Porque el bien es de mala calidad', correcta: false },
{ id: 'b', texto: 'Porque nuestras necesidades se van satisfechando', correcta: true },
{ id: 'c', texto: 'Porque aumenta el precio', correcta: false },
{ id: 'd', texto: 'Porque cambian nuestros gustos', correcta: false },
]
}
];
const handleRespuesta = (preguntaId: string, opcionId: string, esCorrecta: boolean) => {
setRespuestas(prev => ({ ...prev, [preguntaId]: esCorrecta }));
};
const verificarResultados = () => {
setMostrarResultados(true);
const correctas = Object.values(respuestas).filter(Boolean).length;
const score = Math.round((correctas / preguntas.length) * 100);
if (onComplete) onComplete(score);
};
const maxUM = Math.max(...escenarioActivo.datos.map(d => d.um));
const minUM = Math.min(...escenarioActivo.datos.map(d => d.um));
return (
<Card className="max-w-4xl mx-auto">
<CardHeader
title="Ley de Utilidad Marginal Decreciente"
subtitle="Ejemplos prácticos de cómo la satisfacción adicional disminuye con cada unidad consumida"
/>
<div className="space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-bold text-blue-900 mb-2">Ley de Utilidad Marginal Decreciente</h3>
<p className="text-sm text-blue-800">
A medida que un consumidor aumenta el consumo de un bien, <strong>la utilidad marginal</strong> que obtiene de cada unidad adicional tiende a <strong>disminuir</strong>.
</p>
</div>
<div className="flex flex-wrap gap-2">
{escenarios.map((esc) => (
<Button
key={esc.id}
variant={escenarioActivo.id === esc.id ? 'primary' : 'outline'}
size="sm"
onClick={() => {
setEscenarioActivo(esc);
setMostrarResultados(false);
}}
>
{esc.nombre}
</Button>
))}
</div>
<div className="border rounded-lg p-4">
<h4 className="font-bold text-lg mb-2">{escenarioActivo.nombre}</h4>
<p className="text-gray-600 mb-4">{escenarioActivo.descripcion}</p>
<div className="bg-gray-50 p-4 rounded-lg mb-4">
<div className="relative h-64">
<svg viewBox="0 0 500 220" className="w-full h-full">
<line x1="50" y1="200" x2="480" y2="200" stroke="#333" strokeWidth="2" />
<line x1="50" y1="200" x2="50" y2="20" stroke="#333" strokeWidth="2" />
<text x="265" y="218" textAnchor="middle" className="text-xs fill-gray-600">Unidades consumidas</text>
<text x="15" y="110" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90, 15, 110)">Utilidad Marginal</text>
<line x1="50" y1="110" x2="480" y2="110" stroke="#999" strokeDasharray="5,5" />
<text x="40" y="115" textAnchor="end" className="text-xs fill-gray-500">0</text>
{escenarioActivo.datos.map((d, i) => {
const x = 80 + i * 70;
const y = d.um >= 0
? 110 - (d.um / maxUM) * 80
: 110 + (Math.abs(d.um) / Math.abs(minUM)) * 40;
return (
<g key={d.unidad}>
<rect
x={x - 15}
y={d.um >= 0 ? y : 110}
width="30"
height={d.um >= 0 ? 110 - y : y - 110}
fill={d.um >= 0 ? '#3b82f6' : '#ef4444'}
opacity="0.7"
/>
<text x={x} y={y - (d.um >= 0 ? 5 : -15)} textAnchor="middle" className="text-xs fill-gray-700 font-mono">
{d.um}
</text>
<text x={x} y="215" textAnchor="middle" className="text-xs fill-gray-600">{d.unidad}</text>
</g>
);
})}
</svg>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{escenarioActivo.datos.map((d) => (
<div
key={d.unidad}
className={`p-3 rounded-lg border-2 ${
d.um > 0 ? 'border-blue-200 bg-blue-50' : 'border-red-200 bg-red-50'
}`}
>
<div className="flex justify-between items-center mb-1">
<span className="font-bold">Unidad {d.unidad}</span>
<span className={`font-mono font-bold ${d.um >= 0 ? 'text-blue-600' : 'text-red-600'}`}>
UM = {d.um}
</span>
</div>
<p className="text-xs text-gray-600">{d.ejemplo}</p>
</div>
))}
</div>
<div className="mt-4 p-3 bg-yellow-50 border border-yellow-300 rounded-lg">
<p className="text-sm text-yellow-800"><strong>Análisis:</strong> {escenarioActivo.explicacion}</p>
</div>
</div>
<div className="border-t pt-6">
<h4 className="font-bold mb-4">Preguntas de Comprensión</h4>
<div className="space-y-4">
{preguntas.map((pregunta, idx) => (
<div key={pregunta.id} className="border rounded-lg p-4">
<p className="font-medium mb-3">{idx + 1}. {pregunta.texto}</p>
<div className="space-y-2">
{pregunta.opciones.map((opcion) => (
<label
key={opcion.id}
className={`flex items-center gap-2 p-2 rounded cursor-pointer transition-colors ${
respuestas[pregunta.id] !== undefined && mostrarResultados
? opcion.correcta
? 'bg-green-100 border border-green-300'
: 'bg-red-50'
: 'hover:bg-gray-50'
}`}
>
<input
type="radio"
name={pregunta.id}
value={opcion.id}
onChange={() => handleRespuesta(pregunta.id, opcion.id, opcion.correcta)}
disabled={mostrarResultados}
className="text-primary"
/>
<span className="text-sm">{opcion.texto}</span>
{mostrarResultados && opcion.correcta && (
<span className="text-green-600 text-sm ml-auto"> Correcta</span>
)}
</label>
))}
</div> </div>
))}
</div>
<div className="mt-4 flex justify-between items-center">
<div className="text-sm text-gray-600">
{mostrarResultados && (
<>
Puntuación: <strong>{Object.values(respuestas).filter(Boolean).length}/{preguntas.length}</strong>
</>
)}
</div>
<Button
onClick={verificarResultados}
disabled={Object.keys(respuestas).length < preguntas.length || mostrarResultados}
>
Verificar Respuestas
</Button>
</div>
</div>
</div>
</Card>
);
}
export default LeyUtilidadMarginalDecreciente;

View File

@@ -0,0 +1,337 @@
import { useState, useEffect, useCallback } from 'react';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { Card, CardHeader } from '../../ui/Card';
interface MaximizacionUtilidadProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Bien {
nombre: string;
um: number[];
precio: number;
}
const bienes: Record<string, Bien> = {
pizza: {
nombre: 'Pizza',
um: [20, 15, 10, 5, 0, -5],
precio: 10
},
hamburguesa: {
nombre: 'Hamburguesa',
um: [18, 12, 8, 4, 0],
precio: 8
}
};
export function MaximizacionUtilidad({ ejercicioId: _ejercicioId, onComplete }: MaximizacionUtilidadProps) {
const [presupuesto, setPresupuesto] = useState(50);
const [cantidadPizza, setCantidadPizza] = useState(0);
const [cantidadHamburguesa, setCantidadHamburguesa] = useState(0);
const [mostrarCalculos, setMostrarCalculos] = useState(false);
const [mostrarOptimo, setMostrarOptimo] = useState(false);
const [respuestaUsuario, setRespuestaUsuario] = useState({ pizza: '', hamburguesa: '' });
const [verificado, setVerificado] = useState(false);
const gastoTotal = cantidadPizza * bienes.pizza.precio + cantidadHamburguesa * bienes.hamburguesa.precio;
const dentroPresupuesto = gastoTotal <= presupuesto;
const calcularUM = useCallback((tipo: 'pizza' | 'hamburguesa', cantidad: number) => {
const bien = bienes[tipo];
if (cantidad === 0) return 0;
let total = 0;
for (let i = 0; i < Math.min(cantidad, bien.um.length); i++) {
total += bien.um[i];
}
return total;
}, []);
const calcularUMgP = useCallback((tipo: 'pizza' | 'hamburguesa', cantidad: number) => {
const bien = bienes[tipo];
if (cantidad >= bien.um.length) return 0;
return bien.um[cantidad] / bien.precio;
}, []);
const utilidadTotal = calcularUM('pizza', cantidadPizza) + calcularUM('hamburguesa', cantidadHamburguesa);
const encontrarOptimo = useCallback(() => {
let mejorUT = 0;
let mejorCombo = { pizza: 0, hamburguesa: 0 };
for (let p = 0; p <= 5; p++) {
for (let h = 0; h <= 5; h++) {
const costo = p * bienes.pizza.precio + h * bienes.hamburguesa.precio;
if (costo <= presupuesto) {
const ut = calcularUM('pizza', p) + calcularUM('hamburguesa', h);
if (ut > mejorUT) {
mejorUT = ut;
mejorCombo = { pizza: p, hamburguesa: h };
}
}
}
}
return mejorCombo;
}, [presupuesto, calcularUM]);
const optimo = encontrarOptimo();
const verificarRespuesta = () => {
const pizzaCorrecta = parseInt(respuestaUsuario.pizza) === optimo.pizza;
const hamburguesaCorrecta = parseInt(respuestaUsuario.hamburguesa) === optimo.hamburguesa;
setVerificado(true);
if (pizzaCorrecta && hamburguesaCorrecta && onComplete) {
onComplete(100);
}
};
return (
<Card className="max-w-4xl mx-auto">
<CardHeader
title="Maximización de Utilidad"
subtitle="Encuentra la combinación óptima de bienes igualando la utilidad marginal por peso (UMg/P)"
/>
<div className="space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-bold text-blue-900 mb-2">Regla de Maximización de Utilidad</h3>
<p className="text-sm text-blue-800 mb-2">
Para maximizar la utilidad sujeto a un presupuesto, el consumidor debe igualar la <strong>utilidad marginal por peso gastado</strong> en todos los bienes:
</p>
<div className="bg-white p-3 rounded font-mono text-center text-blue-900">
UMg/P = UMg/P = ... = UMgₙ/Pₙ
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="border rounded-lg p-4">
<h4 className="font-bold mb-3">🍕 Pizza</h4>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>Precio:</span>
<span className="font-mono">${bienes.pizza.precio}</span>
</div>
<div className="bg-gray-50 p-2 rounded">
<p className="text-xs font-semibold mb-1">Utilidad Marginal por unidad:</p>
<p className="font-mono text-sm">{bienes.pizza.um.join(', ')}</p>
</div>
</div>
</div>
<div className="border rounded-lg p-4">
<h4 className="font-bold mb-3">🍔 Hamburguesa</h4>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>Precio:</span>
<span className="font-mono">${bienes.hamburguesa.precio}</span>
</div>
<div className="bg-gray-50 p-2 rounded">
<p className="text-xs font-semibold mb-1">Utilidad Marginal por unidad:</p>
<p className="font-mono text-sm">{bienes.hamburguesa.um.join(', ')}</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center gap-4 mb-4">
<label className="font-semibold">Presupuesto: $</label>
<Input
type="number"
value={presupuesto}
onChange={(e) => setPresupuesto(parseInt(e.target.value) || 0)}
className="w-24"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-sm">Cantidad de Pizza:</label>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setCantidadPizza(Math.max(0, cantidadPizza - 1))}
>-</Button>
<span className="font-mono text-xl w-12 text-center">{cantidadPizza}</span>
<Button
size="sm"
onClick={() => setCantidadPizza(cantidadPizza + 1)}
>+</Button>
</div>
</div>
<div>
<label className="text-sm">Cantidad de Hamburguesa:</label>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setCantidadHamburguesa(Math.max(0, cantidadHamburguesa - 1))}
>-</Button>
<span className="font-mono text-xl w-12 text-center">{cantidadHamburguesa}</span>
<Button
size="sm"
onClick={() => setCantidadHamburguesa(cantidadHamburguesa + 1)}
>+</Button>
</div>
</div>
</div>
<div className={`mt-4 p-3 rounded ${dentroPresupuesto ? 'bg-green-100' : 'bg-red-100'}`}>
<p className="font-semibold">Resumen de tu selección:</p>
<div className="grid grid-cols-2 gap-4 mt-2 text-sm">
<div>
<p>Gasto Pizza: ${cantidadPizza * bienes.pizza.precio}</p>
<p>Gasto Hamburguesa: ${cantidadHamburguesa * bienes.hamburguesa.precio}</p>
<p className="font-bold mt-1">Total: ${gastoTotal}</p>
</div>
<div>
<p>UT Pizza: {calcularUM('pizza', cantidadPizza)}</p>
<p>UT Hamburguesa: {calcularUM('hamburguesa', cantidadHamburguesa)}</p>
<p className="font-bold mt-1">UT Total: {utilidadTotal}</p>
</div>
</div>
{!dentroPresupuesto && (
<p className="text-red-600 font-bold mt-2"> ¡Excedes el presupuesto!</p>
)}
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setMostrarCalculos(!mostrarCalculos)}
>
{mostrarCalculos ? 'Ocultar' : 'Ver'} UMg/P
</Button>
<Button
variant="outline"
onClick={() => setMostrarOptimo(!mostrarOptimo)}
>
{mostrarOptimo ? 'Ocultar' : 'Mostrar'} Óptimo
</Button>
</div>
{mostrarCalculos && (
<div className="bg-white border rounded-lg p-4">
<h4 className="font-bold mb-3">Tabla de UMg/P (Utilidad Marginal por peso)</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-100">
<th className="border p-2">Unidad</th>
<th className="border p-2">UMg Pizza</th>
<th className="border p-2">UMg/P Pizza</th>
<th className="border p-2">UMg Hamburguesa</th>
<th className="border p-2">UMg/P Hamburguesa</th>
</tr>
</thead>
<tbody>
{[0, 1, 2, 3, 4, 5].map((i) => (
<tr key={i} className={i < Math.max(cantidadPizza, cantidadHamburguesa) ? 'bg-blue-50' : ''}>
<td className="border p-2 text-center">{i + 1}</td>
<td className="border p-2 text-center">{bienes.pizza.um[i] || '-'}</td>
<td className="border p-2 text-center">
{bienes.pizza.um[i] ? (bienes.pizza.um[i] / bienes.pizza.precio).toFixed(2) : '-'}
</td>
<td className="border p-2 text-center">{bienes.hamburguesa.um[i] || '-'}</td>
<td className="border p-2 text-center">
{bienes.hamburguesa.um[i] ? (bienes.hamburguesa.um[i] / bienes.hamburguesa.precio).toFixed(2) : '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-sm text-gray-600 mt-2">
El consumidor racional comprará primero la unidad con mayor UMg/P, luego la siguiente, hasta agotar el presupuesto.
</p>
</div>
)}
{mostrarOptimo && (
<div className="bg-green-50 border border-green-300 rounded-lg p-4">
<h4 className="font-bold text-green-900 mb-2">Combinación Óptima</h4>
<p className="text-green-800 mb-3">
Con un presupuesto de ${presupuesto}, la combinación que maximiza la utilidad es:
</p>
<div className="bg-white p-3 rounded mb-3">
<p className="font-mono">🍕 Pizza: {optimo.pizza} unidades</p>
<p className="font-mono">🍔 Hamburguesa: {optimo.hamburguesa} unidades</p>
<p className="font-mono font-bold mt-2">
Utilidad Total Máxima: {calcularUM('pizza', optimo.pizza) + calcularUM('hamburguesa', optimo.hamburguesa)}
</p>
</div>
<p className="text-sm text-green-800">
En el óptimo, el consumidor gasta todo su presupuesto en la combinación que proporciona la mayor utilidad total posible.
</p>
</div>
)}
<div className="border-t pt-4">
<h4 className="font-bold mb-3">Ejercicio: Encuentra el Óptimo</h4>
<p className="text-sm text-gray-600 mb-3">
Usando un presupuesto de $50, ¿cuál es la combinación óptima de pizza y hamburguesas que maximiza la utilidad?
</p>
<div className="flex gap-4 mb-3">
<div>
<label className="text-sm">Pizza:</label>
<Input
type="number"
value={respuestaUsuario.pizza}
onChange={(e) => {
setRespuestaUsuario(prev => ({ ...prev, pizza: e.target.value }));
setVerificado(false);
}}
className="w-20"
/>
</div>
<div>
<label className="text-sm">Hamburguesa:</label>
<Input
type="number"
value={respuestaUsuario.hamburguesa}
onChange={(e) => {
setRespuestaUsuario(prev => ({ ...prev, hamburguesa: e.target.value }));
setVerificado(false);
}}
className="w-20"
/>
</div>
<Button
onClick={verificarRespuesta}
disabled={!respuestaUsuario.pizza || !respuestaUsuario.hamburguesa}
className="self-end"
>
Verificar
</Button>
</div>
{verificado && (
<div className={`p-3 rounded ${
parseInt(respuestaUsuario.pizza) === optimo.pizza &&
parseInt(respuestaUsuario.hamburguesa) === optimo.hamburguesa
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{parseInt(respuestaUsuario.pizza) === optimo.pizza &&
parseInt(respuestaUsuario.hamburguesa) === optimo.hamburguesa
? '¡Correcto! Has encontrado la combinación óptima.'
: `Incorrecto. La combinación óptima es: ${optimo.pizza} pizzas y ${optimo.hamburguesa} hamburguesas.`
}
</div>
)}
</div>
</div>
</Card>
);
}
export default MaximizacionUtilidad;

View File

@@ -0,0 +1,247 @@
import { useState, useCallback } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Calculator, RotateCcw, Target } from 'lucide-react';
interface MetodoPuntoMedioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface EjercicioData {
q1: number;
q2: number;
p1: number;
p2: number;
descripcion: string;
}
const ejercicios: EjercicioData[] = [
{
q1: 100,
q2: 120,
p1: 10,
p2: 8,
descripcion: 'Una empresa reduce el precio de su producto de $10 a $8 y las ventas aumentan de 100 a 120 unidades.',
},
{
q1: 500,
q2: 400,
p1: 20,
p2: 25,
descripcion: 'El precio de un medicamento sube de $20 a $25 y la demanda cae de 500 a 400 unidades.',
},
{
q1: 1000,
q2: 1050,
p1: 50,
p2: 48,
descripcion: 'Una tienda baja el precio de un artículo de $50 a $48 y las ventas suben de 1000 a 1050 unidades.',
},
];
export function MetodoPuntoMedio({ ejercicioId: _ejercicioId, onComplete }: MetodoPuntoMedioProps) {
const [ejercicioIndex, setEjercicioIndex] = useState(0);
const [respuesta, setRespuesta] = useState<string>('');
const [validado, setValidado] = useState(false);
const [completado, setCompletado] = useState(false);
const ejercicio = ejercicios[ejercicioIndex];
const calcularPuntoMedio = useCallback(() => {
const { q1, q2, p1, p2 } = ejercicio;
// Método del punto medio (Arc Elasticity)
const qPromedio = (q1 + q2) / 2;
const pPromedio = (p1 + p2) / 2;
const deltaQ = q2 - q1;
const deltaP = p2 - p1;
const porcentajeQ = (deltaQ / qPromedio) * 100;
const porcentajeP = (deltaP / pPromedio) * 100;
const elasticidad = porcentajeQ / porcentajeP;
return {
qPromedio,
pPromedio,
deltaQ,
deltaP,
porcentajeQ,
porcentajeP,
elasticidad,
};
}, [ejercicio]);
const validarRespuesta = () => {
const { elasticidad } = calcularPuntoMedio();
const respuestaNum = parseFloat(respuesta);
const tolerancia = 0.1;
setValidado(true);
if (Math.abs(respuestaNum - elasticidad) <= tolerancia) {
setCompletado(true);
if (onComplete) {
onComplete(100);
}
}
};
const siguienteEjercicio = () => {
setEjercicioIndex((prev) => (prev + 1) % ejercicios.length);
setRespuesta('');
setValidado(false);
setCompletado(false);
};
const reiniciar = () => {
setRespuesta('');
setValidado(false);
setCompletado(false);
};
const calculos = calcularPuntoMedio();
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Método del Punto Medio"
subtitle="Cálculo preciso de elasticidad para cambios de precio"
/>
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 mb-6">
<p className="text-blue-900 font-medium">Fórmula del Punto Medio:</p>
<p className="text-lg text-blue-800 font-bold mt-2">
E<sub>p</sub> = (ΔQ / Q<sub>promedio</sub>) / (ΔP / P<sub>promedio</sub>)
</p>
<p className="text-sm text-blue-700 mt-2">
Donde: Q<sub>promedio</sub> = (Q<sub>1</sub> + Q<sub>2</sub>) / 2, P<sub>promedio</sub> = (P<sub>1</sub> + P<sub>2</sub>) / 2
</p>
</div>
<div className="mb-6">
<h4 className="font-medium text-gray-700 mb-3 flex items-center gap-2">
<Target className="w-4 h-4 text-primary" />
Ejercicio {ejercicioIndex + 1} de {ejercicios.length}
</h4>
<p className="text-gray-600 bg-gray-50 p-4 rounded-lg">{ejercicio.descripcion}</p>
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<h5 className="font-medium text-gray-700 mb-2">Punto 1</h5>
<p className="text-sm text-gray-600">Q = {ejercicio.q1} unidades</p>
<p className="text-sm text-gray-600">P = ${ejercicio.p1}</p>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<h5 className="font-medium text-gray-700 mb-2">Punto 2</h5>
<p className="text-sm text-gray-600">Q = {ejercicio.q2} unidades</p>
<p className="text-sm text-gray-600">P = ${ejercicio.p2}</p>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4 mb-6">
<h4 className="font-medium text-gray-700 mb-3">Desarrollo del Cálculo:</h4>
<div className="space-y-2 text-sm font-mono">
<p>
Q<sub>promedio</sub> = ({ejercicio.q1} + {ejercicio.q2}) / 2 ={' '}
<span className="text-blue-600 font-bold">{calculos.qPromedio.toFixed(2)}</span>
</p>
<p>
P<sub>promedio</sub> = ({ejercicio.p1} + {ejercicio.p2}) / 2 ={' '}
<span className="text-blue-600 font-bold">{calculos.pPromedio.toFixed(2)}</span>
</p>
<p className="pt-2 border-t">
ΔQ = {ejercicio.q2} - {ejercicio.q1} = {calculos.deltaQ}
</p>
<p>ΔP = {ejercicio.p2} - {ejercicio.p1} = {calculos.deltaP}</p>
<p className="pt-2 border-t">
%ΔQ = {calculos.deltaQ} / {calculos.qPromedio.toFixed(2)} ={' '}
<span className="text-green-600 font-bold">{(calculos.porcentajeQ / 100).toFixed(4)}</span>
</p>
<p>
%ΔP = {calculos.deltaP} / {calculos.pPromedio.toFixed(2)} ={' '}
<span className="text-green-600 font-bold">{(calculos.porcentajeP / 100).toFixed(4)}</span>
</p>
<p className="pt-2 border-t text-lg">
E<sub>p</sub> = {(calculos.porcentajeQ / 100).toFixed(4)} / {(calculos.porcentajeP / 100).toFixed(4)} ={' '}
<span className="text-purple-600 font-bold">{calculos.elasticidad.toFixed(2)}</span>
</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tu Respuesta (Coeficiente de Elasticidad):
</label>
<input
type="number"
step="0.01"
value={respuesta}
onChange={(e) => {
setRespuesta(e.target.value);
setValidado(false);
}}
placeholder="Ej: -0.82"
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
/>
</div>
<div className="flex gap-3">
<Button onClick={validarRespuesta} variant="primary">
<Calculator className="w-4 h-4 mr-2" />
Validar
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Reiniciar
</Button>
{completado && (
<Button onClick={siguienteEjercicio} variant="secondary">
Siguiente Ejercicio
</Button>
)}
</div>
</div>
{validado && (
<div
className={`mt-4 p-4 rounded-lg ${
completado
? 'bg-success/10 border border-success'
: 'bg-error/10 border border-error'
}`}
>
<p
className={`font-medium ${
completado ? 'text-success' : 'text-error'
}`}
>
{completado
? `¡Correcto! La elasticidad es ${calculos.elasticidad.toFixed(2)}`
: `Incorrecto. La respuesta correcta es ${calculos.elasticidad.toFixed(2)}`}
</p>
</div>
)}
</Card>
<Card className="bg-yellow-50 border-yellow-200">
<h4 className="font-semibold text-yellow-900 mb-2">Ventaja del Método del Punto Medio:</h4>
<p className="text-sm text-yellow-800 mb-2">
El método del punto medio proporciona el mismo resultado independientemente de si el precio sube o baja,
evitando la asimetría del método tradicional.
</p>
<p className="text-sm text-yellow-800">
<strong>Ejemplo:</strong> Si el precio sube de $10 a $12 y luego baja de $12 a $10,
la elasticidad calculada es la misma en ambas direcciones.
</p>
</Card>
</div>
);
}
export default MetodoPuntoMedio;

View File

@@ -0,0 +1,290 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Card, CardHeader } from '../../ui/Card';
interface ParadojaAguaDiamantesProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function ParadojaAguaDiamantes({ ejercicioId: _ejercicioId, onComplete }: ParadojaAguaDiamantesProps) {
const [respuestas, setRespuestas] = useState<Record<string, string>>({});
const [verificadas, setVerificadas] = useState<Record<string, boolean>>({});
const [mostrarExplicacion, setMostrarExplicacion] = useState(false);
const preguntas = [
{
id: 'p1',
pregunta: 'Según la teoría de la utilidad, ¿por qué el agua es barata a pesar de ser esencial?',
opciones: [
{ id: 'a', texto: 'Porque es fácil de producir', correcta: false },
{ id: 'b', texto: 'Porque su utilidad marginal es baja debido a la abundancia', correcta: true },
{ id: 'c', texto: 'Porque la gente no la valora', correcta: false },
{ id: 'd', texto: 'Porque el gobierno la subsidia', correcta: false },
]
},
{
id: 'p2',
pregunta: '¿Por qué los diamantes son caros a pesar de no ser esenciales?',
opciones: [
{ id: 'a', texto: 'Porque son raros y escasos', correcta: false },
{ id: 'b', texto: 'Porque la gente es irracional', correcta: false },
{ id: 'c', texto: 'Porque su utilidad marginal es alta debido a la escasez', correcta: true },
{ id: 'd', texto: 'Porque cuestan mucho de extraer', correcta: false },
]
},
{
id: 'p3',
pregunta: 'La paradoja se resuelve distinguiendo entre:',
opciones: [
{ id: 'a', texto: 'Utilidad total vs Utilidad marginal', correcta: true },
{ id: 'b', texto: 'Demanda y oferta', correcta: false },
{ id: 'c', texto: 'Bienes de lujo y necesarios', correcta: false },
{ id: 'd', texto: 'Precio y valor', correcta: false },
]
}
];
const handleRespuesta = (preguntaId: string, opcionId: string, esCorrecta: boolean) => {
setRespuestas(prev => ({ ...prev, [preguntaId]: opcionId }));
setVerificadas(prev => ({ ...prev, [preguntaId]: esCorrecta }));
const todasCorrectas = Object.values({
...verificadas,
[preguntaId]: esCorrecta
}).every(Boolean);
if (todasCorrectas && onComplete) {
onComplete(100);
}
};
const correctas = Object.values(verificadas).filter(Boolean).length;
const total = preguntas.length;
return (
<Card className="max-w-4xl mx-auto">
<CardHeader
title="Paradoja del Agua y los Diamantes"
subtitle="¿Por qué el agua, esencial para la vida, es barata, mientras que los diamantes, innecesarios, son caros?"
/>
<div className="space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-bold text-blue-900 mb-2">La Paradoja</h3>
<p className="text-sm text-blue-800">
Adam Smith planteó esta paradoja en "La Riqueza de las Naciones" (1776):
<strong>¿Cómo puede algo tan esencial como el agua tener un valor tan bajo en el mercado,
mientras que los diamantes, que no son necesarios para la supervivencia, valen tanto?</strong>
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="border rounded-lg p-4 bg-blue-50">
<div className="text-4xl mb-2">💧</div>
<h4 className="font-bold text-lg mb-2">Agua</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Utilidad Total:</span>
<span className="font-bold text-green-600">MUY ALTA</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Utilidad Marginal:</span>
<span className="font-bold text-red-600">BAJA</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Disponibilidad:</span>
<span className="font-bold">Abundante</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Precio:</span>
<span className="font-bold text-blue-600">$2 por m³</span>
</div>
</div>
<div className="mt-4 bg-white p-3 rounded">
<p className="text-xs text-gray-600">
<strong>Ejemplo:</strong> La primera botella de agua tiene utilidad infinita (supervivencia),
pero la enésima botella cuando ya estás hidratado tiene UMg cercana a cero.
</p>
</div>
</div>
<div className="border rounded-lg p-4 bg-gray-50">
<div className="text-4xl mb-2">💎</div>
<h4 className="font-bold text-lg mb-2">Diamantes</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Utilidad Total:</span>
<span className="font-bold text-red-600">BAJA</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Utilidad Marginal:</span>
<span className="font-bold text-green-600">ALTA</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Disponibilidad:</span>
<span className="font-bold">Escasa</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Precio:</span>
<span className="font-bold text-blue-600">$10,000 por quilate</span>
</div>
</div>
<div className="mt-4 bg-white p-3 rounded">
<p className="text-xs text-gray-600">
<strong>Ejemplo:</strong> El primer diamante para una joya tiene alta utilidad marginal
(exclusividad, estatus), pero tener muchos diamantes no añade tanta utilidad adicional.
</p>
</div>
</div>
</div>
<div className="bg-yellow-50 border border-yellow-300 p-4 rounded-lg">
<h4 className="font-bold text-yellow-900 mb-3">La Resolución de la Paradoja</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p className="font-semibold text-sm mb-2">🔑 El precio depende de:</p>
<ul className="text-sm text-yellow-800 space-y-1 list-disc ml-4">
<li>La <strong>utilidad marginal</strong> de la última unidad</li>
<li>La <strong>escasez</strong> del bien</li>
<li>La disposición a pagar por una <strong>unidad adicional</strong></li>
</ul>
</div>
<div>
<p className="font-semibold text-sm mb-2">📊 NO del valor total:</p>
<ul className="text-sm text-yellow-800 space-y-1 list-disc ml-4">
<li>El agua tiene <strong>alta utilidad total</strong> pero <strong>baja UMg</strong></li>
<li>Los diamantes tienen <strong>baja utilidad total</strong> pero <strong>alta UMg</strong></li>
<li>Los precios reflejan <strong>valor marginal</strong>, no total</li>
</ul>
</div>
</div>
</div>
<div className="border rounded-lg p-4">
<h4 className="font-bold mb-3">Gráfico Comparativo: Utilidad Marginal</h4>
<div className="bg-white border rounded p-4">
<div className="relative h-64">
<svg viewBox="0 0 500 220" className="w-full h-full">
<line x1="50" y1="200" x2="480" y2="200" stroke="#333" strokeWidth="2" />
<line x1="50" y1="200" x2="50" y2="20" stroke="#333" strokeWidth="2" />
<text x="265" y="218" textAnchor="middle" className="text-xs fill-gray-600">Cantidad consumida</text>
<text x="15" y="110" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90, 15, 110)">Utilidad Marginal</text>
<text x="60" y="215" textAnchor="middle" className="text-xs fill-blue-600 font-bold">Agua</text>
<text x="300" y="215" textAnchor="middle" className="text-xs fill-gray-600 font-bold">Diamantes</text>
<line x1="50" y1="150" x2="150" y2="200" stroke="#3b82f6" strokeWidth="3" />
<text x="100" y="185" textAnchor="middle" className="text-xs fill-blue-600">UMg alta</text>
<line x1="150" y1="200" x2="350" y2="200" stroke="#3b82f6" strokeWidth="3" strokeDasharray="5,5" />
<text x="250" y="195" textAnchor="middle" className="text-xs fill-blue-600">UMg 0 (abundante)</text>
<line x1="300" y1="50" x2="350" y2="150" stroke="#6b7280" strokeWidth="3" />
<text x="340" y="100" textAnchor="middle" className="text-xs fill-gray-600">UMg alta</text>
<circle cx="60" cy="155" r="4" fill="#3b82f6" />
<text x="60" y="145" textAnchor="middle" className="text-xs fill-blue-600">Primera</text>
<circle cx="300" cy="50" r="4" fill="#6b7280" />
<text x="300" y="40" textAnchor="middle" className="text-xs fill-gray-600">Primero</text>
<text x="200" y="25" textAnchor="middle" className="text-sm fill-gray-700 font-bold">
Las curvas de UMg son diferentes por la abundancia vs escasez
</text>
</svg>
</div>
<p className="text-sm text-gray-600 mt-2">
El precio se determina por la <strong>utilidad marginal de la última unidad</strong>.
Como el agua es abundante, su UMg en el margen es baja. Los diamantes son escasos,
manteniendo una UMg alta.
</p>
</div>
</div>
<div className="border-t pt-4">
<h4 className="font-bold mb-4">Preguntas de Comprensión</h4>
<div className="space-y-4">
{preguntas.map((pregunta, idx) => (
<div key={pregunta.id} className="border rounded-lg p-4">
<p className="font-medium mb-3">{idx + 1}. {pregunta.pregunta}</p>
<div className="space-y-2">
{pregunta.opciones.map((opcion) => (
<label
key={opcion.id}
className={`flex items-center gap-2 p-2 rounded cursor-pointer transition-colors ${
verificadas[pregunta.id] !== undefined
? opcion.correcta
? 'bg-green-100 border border-green-300'
: respuestas[pregunta.id] === opcion.id
? 'bg-red-100'
: ''
: 'hover:bg-gray-50'
}`}
>
<input
type="radio"
name={pregunta.id}
value={opcion.id}
onChange={() => handleRespuesta(pregunta.id, opcion.id, opcion.correcta)}
className="text-primary"
/>
<span className="text-sm">{opcion.texto}</span>
{verificadas[pregunta.id] !== undefined && opcion.correcta && (
<span className="text-green-600 text-sm ml-auto"> Correcta</span>
)}
</label>
))}
</div> </div>
))}
</div>
<div className="mt-4 flex justify-between items-center">
<div className="text-sm text-gray-600">
Puntuación: <strong>{correctas}/{total}</strong>
</div>
<Button
variant="outline"
onClick={() => setMostrarExplicacion(!mostrarExplicacion)}
>
{mostrarExplicacion ? 'Ocultar' : 'Ver'} Explicación Detallada
</Button>
</div>
{mostrarExplicacion && (
<div className="mt-4 bg-gray-50 p-4 rounded-lg">
<h5 className="font-bold mb-2">Explicación Detallada</h5>
<div className="space-y-3 text-sm">
<p>
<strong>1. Valor Total vs Valor Marginal:</strong> El valor que asignamos a algo
no depende de su utilidad total, sino de lo que estaríamos dispuestos a pagar por
<em>una unidad adicional</em>.
</p>
<p>
<strong>2. El Agua:</strong> Aunque sin agua moriríamos (utilidad total infinita),
como hay mucha agua disponible, la utilidad marginal de una botella más es muy baja.
Por eso pagamos poco.
</p>
<p>
<strong>3. Los Diamantes:</strong> Aunque no los necesitamos para vivir,
son escasos. La utilidad marginal del primer (y único) diamante es alta porque
representa exclusividad, estatus y belleza.
</p>
<p>
<strong>4. Conclusión:</strong> Los precios reflejan <em>valores marginales</em>,
no valores totales. Esto es fundamental para entender cómo funcionan los mercados.
</p> </div>
</div>
)}
</div>
</div>
</Card>
);
}
export default ParadojaAguaDiamantes;

View File

@@ -0,0 +1,328 @@
import { useState } from 'react';
import { Button } from '../../ui/Button';
import { Card, CardHeader } from '../../ui/Card';
interface ParBienes {
id: number;
bienX: string;
bienY: string;
descripcion: string;
elasticidad: number;
relacionCorrecta: 'sustitutos' | 'complementarios' | 'independientes';
explicacion: string;
}
const paresBienes: ParBienes[] = [
{
id: 1,
bienX: "Cerveza",
bienY: "Vino",
descripcion: "Bebidas alcohólicas que los consumidores pueden intercambiar",
elasticidad: 0.8,
relacionCorrecta: 'sustitutos',
explicacion: "Ecr > 0 indica que son sustitutos. Cuando sube el precio del vino, algunos consumidores cambian a cerveza."
},
{
id: 2,
bienX: "Tinta de impresora",
bienY: "Impresoras",
descripcion: "Productos que se usan juntos",
elasticidad: -1.2,
relacionCorrecta: 'complementarios',
explicacion: "Ecr < 0 indica que son complementarios. Si sube el precio de las impresoras, se compran menos impresoras y por tanto menos tinta."
},
{
id: 3,
bienX: "Mantequilla",
bienY: "Margarina",
descripcion: "Grasas para cocinar/similar uso",
elasticidad: 1.5,
relacionCorrecta: 'sustitutos',
explicacion: "Ecr > 0 indica que son sustitutos cercanos. Son productos muy intercambiables para los consumidores."
},
{
id: 4,
bienX: "Hoteles",
bienY: "Gasolina",
descripcion: "Servicio de alojamiento y combustible",
elasticidad: 0.05,
relacionCorrecta: 'independientes',
explicacion: "Ecr ≈ 0 indica que son independientes. El precio de la gasolina casi no afecta la demanda de hoteles."
},
{
id: 5,
bienX: "Automóviles",
bienY: "Gasolina",
descripcion: "Vehículos y su combustible",
elasticidad: -0.6,
relacionCorrecta: 'complementarios',
explicacion: "Ecr < 0 indica complementariedad. Si sube el precio de la gasolina, la demanda de autos (especialmente grandes) disminuye."
},
{
id: 6,
bienX: "Coca-Cola",
bienY: "Pepsi",
descripcion: "Bebidas gaseosas similares",
elasticidad: 2.1,
relacionCorrecta: 'sustitutos',
explicacion: "Ecr > 0 indica sustitutos. Elasticidad alta porque son productos casi perfectamente intercambiables."
},
{
id: 7,
bienX: "Computadoras",
bienY: "Software",
descripcion: "Hardware y programas",
elasticidad: -1.8,
relacionCorrecta: 'complementarios',
explicacion: "Ecr < 0 indica fuerte complementariedad. Computadoras y software se usan juntos obligatoriamente."
},
{
id: 8,
bienX: "Zapatos",
bienY: "Pan",
descripcion: "Calzado y alimento básico",
elasticidad: 0.01,
relacionCorrecta: 'independientes',
explicacion: "Ecr ≈ 0 indica independencia. No existe relación económica entre estos bienes."
}
];
interface Respuesta {
relacion: string | null;
esCorrecta: boolean | null;
}
interface SustitutosComplementariosProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function SustitutosComplementarios({ ejercicioId: _ejercicioId, onComplete }: SustitutosComplementariosProps) {
const [respuestas, setRespuestas] = useState<Record<number, Respuesta>>({});
const [mostrarResultados, setMostrarResultados] = useState(false);
const seleccionarRelacion = (parId: number, relacion: string) => {
if (mostrarResultados) return;
setRespuestas(prev => ({
...prev,
[parId]: { relacion, esCorrecta: null }
}));
};
const verificarTodo = () => {
const nuevasRespuestas: Record<number, Respuesta> = {};
paresBienes.forEach(par => {
const respuesta = respuestas[par.id];
if (respuesta?.relacion) {
nuevasRespuestas[par.id] = {
relacion: respuesta.relacion,
esCorrecta: respuesta.relacion === par.relacionCorrecta
};
}
});
setRespuestas(nuevasRespuestas);
setMostrarResultados(true);
const correctas = Object.values(nuevasRespuestas).filter(r => r.esCorrecta).length;
if (onComplete) {
onComplete(Math.round((correctas / paresBienes.length) * 100));
}
};
const reiniciar = () => {
setRespuestas({});
setMostrarResultados(false);
};
const getCardStyle = (parId: number) => {
const respuesta = respuestas[parId];
if (!mostrarResultados || !respuesta?.relacion) {
return 'bg-white border-gray-200';
}
return respuesta.esCorrecta
? 'bg-green-50 border-green-400'
: 'bg-red-50 border-red-400';
};
const getRelacionColor = (relacion: string) => {
switch (relacion) {
case 'sustitutos': return 'bg-green-100 border-green-300 text-green-800';
case 'complementarios': return 'bg-red-100 border-red-300 text-red-800';
case 'independientes': return 'bg-gray-100 border-gray-300 text-gray-800';
default: return 'bg-white border-gray-200';
}
};
const correctas = Object.values(respuestas).filter(r => r.esCorrecta).length;
const totalRespondidas = Object.keys(respuestas).length;
return (
<Card className="max-w-4xl mx-auto">
<CardHeader
title="Sustitutos vs. Complementarios"
subtitle="Identifica la relación entre pares de bienes según su elasticidad cruzada"
/>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="bg-green-50 p-4 rounded-lg border-2 border-green-200">
<h4 className="font-bold text-lg text-green-800">Sustitutos</h4>
<p className="text-sm font-mono text-green-600 mt-1">E<sub>cr</sub> &gt; 0</p>
<p className="text-sm text-green-700 mt-2">
Cuando sube el precio de Y, aumenta la demanda de X. Los bienes compiten entre .
</p>
</div>
<div className="bg-red-50 p-4 rounded-lg border-2 border-red-200">
<h4 className="font-bold text-lg text-red-800">Complementarios</h4>
<p className="text-sm font-mono text-red-600 mt-1">E<sub>cr</sub> &lt; 0</p>
<p className="text-sm text-red-700 mt-2">
Cuando sube el precio de Y, disminuye la demanda de X. Se consumen juntos.
</p>
</div>
<div className="bg-gray-50 p-4 rounded-lg border-2 border-gray-200">
<h4 className="font-bold text-lg text-gray-800">Independientes</h4>
<p className="text-sm font-mono text-gray-600 mt-1">E<sub>cr</sub> 0</p>
<p className="text-sm text-gray-700 mt-2">
El precio de Y no afecta la demanda de X. No existe relación entre ellos.
</p>
</div>
</div>
<div className="space-y-4">
{paresBienes.map((par) => {
const respuesta = respuestas[par.id];
return (
<div
key={par.id}
className={`p-4 rounded-lg border-2 transition-all ${getCardStyle(par.id)}`}
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-3">
<div className="bg-gray-100 px-3 py-1 rounded text-sm font-semibold">
{par.bienX}
</div>
<span className="text-gray-400">vs</span>
<div className="bg-gray-100 px-3 py-1 rounded text-sm font-semibold">
{par.bienY}
</div>
</div>
<p className="text-sm text-gray-500 mt-2">{par.descripcion}</p>
{mostrarResultados && (
<div className="mt-3 space-y-2">
<p className="font-mono text-sm">
E<sub>cr</sub> = {par.elasticidad}
</p>
<p className={`text-sm font-semibold ${
par.relacionCorrecta === 'sustitutos'
? 'text-green-600'
: par.relacionCorrecta === 'complementarios'
? 'text-red-600'
: 'text-gray-600'
}`}>
{par.relacionCorrecta === 'sustitutos' && 'Sustitutos'}
{par.relacionCorrecta === 'complementarios' && 'Complementarios'}
{par.relacionCorrecta === 'independientes' && 'Independientes'}
</p>
<p className="text-xs text-gray-500">{par.explicacion}</p>
</div>
)}
</div>
<div className="flex gap-2">
<button
onClick={() => seleccionarRelacion(par.id, 'sustitutos')}
disabled={mostrarResultados}
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all ${
respuesta?.relacion === 'sustitutos'
? 'bg-green-500 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
} ${mostrarResultados ? 'cursor-default opacity-50' : 'cursor-pointer'}`}
>
Sustitutos
</button>
<button
onClick={() => seleccionarRelacion(par.id, 'complementarios')}
disabled={mostrarResultados}
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all ${
respuesta?.relacion === 'complementarios'
? 'bg-red-500 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
} ${mostrarResultados ? 'cursor-default opacity-50' : 'cursor-pointer'}`}
>
Complementarios
</button>
<button
onClick={() => seleccionarRelacion(par.id, 'independientes')}
disabled={mostrarResultados}
className={`px-3 py-2 rounded-lg text-xs font-medium transition-all ${
respuesta?.relacion === 'independientes'
? 'bg-gray-500 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
} ${mostrarResultados ? 'cursor-default opacity-50' : 'cursor-pointer'}`}
>
Independientes
</button>
</div>
</div>
</div>
);
})}
</div>
<div className="flex items-center justify-between pt-4 border-t">
<div className="text-sm text-gray-600">
Progreso: {totalRespondidas} / {paresBienes.length}
</div>
{!mostrarResultados ? (
<Button
onClick={verificarTodo}
disabled={totalRespondidas < paresBienes.length}
>
Verificar Respuestas
</Button>
) : (
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-sm text-gray-600">Puntuación</p>
<p className="text-2xl font-bold text-gray-800">
{correctas} / {paresBienes.length}
</p>
</div>
<Button variant="outline" onClick={reiniciar}>
Reiniciar
</Button>
</div>
)}
</div>
{mostrarResultados && (
<div className={`p-4 rounded-lg ${
correctas === paresBienes.length
? 'bg-green-100 border border-green-300'
: correctas >= paresBienes.length / 2
? 'bg-yellow-100 border border-yellow-300'
: 'bg-red-100 border border-red-300'
}`}>
<p className="font-semibold text-gray-800">
{correctas === paresBienes.length
? '¡Excelente! Has identificado todas las relaciones correctamente.'
: correctas >= paresBienes.length / 2
? '¡Buen trabajo! Algunas relaciones necesitan más atención.'
: 'Necesitas repasar la diferencia entre bienes sustitutos, complementarios e independientes.'}
</p>
</div>
)}
</div>
</Card>
);
}
export default SustitutosComplementarios;

View File

@@ -0,0 +1,243 @@
import { useState, useEffect, useCallback } from 'react';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { Card, CardHeader } from '../../ui/Card';
interface UtilidadTotalVsMarginalProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FilaDatos {
cantidad: number;
utilidadTotal: number;
utilidadMarginal: number | null;
}
const datosBase: Omit<FilaDatos, 'utilidadMarginal'>[] = [
{ cantidad: 0, utilidadTotal: 0 },
{ cantidad: 1, utilidadTotal: 10 },
{ cantidad: 2, utilidadTotal: 18 },
{ cantidad: 3, utilidadTotal: 24 },
{ cantidad: 4, utilidadTotal: 28 },
{ cantidad: 5, utilidadTotal: 30 },
{ cantidad: 6, utilidadTotal: 30 },
{ cantidad: 7, utilidadTotal: 28 },
];
export function UtilidadTotalVsMarginal({ ejercicioId: _ejercicioId, onComplete }: UtilidadTotalVsMarginalProps) {
const [respuestas, setRespuestas] = useState<Record<number, string>>({});
const [verificadas, setVerificadas] = useState<Record<number, boolean>>({});
const [mostrarGrafico, setMostrarGrafico] = useState(false);
const [mostrarExplicacion, setMostrarExplicacion] = useState(false);
const datosCompletos: FilaDatos[] = datosBase.map((fila, index) => ({
...fila,
utilidadMarginal: index === 0 ? null : fila.utilidadTotal - datosBase[index - 1].utilidadTotal
}));
const calcularUMg = useCallback((q: number) => {
const fila = datosCompletos.find(d => d.cantidad === q);
return fila?.utilidadMarginal ?? 0;
}, [datosCompletos]);
const handleRespuesta = (cantidad: number, valor: string) => {
setRespuestas(prev => ({ ...prev, [cantidad]: valor }));
setVerificadas(prev => ({ ...prev, [cantidad]: false }));
};
const verificarRespuesta = (cantidad: number) => {
const respuesta = parseFloat(respuestas[cantidad]);
const correcta = calcularUMg(cantidad);
const esCorrecta = Math.abs(respuesta - correcta) < 0.1;
setVerificadas(prev => ({ ...prev, [cantidad]: esCorrecta }));
const todasCorrectas = datosCompletos
.filter(d => d.cantidad > 0)
.every(d => {
const r = parseFloat(respuestas[d.cantidad]);
return Math.abs(r - calcularUMg(d.cantidad)) < 0.1;
});
if (todasCorrectas && onComplete) {
onComplete(100);
}
};
const puntaje = Object.values(verificadas).filter(Boolean).length;
const total = datosCompletos.length - 1;
const porcentaje = Math.round((puntaje / total) * 100);
const maxUT = Math.max(...datosCompletos.map(d => d.utilidadTotal));
const maxQ = Math.max(...datosCompletos.map(d => d.cantidad));
return (
<Card className="max-w-4xl mx-auto">
<CardHeader
title="Utilidad Total vs Utilidad Marginal"
subtitle="Comprende la relación entre la utilidad total acumulada y la utilidad adicional de cada unidad consumida"
/>
<div className="space-y-6">
<div className="bg-blue-50 p-4 rounded-lg">
<h3 className="font-bold text-blue-900 mb-2">Conceptos Clave</h3>
<ul className="space-y-2 text-sm text-blue-800">
<li><strong>Utilidad Total (UT):</strong> Satisfacción total obtenida de consumir Q unidades de un bien.</li>
<li><strong>Utilidad Marginal (UMg):</strong> Utilidad adicional obtenida de consumir una unidad más.</li>
<li><strong>Fórmula:</strong> UMg = ΔUT / ΔQ = UT(Q) - UT(Q-1)</li>
</ul>
</div>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="border p-3 text-left">Cantidad (Q)</th>
<th className="border p-3 text-left">Utilidad Total (UT)</th>
<th className="border p-3 text-left">Calcular UMg</th>
<th className="border p-3 text-left">Estado</th>
</tr>
</thead>
<tbody>
{datosCompletos.map((fila) => (
<tr key={fila.cantidad} className={fila.cantidad === 0 ? 'bg-gray-50' : ''}>
<td className="border p-3 font-mono">{fila.cantidad}</td>
<td className="border p-3 font-mono">{fila.utilidadTotal}</td>
<td className="border p-3">
{fila.cantidad === 0 ? (
<span className="text-gray-500 text-sm">N/A (punto de partida)</span>
) : (
<div className="flex gap-2 items-center">
<Input
type="number"
step="0.1"
value={respuestas[fila.cantidad] || ''}
onChange={(e) => handleRespuesta(fila.cantidad, e.target.value)}
className="w-24"
placeholder="UMg"
/>
<Button
size="sm"
variant="outline"
onClick={() => verificarRespuesta(fila.cantidad)}
disabled={!respuestas[fila.cantidad]}
>
Verificar
</Button>
</div>
)}
</td>
<td className="border p-3">
{verificadas[fila.cantidad] === true && (
<span className="text-green-600 font-bold"> Correcto</span>
)}
{verificadas[fila.cantidad] === false && (
<span className="text-red-600 font-bold"> Incorrecto</span>
)}
{fila.cantidad > 0 && verificadas[fila.cantidad] === undefined && (
<span className="text-gray-400">-</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<p className="text-sm text-gray-600 mb-2">
Progreso: <strong>{puntaje}/{total}</strong> correctas ({porcentaje}%)
</p>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${porcentaje}%` }}
/>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setMostrarGrafico(!mostrarGrafico)}
>
{mostrarGrafico ? 'Ocultar' : 'Ver'} Gráfico
</Button>
<Button
variant="outline"
onClick={() => setMostrarExplicacion(!mostrarExplicacion)}
>
{mostrarExplicacion ? 'Ocultar' : 'Ver'} Explicación
</Button>
</div>
{mostrarGrafico && (
<div className="bg-white border rounded-lg p-4">
<h4 className="font-bold mb-4">Gráfico de Utilidad Total</h4>
<div className="relative h-64 w-full">
<svg viewBox="0 0 400 250" className="w-full h-full">
<line x1="40" y1="220" x2="380" y2="220" stroke="#333" strokeWidth="2" />
<line x1="40" y1="220" x2="40" y2="20" stroke="#333" strokeWidth="2" />
<text x="200" y="245" textAnchor="middle" className="text-xs fill-gray-600">Cantidad (Q)</text>
<text x="15" y="125" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90, 15, 125)">Utilidad Total</text>
{datosCompletos.map((d, i) => {
const x = 60 + (d.cantidad / maxQ) * 300;
const y = 210 - (d.utilidadTotal / maxUT) * 180;
return (
<g key={d.cantidad}>
<circle cx={x} cy={y} r="4" fill="#3b82f6" />
<text x={x} y={y - 10} textAnchor="middle" className="text-xs fill-blue-600 font-mono">
{d.utilidadTotal}
</text>
<text x={x} y="235" textAnchor="middle" className="text-xs fill-gray-600">{d.cantidad}</text>
</g>
);
})}
<polyline
points={datosCompletos.map(d => {
const x = 60 + (d.cantidad / maxQ) * 300;
const y = 210 - (d.utilidadTotal / maxUT) * 180;
return `${x},${y}`;
}).join(' ')}
fill="none"
stroke="#3b82f6"
strokeWidth="2"
/>
</svg>
</div>
<p className="text-sm text-gray-600 mt-2">
Observa cómo la curva de utilidad total aumenta a tasas decrecientes hasta alcanzar su máximo en Q=5 y Q=6.
</p>
</div>
)}
{mostrarExplicacion && (
<div className="bg-yellow-50 border border-yellow-300 p-4 rounded-lg">
<h4 className="font-bold text-yellow-900 mb-2">Cálculo paso a paso:</h4>
<div className="space-y-2 text-sm text-yellow-800">
{datosCompletos.filter(d => d.cantidad > 0).map((fila) => (
<p key={fila.cantidad}>
<strong>Q={fila.cantidad}:</strong> UMg = UT({fila.cantidad}) - UT({fila.cantidad - 1}) = {fila.utilidadTotal} - {datosCompletos[fila.cantidad - 1].utilidadTotal} = <strong>{fila.utilidadMarginal}</strong>
</p>
))}
</div>
<div className="mt-4 p-3 bg-white rounded">
<p className="font-semibold">Puntos importantes:</p>
<ul className="list-disc ml-5 mt-1 space-y-1">
<li>La UMg es positiva mientras la UT esté aumentando (Q=1 a 5)</li>
<li>La UMg es cero cuando la UT es máxima (Q=6)</li>
<li>La UMg es negativa cuando la UT disminuye (Q=7)</li>
</ul>
</div>
</div>
)}
</div>
</Card>
);
}
export default UtilidadTotalVsMarginal;

View File

@@ -1,3 +1,22 @@
export { UtilidadTotalVsMarginal } from './UtilidadTotalVsMarginal';
export { LeyUtilidadMarginalDecreciente } from './LeyUtilidadMarginalDecreciente';
export { CanastaOptima } from './CanastaOptima';
export { CurvasIndiferencia } from './CurvasIndiferencia';
export { MaximizacionUtilidad } from './MaximizacionUtilidad';
export { ElasticidadRectas } from './ElasticidadRectas';
export { ElasticidadCurva } from './ElasticidadCurva';
export { FormulaElasticidad } from './FormulaElasticidad';
export { MetodoPuntoMedio } from './MetodoPuntoMedio';
export { CalculadoraElasticidad } from './CalculadoraElasticidad';
export { ClasificacionElasticidad } from './ClasificacionElasticidad';
export { DecisionesPrecios } from './DecisionesPrecios';
export { FormulaElasticidadCruzada } from './FormulaElasticidadCruzada';
export { SustitutosComplementarios } from './SustitutosComplementarios';
export { GradoRelacion } from './GradoRelacion';
export { FormulaElasticidadIngreso } from './FormulaElasticidadIngreso';
export { BienesNormalesInferiores } from './BienesNormalesInferiores';
export { BienesLujoNecesarios } from './BienesLujoNecesarios';
export { CurvaEngel } from './CurvaEngel';
export { ParadojaAguaDiamantes } from './ParadojaAguaDiamantes';
export { ClasificadorBienes } from './ClasificadorBienes';
export { EjerciciosExamen } from './EjerciciosExamen';

View File

@@ -0,0 +1,213 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, Clock, Calendar, AlertCircle } from 'lucide-react';
import { QuizExercise, QuizOption } from '../common/QuizExercise';
interface CortoVsLargoPlazoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FactorItem {
id: string;
nombre: string;
tipo: 'fijo' | 'variable';
descripcion: string;
}
const factores: FactorItem[] = [
{ id: '1', nombre: 'Edificio de fábrica', tipo: 'fijo', descripcion: 'No se puede cambiar en el corto plazo' },
{ id: '2', nombre: 'Maquinaria especializada', tipo: 'fijo', descripcion: 'Requiere tiempo para adquirir o vender' },
{ id: '3', nombre: 'Trabajadores temporales', tipo: 'variable', descripcion: 'Se pueden contratar/despedir rápidamente' },
{ id: '4', nombre: 'Materias primas', tipo: 'variable', descripcion: 'Se ajustan según la producción' },
{ id: '5', nombre: 'Contrato de arrendamiento', tipo: 'fijo', descripcion: 'Compromiso a largo plazo' },
{ id: '6', nombre: 'Horas extras', tipo: 'variable', descripcion: 'Se pueden aumentar o disminuir' },
];
export function CortoVsLargoPlazo({ ejercicioId: _ejercicioId, onComplete }: CortoVsLargoPlazoProps) {
const [asignaciones, setAsignaciones] = useState<Record<string, 'fijo' | 'variable' | null>>({});
const [showResults, setShowResults] = useState(false);
const [puntuacion, setPuntuacion] = useState(0);
const handleAsignar = (id: string, tipo: 'fijo' | 'variable') => {
if (showResults) return;
setAsignaciones(prev => ({ ...prev, [id]: tipo }));
};
const handleVerificar = () => {
let correctas = 0;
factores.forEach(factor => {
if (asignaciones[factor.id] === factor.tipo) {
correctas++;
}
});
const puntaje = Math.round((correctas / factores.length) * 100);
setPuntuacion(puntaje);
setShowResults(true);
if (onComplete && puntaje >= 70) {
onComplete(puntaje);
}
};
const handleReiniciar = () => {
setAsignaciones({});
setShowResults(false);
setPuntuacion(0);
};
const todasAsignadas = factores.every(f => asignaciones[f.id] !== undefined);
const quizOptions: QuizOption[] = [
{ id: 'a', text: 'En el corto plazo todos los factores son variables', isCorrect: false },
{ id: 'b', text: 'En el corto plazo al menos un factor es fijo', isCorrect: true },
{ id: 'c', text: 'En el largo plazo no hay factores variables', isCorrect: false },
{ id: 'd', text: 'El tiempo determina si un factor es fijo o variable', isCorrect: false },
];
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Corto Plazo vs Largo Plazo"
subtitle="Clasifica los factores de producción según su variabilidad"
/>
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="bg-orange-50 p-4 rounded-lg border border-orange-200">
<div className="flex items-center gap-2 mb-2">
<Clock className="w-5 h-5 text-orange-600" />
<h4 className="font-semibold text-orange-800">Corto Plazo</h4>
</div>
<p className="text-sm text-orange-700">
Periodo en el que al menos un factor de producción es <strong>fijo</strong>.
No se puede cambiar la cantidad de todos los factores.
</p>
</div>
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
<div className="flex items-center gap-2 mb-2">
<Calendar className="w-5 h-5 text-green-600" />
<h4 className="font-semibold text-green-800">Largo Plazo</h4>
</div>
<p className="text-sm text-green-700">
Periodo en el que <strong>todos los factores son variables</strong>.
La empresa puede ajustar todas sus capacidades productivas.
</p>
</div>
</div>
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-4">
Clasifica cada factor como Fijo o Variable en el corto plazo:
</h4>
<div className="space-y-3">
{factores.map((factor) => (
<div
key={factor.id}
className={`p-4 rounded-lg border-2 transition-all ${
showResults
? asignaciones[factor.id] === factor.tipo
? 'border-success bg-success/10'
: 'border-error bg-error/10'
: 'border-gray-200 hover:border-blue-300'
}`}
>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">{factor.nombre}</p>
<p className="text-sm text-gray-500">{factor.descripcion}</p>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant={asignaciones[factor.id] === 'fijo' ? 'primary' : 'outline'}
onClick={() => handleAsignar(factor.id, 'fijo')}
disabled={showResults}
>
Fijo
</Button>
<Button
size="sm"
variant={asignaciones[factor.id] === 'variable' ? 'primary' : 'outline'}
onClick={() => handleAsignar(factor.id, 'variable')}
disabled={showResults}
>
Variable
</Button>
</div>
</div>
{showResults && (
<p className={`text-sm mt-2 ${
asignaciones[factor.id] === factor.tipo ? 'text-success' : 'text-error'
}`}>
{asignaciones[factor.id] === factor.tipo
? '✓ Correcto'
: `✗ Incorrecto. Es un factor ${factor.tipo}`}
</p>
)}
</div>
))}
</div>
</div>
{!showResults ? (
<div className="flex justify-end">
<Button
onClick={handleVerificar}
disabled={!todasAsignadas}
size="lg"
>
Verificar Respuestas
</Button>
</div>
) : (
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center justify-between">
<div>
<p className="font-semibold text-gray-900">
Puntuación: {puntuacion}%
</p>
<p className="text-sm text-gray-600">
{puntuacion >= 70
? '¡Buen trabajo! Has comprendido la diferencia entre factores fijos y variables.'
: 'Repasa los conceptos e intenta de nuevo.'}
</p>
</div>
<Button onClick={handleReiniciar} variant="outline">
Reiniciar
</Button>
</div>
</div>
)}
</Card>
<QuizExercise
question="¿Cuál es la característica distintiva del corto plazo en la teoría de la producción?"
options={quizOptions}
explanation="En el corto plazo, al menos un factor de producción es fijo (generalmente el capital o la planta), mientras que otros factores como el trabajo pueden variar. Esto contrasta con el largo plazo donde todos los factores son variables."
onComplete={(result) => {
if (result.correct && onComplete && puntuacion >= 70) {
onComplete(Math.max(puntuacion, result.score));
}
}}
exerciseId="corto-largo-plazo-quiz"
/>
<div className="flex justify-end">
<Button
onClick={() => onComplete?.(Math.max(puntuacion, 100))}
size="lg"
disabled={puntuacion < 70}
>
<CheckCircle className="w-5 h-5 mr-2" />
Completar Ejercicio
</Button>
</div>
</div>
);
}
export default CortoVsLargoPlazo;

View File

@@ -0,0 +1,212 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Calculator, CheckCircle, XCircle } from 'lucide-react';
export function CostoTotalMedioMarginal() {
const [respuestas, setRespuestas] = useState<{[key: string]: string}>({
cme_q2: '',
cme_q4: '',
cmg_q3: '',
cmg_q5: '',
});
const [mostrarResultados, setMostrarResultados] = useState(false);
const datos = [
{ q: 0, ct: 100 },
{ q: 1, ct: 150 },
{ q: 2, ct: 180 },
{ q: 3, ct: 220 },
{ q: 4, ct: 300 },
{ q: 5, ct: 450 },
];
// Cálculos correctos
const respuestasCorrectas: { [key: string]: string } = {
cme_q2: '90', // 180/2
cme_q4: '75', // 300/4
cmg_q3: '40', // 220-180
cmg_q5: '150', // 450-300
};
const handleInputChange = (campo: string, valor: string) => {
setRespuestas(prev => ({ ...prev, [campo]: valor }));
setMostrarResultados(false);
};
const validar = () => {
setMostrarResultados(true);
};
const todasCompletadas = Object.values(respuestas).every(r => r !== '');
const esCorrecto = (campo: string) => {
return respuestas[campo] === respuestasCorrectas[campo];
};
const correctas = Object.keys(respuestasCorrectas).filter(esCorrecto).length;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Costo Total, Medio y Marginal"
subtitle="Calcula CMe (CT/Q) y CMg (ΔCT/ΔQ) a partir de los datos"
/>
<div className="space-y-6">
{/* Datos base */}
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-4 py-2 text-left font-medium text-gray-700">Cantidad (Q)</th>
<th className="px-4 py-2 text-left font-medium text-gray-700">Costo Total (CT)</th>
<th className="px-4 py-2 text-left font-medium text-gray-700">CF (100)</th>
<th className="px-4 py-2 text-left font-medium text-gray-700">CV</th>
</tr>
</thead>
<tbody>
{datos.map((fila) => (
<tr key={fila.q} className="border-b">
<td className="px-4 py-2 font-medium">{fila.q}</td>
<td className="px-4 py-2">${fila.ct}</td>
<td className="px-4 py-2 text-gray-600">$100</td>
<td className="px-4 py-2 text-gray-600">${fila.ct - 100}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Preguntas */}
<div className="bg-amber-50 p-4 rounded-lg border border-amber-200">
<h4 className="font-semibold text-amber-900 mb-4 flex items-center gap-2">
<Calculator className="w-5 h-5" />
Calcula los siguientes valores:
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white p-4 rounded-lg border">
<p className="text-sm text-gray-700 mb-2">1. CMe cuando Q = 2</p>
<p className="text-xs text-gray-500 mb-2">Fórmula: CT / Q = 180 / 2</p>
<div className="flex items-center gap-2">
<span className="text-lg">$</span>
<input
type="text"
value={respuestas.cme_q2}
onChange={(e) => handleInputChange('cme_q2', e.target.value)}
className={`w-20 px-2 py-1 border rounded ${
mostrarResultados && esCorrecto('cme_q2')
? 'border-green-500 bg-green-50'
: mostrarResultados && !esCorrecto('cme_q2')
? 'border-red-500 bg-red-50'
: 'border-gray-300'
}`}
disabled={mostrarResultados}
/>
{mostrarResultados && esCorrecto('cme_q2') && <CheckCircle className="w-5 h-5 text-green-600" />}
{mostrarResultados && !esCorrecto('cme_q2') && <XCircle className="w-5 h-5 text-red-600" />}
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<p className="text-sm text-gray-700 mb-2">2. CMe cuando Q = 4</p>
<p className="text-xs text-gray-500 mb-2">Fórmula: CT / Q = 300 / 4</p>
<div className="flex items-center gap-2">
<span className="text-lg">$</span>
<input
type="text"
value={respuestas.cme_q4}
onChange={(e) => handleInputChange('cme_q4', e.target.value)}
className={`w-20 px-2 py-1 border rounded ${
mostrarResultados && esCorrecto('cme_q4')
? 'border-green-500 bg-green-50'
: mostrarResultados && !esCorrecto('cme_q4')
? 'border-red-500 bg-red-50'
: 'border-gray-300'
}`}
disabled={mostrarResultados}
/>
{mostrarResultados && esCorrecto('cme_q4') && <CheckCircle className="w-5 h-5 text-green-600" />}
{mostrarResultados && !esCorrecto('cme_q4') && <XCircle className="w-5 h-5 text-red-600" />}
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<p className="text-sm text-gray-700 mb-2">3. CMg del 2do al 3er trabajador</p>
<p className="text-xs text-gray-500 mb-2">Fórmula: CT - CT = 220 - 180</p>
<div className="flex items-center gap-2">
<span className="text-lg">$</span>
<input
type="text"
value={respuestas.cmg_q3}
onChange={(e) => handleInputChange('cmg_q3', e.target.value)}
className={`w-20 px-2 py-1 border rounded ${
mostrarResultados && esCorrecto('cmg_q3')
? 'border-green-500 bg-green-50'
: mostrarResultados && !esCorrecto('cmg_q3')
? 'border-red-500 bg-red-50'
: 'border-gray-300'
}`}
disabled={mostrarResultados}
/>
{mostrarResultados && esCorrecto('cmg_q3') && <CheckCircle className="w-5 h-5 text-green-600" />}
{mostrarResultados && !esCorrecto('cmg_q3') && <XCircle className="w-5 h-5 text-red-600" />}
</div>
</div>
<div className="bg-white p-4 rounded-lg border">
<p className="text-sm text-gray-700 mb-2">4. CMg del 4to al 5to trabajador</p>
<p className="text-xs text-gray-500 mb-2">Fórmula: CT - CT = 450 - 300</p>
<div className="flex items-center gap-2">
<span className="text-lg">$</span>
<input
type="text"
value={respuestas.cmg_q5}
onChange={(e) => handleInputChange('cmg_q5', e.target.value)}
className={`w-20 px-2 py-1 border rounded ${
mostrarResultados && esCorrecto('cmg_q5')
? 'border-green-500 bg-green-50'
: mostrarResultados && !esCorrecto('cmg_q5')
? 'border-red-500 bg-red-50'
: 'border-gray-300'
}`}
disabled={mostrarResultados}
/>
{mostrarResultados && esCorrecto('cmg_q5') && <CheckCircle className="w-5 h-5 text-green-600" />}
{mostrarResultados && !esCorrecto('cmg_q5') && <XCircle className="w-5 h-5 text-red-600" />}
</div>
</div>
</div>
</div>
<Button onClick={validar} disabled={!todasCompletadas || mostrarResultados}>
Validar Respuestas
</Button>
{mostrarResultados && (
<div className={`p-4 rounded-lg border ${correctas === 4 ? 'bg-green-50 border-green-200' : 'bg-amber-50 border-amber-200'}`}>
<p className="font-semibold">Resultado: {correctas}/4 correctas</p>
{correctas < 4 && (
<p className="text-sm mt-2">Las respuestas correctas son: CMe(Q=2)=$90, CMe(Q=4)=$75, CMg(23)=$40, CMg(45)=$150</p>
)}
</div>
)}
</div>
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Fórmulas Importantes</h4>
<div className="space-y-1 text-sm text-blue-800">
<p><strong>Costo Medio (CMe):</strong> CMe = CT / Q</p>
<p><strong>Costo Marginal (CMg):</strong> CMg = ΔCT / ΔQ = CTₙ - CTₙ</p>
<p className="mt-2 text-blue-700">Observa cómo el CMg aumenta significativamente del 4to al 5to trabajador ($150 vs $40),
mostrando los rendimientos decrecientes.</p>
</div>
</Card>
</div>
);
}
export default CostoTotalMedioMarginal;

View File

@@ -0,0 +1,180 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, DollarSign } from 'lucide-react';
export function CostosFijosVsVariables() {
const [clasificaciones, setClasificaciones] = useState<{[key: string]: 'fijo' | 'variable' | null}>({
alquiler: null,
materias: null,
salarios: null,
luz: null,
depreciacion: null,
publicidad: null,
});
const [mostrarResultados, setMostrarResultados] = useState(false);
const conceptos = [
{ id: 'alquiler', nombre: 'Alquiler del local', tipo: 'fijo' as const, explicacion: 'El alquiler se paga mensualmente independientemente de cuánto produzcas.' },
{ id: 'materias', nombre: 'Materias primas', tipo: 'variable' as const, explicacion: 'A más producción, más materias primas necesitas.' },
{ id: 'salarios', nombre: 'Salarios de obreros temporales', tipo: 'variable' as const, explicacion: 'Los obreros temporales se contratan según la demanda de producción.' },
{ id: 'luz', nombre: 'Electricidad de máquinas', tipo: 'variable' as const, explicacion: 'Más horas de producción = más consumo eléctrico.' },
{ id: 'depreciacion', nombre: 'Depreciación de maquinaria', tipo: 'fijo' as const, explicacion: 'La depreciación ocurre con el paso del tiempo, no con la cantidad producida.' },
{ id: 'publicidad', nombre: 'Publicidad (contrato anual)', tipo: 'fijo' as const, explicacion: 'El contrato de publicidad es un costo fijo por período.' },
];
const clasificar = (id: string, tipo: 'fijo' | 'variable') => {
setClasificaciones(prev => ({ ...prev, [id]: tipo }));
setMostrarResultados(false);
};
const validar = () => {
setMostrarResultados(true);
};
const todasClasificadas = Object.values(clasificaciones).every(c => c !== null);
const correctas = conceptos.filter(c => clasificaciones[c.id] === c.tipo).length;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Costos Fijos vs Variables"
subtitle="Clasifica cada costo como FIJO (no varía con la producción) o VARIABLE (cambia con la producción)"
/>
<div className="space-y-6">
{/* Gráfico comparativo */}
<div className="bg-gray-50 rounded-lg p-4">
<svg className="w-full h-48" viewBox="0 0 600 180">
{/* Título */}
<text x="300" y="20" textAnchor="middle" className="text-base font-bold fill-gray-800">Comportamiento de Costos Fijos y Variables</text>
{/* Gráfico CF */}
<text x="80" y="45" textAnchor="middle" className="text-sm font-bold fill-blue-700">Costo Fijo (CF)</text>
<line x1="30" y1="150" x2="160" y2="150" stroke="#374151" strokeWidth="2" />
<line x1="30" y1="150" x2="30" y2="50" stroke="#374151" strokeWidth="2" />
<text x="100" y="165" textAnchor="middle" className="text-xs fill-gray-600">Q</text>
<text x="15" y="100" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90 15 100)">$</text>
{/* Línea horizontal CF */}
<line x1="30" y1="80" x2="150" y2="80" stroke="#2563eb" strokeWidth="3" />
<text x="95" y="70" textAnchor="middle" className="text-xs fill-blue-600">CF = 1000</text>
{/* Gráfico CV */}
<text x="280" y="45" textAnchor="middle" className="text-sm font-bold fill-green-700">Costo Variable (CV)</text>
<line x1="200" y1="150" x2="330" y2="150" stroke="#374151" strokeWidth="2" />
<line x1="200" y1="150" x2="200" y2="50" stroke="#374151" strokeWidth="2" />
<text x="270" y="165" textAnchor="middle" className="text-xs fill-gray-600">Q</text>
<text x="185" y="100" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90 185 100)">$</text>
{/* Línea creciente CV */}
<path d="M 200,150 L 220,130 L 250,100 L 290,60 L 320,30" fill="none" stroke="#16a34a" strokeWidth="3" />
{/* Gráfico CT */}
<text x="480" y="45" textAnchor="middle" className="text-sm font-bold fill-purple-700">Costo Total (CT)</text>
<line x1="370" y1="150" x2="550" y2="150" stroke="#374151" strokeWidth="2" />
<line x1="370" y1="150" x2="370" y2="50" stroke="#374151" strokeWidth="2" />
<text x="480" y="165" textAnchor="middle" className="text-xs fill-gray-600">Q</text>
<text x="355" y="100" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90 355 100)">$</text>
{/* Línea CT = CF + CV */}
<path d="M 370,80 L 390,70 L 420,55 L 460,40 L 520,30" fill="none" stroke="#9333ea" strokeWidth="3" />
{/* Línea punteada CF */}
<line x1="370" y1="80" x2="550" y2="80" stroke="#2563eb" strokeWidth="1" strokeDasharray="4" opacity="0.5" />
<text x="540" y="75" textAnchor="end" className="text-xs fill-blue-600">CF</text>
</svg>
</div>
{/* Ejercicio de clasificación */}
<div className="space-y-3">
{conceptos.map((concepto) => {
const esCorrecto = mostrarResultados && clasificaciones[concepto.id] === concepto.tipo;
const esIncorrecto = mostrarResultados && clasificaciones[concepto.id] !== concepto.tipo && clasificaciones[concepto.id] !== null;
return (
<div key={concepto.id} className="bg-white border rounded-lg p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<DollarSign className="w-5 h-5 text-gray-500" />
<span className="font-medium text-gray-900">{concepto.nombre}</span>
</div>
<div className="flex gap-2">
<button
onClick={() => clasificar(concepto.id, 'fijo')}
disabled={mostrarResultados}
className={`px-4 py-2 rounded-lg border-2 font-medium transition-all ${
clasificaciones[concepto.id] === 'fijo' && !mostrarResultados
? 'border-blue-500 bg-blue-50'
: mostrarResultados && concepto.tipo === 'fijo'
? 'border-green-500 bg-green-50'
: mostrarResultados && clasificaciones[concepto.id] === 'fijo' && concepto.tipo !== 'fijo'
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
FIJO
{mostrarResultados && concepto.tipo === 'fijo' && (
<CheckCircle className="w-4 h-4 text-green-600 inline ml-1" />
)}
</button>
<button
onClick={() => clasificar(concepto.id, 'variable')}
disabled={mostrarResultados}
className={`px-4 py-2 rounded-lg border-2 font-medium transition-all ${
clasificaciones[concepto.id] === 'variable' && !mostrarResultados
? 'border-blue-500 bg-blue-50'
: mostrarResultados && concepto.tipo === 'variable'
? 'border-green-500 bg-green-50'
: mostrarResultados && clasificaciones[concepto.id] === 'variable' && concepto.tipo !== 'variable'
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
VARIABLE
{mostrarResultados && concepto.tipo === 'variable' && (
<CheckCircle className="w-4 h-4 text-green-600 inline ml-1" />
)}
</button>
</div>
</div>
{mostrarResultados && (
<div className={`mt-3 p-2 rounded text-sm ${esCorrecto ? 'bg-green-50 text-green-800' : 'bg-amber-50 text-amber-800'}`}>
<strong>{concepto.tipo === 'fijo' ? 'FIJO' : 'VARIABLE'}:</strong> {concepto.explicacion}
</div>
)}
</div>
);
})}
</div>
<Button onClick={validar} disabled={!todasClasificadas || mostrarResultados}>
Validar Clasificación
</Button>
{mostrarResultados && (
<div className={`p-4 rounded-lg border ${correctas === 6 ? 'bg-green-50 border-green-200' : 'bg-amber-50 border-amber-200'}`}>
<div className="flex items-center gap-2 mb-2">
{correctas === 6 ? (
<CheckCircle className="w-5 h-5 text-green-600" />
) : (
<XCircle className="w-5 h-5 text-amber-600" />
)}
<span className="font-semibold">Resultado: {correctas}/6 correctas</span>
</div>
</div>
)}
</div>
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Definiciones Clave</h4>
<div className="space-y-2 text-sm">
<p><strong className="text-blue-800">Costo Fijo (CF):</strong> <span className="text-blue-700">No depende del nivel de producción. Se incurren aunque Q = 0.</span></p>
<p><strong className="text-blue-800">Costo Variable (CV):</strong> <span className="text-blue-700">Varía directamente con la cantidad producida. CV = 0 cuando Q = 0.</span></p>
<p><strong className="text-blue-800">Costo Total (CT):</strong> <span className="text-blue-700">CT = CF + CV</span></p>
</div>
</Card>
</div>
);
}
export default CostosFijosVsVariables;

View File

@@ -0,0 +1,204 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, PieChart } from 'lucide-react';
export function CostosMedios() {
const [respuesta, setRespuesta] = useState<string | null>(null);
const [mostrarResultado, setMostrarResultado] = useState(false);
const pregunta = {
texto: 'Según la gráfica, ¿cuál es la relación entre CFMe, CVMe y CMe en Q=4?',
opciones: [
{ id: 'a', texto: 'CFMe > CVMe > CMe', correcta: false },
{ id: 'b', texto: 'CMe = CFMe + CVMe', correcta: true },
{ id: 'c', texto: 'CVMe = CFMe + CMe', correcta: false },
{ id: 'd', texto: 'CFMe = CVMe = CMe', correcta: false },
],
explicacion: 'Correcto. El Costo Medio (CMe) es la suma del Costo Fijo Medio (CFMe) y el Costo Variable Medio (CVMe): CMe = CFMe + CVMe'
};
// Datos para la gráfica
const datos = [
{ q: 1, cfme: 100, cvme: 50, cme: 150 },
{ q: 2, cfme: 50, cvme: 40, cme: 90 },
{ q: 3, cfme: 33.33, cvme: 35, cme: 68.33 },
{ q: 4, cfme: 25, cvme: 32.5, cme: 57.5 },
{ q: 5, cfme: 20, cvme: 35, cme: 55 },
{ q: 6, cfme: 16.67, cvme: 42.5, cme: 59.17 },
];
const validar = () => {
setMostrarResultado(true);
};
const esCorrecta = respuesta === 'b';
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Costos Medios: CFMe, CVMe y CMe"
subtitle="Analiza la composición del costo medio y su relación con los costos fijos y variables"
/>
<div className="space-y-6">
{/* Gráfico de barras apiladas */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 mb-4 text-center">Descomposición del Costo Medio</h4>
<svg className="w-full h-72" viewBox="0 0 600 250">
{/* Ejes */}
<line x1="60" y1="220" x2="550" y2="220" stroke="#374151" strokeWidth="2" />
<line x1="60" y1="220" x2="60" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas */}
<text x="305" y="245" textAnchor="middle" className="text-sm fill-gray-700 font-medium">Cantidad (Q)</text>
<text x="25" y="120" textAnchor="middle" className="text-sm fill-gray-700 font-medium" transform="rotate(-90 25 120)">Costo Medio ($)</text>
{/* Barras apiladas */}
{datos.map((d, i) => {
const x = 90 + i * 80;
const alturaCVMe = (d.cvme / 160) * 180;
const alturaCFMe = (d.cfme / 160) * 180;
const alturaTotal = alturaCVMe + alturaCFMe;
return (
<g key={i}>
{/* CFMe (parte superior) */}
<rect
x={x - 25}
y={220 - alturaTotal}
width="50"
height={alturaCFMe}
fill="#3b82f6"
stroke="white"
strokeWidth="1"
/>
{/* CVMe (parte inferior) */}
<rect
x={x - 25}
y={220 - alturaCVMe}
width="50"
height={alturaCVMe}
fill="#22c55e"
stroke="white"
strokeWidth="1"
/>
{/* Etiqueta Q */}
<text x={x} y="235" textAnchor="middle" className="text-sm fill-gray-700 font-bold">{d.q}</text>
{/* Valor CMe */}
<text x={x} y={220 - alturaTotal - 5} textAnchor="middle" className="text-xs fill-gray-700 font-bold">
${d.cme.toFixed(1)}
</text>
</g>
);
})}
{/* Línea de CMe */}
<polyline
fill="none"
stroke="#7c3aed"
strokeWidth="3"
points={datos.map((d, i) => {
const x = 90 + i * 80;
const alturaTotal = ((d.cvme + d.cfme) / 160) * 180;
return `${x},${220 - alturaTotal}`;
}).join(' ')}
/>
{/* Leyenda */}
<g transform="translate(420, 40)">
<rect x="0" y="0" width="15" height="15" fill="#3b82f6" />
<text x="20" y="12" className="text-xs fill-gray-700">CFMe</text>
<rect x="0" y="25" width="15" height="15" fill="#22c55e" />
<text x="20" y="37" className="text-xs fill-gray-700">CVMe</text>
<line x1="0" y1="55" x2="20" y2="55" stroke="#7c3aed" strokeWidth="3" />
<text x="25" y="59" className="text-xs fill-gray-700">CMe = CFMe + CVMe</text>
</g>
</svg>
</div>
{/* Observaciones */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
<h5 className="font-semibold text-blue-900 mb-2">CFMe (Costo Fijo Medio)</h5>
<p className="text-sm text-blue-800">CFMe = CF / Q</p>
<p className="text-sm text-blue-700 mt-1">Siempre <strong>decreciente</strong>. A mayor producción, el costo fijo se "reparte" entre más unidades.</p>
</div>
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
<h5 className="font-semibold text-green-900 mb-2">CVMe (Costo Variable Medio)</h5>
<p className="text-sm text-green-800">CVMe = CV / Q</p>
<p className="text-sm text-green-700 mt-1">Tiene forma de <strong>U</strong>. Primero baja por eficiencias, luego sube por rendimientos decrecientes.</p>
</div>
</div>
{/* Pregunta */}
<div className="bg-white border rounded-lg p-4">
<h4 className="font-semibold text-gray-900 mb-3">{pregunta.texto}</h4>
<div className="space-y-2">
{pregunta.opciones.map((opcion) => (
<button
key={opcion.id}
onClick={() => {
setRespuesta(opcion.id);
setMostrarResultado(false);
}}
disabled={mostrarResultado}
className={`w-full p-3 rounded-lg border-2 text-left transition-all ${
respuesta === opcion.id && !mostrarResultado
? 'border-blue-500 bg-blue-50'
: mostrarResultado && opcion.correcta
? 'border-green-500 bg-green-50'
: mostrarResultado && respuesta === opcion.id && !opcion.correcta
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<span className="font-medium">{opcion.id})</span> {opcion.texto}
{mostrarResultado && opcion.correcta && <CheckCircle className="w-5 h-5 text-green-600 inline ml-2" />}
{mostrarResultado && respuesta === opcion.id && !opcion.correcta && <XCircle className="w-5 h-5 text-red-600 inline ml-2" />}
</button>
))}
</div>
<Button onClick={validar} disabled={!respuesta || mostrarResultado} className="mt-4">
Validar Respuesta
</Button>
{mostrarResultado && (
<div className={`mt-4 p-4 rounded-lg border ${esCorrecta ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}>
<div className="flex items-center gap-2 mb-2">
{esCorrecta ? <CheckCircle className="w-5 h-5 text-green-600" /> : <XCircle className="w-5 h-5 text-red-600" />}
<span className={`font-semibold ${esCorrecta ? 'text-green-800' : 'text-red-800'}`}>
{esCorrecta ? '¡Correcto!' : 'Incorrecto'}
</span>
</div>
<p className={`text-sm ${esCorrecta ? 'text-green-700' : 'text-red-700'}`}>{pregunta.explicacion}</p>
</div>
)}
</div>
</div>
</Card>
<Card className="bg-purple-50 border-purple-200">
<div className="flex items-center gap-2 mb-2">
<PieChart className="w-5 h-5 text-purple-600" />
<h4 className="font-semibold text-purple-900">Resumen de Fórmulas</h4>
</div>
<div className="space-y-1 text-sm text-purple-800">
<p><strong>CFMe</strong> = CF / Q (siempre decreciente)</p>
<p><strong>CVMe</strong> = CV / Q (forma de U)</p>
<p><strong>CMe</strong> = CFMe + CVMe = CT / Q (forma de U)</p>
<p className="mt-2 text-purple-700">Observa cómo CFMe se vuelve insignificante a altos niveles de producción,
mientras que CVMe domina el costo medio.</p>
</div>
</Card>
</div>
);
}
export default CostosMedios;

View File

@@ -0,0 +1,274 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, TrendingUp, RotateCcw, Calculator } from 'lucide-react';
interface CurvaCostoLargoPlazoProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface DatosEscala {
q: number;
cme: number;
}
export function CurvaCostoLargoPlazo({ ejercicioId: _ejercicioId, onComplete }: CurvaCostoLargoPlazoProps) {
const datosBase: DatosEscala[] = [
{ q: 1, cme: 120 },
{ q: 2, cme: 85 },
{ q: 3, cme: 70 },
{ q: 4, cme: 65 },
{ q: 5, cme: 62 },
{ q: 6, cme: 60 },
{ q: 7, cme: 61 },
{ q: 8, cme: 64 },
{ q: 9, cme: 69 },
{ q: 10, cme: 75 },
];
const [respuestas, setRespuestas] = useState<{[key: string]: string}>({
cmeMinimo: '',
cantidadOptima: '',
ctQ5: '',
});
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const datosCalculados = useMemo(() => {
return datosBase.map(d => ({
...d,
ct: d.q * d.cme,
}));
}, []);
const cmeMinimo = useMemo(() => {
return Math.min(...datosBase.map(d => d.cme));
}, []);
const cantidadOptima = useMemo(() => {
const minCME = Math.min(...datosBase.map(d => d.cme));
return datosBase.find(d => d.cme === minCME)?.q || 0;
}, []);
const handleRespuestaChange = (campo: string, valor: string) => {
setRespuestas(prev => ({ ...prev, [campo]: valor }));
setValidado(false);
};
const validarRespuestas = () => {
const nuevosErrores: string[] = [];
if (parseFloat(respuestas.cmeMinimo) !== cmeMinimo) {
nuevosErrores.push('El CMe mínimo no es correcto. Observa la curva U.');
}
if (parseFloat(respuestas.cantidadOptima) !== cantidadOptima) {
nuevosErrores.push('La cantidad óptima no es correcta. Es donde el CMe es mínimo.');
}
if (parseFloat(respuestas.ctQ5) !== 310) {
nuevosErrores.push('El CT a Q=5 es incorrecto. Recuerda: CT = CMe × Q');
}
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0 && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setRespuestas({ cmeMinimo: '', cantidadOptima: '', ctQ5: '' });
setValidado(false);
setErrores([]);
};
const maxCMe = Math.max(...datosBase.map(d => d.cme));
const escalaY = 120 / maxCMe;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Curva de Costo Largo Plazo (CMe LP)"
subtitle="La curva en U del costo medio a largo plazo"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Concepto</span>
</div>
<p className="text-sm text-blue-700">
A largo plazo todos los factores son variables. La curva CMeLP tiene forma de U
debido a las economías y deseconomías de escala. El punto mínimo representa la
escala eficiente de producción.
</p>
</div>
<div className="h-64 bg-gray-50 rounded-lg p-4 mb-6">
<svg className="w-full h-full" viewBox="0 0 400 200">
<line x1="40" y1="180" x2="380" y2="180" stroke="#374151" strokeWidth="2" />
<line x1="40" y1="180" x2="40" y2="20" stroke="#374151" strokeWidth="2" />
<text x="210" y="195" textAnchor="middle" className="text-sm fill-gray-600 font-medium">Cantidad (Q)</text>
<text x="15" y="100" textAnchor="middle" className="text-sm fill-gray-600 font-medium" transform="rotate(-90 15 100)">CMe ($)</text>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((q, i) => (
<g key={q}>
<line x1={60 + i * 30} y1="180" x2={60 + i * 30} y2="185" stroke="#374151" strokeWidth="1" />
<text x={60 + i * 30} y="195" textAnchor="middle" className="text-xs fill-gray-500">{q}</text>
</g>
))}
{[20, 40, 60, 80, 100, 120].map((val, i) => (
<g key={val}>
<line x1="35" y1={180 - val * escalaY} x2="40" y2={180 - val * escalaY} stroke="#374151" strokeWidth="1" />
<text x="30" y={185 - val * escalaY} textAnchor="end" className="text-xs fill-gray-500">{val}</text>
</g>
))}
<path
d={`M ${60},${180 - datosBase[0].cme * escalaY} ${datosBase.slice(1).map((d, i) => `L ${60 + (i + 1) * 30},${180 - d.cme * escalaY}`).join(' ')}`}
fill="none"
stroke="#7c3aed"
strokeWidth="3"
/>
{datosBase.map((d, i) => (
<circle
key={i}
cx={60 + i * 30}
cy={180 - d.cme * escalaY}
r="5"
fill="#7c3aed"
stroke="white"
strokeWidth="2"
/>
))}
<circle
cx={60 + 5 * 30}
cy={180 - 60 * escalaY}
r="8"
fill="#10b981"
stroke="white"
strokeWidth="3"
/>
<text x={210} y={170 - 60 * escalaY} textAnchor="middle" className="text-xs fill-green-600 font-bold">
Mínimo CMe = $60
</text>
</svg>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CMe ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CT ($)</th>
</tr>
</thead>
<tbody>
{datosCalculados.map((d, i) => (
<tr key={i} className={`border-b hover:bg-gray-50 ${d.cme === cmeMinimo ? 'bg-green-50' : ''}`}>
<td className="px-3 py-2 font-medium">{d.q}</td>
<td className="px-3 py-2 font-medium text-primary">{d.cme}</td>
<td className="px-3 py-2 text-gray-600">{d.ct}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gradient-to-r from-purple-50 to-blue-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Calculator className="w-5 h-5 text-purple-600" />
Responde las siguientes preguntas:
</h4>
<div className="grid md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿Cuál es el CMe mínimo? ($)
</label>
<Input
type="number"
value={respuestas.cmeMinimo}
onChange={(e) => handleRespuestaChange('cmeMinimo', e.target.value)}
className="w-full"
placeholder="Ej: 60"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿A qué cantidad ocurre? (Q)
</label>
<Input
type="number"
value={respuestas.cantidadOptima}
onChange={(e) => handleRespuestaChange('cantidadOptima', e.target.value)}
className="w-full"
placeholder="Ej: 6"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿CT cuando Q = 5? ($)
</label>
<Input
type="number"
value={respuestas.ctQ5}
onChange={(e) => handleRespuestaChange('ctQ5', e.target.value)}
className="w-full"
placeholder="CMe × Q"
/>
</div>
</div>
</div>
<div className="mt-4 flex gap-3">
<Button onClick={validarRespuestas} variant="primary">
<CheckCircle className="w-4 h-4 mr-2" />
Validar Respuestas
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Reiniciar
</Button>
</div>
{validado && errores.length === 0 && (
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
<div className="flex items-center gap-2 text-success">
<CheckCircle className="w-5 h-5" />
<span className="font-medium">¡Correcto! La escala eficiente es Q = 6 con CMe = $60</span>
</div>
</div>
)}
{validado && errores.length > 0 && (
<div className="mt-4 p-4 bg-error/10 border border-error rounded-lg">
<p className="font-medium text-error mb-2">Revisa tus respuestas:</p>
<ul className="list-disc list-inside text-sm text-error">
{errores.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
)}
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Fórmulas importantes:</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li><strong>CT</strong> = CMe × Q (Costo Total)</li>
<li><strong>CMe LP</strong> = Costo medio a largo plazo (todas las plantas posibles)</li>
<li><strong>Escala eficiente</strong>: Cantidad donde CMe es mínimo</li>
</ul>
</Card>
</div>
);
}
export default CurvaCostoLargoPlazo;

View File

@@ -0,0 +1,218 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { TrendingUp, CheckCircle, DollarSign } from 'lucide-react';
export function CurvasCosto() {
const [etapaActiva, setEtapaActiva] = useState<string | null>(null);
// Datos para las curvas
const datosCT = [
{ q: 0, ct: 100 },
{ q: 1, ct: 140 },
{ q: 2, ct: 170 },
{ q: 3, ct: 190 },
{ q: 4, ct: 220 },
{ q: 5, ct: 270 },
{ q: 6, ct: 350 },
{ q: 7, ct: 460 },
{ q: 8, ct: 600 },
];
const datosCMe = [
{ q: 1, cme: 140 },
{ q: 2, cme: 85 },
{ q: 3, cme: 63.33 },
{ q: 4, cme: 55 },
{ q: 5, cme: 54 },
{ q: 6, cme: 58.33 },
{ q: 7, cme: 65.71 },
{ q: 8, cme: 75 },
];
const datosCMg = [
{ q: 1, cmg: 40 },
{ q: 2, cmg: 30 },
{ q: 3, cmg: 20 },
{ q: 4, cmg: 30 },
{ q: 5, cmg: 50 },
{ q: 6, cmg: 80 },
{ q: 7, cmg: 110 },
{ q: 8, cmg: 140 },
];
const puntosCorte = [
{ q: 4, desc: 'CMg corta a CMe en su mínimo' },
{ q: 5, desc: 'CMe mínimo (producción eficiente)' },
];
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Curvas de Costo"
subtitle="Analiza la relación entre CT, CMe y CMg con gráficos interactivos"
/>
<div className="space-y-6">
{/* Gráfico de Costo Total */}
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-700">Curva de Costo Total (CT)</h4>
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">CT = CF + CV</span>
</div>
<svg className="w-full h-56" viewBox="0 0 500 200">
{/* Ejes */}
<line x1="50" y1="170" x2="450" y2="170" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="170" x2="50" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas */}
<text x="250" y="195" textAnchor="middle" className="text-xs fill-gray-600">Cantidad (Q)</text>
<text x="20" y="95" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90 20 95)">Costo Total ($)</text>
{/* CF horizontal */}
<line x1="50" y1="130" x2="450" y2="130" stroke="#2563eb" strokeWidth="2" strokeDasharray="4" />
<text x="430" y="125" textAnchor="end" className="text-xs fill-blue-600">CF = 100</text>
{/* Curva CT */}
<polyline
fill="none"
stroke="#7c3aed"
strokeWidth="3"
points={datosCT.map((d, i) => `${50 + i * 45},${170 - (d.ct / 700) * 150}`).join(' ')}
/>
{/* Puntos */}
{datosCT.map((d, i) => (
<circle
key={i}
cx={50 + i * 45}
cy={170 - (d.ct / 700) * 150}
r="4"
fill="#7c3aed"
/>
))}
{/* Etiquetas de Q */}
{datosCT.map((d, i) => (
<text key={i} x={50 + i * 45} y="185" textAnchor="middle" className="text-xs fill-gray-500">
{d.q}
</text>
))}
</svg>
</div>
{/* Gráfico de CMe y CMg */}
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-700">Curvas de CMe y CMg</h4>
<div className="flex gap-2">
<span className="text-xs bg-purple-100 text-purple-800 px-2 py-1 rounded flex items-center gap-1">
<span className="w-3 h-0.5 bg-purple-600"></span> CMe
</span>
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded flex items-center gap-1">
<span className="w-3 h-0.5 bg-green-600 border-dashed"></span> CMg
</span>
</div>
</div>
<svg className="w-full h-56" viewBox="0 0 500 200">
{/* Ejes */}
<line x1="50" y1="170" x2="450" y2="170" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="170" x2="50" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas */}
<text x="250" y="195" textAnchor="middle" className="text-xs fill-gray-600">Cantidad (Q)</text>
<text x="20" y="95" textAnchor="middle" className="text-xs fill-gray-600" transform="rotate(-90 20 95)">Costo ($)</text>
{/* Curva CMe */}
<polyline
fill="none"
stroke="#7c3aed"
strokeWidth="3"
points={datosCMe.map((d, i) => `${95 + i * 45},${170 - (d.cme / 160) * 150}`).join(' ')}
/>
{/* Curva CMg */}
<polyline
fill="none"
stroke="#16a34a"
strokeWidth="3"
strokeDasharray="5"
points={datosCMg.map((d, i) => `${95 + i * 45},${170 - (d.cmg / 160) * 150}`).join(' ')}
/>
{/* Puntos de corte */}
<circle cx="275" cy="127" r="6" fill="#ef4444" stroke="white" strokeWidth="2" />
<text x="290" y="120" className="text-xs fill-red-600 font-bold">Mínimo CMe</text>
{/* Etiquetas de Q */}
{datosCMe.map((d, i) => (
<text key={i} x={95 + i * 45} y="185" textAnchor="middle" className="text-xs fill-gray-500">
{d.q}
</text>
))}
{/* Leyenda */}
<g transform="translate(350, 40)">
<line x1="0" y1="0" x2="30" y2="0" stroke="#7c3aed" strokeWidth="2" />
<text x="35" y="4" className="text-xs fill-gray-700">CMe</text>
<line x1="0" y1="20" x2="30" y2="20" stroke="#16a34a" strokeWidth="2" strokeDasharray="4" />
<text x="35" y="24" className="text-xs fill-gray-700">CMg</text>
</g>
</svg>
</div>
{/* Puntos clave */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{puntosCorte.map((punto, index) => (
<button
key={index}
onClick={() => setEtapaActiva(etapaActiva === `punto-${index}` ? null : `punto-${index}`)}
className={`p-4 rounded-lg border-2 text-left transition-all ${
etapaActiva === `punto-${index}`
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<div className="flex items-center gap-2 mb-2">
<div className="w-4 h-4 rounded-full bg-red-500"></div>
<span className="font-semibold text-gray-900">Q = {punto.q}</span>
</div>
<p className="text-sm text-gray-600">{punto.desc}</p>
</button>
))}
</div>
{etapaActiva && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-900">Análisis</span>
</div>
<p className="text-sm text-blue-800">
En Q=5 se alcanza el <strong>CMe mínimo</strong> ($54), que es el punto donde CMg = CMe.
Este es el nivel de producción más eficiente en términos de costos medios.
</p>
</div>
)}
</div>
</Card>
<Card className="bg-amber-50 border-amber-200">
<div className="flex items-center gap-2 mb-2">
<DollarSign className="w-5 h-5 text-amber-600" />
<h4 className="font-semibold text-amber-900">Interpretación Económica</h4>
</div>
<ul className="space-y-2 text-sm text-amber-800">
<li><strong>Costo Total (CT):</strong> Siempre crece porque producir más cuesta más</li>
<li><strong>Costo Medio (CMe):</strong> Tiene forma de U debido a los rendimientos decrecientes</li>
<li><strong>Costo Marginal (CMg):</strong> Corta a CMe en su punto mínimo</li>
<li><strong>Regla:</strong> Si CMg {'<'} CMe, el costo medio baja; si CMg {'>'} CMe, el costo medio sube</li>
</ul>
</Card>
</div>
);
}
export default CurvasCosto;

View File

@@ -0,0 +1,309 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, ArrowUp, RotateCcw, AlertTriangle } from 'lucide-react';
interface DiseconomiasEscalaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface RangoEscala {
min: number;
max: number;
tipo: 'economias' | 'constante' | 'diseconomias';
descripcion: string;
}
export function DiseconomiasEscala({ ejercicioId: _ejercicioId, onComplete }: DiseconomiasEscalaProps) {
const rangos: RangoEscala[] = [
{ min: 0, max: 500, tipo: 'economias', descripcion: 'Economías de escala' },
{ min: 500, max: 1000, tipo: 'constante', descripcion: 'Rendimientos constantes a escala' },
{ min: 1000, max: 2000, tipo: 'diseconomias', descripcion: 'Diseconomías de escala' },
];
const calcularCMe = (q: number): number => {
if (q <= 500) {
return 50 - (q / 500) * 20;
} else if (q <= 1000) {
return 30;
} else {
return 30 + ((q - 1000) / 1000) * 25;
}
};
const [cantidad, setCantidad] = useState(600);
const [respuestas, setRespuestas] = useState({
cme: '',
ct: '',
rango: '',
});
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const cmeActual = useMemo(() => calcularCMe(cantidad), [cantidad]);
const ctActual = useMemo(() => cmeActual * cantidad, [cmeActual, cantidad]);
const rangoActual = useMemo(() => {
return rangos.find(r => cantidad >= r.min && cantidad < r.max) || rangos[2];
}, [cantidad]);
const datosGrafico = useMemo(() => {
const puntos = [];
for (let q = 100; q <= 2000; q += 100) {
puntos.push({ q, cme: calcularCMe(q) });
}
return puntos;
}, []);
const handleRespuestaChange = (campo: string, valor: string) => {
setRespuestas(prev => ({ ...prev, [campo]: valor }));
setValidado(false);
};
const validarRespuestas = () => {
const nuevosErrores: string[] = [];
if (Math.abs(parseFloat(respuestas.cme) - cmeActual) > 0.5) {
nuevosErrores.push(`El CMe no es correcto. Debería ser aproximadamente $${cmeActual.toFixed(2)}`);
}
if (Math.abs(parseFloat(respuestas.ct) - ctActual) > 50) {
nuevosErrores.push(`El CT no es correcto. Recuerda: CT = CMe × Q`);
}
if (respuestas.rango.toLowerCase() !== rangoActual.tipo.toLowerCase()) {
nuevosErrores.push(`El rango no es correcto. Estás en la zona de ${rangoActual.descripcion}`);
}
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0 && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setCantidad(600);
setRespuestas({ cme: '', ct: '', rango: '' });
setValidado(false);
setErrores([]);
};
const maxCMe = Math.max(...datosGrafico.map(d => d.cme));
const escalaY = 100 / maxCMe;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Diseconomías de Escala"
subtitle="Aumento del costo medio cuando la empresa crece demasiado"
/>
<div className="bg-red-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<ArrowUp className="w-5 h-5 text-red-600" />
<span className="font-semibold text-red-800">Concepto</span>
</div>
<p className="text-sm text-red-700">
Las deseconomías de escala ocurren cuando la empresa crece tanto que los costos de
coordinación, supervisión y comunición aumentan. El CMe comienza a subir después
de alcanzar el punto óptimo de escala.
</p>
</div>
<div className="bg-gray-50 p-4 rounded-lg mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Cantidad producida (Q)
</label>
<div className="flex items-center gap-4">
<input
type="range"
min="100"
max="2000"
step="100"
value={cantidad}
onChange={(e) => {
setCantidad(parseInt(e.target.value));
setValidado(false);
}}
className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
<span className="font-mono text-lg font-bold text-primary w-20 text-center">
{cantidad}
</span>
</div>
</div>
<div className="h-56 bg-gray-50 rounded-lg p-4 mb-6">
<svg className="w-full h-full" viewBox="0 0 400 180">
<line x1="40" y1="160" x2="380" y2="160" stroke="#374151" strokeWidth="2" />
<line x1="40" y1="160" x2="40" y2="20" stroke="#374151" strokeWidth="2" />
<text x="210" y="175" textAnchor="middle" className="text-sm fill-gray-600 font-medium">Cantidad (Q)</text>
<text x="15" y="90" textAnchor="middle" className="text-sm fill-gray-600 font-medium" transform="rotate(-90 15 90)">CMe ($)</text>
{[500, 1000, 1500, 2000].map((q) => (
<g key={q}>
<line x1={40 + (q / 2000) * 300} y1="160" x2={40 + (q / 2000) * 300} y2="165" stroke="#374151" strokeWidth="1" />
<text x={40 + (q / 2000) * 300} y="175" textAnchor="middle" className="text-xs fill-gray-500">{q}</text>
</g>
))}
{[10, 20, 30, 40, 50].map((val) => (
<g key={val}>
<line x1="35" y1={160 - val * escalaY * 2} x2="40" y2={160 - val * escalaY * 2} stroke="#374151" strokeWidth="1" />
<text x="30" y={165 - val * escalaY * 2} textAnchor="end" className="text-xs fill-gray-500">{val}</text>
</g>
))}
<rect x="40" y="20" width="75" height="140" fill="green" fillOpacity="0.1" />
<rect x="115" y="20" width="75" height="140" fill="blue" fillOpacity="0.1" />
<rect x="190" y="20" width="190" height="140" fill="red" fillOpacity="0.1" />
<text x="77" y="35" textAnchor="middle" className="text-xs fill-green-700 font-medium">Economías</text>
<text x="152" y="35" textAnchor="middle" className="text-xs fill-blue-700 font-medium">Constantes</text>
<text x="285" y="35" textAnchor="middle" className="text-xs fill-red-700 font-medium">Diseconomías</text>
<path
d={`M ${datosGrafico.map((d, i) => `${40 + (d.q / 2000) * 300},${160 - d.cme * escalaY * 2}`).join(' L ')}`}
fill="none"
stroke="#7c3aed"
strokeWidth="3"
/>
<circle
cx={40 + (cantidad / 2000) * 300}
cy={160 - cmeActual * escalaY * 2}
r="6"
fill="#10b981"
stroke="white"
strokeWidth="3"
/>
<line
x1={40 + (cantidad / 2000) * 300}
y1={160 - cmeActual * escalaY * 2}
x2={40 + (cantidad / 2000) * 300}
y2="160"
stroke="#10b981"
strokeWidth="2"
strokeDasharray="4"
/>
</svg>
</div>
<div className="grid md:grid-cols-3 gap-4 mb-6">
<div className="bg-blue-50 p-4 rounded-lg text-center">
<p className="text-sm text-blue-600 mb-1">Costo Medio</p>
<p className="text-3xl font-bold text-blue-800">${cmeActual.toFixed(2)}</p>
</div>
<div className="bg-purple-50 p-4 rounded-lg text-center">
<p className="text-sm text-purple-600 mb-1">Costo Total</p>
<p className="text-3xl font-bold text-purple-800">${ctActual.toLocaleString()}</p>
</div>
<div className={`p-4 rounded-lg text-center ${
rangoActual.tipo === 'economias' ? 'bg-green-50' :
rangoActual.tipo === 'diseconomias' ? 'bg-red-50' : 'bg-blue-50'
}`}>
<p className="text-sm text-gray-600 mb-1">Zona</p>
<p className={`text-lg font-bold ${
rangoActual.tipo === 'economias' ? 'text-green-800' :
rangoActual.tipo === 'diseconomias' ? 'text-red-800' : 'text-blue-800'
}`}>
{rangoActual.descripcion}
</p>
</div>
</div>
<div className="bg-gradient-to-r from-orange-50 to-red-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-orange-600" />
Responde para Q = {cantidad}:
</h4>
<div className="grid md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Costo Medio ($)
</label>
<Input
type="number"
step="0.01"
value={respuestas.cme}
onChange={(e) => handleRespuestaChange('cme', e.target.value)}
className="w-full"
placeholder="Ej: 30.00"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Costo Total ($)
</label>
<Input
type="number"
value={respuestas.ct}
onChange={(e) => handleRespuestaChange('ct', e.target.value)}
className="w-full"
placeholder="CMe × Q"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tipo de escala (economias/constante/diseconomias)
</label>
<Input
type="text"
value={respuestas.rango}
onChange={(e) => handleRespuestaChange('rango', e.target.value)}
className="w-full"
placeholder="economias"
/>
</div>
</div>
</div>
<div className="mt-4 flex gap-3">
<Button onClick={validarRespuestas} variant="primary">
<CheckCircle className="w-4 h-4 mr-2" />
Validar Respuestas
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Reiniciar
</Button>
</div>
{validado && errores.length === 0 && (
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
<div className="flex items-center gap-2 text-success">
<CheckCircle className="w-5 h-5" />
<span className="font-medium">¡Correcto! Observa cómo el CMe cambia según la escala</span>
</div>
</div>
)}
{validado && errores.length > 0 && (
<div className="mt-4 p-4 bg-error/10 border border-error rounded-lg">
<p className="font-medium text-error mb-2">Revisa tus respuestas:</p>
<ul className="list-disc list-inside text-sm text-error">
{errores.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
)}
</Card>
<Card className="bg-red-50 border-red-200">
<h4 className="font-semibold text-red-900 mb-2">Causas de las Diseconomías de Escala:</h4>
<ul className="space-y-1 text-sm text-red-800">
<li> <strong>Problemas de coordinación:</strong> Más difícil coordinar muchos departamentos</li>
<li> <strong>Burocracia:</strong> Decisiones lentas y procesos administrativos complejos</li>
<li> <strong>Problemas de comunicación:</strong> Información se distorsiona en cadenas largas</li>
<li> <strong>Desmotivación:</strong> Trabajadores se sienten insignificantes en empresas grandes</li>
</ul>
</Card>
</div>
);
}
export default DiseconomiasEscala;

View File

@@ -0,0 +1,217 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, ArrowDown, RotateCcw, Factory } from 'lucide-react';
interface EconomiasEscalaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface EscalaData {
planta: string;
capacidad: number;
cf: number;
cvUnitario: number;
}
export function EconomiasEscala({ ejercicioId: _ejercicioId, onComplete }: EconomiasEscalaProps) {
const datosPlantas: EscalaData[] = [
{ planta: 'Pequeña', capacidad: 100, cf: 1000, cvUnitario: 10 },
{ planta: 'Mediana', capacidad: 500, cf: 3000, cvUnitario: 8 },
{ planta: 'Grande', capacidad: 1000, cf: 5000, cvUnitario: 6 },
{ planta: 'Muy Grande', capacidad: 2000, cf: 8000, cvUnitario: 5 },
];
const [produccion, setProduccion] = useState(500);
const [seleccion, setSeleccion] = useState<string | null>(null);
const [validado, setValidado] = useState(false);
const calculos = useMemo(() => {
return datosPlantas.map(p => {
const q = Math.min(produccion, p.capacidad);
const cv = q * p.cvUnitario;
const ct = p.cf + cv;
const cme = q > 0 ? ct / q : 0;
const puedeProducir = produccion <= p.capacidad;
return { ...p, q, cv, ct, cme, puedeProducir };
});
}, [produccion]);
const plantaOptima = useMemo(() => {
const plantasFactibles = calculos.filter(c => c.puedeProducir);
if (plantasFactibles.length === 0) return null;
return plantasFactibles.reduce((min, curr) => curr.cme < min.cme ? curr : min);
}, [calculos]);
const handleValidar = () => {
setValidado(true);
if (seleccion === plantaOptima?.planta && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setProduccion(500);
setSeleccion(null);
setValidado(false);
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Economías de Escala"
subtitle="Reducción del costo medio al aumentar la escala de producción"
/>
<div className="bg-green-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<ArrowDown className="w-5 h-5 text-green-600" />
<span className="font-semibold text-green-800">Concepto</span>
</div>
<p className="text-sm text-green-700">
Las economías de escala ocurren cuando el costo medio disminuye a medida que
aumenta la producción. Esto puede deberse a: especialización, tecnología eficiente,
descuentos por volumen en compras, y distribución de costos fijos.
</p>
</div>
<div className="bg-gray-50 p-4 rounded-lg mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Nivel de producción deseado (Q)
</label>
<div className="flex items-center gap-4">
<input
type="range"
min="50"
max="2000"
step="50"
value={produccion}
onChange={(e) => {
setProduccion(parseInt(e.target.value));
setValidado(false);
setSeleccion(null);
}}
className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
<span className="font-mono text-lg font-bold text-primary w-20 text-center">
{produccion}
</span>
</div>
</div>
<div className="grid md:grid-cols-2 gap-4 mb-6">
{calculos.map((calc) => (
<div
key={calc.planta}
onClick={() => calc.puedeProducir && setSeleccion(calc.planta)}
className={`
p-4 rounded-lg border-2 cursor-pointer transition-all
${!calc.puedeProducir ? 'bg-gray-100 border-gray-200 opacity-50 cursor-not-allowed' : ''}
${seleccion === calc.planta ? 'border-primary bg-primary/5' : 'border-gray-200 hover:border-gray-300'}
${validado && calc.planta === plantaOptima?.planta ? 'border-green-500 bg-green-50' : ''}
`}
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Factory className="w-5 h-5 text-gray-600" />
<span className="font-semibold text-gray-900">{calc.planta}</span>
</div>
{!calc.puedeProducir && (
<span className="text-xs bg-red-100 text-red-700 px-2 py-1 rounded">
Insuficiente
</span>
)}
{validado && calc.planta === plantaOptima?.planta && (
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded font-medium">
Óptima
</span>
)}
</div>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Capacidad máxima:</span>
<span className="font-medium">{calc.capacidad} unidades</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Costo Fijo:</span>
<span className="font-medium">${calc.cf.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">CV unitario:</span>
<span className="font-medium">${calc.cvUnitario}</span>
</div>
{calc.puedeProducir && (
<>
<div className="flex justify-between pt-2 border-t">
<span className="text-gray-600">Costo Total:</span>
<span className="font-medium text-primary">${calc.ct.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Costo Medio:</span>
<span className={`font-bold ${calc.cme === Math.min(...calculos.filter(c => c.puedeProducir).map(c => c.cme)) ? 'text-green-600' : 'text-gray-900'}`}>
${calc.cme.toFixed(2)}
</span>
</div>
</>
)}
</div>
</div>
))}
</div>
<div className="bg-blue-50 p-4 rounded-lg mb-4">
<p className="text-sm text-blue-800 font-medium mb-2">
Selecciona la planta óptima para producir {produccion} unidades:
</p>
<p className="text-sm text-blue-600">
Tip: Elige la planta con el menor costo medio (CMe) que pueda producir la cantidad deseada.
</p>
</div>
<div className="flex gap-3">
<Button onClick={handleValidar} variant="primary" disabled={!seleccion}>
<CheckCircle className="w-4 h-4 mr-2" />
Validar Selección
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Cambiar Producción
</Button>
</div>
{validado && seleccion === plantaOptima?.planta && (
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
<div className="flex items-center gap-2 text-success">
<CheckCircle className="w-5 h-5" />
<span className="font-medium">
¡Correcto! La planta {plantaOptima.planta} tiene el menor CMe (${plantaOptima.cme.toFixed(2)})
</span>
</div>
</div>
)}
{validado && seleccion !== plantaOptima?.planta && (
<div className="mt-4 p-4 bg-error/10 border border-error rounded-lg">
<p className="text-error font-medium">
La planta {seleccion} no es la óptima. La planta {plantaOptima?.planta} tiene un CMe menor (${plantaOptima?.cme.toFixed(2)} vs ${calculos.find(c => c.planta === seleccion)?.cme.toFixed(2)}).
</p>
</div>
)}
</Card>
<Card className="bg-green-50 border-green-200">
<h4 className="font-semibold text-green-900 mb-2">Causas de las Economías de Escala:</h4>
<ul className="space-y-1 text-sm text-green-800">
<li> <strong>Especialización del trabajo:</strong> Tareas más específicas = mayor eficiencia</li>
<li> <strong>Tecnología especializada:</strong> Maquinaria más eficiente a gran escala</li>
<li> <strong>Descuentos por volumen:</strong> Comprar insumos al por mayor es más barato</li>
<li> <strong>División de costos fijos:</strong> Se reparten entre más unidades</li>
</ul>
</Card>
</div>
);
}
export default EconomiasEscala;

View File

@@ -0,0 +1,231 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, Layers } from 'lucide-react';
interface Etapa {
id: string;
nombre: string;
descripcion: string;
color: string;
rango: string;
}
const ETAPAS: Etapa[] = [
{
id: 'i',
nombre: 'Etapa I',
descripcion: 'PMg creciente - Rendimientos crecientes a escala',
color: '#22c55e',
rango: '0 a 3 trabajadores'
},
{
id: 'ii',
nombre: 'Etapa II',
descripcion: 'PMg decreciente pero positivo - Rendimientos decrecientes',
color: '#3b82f6',
rango: '3 a 6 trabajadores'
},
{
id: 'iii',
nombre: 'Etapa III',
descripcion: 'PMg negativo - Producción total disminuye',
color: '#ef4444',
rango: 'Más de 6 trabajadores'
}
];
export function EtapasProduccion() {
const [respuestas, setRespuestas] = useState<{[key: number]: string}>({});
const [mostrarResultados, setMostrarResultados] = useState(false);
const preguntas = [
{
id: 1,
texto: '¿En qué etapa un productor racional NUNCA producirá?',
respuestaCorrecta: 'iii',
explicacion: 'En la Etapa III el producto marginal es negativo, lo que significa que agregar más trabajadores disminuye la producción total. Un productor racional evitará esta etapa.'
},
{
id: 2,
texto: '¿En qué etapa los rendimientos marginales son crecientes?',
respuestaCorrecta: 'i',
explicacion: 'En la Etapa I, cada trabajador adicional aporta más que el anterior debido a la especialización y división del trabajo.'
},
{
id: 3,
texto: '¿En qué etapa se encuentra la mayoría de la producción eficiente?',
respuestaCorrecta: 'ii',
explicacion: 'La Etapa II es donde opera un productor racional. Aunque los rendimientos marginales decrecen, siguen siendo positivos hasta cierto punto.'
}
];
const seleccionarRespuesta = (preguntaId: number, etapaId: string) => {
setRespuestas(prev => ({ ...prev, [preguntaId]: etapaId }));
setMostrarResultados(false);
};
const validarTodas = () => {
setMostrarResultados(true);
};
const todasRespondidas = preguntas.every(p => respuestas[p.id]);
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Etapas de la Producción"
subtitle="Identifica las tres etapas según la Ley de Rendimientos Decrecientes"
/>
<div className="space-y-6">
{/* Gráfico de etapas */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 mb-4">Producto Total y sus Etapas</h4>
<svg className="w-full h-72" viewBox="0 0 600 280">
{/* Ejes */}
<line x1="60" y1="240" x2="550" y2="240" stroke="#374151" strokeWidth="2" />
<line x1="60" y1="240" x2="60" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas eje X */}
<text x="150" y="260" textAnchor="middle" className="text-xs fill-gray-600">L1</text>
<text x="300" y="260" textAnchor="middle" className="text-xs fill-gray-600">L2</text>
<text x="450" y="260" textAnchor="middle" className="text-xs fill-gray-600">L3</text>
<text x="300" y="275" textAnchor="middle" className="text-sm fill-gray-700 font-medium">Cantidad de Trabajo (L)</text>
{/* Etiquetas eje Y */}
<text x="45" y="245" textAnchor="end" className="text-xs fill-gray-600">0</text>
<text x="45" y="180" textAnchor="end" className="text-xs fill-gray-600">Q1</text>
<text x="45" y="100" textAnchor="end" className="text-xs fill-gray-600">Q2</text>
<text x="45" y="40" textAnchor="end" className="text-xs fill-gray-600">Q3</text>
<text x="20" y="130" textAnchor="middle" className="text-sm fill-gray-700 font-medium" transform="rotate(-90 20 130)">Producto Total (PT)</text>
{/* Líneas verticales separadoras de etapas */}
<line x1="150" y1="20" x2="150" y2="240" stroke="#9ca3af" strokeWidth="2" strokeDasharray="8" />
<line x1="300" y1="20" x2="300" y2="240" stroke="#9ca3af" strokeWidth="2" strokeDasharray="8" />
{/* Zonas de etapas */}
<rect x="60" y="20" width="90" height="220" fill="#dcfce7" opacity="0.5" />
<rect x="150" y="20" width="150" height="220" fill="#dbeafe" opacity="0.5" />
<rect x="300" y="20" width="250" height="220" fill="#fee2e2" opacity="0.5" />
{/* Etiquetas de etapas */}
<text x="105" y="35" textAnchor="middle" className="text-sm font-bold fill-green-700">ETAPA I</text>
<text x="105" y="50" textAnchor="middle" className="text-xs fill-green-600">PMg creciente</text>
<text x="225" y="35" textAnchor="middle" className="text-sm font-bold fill-blue-700">ETAPA II</text>
<text x="225" y="50" textAnchor="middle" className="text-xs fill-blue-600">PMg decreciente</text>
<text x="425" y="35" textAnchor="middle" className="text-sm font-bold fill-red-700">ETAPA III</text>
<text x="425" y="50" textAnchor="middle" className="text-xs fill-red-600">PMg negativo</text>
{/* Curva de producto total */}
<path
d="M 60,240 Q 105,200 150,180 Q 200,140 300,100 Q 380,60 450,80 Q 500,120 550,200"
fill="none"
stroke="#1f2937"
strokeWidth="3"
/>
{/* Punto de inflexión */}
<circle cx="150" cy="180" r="6" fill="#22c55e" stroke="white" strokeWidth="2" />
<text x="150" y="170" textAnchor="middle" className="text-xs fill-green-700 font-bold">Punto de Inflexión</text>
{/* Punto máximo */}
<circle cx="450" cy="80" r="6" fill="#ef4444" stroke="white" strokeWidth="2" />
<text x="450" y="70" textAnchor="middle" className="text-xs fill-red-700 font-bold">PT Máximo</text>
{/* Flecha mostrando declive */}
<path d="M 480,100 L 520,160" stroke="#ef4444" strokeWidth="3" markerEnd="url(#arrowRed)" />
<defs>
<marker id="arrowRed" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#ef4444" />
</marker>
</defs>
</svg>
</div>
{/* Leyenda de etapas */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{ETAPAS.map(etapa => (
<div key={etapa.id} className="p-3 rounded-lg border" style={{ backgroundColor: `${etapa.color}15`, borderColor: etapa.color }}>
<h5 className="font-semibold" style={{ color: etapa.color }}>{etapa.nombre}</h5>
<p className="text-xs text-gray-600 mt-1">{etapa.descripcion}</p>
<p className="text-xs text-gray-500 mt-1">{etapa.rango}</p>
</div>
))}
</div>
{/* Preguntas */}
<div className="space-y-4">
{preguntas.map(pregunta => (
<div key={pregunta.id} className="bg-white border rounded-lg p-4">
<h4 className="font-medium text-gray-900 mb-3">{pregunta.id}. {pregunta.texto}</h4>
<div className="flex gap-3">
{ETAPAS.map(etapa => {
const esCorrecta = mostrarResultados && respuestas[pregunta.id] === pregunta.respuestaCorrecta;
const esIncorrecta = mostrarResultados && respuestas[pregunta.id] === etapa.id && respuestas[pregunta.id] !== pregunta.respuestaCorrecta;
const esLaCorrecta = mostrarResultados && etapa.id === pregunta.respuestaCorrecta;
return (
<button
key={etapa.id}
onClick={() => seleccionarRespuesta(pregunta.id, etapa.id)}
disabled={mostrarResultados}
className={`flex-1 p-3 rounded-lg border-2 text-center transition-all ${
respuestas[pregunta.id] === etapa.id && !mostrarResultados
? 'border-blue-500 bg-blue-50'
: esCorrecta
? 'border-green-500 bg-green-50'
: esIncorrecta
? 'border-red-500 bg-red-50'
: esLaCorrecta
? 'border-green-500 bg-green-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<span className="font-bold" style={{ color: etapa.color }}>{etapa.nombre}</span>
{mostrarResultados && esLaCorrecta && (
<CheckCircle className="w-4 h-4 text-green-600 mx-auto mt-1" />
)}
{mostrarResultados && esIncorrecta && (
<XCircle className="w-4 h-4 text-red-600 mx-auto mt-1" />
)}
</button>
);
})}
</div>
{mostrarResultados && (
<div className={`mt-3 p-3 rounded text-sm ${respuestas[pregunta.id] === pregunta.respuestaCorrecta ? 'bg-green-50 text-green-800' : 'bg-amber-50 text-amber-800'}`}>
{pregunta.explicacion}
</div>
)}
</div>
))}
</div>
<Button onClick={validarTodas} disabled={!todasRespondidas || mostrarResultados}>
Validar Respuestas
</Button>
{mostrarResultados && (
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Layers className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-900">Conclusión</span>
</div>
<p className="text-sm text-blue-800">
Un productor racional opera principalmente en la <strong>Etapa II</strong>,
donde aunque los rendimientos marginales decrecen, siguen siendo positivos.
La Etapa I es muy corta y la Etapa III es irracional desde el punto de vista económico.
</p>
</div>
)}
</div>
</Card>
</div>
);
}
export default EtapasProduccion;

View File

@@ -0,0 +1,184 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Factory, Calculator } from 'lucide-react';
interface FuncionProduccionProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function FuncionProduccion({ ejercicioId: _ejercicioId, onComplete }: FuncionProduccionProps) {
const [capital, setCapital] = useState(4);
const [trabajo, setTrabajo] = useState(5);
const tablaProduccion = [
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 8, 12, 17, 20, 22, 23, 23],
[0, 12, 20, 28, 35, 40, 43, 44],
[0, 17, 28, 40, 50, 58, 63, 65],
[0, 20, 35, 50, 65, 75, 83, 87],
[0, 22, 40, 58, 75, 88, 98, 104],
[0, 23, 43, 63, 83, 98, 110, 118],
[0, 23, 44, 65, 87, 104, 118, 128],
];
const output = useMemo(() => {
if (trabajo >= 0 && trabajo <= 7 && capital >= 0 && capital <= 7) {
return tablaProduccion[capital][trabajo];
}
return 0;
}, [capital, trabajo]);
const handleCompletar = () => {
if (onComplete) {
onComplete(100);
}
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Función de Producción: Q = f(K, L)"
subtitle="Observa cómo cambia la producción al variar los factores productivos"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Factory className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Concepto</span>
</div>
<p className="text-sm text-blue-700">
La función de producción muestra la relación técnica entre los factores productivos
(Capital K y Trabajo L) y la cantidad máxima de output (Q) que puede producirse.
</p>
</div>
<div className="grid md:grid-cols-2 gap-6 mb-6">
<div className="space-y-4">
<label className="block text-sm font-medium text-gray-700">
Capital (K) - Unidades de maquinaria
</label>
<div className="flex items-center gap-4">
<Input
type="range"
min="1"
max="7"
value={capital}
onChange={(e) => setCapital(parseInt(e.target.value))}
className="flex-1"
/>
<span className="font-mono text-lg font-bold text-primary w-12">
{capital}
</span>
</div>
</div>
<div className="space-y-4">
<label className="block text-sm font-medium text-gray-700">
Trabajo (L) - Número de trabajadores
</label>
<div className="flex items-center gap-4">
<Input
type="range"
min="1"
max="7"
value={trabajo}
onChange={(e) => setTrabajo(parseInt(e.target.value))}
className="flex-1"
/>
<span className="font-mono text-lg font-bold text-primary w-12">
{trabajo}
</span>
</div>
</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-6">
<div className="flex items-center justify-center gap-4">
<Calculator className="w-8 h-8 text-green-600" />
<div className="text-center">
<p className="text-sm text-green-700 mb-1">Output Total (Q)</p>
<p className="text-4xl font-bold text-green-800">
Q = f({capital}, {trabajo}) = {output}
</p>
<p className="text-sm text-green-600 mt-2">
unidades producidas
</p>
</div>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="px-3 py-2 border text-left">K \ L</th>
{[0, 1, 2, 3, 4, 5, 6, 7].map(l => (
<th key={l} className="px-3 py-2 border text-center">{l}</th>
))}
</tr>
</thead>
<tbody>
{tablaProduccion.map((fila, k) => (
<tr key={k} className={k === capital ? 'bg-blue-50' : ''}>
<td className="px-3 py-2 border font-medium bg-gray-50">{k}</td>
{fila.map((q, l) => (
<td
key={l}
className={`px-3 py-2 border text-center ${
k === capital && l === trabajo
? 'bg-green-200 font-bold text-green-800'
: ''
}`}
>
{q}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-4 text-sm text-gray-600">
<p><strong>Nota:</strong> La celda resaltada en verde muestra el output actual.
Las filas representan niveles de Capital (K) y las columnas niveles de Trabajo (L).</p>
</div>
</Card>
<Card className="bg-gradient-to-r from-purple-50 to-blue-50">
<h4 className="font-semibold text-gray-900 mb-3">Ejercicio de Comprensión</h4>
<div className="space-y-4">
<p className="text-sm text-gray-700">
Si una empresa tiene <strong>3 unidades de capital</strong> y contrata <strong>4 trabajadores</strong>,
¿cuál es el nivel de producción máximo alcanzable según la tabla?
</p>
<div className="flex items-center gap-4">
<Input
type="number"
placeholder="Ingresa el valor de Q"
className="w-40"
readOnly
value={tablaProduccion[3][4]}
/>
<span className="text-sm text-gray-600">
Respuesta correcta: {tablaProduccion[3][4]} unidades
</span>
</div>
</div>
</Card>
<div className="flex justify-end">
<Button onClick={handleCompletar} size="lg">
<CheckCircle className="w-5 h-5 mr-2" />
Marcar como Completado
</Button>
</div>
</div>
);
}
export default FuncionProduccion;

View File

@@ -0,0 +1,278 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Scale, RotateCcw, TrendingUp } from 'lucide-react';
interface IngresoCompetenciaPerfectaProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
export function IngresoCompetenciaPerfecta({ ejercicioId: _ejercicioId, onComplete }: IngresoCompetenciaPerfectaProps) {
const PRECIO_MERCADO = 50;
const [cantidad, setCantidad] = useState(100);
const [respuestas, setRespuestas] = useState({
it: '',
img: '',
relacion: '',
});
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const ingresoTotal = useMemo(() => PRECIO_MERCADO * cantidad, [cantidad]);
const ingresoMarginal = PRECIO_MERCADO;
const ingresoPromedio = PRECIO_MERCADO;
const datosTabla = useMemo(() => {
const datos = [];
for (let q = 0; q <= 200; q += 25) {
datos.push({
q,
p: PRECIO_MERCADO,
it: PRECIO_MERCADO * q,
img: PRECIO_MERCADO,
ip: PRECIO_MERCADO,
});
}
return datos;
}, []);
const handleRespuestaChange = (campo: string, valor: string) => {
setRespuestas(prev => ({ ...prev, [campo]: valor }));
setValidado(false);
};
const validarRespuestas = () => {
const nuevosErrores: string[] = [];
if (parseFloat(respuestas.it) !== ingresoTotal) {
nuevosErrores.push(`IT incorrecto. IT = P × Q = ${PRECIO_MERCADO} × ${cantidad}`);
}
if (parseFloat(respuestas.img) !== ingresoMarginal) {
nuevosErrores.push(`IMg incorrecto. En competencia perfecta, IMg = P`);
}
if (!['igual', 'igual a', 'es igual', 'son iguales'].some(r => respuestas.relacion.toLowerCase().includes(r))) {
nuevosErrores.push('En competencia perfecta, P = IMg = IPMe');
}
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0 && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setCantidad(100);
setRespuestas({ it: '', img: '', relacion: '' });
setValidado(false);
setErrores([]);
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Ingreso en Competencia Perfecta"
subtitle="Precio = Ingreso Marginal = Ingreso Promedio"
/>
<div className="bg-green-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Scale className="w-5 h-5 text-green-600" />
<span className="font-semibold text-green-800">Características</span>
</div>
<p className="text-sm text-green-700">
En competencia perfecta, la empresa es tomadora de precios. El precio de mercado
es constante e independiente de la cantidad que produzca la empresa. Por eso:
<strong>P = IMg = IPMe</strong>. La curva de demanda es horizontal (perfectamente elástica).
</p>
</div>
<div className="grid md:grid-cols-3 gap-4 mb-6">
<div className="bg-blue-100 p-4 rounded-lg text-center border-2 border-blue-300">
<p className="text-sm text-blue-700 mb-1 font-medium">Precio de Mercado (P)</p>
<p className="text-3xl font-bold text-blue-800">${PRECIO_MERCADO}</p>
<p className="text-xs text-blue-600 mt-1">Constante</p>
</div>
<div className="bg-purple-100 p-4 rounded-lg text-center border-2 border-purple-300">
<p className="text-sm text-purple-700 mb-1 font-medium">Ingreso Marginal (IMg)</p>
<p className="text-3xl font-bold text-purple-800">${ingresoMarginal}</p>
<p className="text-xs text-purple-600 mt-1">=P</p>
</div>
<div className="bg-orange-100 p-4 rounded-lg text-center border-2 border-orange-300">
<p className="text-sm text-orange-700 mb-1 font-medium">Ingreso Promedio (IPMe)</p>
<p className="text-3xl font-bold text-orange-800">${ingresoPromedio}</p>
<p className="text-xs text-orange-600 mt-1">=P</p>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Cantidad producida (Q): {cantidad} unidades
</label>
<div className="flex items-center gap-4">
<input
type="range"
min="0"
max="200"
step="10"
value={cantidad}
onChange={(e) => {
setCantidad(parseInt(e.target.value));
setValidado(false);
}}
className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
<span className="font-mono text-lg font-bold text-primary w-20 text-center">
{cantidad}
</span>
</div>
</div>
<div className="bg-green-50 border-2 border-green-200 p-6 rounded-lg mb-6">
<div className="text-center">
<p className="text-sm text-green-700 mb-2">Ingreso Total con Q = {cantidad}</p>
<p className="text-4xl font-bold text-green-800">
IT = ${ingresoTotal.toLocaleString()}
</p>
<p className="text-sm text-green-600 mt-2">
{PRECIO_MERCADO} × {cantidad} = ${ingresoTotal.toLocaleString()}
</p>
</div>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">P ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">IT ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">IMg ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">IPMe ($)</th>
</tr>
</thead>
<tbody>
{datosTabla.filter((_, i) => i % 2 === 0 || i === datosTabla.length - 1).map((d, i) => (
<tr
key={i}
className={`border-b hover:bg-gray-50 ${d.q === cantidad ? 'bg-green-50' : ''}`}
>
<td className="px-3 py-2 font-medium">{d.q}</td>
<td className="px-3 py-2 text-blue-600 font-medium">${d.p}</td>
<td className="px-3 py-2">${d.it.toLocaleString()}</td>
<td className="px-3 py-2 text-purple-600">${d.img}</td>
<td className="px-3 py-2 text-orange-600">${d.ip}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gradient-to-r from-blue-50 to-green-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-blue-600" />
Responde para Q = {cantidad}:
</h4>
<div className="grid md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿Cuál es el IT? ($)
</label>
<Input
type="number"
value={respuestas.it}
onChange={(e) => handleRespuestaChange('it', e.target.value)}
className="w-full"
placeholder={String(PRECIO_MERCADO * cantidad)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿Cuál es el IMg? ($)
</label>
<Input
type="number"
value={respuestas.img}
onChange={(e) => handleRespuestaChange('img', e.target.value)}
className="w-full"
placeholder="?"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿Cómo se relacionan P, IMg e IPMe?
</label>
<Input
type="text"
value={respuestas.relacion}
onChange={(e) => handleRespuestaChange('relacion', e.target.value)}
className="w-full"
placeholder="Son iguales / Diferentes"
/>
</div>
</div>
</div>
<div className="mt-4 flex gap-3">
<Button onClick={validarRespuestas} variant="primary">
<CheckCircle className="w-4 h-4 mr-2" />
Validar Respuestas
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Reiniciar
</Button>
</div>
{validado && errores.length === 0 && (
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
<div className="flex items-center gap-2 text-success">
<CheckCircle className="w-5 h-5" />
<span className="font-medium">¡Correcto! En competencia perfecta: P = IMg = IPMe = ${PRECIO_MERCADO}</span>
</div>
</div>
)}
{validado && errores.length > 0 && (
<div className="mt-4 p-4 bg-error/10 border border-error rounded-lg">
<p className="font-medium text-error mb-2">Revisa tus respuestas:</p>
<ul className="list-disc list-inside text-sm text-error">
{errores.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
)}
</Card>
<Card className="bg-green-50 border-green-200">
<h4 className="font-semibold text-green-900 mb-2">Resumen - Competencia Perfecta:</h4>
<div className="grid md:grid-cols-2 gap-4 text-sm text-green-800">
<div>
<p className="font-medium mb-1">Fórmulas:</p>
<ul className="space-y-1">
<li> IT = P × Q</li>
<li> IMg = P (constante)</li>
<li> IPMe = P (constante)</li>
</ul>
</div>
<div>
<p className="font-medium mb-1">Características:</p>
<ul className="space-y-1">
<li> La empresa es tomadora de precios</li>
<li> Demanda horizontal (perfectamente elástica)</li>
<li> P = IMg = IPMe</li>
</ul>
</div>
</div>
</Card>
</div>
);
}
export default IngresoCompetenciaPerfecta;

View File

@@ -0,0 +1,234 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Activity, RotateCcw, Calculator } from 'lucide-react';
interface IngresoMarginalProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FilaIngreso {
q: number;
p: number;
}
export function IngresoMarginal({ ejercicioId: _ejercicioId, onComplete }: IngresoMarginalProps) {
const datosBase: FilaIngreso[] = [
{ q: 0, p: 100 },
{ q: 1, p: 90 },
{ q: 2, p: 80 },
{ q: 3, p: 70 },
{ q: 4, p: 60 },
{ q: 5, p: 50 },
{ q: 6, p: 40 },
{ q: 7, p: 30 },
{ q: 8, p: 20 },
];
const [respuestas, setRespuestas] = useState<{[key: string]: string}>({});
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const datosCalculados = useMemo(() => {
return datosBase.map((fila, index) => {
const it = fila.p * fila.q;
const itAnterior = index > 0 ? datosBase[index - 1].p * datosBase[index - 1].q : 0;
const img = index > 0 ? it - itAnterior : null;
return { ...fila, it, img };
});
}, []);
const handleRespuestaChange = (q: number, valor: string) => {
setRespuestas(prev => ({ ...prev, [`img_${q}`]: valor }));
setValidado(false);
};
const validarRespuestas = () => {
const nuevosErrores: string[] = [];
datosCalculados.forEach((fila) => {
if (fila.img !== null) {
const respuesta = parseFloat(respuestas[`img_${fila.q}`] || '0');
if (Math.abs(respuesta - fila.img) > 1) {
nuevosErrores.push(`Q=${fila.q}: El IMg debería ser $${fila.img}`);
}
}
});
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0 && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setRespuestas({});
setValidado(false);
setErrores([]);
};
const maxIT = Math.max(...datosCalculados.map(d => d.it));
const maxIMG = Math.max(...datosCalculados.filter(d => d.img !== null).map(d => Math.abs(d.img || 0)));
const escalaIT = maxIT > 0 ? 120 / maxIT : 1;
const escalaIMG = maxIMG > 0 ? 60 / maxIMG : 1;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Ingreso Marginal (IMg)"
subtitle="El ingreso adicional por vender una unidad más"
/>
<div className="bg-purple-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Activity className="w-5 h-5 text-purple-600" />
<span className="font-semibold text-purple-800">Concepto</span>
</div>
<p className="text-sm text-purple-700">
El Ingreso Marginal es el cambio en el ingreso total resultante de vender
una unidad adicional. Se calcula como: <strong>IMg = ΔIT / ΔQ</strong>.
Cuando el precio debe bajar para vender más, el IMg {'<'} IT.
</p>
</div>
<div className="h-56 bg-gray-50 rounded-lg p-4 mb-6">
<svg className="w-full h-full" viewBox="0 0 400 180">
<line x1="40" y1="160" x2="380" y2="160" stroke="#374151" strokeWidth="2" />
<line x1="40" y1="160" x2="40" y2="20" stroke="#374151" strokeWidth="2" />
<text x="210" y="175" textAnchor="middle" className="text-sm fill-gray-600 font-medium">Cantidad (Q)</text>
<text x="15" y="90" textAnchor="middle" className="text-sm fill-gray-600 font-medium" transform="rotate(-90 15 90)">$ (×100)</text>
{datosBase.map((d, i) => (
<g key={i}>
<line x1={60 + i * 35} y1="160" x2={60 + i * 35} y2="165" stroke="#374151" strokeWidth="1" />
<text x={60 + i * 35} y="175" textAnchor="middle" className="text-xs fill-gray-500">{d.q}</text>
</g>
))}
<polyline
fill="none"
stroke="#10b981"
strokeWidth="2"
points={datosCalculados.map((d, i) => `${60 + i * 35},${160 - d.it * escalaIT}`).join(' ')}
/>
<polyline
fill="none"
stroke="#7c3aed"
strokeWidth="2"
strokeDasharray="4"
points={datosCalculados
.filter(d => d.img !== null)
.map((d, i) => `${95 + i * 35},${160 - (d.img || 0) * escalaIMG - 50}`)
.join(' ')}
/>
<g transform="translate(280, 30)">
<line x1="0" y1="0" x2="20" y2="0" stroke="#10b981" strokeWidth="2" />
<text x="25" y="4" className="text-xs fill-gray-600">IT</text>
<line x1="0" y1="15" x2="20" y2="15" stroke="#7c3aed" strokeWidth="2" strokeDasharray="4" />
<text x="25" y="19" className="text-xs fill-gray-600">IMg</text>
</g>
</svg>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">P ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">IT ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700 bg-blue-50">IMg ($)</th>
</tr>
</thead>
<tbody>
{datosCalculados.map((fila) => (
<tr key={fila.q} className="border-b hover:bg-gray-50">
<td className="px-3 py-2 font-medium">{fila.q}</td>
<td className="px-3 py-2">{fila.p}</td>
<td className="px-3 py-2 font-medium text-green-600">{fila.it}</td>
<td className="px-3 py-2 bg-blue-50">
{fila.img !== null ? (
<Input
type="number"
value={respuestas[`img_${fila.q}`] || ''}
onChange={(e) => handleRespuestaChange(fila.q, e.target.value)}
className="w-24"
placeholder="IMg"
/>
) : (
<span className="text-gray-400">-</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gradient-to-r from-purple-50 to-blue-50 p-4 rounded-lg mb-4">
<h4 className="font-semibold text-gray-900 mb-2 flex items-center gap-2">
<Calculator className="w-5 h-5 text-purple-600" />
Cálculo del Ingreso Marginal:
</h4>
<p className="text-sm text-gray-700 mb-2">
IMg = IT(Q) - IT(Q-1)
</p>
<p className="text-sm text-gray-600">
Ejemplo: Cuando Q aumenta de 2 a 3 unidades, el IT pasa de $160 a $210.
El IMg de la 3ra unidad es $210 - $160 = $50.
</p>
</div>
<div className="flex gap-3">
<Button onClick={validarRespuestas} variant="primary">
<CheckCircle className="w-4 h-4 mr-2" />
Validar Cálculos
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Limpiar
</Button>
</div>
{validado && errores.length === 0 && (
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
<div className="flex items-center gap-2 text-success">
<CheckCircle className="w-5 h-5" />
<span className="font-medium">¡Todos los cálculos son correctos!</span>
</div>
</div>
)}
{validado && errores.length > 0 && (
<div className="mt-4 p-4 bg-error/10 border border-error rounded-lg">
<p className="font-medium text-error mb-2">Errores encontrados:</p>
<ul className="list-disc list-inside text-sm text-error">
{errores.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
)}
</Card>
<Card className="bg-purple-50 border-purple-200">
<h4 className="font-semibold text-purple-900 mb-2">Importancia del Ingreso Marginal:</h4>
<ul className="space-y-1 text-sm text-purple-800">
<li> <strong>Regla de maximización:</strong> La empresa maximiza beneficios cuando IMg = CMg</li>
<li> <strong>IMg {'<'} P:</strong> Cuando debe bajar el precio para vender más, el IMg es menor que el precio</li>
<li> <strong>IMg positivo:</strong> Mientras IMg {'>'} 0, el ingreso total aumenta</li>
<li> <strong>IMg negativo:</strong> Si IMg {'<'} 0, vender más reduce el ingreso total</li>
</ul>
</Card>
</div>
);
}
export default IngresoMarginal;

View File

@@ -0,0 +1,273 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, DollarSign, RotateCcw, TrendingUp } from 'lucide-react';
interface IngresoTotalProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Producto {
nombre: string;
precio: number;
}
export function IngresoTotal({ ejercicioId: _ejercicioId, onComplete }: IngresoTotalProps) {
const productos: Producto[] = [
{ nombre: 'Libros', precio: 25 },
{ nombre: 'Electrónicos', precio: 150 },
{ nombre: 'Ropa', precio: 45 },
];
const [productoSeleccionado, setProductoSeleccionado] = useState(0);
const [cantidad, setCantidad] = useState(100);
const [respuestaIT, setRespuestaIT] = useState('');
const [validado, setValidado] = useState(false);
const [error, setError] = useState('');
const precio = productos[productoSeleccionado].precio;
const ingresoTotal = useMemo(() => precio * cantidad, [precio, cantidad]);
const datosTabla = useMemo(() => {
const datos = [];
for (let q = 0; q <= 200; q += 20) {
datos.push({ q, it: precio * q });
}
return datos;
}, [precio]);
const handleValidar = () => {
const respuesta = parseFloat(respuestaIT);
if (Math.abs(respuesta - ingresoTotal) < 1) {
setError('');
setValidado(true);
if (onComplete) {
onComplete(100);
}
} else {
setError(`Incorrecto. IT = P × Q = $${precio} × ${cantidad} = $${ingresoTotal.toLocaleString()}`);
setValidado(true);
}
};
const reiniciar = () => {
setCantidad(100);
setRespuestaIT('');
setValidado(false);
setError('');
};
const maxIT = Math.max(...datosTabla.map(d => d.it));
const escalaY = maxIT > 0 ? 120 / maxIT : 1;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Ingreso Total (IT)"
subtitle="IT = Precio × Cantidad vendida"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<DollarSign className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Fórmula Fundamental</span>
</div>
<p className="text-sm text-blue-700">
El Ingreso Total representa el dinero total que recibe una empresa por la venta
de sus productos. Se calcula multiplicando el precio de venta por la cantidad
vendida: <strong>IT = P × Q</strong>
</p>
</div>
<div className="grid md:grid-cols-2 gap-6 mb-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Producto
</label>
<select
value={productoSeleccionado}
onChange={(e) => {
setProductoSeleccionado(parseInt(e.target.value));
setValidado(false);
setRespuestaIT('');
}}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
>
{productos.map((p, i) => (
<option key={i} value={i}>
{p.nombre} - ${p.precio} c/u
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Cantidad vendida (Q): {cantidad} unidades
</label>
<input
type="range"
min="0"
max="200"
step="10"
value={cantidad}
onChange={(e) => {
setCantidad(parseInt(e.target.value));
setValidado(false);
setRespuestaIT('');
}}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
</div>
</div>
<div className="grid md:grid-cols-3 gap-4 mb-6">
<div className="bg-gray-50 p-4 rounded-lg text-center">
<p className="text-sm text-gray-600 mb-1">Precio (P)</p>
<p className="text-2xl font-bold text-primary">${precio}</p>
</div>
<div className="bg-gray-50 p-4 rounded-lg text-center">
<p className="text-sm text-gray-600 mb-1">Cantidad (Q)</p>
<p className="text-2xl font-bold text-secondary">{cantidad}</p>
</div>
<div className="bg-green-50 p-4 rounded-lg text-center border-2 border-green-200">
<p className="text-sm text-green-600 mb-1">Ingreso Total (IT)</p>
<p className="text-3xl font-bold text-green-700">${ingresoTotal.toLocaleString()}</p>
</div>
</div>
<div className="h-48 bg-gray-50 rounded-lg p-4 mb-6">
<svg className="w-full h-full" viewBox="0 0 400 150">
<line x1="40" y1="130" x2="380" y2="130" stroke="#374151" strokeWidth="2" />
<line x1="40" y1="130" x2="40" y2="20" stroke="#374151" strokeWidth="2" />
<text x="210" y="145" textAnchor="middle" className="text-sm fill-gray-600 font-medium">Cantidad (Q)</text>
<text x="15" y="75" textAnchor="middle" className="text-sm fill-gray-600 font-medium" transform="rotate(-90 15 75)">IT ($)</text>
{[0, 50, 100, 150, 200].map((q) => (
<g key={q}>
<line x1={40 + (q / 200) * 300} y1="130" x2={40 + (q / 200) * 300} y2="135" stroke="#374151" strokeWidth="1" />
<text x={40 + (q / 200) * 300} y="145" textAnchor="middle" className="text-xs fill-gray-500">{q}</text>
</g>
))}
<line
x1="40"
y1="130"
x2="340"
y2={130 - (datosTabla[datosTabla.length - 1].it * escalaY)}
stroke="#10b981"
strokeWidth="3"
/>
<circle
cx={40 + (cantidad / 200) * 300}
cy={130 - (ingresoTotal * escalaY)}
r="6"
fill="#10b981"
stroke="white"
strokeWidth="2"
/>
<text
x={40 + (cantidad / 200) * 300}
y={120 - (ingresoTotal * escalaY)}
textAnchor="middle"
className="text-xs fill-green-700 font-bold"
>
(${ingresoTotal.toLocaleString()})
</text>
</svg>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">P ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">IT ($)</th>
</tr>
</thead>
<tbody>
{datosTabla.filter((_, i) => i % 2 === 0).map((d, i) => (
<tr
key={i}
className={`border-b hover:bg-gray-50 ${d.q === cantidad ? 'bg-green-50' : ''}`}
>
<td className="px-3 py-2 font-medium">{d.q}</td>
<td className="px-3 py-2">${precio}</td>
<td className="px-3 py-2 font-medium text-primary">${d.it.toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gradient-to-r from-blue-50 to-purple-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-blue-600" />
Calcula el Ingreso Total:
</h4>
<div className="flex items-center gap-4">
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
IT = P × Q = ${precio} × {cantidad} = ?
</label>
<Input
type="number"
value={respuestaIT}
onChange={(e) => {
setRespuestaIT(e.target.value);
setValidado(false);
}}
className="w-full"
placeholder="Ingresa el IT"
/>
</div>
</div>
</div>
<div className="mt-4 flex gap-3">
<Button onClick={handleValidar} variant="primary" disabled={!respuestaIT}>
<CheckCircle className="w-4 h-4 mr-2" />
Validar
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Cambiar valores
</Button>
</div>
{validado && !error && (
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
<div className="flex items-center gap-2 text-success">
<CheckCircle className="w-5 h-5" />
<span className="font-medium">¡Correcto! IT = ${ingresoTotal.toLocaleString()}</span>
</div>
</div>
)}
{validado && error && (
<div className="mt-4 p-4 bg-error/10 border border-error rounded-lg">
<p className="text-error">{error}</p>
</div>
)}
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Fórmula del Ingreso Total:</h4>
<div className="text-center py-4">
<p className="text-2xl font-bold text-blue-800">IT = P × Q</p>
<p className="text-sm text-blue-600 mt-2">
Donde: IT = Ingreso Total, P = Precio, Q = Cantidad vendida
</p>
</div>
</Card>
</div>
);
}
export default IngresoTotal;

View File

@@ -0,0 +1,173 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, TrendingDown } from 'lucide-react';
export function LeyRendimientosDecrecientes() {
const [respuesta, setRespuesta] = useState<string | null>(null);
const [mostrarExplicacion, setMostrarExplicacion] = useState(false);
const validarRespuesta = () => {
setMostrarExplicacion(true);
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Ley de Rendimientos Decrecientes"
subtitle="Comprende cómo los rendimientos marginales disminuyen a medida que aumenta una variable productiva"
/>
<div className="space-y-4">
<div className="bg-amber-50 p-4 rounded-lg border border-amber-200">
<p className="text-sm text-amber-800">
<strong>Escenario:</strong> Un granjero tiene 100 hectáreas de tierra fijas.
Puede contratar más trabajadores, pero la cantidad de tierra no cambia.
</p>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 mb-4">Producción de Trigo (toneladas)</h4>
<svg className="w-full h-64" viewBox="0 0 500 220">
{/* Ejes */}
<line x1="50" y1="180" x2="450" y2="180" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="180" x2="50" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas eje X - Trabajadores */}
<text x="90" y="200" textAnchor="middle" className="text-xs fill-gray-600">1</text>
<text x="170" y="200" textAnchor="middle" className="text-xs fill-gray-600">2</text>
<text x="250" y="200" textAnchor="middle" className="text-xs fill-gray-600">3</text>
<text x="330" y="200" textAnchor="middle" className="text-xs fill-gray-600">4</text>
<text x="410" y="200" textAnchor="middle" className="text-xs fill-gray-600">5</text>
<text x="250" y="215" textAnchor="middle" className="text-sm fill-gray-700 font-medium">Número de Trabajadores</text>
{/* Etiquetas eje Y - Producción */}
<text x="35" y="185" textAnchor="end" className="text-xs fill-gray-600">0</text>
<text x="35" y="145" textAnchor="end" className="text-xs fill-gray-600">50</text>
<text x="35" y="105" textAnchor="end" className="text-xs fill-gray-600">100</text>
<text x="35" y="65" textAnchor="end" className="text-xs fill-gray-600">150</text>
<text x="35" y="25" textAnchor="end" className="text-xs fill-gray-600">200</text>
<text x="15" y="100" textAnchor="middle" className="text-sm fill-gray-700 font-medium" transform="rotate(-90 15 100)">Producción (Tn)</text>
{/* Líneas de cuadrícula */}
<line x1="50" y1="140" x2="450" y2="140" stroke="#e5e7eb" strokeWidth="1" strokeDasharray="4" />
<line x1="50" y1="100" x2="450" y2="100" stroke="#e5e7eb" strokeWidth="1" strokeDasharray="4" />
<line x1="50" y1="60" x2="450" y2="60" stroke="#e5e7eb" strokeWidth="1" strokeDasharray="4" />
{/* Curva de producción total */}
<path
d="M 50,180 Q 90,140 170,100 Q 250,70 330,60 Q 410,55 450,65"
fill="none"
stroke="#2563eb"
strokeWidth="3"
/>
{/* Puntos de datos */}
<circle cx="90" cy="140" r="6" fill="#2563eb" />
<circle cx="170" cy="100" r="6" fill="#2563eb" />
<circle cx="250" cy="75" r="6" fill="#2563eb" />
<circle cx="330" cy="65" r="6" fill="#2563eb" />
<circle cx="410" cy="68" r="6" fill="#ef4444" />
{/* Etiquetas de puntos */}
<text x="90" y="125" textAnchor="middle" className="text-xs fill-gray-700">50Tn</text>
<text x="170" y="85" textAnchor="middle" className="text-xs fill-gray-700">100Tn</text>
<text x="250" y="60" textAnchor="middle" className="text-xs fill-gray-700">135Tn</text>
<text x="330" y="50" textAnchor="middle" className="text-xs fill-gray-700">155Tn</text>
<text x="410" y="53" textAnchor="middle" className="text-xs fill-red-600 font-bold">160Tn</text>
{/* Flecha indicando decrecimiento */}
<path d="M 370,50 Q 390,45 400,60" fill="none" stroke="#ef4444" strokeWidth="2" markerEnd="url(#arrow)" />
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#ef4444" />
</marker>
</defs>
</svg>
</div>
<div className="bg-white border rounded-lg p-4">
<h4 className="font-semibold text-gray-900 mb-3">
¿Qué observas en el punto del 5to trabajador?
</h4>
<div className="space-y-2">
<button
onClick={() => setRespuesta('a')}
className={`w-full p-3 rounded-lg border-2 text-left transition-all ${
respuesta === 'a'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
>
<span className="font-medium">a)</span> La producción aumenta más rápido que antes
</button>
<button
onClick={() => setRespuesta('b')}
className={`w-full p-3 rounded-lg border-2 text-left transition-all ${
respuesta === 'b'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
>
<span className="font-medium">b)</span> El incremento de producción es menor (solo 5Tn adicionales)
</button>
<button
onClick={() => setRespuesta('c')}
className={`w-full p-3 rounded-lg border-2 text-left transition-all ${
respuesta === 'c'
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-blue-300'
}`}
>
<span className="font-medium">c)</span> La producción total disminuye
</button>
</div>
</div>
<Button onClick={validarRespuesta} disabled={!respuesta}>
Validar Respuesta
</Button>
{mostrarExplicacion && (
<div className={`p-4 rounded-lg border ${respuesta === 'b' ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}>
<div className="flex items-center gap-2 mb-2">
{respuesta === 'b' ? (
<CheckCircle className="w-5 h-5 text-green-600" />
) : (
<XCircle className="w-5 h-5 text-red-600" />
)}
<span className={`font-semibold ${respuesta === 'b' ? 'text-green-800' : 'text-red-800'}`}>
{respuesta === 'b' ? '¡Correcto!' : 'Incorrecto'}
</span>
</div>
<p className={`text-sm ${respuesta === 'b' ? 'text-green-700' : 'text-red-700'}`}>
La respuesta correcta es <strong>b)</strong>. Con el 5to trabajador, la producción
solo aumenta de 155Tn a 160Tn (5Tn adicionales), mientras que el 2do trabajador
aportó 50Tn adicionales. Esto demuestra la <strong>Ley de Rendimientos Decrecientes</strong>:
a medida que aumentamos una variable productiva (trabajo) manteniendo fijas las demás
(tierra), el producto marginal disminuye.
</p>
</div>
)}
</div>
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2 flex items-center gap-2">
<TrendingDown className="w-5 h-5" />
Fórmula del Producto Marginal
</h4>
<p className="text-sm text-blue-800">
<strong>PMg = ΔProducción Total / ΔTrabajadores</strong>
</p>
<p className="text-sm text-blue-700 mt-2">
PMg (12) = (100-50)/(2-1) = 50 Tn<br />
PMg (45) = (160-155)/(5-4) = 5 Tn
</p>
</Card>
</div>
);
}
export default LeyRendimientosDecrecientes;

View File

@@ -0,0 +1,233 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Calculator, TrendingDown, TrendingUp } from 'lucide-react';
interface ProductoMarginalProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FilaDatos {
L: number;
PT: number;
PMg: number | null;
}
export function ProductoMarginal({ ejercicioId: _ejercicioId, onComplete }: ProductoMarginalProps) {
const [respuestas, setRespuestas] = useState<Record<number, string>>({});
const [verificado, setVerificado] = useState(false);
const datosBase = [
{ L: 0, PT: 0 },
{ L: 1, PT: 10 },
{ L: 2, PT: 25 },
{ L: 3, PT: 45 },
{ L: 4, PT: 60 },
{ L: 5, PT: 70 },
{ L: 6, PT: 75 },
{ L: 7, PT: 75 },
{ L: 8, PT: 70 },
];
const datosCompletos: FilaDatos[] = useMemo(() => {
return datosBase.map((fila, index) => ({
L: fila.L,
PT: fila.PT,
PMg: index > 0 ? fila.PT - datosBase[index - 1].PT : null,
}));
}, []);
const handleInputChange = (L: number, value: string) => {
setRespuestas(prev => ({ ...prev, [L]: value }));
};
const handleVerificar = () => {
setVerificado(true);
let correctas = 0;
let total = 0;
datosCompletos.forEach(fila => {
if (fila.PMg !== null) {
total++;
if (parseInt(respuestas[fila.L]) === fila.PMg) {
correctas++;
}
}
});
if (correctas === total && onComplete) {
onComplete(100);
}
};
const handleReiniciar = () => {
setRespuestas({});
setVerificado(false);
};
const todasRespondidas = datosCompletos
.filter(f => f.PMg !== null)
.every(f => respuestas[f.L] !== undefined && respuestas[f.L] !== '');
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Producto Marginal (PMg)"
subtitle="Calcula el cambio en el producto total al aumentar una unidad de trabajo"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Calculator className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Fórmula</span>
</div>
<div className="bg-white p-3 rounded border border-blue-200">
<p className="font-mono text-lg text-center text-blue-900">
PMg = ΔPT / ΔL = (PT - PT) / (L - L)
</p>
</div>
<p className="text-sm text-blue-700 mt-3">
El <strong>Producto Marginal</strong> mide la producción adicional generada
al emplear una unidad más de trabajo, manteniendo constante el capital.
</p>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="px-4 py-3 border text-left">Trabajo (L)</th>
<th className="px-4 py-3 border text-left">Producto Total (PT)</th>
<th className="px-4 py-3 border text-left">Producto Marginal (PMg)</th>
<th className="px-4 py-3 border text-center">Estado</th>
</tr>
</thead>
<tbody>
{datosCompletos.map((fila) => (
<tr key={fila.L} className="border-b">
<td className="px-4 py-3 border font-medium">{fila.L}</td>
<td className="px-4 py-3 border font-mono">{fila.PT}</td>
<td className="px-4 py-3 border">
{fila.PMg === null ? (
<span className="text-gray-400"></span>
) : (
<div className="flex items-center gap-2">
<Input
type="number"
value={respuestas[fila.L] || ''}
onChange={(e) => handleInputChange(fila.L, e.target.value)}
disabled={verificado}
className={`w-24 ${
verificado
? parseInt(respuestas[fila.L]) === fila.PMg
? 'border-success bg-success/5'
: 'border-error bg-error/5'
: ''
}`}
placeholder="?"
/>
{verificado && (
<span className={
parseInt(respuestas[fila.L]) === fila.PMg
? 'text-success text-sm'
: 'text-error text-sm'
}>
{parseInt(respuestas[fila.L]) === fila.PMg ? '✓' : `${fila.PMg}`}
</span>
)}
</div>
)}
</td>
<td className="px-4 py-3 border text-center">
{fila.PMg !== null && (
<>
{fila.PMg > (datosCompletos[fila.L - 1]?.PMg || 0) ? (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
<TrendingUp className="w-3 h-3 mr-1" />
Creciente
</span>
) : fila.PMg > 0 ? (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
Decreciente
</span>
) : (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
<TrendingDown className="w-3 h-3 mr-1" />
Negativo
</span>
)}
</>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-6 bg-gray-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-3">Ley de los Rendimientos Marginales Decrecientes</h4>
<p className="text-sm text-gray-700 mb-3">
A medida que se agregan más unidades de un factor variable (trabajo) a un factor
fijo (capital), el producto marginal eventualmente disminuirá.
</p>
<div className="grid md:grid-cols-3 gap-3 text-sm">
<div className="bg-green-50 p-3 rounded border border-green-200">
<p className="font-medium text-green-800">Fase 1: PMg creciente</p>
<p className="text-green-700">Especialización y eficiencia</p>
</div>
<div className="bg-yellow-50 p-3 rounded border border-yellow-200">
<p className="font-medium text-yellow-800">Fase 2: PMg decreciente</p>
<p className="text-yellow-700">Ley de rendimientos decrecientes</p>
</div>
<div className="bg-red-50 p-3 rounded border border-red-200">
<p className="font-medium text-red-800">Fase 3: PMg negativo</p>
<p className="text-red-700">Hacinamiento/sobrepoblación</p>
</div>
</div>
</div>
</Card>
<div className="flex justify-between items-center">
<div className="text-sm text-gray-600">
{!verificado ? (
<span>Completa todos los campos para verificar</span>
) : (
<span>
Correctos: {datosCompletos.filter(f =>
f.PMg !== null && parseInt(respuestas[f.L]) === f.PMg
).length} / {datosCompletos.filter(f => f.PMg !== null).length}
</span>
)}
</div>
<div className="flex gap-3">
{!verificado ? (
<Button onClick={handleVerificar} disabled={!todasRespondidas}>
Verificar Cálculos
</Button>
) : (
<>
<Button onClick={handleReiniciar} variant="outline">
Reiniciar
</Button>
{datosCompletos.filter(f =>
f.PMg !== null && parseInt(respuestas[f.L]) === f.PMg
).length === datosCompletos.filter(f => f.PMg !== null).length && (
<Button onClick={() => onComplete?.(100)}>
<CheckCircle className="w-4 h-4 mr-2" />
Completar
</Button>
)}
</>
)}
</div>
</div>
</div>
);
}
export default ProductoMarginal;

View File

@@ -0,0 +1,247 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Divide, ArrowRight } from 'lucide-react';
interface ProductoMedioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FilaDatos {
L: number;
PT: number;
PMe: number | null;
}
export function ProductoMedio({ ejercicioId: _ejercicioId, onComplete }: ProductoMedioProps) {
const [respuestas, setRespuestas] = useState<Record<number, string>>({});
const [verificado, setVerificado] = useState(false);
const datosBase = [
{ L: 1, PT: 10 },
{ L: 2, PT: 24 },
{ L: 3, PT: 39 },
{ L: 4, PT: 52 },
{ L: 5, PT: 60 },
{ L: 6, PT: 66 },
{ L: 7, PT: 70 },
{ L: 8, PT: 72 },
];
const datosCompletos: FilaDatos[] = useMemo(() => {
return datosBase.map(fila => ({
L: fila.L,
PT: fila.PT,
PMe: fila.L > 0 ? parseFloat((fila.PT / fila.L).toFixed(2)) : null,
}));
}, []);
const maxPMe = Math.max(...datosCompletos.map(d => d.PMe || 0));
const maxPMeL = datosCompletos.find(d => d.PMe === maxPMe)?.L;
const handleInputChange = (L: number, value: string) => {
setRespuestas(prev => ({ ...prev, [L]: value }));
};
const handleVerificar = () => {
setVerificado(true);
let correctas = 0;
datosCompletos.forEach(fila => {
const respuesta = parseFloat(respuestas[fila.L]);
if (Math.abs(respuesta - (fila.PMe || 0)) < 0.1) {
correctas++;
}
});
if (correctas === datosCompletos.length && onComplete) {
onComplete(100);
}
};
const handleReiniciar = () => {
setRespuestas({});
setVerificado(false);
};
const todasRespondidas = datosCompletos.every(f =>
respuestas[f.L] !== undefined && respuestas[f.L] !== ''
);
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Producto Medio (PMe)"
subtitle="Calcula el output por unidad de trabajo empleada"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Divide className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Fórmula</span>
</div>
<div className="bg-white p-3 rounded border border-blue-200">
<p className="font-mono text-lg text-center text-blue-900">
PMe = PT / L = Q / L
</p>
</div>
<p className="text-sm text-blue-700 mt-3">
El <strong>Producto Medio</strong> representa la producción por trabajador.
Mide la eficiencia promedio del factor trabajo.
</p>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="px-4 py-3 border text-left">Trabajo (L)</th>
<th className="px-4 py-3 border text-left">Producto Total (PT)</th>
<th className="px-4 py-3 border text-left">Producto Medio (PMe)</th>
<th className="px-4 py-3 border text-center">Estado</th>
</tr>
</thead>
<tbody>
{datosCompletos.map((fila) => (
<tr
key={fila.L}
className={`border-b ${fila.PMe === maxPMe ? 'bg-green-50' : ''}`}
>
<td className="px-4 py-3 border font-medium">{fila.L}</td>
<td className="px-4 py-3 border font-mono">{fila.PT}</td>
<td className="px-4 py-3 border">
<div className="flex items-center gap-2">
<Input
type="number"
step="0.01"
value={respuestas[fila.L] || ''}
onChange={(e) => handleInputChange(fila.L, e.target.value)}
disabled={verificado}
className={`w-24 ${
verificado
? Math.abs(parseFloat(respuestas[fila.L]) - (fila.PMe || 0)) < 0.1
? 'border-success bg-success/5'
: 'border-error bg-error/5'
: ''
}`}
placeholder="?"
/>
{verificado && (
<span className={
Math.abs(parseFloat(respuestas[fila.L]) - (fila.PMe || 0)) < 0.1
? 'text-success text-sm'
: 'text-error text-sm'
}>
{Math.abs(parseFloat(respuestas[fila.L]) - (fila.PMe || 0)) < 0.1
? '✓'
: `${fila.PMe}`}
</span>
)}
</div>
</td>
<td className="px-4 py-3 border text-center">
{fila.PMe === maxPMe && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
Máximo
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gradient-to-r from-purple-50 to-blue-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-3">Relación entre PMg y PMe</h4>
<div className="space-y-3 text-sm text-gray-700">
<div className="flex items-start gap-3">
<ArrowRight className="w-4 h-4 text-purple-600 mt-0.5" />
<p>Cuando <strong>PMg {'>'} PMe</strong>, el producto medio está aumentando</p>
</div>
<div className="flex items-start gap-3">
<ArrowRight className="w-4 h-4 text-purple-600 mt-0.5" />
<p>Cuando <strong>PMg {'<'} PMe</strong>, el producto medio está disminuyendo</p>
</div>
<div className="flex items-start gap-3">
<ArrowRight className="w-4 h-4 text-purple-600 mt-0.5" />
<p>Cuando <strong>PMg = PMe</strong>, el producto medio está en su máximo</p>
</div>
</div>
</div>
</Card>
<Card>
<CardHeader
title="Pregunta de Análisis"
subtitle="Basado en los datos de la tabla"
/>
<div className="space-y-4">
<p className="text-gray-700">
<strong>Pregunta:</strong> ¿En qué nivel de trabajo (L) se alcanza el Producto Medio máximo
y cuál es su valor?
</p>
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
<div className="grid md:grid-cols-2 gap-4">
<div>
<p className="text-sm text-green-700 mb-1">Nivel de trabajo (L):</p>
<p className="font-bold text-green-900 text-xl">{maxPMeL} trabajadores</p>
</div>
<div>
<p className="text-sm text-green-700 mb-1">Producto Medio máximo:</p>
<p className="font-bold text-green-900 text-xl">{maxPMe} unidades/trabajador</p>
</div>
</div>
</div>
<p className="text-sm text-gray-600">
<strong>Interpretación:</strong> Cada trabajador produce en promedio {maxPMe} unidades
cuando hay {maxPMeL} trabajadores. Este es el punto de máxima eficiencia por trabajador.
</p>
</div>
</Card>
<div className="flex justify-between items-center">
<div className="text-sm text-gray-600">
{!verificado ? (
<span>Completa todos los cálculos con 2 decimales</span>
) : (
<span>
Correctos: {datosCompletos.filter(f =>
Math.abs(parseFloat(respuestas[f.L]) - (f.PMe || 0)) < 0.1
).length} / {datosCompletos.length}
</span>
)}
</div>
<div className="flex gap-3">
{!verificado ? (
<Button onClick={handleVerificar} disabled={!todasRespondidas}>
Verificar Cálculos
</Button>
) : (
<>
<Button onClick={handleReiniciar} variant="outline">
Reiniciar
</Button>
{datosCompletos.filter(f =>
Math.abs(parseFloat(respuestas[f.L]) - (f.PMe || 0)) < 0.1
).length === datosCompletos.length && (
<Button onClick={() => onComplete?.(100)}>
<CheckCircle className="w-4 h-4 mr-2" />
Completar
</Button>
)}
</>
)}
</div>
</div>
</div>
);
}
export default ProductoMedio;

View File

@@ -0,0 +1,223 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, TrendingUp, AlertCircle } from 'lucide-react';
interface ProductoTotalProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface FilaProduccion {
L: number;
Q: number;
}
const datosProduccion: FilaProduccion[] = [
{ L: 0, Q: 0 },
{ L: 1, Q: 8 },
{ L: 2, Q: 20 },
{ L: 3, Q: 36 },
{ L: 4, Q: 52 },
{ L: 5, Q: 64 },
{ L: 6, Q: 72 },
{ L: 7, Q: 76 },
{ L: 8, Q: 76 },
{ L: 9, Q: 72 },
];
export function ProductoTotal({ ejercicioId: _ejercicioId, onComplete }: ProductoTotalProps) {
const [respuestaMax, setRespuestaMax] = useState('');
const [respuestaL, setRespuestaL] = useState('');
const [verificado, setVerificado] = useState(false);
const [correcto, setCorrecto] = useState({ max: false, l: false });
const maxQ = Math.max(...datosProduccion.map(d => d.Q));
const maxL = datosProduccion.find(d => d.Q === maxQ)?.L;
const handleVerificar = () => {
const esCorrectoMax = parseInt(respuestaMax) === maxQ;
const esCorrectoL = parseInt(respuestaL) === maxL;
setCorrecto({ max: esCorrectoMax, l: esCorrectoL });
setVerificado(true);
if (esCorrectoMax && esCorrectoL && onComplete) {
onComplete(100);
}
};
const handleReiniciar = () => {
setRespuestaMax('');
setRespuestaL('');
setVerificado(false);
setCorrecto({ max: false, l: false });
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Producto Total (PT)"
subtitle="Analiza el output máximo producido con diferentes niveles de trabajo"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Definición</span>
</div>
<p className="text-sm text-blue-700">
El <strong>Producto Total (PT o Q)</strong> es la cantidad total de output producida
utilizando una cierta cantidad de un factor variable (generalmente trabajo L),
manteniendo fijos los demás factores.
</p>
<p className="text-sm text-blue-600 mt-2">
<strong>Fórmula:</strong> PT = Q = f(L) cuando K es constante
</p>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-100">
<th className="px-4 py-3 border text-left font-medium">Trabajo (L)</th>
<th className="px-4 py-3 border text-left font-medium">Producto Total (Q)</th>
<th className="px-4 py-3 border text-center font-medium">Estado</th>
</tr>
</thead>
<tbody>
{datosProduccion.map((fila, index) => (
<tr
key={fila.L}
className={`border-b ${
fila.Q === maxQ
? 'bg-green-50'
: index % 2 === 0
? 'bg-white'
: 'bg-gray-50'
}`}
>
<td className="px-4 py-3 border">{fila.L}</td>
<td className="px-4 py-3 border font-mono">{fila.Q}</td>
<td className="px-4 py-3 border text-center">
{fila.Q === maxQ && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
Máximo
</span>
)}
{fila.L > 0 && fila.Q < datosProduccion[index - 1].Q && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
Rendimientos negativos
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<div className="flex items-center gap-2 mb-2">
<AlertCircle className="w-5 h-5 text-yellow-600" />
<span className="font-semibold text-yellow-800">Análisis</span>
</div>
<ul className="text-sm text-yellow-700 space-y-1 ml-5 list-disc">
<li>La producción aumenta hasta cierto punto (L = 7 u 8)</li>
<li>Beyond that point, los rendimientos son decrecientes</li>
<li>Con L = 9, el producto total disminuye (rendimientos negativos)</li>
</ul>
</div>
</Card>
<Card>
<CardHeader
title="Ejercicio de Cálculo"
subtitle="Responde basándote en la tabla anterior"
/>
<div className="space-y-6">
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-3">
<label className="block text-sm font-medium text-gray-700">
¿Cuál es el Producto Total máximo?
</label>
<Input
type="number"
value={respuestaMax}
onChange={(e) => setRespuestaMax(e.target.value)}
placeholder="Valor de Q máximo"
disabled={verificado}
className={verificado
? correcto.max
? 'border-success bg-success/5'
: 'border-error bg-error/5'
: ''
}
/>
{verificado && (
<p className={`text-sm ${correcto.max ? 'text-success' : 'text-error'}`}>
{correcto.max ? '✓ Correcto' : `✗ Incorrecto. La respuesta es ${maxQ}`}
</p>
)}
</div>
<div className="space-y-3">
<label className="block text-sm font-medium text-gray-700">
¿Con cuántos trabajadores (L) se alcanza este máximo?
</label>
<Input
type="number"
value={respuestaL}
onChange={(e) => setRespuestaL(e.target.value)}
placeholder="Valor de L"
disabled={verificado}
className={verificado
? correcto.l
? 'border-success bg-success/5'
: 'border-error bg-error/5'
: ''
}
/>
{verificado && (
<p className={`text-sm ${correcto.l ? 'text-success' : 'text-error'}`}>
{correcto.l ? '✓ Correcto' : `✗ Incorrecto. La respuesta es ${maxL}`}
</p>
)}
</div>
</div>
<div className="flex gap-3">
{!verificado ? (
<Button
onClick={handleVerificar}
disabled={!respuestaMax || !respuestaL}
>
Verificar Respuestas
</Button>
) : (
<>
<Button onClick={handleReiniciar} variant="outline">
Intentar de Nuevo
</Button>
{(correcto.max && correcto.l) && (
<Button
onClick={() => onComplete?.(100)}
variant="primary"
>
<CheckCircle className="w-4 h-4 mr-2" />
Completar
</Button>
)}
</>
)}
</div>
</div>
</Card>
</div>
);
}
export default ProductoTotal;

View File

@@ -0,0 +1,199 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, Brain } from 'lucide-react';
export function ProductorRacional() {
const [respuestas, setRespuestas] = useState<{[key: string]: boolean | null}>({
afirmacion1: null,
afirmacion2: null,
afirmacion3: null,
afirmacion4: null,
});
const [mostrarResultados, setMostrarResultados] = useState(false);
const afirmaciones = [
{
id: 'afirmacion1',
texto: 'Un productor racional siempre busca minimizar costos para un nivel dado de producción.',
esCorrecta: true,
explicacion: 'Correcto. La racionalidad económica implica optimizar recursos, lo que incluye minimizar costos para producir una cantidad determinada.'
},
{
id: 'afirmacion2',
texto: 'Producir en la Etapa III es racional si los precios son muy altos.',
esCorrecta: false,
explicacion: 'Incorrecto. En la Etapa III el producto marginal es negativo, por lo que producir más disminuye el output total. Nunca es racional operar aquí.'
},
{
id: 'afirmacion3',
texto: 'El productor racional equilibra el ingreso marginal con el costo marginal.',
esCorrecta: true,
explicacion: 'Correcto. La condición de maximización de beneficios es IMg = CMg. Producir donde el ingreso adicional iguala al costo adicional.'
},
{
id: 'afirmacion4',
texto: 'Producir en la Etapa I es óptimo porque los rendimientos son crecientes.',
esCorrecta: false,
explicacion: 'Incorrecto. Aunque los rendimientos son crecientes en la Etapa I, el productor puede aumentar la producción y los beneficios moviéndose a la Etapa II.'
}
];
const seleccionarRespuesta = (id: string, valor: boolean) => {
setRespuestas(prev => ({ ...prev, [id]: valor }));
setMostrarResultados(false);
};
const validar = () => {
setMostrarResultados(true);
};
const todasRespondidas = Object.values(respuestas).every(r => r !== null);
const correctas = afirmaciones.filter(a => respuestas[a.id] === a.esCorrecta).length;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="El Productor Racional"
subtitle="Determina qué afirmaciones describen correctamente el comportamiento de un productor racional"
/>
<div className="space-y-6">
{/* Diagrama de decisión */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 mb-4 text-center">Zona de Decisión del Productor</h4>
<svg className="w-full h-56" viewBox="0 0 500 200">
{/* Ejes */}
<line x1="50" y1="170" x2="450" y2="170" stroke="#374151" strokeWidth="2" />
<line x1="50" y1="170" x2="50" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas */}
<text x="250" y="195" textAnchor="middle" className="text-sm fill-gray-700 font-medium">Cantidad de Trabajo</text>
<text x="20" y="95" textAnchor="middle" className="text-sm fill-gray-700 font-medium" transform="rotate(-90 20 95)">PT</text>
{/* Curva PT */}
<path
d="M 50,170 Q 150,130 250,100 Q 350,70 400,90 Q 430,110 450,150"
fill="none"
stroke="#374151"
strokeWidth="2"
/>
{/* Zona I */}
<rect x="50" y="20" width="100" height="150" fill="#dcfce7" opacity="0.6" />
<text x="100" y="40" textAnchor="middle" className="text-xs font-bold fill-green-700">ZONA I</text>
<text x="100" y="55" textAnchor="middle" className="text-xs fill-green-600">No óptima</text>
{/* Zona II - ZONA RACIONAL */}
<rect x="150" y="20" width="200" height="150" fill="#dbeafe" opacity="0.8" stroke="#2563eb" strokeWidth="3" strokeDasharray="8" />
<text x="250" y="45" textAnchor="middle" className="text-base font-bold fill-blue-700">ZONA RACIONAL</text>
<text x="250" y="65" textAnchor="middle" className="text-xs fill-blue-600">ETAPA II</text>
<text x="250" y="80" textAnchor="middle" className="text-xs fill-blue-600">Donde opera el</text>
<text x="250" y="95" textAnchor="middle" className="text-xs fill-blue-600">productor eficiente</text>
{/* Zona III */}
<rect x="350" y="20" width="100" height="150" fill="#fee2e2" opacity="0.6" />
<text x="400" y="40" textAnchor="middle" className="text-xs font-bold fill-red-700">ZONA III</text>
<text x="400" y="55" textAnchor="middle" className="text-xs fill-red-600">Irracional</text>
{/* Límites */}
<line x1="150" y1="20" x2="150" y2="170" stroke="#22c55e" strokeWidth="2" />
<line x1="350" y1="20" x2="350" y2="170" stroke="#ef4444" strokeWidth="2" />
</svg>
</div>
{/* Afirmaciones */}
<div className="space-y-4">
{afirmaciones.map((afirmacion, index) => (
<div key={afirmacion.id} className="bg-white border rounded-lg p-4">
<div className="flex items-start gap-3 mb-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-gray-100 text-gray-700 flex items-center justify-center text-sm font-bold">
{index + 1}
</span>
<p className="text-gray-800">{afirmacion.texto}</p>
</div>
<div className="flex gap-3 ml-9">
<button
onClick={() => seleccionarRespuesta(afirmacion.id, true)}
disabled={mostrarResultados}
className={`flex-1 p-2 rounded-lg border-2 text-center transition-all ${
respuestas[afirmacion.id] === true && !mostrarResultados
? 'border-blue-500 bg-blue-50'
: mostrarResultados && afirmacion.esCorrecta === true
? 'border-green-500 bg-green-50'
: mostrarResultados && respuestas[afirmacion.id] === true && afirmacion.esCorrecta === false
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<span className="font-medium">VERDADERO</span>
{mostrarResultados && afirmacion.esCorrecta && (
<CheckCircle className="w-4 h-4 text-green-600 mx-auto mt-1" />
)}
{mostrarResultados && respuestas[afirmacion.id] === true && !afirmacion.esCorrecta && (
<XCircle className="w-4 h-4 text-red-600 mx-auto mt-1" />
)}
</button>
<button
onClick={() => seleccionarRespuesta(afirmacion.id, false)}
disabled={mostrarResultados}
className={`flex-1 p-2 rounded-lg border-2 text-center transition-all ${
respuestas[afirmacion.id] === false && !mostrarResultados
? 'border-blue-500 bg-blue-50'
: mostrarResultados && afirmacion.esCorrecta === false
? 'border-green-500 bg-green-50'
: mostrarResultados && respuestas[afirmacion.id] === false && afirmacion.esCorrecta === true
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<span className="font-medium">FALSO</span>
{mostrarResultados && !afirmacion.esCorrecta && (
<CheckCircle className="w-4 h-4 text-green-600 mx-auto mt-1" />
)}
{mostrarResultados && respuestas[afirmacion.id] === false && afirmacion.esCorrecta && (
<XCircle className="w-4 h-4 text-red-600 mx-auto mt-1" />
)}
</button>
</div>
{mostrarResultados && (
<div className={`mt-3 p-3 rounded text-sm ${respuestas[afirmacion.id] === afirmacion.esCorrecta ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'}`}>
{afirmacion.explicacion}
</div>
)}
</div>
))}
</div>
<Button onClick={validar} disabled={!todasRespondidas || mostrarResultados}>
Validar Respuestas
</Button>
{mostrarResultados && (
<div className={`p-4 rounded-lg border ${correctas === 4 ? 'bg-green-50 border-green-200' : 'bg-amber-50 border-amber-200'}`}>
<div className="flex items-center gap-2 mb-2">
<Brain className="w-5 h-5 text-gray-700" />
<span className="font-semibold">Resultado: {correctas}/4 correctas</span>
</div>
{correctas === 4 && (
<p className="text-sm text-green-700">
¡Excelente! Comprendes perfectamente qué hace racional a un productor.
</p>
)}
{correctas < 4 && (
<p className="text-sm text-amber-700">
Revisa las explicaciones para entender mejor el comportamiento del productor racional.
</p>
)}
</div>
)}
</div>
</Card>
</div>
);
}
export default ProductorRacional;

View File

@@ -0,0 +1,310 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Power, RotateCcw, AlertTriangle, Calculator } from 'lucide-react';
interface PuntoCierreEquilibrioProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface Escenario {
nombre: string;
precio: number;
q: number;
cf: number;
cv: number;
descripcion: string;
}
export function PuntoCierreEquilibrio({ ejercicioId: _ejercicioId, onComplete }: PuntoCierreEquilibrioProps) {
const escenarios: Escenario[] = [
{ nombre: 'Beneficios', precio: 60, q: 100, cf: 2000, cv: 3000, descripcion: 'P > CMe: La empresa gana dinero' },
{ nombre: 'Equilibrio', precio: 50, q: 100, cf: 2000, cv: 3000, descripcion: 'P = CMe: Beneficio = 0 (normal)' },
{ nombre: 'Pérdida pero opera', precio: 35, q: 100, cf: 2000, cv: 3000, descripcion: 'CVMe < P < CMe: Cubre CV, parte de CF' },
{ nombre: 'Punto de cierre', precio: 30, q: 100, cf: 2000, cv: 3000, descripcion: 'P = CVMe: Debe cerrar a largo plazo' },
{ nombre: 'Cierre inmediato', precio: 25, q: 100, cf: 2000, cv: 3000, descripcion: 'P < CVMe: Debe cerrar inmediatamente' },
];
const [escenarioSeleccionado, setEscenarioSeleccionado] = useState(0);
const [respuestas, setRespuestas] = useState({
ingresoTotal: '',
costoTotal: '',
costoVariable: '',
beneficio: '',
decision: '',
});
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const escenario = escenarios[escenarioSeleccionado];
const calculos = useMemo(() => {
const it = escenario.precio * escenario.q;
const ct = escenario.cf + escenario.cv;
const cvme = escenario.cv / escenario.q;
const cme = ct / escenario.q;
const beneficio = it - ct;
return { it, ct, cvme, cme, beneficio };
}, [escenario]);
const decisionCorrecta = useMemo(() => {
if (calculos.beneficio >= 0) return 'producir';
if (escenario.precio > calculos.cvme) return 'producir_perdida';
return 'cerrar';
}, [calculos, escenario.precio]);
const handleRespuestaChange = (campo: string, valor: string) => {
setRespuestas(prev => ({ ...prev, [campo]: valor }));
setValidado(false);
};
const validarRespuestas = () => {
const nuevosErrores: string[] = [];
if (parseFloat(respuestas.ingresoTotal) !== calculos.it) {
nuevosErrores.push(`IT incorrecto. IT = P × Q = ${escenario.precio} × ${escenario.q}`);
}
if (parseFloat(respuestas.costoTotal) !== calculos.ct) {
nuevosErrores.push(`CT incorrecto. CT = CF + CV = ${escenario.cf} + ${escenario.cv}`);
}
if (parseFloat(respuestas.costoVariable) !== escenario.cv) {
nuevosErrores.push(`CV incorrecto. El CV es ${escenario.cv}`);
}
if (parseFloat(respuestas.beneficio) !== calculos.beneficio) {
nuevosErrores.push(`Beneficio incorrecto. Beneficio = IT - CT`);
}
const respDecision = respuestas.decision.toLowerCase().trim();
const esCorrecto =
(decisionCorrecta === 'producir' && (respDecision.includes('producir') || respDecision.includes('continuar'))) ||
(decisionCorrecta === 'producir_perdida' && (respDecision.includes('producir') || respDecision.includes('operar'))) ||
(decisionCorrecta === 'cerrar' && (respDecision.includes('cerrar') || respDecision.includes('parar')));
if (!esCorrecto) {
if (decisionCorrecta === 'producir') {
nuevosErrores.push('La empresa debe seguir produciendo porque obtiene beneficios.');
} else if (decisionCorrecta === 'producir_perdida') {
nuevosErrores.push('La empresa debe seguir produciendo en el corto plazo porque P > CVMe (cubre los costos variables).');
} else {
nuevosErrores.push('La empresa debe cerrar porque P < CVMe (no cubre los costos variables).');
}
}
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0 && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setRespuestas({ ingresoTotal: '', costoTotal: '', costoVariable: '', beneficio: '', decision: '' });
setValidado(false);
setErrores([]);
};
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Punto de Cierre y Equilibrio"
subtitle="Decisiones de producción en el corto plazo"
/>
<div className="bg-orange-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Power className="w-5 h-5 text-orange-600" />
<span className="font-semibold text-orange-800">Reglas de Decisión</span>
</div>
<p className="text-sm text-orange-700">
<strong>Punto de cierre:</strong> Si P {'<'} CVMe, la empresa debe cerrar inmediatamente
porque ni siquiera cubre los costos variables. <strong>Equilibrio:</strong> Si P = CMe,
la empresa obtiene beneficio cero (beneficio normal).
</p>
</div>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">Selecciona un escenario:</label>
<select
value={escenarioSeleccionado}
onChange={(e) => {
setEscenarioSeleccionado(parseInt(e.target.value));
reiniciar();
}}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
>
{escenarios.map((e, i) => (
<option key={i} value={i}>{e.nombre}</option>
))}
</select>
</div>
<div className={`p-4 rounded-lg mb-6 ${
escenarioSeleccionado === 0 ? 'bg-green-50 border-2 border-green-300' :
escenarioSeleccionado === 1 ? 'bg-blue-50 border-2 border-blue-300' :
escenarioSeleccionado === 2 ? 'bg-yellow-50 border-2 border-yellow-300' :
escenarioSeleccionado === 3 ? 'bg-orange-50 border-2 border-orange-300' :
'bg-red-50 border-2 border-red-300'
}`}>
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className={`w-5 h-5 ${
escenarioSeleccionado <= 1 ? 'text-green-600' :
escenarioSeleccionado === 2 ? 'text-yellow-600' :
'text-red-600'
}`} />
<span className={`font-semibold ${
escenarioSeleccionado <= 1 ? 'text-green-800' :
escenarioSeleccionado === 2 ? 'text-yellow-800' :
'text-red-800'
}`}>
{escenario.nombre}
</span>
</div>
<p className="text-sm text-gray-700">{escenario.descripcion}</p>
</div>
<div className="grid md:grid-cols-5 gap-4 mb-6">
<div className="bg-gray-50 p-3 rounded-lg text-center">
<p className="text-xs text-gray-600 mb-1">Precio (P)</p>
<p className="text-xl font-bold text-primary">${escenario.precio}</p>
</div>
<div className="bg-gray-50 p-3 rounded-lg text-center">
<p className="text-xs text-gray-600 mb-1">Cantidad (Q)</p>
<p className="text-xl font-bold text-secondary">{escenario.q}</p>
</div>
<div className="bg-gray-50 p-3 rounded-lg text-center">
<p className="text-xs text-gray-600 mb-1">Costo Fijo (CF)</p>
<p className="text-xl font-bold text-gray-700">${escenario.cf}</p>
</div>
<div className="bg-gray-50 p-3 rounded-lg text-center">
<p className="text-xs text-gray-600 mb-1">Costo Variable (CV)</p>
<p className="text-xl font-bold text-gray-700">${escenario.cv}</p>
</div>
<div className={`p-3 rounded-lg text-center border-2 ${
calculos.beneficio >= 0 ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'
}`}>
<p className="text-xs text-gray-600 mb-1">CMe ($)</p>
<p className="text-xl font-bold">{calculos.cme.toFixed(2)}</p>
</div>
</div>
<div className="bg-gradient-to-r from-blue-50 to-purple-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Calculator className="w-5 h-5 text-blue-600" />
Completa los cálculos:
</h4>
<div className="grid md:grid-cols-5 gap-4">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Ingreso Total ($)</label>
<Input
type="number"
value={respuestas.ingresoTotal}
onChange={(e) => handleRespuestaChange('ingresoTotal', e.target.value)}
className="w-full"
placeholder="P × Q"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Costo Total ($)</label>
<Input
type="number"
value={respuestas.costoTotal}
onChange={(e) => handleRespuestaChange('costoTotal', e.target.value)}
className="w-full"
placeholder="CF + CV"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">CV Total ($)</label>
<Input
type="number"
value={respuestas.costoVariable}
onChange={(e) => handleRespuestaChange('costoVariable', e.target.value)}
className="w-full"
placeholder="CV"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Beneficio ($)</label>
<Input
type="number"
value={respuestas.beneficio}
onChange={(e) => handleRespuestaChange('beneficio', e.target.value)}
className="w-full"
placeholder="IT - CT"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Decisión</label>
<Input
type="text"
value={respuestas.decision}
onChange={(e) => handleRespuestaChange('decision', e.target.value)}
className="w-full"
placeholder="Producir / Cerrar"
/>
</div>
</div>
</div>
<div className="mt-4 flex gap-3">
<Button onClick={validarRespuestas} variant="primary">
<CheckCircle className="w-4 h-4 mr-2" />
Validar Respuestas
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Limpiar
</Button>
</div>
{validado && errores.length === 0 && (
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
<div className="flex items-center gap-2 text-success">
<CheckCircle className="w-5 h-5" />
<span className="font-medium">¡Correcto! Respuestas validadas</span>
</div>
</div>
)}
{validado && errores.length > 0 && (
<div className="mt-4 p-4 bg-error/10 border border-error rounded-lg">
<p className="font-medium text-error mb-2">Revisa tus respuestas:</p>
<ul className="list-disc list-inside text-sm text-error">
{errores.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
)}
</Card>
<div className="grid md:grid-cols-2 gap-6">
<Card className="bg-green-50 border-green-200">
<h4 className="font-semibold text-green-900 mb-2">Punto de Equilibrio:</h4>
<ul className="space-y-2 text-sm text-green-800">
<li> <strong>Definición:</strong> Cuando P = CMe (Beneficio = 0)</li>
<li> <strong>Significado:</strong> La empresa cubre todos sus costos</li>
<li> <strong>Beneficio:</strong> Es el beneficio "normal" del empresario</li>
<li> <strong>Decisión:</strong> Continuar operando</li>
</ul>
</Card>
<Card className="bg-red-50 border-red-200">
<h4 className="font-semibold text-red-900 mb-2">Punto de Cierre:</h4>
<ul className="space-y-2 text-sm text-red-800">
<li> <strong>Definición:</strong> Cuando P = CVMe mínimo</li>
<li> <strong>Si P {'>'} CVMe:</strong> Cubre CV, ayuda con CF Seguir produciendo</li>
<li> <strong>Si P = CVMe:</strong> Indiferente entre producir o cerrar</li>
<li> <strong>Si P {'<'} CVMe:</strong> Ni siquiera cubre CV Cerrar inmediatamente</li>
</ul>
</Card>
</div>
</div>
);
}
export default PuntoCierreEquilibrio;

View File

@@ -0,0 +1,309 @@
import { useState, useMemo } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { Input } from '../../ui/Input';
import { CheckCircle, Target, RotateCcw, Calculator } from 'lucide-react';
interface ReglaImgCmgProps {
ejercicioId: string;
onComplete?: (puntuacion: number) => void;
}
interface DatoMercado {
q: number;
cmg: number;
img: number;
ct: number;
it: number;
}
export function ReglaImgCmg({ ejercicioId: _ejercicioId, onComplete }: ReglaImgCmgProps) {
const datosMercado: DatoMercado[] = [
{ q: 0, cmg: 0, img: 100, ct: 50, it: 0 },
{ q: 1, cmg: 30, img: 90, ct: 80, it: 90 },
{ q: 2, cmg: 40, img: 80, ct: 120, it: 160 },
{ q: 3, cmg: 50, img: 70, ct: 170, it: 210 },
{ q: 4, cmg: 60, img: 60, ct: 230, it: 240 },
{ q: 5, cmg: 70, img: 50, ct: 300, it: 250 },
{ q: 6, cmg: 80, img: 40, ct: 380, it: 240 },
{ q: 7, cmg: 90, img: 30, ct: 470, it: 210 },
{ q: 8, cmg: 100, img: 20, ct: 570, it: 160 },
];
const [respuestas, setRespuestas] = useState({
qOptima: '',
beneficio: '',
condicion: '',
});
const [validado, setValidado] = useState(false);
const [errores, setErrores] = useState<string[]>([]);
const qOptima = useMemo(() => {
const datoOptimo = datosMercado
.filter(d => d.cmg <= d.img && d.q > 0)
.pop();
return datoOptimo?.q || 0;
}, []);
const beneficioMaximo = useMemo(() => {
const datoOptimo = datosMercado.find(d => d.q === qOptima);
return datoOptimo ? datoOptimo.it - datoOptimo.ct : 0;
}, [qOptima]);
const handleRespuestaChange = (campo: string, valor: string) => {
setRespuestas(prev => ({ ...prev, [campo]: valor }));
setValidado(false);
};
const validarRespuestas = () => {
const nuevosErrores: string[] = [];
if (parseInt(respuestas.qOptima) !== qOptima) {
nuevosErrores.push(`La cantidad óptima no es correcta. Busca donde IMg = CMg (o IMg >= CMg más cercano)`);
}
if (parseFloat(respuestas.beneficio) !== beneficioMaximo) {
nuevosErrores.push(`El beneficio máximo es incorrecto. Beneficio = IT - CT`);
}
if (!respuestas.condicion.toLowerCase().includes('img = cmg') &&
!respuestas.condicion.toLowerCase().includes('img igual a cmg') &&
!respuestas.condicion.toLowerCase().includes('ingreso marginal igual a costo marginal')) {
nuevosErrores.push('La condición de maximización es IMg = CMg');
}
setErrores(nuevosErrores);
setValidado(true);
if (nuevosErrores.length === 0 && onComplete) {
onComplete(100);
}
};
const reiniciar = () => {
setRespuestas({ qOptima: '', beneficio: '', condicion: '' });
setValidado(false);
setErrores([]);
};
const maxValor = Math.max(...datosMercado.map(d => Math.max(d.cmg, d.img)));
const escalaY = 100 / maxValor;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Regla de Maximización de Beneficios"
subtitle="IMg = CMg - Producción óptima"
/>
<div className="bg-blue-50 p-4 rounded-lg mb-6">
<div className="flex items-center gap-2 mb-2">
<Target className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-800">Regla Fundamental</span>
</div>
<p className="text-sm text-blue-700">
Una empresa maximiza su beneficio cuando produce la cantidad donde el <strong>Ingreso Marginal (IMg)
es igual al Costo Marginal (CMg)</strong>. Si IMg {'>'} CMg, debe producir más. Si IMg {'<'} CMg,
debe producir menos.
</p>
</div>
<div className="h-56 bg-gray-50 rounded-lg p-4 mb-6">
<svg className="w-full h-full" viewBox="0 0 400 180">
<line x1="40" y1="160" x2="380" y2="160" stroke="#374151" strokeWidth="2" />
<line x1="40" y1="160" x2="40" y2="20" stroke="#374151" strokeWidth="2" />
<text x="210" y="175" textAnchor="middle" className="text-sm fill-gray-600 font-medium">Cantidad (Q)</text>
<text x="15" y="90" textAnchor="middle" className="text-sm fill-gray-600 font-medium" transform="rotate(-90 15 90)">Costo/Ingreso ($)</text>
{datosMercado.map((d, i) => (
<g key={i}>
<line x1={50 + i * 35} y1="160" x2={50 + i * 35} y2="165" stroke="#374151" strokeWidth="1" />
<text x={50 + i * 35} y="175" textAnchor="middle" className="text-xs fill-gray-500">{d.q}</text>
</g>
))}
<polyline
fill="none"
stroke="#dc2626"
strokeWidth="3"
points={datosMercado
.filter(d => d.q > 0)
.map((d, i) => `${85 + i * 35},${160 - d.cmg * escalaY}`)
.join(' ')}
/>
<polyline
fill="none"
stroke="#2563eb"
strokeWidth="3"
points={datosMercado
.filter(d => d.q > 0)
.map((d, i) => `${85 + i * 35},${160 - d.img * escalaY}`)
.join(' ')}
/>
<circle
cx={50 + 4 * 35}
cy={160 - 60 * escalaY}
r="8"
fill="#10b981"
stroke="white"
strokeWidth="3"
/>
<g transform="translate(280, 30)">
<line x1="0" y1="0" x2="20" y2="0" stroke="#dc2626" strokeWidth="2" />
<text x="25" y="4" className="text-xs fill-gray-600">CMg</text>
<line x1="0" y1="15" x2="20" y2="15" stroke="#2563eb" strokeWidth="2" />
<text x="25" y="19" className="text-xs fill-gray-600">IMg</text>
</g>
<text x={50 + 4 * 35} y={130 - 60 * escalaY} textAnchor="middle" className="text-xs fill-green-700 font-bold">
Q* = {qOptima}
</text>
</svg>
</div>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-3 py-2 text-left font-medium text-gray-700 text-red-600">CMg ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700 text-blue-600">IMg ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">CT ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">IT ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">Beneficio ($)</th>
<th className="px-3 py-2 text-left font-medium text-gray-700">Decisión</th>
</tr>
</thead>
<tbody>
{datosMercado.map((d) => {
const beneficio = d.it - d.ct;
const esOptimo = d.q === qOptima;
const debeExpandir = d.img > d.cmg && d.q > 0;
const debeReducir = d.img < d.cmg;
return (
<tr
key={d.q}
className={`border-b hover:bg-gray-50 ${esOptimo ? 'bg-green-50' : ''}`}
>
<td className="px-3 py-2 font-medium">{d.q}</td>
<td className="px-3 py-2 text-red-600">{d.cmg || '-'}</td>
<td className="px-3 py-2 text-blue-600">{d.img}</td>
<td className="px-3 py-2">{d.ct}</td>
<td className="px-3 py-2">{d.it}</td>
<td className={`px-3 py-2 font-medium ${beneficio >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{d.q === 0 ? '-' : beneficio}
</td>
<td className="px-3 py-2">
{d.q === 0 ? '-' : (
<span className={`text-xs px-2 py-1 rounded ${
esOptimo ? 'bg-green-200 text-green-800' :
debeExpandir ? 'bg-blue-100 text-blue-800' :
'bg-red-100 text-red-800'
}`}>
{esOptimo ? 'ÓPTIMO ✓' : debeExpandir ? 'Expandir ↑' : 'Reducir ↓'}
</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="bg-gradient-to-r from-blue-50 to-purple-50 p-4 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Calculator className="w-5 h-5 text-blue-600" />
Responde:
</h4>
<div className="grid md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿Cantidad óptima (Q*)?
</label>
<Input
type="number"
value={respuestas.qOptima}
onChange={(e) => handleRespuestaChange('qOptima', e.target.value)}
className="w-full"
placeholder="Busca IMg = CMg"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿Beneficio máximo? ($)
</label>
<Input
type="number"
value={respuestas.beneficio}
onChange={(e) => handleRespuestaChange('beneficio', e.target.value)}
className="w-full"
placeholder="IT - CT"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
¿Condición de maximización?
</label>
<Input
type="text"
value={respuestas.condicion}
onChange={(e) => handleRespuestaChange('condicion', e.target.value)}
className="w-full"
placeholder="IMg = CMg"
/>
</div>
</div>
</div>
<div className="mt-4 flex gap-3">
<Button onClick={validarRespuestas} variant="primary">
<CheckCircle className="w-4 h-4 mr-2" />
Validar Respuestas
</Button>
<Button onClick={reiniciar} variant="outline">
<RotateCcw className="w-4 h-4 mr-2" />
Reiniciar
</Button>
</div>
{validado && errores.length === 0 && (
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
<div className="flex items-center gap-2 text-success">
<CheckCircle className="w-5 h-5" />
<span className="font-medium">
¡Correcto! La empresa maximiza beneficios con Q* = {qOptima}, obteniendo un beneficio de ${beneficioMaximo}
</span>
</div>
</div>
)}
{validado && errores.length > 0 && (
<div className="mt-4 p-4 bg-error/10 border border-error rounded-lg">
<p className="font-medium text-error mb-2">Revisa tus respuestas:</p>
<ul className="list-disc list-inside text-sm text-error">
{errores.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
)}
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Reglas de Maximización:</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li> <strong>IMg {'>'} CMg:</strong> La empresa debe aumentar la producción</li>
<li> <strong>IMg {'<'} CMg:</strong> La empresa debe reducir la producción</li>
<li> <strong>IMg = CMg:</strong> La empresa está maximizando beneficios</li>
<li> <strong>Beneficio = IT - CT</strong> (o también: (P - CMe) × Q)</li>
</ul>
</Card>
</div>
);
}
export default ReglaImgCmg;

View File

@@ -0,0 +1,235 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, GitCompare } from 'lucide-react';
export function RelacionCMgCMe() {
const [respuestas, setRespuestas] = useState<{[key: string]: string}>({
pregunta1: '',
pregunta2: '',
pregunta3: '',
});
const [mostrarResultados, setMostrarResultados] = useState(false);
const preguntas = [
{
id: 'pregunta1',
texto: 'Cuando CMg < CMe, el costo medio:',
opciones: [
{ id: 'a', texto: 'Aumenta', correcta: false },
{ id: 'b', texto: 'Disminuye', correcta: true },
{ id: 'c', texto: 'Se mantiene constante', correcta: false },
],
explicacion: 'Si el costo marginal es menor que el costo medio, "arrastra" el promedio hacia abajo, haciendo que CMe disminuya.'
},
{
id: 'pregunta2',
texto: 'El CMe alcanza su mínimo cuando:',
opciones: [
{ id: 'a', texto: 'CMg = 0', correcta: false },
{ id: 'b', texto: 'CMg es máximo', correcta: false },
{ id: 'c', texto: 'CMg = CMe', correcta: true },
],
explicacion: 'El CMg corta a CMe en su punto mínimo. Cuando se igualan, CMe deja de caer y empieza a subir.'
},
{
id: 'pregunta3',
texto: 'Cuando CMg > CMe, el costo medio:',
opciones: [
{ id: 'a', texto: 'Aumenta', correcta: true },
{ id: 'b', texto: 'Disminuye', correcta: false },
{ id: 'c', texto: 'Es cero', correcta: false },
],
explicacion: 'Si el costo marginal es mayor que el costo medio, "empuja" el promedio hacia arriba, haciendo que CMe aumente.'
}
];
const seleccionarRespuesta = (preguntaId: string, opcionId: string) => {
setRespuestas(prev => ({ ...prev, [preguntaId]: opcionId }));
setMostrarResultados(false);
};
const validar = () => {
setMostrarResultados(true);
};
const todasRespondidas = Object.values(respuestas).every(r => r !== '');
const esCorrecta = (preguntaId: string) => {
const pregunta = preguntas.find(p => p.id === preguntaId);
return pregunta?.opciones.find(o => o.id === respuestas[preguntaId])?.correcta || false;
};
const correctas = preguntas.filter(p => esCorrecta(p.id)).length;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Relación entre CMg y CMe"
subtitle="Comprende cómo el costo marginal afecta al costo medio"
/>
<div className="space-y-6">
{/* Gráfico animado */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 mb-4 text-center">CMg "jalona" al CMe</h4>
<svg className="w-full h-64" viewBox="0 0 600 250">
{/* Ejes */}
<line x1="60" y1="220" x2="550" y2="220" stroke="#374151" strokeWidth="2" />
<line x1="60" y1="220" x2="60" y2="20" stroke="#374151" strokeWidth="2" />
{/* Etiquetas */}
<text x="305" y="245" textAnchor="middle" className="text-sm fill-gray-700 font-medium">Cantidad (Q)</text>
<text x="25" y="120" textAnchor="middle" className="text-sm fill-gray-700 font-medium" transform="rotate(-90 25 120)">Costo ($)</text>
{/* Curva CMe */}
<path
d="M 90,180 Q 200,80 300,70 Q 400,80 500,160"
fill="none"
stroke="#7c3aed"
strokeWidth="3"
/>
{/* Curva CMg */}
<path
d="M 90,220 Q 180,120 250,70 Q 320,60 380,100 Q 450,180 520,200"
fill="none"
stroke="#16a34a"
strokeWidth="3"
strokeDasharray="5"
/>
{/* Punto de corte */}
<circle cx="300" cy="70" r="8" fill="#ef4444" stroke="white" strokeWidth="3" />
<text x="320" y="60" className="text-sm fill-red-600 font-bold">Mínimo CMe</text>
<text x="300" y="85" textAnchor="middle" className="text-xs fill-red-600">CMg = CMe</text>
{/* Zona 1: CMg < CMe */}
<rect x="90" y="20" width="210" height="200" fill="#22c55e" opacity="0.1" />
<text x="195" y="40" textAnchor="middle" className="text-sm font-bold fill-green-700">ZONA 1</text>
<text x="195" y="60" textAnchor="middle" className="text-xs fill-green-600">CMg {'<'} CMe</text>
<text x="195" y="75" textAnchor="middle" className="text-xs fill-green-600">CMe decrece</text>
{/* Zona 2: CMg > CMe */}
<rect x="300" y="20" width="250" height="200" fill="#ef4444" opacity="0.1" />
<text x="425" y="40" textAnchor="middle" className="text-sm font-bold fill-red-700">ZONA 2</text>
<text x="425" y="60" textAnchor="middle" className="text-xs fill-red-600">CMg {'>'} CMe</text>
<text x="425" y="75" textAnchor="middle" className="text-xs fill-red-600">CMe aumenta</text>
{/* Flechas indicadoras */}
<path d="M 150,100 L 150,130" stroke="#22c55e" strokeWidth="2" markerEnd="url(#arrowGreen)" />
<path d="M 450,140 L 450,110" stroke="#ef4444" strokeWidth="2" markerEnd="url(#arrowRed)" />
<defs>
<marker id="arrowGreen" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#22c55e" />
</marker>
<marker id="arrowRed" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#ef4444" />
</marker>
</defs>
{/* Leyenda */}
<g transform="translate(400, 180)">
<line x1="0" y1="0" x2="30" y2="0" stroke="#7c3aed" strokeWidth="3" />
<text x="35" y="4" className="text-sm fill-gray-700">CMe</text>
<line x1="0" y1="20" x2="30" y2="20" stroke="#16a34a" strokeWidth="3" strokeDasharray="4" />
<text x="35" y="24" className="text-sm fill-gray-700">CMg</text>
</g>
</svg>
</div>
{/* Analogía */}
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Analogía del Promedio y la Nueva Nota</h4>
<p className="text-sm text-blue-800">
Imagina tu promedio académico (<strong>CMe</strong>) y tu próxima nota (<strong>CMg</strong>):
</p>
<ul className="list-disc list-inside text-sm text-blue-700 mt-2 space-y-1">
<li>Si tu nueva nota (CMg) es <strong>menor</strong> que tu promedio (CMe) tu promedio <strong>baja</strong></li>
<li>Si tu nueva nota (CMg) es <strong>igual</strong> a tu promedio (CMe) tu promedio <strong>se mantiene</strong> (mínimo)</li>
<li>Si tu nueva nota (CMg) es <strong>mayor</strong> que tu promedio (CMe) tu promedio <strong>sube</strong></li>
</ul>
</div>
{/* Preguntas */}
<div className="space-y-4">
{preguntas.map((pregunta) => {
const preguntaCorrecta = esCorrecta(pregunta.id);
return (
<div key={pregunta.id} className="bg-white border rounded-lg p-4">
<h4 className="font-medium text-gray-900 mb-3">{pregunta.texto}</h4>
<div className="space-y-2">
{pregunta.opciones.map((opcion) => {
const esSeleccionada = respuestas[pregunta.id] === opcion.id;
const mostrarCorrecta = mostrarResultados && opcion.correcta;
const mostrarIncorrecta = mostrarResultados && esSeleccionada && !opcion.correcta;
return (
<button
key={opcion.id}
onClick={() => seleccionarRespuesta(pregunta.id, opcion.id)}
disabled={mostrarResultados}
className={`w-full p-3 rounded-lg border-2 text-left transition-all ${
esSeleccionada && !mostrarResultados
? 'border-blue-500 bg-blue-50'
: mostrarCorrecta
? 'border-green-500 bg-green-50'
: mostrarIncorrecta
? 'border-red-500 bg-red-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
<span className="font-medium">{opcion.id})</span> {opcion.texto}
{mostrarCorrecta && <CheckCircle className="w-5 h-5 text-green-600 inline ml-2" />}
{mostrarIncorrecta && <XCircle className="w-5 h-5 text-red-600 inline ml-2" />}
</button>
);
})}
</div>
{mostrarResultados && (
<div className={`mt-3 p-3 rounded text-sm ${preguntaCorrecta ? 'bg-green-50 text-green-800' : 'bg-amber-50 text-amber-800'}`}>
{pregunta.explicacion}
</div>
)}
</div>
);
})}
</div>
<Button onClick={validar} disabled={!todasRespondidas || mostrarResultados}>
Validar Respuestas
</Button>
{mostrarResultados && (
<div className={`p-4 rounded-lg border ${correctas === 3 ? 'bg-green-50 border-green-200' : 'bg-amber-50 border-amber-200'}`}>
<div className="flex items-center gap-2 mb-2">
<GitCompare className="w-5 h-5" />
<span className="font-semibold">Resultado: {correctas}/3 correctas</span>
</div>
{correctas === 3 && (
<p className="text-sm text-green-700">¡Excelente! Dominas la relación entre CMg y CMe.</p>
)}
</div>
)}
</div>
</Card>
<Card className="bg-green-50 border-green-200">
<h4 className="font-semibold text-green-900 mb-2">Regla de Oro</h4>
<div className="space-y-2 text-sm text-green-800">
<p><strong>CMg {'<'} CMe CMe decrece</strong> (costo marginal menor que el promedio)</p>
<p><strong>CMg = CMe CMe mínimo</strong> (punto de eficiencia)</p>
<p><strong>CMg {'>'} CMe CMe crece</strong> (costo marginal mayor que el promedio)</p>
</div>
</Card>
</div>
);
}
export default RelacionCMgCMe;

View File

@@ -0,0 +1,200 @@
import { useState } from 'react';
import { Card, CardHeader } from '../../ui/Card';
import { Button } from '../../ui/Button';
import { CheckCircle, XCircle, Table } from 'lucide-react';
interface FilaCostos {
q: number;
cf: number;
cv: number;
ct: number | null;
cme: number | null;
cmg: number | null;
}
export function TablaCostos() {
const CF_BASE = 200;
const [filas, setFilas] = useState<FilaCostos[]>([
{ q: 0, cf: CF_BASE, cv: 0, ct: null, cme: null, cmg: null },
{ q: 1, cf: CF_BASE, cv: 50, ct: null, cme: null, cmg: null },
{ q: 2, cf: CF_BASE, cv: 90, ct: null, cme: null, cmg: null },
{ q: 3, cf: CF_BASE, cv: 120, ct: null, cme: null, cmg: null },
{ q: 4, cf: CF_BASE, cv: 160, ct: null, cme: null, cmg: null },
{ q: 5, cf: CF_BASE, cv: 220, ct: null, cme: null, cmg: null },
{ q: 6, cf: CF_BASE, cv: 300, ct: null, cme: null, cmg: null },
{ q: 7, cf: CF_BASE, cv: 400, ct: null, cme: null, cmg: null },
{ q: 8, cf: CF_BASE, cv: 520, ct: null, cme: null, cmg: null },
]);
const [mostrarResultados, setMostrarResultados] = useState(false);
const handleInputChange = (index: number, campo: 'ct' | 'cme' | 'cmg', valor: string) => {
const numValor = valor === '' ? null : parseFloat(valor);
const nuevasFilas = [...filas];
nuevasFilas[index] = { ...nuevasFilas[index], [campo]: numValor };
setFilas(nuevasFilas);
setMostrarResultados(false);
};
// Valores correctos
const valoresCorrectos = [
{ ct: 200, cme: null, cmg: null },
{ ct: 250, cme: 250, cmg: 50 },
{ ct: 290, cme: 145, cmg: 40 },
{ ct: 320, cme: 106.67, cmg: 30 },
{ ct: 360, cme: 90, cmg: 40 },
{ ct: 420, cme: 84, cmg: 60 },
{ ct: 500, cme: 83.33, cmg: 80 },
{ ct: 600, cme: 85.71, cmg: 100 },
{ ct: 720, cme: 90, cmg: 120 },
];
const validar = () => {
setMostrarResultados(true);
};
const esCorrecto = (index: number, campo: 'ct' | 'cme' | 'cmg', valor: number | null) => {
if (valor === null) return false;
const correcto = valoresCorrectos[index][campo];
if (correcto === null) return true;
if (campo === 'cme' && index > 0) {
return Math.abs(valor - correcto) < 1;
}
return valor === correcto;
};
const todasCompletadas = filas.every((fila, index) => {
if (index === 0) return fila.ct !== null;
return fila.ct !== null && fila.cme !== null && fila.cmg !== null;
});
const calcularCorrectas = () => {
let correctas = 0;
filas.forEach((fila, index) => {
if (esCorrecto(index, 'ct', fila.ct)) correctas++;
if (index > 0) {
if (esCorrecto(index, 'cme', fila.cme)) correctas++;
if (esCorrecto(index, 'cmg', fila.cmg)) correctas++;
}
});
return correctas;
};
const totalCampos = 1 + (filas.length - 1) * 3;
return (
<div className="space-y-6">
<Card>
<CardHeader
title="Tabla Completa de Costos"
subtitle="Completa la tabla calculando CT, CMe y CMg. CF = $200 (constante)"
/>
<div className="space-y-6">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b">
<th className="px-2 py-2 text-left font-medium text-gray-700">Q</th>
<th className="px-2 py-2 text-left font-medium text-gray-700">CF</th>
<th className="px-2 py-2 text-left font-medium text-gray-700">CV</th>
<th className="px-2 py-2 text-left font-medium text-gray-700 bg-blue-50">CT</th>
<th className="px-2 py-2 text-left font-medium text-gray-700 bg-green-50">CMe</th>
<th className="px-2 py-2 text-left font-medium text-gray-700 bg-amber-50">CMg</th>
</tr>
</thead>
<tbody>
{filas.map((fila, index) => (
<tr key={index} className="border-b">
<td className="px-2 py-2 font-medium">{fila.q}</td>
<td className="px-2 py-2 text-gray-600">${fila.cf}</td>
<td className="px-2 py-2 text-gray-600">${fila.cv}</td>
<td className={`px-2 py-2 bg-blue-50 ${mostrarResultados ? (esCorrecto(index, 'ct', fila.ct) ? 'bg-green-100' : 'bg-red-100') : ''}`}>
<div className="flex items-center gap-1">
<span>$</span>
<input
type="number"
value={fila.ct ?? ''}
onChange={(e) => handleInputChange(index, 'ct', e.target.value)}
className="w-16 px-1 py-1 border rounded text-sm"
disabled={mostrarResultados}
/>
{mostrarResultados && esCorrecto(index, 'ct', fila.ct) && <CheckCircle className="w-4 h-4 text-green-600" />}
{mostrarResultados && !esCorrecto(index, 'ct', fila.ct) && <XCircle className="w-4 h-4 text-red-600" />}
</div>
</td>
<td className={`px-2 py-2 bg-green-50 ${mostrarResultados && index > 0 ? (esCorrecto(index, 'cme', fila.cme) ? 'bg-green-100' : 'bg-red-100') : ''}`}>
{index === 0 ? (
<span className="text-gray-400">-</span>
) : (
<div className="flex items-center gap-1">
<span>$</span>
<input
type="number"
value={fila.cme ?? ''}
onChange={(e) => handleInputChange(index, 'cme', e.target.value)}
className="w-16 px-1 py-1 border rounded text-sm"
disabled={mostrarResultados}
step="0.01"
/>
{mostrarResultados && esCorrecto(index, 'cme', fila.cme) && <CheckCircle className="w-4 h-4 text-green-600" />}
{mostrarResultados && !esCorrecto(index, 'cme', fila.cme) && <XCircle className="w-4 h-4 text-red-600" />}
</div>
)}
</td>
<td className={`px-2 py-2 bg-amber-50 ${mostrarResultados && index > 0 ? (esCorrecto(index, 'cmg', fila.cmg) ? 'bg-green-100' : 'bg-red-100') : ''}`}>
{index === 0 ? (
<span className="text-gray-400">-</span>
) : (
<div className="flex items-center gap-1">
<span>$</span>
<input
type="number"
value={fila.cmg ?? ''}
onChange={(e) => handleInputChange(index, 'cmg', e.target.value)}
className="w-16 px-1 py-1 border rounded text-sm"
disabled={mostrarResultados}
/>
{mostrarResultados && esCorrecto(index, 'cmg', fila.cmg) && <CheckCircle className="w-4 h-4 text-green-600" />}
{mostrarResultados && !esCorrecto(index, 'cmg', fila.cmg) && <XCircle className="w-4 h-4 text-red-600" />}
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<Button onClick={validar} disabled={!todasCompletadas || mostrarResultados}>
Validar Tabla
</Button>
{mostrarResultados && (
<div className={`p-4 rounded-lg border ${calcularCorrectas() === totalCampos ? 'bg-green-50 border-green-200' : 'bg-amber-50 border-amber-200'}`}>
<div className="flex items-center gap-2 mb-2">
<Table className="w-5 h-5" />
<span className="font-semibold">Resultado: {calcularCorrectas()}/{totalCampos} campos correctos</span>
</div>
{calcularCorrectas() < totalCampos && (
<p className="text-sm">Revisa tus cálculos. Recuerda: CT = CF + CV, CMe = CT/Q, CMg = CT actual - CT anterior.</p>
)}
</div>
)}
</div>
</Card>
<Card className="bg-blue-50 border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2">Fórmulas</h4>
<div className="space-y-1 text-sm text-blue-800">
<p><strong>CT</strong> = CF + CV</p>
<p><strong>CMe</strong> = CT / Q (solo cuando Q {'>'} 0)</p>
<p><strong>CMg</strong> = CTₙ - CTₙ (costo del último trabajador)</p>
</div>
</Card>
</div>
);
}
export default TablaCostos;

View File

@@ -1,3 +1,25 @@
export { FuncionProduccion } from './FuncionProduccion';
export { ProductoTotal } from './ProductoTotal';
export { ProductoMarginal } from './ProductoMarginal';
export { ProductoMedio } from './ProductoMedio';
export { LeyRendimientosDecrecientes } from './LeyRendimientosDecrecientes';
export { EtapasProduccion } from './EtapasProduccion';
export { ProductorRacional } from './ProductorRacional';
export { CortoVsLargoPlazo } from './CortoVsLargoPlazo';
export { CostosFijosVsVariables } from './CostosFijosVsVariables';
export { CostoTotalMedioMarginal } from './CostoTotalMedioMarginal';
export { TablaCostos } from './TablaCostos';
export { CurvasCosto } from './CurvasCosto';
export { CostosMedios } from './CostosMedios';
export { RelacionCMgCMe } from './RelacionCMgCMe';
export { EconomiasEscala } from './EconomiasEscala';
export { DiseconomiasEscala } from './DiseconomiasEscala';
export { CurvaCostoLargoPlazo } from './CurvaCostoLargoPlazo';
export { IngresoTotal } from './IngresoTotal';
export { IngresoMarginal } from './IngresoMarginal';
export { IngresoCompetenciaPerfecta } from './IngresoCompetenciaPerfecta';
export { PuntoCierreEquilibrio } from './PuntoCierreEquilibrio';
export { ReglaImgCmg } from './ReglaImgCmg';
export { CalculadoraCostos } from './CalculadoraCostos';
export { SimuladorProduccion } from './SimuladorProduccion';
export { VisualizadorExcedentes } from './VisualizadorExcedentes';

View File

@@ -16,39 +16,47 @@ const NIVELES_CONFIG: Record<NivelUsuario, { color: string; bgColor: string; ico
bgColor: 'bg-gray-100',
icon: '🌱'
},
Aprendiz: {
color: 'text-blue-600',
bgColor: 'bg-blue-100',
Estudiante: {
color: 'text-amber-700',
bgColor: 'bg-amber-100',
icon: '📚'
},
Avanzado: {
color: 'text-gray-400',
bgColor: 'bg-gray-200',
icon: '⭐'
},
Experto: {
color: 'text-purple-600',
bgColor: 'bg-purple-100',
color: 'text-yellow-600',
bgColor: 'bg-yellow-100',
icon: '🏆'
},
Maestro: {
color: 'text-yellow-600',
bgColor: 'bg-yellow-100',
icon: '👑'
color: 'text-cyan-500',
bgColor: 'bg-cyan-100',
icon: '💎'
},
};
function calcularNivel(puntuacion: number): NivelUsuario {
if (puntuacion >= 2000) return 'Maestro';
if (puntuacion >= 1000) return 'Experto';
if (puntuacion >= 300) return 'Aprendiz';
if (puntuacion >= 10000) return 'Maestro';
if (puntuacion >= 6000) return 'Experto';
if (puntuacion >= 3000) return 'Avanzado';
if (puntuacion >= 1000) return 'Estudiante';
return 'Novato';
}
function calcularProgresoNivel(puntuacion: number): { actual: number; siguiente: number; porcentaje: number } {
if (puntuacion >= 2000) {
return { actual: 2000, siguiente: 2000, porcentaje: 100 };
if (puntuacion >= 10000) {
return { actual: 10000, siguiente: 10000, porcentaje: 100 };
} else if (puntuacion >= 6000) {
return { actual: puntuacion, siguiente: 10000, porcentaje: ((puntuacion - 6000) / 4000) * 100 };
} else if (puntuacion >= 3000) {
return { actual: puntuacion, siguiente: 6000, porcentaje: ((puntuacion - 3000) / 3000) * 100 };
} else if (puntuacion >= 1000) {
return { actual: puntuacion, siguiente: 2000, porcentaje: ((puntuacion - 1000) / 1000) * 100 };
} else if (puntuacion >= 300) {
return { actual: puntuacion, siguiente: 1000, porcentaje: ((puntuacion - 300) / 700) * 100 };
return { actual: puntuacion, siguiente: 3000, porcentaje: ((puntuacion - 1000) / 2000) * 100 };
} else {
return { actual: puntuacion, siguiente: 300, porcentaje: (puntuacion / 300) * 100 };
return { actual: puntuacion, siguiente: 1000, porcentaje: (puntuacion / 1000) * 100 };
}
}

View File

@@ -1,12 +1,92 @@
import type { Ejercicio } from './introduccion';
export interface EjercicioDetallado extends Ejercicio {
export interface EjercicioDetallado extends Omit<Ejercicio, 'config'> {
instrucciones: string;
pistas?: string[];
solucion?: string;
dificultad: 'facil' | 'medio' | 'dificil';
duracionEstimada: number; // en minutos
objetivosAprendizaje: string[];
// Configuraciones específicas por tipo
config?: Record<string, unknown>;
}
// ============================================
// CONFIGURACIONES ESPECÍFICAS POR TIPO
// ============================================
export interface QuizConfig {
preguntas: QuizPregunta[];
modo: 'seleccion-unica' | 'multiple' | 'clasificacion' | 'identificacion' | 'clasificacion-multiple';
opciones?: string[];
configuracionVisual?: {
mostrarBarraProgreso?: boolean;
mostrarPuntaje?: boolean;
retroalimentacionInmediata?: boolean;
tiempoLimite?: number;
permitirReintentar?: boolean;
};
nivelesDificultad?: Record<string, number>;
}
export interface QuizPregunta {
id: string;
pregunta: string;
opciones?: string[];
respuestaCorrecta: string | string[];
explicacion?: string;
imagen?: string;
expresion?: string;
categoriaElasticidad?: string;
bien?: string;
descripcion?: string;
explicacionDetallada?: string;
}
export interface SliderConfig {
escenario: {
titulo: string;
descripcion: string;
bienA: { nombre: string; unidad: string; maxProduccion: number; color: string };
bienB: { nombre: string; unidad: string; maxProduccion: number; color: string };
};
parametros: {
mostrarFPP?: boolean;
mostrarCostoOportunidad?: boolean;
mostrarPuntoActual?: boolean;
tipoCurva?: 'lineal' | 'concava';
totalRecursos?: number;
puntosDesplazamiento?: number;
};
}
export interface JuegoConfig {
tipoJuego: 'drag-and-drop' | 'memoria' | 'ordenar';
elementosArrastrables?: Array<{ id: string; texto: string; tipo: string; categoria?: string }>;
opcionesParejas?: Array<{ id: string; elementoA: string; elementoB: string }>;
correcta?: boolean;
}
export interface CalculadoraConfig {
formula: string;
variables: Array<{ nombre: string; simbolo: string; unidad: string; valorDefecto?: number }>;
resultadoEsperado?: number;
pasos?: string[];
permiteDecimales?: boolean;
}
export interface MatchingConfig {
columnas: Array<{ titulo: string; elementos: string[] }>;
parejasCorrectas: Array<{ izquierda: string; derecha: string }>;
modo: 'arrastrar' | 'seleccionar';
}
export interface InteractiveConfig {
tipo: 'fpp' | 'grafico' | 'diagrama' | 'clasificacion';
puntosInteractivos?: Array<{ x: number; y: number;movible: boolean; etiqueta?: string }>;
restricciones?: { xMin: number; xMax: number; yMin: number; yMax: number };
diagrama?: Record<string, unknown>;
configuracion?: Record<string, unknown>;
}
export interface ModuloEjercicios {
@@ -392,13 +472,901 @@ Instrucciones:
- Familias → Empresas: Gasto de consumo
- Familias → Estado: Impuestos
- Estado → Familias/Empresas: Transferencias y gasto público`
},
// ============================================
// EJERCICIO 4: Quiz - Microeconomía vs Macroeconomía
// ============================================
{
id: 'quiz-micro-macro',
tipo: 'quiz',
titulo: 'Quiz: Microeconomía vs Macroeconomía',
descripcion: 'Identifica si los siguientes temas pertenecen al ámbito de la microeconomía o la macroeconomía',
instrucciones: `En este quiz debes clasificar cada enunciado o tema según corresponda a microeconomía o macroeconomía:
- **Microeconomía**: Estudia decisiones individuales de hogares, empresas y mercados específicos
- **Macroeconomía**: Estudia la economía en su conjunto (PIB, inflación, desempleo, políticas económicas)
Lee cada pregunta cuidadosamente y selecciona la respuesta correcta.`,
dificultad: 'facil',
duracionEstimada: 10,
objetivosAprendizaje: [
'Distinguir entre microeconomía y macroeconomía',
'Comprender el nivel de análisis de cada rama económica',
'Identificar ejemplos de decisiones individuales vs agregadas'
],
config: {
preguntas: [
{
id: 'p1',
pregunta: '¿Por qué bajan los precios de un teléfono específico?',
opciones: ['Microeconomía', 'Macroeconomía'],
respuestaCorrecta: 'Microeconomía',
explicacion: 'El estudio de cómo se determina el precio de un bien específico es un tema microeconómico'
},
{
id: 'p2',
pregunta: '¿Por qué aumenta el desempleo en el país?',
opciones: ['Microeconomía', 'Macroeconomía'],
respuestaCorrecta: 'Macroeconomía',
explicacion: 'El desempleo es una variable agregada que estudia la macroeconomía'
},
{
id: 'p3',
pregunta: '¿Cuánto debería producir una empresa para maximizar ganancias?',
opciones: ['Microeconomía', 'Macroeconomía'],
respuestaCorrecta: 'Microeconomía',
explicacion: 'Las decisiones de producción de una empresa individual son tema microeconómico'
},
{
id: 'p4',
pregunta: '¿Qué causa la inflación en la economía?',
opciones: ['Microeconomía', 'Macroeconomía'],
respuestaCorrecta: 'Macroeconomía',
explicacion: 'La inflación es un fenómeno macroeconómico que afecta el nivel general de precios'
},
{
id: 'p5',
pregunta: '¿Cómo afecta un impuesto al consumo de un bien específico?',
opciones: ['Microeconomía', 'Macroeconomía'],
respuestaCorrecta: 'Microeconomía',
explicacion: 'El efecto de impuestos en mercados específicos se estudia en microeconomía'
},
{
id: 'p6',
pregunta: '¿Cómo se calcula el Producto Interno Bruto (PIB)?',
opciones: ['Microeconomía', 'Macroeconomía'],
respuestaCorrecta: 'Macroeconomía',
explicacion: 'El PIB es una variable macroeconómica que mide la producción agregada'
},
{
id: 'p7',
pregunta: '¿Por qué algunas personas ganan más que otras?',
opciones: ['Microeconomía', 'Macroeconomía'],
respuestaCorrecta: 'Microeconomía',
explicacion: 'La distribución del ingreso a nivel individual es tema microeconómico'
},
{
id: 'p8',
pregunta: '¿Qué políticas puede usar el gobierno para estimular la economía?',
opciones: ['Microeconomía', 'Macroeconomía'],
respuestaCorrecta: 'Macroeconomía',
explicacion: 'Las políticas fiscal y monetaria son herramientas macroeconómicas'
}
],
modo: 'seleccion-unica',
configuracionVisual: {
mostrarBarraProgreso: true,
mostrarPuntaje: true,
retroalimentacionInmediata: true,
tiempoLimite: 600,
permitirReintentar: true
}
]
};
},
pistas: [
'La microeconomía estudia el "árbol" (individual), la macroeconomía estudia el "bosque" (conjunto)',
'Si la pregunta menciona "un bien", "una empresa" o "un consumidor", probablemente sea microeconomía',
'Si la pregunta menciona "país", "inflación", "desempleo" o "PIB", es macroeconomía'
],
solucion: `Recordatorio:
- **Microeconomía**: Decisiones individuales (empresas, consumidores, mercados específicos)
- **Macroeconomía**: Fenómenos agregados (PIB, inflación, desempleo, políticas económicas)`
},
// ============================================
// EJERCICIO 5: Quiz - Problema Económico Fundamental
// ============================================
{
id: 'quiz-problema-economico',
tipo: 'quiz',
titulo: 'Quiz: Las Tres Preguntas Económicas Fundamentales',
descripcion: 'Identifica cuál de las tres preguntas económicas fundamentales responde cada situación',
instrucciones: `Toda sociedad debe responder tres preguntas económicas básicas:
1. **¿Qué producir?**: Qué bienes y servicios se fabricarán
2. **¿Cómo producir?**: Qué combinación de recursos y tecnología usar
3. **¿Para quién producir?**: Cómo se distribuirán los bienes producidos
Identifica qué pregunta responde cada situación económica.`,
dificultad: 'facil',
duracionEstimada: 8,
objetivosAprendizaje: [
'Identificar las tres preguntas fundamentales de la economía',
'Relacionar decisiones económicas con las preguntas fundamentales',
'Comprender por qué toda sociedad debe resolver estas preguntas'
],
config: {
preguntas: [
{
id: 'p1',
pregunta: 'Una empresa decide usar máquinas en lugar de trabajadores para producir autos. ¿Qué pregunta responde?',
opciones: ['¿Qué producir?', '¿Cómo producir?', '¿Para quién producir?'],
respuestaCorrecta: '¿Cómo producir?',
explicacion: 'Esta decisión se refiere a la tecnología y métodos de producción a utilizar'
},
{
id: 'p2',
pregunta: 'El gobierno decide construir más hospitales que escuelas. ¿Qué pregunta responde?',
opciones: ['¿Qué producir?', '¿Cómo producir?', '¿Para quién producir?'],
respuestaCorrecta: '¿Qué producir?',
explicacion: 'Esta decisión determina qué bienes y servicios se producirán con los recursos disponibles'
},
{
id: 'p3',
pregunta: 'Se decide que los médicos y enfermeras reciban los servicios de salud antes que otros grupos. ¿Qué pregunta responde?',
opciones: ['¿Qué producir?', '¿Cómo producir?', '¿Para quién producir?'],
respuestaCorrecta: '¿Para quién producir?',
explicacion: 'Esta decisión determina cómo se distribuyen los bienes y servicios producidos'
},
{
id: 'p4',
pregunta: 'Una fábrica decide automatizar su producción de textiles. ¿Qué pregunta responde?',
opciones: ['¿Qué producir?', '¿Cómo producir?', '¿Para quién producir?'],
respuestaCorrecta: '¿Cómo producir?',
explicacion: 'La elección entre trabajo manual o automatizado responde a cómo producir'
},
{
id: 'p5',
pregunta: 'La economía debe decidir entre producir alimentos o armamento. ¿Qué pregunta responde?',
opciones: ['¿Qué producir?', '¿Cómo producir?', '¿Para quién producir?'],
respuestaCorrecta: '¿Qué producir?',
explicacion: 'Elegir qué bienes producir (alimentos vs armamento) es la pregunta qué producir'
},
{
id: 'p6',
pregunta: 'Se establece que los ancianos reciban pensiones garantizadas. ¿Qué pregunta responde?',
opciones: ['¿Qué producir?', '¿Cómo producir?', '¿Para quién producir?'],
respuestaCorrecta: '¿Para quién producir?',
explicacion: 'Las políticas de distribución responden a la pregunta para quién producir'
}
],
modo: 'seleccion-unica',
configuracionVisual: {
mostrarBarraProgreso: true,
mostrarPuntaje: true,
retroalimentacionInmediata: true,
tiempoLimite: 480,
permitirReintentar: true
}
},
pistas: [
'¿Qué producir? → Selección de bienes/servicios a fabricar',
'¿Cómo producir? → Selección de tecnología y métodos de producción',
'¿Para quién producir? → Selección de criterios de distribución'
],
solucion: `Las tres preguntas fundamentales:
1. **¿Qué producir?** → Bienes y servicios a fabricar
2. **¿Cómo producir?** → Tecnología y métodos (trabajo vs capital)
3. **¿Para quién producir?** → Distribución de la producción`
},
// ============================================
// EJERCICIO 6: Slider - Escasez y Distribución de Recursos
// ============================================
{
id: 'simulador-escases-recursos',
tipo: 'slider',
titulo: 'Simulador: Escasez y Distribución de Recursos',
descripcion: 'Distribuye 100 puntos de recursos entre diferentes necesidades básicas',
instrucciones: `Tienes 100 puntos de recursos limitados para distribuir entre 5 necesidades básicas de una sociedad:
1. **Alimentación**: Básica para la supervivencia
2. **Salud**: Servicios médicos y medicamentos
3. **Educación**: Formación y escuelas
4. **Vivienda**: Construcción y mantenimiento de hogares
5. **Infraestructura**: Caminos, puentes, servicios públicos
Instrucciones:
1. Usa los sliders para asignar recursos a cada necesidad
2. El total debe sumar exactamente 100 puntos
3. Observa las consecuencias de tu distribución
4. Reflexiona sobre qué significa la escasez`,
dificultad: 'medio',
duracionEstimada: 12,
objetivosAprendizaje: [
'Comprender el concepto de escasez',
'Visualizar la necesidad de elegir entre usos alternativos',
'Entender el trade-off en la asignación de recursos',
'Relacionar la escasez con la toma de decisiones'
],
config: {
escenario: {
titulo: 'Asignación de Recursos Societales',
descripcion: 'Una sociedad debe distribuir recursos limitados entre múltiples necesidades ilimitadas',
bienA: { nombre: 'Total Asignado', unidad: 'puntos', maxProduccion: 100, color: '#4CAF50' },
bienB: { nombre: 'Restante', unidad: 'puntos', maxProduccion: 100, color: '#f44336' }
},
parametros: {
mostrarFPP: false,
mostrarCostoOportunidad: true,
mostrarPuntoActual: true,
tipoCurva: 'lineal',
totalRecursos: 100
}
},
pistas: [
'La escasez existe porque los recursos son limitados pero las necesidades son ilimitadas',
'No puedes satisfacer todas las necesidades completamente con recursos limitados',
'Cada punto que das a una necesidad es un punto que no tiene otra'
],
solucion: `Este ejercicio ilustra el problema fundamental de la escasez:
1. Con recursos limitados (100 puntos), debes elegir entre necesidades ilimitadas
2. No es posible maximizar todas las necesidades simultáneamente
3. La decisión de asignación refleja prioridades sociales y valores
4. Siempre habrá necesidades insatisfechas debido a la escasez`
},
// ============================================
// EJERCICIO 7: Quiz - Economía Positiva vs Normativa
// ============================================
{
id: 'quiz-economia-positiva-normativa',
tipo: 'quiz',
titulo: 'Quiz: Economía Positiva vs Economía Normativa',
descripcion: 'Identifica si los enunciados económicos son positivos (descriptivos) o normativos (prescriptivos)',
instrucciones: `Distingue entre los dos tipos de enunciados económicos:
- **Economía Positiva**: Describe cómo es la economía actualmente (hechos, datos). Se puede verificar empíricamente.
- **Economía Normativa**: Describe cómo debería ser la economía (juicios de valor, opiniones). No se puede verificar empíricamente.
Identifica cada enunciado como positivo o normativo.`,
dificultad: 'facil',
duracionEstimada: 8,
objetivosAprendizaje: [
'Diferenciar entre enunciados positivos y normativos',
'Comprender el método científico en economía',
'Identificar juicios de valor en análisis económicos'
],
config: {
preguntas: [
{
id: 'p1',
pregunta: '"El desempleo en España es del 12%"',
opciones: ['Econ. Positiva', 'Econ. Normativa'],
respuestaCorrecta: 'Econ. Positiva',
explicacion: 'Este es un enunciado verificable con datos reales'
},
{
id: 'p2',
pregunta: '"El gobierno debería aumentar los impuestos a los ricos"',
opciones: ['Econ. Positiva', 'Econ. Normativa'],
respuestaCorrecta: 'Econ. Normativa',
explicacion: 'Este enunciado incluye un juicio de valor ("debería")'
},
{
id: 'p3',
pregunta: '"Cuando sube el precio de un bien, la cantidad demandada baja"',
opciones: ['Econ. Positiva', 'Econ. Normativa'],
respuestaCorrecta: 'Econ. Positiva',
explicacion: 'Es una ley económica verificable empíricamente (Ley de la demanda)'
},
{
id: 'p4',
pregunta: '"Es injusto que algunos ganen tanto mientras otros viven en pobreza"',
opciones: ['Econ. Positiva', 'Econ. Normativa'],
respuestaCorrecta: 'Econ. Normativa',
expresion: 'Expresa un juicio de valor sobre lo que es "injusto"'
},
{
id: 'p5',
pregunta: '"La inflación ha disminuido del 8% al 3% en el último año"',
opciones: ['Econ. Positiva', 'Econ. Normativa'],
respuestaCorrecta: 'Econ. Positiva',
explicacion: 'Es un enunciado verificable con datos económicos'
},
{
id: 'p6',
pregunta: '"El tipo mínimo debería ser del 15%"',
opciones: ['Econ. Positiva', 'Econ. Normativa'],
respuestaCorrecta: 'Econ. Normativa',
explicacion: 'Incluye un juicio de valor sobre lo que "debería ser"'
},
{
id: 'p7',
pregunta: '"Un aumento del salario mínimo reduce el empleo juvenil"',
opciones: ['Econ. Positiva', 'Econ. Normativa'],
respuestaCorrecta: 'Econ. Positiva',
explicacion: 'Es una proposición que podría verificarse empíricamente'
},
{
id: 'p8',
pregunta: '"Es preferable priorizar el crecimiento económico sobre el medio ambiente"',
opciones: ['Econ. Positiva', 'Econ. Normativa'],
respuestaCorrecta: 'Econ. Normativa',
explicacion: 'Expresa una preferencia o juicio de valor'
}
],
modo: 'seleccion-unica',
configuracionVisual: {
mostrarBarraProgreso: true,
mostrarPuntaje: true,
retroalimentacionInmediata: true,
tiempoLimite: 480,
permitirReintentar: true
}
},
pistas: [
'Busca palabras como "debería", "es preferible", "es justo" → son normativos',
'Los positivos describen hechos: "es", "ha sido", "aumentó"',
'Los normativos incluyen opiniones o juicios de valor'
],
solucion: `Diferencia clave:
- **Positivo**: "Cómo ES" - verificable con datos
- **Normativo**: "Cómo DEBERÍA SER" - juicio de valor
La economía positiva busca explicar; la normativa busca recomendar.`
},
// ============================================
// EJERCICIO 8: Quiz - Sistemas Económicos
// ============================================
{
id: 'quiz-sistemas-economicos',
tipo: 'quiz',
titulo: 'Quiz: Sistemas Económicos',
descripcion: 'Identifica las características de los principales sistemas económicos',
instrucciones: `Los tres sistemas económicos principales son:
1. **Sistema de Mercado**: Las decisiones las toman compradores y vendedores. Los precios se determinan por oferta y demanda.
2. **Sistema de Planificación Centralizada**: El Estado decide qué, cómo y para quién producir. No hay propiedad privada de los medios de producción.
3. **Sistema Mixto**: Combinación de elementos de mercado y planificación. El Estado y el mercado comparten las decisiones económicas.
Identifica el sistema al que corresponde cada característica.`,
dificultad: 'medio',
duracionEstimada: 10,
objetivosAprendizaje: [
'Conocer los tres sistemas económicos principales',
'Diferenciar las características de cada sistema',
'Comprender cómo se toman las decisiones económicas en cada sistema'
],
config: {
preguntas: [
{
id: 'p1',
pregunta: 'En este sistema, los precios se determinan por la oferta y la demanda',
opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'],
respuestaCorrecta: 'Mercado',
explicacion: 'En el sistema de mercado, los precios emergen de la interacción de oferta y demanda'
},
{
id: 'p2',
pregunta: 'El Estado decide qué se producirá y en qué cantidad',
opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'],
respuestaCorrecta: 'Planificación Centralizada',
explicacion: 'En la planificación centralizada, el Estado es el único decisor económico'
},
{
id: 'p3',
pregunta: 'Combina elementos del mercado con intervención del Estado',
opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'],
respuestaCorrecta: 'Mixto',
explicacion: 'Los sistemas mixtos usan el mercado pero con participación estatal'
},
{
id: 'p4',
pregunta: 'La propiedad privada de los medios de producción es fundamental',
opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'],
respuestaCorrecta: 'Mercado',
explicacion: 'El sistema de mercado se basa en la propiedad privada'
},
{
id: 'p5',
pregunta: 'El Estado distribuye los bienes según un plan',
opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'],
respuestaCorrecta: 'Planificación Centralizada',
explicacion: 'La planificación central implica distribución estatal según planes'
},
{
id: 'p6',
pregunta: 'Ejemplo actual: La mayoría de los países europeos',
opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'],
respuestaCorrecta: 'Mixto',
explicacion: 'Los países europeos tienen economía de mercado con fuerte intervención estatal'
},
{
id: 'p7',
pregunta: 'La competencia entre empresas impulsa la eficiencia',
opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'],
respuestaCorrecta: 'Mercado',
explicacion: 'La competencia es un elemento central del sistema de mercado'
},
{
id: 'p8',
pregunta: 'El Estado puede nacionalizar industrias estratégicas',
opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'],
respuestaCorrecta: 'Mixto',
explicacion: 'Los sistemas mixtos permiten nacionalizaciones en sectores clave'
}
],
modo: 'seleccion-unica',
configuracionVisual: {
mostrarBarraProgreso: true,
mostrarPuntaje: true,
retroalimentacionInmediata: true,
tiempoLimite: 600,
permitirReintentar: true
}
},
pistas: [
'Mercado → precios por oferta/demanda, propiedad privada, competencia',
'Planificación → el Estado decide todo, propiedad estatal',
'Mixto → combinación de ambos, mayoría de países actuales'
],
solucion: `Sistemas económicos:
- **Mercado**: Decisiones descentralizadas, precios por oferta/demanda, propiedad privada
- **Planificación Centralizada**: Decisiones del Estado, distribución planificada, propiedad estatal
- **Mixto**: Combinación de mercado y Estado (ej: Europa, América Latina)`
},
// ============================================
// EJERCICIO 9: Interactive - Constructor de FPP
// ============================================
{
id: 'interactive-constructor-fpp',
tipo: 'interactive',
titulo: 'Interactive: Constructor de Frontera de Posibilidades de Producción',
descripcion: 'Construye la curva FPP arrastrando puntos para representar diferentes escenarios económicos',
instrucciones: `La Frontera de Posibilidades de Producción (FPP) muestra las combinaciones máximas de dos bienes que una economía puede producir.
En este ejercicio:
1. Arrastra los puntos para dibujar la curva FPP
2. Los puntos naranjas representan puntos de producción
3. Arrastra el punto verde para explorar la curva
4. Observa cómo cambia el costo de oportunidad
Experimenta con diferentes formas de la curva y observa las implicaciones.`,
dificultad: 'medio',
duracionEstimada: 15,
objetivosAprendizaje: [
'Comprender la forma y significado de la FPP',
'Visualizar puntos eficientes, ineficientes e inalcanzables',
'Relacionar la pendiente con el costo de oportunidad',
'Entender por qué la FPP es convexa'
],
config: {
tipo: 'fpp',
puntosInteractivos: [
{ x: 0, y: 100, movible: false, etiqueta: 'Solo Bienes de Capital' },
{ x: 20, y: 85, movible: true, etiqueta: 'A' },
{ x: 40, y: 60, movible: true, etiqueta: 'B' },
{ x: 60, y: 30, movible: true, etiqueta: 'C' },
{ x: 80, y: 10, movible: true, etiqueta: 'D' },
{ x: 100, y: 0, movible: false, etiqueta: 'Solo Bienes de Consumo' }
],
restricciones: { xMin: 0, xMax: 100, yMin: 0, yMax: 100 },
feedbackEnTiempoReal: true
},
pistas: [
'La FPP tiene pendiente negativa: para más de un bien, menos del otro',
'Puntos sobre la curva son eficientes',
'Puntos dentro de la curva son ineficientes',
'Puntos fuera son inalcanzables con los recursos actuales'
],
solucion: `La FPP representa:
1. **Pendiente negativa**: Trade-off entre bienes
2. **Forma convexa**: Costos de oportunidad crecientes
3. **Sobre la curva**: Eficiente
4. **Dentro de la curva**: Ineficiente
5. **Fuera de la curva**: Inalcanzable (sin crecimiento)`
},
// ============================================
// EJERCICIO 10: Quiz - Agentes Económicos
// ============================================
{
id: 'quiz-agentes-economicos',
tipo: 'quiz',
titulo: 'Quiz: Identificación de Agentes Económicos',
descripcion: 'Identifica qué agente económico realiza cada actividad',
instrucciones: `Los cuatro agentes económicos fundamentales son:
1. **Familias/Hogares**: Individuos que consumen bienes y ofrecen factores productivos
2. **Empresas**: Organizaciones que producen bienes y servicios
3. **Estado/Gobierno**: Instuciones públicas que regulan y participan en la economía
4. **Sector Exterior**: Agentes económicos de otros países
Identifica qué agente realiza cada actividad económica.`,
dificultad: 'facil',
duracionEstimada: 8,
objetivosAprendizaje: [
'Identificar los cuatro agentes económicos',
'Reconocer las funciones de cada agente',
'Comprender la interdependencia entre agentes'
],
config: {
preguntas: [
{
id: 'p1',
pregunta: 'Una familia compra un automóvil nuevo',
opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'],
respuestaCorrecta: 'Familias',
explicacion: 'Las familias son agentes consumidores que demandan bienes y servicios'
},
{
id: 'p2',
pregunta: 'Una fábrica de coches produce vehículos',
opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'],
respuestaCorrecta: 'Empresas',
explicacion: 'Las empresas son los agentes productores por excelencia'
},
{
id: 'p3',
pregunta: 'El gobierno recauda impuestos',
opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'],
respuestaCorrecta: 'Estado',
explicacion: 'El Estado tiene la función de recaudo fiscal para financiar gasto público'
},
{
id: 'p4',
pregunta: 'Una empresa importa materias primas de China',
opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'],
respuestaCorrecta: 'Sector Exterior',
explicacion: 'Las importaciones involucran al sector externo (resto del mundo)'
},
{
id: 'p5',
pregunta: 'Un trabajador vende su fuerza de trabajo',
opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'],
respuestaCorrecta: 'Familias',
explicacion: 'Las familias ofrecen factores productivos como el trabajo'
},
{
id: 'p6',
pregunta: 'El gobierno construye carreteras',
opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'],
respuestaCorrecta: 'Estado',
explicacion: 'El Estado realiza gasto público en infraestructura'
},
{
id: 'p7',
pregunta: 'Una empresa exporta productos al extranjero',
opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'],
respuestaCorrecta: 'Sector Exterior',
explicacion: 'Las exportaciones involucran al sector externo'
},
{
id: 'p8',
pregunta: 'Una familia recibe una transferencia del gobierno',
opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'],
respuestaCorrecta: 'Estado',
explicacion: 'Las transferencias (pensiones, subsidios) son realizadas por el Estado'
}
],
modo: 'seleccion-unica',
configuracionVisual: {
mostrarBarraProgreso: true,
mostrarPuntaje: true,
retroalimentacionInmediata: true,
tiempoLimite: 480,
permitirReintentar: true
}
},
pistas: [
'Familias → consumen y ofrecen trabajo/capital',
'Empresas → producen bienes y servicios',
'Estado → recauda, regula, gasta',
'Sector Exterior → importan y exportan con otros países'
],
solucion: `Agentes económicos:
- **Familias**: Consumidores, oferentes de factores
- **Empresas**: Productores de bienes y servicios
- **Estado**: Regulador, recaudador, gasta en bienes públicos
- **Sector Exterior**: Comercia con el resto del mundo`
},
// ============================================
// EJERCICIO 11: Matching - Roles de Agentes Económicos
// ============================================
{
id: 'matching-roles-agentes',
tipo: 'matching',
titulo: 'Matching: Roles y Acciones de los Agentes Económicos',
descripcion: 'Relaciona cada agente económico con sus acciones características',
instrucciones: `Relaciona correctamente cada agente económico con las acciones que realiza.
Tienes dos columnas:
- **Columna Izquierda**: Agentes Económicos
- **Columna Derecha**: Acciones que realizan
Arrastra cada acción a su agente correspondiente.`,
dificultad: 'facil',
duracionEstimada: 8,
objetivosAprendizaje: [
'Identificar las funciones de cada agente económico',
'Comprender el rol de cada agente en la economía',
'Relacionar teoría con ejemplos prácticos'
],
config: {
columnas: [
{
titulo: 'Agentes Económicos',
elementos: ['Familias', 'Empresas', 'Estado', 'Sector Exterior']
},
{
titulo: 'Acciones',
elementos: [
'Consumen bienes y servicios',
'Producen bienes y servicios',
'Recaudan impuestos',
'Importan y exportan',
'Ofrecen factores productivos',
'Realizan gasto público',
'Obtienen beneficios',
'Intercambian con el resto del mundo'
]
}
],
parejasCorrectas: [
{ izquierda: 'Familias', derecha: 'Consumen bienes y servicios' },
{ izquierda: 'Familias', derecha: 'Ofrecen factores productivos' },
{ izquierda: 'Empresas', derecha: 'Producen bienes y servicios' },
{ izquierda: 'Empresas', derecha: 'Obtienen beneficios' },
{ izquierda: 'Estado', derecha: 'Recaudan impuestos' },
{ izquierda: 'Estado', derecha: 'Realizan gasto público' },
{ izquierda: 'Sector Exterior', derecha: 'Importan y exportan' },
{ izquierda: 'Sector Exterior', derecha: 'Intercambian con el resto del mundo' }
],
modo: 'arrastrar'
},
pistas: [
'Las familias ofrecen trabajo y capital a las empresas',
'Las empresas pagan salarios, alquileres e intereses',
'El Estado financia su gasto con impuestos',
'El sector exterior conecta la economía con el mundo'
],
solucion: `Roles principales:
- **Familias**: Consumo, oferta de factores (trabajo, capital, tierra)
- **Empresas**: Producción, creación de empleo, búsqueda de beneficios
- **Estado**: Regulación, redistribución, provisión de bienes públicos
- **Sector Exterior**: Comercio internacional, flujos financieros`
},
// ============================================
// EJERCICIO 12: Quiz - Factores de Producción
// ============================================
{
id: 'quiz-factores-produccion',
tipo: 'quiz',
titulo: 'Quiz: Factores de Producción',
descripcion: 'Identifica los factores de producción y su remuneración',
instrucciones: `Los cuatro factores de producción son:
1. **Tierra**: Recursos naturales. Remuneración: Renta
2. **Trabajo**: Esfuerzo humano. Remuneración: Salario
3. **Capital**: Bienes produzidos para producir otros bienes. Remuneración: Interés
4. **Tecnología/Empresa**: Capacidad organizativa e innovación. Remuneración: Beneficio
Identifica cada factor y su remuneración correspondiente.`,
dificultad: 'facil',
duracionEstimada: 8,
objetivosAprendizaje: [
'Identificar los cuatro factores de producción',
'Conocer la remuneración de cada factor',
'Comprender cómo se genera el ingreso en la economía'
],
config: {
preguntas: [
{
id: 'p1',
pregunta: 'Un agricultor usa un campo fértil para cultivar trigo. ¿Qué factor usa?',
opciones: ['Tierra', 'Trabajo', 'Capital', 'Tecnología'],
respuestaCorrecta: 'Tierra',
explicacion: 'La tierra incluye todos los recursos naturales'
},
{
id: 'p2',
pregunta: 'Un obrero construye una casa. ¿Qué factor representa su trabajo?',
opciones: ['Tierra', 'Trabajo', 'Capital', 'Tecnología'],
respuestaCorrecta: 'Trabajo',
explicacion: 'El esfuerzo físico e intelectual de las personas es el factor trabajo'
},
{
id: 'p3',
pregunta: 'Una empresa compra maquinaria para fabricar muebles. ¿Qué factor es la maquinaria?',
opciones: ['Tierra', 'Trabajo', 'Capital', 'Tecnología'],
respuestaCorrecta: 'Capital',
explicacion: 'El capital son los bienes producidos para producir otros bienes'
},
{
id: 'p4',
pregunta: 'Un trabajador recibe su mensualidad. ¿Cómo se llama esta remuneración?',
opciones: ['Renta', 'Salario', 'Interés', 'Beneficio'],
respuestaCorrecta: 'Salario',
explicacion: 'El salario es la remuneración del factor trabajo'
},
{
id: 'p5',
pregunta: 'Una empresa obtiene ganancias por su actividad. ¿Cómo se llama esta remuneración?',
opciones: ['Renta', 'Salario', 'Interés', 'Beneficio'],
respuestaCorrecta: 'Beneficio',
explicacion: 'El beneficio es la remuneración del factor empresa/tecnología'
},
{
id: 'p6',
pregunta: 'Un propietario alquila un edificio de oficinas. ¿Qué remuneración recibe?',
opciones: ['Renta', 'Salario', 'Interés', 'Beneficio'],
respuestaCorrecta: 'Renta',
explicacion: 'La renta es la remuneración del factor tierra (recursos naturales)'
},
{
id: 'p7',
pregunta: 'Un banco paga intereses a los ahorradores. ¿Qué factor se está remunerando?',
opciones: ['Tierra', 'Trabajo', 'Capital', 'Tecnología'],
respuestaCorrecta: 'Capital',
explicacion: 'El interés es la remuneración del capital (recursos financieros)'
},
{
id: 'p8',
pregunta: 'Un emprendedor desarrolla un nuevo producto. ¿Qué factor está usando?',
opciones: ['Tierra', 'Trabajo', 'Capital', 'Tecnología/Empresa'],
respuestaCorrecta: 'Tecnología/Empresa',
explicacion: 'La capacidad empresarial y la innovación representan el factor tecnología'
}
],
modo: 'seleccion-unica',
configuracionVisual: {
mostrarBarraProgreso: true,
mostrarPuntaje: true,
retroalimentacionInmediata: true,
tiempoLimite: 480,
permitirReintentar: true
}
},
pistas: [
'Tierra → recursos naturales → RENTA',
'Trabajo → esfuerzo humano → SALARIO',
'Capital → bienes de producción → INTERÉS',
'Tecnología/Empresa → innovación → BENEFICIO'
],
solucion: `Factores de producción y remuneraciones:
- **Tierra** (recursos naturales) → **Renta**
- **Trabajo** (esfuerzo humano) → **Salario**
- **Capital** (bienes para producir) → **Interés**
- **Tecnología/Empresa** (innovación) → **Beneficio**`
},
// ============================================
// EJERCICIO 13: Calculadora - Productividad
// ============================================
{
id: 'calculadora-productividad',
tipo: 'calculadora',
titulo: 'Calculadora: Cálculo de Productividad',
descripcion: 'Calcula la productividad laboral usando la fórmula: Productividad = Output / Input',
instrucciones: `La productividad mide la eficiencia con la que se usan los recursos para producir.
**Fórmula**: Productividad = Output (producción) / Input (recursos utilizados)
Ejemplos de productividad laboral:
- Productos por hora de trabajo
- Ventas por empleado
- Unidades producidas por trabajador
Calcula la productividad en cada escenario.`,
dificultad: 'medio',
duracionEstimada: 12,
objetivosAprendizaje: [
'Comprender el concepto de productividad',
'Aplicar la fórmula de productividad',
'Interpretar resultados de productividad',
'Relacionar productividad con eficiencia económica'
],
config: {
formula: 'Productividad = Output / Input',
variables: [
{ nombre: 'Producción total', simbolo: 'Q', unidad: 'unidades' },
{ nombre: 'Horas de trabajo', simbolo: 'L', unidad: 'horas' },
{ nombre: 'Número de trabajadores', simbolo: 'N', unidad: 'trabajadores' },
{ nombre: 'Capital invertido', simbolo: 'K', unidad: 'euros' }
],
permiteDecimales: true,
pasos: [
'1. Identificar el output (producción total)',
'2. Identificar el input (recurso usado)',
'3. Dividir output entre input',
'4. Interpretar el resultado'
],
preguntas: [
{
id: 'calc1',
pregunta: 'Una fábrica produce 500 unidades en 10 horas de trabajo. ¿Cuál es la productividad por hora?',
output: 500,
input: 10,
unidadOutput: 'unidades',
unidadInput: 'horas',
resultadoEsperado: 50,
explicacion: 'Productividad = 500 / 10 = 50 unidades por hora'
},
{
id: 'calc2',
pregunta: 'Un empleado vende 2,000 euros en productos en una jornada de 8 horas. ¿Cuál es su productividad por hora?',
output: 2000,
input: 8,
unidadOutput: 'euros',
unidadInput: 'horas',
resultadoEsperado: 250,
explicacion: 'Productividad = 2000 / 8 = 250 euros/hora'
},
{
id: 'calc3',
pregunta: 'Una empresa produce 10,000 prendas con 25 trabajadores en una semana. ¿Cuál es la productividad por trabajador?',
output: 10000,
input: 25,
unidadOutput: 'prendas',
unidadInput: 'trabajadores',
resultadoEsperado: 400,
explicacion: 'Productividad = 10000 / 25 = 400 prendas/trabajador'
},
{
id: 'calc4',
pregunta: 'Una mina extrae 800 toneladas de carbón con 40 mineros en un día. ¿Cuál es la productividad por minero?',
output: 800,
input: 40,
unidadOutput: 'toneladas',
unidadInput: 'mineros',
resultadoEsperado: 20,
explicacion: 'Productividad = 800 / 40 = 20 toneladas/minero'
},
{
id: 'calc5',
pregunta: 'Un restaurante sirve 360 comidas con 6 cocineros en un turno. ¿Cuál es la productividad por cocinero?',
output: 360,
input: 6,
unidadOutput: 'comidas',
unidadInput: 'cocineros',
resultadoEsperado: 60,
explicacion: 'Productividad = 360 / 6 = 60 comidas/cocinero'
}
]
},
pistas: [
'Productividad = Cantidad producida / Recursos utilizados',
'El resultado siempre tiene unidades: output por cada unidad de input',
'Mayor productividad = mayor eficiencia'
],
solucion: `La productividad mide la eficiencia:
- **Fórmula**: Output / Input
- **Unidades**: unidades de output por unidad de input
- **Mayor productividad** = más eficiente
- **Para mejorarla**: aumentar output o reducir input`
}
]};
// Exportar también los ejercicios individuales para facilitar importaciones selectivas
export const ejercicioDisyuntivas = ejercicios.ejercicios[0];
export const ejercicioClasificacion = ejercicios.ejercicios[1];
export const ejercicioFlujoCircular = ejercicios.ejercicios[2];
export const ejercicioMicroMacro = ejercicios.ejercicios[3];
export const ejercicioProblemaEconomico = ejercicios.ejercicios[4];
export const ejercicioEscasezRecursos = ejercicios.ejercicios[5];
export const ejercicioEconomiaPositivaNormativa = ejercicios.ejercicios[6];
export const ejercicioSistemasEconomicos = ejercicios.ejercicios[7];
export const ejercicioConstructorFPP = ejercicios.ejercicios[8];
export const ejercicioAgentesEconomicos = ejercicios.ejercicios[9];
export const ejercicioRolesAgentes = ejercicios.ejercicios[10];
export const ejercicioFactoresProduccion = ejercicios.ejercicios[11];
export const ejercicioProductividad = ejercicios.ejercicios[12];
export default ejercicios;

View File

@@ -5,7 +5,7 @@ export interface Seccion {
export interface Ejercicio {
id: string;
tipo: 'slider' | 'quiz' | 'juego';
tipo: 'slider' | 'quiz' | 'juego' | 'calculadora' | 'matching' | 'interactive';
titulo: string;
descripcion: string;
config: Record<string, unknown>;

Some files were not shown because too many files have changed in this diff Show More