diff --git a/client/src/pages/Student/JoinRoom/JoinRoom.tsx b/client/src/pages/Student/JoinRoom/JoinRoom.tsx index cb0da86..51baa58 100644 --- a/client/src/pages/Student/JoinRoom/JoinRoom.tsx +++ b/client/src/pages/Student/JoinRoom/JoinRoom.tsx @@ -51,9 +51,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]); @@ -76,6 +75,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 }); @@ -84,6 +84,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 e79e089..1f6114e 100644 --- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx +++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx @@ -45,11 +45,43 @@ 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 [formattedRoomName, setFormattedRoomName] = useState(""); + const [quizStarted, setQuizStarted] = useState(false); + const [formattedRoomName, setFormattedRoomName] = useState(''); + const [newlyConnectedUser, setNewlyConnectedUser] = useState(null); const roomUrl = `${window.location.origin}/student/join-room?roomName=${roomName}`; const [showQrModal, setShowQrModal] = useState(false); + // 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 () => { if (!ApiService.isLoggedIn()) { @@ -61,6 +93,15 @@ const ManageRoom: React.FC = () => { verifyLogin(); }, []); + useEffect(() => { + if (!roomName) { + console.error('Room name is missing!'); + return; + } + + console.log(`Joining room: ${roomName}`); + }, [roomName]); + useEffect(() => { if (!roomName || !quizId) { window.alert( @@ -77,15 +118,6 @@ const ManageRoom: React.FC = () => { }; }, [roomName, navigate]); - useEffect(() => { - if (!roomName) { - console.error('Room name is missing!'); - return; - } - - console.log(`Joining room: ${roomName}`); - }, [roomName]); - useEffect(() => { if (quizId) { const fetchQuiz = async () => { @@ -130,6 +162,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); }); @@ -144,23 +187,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); @@ -175,7 +204,6 @@ const ManageRoom: React.FC = () => { }; useEffect(() => { - if (socket) { console.log(`Listening for submit-answer-room in room ${formattedRoomName}`); socket.on('submit-answer-room', (answerData: AnswerReceptionFromBackendType) => { @@ -249,10 +277,12 @@ const ManageRoom: React.FC = () => { if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return; setCurrentQuestion(quizQuestions[nextQuestionIndex]); - webSocketService.nextQuestion({roomName: formattedRoomName, + webSocketService.nextQuestion({ + roomName: formattedRoomName, questions: quizQuestions, questionIndex: nextQuestionIndex, - isLaunch: false}); + isLaunch: false + }); }; const previousQuestion = () => { @@ -295,7 +325,12 @@ const ManageRoom: React.FC = () => { } setCurrentQuestion(quizQuestions[0]); - webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: 0, isLaunch: true}); + webSocketService.nextQuestion({ + roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: 0, + isLaunch: true + }); }; const launchStudentMode = () => { @@ -311,21 +346,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(); } }; @@ -334,7 +367,12 @@ const ManageRoom: React.FC = () => { if (quiz?.content && quizQuestions) { setCurrentQuestion(quizQuestions[questionIndex]); if (quizMode === 'teacher') { - webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex, isLaunch: false}); + webSocketService.nextQuestion({ + roomName: formattedRoomName, + questions: quizQuestions, + questionIndex, + isLaunch: false + }); } } }; @@ -473,7 +511,6 @@ const ManageRoom: React.FC = () => { -
{ )} 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;