Skip to content

Commit

Permalink
chore [QCMPLUS-38][UI-USER-ROLE] - show users history exam and update
Browse files Browse the repository at this point in the history
Signed-off-by: Teclit <[email protected]>
  • Loading branch information
Teclit committed Aug 20, 2024
2 parents 64ca173 + 4542ac5 commit cfb66dd
Show file tree
Hide file tree
Showing 16 changed files with 247 additions and 123 deletions.
163 changes: 77 additions & 86 deletions qcmplusweb/src/components/Exam/Exam.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import './Exam.css';
import {getLoggedInUser} from "../../services/AuthService";

const MAX_QUESTIONS = 5;
const QUESTION_TIME_LIMIT = 50;
const QUESTION_TIME_LIMIT = 1;

const Exam = ({ quizId }) => {
const getUser = getLoggedInUser();
Expand All @@ -21,42 +21,16 @@ const Exam = ({ quizId }) => {
const [timer, setTimer] = useState(QUESTION_TIME_LIMIT);
const [score, setScore] = useState(0);

useEffect(() => {
const startQuiz = async () => {
setLoading(true);
try {
const response = await getQuestions(quizId);
if (response?.data?.length) {
const selectedQuestions = response.data.slice(0, MAX_QUESTIONS);
setQuestions(selectedQuestions);
setCurrentQuestionIndex(0);
setUserAnswers({});
setExamCompleted(false);
setTimer(QUESTION_TIME_LIMIT);
} else {
setError('No questions found for this quiz.');
}
} catch (error) {
console.error('Error fetching questions:', error);
setError('An error occurred while fetching questions.');
} finally {
setLoading(false);
}
};

if (quizId) {
startQuiz();
}
}, [quizId]);

const calculateScore = useCallback(() => {
return questions.reduce((totalScore, question) => {
let score = 0;
questions.forEach((question) => {
const correctAnswer = answers[question.questionId]?.find(answer => answer.isCorrect);
if (correctAnswer && userAnswers[question.questionId] === correctAnswer.answerId) {
return totalScore + 1;
score += 1;
}
return totalScore;
}, 0);
});
return score;
}, [questions, answers, userAnswers]);

const handleSubmit = useCallback(async () => {
Expand All @@ -67,61 +41,54 @@ const Exam = ({ quizId }) => {
};
try {
await submitExamSession(sessionData);
setScore(calculateScore());
const calculatedScore = calculateScore();
setScore(calculatedScore);
setExamCompleted(true);
setShowResults(true);
} catch (error) {
console.error('Error submitting exam session:', error);
setError('An error occurred while submitting the exam.');
}
}, [getUser.userId, quizId, userAnswers, calculateScore]);

const handleNextQuestion = useCallback(() => {
if (currentQuestionIndex < questions.length - 1) {
setCurrentQuestionIndex(prev => prev + 1);
} else {
setCurrentQuestionIndex((prev) => prev + 1);
} else if (currentQuestionIndex === questions.length - 1) {
handleSubmit();
}
}, [currentQuestionIndex, questions.length, handleSubmit]);

const handlePreviousQuestion = useCallback(() => {
if (currentQuestionIndex > 0) {
setCurrentQuestionIndex(prev => prev - 1);
}
}, [currentQuestionIndex]);

const handleAnswerChange = useCallback((questionId, answerId) => {
setUserAnswers(prev => ({
...prev,
[questionId]: answerId,
}));
}, []);

useEffect(() => {
const fetchAnswers = async (questionId) => {
const startQuiz = async () => {
setLoading(true);
try {
const response = await getAnswersByQuestionId(questionId);
if (response?.data) {
setAnswers(prev => ({ ...prev, [questionId]: response.data }));
const response = await getQuestions(quizId);
if (response && response.data) {
const selectedQuestions = response.data.slice(0, MAX_QUESTIONS);
setQuestions(selectedQuestions);
setCurrentQuestionIndex(0);
setUserAnswers({});
setExamCompleted(false);
setTimer(QUESTION_TIME_LIMIT);
} else {
setError('No answers found for this question.');
setError('No questions found for this quiz.');
}
} catch (error) {
console.error('Error fetching answers:', error);
setError('An error occurred while fetching answers.');
setError('An error occurred while fetching questions.');
} finally {
setLoading(false);
}
};

if (questions.length > 0) {
fetchAnswers(questions[currentQuestionIndex].questionId);
setTimer(QUESTION_TIME_LIMIT);
if (quizId) {
startQuiz();
}
}, [questions, currentQuestionIndex]);
}, [quizId]);

useEffect(() => {
if (questions.length > 0 && timer > 0) {
const intervalId = setInterval(() => {
setTimer(prevTimer => prevTimer - 1);
setTimer((prevTimer) => prevTimer - 1);
}, 1000);

return () => clearInterval(intervalId);
Expand All @@ -130,6 +97,40 @@ const Exam = ({ quizId }) => {
}
}, [questions, timer, handleNextQuestion]);

const fetchAnswers = async (questionId) => {
try {
const response = await getAnswersByQuestionId(questionId);
if (response && response.data) {
setAnswers((prev) => ({ ...prev, [questionId]: response.data }));
} else {
setError('No answers found for this question.');
}
} catch (error) {
console.error('Error fetching answers:', error);
setError('An error occurred while fetching answers.');
}
};

useEffect(() => {
if (questions.length > 0) {
fetchAnswers(questions[currentQuestionIndex].questionId);
setTimer(QUESTION_TIME_LIMIT);
}
}, [questions, currentQuestionIndex]);

const handleAnswerChange = (questionId, answerId) => {
setUserAnswers((prev) => ({
...prev,
[questionId]: answerId,
}));
};

const handlePreviousQuestion = () => {
if (currentQuestionIndex > 0) {
setCurrentQuestionIndex((prev) => prev - 1);
}
};

if (loading) {
return (
<div className="d-flex justify-content-center align-items-center" style={{ height: '100vh' }}>
Expand All @@ -145,38 +146,29 @@ const Exam = ({ quizId }) => {
<Container className="exam-results-container">
<h2 className="text-center">Exam Results</h2>
<h3 className="text-center">Your Score: {score} / {questions.length}</h3>
<div className="exam-results-list mb-3 p-5">
<div className="exam-results-list mb-3 p-3">
{questions.map((question, index) => (
<div key={question.questionId} className="mb-4">
<h5>{index + 1}. {question.quiz.title}: {question.questionText}</h5>
<h5>{index + 1}. {question.questionText}</h5>
<ListGroup>
{answers[question.questionId]?.map(answer => {
const answerClass = answer.isCorrect
? 'bg-success text-white'
: (userAnswers[question.questionId] === answer.answerId
? 'bg-danger text-white'
: ''); // No special background for non-selected incorrect answers

return (
<ListGroup.Item
key={answer.answerId}
className={`mb-2 p-2 ${answerClass} font-weight-bold`}
>
{answer.answerText}
{answer.isCorrect && <strong> (Correct Answer)</strong>}
{userAnswers[question.questionId] === answer.answerId && !answer.isCorrect && (
<strong> (Your Answer)</strong>
)}
</ListGroup.Item>
);
})}
{answers[question.questionId]?.map((answer) => (
<ListGroup.Item
key={answer.answerId}
className={`mb-2 p-2 ${answer.isCorrect ? 'bg-success text-white font-weight-bold' : userAnswers[question.questionId] === answer.answerId ? 'bg-danger text-white' : ''}`}
>
{answer.answerText}
{answer.isCorrect && <strong> (Correct Answer)</strong>}
{userAnswers[question.questionId] === answer.answerId && !answer.isCorrect && (
<strong> (Your Answer)</strong>
)}
</ListGroup.Item>
))}
</ListGroup>
</div>
))}
</div>
<Alert variant="success" className="text-center">Exam completed successfully. Thank you!</Alert>
</Container>

);
}

Expand All @@ -199,10 +191,9 @@ const Exam = ({ quizId }) => {
<h5 className="timer-header text-center">Time Left: {timer} seconds</h5>
{currentQuestion ? (
<>
<h5 className="quiz-title text-center">{currentQuestion.quiz.title}</h5>
<p className="question-text">{currentQuestion.questionText}</p>
<Form>
{answers[currentQuestion.questionId]?.map(answer => (
{answers[currentQuestion.questionId]?.map((answer) => (
<Form.Check
key={answer.answerId}
type="radio"
Expand Down
10 changes: 10 additions & 0 deletions qcmplusweb/src/components/ExamHistory/ExamHistory.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

.exam-taken-list .carousel-control-next-icon {
background-image: url('data:image/svg+xml;charset=utf8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 fill=%27%23ffffff%27 viewBox=%270 0 16 16%27%3E%3Cpath fill=%27%23ffffff%27 d=%27M11.354 8l-4.646 4.646-.708-.708L10.293 8 6 3.707l.708-.708L11.354 8z%27/%3E%3C/svg%3E') !important;
background-color: var(--bg-default-dark-hover-color);
}

.exam-taken-list .carousel-control-prev-icon {
background-image: url('data:image/svg+xml;charset=utf8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 fill=%27%23ffffff%27 viewBox=%270 0 16 16%27%3E%3Cpath fill=%27%23ffffff%27 d=%27M4.646 8l4.646-4.646.708.708L5.707 8l4.293 4.293-.708.708L4.646 8z%27/%3E%3C/svg%3E') !important;
background-color: var(--bg-default-dark-hover-color);
}
87 changes: 87 additions & 0 deletions qcmplusweb/src/components/ExamHistory/ExamHistory.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, {useCallback, useEffect, useState} from 'react';
import {Card, Carousel} from 'react-bootstrap';
import './ExamHistory.css';
import {getAllUserExamHistory} from "../../services/ExamService";

const ExamTaken = ({userId}) => {
const [examsTaken, setExamsTaken] = useState([]);
const [error, setError] = useState(null);

const Img = ({title}) => {
const link = title ? `Images/${title}.jpg` : 'Images/default.jpg';
return (
<div className="img-container">
<img src={link} alt={title || 'Default Image'} className="img-fluid"/>
</div>
);
};

const fetchExams = useCallback(async () => {
try {
const response = await getAllUserExamHistory(userId);
setExamsTaken(response.data);
} catch (error) {
console.error('Error fetching exams:', error);
setError('Failed to load exams. Please try again later.');
}
}, [userId]);

useEffect(() => {
fetchExams();
}, [fetchExams]);

if (error) {
return <div className="alert alert-danger">{error}</div>;
}

const chunkArray = (array, size) => {
const chunkedArr = [];
for (let i = 0; i < array.length; i += size) {
chunkedArr.push(array.slice(i, i + size));
}
return chunkedArr;
};

const formatDate = (dateString) => {
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-based
const year = String(date.getFullYear()).slice(-2);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');

return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`;
}

const chunkedExamsTaken = chunkArray(examsTaken, 4);

return (
<>
<h3 className="text-bold text-center p-4">Evaluate Your Skills: Take the Quiz</h3>
<Carousel className="exam-taken-list" indicators={false}>
{chunkedExamsTaken.map((examChunk, index) => (
<Carousel.Item key={index}>
<div className="d-flex justify-content-center">
{examChunk.map((exam, examIndex) => (
<Card key={examIndex} className="m-2" style={{width: '18rem'}}>
<Img title={exam.quiz ? exam.quiz.title : null}/>
<Card.Body className="text-center">
<Card.Title className="text-bold text-decoration-underline">
{exam.quiz ? exam.quiz.title : 'No Title Available'}
</Card.Title>
<Card.Text>Result: {exam.score}</Card.Text>
<Card.Text>Time Spent: {exam.timeSpent}h</Card.Text>
<Card.Text>Date: {formatDate(exam.dateExam)}</Card.Text>
</Card.Body>
</Card>
))}
</div>
</Carousel.Item>
))}
</Carousel>
</>
);
};

export default ExamTaken;
7 changes: 4 additions & 3 deletions qcmplusweb/src/pages/main/Main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import QuizList from "../../components/Quiz/QuizList";
import ExamSelected from "../../components/ExamSelected/ExamSelected";
import Exam from "../../components/Exam/Exam";
import {useNavigate} from "react-router-dom";
import ExamsTaken from "../../components/ExamHistory/ExamHistory";

const Main = () => {
const isAdmin = isAdminUser();
Expand Down Expand Up @@ -45,7 +46,7 @@ const Main = () => {

const renderContent = () => {
if (examStarted && quizId) {
return <Exam quizId={quizId} />;
return <Exam quizId={quizId}/>;
}

if (!showUserList) {
Expand All @@ -68,9 +69,9 @@ const Main = () => {
case 'Features':
return <h1><AiFillWarning />Features: en cours de construction</h1>;
case 'TakeExams':
return <ExamSelected quizId={quizId} onStartExam={handleStartExam} />; // Pass handleStartExam
return <ExamSelected quizId={quizId || 1} onStartExam={handleStartExam} />; // Pass handleStartExam
case 'HistoryExams':
return <h1><AiFillWarning />History Exams : en cours de construction</h1>;
return <ExamsTaken userId={getUser.userId}></ExamsTaken>;
case 'UserDashboard':
return <QuizList onTakeQuiz={handleTakeQuiz} />;
case 'AdminDashboard':
Expand Down
Loading

0 comments on commit cfb66dd

Please sign in to comment.