Merge branch 'main' into CLASSROOMIO-T-84
This commit is contained in:
+3
-2
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
<CourseIcon fill="#fff" />
|
<CourseIcon color="#fff" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{/if}
|
{/if}
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
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();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 */;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 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);
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user