Navigation rework, added prompt edit to components that were missing

This commit is contained in:
Carlos-Mesquita
2024-11-25 16:50:46 +00:00
parent e9b7bd14cc
commit 114da173be
105 changed files with 3761 additions and 3728 deletions

View File

@@ -1,50 +0,0 @@
import Button from "@/components/Low/Button";
import Input from "@/components/Low/Input";
import {Module} from "@/interfaces";
import useExamStore from "@/stores/examStore";
import {getExamById} from "@/utils/exams";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import {RadioGroup} from "@headlessui/react";
import axios from "axios";
import clsx from "clsx";
import {capitalize} from "lodash";
import {useRouter} from "next/router";
import {FormEvent, useState} from "react";
import {toast} from "react-toastify";
export default function ExamGenerator() {
const [selectedModule, setSelectedModule] = useState<Module>();
const [examId, setExamId] = useState<string>();
const [isLoading, setIsLoading] = useState(false);
const setExams = useExamStore((state) => state.setExams);
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const router = useRouter();
const generateExam = (module: Module) => {
axios.get(`/api/exam/${module}/generate`).then((result) => console.log(result.data));
};
return (
<div className="flex flex-col gap-4 border p-4 border-mti-gray-platinum rounded-xl">
<label className="font-normal text-base text-mti-gray-dim">Exam Generator</label>
<div className="w-full grid grid-cols-2 gap-2">
{MODULE_ARRAY.map((module) => (
<Button
onClick={() => generateExam(module)}
key={module}
className={clsx(
"w-full min-w-[200px]",
module === "reading" && "!bg-ielts-reading/80 !border-ielts-reading hover:!bg-ielts-reading",
module === "listening" && "!bg-ielts-listening/80 !border-ielts-listening hover:!bg-ielts-listening",
module === "writing" && "!bg-ielts-writing/80 !border-ielts-writing hover:!bg-ielts-writing",
module === "speaking" && "!bg-ielts-speaking/80 !border-ielts-speaking hover:!bg-ielts-speaking",
)}>
{capitalize(module)}
</Button>
))}
</div>
</div>
);
}

View File

@@ -1,7 +1,7 @@
import Button from "@/components/Low/Button";
import Input from "@/components/Low/Input";
import {Module} from "@/interfaces";
import useExamStore from "@/stores/examStore";
import useExamStore from "@/stores/exam";
import {getExamById} from "@/utils/exams";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import {RadioGroup} from "@headlessui/react";
@@ -16,8 +16,7 @@ export default function ExamLoader() {
const [examId, setExamId] = useState<string>();
const [isLoading, setIsLoading] = useState(false);
const setExams = useExamStore((state) => state.setExams);
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const dispatch = useExamStore((store) => store.dispatch);
const router = useRouter();
@@ -35,9 +34,7 @@ export default function ExamLoader() {
setIsLoading(false);
return;
}
setExams([exam]);
setSelectedModules([selectedModule]);
dispatch({type: 'INIT_EXAM', payload: {exams: [exam], modules: [selectedModule]}})
router.push("/exam");
}

View File

@@ -5,7 +5,7 @@ import useUsers from "@/hooks/useUsers";
import {Module} from "@/interfaces";
import {Exam} from "@/interfaces/exam";
import {Type, User} from "@/interfaces/user";
import useExamStore from "@/stores/examStore";
import useExamStore from "@/stores/exam";
import {getExamById} from "@/utils/exams";
import {countExercises} from "@/utils/moduleUtils";
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
@@ -92,8 +92,7 @@ export default function ExamList({user, entities}: {user: User; entities: Entity
const {rows: filteredRows, renderSearch} = useListSearch<Exam>(searchFields, parsedExams);
const setExams = useExamStore((state) => state.setExams);
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const dispatch = useExamStore((state) => state.dispatch);
const router = useRouter();
@@ -106,9 +105,7 @@ export default function ExamList({user, entities}: {user: User; entities: Entity
return;
}
setExams([exam]);
setSelectedModules([module]);
dispatch({type: "INIT_EXAM", payload: {exams: [exam], modules: [module]}})
router.push("/exercises");
};

View File

@@ -1,23 +1,14 @@
import Input from "@/components/Low/Input";
import Modal from "@/components/Modal";
import {PERMISSIONS} from "@/constants/userPermissions";
import useExams from "@/hooks/useExams";
import usePackages from "@/hooks/usePackages";
import useUsers from "@/hooks/useUsers";
import {Module} from "@/interfaces";
import {Exam} from "@/interfaces/exam";
import {Package} from "@/interfaces/paypal";
import {Type, User} from "@/interfaces/user";
import useExamStore from "@/stores/examStore";
import {getExamById} from "@/utils/exams";
import {countExercises} from "@/utils/moduleUtils";
import {User} from "@/interfaces/user";
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
import axios from "axios";
import clsx from "clsx";
import {capitalize} from "lodash";
import {useRouter} from "next/router";
import {useState} from "react";
import {BsCheck, BsPencil, BsTrash, BsUpload} from "react-icons/bs";
import {BsPencil, BsTrash} from "react-icons/bs";
import {toast} from "react-toastify";
import Select from "react-select";
import {CURRENCIES} from "@/resources/paypal";

View File

@@ -1,6 +1,6 @@
/* eslint-disable @next/next/no-img-element */
import { Module } from "@/interfaces";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import AbandonPopup from "@/components/AbandonPopup";
import Layout from "@/components/High/Layout";
@@ -11,9 +11,8 @@ import Reading from "@/exams/Reading";
import Selection from "@/exams/Selection";
import Speaking from "@/exams/Speaking";
import Writing from "@/exams/Writing";
import { Exam, LevelExam, UserSolution, Variant } from "@/interfaces/exam";
import { Exam, ExerciseOnlyExam, LevelExam, PartExam, UserSolution, Variant } from "@/interfaces/exam";
import { Stat, User } from "@/interfaces/user";
import useExamStore from "@/stores/examStore";
import { evaluateSpeakingAnswer, evaluateWritingAnswer } from "@/utils/evaluation";
import { defaultExamUserSolutions, getExam } from "@/utils/exams";
import axios from "axios";
@@ -21,6 +20,8 @@ import { useRouter } from "next/router";
import { toast, ToastContainer } from "react-toastify";
import { v4 as uuidv4 } from "uuid";
import ShortUniqueId from "short-unique-id";
import { ExamProps } from "@/exams/types";
import useExamStore from "@/stores/exam";
interface Props {
page: "exams" | "exercises";
@@ -29,198 +30,64 @@ interface Props {
hideSidebar?: boolean
}
export default function ExamPage({ page, user, destination = "/exam", hideSidebar = false }: Props) {
const ExamPage: React.FC<Props> = ({ page, user, destination = "/exam", hideSidebar = false }) => {
const router = useRouter();
const [variant, setVariant] = useState<Variant>("full");
const [avoidRepeated, setAvoidRepeated] = useState(false);
const [hasBeenUploaded, setHasBeenUploaded] = useState(false);
const [showAbandonPopup, setShowAbandonPopup] = useState(false);
const [isEvaluationLoading, setIsEvaluationLoading] = useState(false);
const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState<string[]>([]);
const [inactivityTimer, setInactivityTimer] = useState(0);
const [totalInactivity, setTotalInactivity] = useState(0);
const [timeSpent, setTimeSpent] = useState(0);
const resetStore = useExamStore((state) => state.reset);
const assignment = useExamStore((state) => state.assignment);
const initialTimeSpent = useExamStore((state) => state.timeSpent);
const {
exam, setExam,
exams,
sessionId, setSessionId, setPartIndex,
moduleIndex, setModuleIndex,
setQuestionIndex, setExerciseIndex,
userSolutions, setUserSolutions,
showSolutions, setShowSolutions,
selectedModules, setSelectedModules,
setUser,
inactivity,
timeSpent,
assignment,
bgColor,
flags,
dispatch,
reset: resetStore,
saveStats,
saveSession,
setFlags,
setShuffles
} = useExamStore();
const { exam, setExam } = useExamStore((state) => state);
const { exams, setExams } = useExamStore((state) => state);
const { sessionId, setSessionId } = useExamStore((state) => state);
const { partIndex, setPartIndex } = useExamStore((state) => state);
const { moduleIndex, setModuleIndex } = useExamStore((state) => state);
const { questionIndex, setQuestionIndex } = useExamStore((state) => state);
const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state);
const { userSolutions, setUserSolutions } = useExamStore((state) => state);
const { showSolutions, setShowSolutions } = useExamStore((state) => state);
const { selectedModules, setSelectedModules } = useExamStore((state) => state);
const { inactivity, setInactivity } = useExamStore((state) => state);
const { bgColor, setBgColor } = useExamStore((state) => state);
const setShuffleMaps = useExamStore((state) => state.setShuffles);
const { finalizeModule, finalizeExam } = flags;
const router = useRouter();
// eslint-disable-next-line react-hooks/exhaustive-deps
const resetInactivityTimer = () => {
setInactivityTimer((prev) => {
if (moduleIndex >= selectedModules.length || moduleIndex === -1) return 0;
if (prev >= 120) setTotalInactivity((totalPrev) => totalPrev + prev);
return 0;
});
};
const reset = () => {
resetStore();
setVariant("full");
setAvoidRepeated(false);
setHasBeenUploaded(false);
setShowAbandonPopup(false);
setIsEvaluationLoading(false);
setStatsAwaitingEvaluation([]);
setTimeSpent(0);
setInactivity(0);
document.removeEventListener("keydown", resetInactivityTimer);
document.removeEventListener("mousemove", resetInactivityTimer);
document.removeEventListener("mousedown", resetInactivityTimer);
};
const [isFetchingExams, setIsFetchingExams] = useState(false);
const [isExamLoaded, setIsExamLoaded] = useState(moduleIndex < selectedModules.length);
useEffect(() => {
if (moduleIndex >= selectedModules.length || moduleIndex === -1 || showSolutions) {
document.removeEventListener("keydown", resetInactivityTimer);
document.removeEventListener("mousemove", resetInactivityTimer);
document.removeEventListener("mousedown", resetInactivityTimer);
}
}, [moduleIndex, resetInactivityTimer, selectedModules.length, showSolutions]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const saveSession = async () => {
console.log("Saving your session...");
await axios.post("/api/sessions", {
id: sessionId,
sessionId,
date: new Date().toISOString(),
userSolutions,
moduleIndex,
selectedModules,
assignment,
timeSpent,
inactivity: totalInactivity,
exams,
exam,
partIndex,
exerciseIndex,
questionIndex,
user: user?.id,
});
};
useEffect(() => setTimeSpent(initialTimeSpent), [initialTimeSpent]);
useEffect(() => setTotalInactivity(inactivity), [inactivity]);
setIsExamLoaded(moduleIndex < selectedModules.length);
}, [showSolutions, moduleIndex, selectedModules]);
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 &&
!showSolutions &&
moduleIndex < selectedModules.length &&
selectedModules[moduleIndex] !== "speaking"
)
saveSession();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assignment, exam, exams, moduleIndex, selectedModules, sessionId, userSolutions, user, exerciseIndex, partIndex, questionIndex]);
useEffect(() => {
if (
timeSpent % 20 === 0 &&
timeSpent > 0 &&
moduleIndex < selectedModules.length &&
selectedModules[moduleIndex] !== "speaking" &&
!showSolutions
)
saveSession();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timeSpent]);
useEffect(() => {
if (selectedModules.length > 0 && sessionId.length === 0) {
if (!showSolutions && sessionId.length === 0 && user?.id) {
const shortUID = new ShortUniqueId();
setUser(user.id);
setSessionId(shortUID.randomUUID(8));
}
}, [setSessionId, selectedModules, sessionId]);
}, [setSessionId, isExamLoaded, sessionId, showSolutions, setUser, user?.id]);
useEffect(() => {
if (user?.type === "developer") console.log(exam);
}, [exam, user]);
useEffect(() => {
if (selectedModules.length > 0 && timeSpent === 0 && !showSolutions) {
const timerInterval = setInterval(() => {
setTimeSpent((prev) => prev + 1);
}, 1000);
return () => {
clearInterval(timerInterval);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedModules.length]);
useEffect(() => {
if (selectedModules.length > 0 && !showSolutions && inactivityTimer === 0) {
const inactivityInterval = setInterval(() => {
setInactivityTimer((prev) => prev + 1);
}, 1000);
return () => {
clearInterval(inactivityInterval);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedModules.length]);
useEffect(() => {
document.addEventListener("keydown", resetInactivityTimer);
document.addEventListener("mousemove", resetInactivityTimer);
document.addEventListener("mousedown", resetInactivityTimer);
return () => {
document.removeEventListener("keydown", resetInactivityTimer);
document.removeEventListener("mousemove", resetInactivityTimer);
document.removeEventListener("mousedown", resetInactivityTimer);
};
});
useEffect(() => {
if (showSolutions) setModuleIndex(-1);
}, [setModuleIndex, showSolutions]);
useEffect(() => {
if (selectedModules.length > 0 && exams.length > 0 && moduleIndex < selectedModules.length) {
const nextExam = exams[moduleIndex];
if (partIndex === -1 && nextExam?.module !== "listening") setPartIndex(0);
if (exerciseIndex === -1 && !["reading", "listening"].includes(nextExam?.module)) setExerciseIndex(0);
setExam(nextExam ? updateExamWithUserSolutions(nextExam) : undefined);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedModules, moduleIndex, exams]);
useEffect(() => {
(async () => {
if (selectedModules.length > 0 && exams.length === 0) {
setIsFetchingExams(true);
const examPromises = selectedModules.map((module) =>
getExam(
module,
@@ -230,8 +97,9 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba
),
);
Promise.all(examPromises).then((values) => {
setIsFetchingExams(false);
if (values.every((x) => !!x)) {
setExams(values.map((x) => x!));
dispatch({ type: 'INIT_EXAM', payload: { exams: values.map((x) => x!), modules: selectedModules } })
} else {
toast.error("Something went wrong, please try again");
setTimeout(router.reload, 500);
@@ -240,100 +108,61 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedModules, setExams, exams]);
}, [selectedModules, exams]);
useEffect(() => {
if (selectedModules.length > 0 && exams.length !== 0 && moduleIndex >= selectedModules.length && !hasBeenUploaded && !showSolutions) {
const newStats: Stat[] = userSolutions.map((solution) => ({
...solution,
id: solution.id || uuidv4(),
timeSpent,
inactivity: totalInactivity,
session: sessionId,
exam: solution.exam!,
module: solution.module!,
user: user?.id || "",
date: new Date().getTime(),
isDisabled: solution.isDisabled,
shuffleMaps: solution.shuffleMaps,
...(assignment ? { assignment: assignment.id } : {}),
isPractice: solution.isPractice
}));
axios
.post<{ ok: boolean }>("/api/stats", newStats)
.then((response) => setHasBeenUploaded(response.data.ok))
.catch(() => setHasBeenUploaded(false));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedModules, moduleIndex, hasBeenUploaded]);
useEffect(() => {
setIsEvaluationLoading(statsAwaitingEvaluation.length !== 0);
}, [statsAwaitingEvaluation]);
useEffect(() => {
if (statsAwaitingEvaluation.length > 0) checkIfStatsHaveBeenEvaluated(statsAwaitingEvaluation);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [statsAwaitingEvaluation]);
useEffect(() => {
if (exam && exam.module === "level" && !showSolutions) setBgColor("bg-ielts-level-light");
}, [exam, showSolutions, setBgColor]);
const checkIfStatsHaveBeenEvaluated = (ids: string[]) => {
setTimeout(async () => {
try {
const awaitedStats = await Promise.all(ids.map(async (id) => (await axios.get<Stat>(`/api/stats/${id}`)).data));
const solutionsEvaluated = awaitedStats.every((stat) => stat.solutions.every((x) => x.evaluation !== null));
if (solutionsEvaluated) {
const statsUserSolutions: UserSolution[] = awaitedStats.map((stat) => ({
id: stat.id,
exercise: stat.exercise,
score: stat.score,
solutions: stat.solutions,
type: stat.type,
exam: stat.exam,
module: stat.module,
}));
const updatedUserSolutions = userSolutions.map((x) => {
const respectiveSolution = statsUserSolutions.find((y) => y.exercise === x.exercise);
return respectiveSolution ? respectiveSolution : x;
});
setUserSolutions(updatedUserSolutions);
return setStatsAwaitingEvaluation((prev) => prev.filter((x) => !ids.includes(x)));
}
return checkIfStatsHaveBeenEvaluated(ids);
} catch {
return checkIfStatsHaveBeenEvaluated(ids);
}
}, 5 * 1000);
const reset = () => {
resetStore();
setVariant("full");
setAvoidRepeated(false);
setHasBeenUploaded(false);
setShowAbandonPopup(false);
setIsEvaluationLoading(false);
setStatsAwaitingEvaluation([]);
};
const updateExamWithUserSolutions = (exam: Exam): Exam => {
if (exam.module === "reading" || exam.module === "listening" || exam.module === "level") {
const parts = exam.parts.map((p) =>
Object.assign(p, {
exercises: p.exercises.map((x) =>
Object.assign(x, {
userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions,
}),
),
}),
);
return Object.assign(exam, { parts });
}
useEffect(() => {
if (finalizeModule && !showSolutions) {
/*if (exam && (exam.module === "writing" || exam.module === "speaking") && userSolutions.length > 0 && !showSolutions) {
setIsEvaluationLoading(true);
(async () => {
const responses: UserSolution[] = (
await Promise.all(
exam.exercises.map(async (exercise, index) => {
const evaluationID = uuidv4();
if (exercise.type === "writing")
return await evaluateWritingAnswer(exercise, index + 1, userSolutions.find((x) => x.exercise === exercise.id)!, evaluationID);
const exercises = exam.exercises.map((x) =>
Object.assign(x, {
userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions,
}),
);
return Object.assign(exam, { exercises });
};
if (exercise.type === "interactiveSpeaking" || exercise.type === "speaking")
return await evaluateSpeakingAnswer(
exercise,
userSolutions.find((x) => x.exercise === exercise.id)!,
evaluationID,
index + 1,
);
}),
)
).filter((x) => !!x) as UserSolution[];
})();
}*/
}
}, [exam, finalizeModule, showSolutions, userSolutions]);
/*useEffect(() => {
// poll backend and setIsEvaluationLoading to false
}, []);*/
useEffect(() => {
if (finalizeExam && !isEvaluationLoading) {
(async () => {
axios.get("/api/stats/update");
await saveStats();
setModuleIndex(-1);
setFlags({ finalizeExam: false });
})();
}
}, [finalizeExam, saveStats, setFlags, setModuleIndex, isEvaluationLoading]);
const onFinish = async (solutions: UserSolution[]) => {
const solutionIds = solutions.map((x) => x.exercise);
@@ -375,8 +204,8 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba
setUserSolutions([...userSolutions.filter((x) => !solutionIds.includes(x.exercise)), ...newSolutions]);
setModuleIndex(moduleIndex + 1);
setPartIndex(-1);
setExerciseIndex(-1);
setPartIndex(0);
setExerciseIndex(0);
setQuestionIndex(0);
};
@@ -432,82 +261,19 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba
.map((x) => ({ module: x as Module, ...scores[x as Module] }));
};
const renderScreen = () => {
if (selectedModules.length === 0) {
return (
<Selection
page={page}
user={user!}
onStart={(modules: Module[], avoid: boolean, variant: Variant) => {
setModuleIndex(0);
setAvoidRepeated(avoid);
setSelectedModules(modules);
setVariant(variant);
}}
/>
);
}
const ModuleExamMap: Record<Module, React.ComponentType<ExamProps<Exam>>> = {
"reading": Reading as React.ComponentType<ExamProps<Exam>>,
"listening": Listening as React.ComponentType<ExamProps<Exam>>,
"writing": Writing as React.ComponentType<ExamProps<Exam>>,
"speaking": Speaking as React.ComponentType<ExamProps<Exam>>,
"level": Level as React.ComponentType<ExamProps<Exam>>,
}
if (moduleIndex >= selectedModules.length || moduleIndex === -1) {
return (
<Finish
isLoading={isEvaluationLoading}
user={user!}
modules={selectedModules}
solutions={userSolutions}
assignment={assignment}
information={{
timeSpent,
inactivity: totalInactivity,
}}
destination={destination}
onViewResults={(index?: number) => {
if (exams[0].module === "level") {
const levelExam = exams[0] as LevelExam;
const allExercises = levelExam.parts.flatMap((part) => part.exercises);
const exerciseOrderMap = new Map(allExercises.map((ex, index) => [ex.id, index]));
const orderedSolutions = userSolutions.slice().sort((a, b) => {
const indexA = exerciseOrderMap.get(a.exercise) ?? Infinity;
const indexB = exerciseOrderMap.get(b.exercise) ?? Infinity;
return indexA - indexB;
});
setUserSolutions(orderedSolutions);
} else {
setUserSolutions(userSolutions);
}
setShuffleMaps([]);
setShowSolutions(true);
setModuleIndex(index || 0);
setExerciseIndex(["reading", "listening"].includes(exams[0].module) ? -1 : 0);
setPartIndex(exams[0].module === "listening" ? -1 : 0);
setExam(exams[0]);
}}
scores={aggregateScoresByModule()}
/>
);
}
const CurrentExam = exam?.module ? ModuleExamMap[exam.module] : undefined;
if (exam && exam.module === "reading") {
return <Reading exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
}
if (exam && exam.module === "listening") {
return <Listening exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
}
if (exam && exam.module === "writing") {
return <Writing exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
}
if (exam && exam.module === "speaking") {
return <Speaking exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
}
if (exam && exam.module === "level") {
return <Level exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
}
return <>Loading...</>;
const onAbandon = async () => {
await saveSession();
reset();
};
return (
@@ -522,18 +288,79 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba
focusMode={selectedModules.length !== 0 && !showSolutions && moduleIndex < selectedModules.length}
onFocusLayerMouseEnter={() => setShowAbandonPopup(true)}>
<>
{renderScreen()}
{!showSolutions && moduleIndex < selectedModules.length && (
<AbandonPopup
isOpen={showAbandonPopup}
abandonPopupTitle="Leave Exercise"
abandonPopupDescription="Are you sure you want to leave the exercise? Your progress will be saved and this exam can be resumed on the Dashboard."
abandonConfirmButtonText="Confirm"
onAbandon={() => {
reset();
{/* Modules weren't yet set by an INIT_EXAM or INIT_SOLUTIONS dispatch, show Selection component*/}
{selectedModules.length === 0 && <Selection
page={page}
user={user!}
onStart={(modules: Module[], avoid: boolean, variant: Variant) => {
setModuleIndex(0);
setAvoidRepeated(avoid);
setSelectedModules(modules);
setVariant(variant);
}}
/>}
{isFetchingExams && (
<div className="flex flex-grow flex-col items-center justify-center animate-pulse">
<span className={`loading loading-infinity w-32 bg-ielts-${selectedModules[0]}`} />
<span className={`font-bold text-2xl text-ielts-${selectedModules[0]}`}>Loading Exam ...</span>
</div>
)}
{(moduleIndex === -1 && selectedModules.length !== 0) &&
<Finish
isLoading={isEvaluationLoading}
user={user!}
modules={selectedModules}
solutions={userSolutions}
assignment={assignment}
information={{
timeSpent,
inactivity,
}}
onCancel={() => setShowAbandonPopup(false)}
/>
destination={destination}
onViewResults={(index?: number) => {
if (exams[0].module === "level") {
const levelExam = exams[0] as LevelExam;
const allExercises = levelExam.parts.flatMap((part) => part.exercises);
const exerciseOrderMap = new Map(allExercises.map((ex, index) => [ex.id, index]));
const orderedSolutions = userSolutions.slice().sort((a, b) => {
const indexA = exerciseOrderMap.get(a.exercise) ?? Infinity;
const indexB = exerciseOrderMap.get(b.exercise) ?? Infinity;
return indexA - indexB;
});
setUserSolutions(orderedSolutions);
} else {
setUserSolutions(userSolutions);
}
setShuffles([]);
if (index === undefined) {
setFlags({ reviewAll: true });
setModuleIndex(0);
setExam(exams[0]);
} else {
setModuleIndex(index);
setExam(exams[index]);
}
setShowSolutions(true);
setQuestionIndex(0);
setExerciseIndex(0);
setPartIndex(0);
}}
scores={aggregateScoresByModule()}
/>}
{/* Exam is on going, display it and the abandon modal */}
{isExamLoaded && moduleIndex !== -1 && (
<>
{exam && CurrentExam && <CurrentExam exam={exam} showSolutions={showSolutions} />}
{!showSolutions && <AbandonPopup
isOpen={showAbandonPopup}
abandonPopupTitle="Leave Exercise"
abandonPopupDescription="Are you sure you want to leave the exercise? Your progress will be saved and this exam can be resumed on the Dashboard."
abandonConfirmButtonText="Confirm"
onAbandon={onAbandon}
onCancel={() => setShowAbandonPopup(false)}
/>
}
</>
)}
</>
</Layout>
@@ -541,3 +368,5 @@ export default function ExamPage({ page, user, destination = "/exam", hideSideba
</>
);
}
export default ExamPage;

View File

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

View File

@@ -90,7 +90,7 @@ async function evaluate(body: {answers: object[]}, variant?: "initial" | "final"
},
});
if (typeof backendRequest.data === "string") return evaluate(body);
if (backendRequest.status !== 200) return evaluate(body);
return backendRequest;
}

View File

@@ -7,11 +7,9 @@ import formidable from "formidable-serverless";
import {getDownloadURL, ref, uploadBytes} from "firebase/storage";
import fs from "fs";
import {storage} from "@/firebase";
import client from "@/lib/mongodb";
import {Stat} from "@/interfaces/user";
import {speakingReverseMarking} from "@/utils/score";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
function delay(ms: number) {
@@ -30,54 +28,29 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const audioFile = files.audio;
const audioFileRef = ref(storage, `speaking_recordings/${fields.id}.wav`);
const task = parseInt(fields.task.toString());
const binary = fs.readFileSync((audioFile as any).path).buffer;
const snapshot = await uploadBytes(audioFileRef, binary);
const url = await getDownloadURL(snapshot.ref);
const path = snapshot.metadata.fullPath;
res.status(200).json(null);
console.log("🌱 - Still processing");
const backendRequest = await evaluate({answer: path, question: fields.question}, task);
console.log("🌱 - Process complete");
const correspondingStat = await getCorrespondingStat(fields.id, 1);
const solutions = correspondingStat.solutions.map((x) => ({
/*const solutions = correspondingStat.solutions.map((x) => ({
...x,
evaluation: backendRequest.data,
solution: url,
}));
}));*/
await db.collection("stats").updateOne(
{ id: fields.id },
{
id: fields.id,
solutions,
score: {
correct: speakingReverseMarking[backendRequest.data.overall || 0] || 0,
total: 100,
missing: 0,
},
isDisabled: false,
await axios.post(`${process.env.BACKEND_URL}/grade/speaking/2`, {answer: path, question: fields.question}, {
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
{upsert: true},
);
console.log("🌱 - Updated the DB");
});
});
}
async function getCorrespondingStat(id: string, index: number): Promise<Stat> {
console.log(`🌱 - Try number ${index} - ${id}`);
const correspondingStat = await db.collection("stats").findOne<Stat>({ id: id });
if (correspondingStat) return correspondingStat;
await delay(3 * 10000);
return getCorrespondingStat(id, index + 1);
}
async function evaluate(body: {answer: string; question: string}, task: number): Promise<AxiosResponse> {
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/grade/speaking/2`, body, {
headers: {
@@ -85,7 +58,7 @@ async function evaluate(body: {answer: string; question: string}, task: number):
},
});
if (typeof backendRequest.data === "string") return evaluate(body, task);
if (backendRequest.status !== 200) return evaluate(body, task);
return backendRequest;
}

View File

@@ -1,11 +1,8 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios, { AxiosResponse } from "axios";
import { Stat } from "@/interfaces/user";
import { writingReverseMarking } from "@/utils/score";
import axios from "axios";
interface Body {
question: string;
@@ -14,67 +11,22 @@ interface Body {
id: string;
}
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
res.status(200).json(null);
console.log("🌱 - Still processing");
const backendRequest = await evaluate(req.body as Body);
console.log("🌱 - Process complete");
const correspondingStat = await getCorrespondingStat(req.body.id, 1);
const solutions = correspondingStat.solutions.map((x) => ({ ...x, evaluation: backendRequest.data }));
await db.collection("stats").updateOne(
{ id: (req.body as Body).id },
{
$set: {
id: (req.body as Body).id,
solutions,
score: {
correct: writingReverseMarking[backendRequest.data.overall],
total: 100,
missing: 0,
},
isDisabled: false,
}
},
{ upsert: true },
);
console.log("🌱 - Updated the DB");
}
async function getCorrespondingStat(id: string, index: number): Promise<Stat> {
console.log(`🌱 - Try number ${index} - ${id}`);
const correspondingStat = await db.collection("stats").findOne<Stat>({ id: id });
if (correspondingStat) return correspondingStat;
await delay(3 * 10000);
return getCorrespondingStat(id, index + 1);
}
async function evaluate(body: Body): Promise<AxiosResponse> {
const body = req.body as Body;
const taskNumber = body.task.toString() !== "1" && body.task.toString() !== "2" ? "1" : body.task.toString();
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/grade/writing/${taskNumber}`, body as Body, {
await axios.post(`${process.env.BACKEND_URL}/grade/writing/${taskNumber}`, body, {
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
});
if (typeof backendRequest.data === "string") return evaluate(body);
return backendRequest;
res.status(200);
}

View File

@@ -2,32 +2,31 @@ import Button from "@/components/Low/Button";
import ProgressBar from "@/components/Low/ProgressBar";
import Modal from "@/components/Modal";
import useUsers from "@/hooks/useUsers";
import {Module} from "@/interfaces";
import {Assignment} from "@/interfaces/results";
import {Group, Stat, User} from "@/interfaces/user";
import useExamStore from "@/stores/examStore";
import {getExamById} from "@/utils/exams";
import {sortByModule} from "@/utils/moduleUtils";
import {calculateBandScore} from "@/utils/score";
import {convertToUserSolutions} from "@/utils/stats";
import {getUserName} from "@/utils/users";
import { Module } from "@/interfaces";
import { Assignment } from "@/interfaces/results";
import { Group, Stat, User } from "@/interfaces/user";
import useExamStore from "@/stores/exam";
import { getExamById } from "@/utils/exams";
import { sortByModule } from "@/utils/moduleUtils";
import { calculateBandScore } from "@/utils/score";
import { convertToUserSolutions } from "@/utils/stats";
import { getUserName } from "@/utils/users";
import axios from "axios";
import clsx from "clsx";
import {capitalize, uniqBy} from "lodash";
import { capitalize, uniqBy } from "lodash";
import moment from "moment";
import {useRouter} from "next/router";
import {BsBook, BsBuilding, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
import {toast} from "react-toastify";
import {futureAssignmentFilter} from "@/utils/assignments";
import {withIronSessionSsr} from "iron-session/next";
import {checkAccess, doesEntityAllow} from "@/utils/permissions";
import {mapBy, redirect, serialize} from "@/utils";
import {getAssignment} from "@/utils/assignments.be";
import {getEntitiesUsers, getEntityUsers, getUsers} from "@/utils/users.be";
import {getEntitiesWithRoles, getEntityWithRoles} from "@/utils/entities.be";
import {getGroups, getGroupsByEntities, getGroupsByEntity} from "@/utils/groups.be";
import {sessionOptions} from "@/lib/session";
import {EntityWithRoles} from "@/interfaces/entity";
import { useRouter } from "next/router";
import { BsBook, BsBuilding, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs";
import { toast } from "react-toastify";
import { futureAssignmentFilter } from "@/utils/assignments";
import { withIronSessionSsr } from "iron-session/next";
import { checkAccess, doesEntityAllow } from "@/utils/permissions";
import { mapBy, redirect, serialize } from "@/utils";
import { getAssignment } from "@/utils/assignments.be";
import { getEntityUsers, getUsers } from "@/utils/users.be";
import { getEntityWithRoles } from "@/utils/entities.be";
import { sessionOptions } from "@/lib/session";
import { EntityWithRoles } from "@/interfaces/entity";
import Head from "next/head";
import Layout from "@/components/High/Layout";
import Separator from "@/components/Low/Separator";
@@ -35,7 +34,7 @@ import Link from "next/link";
import { requestUser } from "@/utils/api";
import { useEntityPermission } from "@/hooks/useEntityPermissions";
export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) => {
export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => {
const user = await requestUser(req, res)
if (!user) return redirect("/login")
@@ -44,22 +43,22 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res, params})
res.setHeader("Cache-Control", "public, s-maxage=10, stale-while-revalidate=59");
const {id} = params as {id: string};
const { id } = params as { id: string };
const assignment = await getAssignment(id);
if (!assignment) return redirect("/assignments")
const entity = await getEntityWithRoles(assignment.entity || "")
if (!entity){
if (!entity) {
const users = await getUsers()
return {props: serialize({user, users, assignment})};
return { props: serialize({ user, users, assignment }) };
}
if (!doesEntityAllow(user, entity, 'view_assignments')) return redirect("/assignments")
const users = await (checkAccess(user, ["developer", "admin"]) ? getUsers() : getEntityUsers(entity.id));
return {props: serialize({user, users, entity, assignment})};
return { props: serialize({ user, users, entity, assignment }) };
}, sessionOptions);
interface Props {
@@ -69,17 +68,14 @@ interface Props {
entity?: EntityWithRoles
}
export default function AssignmentView({user, users, entity, assignment}: Props) {
export default function AssignmentView({ user, users, entity, assignment }: Props) {
const canDeleteAssignment = useEntityPermission(user, entity, 'delete_assignment')
const canStartAssignment = useEntityPermission(user, entity, 'start_assignment')
const setExams = useExamStore((state) => state.setExams);
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
const setUserSolutions = useExamStore((state) => state.setUserSolutions);
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const router = useRouter();
const dispatch = useExamStore((state) => state.dispatch);
const deleteAssignment = async () => {
if (!canDeleteAssignment) return
if (!confirm("Are you sure you want to delete this assignment?")) return;
@@ -128,9 +124,9 @@ export default function AssignmentView({user, users, entity, assignment}: Props)
return resultModuleBandScores.length === 0 ? -1 : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / assignment.results.length;
};
const aggregateScoresByModule = (stats: Stat[]): {module: Module; total: number; missing: number; correct: number}[] => {
const aggregateScoresByModule = (stats: Stat[]): { module: Module; total: number; missing: number; correct: number }[] => {
const scores: {
[key in Module]: {total: number; missing: number; correct: number};
[key in Module]: { total: number; missing: number; correct: number };
} = {
reading: {
total: 0,
@@ -169,7 +165,7 @@ export default function AssignmentView({user, users, entity, assignment}: Props)
return Object.keys(scores)
.filter((x) => scores[x as Module].total > 0)
.map((x) => ({module: x as Module, ...scores[x as Module]}));
.map((x) => ({ module: x as Module, ...scores[x as Module] }));
};
const customContent = (stats: Stat[], user: string, focus: "academic" | "general") => {
@@ -189,15 +185,16 @@ export default function AssignmentView({user, users, entity, assignment}: Props)
Promise.all(examPromises).then((exams) => {
if (exams.every((x) => !!x)) {
setUserSolutions(convertToUserSolutions(stats));
setShowSolutions(true);
setExams(exams.map((x) => x!).sort(sortByModule));
setSelectedModules(
exams
.map((x) => x!)
.sort(sortByModule)
.map((x) => x!.module),
);
dispatch({
type: 'INIT_SOLUTIONS', payload: {
exams: exams.map((x) => x!).sort(sortByModule),
modules: exams
.map((x) => x!)
.sort(sortByModule)
.map((x) => x!.module),
stats,
}
});
router.push("/exam");
}
});
@@ -228,7 +225,7 @@ export default function AssignmentView({user, users, entity, assignment}: Props)
<div className="flex w-full flex-col gap-1">
<div className="-md:mt-2 grid w-full grid-cols-4 place-items-start gap-2">
{aggregatedLevels.map(({module, level}) => (
{aggregatedLevels.map(({ module, level }) => (
<div
key={module}
className={clsx(
@@ -306,7 +303,7 @@ export default function AssignmentView({user, users, entity, assignment}: Props)
if (!confirm(`Are you sure you want to remove ${inactiveAssignees.length} assignees?`)) return
axios
.patch(`/api/assignments/${assignment.id}`, {assignees: activeAssignees})
.patch(`/api/assignments/${assignment.id}`, { assignees: activeAssignees })
.then(() => {
toast.success(`The assignment "${assignment.name}" has been updated successfully!`);
router.replace(router.asPath);
@@ -388,7 +385,7 @@ export default function AssignmentView({user, users, entity, assignment}: Props)
<span className="text-xl font-bold">Average Scores</span>
<div className="-md:mt-2 flex w-full items-center gap-4">
{assignment &&
uniqBy(assignment.exams, (x) => x.module).map(({module}) => (
uniqBy(assignment.exams, (x) => x.module).map(({ module }) => (
<div
data-tip={capitalize(module)}
key={module}

View File

@@ -13,7 +13,7 @@ import { InviteWithEntity } from "@/interfaces/invite";
import {Assignment} from "@/interfaces/results";
import {Stat, User} from "@/interfaces/user";
import {sessionOptions} from "@/lib/session";
import useExamStore from "@/stores/examStore";
import useExamStore from "@/stores/exam";
import {findBy, mapBy, redirect, serialize} from "@/utils";
import { requestUser } from "@/utils/api";
import {activeAssignmentFilter} from "@/utils/assignments";
@@ -81,11 +81,7 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
export default function Dashboard({user, entities, assignments, stats, invites, grading, sessions, exams}: Props) {
const router = useRouter();
const setExams = useExamStore((state) => state.setExams);
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
const setUserSolutions = useExamStore((state) => state.setUserSolutions);
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const setAssignment = useExamStore((state) => state.setAssignment);
const dispatch = useExamStore((state) => state.dispatch);
const startAssignment = (assignment: Assignment) => {
const assignmentExams = exams.filter(e => {
@@ -94,11 +90,11 @@ export default function Dashboard({user, entities, assignments, stats, invites,
})
if (assignmentExams.every((x) => !!x)) {
setUserSolutions([]);
setShowSolutions(false);
setExams(assignmentExams.sort(sortByModule));
setSelectedModules(mapBy(assignmentExams.sort(sortByModule), 'module'));
setAssignment(assignment);
dispatch({type: "INIT_EXAM", payload: {
exams: assignmentExams.sort(sortByModule),
modules: mapBy(assignmentExams.sort(sortByModule), 'module'),
assignment
}})
router.push("/exam");
}

View File

@@ -1,4 +1,5 @@
/* eslint-disable @next/next/no-img-element */
//import "@/utils/wdyr";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
@@ -6,20 +7,19 @@ import { shouldRedirectHome } from "@/utils/navigation.disabled";
import ExamPage from "./(exam)/ExamPage";
import Head from "next/head";
import { User } from "@/interfaces/user";
import { filterBy, findBy, redirect, serialize } from "@/utils";
import { filterBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import { getAssignment, getAssignments, getAssignmentsByAssignee } from "@/utils/assignments.be";
import { getAssignment } from "@/utils/assignments.be";
import { Assignment } from "@/interfaces/results";
import useExamStore from "@/stores/examStore";
import useExamStore from "@/stores/exam";
import { useEffect } from "react";
import { Exam } from "@/interfaces/exam";
import { getExamsByIds } from "@/utils/exams.be";
import { sortByModule } from "@/utils/moduleUtils";
import { uniqBy } from "lodash";
import { useRouter } from "next/router";
import { getSessionByAssignment, getSessionsByUser } from "@/utils/sessions.be";
import { getSessionByAssignment } from "@/utils/sessions.be";
import { Session } from "@/hooks/useSessions";
import moment from "moment";
import { activeAssignmentFilter } from "@/utils/assignments";
export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => {
@@ -63,25 +63,23 @@ interface Props {
destinationURL?: string
}
export default function Page({ user, assignment, exams = [], destinationURL = "/exam", session }: Props) {
const Page: React.FC<Props> = ({ user, assignment, exams = [], destinationURL = "/exam", session }) => {
const router = useRouter()
const state = useExamStore((state) => state)
const { assignment: storeAssignment, dispatch } = useExamStore();
useEffect(() => {
if (assignment && exams.length > 0 && !state.assignment && !session) {
if (assignment && exams.length > 0 && !storeAssignment && !session) {
if (!activeAssignmentFilter(assignment)) return
state.setUserSolutions([]);
state.setShowSolutions(false);
state.setAssignment(assignment);
state.setExams(exams.sort(sortByModule));
state.setSelectedModules(
exams
.map((x) => x!)
.sort(sortByModule)
.map((x) => x!.module),
);
dispatch({
type: "INIT_EXAM", payload: {
exams: exams.sort(sortByModule),
modules: exams
.map((x) => x!)
.sort(sortByModule)
.map((x) => x!.module),
assignment
}
});
router.replace(router.asPath)
}
@@ -89,21 +87,8 @@ export default function Page({ user, assignment, exams = [], destinationURL = "/
}, [assignment, exams, session])
useEffect(() => {
if (assignment && exams.length > 0 && !state.assignment && !!session) {
state.setShuffles(session.userSolutions.map((x) => ({ exerciseID: x.exercise, shuffles: x.shuffleMaps ? x.shuffleMaps : [] })));
state.setSelectedModules(session.selectedModules);
state.setExam(session.exam);
state.setExams(session.exams);
state.setSessionId(session.sessionId);
state.setAssignment(session.assignment);
state.setExerciseIndex(session.exerciseIndex);
state.setPartIndex(session.partIndex);
state.setModuleIndex(session.moduleIndex);
state.setTimeSpent(session.timeSpent);
state.setUserSolutions(session.userSolutions);
state.setShowSolutions(false);
state.setQuestionIndex(session.questionIndex);
if (assignment && exams.length > 0 && !storeAssignment && !!session) {
dispatch({ type: "SET_SESSION", payload: { session } })
router.replace(router.asPath)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -120,7 +105,10 @@ export default function Page({ user, assignment, exams = [], destinationURL = "/
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ExamPage page="exams" destination={destinationURL} user={user} hideSidebar={!!assignment || !!state.assignment} />
<ExamPage page="exams" destination={destinationURL} user={user} hideSidebar={!!assignment || !!storeAssignment} />
</>
);
}
//Page.whyDidYouRender = true;
export default Page;

View File

@@ -1,34 +1,34 @@
/* eslint-disable @next/next/no-img-element */
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import ExamPage from "./(exam)/ExamPage";
import Head from "next/head";
import {User} from "@/interfaces/user";
import { filterBy, findBy, redirect, serialize } from "@/utils";
import { User } from "@/interfaces/user";
import { filterBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import { getAssignment, getAssignments, getAssignmentsByAssignee } from "@/utils/assignments.be";
import { getAssignment } from "@/utils/assignments.be";
import { Assignment } from "@/interfaces/results";
import useExamStore from "@/stores/examStore";
import useExamStore from "@/stores/exam";
import { useEffect } from "react";
import { Exam } from "@/interfaces/exam";
import { getExamsByIds } from "@/utils/exams.be";
import { sortByModule } from "@/utils/moduleUtils";
import { uniqBy } from "lodash";
import { useRouter } from "next/router";
import { getSessionByAssignment, getSessionsByUser } from "@/utils/sessions.be";
import { getSessionByAssignment } from "@/utils/sessions.be";
import { Session } from "@/hooks/useSessions";
import moment from "moment";
export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => {
export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => {
const user = await requestUser(req, res)
const destination = Buffer.from(req.url || "/").toString("base64")
if (!user) return redirect(`/login?destination=${destination}`)
if (shouldRedirectHome(user)) return redirect("/")
const {assignment: assignmentID} = query as {assignment?: string}
const { assignment: assignmentID } = query as { assignment?: string }
if (assignmentID) {
const assignment = await getAssignment(assignmentID)
@@ -47,12 +47,12 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) =
return redirect("/exam")
return {
props: serialize({user, assignment, exams, session})
props: serialize({ user, assignment, exams, session })
}
}
return {
props: serialize({user}),
props: serialize({ user }),
};
}, sessionOptions);
@@ -63,48 +63,36 @@ interface Props {
session?: Session
}
export default function Page({user, assignment, exams = [], session}: Props) {
export default function Page({ user, assignment, exams = [], session }: Props) {
const router = useRouter()
const state = useExamStore((state) => state)
const { assignment: storeAssignment, dispatch } = useExamStore()
useEffect(() => {
if (assignment && exams.length > 0 && !state.assignment && !session) {
state.setUserSolutions([]);
state.setShowSolutions(false);
state.setAssignment(assignment);
state.setExams(exams.sort(sortByModule));
state.setSelectedModules(
exams
.map((x) => x!)
.sort(sortByModule)
.map((x) => x!.module),
);
if (assignment && exams.length > 0 && !storeAssignment && !session) {
dispatch({
type: "INIT_EXAM", payload: {
exams: exams.sort(sortByModule),
modules: exams
.map((x) => x!)
.sort(sortByModule)
.map((x) => x!.module),
assignment
}
})
router.replace(router.asPath)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assignment, exams, session])
useEffect(() => {
if (assignment && exams.length > 0 && !state.assignment && !!session) {
state.setShuffles(session.userSolutions.map((x) => ({exerciseID: x.exercise, shuffles: x.shuffleMaps ? x.shuffleMaps : []})));
state.setSelectedModules(session.selectedModules);
state.setExam(session.exam);
state.setExams(session.exams);
state.setSessionId(session.sessionId);
state.setAssignment(session.assignment);
state.setExerciseIndex(session.exerciseIndex);
state.setPartIndex(session.partIndex);
state.setModuleIndex(session.moduleIndex);
state.setTimeSpent(session.timeSpent);
state.setUserSolutions(session.userSolutions);
state.setShowSolutions(false);
state.setQuestionIndex(session.questionIndex);
if (assignment && exams.length > 0 && !storeAssignment && !!session) {
dispatch({ type: "SET_SESSION", payload: { session } });
router.replace(router.asPath)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assignment, exams, session])
return (

View File

@@ -12,7 +12,7 @@ import { InviteWithEntity } from "@/interfaces/invite";
import { Assignment } from "@/interfaces/results";
import { Stat, User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session";
import useExamStore from "@/stores/examStore";
import useExamStore from "@/stores/exam";
import { filterBy, findBy, mapBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import { activeAssignmentFilter, futureAssignmentFilter } from "@/utils/assignments";
@@ -75,7 +75,9 @@ export default function OfficialExam({ user, entities, assignments, sessions, ex
const [isLoading, setIsLoading] = useState(false)
const router = useRouter();
const state = useExamStore((state) => state);
const dispatch = useExamStore((state) => state.dispatch);
const reload = () => {
setIsLoading(true)
router.replace(router.asPath)
@@ -89,31 +91,19 @@ export default function OfficialExam({ user, entities, assignments, sessions, ex
})
if (assignmentExams.every((x) => !!x)) {
state.setUserSolutions([]);
state.setShowSolutions(false);
state.setExams(assignmentExams.sort(sortByModule));
state.setSelectedModules(mapBy(assignmentExams.sort(sortByModule), 'module'));
state.setAssignment(assignment);
dispatch({
type: "INIT_EXAM", payload: {
exams: assignmentExams.sort(sortByModule),
modules: mapBy(assignmentExams.sort(sortByModule), 'module'),
assignment
}
})
router.push(`/exam?assignment=${assignment.id}&destination=${destination}`);
}
};
const loadSession = async (session: Session) => {
state.setShuffles(session.userSolutions.map((x) => ({ exerciseID: x.exercise, shuffles: x.shuffleMaps ? x.shuffleMaps : [] })));
state.setSelectedModules(session.selectedModules);
state.setExam(session.exam);
state.setExams(session.exams);
state.setSessionId(session.sessionId);
state.setAssignment(session.assignment);
state.setExerciseIndex(session.exerciseIndex);
state.setPartIndex(session.partIndex);
state.setModuleIndex(session.moduleIndex);
state.setTimeSpent(session.timeSpent);
state.setUserSolutions(session.userSolutions);
state.setShowSolutions(false);
state.setQuestionIndex(session.questionIndex);
dispatch({type: "SET_SESSION", payload: {session}});
router.push(`/exam?assignment=${session.assignment?.id}&destination=${destination}`);
};

View File

@@ -7,7 +7,6 @@ import { useEffect, useMemo, useState } from "react";
import useFilterRecordsByUser from "@/hooks/useFilterRecordsByUser";
import { groupByDate } from "@/utils/stats";
import moment from "moment";
import useExamStore from "@/stores/examStore";
import { ToastContainer } from "react-toastify";
import Layout from "@/components/High/Layout";
import clsx from "clsx";
@@ -75,12 +74,6 @@ export default function History({ user, users, assignments, entities }: Props) {
const { data: stats, isLoading: isStatsLoading } = useFilterRecordsByUser<Stat[]>(statsUserId || user?.id);
const { gradingSystem } = useGradingSystem();
const setExams = useExamStore((state) => state.setExams);
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
const setUserSolutions = useExamStore((state) => state.setUserSolutions);
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const setInactivity = useExamStore((state) => state.setInactivity);
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
const renderPdfIcon = usePDFDownload("stats");
const [selectedTrainingExams, setSelectedTrainingExams] = useState<string[]>([]);
@@ -174,13 +167,7 @@ export default function History({ user, users, assignments, entities }: Props) {
selectedTrainingExams={selectedTrainingExams}
setSelectedTrainingExams={setSelectedTrainingExams}
maxTrainingExams={MAX_TRAINING_EXAMS}
setExams={setExams}
gradingSystem={gradingSystem?.steps}
setShowSolutions={setShowSolutions}
setUserSolutions={setUserSolutions}
setSelectedModules={setSelectedModules}
setInactivity={setInactivity}
setTimeSpent={setTimeSpent}
renderPdfIcon={renderPdfIcon}
/>
);

View File

@@ -11,7 +11,6 @@ import clsx from "clsx";
import Lists from "./(admin)/Lists";
import BatchCodeGenerator from "./(admin)/BatchCodeGenerator";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import ExamGenerator from "./(admin)/ExamGenerator";
import BatchCreateUser from "./(admin)/Lists/BatchCreateUser";
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
import usePermissions from "@/hooks/usePermissions";

View File

@@ -20,7 +20,7 @@ import {shouldRedirectHome} from "@/utils/navigation.disabled";
import {sessionOptions} from "@/lib/session";
import qs from "qs";
import StatsGridItem from "@/components/Medium/StatGridItem";
import useExamStore from "@/stores/examStore";
import useExamStore from "@/stores/exam";
import {usePDFDownload} from "@/hooks/usePDFDownload";
import useAssignments from "@/hooks/useAssignments";
import useUsers from "@/hooks/useUsers";
@@ -29,7 +29,6 @@ import InfiniteCarousel from "@/components/InfiniteCarousel";
import {LuExternalLink} from "react-icons/lu";
import {uniqBy} from "lodash";
import {getExamById} from "@/utils/exams";
import {convertToUserSolutions} from "@/utils/stats";
import {sortByModule} from "@/utils/moduleUtils";
import { requestUser } from "@/utils/api";
import { redirect, serialize } from "@/utils";
@@ -46,15 +45,10 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
}, sessionOptions);
const TrainingContent: React.FC<{user: User}> = ({user}) => {
// Record stuff
const setExams = useExamStore((state) => state.setExams);
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
const setUserSolutions = useExamStore((state) => state.setUserSolutions);
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const setInactivity = useExamStore((state) => state.setInactivity);
const setTimeSpent = useExamStore((state) => state.setTimeSpent);
const renderPdfIcon = usePDFDownload("stats");
const dispatch = useExamStore((s) => s.dispatch);
const [trainingContent, setTrainingContent] = useState<ITrainingContent | null>(null);
const [loading, setLoading] = useState(true);
const [trainingTips, setTrainingTips] = useState<ITrainingTip[]>([]);
@@ -125,17 +119,18 @@ const TrainingContent: React.FC<{user: User}> = ({user}) => {
Promise.all(examPromises).then((exams) => {
if (exams.every((x) => !!x)) {
if (!!timeSpent) setTimeSpent(timeSpent);
if (!!inactivity) setInactivity(inactivity);
setUserSolutions(convertToUserSolutions(stats));
setShowSolutions(true);
setExams(exams.map((x) => x!).sort(sortByModule));
setSelectedModules(
exams
.map((x) => x!)
.sort(sortByModule)
.map((x) => x!.module),
);
dispatch({
type: 'INIT_SOLUTIONS', payload: {
exams: exams.map((x) => x!).sort(sortByModule),
modules: exams
.map((x) => x!)
.sort(sortByModule)
.map((x) => x!.module),
stats,
timeSpent,
inactivity
}
});
router.push("/exam");
}
});
@@ -185,12 +180,6 @@ const TrainingContent: React.FC<{user: User}> = ({user}) => {
user={user}
assignments={assignments}
users={users}
setExams={setExams}
setShowSolutions={setShowSolutions}
setUserSolutions={setUserSolutions}
setSelectedModules={setSelectedModules}
setInactivity={setInactivity}
setTimeSpent={setTimeSpent}
renderPdfIcon={renderPdfIcon}
/>
))}