Added the ability to view all exams
This commit is contained in:
114
src/pages/(admin)/Lists/ExamList.tsx
Normal file
114
src/pages/(admin)/Lists/ExamList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
85
src/pages/(admin)/Lists/UserList.tsx
Normal file
85
src/pages/(admin)/Lists/UserList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
43
src/pages/(admin)/Lists/index.tsx
Normal file
43
src/pages/(admin)/Lists/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
36
src/pages/api/exam/index.ts
Normal file
36
src/pages/api/exam/index.ts
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user