-
+
+
+ Question {questionInfos.question.id}/{questions.length}
@@ -66,31 +74,30 @@ const StudentModeQuiz: React.FC
= ({
handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question as Question}
showAnswer={isAnswerSubmitted}
+ answer={answers[Number(questionInfos.question.id)-1]?.answer}
/>
-
-
- {/* }
- disabled={Number(questionInfos.question.id) <= 1}
- >
- Question précédente
- */}
-
-
- }
- disabled={Number(questionInfos.question.id) >= questions.length}
- >
- Question suivante
-
-
+
+
+
+
+
+
+
diff --git a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx
index d4c7793..8925c09 100644
--- a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx
+++ b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx
@@ -1,55 +1,59 @@
// 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';
+import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService';
+import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
+// import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
interface TeacherModeQuizProps {
questionInfos: QuestionType;
- submitAnswer: (_answer: string | number | boolean, _idQuestion: number) => void;
+ answers: AnswerSubmissionToBackendType[];
+ submitAnswer: (_answer: AnswerType, _idQuestion: number) => void;
disconnectWebSocket: () => void;
}
const TeacherModeQuiz: React.FC
= ({
questionInfos,
+ answers,
submitAnswer,
disconnectWebSocket
}) => {
const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false);
const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false);
- const [feedbackMessage, setFeedbackMessage] = useState('');
-
- const renderFeedbackMessage = (answer: string) => {
+ const [answer, setAnswer] = useState();
- if(answer === 'true' || answer === 'false'){
- return (
- Votre réponse est: {answer==="true" ? 'Vrai' : 'Faux'}
- )
- }
- else{
- return (
-
- Votre réponse est: {answer.toString()}
-
- );}
- };
+
+ // arrive here the first time after waiting for next question
useEffect(() => {
- // Close the feedback dialog when the question changes
- handleFeedbackDialogClose();
- setIsAnswerSubmitted(false);
-
- }, [questionInfos.question]);
+ console.log(`TeacherModeQuiz: useEffect: answers: ${JSON.stringify(answers)}`);
+ console.log(`TeacherModeQuiz: useEffect: questionInfos.question.id: ${questionInfos.question.id} answer: ${answer}`);
+ const oldAnswer = answers[Number(questionInfos.question.id) -1 ]?.answer;
+ console.log(`TeacherModeQuiz: useEffect: oldAnswer: ${oldAnswer}`);
+ setAnswer(oldAnswer);
+ setIsFeedbackDialogOpen(false);
+ }, [questionInfos.question, answers]);
- const handleOnSubmitAnswer = (answer: string | number | boolean) => {
+ // handle showing the feedback dialog
+ useEffect(() => {
+ console.log(`TeacherModeQuiz: useEffect: answer: ${answer}`);
+ setIsAnswerSubmitted(answer !== undefined);
+ setIsFeedbackDialogOpen(answer !== undefined);
+ }, [answer]);
+
+ useEffect(() => {
+ console.log(`TeacherModeQuiz: useEffect: isAnswerSubmitted: ${isAnswerSubmitted}`);
+ setIsFeedbackDialogOpen(isAnswerSubmitted);
+ }, [isAnswerSubmitted]);
+
+ const handleOnSubmitAnswer = (answer: AnswerType) => {
const idQuestion = Number(questionInfos.question.id) || -1;
submitAnswer(answer, idQuestion);
- setFeedbackMessage(renderFeedbackMessage(answer.toString()));
+ // setAnswer(answer);
setIsFeedbackDialogOpen(true);
};
@@ -60,21 +64,21 @@ const TeacherModeQuiz: React.FC = ({
return (
-
+
-
-
-
-
Question {questionInfos.question.id}
-
-
-
+
+
+
Question {questionInfos.question.id}
- {isAnswerSubmitted ? (
+
+
+
+
+ {isAnswerSubmitted ? (
En attente pour la prochaine question...
@@ -82,6 +86,7 @@ const TeacherModeQuiz: React.FC
= ({
)}
@@ -92,20 +97,21 @@ const TeacherModeQuiz: React.FC = ({
Rétroaction
- {feedbackMessage}
-
Question :
+ wordWrap: 'break-word',
+ whiteSpace: 'pre-wrap',
+ maxHeight: '400px',
+ overflowY: 'auto',
+ }}>
+
Question :
-
-
@@ -114,7 +120,7 @@ const TeacherModeQuiz: React.FC = ({
-
+
);
};
diff --git a/client/src/pages/Student/JoinRoom/JoinRoom.tsx b/client/src/pages/Student/JoinRoom/JoinRoom.tsx
index 5ff30b0..96d1241 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,12 @@ const JoinRoom: React.FC = () => {
};
}, []);
+ useEffect(() => {
+ console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`);
+ setAnswers(questions ? Array(questions.length).fill({} as AnswerSubmissionToBackendType) : []);
+ }, [questions]);
+
+
const handleCreateSocket = () => {
console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`);
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
@@ -45,16 +54,25 @@ 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([]); // clear out from last time (in case quiz is repeated)
+ setQuestions(questions);
+ // wait for next-question
+ });
socket.on('launch-student-mode', (questions: QuestionType[]) => {
console.log('on(launch-student-mode): Received launch-student-mode:', questions);
setQuizMode('student');
setIsWaitingForTeacher(false);
+ setQuestions([]); // clear out from last time (in case quiz is repeated)
setQuestions(questions);
setQuestion(questions[0]);
});
@@ -83,6 +101,7 @@ const JoinRoom: React.FC = () => {
};
const disconnect = () => {
+// localStorage.clear();
webSocketService.disconnect();
setSocket(null);
setQuestion(undefined);
@@ -107,14 +126,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));
+ 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);
};
@@ -152,6 +179,7 @@ const JoinRoom: React.FC = () => {
return (
@@ -161,6 +189,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 d84253c..360d76f 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();
@@ -35,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]);
useEffect(() => {
const verifyLogin = async () => {
@@ -109,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);
});
@@ -123,19 +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(formattedRoomName, currentQuestion);
- } else if (quizMode === 'student') {
- webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions);
- }
+ setNewlyConnectedUser(student);
});
+
socket.on('join-failure', (message) => {
setConnectingError(message);
setSocket(null);
@@ -224,7 +258,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 +271,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 +299,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 = () => {
@@ -278,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();
}
};
@@ -300,9 +335,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});
}
}
};
@@ -313,7 +347,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 3cacf36..9262a59 100644
--- a/client/src/services/WebsocketService.tsx
+++ b/client/src/services/WebsocketService.tsx
@@ -1,18 +1,20 @@
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
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;
};
@@ -59,12 +61,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/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/auth/auth-manager.js b/server/auth/auth-manager.js
index 0233672..3d9c68f 100644
--- a/server/auth/auth-manager.js
+++ b/server/auth/auth-manager.js
@@ -73,13 +73,15 @@ class AuthManager{
console.info(`L'utilisateur '${userInfo.email}' vient de se connecter`)
}
- async register(userInfos){
+ async register(userInfos, sendEmail=false){
console.log(userInfos);
if (!userInfos.email || !userInfos.password) {
throw new AppError(MISSING_REQUIRED_PARAMETER);
}
const user = await this.simpleregister.register(userInfos);
- emailer.registerConfirmation(user.email)
+ if(sendEmail){
+ emailer.registerConfirmation(user.email);
+ }
return user
}
}
diff --git a/server/auth/modules/simpleauth.js b/server/auth/modules/simpleauth.js
index 8ed33bf..5e83ee5 100644
--- a/server/auth/modules/simpleauth.js
+++ b/server/auth/modules/simpleauth.js
@@ -34,7 +34,7 @@ class SimpleAuth {
password: req.body.password,
roles: req.body.roles
};
- let user = await self.authmanager.register(userInfos)
+ let user = await self.authmanager.register(userInfos, true);
if (user) {
return res.status(200).json({
message: 'User created'
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;
diff --git a/server/package-lock.json b/server/package-lock.json
index e0fdb07..0f006f8 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -2498,6 +2498,7 @@
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.1"
},
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);
});