Initial version of PDF export

This commit is contained in:
Joao Ramos
2023-12-28 23:41:21 +00:00
parent 5447c89da4
commit 0b6a66b12d
6 changed files with 670 additions and 6 deletions

141
src/exams/pdf/index.tsx Normal file
View File

@@ -0,0 +1,141 @@
import React from "react";
import { Document, Page, View, Text, StyleSheet } from "@react-pdf/renderer";
import ProgressBar from "./progress.bar";
const styles = StyleSheet.create({
body: {
paddingTop: 35,
paddingBottom: 65,
paddingHorizontal: 35,
},
titleView: {
display: "flex",
// flex: 1,
alignItems: "center",
},
title: {
textTransform: "uppercase",
},
textPadding: {
margin: "16px",
},
userSection: {
fontWeight: "bold",
},
separator: {
width: "100%",
borderBottom: "1px solid blue",
},
textColor: {
color: "blue",
},
textUnderline: {
textDecoration: "underline",
},
skillsTitle: {
fontSize: 14,
},
skillsText: {
fontSize: 12,
},
footerText: {
fontSize: 9,
},
});
interface Props {
date: string;
name: string;
email: string;
id: string;
gender?: string;
}
const PDFReport = ({ date, name, email, id, gender }: Props) => {
return (
<Document>
<Page style={styles.body}>
<View style={styles.titleView}>
<Text style={[styles.textColor, styles.textUnderline, styles.title]}>
English Skills Test Result Report
</Text>
</View>
<View style={styles.textPadding}>
<Text>Date of Test: {date}</Text>
</View>
<Text style={styles.userSection}>Candidate Information:</Text>
<View style={styles.textPadding}>
<Text>Name: {name}</Text>
<Text>ID: {id}</Text>
<Text>Email: {email}</Text>
<Text>Gender: {gender}</Text>
</View>
<View>
<Text>Test Details:</Text>
</View>
<View>
<Text>Performance Summary</Text>
<View>
<Text></Text>
</View>
</View>
<View style={[{ paddingTop: 30 }, styles.separator]}></View>
<View>
<Text style={[styles.textColor, styles.textUnderline]}>
Skills Feedback
</Text>
<View
style={{
paddingTop: 10,
}}
>
<Text style={[styles.textColor, styles.skillsTitle]}>
Listening
</Text>
<Text style={styles.skillsText}>xxx</Text>
<Text style={[styles.textColor, styles.skillsTitle]}>Reading</Text>
<Text style={styles.skillsText}>xxx</Text>
<Text style={[styles.textColor, styles.skillsTitle]}>Writing</Text>
<Text style={styles.skillsText}>xxx</Text>
<Text style={[styles.textColor, styles.skillsTitle]}>Speaking</Text>
<Text style={styles.skillsText}>xxx</Text>
</View>
</View>
<View style={[{ paddingTop: 30 }, styles.separator]}></View>
<View style={[{ paddingTop: 30 }, styles.footerText]}>
<Text>Validity</Text>
<Text>
This report remains valid for a duration of three months from the
test date. Confidential circulated for concern people
</Text>
<Text>Declaration</Text>
<Text>
We hereby declare that exam results on our platform, assessed by AI,
are not the sole determinants of candidates&apos; English
proficiency levels. While EnCoach provides feedback based on
assessments, we recognize that language proficiency encompasses
practical application, cultural understanding, and real-life
communication. We urge users to consider exam results as a measure
of progress and improvement, and we continuously enhance our system
to ensure accuracy and reliability.
</Text>
<View>
<ProgressBar
width={200}
height={50}
backgroundColor="lightblue"
progressColor="red"
percentage={60}
/>
</View>
<View style={styles.textColor}>
<Text>info@encoach.com</Text>
<Text>https://encoach.com</Text>
<Text>Group ID: TRI64BNBOIU5043</Text>
</View>
</View>
</Page>
</Document>
);
};
export default PDFReport;

View File

@@ -0,0 +1,51 @@
import React from "react";
import { View, StyleSheet } from "@react-pdf/renderer";
const styles = StyleSheet.create({
progressBar: {
borderRadius: 16,
overflow: "hidden",
},
progressBarPerc: {
height: "100%",
zIndex: 1,
},
});
interface Props {
width: number;
height: number;
backgroundColor: string;
progressColor: string;
percentage: number;
}
const ProgressBar = ({
width,
height,
backgroundColor,
progressColor,
percentage,
}: Props) => {
return (
<View
style={[
{
width,
height,
backgroundColor,
},
styles.progressBar,
]}
>
<View
style={[
{ width: `${percentage}px`, backgroundColor: progressColor },
styles.progressBarPerc,
]}
></View>
</View>
);
};
export default ProgressBar;

View File

@@ -0,0 +1,100 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { app, storage } from "@/firebase";
import {
getFirestore,
doc,
getDoc,
deleteDoc,
updateDoc,
} from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import ReactPDF from "@react-pdf/renderer";
import PDFReport from "@/exams/pdf";
import {
ref,
uploadBytes,
deleteObject,
getDownloadURL,
} from "firebase/storage";
import blobStream from "blob-stream";
import { Stat } from "@/interfaces/user";
import { User } from "@/interfaces/user";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
}
export const streamToBuffer = async (
stream: NodeJS.ReadableStream,
): Promise<Buffer> => {
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);
});
};
async function post(req: NextApiRequest, res: NextApiResponse) {
// debugger;
if (req.session.user) {
const { id } = req.query as { id: string };
const docRef = doc(db, "stats", id);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
const stat = docSnap.data() as Stat;
if (stat.user !== req.session.user.id) {
res.status(401).json(undefined);
return;
}
// if (stat.pdf) {
// res.status(200).end(docSnap.pdf);
// return;
// }
const docUser = await getDoc(doc(db, "users", stat.user));
if (docUser.exists()) {
const user = docUser.data() as User;
const fileName = `${Date.now().toString()}.pdf`;
const fileRef = ref(storage, `exam_report/${fileName}`);
const pdfStream = await ReactPDF.renderToStream(
<PDFReport
date={new Date(stat.date).toString()}
name={user.name}
email={user.email}
id={user.id}
gender={user.demographicInformation?.gender}
/>
);
const pdfBuffer = await streamToBuffer(pdfStream);
const snapshot = await uploadBytes(fileRef, pdfBuffer, {
contentType: 'application/pdf',
});
await updateDoc(docRef, {
pdf: snapshot.ref.fullPath,
});
res.status(200).end(snapshot.ref.fullPath);
return;
}
}
res.status(500).json({ ok: false });
return;
}
res.status(401).json(undefined);
}

View File

@@ -15,7 +15,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return;
}
const {user} = req.query;
const {id: user} = req.query;
const q = query(collection(db, "stats"), where("user", "==", user));
const snapshot = await getDocs(q);