Initial version of PDF export
This commit is contained in:
141
src/exams/pdf/index.tsx
Normal file
141
src/exams/pdf/index.tsx
Normal 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' 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;
|
||||
51
src/exams/pdf/progress.bar.tsx
Normal file
51
src/exams/pdf/progress.bar.tsx
Normal 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;
|
||||
100
src/pages/api/stats/[id]/export.tsx
Normal file
100
src/pages/api/stats/[id]/export.tsx
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user