Added the ability to set an exam as private

This commit is contained in:
Tiago Ribeiro
2024-08-28 10:26:45 +01:00
parent 951ca5736e
commit dbf262598f
2 changed files with 56 additions and 4 deletions

View File

@@ -13,7 +13,7 @@ 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 {BsBan, BsBanFill, BsCheck, BsCircle, BsStop, BsTrash, BsUpload, BsX} from "react-icons/bs";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {useListSearch} from "@/hooks/useListSearch"; import {useListSearch} from "@/hooks/useListSearch";
@@ -72,6 +72,28 @@ export default function ExamList({user}: {user: User}) {
router.push("/exercises"); router.push("/exercises");
}; };
const privatizeExam = async (exam: Exam) => {
if (!confirm(`Are you sure you want to make this ${capitalize(exam.module)} exam ${exam.private ? "public" : "private"}?`)) return;
axios
.patch(`/api/exam/${exam.module}/${exam.id}`, {private: !exam.private})
.then(() => toast.success(`Updated the "${exam.id}" exam`))
.catch((reason) => {
if (reason.response.status === 404) {
toast.error("Exam not found!");
return;
}
if (reason.response.status === 403) {
toast.error("You do not have permission to update this exam!");
return;
}
toast.error("Something went wrong, please try again later.");
})
.finally(reload);
};
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;
@@ -119,6 +141,10 @@ 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("private", {
header: "Private",
cell: (info) => <span className="w-full flex items-center justify-center">{!info.getValue() ? <BsX /> : <BsCheck />}</span>,
}),
columnHelper.accessor("createdAt", { columnHelper.accessor("createdAt", {
header: "Created At", header: "Created At",
cell: (info) => { cell: (info) => {
@@ -140,12 +166,18 @@ export default function ExamList({user}: {user: User}) {
cell: ({row}: {row: {original: Exam}}) => { cell: ({row}: {row: {original: Exam}}) => {
return ( return (
<div className="flex gap-4"> <div className="flex gap-4">
<div <button
data-tip={row.original.private ? "Set as public" : "Set as private"}
onClick={async () => await privatizeExam(row.original)}
className="cursor-pointer tooltip">
{row.original.private ? <BsCircle /> : <BsBan />}
</button>
<button
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> </button>
{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" />

View File

@@ -1,7 +1,7 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next"; import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase"; import {app} from "@/firebase";
import {getFirestore, doc, getDoc, deleteDoc} from "firebase/firestore"; import {getFirestore, doc, getDoc, deleteDoc, setDoc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next"; import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session"; import {sessionOptions} from "@/lib/session";
import {PERMISSIONS} from "@/constants/userPermissions"; import {PERMISSIONS} from "@/constants/userPermissions";
@@ -12,6 +12,7 @@ export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res); if (req.method === "GET") return get(req, res);
if (req.method === "PATCH") return patch(req, res);
if (req.method === "DELETE") return del(req, res); if (req.method === "DELETE") return del(req, res);
} }
@@ -37,6 +38,25 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
} }
} }
async function patch(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {module, id} = req.query as {module: string; id: string};
const docRef = doc(db, module, id);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
await setDoc(docRef, req.body, {merge: true});
res.status(200).json({ok: true});
} else {
res.status(404).json({ok: false});
}
}
async function del(req: NextApiRequest, res: NextApiResponse) { async function del(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) { if (!req.session.user) {
res.status(401).json({ok: false}); res.status(401).json({ok: false});