Added more columns to exam list
This commit is contained in:
@@ -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 =
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
import { PERMISSIONS } from "@/constants/userPermissions";
|
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";
|
||||||
@@ -7,7 +8,12 @@ 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";
|
||||||
@@ -27,6 +33,23 @@ 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 parsedExams = useMemo(() => {
|
||||||
|
return exams.map((exam) => {
|
||||||
|
if (exam.createdBy) {
|
||||||
|
const user = users.find((u) => u.id === exam.createdBy);
|
||||||
|
if (!user) return exam;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...exam,
|
||||||
|
createdBy: user.type === "developer" ? "system" : user.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return exam;
|
||||||
|
});
|
||||||
|
}, [exams, users]);
|
||||||
|
|
||||||
const setExams = useExamStore((state) => state.setExams);
|
const setExams = useExamStore((state) => state.setExams);
|
||||||
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
|
||||||
@@ -36,9 +59,12 @@ export default function ExamList({user}: {user: User}) {
|
|||||||
const loadExam = async (module: Module, examId: string) => {
|
const loadExam = async (module: Module, examId: string) => {
|
||||||
const exam = await getExamById(module, examId.trim());
|
const exam = await getExamById(module, examId.trim());
|
||||||
if (!exam) {
|
if (!exam) {
|
||||||
toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", {
|
toast.error(
|
||||||
|
"Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID",
|
||||||
|
{
|
||||||
toastId: "invalid-exam-id",
|
toastId: "invalid-exam-id",
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -50,7 +76,12 @@ export default function ExamList({user}: {user: User}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteExam = async (exam: Exam) => {
|
const deleteExam = async (exam: Exam) => {
|
||||||
if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return;
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Are you sure you want to delete this ${capitalize(exam.module)} exam?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.delete(`/api/exam/${exam.module}/${exam.id}`)
|
.delete(`/api/exam/${exam.module}/${exam.id}`)
|
||||||
@@ -72,7 +103,11 @@ export default function ExamList({user}: {user: User}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getTotalExercises = (exam: Exam) => {
|
const getTotalExercises = (exam: Exam) => {
|
||||||
if (exam.module === "reading" || exam.module === "listening" || exam.module === "level") {
|
if (
|
||||||
|
exam.module === "reading" ||
|
||||||
|
exam.module === "listening" ||
|
||||||
|
exam.module === "level"
|
||||||
|
) {
|
||||||
return countExercises(exam.parts.flatMap((x) => x.exercises));
|
return countExercises(exam.parts.flatMap((x) => x.exercises));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +121,11 @@ export default function ExamList({user}: {user: User}) {
|
|||||||
}),
|
}),
|
||||||
columnHelper.accessor("module", {
|
columnHelper.accessor("module", {
|
||||||
header: "Module",
|
header: "Module",
|
||||||
cell: (info) => <span className={CLASSES[info.getValue()]}>{capitalize(info.getValue())}</span>,
|
cell: (info) => (
|
||||||
|
<span className={CLASSES[info.getValue()]}>
|
||||||
|
{capitalize(info.getValue())}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor((x) => getTotalExercises(x), {
|
columnHelper.accessor((x) => getTotalExercises(x), {
|
||||||
header: "Exercises",
|
header: "Exercises",
|
||||||
@@ -96,6 +135,21 @@ export default function ExamList({user}: {user: User}) {
|
|||||||
header: "Timer",
|
header: "Timer",
|
||||||
cell: (info) => <>{info.getValue()} minute(s)</>,
|
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: "",
|
header: "",
|
||||||
id: "actions",
|
id: "actions",
|
||||||
@@ -105,11 +159,18 @@ export default function ExamList({user}: {user: User}) {
|
|||||||
<div
|
<div
|
||||||
data-tip="Load exam"
|
data-tip="Load exam"
|
||||||
className="cursor-pointer tooltip"
|
className="cursor-pointer tooltip"
|
||||||
onClick={async () => await loadExam(row.original.module, row.original.id)}>
|
onClick={async () =>
|
||||||
|
await loadExam(row.original.module, row.original.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
<BsUpload className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
<BsUpload className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
{PERMISSIONS.examManagement.delete.includes(user.type) && (
|
{PERMISSIONS.examManagement.delete.includes(user.type) && (
|
||||||
<div data-tip="Delete" className="cursor-pointer tooltip" onClick={() => deleteExam(row.original)}>
|
<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" />
|
<BsTrash className="hover:text-mti-purple-light transition ease-in-out duration-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -120,7 +181,7 @@ export default function ExamList({user}: {user: User}) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: exams,
|
data: parsedExams,
|
||||||
columns: defaultColumns,
|
columns: defaultColumns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
@@ -132,7 +193,12 @@ export default function ExamList({user}: {user: User}) {
|
|||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th className="p-4 text-left" key={header.id}>
|
<th className="p-4 text-left" key={header.id}>
|
||||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -140,7 +206,10 @@ export default function ExamList({user}: {user: User}) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="px-2">
|
<tbody className="px-2">
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => (
|
||||||
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
|
<tr
|
||||||
|
className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2"
|
||||||
|
key={row.id}
|
||||||
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<td className="px-4 py-2" key={cell.id}>
|
<td className="px-4 py-2" key={cell.id}>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user