Added the ability to view all exams

This commit is contained in:
Tiago Ribeiro
2023-09-23 13:34:14 +01:00
parent 7a957e4d78
commit 6dda49a917
9 changed files with 324 additions and 2 deletions

View File

@@ -0,0 +1,114 @@
import useExams from "@/hooks/useExams";
import useUsers from "@/hooks/useUsers";
import {Module} from "@/interfaces";
import {Exam} from "@/interfaces/exam";
import {Type} from "@/interfaces/user";
import useExamStore from "@/stores/examStore";
import {getExamById} from "@/utils/exams";
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
import clsx from "clsx";
import {capitalize} from "lodash";
import {useRouter} from "next/router";
import {BsCheck, BsUpload} from "react-icons/bs";
import {toast} from "react-toastify";
const CLASSES: {[key in Module]: string} = {
reading: "text-ielts-reading",
listening: "text-ielts-listening",
speaking: "text-ielts-speaking",
writing: "text-ielts-writing",
};
const columnHelper = createColumnHelper<Exam>();
export default function ExamList() {
const {exams} = useExams();
const setExams = useExamStore((state) => state.setExams);
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
const router = useRouter();
const loadExam = async (module: Module, examId: string) => {
const exam = await getExamById(module, examId.trim());
if (!exam) {
toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", {
toastId: "invalid-exam-id",
});
return;
}
setExams([exam]);
setSelectedModules([module]);
router.push("/exercises");
};
const defaultColumns = [
columnHelper.accessor("id", {
header: "ID",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("module", {
header: "Module",
cell: (info) => <span className={CLASSES[info.getValue()]}>{capitalize(info.getValue())}</span>,
}),
columnHelper.accessor((x) => x.exercises.length, {
header: "Exercises",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("minTimer", {
header: "Timer",
cell: (info) => <>{info.getValue()} minute(s)</>,
}),
{
header: "",
id: "actions",
cell: ({row}: {row: {original: Exam}}) => {
console.log(row.original);
return (
<div
data-tip="Load exam"
className="cursor-pointer tooltip"
onClick={async () => await loadExam(row.original.module, row.original.id)}>
<BsUpload className="hover:text-mti-purple-light transition ease-in-out duration-300" />
</div>
);
},
},
];
const table = useReactTable({
data: exams,
columns: defaultColumns,
getCoreRowModel: getCoreRowModel(),
});
return (
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th className="py-4" key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody className="px-2">
{table.getRowModel().rows.map((row) => (
<tr className="bg-white rounded-lg shadow py-2" key={row.id}>
{row.getVisibleCells().map((cell) => (
<td className="px-4 py-2" key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}

View File

@@ -0,0 +1,85 @@
import useUsers from "@/hooks/useUsers";
import {Type} from "@/interfaces/user";
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table";
import clsx from "clsx";
import {capitalize} from "lodash";
import {useState} from "react";
import {BsCheck} from "react-icons/bs";
type TableUser = {
id: string;
name: string;
email: string;
type: Type;
isVerified: boolean;
};
const columnHelper = createColumnHelper<TableUser>();
export default function UserList() {
const {users} = useUsers();
const defaultColumns = [
columnHelper.accessor("name", {
header: "Name",
cell: (info) => info.getValue(),
enableSorting: true,
}),
columnHelper.accessor("email", {
header: "E-mail",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("type", {
header: "Type",
cell: (info) => capitalize(info.getValue()),
}),
columnHelper.accessor("isVerified", {
header: "Verification Status",
cell: (info) => (
<div className="flex gap-3 items-center text-mti-gray-dim text-sm self-center">
<div
className={clsx(
"w-6 h-6 rounded-md flex items-center justify-center border border-mti-purple-light bg-white",
"transition duration-300 ease-in-out",
info.getValue() && "!bg-mti-purple-light ",
)}>
<BsCheck color="white" className="w-full h-full" />
</div>
</div>
),
}),
];
const table = useReactTable({
data: users,
columns: defaultColumns,
getCoreRowModel: getCoreRowModel(),
});
return (
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th className="py-4" key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody className="px-2">
{table.getRowModel().rows.map((row) => (
<tr className="bg-white rounded-lg shadow py-2" key={row.id}>
{row.getVisibleCells().map((cell) => (
<td className="px-4 py-2" key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}

View File

@@ -0,0 +1,43 @@
import {Tab} from "@headlessui/react";
import clsx from "clsx";
import ExamList from "./ExamList";
import UserList from "./UserList";
export default function Lists() {
return (
<Tab.Group>
<Tab.List className="flex space-x-1 rounded-xl bg-mti-purple-ultralight/40 p-1">
<Tab
className={({selected}) =>
clsx(
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
"transition duration-300 ease-in-out",
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
)
}>
User List
</Tab>
<Tab
className={({selected}) =>
clsx(
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light",
"ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2",
"transition duration-300 ease-in-out",
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark",
)
}>
Exam List
</Tab>
</Tab.List>
<Tab.Panels className="mt-2">
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide shadow">
<UserList />
</Tab.Panel>
<Tab.Panel className="overflow-y-scroll max-h-[600px] rounded-xl scrollbar-hide shadow">
<ExamList />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
);
}

View File

@@ -7,6 +7,9 @@ import {ToastContainer} from "react-toastify";
import Layout from "@/components/High/Layout";
import CodeGenerator from "./(admin)/CodeGenerator";
import ExamLoader from "./(admin)/ExamLoader";
import {Tab} from "@headlessui/react";
import clsx from "clsx";
import Lists from "./(admin)/Lists";
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
@@ -54,11 +57,14 @@ export default function Admin() {
</Head>
<ToastContainer />
{user && (
<Layout user={user}>
<Layout user={user} className="gap-6">
<section className="w-full flex gap-8">
<ExamLoader />
<CodeGenerator />
</section>
<section className="w-full">
<Lists />
</section>
</Layout>
)}
</>

View File

@@ -0,0 +1,36 @@
// 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} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {flatten} from "lodash";
import {Exam} from "@/interfaces/exam";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const moduleExamsPromises = MODULE_ARRAY.map(async (module) => {
const moduleRef = collection(db, module);
const q = query(moduleRef, where("isDiagnostic", "==", false));
const snapshot = await getDocs(q);
return snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
module,
})) as Exam[];
});
const moduleExams = await Promise.all(moduleExamsPromises);
res.status(200).json(flatten(moduleExams));
}