EvalueTonSavoir/client/src/components/LiveResults/LiveResults.tsx

448 lines
19 KiB
TypeScript
Raw Normal View History

2024-03-29 20:08:34 -04:00
// LiveResults.tsx
import React, { useEffect, 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';
import './liveResult.css';
import {
FormControlLabel,
FormGroup,
Paper,
Switch,
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableRow
} from '@mui/material';
2024-09-25 14:53:17 -04:00
import { StudentType, Answer } from '../../Types/StudentType';
import { formatLatex } from '../GiftTemplate/templates/TextType';
2024-03-29 20:08:34 -04:00
interface LiveResultsProps {
socket: Socket | null;
questions: QuestionType[];
showSelectedQuestion: (index: number) => void;
quizMode: 'teacher' | 'student';
2024-09-25 14:53:17 -04:00
connectedStudents: StudentType[]
2024-03-29 20:08:34 -04:00
}
2024-09-25 14:53:17 -04:00
// interface Answer {
// answer: string | number | boolean;
// isCorrect: boolean;
// idQuestion: number;
// }
2024-03-29 20:08:34 -04:00
2024-09-25 14:53:17 -04:00
// interface StudentResult {
// username: string;
// idUser: string;
// answers: Answer[];
// }
2024-03-29 20:08:34 -04:00
2024-09-25 14:53:17 -04:00
const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelectedQuestion, connectedStudents }) => {
2024-03-29 20:08:34 -04:00
const [showUsernames, setShowUsernames] = useState<boolean>(false);
const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(false);
2024-09-25 14:53:17 -04:00
const [students, setStudents] = useState<StudentType[]>(connectedStudents);
// const [studentResultsMap, setStudentResultsMap] = useState<Map<string, StudentResult>>(new Map());
2024-03-29 20:08:34 -04:00
const maxQuestions = questions.length;
2024-09-25 14:53:17 -04:00
// useEffect(() => {
// // Initialize the map with the current students
// const newStudentResultsMap = new Map<string, StudentResult>();
2024-03-29 20:08:34 -04:00
2024-09-25 14:53:17 -04:00
// for (const student of students) {
// newStudentResultsMap.set(student.id, { username: student.name, idUser: student.id, answers: [] });
// }
2024-03-29 20:08:34 -04:00
2024-09-25 14:53:17 -04:00
// setStudentResultsMap(newStudentResultsMap);
// }, [])
2024-03-29 20:08:34 -04:00
// update when students change
2024-09-25 14:53:17 -04:00
// useEffect(() => {
// // studentResultsMap is inconsistent with students -- need to update
2024-09-25 14:53:17 -04:00
// for (const student of students as StudentType[]) {
// }
// }, [students])
useEffect(() => {
// Update the students state when the initialStudents prop changes
setStudents(connectedStudents);
console.log(`useEffect: connectedStudents: ${JSON.stringify(connectedStudents)}`);
2024-09-25 14:53:17 -04:00
}, [connectedStudents]);
2024-03-29 20:08:34 -04:00
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;
});
2024-09-25 14:53:17 -04:00
// 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
2024-03-29 20:08:34 -04:00
};
socket.on('submit-answer', submitAnswerHandler);
return () => {
socket.off('submit-answer');
};
}
}, [socket]);
2024-09-25 14:53:17 -04:00
const getStudentGrade = (student: StudentType): number => {
2024-03-29 20:08:34 -04:00
if (student.answers.length === 0) {
return 0;
}
const uniqueQuestions = new Set();
let correctAnswers = 0;
for (const answer of student.answers) {
const { idQuestion, isCorrect } = answer;
if (!uniqueQuestions.has(idQuestion)) {
uniqueQuestions.add(idQuestion);
if (isCorrect) {
correctAnswers++;
}
}
}
return (correctAnswers / questions.length) * 100;
};
const classAverage: number = useMemo(() => {
let classTotal = 0;
2024-09-25 14:53:17 -04:00
students.forEach((student) => {
2024-03-29 20:08:34 -04:00
classTotal += getStudentGrade(student);
});
2024-09-25 14:53:17 -04:00
return classTotal / students.length;
}, [students]);
2024-03-29 20:08:34 -04:00
const getCorrectAnswersPerQuestion = (index: number): number => {
return (
2024-09-25 14:53:17 -04:00
(students.filter((student) =>
2024-03-29 20:08:34 -04:00
student.answers.some(
(answer) =>
parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
)
2024-09-25 14:53:17 -04:00
).length / students.length) * 100
2024-03-29 20:08:34 -04:00
);
};
// (studentResults.filter((student) =>
// student.answers.some(
// (answer) =>
// parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
// )
// ).length /
// studentResults.length) *
// 100
// );
// };
2024-03-29 20:08:34 -04:00
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>
<div className="action-bar mb-1">
<div className="text-2xl text-bold">Résultats du quiz</div>
<FormGroup row>
<FormControlLabel
label={<div className="text-sm">Afficher les noms</div>}
control={
<Switch
value={showUsernames}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setShowUsernames(e.target.checked)
}
/>
}
/>
<FormControlLabel
label={<div className="text-sm">Afficher les réponses</div>}
control={
<Switch
value={showCorrectAnswers}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setShowCorrectAnswers(e.target.checked)
}
/>
}
/>
</FormGroup>
</div>
<div className="table-container">
<Table size="small" component={Paper}>
2024-03-29 20:08:34 -04:00
<TableHead>
<TableRow>
<TableCell className="sticky-column">
2024-03-29 20:08:34 -04:00
<div className="text-base text-bold">Nom d'utilisateur</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index}
sx={{
textAlign: 'center',
cursor: 'pointer',
2024-03-29 20:08:34 -04:00
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"
2024-03-29 20:08:34 -04:00
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>
2024-09-25 14:53:17 -04:00
{students.map((student) => (
<TableRow key={student.id}>
2024-03-29 20:08:34 -04:00
<TableCell
className="sticky-column"
2024-03-29 20:08:34 -04:00
sx={{
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base">
2024-09-25 14:53:17 -04:00
{showUsernames ? student.name : '******'}
2024-03-29 20:08:34 -04:00
</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;
2024-03-29 20:08:34 -04:00
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>
2024-03-29 20:08:34 -04:00
) : 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' }}>
2024-03-29 20:08:34 -04:00
<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)'
}}
>
2024-09-25 14:53:17 -04:00
{students.length > 0
2024-03-29 20:08:34 -04:00
? `${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)'
}}
>
2024-09-25 14:53:17 -04:00
{students.length > 0 ? `${classAverage.toFixed()} %` : '-'}
2024-03-29 20:08:34 -04:00
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
</div>
2024-03-29 20:08:34 -04:00
);
};
export default LiveResults;