Merge pull request #134 from ets-cfuhrman-pfe/fuhrmanator/issue133

Fuhrmanator/issue133
This commit is contained in:
Christopher (Cris) Fuhrman 2024-09-26 01:04:17 -04:00 committed by GitHub
commit 1f78c44be2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 683 additions and 396 deletions

View file

@ -0,0 +1,12 @@
export interface Answer {
answer: string | number | boolean;
isCorrect: boolean;
idQuestion: number;
}
export interface StudentType {
name: string;
id: string;
room?: string;
answers: Answer[];
}

View file

@ -1,4 +0,0 @@
export interface UserType {
name: string;
id: string;
}

View file

@ -0,0 +1,17 @@
//StudentType.test.tsx
import { StudentType, Answer } from "../../Types/StudentType";
const user : StudentType = {
name: 'Student',
id: '123',
answers: new Array<Answer>()
}
describe('StudentType', () => {
test('creates a student with name, id and answers', () => {
expect(user.name).toBe('Student');
expect(user.id).toBe('123');
expect(user.answers.length).toBe(0);
});
});

View file

@ -1,15 +0,0 @@
//UserTyper.test.tsx
import { UserType } from "../../Types/UserType";
const user : UserType = {
name: 'Student',
id: '123'
}
describe('UserType', () => {
test('creates a user with name and id', () => {
expect(user.name).toBe('Student');
expect(user.id).toBe('123');
});
});

View file

@ -1,24 +1,25 @@
// Importez le type UserType s'il n'est pas déjà importé // Importez le type UserType s'il n'est pas déjà importé
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import UserWaitPage from '../../../components/UserWaitPage/UserWaitPage'; import StudentWaitPage from '../../../components/StudentWaitPage/StudentWaitPage';
import { StudentType, Answer } from '../../../Types/StudentType';
describe('UserWaitPage Component', () => { describe('StudentWaitPage Component', () => {
const mockUsers = [ const mockUsers: StudentType[] = [
{ id: '1', name: 'User1' }, { id: '1', name: 'User1', answers: new Array<Answer>() },
{ id: '2', name: 'User2' }, { id: '2', name: 'User2', answers: new Array<Answer>() },
{ id: '3', name: 'User3' }, { id: '3', name: 'User3', answers: new Array<Answer>() },
]; ];
const mockProps = { const mockProps = {
users: mockUsers, students: mockUsers,
launchQuiz: jest.fn(), launchQuiz: jest.fn(),
roomName: 'Test Room', roomName: 'Test Room',
setQuizMode: jest.fn(), setQuizMode: jest.fn(),
}; };
test('renders UserWaitPage with correct content', () => { test('renders StudentWaitPage with correct content', () => {
render(<UserWaitPage {...mockProps} />); render(<StudentWaitPage {...mockProps} />);
//expect(screen.getByText(/Test Room/)).toBeInTheDocument(); //expect(screen.getByText(/Test Room/)).toBeInTheDocument();
@ -31,7 +32,7 @@ describe('UserWaitPage Component', () => {
}); });
test('clicking on "Lancer" button opens LaunchQuizDialog', () => { test('clicking on "Lancer" button opens LaunchQuizDialog', () => {
render(<UserWaitPage {...mockProps} />); render(<StudentWaitPage {...mockProps} />);
fireEvent.click(screen.getByRole('button', { name: /Lancer/i })); fireEvent.click(screen.getByRole('button', { name: /Lancer/i }));

View file

@ -48,7 +48,7 @@ describe('StudentModeQuiz', () => {
fireEvent.click(screen.getByText('Répondre')); fireEvent.click(screen.getByText('Répondre'));
}); });
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', '1'); expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1);
}); });
test('handles quit button click', async () => { test('handles quit button click', async () => {

View file

@ -47,7 +47,7 @@ describe('TeacherModeQuiz', () => {
act(() => { act(() => {
fireEvent.click(screen.getByText('Répondre')); fireEvent.click(screen.getByText('Répondre'));
}); });
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', '1'); expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1);
expect(screen.getByText('Votre réponse est "Option A".')).toBeInTheDocument(); expect(screen.getByText('Votre réponse est "Option A".')).toBeInTheDocument();
}); });

View file

@ -1,7 +1,6 @@
// LiveResults.tsx // LiveResults.tsx
import React, { useEffect, useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { GIFTQuestion } from 'gift-pegjs';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons';
import { QuestionType } from '../../Types/QuestionType'; import { QuestionType } from '../../Types/QuestionType';
@ -15,11 +14,12 @@ import {
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableContainer,
TableFooter, TableFooter,
TableHead, TableHead,
TableRow TableRow
} from '@mui/material'; } from '@mui/material';
import { UserType } from '../../Types/UserType'; import { StudentType } from '../../Types/StudentType';
import { formatLatex } from '../GiftTemplate/templates/TextType'; import { formatLatex } from '../GiftTemplate/templates/TextType';
interface LiveResultsProps { interface LiveResultsProps {
@ -27,79 +27,121 @@ interface LiveResultsProps {
questions: QuestionType[]; questions: QuestionType[];
showSelectedQuestion: (index: number) => void; showSelectedQuestion: (index: number) => void;
quizMode: 'teacher' | 'student'; quizMode: 'teacher' | 'student';
students: UserType[] students: StudentType[]
} }
interface Answer { // interface Answer {
answer: string | number | boolean; // answer: string | number | boolean;
isCorrect: boolean; // isCorrect: boolean;
idQuestion: number; // idQuestion: number;
} // }
interface StudentResult { // interface StudentResult {
username: string; // username: string;
idUser: string; // idUser: string;
answers: Answer[]; // answers: Answer[];
} // }
const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelectedQuestion, students }) => { const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuestion, students }) => {
const [showUsernames, setShowUsernames] = useState<boolean>(false); const [showUsernames, setShowUsernames] = useState<boolean>(false);
const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(false); const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(false);
const [studentResults, setStudentResults] = useState<StudentResult[]>([]); // const [students, setStudents] = useState<StudentType[]>(initialStudents);
// const [studentResultsMap, setStudentResultsMap] = useState<Map<string, StudentResult>>(new Map());
const maxQuestions = questions.length; const maxQuestions = questions.length;
useEffect(() => { // useEffect(() => {
// Set student list before starting // // Initialize the map with the current students
let newStudents: StudentResult[] = []; // const newStudentResultsMap = new Map<string, StudentResult>();
for (const student of students as UserType[]) { // for (const student of students) {
newStudents.push({ username: student.name, idUser: student.id, answers: [] }) // newStudentResultsMap.set(student.id, { username: student.name, idUser: student.id, answers: [] });
} // }
setStudentResults(newStudents); // setStudentResultsMap(newStudentResultsMap);
// }, [])
}, []) // update when students change
// useEffect(() => {
// // studentResultsMap is inconsistent with students -- need to update
useEffect(() => { // for (const student of students as StudentType[]) {
if (socket) { // }
const submitAnswerHandler = ({
idUser,
username,
answer,
idQuestion
}: {
idUser: string;
username: string;
answer: string | number | boolean;
idQuestion: number;
}) => {
setStudentResults((currentResults) => {
const userIndex = currentResults.findIndex(
(result) => result.idUser === idUser
);
const isCorrect = checkIfIsCorrect(answer, idQuestion);
if (userIndex !== -1) {
const newResults = [...currentResults];
newResults[userIndex].answers.push({ answer, isCorrect, idQuestion });
return newResults;
} else {
return [
...currentResults,
{ idUser, username, answers: [{ answer, isCorrect, idQuestion }] }
];
}
});
};
socket.on('submit-answer', submitAnswerHandler); // }, [students])
return () => {
socket.off('submit-answer');
};
}
}, [socket]);
const getStudentGrade = (student: StudentResult): number => { // useEffect(() => {
// if (socket) {
// const submitAnswerHandler = ({
// idUser,
// answer,
// idQuestion
// }: {
// idUser: string;
// username: string;
// answer: string | number | boolean;
// idQuestion: number;
// }) => {
// console.log(`Received answer from ${idUser} for question ${idQuestion}: ${answer}`);
// // print the list of current student names
// console.log('Current students:');
// students.forEach((student) => {
// console.log(student.name);
// });
// // Update the students state using the functional form of setStudents
// setStudents((prevStudents) => {
// let foundStudent = false;
// const updatedStudents = prevStudents.map((student) => {
// if (student.id === idUser) {
// foundStudent = true;
// const updatedAnswers = student.answers.map((ans) => {
// const newAnswer: Answer = { answer, isCorrect: checkIfIsCorrect(answer, idQuestion), idQuestion };
// console.log(`Updating answer for ${student.name} for question ${idQuestion} to ${answer}`);
// return (ans.idQuestion === idQuestion ? { ...ans, newAnswer } : ans);
// }
// );
// return { ...student, answers: updatedAnswers };
// }
// return student;
// });
// if (!foundStudent) {
// console.log(`Student ${idUser} not found in the list of students in LiveResults`);
// }
// return updatedStudents;
// });
// // make a copy of the students array so we can update it
// // const updatedStudents = [...students];
// // const student = updatedStudents.find((student) => student.id === idUser);
// // if (!student) {
// // // this is a bad thing if an answer was submitted but the student isn't in the list
// // console.log(`Student ${idUser} not found in the list of students in LiveResults`);
// // return;
// // }
// // const isCorrect = checkIfIsCorrect(answer, idQuestion);
// // const newAnswer: Answer = { answer, isCorrect, idQuestion };
// // student.answers.push(newAnswer);
// // // print list of answers
// // console.log('Answers:');
// // student.answers.forEach((answer) => {
// // console.log(answer.answer);
// // });
// // setStudents(updatedStudents); // update the state
// };
// socket.on('submit-answer', submitAnswerHandler);
// return () => {
// socket.off('submit-answer');
// };
// }
// }, [socket]);
const getStudentGrade = (student: StudentType): number => {
if (student.answers.length === 0) { if (student.answers.length === 0) {
return 0; return 0;
} }
@ -124,93 +166,103 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
const classAverage: number = useMemo(() => { const classAverage: number = useMemo(() => {
let classTotal = 0; let classTotal = 0;
studentResults.forEach((student) => {
students.forEach((student) => {
classTotal += getStudentGrade(student); classTotal += getStudentGrade(student);
}); });
return classTotal / studentResults.length; return classTotal / students.length;
}, [studentResults]); }, [students]);
const getCorrectAnswersPerQuestion = (index: number): number => { const getCorrectAnswersPerQuestion = (index: number): number => {
return ( return (
(studentResults.filter((student) => (students.filter((student) =>
student.answers.some( student.answers.some(
(answer) => (answer) =>
parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
) )
).length / ).length / students.length) * 100
studentResults.length) *
100
); );
}; };
function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number): boolean { // (studentResults.filter((student) =>
const questionInfo = questions.find((q) => // student.answers.some(
q.question.id ? q.question.id === idQuestion.toString() : false // (answer) =>
) as QuestionType | undefined; // parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
// )
// ).length /
// studentResults.length) *
// 100
// );
// };
const answerText = answer.toString(); // function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number): boolean {
if (questionInfo) { // const questionInfo = questions.find((q) =>
const question = questionInfo.question as GIFTQuestion; // q.question.id ? q.question.id === idQuestion.toString() : false
if (question.type === 'TF') { // ) as QuestionType | undefined;
return (
(question.isTrue && answerText == 'true') || // const answerText = answer.toString();
(!question.isTrue && answerText == 'false') // if (questionInfo) {
); // const question = questionInfo.question as GIFTQuestion;
} else if (question.type === 'MC') { // if (question.type === 'TF') {
return question.choices.some( // return (
(choice) => choice.isCorrect && choice.text.text === answerText // (question.isTrue && answerText == 'true') ||
); // (!question.isTrue && answerText == 'false')
} else if (question.type === 'Numerical') { // );
if (question.choices && !Array.isArray(question.choices)) { // } else if (question.type === 'MC') {
if ( // return question.choices.some(
question.choices.type === 'high-low' && // (choice) => choice.isCorrect && choice.text.text === answerText
question.choices.numberHigh && // );
question.choices.numberLow // } else if (question.type === 'Numerical') {
) { // if (question.choices && !Array.isArray(question.choices)) {
const answerNumber = parseFloat(answerText); // if (
if (!isNaN(answerNumber)) { // question.choices.type === 'high-low' &&
return ( // question.choices.numberHigh &&
answerNumber <= question.choices.numberHigh && // question.choices.numberLow
answerNumber >= question.choices.numberLow // ) {
); // const answerNumber = parseFloat(answerText);
} // if (!isNaN(answerNumber)) {
} // return (
} // answerNumber <= question.choices.numberHigh &&
if (question.choices && Array.isArray(question.choices)) { // answerNumber >= question.choices.numberLow
if ( // );
question.choices[0].text.type === 'range' && // }
question.choices[0].text.number && // }
question.choices[0].text.range // }
) { // if (question.choices && Array.isArray(question.choices)) {
const answerNumber = parseFloat(answerText); // if (
const range = question.choices[0].text.range; // question.choices[0].text.type === 'range' &&
const correctAnswer = question.choices[0].text.number; // question.choices[0].text.number &&
if (!isNaN(answerNumber)) { // question.choices[0].text.range
return ( // ) {
answerNumber <= correctAnswer + range && // const answerNumber = parseFloat(answerText);
answerNumber >= correctAnswer - range // const range = question.choices[0].text.range;
); // const correctAnswer = question.choices[0].text.number;
} // if (!isNaN(answerNumber)) {
} // return (
if ( // answerNumber <= correctAnswer + range &&
question.choices[0].text.type === 'simple' && // answerNumber >= correctAnswer - range
question.choices[0].text.number // );
) { // }
const answerNumber = parseFloat(answerText); // }
if (!isNaN(answerNumber)) { // if (
return answerNumber === question.choices[0].text.number; // question.choices[0].text.type === 'simple' &&
} // question.choices[0].text.number
} // ) {
} // const answerNumber = parseFloat(answerText);
} else if (question.type === 'Short') { // if (!isNaN(answerNumber)) {
return question.choices.some( // return answerNumber === question.choices[0].text.number;
(choice) => choice.text.text.toUpperCase() === answerText.toUpperCase() // }
); // }
} // }
} // } else if (question.type === 'Short') {
return false; // return question.choices.some(
} // (choice) => choice.text.text.toUpperCase() === answerText.toUpperCase()
// );
// }
// }
// return false;
// }
return ( return (
<div> <div>
@ -243,7 +295,8 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
</div> </div>
<div className="table-container"> <div className="table-container">
<Table size="small" component={Paper}> <TableContainer component={Paper}>
<Table size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell className="sticky-column"> <TableCell className="sticky-column">
@ -278,8 +331,8 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{studentResults.map((student) => ( {students.map((student) => (
<TableRow key={student.idUser}> <TableRow key={student.id}>
<TableCell <TableCell
className="sticky-column" className="sticky-column"
sx={{ sx={{
@ -289,7 +342,7 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
}} }}
> >
<div className="text-base"> <div className="text-base">
{showUsernames ? student.username : '******'} {showUsernames ? student.name : '******'}
</div> </div>
</TableCell> </TableCell>
{Array.from({ length: maxQuestions }, (_, index) => { {Array.from({ length: maxQuestions }, (_, index) => {
@ -360,7 +413,7 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
color: 'rgba(0, 0, 0)' color: 'rgba(0, 0, 0)'
}} }}
> >
{studentResults.length > 0 {students.length > 0
? `${getCorrectAnswersPerQuestion(index).toFixed()} %` ? `${getCorrectAnswersPerQuestion(index).toFixed()} %`
: '-'} : '-'}
</TableCell> </TableCell>
@ -376,11 +429,12 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
color: 'rgba(0, 0, 0)' color: 'rgba(0, 0, 0)'
}} }}
> >
{studentResults.length > 0 ? `${classAverage.toFixed()} %` : '-'} {students.length > 0 ? `${classAverage.toFixed()} %` : '-'}
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableFooter> </TableFooter>
</Table> </Table>
</TableContainer>
</div> </div>
</div> </div>
); );

View file

@ -12,7 +12,7 @@ import DisconnectButton from '../../components/DisconnectButton/DisconnectButton
interface StudentModeQuizProps { interface StudentModeQuizProps {
questions: QuestionType[]; questions: QuestionType[];
submitAnswer: (answer: string | number | boolean, idQuestion: string) => void; submitAnswer: (answer: string | number | boolean, idQuestion: number) => void;
disconnectWebSocket: () => void; disconnectWebSocket: () => void;
} }
@ -38,7 +38,7 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
}; };
const handleOnSubmitAnswer = (answer: string | number | boolean) => { const handleOnSubmitAnswer = (answer: string | number | boolean) => {
const idQuestion = questionInfos.question.id || '-1'; const idQuestion = Number(questionInfos.question.id) || -1;
submitAnswer(answer, idQuestion); submitAnswer(answer, idQuestion);
setIsAnswerSubmitted(true); setIsAnswerSubmitted(true);
}; };

View file

@ -1,17 +1,17 @@
import { Button, Chip, Grid } from '@mui/material'; import { Box, Button, Chip } from '@mui/material';
import { UserType } from '../../Types/UserType'; import { StudentType } from '../../Types/StudentType';
import { PlayArrow } from '@mui/icons-material'; import { PlayArrow } from '@mui/icons-material';
import LaunchQuizDialog from '../LaunchQuizDialog/LaunchQuizDialog'; import LaunchQuizDialog from '../LaunchQuizDialog/LaunchQuizDialog';
import { useState } from 'react'; import { useState } from 'react';
import './userWaitPage.css'; import './studentWaitPage.css';
interface Props { interface Props {
users: UserType[]; students: StudentType[];
launchQuiz: () => void; launchQuiz: () => void;
setQuizMode: (mode: 'student' | 'teacher') => void; setQuizMode: (mode: 'student' | 'teacher') => void;
} }
const UserWaitPage: React.FC<Props> = ({ users, launchQuiz, setQuizMode }) => { const StudentWaitPage: React.FC<Props> = ({ students, launchQuiz, setQuizMode }) => {
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false); const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
return ( return (
@ -30,15 +30,15 @@ const UserWaitPage: React.FC<Props> = ({ users, launchQuiz, setQuizMode }) => {
<div className="students"> <div className="students">
<Grid container spacing={3}> <Box display="flex" flexWrap="wrap" gap={3}>
{users.map((user, index) => ( {students.map((student, index) => (
<Grid item key={user.name + index}> <Box key={student.name + index} >
<Chip label={user.name} sx={{ width: '100%' }} /> <Chip label={student.name} sx={{ width: '100%' }} />
</Grid> </Box>
))} ))}
</Grid> </Box>
</div> </div>
@ -53,4 +53,4 @@ const UserWaitPage: React.FC<Props> = ({ users, launchQuiz, setQuizMode }) => {
); );
}; };
export default UserWaitPage; export default StudentWaitPage;

View file

@ -11,7 +11,7 @@ import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/
interface TeacherModeQuizProps { interface TeacherModeQuizProps {
questionInfos: QuestionType; questionInfos: QuestionType;
submitAnswer: (answer: string | number | boolean, idQuestion: string) => void; submitAnswer: (answer: string | number | boolean, idQuestion: number) => void;
disconnectWebSocket: () => void; disconnectWebSocket: () => void;
} }
@ -29,7 +29,7 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
}, [questionInfos]); }, [questionInfos]);
const handleOnSubmitAnswer = (answer: string | number | boolean) => { const handleOnSubmitAnswer = (answer: string | number | boolean) => {
const idQuestion = questionInfos.question.id || '-1'; const idQuestion = Number(questionInfos.question.id) || -1;
submitAnswer(answer, idQuestion); submitAnswer(answer, idQuestion);
setFeedbackMessage(`Votre réponse est "${answer.toString()}".`); setFeedbackMessage(`Votre réponse est "${answer.toString()}".`);
setIsFeedbackDialogOpen(true); setIsFeedbackDialogOpen(true);

View file

@ -5,7 +5,7 @@ import { ENV_VARIABLES } from '../../../constants';
import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz'; import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz';
import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz'; import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz';
import webSocketService from '../../../services/WebsocketService'; import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService';
import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton'; import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton';
import './joinRoom.css'; import './joinRoom.css';
@ -99,8 +99,15 @@ const JoinRoom: React.FC = () => {
} }
}; };
const handleOnSubmitAnswer = (answer: string | number | boolean, idQuestion: string) => { const handleOnSubmitAnswer = (answer: string | number | boolean, idQuestion: number) => {
webSocketService.submitAnswer(roomName, answer, username, idQuestion); const answerData: AnswerSubmissionToBackendType = {
roomName: roomName,
answer: answer,
username: username,
idQuestion: idQuestion
};
webSocketService.submitAnswer(answerData);
}; };
if (isWaitingForTeacher) { if (isWaitingForTeacher) {

View file

@ -2,20 +2,20 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { parse } from 'gift-pegjs'; import { GIFTQuestion, parse } from 'gift-pegjs';
import { QuestionType } from '../../../Types/QuestionType'; import { QuestionType } from '../../../Types/QuestionType';
import LiveResultsComponent from '../../../components/LiveResults/LiveResults'; import LiveResultsComponent from '../../../components/LiveResults/LiveResults';
// import { QuestionService } from '../../../services/QuestionService'; // import { QuestionService } from '../../../services/QuestionService';
import webSocketService from '../../../services/WebsocketService'; import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
import { QuizType } from '../../../Types/QuizType'; import { QuizType } from '../../../Types/QuizType';
import './manageRoom.css'; import './manageRoom.css';
import { ENV_VARIABLES } from '../../../constants'; import { ENV_VARIABLES } from '../../../constants';
import { UserType } from '../../../Types/UserType'; import { StudentType, Answer } from '../../../Types/StudentType';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle'; import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
import { Refresh, Error } from '@mui/icons-material'; import { Refresh, Error } from '@mui/icons-material';
import UserWaitPage from '../../../components/UserWaitPage/UserWaitPage'; import StudentWaitPage from '../../../components/StudentWaitPage/StudentWaitPage';
import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton'; import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton';
import QuestionNavigation from '../../../components/QuestionNavigation/QuestionNavigation'; import QuestionNavigation from '../../../components/QuestionNavigation/QuestionNavigation';
import Question from '../../../components/Questions/Question'; import Question from '../../../components/Questions/Question';
@ -25,7 +25,7 @@ const ManageRoom: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [roomName, setRoomName] = useState<string>(''); const [roomName, setRoomName] = useState<string>('');
const [socket, setSocket] = useState<Socket | null>(null); const [socket, setSocket] = useState<Socket | null>(null);
const [users, setUsers] = useState<UserType[]>([]); const [students, setStudents] = useState<StudentType[]>([]);
const quizId = useParams<{ id: string }>(); const quizId = useParams<{ id: string }>();
const [quizQuestions, setQuizQuestions] = useState<QuestionType[] | undefined>(); const [quizQuestions, setQuizQuestions] = useState<QuestionType[] | undefined>();
const [quiz, setQuiz] = useState<QuizType | null>(null); const [quiz, setQuiz] = useState<QuizType | null>(null);
@ -74,7 +74,7 @@ const ManageRoom: React.FC = () => {
setSocket(null); setSocket(null);
setQuizQuestions(undefined); setQuizQuestions(undefined);
setCurrentQuestion(undefined); setCurrentQuestion(undefined);
setUsers([]); setStudents(new Array<StudentType>());
setRoomName(''); setRoomName('');
} }
}; };
@ -96,9 +96,10 @@ const ManageRoom: React.FC = () => {
socket.on('create-failure', () => { socket.on('create-failure', () => {
console.log('Error creating room.'); console.log('Error creating room.');
}); });
socket.on('user-joined', (user: UserType) => { socket.on('user-joined', (student: StudentType) => {
console.log(`Student joined: name = ${student.name}, id = ${student.id}`);
setUsers((prevUsers) => [...prevUsers, user]); setStudents((prevStudents) => [...prevStudents, student]);
if (quizMode === 'teacher') { if (quizMode === 'teacher') {
webSocketService.nextQuestion(roomName, currentQuestion); webSocketService.nextQuestion(roomName, currentQuestion);
@ -111,7 +112,8 @@ const ManageRoom: React.FC = () => {
setSocket(null); setSocket(null);
}); });
socket.on('user-disconnected', (userId: string) => { socket.on('user-disconnected', (userId: string) => {
setUsers((prevUsers) => prevUsers.filter((user) => user.id !== userId)); console.log(`Student left: id = ${userId}`);
setStudents((prevUsers) => prevUsers.filter((user) => user.id !== userId));
}); });
setSocket(socket); setSocket(socket);
}; };
@ -119,9 +121,8 @@ const ManageRoom: React.FC = () => {
useEffect(() => { useEffect(() => {
// This is here to make sure the correct value is sent when user join // This is here to make sure the correct value is sent when user join
if (socket) { if (socket) {
socket.on('user-joined', (user: UserType) => { console.log(`Listening for user-joined in room ${roomName}`);
socket.on('user-joined', (_student: StudentType) => {
setUsers((prevUsers) => [...prevUsers, user]);
if (quizMode === 'teacher') { if (quizMode === 'teacher') {
webSocketService.nextQuestion(roomName, currentQuestion); webSocketService.nextQuestion(roomName, currentQuestion);
@ -130,7 +131,122 @@ const ManageRoom: React.FC = () => {
} }
}); });
} }
}, [currentQuestion, 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]);
// useEffect(() => {
// if (socket) {
// const submitAnswerHandler = (answerData: answerSubmissionType) => {
// const { answer, idQuestion, username } = answerData;
// console.log(`Received answer from ${username} for question ${idQuestion}: ${answer}`);
// // print the list of current student names
// console.log('Current students:');
// students.forEach((student) => {
// console.log(student.name);
// });
// // Update the students state using the functional form of setStudents
// setStudents((prevStudents) => {
// let foundStudent = false;
// const updatedStudents = prevStudents.map((student) => {
// if (student.id === username) {
// foundStudent = true;
// const updatedAnswers = student.answers.map((ans) => {
// const newAnswer: Answer = { answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!), idQuestion };
// console.log(`Updating answer for ${student.name} for question ${idQuestion} to ${answer}`);
// return (ans.idQuestion === idQuestion ? { ...ans, newAnswer } : ans);
// }
// );
// return { ...student, answers: updatedAnswers };
// }
// return student;
// });
// if (!foundStudent) {
// console.log(`Student ${username} not found in the list of students in LiveResults`);
// }
// return updatedStudents;
// });
// // make a copy of the students array so we can update it
// // const updatedStudents = [...students];
// // const student = updatedStudents.find((student) => student.id === idUser);
// // if (!student) {
// // // this is a bad thing if an answer was submitted but the student isn't in the list
// // console.log(`Student ${idUser} not found in the list of students in LiveResults`);
// // return;
// // }
// // const isCorrect = checkIfIsCorrect(answer, idQuestion);
// // const newAnswer: Answer = { answer, isCorrect, idQuestion };
// // student.answers.push(newAnswer);
// // // print list of answers
// // console.log('Answers:');
// // student.answers.forEach((answer) => {
// // console.log(answer.answer);
// // });
// // setStudents(updatedStudents); // update the state
// };
// socket.on('submit-answer', submitAnswerHandler);
// return () => {
// socket.off('submit-answer');
// };
// }
// }, [socket]);
const nextQuestion = () => { const nextQuestion = () => {
if (!quizQuestions || !currentQuestion || !quiz?.content) return; if (!quizQuestions || !currentQuestion || !quiz?.content) return;
@ -171,8 +287,12 @@ const ManageRoom: React.FC = () => {
const launchTeacherMode = () => { const launchTeacherMode = () => {
const quizQuestions = initializeQuizQuestion(); const quizQuestions = initializeQuizQuestion();
console.log('launchTeacherMode - quizQuestions:', quizQuestions);
if (!quizQuestions) return; if (!quizQuestions) {
console.log('Error launching quiz (launchTeacherMode). No questions found.');
return;
}
setCurrentQuestion(quizQuestions[0]); setCurrentQuestion(quizQuestions[0]);
webSocketService.nextQuestion(roomName, quizQuestions[0]); webSocketService.nextQuestion(roomName, quizQuestions[0]);
@ -180,18 +300,20 @@ const ManageRoom: React.FC = () => {
const launchStudentMode = () => { const launchStudentMode = () => {
const quizQuestions = initializeQuizQuestion(); const quizQuestions = initializeQuizQuestion();
console.log('launchStudentMode - quizQuestions:', quizQuestions);
if (!quizQuestions) { if (!quizQuestions) {
console.log('Error launching quiz (launchStudentMode). No questions found.');
return; return;
} }
setQuizQuestions(quizQuestions);
webSocketService.launchStudentModeQuiz(roomName, quizQuestions); webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
}; };
const launchQuiz = () => { const launchQuiz = () => {
if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) { if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) {
// TODO: This error happens when token expires! Need to handle it properly // TODO: This error happens when token expires! Need to handle it properly
console.log('Error launching quiz. No socket, room name or no questions.'); console.log(`Error launching quiz. socket: ${socket}, roomName: ${roomName}, quiz: ${quiz}`);
return; return;
} }
switch (quizMode) { switch (quizMode) {
@ -217,6 +339,75 @@ const ManageRoom: React.FC = () => {
navigate('/teacher/dashboard'); 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) {
const question = questionInfo.question as GIFTQuestion;
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.text.text === answerText
);
} else if (question.type === 'Numerical') {
if (question.choices && !Array.isArray(question.choices)) {
if (
question.choices.type === 'high-low' &&
question.choices.numberHigh &&
question.choices.numberLow
) {
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return (
answerNumber <= question.choices.numberHigh &&
answerNumber >= question.choices.numberLow
);
}
}
}
if (question.choices && Array.isArray(question.choices)) {
if (
question.choices[0].text.type === 'range' &&
question.choices[0].text.number &&
question.choices[0].text.range
) {
const answerNumber = parseFloat(answerText);
const range = question.choices[0].text.range;
const correctAnswer = question.choices[0].text.number;
if (!isNaN(answerNumber)) {
return (
answerNumber <= correctAnswer + range &&
answerNumber >= correctAnswer - range
);
}
}
if (
question.choices[0].text.type === 'simple' &&
question.choices[0].text.number
) {
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return answerNumber === question.choices[0].text.number;
}
}
}
} else if (question.type === 'Short') {
return question.choices.some(
(choice) => choice.text.text.toUpperCase() === answerText.toUpperCase()
);
}
}
return false;
}
if (!roomName) { if (!roomName) {
return ( return (
<div className="center"> <div className="center">
@ -250,13 +441,13 @@ const ManageRoom: React.FC = () => {
<div className='centerTitle'> <div className='centerTitle'>
<div className='title'>Salle: {roomName}</div> <div className='title'>Salle: {roomName}</div>
<div className='userCount subtitle'>Utilisateurs: {users.length}/60</div> <div className='userCount subtitle'>Utilisateurs: {students.length}/60</div>
</div> </div>
<div className='dumb'></div> <div className='dumb'></div>
</div> </div>
{/* the following breaks the css (nested room classes) */} {/* the following breaks the css (if 'room' classes are nested) */}
<div className=''> <div className=''>
{quizQuestions ? ( {quizQuestions ? (
@ -293,7 +484,7 @@ const ManageRoom: React.FC = () => {
socket={socket} socket={socket}
questions={quizQuestions} questions={quizQuestions}
showSelectedQuestion={showSelectedQuestion} showSelectedQuestion={showSelectedQuestion}
students={users} students={students}
></LiveResultsComponent> ></LiveResultsComponent>
</div> </div>
@ -311,8 +502,8 @@ const ManageRoom: React.FC = () => {
) : ( ) : (
<UserWaitPage <StudentWaitPage
users={users} students={students}
launchQuiz={launchQuiz} launchQuiz={launchQuiz}
setQuizMode={setQuizMode} setQuizMode={setQuizMode}
/> />

View file

@ -1,6 +1,22 @@
// WebSocketService.tsx // WebSocketService.tsx
import { io, Socket } from 'socket.io-client'; import { io, Socket } from 'socket.io-client';
// Must (manually) sync these types to server/socket/socket.js
export type AnswerSubmissionToBackendType = {
roomName: string;
username: string;
answer: string | number | boolean;
idQuestion: number;
};
export type AnswerReceptionFromBackendType = {
idUser: string;
username: string;
answer: string | number | boolean;
idQuestion: number;
};
class WebSocketService { class WebSocketService {
private socket: Socket | null = null; private socket: Socket | null = null;
@ -51,19 +67,22 @@ class WebSocketService {
} }
} }
submitAnswer( submitAnswer(answerData: AnswerSubmissionToBackendType
roomName: string, // roomName: string,
answer: string | number | boolean, // answer: string | number | boolean,
username: string, // username: string,
idQuestion: string // idQuestion: string
) { ) {
if (this.socket) { if (this.socket) {
this.socket?.emit('submit-answer', { this.socket?.emit('submit-answer',
answer: answer, // {
roomName: roomName, // answer: answer,
username: username, // roomName: roomName,
idQuestion: idQuestion // username: username,
}); // idQuestion: idQuestion
// }
answerData
);
} }
} }
} }

View file

@ -125,7 +125,7 @@ describe("websocket server", () => {
answer: "answer1", answer: "answer1",
idQuestion: 1, idQuestion: 1,
}); });
teacherSocket.on("submit-answer", (answer) => { teacherSocket.on("submit-answer-room", (answer) => {
expect(answer).toEqual({ expect(answer).toEqual({
idUser: studentSocket.id, idUser: studentSocket.id,
username: "student1", username: "student1",

View file

@ -49,10 +49,15 @@ const setupWebsocket = (io) => {
io.sockets.adapter.rooms.get(enteredRoomName).size; io.sockets.adapter.rooms.get(enteredRoomName).size;
if (clientsInRoom <= MAX_USERS_PER_ROOM) { if (clientsInRoom <= MAX_USERS_PER_ROOM) {
const newStudent = {
id: socket.id,
name: username,
answers: [],
};
socket.join(enteredRoomName); socket.join(enteredRoomName);
socket socket
.to(enteredRoomName) .to(enteredRoomName)
.emit("user-joined", { name: username, id: socket.id }); .emit("user-joined", newStudent);
socket.emit("join-success"); socket.emit("join-success");
} else { } else {
socket.emit("join-failure", "La salle est remplie"); socket.emit("join-failure", "La salle est remplie");
@ -96,7 +101,7 @@ const setupWebsocket = (io) => {
}); });
socket.on("submit-answer", ({ roomName, username, answer, idQuestion }) => { socket.on("submit-answer", ({ roomName, username, answer, idQuestion }) => {
socket.to(roomName).emit("submit-answer", { socket.to(roomName).emit("submit-answer-room", {
idUser: socket.id, idUser: socket.id,
username, username,
answer, answer,