feat: add classroomio's server to the monorepo (#168)
* add classroomio server to apps * ReadME upadte * Update README.md * rename project to backend * fix lock file --------- Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
co-authored by
Rotimi Best
parent
d040f9f12e
commit
3a22a73380
@@ -76,8 +76,9 @@ Here is what you need to be able to run ClassroomIO.com
|
||||
This repo is a mono repo that consists of 3 projects:
|
||||
|
||||
1. `classroomio-com`: The landing page of ClassroomIO hosted [here](https://classroomio.com)
|
||||
2. `dashboard`: The web application that runs the learning management system.
|
||||
3. `docs`: Official documentation of ClassroomIO hosted [here](https://classroomio.com/docs)
|
||||
2. `backend`: The backend service that handles PDF & video processing.
|
||||
3. `dashboard`: The web application that runs the learning management system hosted [here](https://app.classroomio.com).
|
||||
4. `docs`: Official documentation of ClassroomIO hosted [here](https://classroomio.com/docs)
|
||||
|
||||
## Development
|
||||
|
||||
@@ -173,12 +174,14 @@ This repo is a mono repo that consists of 3 projects:
|
||||
8. All projects should start running
|
||||
|
||||
- `classroomio-com`: [http://localhost:5173](http://localhost:5173)
|
||||
- `backend`: [http://localhost:3002](http://localhost:3002)
|
||||
- `dashboard`: [http://localhost:5174](http://localhost:5174)
|
||||
- `docs`: [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
9. Running a specific project
|
||||
|
||||
- **classroomio-com**: `pnpm dev --filter=classroomio-com`
|
||||
- **backend**: `pnpm dev --filter=backend`
|
||||
- **dashboard**: `pnpm dev --filter=dashboard`
|
||||
- **docs**: `pnpm dev --filter=docs`
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.env
|
||||
.DS_Store
|
||||
@@ -0,0 +1,37 @@
|
||||
FROM node:slim AS app
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
||||
# Install chrome stable from sources, then remove it. Why? The
|
||||
# npm install of `puppeteer` brings its own bundle chromium build,
|
||||
# and puppeteer releases are only guaranteed to work with that version.
|
||||
#
|
||||
# But the bundled chromium implicitly needs a bunch of shared libs on
|
||||
# the host. It's a little tedious to find and maintain that list; but
|
||||
# the official apt distro of `google-chrome-stable` should bring the right
|
||||
# set along. So do that, but immediately uninstall (to free up layer space).
|
||||
#
|
||||
# This is a little brittle, since the puppeteer chrome could in theory diverge
|
||||
# from the official apt chrome's shared lib deps. But it works..
|
||||
RUN apt-get update \
|
||||
&& apt-get install curl gnupg -y \
|
||||
&& curl --location --silent https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
|
||||
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
|
||||
&& apt-get update \
|
||||
&& apt-get install google-chrome-stable -y --no-install-recommends \
|
||||
&& apt-get remove google-chrome-stable -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add emoji fonts.
|
||||
# Source: https://gist.github.com/win0err/9d8c7f0feabdfe8a4c9787b02c79ac51
|
||||
RUN mkdir ~/.fonts/ && \
|
||||
wget https://github.com/samuelngs/apple-emoji-linux/releases/download/ios-15.4/AppleColorEmoji.ttf -O ~/.fonts/AppleColorEmoji.ttf
|
||||
|
||||
COPY package.json /app/
|
||||
RUN npm install --frozen-lockfile
|
||||
COPY ./ /app/
|
||||
|
||||
ENV PORT 3002
|
||||
EXPOSE 3002
|
||||
CMD ["node", "server.js"]
|
||||
@@ -0,0 +1,10 @@
|
||||
# ClassroomIO Backend
|
||||
|
||||
## Supported Features
|
||||
|
||||
1. Certificate download for students.
|
||||
2. Uploading videos to cloudflare (for storage) and Muse.ai (for transcription)
|
||||
|
||||
## Debugging Fly.io
|
||||
|
||||
View logs from fly.io in real time
|
||||
@@ -0,0 +1,98 @@
|
||||
<!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=Qwitcher+Grypen&family=Roboto:wght@300;400;700&display=swap"
|
||||
rel="stylesheet" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<title>Certificate</title>
|
||||
<style>
|
||||
body {
|
||||
width: 1123px;
|
||||
height: 794px;
|
||||
}
|
||||
|
||||
.student-name {
|
||||
font-family: 'Qwitcher Grypen', cursive;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="flex items-center justify-center border border-dashed border-blue-400 h-full w-full">
|
||||
<div class="h-full flex border-l-8 border-blue-800">
|
||||
<div class="h-full w-full px-5 py-10">
|
||||
<p class="text-2xl font-normal my-2 mb-10 uppercase text-gray-500">
|
||||
Certificate<br /> of completion
|
||||
</p>
|
||||
<div class="border-b border-gray-500 mb-4">
|
||||
<p class="text-xl text-black font-normal">This is to certify that</p>
|
||||
<p class="student-name text-center text-9xl">Rotimi Best</p>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<p class="text-xl text-black font-normal">
|
||||
has successfully completed introductory courses in
|
||||
</p>
|
||||
<div>
|
||||
<p class="bg-transparent font-semibold text-blue-800 text-center uppercase my-10 text-3xl">
|
||||
Figma for Beginners
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xl font-light mb-4 text-justify">Netlify Functions can stream data to clients as it becomes
|
||||
available, rather
|
||||
than returning a buffered payload at the end of the computation. This lets developers and frameworks create
|
||||
faster experiences by using streaming and partial hydration to get content and interactions in front of people
|
||||
as quickly as possible. For an extended example that demonstrates a more likely response streaming use case,
|
||||
consider this function</p>
|
||||
<div class="flex items-center gap-1 mt-14">
|
||||
<img
|
||||
src="https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/user/Coding%20Dojo1684164720355.webp"
|
||||
alt="logo" class="w-24 h-24 rounded-md" />
|
||||
<p class="font-semibold capitalize text-5xl">Coding Dojo</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="flex flex-col items-center justify-center border-4 border-pink-700 py-3 w-full h-full">
|
||||
<div class="flex items-center gap-1 my-2 mb-10">
|
||||
<img
|
||||
src="https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/user/Coding%20Dojo1684164720355.webp"
|
||||
alt="logo" class=" w-24 h-24 rounded-md" />
|
||||
<p class="font-semibold capitalize text-5xl">Coding Dojo</p>
|
||||
</div>
|
||||
<div class="w-full px-5 py-3">
|
||||
<p class="text-xl text-center font-normal my-2 uppercase tracking-widest text-gray-500 w-full">
|
||||
Certificate of completion
|
||||
</p>
|
||||
<div class="border-b border-gray-500 mb-4">
|
||||
<p class="text-xl text-black font-normal">This is to certify that</p>
|
||||
<p class="student-name text-center text-8xl">Rotimi Best</p>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<p class="text-xl text-black font-normal">
|
||||
has successfully completed a courses in
|
||||
</p>
|
||||
<div class="my-10">
|
||||
<p class="bg-transparent font-semibold text-pink-700 text-center uppercase text-3xl">
|
||||
Figma for Beginners
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xl text-center text-gray-500 font-light">Netlify Functions can stream data to clients as it becomes
|
||||
available,
|
||||
rather than
|
||||
returning a buffered payload at the end of the computation. This lets developers and frameworks create faster
|
||||
experiences by using streaming and partial hydration to get content and interactions in front of people as
|
||||
quickly as possible. For an extended example that demonstrates a more likely response streaming use case,
|
||||
consider this function</p>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,182 @@
|
||||
<!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;
|
||||
}
|
||||
|
||||
header {
|
||||
border-color: rgb(0, 48, 255);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background-color: rgb(0, 48, 255);
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
margin: 0.5rem 0 3rem 0;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-top: 5%;
|
||||
}
|
||||
|
||||
.root {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 3rem;
|
||||
scale: 3;
|
||||
}
|
||||
</style>
|
||||
<style type="text/tailwindcss">
|
||||
@tailwind base;
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.prose {
|
||||
max-width: unset;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="w-full">
|
||||
<header class="flex justify-center items-center flex-col border-[50px] mx-10 h-[100vh] m-0">
|
||||
<h1 class="font-bold text-8xl text-black m-0 mx-96">React js for Beginners</h1>
|
||||
<h4 class="text-md text-2xl font-light text-black mt-5 ">By TheSpace</h4>
|
||||
</header>
|
||||
|
||||
<div class="root mx-auto mt-5 prose">
|
||||
|
||||
<div class="hero 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-blue-700 bg-white text-md font-bold tracking-tighter">
|
||||
32
|
||||
</h3>
|
||||
<h1 class="font-bold text-5xl text-white m-0">The Yams</h1>
|
||||
<h4 class="text-md font-normal text-white m-0">By The Space</h4>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h1>Lesson Note: Introduction and Counselling</h1>
|
||||
<h2>Course: The Ultimate Guide to relocating to the UK via Study Path</h2>
|
||||
<h3>Lesson Overview:</h3>
|
||||
<p>This lesson serves as the introduction to the course "The Ultimate Guide to relocating to the UK via
|
||||
Study Path." In this session, we will provide an overview of the course and discuss the importance of
|
||||
effective counseling in the relocation process.</p>
|
||||
<h3>Learning Objectives:</h3>
|
||||
<p>By the end of this lesson, students should be able to:</p>
|
||||
<ul>
|
||||
<li>Understand the goals and structure of the course.</li>
|
||||
<li>Recognize the significance of counseling in the relocation process.</li>
|
||||
<li>Acquire knowledge about the necessary steps involved in relocating to the UK through the study path.</li>
|
||||
</ul>
|
||||
<h3>Lesson Content:</h3>
|
||||
<ol>
|
||||
<li>
|
||||
<p><strong>Introduction to the Course</strong></p>
|
||||
<ul>
|
||||
<li>Briefly introduce the course title and explain its relevance to students' interests.</li>
|
||||
<li>Highlight the main topics and modules covered throughout the course.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Importance of Counseling</strong></p>
|
||||
<ul>
|
||||
<li>Define the role of counseling in the relocation process.</li>
|
||||
<li>Discuss how counseling can assist students in making informed decisions regarding their studies and
|
||||
life in the UK.</li>
|
||||
<li>Explain the benefits of seeking professional guidance during the relocation journey.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Steps for Relocating to the UK via Study Path</strong></p>
|
||||
<ul>
|
||||
<li>Provide an overview of the necessary steps involved in relocating to the UK for study purposes.</li>
|
||||
<li>Discuss the importance of thorough planning, research, and documentation in the relocation process.
|
||||
</li>
|
||||
<li>Highlight key points such as visa applications, university admissions, accommodation arrangements, and
|
||||
financial considerations.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Q&A and Discussion Time</strong></p>
|
||||
<ul>
|
||||
<li>Allocate time for students to ask questions, provide comments, or share personal experiences.</li>
|
||||
<li>Encourage active participation and foster a supportive learning environment.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>Conclusion:</h3>
|
||||
<p>In this lesson, we introduced the course "The Ultimate Guide to relocating to the UK via Study
|
||||
Path" and emphasized the significance of counseling in the relocation process. We explored the essential
|
||||
steps involved in relocating to the UK and highlighted the importance of thorough planning and research.
|
||||
Students should now have a better understanding of the course objectives and overall expectations. In the next
|
||||
session, we will delve deeper into the specific requirements for studying in the UK and provide detailed
|
||||
guidance on university admissions.</p>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h1 class="text-xl font-bold my-1">Extra Resources</h1>
|
||||
<div class="flex mt-2 mb-1">
|
||||
<p class="m-0 mr-2">Slide Link:</p> <a href=https://youtu.be/dyLaKhePzj8 style="color: blue"
|
||||
class="underline">Open Here</a>
|
||||
</div>
|
||||
|
||||
<p class="m-0">Video Link:</p>
|
||||
<div class="m-0">
|
||||
<ol class="m-0">
|
||||
|
||||
<li key={index + 1}><a href=https://youtu.be/0fYi8SGA20k style="color: blue"
|
||||
class="underline">https://youtu.be/0fYi8SGA20k</a></li>
|
||||
|
||||
<li key={index + 1}><a href=https://youtu.be/0fYi8SGA20k style="color: blue"
|
||||
class="underline">https://youtu.be/0fYi8SGA20k</a></li>
|
||||
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
# fly.toml app configuration file generated for classroomio-server on 2023-07-27T21:01:09+01:00
|
||||
#
|
||||
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
||||
#
|
||||
|
||||
app = "classroomio-server"
|
||||
primary_region = "lhr"
|
||||
|
||||
[http_service]
|
||||
internal_port = 3002
|
||||
force_https = true
|
||||
auto_stop_machines = true
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
processes = ["app"]
|
||||
@@ -0,0 +1,186 @@
|
||||
<!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;
|
||||
}
|
||||
|
||||
header {
|
||||
background-image: url("https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/course/course-pdf-background.jpg");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
border-color: rgb(0, 48, 255);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background-color: rgb(0, 48, 255);
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
margin: 0.5rem 0 3rem 0;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-top: 5%;
|
||||
}
|
||||
|
||||
.root {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 3rem;
|
||||
scale: 3;
|
||||
}
|
||||
</style>
|
||||
<style type="text/tailwindcss">
|
||||
@tailwind base;
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.prose {
|
||||
max-width: unset;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="w-full">
|
||||
<header class="border-l-[50px] mb-[15%] h-[100vh] m-0 relative">
|
||||
<h1 class="font-bold text-8xl text-black m-0 w-3/4 absolute left-5 top-40">React js for Beginners</h1>
|
||||
<h4 class="text-md text-2xl font-light text-black m-0 absolute left-10 bottom-24">By TheSpace</h4>
|
||||
</header>
|
||||
|
||||
<div class="root mx-auto mt-5 prose">
|
||||
|
||||
<div class="hero 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-blue-700 bg-white text-md font-bold tracking-tighter">
|
||||
32
|
||||
</h3>
|
||||
<h1 class="font-bold text-5xl text-white m-0">The Yams</h1>
|
||||
<h4 class="text-md font-normal text-white m-0">By The Space</h4>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h1>Lesson Note: Introduction and Counselling</h1>
|
||||
<h2>Course: The Ultimate Guide to relocating to the UK via Study Path</h2>
|
||||
<h3>Lesson Overview:</h3>
|
||||
<p>This lesson serves as the introduction to the course "The Ultimate Guide to relocating to the UK via
|
||||
Study Path." In this session, we will provide an overview of the course and discuss the importance of
|
||||
effective counseling in the relocation process.</p>
|
||||
<h3>Learning Objectives:</h3>
|
||||
<p>By the end of this lesson, students should be able to:</p>
|
||||
<ul>
|
||||
<li>Understand the goals and structure of the course.</li>
|
||||
<li>Recognize the significance of counseling in the relocation process.</li>
|
||||
<li>Acquire knowledge about the necessary steps involved in relocating to the UK through the study path.</li>
|
||||
</ul>
|
||||
<h3>Lesson Content:</h3>
|
||||
<ol>
|
||||
<li>
|
||||
<p><strong>Introduction to the Course</strong></p>
|
||||
<ul>
|
||||
<li>Briefly introduce the course title and explain its relevance to students' interests.</li>
|
||||
<li>Highlight the main topics and modules covered throughout the course.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Importance of Counseling</strong></p>
|
||||
<ul>
|
||||
<li>Define the role of counseling in the relocation process.</li>
|
||||
<li>Discuss how counseling can assist students in making informed decisions regarding their studies and
|
||||
life in the UK.</li>
|
||||
<li>Explain the benefits of seeking professional guidance during the relocation journey.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Steps for Relocating to the UK via Study Path</strong></p>
|
||||
<ul>
|
||||
<li>Provide an overview of the necessary steps involved in relocating to the UK for study purposes.</li>
|
||||
<li>Discuss the importance of thorough planning, research, and documentation in the relocation process.
|
||||
</li>
|
||||
<li>Highlight key points such as visa applications, university admissions, accommodation arrangements, and
|
||||
financial considerations.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Q&A and Discussion Time</strong></p>
|
||||
<ul>
|
||||
<li>Allocate time for students to ask questions, provide comments, or share personal experiences.</li>
|
||||
<li>Encourage active participation and foster a supportive learning environment.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>Conclusion:</h3>
|
||||
<p>In this lesson, we introduced the course "The Ultimate Guide to relocating to the UK via Study
|
||||
Path" and emphasized the significance of counseling in the relocation process. We explored the essential
|
||||
steps involved in relocating to the UK and highlighted the importance of thorough planning and research.
|
||||
Students should now have a better understanding of the course objectives and overall expectations. In the next
|
||||
session, we will delve deeper into the specific requirements for studying in the UK and provide detailed
|
||||
guidance on university admissions.</p>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h1 class="text-xl font-bold my-1">Extra Resources</h1>
|
||||
<div class="flex mt-2 mb-1">
|
||||
<p class="m-0 mr-2">Slide Link:</p> <a href=https://youtu.be/dyLaKhePzj8 style="color: blue"
|
||||
class="underline">Open Here</a>
|
||||
</div>
|
||||
|
||||
<p class="m-0">Video Link:</p>
|
||||
<div class="m-0">
|
||||
<ol class="m-0">
|
||||
|
||||
<li key={index + 1}><a href=https://youtu.be/0fYi8SGA20k style="color: blue"
|
||||
class="underline">https://youtu.be/0fYi8SGA20k</a></li>
|
||||
|
||||
<li key={index + 1}><a href=https://youtu.be/0fYi8SGA20k style="color: blue"
|
||||
class="underline">https://youtu.be/0fYi8SGA20k</a></li>
|
||||
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Generated
+4404
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "hellonode",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/fly-examples/hellonode.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/fly-examples/hellonode/issues"
|
||||
},
|
||||
"homepage": "https://github.com/fly-examples/hellonode#readme",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.388.0",
|
||||
"@supabase/supabase-js": "^2.32.0",
|
||||
"axios": "^1.4.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"bull": "^4.11.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^6.9.0",
|
||||
"form-data": "^4.0.0",
|
||||
"marked": "^7.0.3",
|
||||
"multer": "^1.4.3",
|
||||
"nodemon": "^3.0.1",
|
||||
"puppeteer": "^20.9.0",
|
||||
"zod": "^3.21.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const cors = require('cors');
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
const uploadVideo = require('./src/routes/uploadVideo');
|
||||
const downloadCertificate = require('./src/routes/downloadCertificate');
|
||||
const downloadLesson = require('./src/routes/downloadLesson');
|
||||
const downloadCourse = require('./src/routes/downloadCourse');
|
||||
|
||||
// Express server
|
||||
const app = express();
|
||||
|
||||
const port = process.env.PORT || 3002;
|
||||
|
||||
app.use(express.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
app.use(cors());
|
||||
|
||||
// Rate limiting middleware
|
||||
const limiter = rateLimit({
|
||||
windowMs: 1000,
|
||||
max: 10,
|
||||
});
|
||||
|
||||
app.use(limiter);
|
||||
|
||||
app.use('/downloadCertificate', downloadCertificate);
|
||||
app.use('/downloadLesson', downloadLesson);
|
||||
app.use('/downloadCourse', downloadCourse);
|
||||
app.use('/uploadVideo', uploadVideo);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send('Hello ClassroomIO');
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`HelloNode app listening on port ${port}!`);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
const express = require('express');
|
||||
const zod = require('zod');
|
||||
const { generateCertificate } = require('../utils/certificate');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
// Validate
|
||||
const mySchema = zod.object({
|
||||
theme: zod.string(),
|
||||
studentName: zod.string(),
|
||||
courseName: zod.string(),
|
||||
courseDescription: zod.string(),
|
||||
orgLogoUrl: zod.string(),
|
||||
orgName: zod.string(),
|
||||
});
|
||||
console.log('req.body', req.body);
|
||||
|
||||
mySchema.parse(req.body);
|
||||
|
||||
const pdfBuffer = await generateCertificate(req.body);
|
||||
|
||||
res.contentType('application/pdf');
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
console.error('Error', error);
|
||||
res.json({
|
||||
success: false,
|
||||
error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,42 @@
|
||||
const express = require('express');
|
||||
const zod = require('zod');
|
||||
const { generateCoursePdf } = require('../utils/course.js');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// localhost:3002/downloadCourse
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
// Validate
|
||||
const mySchema = zod.object({
|
||||
courseTitle: zod.string(),
|
||||
orgName: zod.string(),
|
||||
orgTheme: zod.string(),
|
||||
lessons: zod.array(
|
||||
zod.object({
|
||||
lessonTitle: zod.string(),
|
||||
lessonNumber: zod.string(),
|
||||
lessonNote: zod.string(),
|
||||
slideUrl: zod.string().optional(),
|
||||
video: zod.string().array().optional(),
|
||||
})
|
||||
),
|
||||
});
|
||||
console.log('req.body', req.body);
|
||||
|
||||
mySchema.parse(req.body);
|
||||
|
||||
const pdfBuffer = await generateCoursePdf(req.body);
|
||||
|
||||
res.contentType('application/pdf');
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
console.error('Error', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,47 @@
|
||||
const express = require('express');
|
||||
const zod = require('zod');
|
||||
const { generateLessonPdf } = require('../utils/lesson');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const lesson = {
|
||||
title: '',
|
||||
number: '01',
|
||||
orgName: '',
|
||||
note: '',
|
||||
slideUrl: '',
|
||||
video: [],
|
||||
courseTitle: '',
|
||||
};
|
||||
|
||||
// localhost:3002/downloadLesson
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
// Validate
|
||||
const mySchema = zod.object({
|
||||
title: zod.string(),
|
||||
number: zod.string(),
|
||||
orgName: zod.string(),
|
||||
note: zod.string(),
|
||||
slideUrl: zod.string().optional(),
|
||||
video: zod.string().array().optional(),
|
||||
courseTitle: zod.string(),
|
||||
});
|
||||
console.log('req.body', req.body);
|
||||
|
||||
mySchema.parse(req.body);
|
||||
|
||||
const pdfBuffer = await generateLessonPdf(req.body);
|
||||
|
||||
res.contentType('application/pdf');
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
console.error('Error', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,251 @@
|
||||
const express = require('express');
|
||||
const FormData = require('form-data');
|
||||
const axios = require('axios');
|
||||
// const Queue = require('bull');
|
||||
const { promisify } = require('util');
|
||||
const fs = require('fs');
|
||||
|
||||
const unlinkAsync = promisify(fs.unlink);
|
||||
|
||||
const {
|
||||
S3Client,
|
||||
PutObjectCommand,
|
||||
// GetObjectCommand,
|
||||
} = require('@aws-sdk/client-s3');
|
||||
|
||||
// const { getSupabase } = require('../utils/supabase');
|
||||
const genUniqueId = require('../utils/genUniqueId');
|
||||
const upload = require('../utils/upload');
|
||||
|
||||
const {
|
||||
CLOUDFLARE_ACCESS_KEY,
|
||||
CLOUDFLARE_SECRET_ACCESS_KEY,
|
||||
CLOUDFLARE_ACCOUNT_ID,
|
||||
CLOUDFLARE_PUBLIC_ACCOUNT_ID,
|
||||
MUSE_API_KEY,
|
||||
} = process.env;
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const S3 = new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: CLOUDFLARE_ACCESS_KEY,
|
||||
secretAccessKey: CLOUDFLARE_SECRET_ACCESS_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
// const supabase = getSupabase();
|
||||
|
||||
// const museUploadQueue = new Queue('video', process.env.REDIS_API_KEY, {
|
||||
// redis: { tls: { rejectUnauthorized: false } },
|
||||
// });
|
||||
|
||||
router.post('/', upload.single('videoFile'), async (req, res) => {
|
||||
const {
|
||||
file,
|
||||
query: { lessonId },
|
||||
} = req;
|
||||
console.log('lessonId', lessonId);
|
||||
console.log('file', file);
|
||||
|
||||
if (!file?.fieldname || !file?.fieldname) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'You must provide a file to upload',
|
||||
});
|
||||
}
|
||||
|
||||
if (file?.size > 30 * 1024 * 1024) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
type: 'FILE_TOO_LARGE',
|
||||
message: 'File is too large',
|
||||
});
|
||||
}
|
||||
const fileData = fs.readFileSync(file.path);
|
||||
|
||||
const fileName = `${genUniqueId()}-${file.originalname}`;
|
||||
const params = {
|
||||
Bucket: 'videos',
|
||||
Key: fileName,
|
||||
Body: fileData,
|
||||
};
|
||||
|
||||
const fileUrl = `https://pub-${CLOUDFLARE_PUBLIC_ACCOUNT_ID}.r2.dev/${fileName}`;
|
||||
let metadata = {};
|
||||
|
||||
try {
|
||||
const s3Upload = S3.send(new PutObjectCommand(params));
|
||||
const museUpload = uploadToMuseNow(fileData, file.originalname);
|
||||
|
||||
Promise.all([s3Upload, museUpload]).then((values) => {
|
||||
const [s3UploadRes, museUploadRes] = values;
|
||||
|
||||
// Delete file saved by mutler in file
|
||||
unlinkAsync(file.path);
|
||||
|
||||
console.log('s3UploadRes', s3UploadRes);
|
||||
console.log('museUploadRes', museUploadRes);
|
||||
|
||||
if (museUploadRes.metadata) {
|
||||
metadata = museUploadRes.metadata;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
url: fileUrl,
|
||||
metadata,
|
||||
message: 'Uploaded successfully',
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
// error handling.
|
||||
const { requestId, cfId, extendedRequestId } = error.$$metadata || {};
|
||||
|
||||
console.error('Error uploading file', error);
|
||||
console.log({ requestId, cfId, extendedRequestId });
|
||||
|
||||
res.json({
|
||||
success: false,
|
||||
message: 'Uploaded failed: ' + error,
|
||||
});
|
||||
} finally {
|
||||
// finally.
|
||||
console.log('File upload complete');
|
||||
|
||||
// console.log('Add muse queue');
|
||||
// museUploadQueue.add({
|
||||
// fileName,
|
||||
// originalFileName: file.originalname,
|
||||
// lessonId,
|
||||
// fileUrl,
|
||||
// unlinkPath: file.path,
|
||||
// });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.send('Get upload video');
|
||||
});
|
||||
|
||||
async function uploadToMuseNow(fileData, fileName) {
|
||||
try {
|
||||
// Add file data to form
|
||||
const form = new FormData();
|
||||
form.append('file', fileData, fileName);
|
||||
|
||||
// Send to muse through axios - fetch is a pain in the ass
|
||||
const response = await axios({
|
||||
method: 'POST',
|
||||
url: 'https://api.muse.ai/api/files/upload',
|
||||
data: form,
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity,
|
||||
headers: {
|
||||
Key: MUSE_API_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
metadata: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log('Upload to Muse failed', error?.response?.data);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Uploads video to muse.ai and store response in DB
|
||||
// *
|
||||
// * @param {Blod} s3Data AWS s3 data
|
||||
// * @param {string} fileName Name of the file
|
||||
// * @param {string} lessonId Associated lesson this video was uploaded to
|
||||
// * @param {string} fileUrl URL to the video in s3
|
||||
// * @param {string} unlinkPath Path to mutler file to delete
|
||||
// */
|
||||
// async function uploadToMuse(s3Key, fileName, lessonId, fileUrl, unlinkPath) {
|
||||
// try {
|
||||
// await unlinkAsync(unlinkPath);
|
||||
|
||||
// const s3Data = await S3.send(
|
||||
// new GetObjectCommand({
|
||||
// Bucket: 'videos',
|
||||
// Key: s3Key,
|
||||
// })
|
||||
// );
|
||||
// // Add file data to form
|
||||
// const form = new FormData();
|
||||
// form.append('file', s3Data.Body, fileName);
|
||||
|
||||
// // Send to muse through axios - fetch is a pain in the ass
|
||||
// const response = await axios({
|
||||
// method: 'POST',
|
||||
// url: 'https://api.muse.ai/api/files/upload',
|
||||
// data: form,
|
||||
// maxContentLength: Infinity,
|
||||
// maxBodyLength: Infinity,
|
||||
// headers: {
|
||||
// Key: MUSE_API_KEY,
|
||||
// },
|
||||
// });
|
||||
|
||||
// console.log('Upload to muse complete', response.data);
|
||||
// const { data: lesson } = await supabase
|
||||
// .from('lesson')
|
||||
// .select(`*`)
|
||||
// .eq('id', lessonId)
|
||||
// .single();
|
||||
|
||||
// if (lesson) {
|
||||
// console.log('Found lesson');
|
||||
// // Add Muse response to metadata
|
||||
// const videos = lesson.videos.map((v) => {
|
||||
// if (v.link === fileUrl) {
|
||||
// console.log('matches');
|
||||
// v.metadata = JSON.parse(JSON.stringify(response.data));
|
||||
// }
|
||||
// return v;
|
||||
// });
|
||||
|
||||
// // Save uploaded data to DB
|
||||
// const { error } = await supabase
|
||||
// .from('lesson')
|
||||
// .update({ videos })
|
||||
// .eq('id', lessonId)
|
||||
// .select();
|
||||
// if (error) {
|
||||
// console.log('Error updating lesson', error);
|
||||
// } else {
|
||||
// console.log('Update DB complete');
|
||||
// }
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.log('Upload to Muse failed', error?.response?.data);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Queue to Upload to muse.ai
|
||||
// museUploadQueue.process(async function (job, done) {
|
||||
// console.log('Initilizing Muse Request\n\n', job.data);
|
||||
// const { fileName, originalFileName, lessonId, fileUrl, unlinkPath } =
|
||||
// job.data;
|
||||
|
||||
// await uploadToMuse(fileName, originalFileName, lessonId, fileUrl, unlinkPath);
|
||||
|
||||
// done();
|
||||
// });
|
||||
|
||||
// museUploadQueue.on('completed', function (job) {
|
||||
// // Job completed with output result!
|
||||
// console.log('Completed all jobs in uploading to muse', job.data);
|
||||
// });
|
||||
|
||||
// museUploadQueue.on('error', function (error) {
|
||||
// // Job completed with output result!
|
||||
// console.log('error on queue', error);
|
||||
// });
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,148 @@
|
||||
const { getPdfBuffer } = require('./puppeteer');
|
||||
|
||||
function getHtmlTemplate(body) {
|
||||
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=Qwitcher+Grypen&family=Roboto:wght@300;400;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<title>Certificate</title>
|
||||
<style>
|
||||
body {
|
||||
width: 1123px;
|
||||
height: 794px;
|
||||
}
|
||||
|
||||
.student-name {
|
||||
font-family: 'Qwitcher Grypen', cursive;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${body}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
function getProfessionalTheme({
|
||||
studentName,
|
||||
courseName,
|
||||
courseDescription,
|
||||
orgLogoUrl,
|
||||
orgName,
|
||||
}) {
|
||||
const themeBody = `
|
||||
<div class="flex items-center justify-center border border-dashed border-blue-400 h-full w-full">
|
||||
<div class="w-full h-full flex border-l-8 border-blue-800">
|
||||
<div class="h-full w-full px-5 py-10">
|
||||
<p class="text-2xl font-normal my-2 mb-10 uppercase text-gray-500">
|
||||
Certificate<br /> of completion
|
||||
</p>
|
||||
<div class="border-b border-gray-500 mb-4">
|
||||
<p class="text-xl text-black font-normal">This is to certify that</p>
|
||||
<p class="student-name text-center text-9xl">${studentName}</p>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<p class="text-xl text-black font-normal">
|
||||
has successfully completed introductory courses in
|
||||
</p>
|
||||
<div>
|
||||
<p class="bg-transparent font-semibold text-blue-800 text-center uppercase my-10 text-3xl">
|
||||
${courseName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xl font-light mb-4 text-justify">
|
||||
${courseDescription}
|
||||
</p>
|
||||
<div class="flex items-center gap-1 mt-14">
|
||||
<img
|
||||
src="${orgLogoUrl}"
|
||||
alt="logo" class="w-24 h-24 rounded-md" />
|
||||
<p class="font-semibold capitalize text-5xl">${orgName}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return getHtmlTemplate(themeBody);
|
||||
}
|
||||
|
||||
function getPlainTheme({
|
||||
studentName,
|
||||
courseName,
|
||||
courseDescription,
|
||||
orgLogoUrl,
|
||||
orgName,
|
||||
}) {
|
||||
const themeBody = `
|
||||
<div class="flex flex-col items-center justify-center border-4 border-pink-700 py-3 w-full h-full">
|
||||
<div class="flex items-center gap-1 my-2 mb-10">
|
||||
<img
|
||||
src="${orgLogoUrl}"
|
||||
alt="logo" class=" w-24 h-24 rounded-md" />
|
||||
<p class="font-semibold capitalize text-5xl">${orgName}</p>
|
||||
</div>
|
||||
<div class="w-full px-5 py-3">
|
||||
<p class="text-xl text-center font-normal my-2 uppercase tracking-widest text-gray-500 w-full">
|
||||
Certificate of completion
|
||||
</p>
|
||||
<div class="border-b border-gray-500 mb-4">
|
||||
<p class="text-xl text-black font-normal">This is to certify that</p>
|
||||
<p class="student-name text-center text-8xl">${studentName}</p>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<p class="text-xl text-black font-normal">
|
||||
has successfully completed a courses in
|
||||
</p>
|
||||
<div class="my-10">
|
||||
<p class="bg-transparent font-semibold text-pink-700 text-center uppercase text-3xl">
|
||||
${courseName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xl text-center text-gray-500 font-light">${courseDescription}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return getHtmlTemplate(themeBody);
|
||||
}
|
||||
|
||||
const generateCertificate = async ({
|
||||
theme,
|
||||
studentName,
|
||||
courseName,
|
||||
courseDescription,
|
||||
orgLogoUrl,
|
||||
orgName,
|
||||
}) => {
|
||||
const params = {
|
||||
studentName,
|
||||
courseName,
|
||||
courseDescription,
|
||||
orgLogoUrl,
|
||||
orgName,
|
||||
};
|
||||
console.log('theme', theme);
|
||||
|
||||
const html =
|
||||
theme === 'plain' ? getPlainTheme(params) : getProfessionalTheme(params);
|
||||
console.log('body', html);
|
||||
|
||||
return await getPdfBuffer(html);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generateCertificate,
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
// const { getPdfBuffer } = require('./puppeteer');
|
||||
const { marked } = require('marked');
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
function getHtmlTemplate(body) {
|
||||
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>
|
||||
`;
|
||||
}
|
||||
|
||||
function getLessonBody(
|
||||
{ lessonTitle, lessonNumber, lessonNote, slideUrl, video },
|
||||
orgName,
|
||||
orgTheme
|
||||
) {
|
||||
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 }) {
|
||||
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))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function generateSinglePdfFromHtml(htmlContent, courseTitle, orgTheme) {
|
||||
const args = [
|
||||
'--disable-web-security',
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
];
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args,
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.setContent(htmlContent);
|
||||
|
||||
const pdfOptions = {
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate:
|
||||
"<div><div class='pageNumber'></div> <div>/</div><div class='totalPages'></div></div>",
|
||||
footerTemplate: `
|
||||
<div class="footer" style="position: relative; display: flex; text-align: right; margin-right: 5%; font-size: 8px; width: 297mm; padding-top: 30px;">
|
||||
<div style="position: absolute; right: 50%; bottom: 3%;"><span class="pageNumber"></span>/<span class="totalPages"></span></div>
|
||||
<div style="position: absolute; right: 0; bottom: 3%;">${courseTitle} | Powered by <a href="https://app.classroomio.com" style="text-decoration: underline;">ClassroomIO</a></div>
|
||||
</div>
|
||||
`,
|
||||
margin: {
|
||||
top: '0.3cm',
|
||||
bottom: '0.5cm',
|
||||
},
|
||||
};
|
||||
|
||||
const numPages = await page.evaluate(() => {
|
||||
const content = document.body; // Change this to the specific element containing content
|
||||
const style = window.getComputedStyle(content);
|
||||
const contentHeight = parseInt(style.height, 10);
|
||||
const pageHeight = 900; // Set this to an appropriate value based on your PDF format
|
||||
|
||||
return Math.ceil(contentHeight / pageHeight);
|
||||
});
|
||||
|
||||
const pagesPdfBuffer = [];
|
||||
|
||||
for (let currentPage = 1; currentPage <= numPages; currentPage++) {
|
||||
await page.evaluate(() => {});
|
||||
|
||||
const pdfBuffer = await page.pdf(pdfOptions);
|
||||
pagesPdfBuffer.push(pdfBuffer);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
return Buffer.concat(pagesPdfBuffer);
|
||||
}
|
||||
|
||||
async function generateCoursePdf(params) {
|
||||
// Generate the course boddy
|
||||
const body = getCourseBody(params);
|
||||
const html = getHtmlTemplate(body);
|
||||
console.log(html);
|
||||
return await generateSinglePdfFromHtml(html, params.courseTitle);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateCoursePdf,
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = () => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
};
|
||||
@@ -0,0 +1,227 @@
|
||||
// const { getPdfBuffer } = require('./puppeteer');
|
||||
const { marked } = require('marked');
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
function getHtmlTemplate(body) {
|
||||
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;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: rgb(0, 48, 255);
|
||||
}
|
||||
|
||||
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">
|
||||
${body}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
function getLessonBody({
|
||||
title,
|
||||
number,
|
||||
orgName,
|
||||
note,
|
||||
slideUrl,
|
||||
video,
|
||||
courseTitle,
|
||||
}) {
|
||||
const noteHtml = marked.parse(note);
|
||||
const showExtraResources = slideUrl || (video && video.length > 0);
|
||||
return `
|
||||
<div class="root mx-auto mt-5 prose">
|
||||
<header
|
||||
class="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-blue-700 bg-white text-md font-bold tracking-tighter">
|
||||
${number}
|
||||
</h3>
|
||||
<h1 class="text-white m-0">${title}</h1>
|
||||
<h4 class="roboto-mono text-md font-normal text-white m-0">By ${orgName}</h4>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<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>
|
||||
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function generateSinglePdfFromHtml(htmlContent, courseTitle) {
|
||||
const args = [
|
||||
'--disable-web-security',
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
];
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args,
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.setContent(htmlContent);
|
||||
|
||||
const pdfOptions = {
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate:
|
||||
"<div><div class='pageNumber'></div> <div>/</div><div class='totalPages'></div></div>",
|
||||
footerTemplate: `<div class="footer" style="text-align: right; margin-right: 5%; font-size: 8px; width: 297mm; padding-top: 30px;">
|
||||
${courseTitle} | Powered by <a href="https://app.classroomio.com" style="color: blue; text-decoration: underline;">ClassroomIO</a>
|
||||
</div>`,
|
||||
margin: {
|
||||
top: '0.4cm',
|
||||
bottom: '1cm',
|
||||
},
|
||||
};
|
||||
|
||||
const numPages = await page.evaluate(() => {
|
||||
const content = document.body; // Change this to the specific element containing content
|
||||
const style = window.getComputedStyle(content);
|
||||
const contentHeight = parseInt(style.height, 10);
|
||||
const pageHeight = 900; // Set this to an appropriate value based on your PDF format
|
||||
|
||||
return Math.ceil(contentHeight / pageHeight);
|
||||
});
|
||||
|
||||
const pagesPdfBuffer = [];
|
||||
|
||||
for (let currentPage = 1; currentPage <= numPages; currentPage++) {
|
||||
await page.evaluate(() => {});
|
||||
|
||||
const pdfBuffer = await page.pdf(pdfOptions);
|
||||
pagesPdfBuffer.push(pdfBuffer);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
return Buffer.concat(pagesPdfBuffer);
|
||||
}
|
||||
|
||||
async function generateLessonPdf(params) {
|
||||
// Generate the lesson boddy
|
||||
const body = getLessonBody(params);
|
||||
const html = getHtmlTemplate(body);
|
||||
console.log(html);
|
||||
return await generateSinglePdfFromHtml(html, params.courseTitle);
|
||||
// return await getPdfBuffer(html, {
|
||||
// path: 'test.pdf',
|
||||
// width: '1000px',
|
||||
// height: undefined,
|
||||
// printBackground: true,
|
||||
// format: 'A4',
|
||||
// displayHeaderFooter: true,
|
||||
// headerTemplate:
|
||||
// "<div><div class='pageNumber'></div> <div>/</div><div class='totalPages'></div></div>",
|
||||
// footerTemplate:
|
||||
// '<div style="text-align: right;width: 297mm;font-size: 8px;"><span style="margin-right: 1cm"><span class="pageNumber"></span> of <span class="totalPages"></span></span></div>',
|
||||
// margin: {
|
||||
// top: '1cm',
|
||||
// bottom: '1cm',
|
||||
// },
|
||||
// });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateLessonPdf,
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
const getPdfBuffer = async (html, pdfParams) => {
|
||||
const args = [
|
||||
'--disable-web-security',
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
];
|
||||
const defaultPdfParams = {
|
||||
width: '1123px',
|
||||
height: '794px',
|
||||
};
|
||||
|
||||
// launch a new chrome instance
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args,
|
||||
});
|
||||
|
||||
// create a new page
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1123, height: 794 });
|
||||
await page.goto('data:text/html,' + html, { waitUntil: 'networkidle2' });
|
||||
await page.addStyleTag({ content: '@page { size: auto; }' });
|
||||
await page.emulateMediaType('screen');
|
||||
|
||||
// create a pdf buffer
|
||||
const pdfBuffer = await page.pdf(pdfParams || defaultPdfParams);
|
||||
|
||||
// close the browser
|
||||
await browser.close();
|
||||
|
||||
return pdfBuffer;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getPdfBuffer,
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
|
||||
const { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } = process.env;
|
||||
|
||||
let supabase;
|
||||
|
||||
const getSupabase = (config) => {
|
||||
if (supabase) return supabase;
|
||||
|
||||
config = {
|
||||
supabaseConfig: {
|
||||
bucketPath:
|
||||
'https://koxqonvbkeakwvmdegcf.supabase.in/storage/v1/object/sign/',
|
||||
url: PUBLIC_SUPABASE_URL || '',
|
||||
anonKey: PUBLIC_SUPABASE_ANON_KEY || '',
|
||||
},
|
||||
};
|
||||
|
||||
supabase = createClient(
|
||||
config.supabaseConfig.url,
|
||||
config.supabaseConfig.anonKey,
|
||||
{ auth: { persistSession: false } }
|
||||
);
|
||||
|
||||
return supabase;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getSupabase,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
const multer = require('multer');
|
||||
|
||||
// const storage = multer.memoryStorage();
|
||||
// const upload = multer({ storage: storage });
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, 'uploads/');
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
cb(null, `${new Date().getTime()}-${file.originalname}`); //Appending extension
|
||||
},
|
||||
});
|
||||
|
||||
var upload = multer({ storage: storage });
|
||||
|
||||
module.exports = upload;
|
||||
Generated
+2096
-60
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user