Files
classroomio/apps/api/src/utils/course.ts
T
Rotimi Best b96d91cd7c feat: setup drizzle (#594)
* chore: migration to SvelteKit 2

* stage 1: migrate to svelte 5 and fix rerendering issues

* stage 2: fix more $: reactivity across dashboard

* fix merge conflicts

* chore: migrate code in main to svelte 5

* chore: update preventDefault

* chore: simplify tsconfig and remove unused mobileMenuRef in MobileMenu.svelte

* chore: enforce .ts across dashboard

* rename from classroomio-com to website

* stage-3: Replace carbon icon svelte with lucide icon

* update lock file

* chore: finish icon replacement

* fix: settings and setup page multiple rerender

* stage 3: More icon and rerendering fixes

* feat: setup drizzle and better auth

* fix: onboarding validation error showing translation key #586 (#593)

* fix: onboarding validation error showing translation key #586

* fix: remove debug log for profile update errors

* chore: add a clean command

* fix: remove message from email verification response
2025-11-01 15:49:29 +01:00

172 lines
4.6 KiB
TypeScript

import type { TCourseDownloadContent } from '@api/types/course';
import { getCloudflarePdfBuffer } from '@api/utils/cloudflare';
import { marked } from 'marked';
function getHtmlTemplate(body: string): string {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100;200;300;400&family=Roboto:wght@100;300;400;500;700;900&display=swap"
rel="stylesheet" />
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp"></script>
<title>Lesson Note</title>
<style>
html {
-webkit-print-color-adjust: exact;
}
body {
font-family: 'Roboto', sans-serif;
}
.prose h1 {
margin: 0;
}
.prose h2 {
margin: 0.5rem 0 1.5rem 0;
}
.prose h3 {
margin: 0.5rem 0;
}
.roboto-mono {
font-family: 'Roboto Mono', monospace;
}
span {
font-size: 0.8rem;
}
section {
margin-top: 5%;
}
.root {
width: 95%;
}
</style>
<style type="text/tailwindcss">
@tailwind base;
@tailwind base;
@tailwind components;
@tailwind utilities;
.prose {
max-width: unset;
}
</style>
</head>
<body class="w-full break-after-page">
${body}
</body>
</html>
`;
}
type TLessonBody = TCourseDownloadContent['lessons'][0];
function getLessonBody(
{ lessonTitle, lessonNumber, lessonNote, slideUrl, video }: TLessonBody,
orgName: string,
orgTheme: string
): string {
const noteHtml = marked.parse(lessonNote);
const showExtraResources = slideUrl || (video && video.length > 0);
return `
<div
class="bg-[${orgTheme ? orgTheme : '#0030FF'}] w-full h-40 flex flex-col items-center justify-center gap-2 rounded-lg m-0">
<h3
class="my-0 rounded-full w-10 text-center mx-auto py-1 text-[${orgTheme ? orgTheme : '#0030FF'}] bg-white text-md font-bold tracking-tighter">
${lessonNumber}
</h3>
<h1 class="text-white m-0">${lessonTitle}</h1>
<h4 class="roboto-mono text-md font-normal text-white m-0">By ${orgName}</h4>
</div>
<main class="break-after-page">
<section>
${noteHtml}
</section>
<section>
${
showExtraResources
? `
<section>
<h1 class='text-xl font-bold my-1'>Extra Resources</h1>
${
slideUrl
? `<div class="flex mt-2 mb-1">
<p class="m-0 mr-2">Slide Link:</p> <a href=${slideUrl} style="color: blue" class="underline">Open Here</a>
</div>`
: ''
}
${
video && video.length > 0
? `
<p class="m-0">Video Link:</p>
<div class="m-0">
<ol class="m-0">
${video
.map(
(videoData, index) => `
<li key=${
index + 1
}><a href=${videoData} style="color: blue" class="underline">${videoData}</a></li>
`
)
.join('')}
</ol>
</div>
`
: ''
}
</section>
`
: ''
}
</section>
</main>
`;
}
function getCourseBody({
courseTitle,
orgName,
orgTheme,
lessons
}: TCourseDownloadContent): string {
return `
<header class="flex justify-center items-center flex-col border-[${orgTheme ? orgTheme : '#0030FF'}] border-[40px] mx-3 h-[100vh] m-0">
<h1 class="font-bold text-8xl text-black m-0 mx-96">${courseTitle}</h1>
<h4 class="text-md text-2xl font-light text-black mt-5 ">${orgName}</h4>
</header>
<div class="root mx-auto mt-5 prose break-after-page">
${lessons.map((lesson) => getLessonBody(lesson, orgName, orgTheme)).join('')}
</div>
`;
}
async function generateSinglePdfFromHtml(
htmlContent: string,
courseTitle: string,
orgTheme: string
): Promise<Buffer> {
return await getCloudflarePdfBuffer(htmlContent);
}
export async function generateCoursePdf(params: TCourseDownloadContent): Promise<Buffer> {
const body = getCourseBody(params);
const html = getHtmlTemplate(body);
console.log(html);
return await generateSinglePdfFromHtml(html, params.courseTitle, params.orgTheme);
}