Skip to content

Commit

Permalink
added: save test to pdf (#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
3LL4N authored Dec 1, 2023
1 parent 9657e6b commit 48de83c
Show file tree
Hide file tree
Showing 9 changed files with 583 additions and 36 deletions.
3 changes: 3 additions & 0 deletions apps/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@
"expo-av": "^13.2.1",
"expo-blur": "^12.2.2",
"expo-document-picker": "^11.2.2",
"expo-file-system": "^15.6.0",
"expo-font": "^11.1.1",
"expo-linear-gradient": "^12.1.2",
"expo-media-library": "^15.6.0",
"expo-random": "^13.1.2",
"expo-secure-store": "^12.1.1",
"expo-sharing": "^11.7.0",
"expo-status-bar": "^1.4.4",
"expo-web-browser": "^12.1.1",
"jest-expo": "^49.0.0",
Expand Down
66 changes: 66 additions & 0 deletions apps/expo/src/device-file-saving/SavePdfToAndroidButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { FC } from "react";
import { Button, Alert, Platform } from "react-native";
import * as FileSystem from "expo-file-system";
import { shareAsync } from "expo-sharing";
import { trpc } from "../utils/trpc";

type DownloadPdfButtonProps = {
testId: string;
};

const DownloadPdfButton: FC<DownloadPdfButtonProps> = (props) => {
const generatePdfMutation = trpc.pdfKit.generatePdfByTestId.useMutation();

const saveFile = async (uri: string, filename: string) => {
if (Platform.OS === "android") {
const permissions =
await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
if (permissions.granted) {
const base64 = await FileSystem.readAsStringAsync(uri, {
encoding: FileSystem.EncodingType.Base64,
});
await FileSystem.StorageAccessFramework.createFileAsync(
permissions.directoryUri,
filename,
"testtrek/saves/pdf",
)
.then(async (uri) => {
await FileSystem.writeAsStringAsync(uri, base64, {
encoding: FileSystem.EncodingType.Base64,
});
Alert.alert("File Saved", "File has been saved successfully");
})
.catch((e) => {
console.log(e);
Alert.alert("Error", "Failed to save file");
});
} else {
shareAsync(uri);
}
} else {
shareAsync(uri);
}
};

const downloadAndSavePdf = async () => {
try {
const result = await generatePdfMutation.mutateAsync(props.testId);
const base64pdf = result.pdfBuffer;
const filename = `test-${Date.now()}.pdf`;
const localUri = FileSystem.documentDirectory + filename;

await FileSystem.writeAsStringAsync(localUri, base64pdf, {
encoding: FileSystem.EncodingType.Base64,
});

await saveFile(localUri, filename);
} catch (error) {
console.error("Error saving PDF:", error);
Alert.alert("Error", "Unable to generate or download PDF");
}
};

return <Button title="Save PDF" onPress={downloadAndSavePdf} />;
};

export default DownloadPdfButton;
2 changes: 2 additions & 0 deletions apps/expo/src/screens/test-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ReusableHeader } from "../components/headers/ReusableHeader";
import StarIcon from "../icons/StarIcon";
import useGoBack from "../hooks/useGoBack";
import { SafeAreaView } from "react-native-safe-area-context";
import SavePdfToAndroidButton from "../device-file-saving/SavePdfToAndroidButton";

export const TestDetailsScreen = ({
route,
Expand Down Expand Up @@ -70,6 +71,7 @@ export const TestDetailsScreen = ({
testId={testId}
goToEditTest={goToEditTest}
/>
<SavePdfToAndroidButton testId={testDetails.id} />
<TestDetailsContent testDetails={testDetails} />
</SafeAreaView>
);
Expand Down
2 changes: 2 additions & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
"@trpc/client": "^10.1.0",
"@trpc/server": "^10.1.0",
"@types/pdf-parse": "^1.1.4",
"@types/pdfkit": "^0.13.3",
"algoliasearch": "^4.20.0",
"dotenv": "^16.3.1",
"p-map": "^6.0.0",
"pdf-parse": "^1.1.1",
"pdfkit": "^0.14.0",
"shutterstock-api": "^1.1.35",
"superjson": "^1.9.1",
"trpc-openapi": "^1.2.0",
Expand Down
55 changes: 55 additions & 0 deletions packages/api/src/functions/pdfKitHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import PDFDocument from "pdfkit";
import { Question, CustomTest, Choice } from "./types/pdfKit";

export const formatChoice = (choices: Choice[]): string => {
return choices
.map((choice, index) => {
return `${String.fromCharCode(65 + index)}. ${choice.text}`;
})
.join(" ");
};

export const formatQuestion = (
question: Question,
itemNumber: number,
): string => {
switch (question.type) {
case "multiple_choice":
case "multi_select":
return `${itemNumber}. ${question.title}\n\n${formatChoice(
question.choices,
)}`;
case "true_or_false":
case "identification":
return `_______________ ${itemNumber}. ${question.title}`;
default:
return "";
}
};

export const formatTestData = (testData: CustomTest): string => {
return testData.questions
.map((question, index) => formatQuestion(question, index + 1))
.join("\n\n");
};

export const generatePdf = async (test: CustomTest): Promise<Buffer> => {
return new Promise((resolve, reject) => {
const doc = new PDFDocument();
const buffers: Buffer[] = [];

doc.on("data", buffers.push.bind(buffers));
doc.on("end", () => {
resolve(Buffer.concat(buffers));
});
doc.on("error", reject);

doc.fontSize(16).text(test.title, { underline: true }).moveDown();
doc.fontSize(12).text(test.description).moveDown(2);

const formattedData = formatTestData(test);
doc.text(formattedData, { indent: 20, align: "left" });

doc.end();
});
};
31 changes: 31 additions & 0 deletions packages/api/src/functions/types/pdfKit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export type Keyword = {
id: string;
name: string;
};

export type Choice = {
id: string;
isCorrect: boolean;
text: string;
};

export type Question = {
id: string;
image?: string | null;
points: number;
time: number;
title: string;
type: "multiple_choice" | "true_or_false" | "multi_select" | "identification";
choices: Choice[];
};

export type CustomTest = {
id: string;
title: string;
description: string;
imageUrl?: string;
keywords: Keyword[];
createdAt: Date;
updatedAt: Date;
questions: Question[];
};
2 changes: 2 additions & 0 deletions packages/api/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { reviewerRouter } from "./reviewer";
import { algoliaSearch } from "./algoliaSearch";
import { textExtractionRouter } from "./pdfTextExtraction";
import { testHistoryRouter } from "./testHistory";
import { pdfKitRouter } from "./pdfKit";

export const appRouter = router({
auth: authRouter,
Expand All @@ -25,6 +26,7 @@ export const appRouter = router({
algolia: algoliaSearch,
pdfTextExtraction: textExtractionRouter,
testHistory: testHistoryRouter,
pdfKit: pdfKitRouter,
});

// export type definition of API
Expand Down
57 changes: 57 additions & 0 deletions packages/api/src/router/pdfKit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { z } from "zod";
import { protectedProcedure, router } from "../trpc";
import { generatePdf } from "../functions/pdfKitHandlers";

export const pdfKitRouter = router({
generatePdfByTestId: protectedProcedure
.input(z.string())
.mutation(async ({ ctx, input }) => {
const testData = await ctx.prisma.test.findUnique({
where: {
id: input,
},
select: {
id: true,
title: true,
description: true,
imageUrl: true,
keywords: {
select: {
id: true,
name: true,
},
},
createdAt: true,
updatedAt: true,
questions: {
select: {
choices: {
select: {
id: true,
isCorrect: true,
text: true,
},
},
id: true,
image: true,
points: true,
time: true,
title: true,
type: true,
},
},
},
});

try {
if (!testData) {
throw new Error("Test data not found");
}
const pdfBuffer = await generatePdf(testData);

return { pdfBuffer: pdfBuffer.toString("base64") };
} catch (error) {
throw new Error("PDF generation failed");
}
}),
});
Loading

1 comment on commit 48de83c

@vercel
Copy link

@vercel vercel bot commented on 48de83c Dec 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

test-trek-backend – ./apps/nextjs

test-trek-backend.vercel.app
test-trek-backend-git-main-dadili-test-trek.vercel.app
test-trek-backend-dadili-test-trek.vercel.app

Please sign in to comment.