feat: optimize docker compose for dashboard and api (#578)
* chore: use turbo for build * fix docker compose file and remove backend * chore: update .dockerignore to include additional files and directories for improved build management * copy everything in both docke filed * refactor: streamline Dockerfile to copy entire dashboard app and remove unnecessary node_modules references * update api to use turbo to start * run ls la for docker files * update cmd commands * update cmds to use node * fix: add redis url to docker compose * fix: reduce memory allocation for dashboard build in Dockerfile * debug dashboard build * update api docker file * update dashboard * add tsconfig * chore: add echo to dockerfile * fix: change echo to run * chore: install pnpm before switching to non-root user in Dockerfile * refactor: update start commands in package.json and Dockerfiles to use pnpm * install turbo * use turbo 1.10.16 * update turbo config * fix: update CMD in Dockerfile to run the API directly using Node.js * change * add commands to turbo.json * copy over turbo * add workspace * fix: update start commands in package.json to specify ports for dashboard and API * publish docker images * add dev branch * update compose * build only for amd64 platform * refactor: update docker-compose to use pre-built images and remove build context * coolify-dockertest * point to latest * chore: update docker-compose to use specific version tags for API and dashboard images * remove envs for docker file * 0.1.3 * use dynamic env * 0.1.4 * add PUBLIC_IS_SELFHOSTED=true * chore: update docker-compose to use version 0.1.5 for API and dashboard images * use static for PUBLIC_IS_SELFHOSTED * disable cache for tag * use 0.1.7 * add PUBLIC_IS_SELFHOSTED=true * comment out caching * update docker-compose to use version 0.1.8 for API and dashboard images * update api port * use tag 0.1.9 * add cache * use latest tag in compose file
This commit is contained in:
+156
-9
@@ -1,18 +1,165 @@
|
|||||||
Dockerfile
|
# Docker files
|
||||||
|
Dockerfile*
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
|
||||||
|
# Git
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
.gitattributes
|
.gitattributes
|
||||||
|
.gitmodules
|
||||||
|
|
||||||
|
# Documentation
|
||||||
README.md
|
README.md
|
||||||
.npmrc
|
CHANGELOG.md
|
||||||
|
CONTRIBUTING.md
|
||||||
|
CODE_OF_CONDUCT.md
|
||||||
|
LICENSE
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# IDE and Editor files
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Node.js
|
||||||
|
node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.npm
|
||||||
|
.yarn-integrity
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
**/dist
|
||||||
|
**/build
|
||||||
|
**/.next
|
||||||
|
**/.svelte-kit
|
||||||
|
**/coverage
|
||||||
|
**/.nyc_output
|
||||||
|
**/test-results
|
||||||
|
**/playwright-report
|
||||||
|
**/playwright/.cache
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
**/node_modules
|
||||||
|
**/.pnpm-store
|
||||||
|
.pnpm-store/*
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
**/.env
|
||||||
|
**/.env.local
|
||||||
|
**/.env.development
|
||||||
|
**/.env.test
|
||||||
|
**/.env.production
|
||||||
|
**/.env.staging
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
public
|
||||||
|
|
||||||
|
# Storybook build outputs
|
||||||
|
.out
|
||||||
|
.storybook-out
|
||||||
|
|
||||||
|
# Temporary folders
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Turbo
|
||||||
|
**/.turbo
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
**/cypress
|
||||||
|
**/cypress.config.js
|
||||||
|
**/cypress.config.ts
|
||||||
|
**/test-results
|
||||||
|
**/playwright-report
|
||||||
|
**/playwright/.cache
|
||||||
|
|
||||||
|
# Package managers (keep pnpm-lock.yaml for builds)
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
# Config files that aren't needed in container
|
||||||
.prettierrc
|
.prettierrc
|
||||||
.eslintrc.cjs
|
.eslintrc.cjs
|
||||||
.graphqlrc
|
.graphqlrc
|
||||||
.editorconfig
|
.editorconfig
|
||||||
.vscode
|
.npmrc
|
||||||
node_modules
|
|
||||||
build
|
|
||||||
package
|
|
||||||
**/.env
|
|
||||||
|
|
||||||
.pnpm-store/*
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
name: Publish Docker Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Version tag (e.g., v1.0.0)"
|
||||||
|
required: false
|
||||||
|
default: "latest"
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME || 'classroomio' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: api
|
||||||
|
dockerfile: ./docker/Dockerfile.api
|
||||||
|
image: api
|
||||||
|
- name: dashboard
|
||||||
|
dockerfile: ./docker/Dockerfile.dashboard
|
||||||
|
image: dashboard
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels)
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.DOCKERHUB_USERNAME }}/${{ matrix.image }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push ${{ matrix.name }}
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ${{ matrix.dockerfile }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=${{ env.DOCKERHUB_USERNAME }}/${{ matrix.image }}:buildcache
|
||||||
|
cache-to: type=registry,ref=${{ env.DOCKERHUB_USERNAME }}/${{ matrix.image }}:buildcache,mode=max
|
||||||
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
FROM node:slim AS app
|
|
||||||
|
|
||||||
# Add build arguments
|
|
||||||
ARG PUBLIC_SUPABASE_ANON_KEY
|
|
||||||
ARG PUBLIC_SUPABASE_URL
|
|
||||||
ARG CLOUDFLARE_BUCKET_DOMAIN
|
|
||||||
ARG CLOUDFLARE_ACCESS_KEY
|
|
||||||
ARG CLOUDFLARE_SECRET_ACCESS_KEY
|
|
||||||
ARG CLOUDFLARE_ACCOUNT_ID
|
|
||||||
ARG CLOUDFLARE_BUCKET_ID
|
|
||||||
ARG SMTP_HOST
|
|
||||||
ARG SMTP_USER
|
|
||||||
ARG SMTP_PASSWORD
|
|
||||||
ARG SMTP_PORT
|
|
||||||
ARG SMTP_SENDER
|
|
||||||
ARG ZOHO_TOKEN
|
|
||||||
ARG CLOUDFLARE_RENDERING_API_KEY
|
|
||||||
ARG OPENAPI_URL
|
|
||||||
|
|
||||||
RUN mkdir /app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install pnpm and healthcheck tools
|
|
||||||
RUN npm install -g pnpm && \
|
|
||||||
apt-get update && \
|
|
||||||
apt-get install -y curl wget && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# 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 pnpm install
|
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
# Set environment variables from build args
|
|
||||||
ENV NODE_ENV=production \
|
|
||||||
PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY \
|
|
||||||
PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL \
|
|
||||||
CLOUDFLARE_BUCKET_DOMAIN=$CLOUDFLARE_BUCKET_DOMAIN \
|
|
||||||
CLOUDFLARE_ACCESS_KEY=$CLOUDFLARE_ACCESS_KEY \
|
|
||||||
CLOUDFLARE_SECRET_ACCESS_KEY=$CLOUDFLARE_SECRET_ACCESS_KEY \
|
|
||||||
CLOUDFLARE_ACCOUNT_ID=$CLOUDFLARE_ACCOUNT_ID \
|
|
||||||
CLOUDFLARE_BUCKET_ID=$CLOUDFLARE_BUCKET_ID \
|
|
||||||
CLOUDFLARE_RENDERING_API_KEY=$CLOUDFLARE_RENDERING_API_KEY \
|
|
||||||
SMTP_HOST=$SMTP_HOST \
|
|
||||||
SMTP_USER=$SMTP_USER \
|
|
||||||
SMTP_PASSWORD=$SMTP_PASSWORD \
|
|
||||||
SMTP_PORT=$SMTP_PORT \
|
|
||||||
SMTP_SENDER=$SMTP_SENDER \
|
|
||||||
ZOHO_TOKEN=$ZOHO_TOKEN \
|
|
||||||
OPENAPI_URL=$OPENAPI_URL \
|
|
||||||
PORT=3002
|
|
||||||
|
|
||||||
RUN pnpm build
|
|
||||||
|
|
||||||
EXPOSE 3002
|
|
||||||
|
|
||||||
# Add healthcheck
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
||||||
CMD curl -f http://localhost:3002 || exit 1
|
|
||||||
|
|
||||||
CMD ["pnpm", "start"]
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
node_modules
|
|
||||||
npm-debug.log
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
## Supabase - DB
|
|
||||||
PUBLIC_SUPABASE_ANON_KEY=
|
|
||||||
PUBLIC_SUPABASE_URL=
|
|
||||||
|
|
||||||
## Cloudflare - Video uploads
|
|
||||||
# Not required - https://cdn.example.com
|
|
||||||
CLOUDFLARE_BUCKET_DOMAIN=
|
|
||||||
|
|
||||||
CLOUDFLARE_ACCESS_KEY=
|
|
||||||
CLOUDFLARE_SECRET_ACCESS_KEY=
|
|
||||||
CLOUDFLARE_ACCOUNT_ID=
|
|
||||||
|
|
||||||
# Email
|
|
||||||
SMTP_HOST=
|
|
||||||
SMTP_USER=
|
|
||||||
SMTP_PASSWORD=
|
|
||||||
SMTP_PORT=
|
|
||||||
|
|
||||||
# Sentry DNS
|
|
||||||
SENTRY_DNS=
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
FROM node:slim AS app
|
|
||||||
|
|
||||||
# Add build arguments
|
|
||||||
ARG PUBLIC_SUPABASE_ANON_KEY
|
|
||||||
ARG PUBLIC_SUPABASE_URL
|
|
||||||
ARG CLOUDFLARE_BUCKET_DOMAIN
|
|
||||||
ARG CLOUDFLARE_ACCESS_KEY
|
|
||||||
ARG CLOUDFLARE_SECRET_ACCESS_KEY
|
|
||||||
ARG CLOUDFLARE_ACCOUNT_ID
|
|
||||||
ARG CLOUDFLARE_BUCKET_ID
|
|
||||||
ARG SMTP_HOST
|
|
||||||
ARG SMTP_USER
|
|
||||||
ARG SMTP_PASSWORD
|
|
||||||
ARG SMTP_PORT
|
|
||||||
ARG SMTP_SENDER
|
|
||||||
|
|
||||||
RUN mkdir /app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install pnpm
|
|
||||||
RUN npm install -g pnpm
|
|
||||||
|
|
||||||
# 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 pnpm install
|
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
# Set environment variables from build args
|
|
||||||
ENV NODE_ENV=production \
|
|
||||||
PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY \
|
|
||||||
PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL \
|
|
||||||
CLOUDFLARE_BUCKET_DOMAIN=$CLOUDFLARE_BUCKET_DOMAIN \
|
|
||||||
CLOUDFLARE_ACCESS_KEY=$CLOUDFLARE_ACCESS_KEY \
|
|
||||||
CLOUDFLARE_SECRET_ACCESS_KEY=$CLOUDFLARE_SECRET_ACCESS_KEY \
|
|
||||||
CLOUDFLARE_ACCOUNT_ID=$CLOUDFLARE_ACCOUNT_ID \
|
|
||||||
CLOUDFLARE_BUCKET_ID=$CLOUDFLARE_BUCKET_ID \
|
|
||||||
SMTP_HOST=$SMTP_HOST \
|
|
||||||
SMTP_USER=$SMTP_USER \
|
|
||||||
SMTP_PASSWORD=$SMTP_PASSWORD \
|
|
||||||
SMTP_PORT=$SMTP_PORT \
|
|
||||||
PORT=3002
|
|
||||||
|
|
||||||
RUN pnpm build
|
|
||||||
|
|
||||||
EXPOSE 3002
|
|
||||||
|
|
||||||
CMD ["pnpm", "start"]
|
|
||||||
@@ -1,431 +0,0 @@
|
|||||||
<!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>
|
|
||||||
.student-name {
|
|
||||||
font-family: 'Qwitcher Grypen', cursive;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<img
|
|
||||||
src="http://localhost:3002/blue-lined-background.png"
|
|
||||||
alt=""
|
|
||||||
class="absolute top-0 left-0 w-full h-[100vh] z-10"
|
|
||||||
/>
|
|
||||||
<div class="absolute z-20 left-[5%] top-16 w-[90%]">
|
|
||||||
<header class="w-full flex justify-center items-center pt-10 gap-3 text-3xl font-bold">
|
|
||||||
<img src="${orgLogoUrl}" alt="" />
|
|
||||||
<h1>${orgName}</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="pl-10 text-lg font-bold mt-[10%]">
|
|
||||||
<h2>This certificate is awarded to</h2>
|
|
||||||
<h1 class="text-3xl text-blue-800">${studentName}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pl-10 text-lg font-bold mt-10 w-[70%]">
|
|
||||||
<h1>has succesfully completed training on</h1>
|
|
||||||
<h2 class="text-4xl leading-[2.5rem]">${courseName}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex border justify-between w-[70%] mx-auto text-center items-end my-14">
|
|
||||||
<div>
|
|
||||||
<p class="font-bold">${dateIssued}</p>
|
|
||||||
<h1 class="border-t border-black h-2/4 font-bold">Date Issued</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-[20%]">
|
|
||||||
<h1 class="h-2/4 border-b border-black"></h1>
|
|
||||||
<p class="font-bold">Signature</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="text-sm pl-10 w-[90%] pb-10">${courseDescription}</footer>
|
|
||||||
</div>
|
|
||||||
<!-- plain blueProfessionalBadgePattern -->
|
|
||||||
<!-- <img
|
|
||||||
src="http://localhost:3002/blue-professional-badge-background.png"
|
|
||||||
alt=""
|
|
||||||
class="absolute top-0 left-0 w-full z-10"
|
|
||||||
/>
|
|
||||||
<div class="absolute w-full pl-20 top-10 z-20">
|
|
||||||
<header class="w-full flex justify-start items-center pt-10 gap-3 text-3xl font-bold">
|
|
||||||
<svg
|
|
||||||
width="34"
|
|
||||||
height="27"
|
|
||||||
viewBox="0 0 24 17"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M19.2076 9.08656C21.5733 9.08656 23.4911 7.16873 23.4911 4.80302C23.4911 2.43731 21.5733 0.519531 19.2076 0.519531C16.8419 0.519531 14.9241 2.43731 14.9241 4.80302C14.9241 7.16873 16.8419 9.08656 19.2076 9.08656Z"
|
|
||||||
fill="#0542CC"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M23.5385 10.7793V16.9927H14.1243V14.7333C14.1243 12.568 15.8659 10.7793 18.0783 10.7793H23.5385Z"
|
|
||||||
fill="#0542CC"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12.5701 0.519531V16.9945H9.03974C6.78031 16.9945 4.70918 16.0531 3.20289 14.5939C1.69661 13.0876 0.802246 11.0165 0.802246 8.75703C0.802246 4.1911 4.47382 0.519531 9.03974 0.519531H12.5701Z"
|
|
||||||
fill="#0542CC"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<h1 class="text-[#282828]">Classroom<span class="text-[#0233BD]">IO</span></h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="text-lg font-bold mt-[9%]">
|
|
||||||
<h2>This certificate is awarded to</h2>
|
|
||||||
<h1 class="text-3xl text-blue-800">Usman Elebute Ayomide</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-lg font-bold mt-10 w-[70%]">
|
|
||||||
<h1>has succesfully completed training on</h1>
|
|
||||||
<h2 class="text-5xl mt-3 leading-[3.5rem]">
|
|
||||||
Building fullstack Applications with superbase & Svelte
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-between w-[70%] text-center my-[10%]">
|
|
||||||
<div>
|
|
||||||
<h1 class="h-2/4">DATE ISSUED:</h1>
|
|
||||||
<p class="border-b-2 border-black font-bold">02,May 2024</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 class="h-2/4">FACILITATOR:</h1>
|
|
||||||
<p class="font-bold">ROTIMI BEST</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-[20%]">
|
|
||||||
<h1 class="h-2/4 border-b-2 border-black"></h1>
|
|
||||||
<p class="font-bold">Signature</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="text-sm w-[90%] pb-10">
|
|
||||||
Building full-stack apps with Superbase & Svelte merges robust backend services from
|
|
||||||
Superbase with Svelte's sleek frontend framework. Leveraging PostgreSQL and Svelte's
|
|
||||||
reactivity, developers create efficient, feature-rich web apps. This stack streamlines
|
|
||||||
development, offering scalability and performance for modern applications with minimal
|
|
||||||
overhead.
|
|
||||||
</footer>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<!-- plain purpleProfessionalBadgePattern -->
|
|
||||||
<!-- <img
|
|
||||||
src="http://localhost:3002/purple-professional-badge-background.png"
|
|
||||||
alt=""
|
|
||||||
class="absolute top-0 left-0 w-full z-10"
|
|
||||||
/>
|
|
||||||
<div class="absolute w-full pl-20 top-10 z-20">
|
|
||||||
<header class="w-full flex justify-start items-center pt-10 gap-3 text-3xl font-bold">
|
|
||||||
<svg
|
|
||||||
width="34"
|
|
||||||
height="27"
|
|
||||||
viewBox="0 0 24 17"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M19.2076 9.08656C21.5733 9.08656 23.4911 7.16873 23.4911 4.80302C23.4911 2.43731 21.5733 0.519531 19.2076 0.519531C16.8419 0.519531 14.9241 2.43731 14.9241 4.80302C14.9241 7.16873 16.8419 9.08656 19.2076 9.08656Z"
|
|
||||||
fill="#0542CC"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M23.5385 10.7793V16.9927H14.1243V14.7333C14.1243 12.568 15.8659 10.7793 18.0783 10.7793H23.5385Z"
|
|
||||||
fill="#0542CC"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12.5701 0.519531V16.9945H9.03974C6.78031 16.9945 4.70918 16.0531 3.20289 14.5939C1.69661 13.0876 0.802246 11.0165 0.802246 8.75703C0.802246 4.1911 4.47382 0.519531 9.03974 0.519531H12.5701Z"
|
|
||||||
fill="#0542CC"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<h1 class="text-[#282828]">Classroom<span class="text-[#0233BD]">IO</span></h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="text-lg font-bold mt-[9%]">
|
|
||||||
<h2>This certificate is awarded to</h2>
|
|
||||||
<h1 class="text-3xl text-fuchsia-800">Usman Elebute Ayomide</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-lg font-bold mt-10 w-[70%]">
|
|
||||||
<h1>has succesfully completed training on</h1>
|
|
||||||
<h2 class="text-5xl mt-3 leading-[3.5rem]">
|
|
||||||
Building fullstack Applications with superbase & Svelte
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-between w-[70%] text-center my-[10%]">
|
|
||||||
<div>
|
|
||||||
<h1 class="h-2/4">DATE ISSUED:</h1>
|
|
||||||
<p class="border-b-2 border-black font-bold">02,May 2024</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 class="h-2/4">FACILITATOR:</h1>
|
|
||||||
<p class="font-bold">ROTIMI BEST</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-[20%]">
|
|
||||||
<h1 class="h-2/4 border-b-2 border-black"></h1>
|
|
||||||
<p class="font-bold">Signature</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="text-sm w-[90%] pb-10">
|
|
||||||
Building full-stack apps with Superbase & Svelte merges robust backend services from
|
|
||||||
Superbase with Svelte's sleek frontend framework. Leveraging PostgreSQL and Svelte's
|
|
||||||
reactivity, developers create efficient, feature-rich web apps. This stack streamlines
|
|
||||||
development, offering scalability and performance for modern applications with minimal
|
|
||||||
overhead.
|
|
||||||
</footer>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<!------------------------------------------------------- new secton ---------------------------------------------------- -->
|
|
||||||
|
|
||||||
<!-- PurpleProfessionalBadge certificate -->
|
|
||||||
|
|
||||||
<!-- <img
|
|
||||||
src="http://localhost:3002/purple-lined-background.png"
|
|
||||||
alt=""
|
|
||||||
class="absolute -top-1 left-0 w-full z-10"
|
|
||||||
/>
|
|
||||||
<div class="absolute z-20 left-[10%] top-16 w-[90%]">
|
|
||||||
<header class="w-full flex justify-center items-center pt-10 gap-3 text-3xl font-bold">
|
|
||||||
<svg
|
|
||||||
width="34"
|
|
||||||
height="27"
|
|
||||||
viewBox="0 0 24 17"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M19.2076 9.08656C21.5733 9.08656 23.4911 7.16873 23.4911 4.80302C23.4911 2.43731 21.5733 0.519531 19.2076 0.519531C16.8419 0.519531 14.9241 2.43731 14.9241 4.80302C14.9241 7.16873 16.8419 9.08656 19.2076 9.08656Z"
|
|
||||||
fill="#0542CC"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M23.5385 10.7793V16.9927H14.1243V14.7333C14.1243 12.568 15.8659 10.7793 18.0783 10.7793H23.5385Z"
|
|
||||||
fill="#0542CC"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12.5701 0.519531V16.9945H9.03974C6.78031 16.9945 4.70918 16.0531 3.20289 14.5939C1.69661 13.0876 0.802246 11.0165 0.802246 8.75703C0.802246 4.1911 4.47382 0.519531 9.03974 0.519531H12.5701Z"
|
|
||||||
fill="#0542CC"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<h1 class="text-[#282828]">Classroom<span class="text-[#0233BD]">IO</span></h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="pl-10 text-lg font-bold mt-[10%]">
|
|
||||||
<h2>This certificate is awarded to</h2>
|
|
||||||
<h1 class="text-3xl text-[#95449A]">Usman Elebute Ayomide</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pl-10 text-lg font-bold mt-10 w-[70%]">
|
|
||||||
<h1>has succesfully completed training on</h1>
|
|
||||||
<h2 class="text-4xl leading-[2.5rem]">
|
|
||||||
Building fullstack Applications with superbase & Svelte
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pl-10 flex justify-between w-[70%] text-center my-[10%]">
|
|
||||||
<div>
|
|
||||||
<h1 class="h-2/4">DATE ISSUED:</h1>
|
|
||||||
<p class="border-b-2 border-black font-bold">02,May 2024</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 class="h-2/4">FACILITATOR:</h1>
|
|
||||||
<p class="font-bold">ROTIMI BEST</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-[20%]">
|
|
||||||
<h1 class="h-2/4 border-b-2 border-black"></h1>
|
|
||||||
<p class="font-bold">Signature</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="text-sm pl-10 w-[90%] pb-10">
|
|
||||||
Building full-stack apps with Superbase & Svelte merges robust backend services from
|
|
||||||
Superbase with Svelte's sleek frontend framework. Leveraging PostgreSQL and Svelte's
|
|
||||||
reactivity, developers create efficient, feature-rich web apps. This stack streamlines
|
|
||||||
development, offering scalability and performance for modern applications with minimal
|
|
||||||
overhead.
|
|
||||||
</footer>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<!-- BlueProfessionalBadge certificate -->
|
|
||||||
|
|
||||||
<!-- <img
|
|
||||||
src="http://localhost:3002/blue-lined-background.png"
|
|
||||||
alt=""
|
|
||||||
class="absolute -top-1 left-0 w-full z-10"
|
|
||||||
/>
|
|
||||||
<div class="absolute z-20 left-[10%] top-16 w-[90%]">
|
|
||||||
<header class="w-full flex justify-center items-center pt-10 gap-3 text-3xl font-bold">
|
|
||||||
<img src="${orgLogoUrl}" alt="" />
|
|
||||||
<h1>${orgName}</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="pl-10 text-lg font-bold mt-[10%]">
|
|
||||||
<h2>This certificate is awarded to</h2>
|
|
||||||
<h1 class="text-3xl">${studentName}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pl-10 text-lg font-bold mt-10 w-[70%]">
|
|
||||||
<h1>has succesfully completed training on</h1>
|
|
||||||
<h2 class="text-4xl leading-[2.5rem]">${courseName}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pl-10 flex justify-between w-[70%] text-center my-[10%]">
|
|
||||||
<div>
|
|
||||||
<h1 class="h-2/4">DATE ISSUED:</h1>
|
|
||||||
<p class="border-b-2 border-black font-bold">02,May 2024</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 class="h-2/4">FACILITATOR:</h1>
|
|
||||||
<p class="font-bold">ROTIMI BEST</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-[20%]">
|
|
||||||
<h1 class="h-2/4 border-b-2 border-black"></h1>
|
|
||||||
<p class="font-bold">Signature</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="text-sm pl-10 w-[90%] pb-10">${courseDescription}</footer>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<!-- --------------------- old certificates ------------------------- -->
|
|
||||||
<!-- <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 justify-between w-[70%] mx-auto text-center my-14">
|
|
||||||
<div>
|
|
||||||
<h1 class="h-2/4">DATE ISSUED:</h1>
|
|
||||||
<p class="border-b border-black font-bold">02,May 2024</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 class="h-2/4">FACILITATOR:</h1>
|
|
||||||
<p class="font-bold">ROTIMI BEST</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-[20%]">
|
|
||||||
<h1 class="h-2/4 border-b border-black"></h1>
|
|
||||||
<p class="font-bold">Signature</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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 class="flex justify-between w-[70%] mx-auto text-center my-14">
|
|
||||||
<div>
|
|
||||||
<h1 class="h-2/4">DATE ISSUED:</h1>
|
|
||||||
<p class="border-b border-black font-bold">02,May 2024</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 class="h-2/4">FACILITATOR:</h1>
|
|
||||||
<p class="font-bold">ROTIMI BEST</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-[20%]">
|
|
||||||
<h1 class="h-2/4 border-b border-black"></h1>
|
|
||||||
<p class="font-bold">Signature</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 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"
|
|
||||||
|
|
||||||
[build]
|
|
||||||
dockerfile = "Dockerfile"
|
|
||||||
|
|
||||||
[http_service]
|
|
||||||
internal_port = 3002
|
|
||||||
force_https = true
|
|
||||||
auto_stop_machines = true
|
|
||||||
auto_start_machines = true
|
|
||||||
min_machines_running = 0
|
|
||||||
processes = ["app"]
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const envSchema = z.object({
|
|
||||||
PORT: z.string().optional(),
|
|
||||||
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
||||||
CLOUDFLARE_BUCKET_DOMAIN: z.string().optional(),
|
|
||||||
CLOUDFLARE_ACCESS_KEY: z.string().optional(),
|
|
||||||
CLOUDFLARE_SECRET_ACCESS_KEY: z.string().optional(),
|
|
||||||
CLOUDFLARE_ACCOUNT_ID: z.string().optional(),
|
|
||||||
PUBLIC_SUPABASE_URL: z.string(),
|
|
||||||
PUBLIC_SUPABASE_ANON_KEY: z.string(),
|
|
||||||
ZOHO_TOKEN: z.string().optional(),
|
|
||||||
SMTP_HOST: z.string().optional(),
|
|
||||||
SMTP_PORT: z.string().optional(),
|
|
||||||
SMTP_USER: z.string().optional(),
|
|
||||||
SMTP_PASSWORD: z.string().optional(),
|
|
||||||
SMTP_SENDER: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const env = envSchema.parse(process.env);
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { env } from '$src/config/env';
|
|
||||||
|
|
||||||
const {
|
|
||||||
CLOUDFLARE_BUCKET_DOMAIN,
|
|
||||||
CLOUDFLARE_ACCESS_KEY,
|
|
||||||
CLOUDFLARE_SECRET_ACCESS_KEY,
|
|
||||||
CLOUDFLARE_ACCOUNT_ID
|
|
||||||
} = env;
|
|
||||||
|
|
||||||
export const CLOUDFLARE = {
|
|
||||||
CONFIGS: {
|
|
||||||
BUCKET_DOMAIN: CLOUDFLARE_BUCKET_DOMAIN!,
|
|
||||||
ACCESS_KEY: CLOUDFLARE_ACCESS_KEY!,
|
|
||||||
SECRET_ACCESS_KEY: CLOUDFLARE_SECRET_ACCESS_KEY!,
|
|
||||||
ACCOUNT_ID: CLOUDFLARE_ACCOUNT_ID!
|
|
||||||
},
|
|
||||||
R2: {
|
|
||||||
BUCKET: 'videos',
|
|
||||||
BUCKET_DOMAIN: CLOUDFLARE_ACCOUNT_ID
|
|
||||||
? `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`
|
|
||||||
: CLOUDFLARE_BUCKET_DOMAIN!,
|
|
||||||
PRESIGN_EXPIRATION_TIME: 60 * 60, // 1 hour
|
|
||||||
DOWNLOAD_EXPIRATION_TIME: 60 * 60 * 3 // 3 hours
|
|
||||||
}
|
|
||||||
} as const;
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { Context, Next } from 'hono';
|
|
||||||
|
|
||||||
import { validateUser } from '$src/utils/auth/validate-user';
|
|
||||||
|
|
||||||
export const authMiddleware = async (c: Context, next: Next) => {
|
|
||||||
try {
|
|
||||||
const authHeader = c.req.header('authorization');
|
|
||||||
|
|
||||||
if (!authHeader) {
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
message: 'No authorization header provided'
|
|
||||||
},
|
|
||||||
401
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the token from "Bearer <token>"
|
|
||||||
const token = authHeader.split(' ')[1];
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
message: 'Invalid authorization header format'
|
|
||||||
},
|
|
||||||
401
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await validateUser(token);
|
|
||||||
c.set('user', user); // Attach the user to the context
|
|
||||||
await next();
|
|
||||||
} catch (error) {
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
message: 'Unauthorized'
|
|
||||||
},
|
|
||||||
401
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { Context, Hono } from 'hono';
|
|
||||||
|
|
||||||
import katex from 'katex';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const katexRouter = new Hono();
|
|
||||||
|
|
||||||
katexRouter.get('/', async (c: Context) => {
|
|
||||||
try {
|
|
||||||
const original = c.req.raw.url;
|
|
||||||
|
|
||||||
const latexString = original.split('?')[1].replaceAll('+', '+').replaceAll('&space;', '');
|
|
||||||
|
|
||||||
const html = katex.renderToString(latexString, {
|
|
||||||
output: 'mathml',
|
|
||||||
throwOnError: false
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.html(html);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: 'Validation error',
|
|
||||||
details: error.errors
|
|
||||||
},
|
|
||||||
400
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Error rendering LaTeX:', error);
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
||||||
},
|
|
||||||
500
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { Context, Hono } from 'hono';
|
|
||||||
import { sendWithNodemailer, sendWithZoho } from '$src/services/mail';
|
|
||||||
|
|
||||||
import { ZSendEmailValidation } from '$src/types/mail';
|
|
||||||
import { env } from '$src/config/env';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const mailRouter = new Hono();
|
|
||||||
|
|
||||||
mailRouter.post('/send', async (c: Context) => {
|
|
||||||
try {
|
|
||||||
const data = await c.req.json();
|
|
||||||
const validatedData = ZSendEmailValidation.parse(data);
|
|
||||||
|
|
||||||
const results = await Promise.all(
|
|
||||||
validatedData.map(async (emailData) => {
|
|
||||||
try {
|
|
||||||
const res = env.ZOHO_TOKEN
|
|
||||||
? await sendWithZoho(emailData)
|
|
||||||
: await sendWithNodemailer(emailData);
|
|
||||||
|
|
||||||
console.log('Email status:', res);
|
|
||||||
return res;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending email:', error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
||||||
details: error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasErrors = results.some((result) => !result.success);
|
|
||||||
if (hasErrors) {
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: 'Some emails failed to send',
|
|
||||||
details: results
|
|
||||||
},
|
|
||||||
500
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ success: true, details: results });
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: 'Validation error',
|
|
||||||
details: error.errors
|
|
||||||
},
|
|
||||||
400
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Error sending emails:', error);
|
|
||||||
return c.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
||||||
details: error
|
|
||||||
},
|
|
||||||
500
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { extractNameAndEmail, withEmailTemplate } from '$src/utils/mail';
|
|
||||||
import { nodemailerTransporter, zohoClient } from '$src/utils/email';
|
|
||||||
|
|
||||||
import { Hono } from 'hono';
|
|
||||||
import type { TSendEmailValidation } from '$src/types/mail';
|
|
||||||
import type { Transporter } from 'nodemailer';
|
|
||||||
import { env } from '$src/config/env';
|
|
||||||
|
|
||||||
export const mailRouter = new Hono();
|
|
||||||
|
|
||||||
interface EmailResponse {
|
|
||||||
success: boolean;
|
|
||||||
error?: string;
|
|
||||||
details?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
let transporter: Transporter | null;
|
|
||||||
|
|
||||||
nodemailerTransporter().then((t: Transporter | null) => {
|
|
||||||
transporter = t;
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function sendWithNodemailer(
|
|
||||||
emailData: TSendEmailValidation[0]
|
|
||||||
): Promise<EmailResponse> {
|
|
||||||
const { from, to, subject, content, replyTo } = emailData;
|
|
||||||
|
|
||||||
if (!transporter) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Email transporter not initialized'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await transporter.sendMail({
|
|
||||||
from: from || env.SMTP_SENDER,
|
|
||||||
to,
|
|
||||||
subject,
|
|
||||||
replyTo,
|
|
||||||
html: withEmailTemplate(content)
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
details: result
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
||||||
details: error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendWithZoho(emailData: TSendEmailValidation[0]): Promise<EmailResponse> {
|
|
||||||
const { from, to, subject, content } = emailData;
|
|
||||||
|
|
||||||
const fromData = extractNameAndEmail(from || '');
|
|
||||||
|
|
||||||
if (!fromData) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Invalid from data'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await zohoClient.sendMail({
|
|
||||||
from: {
|
|
||||||
address: fromData.email,
|
|
||||||
name: fromData.name
|
|
||||||
},
|
|
||||||
to: [
|
|
||||||
{
|
|
||||||
email_address: {
|
|
||||||
address: to
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
subject,
|
|
||||||
htmlbody: content
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
details: result
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
||||||
details: error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { ALLOWED_CONTENT_TYPES, MAX_FILE_SIZE } from '$src/constants/upload';
|
|
||||||
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export * from './lesson';
|
|
||||||
|
|
||||||
export const ZCourseDownloadContent = z.object({
|
|
||||||
courseTitle: z.string(),
|
|
||||||
orgName: z.string(),
|
|
||||||
orgTheme: z.string(),
|
|
||||||
lessons: z.array(
|
|
||||||
z.object({
|
|
||||||
lessonTitle: z.string(),
|
|
||||||
lessonNumber: z.string(),
|
|
||||||
lessonNote: z.string(),
|
|
||||||
slideUrl: z.string().optional(),
|
|
||||||
video: z.array(z.string()).optional()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TCourseDownloadContent = z.infer<typeof ZCourseDownloadContent>;
|
|
||||||
|
|
||||||
export const ZCoursePresignUrlUpload = z.object({
|
|
||||||
fileName: z.string().min(1, 'File name is required'),
|
|
||||||
fileType: z.enum(ALLOWED_CONTENT_TYPES, {
|
|
||||||
errorMap: () => ({
|
|
||||||
message: `Invalid content type. Allowed types: ${ALLOWED_CONTENT_TYPES.join(', ')}`
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TCoursePresignUrlUpload = z.infer<typeof ZCoursePresignUrlUpload>;
|
|
||||||
|
|
||||||
export const ZCourseDownloadPresignedUrl = z.object({
|
|
||||||
keys: z.array(z.string()).min(1, 'File names array is required')
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TCourseDownloadPresignedUrl = z.infer<typeof ZCourseDownloadPresignedUrl>;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const ZLessonDownloadContent = z.object({
|
|
||||||
title: z.string(),
|
|
||||||
number: z.string(),
|
|
||||||
orgName: z.string(),
|
|
||||||
note: z.string(),
|
|
||||||
slideUrl: z.string().optional(),
|
|
||||||
video: z.array(z.string()).optional(),
|
|
||||||
courseTitle: z.string()
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TLessonDownloadContent = z.infer<typeof ZLessonDownloadContent>;
|
|
||||||
|
|
||||||
export const ZCertificateDownload = z.object({
|
|
||||||
theme: z.string().min(1),
|
|
||||||
studentName: z.string().min(1),
|
|
||||||
courseName: z.string().min(1),
|
|
||||||
courseDescription: z.string().min(1),
|
|
||||||
orgName: z.string().min(1),
|
|
||||||
orgLogoUrl: z.string().url().optional(),
|
|
||||||
facilitator: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TCertificateDownload = z.infer<typeof ZCertificateDownload>;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const ZSendEmailValidation = z.array(
|
|
||||||
z.object({
|
|
||||||
from: z.string().optional(),
|
|
||||||
to: z.string().email(),
|
|
||||||
subject: z.string().min(1),
|
|
||||||
content: z.string().min(1),
|
|
||||||
replyTo: z.string().email().optional()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export type TSendEmailValidation = z.infer<typeof ZSendEmailValidation>;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { User } from '@supabase/supabase-js';
|
|
||||||
import { getSupabase } from '../supabase.js';
|
|
||||||
|
|
||||||
const supabase = getSupabase();
|
|
||||||
|
|
||||||
export async function validateUser(accessToken: string): Promise<User> {
|
|
||||||
let user: User | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data } = await supabase.auth.getUser(accessToken);
|
|
||||||
user = data.user;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new Error('Unauthenticated user');
|
|
||||||
}
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import type { TCertificateDownload } from '$src/types/course/lesson';
|
|
||||||
import { getCloudflarePdfBuffer } from '$src/utils/cloudflare';
|
|
||||||
|
|
||||||
export interface CertificateData {
|
|
||||||
theme: string;
|
|
||||||
studentName: string;
|
|
||||||
courseName: string;
|
|
||||||
courseDescription: string;
|
|
||||||
orgLogoUrl: string;
|
|
||||||
orgName: string;
|
|
||||||
facilitator?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CertificateGenerationResult {
|
|
||||||
success: boolean;
|
|
||||||
buffer?: Buffer;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const generateCertificate = async (data: TCertificateDownload) => {
|
|
||||||
const { html, styles } = generateCertificateHtml(data);
|
|
||||||
return getCloudflarePdfBuffer(html, styles);
|
|
||||||
};
|
|
||||||
|
|
||||||
function generateCertificateHtml(data: TCertificateDownload): { html: string; styles: string } {
|
|
||||||
const { studentName, courseName, courseDescription, orgLogoUrl, orgName, facilitator } = data;
|
|
||||||
|
|
||||||
const html = `
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Certificate of Completion</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="certificate">
|
|
||||||
<div class="header">
|
|
||||||
<img src="${orgLogoUrl}" alt="${orgName} Logo" class="logo">
|
|
||||||
<h1 class="title">Certificate of Completion</h1>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<h2 class="student-name">${studentName}</h2>
|
|
||||||
<p class="course-name">${courseName}</p>
|
|
||||||
<p class="description">${courseDescription}</p>
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
<div class="signature">
|
|
||||||
<p>${facilitator || 'Course Facilitator'}</p>
|
|
||||||
<p>${orgName}</p>
|
|
||||||
</div>
|
|
||||||
<p class="date">${new Date().toLocaleDateString()}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const styles = `
|
|
||||||
body {
|
|
||||||
font-family: 'Arial', sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
.certificate {
|
|
||||||
width: 1123px;
|
|
||||||
height: 794px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background-color: white;
|
|
||||||
position: relative;
|
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.logo {
|
|
||||||
max-width: 200px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
font-size: 36px;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
.student-name {
|
|
||||||
font-size: 48px;
|
|
||||||
color: #2c3e50;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.course-name {
|
|
||||||
font-size: 24px;
|
|
||||||
color: #34495e;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.description {
|
|
||||||
font-size: 18px;
|
|
||||||
color: #7f8c8d;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 40px;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.signature {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
.date {
|
|
||||||
margin-top: 10px;
|
|
||||||
color: #95a5a6;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const pdf = {
|
|
||||||
html: html,
|
|
||||||
styles: styles
|
|
||||||
};
|
|
||||||
|
|
||||||
return pdf;
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { CLOUDFLARE } from '$src/constants';
|
|
||||||
|
|
||||||
export const getCloudflarePdfBuffer = async (html: string, styles?: string) => {
|
|
||||||
console.log('Generating PDF with Cloudflare API...');
|
|
||||||
try {
|
|
||||||
const pdfResponse = await fetch(
|
|
||||||
`https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE.CONFIGS.ACCOUNT_ID}/browser-rendering/pdf`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${CLOUDFLARE.CONFIGS.ACCESS_KEY}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
html: html,
|
|
||||||
addStyleTag: [{ content: `${styles}` }]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('PDF response status:', pdfResponse.status);
|
|
||||||
return pdfResponse;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating PDF:', error);
|
|
||||||
throw new Error(error instanceof Error ? error.message : 'Failed to generate PDF');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import type { TCourseDownloadContent } from '$src/types/course';
|
|
||||||
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> {
|
|
||||||
throw 'Download disabled';
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { SendMailClient } from 'zeptomail';
|
|
||||||
import type { Transporter } from 'nodemailer';
|
|
||||||
import { env } from '$src/config/env';
|
|
||||||
import nodemailer from 'nodemailer';
|
|
||||||
|
|
||||||
export const zohoClient = new SendMailClient({
|
|
||||||
url: 'api.zeptomail.eu/',
|
|
||||||
token: env.ZOHO_TOKEN!
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodemailerTransporter = async (): Promise<Transporter | null> => {
|
|
||||||
if (!env.SMTP_HOST || !env.SMTP_PORT || !env.SMTP_USER || !env.SMTP_PASSWORD) {
|
|
||||||
console.error('SMTP configuration missing');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
host: env.SMTP_HOST,
|
|
||||||
port: parseInt(env.SMTP_PORT || '465', 10),
|
|
||||||
secure: true,
|
|
||||||
auth: {
|
|
||||||
user: env.SMTP_USER,
|
|
||||||
pass: env.SMTP_PASSWORD
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await transporter.verify();
|
|
||||||
|
|
||||||
return transporter;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Transporter error:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default function genUniqueId(): string {
|
|
||||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import type { TLessonDownloadContent } from '$src/types/course/lesson';
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}: TLessonDownloadContent): string {
|
|
||||||
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: string,
|
|
||||||
courseTitle: string
|
|
||||||
): Promise<Buffer> {
|
|
||||||
throw 'Download disabled';
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateLessonPdf(params: TLessonDownloadContent): Promise<Buffer> {
|
|
||||||
const body = getLessonBody(params);
|
|
||||||
const html = getHtmlTemplate(body);
|
|
||||||
console.log(html);
|
|
||||||
return await generateSinglePdfFromHtml(html, params.courseTitle);
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
interface FromData {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// format: "ClassroomIO Developers (via ClassroomIO.com)" <[email protected]>
|
|
||||||
export function extractNameAndEmail(str: string): FromData | undefined {
|
|
||||||
// Use regular expressions to match the name and email
|
|
||||||
const regex = /"(.*?)"\s+<\s*(.*?)@(.*?)\s*>/;
|
|
||||||
const match = str.match(regex);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
// Extract the name and email from the match groups
|
|
||||||
const name = match[1];
|
|
||||||
const email = match[2] + '@' + match[3];
|
|
||||||
return { name, email };
|
|
||||||
} else {
|
|
||||||
// Return undefined if the format doesn't match
|
|
||||||
return { name: str, email: str };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const withEmailTemplate = (content: string): string =>
|
|
||||||
`<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
|
|
||||||
<base target="_blank" />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background-color: #F6F9FF;
|
|
||||||
font-family: "Poppins", "Helvetica Neue", "Segoe UI", Helvetica,
|
|
||||||
sans-serif;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 26px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
background: #f4f4f4;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border-top: 1px dashed #94a3b8;
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table td {
|
|
||||||
padding: 5px 10px 0px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.socialicons {
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 5px;
|
|
||||||
width: initial;
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brandcolor {
|
|
||||||
color: #00c4b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
color: #475569;
|
|
||||||
margin-top: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip a {
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
margin-top: 12px;
|
|
||||||
background: #0233BD;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-decoration: none !important;
|
|
||||||
color: #fff !important;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 10px 30px;
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
background: #0233BD;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #cbd5e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a {
|
|
||||||
color: #cbd5e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gutter {
|
|
||||||
padding: 5px 30px;
|
|
||||||
text-align: center;
|
|
||||||
background-color: #fff;
|
|
||||||
border-bottom: 1px solid rgb(221, 208, 208);
|
|
||||||
max-width: 525px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gutter img {
|
|
||||||
max-width: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4 {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
.wrap {
|
|
||||||
max-width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gutter {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body style="
|
|
||||||
background-color: #F6F9FF;
|
|
||||||
font-family: 'Poppins', 'Helvetica Neue', 'Segoe UI', Helvetica,
|
|
||||||
sans-serif;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 26px;
|
|
||||||
padding: 20px 0;
|
|
||||||
margin: 0;
|
|
||||||
color: #1e293b;
|
|
||||||
">
|
|
||||||
<div class="gutter">
|
|
||||||
<a href="https://classroomio.com" target="_blank">
|
|
||||||
<img src="https://brand.cdn.clsrio.com/cio-bg-transparent.png" alt="ClassroomIO Logo" /></a>
|
|
||||||
</div>
|
|
||||||
<div class="wrap" style="
|
|
||||||
background-color: #fff;
|
|
||||||
padding: 40px 30px;
|
|
||||||
max-width: 525px;
|
|
||||||
margin: 0 auto;
|
|
||||||
">
|
|
||||||
${content}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer" style="
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #fff;
|
|
||||||
padding: 30px;
|
|
||||||
background-color: #0542CC;
|
|
||||||
max-width: 525px;
|
|
||||||
margin: 0 auto;">
|
|
||||||
<p>Find us on</p>
|
|
||||||
<table class="socialicons">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a target="_blank" href="https://twitter.com/classroomio"><img title="X"
|
|
||||||
src="https://brand.cdn.clsrio.com/socials/twitter.png" alt="X" width="19" /></a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a target="_blank" href="https://www.youtube.com/@ClassroomIO"><img title="Youtube"
|
|
||||||
src="https://brand.cdn.clsrio.com/socials/youtube.png" alt="Youtube" width="24" /></a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a target="_blank" href="https://www.facebook.com/classroomiohq"><img title="Facebook"
|
|
||||||
src="https://brand.cdn.clsrio.com/socials/facebook.png" alt="Facebook" width="24" /></a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<p style="padding-top: 8px; line-height: initial">
|
|
||||||
ClassroomIO ${new Date().getFullYear()}. All rights reserved.<br />
|
|
||||||
<a style="text-decoration: none" href="https://app.enzuzo.com/policies/tos/958fc978-5477-11ee-a03b-7b111830c594"
|
|
||||||
target="_blank">Terms</a>
|
|
||||||
|
|
|
||||||
<a style="text-decoration: none"
|
|
||||||
href="https://app.enzuzo.com/policies/privacy/958fc978-5477-11ee-a03b-7b111830c594" target="_blank">Privacy
|
|
||||||
Policy</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import {
|
|
||||||
DeleteObjectCommand,
|
|
||||||
DeleteObjectCommandInput,
|
|
||||||
GetObjectCommand,
|
|
||||||
GetObjectCommandInput,
|
|
||||||
PutObjectCommand,
|
|
||||||
PutObjectCommandInput,
|
|
||||||
S3Client
|
|
||||||
} from '@aws-sdk/client-s3';
|
|
||||||
|
|
||||||
import { CLOUDFLARE } from '$src/constants';
|
|
||||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
||||||
|
|
||||||
export type GetSignedUrlParameters = Parameters<typeof getSignedUrl>;
|
|
||||||
|
|
||||||
export const s3Client = new S3Client({
|
|
||||||
region: 'auto',
|
|
||||||
endpoint: `https://${CLOUDFLARE.CONFIGS.ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
|
||||||
credentials: {
|
|
||||||
accessKeyId: CLOUDFLARE.CONFIGS.ACCESS_KEY,
|
|
||||||
secretAccessKey: CLOUDFLARE.CONFIGS.SECRET_ACCESS_KEY
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function uploadToS3(
|
|
||||||
params: PutObjectCommandInput
|
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
|
||||||
try {
|
|
||||||
await s3Client.send(new PutObjectCommand(params));
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getFromS3(
|
|
||||||
params: GetObjectCommandInput
|
|
||||||
): Promise<{ success: boolean; data?: any; error?: string }> {
|
|
||||||
try {
|
|
||||||
const data = await s3Client.send(new GetObjectCommand(params));
|
|
||||||
return { success: true, data };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteFromS3(
|
|
||||||
params: DeleteObjectCommandInput
|
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
|
||||||
try {
|
|
||||||
await s3Client.send(new DeleteObjectCommand(params));
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { SupabaseClient, createClient } from '@supabase/supabase-js';
|
|
||||||
|
|
||||||
import { env } from '$src/config/env';
|
|
||||||
|
|
||||||
const { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } = env;
|
|
||||||
|
|
||||||
let supabase: SupabaseClient | null = null;
|
|
||||||
|
|
||||||
interface SupabaseConfig {
|
|
||||||
supabaseConfig: {
|
|
||||||
url: string;
|
|
||||||
anonKey: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSupabase(config?: SupabaseConfig): SupabaseClient {
|
|
||||||
if (supabase) return supabase;
|
|
||||||
|
|
||||||
const finalConfig = config ?? {
|
|
||||||
supabaseConfig: {
|
|
||||||
url: PUBLIC_SUPABASE_URL,
|
|
||||||
anonKey: PUBLIC_SUPABASE_ANON_KEY
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
supabase = createClient(finalConfig.supabaseConfig.url, finalConfig.supabaseConfig.anonKey, {
|
|
||||||
auth: { persistSession: false }
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error starting supabase', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return supabase;
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { nanoid } from 'nanoid';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely extracts the file extension from a filename.
|
|
||||||
* Returns an empty string if no extension is found.
|
|
||||||
*/
|
|
||||||
export function getExtension(filename: string): string {
|
|
||||||
if (!filename || typeof filename !== 'string') return '';
|
|
||||||
const ext = path.extname(filename).split('.');
|
|
||||||
return ext.length > 1 ? ext[ext.length - 1] : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a unique file key for storage, preserving the file extension.
|
|
||||||
* Throws an error if the filename is invalid or has no extension.
|
|
||||||
*/
|
|
||||||
export function generateFileKey(fileName: string): string {
|
|
||||||
const ext = getExtension(fileName);
|
|
||||||
if (!ext) {
|
|
||||||
throw new Error('Invalid file name or missing file extension');
|
|
||||||
}
|
|
||||||
return `${nanoid()}.${ext}`;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { defineConfig } from 'vitest/config';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
test: {
|
|
||||||
globals: true,
|
|
||||||
environment: 'node',
|
|
||||||
coverage: {
|
|
||||||
provider: 'v8',
|
|
||||||
reporter: ['text', 'json', 'html'],
|
|
||||||
exclude: ['node_modules/', 'dist/']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# ----------- Build Stage -----------
|
|
||||||
FROM node:18.18.0-alpine AS builder
|
|
||||||
|
|
||||||
# Add build arguments
|
|
||||||
ARG PUBLIC_SUPABASE_URL
|
|
||||||
ARG PUBLIC_SUPABASE_ANON_KEY
|
|
||||||
ARG PRIVATE_SUPABASE_SERVICE_ROLE
|
|
||||||
ARG PRIVATE_APP_HOST
|
|
||||||
ARG PRIVATE_APP_SUBDOMAINS
|
|
||||||
ARG UNSPLASH_API_KEY
|
|
||||||
ARG OPENAI_API_KEY
|
|
||||||
ARG PUBLIC_SERVER_URL
|
|
||||||
ARG PUBLIC_IP_REGISTRY_KEY
|
|
||||||
|
|
||||||
WORKDIR /cio
|
|
||||||
|
|
||||||
# Install pnpm
|
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
|
||||||
|
|
||||||
# Copy monorepo files
|
|
||||||
COPY pnpm-lock.yaml package.json pnpm-workspace.yaml ./
|
|
||||||
COPY apps/dashboard/package.json apps/dashboard/
|
|
||||||
COPY packages/tsconfig ./packages/tsconfig
|
|
||||||
COPY packages/shared ./packages/shared
|
|
||||||
|
|
||||||
# Install all dependencies (monorepo context)
|
|
||||||
RUN pnpm install
|
|
||||||
|
|
||||||
# Copy the rest of the dashboard app
|
|
||||||
COPY apps/dashboard ./apps/dashboard
|
|
||||||
|
|
||||||
# Set env vars for SvelteKit to use Node adapter
|
|
||||||
ENV PUBLIC_IS_SELFHOSTED=true \
|
|
||||||
PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL \
|
|
||||||
PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY \
|
|
||||||
PRIVATE_SUPABASE_SERVICE_ROLE=$PRIVATE_SUPABASE_SERVICE_ROLE \
|
|
||||||
PRIVATE_APP_HOST=$PRIVATE_APP_HOST \
|
|
||||||
PRIVATE_APP_SUBDOMAINS=$PRIVATE_APP_SUBDOMAINS \
|
|
||||||
UNSPLASH_API_KEY=$UNSPLASH_API_KEY \
|
|
||||||
OPENAI_API_KEY=$OPENAI_API_KEY \
|
|
||||||
PUBLIC_SERVER_URL=$PUBLIC_SERVER_URL \
|
|
||||||
PUBLIC_IP_REGISTRY_KEY=$PUBLIC_IP_REGISTRY_KEY
|
|
||||||
|
|
||||||
# Build the dashboard app
|
|
||||||
WORKDIR /cio/apps/dashboard
|
|
||||||
RUN pnpm install
|
|
||||||
RUN NODE_OPTIONS=--max-old-space-size=4096 pnpm build
|
|
||||||
|
|
||||||
# ----------- Production Stage -----------
|
|
||||||
FROM node:18.18.0-alpine AS runner
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install pnpm
|
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
|
||||||
|
|
||||||
# Copy only production node_modules for dashboard
|
|
||||||
COPY --from=builder ./cio/node_modules ./node_modules
|
|
||||||
COPY --from=builder ./cio/package.json ./package.json
|
|
||||||
COPY --from=builder ./cio/apps/dashboard/node_modules ./apps/dashboard/node_modules
|
|
||||||
COPY --from=builder ./cio/apps/dashboard/package.json ./apps/dashboard/package.json
|
|
||||||
COPY --from=builder ./cio/apps/dashboard/build ./apps/dashboard/build
|
|
||||||
|
|
||||||
# Expose the port (default for SvelteKit Node adapter)
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
# Set environment variables for production
|
|
||||||
ENV NODE_ENV=production \
|
|
||||||
PUBLIC_IS_SELFHOSTED=true \
|
|
||||||
PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL \
|
|
||||||
PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY \
|
|
||||||
PRIVATE_SUPABASE_SERVICE_ROLE=$PRIVATE_SUPABASE_SERVICE_ROLE \
|
|
||||||
PRIVATE_APP_HOST=$PRIVATE_APP_HOST \
|
|
||||||
PRIVATE_APP_SUBDOMAINS=$PRIVATE_APP_SUBDOMAINS \
|
|
||||||
UNSPLASH_API_KEY=$UNSPLASH_API_KEY \
|
|
||||||
OPENAI_API_KEY=$OPENAI_API_KEY \
|
|
||||||
PUBLIC_SERVER_URL=$PUBLIC_SERVER_URL \
|
|
||||||
PUBLIC_IP_REGISTRY_KEY=$PUBLIC_IP_REGISTRY_KEY
|
|
||||||
|
|
||||||
# Start the app
|
|
||||||
CMD ["pnpm", "dashboard:start"]
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
|
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
|
||||||
|
|
||||||
import type { CurrentOrg } from '$lib/utils/types/org';
|
import type { CurrentOrg } from '$lib/utils/types/org';
|
||||||
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
|
|
||||||
import type { MetaTagsProps } from 'svelte-meta-tags';
|
import type { MetaTagsProps } from 'svelte-meta-tags';
|
||||||
|
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
|
||||||
import { blockedSubdomain } from '$lib/utils/constants/app';
|
import { blockedSubdomain } from '$lib/utils/constants/app';
|
||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
|
|||||||
@@ -140,8 +140,8 @@ docker compose up --build
|
|||||||
|
|
||||||
The services will be available at:
|
The services will be available at:
|
||||||
|
|
||||||
- Dashboard: http://localhost:3000
|
- Dashboard: http://localhost:3082
|
||||||
- Backend: http://localhost:3002
|
- Backend: http://localhost:3081
|
||||||
|
|
||||||
### Service Configuration
|
### Service Configuration
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ The docker-compose file sets up two services:
|
|||||||
|
|
||||||
#### Backend Service
|
#### Backend Service
|
||||||
|
|
||||||
- Runs on port 3002
|
- Runs on port 3081
|
||||||
- Uses Node.js slim image
|
- Uses Node.js slim image
|
||||||
- Mounts the api code directory
|
- Mounts the api code directory
|
||||||
- Requires environment variables for:
|
- Requires environment variables for:
|
||||||
@@ -159,7 +159,7 @@ The docker-compose file sets up two services:
|
|||||||
|
|
||||||
#### Dashboard Service
|
#### Dashboard Service
|
||||||
|
|
||||||
- Runs on port 3000
|
- Runs on port 3082
|
||||||
- Uses Node.js Alpine image
|
- Uses Node.js Alpine image
|
||||||
- Uses pnpm for package management
|
- Uses pnpm for package management
|
||||||
- Requires environment variables for:
|
- Requires environment variables for:
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# This .dockerignore is a symlink reference
|
||||||
|
# The actual .dockerignore is in the root directory
|
||||||
|
# Docker will use the root .dockerignore when building from the docker/ directory
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
FROM node:20.19.3-slim AS app
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install pnpm, turbo and healthcheck tools
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
RUN npm install -g [email protected]
|
||||||
|
|
||||||
|
COPY package.json ./
|
||||||
|
COPY pnpm-lock.yaml ./
|
||||||
|
COPY pnpm-workspace.yaml ./
|
||||||
|
COPY turbo.json ./
|
||||||
|
COPY packages/tsconfig ./packages/tsconfig
|
||||||
|
COPY packages/shared ./packages/shared
|
||||||
|
|
||||||
|
# Copy the api package.json
|
||||||
|
COPY apps/api/package.json ./apps/api/package.json
|
||||||
|
|
||||||
|
RUN pnpm install
|
||||||
|
|
||||||
|
# Copy app source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN pnpm build --filter @cio/api
|
||||||
|
|
||||||
|
EXPOSE 3081
|
||||||
|
|
||||||
|
RUN ls -la
|
||||||
|
|
||||||
|
CMD ["pnpm", "api:start"]
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# ----------- Build Stage -----------
|
||||||
|
FROM node:20.19.3-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /cio
|
||||||
|
# Install pnpm globally so it's available in the final container
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
|
# Copy everything (filtered by .dockerignore)
|
||||||
|
COPY package.json ./
|
||||||
|
COPY pnpm-lock.yaml ./
|
||||||
|
COPY pnpm-workspace.yaml ./
|
||||||
|
COPY turbo.json ./
|
||||||
|
COPY packages/tsconfig ./packages/tsconfig
|
||||||
|
COPY packages/shared ./packages/shared
|
||||||
|
|
||||||
|
# Copy the api package.json
|
||||||
|
COPY apps/dashboard ./apps/dashboard
|
||||||
|
COPY apps/api ./apps/api
|
||||||
|
|
||||||
|
# Install all dependencies (monorepo context)
|
||||||
|
RUN pnpm install
|
||||||
|
|
||||||
|
RUN ls -la
|
||||||
|
|
||||||
|
# Build the dashboard app
|
||||||
|
RUN NODE_OPTIONS="--max-old-space-size=3072" PUBLIC_IS_SELFHOSTED=true pnpm build --filter @cio/dashboard
|
||||||
|
|
||||||
|
# ----------- Production Stage -----------
|
||||||
|
FROM node:20.19.3-alpine AS runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install pnpm and turbo before switching to non-root user
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
RUN npm install -g [email protected]
|
||||||
|
|
||||||
|
# Don't run production as root
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 cio-user
|
||||||
|
USER cio-user
|
||||||
|
|
||||||
|
# Copy the entire dashboard app (including build output)
|
||||||
|
COPY --from=builder --chown=cio-user:nodejs ./cio/apps/dashboard ./apps/dashboard
|
||||||
|
COPY --from=builder --chown=cio-user:nodejs ./cio/node_modules ./node_modules
|
||||||
|
COPY --from=builder --chown=cio-user:nodejs ./cio/package.json ./package.json
|
||||||
|
COPY --from=builder --chown=cio-user:nodejs ./cio/turbo.json ./turbo.json
|
||||||
|
COPY --from=builder --chown=cio-user:nodejs ./cio/pnpm-workspace.yaml ./pnpm-workspace.yaml
|
||||||
|
|
||||||
|
# Expose the port (default for SvelteKit Node adapter)
|
||||||
|
EXPOSE 3082
|
||||||
|
|
||||||
|
# Set environment variables for production
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
PUBLIC_IS_SELFHOSTED=true
|
||||||
|
|
||||||
|
# Start the app
|
||||||
|
CMD ["pnpm", "dashboard:start"]
|
||||||
@@ -8,28 +8,9 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
api:
|
api:
|
||||||
build:
|
image: classroomio/api:latest
|
||||||
context: ./apps/api
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
- PUBLIC_SUPABASE_ANON_KEY=${PUBLIC_SUPABASE_ANON_KEY}
|
|
||||||
- PUBLIC_SUPABASE_URL=${PUBLIC_SUPABASE_URL}
|
|
||||||
- CLOUDFLARE_BUCKET_DOMAIN=${CLOUDFLARE_BUCKET_DOMAIN}
|
|
||||||
- CLOUDFLARE_ACCESS_KEY=${CLOUDFLARE_ACCESS_KEY}
|
|
||||||
- CLOUDFLARE_SECRET_ACCESS_KEY=${CLOUDFLARE_SECRET_ACCESS_KEY}
|
|
||||||
- CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID}
|
|
||||||
- CLOUDFLARE_BUCKET_ID=${CLOUDFLARE_BUCKET_ID}
|
|
||||||
- SMTP_HOST=${SMTP_HOST}
|
|
||||||
- SMTP_USER=${SMTP_USER}
|
|
||||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
|
||||||
- SMTP_PORT=${SMTP_PORT}
|
|
||||||
ports:
|
ports:
|
||||||
- "3002:3002"
|
- "3081:3081"
|
||||||
volumes:
|
|
||||||
# Node modules (optional - for faster development)
|
|
||||||
- ./apps/api/node_modules:/app/node_modules
|
|
||||||
# Build output directory (if you want to access built files)
|
|
||||||
- ./apps/api/dist:/app/dist
|
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
# Supabase - Database
|
# Supabase - Database
|
||||||
@@ -46,27 +27,13 @@ services:
|
|||||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||||
- SMTP_PORT=${SMTP_PORT}
|
- SMTP_PORT=${SMTP_PORT}
|
||||||
- SMTP_SENDER=${SMTP_SENDER}
|
- SMTP_SENDER=${SMTP_SENDER}
|
||||||
|
# Redis
|
||||||
|
- REDIS_URL=${REDIS_URL}
|
||||||
|
|
||||||
dashboard:
|
dashboard:
|
||||||
build:
|
image: classroomio/dashboard:latest
|
||||||
context: .
|
|
||||||
dockerfile: ./apps/dashboard/Dockerfile
|
|
||||||
args:
|
|
||||||
- PUBLIC_SUPABASE_URL=${PUBLIC_SUPABASE_URL}
|
|
||||||
- PUBLIC_SUPABASE_ANON_KEY=${PUBLIC_SUPABASE_ANON_KEY}
|
|
||||||
- PRIVATE_SUPABASE_SERVICE_ROLE=${PRIVATE_SUPABASE_SERVICE_ROLE}
|
|
||||||
- PRIVATE_APP_HOST=${PRIVATE_APP_HOST}
|
|
||||||
- PRIVATE_APP_SUBDOMAINS=${PRIVATE_APP_SUBDOMAINS}
|
|
||||||
- UNSPLASH_API_KEY=${UNSPLASH_API_KEY}
|
|
||||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
|
||||||
- PUBLIC_SERVER_URL=${PUBLIC_SERVER_URL}
|
|
||||||
- PUBLIC_IP_REGISTRY_KEY=${PUBLIC_IP_REGISTRY_KEY}
|
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3082:3082"
|
||||||
volumes:
|
|
||||||
- .:/app
|
|
||||||
- /app/node_modules
|
|
||||||
- /app/apps/dashboard/node_modules
|
|
||||||
environment:
|
environment:
|
||||||
- PUBLIC_IS_SELFHOSTED=true
|
- PUBLIC_IS_SELFHOSTED=true
|
||||||
# Supabase - Database
|
# Supabase - Database
|
||||||
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Docker Hub Push Script for ClassroomIO
|
||||||
|
# This script builds and pushes the API and Dashboard images to Docker Hub
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DOCKERHUB_USERNAME="${DOCKERHUB_USERNAME:-classroomio}" # Change this to your Docker Hub username
|
||||||
|
VERSION="${VERSION:-latest}" # Can be overridden with VERSION env var
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${BLUE}================================${NC}"
|
||||||
|
echo -e "${BLUE}ClassroomIO Docker Hub Publisher${NC}"
|
||||||
|
echo -e "${BLUE}================================${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build and push API
|
||||||
|
echo -e "${GREEN}Building API image...${NC}"
|
||||||
|
docker build \
|
||||||
|
-f docker/Dockerfile.api \
|
||||||
|
-t ${DOCKERHUB_USERNAME}/api:${VERSION} \
|
||||||
|
-t ${DOCKERHUB_USERNAME}/api:latest \
|
||||||
|
.
|
||||||
|
|
||||||
|
echo -e "${GREEN}Pushing API image to Docker Hub...${NC}"
|
||||||
|
docker push ${DOCKERHUB_USERNAME}/api:${VERSION}
|
||||||
|
docker push ${DOCKERHUB_USERNAME}/api:latest
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ API image published successfully!${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build and push Dashboard
|
||||||
|
echo -e "${GREEN}Building Dashboard image...${NC}"
|
||||||
|
docker build \
|
||||||
|
-f docker/Dockerfile.dashboard \
|
||||||
|
-t ${DOCKERHUB_USERNAME}/dashboard:${VERSION} \
|
||||||
|
-t ${DOCKERHUB_USERNAME}/dashboard:latest \
|
||||||
|
.
|
||||||
|
|
||||||
|
echo -e "${GREEN}Pushing Dashboard image to Docker Hub...${NC}"
|
||||||
|
docker push ${DOCKERHUB_USERNAME}/dashboard:${VERSION}
|
||||||
|
docker push ${DOCKERHUB_USERNAME}/dashboard:latest
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Dashboard image published successfully!${NC}"
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo -e "${BLUE}================================${NC}"
|
||||||
|
echo -e "${GREEN}All images published successfully!${NC}"
|
||||||
|
echo -e "${BLUE}================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "API Image: ${DOCKERHUB_USERNAME}/api:${VERSION}"
|
||||||
|
echo "Dashboard Image: ${DOCKERHUB_USERNAME}/dashboard:${VERSION}"
|
||||||
|
echo ""
|
||||||
|
echo "To pull these images:"
|
||||||
|
echo " docker pull ${DOCKERHUB_USERNAME}/api:${VERSION}"
|
||||||
|
echo " docker pull ${DOCKERHUB_USERNAME}/dashboard:${VERSION}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
# Publishing ClassroomIO Docker Images to Docker Hub
|
||||||
|
|
||||||
|
This guide explains how to publish the ClassroomIO API and Dashboard Docker images to Docker Hub.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. **Docker Hub Account**: Create an account at https://hub.docker.com if you don't have one
|
||||||
|
2. **Docker Installed**: Make sure Docker is installed and running on your machine
|
||||||
|
3. **Docker Hub Access Token**: Create a personal access token at https://hub.docker.com/settings/security
|
||||||
|
|
||||||
|
## Method 1: Manual Publishing (Local)
|
||||||
|
|
||||||
|
### Step 1: Login to Docker Hub
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker login
|
||||||
|
# Enter your Docker Hub username and password/token when prompted
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Build and Push Images
|
||||||
|
|
||||||
|
Use the provided script to build and push both images:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default (uses 'classroomio' as username and 'latest' as version)
|
||||||
|
./docker/docker-push.sh
|
||||||
|
|
||||||
|
# Custom username
|
||||||
|
DOCKERHUB_USERNAME=your-username ./docker/docker-push.sh
|
||||||
|
|
||||||
|
# Specific version
|
||||||
|
VERSION=v0.1.0 ./docker/docker-push.sh
|
||||||
|
|
||||||
|
# Both custom username and version
|
||||||
|
DOCKERHUB_USERNAME=classroomio VERSION=v0.1.0 ./docker/docker-push.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Verify Images
|
||||||
|
|
||||||
|
Check your Docker Hub repository at:
|
||||||
|
- https://hub.docker.com/r/classroomio/api
|
||||||
|
- https://hub.docker.com/r/classroomio/dashboard
|
||||||
|
|
||||||
|
## Method 2: GitHub Actions (Automated)
|
||||||
|
|
||||||
|
### Setup (One-time)
|
||||||
|
|
||||||
|
1. **Add Docker Hub Secrets to GitHub**:
|
||||||
|
- Go to your GitHub repository → Settings → Secrets and variables → Actions
|
||||||
|
- Add two secrets:
|
||||||
|
- `DOCKERHUB_USERNAME`: Your Docker Hub username
|
||||||
|
- `DOCKERHUB_TOKEN`: Your Docker Hub access token
|
||||||
|
|
||||||
|
2. **Push the workflow file** (already created at `.github/workflows/docker-publish.yml`)
|
||||||
|
|
||||||
|
### Publishing via GitHub Actions
|
||||||
|
|
||||||
|
The workflow will automatically publish images when:
|
||||||
|
|
||||||
|
1. **Push to main/master branch**: Creates a `latest` tag
|
||||||
|
2. **Create a version tag**: Creates versioned tags
|
||||||
|
3. **Manual trigger**: Use the Actions tab in GitHub
|
||||||
|
|
||||||
|
#### Create a version tag and push:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v0.1.0
|
||||||
|
git push origin v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Manual trigger from GitHub:
|
||||||
|
|
||||||
|
1. Go to Actions tab in your repository
|
||||||
|
2. Select "Publish Docker Images" workflow
|
||||||
|
3. Click "Run workflow"
|
||||||
|
4. Enter optional version tag (or leave default 'latest')
|
||||||
|
|
||||||
|
## Method 3: Individual Image Building
|
||||||
|
|
||||||
|
### Build API Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f docker/Dockerfile.api -t classroomio/api:latest .
|
||||||
|
docker push classroomio/api:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Dashboard Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f docker/Dockerfile.dashboard -t classroomio/dashboard:latest .
|
||||||
|
docker push classroomio/dashboard:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Published Images
|
||||||
|
|
||||||
|
### Pull Images
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull classroomio/api:latest
|
||||||
|
docker pull classroomio/dashboard:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update docker-compose.yml
|
||||||
|
|
||||||
|
Instead of building locally, use the published images:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
image: classroomio/api:latest
|
||||||
|
# Remove the 'build' section
|
||||||
|
ports:
|
||||||
|
- "3081:3081"
|
||||||
|
environment:
|
||||||
|
# ... your environment variables
|
||||||
|
|
||||||
|
dashboard:
|
||||||
|
image: classroomio/dashboard:latest
|
||||||
|
# Remove the 'build' section
|
||||||
|
ports:
|
||||||
|
- "3082:3082"
|
||||||
|
environment:
|
||||||
|
# ... your environment variables
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning Strategy
|
||||||
|
|
||||||
|
### Recommended Tags:
|
||||||
|
|
||||||
|
- `latest`: Most recent stable build
|
||||||
|
- `v0.1.0`: Specific semantic version
|
||||||
|
- `v1.0`: Minor version
|
||||||
|
- `v1`: Major version
|
||||||
|
- `main`: Latest main branch build
|
||||||
|
- `dev`: Development builds
|
||||||
|
|
||||||
|
### Example: Publishing a new version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build with multiple tags
|
||||||
|
docker build -f docker/Dockerfile.api \
|
||||||
|
-t classroomio/api:latest \
|
||||||
|
-t classroomio/api:v1.2.0 \
|
||||||
|
-t classroomio/api:v1.2 \
|
||||||
|
-t classroomio/api:v1 \
|
||||||
|
.
|
||||||
|
|
||||||
|
# Push all tags
|
||||||
|
docker push classroomio/api:latest
|
||||||
|
docker push classroomio/api:v1.2.0
|
||||||
|
docker push classroomio/api:v1.2
|
||||||
|
docker push classroomio/api:v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image Sizes and Build Times
|
||||||
|
|
||||||
|
- **API Image**: ~400-600 MB (Node.js Alpine + dependencies)
|
||||||
|
- **Dashboard Image**: ~600-800 MB (Node.js Alpine + SvelteKit build)
|
||||||
|
- **Build Time**: 5-10 minutes per image (depending on your machine)
|
||||||
|
|
||||||
|
## Multi-Architecture Support
|
||||||
|
|
||||||
|
The GitHub Actions workflow builds for both `linux/amd64` and `linux/arm64` platforms, making the images compatible with:
|
||||||
|
- x86_64 servers
|
||||||
|
- ARM-based servers (like AWS Graviton)
|
||||||
|
- Apple Silicon Macs (M1/M2/M3)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "unauthorized: authentication required"
|
||||||
|
- Run `docker login` again
|
||||||
|
- Check your Docker Hub token hasn't expired
|
||||||
|
|
||||||
|
### "denied: requested access to the resource is denied"
|
||||||
|
- Make sure you're pushing to your own username
|
||||||
|
- Repository might need to be created first on Docker Hub
|
||||||
|
|
||||||
|
### Build failures
|
||||||
|
- Check Dockerfile syntax
|
||||||
|
- Ensure all build arguments are provided
|
||||||
|
- Check available disk space: `docker system df`
|
||||||
|
|
||||||
|
### Clean up old images
|
||||||
|
```bash
|
||||||
|
# Remove dangling images
|
||||||
|
docker image prune
|
||||||
|
|
||||||
|
# Remove all unused images
|
||||||
|
docker image prune -a
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Never commit secrets** to the repository
|
||||||
|
2. **Use Docker Hub access tokens** instead of passwords
|
||||||
|
3. **Enable 2FA** on your Docker Hub account
|
||||||
|
4. **Scan images** for vulnerabilities:
|
||||||
|
```bash
|
||||||
|
docker scan classroomio/api:latest
|
||||||
|
```
|
||||||
|
5. **Use specific version tags** in production, not `latest`
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues with publishing, please check:
|
||||||
|
- Docker Hub status: https://status.docker.com
|
||||||
|
- GitHub Actions logs in your repository
|
||||||
|
- Docker daemon logs: `docker info`
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Docker Hub Documentation](https://docs.docker.com/docker-hub/)
|
||||||
|
- [GitHub Actions for Docker](https://docs.github.com/en/actions/publishing-packages/publishing-docker-images)
|
||||||
|
- [Docker Build Documentation](https://docs.docker.com/engine/reference/commandline/build/)
|
||||||
|
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# Docker Usage Guide
|
||||||
|
|
||||||
|
This directory contains all Docker-related files for the ClassroomIO project.
|
||||||
|
|
||||||
|
## 📁 Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
docker/
|
||||||
|
├── README.md # Full publishing guide
|
||||||
|
├── USAGE.md # This file - quick usage guide
|
||||||
|
├── commands.md # Quick command reference
|
||||||
|
├── docker-compose.yaml # Docker Compose configuration
|
||||||
|
├── docker-push.sh # Script to publish images to Docker Hub
|
||||||
|
├── Dockerfile.api # API service Dockerfile
|
||||||
|
└── Dockerfile.dashboard # Dashboard service Dockerfile
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Using Docker Compose (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the project root
|
||||||
|
cd docker
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Or from project root
|
||||||
|
docker-compose -f docker/docker-compose.yaml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building Individual Images
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From project root
|
||||||
|
docker build -f docker/Dockerfile.api -t api .
|
||||||
|
docker build -f docker/Dockerfile.dashboard -t dashboard .
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Publishing to Docker Hub
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From project root
|
||||||
|
./docker/docker-push.sh
|
||||||
|
|
||||||
|
# With custom options
|
||||||
|
DOCKERHUB_USERNAME=your-username VERSION=v1.0.0 ./docker/docker-push.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Common Commands
|
||||||
|
|
||||||
|
### Start Services
|
||||||
|
```bash
|
||||||
|
cd docker && docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop Services
|
||||||
|
```bash
|
||||||
|
cd docker && docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
```bash
|
||||||
|
cd docker && docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild Services
|
||||||
|
```bash
|
||||||
|
cd docker && docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
- **README.md**: Complete Docker Hub publishing guide
|
||||||
|
- **commands.md**: Quick command reference
|
||||||
|
- **GitHub Actions**: Automated publishing via `.github/workflows/docker-publish.yml`
|
||||||
|
|
||||||
|
## 🌍 Environment Variables
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` in the project root and configure:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp ../.env.example ../.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Required variables are listed in `docker-compose.yaml`.
|
||||||
|
|
||||||
|
## 🔗 Published Images
|
||||||
|
|
||||||
|
- **API**: `classroomio/api` or `classroomio/api`
|
||||||
|
- **Dashboard**: `classroomio/dashboard` or `classroomio/dashboard`
|
||||||
|
|
||||||
|
Pull from Docker Hub:
|
||||||
|
```bash
|
||||||
|
docker pull classroomio/api:latest
|
||||||
|
docker pull classroomio/dashboard:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 Tips
|
||||||
|
|
||||||
|
1. **Always run builds from project root** - the context needs to be the entire monorepo
|
||||||
|
2. **Use docker-compose** for local development
|
||||||
|
3. **Use published images** for production deployments
|
||||||
|
4. **Check logs** if services fail to start: `docker-compose logs -f`
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Services won't start
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Rebuild from scratch
|
||||||
|
docker-compose down
|
||||||
|
docker-compose build --no-cache
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port conflicts
|
||||||
|
```bash
|
||||||
|
# Check if ports are in use
|
||||||
|
lsof -i :3081 # API
|
||||||
|
lsof -i :3082 # Dashboard
|
||||||
|
|
||||||
|
# Change ports in docker-compose.yaml if needed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build failures
|
||||||
|
```bash
|
||||||
|
# Clean up Docker
|
||||||
|
docker system prune -a
|
||||||
|
|
||||||
|
# Try building again
|
||||||
|
docker-compose build --no-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
For more detailed information, see:
|
||||||
|
- `README.md` - Full Docker Hub publishing guide
|
||||||
|
- `commands.md` - Quick command reference
|
||||||
|
- Project main README for general setup
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Quick Docker Commands Reference
|
||||||
|
|
||||||
|
## Login to Docker Hub
|
||||||
|
```bash
|
||||||
|
docker login
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Images Locally
|
||||||
|
|
||||||
|
### API
|
||||||
|
```bash
|
||||||
|
docker build -f ./docker/Dockerfile.api -t classroomio/api:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
```bash
|
||||||
|
docker build -f ./docker/Dockerfile.dashboard -t classroomio/dashboard:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Push to Docker Hub
|
||||||
|
|
||||||
|
### API
|
||||||
|
```bash
|
||||||
|
docker push classroomio/api:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
```bash
|
||||||
|
docker push classroomio/dashboard:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## All in One Script
|
||||||
|
```bash
|
||||||
|
# Make script executable (first time only)
|
||||||
|
chmod +x docker-push.sh
|
||||||
|
|
||||||
|
# Run the script
|
||||||
|
./docker-push.sh
|
||||||
|
|
||||||
|
# With custom username
|
||||||
|
DOCKERHUB_USERNAME=your-username ./docker-push.sh
|
||||||
|
|
||||||
|
# With version tag
|
||||||
|
VERSION=v1.0.0 ./docker-push.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pull Published Images
|
||||||
|
```bash
|
||||||
|
docker pull classroomio/api:latest
|
||||||
|
docker pull classroomio/dashboard:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run Published Images
|
||||||
|
```bash
|
||||||
|
# API
|
||||||
|
docker run -d -p 3081:3081 \
|
||||||
|
-e PUBLIC_SUPABASE_ANON_KEY=your_key \
|
||||||
|
-e PUBLIC_SUPABASE_URL=your_url \
|
||||||
|
classroomio/api:latest
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
docker run -d -p 3082:3082 \
|
||||||
|
-e PUBLIC_SUPABASE_ANON_KEY=your_key \
|
||||||
|
-e PUBLIC_SUPABASE_URL=your_url \
|
||||||
|
classroomio/dashboard:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful Commands
|
||||||
|
|
||||||
|
### Check local images
|
||||||
|
```bash
|
||||||
|
docker images | grep classroomio
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove local images
|
||||||
|
```bash
|
||||||
|
docker rmi classroomio/api:latest
|
||||||
|
docker rmi classroomio/dashboard:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### View image details
|
||||||
|
```bash
|
||||||
|
docker inspect classroomio/api:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check image size
|
||||||
|
```bash
|
||||||
|
docker images classroomio/api:latest --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
|
||||||
|
```
|
||||||
|
|
||||||
+3
-2
@@ -3,14 +3,15 @@
|
|||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
|
"start": "turbo run start",
|
||||||
"dev": "turbo prepare --concurrency 13 && turbo run dev",
|
"dev": "turbo prepare --concurrency 13 && turbo run dev",
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
"clean": "turbo clean && rm -rf node_modules",
|
"clean": "turbo clean && rm -rf node_modules",
|
||||||
"format": "prettier . --write",
|
"format": "prettier . --write",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"ci": "cypress run",
|
"ci": "cypress run",
|
||||||
"dashboard:build": "cd apps/dashboard && pnpm i && PUBLIC_IS_SELFHOSTED=true NODE_OPTIONS='--max-old-space-size=20384' pnpm build",
|
"dashboard:start": "PORT=3082 pnpm start --filter @cio/dashboard",
|
||||||
"dashboard:start": "cd apps/dashboard && node build",
|
"api:start": "PORT=3081 pnpm start --filter @cio/api",
|
||||||
"supabase:push": "pnpx supabase link --project-ref $PROJECT_ID && pnpx supabase db push --include-all"
|
"supabase:push": "pnpx supabase link --project-ref $PROJECT_ID && pnpx supabase db push --include-all"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"outputs": [".next/**", "!.next/cache/**", ".svelte-kit/**", ".vercel/**", "dist/**"]
|
"outputs": [".next/**", "!.next/cache/**", ".svelte-kit/**", ".vercel/**", "dist/**"]
|
||||||
},
|
},
|
||||||
|
"start": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"dependsOn": ["^build"]
|
"dependsOn": ["^build"]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user