mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-17 00:30:05 +10:00
feat: add PDF download endpoint and export createLocalizedResumeDocument function
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import type { Template } from "@reactive-resume/schema/templates";
|
||||
import { renderToBuffer } from "@react-pdf/renderer";
|
||||
import { createLocalizedResumeDocument } from "./pdf-document";
|
||||
|
||||
export const createResumePdfFile = async (data: ResumeData, filename: string, template?: Template) => {
|
||||
const document = await createLocalizedResumeDocument(data, template);
|
||||
const buffer = await renderToBuffer(document);
|
||||
const bytes = new Uint8Array(new ArrayBuffer(buffer.byteLength));
|
||||
bytes.set(buffer);
|
||||
|
||||
return new File([bytes], filename, { type: "application/pdf" });
|
||||
};
|
||||
@@ -21,7 +21,7 @@ export const useLocalizedResumeDocument = (data?: ResumeData, template?: Templat
|
||||
}, [data, template, sectionTitleResolver]);
|
||||
};
|
||||
|
||||
const createLocalizedResumeDocument = async (data: ResumeData, template?: Template) => {
|
||||
export const createLocalizedResumeDocument = async (data: ResumeData, template?: Template) => {
|
||||
const sectionTitleResolver = await createSectionTitleResolverForLocale(data.metadata.page.locale);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ORPCError } from "@orpc/server";
|
||||
import z from "zod";
|
||||
import { protectedProcedure } from "@reactive-resume/api/context";
|
||||
import { resumeService } from "@reactive-resume/api/services/resume";
|
||||
import { generateFilename } from "@reactive-resume/utils/file";
|
||||
import { createResumePdfFile } from "@/libs/resume/pdf-document.server";
|
||||
|
||||
export const downloadResumePdfProcedure = protectedProcedure
|
||||
.route({
|
||||
method: "GET",
|
||||
path: "/resumes/{id}/pdf",
|
||||
tags: ["Resumes"],
|
||||
operationId: "downloadResumePdf",
|
||||
summary: "Download resume as PDF",
|
||||
description:
|
||||
"Generates a PDF for the specified resume and returns it as a forced download. Only resumes belonging to the authenticated user can be downloaded. Requires authentication.",
|
||||
successDescription: "The generated resume PDF.",
|
||||
outputStructure: "detailed",
|
||||
})
|
||||
.input(z.object({ id: z.string().describe("The ID of the resume.") }))
|
||||
.output(
|
||||
z.object({
|
||||
headers: z.object({
|
||||
"content-disposition": z.string(),
|
||||
}),
|
||||
body: z.file().mime("application/pdf"),
|
||||
}),
|
||||
)
|
||||
.handler(async ({ context, input }) => {
|
||||
const resume = await resumeService.getById({ id: input.id, userId: context.user.id });
|
||||
const filename = generateFilename(resume.name, "pdf");
|
||||
|
||||
try {
|
||||
const body = await createResumePdfFile(resume.data, filename);
|
||||
|
||||
return {
|
||||
headers: {
|
||||
"content-disposition": `attachment; filename="${filename}"`,
|
||||
},
|
||||
body,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[PDF API] Failed to render resume PDF", { resumeId: input.id, error });
|
||||
throw new ORPCError("INTERNAL_SERVER_ERROR", { message: "Failed to generate resume PDF" });
|
||||
}
|
||||
});
|
||||
@@ -9,9 +9,18 @@ import router from "@reactive-resume/api/routers";
|
||||
import { env } from "@reactive-resume/env/server";
|
||||
import { resumeDataSchema } from "@reactive-resume/schema/resume/data";
|
||||
import { getLocale } from "@/libs/locale";
|
||||
import { downloadResumePdfProcedure } from "./-helpers/resume-pdf";
|
||||
|
||||
const openAPIRouter = {
|
||||
...router,
|
||||
resume: {
|
||||
...router.resume,
|
||||
downloadPdf: downloadResumePdfProcedure,
|
||||
},
|
||||
};
|
||||
|
||||
async function handler({ request }: { request: Request }) {
|
||||
const openAPIHandler = new OpenAPIHandler(router, {
|
||||
const openAPIHandler = new OpenAPIHandler(openAPIRouter, {
|
||||
plugins: [
|
||||
new BatchHandlerPlugin(),
|
||||
new RequestHeadersPlugin(),
|
||||
@@ -34,7 +43,7 @@ async function handler({ request }: { request: Request }) {
|
||||
const locale = await getLocale();
|
||||
|
||||
if (request.method === "GET" && (request.url.endsWith("/spec.json") || request.url.endsWith("/spec"))) {
|
||||
const spec = await openAPIGenerator.generate(router, {
|
||||
const spec = await openAPIGenerator.generate(openAPIRouter, {
|
||||
info: {
|
||||
title: "Reactive Resume",
|
||||
version: __APP_VERSION__,
|
||||
|
||||
+25
-1
@@ -1730,7 +1730,8 @@
|
||||
"description": "Metadata for the resume, such as template, layout, typography, etc. This section describes the overall design and appearance of the resume."
|
||||
}
|
||||
},
|
||||
"required": ["picture", "basics", "summary", "sections", "customSections", "metadata"]
|
||||
"required": ["picture", "basics", "summary", "sections", "customSections", "metadata"],
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3121,6 +3122,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/resumes/{id}/pdf": {
|
||||
"get": {
|
||||
"operationId": "downloadResumePdf",
|
||||
"summary": "Download resume as PDF",
|
||||
"description": "Generates a PDF for the specified resume and returns it as a forced download. Only resumes belonging to the authenticated user can be downloaded. Requires authentication.",
|
||||
"tags": ["Resumes"],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string", "description": "The ID of the resume." }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The generated resume PDF.",
|
||||
"headers": { "content-disposition": { "schema": { "type": "string" }, "required": true } },
|
||||
"content": { "application/pdf": { "schema": { "type": "string", "contentMediaType": "application/pdf" } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/statistics/users": {
|
||||
"get": {
|
||||
"operationId": "getUserCount",
|
||||
|
||||
Reference in New Issue
Block a user