Merge branch 'main' into CLASSROOMIO-T-84

This commit is contained in:
tunny17
2023-08-11 12:39:37 +01:00
31 changed files with 525 additions and 251 deletions
+3 -2
View File
@@ -13,6 +13,7 @@
"prepare": "svelte-kit sync" "prepare": "svelte-kit sync"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.386.0",
"@sentry/browser": "^7.61.0", "@sentry/browser": "^7.61.0",
"@sentry/integrations": "^7.61.0", "@sentry/integrations": "^7.61.0",
"@sentry/tracing": "^7.61.0", "@sentry/tracing": "^7.61.0",
@@ -25,10 +26,12 @@
"@types/pluralize": "0.0.30", "@types/pluralize": "0.0.30",
"ai": "^2.1.31", "ai": "^2.1.31",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"axios": "^1.4.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"canvas-confetti": "^1.6.0", "canvas-confetti": "^1.6.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"form-data": "^4.0.0",
"hotkeys-js": "^3.11.2", "hotkeys-js": "^3.11.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@@ -47,8 +50,6 @@
"@babel/plugin-transform-runtime": "^7.22.9", "@babel/plugin-transform-runtime": "^7.22.9",
"@babel/preset-env": "^7.22.9", "@babel/preset-env": "^7.22.9",
"@babel/runtime": "^7.22.6", "@babel/runtime": "^7.22.6",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@sveltejs/kit": "^1.22.4", "@sveltejs/kit": "^1.22.4",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.14.196", "@types/lodash": "^4.14.196",
@@ -3,6 +3,9 @@
import LocalVideo from './LocalVideo.svelte'; import LocalVideo from './LocalVideo.svelte';
import * as CONSTANTS from './constants'; import * as CONSTANTS from './constants';
export let lessonId = '';
export let saveLesson = () => {};
let tabs = CONSTANTS.videoTabs; let tabs = CONSTANTS.videoTabs;
let currentTab = tabs[0].value; let currentTab = tabs[0].value;
@@ -34,7 +37,7 @@
{#if currentTab === 1} {#if currentTab === 1}
<YoutubeVideo /> <YoutubeVideo />
{:else} {:else}
<LocalVideo /> <LocalVideo {lessonId} {saveLesson} />
{/if} {/if}
</main> </main>
</section> </section>
@@ -1,45 +1,95 @@
<script> <script>
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { deleteLessonVideo } from '$lib/components/Course/components/Lesson/store/lessons'; import {
lesson,
deleteLessonVideo,
uploadCourseVideoStore
} from '$lib/components/Course/components/Lesson/store/lessons';
import { enhance } from '$app/forms';
import { Moon } from 'svelte-loading-spinners';
export let lessonId = '';
export let saveLesson = () => {};
let isWrongFormat = false; let isWrongFormat = false;
let isUploaded = false; let isUploaded = false;
let isLoaded = false; let isLoaded = false;
let isBig = false; let isBig = false;
let fileInput;
let submit;
let uploadedFileUrl = '';
let isLoading = false;
const uploadVideo = () => { function isVideoAdded(link) {
// if the file is too big or not the correct format return $lesson.materials?.videos?.find((v) => v.link === link);
isWrongFormat = true; }
// when the upload has been sucessfully completed async function isDoneUploading(response) {
// here the you set the video_url to something and that would be displayed on the page uploadedFileUrl = response?.success && response?.url;
isUploaded = false;
// closes the modal if (uploadedFileUrl && !isVideoAdded(uploadedFileUrl)) {
// $uploadCourseVideo.isModalOpen = false; $lesson.materials.videos = [
}; ...$lesson.materials.videos,
{
type: 'muse',
link: uploadedFileUrl,
metadata: {}
}
];
saveLesson();
}
}
const onUpload = () => { $: isDoneUploading($uploadCourseVideoStore.formRes);
// PROBABLY GET THE SNAPSHOT OR SOMETHING
isLoaded = true;
};
</script> </script>
{#if isLoaded == false} {#if isLoaded === false}
<button <button
on:click={onUpload} type="button"
class="h-full w-full flex flex-col items-center justify-center border border-blue-300 border-dashed rounded-xl" on:click={() => (fileInput && !isLoading ? fileInput.click() : null)}
class="w-full h-full"
disabled={isLoading}
> >
<img src="/upload-video.svg" alt="upload" /> <form
<span class="pt-3"> class="h-full w-full flex flex-col items-center justify-center border border-blue-300 border-dashed rounded-xl"
<h3 class="text-center text-xl font-normal dark:text-white">Upload video</h3> method="POST"
<p class=" text-center text-sm font-normal"> action="?/create"
Select file( Mp4, MOV, AVI) to upload or drag and drop video here.<br /> Size must be less than enctype="multipart/form-data"
20 MB. use:enhance={() => {
</p> isLoading = true;
</span>
return async ({ update }) => {
await update();
isLoading = false;
isLoaded = true;
};
}}
>
{#if isLoading}
<Moon size="40" color="#1d4ed8" unit="px" duration="1s" />
<p class="mt-5">Uploading Video</p>
{:else}
<img src="/upload-video.svg" alt="upload" />
<span class="pt-3">
<h3 class="text-center text-xl font-normal dark:text-white">Upload video</h3>
<p class=" text-center text-sm font-normal">
Select file( Mp4, MOV, AVI) to upload to your lesson.
</p>
</span>
{/if}
<input
style="display:none;"
type="file"
accept="video/*"
name="file"
on:change={() => submit.click()}
bind:this={fileInput}
/>
<input type="text" name="lessonId" value={lessonId} style="display: none;" />
<input style="display:none;" type="submit" bind:this={submit} />
</form>
</button> </button>
{:else if isBig && isUploaded == false} {:else if $uploadCourseVideoStore.formRes?.type === 'FILE_TOO_LARGE'}
<div class="h-full w-full flex flex-col items-center justify-center rounded-xl"> <div class="h-full w-full flex flex-col items-center justify-center rounded-xl">
<img src="/video-upload-error.svg" alt="upload error" /> <img src="/video-upload-error.svg" alt="upload error" />
<span class="pt-3 pb-2"> <span class="pt-3 pb-2">
@@ -52,7 +102,7 @@
</span> </span>
<PrimaryButton label="Try again" /> <PrimaryButton label="Try again" />
</div> </div>
{:else if isWrongFormat && isUploaded == false} {:else if !$uploadCourseVideoStore.formRes?.success}
<div class="h-full w-full flex flex-col items-center justify-center rounded-xl"> <div class="h-full w-full flex flex-col items-center justify-center rounded-xl">
<img src="/video-upload-error.svg" alt="upload error" /> <img src="/video-upload-error.svg" alt="upload error" />
<span class="pt-3 pb-2"> <span class="pt-3 pb-2">
@@ -72,17 +122,19 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<div class="rounded-sm overflow-hidden"> <div class="rounded-sm overflow-hidden">
<img src="/course-video.png" alt="coursevideo" class="object-cover" /> <video class="w-[200px]">
<source src={uploadedFileUrl} type="video/mp4" />
<!-- <source src="/path/to/video.webm" type="video/webm" /> -->
<!-- Captions are optional -->
<track kind="captions" />
</video>
</div> </div>
<p>Video.png</p> <p>{fileInput?.files?.[0]?.name || uploadedFileUrl}</p>
</span> </span>
<button on:click={deleteLessonVideo}> <button on:click={deleteLessonVideo}>
<img src="/delete-video.svg" alt="deletevideo" class="dark:invert" /> <img src="/delete-video.svg" alt="deletevideo" class="dark:invert" />
</button> </button>
</div> </div>
</div> </div>
<div class="ml-auto">
<PrimaryButton label="upload" onClick={uploadVideo} />
</div>
</div> </div>
{/if} {/if}
@@ -1,47 +1,49 @@
<script> <script>
import { CopyButton, Tag } from 'carbon-components-svelte'; import { CopyButton, Tag } from 'carbon-components-svelte';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte'; import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons'; import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Chip from '$lib/components/Chip/Text.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
let youtubeLinks = ''; let youtubeLinks = '';
let error = ''; let error = '';
function getVideoUrls(urls = '') {
return (urls || '').split(',').filter((url) => !!url.trim());
}
function addVideo() { function addVideo() {
const links = youtubeLinks.split(',').map((link) => link.trim()); const links = getVideoUrls(youtubeLinks);
const validLinks = links.filter(isValidYouTubeLink); const validLinks = links.filter(isValidYouTubeLink);
if (validLinks.length === 0) { if (validLinks.length === 0) {
error = 'Invalid YouTube link(s)'; error = 'Invalid YouTube link(s)';
} else { } else {
const existingLinks = $lesson.materials.video_url const existingLinks = $lesson?.materials?.videos || [];
.split(',')
.map((link) => link.trim()) $lesson.materials.videos = [
.filter(Boolean);
const updatedLinks = [
...existingLinks, ...existingLinks,
...validLinks.filter((link) => !existingLinks.includes(link)) ...validLinks.map((link = '') => ({
type: 'youtube',
link,
metadata: {}
}))
]; ];
$lesson.materials.video_url = updatedLinks.join(',').trim();
youtubeLinks = ''; youtubeLinks = '';
error = ''; error = '';
} }
} }
function isValidYouTubeLink(link) { function isValidYouTubeLink(link = '') {
const youtubeRegex = const youtubeRegex =
/^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/; /^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
return youtubeRegex.test(link.trim()); return youtubeRegex.test(link.trim());
} }
function removeVideo(index) { function removeVideo(index = 0) {
const links = $lesson.materials.video_url.split(',').map((link) => link.trim()); $lesson.materials.videos = $lesson.materials.videos.filter((v, i) => i !== index);
links.splice(index, 1);
$lesson.materials.video_url = links.join(',').trim();
} }
</script> </script>
@@ -58,22 +60,23 @@
<PrimaryButton label="Add Video" className="rounded-md" onClick={addVideo} /> <PrimaryButton label="Add Video" className="rounded-md" onClick={addVideo} />
</div> </div>
<p class="mt-4 pl-2 text-sm"> <p class="mt-4 pl-2 text-sm">
Added : <strong Videos added: <strong>
>{($lesson?.materials?.video_url && $lesson.materials.video_url.trim().split(',').length) || 0} Videos</strong {$lesson?.materials?.videos.filter((v) => v.type === 'youtube' && isValidYouTubeLink(v.link))
> .length || 0}
</strong>
</p> </p>
<div class=""> <div class="">
{#each $lesson?.materials?.video_url && $lesson.materials.video_url {#each $lesson?.materials?.videos as video, index}
.trim() {#if video.type === 'youtube'}
.split(',') as videoLink, index} <div class="flex items-center gap-1">
<div class="flex items-center gap-1"> <Tag type="blue">
<Tag type="blue"> {video.link}
{videoLink} </Tag>
</Tag> <CopyButton text={video.link} feedback="Copied to clipboard" />
<CopyButton text={videoLink} feedback="Copied to clipboard" /> <IconButton value="delete-video" onClick={() => removeVideo(index)}>
<IconButton value="delete-video" onClick={() => removeVideo(index)}> <TrashCanIcon size={16} class="carbon-icon dark:text-white" />
<TrashCanIcon size={16} class="carbon-icon dark:text-white" /> </IconButton>
</IconButton> </div>
</div> {/if}
{/each} {/each}
</div> </div>
@@ -1,11 +1,13 @@
<script lang="ts"> <script lang="ts">
import { useCompletion } from 'ai/svelte'; import { useCompletion } from 'ai/svelte';
import merge from 'lodash/merge';
import MODES from '$lib/utils/constants/mode.js'; import MODES from '$lib/utils/constants/mode.js';
import { getEmbedId } from '$lib/utils/functions/formatYoutubeVideo'; import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
import Modal from '$lib/components/Modal/index.svelte'; import Modal from '$lib/components/Modal/index.svelte';
import { Popover } from 'carbon-components-svelte'; import { Popover } from 'carbon-components-svelte';
import AlignBoxTopLeftIcon from 'carbon-icons-svelte/lib/AlignBoxTopLeft.svelte'; import AlignBoxTopLeftIcon from 'carbon-icons-svelte/lib/AlignBoxTopLeft.svelte';
import TrashCan from 'carbon-icons-svelte/lib/TrashCan.svelte';
import ListIcon from 'carbon-icons-svelte/lib/List.svelte'; import ListIcon from 'carbon-icons-svelte/lib/List.svelte';
import IbmWatsonKnowledgeStudioIcon from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeStudio.svelte'; import IbmWatsonKnowledgeStudioIcon from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeStudio.svelte';
import MachineLearningModel from 'carbon-icons-svelte/lib/MachineLearningModel.svelte'; import MachineLearningModel from 'carbon-icons-svelte/lib/MachineLearningModel.svelte';
@@ -27,7 +29,8 @@
import VideoUploader from '$lib/components/Course/components/Lesson/Materials/Video/Index.svelte'; import VideoUploader from '$lib/components/Course/components/Lesson/Materials/Video/Index.svelte';
import { course } from '$lib/components/Course/store'; import { course } from '$lib/components/Course/store';
import * as CONSTANTS from './constants'; import * as CONSTANTS from './constants';
import { browser } from '$app/environment'; import { fetchLesson } from '$lib/utils/services/courses';
import type { LessonPage } from '$lib/utils/types';
export let mode = MODES.view; export let mode = MODES.view;
export let prevMode = ''; export let prevMode = '';
@@ -36,11 +39,10 @@
let tabs = CONSTANTS.tabs; let tabs = CONSTANTS.tabs;
let currentTab = tabs[0].value; let currentTab = tabs[0].value;
let errors = {}; let errors = {};
let player = {};
let textareaRef = {}; let textareaRef = {};
let aiButtonRef = {}; let aiButtonRef = {};
let openPopover = false; let openPopover = false;
let videoUrls: Array<string> = []; let player = null;
let aiButtonClass = let aiButtonClass =
'flex items-center px-5 py-2 border border-gray-300 hover:bg-gray-200 rounded-md w-full mb-2'; 'flex items-center px-5 py-2 border border-gray-300 hover:bg-gray-200 rounded-md w-full mb-2';
@@ -49,26 +51,26 @@
() => () =>
(currentTab = tab); (currentTab = tab);
function initPlyr(_player, videoUrl = '') { // problem: after we save an uploaded video,
if (!browser) return; async function saveLesson() {
const { data } = await fetchLesson(lessonId);
const materials = merge(data, $lesson.materials);
const players = Array.from(document.querySelectorAll('.plyr-video-trigger')).map((p) => { lesson.update((l) => ({
// @ts-ignore ...l,
return new Plyr(p); materials
}); }));
handleUpdateLessonMaterials($lesson, lessonId);
// @ts-ignore
window.players = players;
} }
function handleSave(prevMode) { function handleSave(prevMode: string) {
if (prevMode === MODES.edit) { if (prevMode === MODES.edit) {
handleUpdateLessonMaterials($lesson, lessonId); saveLesson();
} }
} }
function addBadgeValueToTab(materials) { function addBadgeValueToTab(materials) {
const { slide_url, video_url, note } = materials; const { slide_url, videos, note } = materials;
tabs = tabs.map((tab) => { tabs = tabs.map((tab) => {
let badgeValue; let badgeValue;
@@ -78,7 +80,7 @@
} else if (tab.value === 2) { } else if (tab.value === 2) {
badgeValue = !!note ? 1 : 0; badgeValue = !!note ? 1 : 0;
} else { } else {
badgeValue = typeof video_url === 'string' && !!video_url ? video_url.split(',').length : 0; badgeValue = Array.isArray(videos) ? videos.length : 0;
} }
tab.badgeValue = badgeValue; tab.badgeValue = badgeValue;
@@ -113,20 +115,31 @@
}, 500); }, 500);
} }
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;
}
const onClose = () => { const onClose = () => {
handleUpdateLessonMaterials($lesson, lessonId); saveLesson();
$uploadCourseVideoStore.isModalOpen = false; $uploadCourseVideoStore.isModalOpen = false;
$uploadCourseVideoStore.formRes = null;
}; };
$: handleSave(prevMode); $: handleSave(prevMode);
$: initPlyr(player, $lesson.materials.video_url);
$: addBadgeValueToTab($lesson.materials); $: addBadgeValueToTab($lesson.materials);
$: updateNoteByCompletion($completion); $: updateNoteByCompletion($completion);
$: videoUrls = ($lesson?.materials?.video_url || '').split(',').filter((url) => !!url.trim()); $: initPlyr(player, $lesson.materials.videos);
</script> </script>
<Modal <Modal
@@ -135,7 +148,7 @@
width="w-4/5 h-[566px]" width="w-4/5 h-[566px]"
modalHeading="Add a Video" modalHeading="Add a Video"
> >
<VideoUploader {onClose} /> <VideoUploader {lessonId} {saveLesson} />
</Modal> </Modal>
<Tabs {tabs} {currentTab} {onChange}> <Tabs {tabs} {currentTab} {onChange}>
@@ -236,34 +249,56 @@
</TabContent> </TabContent>
<TabContent value={tabs[2].value} index={currentTab}> <TabContent value={tabs[2].value} index={currentTab}>
{#if mode === MODES.edit && videoUrls.length} {#if mode === MODES.edit && $lesson.materials.videos.length}
<PrimaryButton label="Add/Edit Video" onClick={openAddVideoModal} className="mb-2" /> <PrimaryButton label="Add/Edit Video(s)" onClick={openAddVideoModal} className="mb-2" />
{/if} {/if}
<!-- Line 230 would run if its in edit mode or the video is truthy but then in line 235 we are mapping for an empty string --> {#if $lesson.materials.videos.length}
{#if videoUrls.length}
<div class="flex flex-col items-start w-full h-full"> <div class="flex flex-col items-start w-full h-full">
{#each videoUrls as url, index} {#each $lesson.materials.videos as video, index}
{#if mode === MODES.edit}
<div class="ml-auto">
<IconButton
value="delete-video"
contained={true}
onClick={() => deleteLessonVideo(index)}
>
<TrashCanIcon size={20} class="carbon-icon dark:text-white" />
</IconButton>
</div>
{/if}
<div class="w-full h-full flex flex-col gap-2 overflow-hidden"> <div class="w-full h-full flex flex-col gap-2 overflow-hidden">
{#if url.includes('youtube')} {#key video.link}
<div class="mb-5"> <div class="mb-5">
<div {#if video.type === 'youtube'}
bind:this={player} <iframe
class="plyr-video-trigger" width="100%"
data-plyr-provider="youtube" height="569"
data-plyr-embed-id={getEmbedId(url)} class="iframe"
/> src={formatYoutubeVideo(video.link, errors)}
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
/>
{:else if video.metadata?.svid}
<div style="position:relative;padding-bottom:51.416579%">
<iframe
src="https://muse.ai/embed/{video.metadata
?.svid}?logo=https://app.classroomio.com/logo-512.png&subtitles=auto&cover_play_position=center"
style="width:100%;height:100%;position:absolute;left:0;top:0"
frameborder="0"
allowfullscreen
title="Muse AI Video Embed"
/>
</div>
{:else}
<video bind:this={player} class="plyr-video-trigger" playsinline controls>
<source src={video.link} type="video/mp4" />
<track kind="captions" />
</video>
{/if}
</div> </div>
{:else} {/key}
<video bind:this={player} class="plyr-video-trigger" playsinline controls>
<source
src="https://pub-bd38ff7251aa402a93762181a947f7ea.r2.dev/generate-lesson-draft.mp4"
type="video/mp4"
/>
<!-- <source src="/path/to/video.webm" type="video/webm" /> -->
<!-- Captions are optional -->
<track kind="captions" />
</video>
{/if}
</div> </div>
{/each} {/each}
</div> </div>
@@ -1,21 +1,23 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import type { Writable, Updater } from 'svelte/store'; import type { Writable, Updater } from 'svelte/store';
import { createLesson, updateLesson, deleteLesson } from '$lib/utils/services/courses'; import { createLesson, updateLesson, deleteLesson } from '$lib/utils/services/courses';
import type { Lesson, Course } from '$lib/utils/types'; import type { Lesson, Course, LessonPage } from '$lib/utils/types';
export const uploadCourseVideoStore = writable({ export const uploadCourseVideoStore = writable({
isModalOpen: false isModalOpen: false,
formRes: null
}); });
export const lessons: Writable<Lesson[]> = writable([]); export const lessons: Writable<Lesson[]> = writable([]);
export const lesson = writable({
export const lesson = writable<LessonPage>({
id: null, id: null,
totalExercises: 0, totalExercises: 0,
is_complete: false, is_complete: false,
materials: { materials: {
note: '', note: '',
slide_url: '', slide_url: '',
video_url: '' videos: []
}, },
exercises: [] exercises: []
}); });
@@ -103,14 +105,12 @@ export async function handleUpdateLessonMaterials(lesson: any, lessonId: Lesson[
export const deleteLessonVideo = (index: any) => { export const deleteLessonVideo = (index: any) => {
lesson.update((currentLesson) => { lesson.update((currentLesson) => {
const links = currentLesson.materials.video_url.split(',').map((link) => link.trim()); const videos = currentLesson.materials.videos || [];
links.splice(index, 1);
const updatedVideoUrl = links.join(',').trim();
return { return {
...currentLesson, ...currentLesson,
materials: { materials: {
...currentLesson.materials, ...currentLesson.materials,
video_url: updatedVideoUrl videos: videos?.filter((video, i) => i !== index)
} }
}; };
}); });
@@ -1,8 +1,9 @@
<script> <script>
import CaretSortDownIcon from 'carbon-icons-svelte/lib/CaretSortDown.svelte'; import { slide } from 'svelte/transition';
import ChevronDownIcon from 'carbon-icons-svelte/lib/ChevronDown.svelte'; import ChevronDownIcon from 'carbon-icons-svelte/lib/ChevronDown.svelte';
import ChevronUpIcon from 'carbon-icons-svelte/lib/ChevronUp.svelte'; import ChevronUpIcon from 'carbon-icons-svelte/lib/ChevronUp.svelte';
import IconButton from '$lib/components/IconButton/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte';
import { NavClasses } from '$lib/utils/constants/reusableClass';
import NavIcons from './NavIcons.svelte'; import NavIcons from './NavIcons.svelte';
export let handleClick = () => {}; export let handleClick = () => {};
@@ -12,8 +13,6 @@
export let total = 0; export let total = 0;
// export let subMenuItems = []; // export let subMenuItems = [];
let activeClass = 'bg-gray-200 dark:bg-gray-800';
function onClick() { function onClick() {
handleClick(); handleClick();
} }
@@ -25,8 +24,8 @@
<div class=""> <div class="">
<button <button
class="item relative flex items-center py-3 px-4 ml-2 mb-1 rounded hover:bg-gray-200 dark:hover:bg-gray-800 {isGroupActive && class="item relative flex items-center py-3 px-4 ml-2 mb-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 {isGroupActive &&
activeClass} w-[95%]" NavClasses.active} w-[95%]"
tabindex="0" tabindex="0"
on:click={onClick} on:click={onClick}
> >
@@ -48,7 +47,7 @@
{/if} {/if}
</button> </button>
{#if isExpanded} {#if isExpanded}
<div class="flex flex-col"> <div in:slide out:slide class="flex flex-col">
<slot /> <slot />
</div> </div>
{/if} {/if}
@@ -24,7 +24,7 @@
import { course } from '$lib/components/Course/store'; import { course } from '$lib/components/Course/store';
import { updateCourse } from '$lib/utils/services/courses'; import { updateCourse } from '$lib/utils/services/courses';
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
import { currentOrgPath } from '$lib/utils/store/org'; import { NavClasses } from '$lib/utils/constants/reusableClass';
import { isMobile } from '$lib/utils/store/useMobile'; import { isMobile } from '$lib/utils/store/useMobile';
// export let lessonId; // export let lessonId;
@@ -32,7 +32,6 @@
export let isStudent = null; export let isStudent = null;
let show = null; let show = null;
let activeClass = 'bg-gray-200 dark:bg-gray-800';
let isLessonActive = false; let isLessonActive = false;
function handleMainGroupClick(href) { function handleMainGroupClick(href) {
@@ -151,7 +150,7 @@
<div class="root z-10 {!show && 'hide'} {$isMobile ? 'fixed shadow-xl' : 'sticky'}"> <div class="root z-10 {!show && 'hide'} {$isMobile ? 'fixed shadow-xl' : 'sticky'}">
{#if show} {#if show}
<div class="relative h-full dark:bg-gray-700"> <div class="relative h-full dark:bg-gray-800">
<!-- <PageNav title="Back to courses" paddingClass="pl-2" titleSize="text-md"> <!-- <PageNav title="Back to courses" paddingClass="pl-2" titleSize="text-md">
<slot:fragment slot="image"> <slot:fragment slot="image">
<Avatar src={$course.logo} /> <Avatar src={$course.logo} />
@@ -202,12 +201,12 @@
<div <div
class="flex items-center py-3 px-4 rounded hover:bg-gray-200 dark:hover:bg-gray-800 w-full {( class="flex items-center py-3 px-4 rounded hover:bg-gray-200 dark:hover:bg-gray-800 w-full {(
path || $page.url.pathname path || $page.url.pathname
).includes(item.id) && activeClass}" ).includes(item.id) && NavClasses.active}"
> >
<TextChip <TextChip
value={getLectureNo(index + 1)} value={getLectureNo(index + 1)}
className="bg-blue-200 text-blue-600 text-xs mr-2" className="bg-blue-200 text-blue-600 text-xs mr-2"
size="xl" size="sm"
shape="rounded-full" shape="rounded-full"
/> />
<span>{item.title}</span> <span>{item.title}</span>
@@ -249,10 +249,13 @@
{/if} {/if}
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
{#if lesson.video_url} {#if lesson.videos}
<span class="text-sm font-light flex w-2/4" <span class="text-sm font-light flex w-2/4"
><Video size={16} class="mr-1" />1 video</span ><Video size={16} class="mr-1" />{lesson.videos.length} video{lesson.videos
> .length > 1
? 's'
: ''}
</span>
{/if} {/if}
{#if get(lesson, 'totalExercises[0].count')} {#if get(lesson, 'totalExercises[0].count')}
<span class="text-sm font-light flex w-2/4" <span class="text-sm font-light flex w-2/4"
@@ -3,7 +3,7 @@
</script> </script>
<div <div
class="root w-72 border bg-gray-100 dark:bg-gray-700 mr-4 mb-4 rounded-md transition ease-in-out relative" class="root w-72 border bg-gray-100 dark:bg-gray-800 mr-4 mb-4 rounded-md transition ease-in-out relative"
> >
<SkeletonPlaceholder style="width: 100%; height: 10rem;" /> <SkeletonPlaceholder style="width: 100%; height: 10rem;" />
<div class="p-3 pb-0"> <div class="p-3 pb-0">
@@ -47,7 +47,7 @@
<a rel="prefetch" href={isOnLandingPage ? `/course/${slug}` : `/courses/${id}`} class=" text-black"> <a rel="prefetch" href={isOnLandingPage ? `/course/${slug}` : `/courses/${id}`} class=" text-black">
<div <div
class="root w-80 border bg-gray-100 dark:bg-gray-700 sm:mr-4 mb-4 rounded-md hover:shadow-2xl transition ease-in-out relative" class="root w-80 border bg-gray-100 dark:bg-gray-800 sm:mr-4 mb-4 rounded-md hover:shadow-2xl transition ease-in-out relative"
> >
{#if !isOnLandingPage} {#if !isOnLandingPage}
<Dropdown options={getOptions()} classNames="absolute top-4 right-4" isIcon={true}> <Dropdown options={getOptions()} classNames="absolute top-4 right-4" isIcon={true}>
+4 -5
View File
@@ -11,8 +11,7 @@
import Avatar from '$lib/components/Avatar/index.svelte'; import Avatar from '$lib/components/Avatar/index.svelte';
import { currentOrg } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
import { NavClasses } from '$lib/utils/constants/reusableClass';
let activeClass = 'bg-gray-200 dark:bg-gray-800';
function isActive(pagePath, itemPath) { function isActive(pagePath, itemPath) {
const pageLinkItems = pagePath.split('/'); const pageLinkItems = pagePath.split('/');
@@ -77,7 +76,7 @@
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-500 {isActive( class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-500 {isActive(
$page.url.pathname, $page.url.pathname,
`${item.link}` `${item.link}`
) && activeClass}" ) && NavClasses.active}"
> >
<svelte:component <svelte:component
this={item.icon} this={item.icon}
@@ -101,10 +100,10 @@
</a> </a>
<a href="/lms/settings" class="text-black"> <a href="/lms/settings" class="text-black">
<li <li
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-800 {isActive( class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 {isActive(
$page.url.pathname, $page.url.pathname,
`/lms/settings` `/lms/settings`
) && activeClass}" ) && NavClasses.active}"
> >
<Avatar src={$profile.avatar_url} name={$profile.username} width="w-7" height="h-7" /> <Avatar src={$profile.avatar_url} name={$profile.username} width="w-7" height="h-7" />
<p class="dark:text-white ml-2">Settings</p> <p class="dark:text-white ml-2">Settings</p>
+1 -1
View File
@@ -46,7 +46,7 @@
}} }}
size="small" size="small"
> >
<CourseIcon fill="#fff" /> <CourseIcon color="#fff" />
</IconButton> </IconButton>
{/if} {/if}
<a <a
+7 -11
View File
@@ -1,5 +1,6 @@
<script> <script>
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import { ImageLoader } from 'carbon-components-svelte';
import OverflowMenuHorizontalIcon from 'carbon-icons-svelte/lib/OverflowMenuHorizontal.svelte'; import OverflowMenuHorizontalIcon from 'carbon-icons-svelte/lib/OverflowMenuHorizontal.svelte';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
@@ -33,7 +34,7 @@
label: 'Rename', label: 'Rename',
onClick() { onClick() {
onRename(quiz); onRename(quiz);
}, }
}, },
// { // {
// label: 'Make a copy', // label: 'Make a copy',
@@ -45,8 +46,8 @@
label: 'Delete', label: 'Delete',
onClick() { onClick() {
onDelete(quiz); onDelete(quiz);
}, }
}, }
]; ];
} }
@@ -55,12 +56,11 @@
{#if quiz} {#if quiz}
<div <div
class="root w-full border hover:shadow-2xl transition ease-in-out rounded-lg bg-gray-100 dark:bg-gray-700 mr-4 mb-5 relative flex p-3" class="root w-full border hover:shadow-2xl transition ease-in-out rounded-lg bg-gray-100 dark:bg-gray-800 mr-4 mb-5 relative flex p-3"
> >
<img <ImageLoader
src={themeImages[quiz.theme]?.card || themeImages.standard.card} src={themeImages[quiz.theme]?.card || themeImages.standard.card}
alt="quiz-card" alt="quiz-card"
class=""
style="max-width: 300px;" style="max-width: 300px;"
/> />
@@ -70,11 +70,7 @@
<a href="{$currentOrgPath}/quiz/{quiz.id}">{quiz.title}</a> <a href="{$currentOrgPath}/quiz/{quiz.id}">{quiz.title}</a>
</h4> </h4>
<Dropdown <Dropdown options={getOptions()} classNames="absolute top-4 right-4" isIcon={true}>
options={getOptions()}
classNames="absolute top-4 right-4"
isIcon={true}
>
<div class="p-1 rounded-full bg-gray-200 dark:bg-gray-600"> <div class="p-1 rounded-full bg-gray-200 dark:bg-gray-600">
<OverflowMenuHorizontalIcon size={20} class="carbon-icon active" /> <OverflowMenuHorizontalIcon size={20} class="carbon-icon active" />
</div> </div>
+14 -10
View File
@@ -13,8 +13,7 @@
import Avatar from '$lib/components/Avatar/index.svelte'; import Avatar from '$lib/components/Avatar/index.svelte';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org'; import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
import { NavClasses } from '$lib/utils/constants/reusableClass';
let activeClass = 'bg-gray-200 dark:bg-gray-800';
const menuItems = [ const menuItems = [
{ {
@@ -55,7 +54,9 @@
} }
</script> </script>
<aside class="root bg-gray-100 dark:bg-gray-700 h-full"> <aside
class="root bg-gray-100 dark:bg-gray-800 h-full border border-l-0 border-t-0 border-b-0 border-r-1"
>
<div class="h-full flex flex-col"> <div class="h-full flex flex-col">
<div class="border-b border-gray-200 pt-5 px-4"> <div class="border-b border-gray-200 pt-5 px-4">
{#if $currentOrg.avatar_url && $currentOrg.name} {#if $currentOrg.avatar_url && $currentOrg.name}
@@ -76,19 +77,22 @@
{#each menuItems as menuItem} {#each menuItems as menuItem}
<a href="{$currentOrgPath}{menuItem.path}" class="text-black no-underline"> <a href="{$currentOrgPath}{menuItem.path}" class="text-black no-underline">
<li <li
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-800 {isActive( class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 {isActive(
$page.url.pathname, $page.url.pathname,
`${$currentOrgPath}${menuItem.path}` `${$currentOrgPath}${menuItem.path}`
) && activeClass}" ) && NavClasses.active}"
> >
{#if menuItem.path === ''} {#if menuItem.path === ''}
<HomeIcon /> <HomeIcon color="#7A797D" />
{:else if menuItem.path === '/courses'} {:else if menuItem.path === '/courses'}
<CourseIcon /> <CourseIcon color="#7A797D" />
{:else if menuItem.path === '/site'} {:else if menuItem.path === '/site'}
<SiteSettingsIcon /> <SiteSettingsIcon />
{:else if menuItem.path === '/community'} {:else if menuItem.path === '/community'}
<ForumIcon size={20} class="carbon-icon dark:text-white" /> <ForumIcon
size={24}
class="carbon-icon dark:text-white fill-[#7A797D] dark:fill-[#fff]"
/>
{:else if menuItem.path === '/quiz'} {:else if menuItem.path === '/quiz'}
<QuizIcon /> <QuizIcon />
{:else if menuItem.path === '/audience'} {:else if menuItem.path === '/audience'}
@@ -112,10 +116,10 @@
</a> </a>
<a href="{$currentOrgPath}/settings" class="text-black no-underline"> <a href="{$currentOrgPath}/settings" class="text-black no-underline">
<li <li
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-800 {isActive( class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 {isActive(
$page.url.pathname, $page.url.pathname,
`${$currentOrgPath}/settings` `${$currentOrgPath}/settings`
) && activeClass}" ) && NavClasses.active}"
> >
<Avatar src={$profile.avatar_url} name={$profile.username} width="w-7" height="h-7" /> <Avatar src={$profile.avatar_url} name={$profile.username} width="w-7" height="h-7" />
<p class="dark:text-white ml-2">Settings</p> <p class="dark:text-white ml-2">Settings</p>
+2 -2
View File
@@ -20,7 +20,7 @@
</script> </script>
<div <div
class="header dark:bg-gray-700 bg-white flex items-center justify-between {!disableSticky && class="header dark:bg-gray-800 bg-white flex items-center justify-between {!disableSticky &&
'sticky'} {paddingClass} {dynamicRootClass}" 'sticky'} {paddingClass} {dynamicRootClass}"
style={overidableStyle} style={overidableStyle}
> >
@@ -29,7 +29,7 @@
{#if !enterEditTitleMode} {#if !enterEditTitleMode}
<button class="w-full" on:click={() => (enterEditTitleMode = true)}> <button class="w-full" on:click={() => (enterEditTitleMode = true)}>
<h4 <h4
class="editable-title hover:bg-gray-100 dark:bg-gray-700 px-3 rounded-md overflow-ellipsis" class="editable-title hover:bg-gray-100 dark:bg-gray-800 px-3 rounded-md overflow-ellipsis"
> >
{title} {title}
</h4> </h4>
@@ -1,6 +1,7 @@
<script> <script>
import { Moon } from 'svelte-loading-spinners'; // import { Moon } from 'svelte-loading-spinners';
import { VARIANTS, VARIANTS_CLASS } from './constants'; import { VARIANTS, VARIANTS_CLASS } from './constants';
import { Loading } from 'carbon-components-svelte';
export let label = ''; export let label = '';
export let className = ''; export let className = '';
@@ -33,7 +34,8 @@
disabled={isDisabled} disabled={isDisabled}
> >
{#if isLoading} {#if isLoading}
<Moon size="20" color="#1d4ed8" unit="px" duration="1s" /> <Loading withOverlay={false} small />
<!-- <Moon size="20" color="#1d4ed8" unit="px" duration="1s" /> -->
{:else if !!label} {:else if !!label}
{label} {label}
{:else} {:else}
-58
View File
@@ -1,58 +0,0 @@
<script>
import Icon from 'svelte-awesome/components/Icon.svelte';
import { faTelegram } from '@fortawesome/free-brands-svg-icons';
import { faEnvelope } from '@fortawesome/free-regular-svg-icons';
export let avatarUrl =
'https://tailwindcss.com/_next/static/media/sarah-dayan.a8ff3f1095a58085a82e3bb6aab12eb2.jpg';
export let name = 'Sarah Dayan';
export let title = 'Staff Engineer, Algolia';
export let mailLink;
export let telegramLink;
</script>
<figure class="w-52 bg-gray-100 dark:bg-gray-700 px-4 py-7 mr-4 mb-4">
<img
class="w-32 h-32 rounded-full mx-auto"
src={avatarUrl}
alt=""
width="384"
height="512"
/>
<div class="pt-6 text-center space-y-4">
<div class="description font-medium flex flex-col justify-around">
<div class="text-cyan-600">{name}</div>
<div class="text-gray-500 dark:text-white">{title}</div>
</div>
</div>
{#if telegramLink && mailLink}
<div class="flex justify-center">
<a href={telegramLink} target="_blank">
<Icon
data={faTelegram}
scale="1"
class="social-icon"
style="color:#4dabf7;margin:0 5px;"
/>
</a>
<a href={mailLink} target="_blank">
<Icon
data={faEnvelope}
scale="1"
class="social-icon"
style="color:#d44c3d;margin:0 5px;"
/>
</a>
</div>
{/if}
</figure>
<style>
img {
width: 104px;
height: 104px;
}
.description {
min-height: 70px;
}
</style>
+4
View File
@@ -0,0 +1,4 @@
export const NavClasses = {
active: 'bg-gray-200 dark:bg-gray-700',
background: ''
};
+1
View File
@@ -0,0 +1 @@
export default () => Date.now().toString(36) + Math.random().toString(36).substr(2);
+1 -1
View File
@@ -108,7 +108,7 @@ async function cloneLessons(lessons: Lesson[], courseId: Course['id']) {
public: lesson.public, public: lesson.public,
slide_url: lesson.slide_url, slide_url: lesson.slide_url,
title: lesson.title, title: lesson.title,
video_url: lesson.video_url videos: lesson.videos
})); }));
const { data } = await supabase.from('lesson').insert(clonedLessons).select(); const { data } = await supabase.from('lesson').insert(clonedLessons).select();
+2 -2
View File
@@ -34,7 +34,7 @@ export async function fetchCourse(courseId?: Course['id'], slug?: Course['slug']
certificate_theme, certificate_theme,
lessons:lesson( lessons:lesson(
id, title,public, lesson_at, call_url, is_unlocked, is_complete, order, created_at, id, title,public, lesson_at, call_url, is_unlocked, is_complete, order, created_at,
note, video_url, slide_url, call_url, totalExercises:exercise(count), note, videos, slide_url, call_url, totalExercises:exercise(count),
profile:teacher_id(id, avatar_url, fullname) profile:teacher_id(id, avatar_url, fullname)
), ),
attendance:group_attendance(*) attendance:group_attendance(*)
@@ -150,7 +150,7 @@ export function deleteGroupMember(groupMemberId: Groupmember['id']) {
export function fetchLesson(lessonId: Lesson['id']) { export function fetchLesson(lessonId: Lesson['id']) {
return supabase return supabase
.from('lesson') .from('lesson')
.select(`id, note, video_url, slide_url, call_url, totalExercises:exercise(count)`) .select(`id, note, videos, slide_url, call_url, totalExercises:exercise(count)`)
.eq('id', lessonId) .eq('id', lessonId)
.single(); .single();
} }
+21 -1
View File
@@ -136,9 +136,29 @@ export interface Group_attendance {
groupmember?: Groupmember; groupmember?: Groupmember;
} }
enum VideoType {
youtube,
muse
}
export interface LessonPage {
id?: string | null;
totalExercises: Number;
is_complete: Boolean;
materials: {
note: string;
slide_url: string;
videos: Array<{
type: string;
link: string;
metadata?: {};
}>;
};
exercises: [];
}
export interface Lesson { export interface Lesson {
note?: any; // type unknown; note?: any; // type unknown;
video_url?: any; // type unknown; videos?: []; // type unknown;
slide_url?: any; // type unknown; slide_url?: any; // type unknown;
course_id: string /* foreign key to course.id */; course_id: string /* foreign key to course.id */;
id?: string /* primary key */; id?: string /* primary key */;
+8 -2
View File
@@ -39,7 +39,7 @@
import '../app.postcss'; import '../app.postcss';
let supabase = getSupabase(); let supabase = getSupabase();
let path = $page.url.pathname.replace('/', ''); let path = $page.url?.pathname?.replace('/', '');
let theme = 'white'; let theme = 'white';
let skipAuth = false; let skipAuth = false;
@@ -261,7 +261,7 @@
}; };
}); });
$: path = $page.url.pathname.replace('/', ''); $: path = $page.url?.pathname?.replace('/', '');
$: theme = $appStore.isDark ? 'g100' : 'white'; $: theme = $appStore.isDark ? 'g100' : 'white';
$: setOrgBasedOnUrl($page.url.host); $: setOrgBasedOnUrl($page.url.host);
</script> </script>
@@ -384,4 +384,10 @@
:global(.dark .bx--search-close:hover > svg) { :global(.dark .bx--search-close:hover > svg) {
fill: #fff; fill: #fff;
} }
:global(.plyr__controls) {
background: url(/logo-192.png) 99% 70% no-repeat,
linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.5)) !important;
background-size: 50px auto, auto !important;
}
</style> </style>
+42
View File
@@ -0,0 +1,42 @@
import { json } from '@sveltejs/kit';
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import {
CLOUDFLARE_ACCESS_KEY,
CLOUDFLARE_SECRET_ACCESS_KEY,
CLOUDFLARE_ACCOUNT_ID,
CLOUDFLARE_PUBLIC_ACCOUNT_ID,
MUSE_API_KEY
} from '$env/static/private';
const S3 = new S3Client({
region: 'auto',
endpoint: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: CLOUDFLARE_ACCESS_KEY,
secretAccessKey: CLOUDFLARE_SECRET_ACCESS_KEY
}
});
const downloadTranscript = async () => {};
const uploadTranscriptToS3 = async () => {};
export async function GET({ url }) {
const museFid = url.searchParams.get('museFid');
console.log('museFid');
// check the transcript table and see if we find a record
// if we find one
// check if downloaded = true
// if true
// return the transcript
// if false
// download the transcript from muse
// upload it to S3
// return the transcript
// if we dont
return json({ success: true, museFid });
}
+2 -1
View File
@@ -3,6 +3,7 @@
import CourseContainer from '$lib/components/CourseContainer/index.svelte'; import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { setCourse } from '$lib/components/Course/store'; import { setCourse } from '$lib/components/Course/store';
import { fetchCourse } from '$lib/utils/services/courses'; import { fetchCourse } from '$lib/utils/services/courses';
import { browser } from '$app/environment';
export let data; export let data;
const { courseId } = data; const { courseId } = data;
@@ -10,7 +11,7 @@
let prevCourseId = ''; let prevCourseId = '';
async function onCourseIdChange(courseId = '') { async function onCourseIdChange(courseId = '') {
if (!courseId || prevCourseId === courseId) return; if (!courseId || prevCourseId === courseId || !browser) return;
const { data: _data } = await fetchCourse(courseId); const { data: _data } = await fetchCourse(courseId);
+2 -2
View File
@@ -135,12 +135,12 @@
{#each $lessons as lesson (lesson.id)} {#each $lessons as lesson (lesson.id)}
<div <div
bind:this={ref} bind:this={ref}
class="sm:h-48 md:h-40 lg:h-32 relative m-auto rounded-md border-2 border-gray-100 py-3 px-5 mb-4 flex items-center dark:bg-gray-700" class="sm:h-48 md:h-40 lg:h-32 relative m-auto rounded-md border-2 border-gray-200 py-3 px-5 mb-4 flex items-center dark:bg-gray-700"
> >
<div class="mr-5"> <div class="mr-5">
<TextChip <TextChip
value={getLessonOrder(lesson.id)} value={getLessonOrder(lesson.id)}
size="xl" size="sm"
shape="rounded-full" shape="rounded-full"
className="bg-blue-200 text-blue-600 text-xs" className="bg-blue-200 text-blue-600 text-xs"
/> />
@@ -1,13 +0,0 @@
export const load = ({ params = { id: '' } }) => {
const { id: courseId, lessonParams = [] } = params;
const splitparams = lessonParams.split('/');
const [lessonId, exerciseRouteName, exerciseId] = splitparams;
return {
courseId,
lessonId,
exerciseRouteName,
exerciseId,
isMaterialsTabActive: !exerciseRouteName
};
};
@@ -0,0 +1,167 @@
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import FormData from 'form-data';
import axios from 'axios';
import { fail } from '@sveltejs/kit';
import genUniqueId from '$lib/utils/functions/genUniqueId.js';
import { getSupabase } from '$lib/utils/functions/supabase';
import {
CLOUDFLARE_ACCESS_KEY,
CLOUDFLARE_SECRET_ACCESS_KEY,
CLOUDFLARE_ACCOUNT_ID,
CLOUDFLARE_PUBLIC_ACCOUNT_ID,
MUSE_API_KEY
} from '$env/static/private';
// Set the runtime to edge for best performance
export const config = {
runtime: 'edge'
};
const S3 = new S3Client({
region: 'auto',
endpoint: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: CLOUDFLARE_ACCESS_KEY,
secretAccessKey: CLOUDFLARE_SECRET_ACCESS_KEY
}
});
const supabase = getSupabase();
const uploadToMuse = (s3Data, fileName, lessonId, fileUrl) => {
try {
const form = new FormData();
form.append('file', s3Data, fileName);
axios({
method: 'POST',
url: 'https://api.muse.ai/api/files/upload',
data: form,
maxContentLength: Infinity,
maxBodyLength: Infinity,
headers: {
Key: MUSE_API_KEY
}
})
.then(async function (response) {
console.log('Upload to muse complete', response.data);
const { data: lesson } = await supabase
.from('lesson')
.select(`*`)
.eq('id', lessonId)
.single();
if (lesson) {
// Add Muse response to metadata
const videos = lesson.videos.map((v) => {
if (v.link === fileUrl) {
console.log('matches');
v.metadata = JSON.parse(JSON.stringify(response.data));
}
return v;
});
const { error, data } = await supabase
.from('lesson')
.update({ videos })
.eq('id', lessonId)
.select();
if (error) {
console.log('Error updating lesson', error);
}
console.log('update data', data);
}
})
.catch(function (error) {
console.log('Upload to Muse failed', error);
});
} catch (error) {
console.log('Upload to Muse failed', error);
}
};
export const load = ({ params = { id: '' } }) => {
const { id: courseId, lessonParams = [] } = params;
const splitparams = lessonParams.split('/');
const [lessonId, exerciseRouteName, exerciseId] = splitparams;
return {
courseId,
lessonId,
exerciseRouteName,
exerciseId,
isMaterialsTabActive: !exerciseRouteName
};
};
export const actions = {
create: async ({ request }) => {
const formData = Object.fromEntries(await request.formData());
console.log('formData', formData);
if (!formData.file?.name || formData.file?.name === 'undefined') {
return fail(400, {
success: false,
message: 'You must provide a file to upload'
});
}
if (formData.file?.size > 20 * 1024 * 1024) {
return fail(400, {
success: false,
type: 'FILE_TOO_LARGE',
message: 'File is too large'
});
}
const { file, lessonId } = formData;
console.log('file', file);
console.log('lessonId', lessonId);
const stream = await file.arrayBuffer();
const fileName = `${genUniqueId()}-${file.name}`;
const params = {
Bucket: 'videos',
Key: fileName,
Body: stream,
ContentLength: stream.length
};
console.log('params', params);
let upload;
const fileUrl = `https://pub-${CLOUDFLARE_PUBLIC_ACCOUNT_ID}.r2.dev/${fileName}`;
try {
upload = await S3.send(new PutObjectCommand(params));
// process data.
} catch (error) {
// error handling.
const { requestId, cfId, extendedRequestId } = error.$$metadata;
console.error('Error uploading file');
console.log({ requestId, cfId, extendedRequestId });
} finally {
// finally.
console.log('File upload complete', upload);
console.log('\n\nInitilizing Muse Request');
const data = await S3.send(
new GetObjectCommand({
Bucket: 'videos',
Key: fileName
})
);
// console.log('GetObjectCommand', data);
uploadToMuse(data.Body, file.name, lessonId, fileUrl);
}
return {
success: true,
url: fileUrl,
message: 'Uploaded successfully'
};
}
};
@@ -12,11 +12,17 @@
import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte'; import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
import MODES from '$lib/utils/constants/mode.js'; import MODES from '$lib/utils/constants/mode.js';
import { setCourse, course } from '$lib/components/Course/store'; import { setCourse, course } from '$lib/components/Course/store';
import { lesson, handleSaveLesson } from '$lib/components/Course/components/Lesson/store/lessons'; import {
lesson,
handleSaveLesson,
uploadCourseVideoStore
} from '$lib/components/Course/components/Lesson/store/lessons';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
export let data; export let data;
export let form;
let path = ''; let path = '';
let mode = MODES.view; let mode = MODES.view;
let prevMode = ''; let prevMode = '';
@@ -68,12 +74,14 @@
mode = mode === MODES.edit ? MODES.view : MODES.edit; mode = mode === MODES.edit ? MODES.view : MODES.edit;
} }
$: path = $page.url.pathname.replace(/\/exercises[\/ 0-9 a-z -]*/, ''); $: path = $page.url?.pathname?.replace(/\/exercises[\/ 0-9 a-z -]*/, '');
$: if (data.courseId && browser) { $: if (data.courseId && browser) {
mode = MODES.view; mode = MODES.view;
fetchReqData(data.lessonId); fetchReqData(data.lessonId);
} }
$: $uploadCourseVideoStore.formRes = form;
</script> </script>
<CourseContainer {isFetching} {path} isExercisePage={!data.isMaterialsTabActive && data.exerciseId}> <CourseContainer {isFetching} {path} isExercisePage={!data.isMaterialsTabActive && data.exerciseId}>
@@ -112,7 +120,7 @@
<Exercises lessonId={data.lessonId} exerciseId={data.exerciseId} path={`${path}/exercises`} /> <Exercises lessonId={data.lessonId} exerciseId={data.exerciseId} path={`${path}/exercises`} />
{:else if !!data.lessonId} {:else if !!data.lessonId}
<PageBody> <PageBody>
<Materials lessonId={data.lessonId} {mode} {prevMode} {toggleMode} /> <Materials lessonId={data.lessonId} {mode} {prevMode} />
<div class="w-full flex flex-row-reverse"> <div class="w-full flex flex-row-reverse">
<PrimaryButton <PrimaryButton
@@ -248,7 +248,7 @@
<div class="flex items-center w-full"> <div class="flex items-center w-full">
{#each sections as { id, title, items }, idx (id)} {#each sections as { id, title, items }, idx (id)}
<div <div
class="section rounded-md bg-gray-100 dark:bg-gray-700 border border-gray-50 p-3 h-80 mr-3 overflow-hidden" class="section rounded-md bg-gray-100 dark:bg-gray-800 border border-gray-50 p-3 h-80 mr-3 overflow-hidden"
animate:flip={{ duration: flipDurationMs }} animate:flip={{ duration: flipDurationMs }}
> >
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
@@ -293,7 +293,7 @@
</a> </a>
<a <a
class="flex items-center no-underline hover:underline text-black my-2" class="flex items-center no-underline hover:underline text-black my-2"
href="{$page.url.pathname.replace('submissions', 'lessons')}/{item.lesson href="{$page.url?.pathname?.replace('submissions', 'lessons')}/{item.lesson
.id}/exercises/{item.exercise.id}" .id}/exercises/{item.exercise.id}"
> >
<p class="dark:text-white text-grey text-sm"> <p class="dark:text-white text-grey text-sm">