Added level report

This commit is contained in:
Joao Ramos
2024-01-04 22:20:00 +00:00
parent 432f4a735f
commit 7a297a6f6c
6 changed files with 310 additions and 110 deletions

View File

@@ -0,0 +1,165 @@
import React from "react";
import { View, Text } from "@react-pdf/renderer";
import { ModuleScore } from "@/interfaces/module.scores";
import { styles } from "../styles";
import { RadialResult } from "./radial.result";
interface Props {
detail: ModuleScore;
}
const thresholds = [
{
level: "Low A1",
label: "Begginner",
minValue: 0,
maxValue: 3,
},
{
level: "High A1/Low A2",
label: "Elementary",
minValue: 4,
maxValue: 7,
},
{
level: "High A2/Low B1",
label: "Pre-Intermediate",
minValue: 8,
maxValue: 12,
},
{
level: "High B2/Low C1",
label: "Upper-Intermediate",
minValue: 16,
maxValue: 21,
},
{
level: "C1",
label: "Advanced",
minValue: 22,
maxValue: 25,
},
];
export const LevelExamDetails = ({ detail }: Props) => {
const updatedThresholds = thresholds.map((t) => ({
...t,
match: detail.score >= t.minValue && detail.score <= t.maxValue,
}));
const getBackgroundColor = (match: boolean, base: boolean) => {
if (match) return "#c2bfdd";
return base ? "#553b25" : "#ea7c7b";
};
const getTextColor = (match: boolean, base: boolean) => {
if (match) return "#9e7936";
return base ? "white" : "#553b25";
};
return (
<View
style={[
styles.textFont,
{
display: "flex",
flexDirection: "row",
gap: 30,
justifyContent: "space-between",
},
]}
>
<RadialResult {...detail} />
<View
style={{
display: "flex",
flex: 1,
flexDirection: "column",
}}
>
<View
style={{
display: "flex",
alignItems: "center",
}}
>
<Text
style={[styles.textBold, styles.textColor, { fontSize: "10px" }]}
>
Level as per CEFR Levels
</Text>
</View>
<View style={{ display: "flex", flex: 1, flexDirection: "row" }}>
{updatedThresholds.map(
({ level, label, minValue, maxValue, match }, index, arr) => (
<View
key={label}
style={{
width: `calc(100% / ${arr.length})`,
display: "flex",
flexDirection: "column",
}}
>
<View
style={{
backgroundColor: getBackgroundColor(match, true),
paddingVertical: "8px",
alignItems: "center",
}}
>
<Text
style={[
styles.textBold,
{
color: getTextColor(match, true),
fontSize: "6px",
},
]}
>
{level}
</Text>
</View>
<View
style={{
backgroundColor: getBackgroundColor(match, false),
paddingVertical: "8px",
alignItems: "center",
}}
>
<Text
style={[
styles.textBold,
{
color: getTextColor(match, false),
fontSize: "6px",
},
]}
>
{label}
</Text>
</View>
<View
style={{
backgroundColor: getBackgroundColor(match, true),
paddingVertical: "24px",
alignItems: "center",
}}
>
<Text
style={[
styles.textBold,
{
color: getTextColor(match, true),
fontSize: "10px",
},
]}
>
{minValue}-{maxValue}
</Text>
</View>
</View>
)
)}
</View>
</View>
</View>
);
};

View File

@@ -0,0 +1,36 @@
import React from "react";
import {
Document,
Page,
View,
Text,
StyleSheet,
Image,
} from "@react-pdf/renderer";
import { styles } from "../styles";
import { ModuleScore } from "@/interfaces/module.scores";
export const RadialResult = ({ module, score, total }: ModuleScore) => (
<View
key="module"
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 8,
}}
>
<Text
style={[
styles.textFont,
styles.textColor,
styles.textBold,
{ fontSize: 10 },
]}
>
{module}
</Text>
<Text>{score}</Text>
<Text>Out of {total}</Text>
</View>
);

View File

@@ -0,0 +1,25 @@
import React from "react";
import {
Document,
Page,
View,
Text,
StyleSheet,
Image,
} from "@react-pdf/renderer";
import { ModuleScore } from "@/interfaces/module.scores";
import { styles } from "../styles";
import { RadialResult } from "./radial.result";
interface Props {
testDetails: ModuleScore[];
}
export const SkillExamDetails = ({ testDetails }: Props) => (
<View style={{ display: "flex", flexDirection: "row", gap: 30 }}>
{testDetails.map((detail) => {
const { module } = detail;
return <RadialResult key={module} {...detail} />;
})}
</View>
);

View File

@@ -1,64 +1,13 @@
/* eslint-disable jsx-a11y/alt-text */ /* eslint-disable jsx-a11y/alt-text */
import React from "react"; import React from "react";
import { import { Document, Page, View, Text, Image } from "@react-pdf/renderer";
Document,
Page,
View,
Text,
StyleSheet,
Image,
} from "@react-pdf/renderer";
import ProgressBar from "./progress.bar"; import ProgressBar from "./progress.bar";
// import RadialProgress from "./radial.progress"; // import RadialProgress from "./radial.progress";
// import RadialProgressSvg from "./radial.progress.svg"; // import RadialProgressSvg from "./radial.progress.svg";
import { Module } from "@/interfaces"; import { Module } from "@/interfaces";
import { ModuleScore } from "@/interfaces/module.scores"; import { ModuleScore } from "@/interfaces/module.scores";
// import logo from './logo_title.png'; // import logo from './logo_title.png';
import { styles } from "./styles";
const styles = StyleSheet.create({
body: {
paddingTop: 10,
paddingBottom: 20,
paddingHorizontal: 35,
},
titleView: {
display: "flex",
// flex: 1,
alignItems: "center",
},
title: {
textTransform: "uppercase",
},
textPadding: {
margin: "8px",
},
separator: {
width: "100%",
borderBottom: "1px solid #89b0c2",
},
textFont: {
fontFamily: "Helvetica",
},
textBold: {
fontFamily: "Helvetica-Bold",
fontWeight: "bold",
},
textColor: {
color: "#4e4969",
},
textUnderline: {
textDecoration: "underline",
},
spacedRow: {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
},
alignRightRow: {
display: "flex",
flexDirection: "row-reverse",
}
});
interface Props { interface Props {
date: string; date: string;
@@ -70,6 +19,7 @@ interface Props {
summary: string; summary: string;
logo: string; logo: string;
qrcode: string; qrcode: string;
renderDetails: React.ReactNode;
} }
const PDFReport = ({ const PDFReport = ({
@@ -82,6 +32,7 @@ const PDFReport = ({
summary, summary,
logo, logo,
qrcode, qrcode,
renderDetails,
}: Props) => { }: Props) => {
const defaultTextStyle = [styles.textFont, { fontSize: 8 }]; const defaultTextStyle = [styles.textFont, { fontSize: 8 }];
const defaultSkillsTextStyle = [styles.textFont, { fontSize: 8 }]; const defaultSkillsTextStyle = [styles.textFont, { fontSize: 8 }];
@@ -94,14 +45,8 @@ const PDFReport = ({
return ( return (
<Document> <Document>
<Page style={styles.body}> <Page style={styles.body}>
<View <View style={styles.alignRightRow}>
style={styles.alignRightRow} <Image src={logo} fixed style={{ height: "64px", width: "64px" }} />
>
<Image
src={logo}
fixed
style={{ height: "64px", width: "64px" }}
/>
</View> </View>
<View style={styles.titleView}> <View style={styles.titleView}>
<Text <Text
@@ -129,7 +74,7 @@ const PDFReport = ({
<Text style={defaultTextStyle}>Email: {email}</Text> <Text style={defaultTextStyle}>Email: {email}</Text>
<Text style={defaultTextStyle}>Gender: {gender}</Text> <Text style={defaultTextStyle}>Gender: {gender}</Text>
</View> </View>
<View> <View style={{ flex: 1}}>
<Text <Text
style={[ style={[
styles.textFont, styles.textFont,
@@ -140,32 +85,7 @@ const PDFReport = ({
> >
Test Details: Test Details:
</Text> </Text>
<View style={{ display: "flex", flexDirection: "row", gap: 30 }}> <View>{renderDetails}</View>
{testDetails.map(({ module, score, total }) => (
<View
key="module"
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 8,
}}
>
<Text
style={[
styles.textFont,
styles.textColor,
styles.textBold,
{ fontSize: 10 },
]}
>
{module}
</Text>
<Text>{score}</Text>
<Text>Out of {total}</Text>
</View>
))}
</View>
</View> </View>
<View> <View>
<Text <Text
@@ -213,10 +133,13 @@ const PDFReport = ({
))} ))}
</View> </View>
<View style={styles.alignRightRow}> <View style={styles.alignRightRow}>
<Image src={qrcode} style={{ <Image
width: '80px', src={qrcode}
height: '80px', style={{
}}/> width: "80px",
height: "80px",
}}
/>
</View> </View>
</View> </View>
<View style={[{ paddingTop: 30 }, styles.separator]}> <View style={[{ paddingTop: 30 }, styles.separator]}>

46
src/exams/pdf/styles.ts Normal file
View File

@@ -0,0 +1,46 @@
import { StyleSheet } from "@react-pdf/renderer";
export const styles = StyleSheet.create({
body: {
paddingTop: 10,
paddingBottom: 20,
paddingHorizontal: 35,
},
titleView: {
display: "flex",
// flex: 1,
alignItems: "center",
},
title: {
textTransform: "uppercase",
},
textPadding: {
margin: "8px",
},
separator: {
width: "100%",
borderBottom: "1px solid #89b0c2",
},
textFont: {
fontFamily: "Helvetica",
},
textBold: {
fontFamily: "Helvetica-Bold",
fontWeight: "bold",
},
textColor: {
color: "#4e4969",
},
textUnderline: {
textDecoration: "underline",
},
spacedRow: {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
},
alignRightRow: {
display: "flex",
flexDirection: "row-reverse",
},
});

View File

@@ -14,16 +14,14 @@ import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session"; import { sessionOptions } from "@/lib/session";
import ReactPDF from "@react-pdf/renderer"; import ReactPDF from "@react-pdf/renderer";
import PDFReport from "@/exams/pdf"; import PDFReport from "@/exams/pdf";
import { import { ref, uploadBytes } from "firebase/storage";
ref,
uploadBytes,
} from "firebase/storage";
import { Stat } from "@/interfaces/user"; import { Stat } from "@/interfaces/user";
import { User } from "@/interfaces/user"; import { User } from "@/interfaces/user";
import { Module } from "@/interfaces"; import { Module } from "@/interfaces";
import { ModuleScore } from "@/interfaces/module.scores"; import { ModuleScore } from "@/interfaces/module.scores";
import qrcode from 'qrcode'; import qrcode from "qrcode";
import { SkillExamDetails } from "@/exams/pdf/details/skill.exam";
import { LevelExamDetails } from "@/exams/pdf/details/level.exam";
const db = getFirestore(app); const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions); export default withIronSessionApiRoute(handler, sessionOptions);
@@ -122,7 +120,7 @@ const generateQRCode = async (link: string) => {
const qrCodeDataURL = await qrcode.toDataURL(link); const qrCodeDataURL = await qrcode.toDataURL(link);
return qrCodeDataURL; return qrCodeDataURL;
} catch (error) { } catch (error) {
console.error('Error generating QR code:', error); console.error("Error generating QR code:", error);
return null; return null;
} }
}; };
@@ -157,7 +155,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
const stats = docsSnap.docs.map((d) => d.data()); const stats = docsSnap.docs.map((d) => d.data());
const results = stats.reduce((accm: ModuleScore[], { module, score }) => { const results = stats.reduce((accm: ModuleScore[], { module, score }) => {
const fixedModuleStr = module[0].toUpperCase() + module.substring(1) const fixedModuleStr = module[0].toUpperCase() + module.substring(1);
if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) { if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) {
return accm.map((e: ModuleScore) => { return accm.map((e: ModuleScore) => {
if (e.module === fixedModuleStr) { if (e.module === fixedModuleStr) {
@@ -192,9 +190,22 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
const overallResult = overallScore / overallTotal; const overallResult = overallScore / overallTotal;
const performanceSummary = getPerformanceSummary("level", overallResult); const performanceSummary = getPerformanceSummary("level", overallResult);
const qrcode = await generateQRCode((req.headers.origin || '') + req.url); const qrcode = await generateQRCode((req.headers.origin || "") + req.url);
if(qrcode) { const overallDetail = {
module: "Overall",
score: overallScore,
total: overallTotal,
} as ModuleScore;
const testDetails = [overallDetail, ...results];
const renderDetails = () => {
if (stats[0].module === "level") {
return <LevelExamDetails detail={overallDetail} />;
}
return <SkillExamDetails testDetails={testDetails} />;
};
if (qrcode) {
const pdfStream = await ReactPDF.renderToStream( const pdfStream = await ReactPDF.renderToStream(
<PDFReport <PDFReport
date={new Date(stat.date).toLocaleString()} date={new Date(stat.date).toLocaleString()}
@@ -203,14 +214,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
id={user.id} id={user.id}
gender={user.demographicInformation?.gender} gender={user.demographicInformation?.gender}
summary={performanceSummary} summary={performanceSummary}
testDetails={[ testDetails={testDetails}
{ renderDetails={renderDetails()}
module: "Overall",
score: overallScore,
total: overallTotal,
},
...results,
]}
logo={"public/logo_title.png"} logo={"public/logo_title.png"}
qrcode={qrcode} qrcode={qrcode}
/> />