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(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(() => shuffleItems ? shuffleArray(leftItems) : leftItems ); const [displayRightItems, setDisplayRightItems] = useState(() => shuffleItems ? shuffleArray(rightItems) : rightItems ); const [matches, setMatches] = useState([]); const [selectedLeft, setSelectedLeft] = useState(null); const [selectedRight, setSelectedRight] = useState(null); const [attempts, setAttempts] = useState(0); const [showResults, setShowResults] = useState(false); const [dragOverLeft, setDragOverLeft] = useState(null); const [dragOverRight, setDragOverRight] = useState(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, 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, 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, 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 (
{/* Header */}

{title}

{description}

{maxPoints} pts
{attempts} {attempts === 1 ? 'intento' : 'intentos'}
{/* Matching Area */}
{/* Left Column */}

Columna A

{displayLeftItems.map(item => { const matchedItem = getMatchedRightItem(item.id); const status = getMatchStatus(item.id); const isSelected = selectedLeft === item.id; const isMatched = isLeftMatched(item.id); return ( handleDragStart(e as unknown as DragEvent, item.id, 'left')} onDragOver={(e) => !showResults && handleDragOver(e as unknown as DragEvent, item.id, 'left')} onDragLeave={() => handleDragLeave('left')} onDrop={(e) => handleDrop(e as unknown as DragEvent, 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' : ''} `} >
{item.content}
{/* Match indicator */} {matchedItem && (
{matchedItem.content}
)} {/* Status icons */} {showResults && status && (
{status === 'correct' ? ( ) : ( )}
)} {/* Remove button */} {isMatched && !showResults && ( )}
); })}
{/* Right Column */}

Columna B

{displayRightItems.map(item => { const matchedItem = getMatchedLeftItem(item.id); const isSelected = selectedRight === item.id; const isMatched = isRightMatched(item.id); return ( handleDragStart(e as unknown as DragEvent, item.id, 'right')} onDragOver={(e) => !showResults && handleDragOver(e as unknown as DragEvent, item.id, 'right')} onDragLeave={() => handleDragLeave('right')} onDrop={(e) => handleDrop(e as unknown as DragEvent, 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' : ''} `} >
{item.content}
{/* Match indicator */} {matchedItem && (
{matchedItem.content}
)}
); })}
{/* Results Section */} {showResults && (

{correctCount === leftItems.length ? '¡Perfecto!' : correctCount >= leftItems.length * 0.7 ? '¡Muy bien!' : '¡Sigue practicando!'}

{correctCount} de {leftItems.length} emparejamientos correctos

{/* Score Display */}

{score}

Puntuación

{maxPoints}

Máximo

{Math.round((score / maxPoints) * 100)}%

Precisión

)}
{/* Action Buttons */}
{!showResults ? ( ) : ( )}
{/* Instructions */} {!allMatched && matches.length > 0 && !showResults && (

Arrastra elementos o haz clic para conectar. Tienes{' '} {leftItems.length - matches.length}{' '} emparejamientos pendientes.

)} {allMatched && !showResults && (

Todos los elementos están emparejados. ¡Valida tu respuesta!

)}
); } export default MatchingExercise;