this repo has no description
at push-notifications 163 lines 5.2 kB view raw
1import { useState, useEffect } from 'react'; 2import Box from '@mui/material/Box'; 3import Chip from '@mui/material/Chip'; 4import Typography from '@mui/material/Typography'; 5import { keyframes } from '@emotion/react'; 6import type { MatchingPairsExercise } from '../../types/lesson'; 7 8const fadeOut = keyframes` 9 from { opacity: 1; transform: scale(1); } 10 to { opacity: 0; transform: scale(0.8); } 11`; 12 13interface MatchingPairsProps { 14 exercise: MatchingPairsExercise; 15 selectedAnswer: Array<[string, string]> | null; 16 isChecked: boolean; 17 onSelect: (pairs: Array<[string, string]>) => void; 18} 19 20export function MatchingPairs({ 21 exercise, 22 selectedAnswer, 23 isChecked, 24 onSelect, 25}: MatchingPairsProps) { 26 const [selectedLeft, setSelectedLeft] = useState<string | null>(null); 27 const [selectedRight, setSelectedRight] = useState<string | null>(null); 28 const [matchedPairs, setMatchedPairs] = useState<Array<[string, string]>>([]); 29 const [fadingPairs, setFadingPairs] = useState<Set<string>>(new Set()); 30 31 // Sync with external state 32 useEffect(() => { 33 if (selectedAnswer) { 34 setMatchedPairs(selectedAnswer); 35 } 36 }, [selectedAnswer]); 37 38 // Shuffled lists (stable within exercise) 39 const leftItems = exercise.pairs.map((p) => p.left); 40 const rightItems = exercise.pairs.map((p) => p.right); 41 42 const matchedLefts = new Set(matchedPairs.map(([l]) => l)); 43 const matchedRights = new Set(matchedPairs.map(([, r]) => r)); 44 45 function handleLeftClick(item: string) { 46 if (isChecked || matchedLefts.has(item)) return; 47 setSelectedLeft(item); 48 49 if (selectedRight) { 50 attemptMatch(item, selectedRight); 51 } 52 } 53 54 function handleRightClick(item: string) { 55 if (isChecked || matchedRights.has(item)) return; 56 setSelectedRight(item); 57 58 if (selectedLeft) { 59 attemptMatch(selectedLeft, item); 60 } 61 } 62 63 function attemptMatch(left: string, right: string) { 64 const isCorrectPair = exercise.pairs.some( 65 (p) => p.left === left && p.right === right, 66 ); 67 68 if (isCorrectPair) { 69 const fadingKey = `${left}::${right}`; 70 setFadingPairs((prev) => new Set(prev).add(fadingKey)); 71 72 setTimeout(() => { 73 const newPairs = [...matchedPairs, [left, right] as [string, string]]; 74 setMatchedPairs(newPairs); 75 setFadingPairs((prev) => { 76 const next = new Set(prev); 77 next.delete(fadingKey); 78 return next; 79 }); 80 onSelect(newPairs); 81 }, 400); 82 } 83 84 setSelectedLeft(null); 85 setSelectedRight(null); 86 } 87 88 return ( 89 <Box sx={{ width: '100%', maxWidth: 500, mx: 'auto' }}> 90 <Typography variant="h5" sx={{ textAlign: 'center', mb: 4 }}> 91 {exercise.prompt} 92 </Typography> 93 94 <Box sx={{ display: 'flex', gap: 4, justifyContent: 'center' }}> 95 {/* Left column */} 96 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> 97 {leftItems.map((item) => { 98 const isMatched = matchedLefts.has(item); 99 const isFading = [...fadingPairs].some((fp) => 100 fp.startsWith(item + '::'), 101 ); 102 return ( 103 <Chip 104 key={item} 105 label={item} 106 onClick={() => handleLeftClick(item)} 107 variant={selectedLeft === item ? 'filled' : 'outlined'} 108 color={selectedLeft === item ? 'secondary' : 'default'} 109 disabled={isChecked} 110 sx={{ 111 fontSize: '1rem', 112 py: 2.5, 113 px: 1, 114 visibility: isMatched ? 'hidden' : 'visible', 115 animation: isFading 116 ? `${fadeOut} 0.4s ease-out forwards` 117 : 'none', 118 cursor: isMatched ? 'default' : 'pointer', 119 '&.MuiChip-outlined': { 120 borderColor: 'rgba(255,255,255,0.3)', 121 }, 122 }} 123 /> 124 ); 125 })} 126 </Box> 127 128 {/* Right column */} 129 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> 130 {rightItems.map((item) => { 131 const isMatched = matchedRights.has(item); 132 const isFading = [...fadingPairs].some((fp) => 133 fp.endsWith('::' + item), 134 ); 135 return ( 136 <Chip 137 key={item} 138 label={item} 139 onClick={() => handleRightClick(item)} 140 variant={selectedRight === item ? 'filled' : 'outlined'} 141 color={selectedRight === item ? 'secondary' : 'default'} 142 disabled={isChecked} 143 sx={{ 144 fontSize: '1rem', 145 py: 2.5, 146 px: 1, 147 visibility: isMatched ? 'hidden' : 'visible', 148 animation: isFading 149 ? `${fadeOut} 0.4s ease-out forwards` 150 : 'none', 151 cursor: isMatched ? 'default' : 'pointer', 152 '&.MuiChip-outlined': { 153 borderColor: 'rgba(255,255,255,0.3)', 154 }, 155 }} 156 /> 157 ); 158 })} 159 </Box> 160 </Box> 161 </Box> 162 ); 163}