Added more columns to exam list

This commit is contained in:
Joao Ramos
2024-08-06 18:40:49 +01:00
parent cf64a91651
commit 2a58e0d33f
3 changed files with 211 additions and 157 deletions

View File

@@ -5,15 +5,20 @@ export type Variant = "full" | "partial";
export type InstructorGender = "male" | "female" | "varied"; export type InstructorGender = "male" | "female" | "varied";
export type Difficulty = "easy" | "medium" | "hard"; export type Difficulty = "easy" | "medium" | "hard";
export interface ReadingExam { interface ExamBase {
parts: ReadingPart[];
id: string; id: string;
module: "reading"; module: Module;
minTimer: number; minTimer: number;
type: "academic" | "general";
isDiagnostic: boolean; isDiagnostic: boolean;
variant?: Variant; variant?: Variant;
difficulty?: Difficulty; 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 { export interface ReadingPart {
@@ -24,14 +29,9 @@ export interface ReadingPart {
exercises: Exercise[]; exercises: Exercise[];
} }
export interface LevelExam { export interface LevelExam extends ExamBase {
module: "level"; module: "level";
id: string;
parts: LevelPart[]; parts: LevelPart[];
minTimer: number;
isDiagnostic: boolean;
variant?: Variant;
difficulty?: Difficulty;
} }
export interface LevelPart { export interface LevelPart {
@@ -39,14 +39,9 @@ export interface LevelPart {
exercises: Exercise[]; exercises: Exercise[];
} }
export interface ListeningExam { export interface ListeningExam extends ExamBase {
parts: ListeningPart[]; parts: ListeningPart[];
id: string;
module: "listening"; module: "listening";
minTimer: number;
isDiagnostic: boolean;
variant?: Variant;
difficulty?: Difficulty;
} }
export interface ListeningPart { export interface ListeningPart {
@@ -72,14 +67,9 @@ export interface UserSolution {
isDisabled?: boolean; isDisabled?: boolean;
} }
export interface WritingExam { export interface WritingExam extends ExamBase {
module: "writing"; module: "writing";
id: string;
exercises: WritingExercise[]; exercises: WritingExercise[];
minTimer: number;
isDiagnostic: boolean;
variant?: Variant;
difficulty?: Difficulty;
} }
interface WordCounter { interface WordCounter {
@@ -87,15 +77,10 @@ interface WordCounter {
limit: number; limit: number;
} }
export interface SpeakingExam { export interface SpeakingExam extends ExamBase {
id: string;
module: "speaking"; module: "speaking";
exercises: (SpeakingExercise | InteractiveSpeakingExercise)[]; exercises: (SpeakingExercise | InteractiveSpeakingExercise)[];
minTimer: number;
isDiagnostic: boolean;
variant?: Variant;
instructorGender: InstructorGender; instructorGender: InstructorGender;
difficulty?: Difficulty;
} }
export type Exercise = export type Exercise =

View File

@@ -1,154 +1,223 @@
import {PERMISSIONS} from "@/constants/userPermissions"; import { useMemo } from "react";
import { PERMISSIONS } from "@/constants/userPermissions";
import useExams from "@/hooks/useExams"; import useExams from "@/hooks/useExams";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import {Module} from "@/interfaces"; import { Module } from "@/interfaces";
import {Exam} from "@/interfaces/exam"; import { Exam } from "@/interfaces/exam";
import {Type, User} from "@/interfaces/user"; import { Type, User } from "@/interfaces/user";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import {getExamById} from "@/utils/exams"; import { getExamById } from "@/utils/exams";
import {countExercises} from "@/utils/moduleUtils"; import { countExercises } from "@/utils/moduleUtils";
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import axios from "axios"; import axios from "axios";
import clsx from "clsx"; import clsx from "clsx";
import {capitalize} from "lodash"; import { capitalize } from "lodash";
import {useRouter} from "next/router"; import { useRouter } from "next/router";
import {BsCheck, BsTrash, BsUpload} from "react-icons/bs"; import { BsCheck, BsTrash, BsUpload } from "react-icons/bs";
import {toast} from "react-toastify"; import { toast } from "react-toastify";
const CLASSES: {[key in Module]: string} = { const CLASSES: { [key in Module]: string } = {
reading: "text-ielts-reading", reading: "text-ielts-reading",
listening: "text-ielts-listening", listening: "text-ielts-listening",
speaking: "text-ielts-speaking", speaking: "text-ielts-speaking",
writing: "text-ielts-writing", writing: "text-ielts-writing",
level: "text-ielts-level", level: "text-ielts-level",
}; };
const columnHelper = createColumnHelper<Exam>(); const columnHelper = createColumnHelper<Exam>();
export default function ExamList({user}: {user: User}) { export default function ExamList({ user }: { user: User }) {
const {exams, reload} = useExams(); const { exams, reload } = useExams();
const { users } = useUsers();
const setExams = useExamStore((state) => state.setExams); const parsedExams = useMemo(() => {
const setSelectedModules = useExamStore((state) => state.setSelectedModules); 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) => { return exam;
const exam = await getExamById(module, examId.trim()); });
if (!exam) { }, [exams, users]);
toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", {
toastId: "invalid-exam-id",
});
return; const setExams = useExamStore((state) => state.setExams);
} const setSelectedModules = useExamStore((state) => state.setSelectedModules);
setExams([exam]); const router = useRouter();
setSelectedModules([module]);
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) => { return;
if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return; }
axios setExams([exam]);
.delete(`/api/exam/${exam.module}/${exam.id}`) setSelectedModules([module]);
.then(() => toast.success(`Deleted the "${exam.id}" exam`))
.catch((reason) => {
if (reason.response.status === 404) {
toast.error("Exam not found!");
return;
}
if (reason.response.status === 403) { router.push("/exercises");
toast.error("You do not have permission to delete this exam!"); };
return;
}
toast.error("Something went wrong, please try again later."); const deleteExam = async (exam: Exam) => {
}) if (
.finally(reload); !confirm(
}; `Are you sure you want to delete this ${capitalize(exam.module)} exam?`
)
)
return;
const getTotalExercises = (exam: Exam) => { axios
if (exam.module === "reading" || exam.module === "listening" || exam.module === "level") { .delete(`/api/exam/${exam.module}/${exam.id}`)
return countExercises(exam.parts.flatMap((x) => x.exercises)); .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 = [ toast.error("Something went wrong, please try again later.");
columnHelper.accessor("id", { })
header: "ID", .finally(reload);
cell: (info) => info.getValue(), };
}),
columnHelper.accessor("module", {
header: "Module",
cell: (info) => <span className={CLASSES[info.getValue()]}>{capitalize(info.getValue())}</span>,
}),
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 (
<div className="flex gap-4">
<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>
{PERMISSIONS.examManagement.delete.includes(user.type) && (
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteExam(row.original)}>
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
</div>
)}
</div>
);
},
},
];
const table = useReactTable({ const getTotalExercises = (exam: Exam) => {
data: exams, if (
columns: defaultColumns, exam.module === "reading" ||
getCoreRowModel: getCoreRowModel(), exam.module === "listening" ||
}); exam.module === "level"
) {
return countExercises(exam.parts.flatMap((x) => x.exercises));
}
return ( return countExercises(exam.exercises);
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full"> };
<thead>
{table.getHeaderGroups().map((headerGroup) => ( const defaultColumns = [
<tr key={headerGroup.id}> columnHelper.accessor("id", {
{headerGroup.headers.map((header) => ( header: "ID",
<th className="p-4 text-left" key={header.id}> cell: (info) => info.getValue(),
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} }),
</th> columnHelper.accessor("module", {
))} header: "Module",
</tr> cell: (info) => (
))} <span className={CLASSES[info.getValue()]}>
</thead> {capitalize(info.getValue())}
<tbody className="px-2"> </span>
{table.getRowModel().rows.map((row) => ( ),
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}> }),
{row.getVisibleCells().map((cell) => ( columnHelper.accessor((x) => getTotalExercises(x), {
<td className="px-4 py-2" key={cell.id}> header: "Exercises",
{flexRender(cell.column.columnDef.cell, cell.getContext())} cell: (info) => info.getValue(),
</td> }),
))} columnHelper.accessor("minTimer", {
</tr> header: "Timer",
))} cell: (info) => <>{info.getValue()} minute(s)</>,
</tbody> }),
</table> 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 (
<div className="flex gap-4">
<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>
{PERMISSIONS.examManagement.delete.includes(user.type) && (
<div
data-tip="Delete"
className="cursor-pointer tooltip"
onClick={() => deleteExam(row.original)}
>
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
</div>
)}
</div>
);
},
},
];
const table = useReactTable({
data: parsedExams,
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="p-4 text-left" 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="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg 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

@@ -47,7 +47,7 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
} }
const {module} = req.query as {module: string}; 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); await setDoc(doc(db, module, req.body.id), exam);
res.status(200).json(exam); res.status(200).json(exam);