chore: move backend to hono + typescript (#558)

* feat: setup secure presigned uploads and downloads

* clean up

* update lock

* Get presigned url finally working

* 557: Migrate backend to hono + ts

* chore: update Dockerfile and package.json for backend configuration

* add dev command

* remove unused packages

* Enhance backend API and frontend components for course management

- Updated CORS configuration in the backend to allow specific methods and headers.
- Refactored lesson download endpoint to use a new route for PDF downloads.
- Removed unused certificate download functionality from the lesson router.
- Updated frontend components to reflect changes in API endpoints and improve video upload functionality.
- Introduced new video upload component and adjusted video handling in lesson materials.
- Cleaned up unnecessary imports and optimized component structure for better performance.

* PR cleanups

* Enhance email configuration by adding SMTP_SENDER environment variable

- Updated docker-compose.yaml to include SMTP_SENDER.
- Modified Dockerfile to accept SMTP_SENDER as an argument.
- Updated environment schema in env.ts to define SMTP_SENDER as optional.
- Adjusted mail service to use SMTP_SENDER for the email 'from' field.
- Updated documentation to reflect the new SMTP_SENDER variable.

* Remove debug log statement from sendEmail function

---------

Co-authored-by: Iniubong Obonguko <[email protected]>
This commit is contained in:
Rotimi Best
2025-06-09 01:38:57 +01:00
committed by GitHub
co-authored by Iniubong Obonguko
parent 9eb7b01587
commit 82be557a42
80 changed files with 3291 additions and 2302 deletions
@@ -13,6 +13,7 @@
import { fetchProfileCourseProgress } from '$lib/utils/services/courses';
import type { ProfileCourseProgress } from '$lib/utils/types';
import { snackbar } from '$lib/components/Snackbar/store';
import { apiClient } from '$lib/utils/services/api';
let isLoading = false;
let isCourseComplete = false;
@@ -23,24 +24,22 @@
isLoading = true;
try {
const response = await fetch(env.PUBLIC_SERVER_URL + '/downloadCertificate', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
const response = await apiClient.post(
'/course/download/certificate',
{
theme: `${$course.certificate_theme}`,
studentName: `${$profile.fullname}`,
courseName: `${$course.title}`,
courseDescription: `${$course.description}`,
orgLogoUrl: `${$currentOrg.avatar_url}`,
orgName: `${$currentOrg.name}`
})
});
const data = await response.blob();
console.log(data);
const file = new Blob([data], { type: 'application/pdf' });
},
{
responseType: 'blob'
}
);
const file = new Blob([response.data], { type: 'application/pdf' });
const fileURL = URL.createObjectURL(file);
let a = document.createElement('a');
@@ -85,12 +84,12 @@
</script>
<Box>
<div class="flex flex-col items-center justify-center w-max h-full gap-5">
<div class="flex h-full w-max flex-col items-center justify-center gap-5">
<img src="/images/student-certificate-preview.png" alt="Certificate" class="max-w-[218px]" />
<p class="text-xl font-normal text-center">
<p class="text-center text-xl font-normal">
{$t(title)}
</p>
<p class="text-sm font-normal text-center max-w-md">
<p class="max-w-md text-center text-sm font-normal">
{$t(subtitle)}
</p>
<PrimaryButton
@@ -1,4 +1,5 @@
<script>
<script lang="ts">
import type { LessonVideoType } from '$lib/utils/types';
import { isLessonDirty, lesson } from '$lib/components/Course/components/Lesson/store/lessons';
import TextField from '$lib/components/Form/TextField.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
@@ -26,7 +27,7 @@
$lesson.materials.videos = [
...existingLinks,
...validLinks.map((link = '') => ({
type: 'generic',
type: 'generic' as LessonVideoType,
link,
metadata: {}
}))
@@ -1,6 +1,6 @@
<script>
import YoutubeVideo from './YoutubeVideo.svelte';
import LocalVideo from './LocalVideo.svelte';
import UploadVideo from './UploadVideo.svelte';
import EmbedLink from './EmbedLink.svelte';
import * as CONSTANTS from './constants';
import { t } from '$lib/utils/functions/translations';
@@ -9,25 +9,25 @@
export let lessonId = '';
let tabs = CONSTANTS.videoTabs;
const tabs = CONSTANTS.videoTabs;
let currentTab = tabs[0].value;
const onChange = (tab) => () => (currentTab = tab);
</script>
<section class="w-full h-full flex flex-col md:flex-row items-start gap-3">
<div class="flex flex-row md:flex-col gap-2 items-center">
<p class="text-sm font-normal text-start text-[#4F4B4B] dark:text-[#b0a9a9] mb-3 w-full">
<section class="flex h-full w-full flex-col items-start gap-3 md:flex-row">
<div class="flex flex-row items-center gap-2 md:flex-col">
<p class="mb-3 w-full text-start text-sm font-normal text-[#4F4B4B] dark:text-[#b0a9a9]">
{$t('course.navItem.lessons.materials.tabs.video.add_video.add_by')}
</p>
{#each tabs as item (item.value)}
<button
on:click={onChange(item.value)}
class={`w-full px-4 py-3 my-1 border ${
class={`my-1 w-full border px-4 py-3 ${
currentTab === item.value
? 'border border-[#0233BD] bg-[#F5F8FE] dark:text-black'
: 'border border-gray-200 dark:border-neutral-600'
} rounded-md cursor-pointer flex flex-row items-center justify-start gap-2 whitespace-nowrap`}
} flex cursor-pointer flex-row items-center justify-start gap-2 whitespace-nowrap rounded-md`}
>
{#if $isFreePlan && item.value === 3}
<FlashFilled size={20} class="text-blue-700" />
@@ -42,13 +42,13 @@
</button>
{/each}
</div>
<main class="w-full h-full">
<main class="h-full w-full">
{#if currentTab === 1}
<YoutubeVideo />
{:else if currentTab === 2}
<EmbedLink />
{:else}
<LocalVideo {lessonId} />
<UploadVideo {lessonId} />
{/if}
</main>
</section>
@@ -1,69 +1,96 @@
<script lang="ts">
import axios from 'axios';
import { env } from '$env/dynamic/public';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import axios from 'axios';
import {
lesson,
deleteLessonVideo,
uploadCourseVideoStore
} from '$lib/components/Course/components/Lesson/store/lessons';
import { onMount, onDestroy } from 'svelte';
import { type ComponentProps } from 'svelte';
import { ProgressBar } from 'carbon-components-svelte';
import { supabase } from '$lib/utils/functions/supabase';
import { isFreePlan } from '$lib/utils/store/org';
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
let value = 0;
let max = 100;
let status = 'active';
let fileSize;
let isDisabled = false;
import { apiClient } from '$lib/utils/services/api';
import { t } from '$lib/utils/functions/translations';
export let lessonId = '';
let value = 0;
let max = 100;
let status: ComponentProps<ProgressBar>['status'] = 'active';
let fileSize;
let isDisabled = false;
let formRes;
let isLoaded = false;
let fileInput;
let submit;
let uploadedFileUrl = '';
let isLoading = false;
let prevProgress = 0;
let uploadChannel;
let fileName = '';
function isVideoAdded(link) {
return $lesson.materials?.videos?.find((v) => v.link === link);
}
async function onUpload(e) {
isLoading = true;
async function onUpload() {
if (!fileInput) return;
const formData = new FormData();
$uploadCourseVideoStore.isUploading = true;
const videoFile = fileInput.files[0];
formData.append('videoFile', videoFile);
console.log({ size: videoFile?.size });
fileSize = videoFile?.size / (1024 * 1024);
try {
const response = await axios({
method: 'POST',
url: env.PUBLIC_SERVER_URL + '/uploadVideo?lessonId=' + lessonId,
data: formData,
const uploadPresignResponse = await apiClient.post('/course/presign/upload', {
fileName: videoFile.name,
fileType: videoFile.type
});
const presignedUrl = uploadPresignResponse.data.url;
const uploadResponse = await axios.put(presignedUrl, videoFile, {
headers: {
'Content-Type': videoFile.type
},
maxContentLength: Infinity,
maxBodyLength: Infinity,
headers: {
'Content-Type': 'multipart/form-data; boundary=MyBoundary'
},
onUploadProgress: (progressEvent) => {
value = Math.round((progressEvent.loaded * 100) / progressEvent?.total / 2);
value = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 1) / 2);
}
});
formRes = response.data;
console.log('Upload res', formRes);
isLoading = false;
const downloadPresignedResponse = await apiClient.post('/course/presign/download', {
keys: [uploadPresignResponse.data.fileKey]
});
if (!downloadPresignedResponse.data.success) {
formRes = {
type: 'INTERNAL_ERROR',
status: 500,
message: downloadPresignedResponse.data.message
};
return;
}
const fileKey = uploadPresignResponse.data.fileKey;
const presignedUrls = downloadPresignedResponse.data.urls;
formRes = {
url: presignedUrls[fileKey],
fileKey: uploadPresignResponse.data.fileKey,
status: uploadResponse.status
};
console.log('formRes', formRes);
$lesson.materials.videos = [
...$lesson.materials.videos,
{
type: 'upload',
link: formRes.url,
key: formRes?.fileKey
}
];
console.log('Upload res', uploadResponse, 'formRes', formRes);
$uploadCourseVideoStore.isUploading = false;
isLoaded = false;
} catch (err: any) {
console.error('Error uploading video', err, '\n\n', err.response);
@@ -76,50 +103,20 @@
isLoaded = true;
}
async function isDoneUploading(response) {
uploadedFileUrl = response?.success && response?.url;
if (uploadedFileUrl && !isVideoAdded(uploadedFileUrl)) {
$lesson.materials.videos = [
...$lesson.materials.videos,
{
type: 'muse',
link: uploadedFileUrl,
metadata: response?.metadata || {}
}
];
}
}
function tryAgain() {
formRes = null;
isLoaded = false;
isLoading = false;
$uploadCourseVideoStore.isUploading = false;
value = 0;
}
onMount(async () => {
uploadChannel = supabase
.channel('upload-progress')
.on('broadcast', { event: lessonId }, (payload) => {
console.log('The progress of the upload', payload);
value = value + payload?.payload - prevProgress;
prevProgress = payload.payload;
})
.subscribe();
});
onDestroy(() => {
supabase.removeChannel(uploadChannel);
});
$: helperText = value + '% of ' + Math.round(fileSize) + 'MB';
$: if (value === max) {
helperText = 'Done';
status = 'finished';
}
$: isDoneUploading(formRes);
$: isDisabled = isLoading || !env.PUBLIC_SERVER_URL || $isFreePlan;
$: isDisabled = $uploadCourseVideoStore.isUploading || !env.PUBLIC_SERVER_URL || $isFreePlan;
</script>
<UpgradeBanner className="mb-3" onClick={() => ($uploadCourseVideoStore.isModalOpen = false)}>
@@ -129,16 +126,16 @@
{#if !isLoaded}
<button
type="button"
on:click={() => (fileInput && !isLoading ? fileInput.click() : null)}
class="h-full w-full {isDisabled && 'hover:cursor-not-allowed opacity-50'}"
on:click={() => (fileInput && !$uploadCourseVideoStore.isUploading ? fileInput.click() : null)}
class="h-full w-full {isDisabled && 'opacity-50 hover:cursor-not-allowed'}"
disabled={isDisabled}
>
<form
class="border-primary-300 flex h-full w-full flex-col items-center justify-center rounded-xl border border-dashed"
on:submit|preventDefault={onUpload}
>
{#if isLoading}
<div class="flex flex-col gap-5 max-w-[500px] w-[60%] justify-center">
{#if $uploadCourseVideoStore.isUploading}
<div class="flex w-[60%] max-w-[500px] flex-col justify-center gap-5">
<p class="mt-5 text-center">
{$t('course.navItem.lessons.materials.tabs.video.add_video.uploading')}
</p>
@@ -186,7 +183,7 @@
onClick={tryAgain}
/>
</div>
{:else if !formRes?.success}
{:else if formRes?.status !== 200}
<div class="flex h-full w-full flex-col items-center justify-center rounded-xl">
<img src="/video-upload-error.svg" alt="upload error" />
<span class="pb-2 pt-3">
@@ -210,13 +207,11 @@
<span class="flex items-center gap-2">
<div class="overflow-hidden rounded-sm">
<video class="w-[200px]">
<source src={uploadedFileUrl} type="video/mp4" />
<!-- <source src="/path/to/video.webm" type="video/webm" /> -->
<!-- Captions are optional -->
<source src={formRes.url} type="video/mp4" />
<track kind="captions" />
</video>
</div>
<p>{fileInput?.files?.[0]?.name || uploadedFileUrl}</p>
<p>{fileInput?.files?.[0]?.name || fileName}</p>
</span>
<button on:click={deleteLessonVideo}>
<img src="/delete-video.svg" alt="deletevideo" class="dark:invert" />
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { CopyButton, Tag } from 'carbon-components-svelte';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
@@ -6,6 +6,7 @@
import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { t } from '$lib/utils/functions/translations';
import type { LessonVideoType } from '$lib/utils/types';
let youtubeLinks = '';
let error = '';
@@ -26,7 +27,7 @@
$lesson.materials.videos = [
...existingLinks,
...validLinks.map((link = '') => ({
type: 'youtube',
type: 'youtube' as LessonVideoType,
link,
metadata: {}
}))
@@ -48,7 +49,7 @@
}
</script>
<div class="w-full flex items-{error ? 'center' : 'end'} justify-between gap-5">
<div class="flex w-full items-{error ? 'center' : 'end'} justify-between gap-5">
<TextField
label={$t('course.navItem.lessons.materials.tabs.video.add_video.youtube_link')}
bind:value={youtubeLinks}
@@ -1,29 +1,41 @@
<script lang="ts">
import { onMount } from 'svelte';
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
import { lesson } from '../../store/lessons';
let errors = {};
let player = null;
let errors: Record<string, string> = {};
let videoElements: (HTMLVideoElement | null)[] = [];
function initPlyr(_player: any, _video: string | undefined) {
if (!_player) return;
const players = Array.from(document.querySelectorAll('.plyr-video-trigger')).map((p) => {
// @ts-ignore
return new Plyr(p);
});
// @ts-ignore
window.players = players;
function initPlyr() {
if (typeof (window as any).Plyr === 'undefined') {
console.warn('Plyr is not defined. Make sure it is properly imported.');
return;
}
const players = Array.from(document.querySelectorAll('.plyr-video-trigger')).map(
(p) => new (window as any).Plyr(p as HTMLElement)
);
if (typeof window !== 'undefined') {
(window as any).players = players;
}
}
$: initPlyr(player, $lesson.materials.videos);
$: {
videoElements = videoElements.slice(0, $lesson.materials.videos.length);
videoElements = [
...videoElements,
...Array($lesson.materials.videos.length - videoElements.length).fill(null)
];
}
onMount(() => {
initPlyr();
});
</script>
{#if $lesson.materials.videos.length}
<div class="w-full">
{#each $lesson.materials.videos as video}
<div class="w-full overflow-hidden mb-5">
{#each $lesson.materials.videos as video, index}
<div class="mb-5 w-full overflow-hidden">
{#key video.link}
<div class="mb-5">
{#if video.type === 'youtube'}
@@ -58,7 +70,12 @@
/>
</div>
{:else}
<video bind:this={player} class="plyr-video-trigger" playsinline controls>
<video
bind:this={videoElements[index]}
class="plyr-video-trigger h-full w-full"
playsinline
controls
>
<source src={video.link} type="video/mp4" />
<track kind="captions" />
</video>
@@ -59,9 +59,7 @@
let timeoutId: NodeJS.Timeout;
let tabs = CONSTANTS.tabs;
let currentTab = tabs[0].value;
let errors: {
video: string;
};
let errors: Record<string, string> = {};
let editorWindowRef: Window;
let aiButtonRef: HTMLDivElement;
let openPopover = false;
@@ -268,7 +266,10 @@
}
const onClose = () => {
if ($uploadCourseVideoStore.isUploading) return;
$uploadCourseVideoStore.isModalOpen = false;
autoSave($lesson.materials, $lessonByTranslation[lessonId], $isLoading, lessonId);
};
function getComponentOrder(tabs = CONSTANTS.tabs) {
@@ -1,19 +1,21 @@
import { writable } from 'svelte/store';
import type { Writable, Updater } from 'svelte/store';
import type { Course, Lesson, LessonComment, LessonPage, LessonSection } from '$lib/utils/types';
import type { Updater, Writable } from 'svelte/store';
import {
createLesson,
updateLesson,
deleteLesson,
createLessonSection,
updateLessonSection,
deleteLessonSection
deleteLesson,
deleteLessonSection,
updateLesson,
updateLessonSection
} from '$lib/utils/services/courses';
import type { Lesson, Course, LessonPage, LessonComment, LessonSection } from '$lib/utils/types';
import { LOCALE } from '$lib/utils/types';
import { snackbar } from '$lib/components/Snackbar/store';
import { lessonValidation } from '$lib/utils/functions/validator';
import { snackbar } from '$lib/components/Snackbar/store';
import { writable } from 'svelte/store';
export const uploadCourseVideoStore = writable({
isUploading: false,
isModalOpen: false
});
@@ -16,7 +16,6 @@
</script>
<div
role="button"
tabindex="0"
class="overflow-y-auto {isPageNavHidden
? 'h-[calc(100vh-65px)] lg:h-[calc(100vh-127px)]'
@@ -1,4 +1,4 @@
export function formatYoutubeVideo(url: string, errors: { video: string }) {
export function formatYoutubeVideo(url: string, errors: Record<string, string>) {
console.log(`url`, url);
const prefix = 'https://www.youtube.com/embed';
@@ -89,7 +89,7 @@ export const addMathPlugin = (tinymce: any) => {
if (srcMatch) {
// 2. Point the formula to our server
const customAPI = env.PUBLIC_SERVER_URL + '/katex';
const customAPI = env.PUBLIC_SERVER_URL + '/course/katex';
const url = srcMatch[1]?.replaceAll('https://latex.codecogs.com/svg.image', customAPI);
// 3. Fetch the HTML from the server
@@ -0,0 +1,19 @@
import axios from 'axios';
import { env } from '$env/dynamic/public';
import { getAccessToken } from '$lib/utils/functions/supabase';
export const apiClient = axios.create({
baseURL: `${env.PUBLIC_SERVER_URL}`,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
});
apiClient.interceptors.request.use(async (config) => {
const token = await getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
@@ -1,8 +1,3 @@
import { QUESTION_TYPE } from '$lib/components/Question/constants';
import { STATUS } from '$lib/utils/constants/course';
import { isUUID } from '$lib/utils/functions/isUUID';
import { supabase } from '$lib/utils/functions/supabase';
import { isOrgAdmin } from '$lib/utils/store/org';
import type {
Course,
Exercise,
@@ -15,12 +10,19 @@ import type {
ProfileCourseProgress
} from '$lib/utils/types';
import type { PostgrestError, PostgrestSingleResponse } from '@supabase/supabase-js';
import { QUESTION_TYPE } from '$lib/components/Question/constants';
import { STATUS } from '$lib/utils/constants/course';
import { apiClient } from '$lib/utils/services/api';
import { get } from 'svelte/store';
import { isOrgAdmin } from '$lib/utils/store/org';
import { isUUID } from '$lib/utils/functions/isUUID';
import { supabase } from '$lib/utils/functions/supabase';
export async function fetchCourses(profileId, orgId) {
if (!orgId || !profileId) return;
const match = {};
const match: { member_profile_id?: string } = {};
// Filter by profile_id if role isn't admin within organization
if (!get(isOrgAdmin)) {
match.member_profile_id = profileId;
@@ -257,15 +259,13 @@ export async function getMarks(courseId) {
if (!courseId) return;
// Gets courses for a particular organisation where the current logged in user is a groupmember
const { data: marks } = await supabase
.rpc('get_marks')
.eq('course_id', courseId);
const { data: marks } = await supabase.rpc('get_marks').eq('course_id', courseId);
return { marks };
}
export function fetchLesson(lessonId: Lesson['id']) {
return supabase
export async function fetchLesson(lessonId: Lesson['id']) {
const { data, error } = await supabase
.from('lesson')
.select(
`id,
@@ -282,6 +282,29 @@ export function fetchLesson(lessonId: Lesson['id']) {
)
.eq('id', lessonId)
.single();
if (data) {
const keys = data.videos.filter((video) => video.type === 'upload').map((video) => video.key);
if (!keys.length) {
return { data, error };
}
try {
const response = await apiClient.post('/course/presign/download', { keys });
data.videos = data.videos.map((video) => {
if (video.type === 'upload') {
video.link = response.data.urls[video.key];
}
return video;
});
} catch (error) {
console.error('Error retrieving videos:', error);
}
}
return { data, error };
}
export function fetchLesssonLanguageHistory(lessonId: string, locale: string, endRange: number) {
+3 -5
View File
@@ -279,10 +279,7 @@ export interface Group_attendance {
groupmember?: Groupmember;
}
export enum VideoType {
youtube,
muse
}
export type LessonVideoType = 'youtube' | 'generic' | 'upload';
export interface LessonPage {
id?: string | null;
@@ -296,8 +293,9 @@ export interface LessonPage {
note: string;
slide_url: string;
videos: Array<{
type: string;
type: LessonVideoType;
link: string;
key?: string;
metadata?: {
svid?: string;
};
+29 -28
View File
@@ -1,35 +1,36 @@
import { env } from '$env/dynamic/public';
const sendEmail = async (
emailDataArray: {
from?: string;
to: string;
subject: string;
content: string;
replyTo?: string;
isPersonalEmail?: boolean;
}[]
) => {
try {
const response = await fetch(`${env.PUBLIC_SERVER_URL}/sendEmail`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(emailDataArray)
});
const sendEmail = (sFetch: typeof fetch) => {
return async (
emailDataArray: {
from?: string;
to: string;
subject: string;
content: string;
replyTo?: string;
isPersonalEmail?: boolean;
}[]
) => {
try {
const response = await sFetch(`${env.PUBLIC_SERVER_URL}/mail/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(emailDataArray)
});
if (!response.ok) {
console.log('Failed to send emails');
if (!response.ok) {
console.log('Failed to send emails');
return;
}
console.log('Emails sent successfully:', response);
return response;
} catch (error) {
console.error('Error sending emails:', error);
return;
}
const responseData = await response.json();
console.log('Emails sent successfully:', responseData);
return responseData;
} catch (error) {
console.error('Error sending emails:', error);
return;
}
};
};
export default sendEmail;
@@ -1,5 +1,6 @@
import { Configuration, OpenAIApi } from 'openai-edge';
import { OpenAIStream, StreamingTextResponse } from 'ai';
import { env } from '$env/dynamic/private';
// Create an OpenAI API client (that's edge friendly!)
@@ -1,7 +1,7 @@
import sendEmail from '$mail/sendEmail';
import { json } from '@sveltejs/kit';
import sendEmail from '$mail/sendEmail';
export async function POST({ request }) {
export async function POST({ fetch, request }) {
const { to, content, orgName, exerciseTitle } = await request.json();
if (!to || !content || !orgName) {
@@ -17,7 +17,7 @@ export async function POST({ request }) {
}
];
await sendEmail(emailData);
await sendEmail(fetch)(emailData);
return json({
success: true,
@@ -1,11 +1,16 @@
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
import { getFeedForNotification } from '$lib/utils/services/newsfeed/index';
import sendEmail from '$mail/sendEmail';
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
import { json } from '@sveltejs/kit';
import sendEmail from '$mail/sendEmail';
const supabase = getServerSupabase();
const sendEmailNotification = async (feedId: string, authorId: string, comment?: string) => {
const sendEmailNotification = async (
sFetch: typeof fetch,
feedId: string,
authorId: string,
comment?: string
) => {
const feed = await getFeedForNotification({
supabase,
feedId,
@@ -34,7 +39,7 @@ const sendEmailNotification = async (feedId: string, authorId: string, comment?:
}
];
await sendEmail(emailData);
await sendEmail(sFetch)(emailData);
// dont continue
return;
@@ -62,10 +67,10 @@ const sendEmailNotification = async (feedId: string, authorId: string, comment?:
console.log('Sending emails to all students', feed.courseMembers.length);
// This is the email sending function with a loop
await sendEmail(emailsData);
await sendEmail(sFetch)(emailsData);
};
export async function POST({ request }) {
export async function POST({ fetch, request }) {
const { authorId, feedId, comment } = await request.json();
console.log('/POST api/email/course/newsfeed');
@@ -73,7 +78,7 @@ export async function POST({ request }) {
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
}
await sendEmailNotification(feedId, authorId, comment);
await sendEmailNotification(fetch, feedId, authorId, comment);
// TODO: Support sending to other platforms like telegram and discord
// sendDiscordNotification(); sendTelegramNotification();
@@ -4,7 +4,7 @@ import sendEmail from '$mail/sendEmail';
// const supabase = getSupabase();
export async function POST({ request }) {
export async function POST({ fetch, request }) {
const { to, courseName, teacherEmail, studentFullname, orgName } = await request.json();
// const accessToken = request.headers.get('Authorization') || '';
console.log(
@@ -48,7 +48,7 @@ export async function POST({ request }) {
}
];
await sendEmail(emailData);
await sendEmail(fetch)(emailData);
return json({
success: true,
@@ -1,8 +1,8 @@
import sendEmail from '$mail/sendEmail';
import { json } from '@sveltejs/kit';
import sendEmail from '$mail/sendEmail';
// Sends email to student when they successfully join a course
export async function POST({ request }) {
export async function POST({ fetch, request }) {
const { to, orgName, courseName } = await request.json();
console.log('/POST api/email/course/student_welcome', to, orgName);
@@ -25,7 +25,7 @@ export async function POST({ request }) {
`
}
];
await sendEmail(emailData);
await sendEmail(fetch)(emailData);
return json({
success: true,
@@ -1,7 +1,7 @@
import sendEmail from '$mail/sendEmail';
import { json } from '@sveltejs/kit';
import sendEmail from '$mail/sendEmail';
export async function POST({ request }) {
export async function POST({ fetch, request }) {
const { to, content, orgName } = await request.json();
if (!to || !content || !orgName) {
@@ -17,7 +17,7 @@ export async function POST({ request }) {
}
];
await sendEmail(emailData);
await sendEmail(fetch)(emailData);
return json({
success: true,
@@ -1,7 +1,7 @@
import sendEmail from '$mail/sendEmail';
import { json } from '@sveltejs/kit';
import sendEmail from '$mail/sendEmail';
export async function POST({ request }) {
export async function POST({ fetch, request }) {
const { to, courseName, studentEmail, studentFullname } = await request.json();
console.log(
'/POST api/email/course/teacher_student_buycourse',
@@ -31,7 +31,7 @@ export async function POST({ request }) {
`
}
];
await sendEmail(emailData);
await sendEmail(fetch)(emailData);
return json({
success: true,
@@ -1,7 +1,7 @@
import sendEmail from '$mail/sendEmail';
import { json } from '@sveltejs/kit';
import sendEmail from '$mail/sendEmail';
export async function POST({ request }) {
export async function POST({ fetch, request }) {
const { to, courseName, studentName, studentEmail } = await request.json();
console.log('/POST api/email/course/teacher_student_joined', to, courseName);
@@ -23,7 +23,7 @@ export async function POST({ request }) {
}
];
await sendEmail(emailData);
await sendEmail(fetch)(emailData);
return json({
success: true,
@@ -1,7 +1,7 @@
import sendEmail from '$mail/sendEmail';
import { json } from '@sveltejs/kit';
import sendEmail from '$mail/sendEmail';
export async function POST({ request }) {
export async function POST({ fetch, request }) {
const { to, name, orgName, courseName, orgSiteName } = await request.json();
console.log('/POST api/email/course/teacher_welcome', to, name, orgName);
@@ -28,7 +28,7 @@ export async function POST({ request }) {
}
];
await sendEmail(emailData);
await sendEmail(fetch)(emailData);
return json({
success: true,
@@ -1,8 +1,8 @@
import sendEmail from '$mail/sendEmail';
import { json } from '@sveltejs/kit';
import sendEmail from '$mail/sendEmail';
// API to send invite to teacher
export async function POST({ request }) {
export async function POST({ fetch, request }) {
const body = await request.json();
const { org, email } = body;
@@ -40,7 +40,7 @@ export async function POST({ request }) {
`
}
];
await sendEmail(emailData);
await sendEmail(fetch)(emailData);
return json({
success: true,
@@ -1,7 +1,7 @@
import sendEmail from '$mail/sendEmail';
import { json } from '@sveltejs/kit';
import sendEmail from '$mail/sendEmail';
export async function POST({ request }) {
export async function POST({ fetch, request }) {
const { to, profileId, orgSiteName, fullname } = await request.json();
console.log('/POST api/email/verify_email', to, profileId);
@@ -36,7 +36,7 @@ export async function POST({ request }) {
}
];
await sendEmail(emailData);
await sendEmail(fetch)(emailData);
return json({
success: true,
@@ -1,7 +1,7 @@
import sendEmail from '$mail/sendEmail';
import { json } from '@sveltejs/kit';
import sendEmail from '$mail/sendEmail';
export async function POST({ request }) {
export async function POST({ fetch, request }) {
const { to, name } = await request.json();
console.log('/POST api/email/welcome', to, name);
@@ -33,7 +33,7 @@ export async function POST({ request }) {
}
];
await sendEmail(emailData);
await sendEmail(fetch)(emailData);
return json({
success: true,
@@ -1,7 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { env } from '$env/dynamic/public';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import {
checkExercisesComplete,
@@ -30,8 +29,6 @@
import IconButton from '$lib/components/IconButton/index.svelte';
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
import { PageBody, PageNav } from '$lib/components/Page';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store';
import MODES from '$lib/utils/constants/mode';
@@ -42,9 +39,9 @@
import { COURSE_VERSION, type Lesson, type LessonCompletion } from '$lib/utils/types';
import { Dropdown } from 'carbon-components-svelte';
import { ChevronLeft, ChevronRight, Edit, Save } from 'carbon-icons-svelte';
import Download from 'carbon-icons-svelte/lib/Download.svelte';
import OverflowMenuVertical from 'carbon-icons-svelte/lib/OverflowMenuVertical.svelte';
import ResultOld from 'carbon-icons-svelte/lib/ResultOld.svelte';
import { apiClient } from '$lib/utils/services/api';
export let data;
@@ -163,13 +160,9 @@
const lessonNumber = getLessonOrder(currentLesson.id);
const slideUrl = $lesson.materials.slide_url || '';
const response = await fetch(env.PUBLIC_SERVER_URL + '/downloadLesson', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
const response = await apiClient.post(
'/course/lesson/download/pdf',
{
title: currentLesson.title,
number: lessonNumber,
orgName: $currentOrg.name,
@@ -177,16 +170,13 @@
slideUrl: slideUrl,
video: lessonVideo,
courseTitle: $course.title
})
});
},
{
responseType: 'blob'
}
);
if (!response.ok) {
throw new Error(await response.text());
}
const data = await response.blob();
console.log(data);
const file = new Blob([data], { type: 'application/pdf' });
const file = new Blob([response.data], { type: 'application/pdf' });
const fileURL = URL.createObjectURL(file);
let a = document.createElement('a');
@@ -386,17 +376,6 @@
<Edit size={24} />
{/if}
</IconButton>
{#if $course.metadata.lessonDownload && !!env.PUBLIC_SERVER_URL}
<PrimaryButton
className="mr-"
variant={VARIANTS.OUTLINED}
onClick={downloadLesson}
{isLoading}
>
<Download size={16} />
</PrimaryButton>
{/if}
</div>
<Dropdown items={LANGUAGES} bind:selectedId={$lesson.locale} class="h-full" />