Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | 1x 137x 137x 173x 173x 137x 137x 137x 137x 137x 137x 137x 137x 137x 34x 137x 105x 137x 34x 30x 34x 30x 137x 34x 34x 33x 137x 16x 16x 16x 16x 12x 11x 4x 4x 137x 34x 30x 341x 326x 175x 16x 16x 16x 175x 30x 137x 2x 2x 2x 137x 1x 2x | /**
* Modified by Allan Ninal — 2026-08-11
*
* Scoped the saved countdown to its own assignment and stopped persisting it for
* unlimited assignments; parsing/formatting moved to takeAssignmentTimer.js (PR #207).
*/
import { Box, Button, Typography } from "@mui/material";
import React, { useEffect, useState, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { _updateShowTimer } from "@/store/slices/student/StudentQuestionPage";
import { API_ENDPOINTS } from "@/api/endpoints";
import axios from "axios";
import { enqueueSnackbar } from "notistack";
import { submitFailureMessage } from "./AnswerTracker";
import { useNavigate } from "react-router-dom";
import OKOption from "@/utils/components/dialogs/OKOption";
import {
clearSavedSeconds,
formatSecondsToTime,
parseTimeStringToSeconds,
readSavedSeconds,
resolveInitialSeconds,
saveSeconds,
} from "./takeAssignmentTimer";
const Timer = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { specificAssignmentData } = useSelector((state) => state.StudentSpecificAssignment);
const { myAnswers, settings, remainingTime } = useSelector((state) => state.StudentQuestionPage);
// Main.jsx renders nothing until specificAssignmentData is loaded, so the id
// is available on this component's first render.
const assignmentId = specificAssignmentData?.details?._id;
// A falsy time_allowed on the assignment's own settings (not just the fetched
// remaining_time, which is also null before load) means no time limit was set.
const isUnlimited = !specificAssignmentData?.details?.settings?.time_allowed;
const [totalSeconds, setTotalSeconds] = useState(() =>
resolveInitialSeconds({ saved: readSavedSeconds(sessionStorage, assignmentId), remainingTime })
);
const [timeOver, setTimeOver] = useState(false);
// Modified by Allan Ninal — see the dialog copy below.
const [submitFailed, setSubmitFailed] = useState(false);
const answersRef = useRef(myAnswers);
const hasSubmittedRef = useRef(false);
const totalSecondsRef = useRef(totalSeconds);
useEffect(() => {
answersRef.current = myAnswers;
}, [myAnswers]);
useEffect(() => {
totalSecondsRef.current = totalSeconds;
}, [totalSeconds]);
// Nothing is persisted for an unlimited assignment: totalSeconds never
// decrements on that path, so saving would leave the default 3600 in a shared
// key for the next assignment to pick up as its own remaining time.
useEffect(() => {
if (isUnlimited) return;
const saveInterval = setInterval(() => {
saveSeconds(sessionStorage, assignmentId, totalSecondsRef.current);
}, 10000);
return () => clearInterval(saveInterval);
}, [isUnlimited, assignmentId]);
useEffect(() => {
const seconds = parseTimeStringToSeconds(remainingTime);
if (seconds !== null && readSavedSeconds(sessionStorage, assignmentId) === null) {
setTotalSeconds(seconds);
}
}, [remainingTime, assignmentId]);
const handleTimeUp = async () => {
clearSavedSeconds(sessionStorage);
const answers = answersRef.current.data.map(({ questionType, answer, ...rest }) => ({
...rest,
studentAnswer: answer ?? "",
}));
try {
const { data, status } = await axios.post(
API_ENDPOINTS.student.assignment.submitAnswers(specificAssignmentData.details._id),
{ answers }
);
if (status === 200) {
enqueueSnackbar(data.message, { variant: "success" });
}
} catch (error) {
// Modified by Allan Ninal — a submission that failed when the clock ran out
// told the student nothing. Their time is gone and their answers did not
// arrive; saying so is the only chance they have to raise it.
// The dialog below is shown either way, so without this it keeps telling
// the student their work "has been submitted automatically" when it has not.
setSubmitFailed(true);
enqueueSnackbar(submitFailureMessage(error), { variant: "error" });
}
};
useEffect(() => {
if (isUnlimited) return;
const intervalId = setInterval(() => {
setTotalSeconds((prev) => {
if (prev > 1) return prev - 1;
if (prev === 1 && !hasSubmittedRef.current) {
hasSubmittedRef.current = true;
handleTimeUp();
setTimeOver(true);
}
return 0;
});
}, 1000);
return () => clearInterval(intervalId);
}, [isUnlimited]);
const handleDialogClose = () => {
clearSavedSeconds(sessionStorage);
setTimeOver(false);
navigate(-1);
};
return (
<>
<OKOption
isDialog={timeOver}
setDialog={handleDialogClose}
title={submitFailed ? "Not Submitted" : "Time Expired"}
message={
submitFailed
? "Time has expired, but your answers could NOT be submitted. Please tell your teacher before leaving this page."
: "Time has expired for this assignment. Your work has been submitted automatically."
}
/>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
{settings.showTimer ? (
<>
<Typography>
{isUnlimited ? "Unlimited time" : `Remaining time: ${formatSecondsToTime(totalSeconds)}`}
</Typography>
<Button variant="text" onClick={() => dispatch(_updateShowTimer())}>
Hide
</Button>
</>
) : (
<Button variant="text" onClick={() => dispatch(_updateShowTimer())}>
Show Timer
</Button>
)}
</Box>
</>
);
};
export default Timer;
|