diff --git a/client/src/Types/StudentType.tsx b/client/src/Types/StudentType.tsx new file mode 100644 index 0000000..b484af5 --- /dev/null +++ b/client/src/Types/StudentType.tsx @@ -0,0 +1,12 @@ +export interface Answer { + answer: string | number | boolean; + isCorrect: boolean; + idQuestion: number; +} + +export interface StudentType { + name: string; + id: string; + room?: string; + answers: Answer[]; +} diff --git a/client/src/Types/UserType.tsx b/client/src/Types/UserType.tsx deleted file mode 100644 index e7ef41d..0000000 --- a/client/src/Types/UserType.tsx +++ /dev/null @@ -1,4 +0,0 @@ -export interface UserType { - name: string; - id: string; -} diff --git a/client/src/__tests__/Types/StudentType.test.tsx b/client/src/__tests__/Types/StudentType.test.tsx new file mode 100644 index 0000000..4e7c849 --- /dev/null +++ b/client/src/__tests__/Types/StudentType.test.tsx @@ -0,0 +1,17 @@ +//StudentType.test.tsx +import { StudentType, Answer } from "../../Types/StudentType"; + +const user : StudentType = { + name: 'Student', + id: '123', + answers: new Array() +} + +describe('StudentType', () => { + test('creates a student with name, id and answers', () => { + + expect(user.name).toBe('Student'); + expect(user.id).toBe('123'); + expect(user.answers.length).toBe(0); + }); +}); diff --git a/client/src/__tests__/Types/UserType.test.tsx b/client/src/__tests__/Types/UserType.test.tsx deleted file mode 100644 index 97ebfa9..0000000 --- a/client/src/__tests__/Types/UserType.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -//UserTyper.test.tsx -import { UserType } from "../../Types/UserType"; - -const user : UserType = { - name: 'Student', - id: '123' -} - -describe('UserType', () => { - test('creates a user with name and id', () => { - - expect(user.name).toBe('Student'); - expect(user.id).toBe('123'); - }); -}); diff --git a/client/src/__tests__/components/UserWaitPage/UserWaitPage.test.tsx b/client/src/__tests__/components/StudentWaitPage/StudentWaitPage.test.tsx similarity index 57% rename from client/src/__tests__/components/UserWaitPage/UserWaitPage.test.tsx rename to client/src/__tests__/components/StudentWaitPage/StudentWaitPage.test.tsx index 6722794..c5c8dad 100644 --- a/client/src/__tests__/components/UserWaitPage/UserWaitPage.test.tsx +++ b/client/src/__tests__/components/StudentWaitPage/StudentWaitPage.test.tsx @@ -1,24 +1,25 @@ // Importez le type UserType s'il n'est pas déjà importé import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; -import UserWaitPage from '../../../components/UserWaitPage/UserWaitPage'; +import StudentWaitPage from '../../../components/StudentWaitPage/StudentWaitPage'; +import { StudentType, Answer } from '../../../Types/StudentType'; -describe('UserWaitPage Component', () => { - const mockUsers = [ - { id: '1', name: 'User1' }, - { id: '2', name: 'User2' }, - { id: '3', name: 'User3' }, +describe('StudentWaitPage Component', () => { + const mockUsers: StudentType[] = [ + { id: '1', name: 'User1', answers: new Array() }, + { id: '2', name: 'User2', answers: new Array() }, + { id: '3', name: 'User3', answers: new Array() }, ]; const mockProps = { - users: mockUsers, + students: mockUsers, launchQuiz: jest.fn(), roomName: 'Test Room', setQuizMode: jest.fn(), }; - test('renders UserWaitPage with correct content', () => { - render(); + test('renders StudentWaitPage with correct content', () => { + render(); //expect(screen.getByText(/Test Room/)).toBeInTheDocument(); @@ -31,7 +32,7 @@ describe('UserWaitPage Component', () => { }); test('clicking on "Lancer" button opens LaunchQuizDialog', () => { - render(); + render(); fireEvent.click(screen.getByRole('button', { name: /Lancer/i })); 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 9539a53..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 { UserType } from '../../Types/UserType'; +import { StudentType } from '../../Types/StudentType'; import { formatLatex } from '../GiftTemplate/templates/TextType'; interface LiveResultsProps { @@ -27,79 +27,121 @@ interface LiveResultsProps { questions: QuestionType[]; showSelectedQuestion: (index: number) => void; quizMode: 'teacher' | 'student'; - students: UserType[] + students: StudentType[] } -interface Answer { - answer: string | number | boolean; - isCorrect: boolean; - idQuestion: number; -} +// interface Answer { +// answer: string | number | boolean; +// isCorrect: boolean; +// idQuestion: number; +// } -interface StudentResult { - username: string; - idUser: string; - answers: Answer[]; -} +// interface StudentResult { +// username: string; +// idUser: string; +// answers: Answer[]; +// } -const LiveResults: React.FC = ({ socket, questions, showSelectedQuestion, students }) => { +const LiveResults: React.FC = ({ questions, showSelectedQuestion, students }) => { const [showUsernames, setShowUsernames] = useState(false); const [showCorrectAnswers, setShowCorrectAnswers] = useState(false); - const [studentResults, setStudentResults] = useState([]); + // const [students, setStudents] = useState(initialStudents); + // const [studentResultsMap, setStudentResultsMap] = useState>(new Map()); const maxQuestions = questions.length; - useEffect(() => { - // Set student list before starting - let newStudents: StudentResult[] = []; + // useEffect(() => { + // // Initialize the map with the current students + // const newStudentResultsMap = new Map(); - for (const student of students as UserType[]) { - newStudents.push({ username: student.name, idUser: student.id, answers: [] }) - } + // for (const student of students) { + // newStudentResultsMap.set(student.id, { username: student.name, idUser: student.id, answers: [] }); + // } - setStudentResults(newStudents); + // setStudentResultsMap(newStudentResultsMap); + // }, []) - }, []) + // update when students change + // useEffect(() => { + // // studentResultsMap is inconsistent with students -- need to update - useEffect(() => { - if (socket) { - const submitAnswerHandler = ({ - idUser, - username, - answer, - idQuestion - }: { - idUser: string; - username: string; - answer: string | number | boolean; - idQuestion: number; - }) => { - setStudentResults((currentResults) => { - const userIndex = currentResults.findIndex( - (result) => result.idUser === idUser - ); - const isCorrect = checkIfIsCorrect(answer, idQuestion); - if (userIndex !== -1) { - const newResults = [...currentResults]; - newResults[userIndex].answers.push({ answer, isCorrect, idQuestion }); - return newResults; - } else { - return [ - ...currentResults, - { idUser, username, answers: [{ answer, isCorrect, idQuestion }] } - ]; - } - }); - }; + // for (const student of students as StudentType[]) { + // } - socket.on('submit-answer', submitAnswerHandler); - return () => { - socket.off('submit-answer'); - }; - } - }, [socket]); + // }, [students]) - const getStudentGrade = (student: StudentResult): number => { + // 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; + // }); + + + // // 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 getStudentGrade = (student: StudentType): number => { if (student.answers.length === 0) { return 0; } @@ -124,93 +166,103 @@ const LiveResults: React.FC = ({ socket, questions, showSelect const classAverage: number = useMemo(() => { let classTotal = 0; - studentResults.forEach((student) => { + + students.forEach((student) => { classTotal += getStudentGrade(student); }); - return classTotal / studentResults.length; - }, [studentResults]); + return classTotal / students.length; + }, [students]); const getCorrectAnswersPerQuestion = (index: number): number => { return ( - (studentResults.filter((student) => + (students.filter((student) => student.answers.some( (answer) => parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect ) - ).length / - studentResults.length) * - 100 + ).length / students.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; + // (studentResults.filter((student) => + // student.answers.some( + // (answer) => + // parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect + // ) + // ).length / + // studentResults.length) * + // 100 + // ); + // }; - 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; - } + // 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; + // } return (
@@ -243,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
-
-
-
- - {studentResults.map((student) => ( - - -
- {showUsernames ? student.username : '******'} -
-
- {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) => ( - - {studentResults.length > 0 - ? `${getCorrectAnswersPerQuestion(index).toFixed()} %` - : '-'} - - ))} - - {studentResults.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/UserWaitPage/UserWaitPage.tsx b/client/src/components/StudentWaitPage/StudentWaitPage.tsx similarity index 65% rename from client/src/components/UserWaitPage/UserWaitPage.tsx rename to client/src/components/StudentWaitPage/StudentWaitPage.tsx index 41b608c..5937f08 100644 --- a/client/src/components/UserWaitPage/UserWaitPage.tsx +++ b/client/src/components/StudentWaitPage/StudentWaitPage.tsx @@ -1,56 +1,56 @@ -import { Button, Chip, Grid } from '@mui/material'; -import { UserType } from '../../Types/UserType'; -import { PlayArrow } from '@mui/icons-material'; -import LaunchQuizDialog from '../LaunchQuizDialog/LaunchQuizDialog'; -import { useState } from 'react'; -import './userWaitPage.css'; - -interface Props { - users: UserType[]; - launchQuiz: () => void; - setQuizMode: (mode: 'student' | 'teacher') => void; -} - -const UserWaitPage: React.FC = ({ users, launchQuiz, setQuizMode }) => { - const [isDialogOpen, setIsDialogOpen] = useState(false); - - return ( -
-
- -
- -
- - - - {users.map((user, index) => ( - - - - ))} - - - -
- - setIsDialogOpen(false)} - launchQuiz={launchQuiz} - setQuizMode={setQuizMode} - /> - -
- ); -}; - -export default UserWaitPage; +import { Box, Button, Chip } from '@mui/material'; +import { StudentType } from '../../Types/StudentType'; +import { PlayArrow } from '@mui/icons-material'; +import LaunchQuizDialog from '../LaunchQuizDialog/LaunchQuizDialog'; +import { useState } from 'react'; +import './studentWaitPage.css'; + +interface Props { + students: StudentType[]; + launchQuiz: () => void; + setQuizMode: (mode: 'student' | 'teacher') => void; +} + +const StudentWaitPage: React.FC = ({ students, launchQuiz, setQuizMode }) => { + const [isDialogOpen, setIsDialogOpen] = useState(false); + + return ( +
+
+ +
+ +
+ + + + {students.map((student, index) => ( + + + + ))} + + + +
+ + setIsDialogOpen(false)} + launchQuiz={launchQuiz} + setQuizMode={setQuizMode} + /> + +
+ ); +}; + +export default StudentWaitPage; diff --git a/client/src/components/UserWaitPage/userWaitPage.css b/client/src/components/StudentWaitPage/studentWaitPage.css similarity index 100% rename from client/src/components/UserWaitPage/userWaitPage.css rename to client/src/components/StudentWaitPage/studentWaitPage.css 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 68ef7bd..6670240 100644 --- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx +++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx @@ -2,20 +2,20 @@ 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 { UserType } from '../../../Types/UserType'; +import { StudentType, Answer } from '../../../Types/StudentType'; import { Button } from '@mui/material'; import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle'; import { Refresh, Error } from '@mui/icons-material'; -import UserWaitPage from '../../../components/UserWaitPage/UserWaitPage'; +import StudentWaitPage from '../../../components/StudentWaitPage/StudentWaitPage'; import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton'; import QuestionNavigation from '../../../components/QuestionNavigation/QuestionNavigation'; import Question from '../../../components/Questions/Question'; @@ -25,7 +25,7 @@ const ManageRoom: React.FC = () => { const navigate = useNavigate(); const [roomName, setRoomName] = useState(''); const [socket, setSocket] = useState(null); - const [users, setUsers] = useState([]); + const [students, setStudents] = useState([]); const quizId = useParams<{ id: string }>(); const [quizQuestions, setQuizQuestions] = useState(); const [quiz, setQuiz] = useState(null); @@ -74,7 +74,7 @@ const ManageRoom: React.FC = () => { setSocket(null); setQuizQuestions(undefined); setCurrentQuestion(undefined); - setUsers([]); + setStudents(new Array()); setRoomName(''); } }; @@ -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(); }); @@ -96,9 +96,10 @@ const ManageRoom: React.FC = () => { socket.on('create-failure', () => { console.log('Error creating room.'); }); - socket.on('user-joined', (user: UserType) => { - - setUsers((prevUsers) => [...prevUsers, user]); + socket.on('user-joined', (student: StudentType) => { + console.log(`Student joined: name = ${student.name}, id = ${student.id}`); + + setStudents((prevStudents) => [...prevStudents, student]); if (quizMode === 'teacher') { webSocketService.nextQuestion(roomName, currentQuestion); @@ -111,7 +112,8 @@ const ManageRoom: React.FC = () => { setSocket(null); }); socket.on('user-disconnected', (userId: string) => { - setUsers((prevUsers) => prevUsers.filter((user) => user.id !== userId)); + console.log(`Student left: id = ${userId}`); + setStudents((prevUsers) => prevUsers.filter((user) => user.id !== userId)); }); setSocket(socket); }; @@ -119,10 +121,9 @@ const ManageRoom: React.FC = () => { useEffect(() => { // This is here to make sure the correct value is sent when user join if (socket) { - socket.on('user-joined', (user: UserType) => { - - setUsers((prevUsers) => [...prevUsers, user]); - + console.log(`Listening for user-joined in room ${roomName}`); + socket.on('user-joined', (_student: StudentType) => { + if (quizMode === 'teacher') { webSocketService.nextQuestion(roomName, currentQuestion); } else if (quizMode === 'student') { @@ -130,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; @@ -171,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]); @@ -180,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) { @@ -205,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]); } @@ -217,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 (
@@ -250,13 +441,13 @@ const ManageRoom: React.FC = () => {
Salle: {roomName}
-
Utilisateurs: {users.length}/60
+
Utilisateurs: {students.length}/60
-{/* the following breaks the css (nested room classes) */} + {/* the following breaks the css (if 'room' classes are nested) */}
{quizQuestions ? ( @@ -293,7 +484,7 @@ const ManageRoom: React.FC = () => { socket={socket} questions={quizQuestions} showSelectedQuestion={showSelectedQuestion} - students={users} + students={students} >
@@ -311,8 +502,8 @@ const ManageRoom: React.FC = () => { ) : ( - 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 0d63ab2..5efe1fe 100644 --- a/server/socket/socket.js +++ b/server/socket/socket.js @@ -49,10 +49,15 @@ const setupWebsocket = (io) => { io.sockets.adapter.rooms.get(enteredRoomName).size; if (clientsInRoom <= MAX_USERS_PER_ROOM) { + const newStudent = { + id: socket.id, + name: username, + answers: [], + }; socket.join(enteredRoomName); socket .to(enteredRoomName) - .emit("user-joined", { name: username, id: socket.id }); + .emit("user-joined", newStudent); socket.emit("join-success"); } else { socket.emit("join-failure", "La salle est remplie"); @@ -96,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,