From fe44409e16b12e588dab5b22a4e62e3d6d42241c Mon Sep 17 00:00:00 2001 From: JubaAzul <118773284+JubaAzul@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:00:42 -0500 Subject: [PATCH 01/19] =?UTF-8?q?R=C3=A9ponse=20=C3=A0=20une=20question=20?= =?UTF-8?q?non=20enregistr=C3=A9e=20lorsque=20=C3=89tudiant=20reviens=20en?= =?UTF-8?q?=20arri=C3=A8re=20dans=20le=20quiz=20Fixes=20#200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StudentModeQuiz/StudentModeQuiz.tsx | 71 +++++++++++-------- .../TeacherModeQuiz/TeacherModeQuiz.tsx | 13 ++-- .../src/pages/Student/JoinRoom/JoinRoom.tsx | 4 +- server/socket/socket.js | 1 - 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx index eb70432..137c872 100644 --- a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx +++ b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx @@ -21,20 +21,33 @@ const StudentModeQuiz: React.FC = ({ submitAnswer, disconnectWebSocket }) => { + //Ajouter type AnswerQuestionType en remplacement de QuestionType const [questionInfos, setQuestion] = useState(questions[0]); const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false); // const [imageUrl, setImageUrl] = useState(''); - // const previousQuestion = () => { - // setQuestion(questions[Number(questionInfos.question?.id) - 2]); - // setIsAnswerSubmitted(false); - // }; + const previousQuestion = () => { + setQuestion(questions[Number(questionInfos.question?.id) - 2]); + setIsAnswerSubmitted(false); + + }; - useEffect(() => {}, [questionInfos]); + + + useEffect(() => { + const answer = localStorage.getItem(`Answer${questionInfos.question.id}`); + if (answer !== null) { + setIsAnswerSubmitted(true); + } else { + setIsAnswerSubmitted(false); + + } + }, [questionInfos.question]); const nextQuestion = () => { setQuestion(questions[Number(questionInfos.question?.id)]); setIsAnswerSubmitted(false); + }; const handleOnSubmitAnswer = (answer: string | number | boolean) => { @@ -46,11 +59,13 @@ const StudentModeQuiz: React.FC = ({ return (
- +
+
+ Question {questionInfos.question.id}/{questions.length}
@@ -67,30 +82,28 @@ const StudentModeQuiz: React.FC = ({ question={questionInfos.question as Question} showAnswer={isAnswerSubmitted} /> -
-
- {/* */} -
-
- -
+
+
+
+
+ +
+
diff --git a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx index 3188211..c28e708 100644 --- a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx +++ b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx @@ -1,11 +1,8 @@ // TeacherModeQuiz.tsx import React, { useEffect, useState } from 'react'; - import QuestionComponent from '../QuestionsDisplay/QuestionDisplay'; - import '../../pages/Student/JoinRoom/joinRoom.css'; import { QuestionType } from '../../Types/QuestionType'; -// import { QuestionService } from '../../services/QuestionService'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material'; import { Question } from 'gift-pegjs'; @@ -24,7 +21,7 @@ const TeacherModeQuiz: React.FC = ({ const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false); const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); const [feedbackMessage, setFeedbackMessage] = useState(''); - + const renderFeedbackMessage = (answer: string) => { if(answer === 'true' || answer === 'false'){ @@ -43,12 +40,18 @@ const TeacherModeQuiz: React.FC = ({ // Close the feedback dialog when the question changes handleFeedbackDialogClose(); setIsAnswerSubmitted(false); - + const answer = localStorage.getItem(`Answer${questionInfos.question.id}`); + if (answer !== null) { + setIsAnswerSubmitted(true); + setIsFeedbackDialogOpen(true); + } + }, [questionInfos.question]); const handleOnSubmitAnswer = (answer: string | number | boolean) => { const idQuestion = Number(questionInfos.question.id) || -1; submitAnswer(answer, idQuestion); + setFeedbackMessage(renderFeedbackMessage(answer.toString())); setIsFeedbackDialogOpen(true); }; diff --git a/client/src/pages/Student/JoinRoom/JoinRoom.tsx b/client/src/pages/Student/JoinRoom/JoinRoom.tsx index f0ac8d7..94ffced 100644 --- a/client/src/pages/Student/JoinRoom/JoinRoom.tsx +++ b/client/src/pages/Student/JoinRoom/JoinRoom.tsx @@ -45,6 +45,7 @@ const JoinRoom: React.FC = () => { socket.on('next-question', (question: QuestionType) => { setQuizMode('teacher'); setIsWaitingForTeacher(false); + setQuestion(question); }); socket.on('launch-student-mode', (questions: QuestionType[]) => { @@ -78,6 +79,7 @@ const JoinRoom: React.FC = () => { }; const disconnect = () => { + localStorage.clear(); webSocketService.disconnect(); setSocket(null); setQuestion(undefined); @@ -107,7 +109,7 @@ const JoinRoom: React.FC = () => { username: username, idQuestion: idQuestion }; - + localStorage.setItem(`Answer${idQuestion}`, JSON.stringify(answer)); webSocketService.submitAnswer(answerData); }; diff --git a/server/socket/socket.js b/server/socket/socket.js index fc21632..13f882c 100644 --- a/server/socket/socket.js +++ b/server/socket/socket.js @@ -68,7 +68,6 @@ const setupWebsocket = (io) => { }); socket.on("next-question", ({ roomName, question }) => { - // console.log("next-question", roomName, question); socket.to(roomName).emit("next-question", question); }); From 60ad2df67d386ca322cc7fb647247db1c84545d6 Mon Sep 17 00:00:00 2001 From: JubaAzul <118773284+JubaAzul@users.noreply.github.com> Date: Tue, 4 Mar 2025 16:49:12 -0500 Subject: [PATCH 02/19] =?UTF-8?q?R=C3=A9ponse=20=C3=A0=20une=20question=20?= =?UTF-8?q?non=20enregistr=C3=A9e=20lorsque=20=C3=89tudiant=20reviens=20en?= =?UTF-8?q?=20arri=C3=A8re=20dans=20le=20quiz=20Fixes=20#200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MultipleChoiceQuestionDisplay.tsx | 17 ++++++----- .../QuestionsDisplay/QuestionDisplay.tsx | 4 +++ .../TrueFalseQuestionDisplay.tsx | 28 +++++++++++++------ .../StudentModeQuiz/StudentModeQuiz.tsx | 3 +- .../TeacherModeQuiz/TeacherModeQuiz.tsx | 11 ++++++-- 5 files changed, 40 insertions(+), 23 deletions(-) diff --git a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx index e5e7b6b..790848f 100644 --- a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx @@ -1,5 +1,5 @@ // MultipleChoiceQuestionDisplay.tsx -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import '../questionStyle.css'; import { Button } from '@mui/material'; import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate'; @@ -7,22 +7,21 @@ import { MultipleChoiceQuestion } from 'gift-pegjs'; interface Props { question: MultipleChoiceQuestion; - handleOnSubmitAnswer?: (answer: string) => void; + handleOnSubmitAnswer?: (answer: string | number | boolean) => void; showAnswer?: boolean; + passedAnswer?: string | number | boolean; } const MultipleChoiceQuestionDisplay: React.FC = (props) => { - const { question, showAnswer, handleOnSubmitAnswer } = props; - const [answer, setAnswer] = useState(); + const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; + const [answer, setAnswer] = useState(passedAnswer || ''); - useEffect(() => { - setAnswer(undefined); - }, [question]); const handleOnClickAnswer = (choice: string) => { setAnswer(choice); }; + const alpha = Array.from(Array(26)).map((_e, i) => i + 65); const alphabet = alpha.map((x) => String.fromCharCode(x)); return ( @@ -67,9 +66,9 @@ const MultipleChoiceQuestionDisplay: React.FC = (props) => {
{showAnswer ? ( <> -
{correctAnswer}
+
+ La bonne réponse est: + {correctAnswer}
+ + Votre réponse est: {answer.toString()} + {question.formattedGlobalFeedback &&
} + ) : ( <> @@ -75,7 +86,7 @@ const NumericalQuestionDisplay: React.FC = (props) => { handleOnSubmitAnswer && handleOnSubmitAnswer(answer) } - disabled={answer === undefined || isNaN(answer)} + disabled={answer === "" || isNaN(answer as number)} > Répondre diff --git a/client/src/components/QuestionsDisplay/QuestionDisplay.tsx b/client/src/components/QuestionsDisplay/QuestionDisplay.tsx index 4ec3312..a1f0f30 100644 --- a/client/src/components/QuestionsDisplay/QuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/QuestionDisplay.tsx @@ -12,6 +12,7 @@ interface QuestionProps { handleOnSubmitAnswer?: (answer: string | number | boolean) => void; showAnswer?: boolean; answer?: string | number | boolean; + } const QuestionDisplay: React.FC = ({ question, @@ -37,6 +38,7 @@ const QuestionDisplay: React.FC = ({ ); break; case 'MC': + questionTypeComponent = ( = ({ break; case 'Numerical': if (question.choices) { - if (!Array.isArray(question.choices)) { questionTypeComponent = ( ); - } else { - questionTypeComponent = ( // TODO fix NumericalQuestion (correctAnswers is borked) - - ); - } } break; case 'Short': @@ -73,6 +67,7 @@ const QuestionDisplay: React.FC = ({ question={question} handleOnSubmitAnswer={handleOnSubmitAnswer} showAnswer={showAnswer} + passedAnswer={answer} /> ); break; diff --git a/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx index 50c2261..f415679 100644 --- a/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import '../questionStyle.css'; import { Button, TextField } from '@mui/material'; import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate'; @@ -6,14 +6,22 @@ import { ShortAnswerQuestion } from 'gift-pegjs'; interface Props { question: ShortAnswerQuestion; - handleOnSubmitAnswer?: (answer: string) => void; + handleOnSubmitAnswer?: (answer: string | number | boolean) => void; showAnswer?: boolean; + passedAnswer?: string | number | boolean; + } const ShortAnswerQuestionDisplay: React.FC = (props) => { - const { question, showAnswer, handleOnSubmitAnswer } = props; - const [answer, setAnswer] = useState(); - + const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; + const [answer, setAnswer] = useState(passedAnswer || ' '); + + useEffect(() => { + if (passedAnswer !== undefined) { + setAnswer(passedAnswer); + } + }, [passedAnswer]); + return (
@@ -22,11 +30,18 @@ const ShortAnswerQuestionDisplay: React.FC = (props) => { {showAnswer ? ( <>
+ + La bonne réponse est: + {question.choices.map((choice) => (
{choice.text}
))} +
+ + Votre réponse est: {answer} +
{question.formattedGlobalFeedback &&
@@ -54,7 +69,7 @@ const ShortAnswerQuestionDisplay: React.FC = (props) => { handleOnSubmitAnswer && handleOnSubmitAnswer(answer) } - disabled={answer === undefined || answer === ''} + disabled={answer === null || answer === ''} > Répondre diff --git a/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx index 3adc3b8..599888b 100644 --- a/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx @@ -1,14 +1,13 @@ // TrueFalseQuestion.tsx -import React, { useState } from 'react'; +import React, { useState,useEffect } from 'react'; import '../questionStyle.css'; import { Button } from '@mui/material'; import { TrueFalseQuestion } from 'gift-pegjs'; import { FormattedTextTemplate } from 'src/components/GiftTemplate/templates/TextTypeTemplate'; -import { Answer } from 'src/Types/StudentType'; interface Props { question: TrueFalseQuestion; - handleOnSubmitAnswer?: (answer: Answer) => void; + handleOnSubmitAnswer?: (answer: string | number | boolean) => void; showAnswer?: boolean; passedAnswer?: string | number | boolean; } @@ -16,12 +15,26 @@ interface Props { const TrueFalseQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer} = props; - console.log("Passedanswer", passedAnswer); + + let disableButton = false; + if(handleOnSubmitAnswer === undefined){ + disableButton = true; + } + + useEffect(() => { + if (passedAnswer === true || passedAnswer === false) { + setAnswer(passedAnswer); + } else { + setAnswer(undefined); + } + }, [passedAnswer]); const [answer, setAnswer] = useState(() => { - if (typeof passedAnswer === 'boolean') { + + if (passedAnswer === true || passedAnswer === false) { return passedAnswer; } + return undefined; }); @@ -29,7 +42,6 @@ const TrueFalseQuestionDisplay: React.FC = (props) => { setAnswer(choice); }; - console.log("Answer", answer); const selectedTrue = answer ? 'selected' : ''; const selectedFalse = answer !== undefined && !answer ? 'selected' : ''; return ( @@ -42,33 +54,36 @@ const TrueFalseQuestionDisplay: React.FC = (props) => { className="button-wrapper" onClick={() => !showAnswer && handleOnClickAnswer(true)} fullWidth + disabled={disableButton} > {showAnswer? (
{(question.isTrue ? '✅' : '❌')}
):``}
V
Vrai
+ + {showAnswer && answer && question.trueFormattedFeedback && ( +
+
+
+ )}
- {/* selected TRUE, show True feedback if it exists */} - {showAnswer && answer && question.trueFormattedFeedback && ( -
-
-
- )} - {/* selected FALSE, show False feedback if it exists */} - {showAnswer && !answer && question.falseFormattedFeedback && ( -
-
-
- )} {question.formattedGlobalFeedback && showAnswer && (
diff --git a/client/src/components/QuestionsDisplay/questionStyle.css b/client/src/components/QuestionsDisplay/questionStyle.css index cdf611f..f300ba2 100644 --- a/client/src/components/QuestionsDisplay/questionStyle.css +++ b/client/src/components/QuestionsDisplay/questionStyle.css @@ -147,6 +147,25 @@ box-shadow: 0px 2px 5px hsl(0, 0%, 74%); } +.true-feedback { + position: relative; + padding: 0 1rem; + background-color: hsl(43, 100%, 94%); + color: hsl(43, 95%, 9%); + border: hsl(36, 84%, 93%) 1px solid; + border-radius: 6px; + box-shadow: 0px 2px 5px hsl(0, 0%, 74%); +} +.false-feedback { + position: relative; + padding: 0 1rem; + background-color: hsl(43, 100%, 94%); + color: hsl(43, 95%, 9%); + border: hsl(36, 84%, 93%) 1px solid; + border-radius: 6px; + box-shadow: 0px 2px 5px hsl(0, 0%, 74%); +} + .choices-wrapper { width: 90%; } diff --git a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx index 4af330e..d10411f 100644 --- a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx +++ b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx @@ -3,10 +3,8 @@ import React, { useEffect, useState } from 'react'; import QuestionComponent from '../QuestionsDisplay/QuestionDisplay'; import '../../pages/Student/JoinRoom/joinRoom.css'; import { QuestionType } from '../../Types/QuestionType'; -// import { QuestionService } from '../../services/QuestionService'; import { Button } from '@mui/material'; //import QuestionNavigation from '../QuestionNavigation/QuestionNavigation'; -//import { ChevronLeft, ChevronRight } from '@mui/icons-material'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton'; import { Question } from 'gift-pegjs'; @@ -24,28 +22,24 @@ const StudentModeQuiz: React.FC = ({ //Ajouter type AnswerQuestionType en remplacement de QuestionType const [questionInfos, setQuestion] = useState(questions[0]); const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false); - // const [imageUrl, setImageUrl] = useState(''); + const [answer, setAnswer] = useState(''); + const previousQuestion = () => { - setQuestion(questions[Number(questionInfos.question?.id) - 2]); - setIsAnswerSubmitted(false); - + setQuestion(questions[Number(questionInfos.question?.id) - 2]); }; useEffect(() => { - const answer = localStorage.getItem(`Answer${questionInfos.question.id}`); + setAnswer(JSON.parse(localStorage.getItem(`Answer${questionInfos.question.id}`)||'null')); if (answer !== null) { setIsAnswerSubmitted(true); } else { setIsAnswerSubmitted(false); - } - }, [questionInfos.question]); + }, [questionInfos.question , answer]); const nextQuestion = () => { setQuestion(questions[Number(questionInfos.question?.id)]); - setIsAnswerSubmitted(false); - }; const handleOnSubmitAnswer = (answer: string | number | boolean) => { @@ -79,7 +73,7 @@ const StudentModeQuiz: React.FC = ({ handleOnSubmitAnswer={handleOnSubmitAnswer} question={questionInfos.question as Question} showAnswer={isAnswerSubmitted} - answer={localStorage.getItem(`Answer${questionInfos.question.id}`) || undefined} + answer={answer} />
diff --git a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx index 9ee0537..456b227 100644 --- a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx +++ b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx @@ -20,30 +20,16 @@ const TeacherModeQuiz: React.FC = ({ }) => { const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false); const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); - const [feedbackMessage, setFeedbackMessage] = useState(''); const [answer, setAnswer] = useState(''); - const renderFeedbackMessage = (answer: string) => { - if(answer === 'true' || answer === 'false'){ - return ( - Votre réponse est: {answer==="true" ? 'Vrai' : 'Faux'} - ) - } - else{ - return ( - - Votre réponse est: {answer.toString()} - - );} - }; useEffect(() => { // Close the feedback dialog when the question changes handleFeedbackDialogClose(); setIsAnswerSubmitted(false); setAnswer(JSON.parse(localStorage.getItem(`Answer${questionInfos.question.id}`)||'null')); - if (answer) { + if (typeof answer !== "object") { setIsAnswerSubmitted(true); setIsFeedbackDialogOpen(true); } @@ -53,8 +39,7 @@ const TeacherModeQuiz: React.FC = ({ const handleOnSubmitAnswer = (answer: string | number | boolean) => { const idQuestion = Number(questionInfos.question.id) || -1; submitAnswer(answer, idQuestion); - - setFeedbackMessage(renderFeedbackMessage(answer.toString())); + setAnswer(answer); setIsFeedbackDialogOpen(true); }; @@ -103,7 +88,6 @@ const TeacherModeQuiz: React.FC = ({ maxHeight: '400px', overflowY: 'auto', }}> - {feedbackMessage}
Question :
diff --git a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx index 7af12f9..f63b60a 100644 --- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx +++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx @@ -435,8 +435,6 @@ const ManageRoom: React.FC = () => { message={`Êtes-vous sûr de vouloir quitter?`} /> - -
Salle: {roomName}
@@ -485,6 +483,7 @@ const ManageRoom: React.FC = () => { )} From 6a340556e21231e9c27ac26c2496f7b27c92eed7 Mon Sep 17 00:00:00 2001 From: JubaAzul <118773284+JubaAzul@users.noreply.github.com> Date: Fri, 7 Mar 2025 12:20:39 -0500 Subject: [PATCH 08/19] =?UTF-8?q?Am=C3=A9lioration=20de=20beaucoup=20de=20?= =?UTF-8?q?features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx | 6 ++---- .../MultipleChoiceQuestionDisplay.tsx | 7 +++---- client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx | 1 + 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx index 0332f1a..6734114 100644 --- a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx +++ b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx @@ -4,13 +4,11 @@ import { render, fireEvent, act } from '@testing-library/react'; import { screen } from '@testing-library/dom'; import '@testing-library/jest-dom'; import { MultipleChoiceQuestion, parse } from 'gift-pegjs'; - import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz'; import { MemoryRouter } from 'react-router-dom'; -// import { mock } from 'node:test'; const mockGiftQuestions = parse( - `::Sample Question:: Sample Question {=Option A ~Option B}`); + `::Question:: Sample Question {=Option A ~Option B}`); describe('TeacherModeQuiz', () => { @@ -36,6 +34,7 @@ describe('TeacherModeQuiz', () => { }); test('renders the initial question', () => { + expect(screen.getByText('Question 1')).toBeInTheDocument(); expect(screen.getByText('Sample Question')).toBeInTheDocument(); expect(screen.getByText('Option A')).toBeInTheDocument(); @@ -53,7 +52,6 @@ describe('TeacherModeQuiz', () => { fireEvent.click(screen.getByText('Répondre')); }); expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); - expect(screen.getByText('Votre réponse est:')).toBeInTheDocument(); }); test('handles disconnect button click', () => { diff --git a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx index c8e8f69..c3658c2 100644 --- a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx @@ -14,7 +14,7 @@ interface Props { const MultipleChoiceQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; - const [answer, setAnswer] = useState(passedAnswer || ' '); + const [answer, setAnswer] = useState(passedAnswer || ''); let disableButton = false; @@ -33,8 +33,8 @@ const MultipleChoiceQuestionDisplay: React.FC = (props) => { }; const alpha = Array.from(Array(26)).map((_e, i) => i + 65); const alphabet = alpha.map((x) => String.fromCharCode(x)); - return ( +
@@ -43,7 +43,6 @@ const MultipleChoiceQuestionDisplay: React.FC = (props) => { {question.choices.map((choice, i) => { const selected = answer === choice.formattedText.text ? 'selected' : ''; - console.log("dsa", selected) return (
-
+
); }; diff --git a/client/src/pages/Student/JoinRoom/JoinRoom.tsx b/client/src/pages/Student/JoinRoom/JoinRoom.tsx index a196ef8..dc7e80c 100644 --- a/client/src/pages/Student/JoinRoom/JoinRoom.tsx +++ b/client/src/pages/Student/JoinRoom/JoinRoom.tsx @@ -17,6 +17,8 @@ import LoginContainer from 'src/components/LoginContainer/LoginContainer' import ApiService from '../../../services/ApiService' +export type AnswerType = string | number | boolean; + const JoinRoom: React.FC = () => { const [roomName, setRoomName] = useState(''); const [username, setUsername] = useState(ApiService.getUsername()); @@ -25,6 +27,7 @@ const JoinRoom: React.FC = () => { const [question, setQuestion] = useState(); const [quizMode, setQuizMode] = useState(); const [questions, setQuestions] = useState([]); + const [answers, setAnswers] = useState([]); const [connectionError, setConnectionError] = useState(''); const [isConnecting, setIsConnecting] = useState(false); @@ -35,6 +38,13 @@ const JoinRoom: React.FC = () => { }; }, []); + useEffect(() => { + // init the answers array, one for each question + setAnswers(Array(questions.length).fill({} as AnswerSubmissionToBackendType)); + console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`); + }, [questions]); + + const handleCreateSocket = () => { console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`); const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); @@ -45,12 +55,18 @@ const JoinRoom: React.FC = () => { console.log(`on(join-success): Successfully joined the room ${roomJoinedName}`); }); socket.on('next-question', (question: QuestionType) => { - console.log('on(next-question): Received next-question:', question); + console.log('JoinRoom: on(next-question): Received next-question:', question); setQuizMode('teacher'); setIsWaitingForTeacher(false); - setQuestion(question); }); + socket.on('launch-teacher-mode', (questions: QuestionType[]) => { + console.log('on(launch-teacher-mode): Received launch-teacher-mode:', questions); + setQuizMode('teacher'); + setIsWaitingForTeacher(true); + setQuestions(questions); + // wait for next-question + }); socket.on('launch-student-mode', (questions: QuestionType[]) => { console.log('on(launch-student-mode): Received launch-student-mode:', questions); @@ -84,7 +100,7 @@ const JoinRoom: React.FC = () => { }; const disconnect = () => { - localStorage.clear(); +// localStorage.clear(); webSocketService.disconnect(); setSocket(null); setQuestion(undefined); @@ -109,14 +125,22 @@ const JoinRoom: React.FC = () => { } }; - const handleOnSubmitAnswer = (answer: string | number | boolean, idQuestion: number) => { + const handleOnSubmitAnswer = (answer: AnswerType, idQuestion: number) => { + console.info(`JoinRoom: handleOnSubmitAnswer: answer: ${answer}, idQuestion: ${idQuestion}`); const answerData: AnswerSubmissionToBackendType = { roomName: roomName, answer: answer, username: username, idQuestion: idQuestion }; - localStorage.setItem(`Answer${idQuestion}`, JSON.stringify(answer)); + // localStorage.setItem(`Answer${idQuestion}`, JSON.stringify(answer)); + setAnswers((prevAnswers) => { + console.log(`JoinRoom: handleOnSubmitAnswer: prevAnswers: ${JSON.stringify(prevAnswers)}`); + const newAnswers = [...prevAnswers]; // Create a copy of the previous answers array + newAnswers[idQuestion - 1] = answerData; // Update the specific answer + return newAnswers; // Return the new array + }); + console.log(`JoinRoom: handleOnSubmitAnswer: answers: ${JSON.stringify(answers)}`); webSocketService.submitAnswer(answerData); }; @@ -154,6 +178,7 @@ const JoinRoom: React.FC = () => { return ( @@ -163,6 +188,7 @@ const JoinRoom: React.FC = () => { question && ( diff --git a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx index 05cc340..411567c 100644 --- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx +++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx @@ -131,7 +131,11 @@ const ManageRoom: React.FC = () => { if (!quizStarted) return; if (quizMode === 'teacher') { - webSocketService.nextQuestion(formattedRoomName, currentQuestion); + webSocketService.nextQuestion( + {roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: Number(currentQuestion?.question.id) - 1, + isLaunch: false}); } else if (quizMode === 'student') { webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions); } @@ -224,7 +228,10 @@ const ManageRoom: React.FC = () => { if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return; setCurrentQuestion(quizQuestions[nextQuestionIndex]); - webSocketService.nextQuestion(formattedRoomName, quizQuestions[nextQuestionIndex]); + webSocketService.nextQuestion({roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: nextQuestionIndex, + isLaunch: false}); }; const previousQuestion = () => { @@ -234,7 +241,7 @@ const ManageRoom: React.FC = () => { if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return; setCurrentQuestion(quizQuestions[prevQuestionIndex]); - webSocketService.nextQuestion(formattedRoomName, quizQuestions[prevQuestionIndex]); + webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: prevQuestionIndex, isLaunch: false}); }; const initializeQuizQuestion = () => { @@ -262,7 +269,7 @@ const ManageRoom: React.FC = () => { } setCurrentQuestion(quizQuestions[0]); - webSocketService.nextQuestion(formattedRoomName, quizQuestions[0]); + webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: 0, isLaunch: true}); }; const launchStudentMode = () => { @@ -300,9 +307,8 @@ const ManageRoom: React.FC = () => { const showSelectedQuestion = (questionIndex: number) => { if (quiz?.content && quizQuestions) { setCurrentQuestion(quizQuestions[questionIndex]); - if (quizMode === 'teacher') { - webSocketService.nextQuestion(formattedRoomName, quizQuestions[questionIndex]); + webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex, isLaunch: false}); } } }; diff --git a/client/src/services/WebsocketService.tsx b/client/src/services/WebsocketService.tsx index 3cacf36..212aa21 100644 --- a/client/src/services/WebsocketService.tsx +++ b/client/src/services/WebsocketService.tsx @@ -1,4 +1,5 @@ import { io, Socket } from 'socket.io-client'; +import { QuestionType } from 'src/Types/QuestionType'; // Must (manually) sync these types to server/socket/socket.js @@ -59,12 +60,19 @@ class WebSocketService { // } // } - nextQuestion(roomName: string, question: unknown) { - console.log('WebsocketService: nextQuestion', roomName, question); - if (!question) { + nextQuestion(args: {roomName: string, questions: QuestionType[] | undefined, questionIndex: number, isLaunch: boolean}) { + // deconstruct args + const { roomName, questions, questionIndex, isLaunch } = args; + console.log('WebsocketService: nextQuestion', roomName, questions, questionIndex, isLaunch); + if (!questions || !questions[questionIndex]) { throw new Error('WebsocketService: nextQuestion: question is null'); } + if (this.socket) { + if (isLaunch) { + this.socket.emit('launch-teacher-mode', { roomName, questions }); + } + const question = questions[questionIndex]; this.socket.emit('next-question', { roomName, question }); } } diff --git a/server/__tests__/socket.test.js b/server/__tests__/socket.test.js index 739d79d..2d84da4 100644 --- a/server/__tests__/socket.test.js +++ b/server/__tests__/socket.test.js @@ -109,17 +109,29 @@ describe("websocket server", () => { }); }); - test("should send next question", (done) => { - studentSocket.on("next-question", (question) => { - expect(question).toEqual({ question: "question2" }); + test("should launch teacher mode", (done) => { + studentSocket.on("launch-teacher-mode", (questions) => { + expect(questions).toEqual([ + { question: "question1" }, + { question: "question2" }, + ]); done(); }); - teacherSocket.emit("next-question", { + teacherSocket.emit("launch-teacher-mode", { roomName: "ROOM1", - question: { question: "question2" }, + questions: [{ question: "question1" }, { question: "question2" }], }); }); + test("should send next question", (done) => { + studentSocket.on("next-question", ( question ) => { + expect(question).toBe("question2"); + done(); + }); + teacherSocket.emit("next-question", { roomName: "ROOM1", question: 'question2'}, + ); + }); + test("should send answer", (done) => { teacherSocket.on("submit-answer-room", (answer) => { expect(answer).toEqual({ diff --git a/server/socket/socket.js b/server/socket/socket.js index 393135c..0adaf92 100644 --- a/server/socket/socket.js +++ b/server/socket/socket.js @@ -81,6 +81,10 @@ const setupWebsocket = (io) => { socket.to(roomName).emit("next-question", question); }); + socket.on("launch-teacher-mode", ({ roomName, questions }) => { + socket.to(roomName).emit("launch-teacher-mode", questions); + }); + socket.on("launch-student-mode", ({ roomName, questions }) => { socket.to(roomName).emit("launch-student-mode", questions); }); From 73e0326f44d371d5a02d7ed612979b783b78660c Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Sat, 8 Mar 2025 02:31:55 -0500 Subject: [PATCH 15/19] =?UTF-8?q?tests=20am=C3=A9lior=C3=A9s=20(toujours?= =?UTF-8?q?=20pas=20id=C3=A9al)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StudentModeQuiz/StudentModeQuiz.test.tsx | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx b/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx index 4a670a3..1218694 100644 --- a/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx +++ b/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'; +import { render, screen, fireEvent, act } from '@testing-library/react'; import '@testing-library/jest-dom'; import { MemoryRouter } from 'react-router-dom'; import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz'; @@ -55,13 +55,9 @@ describe('StudentModeQuiz', () => { }); expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); - - // await waitFor(() => { - // expect(localStorage.getItem('Answer1')).toBe(JSON.stringify('Option A')); - // }); }); - test.skip('handles shows feedback for an already answered question', async () => { + test('handles shows feedback for an already answered question', async () => { // Answer the first question act(() => { fireEvent.click(screen.getByText('Option A')); @@ -71,17 +67,13 @@ describe('StudentModeQuiz', () => { }); expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); - await waitFor(() => { - expect(localStorage.getItem('Answer1')).toBe(JSON.stringify('Option A')); - }); + const firstButtonA = screen.getByRole("button", {name: '✅ A Option A'}); + expect(firstButtonA).toBeInTheDocument(); + expect(firstButtonA.querySelector('.selected')).toBeInTheDocument(); + expect(screen.getByRole("button", {name: '❌ B Option B'})).toBeInTheDocument(); expect(screen.queryByText('Répondre')).not.toBeInTheDocument(); - // Simulate feedback display (e.g., a checkmark or feedback message) - // This part depends on how feedback is displayed in your component - // For example, if you display a checkmark, you can check for it: - expect(screen.getByText('✅')).toBeInTheDocument(); - // Navigate to the next question act(() => { fireEvent.click(screen.getByText('Question suivante')); @@ -93,9 +85,19 @@ describe('StudentModeQuiz', () => { act(() => { fireEvent.click(screen.getByText('Question précédente')); }); - expect(screen.getByText('Sample Question 1')).toBeInTheDocument(); - // Check if feedback is shown again - expect(screen.getByText('✅')).toBeInTheDocument(); + expect(await screen.findByText('Sample Question 1')).toBeInTheDocument(); + + // Since answers are mocked, the it doesn't recognize the question as already answered + // TODO these tests are partially faked, need to be fixed if we can mock the answers + // const buttonA = screen.getByRole("button", {name: '✅ A Option A'}); + const buttonA = screen.getByRole("button", {name: 'A Option A'}); + expect(buttonA).toBeInTheDocument(); + // const buttonB = screen.getByRole("button", {name: '❌ B Option B'}); + const buttonB = screen.getByRole("button", {name: 'B Option B'}); + expect(buttonB).toBeInTheDocument(); + // // "Option A" div inside the name of button should have selected class + // expect(buttonA.querySelector('.selected')).toBeInTheDocument(); + }); test('handles quit button click', async () => { From 623b749e4ffc533cb069a98d49afcb0469c09e9a Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Sat, 8 Mar 2025 11:05:25 -0500 Subject: [PATCH 16/19] refactor AnswerType --- client/src/Types/StudentType.tsx | 4 +++- .../MultipleChoiceQuestionDisplay.test.tsx | 3 ++- .../TrueFalseQuestionDisplay.test.tsx | 3 ++- .../MultipleChoiceQuestionDisplay.tsx | 7 ++++--- .../NumericalQuestionDisplay/NumericalQuestionDisplay.tsx | 7 ++++--- client/src/components/QuestionsDisplay/QuestionDisplay.tsx | 5 +++-- .../ShortAnswerQuestionDisplay.tsx | 7 ++++--- .../TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx | 5 +++-- client/src/components/StudentModeQuiz/StudentModeQuiz.tsx | 7 ++++--- client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx | 4 ++-- client/src/pages/Teacher/ManageRoom/ManageRoom.tsx | 3 ++- client/src/services/WebsocketService.tsx | 5 +++-- 12 files changed, 36 insertions(+), 24 deletions(-) diff --git a/client/src/Types/StudentType.tsx b/client/src/Types/StudentType.tsx index b484af5..41a4a63 100644 --- a/client/src/Types/StudentType.tsx +++ b/client/src/Types/StudentType.tsx @@ -1,5 +1,7 @@ +import { AnswerType } from "src/pages/Student/JoinRoom/JoinRoom"; + export interface Answer { - answer: string | number | boolean; + answer: AnswerType; isCorrect: boolean; idQuestion: number; } diff --git a/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx b/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx index 018d96d..8751e8b 100644 --- a/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx +++ b/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx @@ -5,6 +5,7 @@ import { act } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { MultipleChoiceQuestion, parse } from 'gift-pegjs'; import MultipleChoiceQuestionDisplay from 'src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; const questions = parse( `::Sample Question 1:: Question stem @@ -21,7 +22,7 @@ describe('MultipleChoiceQuestionDisplay', () => { const TestWrapper = ({ showAnswer }: { showAnswer: boolean }) => { const [showAnswerState, setShowAnswerState] = useState(showAnswer); - const handleOnSubmitAnswer = (answer: string | number | boolean) => { + const handleOnSubmitAnswer = (answer: AnswerType) => { mockHandleOnSubmitAnswer(answer); setShowAnswerState(true); }; diff --git a/client/src/__tests__/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.test.tsx b/client/src/__tests__/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.test.tsx index f640710..e6910d4 100644 --- a/client/src/__tests__/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.test.tsx +++ b/client/src/__tests__/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.test.tsx @@ -5,6 +5,7 @@ import '@testing-library/jest-dom'; import { MemoryRouter } from 'react-router-dom'; import TrueFalseQuestionDisplay from 'src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay'; import { parse, TrueFalseQuestion } from 'gift-pegjs'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; describe('TrueFalseQuestion Component', () => { const mockHandleSubmitAnswer = jest.fn(); @@ -16,7 +17,7 @@ describe('TrueFalseQuestion Component', () => { const TestWrapper = ({ showAnswer }: { showAnswer: boolean }) => { const [showAnswerState, setShowAnswerState] = useState(showAnswer); - const handleOnSubmitAnswer = (answer: string | number | boolean) => { + const handleOnSubmitAnswer = (answer: AnswerType) => { mockHandleSubmitAnswer(answer); setShowAnswerState(true); }; diff --git a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx index c3658c2..df46193 100644 --- a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx @@ -4,17 +4,18 @@ import '../questionStyle.css'; import { Button } from '@mui/material'; import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate'; import { MultipleChoiceQuestion } from 'gift-pegjs'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface Props { question: MultipleChoiceQuestion; - handleOnSubmitAnswer?: (answer: string | number | boolean) => void; + handleOnSubmitAnswer?: (answer: AnswerType) => void; showAnswer?: boolean; - passedAnswer?: string | number | boolean; + passedAnswer?: AnswerType; } const MultipleChoiceQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; - const [answer, setAnswer] = useState(passedAnswer || ''); + const [answer, setAnswer] = useState(passedAnswer || ''); let disableButton = false; diff --git a/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx index 4828185..525501d 100644 --- a/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx @@ -5,18 +5,19 @@ import { Button, TextField } from '@mui/material'; import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate'; import { NumericalQuestion, SimpleNumericalAnswer, RangeNumericalAnswer, HighLowNumericalAnswer } from 'gift-pegjs'; import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer, isMultipleNumericalAnswer } from 'gift-pegjs/typeGuards'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface Props { question: NumericalQuestion; - handleOnSubmitAnswer?: (answer: string | number | boolean) => void; + handleOnSubmitAnswer?: (answer: AnswerType) => void; showAnswer?: boolean; - passedAnswer?: string | number | boolean; + passedAnswer?: AnswerType; } const NumericalQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; - const [answer, setAnswer] = useState(passedAnswer || ''); + const [answer, setAnswer] = useState(passedAnswer || ''); const correctAnswers = question.choices; let correctAnswer = ''; diff --git a/client/src/components/QuestionsDisplay/QuestionDisplay.tsx b/client/src/components/QuestionsDisplay/QuestionDisplay.tsx index a1f0f30..af6e6d8 100644 --- a/client/src/components/QuestionsDisplay/QuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/QuestionDisplay.tsx @@ -5,13 +5,14 @@ import TrueFalseQuestionDisplay from './TrueFalseQuestionDisplay/TrueFalseQuesti import MultipleChoiceQuestionDisplay from './MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay'; import NumericalQuestionDisplay from './NumericalQuestionDisplay/NumericalQuestionDisplay'; import ShortAnswerQuestionDisplay from './ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; // import useCheckMobileScreen from '../../services/useCheckMobileScreen'; interface QuestionProps { question: Question; - handleOnSubmitAnswer?: (answer: string | number | boolean) => void; + handleOnSubmitAnswer?: (answer: AnswerType) => void; showAnswer?: boolean; - answer?: string | number | boolean; + answer?: AnswerType; } const QuestionDisplay: React.FC = ({ diff --git a/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx index 1dc02b5..28876f9 100644 --- a/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx @@ -3,19 +3,20 @@ import '../questionStyle.css'; import { Button, TextField } from '@mui/material'; import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate'; import { ShortAnswerQuestion } from 'gift-pegjs'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface Props { question: ShortAnswerQuestion; - handleOnSubmitAnswer?: (answer: string | number | boolean) => void; + handleOnSubmitAnswer?: (answer: AnswerType) => void; showAnswer?: boolean; - passedAnswer?: string | number | boolean; + passedAnswer?: AnswerType; } const ShortAnswerQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; - const [answer, setAnswer] = useState(passedAnswer || ''); + const [answer, setAnswer] = useState(passedAnswer || ''); useEffect(() => { if (passedAnswer !== undefined) { diff --git a/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx index 736563b..8908338 100644 --- a/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx @@ -4,12 +4,13 @@ import '../questionStyle.css'; import { Button } from '@mui/material'; import { TrueFalseQuestion } from 'gift-pegjs'; import { FormattedTextTemplate } from 'src/components/GiftTemplate/templates/TextTypeTemplate'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface Props { question: TrueFalseQuestion; - handleOnSubmitAnswer?: (answer: string | number | boolean) => void; + handleOnSubmitAnswer?: (answer: AnswerType) => void; showAnswer?: boolean; - passedAnswer?: string | number | boolean; + passedAnswer?: AnswerType; } const TrueFalseQuestionDisplay: React.FC = (props) => { diff --git a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx index 1feb4a1..192c0b2 100644 --- a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx +++ b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx @@ -8,11 +8,12 @@ import { Button } from '@mui/material'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton'; import { Question } from 'gift-pegjs'; import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface StudentModeQuizProps { questions: QuestionType[]; answers: AnswerSubmissionToBackendType[]; - submitAnswer: (_answer: string | number | boolean, _idQuestion: number) => void; + submitAnswer: (_answer: AnswerType, _idQuestion: number) => void; disconnectWebSocket: () => void; } @@ -25,7 +26,7 @@ const StudentModeQuiz: React.FC = ({ //Ajouter type AnswerQuestionType en remplacement de QuestionType const [questionInfos, setQuestion] = useState(questions[0]); const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false); - // const [answer, setAnswer] = useState(''); + // const [answer, setAnswer] = useState(''); const previousQuestion = () => { @@ -42,7 +43,7 @@ const StudentModeQuiz: React.FC = ({ setQuestion(questions[Number(questionInfos.question?.id)]); }; - const handleOnSubmitAnswer = (answer: string | number | boolean) => { + const handleOnSubmitAnswer = (answer: AnswerType) => { 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 c11e8a5..8925c09 100644 --- a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx +++ b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx @@ -13,7 +13,7 @@ import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface TeacherModeQuizProps { questionInfos: QuestionType; answers: AnswerSubmissionToBackendType[]; - submitAnswer: (_answer: string | number | boolean, _idQuestion: number) => void; + submitAnswer: (_answer: AnswerType, _idQuestion: number) => void; disconnectWebSocket: () => void; } @@ -50,7 +50,7 @@ const TeacherModeQuiz: React.FC = ({ setIsFeedbackDialogOpen(isAnswerSubmitted); }, [isAnswerSubmitted]); - const handleOnSubmitAnswer = (answer: string | number | boolean) => { + const handleOnSubmitAnswer = (answer: AnswerType) => { const idQuestion = Number(questionInfos.question.id) || -1; submitAnswer(answer, idQuestion); // setAnswer(answer); diff --git a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx index 411567c..bcdc80d 100644 --- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx +++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx @@ -24,6 +24,7 @@ import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay'; import ApiService from '../../../services/ApiService'; import { QuestionType } from 'src/Types/QuestionType'; import { Button } from '@mui/material'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; const ManageRoom: React.FC = () => { const navigate = useNavigate(); @@ -319,7 +320,7 @@ const ManageRoom: React.FC = () => { }; function checkIfIsCorrect( - answer: string | number | boolean, + answer: AnswerType, idQuestion: number, questions: QuestionType[] ): boolean { diff --git a/client/src/services/WebsocketService.tsx b/client/src/services/WebsocketService.tsx index 212aa21..9262a59 100644 --- a/client/src/services/WebsocketService.tsx +++ b/client/src/services/WebsocketService.tsx @@ -1,4 +1,5 @@ import { io, Socket } from 'socket.io-client'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; import { QuestionType } from 'src/Types/QuestionType'; // Must (manually) sync these types to server/socket/socket.js @@ -6,14 +7,14 @@ import { QuestionType } from 'src/Types/QuestionType'; export type AnswerSubmissionToBackendType = { roomName: string; username: string; - answer: string | number | boolean; + answer: AnswerType; idQuestion: number; }; export type AnswerReceptionFromBackendType = { idUser: string; username: string; - answer: string | number | boolean; + answer: AnswerType; idQuestion: number; }; From fe67f020eb71e2c6a695a1247232c12db1c71ed2 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Sun, 9 Mar 2025 00:54:21 -0500 Subject: [PATCH 17/19] =?UTF-8?q?[BUG]=20=C3=A9tudiant=20qui=20se=20joint?= =?UTF-8?q?=20=C3=A0=20une=20salle=20apr=C3=A8s=20le=20d=C3=A9marrage=20du?= =?UTF-8?q?=20quiz=20est=20bloqu=C3=A9=20Fixes=20#283=20Valeurs=20de=20l'?= =?UTF-8?q?=C3=A9tat=20de=20la=20page=20(quizStarted)=20n'ont=20pas=20leur?= =?UTF-8?q?=20valeur=20actuelle=20dans=20un=20on().=20Alors,=20on=20d?= =?UTF-8?q?=C3=A9place=20la=20logique=20du=20traitement=20du=20nouvel=20?= =?UTF-8?q?=C3=A9tudiant=20dans=20un=20useEffect=20et=20on=20provoque=20le?= =?UTF-8?q?=20useEffect=20dans=20le=20on()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/Student/JoinRoom/JoinRoom.tsx | 5 +- .../pages/Teacher/ManageRoom/ManageRoom.tsx | 69 +++++++++++++------ 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/client/src/pages/Student/JoinRoom/JoinRoom.tsx b/client/src/pages/Student/JoinRoom/JoinRoom.tsx index dc7e80c..96d1241 100644 --- a/client/src/pages/Student/JoinRoom/JoinRoom.tsx +++ b/client/src/pages/Student/JoinRoom/JoinRoom.tsx @@ -39,9 +39,8 @@ const JoinRoom: React.FC = () => { }, []); useEffect(() => { - // init the answers array, one for each question - setAnswers(Array(questions.length).fill({} as AnswerSubmissionToBackendType)); console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`); + setAnswers(questions ? Array(questions.length).fill({} as AnswerSubmissionToBackendType) : []); }, [questions]); @@ -64,6 +63,7 @@ const JoinRoom: React.FC = () => { console.log('on(launch-teacher-mode): Received launch-teacher-mode:', questions); setQuizMode('teacher'); setIsWaitingForTeacher(true); + setQuestions([]); // clear out from last time (in case quiz is repeated) setQuestions(questions); // wait for next-question }); @@ -72,6 +72,7 @@ const JoinRoom: React.FC = () => { setQuizMode('student'); setIsWaitingForTeacher(false); + setQuestions([]); // clear out from last time (in case quiz is repeated) setQuestions(questions); setQuestion(questions[0]); }); diff --git a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx index bcdc80d..f9d3791 100644 --- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx +++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx @@ -36,8 +36,40 @@ const ManageRoom: React.FC = () => { const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher'); const [connectingError, setConnectingError] = useState(''); const [currentQuestion, setCurrentQuestion] = useState(undefined); - const [quizStarted, setQuizStarted] = useState(false); + const [quizStarted, setQuizStarted] = useState(false); const [formattedRoomName, setFormattedRoomName] = useState(""); + const [newlyConnectedUser, setNewlyConnectedUser] = useState(null); + + // Handle the newly connected user in useEffect, because it needs state info + // not available in the socket.on() callback + useEffect(() => { + if (newlyConnectedUser) { + console.log(`Handling newly connected user: ${newlyConnectedUser.name}`); + setStudents((prevStudents) => [...prevStudents, newlyConnectedUser]); + + // only send nextQuestion if the quiz has started + if (!quizStarted) { + console.log(`!quizStarted: returning.... `); + return; + } + + if (quizMode === 'teacher') { + webSocketService.nextQuestion({ + roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: Number(currentQuestion?.question.id) - 1, + isLaunch: true // started late + }); + } else if (quizMode === 'student') { + webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions); + } else { + console.error('Invalid quiz mode:', quizMode); + } + + // Reset the newly connected user state + setNewlyConnectedUser(null); + } + }, [newlyConnectedUser, quizStarted, quizMode, formattedRoomName, quizQuestions, currentQuestion]); useEffect(() => { const verifyLogin = async () => { @@ -110,6 +142,17 @@ const ManageRoom: React.FC = () => { const roomNameUpper = roomName.toUpperCase(); setFormattedRoomName(roomNameUpper); console.log(`Creating WebSocket room named ${roomNameUpper}`); + + /** + * ATTENTION: Lire les variables d'état dans + * les .on() n'est pas une bonne pratique. + * Les valeurs sont celles au moment de la création + * de la fonction et non au moment de l'exécution. + * Il faut utiliser des refs pour les valeurs qui + * changent fréquemment. Sinon, utiliser un trigger + * de useEffect pour mettre déclencher un traitement + * (voir user-joined plus bas). + */ socket.on('connect', () => { webSocketService.createRoom(roomNameUpper); }); @@ -124,23 +167,9 @@ const ManageRoom: React.FC = () => { }); socket.on('user-joined', (student: StudentType) => { - console.log(`Student joined: name = ${student.name}, id = ${student.id}, quizMode = ${quizMode}, quizStarted = ${quizStarted}`); - - setStudents((prevStudents) => [...prevStudents, student]); - - // only send nextQuestion if the quiz has started - if (!quizStarted) return; - - if (quizMode === 'teacher') { - webSocketService.nextQuestion( - {roomName: formattedRoomName, - questions: quizQuestions, - questionIndex: Number(currentQuestion?.question.id) - 1, - isLaunch: false}); - } else if (quizMode === 'student') { - webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions); - } + setNewlyConnectedUser(student); }); + socket.on('join-failure', (message) => { setConnectingError(message); setSocket(null); @@ -286,21 +315,19 @@ const ManageRoom: React.FC = () => { }; const launchQuiz = () => { + setQuizStarted(true); if (!socket || !formattedRoomName || !quiz?.content || quiz?.content.length === 0) { // TODO: This error happens when token expires! Need to handle it properly console.log( `Error launching quiz. socket: ${socket}, roomName: ${formattedRoomName}, quiz: ${quiz}` ); - setQuizStarted(true); - return; } + console.log(`Launching quiz in ${quizMode} mode...`); switch (quizMode) { case 'student': - setQuizStarted(true); return launchStudentMode(); case 'teacher': - setQuizStarted(true); return launchTeacherMode(); } }; From 29de2a7671f84eb8b6d7bb7c1d72c2161a2d064e Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Sun, 9 Mar 2025 01:19:31 -0500 Subject: [PATCH 18/19] =?UTF-8?q?Correction=20de=20bogue=20trouv=C3=A9=20p?= =?UTF-8?q?ar=20test!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Teacher/ManageRoom/ManageRoom.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx index f9d3791..01d9c27 100644 --- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx +++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx @@ -69,7 +69,7 @@ const ManageRoom: React.FC = () => { // Reset the newly connected user state setNewlyConnectedUser(null); } - }, [newlyConnectedUser, quizStarted, quizMode, formattedRoomName, quizQuestions, currentQuestion]); + }, [newlyConnectedUser]); useEffect(() => { const verifyLogin = async () => { From dcaee719d17f9f67d8447440fae0683320f10668 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Mon, 10 Mar 2025 15:20:07 -0400 Subject: [PATCH 19/19] =?UTF-8?q?Fixes=20#286=20-=20Faire=20un=20vrai=20si?= =?UTF-8?q?ngleton=20pour=20la=20connexion=20=C3=A0=20la=20base=20de=20don?= =?UTF-8?q?n=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/app.js | 7 +++++++ server/config/db.js | 39 ++++++++++++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/server/app.js b/server/app.js index 32f64b9..938d2f0 100644 --- a/server/app.js +++ b/server/app.js @@ -127,4 +127,11 @@ async function start() { }); } +// Graceful shutdown on SIGINT (Ctrl+C) +process.on('SIGINT', async () => { + console.log('Shutting down...'); + await db.closeConnection(); + process.exit(0); +}); + start(); diff --git a/server/config/db.js b/server/config/db.js index cf492bf..ccc43e7 100644 --- a/server/config/db.js +++ b/server/config/db.js @@ -1,28 +1,53 @@ const { MongoClient } = require('mongodb'); -const dotenv = require('dotenv') +const dotenv = require('dotenv'); dotenv.config(); class DBConnection { - constructor() { this.mongoURI = process.env.MONGO_URI; this.databaseName = process.env.MONGO_DATABASE; + this.client = null; this.connection = null; } + // Connect to the database, but don't reconnect if already connected async connect() { - const client = new MongoClient(this.mongoURI); - this.connection = await client.connect(); + if (this.connection) { + console.log('Using existing MongoDB connection'); + return this.connection; + } + + try { + // Create the MongoClient only if the connection does not exist + this.client = new MongoClient(this.mongoURI); + await this.client.connect(); + this.connection = this.client.db(this.databaseName); + console.log('MongoDB connected'); + return this.connection; + } catch (error) { + console.error('MongoDB connection error:', error); + throw new Error('Failed to connect to MongoDB'); + } } + // Return the current database connection getConnection() { if (!this.connection) { - throw new Error('Connexion MongoDB non établie'); + throw new Error('MongoDB connection not established'); + } + return this.connection; + } + + // Close the MongoDB connection gracefully + async closeConnection() { + if (this.client) { + await this.client.close(); + console.log('MongoDB connection closed'); } - return this.connection.db(this.databaseName); } } +// Exporting the singleton instance const instance = new DBConnection(); -module.exports = instance; \ No newline at end of file +module.exports = instance;