- Adapted the exam to store all of its information to Zustand;

- Made it so, every time there is a change or every X seconds, it saves the session;
This commit is contained in:
Tiago Ribeiro
2024-02-06 12:34:45 +00:00
parent 8baa25c445
commit c4b61c4787
13 changed files with 270 additions and 77 deletions

View File

@@ -22,9 +22,9 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
const [questionIndex, setQuestionIndex] = useState(0); const [questionIndex, setQuestionIndex] = useState(0);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [exerciseIndex, setExerciseIndex] = useState(0); const [exerciseIndex, setExerciseIndex] = useState(0);
const [userSolutions, setUserSolutions] = useState<UserSolution[]>(exam.exercises.map((x) => defaultUserSolutions(x, exam)));
const [hasExamEnded, setHasExamEnded] = useExamStore((state) => [state.hasExamEnded, state.setHasExamEnded]); const {userSolutions, setUserSolutions} = useExamStore((state) => state);
const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state);
useEffect(() => { useEffect(() => {
setCurrentQuestionIndex(0); setCurrentQuestionIndex(0);
@@ -38,7 +38,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
const nextExercise = (solution?: UserSolution) => { const nextExercise = (solution?: UserSolution) => {
if (solution) { if (solution) {
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]);
} }
setQuestionIndex((prev) => prev + currentQuestionIndex); setQuestionIndex((prev) => prev + currentQuestionIndex);
@@ -62,7 +62,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
const previousExercise = (solution?: UserSolution) => { const previousExercise = (solution?: UserSolution) => {
if (solution) { if (solution) {
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]);
} }
if (exerciseIndex > 0) { if (exerciseIndex > 0) {

View File

@@ -7,7 +7,6 @@ import AudioPlayer from "@/components/Low/AudioPlayer";
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import BlankQuestionsModal from "@/components/BlankQuestionsModal"; import BlankQuestionsModal from "@/components/BlankQuestionsModal";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import {defaultUserSolutions} from "@/utils/exams";
import {countExercises} from "@/utils/moduleUtils"; import {countExercises} from "@/utils/moduleUtils";
interface Props { interface Props {
@@ -22,23 +21,29 @@ const INSTRUCTIONS_AUDIO_SRC =
export default function Listening({exam, showSolutions = false, onFinish}: Props) { export default function Listening({exam, showSolutions = false, onFinish}: Props) {
const [questionIndex, setQuestionIndex] = useState(0); const [questionIndex, setQuestionIndex] = useState(0);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [exerciseIndex, setExerciseIndex] = useState(showSolutions ? 0 : -1);
const [partIndex, setPartIndex] = useState(exam.variant === "partial" ? 0 : -1);
const [timesListened, setTimesListened] = useState(0); const [timesListened, setTimesListened] = useState(0);
const [userSolutions, setUserSolutions] = useState<UserSolution[]>(
exam.parts.flatMap((x) => x.exercises).map((x) => defaultUserSolutions(x, exam)),
);
const [showBlankModal, setShowBlankModal] = useState(false); const [showBlankModal, setShowBlankModal] = useState(false);
const [hasExamEnded, setHasExamEnded] = useExamStore((state) => [state.hasExamEnded, state.setHasExamEnded]); const {userSolutions, setUserSolutions} = useExamStore((state) => state);
const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state);
const {partIndex, setPartIndex} = useExamStore((state) => state);
const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state);
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
useEffect(() => {
if (showSolutions) setExerciseIndex(-1);
}, [setExerciseIndex, showSolutions]);
useEffect(() => {
if (exam.variant !== "partial") setPartIndex(-1);
}, [exam.variant, setPartIndex]);
useEffect(() => { useEffect(() => {
if (hasExamEnded && exerciseIndex === -1) { if (hasExamEnded && exerciseIndex === -1) {
setExerciseIndex((prev) => prev + 1); setExerciseIndex(exerciseIndex + 1);
} }
}, [hasExamEnded, exerciseIndex]); }, [hasExamEnded, exerciseIndex, setExerciseIndex]);
useEffect(() => { useEffect(() => {
setCurrentQuestionIndex(0); setCurrentQuestionIndex(0);
@@ -56,17 +61,17 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
const nextExercise = (solution?: UserSolution) => { const nextExercise = (solution?: UserSolution) => {
scrollToTop(); scrollToTop();
if (solution) { if (solution) {
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]);
} }
setQuestionIndex((prev) => prev + currentQuestionIndex); setQuestionIndex((prev) => prev + currentQuestionIndex);
if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) { if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) {
setExerciseIndex((prev) => prev + 1); setExerciseIndex(exerciseIndex + 1);
return; return;
} }
if (partIndex + 1 < exam.parts.length && !hasExamEnded) { if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
setPartIndex((prev) => prev + 1); setPartIndex(partIndex + 1);
setExerciseIndex(showSolutions ? 0 : -1); setExerciseIndex(showSolutions ? 0 : -1);
return; return;
} }
@@ -97,10 +102,10 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
const previousExercise = (solution?: UserSolution) => { const previousExercise = (solution?: UserSolution) => {
scrollToTop(); scrollToTop();
if (solution) { if (solution) {
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]);
} }
setExerciseIndex((prev) => prev - 1); setExerciseIndex(exerciseIndex - 1);
}; };
const getExercise = () => { const getExercise = () => {
@@ -175,12 +180,14 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
{/* Exercise renderer */} {/* Exercise renderer */}
{exerciseIndex > -1 && {exerciseIndex > -1 &&
partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length && exerciseIndex < exam.parts[partIndex].exercises.length &&
!showSolutions && !showSolutions &&
renderExercise(getExercise(), nextExercise, previousExercise, setCurrentQuestionIndex)} renderExercise(getExercise(), nextExercise, previousExercise, setCurrentQuestionIndex)}
{/* Solution renderer */} {/* Solution renderer */}
{exerciseIndex > -1 && {exerciseIndex > -1 &&
partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length && exerciseIndex < exam.parts[partIndex].exercises.length &&
showSolutions && showSolutions &&
renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise, setCurrentQuestionIndex)} renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise, setCurrentQuestionIndex)}
@@ -195,7 +202,7 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
if (partIndex === 0) return setPartIndex(-1); if (partIndex === 0) return setPartIndex(-1);
setExerciseIndex(exam.parts[partIndex - 1].exercises.length - 1); setExerciseIndex(exam.parts[partIndex - 1].exercises.length - 1);
setPartIndex((prev) => prev - 1); setPartIndex(partIndex - 1);
}} }}
className="max-w-[200px] w-full"> className="max-w-[200px] w-full">
Back Back

View File

@@ -83,18 +83,20 @@ function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: s
export default function Reading({exam, showSolutions = false, onFinish}: Props) { export default function Reading({exam, showSolutions = false, onFinish}: Props) {
const [questionIndex, setQuestionIndex] = useState(0); const [questionIndex, setQuestionIndex] = useState(0);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [exerciseIndex, setExerciseIndex] = useState(showSolutions ? 0 : -1);
const [partIndex, setPartIndex] = useState(0);
const [showTextModal, setShowTextModal] = useState(false); const [showTextModal, setShowTextModal] = useState(false);
const [userSolutions, setUserSolutions] = useState<UserSolution[]>(
exam.parts.flatMap((x) => x.exercises).map((x) => defaultUserSolutions(x, exam)),
);
const [showBlankModal, setShowBlankModal] = useState(false); const [showBlankModal, setShowBlankModal] = useState(false);
const [hasExamEnded, setHasExamEnded] = useExamStore((state) => [state.hasExamEnded, state.setHasExamEnded]); const {userSolutions, setUserSolutions} = useExamStore((state) => state);
const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state);
const {partIndex, setPartIndex} = useExamStore((state) => state);
const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state);
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
useEffect(() => {
if (showSolutions) setExerciseIndex(-1);
}, [setExerciseIndex, showSolutions]);
useEffect(() => { useEffect(() => {
const listener = (e: KeyboardEvent) => { const listener = (e: KeyboardEvent) => {
if (e.key === "F3" || ((e.ctrlKey || e.metaKey) && e.key === "f")) { if (e.key === "F3" || ((e.ctrlKey || e.metaKey) && e.key === "f")) {
@@ -115,9 +117,9 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props)
useEffect(() => { useEffect(() => {
if (hasExamEnded && exerciseIndex === -1) { if (hasExamEnded && exerciseIndex === -1) {
setExerciseIndex((prev) => prev + 1); setExerciseIndex(exerciseIndex + 1);
} }
}, [hasExamEnded, exerciseIndex]); }, [hasExamEnded, exerciseIndex, setExerciseIndex]);
const confirmFinishModule = (keepGoing?: boolean) => { const confirmFinishModule = (keepGoing?: boolean) => {
if (!keepGoing) { if (!keepGoing) {
@@ -131,17 +133,17 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props)
const nextExercise = (solution?: UserSolution) => { const nextExercise = (solution?: UserSolution) => {
scrollToTop(); scrollToTop();
if (solution) { if (solution) {
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]);
} }
setQuestionIndex((prev) => prev + currentQuestionIndex); setQuestionIndex((prev) => prev + currentQuestionIndex);
if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) { if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) {
setExerciseIndex((prev) => prev + 1); setExerciseIndex(exerciseIndex + 1);
return; return;
} }
if (partIndex + 1 < exam.parts.length && !hasExamEnded) { if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
setPartIndex((prev) => prev + 1); setPartIndex(partIndex + 1);
setExerciseIndex(showSolutions ? 0 : -1); setExerciseIndex(showSolutions ? 0 : -1);
return; return;
} }
@@ -172,10 +174,10 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props)
const previousExercise = (solution?: UserSolution) => { const previousExercise = (solution?: UserSolution) => {
scrollToTop(); scrollToTop();
if (solution) { if (solution) {
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]);
} }
setExerciseIndex((prev) => prev - 1); setExerciseIndex(exerciseIndex - 1);
}; };
const getExercise = () => { const getExercise = () => {
@@ -256,7 +258,7 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props)
variant="outline" variant="outline"
onClick={() => { onClick={() => {
setExerciseIndex(exam.parts[partIndex - 1].exercises.length - 1); setExerciseIndex(exam.parts[partIndex - 1].exercises.length - 1);
setPartIndex((prev) => prev - 1); setPartIndex(partIndex - 1);
}} }}
className="max-w-[200px] w-full"> className="max-w-[200px] w-full">
Back Back

View File

@@ -22,9 +22,10 @@ interface Props {
export default function Speaking({exam, showSolutions = false, onFinish}: Props) { export default function Speaking({exam, showSolutions = false, onFinish}: Props) {
const [questionIndex, setQuestionIndex] = useState(0); const [questionIndex, setQuestionIndex] = useState(0);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [exerciseIndex, setExerciseIndex] = useState(0);
const [userSolutions, setUserSolutions] = useState<UserSolution[]>(exam.exercises.map((x) => defaultUserSolutions(x, exam))); const {userSolutions, setUserSolutions} = useExamStore((state) => state);
const [hasExamEnded, setHasExamEnded] = useExamStore((state) => [state.hasExamEnded, state.setHasExamEnded]); const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state);
const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state);
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
@@ -35,12 +36,12 @@ export default function Speaking({exam, showSolutions = false, onFinish}: Props)
const nextExercise = (solution?: UserSolution) => { const nextExercise = (solution?: UserSolution) => {
scrollToTop(); scrollToTop();
if (solution) { if (solution) {
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]);
} }
setQuestionIndex((prev) => prev + currentQuestionIndex); setQuestionIndex((prev) => prev + currentQuestionIndex);
if (exerciseIndex + 1 < exam.exercises.length) { if (exerciseIndex + 1 < exam.exercises.length) {
setExerciseIndex((prev) => prev + 1); setExerciseIndex(exerciseIndex + 1);
return; return;
} }
@@ -60,11 +61,11 @@ export default function Speaking({exam, showSolutions = false, onFinish}: Props)
const previousExercise = (solution?: UserSolution) => { const previousExercise = (solution?: UserSolution) => {
scrollToTop(); scrollToTop();
if (solution) { if (solution) {
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]);
} }
if (exerciseIndex > 0) { if (exerciseIndex > 0) {
setExerciseIndex((prev) => prev - 1); setExerciseIndex(exerciseIndex - 1);
} }
}; };

View File

@@ -19,21 +19,20 @@ interface Props {
} }
export default function Writing({exam, showSolutions = false, onFinish}: Props) { export default function Writing({exam, showSolutions = false, onFinish}: Props) {
const [exerciseIndex, setExerciseIndex] = useState(0); const {userSolutions, setUserSolutions} = useExamStore((state) => state);
const [userSolutions, setUserSolutions] = useState<UserSolution[]>(exam.exercises.map((x) => defaultUserSolutions(x, exam))); const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state);
const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state);
const [hasExamEnded, setHasExamEnded] = useExamStore((state) => [state.hasExamEnded, state.setHasExamEnded]);
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
const nextExercise = (solution?: UserSolution) => { const nextExercise = (solution?: UserSolution) => {
scrollToTop(); scrollToTop();
if (solution) { if (solution) {
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]);
} }
if (exerciseIndex + 1 < exam.exercises.length) { if (exerciseIndex + 1 < exam.exercises.length) {
setExerciseIndex((prev) => prev + 1); setExerciseIndex(exerciseIndex + 1);
return; return;
} }
@@ -53,11 +52,11 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props)
const previousExercise = (solution?: UserSolution) => { const previousExercise = (solution?: UserSolution) => {
scrollToTop(); scrollToTop();
if (solution) { if (solution) {
setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]);
} }
if (exerciseIndex > 0) { if (exerciseIndex > 0) {
setExerciseIndex((prev) => prev - 1); setExerciseIndex(exerciseIndex - 1);
} }
}; };

24
src/hooks/useSessions.tsx Normal file
View File

@@ -0,0 +1,24 @@
import {Exam} from "@/interfaces/exam";
import {ExamState} from "@/stores/examStore";
import axios from "axios";
import {useEffect, useState} from "react";
export type Session = ExamState & {user: string};
export default function useSessions(user?: string) {
const [sessions, setSessions] = useState<Session[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const getData = () => {
setIsLoading(true);
axios
.get<Session[]>(`/api/sessions${user ? `?user=${user}` : ""}`)
.then((response) => setSessions(response.data))
.finally(() => setIsLoading(false));
};
useEffect(getData, [user]);
return {sessions, isLoading, isError, reload: getData};
}

View File

@@ -16,11 +16,12 @@ import {Exam, UserSolution, Variant} from "@/interfaces/exam";
import {Stat} from "@/interfaces/user"; import {Stat} from "@/interfaces/user";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation"; import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation";
import {getExam} from "@/utils/exams"; import {defaultExamUserSolutions, getExam} from "@/utils/exams";
import axios from "axios"; import axios from "axios";
import {useRouter} from "next/router"; import {useRouter} from "next/router";
import {toast, ToastContainer} from "react-toastify"; import {toast, ToastContainer} from "react-toastify";
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import useSessions from "@/hooks/useSessions";
interface Props { interface Props {
page: "exams" | "exercises"; page: "exams" | "exercises";
@@ -33,11 +34,15 @@ export default function ExamPage({page}: Props) {
const [showAbandonPopup, setShowAbandonPopup] = useState(false); const [showAbandonPopup, setShowAbandonPopup] = useState(false);
const [isEvaluationLoading, setIsEvaluationLoading] = useState(false); const [isEvaluationLoading, setIsEvaluationLoading] = useState(false);
const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState<string[]>([]); const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState<string[]>([]);
const [timeSpent, setTimeSpent] = useState(0);
const partIndex = useExamStore((state) => state.partIndex);
const assignment = useExamStore((state) => state.assignment);
const initialTimeSpent = useExamStore((state) => state.timeSpent);
const exerciseIndex = useExamStore((state) => state.exerciseIndex);
const {assignment} = useExamStore((state) => state);
const {exam, setExam} = useExamStore((state) => state); const {exam, setExam} = useExamStore((state) => state);
const {exams, setExams} = useExamStore((state) => state); const {exams, setExams} = useExamStore((state) => state);
const {timeSpent, setTimeSpent} = useExamStore((state) => state);
const {sessionId, setSessionId} = useExamStore((state) => state); const {sessionId, setSessionId} = useExamStore((state) => state);
const {moduleIndex, setModuleIndex} = useExamStore((state) => state); const {moduleIndex, setModuleIndex} = useExamStore((state) => state);
const {userSolutions, setUserSolutions} = useExamStore((state) => state); const {userSolutions, setUserSolutions} = useExamStore((state) => state);
@@ -47,16 +52,56 @@ export default function ExamPage({page}: Props) {
const {user} = useUser({redirectTo: "/login"}); const {user} = useUser({redirectTo: "/login"});
const router = useRouter(); const router = useRouter();
useEffect(() => setSessionId(uuidv4()), [setSessionId]); // eslint-disable-next-line react-hooks/exhaustive-deps
const saveSession = async () => {
await axios.post("/api/sessions", {
id: sessionId,
userSolutions,
moduleIndex,
selectedModules,
assignment,
timeSpent,
exams,
exam,
partIndex,
exerciseIndex,
user: user?.id,
});
};
useEffect(() => setTimeSpent((prev) => prev + initialTimeSpent), [initialTimeSpent]);
useEffect(() => {
if (userSolutions.length === 0 && exams.length > 0) {
const defaultSolutions = exams.map(defaultExamUserSolutions).flat();
setUserSolutions(defaultSolutions);
}
}, [exams, setUserSolutions, userSolutions]);
useEffect(() => {
if (sessionId.length > 0 && userSolutions.length > 0 && selectedModules.length > 0 && exams.length > 0 && !!exam && timeSpent > 0)
saveSession();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assignment, exam, exams, moduleIndex, selectedModules, sessionId, userSolutions, user, exerciseIndex, partIndex]);
useEffect(() => {
if (timeSpent % 20 === 0 && timeSpent > 0) saveSession();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timeSpent]);
useEffect(() => {
if (selectedModules.length > 0) {
setSessionId(uuidv4());
}
}, [setSessionId, selectedModules]);
useEffect(() => { useEffect(() => {
if (user?.type === "developer") console.log(exam); if (user?.type === "developer") console.log(exam);
}, [exam, user]); }, [exam, user]);
useEffect(() => { useEffect(() => {
selectedModules.length > 0 && timeSpent === 0 && !showSolutions;
if (selectedModules.length > 0 && timeSpent === 0 && !showSolutions) { if (selectedModules.length > 0 && timeSpent === 0 && !showSolutions) {
const timerInterval = setInterval(() => { const timerInterval = setInterval(() => {
setTimeSpent(timeSpent + 1); setTimeSpent((prev) => prev + 1);
}, 1000); }, 1000);
return () => { return () => {

View File

@@ -10,9 +10,10 @@ import {useRouter} from "next/router";
import {useEffect} from "react"; import {useEffect} from "react";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import usePreferencesStore from "@/stores/preferencesStore"; import usePreferencesStore from "@/stores/preferencesStore";
import axios from "axios";
export default function App({Component, pageProps}: AppProps) { export default function App({Component, pageProps}: AppProps) {
const reset = useExamStore((state) => state.reset); const {reset} = useExamStore((state) => state);
const setIsSidebarMinimized = usePreferencesStore((state) => state.setSidebarMinimized); const setIsSidebarMinimized = usePreferencesStore((state) => state.setSidebarMinimized);
const router = useRouter(); const router = useRouter();

View File

@@ -0,0 +1,53 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, doc, getDoc, deleteDoc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
if (req.method === "DELETE") return del(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {id} = req.query as {id: string};
const docRef = doc(db, "sessions", id);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
res.status(200).json({
id: docSnap.id,
...docSnap.data(),
});
} else {
res.status(404).json(undefined);
}
}
async function del(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {id} = req.query as {id: string};
const docRef = doc(db, "sessions", id);
const docSnap = await getDoc(docRef);
if (!docSnap.exists()) return res.status(404).json({ok: false});
await deleteDoc(docRef);
return res.status(200).json({ok: true});
}

View File

@@ -0,0 +1,46 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, collection, getDocs, query, where, doc, setDoc, addDoc, getDoc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
if (req.method === "POST") return post(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {user} = req.query as {user?: string};
const q = user ? query(collection(db, "sessions"), where("user", "==", user)) : collection(db, "sessions");
const snapshot = await getDocs(q);
res.status(200).json(
snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})),
);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const session = req.body;
await setDoc(doc(db, "sessions", session.id), session, {merge: true});
res.status(200).json({ok: true});
}

View File

@@ -1,7 +1,7 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next"; import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase"; import {app} from "@/firebase";
import {getFirestore, collection, getDocs, query, where, doc, setDoc, addDoc, getDoc} from "firebase/firestore"; import {getFirestore, collection, getDocs, query, where, doc, setDoc, addDoc, getDoc, deleteDoc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next"; import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session"; import {sessionOptions} from "@/lib/session";
import {Stat} from "@/interfaces/user"; import {Stat} from "@/interfaces/user";
@@ -43,6 +43,10 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
const stats = req.body as Stat[]; const stats = req.body as Stat[];
await stats.forEach(async (stat) => await setDoc(doc(db, "stats", stat.id), stat)); await stats.forEach(async (stat) => await setDoc(doc(db, "stats", stat.id), stat));
await stats.forEach(async (stat) => {
const sessionDoc = await getDoc(doc(db, "sessions", stat.session));
if (sessionDoc.exists()) await deleteDoc(sessionDoc.ref);
});
const groupedStatsByAssignment = groupBy( const groupedStatsByAssignment = groupBy(
stats.filter((x) => !!x.assignment), stats.filter((x) => !!x.assignment),

View File

@@ -5,39 +5,36 @@ import {create} from "zustand";
export interface ExamState { export interface ExamState {
exams: Exam[]; exams: Exam[];
setExams: (exams: Exam[]) => void;
userSolutions: UserSolution[]; userSolutions: UserSolution[];
setUserSolutions: (userSolutions: UserSolution[]) => void;
showSolutions: boolean; showSolutions: boolean;
setShowSolutions: (showSolutions: boolean) => void;
hasExamEnded: boolean; hasExamEnded: boolean;
setHasExamEnded: (hasExamEnded: boolean) => void;
selectedModules: Module[]; selectedModules: Module[];
setSelectedModules: (modules: Module[]) => void;
assignment?: Assignment; assignment?: Assignment;
setAssignment: (assignment: Assignment) => void;
timeSpent: number; timeSpent: number;
setTimeSpent: (timeSpent: number) => void;
sessionId: string; sessionId: string;
setSessionId: (sessionId: string) => void;
moduleIndex: number; moduleIndex: number;
setModuleIndex: (moduleIndex: number) => void;
exam?: Exam; exam?: Exam;
setExam: (exam?: Exam) => void; partIndex: number;
exerciseIndex: number;
}
export interface ExamFunctions {
setExams: (exams: Exam[]) => void;
setUserSolutions: (userSolutions: UserSolution[]) => void;
setShowSolutions: (showSolutions: boolean) => void;
setHasExamEnded: (hasExamEnded: boolean) => void;
setSelectedModules: (modules: Module[]) => void;
setAssignment: (assignment: Assignment) => void;
setTimeSpent: (timeSpent: number) => void;
setSessionId: (sessionId: string) => void;
setModuleIndex: (moduleIndex: number) => void;
setExam: (exam?: Exam) => void;
setPartIndex: (partIndex: number) => void;
setExerciseIndex: (exerciseIndex: number) => void;
reset: () => void; reset: () => void;
} }
export const initialState = { export const initialState: ExamState = {
exams: [], exams: [],
userSolutions: [], userSolutions: [],
showSolutions: false, showSolutions: false,
@@ -48,9 +45,11 @@ export const initialState = {
sessionId: "", sessionId: "",
exam: undefined, exam: undefined,
moduleIndex: 0, moduleIndex: 0,
partIndex: 0,
exerciseIndex: 0,
}; };
const useExamStore = create<ExamState>((set) => ({ const useExamStore = create<ExamState & ExamFunctions>((set) => ({
...initialState, ...initialState,
setUserSolutions: (userSolutions: UserSolution[]) => set(() => ({userSolutions})), setUserSolutions: (userSolutions: UserSolution[]) => set(() => ({userSolutions})),
@@ -63,6 +62,8 @@ const useExamStore = create<ExamState>((set) => ({
setSessionId: (sessionId: string) => set(() => ({sessionId})), setSessionId: (sessionId: string) => set(() => ({sessionId})),
setExam: (exam?: Exam) => set(() => ({exam})), setExam: (exam?: Exam) => set(() => ({exam})),
setModuleIndex: (moduleIndex: number) => set(() => ({moduleIndex})), setModuleIndex: (moduleIndex: number) => set(() => ({moduleIndex})),
setPartIndex: (partIndex: number) => set(() => ({partIndex})),
setExerciseIndex: (exerciseIndex: number) => set(() => ({exerciseIndex})),
reset: () => set(() => initialState), reset: () => set(() => initialState),
})); }));

View File

@@ -50,6 +50,13 @@ export const getExamById = async (module: Module, id: string): Promise<Exam | un
} }
}; };
export const defaultExamUserSolutions = (exam: Exam) => {
if (exam.module === "reading" || exam.module === "listening")
return exam.parts.flatMap((x) => x.exercises).map((x) => defaultUserSolutions(x, exam));
return exam.exercises.map((x) => defaultUserSolutions(x, exam));
};
export const defaultUserSolutions = (exercise: Exercise, exam: Exam): UserSolution => { export const defaultUserSolutions = (exercise: Exercise, exam: Exam): UserSolution => {
const defaultSettings = { const defaultSettings = {
exam: exam.id, exam: exam.id,
@@ -73,6 +80,9 @@ export const defaultUserSolutions = (exercise: Exercise, exam: Exam): UserSoluti
case "writeBlanks": case "writeBlanks":
total = exercise.text.match(/({{\d+}})/g)?.length || 0; total = exercise.text.match(/({{\d+}})/g)?.length || 0;
return {...defaultSettings, score: {correct: 0, total, missing: total}}; return {...defaultSettings, score: {correct: 0, total, missing: total}};
case "trueFalse":
total = exercise.questions.length;
return {...defaultSettings, score: {correct: 0, total, missing: total}};
case "writing": case "writing":
total = 1; total = 1;
return {...defaultSettings, score: {correct: 0, total, missing: total}}; return {...defaultSettings, score: {correct: 0, total, missing: total}};