- 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:
@@ -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<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(() => {
|
||||
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) {
|
||||
|
||||
@@ -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<UserSolution[]>(
|
||||
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
|
||||
|
||||
@@ -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<UserSolution[]>(
|
||||
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
|
||||
|
||||
@@ -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<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);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -19,21 +19,20 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function Writing({exam, showSolutions = false, onFinish}: Props) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
24
src/hooks/useSessions.tsx
Normal file
24
src/hooks/useSessions.tsx
Normal 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};
|
||||
}
|
||||
@@ -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<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 {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 () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
53
src/pages/api/sessions/[id].ts
Normal file
53
src/pages/api/sessions/[id].ts
Normal 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});
|
||||
}
|
||||
46
src/pages/api/sessions/index.ts
Normal file
46
src/pages/api/sessions/index.ts
Normal 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});
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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<ExamState>((set) => ({
|
||||
const useExamStore = create<ExamState & ExamFunctions>((set) => ({
|
||||
...initialState,
|
||||
|
||||
setUserSolutions: (userSolutions: UserSolution[]) => set(() => ({userSolutions})),
|
||||
@@ -63,6 +62,8 @@ const useExamStore = create<ExamState>((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),
|
||||
}));
|
||||
|
||||
@@ -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 => {
|
||||
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}};
|
||||
|
||||
Reference in New Issue
Block a user