From 2a58e0d33f9a284ae237d46989f9c962ee5e471c Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Tue, 6 Aug 2024 18:40:49 +0100 Subject: [PATCH] Added more columns to exam list --- src/interfaces/exam.ts | 41 ++-- src/pages/(admin)/Lists/ExamList.tsx | 325 ++++++++++++++++----------- src/pages/api/exam/[module]/index.ts | 2 +- 3 files changed, 211 insertions(+), 157 deletions(-) diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index a7756dc5..58631a50 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -5,15 +5,20 @@ export type Variant = "full" | "partial"; export type InstructorGender = "male" | "female" | "varied"; export type Difficulty = "easy" | "medium" | "hard"; -export interface ReadingExam { - parts: ReadingPart[]; +interface ExamBase { id: string; - module: "reading"; + module: Module; minTimer: number; - type: "academic" | "general"; isDiagnostic: boolean; variant?: Variant; difficulty?: Difficulty; + createdBy?: string; // option as it has been added later + createdAt?: string; // option as it has been added later +} +export interface ReadingExam extends ExamBase { + module: "reading"; + parts: ReadingPart[]; + type: "academic" | "general"; } export interface ReadingPart { @@ -24,14 +29,9 @@ export interface ReadingPart { exercises: Exercise[]; } -export interface LevelExam { +export interface LevelExam extends ExamBase { module: "level"; - id: string; parts: LevelPart[]; - minTimer: number; - isDiagnostic: boolean; - variant?: Variant; - difficulty?: Difficulty; } export interface LevelPart { @@ -39,14 +39,9 @@ export interface LevelPart { exercises: Exercise[]; } -export interface ListeningExam { +export interface ListeningExam extends ExamBase { parts: ListeningPart[]; - id: string; module: "listening"; - minTimer: number; - isDiagnostic: boolean; - variant?: Variant; - difficulty?: Difficulty; } export interface ListeningPart { @@ -72,14 +67,9 @@ export interface UserSolution { isDisabled?: boolean; } -export interface WritingExam { +export interface WritingExam extends ExamBase { module: "writing"; - id: string; exercises: WritingExercise[]; - minTimer: number; - isDiagnostic: boolean; - variant?: Variant; - difficulty?: Difficulty; } interface WordCounter { @@ -87,15 +77,10 @@ interface WordCounter { limit: number; } -export interface SpeakingExam { - id: string; +export interface SpeakingExam extends ExamBase { module: "speaking"; exercises: (SpeakingExercise | InteractiveSpeakingExercise)[]; - minTimer: number; - isDiagnostic: boolean; - variant?: Variant; instructorGender: InstructorGender; - difficulty?: Difficulty; } export type Exercise = diff --git a/src/pages/(admin)/Lists/ExamList.tsx b/src/pages/(admin)/Lists/ExamList.tsx index efd212ca..71f685c1 100644 --- a/src/pages/(admin)/Lists/ExamList.tsx +++ b/src/pages/(admin)/Lists/ExamList.tsx @@ -1,154 +1,223 @@ -import {PERMISSIONS} from "@/constants/userPermissions"; +import { useMemo } from "react"; +import { PERMISSIONS } from "@/constants/userPermissions"; import useExams from "@/hooks/useExams"; import useUsers from "@/hooks/useUsers"; -import {Module} from "@/interfaces"; -import {Exam} from "@/interfaces/exam"; -import {Type, User} from "@/interfaces/user"; +import { Module } from "@/interfaces"; +import { Exam } from "@/interfaces/exam"; +import { Type, User } from "@/interfaces/user"; import useExamStore from "@/stores/examStore"; -import {getExamById} from "@/utils/exams"; -import {countExercises} from "@/utils/moduleUtils"; -import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; +import { getExamById } from "@/utils/exams"; +import { countExercises } from "@/utils/moduleUtils"; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; import axios from "axios"; import clsx from "clsx"; -import {capitalize} from "lodash"; -import {useRouter} from "next/router"; -import {BsCheck, BsTrash, BsUpload} from "react-icons/bs"; -import {toast} from "react-toastify"; +import { capitalize } from "lodash"; +import { useRouter } from "next/router"; +import { BsCheck, BsTrash, 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", - level: "text-ielts-level", +const CLASSES: { [key in Module]: string } = { + reading: "text-ielts-reading", + listening: "text-ielts-listening", + speaking: "text-ielts-speaking", + writing: "text-ielts-writing", + level: "text-ielts-level", }; const columnHelper = createColumnHelper(); -export default function ExamList({user}: {user: User}) { - const {exams, reload} = useExams(); +export default function ExamList({ user }: { user: User }) { + const { exams, reload } = useExams(); + const { users } = useUsers(); - const setExams = useExamStore((state) => state.setExams); - const setSelectedModules = useExamStore((state) => state.setSelectedModules); + const parsedExams = useMemo(() => { + return exams.map((exam) => { + if (exam.createdBy) { + const user = users.find((u) => u.id === exam.createdBy); + if (!user) return exam; - const router = useRouter(); + return { + ...exam, + createdBy: user.type === "developer" ? "system" : user.name, + }; + } - 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 exam; + }); + }, [exams, users]); - return; - } + const setExams = useExamStore((state) => state.setExams); + const setSelectedModules = useExamStore((state) => state.setSelectedModules); - setExams([exam]); - setSelectedModules([module]); + const router = useRouter(); - router.push("/exercises"); - }; + 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", + } + ); - const deleteExam = async (exam: Exam) => { - if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return; + return; + } - axios - .delete(`/api/exam/${exam.module}/${exam.id}`) - .then(() => toast.success(`Deleted the "${exam.id}" exam`)) - .catch((reason) => { - if (reason.response.status === 404) { - toast.error("Exam not found!"); - return; - } + setExams([exam]); + setSelectedModules([module]); - if (reason.response.status === 403) { - toast.error("You do not have permission to delete this exam!"); - return; - } + router.push("/exercises"); + }; - toast.error("Something went wrong, please try again later."); - }) - .finally(reload); - }; + const deleteExam = async (exam: Exam) => { + if ( + !confirm( + `Are you sure you want to delete this ${capitalize(exam.module)} exam?` + ) + ) + return; - const getTotalExercises = (exam: Exam) => { - if (exam.module === "reading" || exam.module === "listening" || exam.module === "level") { - return countExercises(exam.parts.flatMap((x) => x.exercises)); - } + axios + .delete(`/api/exam/${exam.module}/${exam.id}`) + .then(() => toast.success(`Deleted the "${exam.id}" exam`)) + .catch((reason) => { + if (reason.response.status === 404) { + toast.error("Exam not found!"); + return; + } - return countExercises(exam.exercises); - }; + if (reason.response.status === 403) { + toast.error("You do not have permission to delete this exam!"); + return; + } - const defaultColumns = [ - columnHelper.accessor("id", { - header: "ID", - cell: (info) => info.getValue(), - }), - columnHelper.accessor("module", { - header: "Module", - cell: (info) => {capitalize(info.getValue())}, - }), - columnHelper.accessor((x) => getTotalExercises(x), { - 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}}) => { - return ( -
-
await loadExam(row.original.module, row.original.id)}> - -
- {PERMISSIONS.examManagement.delete.includes(user.type) && ( -
deleteExam(row.original)}> - -
- )} -
- ); - }, - }, - ]; + toast.error("Something went wrong, please try again later."); + }) + .finally(reload); + }; - const table = useReactTable({ - data: exams, - columns: defaultColumns, - getCoreRowModel: getCoreRowModel(), - }); + const getTotalExercises = (exam: Exam) => { + if ( + exam.module === "reading" || + exam.module === "listening" || + exam.module === "level" + ) { + return countExercises(exam.parts.flatMap((x) => x.exercises)); + } - 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())} -
- ); + return countExercises(exam.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) => getTotalExercises(x), { + header: "Exercises", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("minTimer", { + header: "Timer", + cell: (info) => <>{info.getValue()} minute(s), + }), + columnHelper.accessor("createdAt", { + header: "Created At", + cell: (info) => { + const value = info.getValue(); + if (value) { + return new Date(value).toLocaleDateString(); + } + + return null; + }, + }), + columnHelper.accessor("createdBy", { + header: "Created By", + cell: (info) => info.getValue(), + }), + { + header: "", + id: "actions", + cell: ({ row }: { row: { original: Exam } }) => { + return ( +
+
+ await loadExam(row.original.module, row.original.id) + } + > + +
+ {PERMISSIONS.examManagement.delete.includes(user.type) && ( +
deleteExam(row.original)} + > + +
+ )} +
+ ); + }, + }, + ]; + + const table = useReactTable({ + data: parsedExams, + 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/api/exam/[module]/index.ts b/src/pages/api/exam/[module]/index.ts index e7ff0699..1c845cc7 100644 --- a/src/pages/api/exam/[module]/index.ts +++ b/src/pages/api/exam/[module]/index.ts @@ -47,7 +47,7 @@ async function POST(req: NextApiRequest, res: NextApiResponse) { } const {module} = req.query as {module: string}; - const exam = {...req.body, module: module}; + const exam = {...req.body, module: module, createdBy: req.session.user.id, createdAt: new Date().toISOString()}; await setDoc(doc(db, module, req.body.id), exam); res.status(200).json(exam);