CLASSROOMIO-T-90:Implemented the course settings
only update order when lesson id changes CLASSROOMIO-T-90: Update banner image on course card CLASSROOMIO-T-90: Fix merge conflicts Merge branch 'main' into CLASSROOMIO-T-90 CLASSROOMIO-T-90:removed unused values and console log in the Course Settings CLASSROOMIO-T-90:removed unused import in the Course Settings CLASSROOMIO-T-90:fixed bugs in the Course Settings Merge branch 'main' into CLASSROOMIO-T-90 CLASSROOMIO-T-90:Implemented the Course Settings CLASSROOMIO-T-90:Worked on the course settings Merge branch 'main' into CLASSROOMIO-T-90 CLASSROOMIO-T-90:Fixed the landing page issue CLASSROOMIO-T-146:Made updates to the code following the reviews CLASSROOMIO-T-146:Made updates to the Z-Index Merge branch 'main' into CLASSROOMIO-T-146 Merge branch 'main' into CLASSROOMIO-T-146 CLASSROOMIO-T-146:Made the course page responsive CLASSROOMIO-T-146:working on the course page responsiveness Merge branch 'main' into CLASSROOMIO-T-145 CLASSROOMIO-T-146:Making the course page responsive CLASSROOMIO-T-145: All done and ready CLASSROOMIO-T-145: All done and ready CLASSROOMIO-T-145: Fixed the Z-index issue Merge branch 'main' into CLASSROOMIO-T-145 CLASSROOMIO-T-145: Finished implementing the responsive design for the Main dashboard and LMS dashboard Merge branch 'main' into CLASSROOMIO-T-145 CLASSROOMIO-T-144:Updating the dashboard responsiveness ooth for LMS and the Main dashboard CLASSROOMIO-T-144:Updated the dashboard responsiveness ooth for LMS and the Main dashboard Merge branch 'main' into CLASSROOMIO-T-145 CLASSROOMIO-T-145:Made the LMS dashboard responsive Co-authored-by: Ifeanyi Nwosu <ifeanyi@digdippa.com> Co-authored-by: Rotimi Best <rotimiibitoyeemma@gmail.com> Merge-request: CLASSROOMIO-MR-76 Merged-by: Best Admin <best@digdippa.com>
This commit is contained in:
committed by
Space Cloud
co-authored by
Rotimi Best
parent
e44353ac0a
commit
103a2eda7b
@@ -14,7 +14,6 @@
|
|||||||
import Notes from './components/Notes/index.svelte';
|
import Notes from './components/Notes/index.svelte';
|
||||||
import Poll from './components/Poll/index.svelte';
|
import Poll from './components/Poll/index.svelte';
|
||||||
import APPS_CONSTANTS from './constants';
|
import APPS_CONSTANTS from './constants';
|
||||||
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
|
|
||||||
|
|
||||||
let selectedApp;
|
let selectedApp;
|
||||||
|
|
||||||
@@ -28,8 +27,8 @@
|
|||||||
} else {
|
} else {
|
||||||
selectedApp = appName;
|
selectedApp = appName;
|
||||||
}
|
}
|
||||||
|
$apps.dropdown = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
hotkeys('A+1,A+2,A+3,A+4', function (event, handler) {
|
hotkeys('A+1,A+2,A+3,A+4', function (event, handler) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -146,7 +145,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1024px) {
|
@media screen and (max-width: 1023px) {
|
||||||
.root {
|
.root {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: calc(100vh - 109px);
|
height: calc(100vh - 109px);
|
||||||
@@ -166,4 +165,33 @@
|
|||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1024px) {
|
||||||
|
.root {
|
||||||
|
height: calc(100vh - 48px);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
border-left: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
& .apps {
|
||||||
|
padding-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 60px;
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .app {
|
||||||
|
width: 400px;
|
||||||
|
transition: all 1s ease-out;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { lesson } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||||
|
|
||||||
|
function isNoteEmpty(note) {
|
||||||
|
if (!note || typeof note !== 'string') return true;
|
||||||
|
|
||||||
|
if (!document) return false;
|
||||||
|
|
||||||
|
const dummyDiv = document.createElement('div');
|
||||||
|
dummyDiv.innerHTML = note;
|
||||||
|
|
||||||
|
const rawText = dummyDiv.textContent?.trim();
|
||||||
|
|
||||||
|
return rawText === '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !isNoteEmpty($lesson.materials?.note)}
|
||||||
|
<article class="preview prose prose-sm sm:prose p-2">
|
||||||
|
{@html $lesson.materials.note}
|
||||||
|
</article>
|
||||||
|
{/if}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<script>
|
||||||
|
import { lesson } from '../../store/lessons';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $lesson.materials.slide_url}
|
||||||
|
<iframe
|
||||||
|
title="Embeded Slides"
|
||||||
|
src={$lesson.materials.slide_url}
|
||||||
|
frameborder="0"
|
||||||
|
width="100%"
|
||||||
|
height="569"
|
||||||
|
class="iframe my-3"
|
||||||
|
allowfullscreen="true"
|
||||||
|
mozallowfullscreen="true"
|
||||||
|
webkitallowfullscreen="true"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
|
||||||
|
import { lesson } from '../../store/lessons';
|
||||||
|
|
||||||
|
let errors = {};
|
||||||
|
let player = null;
|
||||||
|
|
||||||
|
function initPlyr(_player: any, _video: string | undefined) {
|
||||||
|
if (!_player) return;
|
||||||
|
|
||||||
|
const players = Array.from(document.querySelectorAll('.plyr-video-trigger')).map((p) => {
|
||||||
|
// @ts-ignore
|
||||||
|
return new Plyr(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
window.players = players;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: initPlyr(player, $lesson.materials.videos);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $lesson.materials.videos.length}
|
||||||
|
<div class="w-full">
|
||||||
|
{#each $lesson.materials.videos as video}
|
||||||
|
<div class="w-full overflow-hidden mb-5">
|
||||||
|
{#key video.link}
|
||||||
|
<div class="mb-5">
|
||||||
|
{#if video.type === 'youtube'}
|
||||||
|
<iframe
|
||||||
|
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>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 450px;
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.iframe {
|
||||||
|
height: 209px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Video, Notebook, PresentationFile } from 'carbon-icons-svelte';
|
import { Video, Notebook, PresentationFile } from 'carbon-icons-svelte';
|
||||||
|
|
||||||
export const tabs = [
|
export let tabs = [
|
||||||
{
|
{
|
||||||
label: 'Note',
|
label: 'Note',
|
||||||
icon: Notebook,
|
icon: Notebook,
|
||||||
@@ -20,3 +20,20 @@ export const tabs = [
|
|||||||
badgeValue: 0
|
badgeValue: 0
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function orderedTabs(tabs, Settingtabs) {
|
||||||
|
if (!Array.isArray(Settingtabs)) return tabs;
|
||||||
|
const reorderedTabs = [];
|
||||||
|
|
||||||
|
const tabMap = new Map(tabs.map((tab) => [tab.label, tab]));
|
||||||
|
|
||||||
|
Settingtabs.forEach((settingTab, index) => {
|
||||||
|
const tab = tabMap.get(settingTab.name);
|
||||||
|
if (tab) {
|
||||||
|
reorderedTabs[index] = tab;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredReorderedTabs = reorderedTabs.filter((tab) => tab !== undefined);
|
||||||
|
return filteredReorderedTabs;
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@
|
|||||||
import { course } from '$lib/components/Course/store';
|
import { course } from '$lib/components/Course/store';
|
||||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||||
import * as CONSTANTS from './constants';
|
import * as CONSTANTS from './constants';
|
||||||
|
import { orderedTabs } from './constants';
|
||||||
|
import ComponentNote from './components/ComponentNote.svelte';
|
||||||
|
import ComponentSlide from './components/ComponentSlide.svelte';
|
||||||
|
import ComponentVideo from './components/ComponentVideo.svelte';
|
||||||
|
|
||||||
export let mode = MODES.view;
|
export let mode = MODES.view;
|
||||||
export let prevMode = '';
|
export let prevMode = '';
|
||||||
@@ -47,6 +51,7 @@
|
|||||||
let aiButtonRef = {};
|
let aiButtonRef = {};
|
||||||
let openPopover = false;
|
let openPopover = false;
|
||||||
let player = null;
|
let player = null;
|
||||||
|
let componentsToRender = getComponentOrder(tabs);
|
||||||
let aiButtonClass =
|
let aiButtonClass =
|
||||||
'flex items-center px-5 py-2 border border-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md w-full mb-2';
|
'flex items-center px-5 py-2 border border-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md w-full mb-2';
|
||||||
|
|
||||||
@@ -55,6 +60,10 @@
|
|||||||
() =>
|
() =>
|
||||||
(currentTab = tab);
|
(currentTab = tab);
|
||||||
|
|
||||||
|
const getValue = (label) => {
|
||||||
|
const tabValue = tabs.find((tab) => tab.label === label)?.value;
|
||||||
|
return tabValue;
|
||||||
|
};
|
||||||
async function saveLesson(materials = undefined) {
|
async function saveLesson(materials = undefined) {
|
||||||
const _lesson = !!materials
|
const _lesson = !!materials
|
||||||
? {
|
? {
|
||||||
@@ -174,12 +183,33 @@
|
|||||||
function onLessonIdChange(_lid: string) {
|
function onLessonIdChange(_lid: string) {
|
||||||
initAutoSave = false;
|
initAutoSave = false;
|
||||||
isSaving = false;
|
isSaving = false;
|
||||||
|
|
||||||
|
tabs = orderedTabs(tabs, $course.metadata?.lessonTabsOrder);
|
||||||
|
currentTab = tabs[0].value;
|
||||||
|
componentsToRender = getComponentOrder(tabs);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
$uploadCourseVideoStore.isModalOpen = false;
|
$uploadCourseVideoStore.isModalOpen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getComponentOrder(tabs) {
|
||||||
|
const componentMap = {
|
||||||
|
Video: ComponentVideo,
|
||||||
|
Slide: ComponentSlide,
|
||||||
|
Note: ComponentNote
|
||||||
|
};
|
||||||
|
|
||||||
|
const componentNames = tabs
|
||||||
|
.map((tab) => {
|
||||||
|
const component = componentMap[tab.label];
|
||||||
|
return component || null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return componentNames;
|
||||||
|
}
|
||||||
|
|
||||||
$: autoSave($lesson.materials, $isLoading, lessonId);
|
$: autoSave($lesson.materials, $isLoading, lessonId);
|
||||||
|
|
||||||
$: onLessonIdChange(lessonId);
|
$: onLessonIdChange(lessonId);
|
||||||
@@ -211,7 +241,7 @@
|
|||||||
{#if mode === MODES.edit}
|
{#if mode === MODES.edit}
|
||||||
<Tabs {tabs} {currentTab} {onChange}>
|
<Tabs {tabs} {currentTab} {onChange}>
|
||||||
<slot:fragment slot="content">
|
<slot:fragment slot="content">
|
||||||
<TabContent value={tabs[0].value} index={currentTab}>
|
<TabContent value={getValue('Note')} index={currentTab}>
|
||||||
<div bind:this={aiButtonRef} class="w-full flex flex-row-reverse">
|
<div bind:this={aiButtonRef} class="w-full flex flex-row-reverse">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
className="flex items-center relative"
|
className="flex items-center relative"
|
||||||
@@ -261,7 +291,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
|
||||||
<TabContent value={tabs[1].value} index={currentTab}>
|
<TabContent value={getValue('Slide')} index={currentTab}>
|
||||||
{#if mode === MODES.edit}
|
{#if mode === MODES.edit}
|
||||||
<TextField
|
<TextField
|
||||||
label="Slide link"
|
label="Slide link"
|
||||||
@@ -270,8 +300,7 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
<TabContent value={getValue('Video')} index={currentTab}>
|
||||||
<TabContent value={tabs[2].value} index={currentTab}>
|
|
||||||
<PrimaryButton label="Add/Edit Video(s)" onClick={openAddVideoModal} className="mb-2" />
|
<PrimaryButton label="Add/Edit Video(s)" onClick={openAddVideoModal} className="mb-2" />
|
||||||
{#if $lesson.materials.videos.length}
|
{#if $lesson.materials.videos.length}
|
||||||
<div class="flex flex-col items-start w-full h-full">
|
<div class="flex flex-col items-start w-full h-full">
|
||||||
@@ -328,64 +357,11 @@
|
|||||||
</slot:fragment>
|
</slot:fragment>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{:else if !isMaterialsEmpty($lesson.materials)}
|
{:else if !isMaterialsEmpty($lesson.materials)}
|
||||||
{#if $lesson.materials.videos.length}
|
<div class="w-full">
|
||||||
<div class="w-full">
|
{#each componentsToRender as Component, index}
|
||||||
{#each $lesson.materials.videos as video}
|
<svelte:component this={Component} key={index} />
|
||||||
<div class="w-full overflow-hidden mb-5">
|
{/each}
|
||||||
{#key video.link}
|
</div>
|
||||||
<div class="mb-5">
|
|
||||||
{#if video.type === 'youtube'}
|
|
||||||
<iframe
|
|
||||||
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>
|
|
||||||
{/key}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $lesson.materials.slide_url}
|
|
||||||
<iframe
|
|
||||||
title="Embeded Slides"
|
|
||||||
src={$lesson.materials.slide_url}
|
|
||||||
frameborder="0"
|
|
||||||
width="100%"
|
|
||||||
height="569"
|
|
||||||
class="iframe my-3"
|
|
||||||
allowfullscreen="true"
|
|
||||||
mozallowfullscreen="true"
|
|
||||||
webkitallowfullscreen="true"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !isNoteEmpty($lesson.materials?.note)}
|
|
||||||
<article class="preview prose prose-sm sm:prose p-2">
|
|
||||||
{@html $lesson.materials.note}
|
|
||||||
</article>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
{:else}
|
||||||
<Box className="text-center">
|
<Box className="text-center">
|
||||||
<img src="/no-video.svg" alt="Video not found" />
|
<img src="/no-video.svg" alt="Video not found" />
|
||||||
@@ -399,16 +375,3 @@
|
|||||||
<PrimaryButton label="Get started" className="rounded-md" onClick={toggleMode} />
|
<PrimaryButton label="Get started" className="rounded-md" onClick={toggleMode} />
|
||||||
</Box>
|
</Box>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
|
||||||
.iframe {
|
|
||||||
width: 100%;
|
|
||||||
height: 450px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
|
||||||
.iframe {
|
|
||||||
height: 209px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import ApplicationWeb from 'carbon-icons-svelte/lib/ApplicationWeb.svelte';
|
import ApplicationWeb from 'carbon-icons-svelte/lib/ApplicationWeb.svelte';
|
||||||
import Badge from 'carbon-icons-svelte/lib/Badge.svelte';
|
import Badge from 'carbon-icons-svelte/lib/Badge.svelte';
|
||||||
import IbmWatsonKnowledgeCatalog from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeCatalog.svelte';
|
import IbmWatsonKnowledgeCatalog from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeCatalog.svelte';
|
||||||
|
import Settings from 'carbon-icons-svelte/lib/Settings.svelte';
|
||||||
|
|
||||||
export let name = '';
|
export let name = '';
|
||||||
</script>
|
</script>
|
||||||
@@ -27,4 +28,6 @@
|
|||||||
<ApplicationWeb size={20} class="carbon-icon mr-2" />
|
<ApplicationWeb size={20} class="carbon-icon mr-2" />
|
||||||
{:else if name === 'Certificates'}
|
{:else if name === 'Certificates'}
|
||||||
<Badge size={20} class="carbon-icon mr-2" />
|
<Badge size={20} class="carbon-icon mr-2" />
|
||||||
|
{:else if name === 'Settings'}
|
||||||
|
<Settings size={20} class="carbon-icon mr-2" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -5,20 +5,10 @@
|
|||||||
import hotkeys from 'hotkeys-js';
|
import hotkeys from 'hotkeys-js';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import LockedIcon from 'carbon-icons-svelte/lib/Locked.svelte';
|
import LockedIcon from 'carbon-icons-svelte/lib/Locked.svelte';
|
||||||
// import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
|
||||||
import CheckmarkFilled from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
|
import CheckmarkFilled from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
|
||||||
// import SettingsIcon from 'carbon-icons-svelte/lib/Settings.svelte';
|
|
||||||
import ChevronLeftIcon from 'carbon-icons-svelte/lib/ChevronLeft.svelte';
|
|
||||||
import ChevronRightIcon from 'carbon-icons-svelte/lib/ChevronRight.svelte';
|
|
||||||
import NavExpandable from './NavExpandable.svelte';
|
import NavExpandable from './NavExpandable.svelte';
|
||||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
|
||||||
// import Avatar from '$lib/components/Avatar/index.svelte';
|
|
||||||
import { getNavItemRoute, getLessonsRoute, getLectureNo } from '$lib/components/Course/function';
|
import { getNavItemRoute, getLessonsRoute, getLectureNo } from '$lib/components/Course/function';
|
||||||
|
|
||||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||||
// import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
|
||||||
import Settings from '../Settings/index.svelte';
|
|
||||||
// import { settingsDialog } from '../Settings/store';
|
|
||||||
import { lessons } from '../Lesson/store/lessons';
|
import { lessons } from '../Lesson/store/lessons';
|
||||||
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';
|
||||||
@@ -26,7 +16,6 @@
|
|||||||
import { isMobile } from '$lib/utils/store/useMobile';
|
import { isMobile } from '$lib/utils/store/useMobile';
|
||||||
import { menu } from '$lib/components/Org/store';
|
import { menu } from '$lib/components/Org/store';
|
||||||
|
|
||||||
// export let lessonId;
|
|
||||||
export let path;
|
export let path;
|
||||||
export let isStudent = null;
|
export let isStudent = null;
|
||||||
|
|
||||||
@@ -142,19 +131,25 @@
|
|||||||
show() {
|
show() {
|
||||||
return !isStudent;
|
return !isStudent;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Settings',
|
||||||
|
to: getNavItemRoute($course.id, 'settings'),
|
||||||
|
hideSortIcon: true,
|
||||||
|
show() {
|
||||||
|
return !isStudent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Settings />
|
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
class={`${
|
class={`${
|
||||||
$menu.hidden
|
$menu.hidden
|
||||||
? '-translate-x-[100%] absolute md:translate-x-0 md:relative z-[40]'
|
? '-translate-x-[100%] absolute md:translate-x-0 md:relative z-[40]'
|
||||||
: 'translate-x-0 absolute md:relative z-[40]'
|
: 'translate-x-0 absolute md:relative z-[40]'
|
||||||
} transition w-[350px] min-w-[350px] md:max-w-[350px] bg-gray-100 dark:bg-black h-[calc(100vh-48px)] overflow-y-auto border border-l-0 border-t-0 border-b-0 border-r-1`}
|
} transition w-[90vw] md:max-w-[350px] bg-gray-100 dark:bg-black h-[calc(100vh-48px)] overflow-y-auto 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">
|
||||||
<ul class="my-5">
|
<ul class="my-5">
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script>
|
||||||
|
import { flip } from 'svelte/animate';
|
||||||
|
import { settings } from './store';
|
||||||
|
import { dndzone } from 'svelte-dnd-action';
|
||||||
|
import Draggable from 'carbon-icons-svelte/lib/Draggable.svelte';
|
||||||
|
|
||||||
|
const flipDurationMs = 300;
|
||||||
|
|
||||||
|
function handleDndConsider(e) {
|
||||||
|
$settings.tabs = e.detail.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDndFinalize(e) {
|
||||||
|
$settings.tabs = e.detail.items;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section
|
||||||
|
use:dndzone={{
|
||||||
|
items: $settings.tabs,
|
||||||
|
flipDurationMs,
|
||||||
|
dropTargetStyle: {
|
||||||
|
border: '2px #1d4ed8 solid',
|
||||||
|
'border-style': 'dashed'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:consider={handleDndConsider}
|
||||||
|
on:finalize={handleDndFinalize}
|
||||||
|
class="w-fit flex gap-1 md:gap-3 p-1"
|
||||||
|
>
|
||||||
|
{#each $settings.tabs as item (item.id)}
|
||||||
|
<div
|
||||||
|
animate:flip={{ duration: flipDurationMs }}
|
||||||
|
class="flex items-center justify-start text-center text-xs md:text-base gap-1 md:gap-2 dark:text-white bg-slate-100 dark:bg-slate-700 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<Draggable size={16} />
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
@@ -1,112 +1,342 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import Modal from '$lib/components/Modal/index.svelte';
|
import { Grid, Row, Column, Toggle } from 'carbon-components-svelte';
|
||||||
|
|
||||||
|
import SectionTitle from '$lib/components/Org/SectionTitle.svelte';
|
||||||
import TextField from '$lib/components/Form/TextField.svelte';
|
import TextField from '$lib/components/Form/TextField.svelte';
|
||||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
import DragAndDrop from './DragAndDrop.svelte';
|
||||||
|
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
import { settingsDialog } from './store';
|
import { settings } from './store';
|
||||||
import { course } from '$lib/components/Course/store';
|
import { course } from '$lib/components/Course/store';
|
||||||
import { updateCourse, deleteCourse } from '$lib/utils/services/courses';
|
import { updateCourse, deleteCourse } from '$lib/utils/services/courses';
|
||||||
import { currentOrgPath } from '$lib/utils/store/org';
|
import { currentOrgPath } from '$lib/utils/store/org';
|
||||||
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
|
import { Loading } from 'carbon-components-svelte';
|
||||||
|
import { isObject } from '$lib/utils/functions/isObject';
|
||||||
|
import { lessons } from '../Lesson/store/lessons';
|
||||||
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
import { snackbarStore } from '$lib/components/Snackbar/store';
|
||||||
|
import { SNACKBAR_SEVERITY } from '$lib/components/Snackbar/constants';
|
||||||
|
|
||||||
let shouldDelete = false;
|
let uploadingImage = false;
|
||||||
let avatar;
|
let isSaving = false;
|
||||||
|
let fileInput;
|
||||||
|
let isLoading = false;
|
||||||
|
let isDeleting = false;
|
||||||
let errors = {};
|
let errors = {};
|
||||||
|
let avatar;
|
||||||
|
let imagebuffer;
|
||||||
|
|
||||||
async function updateDescription() {
|
// FILE UPLOAD
|
||||||
if (!$course.title) {
|
const uploadImage = async (image: File) => {
|
||||||
errors.title = 'Title cannot be empty';
|
if (image) {
|
||||||
return;
|
const filename = `bannerimage/${Date.now()}` + image.name;
|
||||||
|
const { data } = await supabase.storage.from('avatars').upload(filename, image, {
|
||||||
|
cacheControl: '3600',
|
||||||
|
upsert: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const { data: response } = await supabase.storage.from('avatars').getPublicUrl(filename);
|
||||||
|
|
||||||
|
$settings.image = response.publicUrl;
|
||||||
|
|
||||||
|
uploadingImage = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!$course.description) {
|
};
|
||||||
errors.description = 'Description cannot be empty';
|
|
||||||
return;
|
const onFileSelected = () => {
|
||||||
|
uploadingImage = true;
|
||||||
|
const image = fileInput.files[0];
|
||||||
|
if (image) {
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.readAsDataURL(image);
|
||||||
|
reader.onload = (e) => {
|
||||||
|
imagebuffer = image;
|
||||||
|
uploadImage(image);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
$settingsDialog.open = false;
|
function getLessonOrder(id) {
|
||||||
const logo = await updateCourse($course.id, avatar, {
|
const index = $lessons.findIndex((lesson) => lesson.id === id);
|
||||||
title: $course.title,
|
if (index < 9) {
|
||||||
description: $course.description
|
return '0' + (index + 1);
|
||||||
});
|
} else {
|
||||||
|
return index + 1;
|
||||||
if (logo) {
|
|
||||||
$course.logo = logo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
avatar = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete() {
|
const downloadCourse = async () => {
|
||||||
$settingsDialog.open = false;
|
isLoading = true;
|
||||||
await deleteCourse($course.id);
|
try {
|
||||||
goto($currentOrgPath + '/courses');
|
const lessonsList = $lessons.map((lesson) => ({
|
||||||
|
lessonTitle: lesson.title,
|
||||||
|
lessonNumber: getLessonOrder(lesson.id),
|
||||||
|
lessonNote: lesson.note,
|
||||||
|
slideUrl: lesson.slide_url || '',
|
||||||
|
video: lesson.videos || ''
|
||||||
|
}));
|
||||||
|
const response = await fetch('https://classroomio-server.fly.dev/downloadCourse', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
courseTitle: $course.title,
|
||||||
|
orgName: $currentOrg.name,
|
||||||
|
orgTheme: $currentOrg.theme || '',
|
||||||
|
lessons: lessonsList
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await response.text());
|
||||||
|
}
|
||||||
|
const data = await response.blob();
|
||||||
|
const file = new Blob([data], { type: 'application/pdf' });
|
||||||
|
const fileURL = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
let a = document.createElement('a');
|
||||||
|
document.body.append(a);
|
||||||
|
a.download = $course.title + ' - ' + 'Course ';
|
||||||
|
a.href = fileURL;
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
|
||||||
|
$snackbarStore.message = 'Download Complete';
|
||||||
|
$snackbarStore.severity = SNACKBAR_SEVERITY.SUCCESS;
|
||||||
|
$snackbarStore.open = true;
|
||||||
|
} catch (error) {
|
||||||
|
$snackbarStore.message = "Something's not right - Please try later";
|
||||||
|
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
|
||||||
|
$snackbarStore.open = true;
|
||||||
|
}
|
||||||
|
isLoading = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBannerImage = () => {
|
||||||
|
$course.logo = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handleDeleteCourse() {
|
||||||
|
isDeleting = true;
|
||||||
|
try {
|
||||||
|
await deleteCourse($course.id);
|
||||||
|
isDeleting = false;
|
||||||
|
goto($currentOrgPath + '/courses');
|
||||||
|
} catch (error) {
|
||||||
|
$snackbarStore.message = 'Something went wrong - Please try later';
|
||||||
|
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
|
||||||
|
$snackbarStore.open = true;
|
||||||
|
isDeleting = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// function setAvatar(logo) {
|
const handleSave = async () => {
|
||||||
// avatar = logo;
|
if (!$settings.course_title) {
|
||||||
// }
|
errors.title = 'title cannot be empty';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!$settings.course_description) {
|
||||||
|
errors.description = 'description cannot be empty';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isSaving = true;
|
||||||
|
try {
|
||||||
|
const { course_title, course_description, image, tabs, grading, lesson_download } = $settings;
|
||||||
|
await updateCourse($course.id, avatar, {
|
||||||
|
title: course_title,
|
||||||
|
description: course_description,
|
||||||
|
logo: image,
|
||||||
|
metadata: {
|
||||||
|
...(isObject($course.metadata) ? $course.metadata : {}),
|
||||||
|
lessonTabsOrder: tabs,
|
||||||
|
grading: grading,
|
||||||
|
lessonDownload: lesson_download
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// $: setAvatar($course.logo);
|
$course.title = course_title;
|
||||||
|
$course.description = course_description;
|
||||||
|
$course.logo = image;
|
||||||
|
$course.metadata = {
|
||||||
|
...(isObject($course.metadata) ? $course.metadata : {}),
|
||||||
|
lessonTabsOrder: tabs,
|
||||||
|
grading: grading,
|
||||||
|
lessonDownload: lesson_download
|
||||||
|
};
|
||||||
|
$snackbarStore.message = 'saved successfully';
|
||||||
|
$snackbarStore.severity = SNACKBAR_SEVERITY.SUCCESS;
|
||||||
|
$snackbarStore.open = true;
|
||||||
|
} catch (error) {
|
||||||
|
$snackbarStore.message = 'Something went wrong - Please try later';
|
||||||
|
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
|
||||||
|
$snackbarStore.open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSaving = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
function setDefault(course) {
|
||||||
|
if (course && Object.keys(course).length) {
|
||||||
|
$settings = {
|
||||||
|
course_title: course.title,
|
||||||
|
course_description: course.description,
|
||||||
|
image: course.logo,
|
||||||
|
tabs: course.metadata.lessonTabsOrder || $settings.tabs,
|
||||||
|
grading: course.metadata.grading,
|
||||||
|
lesson_download: course.metadata.lessonDownload
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: $settings.course_description = $course.description;
|
||||||
|
$: $settings.course_title = $course.title;
|
||||||
|
$: setDefault($course);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal
|
<Grid class="border-c rounded border-gray-200">
|
||||||
onClose={() => ($settingsDialog.open = false)}
|
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||||
bind:open={$settingsDialog.open}
|
<Column sm={8} md={8} lg={8}>
|
||||||
width="w-96"
|
<SectionTitle>Banner Image</SectionTitle>
|
||||||
modalHeading="Update Course"
|
<p>
|
||||||
>
|
This optional image will show up on your welcome page. If you include one, it should be at
|
||||||
{#if shouldDelete}
|
least 300 x 200"
|
||||||
<form on:submit|preventDefault={() => (shouldDelete = false)}>
|
</p>
|
||||||
<h1 class="dark:text-white text-2xl">Are you sure?</h1>
|
<span class="flex items-center justify-start">
|
||||||
|
<PrimaryButton
|
||||||
|
variant={VARIANTS.LINK}
|
||||||
|
label="Replace Banner image"
|
||||||
|
className="-ml-6"
|
||||||
|
onClick={() => fileInput.click()}
|
||||||
|
/>
|
||||||
|
<PrimaryButton
|
||||||
|
variant={VARIANTS.CONTAINED_DANGER}
|
||||||
|
label="Delete"
|
||||||
|
onClick={deleteBannerImage}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Column>
|
||||||
|
|
||||||
<div class="mt-5 flex items-center justify-between">
|
<Column sm={8} md={8} lg={6}>
|
||||||
<PrimaryButton
|
<div class="w-fit relative">
|
||||||
className="px-6 py-3"
|
<input
|
||||||
variant={VARIANTS.OUTLINED}
|
type="file"
|
||||||
label="No, cancel"
|
accept=".jpg, .jpeg, .png"
|
||||||
type="submit"
|
style="display: none;"
|
||||||
|
bind:this={fileInput}
|
||||||
|
on:change={onFileSelected}
|
||||||
/>
|
/>
|
||||||
<PrimaryButton
|
<img
|
||||||
className="px-6 py-3"
|
style="width:250px; height:150px"
|
||||||
variant={VARIANTS.OUTLINED}
|
alt="About us"
|
||||||
label="Yes, delete"
|
src={$settings.image ? $settings.image : '/images/classroomio-course-img-template.jpg'}
|
||||||
onClick={handleDelete}
|
class="mt-2 md:mt-0 rounded-md w-full relative"
|
||||||
/>
|
/>
|
||||||
|
{#if uploadingImage}
|
||||||
|
<Loading withOverlay={true} small class="absolute top-0 w-[250px]" />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</Column>
|
||||||
{:else}
|
</Row>
|
||||||
<form on:submit|preventDefault={updateDescription}>
|
|
||||||
<UploadImage bind:avatar src={$course.logo} />
|
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||||
|
<Column sm={8} md={8} lg={8}>
|
||||||
|
<SectionTitle>Update course details</SectionTitle>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column sm={8} md={8} lg={8}>
|
||||||
<TextField
|
<TextField
|
||||||
bind:value={$course.title}
|
label="Course title"
|
||||||
placeholder="Course title"
|
placeholder="Write the course title here"
|
||||||
className="mb-4"
|
className="w-full mb-5"
|
||||||
isRequired={true}
|
bind:value={$settings.course_title}
|
||||||
errorMessage={errors.title}
|
errorMessage={errors.title}
|
||||||
/>
|
/>
|
||||||
<TextArea
|
<TextArea
|
||||||
label="Short description"
|
label="Course description"
|
||||||
bind:value={$course.description}
|
placeholder="Write your course description here"
|
||||||
rows="4"
|
className="w-full mb-5"
|
||||||
maxRows="4"
|
bind:value={$settings.course_description}
|
||||||
placeholder="A little description about this course"
|
|
||||||
className="mb-4"
|
|
||||||
isRequired={true}
|
|
||||||
errorMessage={errors.description}
|
errorMessage={errors.description}
|
||||||
/>
|
/>
|
||||||
|
</Column>
|
||||||
<div class="mt-5 flex items-center justify-between">
|
</Row>
|
||||||
<PrimaryButton
|
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||||
className="px-6 py-3"
|
<Column sm={8} md={8} lg={8}>
|
||||||
variant={VARIANTS.CONTAINED}
|
<SectionTitle>Grading</SectionTitle>
|
||||||
label="Delete"
|
<p>Make grading reports available for lessons</p>
|
||||||
onClick={() => (shouldDelete = true)}
|
</Column>
|
||||||
/>
|
<Column sm={8} md={8} lg={8}>
|
||||||
<PrimaryButton className="px-6 py-3" label="Update" type="submit" />
|
<Toggle size="sm" bind:toggled={$settings.grading}>
|
||||||
</div>
|
<span slot="labelA" style="color: gray">Disable</span>
|
||||||
</form>
|
<span slot="labelB" style="color: gray">Enable</span>
|
||||||
{/if}
|
</Toggle>
|
||||||
</Modal>
|
</Column>
|
||||||
|
</Row>
|
||||||
|
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||||
|
<Column sm={8} md={8} lg={8}>
|
||||||
|
<SectionTitle>Order Lessons Tab</SectionTitle>
|
||||||
|
<p>Drag and drop the labels to re-order your material tabs</p>
|
||||||
|
</Column>
|
||||||
|
<Column sm={8} md={8} lg={8}>
|
||||||
|
<DragAndDrop />
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||||
|
<Column sm={8} md={8} lg={8}>
|
||||||
|
<SectionTitle>Lesson Download</SectionTitle>
|
||||||
|
<p>Make the Lesson available for Download in pdf for registered students</p>
|
||||||
|
</Column>
|
||||||
|
<Column sm={8} md={8} lg={8}>
|
||||||
|
<Toggle size="sm" bind:toggled={$settings.lesson_download}>
|
||||||
|
<span slot="labelA" style="color: gray">Disable</span>
|
||||||
|
<span slot="labelB" style="color: gray">Enable</span>
|
||||||
|
</Toggle>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||||
|
<Column sm={8} md={8} lg={8}>
|
||||||
|
<SectionTitle>Course Download</SectionTitle>
|
||||||
|
<p>Make the Course available for Download for registered students</p>
|
||||||
|
</Column>
|
||||||
|
<Column sm={8} md={8} lg={8}>
|
||||||
|
<PrimaryButton
|
||||||
|
variant={VARIANTS.LINK}
|
||||||
|
label="Download Course"
|
||||||
|
onClick={downloadCourse}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
{isLoading}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||||
|
<Column sm={8} md={8} lg={8}>
|
||||||
|
<SectionTitle>Delete course</SectionTitle>
|
||||||
|
<p>Delete this course,this action cannot be undone</p>
|
||||||
|
</Column>
|
||||||
|
<Column sm={8} md={8} lg={8}>
|
||||||
|
<PrimaryButton
|
||||||
|
variant={VARIANTS.CONTAINED_DANGER}
|
||||||
|
label="Delete Course"
|
||||||
|
onClick={handleDeleteCourse}
|
||||||
|
isLoading={isDeleting}
|
||||||
|
isDisabled={isDeleting}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
<Row class="p-5 w-full flex items-center justify-end">
|
||||||
|
<PrimaryButton
|
||||||
|
label="Save Changes"
|
||||||
|
isLoading={isSaving}
|
||||||
|
isDisabled={isSaving}
|
||||||
|
onClick={handleSave}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Grid>
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export const settingsDialog = writable({
|
export const settings = writable({
|
||||||
open: false,
|
image: '',
|
||||||
|
course_title: '',
|
||||||
|
course_description: '',
|
||||||
|
grading: false,
|
||||||
|
tabs: [
|
||||||
|
{ id: 1, name: 'Note' },
|
||||||
|
{ id: 2, name: 'Slide' },
|
||||||
|
{ id: 3, name: 'Video' }
|
||||||
|
],
|
||||||
|
lesson_download: false
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,7 +32,14 @@ export const course: Writable<Course> = writable({
|
|||||||
coursesNo: 0,
|
coursesNo: 0,
|
||||||
description: '',
|
description: '',
|
||||||
imgUrl: ''
|
imgUrl: ''
|
||||||
}
|
},
|
||||||
|
lessonTabsOrder: [
|
||||||
|
{ id: 1, name: 'Note' },
|
||||||
|
{ id: 2, name: 'Slide' },
|
||||||
|
{ id: 3, name: 'Video' }
|
||||||
|
],
|
||||||
|
grading: false,
|
||||||
|
lessonDownload: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
import { Moon } from 'svelte-loading-spinners';
|
import { Moon } from 'svelte-loading-spinners';
|
||||||
import Navigation from '../Course/components/Navigation/index.svelte';
|
import Navigation from '../Course/components/Navigation/index.svelte';
|
||||||
import Backdrop from '$lib/components/Backdrop/index.svelte';
|
import Backdrop from '$lib/components/Backdrop/index.svelte';
|
||||||
@@ -54,8 +56,4 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
width: calc(100% - 360px);
|
width: calc(100% - 360px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rightBar.isMobile {
|
|
||||||
/* margin-left: 10px; */
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -146,8 +146,8 @@
|
|||||||
|
|
||||||
<aside
|
<aside
|
||||||
class={`${
|
class={`${
|
||||||
show ? '-translate-x-[100%] fixed z-[50]' : 'translate-x-0 fixed z-[50]'
|
show ? '-translate-x-[100%] fixed md:absolute z-[50]' : 'translate-x-0 fixed md:relative z-[50]'
|
||||||
} left-0 transition w-[350px] min-w-[300px] bg-gray-100 dark:bg-black h-full border border-l-0 border-t-0 border-b-0 border-r-1`}
|
}left-0 transition w-[350px] min-w-[300px] bg-gray-100 dark:bg-gray-800 h-full border border-l-0 border-t-0 border-b-0 border-r-1`}
|
||||||
>
|
>
|
||||||
<div class="toggler rounded-full shadow-lg absolute">
|
<div class="toggler rounded-full shadow-lg absolute">
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -146,10 +146,10 @@
|
|||||||
height: fit-content;
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) and (max-width: 1024px) {
|
@media screen and (max-width: 1024px) {
|
||||||
.price-container {
|
.price-container {
|
||||||
width: 405px;
|
width: 350px;
|
||||||
min-width: unset;
|
min-width: 250px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,17 +159,17 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<PricingSection className="md:hidden" {courseData} {editMode} bind:startCoursePayment />
|
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
<div class="bg-white dark:bg-black w-full">
|
<div class="bg-white dark:bg-black w-full">
|
||||||
<div
|
<div
|
||||||
class="py-8 lg:w-11/12 w-full m-2 lg:m-auto flex flex-col-reverse lg:flex-row items-center lg:items-start justify-between max-w-[1200px]"
|
class="py-8 lg:w-11/12 w-full m-2 lg:m-auto flex flex-col-reverse lg:flex-row items-center lg:items-start justify-between max-w-[1200px]"
|
||||||
>
|
>
|
||||||
<!-- Course Details -->
|
<!-- Course Details -->
|
||||||
<div class="course-content w-full p-3 lg:w-10/12 lg:mr-14">
|
<div class="course-content w-full p-3 lg:w-10/12 lg:mr-10">
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav
|
<nav
|
||||||
class="flex items-center border-b border-gray-300 py-3 sticky top-11 bg-white dark:bg-neutral-800"
|
class="flex items-center border-b border-gray-300 py-3 sticky top-0 {!editMode &&
|
||||||
|
'lg:top-11'} bg-white dark:bg-neutral-800"
|
||||||
>
|
>
|
||||||
{#each NAV_ITEMS as navItem}
|
{#each NAV_ITEMS as navItem}
|
||||||
<a
|
<a
|
||||||
@@ -416,7 +416,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pricing Details -->
|
<!-- Pricing Details -->
|
||||||
<PricingSection {courseData} />
|
<PricingSection {courseData} {editMode} bind:startCoursePayment />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -464,7 +464,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.course-content {
|
.course-content {
|
||||||
max-width: 800px;
|
max-width: 608px;
|
||||||
|
min-width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
@@ -495,4 +496,9 @@
|
|||||||
color: blue;
|
color: blue;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
@media screen and (max-width: 1023px) {
|
||||||
|
.course-content {
|
||||||
|
min-width: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
// export let onDelete = (id: string) => {};
|
// export let onDelete = (id: string) => {};
|
||||||
|
|
||||||
let formatter = getCurrencyFormatter(currency);
|
let formatter = getCurrencyFormatter(currency);
|
||||||
|
|
||||||
// function getOptions() {
|
// function getOptions() {
|
||||||
// return [
|
// return [
|
||||||
// {
|
// {
|
||||||
@@ -51,7 +50,7 @@
|
|||||||
<a
|
<a
|
||||||
rel="prefetch"
|
rel="prefetch"
|
||||||
href={isOnLandingPage ? `/course/${slug}` : `/courses/${id}`}
|
href={isOnLandingPage ? `/course/${slug}` : `/courses/${id}`}
|
||||||
class=" text-black w-fit"
|
class=" text-black w-full"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="border border-gray rounded w-full max-w-[350px] relative hover:shadow-lg transition-all ease-in-out"
|
class="border border-gray rounded w-full max-w-[350px] relative hover:shadow-lg transition-all ease-in-out"
|
||||||
@@ -59,12 +58,12 @@
|
|||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div class=" mb-5">
|
<div class=" mb-5">
|
||||||
<ImageLoader
|
<ImageLoader
|
||||||
src={bannerImage ?? '/images/classroomio-course-img-template.jpg'}
|
src={bannerImage ? bannerImage : '/images/classroomio-course-img-template.jpg'}
|
||||||
alt="Course Logo"
|
alt="Course Logo"
|
||||||
class="h-2/5 w-full rounded dark:border dark:border-white"
|
class="h-[200px] w-full rounded dark:border dark:border-white"
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="loading">
|
<svelte:fragment slot="loading">
|
||||||
<SkeletonPlaceholder style="width: 100%; height: 10rem;" />
|
<SkeletonPlaceholder style="width: 100%; height: 200px;" />
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="error">An error occurred.</svelte:fragment>
|
<svelte:fragment slot="error">An error occurred.</svelte:fragment>
|
||||||
</ImageLoader>
|
</ImageLoader>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import CoursesEmptyIcon from '../Icons/CoursesEmptyIcon.svelte';
|
import CoursesEmptyIcon from '../Icons/CoursesEmptyIcon.svelte';
|
||||||
import CopyCourseModal from './components/CopyCourseModal/index.svelte';
|
import CopyCourseModal from './components/CopyCourseModal/index.svelte';
|
||||||
import { courses, courseMetaDeta } from './store';
|
import { courses, courseMetaDeta } from './store';
|
||||||
import { isMobile } from '$lib/utils/store/useMobile';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CopyCourseModal />
|
<CopyCourseModal />
|
||||||
@@ -21,7 +20,7 @@
|
|||||||
{#each $courses as courseData}
|
{#each $courses as courseData}
|
||||||
<Card
|
<Card
|
||||||
id={courseData.id}
|
id={courseData.id}
|
||||||
bannerImage={courseData.banner_image}
|
bannerImage={courseData.logo}
|
||||||
title={courseData.title}
|
title={courseData.title}
|
||||||
description={courseData.description}
|
description={courseData.description}
|
||||||
role_id={courseData.role_id}
|
role_id={courseData.role_id}
|
||||||
@@ -30,14 +29,6 @@
|
|||||||
currency={courseData.currency}
|
currency={courseData.currency}
|
||||||
totalLessons={courseData.total_lessons}
|
totalLessons={courseData.total_lessons}
|
||||||
/>
|
/>
|
||||||
<!-- {:else}
|
|
||||||
<Box className="w-full">
|
|
||||||
<CoursesEmptyIcon />
|
|
||||||
<h3 class="dark:text-white text-2xl my-5">No Courses Created</h3>
|
|
||||||
<p class="dark:text-white w-1/3 text-center">
|
|
||||||
Share your knowledge with the world by creating engaging courses for your students.
|
|
||||||
</p>
|
|
||||||
</Box> -->
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script>
|
<script>
|
||||||
import { page } from '$app/stores';
|
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import NotificationIcon from 'carbon-icons-svelte/lib/Notification.svelte';
|
import NotificationIcon from 'carbon-icons-svelte/lib/Notification.svelte';
|
||||||
import Moon from 'carbon-icons-svelte/lib/Moon.svelte';
|
import Moon from 'carbon-icons-svelte/lib/Moon.svelte';
|
||||||
|
|||||||
@@ -5,9 +5,10 @@
|
|||||||
export let padding = 'pb-5 px-4';
|
export let padding = 'pb-5 px-4';
|
||||||
export let className = '';
|
export let className = '';
|
||||||
export let headerClassName = '';
|
export let headerClassName = '';
|
||||||
|
export let onClick = () => {};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root mt-4 mx-auto {width} {className} relative {padding}">
|
<div class="root mt-4 mx-auto {width} {className} relative {padding}" on:click={onClick}>
|
||||||
{#if $$slots.header}
|
{#if $$slots.header}
|
||||||
<div
|
<div
|
||||||
class="head dark:bg-black flex items-center justify-between sticky right-0 w-full px-3 {headerClassName}"
|
class="head dark:bg-black flex items-center justify-between sticky right-0 w-full px-3 {headerClassName}"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||||
|
|
||||||
export let tabs = [];
|
export let tabs = [];
|
||||||
export let currentTab;
|
export let currentTab;
|
||||||
export let onChange;
|
export let onChange;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function isObject(obj) {
|
||||||
|
return obj === Object(obj);
|
||||||
|
}
|
||||||
@@ -21,6 +21,9 @@ interface CourseMetadata {
|
|||||||
imgUrl: string;
|
imgUrl: string;
|
||||||
};
|
};
|
||||||
reviews?: Array<Review>;
|
reviews?: Array<Review>;
|
||||||
|
lessonTabsOrder: Array<Tabs>;
|
||||||
|
grading: boolean;
|
||||||
|
lessonDownload: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generated from https://supabase-schema.vercel.app/
|
// Generated from https://supabase-schema.vercel.app/
|
||||||
@@ -255,3 +258,8 @@ export interface Review {
|
|||||||
created_at: number;
|
created_at: number;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Tabs {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
interface Data {
|
interface Data {
|
||||||
course: Course;
|
course: Course;
|
||||||
}
|
}
|
||||||
|
|
||||||
export let data: Data;
|
export let data: Data;
|
||||||
|
|
||||||
let courseData: Course;
|
let courseData: Course;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import CourseLandingPage from '$lib/components/CourseLandingPage/index.svelte';
|
import CourseLandingPage from '$lib/components/CourseLandingPage/index.svelte';
|
||||||
import Editor from '$lib/components/CourseLandingPage/components/Editor/index.svelte';
|
import Editor from '$lib/components/CourseLandingPage/components/Editor/index.svelte';
|
||||||
import { isMobile } from '$lib/utils/store/useMobile';
|
|
||||||
import { profile } from '$lib/utils/store/user';
|
import { profile } from '$lib/utils/store/user';
|
||||||
import { setCourse, course } from '$lib/components/Course/store';
|
import { setCourse, course } from '$lib/components/Course/store';
|
||||||
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
|
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||||
@@ -30,7 +29,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 z-50 bg-white"
|
class="absolute flex inset-0 z-50 bg-white"
|
||||||
in:fly={{ y: 500, duration: 500 }}
|
in:fly={{ y: 500, duration: 500 }}
|
||||||
out:fly={{ y: 500, duration: 500 }}
|
out:fly={{ y: 500, duration: 500 }}
|
||||||
>
|
>
|
||||||
@@ -45,6 +44,7 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
import { currentOrg } from '$lib/utils/store/org';
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
import { snackbarStore } from '$lib/components/Snackbar/store';
|
import { snackbarStore } from '$lib/components/Snackbar/store';
|
||||||
import { SNACKBAR_SEVERITY } from '$lib/components/Snackbar/constants';
|
import { SNACKBAR_SEVERITY } from '$lib/components/Snackbar/constants';
|
||||||
|
import { FaceSatisfied } from 'carbon-icons-svelte';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@
|
|||||||
if (!$course.id) {
|
if (!$course.id) {
|
||||||
const { data: _data } = await fetchCourse(data.courseId);
|
const { data: _data } = await fetchCourse(data.courseId);
|
||||||
|
|
||||||
lessonData = _data.lessons.find((lesson = { id: '' }) => lesson.id === lessonId);
|
lessonData = _data?.lessons.find((lesson = { id: '' }) => lesson.id === lessonId);
|
||||||
setCourse(_data);
|
setCourse(_data);
|
||||||
} else if (prevLessonId !== lessonId) {
|
} else if (prevLessonId !== lessonId) {
|
||||||
const lesson = await fetchLesson(lessonId);
|
const lesson = await fetchLesson(lessonId);
|
||||||
@@ -163,7 +164,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CourseContainer {isFetching} {path} isExercisePage={!data.isMaterialsTabActive && data.exerciseId}>
|
<CourseContainer {path} isExercisePage={!data.isMaterialsTabActive && data.exerciseId}>
|
||||||
<PageNav
|
<PageNav
|
||||||
navItems={[
|
navItems={[
|
||||||
{
|
{
|
||||||
@@ -182,7 +183,7 @@
|
|||||||
<svelte:fragment slot="widget">
|
<svelte:fragment slot="widget">
|
||||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||||
{#if data.isMaterialsTabActive}
|
{#if data.isMaterialsTabActive}
|
||||||
<div class="lg:hidden">
|
<div class="tab">
|
||||||
<IconButton onClick={toggleApps} buttonClassName="">
|
<IconButton onClick={toggleApps} buttonClassName="">
|
||||||
<OverflowMenuVertical size={24} />
|
<OverflowMenuVertical size={24} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -190,7 +191,7 @@
|
|||||||
<div
|
<div
|
||||||
class={`flex-row ${
|
class={`flex-row ${
|
||||||
$apps.dropdown && $apps.open
|
$apps.dropdown && $apps.open
|
||||||
? 'absolute lg:relative top-[85px] lg:top-0 right-14 lg:right-0 z-40 dark:bg-black p-3 lg:p-0'
|
? 'absolute lg:relative top-[85px] lg:top-0 right-14 lg:right-0 z-40 dark:bg-slate-800 p-3 lg:p-0'
|
||||||
: 'hidden'
|
: 'hidden'
|
||||||
} lg:flex items-center`}
|
} lg:flex items-center`}
|
||||||
>
|
>
|
||||||
@@ -221,7 +222,13 @@
|
|||||||
{#if !data.isMaterialsTabActive}
|
{#if !data.isMaterialsTabActive}
|
||||||
<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
|
||||||
|
onClick={() => {
|
||||||
|
$apps.open = false;
|
||||||
|
$apps.dropdown = false;
|
||||||
|
}}
|
||||||
|
width="lg:w-full xl:w-11/12"
|
||||||
|
>
|
||||||
<Materials lessonId={data.lessonId} {mode} {prevMode} {toggleMode} bind:isSaving />
|
<Materials lessonId={data.lessonId} {mode} {prevMode} {toggleMode} bind:isSaving />
|
||||||
|
|
||||||
<div class="w-full flex flex-row-reverse mt-10">
|
<div class="w-full flex flex-row-reverse mt-10">
|
||||||
@@ -239,3 +246,11 @@
|
|||||||
</PageBody>
|
</PageBody>
|
||||||
{/if}
|
{/if}
|
||||||
</CourseContainer>
|
</CourseContainer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media screen and (min-width: 1023px) {
|
||||||
|
.tab {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const load = ({ params = { id: '' } }) => {
|
||||||
|
return {
|
||||||
|
courseId: params.id
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script>
|
||||||
|
import { course, setCourse } from '$lib/components/Course/store';
|
||||||
|
import { fetchCourse } from '$lib/utils/services/courses';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
|
||||||
|
import PageNav from '$lib/components/PageNav/index.svelte';
|
||||||
|
import PageBody from '$lib/components/PageBody/index.svelte';
|
||||||
|
import Settings from '$lib/components/Course/components/Settings/index.svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
const { courseId } = data;
|
||||||
|
let isStudent = false;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if ($course.id) return;
|
||||||
|
const { data } = await fetchCourse(courseId);
|
||||||
|
setCourse(data);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CourseContainer bind:isStudent>
|
||||||
|
<PageNav title="Settings" />
|
||||||
|
<PageBody>
|
||||||
|
<Settings />
|
||||||
|
</PageBody>
|
||||||
|
</CourseContainer>
|
||||||
@@ -20,11 +20,8 @@
|
|||||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
const { orgName } = data;
|
|
||||||
|
|
||||||
function createCourse() {
|
const { orgName } = data;
|
||||||
goto(`/org/${orgName}/courses?create=true`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const theme = {
|
const theme = {
|
||||||
calendar: {
|
calendar: {
|
||||||
@@ -93,6 +90,10 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function createCourse() {
|
||||||
|
goto(`/org/${orgName}/courses?create=true`);
|
||||||
|
}
|
||||||
|
|
||||||
const addDotsToCalendar = (currentMonthIndexInRenderedMonth: number) =>
|
const addDotsToCalendar = (currentMonthIndexInRenderedMonth: number) =>
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!calendar) calendar = document.getElementById('calendar');
|
if (!calendar) calendar = document.getElementById('calendar');
|
||||||
|
|||||||
Reference in New Issue
Block a user