diff --git a/src/exams/Level.tsx b/src/exams/Level.tsx index ff85c486..0af97db7 100644 --- a/src/exams/Level.tsx +++ b/src/exams/Level.tsx @@ -22,9 +22,9 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) { const [questionIndex, setQuestionIndex] = useState(0); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [exerciseIndex, setExerciseIndex] = useState(0); - const [userSolutions, setUserSolutions] = useState(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(() => { setCurrentQuestionIndex(0); @@ -38,7 +38,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) { const nextExercise = (solution?: UserSolution) => { 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); @@ -62,7 +62,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) { const previousExercise = (solution?: UserSolution) => { if (solution) { - setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]); } if (exerciseIndex > 0) { diff --git a/src/exams/Listening.tsx b/src/exams/Listening.tsx index 6d344812..69d2f698 100644 --- a/src/exams/Listening.tsx +++ b/src/exams/Listening.tsx @@ -7,7 +7,6 @@ import AudioPlayer from "@/components/Low/AudioPlayer"; import Button from "@/components/Low/Button"; import BlankQuestionsModal from "@/components/BlankQuestionsModal"; import useExamStore from "@/stores/examStore"; -import {defaultUserSolutions} from "@/utils/exams"; import {countExercises} from "@/utils/moduleUtils"; interface Props { @@ -22,23 +21,29 @@ const INSTRUCTIONS_AUDIO_SRC = export default function Listening({exam, showSolutions = false, onFinish}: Props) { const [questionIndex, setQuestionIndex] = 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 [userSolutions, setUserSolutions] = useState( - exam.parts.flatMap((x) => x.exercises).map((x) => defaultUserSolutions(x, exam)), - ); 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)); + useEffect(() => { + if (showSolutions) setExerciseIndex(-1); + }, [setExerciseIndex, showSolutions]); + + useEffect(() => { + if (exam.variant !== "partial") setPartIndex(-1); + }, [exam.variant, setPartIndex]); + useEffect(() => { if (hasExamEnded && exerciseIndex === -1) { - setExerciseIndex((prev) => prev + 1); + setExerciseIndex(exerciseIndex + 1); } - }, [hasExamEnded, exerciseIndex]); + }, [hasExamEnded, exerciseIndex, setExerciseIndex]); useEffect(() => { setCurrentQuestionIndex(0); @@ -56,17 +61,17 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props const nextExercise = (solution?: UserSolution) => { scrollToTop(); 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); if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) { - setExerciseIndex((prev) => prev + 1); + setExerciseIndex(exerciseIndex + 1); return; } if (partIndex + 1 < exam.parts.length && !hasExamEnded) { - setPartIndex((prev) => prev + 1); + setPartIndex(partIndex + 1); setExerciseIndex(showSolutions ? 0 : -1); return; } @@ -97,10 +102,10 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props const previousExercise = (solution?: UserSolution) => { scrollToTop(); 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 = () => { @@ -175,12 +180,14 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props {/* Exercise renderer */} {exerciseIndex > -1 && + partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && !showSolutions && renderExercise(getExercise(), nextExercise, previousExercise, setCurrentQuestionIndex)} {/* Solution renderer */} {exerciseIndex > -1 && + partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && showSolutions && 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); setExerciseIndex(exam.parts[partIndex - 1].exercises.length - 1); - setPartIndex((prev) => prev - 1); + setPartIndex(partIndex - 1); }} className="max-w-[200px] w-full"> Back diff --git a/src/exams/Reading.tsx b/src/exams/Reading.tsx index 573d2c3c..197268fa 100644 --- a/src/exams/Reading.tsx +++ b/src/exams/Reading.tsx @@ -83,18 +83,20 @@ function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: s export default function Reading({exam, showSolutions = false, onFinish}: Props) { const [questionIndex, setQuestionIndex] = 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 [userSolutions, setUserSolutions] = useState( - exam.parts.flatMap((x) => x.exercises).map((x) => defaultUserSolutions(x, exam)), - ); 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)); + useEffect(() => { + if (showSolutions) setExerciseIndex(-1); + }, [setExerciseIndex, showSolutions]); + useEffect(() => { const listener = (e: KeyboardEvent) => { 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(() => { if (hasExamEnded && exerciseIndex === -1) { - setExerciseIndex((prev) => prev + 1); + setExerciseIndex(exerciseIndex + 1); } - }, [hasExamEnded, exerciseIndex]); + }, [hasExamEnded, exerciseIndex, setExerciseIndex]); const confirmFinishModule = (keepGoing?: boolean) => { if (!keepGoing) { @@ -131,17 +133,17 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) const nextExercise = (solution?: UserSolution) => { scrollToTop(); 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); if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) { - setExerciseIndex((prev) => prev + 1); + setExerciseIndex(exerciseIndex + 1); return; } if (partIndex + 1 < exam.parts.length && !hasExamEnded) { - setPartIndex((prev) => prev + 1); + setPartIndex(partIndex + 1); setExerciseIndex(showSolutions ? 0 : -1); return; } @@ -172,10 +174,10 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) const previousExercise = (solution?: UserSolution) => { scrollToTop(); 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 = () => { @@ -256,7 +258,7 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) variant="outline" onClick={() => { setExerciseIndex(exam.parts[partIndex - 1].exercises.length - 1); - setPartIndex((prev) => prev - 1); + setPartIndex(partIndex - 1); }} className="max-w-[200px] w-full"> Back diff --git a/src/exams/Speaking.tsx b/src/exams/Speaking.tsx index d3e3564c..ecd52000 100644 --- a/src/exams/Speaking.tsx +++ b/src/exams/Speaking.tsx @@ -22,9 +22,10 @@ interface Props { export default function Speaking({exam, showSolutions = false, onFinish}: Props) { const [questionIndex, setQuestionIndex] = useState(0); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); - const [exerciseIndex, setExerciseIndex] = useState(0); - const [userSolutions, setUserSolutions] = useState(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); + const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state); 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) => { scrollToTop(); 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); if (exerciseIndex + 1 < exam.exercises.length) { - setExerciseIndex((prev) => prev + 1); + setExerciseIndex(exerciseIndex + 1); return; } @@ -60,11 +61,11 @@ export default function Speaking({exam, showSolutions = false, onFinish}: Props) const previousExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { - setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]); } if (exerciseIndex > 0) { - setExerciseIndex((prev) => prev - 1); + setExerciseIndex(exerciseIndex - 1); } }; diff --git a/src/exams/Writing.tsx b/src/exams/Writing.tsx index c2a6a692..d9ec398f 100644 --- a/src/exams/Writing.tsx +++ b/src/exams/Writing.tsx @@ -19,21 +19,20 @@ interface Props { } export default function Writing({exam, showSolutions = false, onFinish}: Props) { - const [exerciseIndex, setExerciseIndex] = useState(0); - const [userSolutions, setUserSolutions] = useState(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); + const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const nextExercise = (solution?: UserSolution) => { scrollToTop(); 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) { - setExerciseIndex((prev) => prev + 1); + setExerciseIndex(exerciseIndex + 1); return; } @@ -53,11 +52,11 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props) const previousExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { - setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]); } if (exerciseIndex > 0) { - setExerciseIndex((prev) => prev - 1); + setExerciseIndex(exerciseIndex - 1); } }; diff --git a/src/hooks/useSessions.tsx b/src/hooks/useSessions.tsx new file mode 100644 index 00000000..3cc89032 --- /dev/null +++ b/src/hooks/useSessions.tsx @@ -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([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const getData = () => { + setIsLoading(true); + axios + .get(`/api/sessions${user ? `?user=${user}` : ""}`) + .then((response) => setSessions(response.data)) + .finally(() => setIsLoading(false)); + }; + + useEffect(getData, [user]); + + return {sessions, isLoading, isError, reload: getData}; +} diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index cfe36e8b..82ceebb8 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -16,11 +16,12 @@ import {Exam, UserSolution, Variant} from "@/interfaces/exam"; import {Stat} from "@/interfaces/user"; import useExamStore from "@/stores/examStore"; import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation"; -import {getExam} from "@/utils/exams"; +import {defaultExamUserSolutions, getExam} from "@/utils/exams"; import axios from "axios"; import {useRouter} from "next/router"; import {toast, ToastContainer} from "react-toastify"; import {v4 as uuidv4} from "uuid"; +import useSessions from "@/hooks/useSessions"; interface Props { page: "exams" | "exercises"; @@ -33,11 +34,15 @@ export default function ExamPage({page}: Props) { const [showAbandonPopup, setShowAbandonPopup] = useState(false); const [isEvaluationLoading, setIsEvaluationLoading] = useState(false); const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState([]); + 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 {exams, setExams} = useExamStore((state) => state); - const {timeSpent, setTimeSpent} = useExamStore((state) => state); const {sessionId, setSessionId} = useExamStore((state) => state); const {moduleIndex, setModuleIndex} = 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 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(() => { if (user?.type === "developer") console.log(exam); }, [exam, user]); useEffect(() => { - selectedModules.length > 0 && timeSpent === 0 && !showSolutions; if (selectedModules.length > 0 && timeSpent === 0 && !showSolutions) { const timerInterval = setInterval(() => { - setTimeSpent(timeSpent + 1); + setTimeSpent((prev) => prev + 1); }, 1000); return () => { diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index f87a73b9..0f17938c 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -10,9 +10,10 @@ import {useRouter} from "next/router"; import {useEffect} from "react"; import useExamStore from "@/stores/examStore"; import usePreferencesStore from "@/stores/preferencesStore"; +import axios from "axios"; 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 router = useRouter(); diff --git a/src/pages/api/sessions/[id].ts b/src/pages/api/sessions/[id].ts new file mode 100644 index 00000000..58e1af15 --- /dev/null +++ b/src/pages/api/sessions/[id].ts @@ -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}); +} diff --git a/src/pages/api/sessions/index.ts b/src/pages/api/sessions/index.ts new file mode 100644 index 00000000..c5b30692 --- /dev/null +++ b/src/pages/api/sessions/index.ts @@ -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}); +} diff --git a/src/pages/api/stats/index.ts b/src/pages/api/stats/index.ts index 779427ba..ce8b4b85 100644 --- a/src/pages/api/stats/index.ts +++ b/src/pages/api/stats/index.ts @@ -1,7 +1,7 @@ // 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 {getFirestore, collection, getDocs, query, where, doc, setDoc, addDoc, getDoc, deleteDoc} from "firebase/firestore"; import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import {Stat} from "@/interfaces/user"; @@ -43,6 +43,10 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const stats = req.body as 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( stats.filter((x) => !!x.assignment), diff --git a/src/stores/examStore.ts b/src/stores/examStore.ts index 1102c01b..875a712e 100644 --- a/src/stores/examStore.ts +++ b/src/stores/examStore.ts @@ -5,39 +5,36 @@ import {create} from "zustand"; export interface ExamState { exams: Exam[]; - setExams: (exams: Exam[]) => void; - userSolutions: UserSolution[]; - setUserSolutions: (userSolutions: UserSolution[]) => void; - showSolutions: boolean; - setShowSolutions: (showSolutions: boolean) => void; - hasExamEnded: boolean; - setHasExamEnded: (hasExamEnded: boolean) => void; - selectedModules: Module[]; - setSelectedModules: (modules: Module[]) => void; - assignment?: Assignment; - setAssignment: (assignment: Assignment) => void; - timeSpent: number; - setTimeSpent: (timeSpent: number) => void; - sessionId: string; - setSessionId: (sessionId: string) => void; - moduleIndex: number; - setModuleIndex: (moduleIndex: number) => void; - 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; } -export const initialState = { +export const initialState: ExamState = { exams: [], userSolutions: [], showSolutions: false, @@ -48,9 +45,11 @@ export const initialState = { sessionId: "", exam: undefined, moduleIndex: 0, + partIndex: 0, + exerciseIndex: 0, }; -const useExamStore = create((set) => ({ +const useExamStore = create((set) => ({ ...initialState, setUserSolutions: (userSolutions: UserSolution[]) => set(() => ({userSolutions})), @@ -63,6 +62,8 @@ const useExamStore = create((set) => ({ setSessionId: (sessionId: string) => set(() => ({sessionId})), setExam: (exam?: Exam) => set(() => ({exam})), setModuleIndex: (moduleIndex: number) => set(() => ({moduleIndex})), + setPartIndex: (partIndex: number) => set(() => ({partIndex})), + setExerciseIndex: (exerciseIndex: number) => set(() => ({exerciseIndex})), reset: () => set(() => initialState), })); diff --git a/src/utils/exams.ts b/src/utils/exams.ts index 370d1173..db80b5d0 100644 --- a/src/utils/exams.ts +++ b/src/utils/exams.ts @@ -50,6 +50,13 @@ export const getExamById = async (module: Module, id: string): Promise { + 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 => { const defaultSettings = { exam: exam.id, @@ -73,6 +80,9 @@ export const defaultUserSolutions = (exercise: Exercise, exam: Exam): UserSoluti case "writeBlanks": total = exercise.text.match(/({{\d+}})/g)?.length || 0; return {...defaultSettings, score: {correct: 0, total, missing: total}}; + case "trueFalse": + total = exercise.questions.length; + return {...defaultSettings, score: {correct: 0, total, missing: total}}; case "writing": total = 1; return {...defaultSettings, score: {correct: 0, total, missing: total}};