1+ import React , { useState , useEffect , useRef } from 'react' ;
2+ import styles from './StudentPage.scss' ;
3+
4+ import { MatchItem } from './matchingTable.types' ;
5+
6+ interface MatchingTableProps {
7+ items : MatchItem [ ] ;
8+ onAnswerChange ?: ( id : string , answer : string ) => void ;
9+ readOnly ?: boolean ;
10+ }
11+
12+ const MatchingTableStudent : React . FC < MatchingTableProps > = ( {
13+ items = [ ] ,
14+ onAnswerChange,
15+ readOnly = false
16+ } ) => {
17+ const [ matchItems , setMatchItems ] = useState < MatchItem [ ] > ( items ) ;
18+ const [ availableOptions , setAvailableOptions ] = useState < string [ ] > ( [ ] ) ;
19+ const [ selectedOptions , setSelectedOptions ] = useState < { [ key : string ] : string } > ( { } ) ;
20+ const [ activePrompt , setActivePrompt ] = useState < string | null > ( null ) ;
21+ const containerRef = useRef < HTMLDivElement > ( null ) ;
22+
23+ useEffect ( ( ) => {
24+ setMatchItems ( items ) ;
25+ const allOptions = [ ...new Set ( items . map ( item => item . correctAnswer ) ) ] ;
26+ setAvailableOptions ( allOptions . sort ( ( ) => 0.5 - Math . random ( ) ) ) ;
27+ const currentSelections : { [ key : string ] : string } = { } ;
28+ items . forEach ( item => {
29+ if ( item . studentAnswer ) {
30+ currentSelections [ item . id ] = item . studentAnswer ;
31+ }
32+ } ) ;
33+ setSelectedOptions ( currentSelections ) ;
34+ } , [ items ] ) ;
35+
36+ useEffect ( ( ) => {
37+ if ( readOnly ) return ;
38+ const handleKeyDown = ( e : KeyboardEvent ) => {
39+ const key = e . key ;
40+ if ( / ^ [ 0 - 9 ] $ / . test ( key ) ) {
41+ const index = parseInt ( key , 10 ) ;
42+ if ( activePrompt ) {
43+ const optionIndex = index ;
44+ if ( optionIndex < availableOptions . length ) {
45+ const selectedOption = availableOptions [ optionIndex ] ;
46+ handleMatchSelection ( activePrompt , selectedOption ) ;
47+ setActivePrompt ( null ) ;
48+ }
49+ } else {
50+ const promptIndex = index ;
51+ if ( promptIndex < matchItems . length ) {
52+ setActivePrompt ( matchItems [ promptIndex ] . id ) ;
53+ }
54+ }
55+ } else if ( key === 'Escape' ) {
56+ setActivePrompt ( null ) ;
57+ }
58+ } ;
59+
60+ document . addEventListener ( 'keydown' , handleKeyDown ) ;
61+ return ( ) => document . removeEventListener ( 'keydown' , handleKeyDown ) ;
62+ } , [ activePrompt , availableOptions , matchItems , readOnly ] ) ;
63+
64+ const handlePromptClick = ( itemId : string ) => {
65+ if ( readOnly ) return ;
66+ setActivePrompt ( activePrompt === itemId ? null : itemId ) ;
67+ } ;
68+
69+ const handleOptionClick = ( option : string ) => {
70+ if ( readOnly || ! activePrompt ) return ;
71+ handleMatchSelection ( activePrompt , option ) ;
72+ setActivePrompt ( null ) ;
73+ } ;
74+
75+ const handleMatchSelection = ( itemId : string , option : string ) => {
76+ const newSelectedOptions = { ...selectedOptions } ;
77+
78+ // Remove option from any other prompt that might be using it
79+ Object . keys ( newSelectedOptions ) . forEach ( key => {
80+ if ( newSelectedOptions [ key ] === option && key !== itemId ) {
81+ delete newSelectedOptions [ key ] ;
82+ }
83+ } ) ;
84+
85+ newSelectedOptions [ itemId ] = option ;
86+ setSelectedOptions ( newSelectedOptions ) ;
87+
88+ const updatedItems = matchItems . map ( item =>
89+ item . id === itemId ? { ...item , studentAnswer : option } : item
90+ ) ;
91+ setMatchItems ( updatedItems ) ;
92+
93+ if ( onAnswerChange ) {
94+ onAnswerChange ( itemId , option ) ;
95+ }
96+ } ;
97+
98+ const handleRemoveAnswer = ( itemId : string , e : React . MouseEvent ) => {
99+ e . stopPropagation ( ) ;
100+ if ( readOnly ) return ;
101+
102+ const updatedItems = matchItems . map ( item =>
103+ item . id === itemId ? { ...item , studentAnswer : '' } : item
104+ ) ;
105+ setMatchItems ( updatedItems ) ;
106+
107+ const newSelectedOptions = { ...selectedOptions } ;
108+ delete newSelectedOptions [ itemId ] ;
109+ setSelectedOptions ( newSelectedOptions ) ;
110+
111+ if ( onAnswerChange ) {
112+ onAnswerChange ( itemId , '' ) ;
113+ }
114+ } ;
115+
116+ return (
117+ < div className = { styles [ 'matching-table-container' ] } ref = { containerRef } >
118+ < div className = { styles [ 'matching-columns' ] } >
119+ < div className = { styles [ 'prompts-column' ] } >
120+ < div className = { styles [ 'column-header' ] } > Prompt</ div >
121+ { matchItems . map ( ( item , index ) => {
122+ const isMatched = ! ! selectedOptions [ item . id ] ;
123+ const isActive = activePrompt === item . id ;
124+ const itemClass = `prompt-item ${ isActive ? 'active' : '' } ${ isMatched ? 'matched' : '' } ` ;
125+
126+ return (
127+ < div key = { item . id } className = { itemClass } onClick = { ( ) => handlePromptClick ( item . id ) } >
128+ < div className = { styles [ 'item-number' ] } > { index } </ div >
129+ < div className = { styles [ 'item-content' ] } > { item . prompt } </ div >
130+ { isMatched && ! readOnly && (
131+ < button
132+ className = { styles [ 'remove-answer-btn' ] }
133+ onClick = { ( e ) => handleRemoveAnswer ( item . id , e ) }
134+ aria-label = "Remove match"
135+ >
136+ ✕
137+ </ button >
138+ ) }
139+ </ div >
140+ ) ;
141+ } ) }
142+ </ div >
143+
144+ < div className = { styles [ 'options-column' ] } >
145+ < div className = { styles [ 'column-header' ] } > Response</ div >
146+ { availableOptions . map ( ( option , index ) => {
147+ const isUsed = Object . values ( selectedOptions ) . includes ( option ) ;
148+
149+ return (
150+ < div
151+ key = { option }
152+ className = { `option-item ${ isUsed ? 'used' : '' } ` }
153+ onClick = { ( ) => handleOptionClick ( option ) }
154+ >
155+ < div className = { styles [ 'item-number' ] } > { index } </ div >
156+ < div className = { styles [ 'item-content' ] } > { option } </ div >
157+ </ div >
158+ ) ;
159+ } ) }
160+ </ div >
161+ </ div >
162+
163+ { ! readOnly && (
164+ < div className = { styles [ 'matching-instructions' ] } >
165+ < p > Select a number on the left, then select a number on the right to match them. You can also click items directly.</ p >
166+ </ div >
167+ ) }
168+ </ div >
169+ ) ;
170+ } ;
171+
172+ export default MatchingTableStudent ;
0 commit comments