diff --git a/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx b/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx index 02977db..3c633bb 100644 --- a/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx +++ b/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx @@ -48,7 +48,7 @@ describe('StudentModeQuiz', () => { fireEvent.click(screen.getByText('Répondre')); }); - expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', '1'); + expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); }); test('handles quit button click', async () => { diff --git a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx index 8bc4b0f..57ab031 100644 --- a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx +++ b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx @@ -47,7 +47,7 @@ describe('TeacherModeQuiz', () => { act(() => { 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(); }); diff --git a/client/src/components/LiveResults/LiveResults.tsx b/client/src/components/LiveResults/LiveResults.tsx index f556f15..f08ad27 100644 --- a/client/src/components/LiveResults/LiveResults.tsx +++ b/client/src/components/LiveResults/LiveResults.tsx @@ -1,7 +1,6 @@ // LiveResults.tsx -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { Socket } from 'socket.io-client'; -import { GIFTQuestion } from 'gift-pegjs'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons'; import { QuestionType } from '../../Types/QuestionType'; @@ -15,11 +14,12 @@ import { Table, TableBody, TableCell, + TableContainer, TableFooter, TableHead, TableRow } from '@mui/material'; -import { StudentType, Answer } from '../../Types/StudentType'; +import { StudentType } from '../../Types/StudentType'; import { formatLatex } from '../GiftTemplate/templates/TextType'; interface LiveResultsProps { @@ -27,7 +27,7 @@ interface LiveResultsProps { questions: QuestionType[]; showSelectedQuestion: (index: number) => void; quizMode: 'teacher' | 'student'; - connectedStudents: StudentType[] + students: StudentType[] } // interface Answer { @@ -42,10 +42,10 @@ interface LiveResultsProps { // answers: Answer[]; // } -const LiveResults: React.FC = ({ socket, questions, showSelectedQuestion, connectedStudents }) => { +const LiveResults: React.FC = ({ questions, showSelectedQuestion, students }) => { const [showUsernames, setShowUsernames] = useState(false); const [showCorrectAnswers, setShowCorrectAnswers] = useState(false); - const [students, setStudents] = useState(connectedStudents); + // const [students, setStudents] = useState(initialStudents); // const [studentResultsMap, setStudentResultsMap] = useState>(new Map()); const maxQuestions = questions.length; @@ -69,83 +69,77 @@ const LiveResults: React.FC = ({ socket, questions, showSelect // } // }, [students]) - useEffect(() => { - // Update the students state when the initialStudents prop changes - setStudents(connectedStudents); - console.log(`useEffect: connectedStudents: ${JSON.stringify(connectedStudents)}`); - }, [connectedStudents]); + + // useEffect(() => { + // if (socket) { + // 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 + // 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(() => { - if (socket) { - 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}`); + // // make a copy of the students array so we can update it + // // const updatedStudents = [...students]; - // print the list of current student names - console.log('Current students:'); - students.forEach((student) => { - console.log(student.name); - }); + // // 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; + // // } - // 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; - }); + // // 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 + // }; - - // 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]); + // socket.on('submit-answer', submitAnswerHandler); + // return () => { + // socket.off('submit-answer'); + // }; + // } + // }, [socket]); const getStudentGrade = (student: StudentType): number => { if (student.answers.length === 0) { @@ -172,7 +166,7 @@ const LiveResults: React.FC = ({ socket, questions, showSelect const classAverage: number = useMemo(() => { let classTotal = 0; - + students.forEach((student) => { classTotal += getStudentGrade(student); }); @@ -191,84 +185,84 @@ const LiveResults: React.FC = ({ socket, questions, showSelect ); }; - // (studentResults.filter((student) => - // student.answers.some( - // (answer) => - // parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect - // ) - // ).length / - // studentResults.length) * - // 100 - // ); + // (studentResults.filter((student) => + // student.answers.some( + // (answer) => + // parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect + // ) + // ).length / + // studentResults.length) * + // 100 + // ); // }; - function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number): boolean { - const questionInfo = questions.find((q) => - q.question.id ? q.question.id === idQuestion.toString() : false - ) as QuestionType | undefined; + // function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number): 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; - } + // 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; + // } return (
@@ -301,145 +295,147 @@ const LiveResults: React.FC = ({ socket, questions, showSelect
- - - - -
Nom d'utilisateur
-
- {Array.from({ length: maxQuestions }, (_, index) => ( - showSelectedQuestion(index)} - > -
{`Q${index + 1}`}
-
- ))} - -
% réussite
-
-
-
- - {students.map((student) => ( - - -
- {showUsernames ? student.name : '******'} -
-
- {Array.from({ length: maxQuestions }, (_, index) => { - const answer = student.answers.find( - (answer) => parseInt(answer.idQuestion.toString()) === index + 1 - ); - const answerText = answer ? answer.answer.toString() : ''; - const isCorrect = answer ? answer.isCorrect : false; + +
+ + + +
Nom d'utilisateur
+
+ {Array.from({ length: maxQuestions }, (_, index) => ( + showSelectedQuestion(index)} + > +
{`Q${index + 1}`}
+
+ ))} + +
% réussite
+
+
+
+ + {students.map((student) => ( + + +
+ {showUsernames ? student.name : '******'} +
+
+ {Array.from({ length: maxQuestions }, (_, index) => { + const answer = student.answers.find( + (answer) => parseInt(answer.idQuestion.toString()) === index + 1 + ); + const answerText = answer ? answer.answer.toString() : ''; + const isCorrect = answer ? answer.isCorrect : false; - return ( + return ( + + {showCorrectAnswers ? ( +
{formatLatex(answerText)}
+ ) : isCorrect ? ( + + ) : ( + answerText !== '' && ( + + ) + )} +
+ ); + })} + + {getStudentGrade(student).toFixed()} % + +
+ ))} +
+ + + +
% réussite
+
+ {Array.from({ length: maxQuestions }, (_, index) => ( - {showCorrectAnswers ? ( -
{formatLatex(answerText)}
- ) : isCorrect ? ( - - ) : ( - answerText !== '' && ( - - ) - )} + {students.length > 0 + ? `${getCorrectAnswersPerQuestion(index).toFixed()} %` + : '-'}
- ); - })} - - {getStudentGrade(student).toFixed()} % - -
- ))} - - - - -
% réussite
-
- {Array.from({ length: maxQuestions }, (_, index) => ( - - {students.length > 0 - ? `${getCorrectAnswersPerQuestion(index).toFixed()} %` - : '-'} - - ))} - - {students.length > 0 ? `${classAverage.toFixed()} %` : '-'} - -
-
-
-
+ ))} + + {students.length > 0 ? `${classAverage.toFixed()} %` : '-'} + + + + + + ); }; diff --git a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx index 1d4dbfe..418405f 100644 --- a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx +++ b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx @@ -12,7 +12,7 @@ import DisconnectButton from '../../components/DisconnectButton/DisconnectButton interface StudentModeQuizProps { questions: QuestionType[]; - submitAnswer: (answer: string | number | boolean, idQuestion: string) => void; + submitAnswer: (answer: string | number | boolean, idQuestion: number) => void; disconnectWebSocket: () => void; } @@ -38,7 +38,7 @@ const StudentModeQuiz: React.FC = ({ }; const handleOnSubmitAnswer = (answer: string | number | boolean) => { - const idQuestion = questionInfos.question.id || '-1'; + const idQuestion = Number(questionInfos.question.id) || -1; submitAnswer(answer, idQuestion); setIsAnswerSubmitted(true); }; diff --git a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx index 4c15cef..82f12a6 100644 --- a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx +++ b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx @@ -11,7 +11,7 @@ import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/ interface TeacherModeQuizProps { questionInfos: QuestionType; - submitAnswer: (answer: string | number | boolean, idQuestion: string) => void; + submitAnswer: (answer: string | number | boolean, idQuestion: number) => void; disconnectWebSocket: () => void; } @@ -29,7 +29,7 @@ const TeacherModeQuiz: React.FC = ({ }, [questionInfos]); const handleOnSubmitAnswer = (answer: string | number | boolean) => { - const idQuestion = questionInfos.question.id || '-1'; + const idQuestion = Number(questionInfos.question.id) || -1; submitAnswer(answer, idQuestion); setFeedbackMessage(`Votre réponse est "${answer.toString()}".`); setIsFeedbackDialogOpen(true); diff --git a/client/src/pages/Student/JoinRoom/JoinRoom.tsx b/client/src/pages/Student/JoinRoom/JoinRoom.tsx index 7d8b942..e29bfb7 100644 --- a/client/src/pages/Student/JoinRoom/JoinRoom.tsx +++ b/client/src/pages/Student/JoinRoom/JoinRoom.tsx @@ -5,7 +5,7 @@ import { ENV_VARIABLES } from '../../../constants'; import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz'; import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz'; -import webSocketService from '../../../services/WebsocketService'; +import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService'; import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton'; import './joinRoom.css'; @@ -99,8 +99,15 @@ const JoinRoom: React.FC = () => { } }; - const handleOnSubmitAnswer = (answer: string | number | boolean, idQuestion: string) => { - webSocketService.submitAnswer(roomName, answer, username, idQuestion); + const handleOnSubmitAnswer = (answer: string | number | boolean, idQuestion: number) => { + const answerData: AnswerSubmissionToBackendType = { + roomName: roomName, + answer: answer, + username: username, + idQuestion: idQuestion + }; + + webSocketService.submitAnswer(answerData); }; if (isWaitingForTeacher) { diff --git a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx index 70adca8..6670240 100644 --- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx +++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx @@ -2,16 +2,16 @@ import React, { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { Socket } from 'socket.io-client'; -import { parse } from 'gift-pegjs'; +import { GIFTQuestion, parse } from 'gift-pegjs'; import { QuestionType } from '../../../Types/QuestionType'; import LiveResultsComponent from '../../../components/LiveResults/LiveResults'; // import { QuestionService } from '../../../services/QuestionService'; -import webSocketService from '../../../services/WebsocketService'; +import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService'; import { QuizType } from '../../../Types/QuizType'; import './manageRoom.css'; import { ENV_VARIABLES } from '../../../constants'; -import { StudentType } from '../../../Types/StudentType'; +import { StudentType, Answer } from '../../../Types/StudentType'; import { Button } from '@mui/material'; import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle'; import { Refresh, Error } from '@mui/icons-material'; @@ -82,7 +82,7 @@ const ManageRoom: React.FC = () => { const createWebSocketRoom = () => { setConnectingError(''); const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); - + socket.on('connect', () => { webSocketService.createRoom(); }); @@ -98,7 +98,7 @@ const ManageRoom: React.FC = () => { }); socket.on('user-joined', (student: StudentType) => { console.log(`Student joined: name = ${student.name}, id = ${student.id}`); - + setStudents((prevStudents) => [...prevStudents, student]); if (quizMode === 'teacher') { @@ -121,10 +121,9 @@ const ManageRoom: React.FC = () => { useEffect(() => { // This is here to make sure the correct value is sent when user join if (socket) { + console.log(`Listening for user-joined in room ${roomName}`); socket.on('user-joined', (_student: StudentType) => { - - // setUsers((prevUsers) => [...prevUsers, user]); - + if (quizMode === 'teacher') { webSocketService.nextQuestion(roomName, currentQuestion); } 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 = () => { if (!quizQuestions || !currentQuestion || !quiz?.content) return; @@ -173,8 +287,12 @@ const ManageRoom: React.FC = () => { const launchTeacherMode = () => { 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]); webSocketService.nextQuestion(roomName, quizQuestions[0]); @@ -182,18 +300,20 @@ const ManageRoom: React.FC = () => { const launchStudentMode = () => { const quizQuestions = initializeQuizQuestion(); + console.log('launchStudentMode - quizQuestions:', quizQuestions); if (!quizQuestions) { + console.log('Error launching quiz (launchStudentMode). No questions found.'); return; } - + setQuizQuestions(quizQuestions); webSocketService.launchStudentModeQuiz(roomName, quizQuestions); }; const launchQuiz = () => { if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) { // 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; } switch (quizMode) { @@ -207,7 +327,7 @@ const ManageRoom: React.FC = () => { const showSelectedQuestion = (questionIndex: number) => { if (quiz?.content && quizQuestions) { setCurrentQuestion(quizQuestions[questionIndex]); - + if (quizMode === 'teacher') { webSocketService.nextQuestion(roomName, quizQuestions[questionIndex]); } @@ -219,6 +339,75 @@ const ManageRoom: React.FC = () => { 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) { return (
@@ -258,7 +447,7 @@ const ManageRoom: React.FC = () => {
-{/* the following breaks the css (if 'room' classes are nested) */} + {/* the following breaks the css (if 'room' classes are nested) */}
{quizQuestions ? ( @@ -295,7 +484,7 @@ const ManageRoom: React.FC = () => { socket={socket} questions={quizQuestions} showSelectedQuestion={showSelectedQuestion} - connectedStudents={students} + students={students} >
diff --git a/client/src/services/WebsocketService.tsx b/client/src/services/WebsocketService.tsx index 3c3db74..c5f74be 100644 --- a/client/src/services/WebsocketService.tsx +++ b/client/src/services/WebsocketService.tsx @@ -1,6 +1,22 @@ // WebSocketService.tsx 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 { private socket: Socket | null = null; @@ -51,19 +67,22 @@ class WebSocketService { } } - submitAnswer( - roomName: string, - answer: string | number | boolean, - username: string, - idQuestion: string + submitAnswer(answerData: AnswerSubmissionToBackendType + // roomName: string, + // answer: string | number | boolean, + // username: string, + // idQuestion: string ) { if (this.socket) { - this.socket?.emit('submit-answer', { - answer: answer, - roomName: roomName, - username: username, - idQuestion: idQuestion - }); + this.socket?.emit('submit-answer', + // { + // answer: answer, + // roomName: roomName, + // username: username, + // idQuestion: idQuestion + // } + answerData + ); } } } diff --git a/server/__tests__/socket.test.js b/server/__tests__/socket.test.js index 36bc5b3..141a31a 100644 --- a/server/__tests__/socket.test.js +++ b/server/__tests__/socket.test.js @@ -125,7 +125,7 @@ describe("websocket server", () => { answer: "answer1", idQuestion: 1, }); - teacherSocket.on("submit-answer", (answer) => { + teacherSocket.on("submit-answer-room", (answer) => { expect(answer).toEqual({ idUser: studentSocket.id, username: "student1", diff --git a/server/socket/socket.js b/server/socket/socket.js index dc7e99f..5efe1fe 100644 --- a/server/socket/socket.js +++ b/server/socket/socket.js @@ -101,7 +101,7 @@ const setupWebsocket = (io) => { }); socket.on("submit-answer", ({ roomName, username, answer, idQuestion }) => { - socket.to(roomName).emit("submit-answer", { + socket.to(roomName).emit("submit-answer-room", { idUser: socket.id, username, answer,