diff --git a/src/components/Exam/PreventBackButton.tsx b/src/components/Exam/PreventBackButton.tsx
new file mode 100644
index 0000000..dc3d474
--- /dev/null
+++ b/src/components/Exam/PreventBackButton.tsx
@@ -0,0 +1,26 @@
+import { useEffect } from 'react';
+
+export const PreventBackButton = () => {
+ useEffect(() => {
+ const preventBack = (event:any) => {
+ event.preventDefault();
+ event.returnValue = '';
+ window.history.pushState(null, document.title, window.location.href); // Push a new state to the history stack
+ };
+
+ // Add event listener for popstate event
+ window.addEventListener('popstate', preventBack);
+
+ // Push a state to prevent going back initially
+ window.history.pushState(null, document.title, window.location.href);
+
+ return () => {
+ // Clean up the event listener on component unmount
+ window.removeEventListener('popstate', preventBack);
+ };
+ }, []);
+
+ return null;
+};
+
+export default PreventBackButton;
diff --git a/src/components/Exam/TimeContainer.tsx b/src/components/Exam/TimeContainer.tsx
index a016913..39d011f 100644
--- a/src/components/Exam/TimeContainer.tsx
+++ b/src/components/Exam/TimeContainer.tsx
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { QuestionIndexes } from './QuestionIndexes';
import { CountdownTimer } from './CountdownTimer';
import axios from 'axios';
+import { useEffect } from 'react';
import './styles.css';
interface TimeContainerProps {
@@ -22,6 +23,15 @@ export const TimeContainer = ({
}: TimeContainerProps) => {
const navigate = useNavigate();
+ // Calculate remaining time
+ const endTime = sessionStorage.getItem('endTime');
+ const remainingTimeInMilliseconds =
+ endTime ? new Date(endTime).getTime() - new Date().getTime() : 0;
+
+ const remainingTimeInSeconds = Math.max(0, Math.floor(remainingTimeInMilliseconds / 1000));
+ const remainingMinutes = Math.floor(remainingTimeInSeconds / 60);
+ const remainingSeconds = remainingTimeInSeconds % 60;
+
const handleSubmitExam = async () => {
const token = sessionStorage.getItem('accessToken');
const sessionId = sessionStorage.getItem('sessionId');
@@ -30,7 +40,6 @@ export const TimeContainer = ({
message.error('Failed to submit exam. Please try again.');
return;
}
-
try {
const response = await axios.put(
@@ -55,13 +64,28 @@ export const TimeContainer = ({
}
};
+ // Use effect to automatically submit the exam when time expires
+ useEffect(() => {
+ if (remainingTimeInSeconds <= 0) {
+ handleSubmitExam();
+ }
+ }, [remainingTimeInSeconds]);
+
return (
Time Remaining
-
+ {remainingTimeInSeconds > 0 ? (
+
+ ) : (
+ Time's up!
+ )}
Submit Exam
diff --git a/src/components/Exam/index.ts b/src/components/Exam/index.ts
index fe551c4..a390116 100644
--- a/src/components/Exam/index.ts
+++ b/src/components/Exam/index.ts
@@ -5,3 +5,4 @@ export {QuestionIndexes} from './QuestionIndexes.tsx';
export {EssayQuestionView} from './EssayQuestionView.tsx';
export {McqQuestionView} from './McqQuestionView.tsx';
export {SortingComponent} from './SortingComponent.tsx';
+export {PreventBackButton} from './PreventBackButton.tsx';
diff --git a/src/pages/candidate/CandidateDashboard.tsx b/src/pages/candidate/CandidateDashboard.tsx
index cd9c32b..1417acb 100644
--- a/src/pages/candidate/CandidateDashboard.tsx
+++ b/src/pages/candidate/CandidateDashboard.tsx
@@ -17,6 +17,7 @@ import {
} from 'antd';
import { getLoggedInUser } from '../../utils/authUtils';
import { useNavigate } from 'react-router-dom';
+import axios from 'axios';
import { getCandidateExams } from '../../api/services/candidate';
import { Exam } from '../../types';
import moment from 'moment';
@@ -33,18 +34,57 @@ export const CandidateDashboard = () => {
// Example API call simulation
useEffect(() => {
- // Simulate checking for an ongoing exam
const checkOngoingExam = async () => {
- const ongoingExam = true; // Replace this with actual API call
- if (ongoingExam) {
- setIsModalVisible(false);
+ const userString = sessionStorage.getItem('user'); // Retrieve the user string
+ const token = sessionStorage.getItem('accessToken'); // Retrieve the access token
+
+ if (!userString || !token) {
+ console.error("User information or token not found in session storage.");
+ return;
+ }
+
+ const user = JSON.parse(userString); // Parse the user object
+ const candidateId = user?.id; // Extract the candidateId
+
+ if (!candidateId) {
+ console.error("Candidate ID not found in user object.");
+ return;
+ }
+
+ try {
+ const response = await axios.post(
+ 'http://localhost:8080/api/v1/candidate/check-active-session',
+ { candidateId },
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ );
+
+ const data = response.data;
+ if (data.examId && data.examName && data.examType) {
+ // If an active exam session exists, show the modal
+ setIsModalVisible(true);
+ // Store the exam data for navigation
+ sessionStorage.setItem('ongoingExam', JSON.stringify(data));
+ sessionStorage.setItem('sessionId', data.sessionId);
+ sessionStorage.setItem('endTime', data.endTime);
+ }
+ } catch (error) {
+ console.error('Error checking active session:', error);
}
};
+
checkOngoingExam();
}, []);
-
+
const handleOk = () => {
- navigate('/exam/view'); // Navigate to the exam view page
+ const ongoingExam = sessionStorage.getItem('ongoingExam');
+ if (ongoingExam) {
+ const { examId, examName, examType } = JSON.parse(ongoingExam);
+ navigate('/candidate/exam/view', { state: { id: examId, name: examName, type: examType } });
+ }
};
const handleCancel = () => {
diff --git a/src/pages/candidate/ExamMcqResults.tsx b/src/pages/candidate/ExamMcqResults.tsx
index a22438e..b45b61a 100644
--- a/src/pages/candidate/ExamMcqResults.tsx
+++ b/src/pages/candidate/ExamMcqResults.tsx
@@ -27,13 +27,7 @@ export const ExamMcqResults = () => {
correct: boolean;
marks: number;
}
-
- interface Question {
- questionId: number;
- questionText: string;
- options: Option[];
- }
-
+
interface UserAnswer {
questionId: number;
optionId: number;
@@ -46,10 +40,27 @@ export const ExamMcqResults = () => {
const [incorrectAnswers, setIncorrectAnswers] = useState(0);
const [skippedQuestions, setSkippedQuestions] = useState(0);
const [loading, setLoading] = useState(true);
+ const [gradingScheme, setGradingScheme] = useState([]);
+ const [candidateGrade, setCandidateGrade] = useState('');
+ const [status, setStatus] = useState('PASS');
const token = sessionStorage.getItem('accessToken');
const sessionId = sessionStorage.getItem('sessionId');
const examId = sessionStorage.getItem('examId');
+ const userString = sessionStorage.getItem('user');
+ const name = sessionStorage.getItem('examName');
+ if (!userString || !token) {
+ console.error("User information or token not found in session storage.");
+ return;
+ }
+
+ const user = JSON.parse(userString); // Parse the user object
+ const candidateId = user?.id; // Extract the candidateId
+
+ if (!candidateId) {
+ console.error("Candidate ID not found in user object.");
+ return;
+ }
const handleDashboard = () => {
navigate('/candidate/');
@@ -88,6 +99,19 @@ export const ExamMcqResults = () => {
);
const examData: { questions: Question[] } = await examResponse.json();
+ // Fetch Grading Scheme (Only Once)
+ const gradingResponse = await fetch(
+ `http://localhost:8080/api/v1/grade/1/grading-scheme`,
+ {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ );
+ const gradingData = await gradingResponse.json();
+ setGradingScheme(gradingData);
+
if (!Array.isArray(mcqData) || !Array.isArray(examData.questions)) {
console.error("Invalid data structure");
return;
@@ -100,47 +124,59 @@ export const ExamMcqResults = () => {
// Calculate Statistics
let correctAnswersCount = 0;
let incorrectAnswersCount = 0;
- let correctAnswersMarks = 0;
- let skippedQuestions = 0;
- let totalMarks = 0;
+ let totalMarks = 0;
+ let correctAnswersMarks = 0;
+ let skippedQuestions = 0;
+
+ // Assuming each correct answer is worth 10 marks
+ const marksPerAnswer = 10;
examData.questions.forEach((question) => {
- // Calculate total marks for all correct options
- totalMarks += question.options
- .filter((opt) => opt.correct)
- .reduce((sum, opt) => sum + opt.marks, 0);
+ // Calculate total marks for the exam
+ totalMarks += question.options.filter((opt) => opt.correct).length * marksPerAnswer;
- // Find user's answer for the current question
const userAnswer = mcqData.find(
(answer) => Number(answer.questionId) === question.questionId
);
if (userAnswer) {
- // Find the selected option and the correct option
const selectedOption = question.options.find(
(opt) => opt.optionId === Number(userAnswer.optionId)
);
const correctOption = question.options.find((opt) => opt.correct);
if (selectedOption && correctOption) {
- if (selectedOption.optionId == correctOption.optionId) {
+ if (selectedOption.optionId === correctOption.optionId) {
correctAnswersCount += 1;
- console.log(selectedOption.optionId);
- correctAnswersMarks += selectedOption.marks || 0;
+ correctAnswersMarks += marksPerAnswer;
} else {
incorrectAnswersCount += 1;
- console.log(selectedOption.optionId);
}
}
} else {
skippedQuestions += 1;
}
});
-
+
setTotalMarks(totalMarks);
setCorrectAnswers(correctAnswersCount);
setIncorrectAnswers(incorrectAnswersCount);
setSkippedQuestions(skippedQuestions);
+
+ // Determine Grade
+ const scorePercentage = (correctAnswersMarks / totalMarks) * 100;
+ let grade = 'F';
+
+ gradingScheme.forEach((gradeScheme) => {
+ if (scorePercentage >= gradeScheme.minMarks && scorePercentage <= gradeScheme.maxMarks) {
+ grade = gradeScheme.grade;
+ }
+ });
+
+ setCandidateGrade(grade);
+ setStatus(grade === 'F' ? 'FAIL' : 'PASS');
+
+
} catch (error) {
console.error("Error fetching data", error);
} finally {
@@ -149,12 +185,40 @@ export const ExamMcqResults = () => {
};
fetchExamResults();
- }, [token, sessionId, examId]);
-
-
+ }, [examId, sessionId, token]);
+
+ const percentage = totalMarks > 0 ? Math.round((correctAnswers * 10 / totalMarks) * 100) : 0;
+ const examName = name;
- const percentage = totalMarks > 0 ? Math.round((correctAnswers*10 / totalMarks) * 100) : 0;
- const examName = 'Machine Learning - Quiz 2'; // Replace with actual exam name if needed
+ const handleGradeSubmission = async () => {
+ try {
+ const response = await fetch(
+ 'http://localhost:8080/api/v1/grade/setExamCandidateGrade',
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify({
+ examID: examId,
+ candidateID: candidateId,
+ grade: candidateGrade,
+ score: correctAnswers * 10,
+ status: status,
+ }),
+ }
+ );
+ const result = await response.json();
+ if (result.success) {
+ console.log('Grade submitted successfully');
+ } else {
+ console.error('Error submitting grade');
+ }
+ } catch (error) {
+ console.error('Error submitting grade:', error);
+ }
+ };
return (
@@ -224,7 +288,7 @@ export const ExamMcqResults = () => {
)}
/>
- Scored {correctAnswers*10} out of {totalMarks}
+ Scored {correctAnswers * 10} out of {totalMarks}
@@ -243,25 +307,43 @@ export const ExamMcqResults = () => {
Skipped Questions:
{skippedQuestions}
-
-
- {!loading &&
- examDetails.map((question: Question, index: number) => {
+
+
+
+
+ }
+ >
+ Go to Dashboard
+
+
+
+
+ }
+ >
+ Submit Grade
+
+
+
+
+ {!loading && examDetails.map((question, index) => {
const userAnswer = examResults.find(
- (answer: UserAnswer) => Number(answer.questionId) === question.questionId
+ (answer) => Number(answer.questionId) === question.questionId
);
- const userSelectedOptionId = userAnswer ? userAnswer.optionId : null;
+ const userSelectedOptionId = userAnswer ? Number(userAnswer.optionId) : null;
return (
{
Marks:{" "}
{userSelectedOptionId
- ? question.options.find((opt: Option) => opt.optionId === userSelectedOptionId)?.correct
+ ? question.options.find((opt) => opt.optionId === userSelectedOptionId)?.correct
? "10"
: "0"
: "0"}
{" "}
/{" "}
{question.options.reduce(
- (acc: number, opt: Option) => acc + (opt.correct ? 10 : 0),
+ (acc, opt) => acc + (opt.correct ? 10 : 0),
0
)}
- {question.options.map((option: Option, optIndex: number) => {
+ {question.options.map((option, optIndex) => {
const isCorrect = option.correct;
- const isSelected = option.optionId == userSelectedOptionId;
+ const isSelected = Number(option.optionId) === userSelectedOptionId;
- // Add CSS classes based on whether the option is correct, selected, or incorrect
let optionClass = '';
if (isCorrect) {
- optionClass = 'correct'; // Correct answer
+ optionClass = 'correct';
} else if (isSelected) {
- optionClass = 'incorrect'; // User's incorrect answer
+ optionClass = 'incorrect';
}
return (
- -
+
-
{option.optionText}
{isCorrect && (
@@ -323,17 +401,15 @@ export const ExamMcqResults = () => {
);
})}
-
-
-
-
+ }
+ >
+ Go to Dashboard
+
diff --git a/src/pages/candidate/ExamSummary.tsx b/src/pages/candidate/ExamSummary.tsx
index 544e246..fa9d743 100644
--- a/src/pages/candidate/ExamSummary.tsx
+++ b/src/pages/candidate/ExamSummary.tsx
@@ -101,10 +101,13 @@ export const ExamSummaryPage = () => {
setSessionExists(true);
// Store the sessionId in sessionStorage
- const sessionId = response.data.sessionId; // Assuming the response structure contains the sessionId
+ const sessionId = response.data.sessionId;
+ console.log('sessionId:', response.data.endTime);
sessionStorage.setItem('sessionId', sessionId);
+ sessionStorage.setItem('endTime', response.data.endTime);
sessionStorage.setItem('examType', examData?.examType || '');
sessionStorage.setItem('examId', id);
+ sessionStorage.setItem('examName', examName);
// Navigate to the exam view page
navigate(`/candidate/exam/view`, {
diff --git a/src/pages/candidate/ExamView.tsx b/src/pages/candidate/ExamView.tsx
index 3b09ab8..e565431 100644
--- a/src/pages/candidate/ExamView.tsx
+++ b/src/pages/candidate/ExamView.tsx
@@ -151,7 +151,32 @@ export const ExamViewPage = () => {
};
fetchQuestions();
- }, [id]);
+
+ // Request fullscreen on load
+ const requestFullscreen = () => {
+ const elem = document.documentElement;
+ if (elem.requestFullscreen) {
+ elem.requestFullscreen();
+ }
+ };
+
+ // Handle fullscreen change
+ const onFullscreenChange = () => {
+ if (!document.fullscreenElement) {
+ console.log('Exited fullscreen');
+ }
+ };
+
+ document.addEventListener('fullscreenchange', onFullscreenChange);
+ document.addEventListener('webkitfullscreenchange', onFullscreenChange);
+
+ requestFullscreen();
+
+ return () => {
+ document.removeEventListener('fullscreenchange', onFullscreenChange);
+ document.removeEventListener('webkitfullscreenchange', onFullscreenChange);
+ };
+ }, [id, token]);
if (loading) {
return (
@@ -166,6 +191,7 @@ export const ExamViewPage = () => {
const isUnanswered = !answeredIndexes.includes(currentQuestionIndex + 1);
return (
+
Testify | Exam