Added level report
This commit is contained in:
165
src/exams/pdf/details/level.exam.tsx
Normal file
165
src/exams/pdf/details/level.exam.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
36
src/exams/pdf/details/radial.result.tsx
Normal file
36
src/exams/pdf/details/radial.result.tsx
Normal 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>
|
||||||
|
);
|
||||||
25
src/exams/pdf/details/skill.exam.tsx
Normal file
25
src/exams/pdf/details/skill.exam.tsx
Normal 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>
|
||||||
|
);
|
||||||
@@ -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
46
src/exams/pdf/styles.ts
Normal 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",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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,8 +190,21 @@ 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);
|
||||||
|
|
||||||
|
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) {
|
if (qrcode) {
|
||||||
const pdfStream = await ReactPDF.renderToStream(
|
const pdfStream = await ReactPDF.renderToStream(
|
||||||
<PDFReport
|
<PDFReport
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user