Compare commits

...

4 Commits

Author SHA1 Message Date
José Lima
25aef3afdf Added new pages and nav with menu 2025-03-07 04:38:57 +00:00
Francisco Lima
df84aaadf4 Merged in limit5SessionsUser (pull request #161)
Implemented limit 5 sessions per User

Approved-by: Tiago Ribeiro
2025-03-05 08:17:05 +00:00
José Lima
2789660e8a Implemented limit 5 sessions per User 2025-03-05 04:42:54 +00:00
Francisco Lima
6c7d189957 Merged in fixStudentPerformanceFreeze (pull request #159)
FixStudentPerformanceFreeze

Approved-by: Tiago Ribeiro
2025-03-04 23:24:17 +00:00
5 changed files with 190 additions and 27 deletions

View File

@@ -9,7 +9,7 @@ import {
useReactTable, useReactTable,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import clsx from "clsx"; import clsx from "clsx";
import { useEffect, useState } from "react"; import { useState } from "react";
import { BsArrowDown, BsArrowUp } from "react-icons/bs"; import { BsArrowDown, BsArrowUp } from "react-icons/bs";
import Button from "../Low/Button"; import Button from "../Low/Button";

View File

@@ -12,6 +12,10 @@ import {
BsCurrencyDollar, BsCurrencyDollar,
BsClipboardData, BsClipboardData,
BsPeople, BsPeople,
BsChevronDown,
BsChevronUp,
BsChatText,
BsCardText,
} from "react-icons/bs"; } from "react-icons/bs";
import { GoWorkflow } from "react-icons/go"; import { GoWorkflow } from "react-icons/go";
import { CiDumbbell } from "react-icons/ci"; import { CiDumbbell } from "react-icons/ci";
@@ -31,7 +35,7 @@ import {
useAllowedEntities, useAllowedEntities,
useAllowedEntitiesSomePermissions, useAllowedEntitiesSomePermissions,
} from "@/hooks/useEntityPermissions"; } from "@/hooks/useEntityPermissions";
import { useMemo } from "react"; import { useMemo, useState } from "react";
import { PermissionType } from "../interfaces/permissions"; import { PermissionType } from "../interfaces/permissions";
interface Props { interface Props {
@@ -52,6 +56,7 @@ interface NavProps {
disabled?: boolean; disabled?: boolean;
isMinimized?: boolean; isMinimized?: boolean;
badge?: number; badge?: number;
children?: React.ReactNode;
} }
const Nav = ({ const Nav = ({
@@ -62,8 +67,16 @@ const Nav = ({
disabled = false, disabled = false,
isMinimized = false, isMinimized = false,
badge, badge,
children,
}: NavProps) => { }: NavProps) => {
const [open, setOpen] = useState(false);
return ( return (
<div
className={clsx(
"flex flex-col gap-2 transition-all duration-300 ease-in-out",
open && !isMinimized && "bg-white rounded-xl"
)}
>
<Link <Link
href={!disabled ? keyPath : ""} href={!disabled ? keyPath : ""}
className={clsx( className={clsx(
@@ -89,7 +102,36 @@ const Nav = ({
{badge} {badge}
</div> </div>
)} )}
{children && (
<button
className="flex items-center gap-4 rounded-full p-4 absolute right-0"
onClick={(e) => {
setOpen((prev) => !prev);
e.preventDefault();
}}
>
{open ? (
<BsChevronUp
size={24}
className={clsx(
isMinimized && "hidden",
"transition ease-in-out duration-300"
)}
/>
) : (
<BsChevronDown
size={24}
className={clsx(
isMinimized && "hidden",
"transition ease-in-out duration-300"
)}
/>
)}
</button>
)}
</Link> </Link>
{open || isMinimized ? children : null}
</div>
); );
}; };
@@ -325,7 +367,24 @@ export default function Sidebar({
path={path} path={path}
keyPath="/training" keyPath="/training"
isMinimized={isMinimized} isMinimized={isMinimized}
>
<Nav
disabled={disableNavigation}
Icon={BsChatText}
label="Vocabulary"
path={path}
keyPath="/training/vocabulary"
isMinimized={isMinimized}
/> />
<Nav
disabled={disableNavigation}
Icon={BsCardText}
label="Grammar"
path={path}
keyPath="/training/grammar"
isMinimized={isMinimized}
/>
</Nav>
)} )}
{sidebarPermissions["viewPaymentRecords"] && ( {sidebarPermissions["viewPaymentRecords"] && (
<Nav <Nav
@@ -424,7 +483,24 @@ export default function Sidebar({
path={path} path={path}
keyPath="/training" keyPath="/training"
isMinimized isMinimized
>
<Nav
disabled={disableNavigation}
Icon={BsChatText}
label="Vocabulary"
path={path}
keyPath="/training/vocabulary"
isMinimized
/> />
<Nav
disabled={disableNavigation}
Icon={BsCardText}
label="Grammar"
path={path}
keyPath="/training/grammar"
isMinimized
/>
</Nav>
)} )}
{sidebarPermissions["viewPaymentRecords"] && ( {sidebarPermissions["viewPaymentRecords"] && (
<Nav <Nav

View File

@@ -48,4 +48,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
await db.collection("sessions").updateOne({ id: session.id }, { $set: session }, { upsert: true }); await db.collection("sessions").updateOne({ id: session.id }, { $set: session }, { upsert: true });
res.status(200).json({ ok: true }); res.status(200).json({ ok: true });
const sessions = await db.collection("sessions").find<Session>({ user: session.user }, { projection: { id: 1 } }).sort({ date: 1 }).toArray();
// Delete old sessions
if (sessions.length > 5) {
await db.collection("sessions").deleteOne({ id: { $in: sessions.slice(0, sessions.length - 5).map(x => x.id) } });
}
} }

View File

@@ -0,0 +1,41 @@
/* eslint-disable @next/next/no-img-element */
import Head from "next/head";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { User } from "@/interfaces/user";
import { ToastContainer } from "react-toastify";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res);
if (!user) return redirect("/login");
if (shouldRedirectHome(user)) return redirect("/");
return {
props: serialize({ user }),
};
}, sessionOptions);
const Grammar: React.FC<{
user: User;
}> = ({ user }) => {
return (
<>
<Head>
<title>Training | EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
</>
);
};
export default Grammar;

View File

@@ -0,0 +1,41 @@
/* eslint-disable @next/next/no-img-element */
import Head from "next/head";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { User } from "@/interfaces/user";
import { ToastContainer } from "react-toastify";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res);
if (!user) return redirect("/login");
if (shouldRedirectHome(user)) return redirect("/");
return {
props: serialize({ user }),
};
}, sessionOptions);
const Vocabulary: React.FC<{
user: User;
}> = ({ user }) => {
return (
<>
<Head>
<title>Training | EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
</>
);
};
export default Vocabulary;