EvalueTonSavoir/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx

549 lines
23 KiB
TypeScript
Raw Normal View History

2024-03-29 20:08:34 -04:00
// ManageRoom.tsx
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Socket } from 'socket.io-client';
2025-01-25 02:02:18 -05:00
import { ParsedGIFTQuestion, BaseQuestion, parse, Question } from 'gift-pegjs';
import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer } from "gift-pegjs/typeGuards";
2025-01-16 12:37:07 -05:00
import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
// import { QuestionService } from '../../../services/QuestionService';
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
2024-03-29 20:08:34 -04:00
import { QuizType } from '../../../Types/QuizType';
import GroupIcon from '@mui/icons-material/Group';
2024-03-29 20:08:34 -04:00
import './manageRoom.css';
import { ENV_VARIABLES } from 'src/constants';
import { StudentType, Answer } from '../../../Types/StudentType';
2025-01-16 12:37:07 -05:00
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
2024-03-29 20:08:34 -04:00
import { Refresh, Error } from '@mui/icons-material';
2025-01-16 12:37:07 -05:00
import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
//import QuestionNavigation from 'src/components/QuestionNavigation/QuestionNavigation';
2025-01-25 02:02:18 -05:00
import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay';
2024-03-29 20:08:34 -04:00
import ApiService from '../../../services/ApiService';
2025-01-25 02:02:18 -05:00
import { QuestionType } from 'src/Types/QuestionType';
2025-02-20 00:19:32 -05:00
import { RoomType } from 'src/Types/RoomType';
import {
IconButton,
Button,
Tooltip,
NativeSelect,
} from '@mui/material';
import {
Add
} from '@mui/icons-material';
2024-03-29 20:08:34 -04:00
const ManageRoom: React.FC = () => {
const navigate = useNavigate();
const [roomName, setRoomName] = useState<string>('');
const [socket, setSocket] = useState<Socket | null>(null);
2024-09-25 13:05:36 -04:00
const [students, setStudents] = useState<StudentType[]>([]);
2024-03-29 20:08:34 -04:00
const quizId = useParams<{ id: string }>();
const [quizQuestions, setQuizQuestions] = useState<QuestionType[] | undefined>();
const [quiz, setQuiz] = useState<QuizType | null>(null);
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
const [connectingError, setConnectingError] = useState<string>('');
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined);
const [quizStarted, setQuizStarted] = useState(false);
2025-02-20 00:19:32 -05:00
const [rooms, setFolders] = useState<RoomType[]>([]);
const [selectedRoomId, setSelectedRoomId] = useState<string>('');
useEffect(() => {
const fetchData = async () => {
if (!ApiService.isLoggedIn()) {
navigate("/teacher/login");
return;
}
else {
const userFolders = await ApiService.getUserRooms();
setFolders(userFolders as RoomType[]);
}
};
fetchData();
}, []);
const handleSelectRoom = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedRoomId(event.target.value);
};
const handleCreateRoom = async () => {
try {
const roomTitle = prompt('Titre de la salle');
if (roomTitle) {
await ApiService.createFolder(roomTitle);
const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as RoomType[]);
const newlyCreatedFolder = userFolders[userFolders.length - 1] as RoomType;
setSelectedRoomId(newlyCreatedFolder._id);
}
} catch (error) {
console.error('Error creating folder:', error);
}
};
2024-03-29 20:08:34 -04:00
useEffect(() => {
if (quizId.id) {
const fetchquiz = async () => {
const quiz = await ApiService.getQuiz(quizId.id as string);
if (!quiz) {
window.alert(`Une erreur est survenue.\n Le quiz ${quizId.id} n'a pas été trouvé\nVeuillez réessayer plus tard`)
console.error('Quiz not found for id:', quizId.id);
navigate('/teacher/dashboard');
return;
}
setQuiz(quiz as QuizType);
if (!socket) {
console.log(`no socket in ManageRoom, creating one.`);
2024-03-29 20:08:34 -04:00
createWebSocketRoom();
}
// return () => {
// webSocketService.disconnect();
// };
};
fetchquiz();
} else {
window.alert(`Une erreur est survenue.\n Le quiz ${quizId.id} n'a pas été trouvé\nVeuillez réessayer plus tard`)
console.error('Quiz not found for id:', quizId.id);
navigate('/teacher/dashboard');
return;
}
}, [quizId]);
const disconnectWebSocket = () => {
if (socket) {
webSocketService.endQuiz(roomName);
webSocketService.disconnect();
setSocket(null);
setQuizQuestions(undefined);
setCurrentQuestion(undefined);
2024-09-25 14:53:17 -04:00
setStudents(new Array<StudentType>());
2024-03-29 20:08:34 -04:00
setRoomName('');
}
};
const createWebSocketRoom = () => {
2024-10-30 17:19:11 -04:00
console.log('Creating WebSocket room...');
2024-03-29 20:08:34 -04:00
setConnectingError('');
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
2024-03-29 20:08:34 -04:00
socket.on('connect', () => {
webSocketService.createRoom();
2025-02-19 18:56:37 -05:00
console.error('socket.on(connect:)');
2024-03-29 20:08:34 -04:00
});
socket.on('connect_error', (error) => {
setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
2024-10-30 17:19:11 -04:00
console.error('ManageRoom: WebSocket connection error:', error);
2024-03-29 20:08:34 -04:00
});
socket.on('create-success', (roomName: string) => {
setRoomName(roomName);
2025-02-19 18:56:37 -05:00
console.error('create-success', roomName);
2024-03-29 20:08:34 -04:00
});
socket.on('create-failure', () => {
console.log('Error creating room.');
});
2024-09-25 13:05:36 -04:00
socket.on('user-joined', (student: StudentType) => {
console.log(`Student joined: name = ${student.name}, id = ${student.id}`);
setStudents((prevStudents) => [...prevStudents, student]);
2024-04-09 10:10:17 -04:00
if (quizMode === 'teacher') {
webSocketService.nextQuestion(roomName, currentQuestion);
} else if (quizMode === 'student') {
webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
}
});
2024-03-29 20:08:34 -04:00
socket.on('join-failure', (message) => {
setConnectingError(message);
setSocket(null);
});
socket.on('user-disconnected', (userId: string) => {
console.log(`Student left: id = ${userId}`);
setStudents((prevUsers) => prevUsers.filter((user) => user.id !== userId));
2024-03-29 20:08:34 -04:00
});
setSocket(socket);
};
2024-04-06 19:25:30 -04:00
useEffect(() => {
// This is here to make sure the correct value is sent when user join
if (socket) {
console.log(`Listening for user-joined in room ${roomName}`);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2024-09-25 13:05:36 -04:00
socket.on('user-joined', (_student: StudentType) => {
2024-04-06 19:25:30 -04:00
if (quizMode === 'teacher') {
webSocketService.nextQuestion(roomName, currentQuestion);
} else if (quizMode === 'student') {
webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
}
});
}
if (socket) {
// handle the case where user submits an answer
console.log(`Listening for submit-answer-room in room ${roomName}`);
socket.on('submit-answer-room', (answerData: AnswerReceptionFromBackendType) => {
const { answer, idQuestion, idUser, username } = answerData;
console.log(`Received answer from ${username} for question ${idQuestion}: ${answer}`);
if (!quizQuestions) {
console.log('Quiz questions not found (cannot update answers without them).');
return;
}
// Update the students state using the functional form of setStudents
setStudents((prevStudents) => {
// print the list of current student names
console.log('Current students:');
prevStudents.forEach((student) => {
console.log(student.name);
});
let foundStudent = false;
const updatedStudents = prevStudents.map((student) => {
console.log(`Comparing ${student.id} to ${idUser}`);
if (student.id === idUser) {
foundStudent = true;
const existingAnswer = student.answers.find((ans) => ans.idQuestion === idQuestion);
let updatedAnswers: Answer[] = [];
if (existingAnswer) {
// Update the existing answer
updatedAnswers = student.answers.map((ans) => {
console.log(`Comparing ${ans.idQuestion} to ${idQuestion}`);
return (ans.idQuestion === idQuestion ? { ...ans, answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!) } : ans);
});
} else {
// Add a new answer
const newAnswer = { idQuestion, answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!) };
updatedAnswers = [...student.answers, newAnswer];
}
return { ...student, answers: updatedAnswers };
}
return student;
});
if (!foundStudent) {
console.log(`Student ${username} not found in the list.`);
}
return updatedStudents;
});
});
setSocket(socket);
}
}, [socket, currentQuestion, quizQuestions]);
2024-03-29 20:08:34 -04:00
const nextQuestion = () => {
if (!quizQuestions || !currentQuestion || !quiz?.content) return;
const nextQuestionIndex = Number(currentQuestion?.question.id);
if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return;
setCurrentQuestion(quizQuestions[nextQuestionIndex]);
webSocketService.nextQuestion(roomName, quizQuestions[nextQuestionIndex]);
};
const previousQuestion = () => {
if (!quizQuestions || !currentQuestion || !quiz?.content) return;
2024-03-29 20:08:34 -04:00
const prevQuestionIndex = Number(currentQuestion?.question.id) - 2; // -2 because question.id starts at index 1
2024-03-29 20:08:34 -04:00
if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return;
setCurrentQuestion(quizQuestions[prevQuestionIndex]);
webSocketService.nextQuestion(roomName, quizQuestions[prevQuestionIndex]);
};
2024-03-29 20:08:34 -04:00
const initializeQuizQuestion = () => {
const quizQuestionArray = quiz?.content;
if (!quizQuestionArray) return null;
const parsedQuestions = [] as QuestionType[];
quizQuestionArray.forEach((question, index) => {
2025-01-25 02:02:18 -05:00
parsedQuestions.push({ question: parse(question)[0] as BaseQuestion });
2024-03-29 20:08:34 -04:00
parsedQuestions[index].question.id = (index + 1).toString();
});
if (parsedQuestions.length === 0) return null;
setQuizQuestions(parsedQuestions);
return parsedQuestions;
};
const launchTeacherMode = () => {
const quizQuestions = initializeQuizQuestion();
console.log('launchTeacherMode - quizQuestions:', quizQuestions);
2024-03-29 20:08:34 -04:00
if (!quizQuestions) {
console.log('Error launching quiz (launchTeacherMode). No questions found.');
return;
}
2024-03-29 20:08:34 -04:00
setCurrentQuestion(quizQuestions[0]);
webSocketService.nextQuestion(roomName, quizQuestions[0]);
};
const launchStudentMode = () => {
const quizQuestions = initializeQuizQuestion();
console.log('launchStudentMode - quizQuestions:', quizQuestions);
2024-03-29 20:08:34 -04:00
if (!quizQuestions) {
console.log('Error launching quiz (launchStudentMode). No questions found.');
2024-03-29 20:08:34 -04:00
return;
}
setQuizQuestions(quizQuestions);
2024-03-29 20:08:34 -04:00
webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
};
const launchQuiz = () => {
if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) {
// TODO: This error happens when token expires! Need to handle it properly
console.log(`Error launching quiz. socket: ${socket}, roomName: ${roomName}, quiz: ${quiz}`);
setQuizStarted(true);
2024-03-29 20:08:34 -04:00
return;
}
switch (quizMode) {
case 'student':
setQuizStarted(true);
2024-03-29 20:08:34 -04:00
return launchStudentMode();
case 'teacher':
setQuizStarted(true);
2024-03-29 20:08:34 -04:00
return launchTeacherMode();
2024-03-29 20:08:34 -04:00
}
};
const showSelectedQuestion = (questionIndex: number) => {
if (quiz?.content && quizQuestions) {
setCurrentQuestion(quizQuestions[questionIndex]);
2024-03-29 20:08:34 -04:00
if (quizMode === 'teacher') {
webSocketService.nextQuestion(roomName, quizQuestions[questionIndex]);
}
}
};
const handleReturn = () => {
disconnectWebSocket();
navigate('/teacher/dashboard');
};
function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number, questions: QuestionType[]): boolean {
const questionInfo = questions.find((q) =>
q.question.id ? q.question.id === idQuestion.toString() : false
) as QuestionType | undefined;
const answerText = answer.toString();
if (questionInfo) {
2025-01-25 02:02:18 -05:00
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(
2025-01-25 02:02:18 -05:00
(choice) => choice.isCorrect && choice.formattedText.text === answerText
);
} else if (question.type === 'Numerical') {
2025-01-25 02:02:18 -05:00
if (isHighLowNumericalAnswer(question.choices[0])) {
const choice = question.choices[0];
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return (
answerNumber <= choice.numberHigh &&
answerNumber >= choice.numberLow
);
}
}
2025-01-25 02:02:18 -05:00
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
);
}
2025-01-25 02:02:18 -05:00
}
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(
2025-01-25 02:02:18 -05:00
(choice) => choice.text.toUpperCase() === answerText.toUpperCase()
);
}
}
return false;
}
2024-03-29 20:08:34 -04:00
if (!roomName) {
return (
<div className="center">
{!connectingError ? (
<LoadingCircle text="Veuillez attendre la connexion au serveur..." />
) : (
<div className="center-v-align">
<Error sx={{ padding: 0 }} />
<div className="text-base">{connectingError}</div>
<Button
variant="contained"
startIcon={<Refresh />}
onClick={createWebSocketRoom}
>
Reconnecter
</Button>
</div>
)}
</div>
);
}
return (
<div className='room'>
<div className='roomHeader'>
2025-02-20 00:19:32 -05:00
2024-03-29 20:08:34 -04:00
<DisconnectButton
onReturn={handleReturn}
askConfirm
message={`Êtes-vous sûr de vouloir quitter?`} />
2025-02-20 00:19:32 -05:00
<div className='headerContent' style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
<div style={{ flex: 1, display: 'flex', justifyContent: 'center' }}>
<div className='title'>Salle: {roomName}</div>
</div>
{quizStarted && (
<div className='userCount subtitle smallText' style={{ display: 'flex', alignItems: 'center' }}>
<GroupIcon style={{ marginRight: '5px' }} />
{students.length}/60
</div>
)}
2024-03-29 20:08:34 -04:00
</div>
2025-02-20 00:19:32 -05:00
2024-03-29 20:08:34 -04:00
<div className='dumb'></div>
2025-02-20 00:19:32 -05:00
</div>
{/* bloc Room */}
<div className='room'>
<div className='select'>
<NativeSelect
id="select-room"
color="primary"
value={selectedRoomId}
onChange={handleSelectRoom}
>
<option value=""> Sélectionner une salle </option>
{rooms.map((room: RoomType) => (
<option value={room._id} key={room._id}> {room.title} </option>
))}
</NativeSelect>
</div>
<div className='actions'>
<Tooltip title="Ajouter room" placement="top">
<IconButton color="primary" onClick={handleCreateRoom}>
<Add />
</IconButton>
</Tooltip>
</div>
2024-03-29 20:08:34 -04:00
</div>
{/* the following breaks the css (if 'room' classes are nested) */}
<div className=''>
2024-03-29 20:08:34 -04:00
{quizQuestions ? (
2025-02-20 00:19:32 -05:00
2024-03-29 20:08:34 -04:00
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div className="title center-h-align mb-2">{quiz?.title}</div>
{!isNaN(Number(currentQuestion?.question.id)) && (
<strong className='number of questions'>
Question {Number(currentQuestion?.question.id)}/{quizQuestions?.length}
</strong>
)}
2025-02-20 00:19:32 -05:00
2024-03-29 20:08:34 -04:00
{quizMode === 'teacher' && (
<div className="mb-1">
{/* <QuestionNavigation
2024-03-29 20:08:34 -04:00
currentQuestionId={Number(currentQuestion?.question.id)}
questionsLength={quizQuestions?.length}
previousQuestion={previousQuestion}
nextQuestion={nextQuestion}
/> */}
2024-03-29 20:08:34 -04:00
</div>
)}
2025-02-20 00:19:32 -05:00
2024-03-29 20:08:34 -04:00
<div className="mb-2 flex-column-wrapper">
<div className="preview-and-result-container">
{currentQuestion && (
<QuestionDisplay
2024-03-29 20:08:34 -04:00
showAnswer={false}
2025-01-25 02:02:18 -05:00
question={currentQuestion?.question as Question}
2024-03-29 20:08:34 -04:00
/>
)}
<LiveResultsComponent
quizMode={quizMode}
socket={socket}
questions={quizQuestions}
showSelectedQuestion={showSelectedQuestion}
students={students}
2024-03-29 20:08:34 -04:00
></LiveResultsComponent>
</div>
</div>
2025-02-20 00:19:32 -05:00
2024-03-29 20:08:34 -04:00
{quizMode === 'teacher' && (
<div className="questionNavigationButtons" style={{ display: 'flex', justifyContent: 'center' }}>
<div className="previousQuestionButton">
<Button onClick={previousQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) <= 1}>
Question précédente
</Button>
</div>
<div className="nextQuestionButton">
<Button onClick={nextQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) >= quizQuestions.length}
>
Prochaine question
</Button>
</div>
</div>)}
2024-03-29 20:08:34 -04:00
</div>
) : (
2024-09-25 13:20:09 -04:00
<StudentWaitPage
2024-09-25 14:53:17 -04:00
students={students}
2024-03-29 20:08:34 -04:00
launchQuiz={launchQuiz}
setQuizMode={setQuizMode}
/>
)}
</div>
</div>
);
};
2025-02-20 00:19:32 -05:00
export default ManageRoom;