From 6dda49a917c8aa243ed7227bc8b2a764c0de0513 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sat, 23 Sep 2023 13:34:14 +0100 Subject: [PATCH] Added the ability to view all exams --- package.json | 2 + src/hooks/useExams.tsx | 19 +++++ src/pages/(admin)/Lists/ExamList.tsx | 114 +++++++++++++++++++++++++++ src/pages/(admin)/Lists/UserList.tsx | 85 ++++++++++++++++++++ src/pages/(admin)/Lists/index.tsx | 43 ++++++++++ src/pages/admin.tsx | 8 +- src/pages/api/exam/index.ts | 36 +++++++++ tailwind.config.js | 2 +- yarn.lock | 17 ++++ 9 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 src/hooks/useExams.tsx create mode 100644 src/pages/(admin)/Lists/ExamList.tsx create mode 100644 src/pages/(admin)/Lists/UserList.tsx create mode 100644 src/pages/(admin)/Lists/index.tsx create mode 100644 src/pages/api/exam/index.ts diff --git a/package.json b/package.json index 63bd6c08..4cf6e4a2 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@mdi/js": "^7.1.96", "@mdi/react": "^1.6.1", "@next/font": "13.1.6", + "@tanstack/react-table": "^8.10.1", "@types/node": "18.13.0", "@types/react": "18.0.27", "@types/react-dom": "18.0.10", @@ -48,6 +49,7 @@ "react-xarrows": "^2.0.2", "short-unique-id": "^5.0.2", "swr": "^2.1.3", + "tailwind-scrollbar-hide": "^1.1.7", "typescript": "4.9.5", "uuid": "^9.0.0", "wavesurfer.js": "^6.6.4", diff --git a/src/hooks/useExams.tsx b/src/hooks/useExams.tsx new file mode 100644 index 00000000..bd645c45 --- /dev/null +++ b/src/hooks/useExams.tsx @@ -0,0 +1,19 @@ +import {Exam} from "@/interfaces/exam"; +import axios from "axios"; +import {useEffect, useState} from "react"; + +export default function useExams() { + const [exams, setExams] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + useEffect(() => { + setIsLoading(true); + axios + .get("/api/exam") + .then((response) => setExams(response.data)) + .finally(() => setIsLoading(false)); + }, []); + + return {exams, isLoading, isError}; +} diff --git a/src/pages/(admin)/Lists/ExamList.tsx b/src/pages/(admin)/Lists/ExamList.tsx new file mode 100644 index 00000000..c7f220a7 --- /dev/null +++ b/src/pages/(admin)/Lists/ExamList.tsx @@ -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(); + +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) => {capitalize(info.getValue())}, + }), + 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 ( +
await loadExam(row.original.module, row.original.id)}> + +
+ ); + }, + }, + ]; + + const table = useReactTable({ + data: exams, + columns: defaultColumns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ); +} diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx new file mode 100644 index 00000000..f0e692fb --- /dev/null +++ b/src/pages/(admin)/Lists/UserList.tsx @@ -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(); + +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) => ( +
+
+ +
+
+ ), + }), + ]; + + const table = useReactTable({ + data: users, + columns: defaultColumns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ); +} diff --git a/src/pages/(admin)/Lists/index.tsx b/src/pages/(admin)/Lists/index.tsx new file mode 100644 index 00000000..677ab1a4 --- /dev/null +++ b/src/pages/(admin)/Lists/index.tsx @@ -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 ( + + + + 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 + + + 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 + + + + + + + + + + + + ); +} diff --git a/src/pages/admin.tsx b/src/pages/admin.tsx index 0b72ce4e..5dde71b3 100644 --- a/src/pages/admin.tsx +++ b/src/pages/admin.tsx @@ -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() { {user && ( - +
+
+ +
)} diff --git a/src/pages/api/exam/index.ts b/src/pages/api/exam/index.ts new file mode 100644 index 00000000..7ef031f1 --- /dev/null +++ b/src/pages/api/exam/index.ts @@ -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)); +} diff --git a/tailwind.config.js b/tailwind.config.js index 1746dd34..0660d164 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -41,5 +41,5 @@ module.exports = { }, }, }, - plugins: [require("daisyui")], + plugins: [require("daisyui"), require("tailwind-scrollbar-hide")], }; diff --git a/yarn.lock b/yarn.lock index 29d57528..8857e181 100644 --- a/yarn.lock +++ b/yarn.lock @@ -725,6 +725,18 @@ dependencies: tslib "^2.4.0" +"@tanstack/react-table@^8.10.1": + version "8.10.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.10.1.tgz#f3e7d6e3f82dd43947e8893617a3c50e9e3fa383" + integrity sha512-pD58vH5ahZv1qzAK9Xl87A5dydBnKiDGdyEsd5VK2bG2wGRbfbpBfH915KdUv+8XqabDQgUo8nU8qHBEQv1qvg== + dependencies: + "@tanstack/table-core" "8.10.1" + +"@tanstack/table-core@8.10.1": + version "8.10.1" + resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.10.1.tgz#04bd980fad9f3205840449c6ed16553c35ed48c9" + integrity sha512-dvO7wz+WjnT+7KI6ZZ+GAe9tljIFResDaV/TfOhfpeTB0ud9pILsavuM22HAXG2NsVaIG2Zax2OaVIsNt0z7Og== + "@types/accepts@*": version "1.3.5" resolved "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz" @@ -3425,6 +3437,11 @@ synckit@^0.8.4: "@pkgr/utils" "^2.3.1" tslib "^2.5.0" +tailwind-scrollbar-hide@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/tailwind-scrollbar-hide/-/tailwind-scrollbar-hide-1.1.7.tgz#90b481fb2e204030e3919427416650c54f56f847" + integrity sha512-X324n9OtpTmOMqEgDUEA/RgLrNfBF/jwJdctaPZDzB3mppxJk7TLIDmOreEDm1Bq4R9LSPu4Epf8VSdovNU+iA== + tailwindcss@^3, tailwindcss@^3.2.4: version "3.3.1" resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz"