diff --git a/src/dashboards/AssignmentCard.tsx b/src/dashboards/AssignmentCard.tsx
index 36fa5a43..52b000c1 100644
--- a/src/dashboards/AssignmentCard.tsx
+++ b/src/dashboards/AssignmentCard.tsx
@@ -2,19 +2,20 @@ import ProgressBar from "@/components/Low/ProgressBar";
import useUsers from "@/hooks/useUsers";
import {Module} from "@/interfaces";
import {Assignment} from "@/interfaces/results";
-import {Stat} from "@/interfaces/user";
import {calculateBandScore} from "@/utils/score";
import clsx from "clsx";
import moment from "moment";
-import {useState} from "react";
import {BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen} from "react-icons/bs";
+import { usePDFDownload } from "@/hooks/usePDFDownload";
interface Props {
onClick?: () => void;
+ allowDownload?: boolean;
}
-export default function AssignmentCard({id, name, assigner, startDate, endDate, assignees, results, exams, onClick}: Assignment & Props) {
+export default function AssignmentCard({id, name, assigner, startDate, endDate, assignees, results, exams, onClick, allowDownload}: Assignment & Props) {
const {users} = useUsers();
+ const renderPdfIcon = usePDFDownload("assignments");
const calculateAverageModuleScore = (module: Module) => {
const resultModuleBandScores = results.map((r) => {
@@ -33,7 +34,10 @@ export default function AssignmentCard({id, name, assigner, startDate, endDate,
onClick={onClick}
className="w-[350px] h-fit flex flex-col gap-6 bg-white border border-mti-gray-platinum hover:drop-shadow p-4 cursor-pointer rounded-xl transition ease-in-out duration-300">
-
{name}
+
+
{name}
+ {allowDownload && renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")}
+
Past Assignments ({assignments.filter(pastFilter).length})
{assignments.filter(pastFilter).map((a) => (
-
setSelectedAssignment(a)} key={a.id} />
+ setSelectedAssignment(a)} key={a.id} allowDownload/>
))}
diff --git a/src/hooks/usePDFDownload.tsx b/src/hooks/usePDFDownload.tsx
new file mode 100644
index 00000000..54b83884
--- /dev/null
+++ b/src/hooks/usePDFDownload.tsx
@@ -0,0 +1,60 @@
+import React from "react";
+import axios from "axios";
+import { toast } from "react-toastify";
+import { BsFilePdf } from "react-icons/bs";
+
+type DownloadingPdf = {
+ [key: string]: boolean;
+};
+
+type PdfEndpoint = "stats" | "assignments";
+
+export const usePDFDownload = (endpoint: PdfEndpoint) => {
+ const [downloadingPdf, setDownloadingPdf] = React.useState(
+ {}
+ );
+
+ const triggerDownload = async (id: string) => {
+ try {
+ setDownloadingPdf((prev) => ({ ...prev, [id]: true }));
+ const res = await axios.post(`/api/${endpoint}/${id}/export`);
+ toast.success("Report ready!");
+ const link = document.createElement("a");
+ link.href = res.data;
+ // download should have worked but there are some CORS issues
+ // https://firebase.google.com/docs/storage/web/download-files#cors_configuration
+ // link.download="report.pdf";
+ link.target = "_blank";
+ link.rel = "noreferrer";
+ link.click();
+ setDownloadingPdf((prev) => ({ ...prev, [id]: false }));
+ } catch (err) {
+ toast.error("Failed to display the report!");
+ console.error(err);
+ setDownloadingPdf((prev) => ({ ...prev, [id]: false }));
+ }
+ };
+
+ const renderIcon = (
+ id: string,
+ downloadClasses: string,
+ loadingClasses: string
+ ) => {
+ if (downloadingPdf[id]) {
+ return (
+
+ );
+ }
+ return (
+ {
+ e.stopPropagation();
+ triggerDownload(id);
+ }}
+ />
+ );
+ };
+
+ return renderIcon;
+};
diff --git a/src/pages/api/assignments/[id]/export.tsx b/src/pages/api/assignments/[id]/export.tsx
index 380cd846..e3bb9e13 100644
--- a/src/pages/api/assignments/[id]/export.tsx
+++ b/src/pages/api/assignments/[id]/export.tsx
@@ -15,17 +15,14 @@ import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import ReactPDF from "@react-pdf/renderer";
import GroupTestReport from "@/exams/pdf/group.test.report";
-import { ref, uploadBytes } from "firebase/storage";
+import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
import { Stat } from "@/interfaces/user";
import { User } from "@/interfaces/user";
import { Module } from "@/interfaces";
import { ModuleScore, StudentData } from "@/interfaces/module.scores";
-import qrcode from "qrcode";
import { SkillExamDetails } from "@/exams/pdf/details/skill.exam";
import { LevelExamDetails } from "@/exams/pdf/details/level.exam";
import { calculateBandScore, getLevelScore } from "@/utils/score";
-import axios from "axios";
-import { moduleLabels } from "@/utils/moduleUtils";
import {
generateQRCode,
getRadialProgressPNG,
@@ -121,17 +118,25 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
results: any;
exams: { module: Module }[];
startDate: string;
+ pdf?: string;
};
if (!data) {
res.status(400).end();
return;
}
- // TODO: Reenable this
- // if (data.assigner !== req.session.user.id) {
- // res.status(401).json({ ok: false });
- // return;
- // }
+ if (data.assigner !== req.session.user.id) {
+ res.status(401).json({ ok: false });
+ return;
+ }
+ if (data.pdf) {
+ // if it does, return the pdf url
+ const fileRef = ref(storage, data.pdf);
+ const url = await getDownloadURL(fileRef);
+
+ res.status(200).end(url);
+ return;
+ }
try {
const docUser = await getDoc(doc(db, "users", req.session.user.id));
@@ -185,9 +190,10 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
(e) => e.module === module
);
- const bandScore =
+ const baseBandScore =
moduleResults.reduce((accm, curr) => accm + curr.bandScore, 0) /
moduleResults.length;
+ const bandScore = isNaN(baseBandScore) ? 0 : baseBandScore;
const { correct, total } = getScoreAndTotal(moduleResults);
const png = getRadialProgressPNG("azul", correct, total);
@@ -203,7 +209,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
const { correct: overallCorrect, total: overallTotal } =
getScoreAndTotal(flattenResults);
- const overallResult = overallCorrect / overallTotal;
+ const baseOverallResult = overallCorrect / overallTotal;
+ const overallResult = isNaN(baseOverallResult) ? 0 : baseOverallResult;
const overallPNG = getRadialProgressPNG(
"laranja",
@@ -365,7 +372,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
// generate the file ref for storage
const fileName = `${Date.now().toString()}.pdf`;
- const fileRef = ref(storage, `assignment_report/${fileName}`);
+ const refName = `assignment_report/${fileName}`;
+ const fileRef = ref(storage, refName);
// upload the pdf to storage
const pdfBuffer = await streamToBuffer(pdfStream);
@@ -375,9 +383,10 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
// update the stats entries with the pdf url to prevent duplication
await updateDoc(docSnap.ref, {
- pdf: snapshot.ref.fullPath,
+ pdf: refName,
});
- res.status(200).end(snapshot.ref.fullPath);
+ const url = await getDownloadURL(fileRef);
+ res.status(200).end(url);
return;
}
@@ -407,7 +416,9 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
}
if (data.pdf) {
- return res.end(data.pdf);
+ const fileRef = ref(storage, data.pdf);
+ const url = await getDownloadURL(fileRef);
+ return res.redirect(url);
}
res.status(404).end();
diff --git a/src/pages/record.tsx b/src/pages/record.tsx
index 80323849..728cbbc5 100644
--- a/src/pages/record.tsx
+++ b/src/pages/record.tsx
@@ -24,9 +24,7 @@ import useGroups from "@/hooks/useGroups";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import useAssignments from "@/hooks/useAssignments";
import {uuidv4} from "@firebase/util";
-import { BsFilePdf } from "react-icons/bs";
-import axios from "axios";
-import {toast} from "react-toastify";
+import { usePDFDownload } from "@/hooks/usePDFDownload";
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
@@ -58,10 +56,6 @@ export const getServerSideProps = withIronSessionSsr(({req, res}) => {
};
}, sessionOptions);
-type DownloadingPdf = {
- [key: string]: boolean;
-};
-
export default function History({user}: {user: User}) {
const [statsUserId, setStatsUserId] = useState(user.id);
const [groupedStats, setGroupedStats] = useState<{[key: string]: Stat[]}>();
@@ -76,8 +70,8 @@ export default function History({user}: {user: User}) {
const setShowSolutions = useExamStore((state) => state.setShowSolutions);
const setUserSolutions = useExamStore((state) => state.setUserSolutions);
const setSelectedModules = useExamStore((state) => state.setSelectedModules);
- const [downloadingPdf, setDownloadingPdf] = useState({});
const router = useRouter();
+ const renderPdfIcon = usePDFDownload("stats");
useEffect(() => {
if (stats && !isStatsLoading) {
@@ -208,28 +202,6 @@ export default function History({user}: {user: User}) {
correct / total < 0.3 && "text-mti-rose",
);
- const triggerDownload = async () => {
- try {
- setDownloadingPdf((prev) => ({...prev, [session]: true}));
- const res = await axios.post(`/api/stats/${session}/export`);
- toast.success("Report ready!");
- const link = document.createElement("a");
- link.href = res.data;
- // download should have worked but there are some CORS issues
- // https://firebase.google.com/docs/storage/web/download-files#cors_configuration
- // link.download="report.pdf";
- link.target = '_blank';
- link.rel="noreferrer"
- link.click();
- setDownloadingPdf((prev) => ({...prev, [session]: false}));
- } catch(err) {
- toast.error("Failed to display the report!");
- console.error(err);
- setDownloadingPdf((prev) => ({...prev, [session]: false}));
-
- }
-
- }
const content = (
<>
@@ -248,32 +220,7 @@ export default function History({user}: {user: User}) {
Level{" "}
{(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)}
- {/*
- {
- e.stopPropagation();
- triggerDownload();
- }}
- />
- */}
- {downloadingPdf[session] ?
-
:
- (
-
{
- e.stopPropagation();
- triggerDownload();
- }}
- />
- )
- }
+ {renderPdfIcon(session, textColor, textColor)}
diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts
index 16c0f471..596737b5 100644
--- a/src/utils/pdf.ts
+++ b/src/utils/pdf.ts
@@ -22,22 +22,23 @@ export const getRadialProgressPNG = (
// calculate the percentage of the score
// and round it to the closest available image
const percent = (score / total) * 100;
+ if (isNaN(percent)) return `public/radial_progress/${color}_0.png`;
const remainder = percent % 10;
const roundedPercent = percent - remainder;
return `public/radial_progress/${color}_${roundedPercent}.png`;
};
export const streamToBuffer = async (
- stream: NodeJS.ReadableStream
- ): Promise => {
- return new Promise((resolve, reject) => {
- const chunks: Buffer[] = [];
- stream.on("data", (data) => {
- chunks.push(data);
- });
- stream.on("end", () => {
- resolve(Buffer.concat(chunks));
- });
- stream.on("error", reject);
+ stream: NodeJS.ReadableStream
+): Promise => {
+ return new Promise((resolve, reject) => {
+ const chunks: Buffer[] = [];
+ stream.on("data", (data) => {
+ chunks.push(data);
});
- };
\ No newline at end of file
+ stream.on("end", () => {
+ resolve(Buffer.concat(chunks));
+ });
+ stream.on("error", reject);
+ });
+};