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é
import { render, screen, fireEvent } from '@testing-library/react';
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', () => {
const mockUsers = [
{ id: '1', name: 'User1' },
{ id: '2', name: 'User2' },
{ id: '3', name: 'User3' },
describe('StudentWaitPage Component', () => {
const mockUsers: StudentType[] = [
{ id: '1', name: 'User1', answers: new Array<Answer>() },
{ id: '2', name: 'User2', answers: new Array<Answer>() },
{ id: '3', name: 'User3', answers: new Array<Answer>() },
];
const mockProps = {
users: mockUsers,
students: mockUsers,
launchQuiz: jest.fn(),
roomName: 'Test Room',
setQuizMode: jest.fn(),
};
test('renders UserWaitPage with correct content', () => {
render(<UserWaitPage {...mockProps} />);
test('renders StudentWaitPage with correct content', () => {
render(<StudentWaitPage {...mockProps} />);
//expect(screen.getByText(/Test Room/)).toBeInTheDocument();
@ -31,7 +32,7 @@ describe('UserWaitPage Component', () => {
});
test('clicking on "Lancer" button opens LaunchQuizDialog', () => {
render(<UserWaitPage {...mockProps} />);
render(<StudentWaitPage {...mockProps} />);
fireEvent.click(screen.getByRole('button', { name: /Lancer/i }));

View file

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

View file

@ -47,7 +47,7 @@ describe('TeacherModeQuiz', () => {
act(() => {
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();
});

View file

@ -1,7 +1,6 @@
// LiveResults.tsx
import React, { useEffect, useMemo, useState } from 'react';
import React, { useMemo, useState } from 'react';
import { Socket } from 'socket.io-client';
import { GIFTQuestion } from 'gift-pegjs';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons';
import { QuestionType } from '../../Types/QuestionType';
@ -15,11 +14,12 @@ import {
Table,
TableBody,
TableCell,
TableContainer,
TableFooter,
TableHead,
TableRow
} from '@mui/material';
import { UserType } from '../../Types/UserType';
import { StudentType } from '../../Types/StudentType';
import { formatLatex } from '../GiftTemplate/templates/TextType';
interface LiveResultsProps {
@ -27,79 +27,121 @@ interface LiveResultsProps {
questions: QuestionType[];
showSelectedQuestion: (index: number) => void;
quizMode: 'teacher' | 'student';
students: UserType[]
students: StudentType[]
}
interface Answer {
answer: string | number | boolean;
isCorrect: boolean;
idQuestion: number;
}
// interface Answer {
// answer: string | number | boolean;
// isCorrect: boolean;
// idQuestion: number;
// }
interface StudentResult {
username: string;
idUser: string;
answers: Answer[];
}
// interface StudentResult {
// username: string;
// idUser: string;
// 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 [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;
useEffect(() => {
// Set student list before starting
let newStudents: StudentResult[] = [];
// useEffect(() => {
// // Initialize the map with the current students
// const newStudentResultsMap = new Map<string, StudentResult>();
for (const student of students as UserType[]) {
newStudents.push({ username: student.name, idUser: student.id, answers: [] })
}
// for (const student of students) {
// 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(() => {
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 }] }
];
}
});
};
// for (const student of students as StudentType[]) {
// }
socket.on('submit-answer', submitAnswerHandler);
return () => {
socket.off('submit-answer');
};
}
}, [socket]);
// }, [students])
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) {
return 0;
}
@ -124,93 +166,103 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
const classAverage: number = useMemo(() => {
let classTotal = 0;
studentResults.forEach((student) => {
students.forEach((student) => {
classTotal += getStudentGrade(student);
});
return classTotal / studentResults.length;
}, [studentResults]);
return classTotal / students.length;
}, [students]);
const getCorrectAnswersPerQuestion = (index: number): number => {
return (
(studentResults.filter((student) =>
(students.filter((student) =>
student.answers.some(
(answer) =>
parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
)
).length /
studentResults.length) *
100
).length / students.length) * 100
);
};
function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number): boolean {
const questionInfo = questions.find((q) =>
q.question.id ? q.question.id === idQuestion.toString() : false
) as QuestionType | undefined;
// (studentResults.filter((student) =>
// student.answers.some(
// (answer) =>
// parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
// )
// ).length /
// studentResults.length) *
// 100
// );
// };
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;
}
// function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number): 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;
// }
return (
<div>
@ -243,145 +295,147 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
</div>
<div className="table-container">
<Table size="small" component={Paper}>
<TableHead>
<TableRow>
<TableCell className="sticky-column">
<div className="text-base text-bold">Nom d'utilisateur</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index}
sx={{
textAlign: 'center',
cursor: 'pointer',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
onClick={() => showSelectedQuestion(index)}
>
<div className="text-base text-bold blue">{`Q${index + 1}`}</div>
</TableCell>
))}
<TableCell
className="sticky-header"
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base text-bold">% réussite</div>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{studentResults.map((student) => (
<TableRow key={student.idUser}>
<TableCell
className="sticky-column"
sx={{
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base">
{showUsernames ? student.username : '******'}
</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => {
const answer = student.answers.find(
(answer) => parseInt(answer.idQuestion.toString()) === index + 1
);
const answerText = answer ? answer.answer.toString() : '';
const isCorrect = answer ? answer.isCorrect : false;
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell className="sticky-column">
<div className="text-base text-bold">Nom d'utilisateur</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index}
sx={{
textAlign: 'center',
cursor: 'pointer',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
onClick={() => showSelectedQuestion(index)}
>
<div className="text-base text-bold blue">{`Q${index + 1}`}</div>
</TableCell>
))}
<TableCell
className="sticky-header"
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base text-bold">% réussite</div>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{students.map((student) => (
<TableRow key={student.id}>
<TableCell
className="sticky-column"
sx={{
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base">
{showUsernames ? student.name : '******'}
</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => {
const answer = student.answers.find(
(answer) => parseInt(answer.idQuestion.toString()) === index + 1
);
const answerText = answer ? answer.answer.toString() : '';
const isCorrect = answer ? answer.isCorrect : false;
return (
return (
<TableCell
key={index}
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
className={
answerText === ''
? ''
: isCorrect
? 'correct-answer'
: 'incorrect-answer'
}
>
{showCorrectAnswers ? (
<div>{formatLatex(answerText)}</div>
) : isCorrect ? (
<FontAwesomeIcon icon={faCheck} />
) : (
answerText !== '' && (
<FontAwesomeIcon icon={faCircleXmark} />
)
)}
</TableCell>
);
})}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
>
{getStudentGrade(student).toFixed()} %
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow sx={{ backgroundColor: '#d3d3d34f' }}>
<TableCell className="sticky-column" sx={{ color: 'black' }}>
<div className="text-base text-bold">% réussite</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index}
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
className={
answerText === ''
? ''
: isCorrect
? 'correct-answer'
: 'incorrect-answer'
}
>
{showCorrectAnswers ? (
<div>{formatLatex(answerText)}</div>
) : isCorrect ? (
<FontAwesomeIcon icon={faCheck} />
) : (
answerText !== '' && (
<FontAwesomeIcon icon={faCircleXmark} />
)
)}
{students.length > 0
? `${getCorrectAnswersPerQuestion(index).toFixed()} %`
: '-'}
</TableCell>
);
})}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
>
{getStudentGrade(student).toFixed()} %
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow sx={{ backgroundColor: '#d3d3d34f' }}>
<TableCell className="sticky-column" sx={{ color: 'black' }}>
<div className="text-base text-bold">% réussite</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index}
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
>
{studentResults.length > 0
? `${getCorrectAnswersPerQuestion(index).toFixed()} %`
: '-'}
</TableCell>
))}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
fontSize: '1rem',
color: 'rgba(0, 0, 0)'
}}
>
{studentResults.length > 0 ? `${classAverage.toFixed()} %` : '-'}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
))}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
fontSize: '1rem',
color: 'rgba(0, 0, 0)'
}}
>
{students.length > 0 ? `${classAverage.toFixed()} %` : '-'}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</TableContainer>
</div>
</div>
);
};

View file

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

View file

@ -1,17 +1,17 @@
import { Button, Chip, Grid } from '@mui/material';
import { UserType } from '../../Types/UserType';
import { Box, Button, Chip } from '@mui/material';
import { StudentType } from '../../Types/StudentType';
import { PlayArrow } from '@mui/icons-material';
import LaunchQuizDialog from '../LaunchQuizDialog/LaunchQuizDialog';
import { useState } from 'react';
import './userWaitPage.css';
import './studentWaitPage.css';
interface Props {
users: UserType[];
students: StudentType[];
launchQuiz: () => 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);
return (
@ -30,15 +30,15 @@ const UserWaitPage: React.FC<Props> = ({ users, launchQuiz, setQuizMode }) => {
<div className="students">
<Grid container spacing={3}>
<Box display="flex" flexWrap="wrap" gap={3}>
{users.map((user, index) => (
<Grid item key={user.name + index}>
<Chip label={user.name} sx={{ width: '100%' }} />
</Grid>
{students.map((student, index) => (
<Box key={student.name + index} >
<Chip label={student.name} sx={{ width: '100%' }} />
</Box>
))}
</Grid>
</Box>
</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 {
questionInfos: QuestionType;
submitAnswer: (answer: string | number | boolean, idQuestion: string) => void;
submitAnswer: (answer: string | number | boolean, idQuestion: number) => void;
disconnectWebSocket: () => void;
}
@ -29,7 +29,7 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
}, [questionInfos]);
const handleOnSubmitAnswer = (answer: string | number | boolean) => {
const idQuestion = questionInfos.question.id || '-1';
const idQuestion = Number(questionInfos.question.id) || -1;
submitAnswer(answer, idQuestion);
setFeedbackMessage(`Votre réponse est "${answer.toString()}".`);
setIsFeedbackDialogOpen(true);

View file

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

View file

@ -2,20 +2,20 @@
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Socket } from 'socket.io-client';
import { parse } from 'gift-pegjs';
import { GIFTQuestion, parse } from 'gift-pegjs';
import { QuestionType } from '../../../Types/QuestionType';
import LiveResultsComponent from '../../../components/LiveResults/LiveResults';
// import { QuestionService } from '../../../services/QuestionService';
import webSocketService from '../../../services/WebsocketService';
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
import { QuizType } from '../../../Types/QuizType';
import './manageRoom.css';
import { ENV_VARIABLES } from '../../../constants';
import { UserType } from '../../../Types/UserType';
import { StudentType, Answer } from '../../../Types/StudentType';
import { Button } from '@mui/material';
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
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 QuestionNavigation from '../../../components/QuestionNavigation/QuestionNavigation';
import Question from '../../../components/Questions/Question';
@ -25,7 +25,7 @@ const ManageRoom: React.FC = () => {
const navigate = useNavigate();
const [roomName, setRoomName] = useState<string>('');
const [socket, setSocket] = useState<Socket | null>(null);
const [users, setUsers] = useState<UserType[]>([]);
const [students, setStudents] = useState<StudentType[]>([]);
const quizId = useParams<{ id: string }>();
const [quizQuestions, setQuizQuestions] = useState<QuestionType[] | undefined>();
const [quiz, setQuiz] = useState<QuizType | null>(null);
@ -74,7 +74,7 @@ const ManageRoom: React.FC = () => {
setSocket(null);
setQuizQuestions(undefined);
setCurrentQuestion(undefined);
setUsers([]);
setStudents(new Array<StudentType>());
setRoomName('');
}
};
@ -96,9 +96,10 @@ const ManageRoom: React.FC = () => {
socket.on('create-failure', () => {
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') {
webSocketService.nextQuestion(roomName, currentQuestion);
@ -111,7 +112,8 @@ const ManageRoom: React.FC = () => {
setSocket(null);
});
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);
};
@ -119,9 +121,8 @@ const ManageRoom: React.FC = () => {
useEffect(() => {
// This is here to make sure the correct value is sent when user join
if (socket) {
socket.on('user-joined', (user: UserType) => {
setUsers((prevUsers) => [...prevUsers, user]);
console.log(`Listening for user-joined in room ${roomName}`);
socket.on('user-joined', (_student: StudentType) => {
if (quizMode === 'teacher') {
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 = () => {
if (!quizQuestions || !currentQuestion || !quiz?.content) return;
@ -171,8 +287,12 @@ const ManageRoom: React.FC = () => {
const launchTeacherMode = () => {
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]);
webSocketService.nextQuestion(roomName, quizQuestions[0]);
@ -180,18 +300,20 @@ const ManageRoom: React.FC = () => {
const launchStudentMode = () => {
const quizQuestions = initializeQuizQuestion();
console.log('launchStudentMode - quizQuestions:', quizQuestions);
if (!quizQuestions) {
console.log('Error launching quiz (launchStudentMode). No questions found.');
return;
}
setQuizQuestions(quizQuestions);
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. No socket, room name or no questions.');
console.log(`Error launching quiz. socket: ${socket}, roomName: ${roomName}, quiz: ${quiz}`);
return;
}
switch (quizMode) {
@ -217,6 +339,75 @@ const ManageRoom: React.FC = () => {
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) {
return (
<div className="center">
@ -250,13 +441,13 @@ const ManageRoom: React.FC = () => {
<div className='centerTitle'>
<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 className='dumb'></div>
</div>
{/* the following breaks the css (nested room classes) */}
{/* the following breaks the css (if 'room' classes are nested) */}
<div className=''>
{quizQuestions ? (
@ -293,7 +484,7 @@ const ManageRoom: React.FC = () => {
socket={socket}
questions={quizQuestions}
showSelectedQuestion={showSelectedQuestion}
students={users}
students={students}
></LiveResultsComponent>
</div>
@ -311,8 +502,8 @@ const ManageRoom: React.FC = () => {
) : (
<UserWaitPage
users={users}
<StudentWaitPage
students={students}
launchQuiz={launchQuiz}
setQuizMode={setQuizMode}
/>

View file

@ -1,6 +1,22 @@
// WebSocketService.tsx
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 {
private socket: Socket | null = null;
@ -51,19 +67,22 @@ class WebSocketService {
}
}
submitAnswer(
roomName: string,
answer: string | number | boolean,
username: string,
idQuestion: string
submitAnswer(answerData: AnswerSubmissionToBackendType
// roomName: string,
// answer: string | number | boolean,
// username: string,
// idQuestion: string
) {
if (this.socket) {
this.socket?.emit('submit-answer', {
answer: answer,
roomName: roomName,
username: username,
idQuestion: idQuestion
});
this.socket?.emit('submit-answer',
// {
// answer: answer,
// roomName: roomName,
// username: username,
// idQuestion: idQuestion
// }
answerData
);
}
}
}

View file

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

View file

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