Merge branch 'main' into CLASSROOMIO-T-84
This commit is contained in:
+3
-2
@@ -13,6 +13,7 @@
|
||||
"prepare": "svelte-kit sync"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.386.0",
|
||||
"@sentry/browser": "^7.61.0",
|
||||
"@sentry/integrations": "^7.61.0",
|
||||
"@sentry/tracing": "^7.61.0",
|
||||
@@ -25,10 +26,12 @@
|
||||
"@types/pluralize": "0.0.30",
|
||||
"ai": "^2.1.31",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"axios": "^1.4.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"canvas-confetti": "^1.6.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"form-data": "^4.0.0",
|
||||
"hotkeys-js": "^3.11.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -47,8 +50,6 @@
|
||||
"@babel/plugin-transform-runtime": "^7.22.9",
|
||||
"@babel/preset-env": "^7.22.9",
|
||||
"@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",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/lodash": "^4.14.196",
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
import LocalVideo from './LocalVideo.svelte';
|
||||
import * as CONSTANTS from './constants';
|
||||
|
||||
export let lessonId = '';
|
||||
export let saveLesson = () => {};
|
||||
|
||||
let tabs = CONSTANTS.videoTabs;
|
||||
let currentTab = tabs[0].value;
|
||||
|
||||
@@ -34,7 +37,7 @@
|
||||
{#if currentTab === 1}
|
||||
<YoutubeVideo />
|
||||
{:else}
|
||||
<LocalVideo />
|
||||
<LocalVideo {lessonId} {saveLesson} />
|
||||
{/if}
|
||||
</main>
|
||||
</section>
|
||||
|
||||
@@ -1,45 +1,95 @@
|
||||
<script>
|
||||
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 isUploaded = false;
|
||||
let isLoaded = false;
|
||||
let isBig = false;
|
||||
let fileInput;
|
||||
let submit;
|
||||
let uploadedFileUrl = '';
|
||||
let isLoading = false;
|
||||
|
||||
const uploadVideo = () => {
|
||||
// if the file is too big or not the correct format
|
||||
isWrongFormat = true;
|
||||
function isVideoAdded(link) {
|
||||
return $lesson.materials?.videos?.find((v) => v.link === link);
|
||||
}
|
||||
|
||||
// when the upload has been sucessfully completed
|
||||
// here the you set the video_url to something and that would be displayed on the page
|
||||
isUploaded = false;
|
||||
async function isDoneUploading(response) {
|
||||
uploadedFileUrl = response?.success && response?.url;
|
||||
|
||||
// closes the modal
|
||||
// $uploadCourseVideo.isModalOpen = false;
|
||||
};
|
||||
if (uploadedFileUrl && !isVideoAdded(uploadedFileUrl)) {
|
||||
$lesson.materials.videos = [
|
||||
...$lesson.materials.videos,
|
||||
{
|
||||
type: 'muse',
|
||||
link: uploadedFileUrl,
|
||||
metadata: {}
|
||||
}
|
||||
];
|
||||
saveLesson();
|
||||
}
|
||||
}
|
||||
|
||||
const onUpload = () => {
|
||||
// PROBABLY GET THE SNAPSHOT OR SOMETHING
|
||||
isLoaded = true;
|
||||
};
|
||||
$: isDoneUploading($uploadCourseVideoStore.formRes);
|
||||
</script>
|
||||
|
||||
{#if isLoaded == false}
|
||||
{#if isLoaded === false}
|
||||
<button
|
||||
on:click={onUpload}
|
||||
class="h-full w-full flex flex-col items-center justify-center border border-blue-300 border-dashed rounded-xl"
|
||||
type="button"
|
||||
on:click={() => (fileInput && !isLoading ? fileInput.click() : null)}
|
||||
class="w-full h-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<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 or drag and drop video here.<br /> Size must be less than
|
||||
20 MB.
|
||||
</p>
|
||||
</span>
|
||||
<form
|
||||
class="h-full w-full flex flex-col items-center justify-center border border-blue-300 border-dashed rounded-xl"
|
||||
method="POST"
|
||||
action="?/create"
|
||||
enctype="multipart/form-data"
|
||||
use:enhance={() => {
|
||||
isLoading = true;
|
||||
|
||||
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>
|
||||
{: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">
|
||||
<img src="/video-upload-error.svg" alt="upload error" />
|
||||
<span class="pt-3 pb-2">
|
||||
@@ -52,7 +102,7 @@
|
||||
</span>
|
||||
<PrimaryButton label="Try again" />
|
||||
</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">
|
||||
<img src="/video-upload-error.svg" alt="upload error" />
|
||||
<span class="pt-3 pb-2">
|
||||
@@ -72,17 +122,19 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center gap-2">
|
||||
<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>
|
||||
<p>Video.png</p>
|
||||
<p>{fileInput?.files?.[0]?.name || uploadedFileUrl}</p>
|
||||
</span>
|
||||
<button on:click={deleteLessonVideo}>
|
||||
<img src="/delete-video.svg" alt="deletevideo" class="dark:invert" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<PrimaryButton label="upload" onClick={uploadVideo} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,47 +1,49 @@
|
||||
<script>
|
||||
import { CopyButton, Tag } from 'carbon-components-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 { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
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 error = '';
|
||||
|
||||
function getVideoUrls(urls = '') {
|
||||
return (urls || '').split(',').filter((url) => !!url.trim());
|
||||
}
|
||||
|
||||
function addVideo() {
|
||||
const links = youtubeLinks.split(',').map((link) => link.trim());
|
||||
const links = getVideoUrls(youtubeLinks);
|
||||
const validLinks = links.filter(isValidYouTubeLink);
|
||||
|
||||
if (validLinks.length === 0) {
|
||||
error = 'Invalid YouTube link(s)';
|
||||
} else {
|
||||
const existingLinks = $lesson.materials.video_url
|
||||
.split(',')
|
||||
.map((link) => link.trim())
|
||||
.filter(Boolean);
|
||||
const updatedLinks = [
|
||||
const existingLinks = $lesson?.materials?.videos || [];
|
||||
|
||||
$lesson.materials.videos = [
|
||||
...existingLinks,
|
||||
...validLinks.filter((link) => !existingLinks.includes(link))
|
||||
...validLinks.map((link = '') => ({
|
||||
type: 'youtube',
|
||||
link,
|
||||
metadata: {}
|
||||
}))
|
||||
];
|
||||
$lesson.materials.video_url = updatedLinks.join(',').trim();
|
||||
youtubeLinks = '';
|
||||
error = '';
|
||||
}
|
||||
}
|
||||
|
||||
function isValidYouTubeLink(link) {
|
||||
function isValidYouTubeLink(link = '') {
|
||||
const youtubeRegex =
|
||||
/^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
|
||||
|
||||
return youtubeRegex.test(link.trim());
|
||||
}
|
||||
|
||||
function removeVideo(index) {
|
||||
const links = $lesson.materials.video_url.split(',').map((link) => link.trim());
|
||||
links.splice(index, 1);
|
||||
$lesson.materials.video_url = links.join(',').trim();
|
||||
function removeVideo(index = 0) {
|
||||
$lesson.materials.videos = $lesson.materials.videos.filter((v, i) => i !== index);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -58,22 +60,23 @@
|
||||
<PrimaryButton label="Add Video" className="rounded-md" onClick={addVideo} />
|
||||
</div>
|
||||
<p class="mt-4 pl-2 text-sm">
|
||||
Added : <strong
|
||||
>{($lesson?.materials?.video_url && $lesson.materials.video_url.trim().split(',').length) || 0} Videos</strong
|
||||
>
|
||||
Videos added: <strong>
|
||||
{$lesson?.materials?.videos.filter((v) => v.type === 'youtube' && isValidYouTubeLink(v.link))
|
||||
.length || 0}
|
||||
</strong>
|
||||
</p>
|
||||
<div class="">
|
||||
{#each $lesson?.materials?.video_url && $lesson.materials.video_url
|
||||
.trim()
|
||||
.split(',') as videoLink, index}
|
||||
<div class="flex items-center gap-1">
|
||||
<Tag type="blue">
|
||||
{videoLink}
|
||||
</Tag>
|
||||
<CopyButton text={videoLink} feedback="Copied to clipboard" />
|
||||
<IconButton value="delete-video" onClick={() => removeVideo(index)}>
|
||||
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
|
||||
</IconButton>
|
||||
</div>
|
||||
{#each $lesson?.materials?.videos as video, index}
|
||||
{#if video.type === 'youtube'}
|
||||
<div class="flex items-center gap-1">
|
||||
<Tag type="blue">
|
||||
{video.link}
|
||||
</Tag>
|
||||
<CopyButton text={video.link} feedback="Copied to clipboard" />
|
||||
<IconButton value="delete-video" onClick={() => removeVideo(index)}>
|
||||
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
|
||||
</IconButton>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { useCompletion } from 'ai/svelte';
|
||||
import merge from 'lodash/merge';
|
||||
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 { Popover } from 'carbon-components-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 IbmWatsonKnowledgeStudioIcon from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeStudio.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 { course } from '$lib/components/Course/store';
|
||||
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 prevMode = '';
|
||||
@@ -36,11 +39,10 @@
|
||||
let tabs = CONSTANTS.tabs;
|
||||
let currentTab = tabs[0].value;
|
||||
let errors = {};
|
||||
let player = {};
|
||||
let textareaRef = {};
|
||||
let aiButtonRef = {};
|
||||
let openPopover = false;
|
||||
let videoUrls: Array<string> = [];
|
||||
let player = null;
|
||||
let aiButtonClass =
|
||||
'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);
|
||||
|
||||
function initPlyr(_player, videoUrl = '') {
|
||||
if (!browser) return;
|
||||
// problem: after we save an uploaded video,
|
||||
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) => {
|
||||
// @ts-ignore
|
||||
return new Plyr(p);
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
window.players = players;
|
||||
lesson.update((l) => ({
|
||||
...l,
|
||||
materials
|
||||
}));
|
||||
handleUpdateLessonMaterials($lesson, lessonId);
|
||||
}
|
||||
|
||||
function handleSave(prevMode) {
|
||||
function handleSave(prevMode: string) {
|
||||
if (prevMode === MODES.edit) {
|
||||
handleUpdateLessonMaterials($lesson, lessonId);
|
||||
saveLesson();
|
||||
}
|
||||
}
|
||||
|
||||
function addBadgeValueToTab(materials) {
|
||||
const { slide_url, video_url, note } = materials;
|
||||
const { slide_url, videos, note } = materials;
|
||||
|
||||
tabs = tabs.map((tab) => {
|
||||
let badgeValue;
|
||||
@@ -78,7 +80,7 @@
|
||||
} else if (tab.value === 2) {
|
||||
badgeValue = !!note ? 1 : 0;
|
||||
} else {
|
||||
badgeValue = typeof video_url === 'string' && !!video_url ? video_url.split(',').length : 0;
|
||||
badgeValue = Array.isArray(videos) ? videos.length : 0;
|
||||
}
|
||||
|
||||
tab.badgeValue = badgeValue;
|
||||
@@ -113,20 +115,31 @@
|
||||
}, 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 = () => {
|
||||
handleUpdateLessonMaterials($lesson, lessonId);
|
||||
saveLesson();
|
||||
$uploadCourseVideoStore.isModalOpen = false;
|
||||
$uploadCourseVideoStore.formRes = null;
|
||||
};
|
||||
|
||||
$: handleSave(prevMode);
|
||||
|
||||
$: initPlyr(player, $lesson.materials.video_url);
|
||||
|
||||
$: addBadgeValueToTab($lesson.materials);
|
||||
|
||||
$: updateNoteByCompletion($completion);
|
||||
|
||||
$: videoUrls = ($lesson?.materials?.video_url || '').split(',').filter((url) => !!url.trim());
|
||||
$: initPlyr(player, $lesson.materials.videos);
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
@@ -135,7 +148,7 @@
|
||||
width="w-4/5 h-[566px]"
|
||||
modalHeading="Add a Video"
|
||||
>
|
||||
<VideoUploader {onClose} />
|
||||
<VideoUploader {lessonId} {saveLesson} />
|
||||
</Modal>
|
||||
|
||||
<Tabs {tabs} {currentTab} {onChange}>
|
||||
@@ -236,34 +249,56 @@
|
||||
</TabContent>
|
||||
|
||||
<TabContent value={tabs[2].value} index={currentTab}>
|
||||
{#if mode === MODES.edit && videoUrls.length}
|
||||
<PrimaryButton label="Add/Edit Video" onClick={openAddVideoModal} className="mb-2" />
|
||||
{#if mode === MODES.edit && $lesson.materials.videos.length}
|
||||
<PrimaryButton label="Add/Edit Video(s)" onClick={openAddVideoModal} className="mb-2" />
|
||||
{/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 videoUrls.length}
|
||||
{#if $lesson.materials.videos.length}
|
||||
<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">
|
||||
{#if url.includes('youtube')}
|
||||
{#key video.link}
|
||||
<div class="mb-5">
|
||||
<div
|
||||
bind:this={player}
|
||||
class="plyr-video-trigger"
|
||||
data-plyr-provider="youtube"
|
||||
data-plyr-embed-id={getEmbedId(url)}
|
||||
/>
|
||||
{#if video.type === 'youtube'}
|
||||
<iframe
|
||||
width="100%"
|
||||
height="569"
|
||||
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>
|
||||
{:else}
|
||||
<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}
|
||||
{/key}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Writable, Updater } from 'svelte/store';
|
||||
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({
|
||||
isModalOpen: false
|
||||
isModalOpen: false,
|
||||
formRes: null
|
||||
});
|
||||
|
||||
export const lessons: Writable<Lesson[]> = writable([]);
|
||||
export const lesson = writable({
|
||||
|
||||
export const lesson = writable<LessonPage>({
|
||||
id: null,
|
||||
totalExercises: 0,
|
||||
is_complete: false,
|
||||
materials: {
|
||||
note: '',
|
||||
slide_url: '',
|
||||
video_url: ''
|
||||
videos: []
|
||||
},
|
||||
exercises: []
|
||||
});
|
||||
@@ -103,14 +105,12 @@ export async function handleUpdateLessonMaterials(lesson: any, lessonId: Lesson[
|
||||
|
||||
export const deleteLessonVideo = (index: any) => {
|
||||
lesson.update((currentLesson) => {
|
||||
const links = currentLesson.materials.video_url.split(',').map((link) => link.trim());
|
||||
links.splice(index, 1);
|
||||
const updatedVideoUrl = links.join(',').trim();
|
||||
const videos = currentLesson.materials.videos || [];
|
||||
return {
|
||||
...currentLesson,
|
||||
materials: {
|
||||
...currentLesson.materials,
|
||||
video_url: updatedVideoUrl
|
||||
videos: videos?.filter((video, i) => i !== index)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<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 ChevronUpIcon from 'carbon-icons-svelte/lib/ChevronUp.svelte';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||
import NavIcons from './NavIcons.svelte';
|
||||
|
||||
export let handleClick = () => {};
|
||||
@@ -12,8 +13,6 @@
|
||||
export let total = 0;
|
||||
// export let subMenuItems = [];
|
||||
|
||||
let activeClass = 'bg-gray-200 dark:bg-gray-800';
|
||||
|
||||
function onClick() {
|
||||
handleClick();
|
||||
}
|
||||
@@ -25,8 +24,8 @@
|
||||
|
||||
<div class="">
|
||||
<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 &&
|
||||
activeClass} w-[95%]"
|
||||
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 &&
|
||||
NavClasses.active} w-[95%]"
|
||||
tabindex="0"
|
||||
on:click={onClick}
|
||||
>
|
||||
@@ -48,7 +47,7 @@
|
||||
{/if}
|
||||
</button>
|
||||
{#if isExpanded}
|
||||
<div class="flex flex-col">
|
||||
<div in:slide out:slide class="flex flex-col">
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { updateCourse } from '$lib/utils/services/courses';
|
||||
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';
|
||||
|
||||
// export let lessonId;
|
||||
@@ -32,7 +32,6 @@
|
||||
export let isStudent = null;
|
||||
|
||||
let show = null;
|
||||
let activeClass = 'bg-gray-200 dark:bg-gray-800';
|
||||
let isLessonActive = false;
|
||||
|
||||
function handleMainGroupClick(href) {
|
||||
@@ -151,7 +150,7 @@
|
||||
|
||||
<div class="root z-10 {!show && 'hide'} {$isMobile ? 'fixed shadow-xl' : 'sticky'}">
|
||||
{#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">
|
||||
<slot:fragment slot="image">
|
||||
<Avatar src={$course.logo} />
|
||||
@@ -202,12 +201,12 @@
|
||||
<div
|
||||
class="flex items-center py-3 px-4 rounded hover:bg-gray-200 dark:hover:bg-gray-800 w-full {(
|
||||
path || $page.url.pathname
|
||||
).includes(item.id) && activeClass}"
|
||||
).includes(item.id) && NavClasses.active}"
|
||||
>
|
||||
<TextChip
|
||||
value={getLectureNo(index + 1)}
|
||||
className="bg-blue-200 text-blue-600 text-xs mr-2"
|
||||
size="xl"
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
/>
|
||||
<span>{item.title}</span>
|
||||
|
||||
@@ -249,10 +249,13 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
{#if lesson.video_url}
|
||||
{#if lesson.videos}
|
||||
<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 get(lesson, 'totalExercises[0].count')}
|
||||
<span class="text-sm font-light flex w-2/4"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</script>
|
||||
|
||||
<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;" />
|
||||
<div class="p-3 pb-0">
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<a rel="prefetch" href={isOnLandingPage ? `/course/${slug}` : `/courses/${id}`} class=" text-black">
|
||||
<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}
|
||||
<Dropdown options={getOptions()} classNames="absolute top-4 right-4" isIcon={true}>
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
|
||||
let activeClass = 'bg-gray-200 dark:bg-gray-800';
|
||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||
|
||||
function isActive(pagePath, itemPath) {
|
||||
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(
|
||||
$page.url.pathname,
|
||||
`${item.link}`
|
||||
) && activeClass}"
|
||||
) && NavClasses.active}"
|
||||
>
|
||||
<svelte:component
|
||||
this={item.icon}
|
||||
@@ -101,10 +100,10 @@
|
||||
</a>
|
||||
<a href="/lms/settings" class="text-black">
|
||||
<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,
|
||||
`/lms/settings`
|
||||
) && activeClass}"
|
||||
) && NavClasses.active}"
|
||||
>
|
||||
<Avatar src={$profile.avatar_url} name={$profile.username} width="w-7" height="h-7" />
|
||||
<p class="dark:text-white ml-2">Settings</p>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<CourseIcon fill="#fff" />
|
||||
<CourseIcon color="#fff" />
|
||||
</IconButton>
|
||||
{/if}
|
||||
<a
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import pluralize from 'pluralize';
|
||||
import { ImageLoader } from 'carbon-components-svelte';
|
||||
import OverflowMenuHorizontalIcon from 'carbon-icons-svelte/lib/OverflowMenuHorizontal.svelte';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
@@ -33,7 +34,7 @@
|
||||
label: 'Rename',
|
||||
onClick() {
|
||||
onRename(quiz);
|
||||
},
|
||||
}
|
||||
},
|
||||
// {
|
||||
// label: 'Make a copy',
|
||||
@@ -45,8 +46,8 @@
|
||||
label: 'Delete',
|
||||
onClick() {
|
||||
onDelete(quiz);
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -55,12 +56,11 @@
|
||||
|
||||
{#if quiz}
|
||||
<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}
|
||||
alt="quiz-card"
|
||||
class=""
|
||||
style="max-width: 300px;"
|
||||
/>
|
||||
|
||||
@@ -70,11 +70,7 @@
|
||||
<a href="{$currentOrgPath}/quiz/{quiz.id}">{quiz.title}</a>
|
||||
</h4>
|
||||
|
||||
<Dropdown
|
||||
options={getOptions()}
|
||||
classNames="absolute top-4 right-4"
|
||||
isIcon={true}
|
||||
>
|
||||
<Dropdown options={getOptions()} classNames="absolute top-4 right-4" isIcon={true}>
|
||||
<div class="p-1 rounded-full bg-gray-200 dark:bg-gray-600">
|
||||
<OverflowMenuHorizontalIcon size={20} class="carbon-icon active" />
|
||||
</div>
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
|
||||
let activeClass = 'bg-gray-200 dark:bg-gray-800';
|
||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
@@ -55,7 +54,9 @@
|
||||
}
|
||||
</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="border-b border-gray-200 pt-5 px-4">
|
||||
{#if $currentOrg.avatar_url && $currentOrg.name}
|
||||
@@ -76,19 +77,22 @@
|
||||
{#each menuItems as menuItem}
|
||||
<a href="{$currentOrgPath}{menuItem.path}" class="text-black no-underline">
|
||||
<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,
|
||||
`${$currentOrgPath}${menuItem.path}`
|
||||
) && activeClass}"
|
||||
) && NavClasses.active}"
|
||||
>
|
||||
{#if menuItem.path === ''}
|
||||
<HomeIcon />
|
||||
<HomeIcon color="#7A797D" />
|
||||
{:else if menuItem.path === '/courses'}
|
||||
<CourseIcon />
|
||||
<CourseIcon color="#7A797D" />
|
||||
{:else if menuItem.path === '/site'}
|
||||
<SiteSettingsIcon />
|
||||
{: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'}
|
||||
<QuizIcon />
|
||||
{:else if menuItem.path === '/audience'}
|
||||
@@ -112,10 +116,10 @@
|
||||
</a>
|
||||
<a href="{$currentOrgPath}/settings" class="text-black no-underline">
|
||||
<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,
|
||||
`${$currentOrgPath}/settings`
|
||||
) && activeClass}"
|
||||
) && NavClasses.active}"
|
||||
>
|
||||
<Avatar src={$profile.avatar_url} name={$profile.username} width="w-7" height="h-7" />
|
||||
<p class="dark:text-white ml-2">Settings</p>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</script>
|
||||
|
||||
<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}"
|
||||
style={overidableStyle}
|
||||
>
|
||||
@@ -29,7 +29,7 @@
|
||||
{#if !enterEditTitleMode}
|
||||
<button class="w-full" on:click={() => (enterEditTitleMode = true)}>
|
||||
<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}
|
||||
</h4>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { Moon } from 'svelte-loading-spinners';
|
||||
// import { Moon } from 'svelte-loading-spinners';
|
||||
import { VARIANTS, VARIANTS_CLASS } from './constants';
|
||||
import { Loading } from 'carbon-components-svelte';
|
||||
|
||||
export let label = '';
|
||||
export let className = '';
|
||||
@@ -33,7 +34,8 @@
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{#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}
|
||||
{label}
|
||||
{:else}
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
export const NavClasses = {
|
||||
active: 'bg-gray-200 dark:bg-gray-700',
|
||||
background: ''
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export default () => Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
@@ -108,7 +108,7 @@ async function cloneLessons(lessons: Lesson[], courseId: Course['id']) {
|
||||
public: lesson.public,
|
||||
slide_url: lesson.slide_url,
|
||||
title: lesson.title,
|
||||
video_url: lesson.video_url
|
||||
videos: lesson.videos
|
||||
}));
|
||||
|
||||
const { data } = await supabase.from('lesson').insert(clonedLessons).select();
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function fetchCourse(courseId?: Course['id'], slug?: Course['slug']
|
||||
certificate_theme,
|
||||
lessons:lesson(
|
||||
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)
|
||||
),
|
||||
attendance:group_attendance(*)
|
||||
@@ -150,7 +150,7 @@ export function deleteGroupMember(groupMemberId: Groupmember['id']) {
|
||||
export function fetchLesson(lessonId: Lesson['id']) {
|
||||
return supabase
|
||||
.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)
|
||||
.single();
|
||||
}
|
||||
|
||||
@@ -136,9 +136,29 @@ export interface Group_attendance {
|
||||
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 {
|
||||
note?: any; // type unknown;
|
||||
video_url?: any; // type unknown;
|
||||
videos?: []; // type unknown;
|
||||
slide_url?: any; // type unknown;
|
||||
course_id: string /* foreign key to course.id */;
|
||||
id?: string /* primary key */;
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
import '../app.postcss';
|
||||
|
||||
let supabase = getSupabase();
|
||||
let path = $page.url.pathname.replace('/', '');
|
||||
let path = $page.url?.pathname?.replace('/', '');
|
||||
let theme = 'white';
|
||||
let skipAuth = false;
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
};
|
||||
});
|
||||
|
||||
$: path = $page.url.pathname.replace('/', '');
|
||||
$: path = $page.url?.pathname?.replace('/', '');
|
||||
$: theme = $appStore.isDark ? 'g100' : 'white';
|
||||
$: setOrgBasedOnUrl($page.url.host);
|
||||
</script>
|
||||
@@ -384,4 +384,10 @@
|
||||
:global(.dark .bx--search-close:hover > svg) {
|
||||
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>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
|
||||
import { setCourse } from '$lib/components/Course/store';
|
||||
import { fetchCourse } from '$lib/utils/services/courses';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export let data;
|
||||
const { courseId } = data;
|
||||
@@ -10,7 +11,7 @@
|
||||
let prevCourseId = '';
|
||||
|
||||
async function onCourseIdChange(courseId = '') {
|
||||
if (!courseId || prevCourseId === courseId) return;
|
||||
if (!courseId || prevCourseId === courseId || !browser) return;
|
||||
|
||||
const { data: _data } = await fetchCourse(courseId);
|
||||
|
||||
|
||||
@@ -135,12 +135,12 @@
|
||||
{#each $lessons as lesson (lesson.id)}
|
||||
<div
|
||||
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">
|
||||
<TextChip
|
||||
value={getLessonOrder(lesson.id)}
|
||||
size="xl"
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
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 MODES from '$lib/utils/constants/mode.js';
|
||||
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';
|
||||
|
||||
export let data;
|
||||
|
||||
export let form;
|
||||
|
||||
let path = '';
|
||||
let mode = MODES.view;
|
||||
let prevMode = '';
|
||||
@@ -68,12 +74,14 @@
|
||||
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) {
|
||||
mode = MODES.view;
|
||||
fetchReqData(data.lessonId);
|
||||
}
|
||||
|
||||
$: $uploadCourseVideoStore.formRes = form;
|
||||
</script>
|
||||
|
||||
<CourseContainer {isFetching} {path} isExercisePage={!data.isMaterialsTabActive && data.exerciseId}>
|
||||
@@ -112,7 +120,7 @@
|
||||
<Exercises lessonId={data.lessonId} exerciseId={data.exerciseId} path={`${path}/exercises`} />
|
||||
{:else if !!data.lessonId}
|
||||
<PageBody>
|
||||
<Materials lessonId={data.lessonId} {mode} {prevMode} {toggleMode} />
|
||||
<Materials lessonId={data.lessonId} {mode} {prevMode} />
|
||||
|
||||
<div class="w-full flex flex-row-reverse">
|
||||
<PrimaryButton
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
<div class="flex items-center w-full">
|
||||
{#each sections as { id, title, items }, idx (id)}
|
||||
<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 }}
|
||||
>
|
||||
<div class="flex items-center mb-2">
|
||||
@@ -293,7 +293,7 @@
|
||||
</a>
|
||||
<a
|
||||
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}"
|
||||
>
|
||||
<p class="dark:text-white text-grey text-sm">
|
||||
|
||||
Reference in New Issue
Block a user