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 <iniubongobonguko2018@gmail.com>
This commit is contained in:
co-authored by
Iniubong Obonguko
parent
9eb7b01587
commit
82be557a42
+14
-15
@@ -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
|
||||
|
||||
+3
-2
@@ -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: {}
|
||||
}))
|
||||
|
||||
+9
-9
@@ -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>
|
||||
|
||||
+72
-77
@@ -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" />
|
||||
+4
-3
@@ -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}
|
||||
|
||||
+33
-16
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
@@ -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!)
|
||||
+3
-3
@@ -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();
|
||||
|
||||
|
||||
+2
-2
@@ -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,
|
||||
+3
-3
@@ -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,
|
||||
+3
-3
@@ -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,
|
||||
+3
-3
@@ -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,
|
||||
+3
-3
@@ -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,
|
||||
+3
-3
@@ -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,
|
||||
+3
-3
@@ -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,
|
||||
+3
-3
@@ -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,
|
||||
+3
-3
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user