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 <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>


Merge-request: CLASSROOMIO-MR-76
Merged-by: Best Admin <[email protected]>
This commit is contained in:
emmanuel
2023-09-24 13:20:04 +00:00
committed by Space Cloud
co-authored by Rotimi Best
parent e44353ac0a
commit 103a2eda7b
29 changed files with 675 additions and 217 deletions
+31 -3
View File
@@ -14,7 +14,6 @@
import Notes from './components/Notes/index.svelte';
import Poll from './components/Poll/index.svelte';
import APPS_CONSTANTS from './constants';
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
let selectedApp;
@@ -28,8 +27,8 @@
} else {
selectedApp = appName;
}
$apps.dropdown = false;
}
onMount(() => {
hotkeys('A+1,A+2,A+3,A+4', function (event, handler) {
event.preventDefault();
@@ -146,7 +145,7 @@
}
}
@media screen and (max-width: 1024px) {
@media screen and (max-width: 1023px) {
.root {
position: absolute;
height: calc(100vh - 109px);
@@ -166,4 +165,33 @@
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>
@@ -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}
@@ -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}
@@ -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';
export const tabs = [
export let tabs = [
{
label: 'Note',
icon: Notebook,
@@ -20,3 +20,20 @@ export const tabs = [
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 TextEditor from '$lib/components/TextEditor/index.svelte';
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 prevMode = '';
@@ -47,6 +51,7 @@
let aiButtonRef = {};
let openPopover = false;
let player = null;
let componentsToRender = getComponentOrder(tabs);
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';
@@ -55,6 +60,10 @@
() =>
(currentTab = tab);
const getValue = (label) => {
const tabValue = tabs.find((tab) => tab.label === label)?.value;
return tabValue;
};
async function saveLesson(materials = undefined) {
const _lesson = !!materials
? {
@@ -174,12 +183,33 @@
function onLessonIdChange(_lid: string) {
initAutoSave = false;
isSaving = false;
tabs = orderedTabs(tabs, $course.metadata?.lessonTabsOrder);
currentTab = tabs[0].value;
componentsToRender = getComponentOrder(tabs);
}
const onClose = () => {
$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);
$: onLessonIdChange(lessonId);
@@ -211,7 +241,7 @@
{#if mode === MODES.edit}
<Tabs {tabs} {currentTab} {onChange}>
<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">
<PrimaryButton
className="flex items-center relative"
@@ -261,7 +291,7 @@
</div>
</TabContent>
<TabContent value={tabs[1].value} index={currentTab}>
<TabContent value={getValue('Slide')} index={currentTab}>
{#if mode === MODES.edit}
<TextField
label="Slide link"
@@ -270,8 +300,7 @@
/>
{/if}
</TabContent>
<TabContent value={tabs[2].value} index={currentTab}>
<TabContent value={getValue('Video')} index={currentTab}>
<PrimaryButton label="Add/Edit Video(s)" onClick={openAddVideoModal} className="mb-2" />
{#if $lesson.materials.videos.length}
<div class="flex flex-col items-start w-full h-full">
@@ -328,64 +357,11 @@
</slot:fragment>
</Tabs>
{:else if !isMaterialsEmpty($lesson.materials)}
{#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}
{#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}
<div class="w-full">
{#each componentsToRender as Component, index}
<svelte:component this={Component} key={index} />
{/each}
</div>
{:else}
<Box className="text-center">
<img src="/no-video.svg" alt="Video not found" />
@@ -399,16 +375,3 @@
<PrimaryButton label="Get started" className="rounded-md" onClick={toggleMode} />
</Box>
{/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 Badge from 'carbon-icons-svelte/lib/Badge.svelte';
import IbmWatsonKnowledgeCatalog from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeCatalog.svelte';
import Settings from 'carbon-icons-svelte/lib/Settings.svelte';
export let name = '';
</script>
@@ -27,4 +28,6 @@
<ApplicationWeb size={20} class="carbon-icon mr-2" />
{:else if name === 'Certificates'}
<Badge size={20} class="carbon-icon mr-2" />
{:else if name === 'Settings'}
<Settings size={20} class="carbon-icon mr-2" />
{/if}
@@ -5,20 +5,10 @@
import hotkeys from 'hotkeys-js';
import { browser } from '$app/environment';
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 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 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 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 { course } from '$lib/components/Course/store';
import { updateCourse } from '$lib/utils/services/courses';
@@ -26,7 +16,6 @@
import { isMobile } from '$lib/utils/store/useMobile';
import { menu } from '$lib/components/Org/store';
// export let lessonId;
export let path;
export let isStudent = null;
@@ -142,19 +131,25 @@
show() {
return !isStudent;
}
},
{
label: 'Settings',
to: getNavItemRoute($course.id, 'settings'),
hideSortIcon: true,
show() {
return !isStudent;
}
}
];
}
</script>
<Settings />
<aside
class={`${
$menu.hidden
? '-translate-x-[100%] absolute md:translate-x-0 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">
<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 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 TextArea from '$lib/components/Form/TextArea.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 { settingsDialog } from './store';
import { settings } from './store';
import { course } from '$lib/components/Course/store';
import { updateCourse, deleteCourse } from '$lib/utils/services/courses';
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 avatar;
let uploadingImage = false;
let isSaving = false;
let fileInput;
let isLoading = false;
let isDeleting = false;
let errors = {};
let avatar;
let imagebuffer;
async function updateDescription() {
if (!$course.title) {
errors.title = 'Title cannot be empty';
return;
// FILE UPLOAD
const uploadImage = async (image: File) => {
if (image) {
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;
const logo = await updateCourse($course.id, avatar, {
title: $course.title,
description: $course.description
});
if (logo) {
$course.logo = logo;
function getLessonOrder(id) {
const index = $lessons.findIndex((lesson) => lesson.id === id);
if (index < 9) {
return '0' + (index + 1);
} else {
return index + 1;
}
avatar = undefined;
}
async function handleDelete() {
$settingsDialog.open = false;
await deleteCourse($course.id);
goto($currentOrgPath + '/courses');
const downloadCourse = async () => {
isLoading = true;
try {
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) {
// avatar = logo;
// }
const handleSave = async () => {
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>
<Modal
onClose={() => ($settingsDialog.open = false)}
bind:open={$settingsDialog.open}
width="w-96"
modalHeading="Update Course"
>
{#if shouldDelete}
<form on:submit|preventDefault={() => (shouldDelete = false)}>
<h1 class="dark:text-white text-2xl">Are you sure?</h1>
<Grid class="border-c rounded border-gray-200">
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={8} md={8} lg={8}>
<SectionTitle>Banner Image</SectionTitle>
<p>
This optional image will show up on your welcome page. If you include one, it should be at
least 300 x 200"
</p>
<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">
<PrimaryButton
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="No, cancel"
type="submit"
<Column sm={8} md={8} lg={6}>
<div class="w-fit relative">
<input
type="file"
accept=".jpg, .jpeg, .png"
style="display: none;"
bind:this={fileInput}
on:change={onFileSelected}
/>
<PrimaryButton
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="Yes, delete"
onClick={handleDelete}
<img
style="width:250px; height:150px"
alt="About us"
src={$settings.image ? $settings.image : '/images/classroomio-course-img-template.jpg'}
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>
</form>
{:else}
<form on:submit|preventDefault={updateDescription}>
<UploadImage bind:avatar src={$course.logo} />
</Column>
</Row>
<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
bind:value={$course.title}
placeholder="Course title"
className="mb-4"
isRequired={true}
label="Course title"
placeholder="Write the course title here"
className="w-full mb-5"
bind:value={$settings.course_title}
errorMessage={errors.title}
/>
<TextArea
label="Short description"
bind:value={$course.description}
rows="4"
maxRows="4"
placeholder="A little description about this course"
className="mb-4"
isRequired={true}
label="Course description"
placeholder="Write your course description here"
className="w-full mb-5"
bind:value={$settings.course_description}
errorMessage={errors.description}
/>
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-3"
variant={VARIANTS.CONTAINED}
label="Delete"
onClick={() => (shouldDelete = true)}
/>
<PrimaryButton className="px-6 py-3" label="Update" type="submit" />
</div>
</form>
{/if}
</Modal>
</Column>
</Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={8} md={8} lg={8}>
<SectionTitle>Grading</SectionTitle>
<p>Make grading reports available for lessons</p>
</Column>
<Column sm={8} md={8} lg={8}>
<Toggle size="sm" bind:toggled={$settings.grading}>
<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>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';
export const settingsDialog = writable({
open: false,
export const settings = writable({
image: '',
course_title: '',
course_description: '',
grading: false,
tabs: [
{ id: 1, name: 'Note' },
{ id: 2, name: 'Slide' },
{ id: 3, name: 'Video' }
],
lesson_download: false
});
+8 -1
View File
@@ -32,7 +32,14 @@ export const course: Writable<Course> = writable({
coursesNo: 0,
description: '',
imgUrl: ''
}
},
lessonTabsOrder: [
{ id: 1, name: 'Note' },
{ id: 2, name: 'Slide' },
{ id: 3, name: 'Video' }
],
grading: false,
lessonDownload: true
}
});
@@ -1,4 +1,6 @@
<script>
// @ts-nocheck
import { Moon } from 'svelte-loading-spinners';
import Navigation from '../Course/components/Navigation/index.svelte';
import Backdrop from '$lib/components/Backdrop/index.svelte';
@@ -54,8 +56,4 @@
flex-grow: 1;
width: calc(100% - 360px);
}
.rightBar.isMobile {
/* margin-left: 10px; */
}
</style>
@@ -146,8 +146,8 @@
<aside
class={`${
show ? '-translate-x-[100%] fixed z-[50]' : 'translate-x-0 fixed 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`}
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-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">
<IconButton
@@ -146,10 +146,10 @@
height: fit-content;
}
@media screen and (min-width: 768px) and (max-width: 1024px) {
@media screen and (max-width: 1024px) {
.price-container {
width: 405px;
min-width: unset;
width: 350px;
min-width: 250px;
}
}
@@ -159,17 +159,17 @@
{/if}
</div>
</header>
<PricingSection className="md:hidden" {courseData} {editMode} bind:startCoursePayment />
<!-- Body -->
<div class="bg-white dark:bg-black w-full">
<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]"
>
<!-- 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 -->
<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}
<a
@@ -416,7 +416,7 @@
</div>
<!-- Pricing Details -->
<PricingSection {courseData} />
<PricingSection {courseData} {editMode} bind:startCoursePayment />
</div>
</div>
</div>
@@ -464,7 +464,8 @@
}
.course-content {
max-width: 800px;
max-width: 608px;
min-width: 60%;
}
nav {
@@ -495,4 +496,9 @@
color: blue;
text-decoration: underline;
}
@media screen and (max-width: 1023px) {
.course-content {
min-width: 80%;
}
}
</style>
@@ -17,7 +17,6 @@
// export let onDelete = (id: string) => {};
let formatter = getCurrencyFormatter(currency);
// function getOptions() {
// return [
// {
@@ -51,7 +50,7 @@
<a
rel="prefetch"
href={isOnLandingPage ? `/course/${slug}` : `/courses/${id}`}
class=" text-black w-fit"
class=" text-black w-full"
>
<div
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=" mb-5">
<ImageLoader
src={bannerImage ?? '/images/classroomio-course-img-template.jpg'}
src={bannerImage ? bannerImage : '/images/classroomio-course-img-template.jpg'}
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">
<SkeletonPlaceholder style="width: 100%; height: 10rem;" />
<SkeletonPlaceholder style="width: 100%; height: 200px;" />
</svelte:fragment>
<svelte:fragment slot="error">An error occurred.</svelte:fragment>
</ImageLoader>
+1 -10
View File
@@ -5,7 +5,6 @@
import CoursesEmptyIcon from '../Icons/CoursesEmptyIcon.svelte';
import CopyCourseModal from './components/CopyCourseModal/index.svelte';
import { courses, courseMetaDeta } from './store';
import { isMobile } from '$lib/utils/store/useMobile';
</script>
<CopyCourseModal />
@@ -21,7 +20,7 @@
{#each $courses as courseData}
<Card
id={courseData.id}
bannerImage={courseData.banner_image}
bannerImage={courseData.logo}
title={courseData.title}
description={courseData.description}
role_id={courseData.role_id}
@@ -30,14 +29,6 @@
currency={courseData.currency}
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}
{/if}
</div>
-1
View File
@@ -1,5 +1,4 @@
<script>
import { page } from '$app/stores';
import { browser } from '$app/environment';
import NotificationIcon from 'carbon-icons-svelte/lib/Notification.svelte';
import Moon from 'carbon-icons-svelte/lib/Moon.svelte';
+2 -1
View File
@@ -5,9 +5,10 @@
export let padding = 'pb-5 px-4';
export let className = '';
export let headerClassName = '';
export let onClick = () => {};
</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}
<div
class="head dark:bg-black flex items-center justify-between sticky right-0 w-full px-3 {headerClassName}"
+1
View File
@@ -1,5 +1,6 @@
<script>
import TextChip from '$lib/components/Chip/Text.svelte';
export let tabs = [];
export let currentTab;
export let onChange;
+3
View File
@@ -0,0 +1,3 @@
export function isObject(obj) {
return obj === Object(obj);
}
+8
View File
@@ -21,6 +21,9 @@ interface CourseMetadata {
imgUrl: string;
};
reviews?: Array<Review>;
lessonTabsOrder: Array<Tabs>;
grading: boolean;
lessonDownload: boolean;
}
// Generated from https://supabase-schema.vercel.app/
@@ -255,3 +258,8 @@ export interface Review {
created_at: number;
description: string;
}
interface Tabs {
id: number;
name: string;
}
+1
View File
@@ -5,6 +5,7 @@
interface Data {
course: Course;
}
export let data: Data;
let courseData: Course;
@@ -2,7 +2,6 @@
import { fly } from 'svelte/transition';
import CourseLandingPage from '$lib/components/CourseLandingPage/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 { setCourse, course } from '$lib/components/Course/store';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
@@ -30,7 +29,7 @@
</script>
<div
class="absolute inset-0 z-50 bg-white"
class="absolute flex inset-0 z-50 bg-white"
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
@@ -45,6 +44,7 @@
flex-grow: 1;
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
</style>
@@ -26,6 +26,7 @@
import { currentOrg } from '$lib/utils/store/org';
import { snackbarStore } from '$lib/components/Snackbar/store';
import { SNACKBAR_SEVERITY } from '$lib/components/Snackbar/constants';
import { FaceSatisfied } from 'carbon-icons-svelte';
export let data;
@@ -55,7 +56,7 @@
if (!$course.id) {
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);
} else if (prevLessonId !== lessonId) {
const lesson = await fetchLesson(lessonId);
@@ -163,7 +164,7 @@
}
</script>
<CourseContainer {isFetching} {path} isExercisePage={!data.isMaterialsTabActive && data.exerciseId}>
<CourseContainer {path} isExercisePage={!data.isMaterialsTabActive && data.exerciseId}>
<PageNav
navItems={[
{
@@ -182,7 +183,7 @@
<svelte:fragment slot="widget">
<RoleBasedSecurity allowedRoles={[1, 2]}>
{#if data.isMaterialsTabActive}
<div class="lg:hidden">
<div class="tab">
<IconButton onClick={toggleApps} buttonClassName="">
<OverflowMenuVertical size={24} />
</IconButton>
@@ -190,7 +191,7 @@
<div
class={`flex-row ${
$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'
} lg:flex items-center`}
>
@@ -221,7 +222,13 @@
{#if !data.isMaterialsTabActive}
<Exercises lessonId={data.lessonId} exerciseId={data.exerciseId} path={`${path}/exercises`} />
{: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 />
<div class="w-full flex flex-row-reverse mt-10">
@@ -239,3 +246,11 @@
</PageBody>
{/if}
</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>
+5 -4
View File
@@ -20,11 +20,8 @@
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
export let data;
const { orgName } = data;
function createCourse() {
goto(`/org/${orgName}/courses?create=true`);
}
const { orgName } = data;
const theme = {
calendar: {
@@ -93,6 +90,10 @@
}
];
function createCourse() {
goto(`/org/${orgName}/courses?create=true`);
}
const addDotsToCalendar = (currentMonthIndexInRenderedMonth: number) =>
setTimeout(() => {
if (!calendar) calendar = document.getElementById('calendar');