Skip to content

Commit 9bd2e23

Browse files
committed
update the UI for the matching pairs
1 parent c1cd8c9 commit 9bd2e23

File tree

10 files changed

+759
-541
lines changed

10 files changed

+759
-541
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
@import 'variables';
2+
3+
.matching-instructor-wrapper {
4+
max-width: 960px;
5+
margin: 2rem auto;
6+
padding: 2rem;
7+
}
8+
9+
.section-title {
10+
font-size: 1.8rem;
11+
font-weight: 600;
12+
text-align: center;
13+
margin-bottom: 2rem;
14+
}
15+
16+
.matching-input-form {
17+
background-color: white;
18+
padding: 2rem;
19+
border-radius: 1rem;
20+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
21+
margin-bottom: 2rem;
22+
}
23+
24+
.questions-header {
25+
font-size: 1.4rem;
26+
font-weight: 600;
27+
color: #111827;
28+
margin-bottom: 1.5rem;
29+
padding-bottom: 0.75rem;
30+
border-bottom: 2px solid #e5e7eb;
31+
}
32+
33+
.matching-input-row {
34+
display: flex;
35+
flex-direction: row;
36+
gap: 1rem;
37+
margin-bottom: 1.25rem;
38+
position: relative;
39+
40+
&:last-of-type {
41+
margin-bottom: 2rem;
42+
}
43+
44+
.input {
45+
flex: 1;
46+
padding: 0.75rem 1rem;
47+
border-radius: 0.5rem;
48+
border: 2px solid #e5e7eb;
49+
font-size: 1rem;
50+
transition: border-color 0.2s ease;
51+
52+
&:focus {
53+
outline: none;
54+
border-color: #3b82f6;
55+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
56+
}
57+
58+
&::placeholder {
59+
color: #9ca3af;
60+
}
61+
}
62+
63+
.btnDelete {
64+
display: flex;
65+
align-items: center;
66+
justify-content: center;
67+
width: 2.5rem;
68+
height: 2.5rem;
69+
flex-shrink: 0;
70+
background-color: #ef4444;
71+
color: white;
72+
border: none;
73+
border-radius: 0.5rem;
74+
font-size: 1rem;
75+
cursor: pointer;
76+
transition: background-color 0.2s ease;
77+
78+
&:hover:not(:disabled) {
79+
background-color: #dc2626;
80+
}
81+
82+
&:disabled {
83+
background-color: #fca5a5;
84+
cursor: not-allowed;
85+
}
86+
}
87+
}
88+
89+
.btnSecondary {
90+
display: inline-flex;
91+
align-items: center;
92+
justify-content: center;
93+
padding: 0.75rem 1.5rem;
94+
background-color: #f3f4f6;
95+
color: #4b5563;
96+
border: 2px solid #e5e7eb;
97+
border-radius: 0.5rem;
98+
font-size: 1rem;
99+
font-weight: 500;
100+
cursor: pointer;
101+
transition: all 0.2s ease;
102+
103+
&:hover {
104+
background-color: #e5e7eb;
105+
color: #1f2937;
106+
}
107+
}
108+
109+
.submit-container {
110+
margin-top: 2rem;
111+
text-align: center;
112+
}
113+
114+
.submit-question-btn {
115+
padding: 0.75rem 2rem;
116+
background-color: #3b82f6;
117+
color: white;
118+
border: none;
119+
border-radius: 0.5rem;
120+
font-size: 1.1rem;
121+
font-weight: 600;
122+
cursor: pointer;
123+
transition: background-color 0.2s ease;
124+
125+
&:hover:not(:disabled) {
126+
background-color: #2563eb;
127+
}
128+
129+
&:disabled {
130+
background-color: #bfdbfe;
131+
cursor: not-allowed;
132+
}
133+
}
134+
135+
.note {
136+
margin-top: 1.5rem;
137+
padding: 1rem;
138+
background-color: #f9fafb;
139+
border-left: 4px solid #9ca3af;
140+
border-radius: 0.25rem;
141+
font-size: 0.9rem;
142+
color: #4b5563;
143+
144+
strong {
145+
color: #111827;
146+
font-weight: 600;
147+
}
148+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from 'react';
2+
import { MatchItem } from './matchingTable.types';
3+
import styles from './InstructorPage.scss';
4+
5+
6+
interface MatchingTableInstructorProps {
7+
matchItems: MatchItem[];
8+
onItemChange: (index: number, field: keyof MatchItem, value: string) => void;
9+
onAddRow: () => void;
10+
onRemoveRow: (index: number) => void;
11+
}
12+
13+
const MatchingTableInstructor: React.FC<MatchingTableInstructorProps> = ({
14+
matchItems,
15+
onItemChange,
16+
onAddRow,
17+
onRemoveRow
18+
}) => {
19+
return (
20+
<div className={styles['matching-input-form']}>
21+
<h3 className={styles['questions-header']}>Questions & Answers</h3>
22+
23+
<div className={styles['matching-inputs-container']}>
24+
{matchItems.map((item, index) => (
25+
<div key={item.id} className={styles['matching-input-row']}>
26+
<input
27+
type="text"
28+
className={styles['input']}
29+
placeholder="Enter prompt/question"
30+
value={item.prompt}
31+
onChange={(e) => onItemChange(index, 'prompt', e.target.value)}
32+
aria-label={`Question ${index + 1}`}
33+
/>
34+
<input
35+
type="text"
36+
className={styles['input']}
37+
placeholder="Enter correct answer"
38+
value={item.correctAnswer}
39+
onChange={(e) => onItemChange(index, 'correctAnswer', e.target.value)}
40+
aria-label={`Answer ${index + 1}`}
41+
/>
42+
<button
43+
className={styles['btnDelete']}
44+
onClick={() => onRemoveRow(index)}
45+
disabled={matchItems.length <= 1}
46+
title="Remove row"
47+
aria-label="Remove row"
48+
>
49+
50+
</button>
51+
</div>
52+
))}
53+
</div>
54+
55+
<div style={{ marginTop: '1.5rem', display: 'flex', justifyContent: 'space-between' }}>
56+
<button className={styles['btnSecondary']} onClick={onAddRow}>
57+
+ Add Row
58+
</button>
59+
</div>
60+
61+
<div className={styles['note']}>
62+
<p>
63+
<strong>Note:</strong> Students will be presented with all correct answers as options and will need to match them with the corresponding prompts.
64+
</p>
65+
</div>
66+
</div>
67+
);
68+
};
69+
70+
export default MatchingTableInstructor;
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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

Comments
 (0)