Redesign how answers are submitted and updated (react state management)
This commit is contained in:
C. Fuhrman 2024-09-26 00:34:30 -04:00
parent ab18283db0
commit c0cc4d01e0
10 changed files with 531 additions and 320 deletions

View file

@ -48,7 +48,7 @@ describe('StudentModeQuiz', () => {
fireEvent.click(screen.getByText('Répondre')); fireEvent.click(screen.getByText('Répondre'));
}); });
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', '1'); expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1);
}); });
test('handles quit button click', async () => { test('handles quit button click', async () => {

View file

@ -47,7 +47,7 @@ describe('TeacherModeQuiz', () => {
act(() => { act(() => {
fireEvent.click(screen.getByText('Répondre')); fireEvent.click(screen.getByText('Répondre'));
}); });
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', '1'); expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1);
expect(screen.getByText('Votre réponse est "Option A".')).toBeInTheDocument(); expect(screen.getByText('Votre réponse est "Option A".')).toBeInTheDocument();
}); });

View file

@ -1,7 +1,6 @@
// LiveResults.tsx // LiveResults.tsx
import React, { useEffect, useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { GIFTQuestion } from 'gift-pegjs';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons';
import { QuestionType } from '../../Types/QuestionType'; import { QuestionType } from '../../Types/QuestionType';
@ -15,11 +14,12 @@ import {
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableContainer,
TableFooter, TableFooter,
TableHead, TableHead,
TableRow TableRow
} from '@mui/material'; } from '@mui/material';
import { StudentType, Answer } from '../../Types/StudentType'; import { StudentType } from '../../Types/StudentType';
import { formatLatex } from '../GiftTemplate/templates/TextType'; import { formatLatex } from '../GiftTemplate/templates/TextType';
interface LiveResultsProps { interface LiveResultsProps {
@ -27,7 +27,7 @@ interface LiveResultsProps {
questions: QuestionType[]; questions: QuestionType[];
showSelectedQuestion: (index: number) => void; showSelectedQuestion: (index: number) => void;
quizMode: 'teacher' | 'student'; quizMode: 'teacher' | 'student';
connectedStudents: StudentType[] students: StudentType[]
} }
// interface Answer { // interface Answer {
@ -42,10 +42,10 @@ interface LiveResultsProps {
// answers: Answer[]; // answers: Answer[];
// } // }
const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelectedQuestion, connectedStudents }) => { const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuestion, students }) => {
const [showUsernames, setShowUsernames] = useState<boolean>(false); const [showUsernames, setShowUsernames] = useState<boolean>(false);
const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(false); const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(false);
const [students, setStudents] = useState<StudentType[]>(connectedStudents); // const [students, setStudents] = useState<StudentType[]>(initialStudents);
// const [studentResultsMap, setStudentResultsMap] = useState<Map<string, StudentResult>>(new Map()); // const [studentResultsMap, setStudentResultsMap] = useState<Map<string, StudentResult>>(new Map());
const maxQuestions = questions.length; const maxQuestions = questions.length;
@ -69,83 +69,77 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
// } // }
// }, [students]) // }, [students])
useEffect(() => {
// Update the students state when the initialStudents prop changes // useEffect(() => {
setStudents(connectedStudents); // if (socket) {
console.log(`useEffect: connectedStudents: ${JSON.stringify(connectedStudents)}`); // const submitAnswerHandler = ({
}, [connectedStudents]); // idUser,
// answer,
// idQuestion
// }: {
// idUser: string;
// username: string;
// answer: string | number | boolean;
// idQuestion: number;
// }) => {
// console.log(`Received answer from ${idUser} for question ${idQuestion}: ${answer}`);
// // print the list of current student names
// console.log('Current students:');
// students.forEach((student) => {
// console.log(student.name);
// });
// // Update the students state using the functional form of setStudents
// setStudents((prevStudents) => {
// let foundStudent = false;
// const updatedStudents = prevStudents.map((student) => {
// if (student.id === idUser) {
// foundStudent = true;
// const updatedAnswers = student.answers.map((ans) => {
// const newAnswer: Answer = { answer, isCorrect: checkIfIsCorrect(answer, idQuestion), idQuestion };
// console.log(`Updating answer for ${student.name} for question ${idQuestion} to ${answer}`);
// return (ans.idQuestion === idQuestion ? { ...ans, newAnswer } : ans);
// }
// );
// return { ...student, answers: updatedAnswers };
// }
// return student;
// });
// if (!foundStudent) {
// console.log(`Student ${idUser} not found in the list of students in LiveResults`);
// }
// return updatedStudents;
// });
useEffect(() => { // // make a copy of the students array so we can update it
if (socket) { // // const updatedStudents = [...students];
const submitAnswerHandler = ({
idUser,
answer,
idQuestion
}: {
idUser: string;
username: string;
answer: string | number | boolean;
idQuestion: number;
}) => {
console.log(`Received answer from ${idUser} for question ${idQuestion}: ${answer}`);
// print the list of current student names // // const student = updatedStudents.find((student) => student.id === idUser);
console.log('Current students:'); // // if (!student) {
students.forEach((student) => { // // // this is a bad thing if an answer was submitted but the student isn't in the list
console.log(student.name); // // console.log(`Student ${idUser} not found in the list of students in LiveResults`);
}); // // return;
// // }
// Update the students state using the functional form of setStudents // // const isCorrect = checkIfIsCorrect(answer, idQuestion);
setStudents((prevStudents) => { // // const newAnswer: Answer = { answer, isCorrect, idQuestion };
let foundStudent = false; // // student.answers.push(newAnswer);
const updatedStudents = prevStudents.map((student) => { // // // print list of answers
if (student.id === idUser) { // // console.log('Answers:');
foundStudent = true; // // student.answers.forEach((answer) => {
const updatedAnswers = student.answers.map((ans) => { // // console.log(answer.answer);
const newAnswer: Answer = { answer, isCorrect: checkIfIsCorrect(answer, idQuestion), idQuestion }; // // });
console.log(`Updating answer for ${student.name} for question ${idQuestion} to ${answer}`); // // setStudents(updatedStudents); // update the state
return (ans.idQuestion === idQuestion ? { ...ans, newAnswer } : ans); // };
}
);
return { ...student, answers: updatedAnswers };
}
return student;
});
if (!foundStudent) {
console.log(`Student ${idUser} not found in the list of students in LiveResults`);
}
return updatedStudents;
});
// socket.on('submit-answer', submitAnswerHandler);
// make a copy of the students array so we can update it // return () => {
// const updatedStudents = [...students]; // socket.off('submit-answer');
// };
// const student = updatedStudents.find((student) => student.id === idUser); // }
// if (!student) { // }, [socket]);
// // this is a bad thing if an answer was submitted but the student isn't in the list
// console.log(`Student ${idUser} not found in the list of students in LiveResults`);
// return;
// }
// const isCorrect = checkIfIsCorrect(answer, idQuestion);
// const newAnswer: Answer = { answer, isCorrect, idQuestion };
// student.answers.push(newAnswer);
// // print list of answers
// console.log('Answers:');
// student.answers.forEach((answer) => {
// console.log(answer.answer);
// });
// setStudents(updatedStudents); // update the state
};
socket.on('submit-answer', submitAnswerHandler);
return () => {
socket.off('submit-answer');
};
}
}, [socket]);
const getStudentGrade = (student: StudentType): number => { const getStudentGrade = (student: StudentType): number => {
if (student.answers.length === 0) { if (student.answers.length === 0) {
@ -191,84 +185,84 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
); );
}; };
// (studentResults.filter((student) => // (studentResults.filter((student) =>
// student.answers.some( // student.answers.some(
// (answer) => // (answer) =>
// parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect // parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
// ) // )
// ).length / // ).length /
// studentResults.length) * // studentResults.length) *
// 100 // 100
// ); // );
// }; // };
function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number): boolean { // function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number): boolean {
const questionInfo = questions.find((q) => // const questionInfo = questions.find((q) =>
q.question.id ? q.question.id === idQuestion.toString() : false // q.question.id ? q.question.id === idQuestion.toString() : false
) as QuestionType | undefined; // ) as QuestionType | undefined;
const answerText = answer.toString(); // const answerText = answer.toString();
if (questionInfo) { // if (questionInfo) {
const question = questionInfo.question as GIFTQuestion; // const question = questionInfo.question as GIFTQuestion;
if (question.type === 'TF') { // if (question.type === 'TF') {
return ( // return (
(question.isTrue && answerText == 'true') || // (question.isTrue && answerText == 'true') ||
(!question.isTrue && answerText == 'false') // (!question.isTrue && answerText == 'false')
); // );
} else if (question.type === 'MC') { // } else if (question.type === 'MC') {
return question.choices.some( // return question.choices.some(
(choice) => choice.isCorrect && choice.text.text === answerText // (choice) => choice.isCorrect && choice.text.text === answerText
); // );
} else if (question.type === 'Numerical') { // } else if (question.type === 'Numerical') {
if (question.choices && !Array.isArray(question.choices)) { // if (question.choices && !Array.isArray(question.choices)) {
if ( // if (
question.choices.type === 'high-low' && // question.choices.type === 'high-low' &&
question.choices.numberHigh && // question.choices.numberHigh &&
question.choices.numberLow // question.choices.numberLow
) { // ) {
const answerNumber = parseFloat(answerText); // const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) { // if (!isNaN(answerNumber)) {
return ( // return (
answerNumber <= question.choices.numberHigh && // answerNumber <= question.choices.numberHigh &&
answerNumber >= question.choices.numberLow // answerNumber >= question.choices.numberLow
); // );
} // }
} // }
} // }
if (question.choices && Array.isArray(question.choices)) { // if (question.choices && Array.isArray(question.choices)) {
if ( // if (
question.choices[0].text.type === 'range' && // question.choices[0].text.type === 'range' &&
question.choices[0].text.number && // question.choices[0].text.number &&
question.choices[0].text.range // question.choices[0].text.range
) { // ) {
const answerNumber = parseFloat(answerText); // const answerNumber = parseFloat(answerText);
const range = question.choices[0].text.range; // const range = question.choices[0].text.range;
const correctAnswer = question.choices[0].text.number; // const correctAnswer = question.choices[0].text.number;
if (!isNaN(answerNumber)) { // if (!isNaN(answerNumber)) {
return ( // return (
answerNumber <= correctAnswer + range && // answerNumber <= correctAnswer + range &&
answerNumber >= correctAnswer - range // answerNumber >= correctAnswer - range
); // );
} // }
} // }
if ( // if (
question.choices[0].text.type === 'simple' && // question.choices[0].text.type === 'simple' &&
question.choices[0].text.number // question.choices[0].text.number
) { // ) {
const answerNumber = parseFloat(answerText); // const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) { // if (!isNaN(answerNumber)) {
return answerNumber === question.choices[0].text.number; // return answerNumber === question.choices[0].text.number;
} // }
} // }
} // }
} else if (question.type === 'Short') { // } else if (question.type === 'Short') {
return question.choices.some( // return question.choices.some(
(choice) => choice.text.text.toUpperCase() === answerText.toUpperCase() // (choice) => choice.text.text.toUpperCase() === answerText.toUpperCase()
); // );
} // }
} // }
return false; // return false;
} // }
return ( return (
<div> <div>
@ -301,145 +295,147 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
</div> </div>
<div className="table-container"> <div className="table-container">
<Table size="small" component={Paper}> <TableContainer component={Paper}>
<TableHead> <Table size="small">
<TableRow> <TableHead>
<TableCell className="sticky-column"> <TableRow>
<div className="text-base text-bold">Nom d'utilisateur</div> <TableCell className="sticky-column">
</TableCell> <div className="text-base text-bold">Nom d'utilisateur</div>
{Array.from({ length: maxQuestions }, (_, index) => ( </TableCell>
<TableCell {Array.from({ length: maxQuestions }, (_, index) => (
key={index} <TableCell
sx={{ key={index}
textAlign: 'center', sx={{
cursor: 'pointer', textAlign: 'center',
borderStyle: 'solid', cursor: 'pointer',
borderWidth: 1, borderStyle: 'solid',
borderColor: 'rgba(224, 224, 224, 1)' borderWidth: 1,
}} borderColor: 'rgba(224, 224, 224, 1)'
onClick={() => showSelectedQuestion(index)} }}
> onClick={() => showSelectedQuestion(index)}
<div className="text-base text-bold blue">{`Q${index + 1}`}</div> >
</TableCell> <div className="text-base text-bold blue">{`Q${index + 1}`}</div>
))} </TableCell>
<TableCell ))}
className="sticky-header" <TableCell
sx={{ className="sticky-header"
textAlign: 'center', sx={{
borderStyle: 'solid', textAlign: 'center',
borderWidth: 1, borderStyle: 'solid',
borderColor: 'rgba(224, 224, 224, 1)' borderWidth: 1,
}} borderColor: 'rgba(224, 224, 224, 1)'
> }}
<div className="text-base text-bold">% réussite</div> >
</TableCell> <div className="text-base text-bold">% réussite</div>
</TableRow> </TableCell>
</TableHead> </TableRow>
<TableBody> </TableHead>
{students.map((student) => ( <TableBody>
<TableRow key={student.id}> {students.map((student) => (
<TableCell <TableRow key={student.id}>
className="sticky-column" <TableCell
sx={{ className="sticky-column"
borderStyle: 'solid', sx={{
borderWidth: 1, borderStyle: 'solid',
borderColor: 'rgba(224, 224, 224, 1)' borderWidth: 1,
}} borderColor: 'rgba(224, 224, 224, 1)'
> }}
<div className="text-base"> >
{showUsernames ? student.name : '******'} <div className="text-base">
</div> {showUsernames ? student.name : '******'}
</TableCell> </div>
{Array.from({ length: maxQuestions }, (_, index) => { </TableCell>
const answer = student.answers.find( {Array.from({ length: maxQuestions }, (_, index) => {
(answer) => parseInt(answer.idQuestion.toString()) === index + 1 const answer = student.answers.find(
); (answer) => parseInt(answer.idQuestion.toString()) === index + 1
const answerText = answer ? answer.answer.toString() : ''; );
const isCorrect = answer ? answer.isCorrect : false; const answerText = answer ? answer.answer.toString() : '';
const isCorrect = answer ? answer.isCorrect : false;
return ( return (
<TableCell
key={index}
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
className={
answerText === ''
? ''
: isCorrect
? 'correct-answer'
: 'incorrect-answer'
}
>
{showCorrectAnswers ? (
<div>{formatLatex(answerText)}</div>
) : isCorrect ? (
<FontAwesomeIcon icon={faCheck} />
) : (
answerText !== '' && (
<FontAwesomeIcon icon={faCircleXmark} />
)
)}
</TableCell>
);
})}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
>
{getStudentGrade(student).toFixed()} %
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow sx={{ backgroundColor: '#d3d3d34f' }}>
<TableCell className="sticky-column" sx={{ color: 'black' }}>
<div className="text-base text-bold">% réussite</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell <TableCell
key={index} key={index}
sx={{ sx={{
textAlign: 'center', textAlign: 'center',
borderStyle: 'solid', borderStyle: 'solid',
borderWidth: 1, borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)' borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}} }}
className={
answerText === ''
? ''
: isCorrect
? 'correct-answer'
: 'incorrect-answer'
}
> >
{showCorrectAnswers ? ( {students.length > 0
<div>{formatLatex(answerText)}</div> ? `${getCorrectAnswersPerQuestion(index).toFixed()} %`
) : isCorrect ? ( : '-'}
<FontAwesomeIcon icon={faCheck} />
) : (
answerText !== '' && (
<FontAwesomeIcon icon={faCircleXmark} />
)
)}
</TableCell> </TableCell>
); ))}
})} <TableCell
<TableCell sx={{
sx={{ textAlign: 'center',
textAlign: 'center', borderStyle: 'solid',
borderStyle: 'solid', borderWidth: 1,
borderWidth: 1, borderColor: 'rgba(224, 224, 224, 1)',
borderColor: 'rgba(224, 224, 224, 1)', fontWeight: 'bold',
fontWeight: 'bold', fontSize: '1rem',
color: 'rgba(0, 0, 0)' color: 'rgba(0, 0, 0)'
}} }}
> >
{getStudentGrade(student).toFixed()} % {students.length > 0 ? `${classAverage.toFixed()} %` : '-'}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} </TableFooter>
</TableBody> </Table>
<TableFooter> </TableContainer>
<TableRow sx={{ backgroundColor: '#d3d3d34f' }}> </div>
<TableCell className="sticky-column" sx={{ color: 'black' }}>
<div className="text-base text-bold">% réussite</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index}
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
>
{students.length > 0
? `${getCorrectAnswersPerQuestion(index).toFixed()} %`
: '-'}
</TableCell>
))}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
fontSize: '1rem',
color: 'rgba(0, 0, 0)'
}}
>
{students.length > 0 ? `${classAverage.toFixed()} %` : '-'}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
</div> </div>
); );
}; };

View file

@ -12,7 +12,7 @@ import DisconnectButton from '../../components/DisconnectButton/DisconnectButton
interface StudentModeQuizProps { interface StudentModeQuizProps {
questions: QuestionType[]; questions: QuestionType[];
submitAnswer: (answer: string | number | boolean, idQuestion: string) => void; submitAnswer: (answer: string | number | boolean, idQuestion: number) => void;
disconnectWebSocket: () => void; disconnectWebSocket: () => void;
} }
@ -38,7 +38,7 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
}; };
const handleOnSubmitAnswer = (answer: string | number | boolean) => { const handleOnSubmitAnswer = (answer: string | number | boolean) => {
const idQuestion = questionInfos.question.id || '-1'; const idQuestion = Number(questionInfos.question.id) || -1;
submitAnswer(answer, idQuestion); submitAnswer(answer, idQuestion);
setIsAnswerSubmitted(true); setIsAnswerSubmitted(true);
}; };

View file

@ -11,7 +11,7 @@ import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/
interface TeacherModeQuizProps { interface TeacherModeQuizProps {
questionInfos: QuestionType; questionInfos: QuestionType;
submitAnswer: (answer: string | number | boolean, idQuestion: string) => void; submitAnswer: (answer: string | number | boolean, idQuestion: number) => void;
disconnectWebSocket: () => void; disconnectWebSocket: () => void;
} }
@ -29,7 +29,7 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
}, [questionInfos]); }, [questionInfos]);
const handleOnSubmitAnswer = (answer: string | number | boolean) => { const handleOnSubmitAnswer = (answer: string | number | boolean) => {
const idQuestion = questionInfos.question.id || '-1'; const idQuestion = Number(questionInfos.question.id) || -1;
submitAnswer(answer, idQuestion); submitAnswer(answer, idQuestion);
setFeedbackMessage(`Votre réponse est "${answer.toString()}".`); setFeedbackMessage(`Votre réponse est "${answer.toString()}".`);
setIsFeedbackDialogOpen(true); setIsFeedbackDialogOpen(true);

View file

@ -5,7 +5,7 @@ import { ENV_VARIABLES } from '../../../constants';
import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz'; import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz';
import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz'; import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz';
import webSocketService from '../../../services/WebsocketService'; import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService';
import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton'; import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton';
import './joinRoom.css'; import './joinRoom.css';
@ -99,8 +99,15 @@ const JoinRoom: React.FC = () => {
} }
}; };
const handleOnSubmitAnswer = (answer: string | number | boolean, idQuestion: string) => { const handleOnSubmitAnswer = (answer: string | number | boolean, idQuestion: number) => {
webSocketService.submitAnswer(roomName, answer, username, idQuestion); const answerData: AnswerSubmissionToBackendType = {
roomName: roomName,
answer: answer,
username: username,
idQuestion: idQuestion
};
webSocketService.submitAnswer(answerData);
}; };
if (isWaitingForTeacher) { if (isWaitingForTeacher) {

View file

@ -2,16 +2,16 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { parse } from 'gift-pegjs'; import { GIFTQuestion, parse } from 'gift-pegjs';
import { QuestionType } from '../../../Types/QuestionType'; import { QuestionType } from '../../../Types/QuestionType';
import LiveResultsComponent from '../../../components/LiveResults/LiveResults'; import LiveResultsComponent from '../../../components/LiveResults/LiveResults';
// import { QuestionService } from '../../../services/QuestionService'; // import { QuestionService } from '../../../services/QuestionService';
import webSocketService from '../../../services/WebsocketService'; import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
import { QuizType } from '../../../Types/QuizType'; import { QuizType } from '../../../Types/QuizType';
import './manageRoom.css'; import './manageRoom.css';
import { ENV_VARIABLES } from '../../../constants'; import { ENV_VARIABLES } from '../../../constants';
import { StudentType } from '../../../Types/StudentType'; import { StudentType, Answer } from '../../../Types/StudentType';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle'; import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
import { Refresh, Error } from '@mui/icons-material'; import { Refresh, Error } from '@mui/icons-material';
@ -121,10 +121,9 @@ const ManageRoom: React.FC = () => {
useEffect(() => { useEffect(() => {
// This is here to make sure the correct value is sent when user join // This is here to make sure the correct value is sent when user join
if (socket) { if (socket) {
console.log(`Listening for user-joined in room ${roomName}`);
socket.on('user-joined', (_student: StudentType) => { socket.on('user-joined', (_student: StudentType) => {
// setUsers((prevUsers) => [...prevUsers, user]);
if (quizMode === 'teacher') { if (quizMode === 'teacher') {
webSocketService.nextQuestion(roomName, currentQuestion); webSocketService.nextQuestion(roomName, currentQuestion);
} else if (quizMode === 'student') { } else if (quizMode === 'student') {
@ -132,7 +131,122 @@ const ManageRoom: React.FC = () => {
} }
}); });
} }
}, [currentQuestion, quizQuestions]);
if (socket) {
// handle the case where user submits an answer
console.log(`Listening for submit-answer-room in room ${roomName}`);
socket.on('submit-answer-room', (answerData: AnswerReceptionFromBackendType) => {
const { answer, idQuestion, idUser, username } = answerData;
console.log(`Received answer from ${username} for question ${idQuestion}: ${answer}`);
if (!quizQuestions) {
console.log('Quiz questions not found (cannot update answers without them).');
return;
}
// Update the students state using the functional form of setStudents
setStudents((prevStudents) => {
// print the list of current student names
console.log('Current students:');
prevStudents.forEach((student) => {
console.log(student.name);
});
let foundStudent = false;
const updatedStudents = prevStudents.map((student) => {
console.log(`Comparing ${student.id} to ${idUser}`);
if (student.id === idUser) {
foundStudent = true;
const existingAnswer = student.answers.find((ans) => ans.idQuestion === idQuestion);
let updatedAnswers: Answer[] = [];
if (existingAnswer) {
// Update the existing answer
updatedAnswers = student.answers.map((ans) => {
console.log(`Comparing ${ans.idQuestion} to ${idQuestion}`);
return (ans.idQuestion === idQuestion ? { ...ans, answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!) } : ans);
});
} else {
// Add a new answer
const newAnswer = { idQuestion, answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!) };
updatedAnswers = [...student.answers, newAnswer];
}
return { ...student, answers: updatedAnswers };
}
return student;
});
if (!foundStudent) {
console.log(`Student ${username} not found in the list.`);
}
return updatedStudents;
});
});
setSocket(socket);
}
}, [socket, currentQuestion, quizQuestions]);
// useEffect(() => {
// if (socket) {
// const submitAnswerHandler = (answerData: answerSubmissionType) => {
// const { answer, idQuestion, username } = answerData;
// console.log(`Received answer from ${username} for question ${idQuestion}: ${answer}`);
// // print the list of current student names
// console.log('Current students:');
// students.forEach((student) => {
// console.log(student.name);
// });
// // Update the students state using the functional form of setStudents
// setStudents((prevStudents) => {
// let foundStudent = false;
// const updatedStudents = prevStudents.map((student) => {
// if (student.id === username) {
// foundStudent = true;
// const updatedAnswers = student.answers.map((ans) => {
// const newAnswer: Answer = { answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!), idQuestion };
// console.log(`Updating answer for ${student.name} for question ${idQuestion} to ${answer}`);
// return (ans.idQuestion === idQuestion ? { ...ans, newAnswer } : ans);
// }
// );
// return { ...student, answers: updatedAnswers };
// }
// return student;
// });
// if (!foundStudent) {
// console.log(`Student ${username} not found in the list of students in LiveResults`);
// }
// return updatedStudents;
// });
// // make a copy of the students array so we can update it
// // const updatedStudents = [...students];
// // const student = updatedStudents.find((student) => student.id === idUser);
// // if (!student) {
// // // this is a bad thing if an answer was submitted but the student isn't in the list
// // console.log(`Student ${idUser} not found in the list of students in LiveResults`);
// // return;
// // }
// // const isCorrect = checkIfIsCorrect(answer, idQuestion);
// // const newAnswer: Answer = { answer, isCorrect, idQuestion };
// // student.answers.push(newAnswer);
// // // print list of answers
// // console.log('Answers:');
// // student.answers.forEach((answer) => {
// // console.log(answer.answer);
// // });
// // setStudents(updatedStudents); // update the state
// };
// socket.on('submit-answer', submitAnswerHandler);
// return () => {
// socket.off('submit-answer');
// };
// }
// }, [socket]);
const nextQuestion = () => { const nextQuestion = () => {
if (!quizQuestions || !currentQuestion || !quiz?.content) return; if (!quizQuestions || !currentQuestion || !quiz?.content) return;
@ -173,8 +287,12 @@ const ManageRoom: React.FC = () => {
const launchTeacherMode = () => { const launchTeacherMode = () => {
const quizQuestions = initializeQuizQuestion(); const quizQuestions = initializeQuizQuestion();
console.log('launchTeacherMode - quizQuestions:', quizQuestions);
if (!quizQuestions) return; if (!quizQuestions) {
console.log('Error launching quiz (launchTeacherMode). No questions found.');
return;
}
setCurrentQuestion(quizQuestions[0]); setCurrentQuestion(quizQuestions[0]);
webSocketService.nextQuestion(roomName, quizQuestions[0]); webSocketService.nextQuestion(roomName, quizQuestions[0]);
@ -182,18 +300,20 @@ const ManageRoom: React.FC = () => {
const launchStudentMode = () => { const launchStudentMode = () => {
const quizQuestions = initializeQuizQuestion(); const quizQuestions = initializeQuizQuestion();
console.log('launchStudentMode - quizQuestions:', quizQuestions);
if (!quizQuestions) { if (!quizQuestions) {
console.log('Error launching quiz (launchStudentMode). No questions found.');
return; return;
} }
setQuizQuestions(quizQuestions);
webSocketService.launchStudentModeQuiz(roomName, quizQuestions); webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
}; };
const launchQuiz = () => { const launchQuiz = () => {
if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) { if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) {
// TODO: This error happens when token expires! Need to handle it properly // TODO: This error happens when token expires! Need to handle it properly
console.log('Error launching quiz. No socket, room name or no questions.'); console.log(`Error launching quiz. socket: ${socket}, roomName: ${roomName}, quiz: ${quiz}`);
return; return;
} }
switch (quizMode) { switch (quizMode) {
@ -219,6 +339,75 @@ const ManageRoom: React.FC = () => {
navigate('/teacher/dashboard'); navigate('/teacher/dashboard');
}; };
function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number, questions: QuestionType[]): boolean {
const questionInfo = questions.find((q) =>
q.question.id ? q.question.id === idQuestion.toString() : false
) as QuestionType | undefined;
const answerText = answer.toString();
if (questionInfo) {
const question = questionInfo.question as GIFTQuestion;
if (question.type === 'TF') {
return (
(question.isTrue && answerText == 'true') ||
(!question.isTrue && answerText == 'false')
);
} else if (question.type === 'MC') {
return question.choices.some(
(choice) => choice.isCorrect && choice.text.text === answerText
);
} else if (question.type === 'Numerical') {
if (question.choices && !Array.isArray(question.choices)) {
if (
question.choices.type === 'high-low' &&
question.choices.numberHigh &&
question.choices.numberLow
) {
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return (
answerNumber <= question.choices.numberHigh &&
answerNumber >= question.choices.numberLow
);
}
}
}
if (question.choices && Array.isArray(question.choices)) {
if (
question.choices[0].text.type === 'range' &&
question.choices[0].text.number &&
question.choices[0].text.range
) {
const answerNumber = parseFloat(answerText);
const range = question.choices[0].text.range;
const correctAnswer = question.choices[0].text.number;
if (!isNaN(answerNumber)) {
return (
answerNumber <= correctAnswer + range &&
answerNumber >= correctAnswer - range
);
}
}
if (
question.choices[0].text.type === 'simple' &&
question.choices[0].text.number
) {
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return answerNumber === question.choices[0].text.number;
}
}
}
} else if (question.type === 'Short') {
return question.choices.some(
(choice) => choice.text.text.toUpperCase() === answerText.toUpperCase()
);
}
}
return false;
}
if (!roomName) { if (!roomName) {
return ( return (
<div className="center"> <div className="center">
@ -258,7 +447,7 @@ const ManageRoom: React.FC = () => {
<div className='dumb'></div> <div className='dumb'></div>
</div> </div>
{/* the following breaks the css (if 'room' classes are nested) */} {/* the following breaks the css (if 'room' classes are nested) */}
<div className=''> <div className=''>
{quizQuestions ? ( {quizQuestions ? (
@ -295,7 +484,7 @@ const ManageRoom: React.FC = () => {
socket={socket} socket={socket}
questions={quizQuestions} questions={quizQuestions}
showSelectedQuestion={showSelectedQuestion} showSelectedQuestion={showSelectedQuestion}
connectedStudents={students} students={students}
></LiveResultsComponent> ></LiveResultsComponent>
</div> </div>

View file

@ -1,6 +1,22 @@
// WebSocketService.tsx // WebSocketService.tsx
import { io, Socket } from 'socket.io-client'; import { io, Socket } from 'socket.io-client';
// Must (manually) sync these types to server/socket/socket.js
export type AnswerSubmissionToBackendType = {
roomName: string;
username: string;
answer: string | number | boolean;
idQuestion: number;
};
export type AnswerReceptionFromBackendType = {
idUser: string;
username: string;
answer: string | number | boolean;
idQuestion: number;
};
class WebSocketService { class WebSocketService {
private socket: Socket | null = null; private socket: Socket | null = null;
@ -51,19 +67,22 @@ class WebSocketService {
} }
} }
submitAnswer( submitAnswer(answerData: AnswerSubmissionToBackendType
roomName: string, // roomName: string,
answer: string | number | boolean, // answer: string | number | boolean,
username: string, // username: string,
idQuestion: string // idQuestion: string
) { ) {
if (this.socket) { if (this.socket) {
this.socket?.emit('submit-answer', { this.socket?.emit('submit-answer',
answer: answer, // {
roomName: roomName, // answer: answer,
username: username, // roomName: roomName,
idQuestion: idQuestion // username: username,
}); // idQuestion: idQuestion
// }
answerData
);
} }
} }
} }

View file

@ -125,7 +125,7 @@ describe("websocket server", () => {
answer: "answer1", answer: "answer1",
idQuestion: 1, idQuestion: 1,
}); });
teacherSocket.on("submit-answer", (answer) => { teacherSocket.on("submit-answer-room", (answer) => {
expect(answer).toEqual({ expect(answer).toEqual({
idUser: studentSocket.id, idUser: studentSocket.id,
username: "student1", username: "student1",

View file

@ -101,7 +101,7 @@ const setupWebsocket = (io) => {
}); });
socket.on("submit-answer", ({ roomName, username, answer, idQuestion }) => { socket.on("submit-answer", ({ roomName, username, answer, idQuestion }) => {
socket.to(roomName).emit("submit-answer", { socket.to(roomName).emit("submit-answer-room", {
idUser: socket.id, idUser: socket.id,
username, username,
answer, answer,