(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 () => {
@@ -110,6 +137,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 +162,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 +310,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();
}
};
@@ -319,63 +341,6 @@ const ManageRoom: React.FC = () => {
navigate('/teacher/dashboard');
};
- function checkIfIsCorrect(
- answer: AnswerType,
- 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 ParsedGIFTQuestion;
- 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.formattedText.text === answerText
- );
- } else if (question.type === 'Numerical') {
- if (isHighLowNumericalAnswer(question.choices[0])) {
- const choice = question.choices[0];
- const answerNumber = parseFloat(answerText);
- if (!isNaN(answerNumber)) {
- return (
- answerNumber <= choice.numberHigh && answerNumber >= choice.numberLow
- );
- }
- }
- if (isRangeNumericalAnswer(question.choices[0])) {
- const answerNumber = parseFloat(answerText);
- const range = question.choices[0].range;
- const correctAnswer = question.choices[0].number;
- if (!isNaN(answerNumber)) {
- return (
- answerNumber <= correctAnswer + range &&
- answerNumber >= correctAnswer - range
- );
- }
- }
- if (isSimpleNumericalAnswer(question.choices[0])) {
- const answerNumber = parseFloat(answerText);
- if (!isNaN(answerNumber)) {
- return answerNumber === question.choices[0].number;
- }
- }
- } else if (question.type === 'Short') {
- return question.choices.some(
- (choice) => choice.text.toUpperCase() === answerText.toUpperCase()
- );
- }
- }
- return false;
- }
-
if (!formattedRoomName) {
return (
diff --git a/client/src/pages/Teacher/ManageRoom/useRooms.ts b/client/src/pages/Teacher/ManageRoom/useRooms.ts
index f0cacc8..2dadbfb 100644
--- a/client/src/pages/Teacher/ManageRoom/useRooms.ts
+++ b/client/src/pages/Teacher/ManageRoom/useRooms.ts
@@ -1,8 +1,15 @@
import { useContext } from 'react';
import { RoomType } from 'src/Types/RoomType';
import { createContext } from 'react';
-
-//import { RoomContext } from './RoomContext';
+import { MultipleNumericalAnswer, NumericalAnswer, ParsedGIFTQuestion } from 'gift-pegjs';
+import { QuestionType } from 'src/Types/QuestionType';
+import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
+import {
+ isSimpleNumericalAnswer,
+ isRangeNumericalAnswer,
+ isHighLowNumericalAnswer,
+ isMultipleNumericalAnswer
+} from 'gift-pegjs/typeGuards';
type RoomContextType = {
rooms: RoomType[];
@@ -18,3 +25,137 @@ export const useRooms = () => {
if (!context) throw new Error('useRooms must be used within a RoomProvider');
return context;
};
+
+/**
+ * Checks if the answer is correct - logic varies by type of question!
+ * True/False: answer must match the isTrue property
+ * Multiple Choice: answer must match the correct choice(s)
+ * Numerical: answer must be within the range or equal to the number (for each type of correct answer)
+ * Short Answer: answer must match the correct choice(s) (case-insensitive)
+ * @param answer
+ * @param idQuestion
+ * @param questions
+ * @returns
+ */
+export function checkIfIsCorrect(
+ answer: AnswerType,
+ idQuestion: number,
+ questions: QuestionType[]
+): boolean {
+ const questionInfo = questions.find((q) =>
+ q.question.id ? q.question.id === idQuestion.toString() : false
+ ) as QuestionType | undefined;
+
+ const simpleAnswerText = answer.toString();
+ if (questionInfo) {
+ const question = questionInfo.question as ParsedGIFTQuestion;
+ if (question.type === 'TF') {
+ return (
+ (question.isTrue && simpleAnswerText == 'true') ||
+ (!question.isTrue && simpleAnswerText == 'false')
+ );
+ } else if (question.type === 'MC') {
+ const correctChoices = question.choices.filter((choice) => choice.isCorrect
+ /* || (choice.weight && choice.weight > 0)*/ // handle weighted answers
+ );
+ const multipleAnswers = Array.isArray(answer) ? answer : [answer as string];
+ if (correctChoices.length === 0) {
+ return false;
+ }
+ // check if all (and only) correct choices are in the multipleAnswers array
+ return correctChoices.length === multipleAnswers.length && correctChoices.every(
+ (choice) => multipleAnswers.includes(choice.formattedText.text)
+ );
+ } else if (question.type === 'Numerical') {
+ if (isMultipleNumericalAnswer(question.choices[0])) { // Multiple numerical answers
+ // check to see if answer[0] is a match for any of the choices that isCorrect
+ const correctChoices = question.choices.filter((choice) => isMultipleNumericalAnswer(choice) && choice.isCorrect);
+ if (correctChoices.length === 0) { // weird case where there are multiple numerical answers but none are correct
+ return false;
+ }
+ return correctChoices.some((choice) => {
+ // narrow choice to MultipleNumericalAnswer type
+ const multipleNumericalChoice = choice as MultipleNumericalAnswer;
+ return isCorrectNumericalAnswer(multipleNumericalChoice.answer, simpleAnswerText);
+ });
+ }
+ if (isHighLowNumericalAnswer(question.choices[0])) {
+ // const choice = question.choices[0];
+ // const answerNumber = parseFloat(simpleAnswerText);
+ // if (!isNaN(answerNumber)) {
+ // return (
+ // answerNumber <= choice.numberHigh && answerNumber >= choice.numberLow
+ // );
+ // }
+ return isCorrectNumericalAnswer(question.choices[0], simpleAnswerText);
+ }
+ if (isRangeNumericalAnswer(question.choices[0])) {
+ // const answerNumber = parseFloat(simpleAnswerText);
+ // const range = question.choices[0].range;
+ // const correctAnswer = question.choices[0].number;
+ // if (!isNaN(answerNumber)) {
+ // return (
+ // answerNumber <= correctAnswer + range &&
+ // answerNumber >= correctAnswer - range
+ // );
+ // }
+ return isCorrectNumericalAnswer(question.choices[0], simpleAnswerText);
+ }
+ if (isSimpleNumericalAnswer(question.choices[0])) {
+ // const answerNumber = parseFloat(simpleAnswerText);
+ // if (!isNaN(answerNumber)) {
+ // return answerNumber === question.choices[0].number;
+ // }
+ return isCorrectNumericalAnswer(question.choices[0], simpleAnswerText);
+ }
+ } else if (question.type === 'Short') {
+ return question.choices.some(
+ (choice) => choice.text.toUpperCase() === simpleAnswerText.toUpperCase()
+ );
+ }
+ }
+ return false;
+}
+
+/**
+* Determines if a numerical answer is correct based on the type of numerical answer.
+* @param correctAnswer The correct answer (of type NumericalAnswer).
+* @param userAnswer The user's answer (as a string or number).
+* @returns True if the user's answer is correct, false otherwise.
+*/
+export function isCorrectNumericalAnswer(
+ correctAnswer: NumericalAnswer,
+ userAnswer: string | number
+): boolean {
+ const answerNumber = typeof userAnswer === 'string' ? parseFloat(userAnswer) : userAnswer;
+
+ if (isNaN(answerNumber)) {
+ return false; // User's answer is not a valid number
+ }
+
+ if (isSimpleNumericalAnswer(correctAnswer)) {
+ // Exact match for simple numerical answers
+ return answerNumber === correctAnswer.number;
+ }
+
+ if (isRangeNumericalAnswer(correctAnswer)) {
+ // Check if the user's answer is within the range
+ const { number, range } = correctAnswer;
+ return answerNumber >= number - range && answerNumber <= number + range;
+ }
+
+ if (isHighLowNumericalAnswer(correctAnswer)) {
+ // Check if the user's answer is within the high-low range
+ const { numberLow, numberHigh } = correctAnswer;
+ return answerNumber >= numberLow && answerNumber <= numberHigh;
+ }
+
+ // if (isMultipleNumericalAnswer(correctAnswer)) {
+ // // Check if the user's answer matches any of the multiple numerical answers
+ // return correctAnswer.answer.some((choice) =>
+ // isCorrectNumericalAnswer(choice, answerNumber)
+ // );
+ // }
+
+ return false; // Default to false if the answer type is not recognized
+}
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;