Created a system to go directly to an assignment from a URL

This commit is contained in:
Tiago Ribeiro
2024-10-17 18:24:39 +01:00
parent a0a9402945
commit 4917583c67
10 changed files with 232 additions and 57 deletions

View File

@@ -11,16 +11,18 @@ interface Props {
className?: string;
navDisabled?: boolean;
focusMode?: boolean;
hideSidebar?: boolean
bgColor?: string;
onFocusLayerMouseEnter?: () => void;
}
export default function Layout({user, children, className, bgColor="bg-white", navDisabled = false, focusMode = false, onFocusLayerMouseEnter}: Props) {
export default function Layout({user, children, className, bgColor="bg-white", hideSidebar, navDisabled = false, focusMode = false, onFocusLayerMouseEnter}: Props) {
const router = useRouter();
return (
<main className={clsx("w-full min-h-full h-screen flex flex-col bg-mti-gray-smoke relative")}>
<ToastContainer />
{!hideSidebar && (
<Navbar
path={router.pathname}
user={user}
@@ -28,7 +30,9 @@ export default function Layout({user, children, className, bgColor="bg-white", n
focusMode={focusMode}
onFocusLayerMouseEnter={onFocusLayerMouseEnter}
/>
<div className="h-full w-full flex gap-2">
)}
<div className={clsx("h-full w-full flex gap-2")}>
{!hideSidebar && (
<Sidebar
path={router.pathname}
navDisabled={navDisabled}
@@ -37,10 +41,12 @@ export default function Layout({user, children, className, bgColor="bg-white", n
className="-md:hidden"
user={user}
/>
)}
<div
className={clsx(
`w-full min-h-full md:mr-8 ${bgColor} shadow-md rounded-2xl p-4 xl:p-10 pb-8 flex flex-col gap-8 relative overflow-hidden mt-2`,
`w-full min-h-full ${bgColor} shadow-md rounded-2xl p-4 xl:p-10 pb-8 flex flex-col gap-8 relative overflow-hidden mt-2`,
bgColor !== "bg-white" ? "justify-center" : "h-fit",
hideSidebar ? "md:mx-8" : "md:mr-8",
className,
)}>
{children}

View File

@@ -25,13 +25,16 @@ import useSessions from "@/hooks/useSessions";
import ShortUniqueId from "short-unique-id";
import clsx from "clsx";
import useGradingSystem from "@/hooks/useGrading";
import { Assignment } from "@/interfaces/results";
import { mapBy } from "@/utils";
interface Props {
page: "exams" | "exercises";
user: User;
hideSidebar?: boolean
}
export default function ExamPage({page, user}: Props) {
export default function ExamPage({page, user, hideSidebar = false}: Props) {
const [variant, setVariant] = useState<Variant>("full");
const [avoidRepeated, setAvoidRepeated] = useState(false);
const [hasBeenUploaded, setHasBeenUploaded] = useState(false);
@@ -210,7 +213,7 @@ export default function ExamPage({page, user}: Props) {
}, [setModuleIndex, showSolutions]);
useEffect(() => {
(async () => {
console.log(selectedModules)
if (selectedModules.length > 0 && exams.length > 0 && moduleIndex < selectedModules.length) {
const nextExam = exams[moduleIndex];
@@ -218,7 +221,6 @@ export default function ExamPage({page, user}: Props) {
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]);
@@ -520,6 +522,7 @@ export default function ExamPage({page, user}: Props) {
<Layout
user={user}
bgColor={bgColor}
hideSidebar={hideSidebar}
className="justify-between"
focusMode={selectedModules.length !== 0 && !showSolutions && moduleIndex < selectedModules.length}
onFocusLayerMouseEnter={() => setShowAbandonPopup(true)}>

View File

@@ -52,7 +52,8 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res, params})
const entity = await getEntityWithRoles(assignment.entity || "")
if (!entity) return redirect("/assignments")
if (!doesEntityAllow(user, entity, 'view_assignments')) return redirect("/assignments")
if (!doesEntityAllow(user, entity, 'view_assignments') && !["admin", "developer"].includes(user.type))
return redirect("/assignments")
const users = await (checkAccess(user, ["developer", "admin"]) ? getUsers() : getEntityUsers(entity.id));

View File

@@ -119,15 +119,6 @@ export default function AssignmentsPage({assignments, corporateAssignments, enti
<b>Total:</b> {activeAssignments.reduce((acc, curr) => acc + curr.results.length, 0)}/
{activeAssignments.reduce((acc, curr) => curr.exams.length + acc, 0)}
</span>
{Object.keys(groupBy(corporateAssignments, (x) => x.corporate?.id)).map((x) => (
<div key={x}>
<span className="font-semibold">{getUserCompanyName(users.find((u) => u.id === x)!, users, groups)}: </span>
<span>
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.results.length + acc, 0)}/
{groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.exams.length + acc, 0)}
</span>
</div>
))}
</div>
</div>

View File

@@ -9,11 +9,11 @@ import {Entity, EntityWithRoles} from "@/interfaces/entity";
import {User} from "@/interfaces/user";
import {sessionOptions} from "@/lib/session";
import {USER_TYPE_LABELS} from "@/resources/user";
import {mapBy, redirect, serialize} from "@/utils";
import {filterBy, mapBy, redirect, serialize} from "@/utils";
import {getEntities, getEntitiesWithRoles} from "@/utils/entities.be";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import {getUserName} from "@/utils/users";
import {getLinkedUsers} from "@/utils/users.be";
import {getEntitiesUsers, getLinkedUsers} from "@/utils/users.be";
import axios from "axios";
import clsx from "clsx";
import {withIronSessionSsr} from "iron-session/next";
@@ -22,7 +22,7 @@ import Head from "next/head";
import Link from "next/link";
import {useRouter} from "next/router";
import {Divider} from "primereact/divider";
import {useState} from "react";
import {useMemo, useState} from "react";
import {BsCheck, BsChevronLeft, BsClockFill, BsEnvelopeFill, BsStopwatchFill} from "react-icons/bs";
import {toast, ToastContainer} from "react-toastify";
import { requestUser } from "@/utils/api";
@@ -34,12 +34,12 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
if (shouldRedirectHome(user)) return redirect("/")
const linkedUsers = await getLinkedUsers(user.id, user.type);
const users = await getEntitiesUsers(mapBy(user.entities, 'id'))
const entities = await getEntitiesWithRoles(mapBy(user.entities, "id"));
const allowedEntities = findAllowedEntities(user, entities, "create_classroom")
return {
props: serialize({user, entities: allowedEntities, users: linkedUsers.users.filter((x) => x.id !== user.id)}),
props: serialize({user, entities: allowedEntities, users: users.filter((x) => x.id !== user.id)}),
};
}, sessionOptions);
@@ -55,7 +55,9 @@ export default function Home({user, users, entities}: Props) {
const [name, setName] = useState("");
const [entity, setEntity] = useState<string | undefined>(entities[0]?.id);
const {rows, renderSearch} = useListSearch<User>([["name"], ["corporateInformation", "companyInformation", "name"]], users);
const entityUsers = useMemo(() => !entity ? users : users.filter(u => mapBy(u.entities, 'id').includes(entity)), [entity, users])
const {rows, renderSearch} = useListSearch<User>([["name"], ["corporateInformation", "companyInformation", "name"]], entityUsers);
const {items, renderMinimal} = usePagination<User>(rows, 16);
const router = useRouter();

View File

@@ -6,15 +6,52 @@ import {shouldRedirectHome} from "@/utils/navigation.disabled";
import ExamPage from "./(exam)/ExamPage";
import Head from "next/head";
import {User} from "@/interfaces/user";
import { redirect, serialize } from "@/utils";
import { filterBy, findBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import { getAssignment, getAssignments, getAssignmentsByAssignee } from "@/utils/assignments.be";
import { Assignment } from "@/interfaces/results";
import useExamStore from "@/stores/examStore";
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 { Session } from "@/hooks/useSessions";
import moment from "moment";
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => {
const user = await requestUser(req, res)
if (!user) return redirect("/login")
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}
if (assignmentID) {
const assignment = await getAssignment(assignmentID)
if (!assignment) return redirect("/exam")
if (!assignment.assignees.includes(user.id) && !["admin", "developer"].includes(user.type))
return redirect("/exam")
const exams = await getExamsByIds(uniqBy(assignment.exams, "id"))
const session = await getSessionByAssignment(assignmentID)
if (
filterBy(assignment.results, 'user', user.id).length > 0 ||
moment(assignment.startDate).isAfter(moment()) ||
moment(assignment.endDate).isBefore(moment())
)
return redirect("/exam")
return {
props: serialize({user, assignment, exams, session})
}
}
return {
props: serialize({user}),
};
@@ -22,9 +59,55 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
interface Props {
user: User;
assignment?: Assignment
exams?: Exam[]
session?: Session
}
export default function Page({user}: Props) {
export default function Page({user, assignment, exams = [], session}: Props) {
const router = useRouter()
const state = useExamStore((state) => state)
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),
);
router.replace(router.asPath)
}
// 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);
router.replace(router.asPath)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assignment, exams, session])
return (
<>
<Head>
@@ -36,7 +119,7 @@ export default function Page({user}: Props) {
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ExamPage page="exams" user={user} />
<ExamPage page="exams" user={user} hideSidebar={!!assignment} />
</>
);
}

View File

@@ -6,15 +6,51 @@ import {shouldRedirectHome} from "@/utils/navigation.disabled";
import ExamPage from "./(exam)/ExamPage";
import Head from "next/head";
import {User} from "@/interfaces/user";
import { redirect, serialize } from "@/utils";
import { filterBy, findBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import { getAssignment, getAssignments, getAssignmentsByAssignee } from "@/utils/assignments.be";
import { Assignment } from "@/interfaces/results";
import useExamStore from "@/stores/examStore";
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 { Session } from "@/hooks/useSessions";
import moment from "moment";
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => {
const user = await requestUser(req, res)
if (!user) return redirect("/login")
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}
if (assignmentID) {
const assignment = await getAssignment(assignmentID)
if (!assignment) return redirect("/exercises")
if (!["admin", "developer"].includes(user.type) && !assignment.assignees.includes(user.id)) return redirect("/exercises")
const exams = await getExamsByIds(uniqBy(assignment.exams, "id"))
const session = await getSessionByAssignment(assignmentID)
if (
filterBy(assignment.results, 'user', user.id) ||
moment(assignment.startDate).isBefore(moment()) ||
moment(assignment.endDate).isAfter(moment())
)
return redirect("/exercises")
return {
props: serialize({user, assignment, exams, session})
}
}
return {
props: serialize({user}),
};
@@ -22,13 +58,59 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
interface Props {
user: User;
assignment?: Assignment
exams?: Exam[]
session?: Session
}
export default function Page({user}: Props) {
export default function Page({user, assignment, exams = [], session}: Props) {
const router = useRouter()
const state = useExamStore((state) => state)
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),
);
router.replace(router.asPath)
}
// 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);
router.replace(router.asPath)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assignment, exams, session])
return (
<>
<Head>
<title>Exercises | EnCoach</title>
<title>Exams | EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
@@ -36,7 +118,7 @@ export default function Page({user}: Props) {
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ExamPage page="exercises" user={user} />
<ExamPage page="exams" user={user} hideSidebar={!!assignment} />
</>
);
}

View File

@@ -20,16 +20,17 @@ import { redirect } from "@/utils";
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/g);
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => {
const destination = !query.destination ? "/" : Buffer.from(query.destination as string, 'base64').toString()
const user = await requestUser(req, res)
if (user) return redirect("/")
if (user) return redirect(destination)
return {
props: {user: null},
props: {user: null, destination},
};
}, sessionOptions);
export default function Login() {
export default function Login({ destination }: { destination: string }) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [rememberPassword, setRememberPassword] = useState(false);
@@ -38,13 +39,13 @@ export default function Login() {
const router = useRouter();
const {user, mutateUser} = useUser({
redirectTo: "/",
redirectTo: destination,
redirectIfFound: true,
});
useEffect(() => {
if (user) router.push("/");
}, [router, user]);
if (user) router.push(destination);
}, [router, user, destination]);
const forgotPassword = () => {
if (!email || email.length < 0 || !EMAIL_REGEX.test(email)) {

View File

@@ -8,6 +8,7 @@ import {getUserCorporate} from "./groups.be";
import {Db, ObjectId} from "mongodb";
import client from "@/lib/mongodb";
import {MODULE_ARRAY} from "./moduleUtils";
import { mapBy } from ".";
const db = client.db(process.env.MONGODB_DB);
@@ -37,7 +38,7 @@ export const getExamsByIds = async (ids: {module: Module; id: string}[]) => {
async (m) =>
await db
.collection(m)
.find<Exam>({id: {$in: groupedByModule[m]}})
.find<Exam>({id: {$in: mapBy(groupedByModule[m], 'id')}})
.toArray(),
),
)

View File

@@ -9,3 +9,8 @@ export const getSessionsByUser = async (id: string, limit?: number) =>
.find<Session>({user: id})
.limit(limit || 0)
.toArray();
export const getSessionByAssignment = async (assignmentID: string) =>
await db
.collection("sessions")
.findOne<Session>({"assignment.id": assignmentID})