CLASSROOMIO-T-108: Revamp landing page & sidebar improvement

This commit is contained in:
Rotimi Best
2023-09-30 09:56:42 +01:00
parent 8777ab57f9
commit 1fa1edecb9
16 changed files with 368 additions and 550 deletions
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { lesson } from '$lib/components/Course/components/Lesson/store/lessons'; import { lesson } from '$lib/components/Course/components/Lesson/store/lessons';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
function isNoteEmpty(note) { function isNoteEmpty(note: string) {
if (!note || typeof note !== 'string') return true; if (!note || typeof note !== 'string') return true;
if (!document) return false; if (!document) return false;
@@ -16,7 +17,11 @@
</script> </script>
{#if !isNoteEmpty($lesson.materials?.note)} {#if !isNoteEmpty($lesson.materials?.note)}
<div> <HtmlRender>
{@html $lesson.materials.note} <svelte:fragment slot="content">
</div> <div>
{@html $lesson.materials.note}
</div>
</svelte:fragment>
</HtmlRender>
{/if} {/if}
@@ -10,7 +10,6 @@
import AlignBoxTopLeftIcon from 'carbon-icons-svelte/lib/AlignBoxTopLeft.svelte'; import AlignBoxTopLeftIcon from 'carbon-icons-svelte/lib/AlignBoxTopLeft.svelte';
import ListIcon from 'carbon-icons-svelte/lib/List.svelte'; import ListIcon from 'carbon-icons-svelte/lib/List.svelte';
import IbmWatsonKnowledgeStudioIcon from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeStudio.svelte'; import IbmWatsonKnowledgeStudioIcon from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeStudio.svelte';
import MachineLearningModel from 'carbon-icons-svelte/lib/MachineLearningModel.svelte';
import MagicWandFilled from 'carbon-icons-svelte/lib/MagicWandFilled.svelte'; import MagicWandFilled from 'carbon-icons-svelte/lib/MagicWandFilled.svelte';
import Tabs from '$lib/components/Tabs/index.svelte'; import Tabs from '$lib/components/Tabs/index.svelte';
import TabContent from '$lib/components/TabContent/index.svelte'; import TabContent from '$lib/components/TabContent/index.svelte';
@@ -126,7 +125,7 @@
api: '/api/completion' api: '/api/completion'
}); });
function updateNoteByCompletion(completion) { function updateNoteByCompletion(completion: string) {
$lesson.materials.note = completion; $lesson.materials.note = completion;
autoSave($lesson.materials); autoSave($lesson.materials);
@@ -240,143 +239,143 @@
<h1 class="text-4xl mt-0 capitalize"> <h1 class="text-4xl mt-0 capitalize">
{lessonTitle} {lessonTitle}
</h1> </h1>
{#if mode === MODES.edit}
<Tabs {tabs} {currentTab} {onChange}>
<slot:fragment slot="content">
<TabContent value={getValue('Note')} index={currentTab}>
<div bind:this={aiButtonRef} class="w-full flex flex-row-reverse">
<PrimaryButton
className="flex items-center relative"
onClick={() => {
openPopover = !openPopover;
}}
isLoading={$isLoading}
isDisabled={$isLoading}
variant={VARIANTS.OUTLINED}
>
<MagicWandFilled size={20} class="carbon-icon mr-3" />
AI
<Popover
caret
align="left"
bind:open={openPopover}
on:click:outside={({ detail }) => {
openPopover = aiButtonRef?.contains(detail.target);
}}
>
<div class="p-2">
<button class={aiButtonClass} on:click={() => callAI('outline')}>
<ListIcon class="carbon-icon mr-2" />
Generate Lesson Outline
</button>
<button class={aiButtonClass} on:click={() => callAI('note')}>
<AlignBoxTopLeftIcon class="carbon-icon mr-2" />
Generate Lesson Note
</button>
<button class={aiButtonClass} on:click={() => callAI('activities')}>
<IbmWatsonKnowledgeStudioIcon class="carbon-icon mr-2" />
Generate Lesson Activities
</button>
</div>
</Popover>
</PrimaryButton>
</div>
<div class="h-[60vh] mt-5">
<TextEditor
id={lessonId}
bind:editorWindowRef
value={$lesson.materials.note}
onChange={(html) => ($lesson.materials.note = html)}
placeholder="Write your lesson note here"
/>
</div>
</TabContent>
<TabContent value={getValue('Slide')} index={currentTab}>
{#if mode === MODES.edit}
<TextField
label="Slide link"
bind:value={$lesson.materials.slide_url}
onInputChange={handleInputChange}
/>
{/if}
</TabContent>
<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">
{#each $lesson.materials.videos as video, index}
{#if mode === MODES.edit}
<div class="ml-auto">
<IconButton
value="delete-video"
contained={true}
onClick={() => deleteLessonVideo(index)}
>
<TrashCanIcon size={20} class="carbon-icon dark:text-white" />
</IconButton>
</div>
{/if}
<div class="w-full h-full flex flex-col gap-2 overflow-hidden">
{#key video.link}
<div class="mb-5">
{#if video.type === 'youtube'}
<iframe
width="100%"
height="569"
class="iframe"
src={formatYoutubeVideo(video.link, errors)}
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
/>
{:else if video.metadata?.svid}
<div style="position:relative;padding-bottom:51.416579%">
<iframe
src="https://muse.ai/embed/{video.metadata
?.svid}?logo=https://app.classroomio.com/logo-512.png&subtitles=auto&cover_play_position=center"
style="width:100%;height:100%;position:absolute;left:0;top:0"
frameborder="0"
allowfullscreen
title="Muse AI Video Embed"
/>
</div>
{:else}
<video bind:this={player} class="plyr-video-trigger" playsinline controls>
<source src={video.link} type="video/mp4" />
<track kind="captions" />
</video>
{/if}
</div>
{/key}
</div>
{/each}
</div>
{/if}
</TabContent>
</slot:fragment>
</Tabs>
{:else if !isMaterialsEmpty($lesson.materials)}
<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" />
<h3 class="text-xl font-normal dark:text-white py-2">
No note, video or slide added for this lesson yet
</h3>
<p class="text-sm text-center font-normal py-2">
Share your knowledge with your students by creating engaging content<br />
Start by clicking on <strong>Get started</strong> button.
</p>
<PrimaryButton label="Get started" className="rounded-md" onClick={toggleMode} />
</Box>
{/if}
</svelte:fragment> </svelte:fragment>
</HtmlRender> </HtmlRender>
{#if mode === MODES.edit}
<Tabs {tabs} {currentTab} {onChange}>
<slot:fragment slot="content">
<TabContent value={getValue('Note')} index={currentTab}>
<div bind:this={aiButtonRef} class="w-full flex flex-row-reverse">
<PrimaryButton
className="flex items-center relative"
onClick={() => {
openPopover = !openPopover;
}}
isLoading={$isLoading}
isDisabled={$isLoading}
variant={VARIANTS.OUTLINED}
>
<MagicWandFilled size={20} class="carbon-icon mr-3" />
AI
<Popover
caret
align="left"
bind:open={openPopover}
on:click:outside={({ detail }) => {
openPopover = aiButtonRef?.contains(detail.target);
}}
>
<div class="p-2">
<button class={aiButtonClass} on:click={() => callAI('outline')}>
<ListIcon class="carbon-icon mr-2" />
Generate Lesson Outline
</button>
<button class={aiButtonClass} on:click={() => callAI('note')}>
<AlignBoxTopLeftIcon class="carbon-icon mr-2" />
Generate Lesson Note
</button>
<button class={aiButtonClass} on:click={() => callAI('activities')}>
<IbmWatsonKnowledgeStudioIcon class="carbon-icon mr-2" />
Generate Lesson Activities
</button>
</div>
</Popover>
</PrimaryButton>
</div>
<div class="h-[60vh] mt-5">
<TextEditor
id={lessonId}
bind:editorWindowRef
value={$lesson.materials.note}
onChange={(html) => ($lesson.materials.note = html)}
placeholder="Write your lesson note here"
/>
</div>
</TabContent>
<TabContent value={getValue('Slide')} index={currentTab}>
{#if mode === MODES.edit}
<TextField
label="Slide link"
bind:value={$lesson.materials.slide_url}
onInputChange={handleInputChange}
/>
{/if}
</TabContent>
<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">
{#each $lesson.materials.videos as video, index}
{#if mode === MODES.edit}
<div class="ml-auto">
<IconButton
value="delete-video"
contained={true}
onClick={() => deleteLessonVideo(index)}
>
<TrashCanIcon size={20} class="carbon-icon dark:text-white" />
</IconButton>
</div>
{/if}
<div class="w-full h-full flex flex-col gap-2 overflow-hidden">
{#key video.link}
<div class="mb-5">
{#if video.type === 'youtube'}
<iframe
width="100%"
height="569"
class="iframe"
src={formatYoutubeVideo(video.link, errors)}
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
/>
{:else if video.metadata?.svid}
<div style="position:relative;padding-bottom:51.416579%">
<iframe
src="https://muse.ai/embed/{video.metadata
?.svid}?logo=https://app.classroomio.com/logo-512.png&subtitles=auto&cover_play_position=center"
style="width:100%;height:100%;position:absolute;left:0;top:0"
frameborder="0"
allowfullscreen
title="Muse AI Video Embed"
/>
</div>
{:else}
<video bind:this={player} class="plyr-video-trigger" playsinline controls>
<source src={video.link} type="video/mp4" />
<track kind="captions" />
</video>
{/if}
</div>
{/key}
</div>
{/each}
</div>
{/if}
</TabContent>
</slot:fragment>
</Tabs>
{:else if !isMaterialsEmpty($lesson.materials)}
<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" />
<h3 class="text-xl font-normal dark:text-white py-2">
No note, video or slide added for this lesson yet
</h3>
<p class="text-sm text-center font-normal py-2">
Share your knowledge with your students by creating engaging content<br />
Start by clicking on <strong>Get started</strong> button.
</p>
<PrimaryButton label="Get started" className="rounded-md" onClick={toggleMode} />
</Box>
{/if}
@@ -187,7 +187,7 @@
<a <a
href="{$page.url.pathname}{navItem.key}" href="{$page.url.pathname}{navItem.key}"
class="{navItem.key === activeNav && class="{navItem.key === activeNav &&
'active text-primary-700'} rounded-lg px-2 mr-6 text-slate-700 font-normal hover:bg-neutral-600 hover:text-slate-900 dark:text-white z-0" 'active text-primary-700'} rounded-lg px-2 mr-6 text-slate-700 font-normal hover:bg-neutral-600 dark:hover:text-slate-900 dark:text-white z-0"
> >
{navItem.label} {navItem.label}
</a> </a>
+5 -4
View File
@@ -3,12 +3,14 @@
import Card from './components/Card/index.svelte'; import Card from './components/Card/index.svelte';
import CardLoader from './components/Card/Loader.svelte'; import CardLoader from './components/Card/Loader.svelte';
import CoursesEmptyIcon from '../Icons/CoursesEmptyIcon.svelte'; import CoursesEmptyIcon from '../Icons/CoursesEmptyIcon.svelte';
// import CopyCourseModal from './components/CopyCourseModal/index.svelte';
import { courseMetaDeta } from './store'; import { courseMetaDeta } from './store';
import type { Course } from '$lib/utils/types'; import type { Course } from '$lib/utils/types';
import { appStore } from '$lib/utils/store/app'; import { appStore } from '$lib/utils/store/app';
export let courses: Course[] = []; export let courses: Course[] = [];
export let emptyTitle = 'No Courses Created';
export let emptyDescription =
'Share your knowledge with the world by creating engaging courses for your students.';
function calcProgressRate(progressRate?: number, totalLessons?: number): number { function calcProgressRate(progressRate?: number, totalLessons?: number): number {
if (!progressRate || !totalLessons) { if (!progressRate || !totalLessons) {
@@ -34,7 +36,6 @@
bannerImage={courseData.logo || '/images/classroomio-course-img-template.jpg'} bannerImage={courseData.logo || '/images/classroomio-course-img-template.jpg'}
title={courseData.title} title={courseData.title}
description={courseData.description} description={courseData.description}
role_id={courseData.role_id}
isPublished={courseData.is_published} isPublished={courseData.is_published}
cost={courseData.cost} cost={courseData.cost}
currency={courseData.currency} currency={courseData.currency}
@@ -49,9 +50,9 @@
{#if !$courseMetaDeta.isLoading && !courses.length} {#if !$courseMetaDeta.isLoading && !courses.length}
<Box className="w-full"> <Box className="w-full">
<CoursesEmptyIcon /> <CoursesEmptyIcon />
<h3 class="dark:text-white text-2xl my-5">No Courses Created</h3> <h3 class="dark:text-white text-2xl my-5">{emptyTitle}</h3>
<p class="dark:text-white w-1/3 text-center"> <p class="dark:text-white w-1/3 text-center">
Share your knowledge with the world by creating engaging courses for your students. {emptyDescription}
</p> </p>
</Box> </Box>
{/if} {/if}
+8 -1
View File
@@ -2,9 +2,16 @@ import { writable, derived } from 'svelte/store';
import type { Course } from '$lib/utils/types'; import type { Course } from '$lib/utils/types';
export const courses = writable<Course[]>([]); export const courses = writable<Course[]>([]);
export const courseInProgress = derived(courses, ($courses) => export const coursesInProgress = derived(courses, ($courses) =>
$courses.length > 0 ? $courses.slice(0, 3) : [] $courses.length > 0 ? $courses.slice(0, 3) : []
); );
export const coursesComplete = derived(courses, ($courses) =>
$courses.length > 0
? $courses.filter((course) => {
return course.total_lessons === course.progress_rate;
})
: []
);
export const courseMetaDeta = writable({ export const courseMetaDeta = writable({
isLoading: true isLoading: true
}); });
+14 -13
View File
@@ -2,8 +2,7 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import HelpIcon from 'carbon-icons-svelte/lib/Help.svelte'; import HelpIcon from 'carbon-icons-svelte/lib/Help.svelte';
import LicenseDraft from 'carbon-icons-svelte/lib/LicenseDraft.svelte'; import LicenseDraft from 'carbon-icons-svelte/lib/LicenseDraft.svelte';
import TextChip from '$lib/components/Chip/Text.svelte'; import Settings from 'carbon-icons-svelte/lib/Settings.svelte';
import OrgSelector from '$lib/components/OrgSelector/OrgSelector.svelte';
import HomeIcon from '$lib/components/Icons/HomeIcon.svelte'; import HomeIcon from '$lib/components/Icons/HomeIcon.svelte';
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte'; import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
import CommunityIcon from '$lib/components/Icons/CommunityIcon.svelte'; import CommunityIcon from '$lib/components/Icons/CommunityIcon.svelte';
@@ -59,19 +58,21 @@
> >
<div class="h-full flex flex-col"> <div class="h-full flex flex-col">
<div class="border-b border-gray-200 pt-5 px-4"> <div class="border-b border-gray-200 pt-5 px-4">
{#if $currentOrg.avatar_url} <div class="w-full flex flex-col items-center">
<Avatar <Avatar
src={$currentOrg.avatar_url} src={$profile.avatar_url}
name={$currentOrg.name} name={$profile.fullname}
shape="rounded-md" shape="rounded-full"
width="w-7" width="w-20"
height="h-7" height="h-20"
/> />
{:else}
<TextChip value={$currentOrg.shortName} className="bg-primary-200 font-bold" />
{/if}
<OrgSelector canAddOrg={false} /> <div class="mt-5">
<p class="dark:text-white text-lg font-bold whitespace-nowrap truncate">
{$profile.fullname}
</p>
</div>
</div>
<ul class="my-5"> <ul class="my-5">
{#each sideLinks as item (item.name)} {#each sideLinks as item (item.name)}
@@ -106,7 +107,7 @@
`/lms/settings` `/lms/settings`
) && NavClasses.active}" ) && NavClasses.active}"
> >
<Avatar src={$profile.avatar_url} name={$profile.username} width="w-7" height="h-7" /> <Settings size={20} class="carbon-icon dark:text-white" />
<p class="dark:text-white ml-2">Settings</p> <p class="dark:text-white ml-2">Settings</p>
</li> </li>
</a> </a>
@@ -3,7 +3,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte'; import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte';
import { courseInProgress } from '$lib/components/Courses/store'; import { coursesInProgress } from '$lib/components/Courses/store';
const gotoCourse = (id: string | undefined) => { const gotoCourse = (id: string | undefined) => {
if (!id) return; if (!id) return;
@@ -11,14 +11,14 @@
}; };
</script> </script>
<section class="h-full mt-10 mb-20 xl:mt-2"> <section class="h-full">
<p class="text-base font-semibold text-[#040F2D] pb-3 dark:text-white">Currently Learning</p> <p class="text-base font-semibold text-[#040F2D] pb-3 dark:text-white">Currently Learning</p>
<div <div
class="flex items-center flex-col border border-[#EAEAEA] dark:bg-neutral-800 gap-2 rounded w-full h-fit xl:h-[516px] p-3" class="flex items-center flex-col border border-[#EAEAEA] dark:bg-neutral-800 gap-2 rounded w-full lg:h-[40vh] p-3"
> >
{#if $courseInProgress.length > 0} {#if $coursesInProgress.length > 0}
<div class="w-full h-full xl:h-[516px] flex flex-col justify-start"> <div class="w-full h-full flex flex-col justify-start overflow-y-auto">
{#each $courseInProgress as course} {#each $coursesInProgress as course}
<div class="p-5"> <div class="p-5">
<span class="flex flex-col md:flex-row gap-3 items-start pb-5"> <span class="flex flex-col md:flex-row gap-3 items-start pb-5">
<img <img
@@ -29,7 +29,7 @@
<p class="text-base font-semibold text-[#040F2D] pb-3 dark:text-white">Your Schedule</p> <p class="text-base font-semibold text-[#040F2D] pb-3 dark:text-white">Your Schedule</p>
<div <div
class="flex items-center flex-col border border-[#EAEAEA] dark:bg-neutral-800 rounded w-full h-fit xl:h-[516px] p-3" class="flex items-center flex-col border border-[#EAEAEA] dark:bg-neutral-800 rounded w-full h-fit lg:h-[40vh] p-3 overflow-y-auto"
> >
<div class="w-full h-fit hidden xl:flex items-center justify-center"> <div class="w-full h-fit hidden xl:flex items-center justify-center">
<InlineCalendar bind:store {theme} /> <InlineCalendar bind:store {theme} />
+24 -2
View File
@@ -4,11 +4,14 @@
import NotificationIcon from 'carbon-icons-svelte/lib/Notification.svelte'; import NotificationIcon from 'carbon-icons-svelte/lib/Notification.svelte';
import MoonIcon from 'carbon-icons-svelte/lib/Moon.svelte'; import MoonIcon from 'carbon-icons-svelte/lib/Moon.svelte';
import SunIcon from 'carbon-icons-svelte/lib/Sun.svelte'; import SunIcon from 'carbon-icons-svelte/lib/Sun.svelte';
import Avatar from '$lib/components/Avatar/index.svelte';
import TextChip from '$lib/components/Chip/Text.svelte';
import IconButton from '$lib/components/IconButton/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte';
import { appStore } from '$lib/utils/store/app'; import { appStore } from '$lib/utils/store/app';
import { Close, Menu } from 'carbon-icons-svelte'; import { Close, Menu } from 'carbon-icons-svelte';
import { menu } from '$lib/components/Org/store'; import { menu } from '$lib/components/Org/store';
import { currentOrg } from '$lib/utils/store/org';
import { toggleBodyByMode } from '$lib/utils/functions/app'; import { toggleBodyByMode } from '$lib/utils/functions/app';
export let navClass = ''; export let navClass = '';
@@ -39,8 +42,27 @@
</IconButton> </IconButton>
</li> </li>
<div class=""> <div class="">
<a href={$page.url.pathname} title="Go to ClassroomIO Home" id="logo" class="text-lg"> <a
ClassroomIO href={$page.url.pathname}
title="Go to ClassroomIO Home"
id="logo"
class="text-lg flex items-center"
>
{#if $currentOrg.avatar_url}
<Avatar
src={$currentOrg.avatar_url}
name={$currentOrg.name}
shape="rounded-md"
width="w-7"
height="h-7"
className="mr-2"
/>
{:else}
<TextChip value={$currentOrg.shortName} className="bg-primary-200 font-bold" />
{/if}
<span class="line-clamp-1">
{$currentOrg.name}
</span>
</a> </a>
</div> </div>
-255
View File
@@ -1,255 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { InlineCalendar, Datepicker } from 'svelte-calendar';
import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
import { formatUserUpcomingData, formatDate } from '$lib/utils/functions/routes/dashboard';
import { user } from '$lib/utils/store/user';
import { fetchUserUpcomingData } from '$lib/utils/services/dashboard';
import { isMobile } from '$lib/utils/store/useMobile';
import type { UserLessonDataType, LessonsByMonthIndexType } from '$lib/utils/types';
const theme = {
calendar: {
width: '400px',
shadow: '0px 0px 5px rgba(0, 0, 0, 0.25)'
}
};
let store;
let calendar: HTMLElement | null = null;
let lessonsByMonth: LessonsByMonthIndexType = {};
let prevSelectedDate: string = new Date().toUTCString();
let selectedDateLessonData: UserLessonDataType[] | [] = [];
const addDotsToCalendar = (currentMonthIndexInRenderedMonth: number) =>
setTimeout(() => {
if (!calendar) calendar = document.getElementById('calendar');
// console.log(
// `currentMonthIndexInRenderedMonth`,
// currentMonthIndexInRenderedMonth
// );
const renderedMonths =
calendar?.querySelectorAll('.contents .container .stage .grid .grid') || [];
// console.log(`renderedMonths`, renderedMonths);
const currentMonthIndex = new Date($store?.selected).getMonth();
const currentMonth = renderedMonths[currentMonthIndexInRenderedMonth];
// console.log(`currentMonth`, currentMonth);
// All elements for each day in the month - with prev or next month
const currentMonthElements =
currentMonth?.querySelectorAll<HTMLElement>('a:not(.outsider)') || [];
// console.log(`currentMonthElements`, currentMonthElements, '\n\n');
const currentMonthData = lessonsByMonth[currentMonthIndex];
if (!currentMonthData) return;
for (let i in currentMonthElements) {
if (!currentMonthElements.hasOwnProperty(i)) {
continue;
}
const dayElement = currentMonthElements[i];
dayElement.style.position = 'relative';
// The date is inside the element like <a>2</a>
const dateIndex = Number(dayElement.innerHTML);
if (!!currentMonthData[dateIndex]) {
// console.log(`dateIndex`, dateIndex);
// console.log(`lesson data`, currentMonthData[dateIndex]);
dayElement.insertAdjacentHTML('beforeend', '<span class="active-day">•</span>');
}
}
}, 500);
async function runFirstThings(currentSession: { id: string } | null) {
if (!currentSession || !currentSession.id) return;
const userUpcomingData = await fetchUserUpcomingData(currentSession.id);
lessonsByMonth = formatUserUpcomingData(userUpcomingData);
// Only for desktop. Only run on mobile when the user clicks on the date
if (!$isMobile) {
addDotsToCalendar(1);
}
}
function getDataOfSelectedDate(_selectedDate: string): UserLessonDataType[] | [] {
if (!_selectedDate) return [];
const selectedDate = new Date(_selectedDate).toUTCString();
const monthIndex = new Date(selectedDate).getMonth();
const dateIndex = new Date(selectedDate).getDate();
// If we changed the month, update the calendar dots
const prevMonthIndex = new Date(prevSelectedDate).getMonth();
if (prevMonthIndex !== monthIndex) {
const currentMonthIndexInRenderedMonth = monthIndex < prevMonthIndex ? 1 : 2;
addDotsToCalendar(currentMonthIndexInRenderedMonth);
}
prevSelectedDate = selectedDate;
const monthLessonData = lessonsByMonth[monthIndex];
if (!monthLessonData) {
return [];
}
const dateLessonData = monthLessonData[dateIndex];
if (!dateLessonData) {
return [];
}
return dateLessonData;
}
onMount(() => {
// Remove default shouldEnlargeDay=true when using <DatePicker /> on mobile
if ($store && $store.shouldEnlargeDay) {
$store.shouldEnlargeDay = false;
}
});
// This is here not called onMount because we want to call it as soon as we have the user's `currentSession` value
$: runFirstThings($user.currentSession);
// Every time the date changes
$: selectedDateLessonData = getDataOfSelectedDate($store?.selected);
</script>
<svelte:head>
<title>Dashboard - ClassroomIO</title>
</svelte:head>
<section class="root w-full py-10 bg-gray-100 dark:bg-neutral-800">
<div class="calendar-root flex mx-2">
{#if !$isMobile}
<div id="calendar">
<InlineCalendar bind:store {theme} />
</div>
{/if}
<div
id={$isMobile ? 'calendar' : ''}
class="calendar-info flex-grow border border-gray-300 mx-2 rounded-md"
>
{#if $isMobile}
<div class="header w-full flex items-center">
<Datepicker bind:store let:key let:send let:receive>
<button
class="text-lg font-bold text-primary-700 p-3 hover:bg-gray-300 rounded-md"
in:receive|local={{ key }}
out:send|local={{ key }}
on:click={() => {
addDotsToCalendar(1);
}}
>
{formatDate($store?.selected)}
</button>
</Datepicker>
</div>
{:else}
<div class="header text-lg font-bold text-primary-700 flex px-5 items-center">
{formatDate($store?.selected)}
</div>
{/if}
{#each selectedDateLessonData as lessonData}
<div class="flex items-center justify-between p-5 lesson-data">
<div class="w-4/5">
<a
class="text-black-700 text-lg font-bold flex items-center"
href={`/courses/${lessonData.course_id}/lessons/${
lessonData.is_unlocked ? lessonData.lesson_id : ''
}`}
>
{lessonData.lesson_title}
{#if lessonData.is_unlocked}
<span class="ml-2 success">
<CheckmarkFilledIcon size={20} class="carbon-icon dark:text-white" />
</span>
{:else}
<span class="text-md ml-2">🔒</span>
{/if}
</a>
<p class="dark:text-white text-grey text-sm flex items-center">
<a class="underline text-primary-700 my-2" href="/courses/{lessonData.course_id}">
{` ${lessonData.course_title}`}
</a>
</p>
</div>
{#if lessonData.is_unlocked}
<a
class="join-call rounded-3xl bg-primary-600 text-white py-3 font-bold shadow-lg {!lessonData.call_url &&
'opacity-50 pointer-events-none cursor-not-allowed'}"
href={!!lessonData.call_url ? lessonData.call_url : undefined}
target="_blank"
title={!!lessonData.call_url
? 'Click to join the call'
: 'No call link was added for this lesson. Ask your trainer'}
>
Join call
</a>
{/if}
</div>
{:else}
<p class="dark:text-white flex items-center justify-center w-full no-data">
No lesson on this day
</p>
{/each}
</div>
</div>
</section>
<style>
.root {
min-height: 93vh;
}
.calendar-root {
max-width: 1000px;
margin: 0 auto;
}
.calendar-info {
max-width: 600px;
width: 100%;
height: 100%;
}
.header {
border-bottom: 1px solid var(--border-color);
min-height: 61px;
}
.lesson-data {
border-bottom: 1px solid var(--border-color);
}
.no-data {
height: 100px;
}
.join-call {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-height: 48px;
width: 110px;
text-align: center;
}
:global(span.active-day) {
position: absolute;
bottom: -8px;
left: 40%;
color: var(--main-primary-color);
}
@media (max-width: 640px) {
:global(span.active-day) {
font-size: 30px;
}
}
</style>
+28 -28
View File
@@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import Schedule from '$lib/components/LMS/components/Schedule.svelte';
import Learning from '$lib/components/LMS/components/Learning.svelte'; import Learning from '$lib/components/LMS/components/Learning.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
import { fetchCourses } from '$lib/components/Courses/api';
import { currentOrg } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
import { fetchCourses } from '$lib/components/Courses/api';
import { courses, courseMetaDeta } from '$lib/components/Courses/store'; import { courses, courseMetaDeta } from '$lib/components/Courses/store';
import type { Course } from '$lib/utils/types'; import type { Course } from '$lib/utils/types';
@@ -70,38 +69,39 @@
class="w-28 md:block md:w-1/3 lg:w-[275px] lg:h-[205px] mb-3 md:mb-0" class="w-28 md:block md:w-1/3 lg:w-[275px] lg:h-[205px] mb-3 md:mb-0"
/> />
</div> </div>
<main class="flex w-full h-full flex-col xl:flex-row xl:gap-5"> <section class="flex w-full flex-col xl:flex-row xl:gap-5">
<div class="w-full h-full"> <div class="w-full md:w-[75%] mt-10 xl:mt-2">
<Schedule />
</div>
<div class="w-full h-full">
<Learning /> <Learning />
</div> </div>
<div <div class="mt-10 xl:mt-2 w-full md:w-[75%] xl:w-[400px]">
class="flex items-center justify-center border border-[#EAEAEA] dark:bg-neutral-800 gap-2 rounded w-full xl:w-[400px] h-fit xl:h-[516px] overflow-y-auto p-3 xl:mt-11" <p class="text-base font-semibold text-[#040F2D] pb-3 dark:text-white">Your Progress</p>
>
<div <div
class="w-full h-full flex flex-col sm:flex-row xl:flex-col items-center justify-between md:items-center xl:items-start gap-5 md:justify-around" class="flex items-center justify-center border border-[#EAEAEA] dark:bg-neutral-800 gap-2 rounded h-fit lg:h-[40vh] lg:overflow-y-auto p-3"
> >
<div> <div
<img src="/images/target.svg" alt="student Learning score" /> class="w-full h-full flex flex-col sm:flex-row xl:flex-col items-center justify-between md:items-center xl:items-start gap-5 md:justify-around"
</div> >
<span class="text-center xl:text-start"> <div>
<p class="text-base font-semibold py-2 text-[#040F2D] dark:text-white">Your Progress</p> <img src="/images/target.svg" alt="student Learning score" />
{#if totalLessons > 0} </div>
<p class="text-xs font-normal text-[#656565] dark:text-white"> <span class="text-center xl:text-start">
{totalComPleted}/{totalLessons} lessons completed <p class="text-base font-semibold py-2 text-[#040F2D] dark:text-white">
Your Progress
</p> </p>
{:else} {#if totalLessons > 0}
<p class="text-xs font-normal text-[#656565] dark:text-white">No courses started</p> <p class="text-xs font-normal text-[#656565] dark:text-white">
{/if} {totalComPleted}/{totalLessons} lessons completed
</span> </p>
<h1 class="text-5xl md:text-6xl font-bold text-[#262626] dark:text-white my-0"> {:else}
{progressPercentage} % <p class="text-xs font-normal text-[#656565] dark:text-white">No courses started</p>
</h1> {/if}
</span>
<h1 class="text-5xl md:text-6xl font-bold text-[#262626] dark:text-white my-0">
{progressPercentage} %
</h1>
</div>
</div> </div>
</div> </div>
</main> </section>
</div> </div>
</section> </section>
+76 -71
View File
@@ -114,84 +114,89 @@
} }
</script> </script>
<section class="max-w-6xl mx-auto"> <section class="w-full max-w-6xl mx-auto">
<div class="m-5"> <div class="py-10 px-5">
<h1 class="text-3xl font-semibold my-4">Exercises</h1> <div class="flex items-center justify-between mb-10">
<div class="flex items-center w-full"> <h1 class="dark:text-white text-3xl font-bold">Exercises</h1>
{#each sections as { id, title, items, className }, idx (id)} </div>
<div
class="min-w-[355px] max-w-[355px] h-[516px] rounded-md bg-gray-100 dark:bg-black border border-gray-50 p-3 mr-3 overflow-hidden" <div>
animate:flip={{ duration: flipDurationMs }} <div class="flex items-center w-full">
> {#each sections as { id, title, items, className }, idx (id)}
<div class="flex items-center mb-2 gap-2">
<p class="dark:text-white ml-2 font-bold">{title}</p>
<Chip value={items.length} {className} />
</div>
<div <div
class="content pr-2 overflow-y-auto mb-3" class="min-w-[355px] max-w-[355px] h-[516px] rounded-md bg-gray-100 dark:bg-black border border-gray-50 p-3 mr-3 overflow-hidden"
use:dndzone={{ animate:flip={{ duration: flipDurationMs }}
items,
flipDurationMs,
dropTargetStyle: { outline: 'blue' }
}}
on:consider={handleDndConsiderCards(idx)}
on:finalize={handleDndFinalizeCards(idx)}
> >
{#if items.length === 0} <div class="flex items-center mb-2 gap-2">
<div <p class="dark:text-white ml-2 font-bold">{title}</p>
class="h-[100px]" <Chip value={items.length} {className} />
use:dndzone={{ </div>
items: [], // Empty array for placeholder <div
flipDurationMs, class="content pr-2 overflow-y-auto mb-3"
dropTargetStyle: { outline: 'blue' } use:dndzone={{
}} items,
on:consider={handleDndConsiderCards(idx)} flipDurationMs,
on:finalize={handleDndFinalizeCards(idx)} dropTargetStyle: { outline: 'blue' }
> }}
<!-- Placeholder content --> on:consider={handleDndConsiderCards(idx)}
<p class="text-gray-500 dark:text-gray-400 text-left p-2"> on:finalize={handleDndFinalizeCards(idx)}
drop your {title} exercises here >
</p> {#if items.length === 0}
</div>
{:else}
{#each items as item (item.id)}
<div <div
class=" w-full my-2 mx-0 rounded-md bg-white dark:bg-gray-600 py-3 px-3" class="h-[100px]"
animate:flip={{ duration: flipDurationMs }} use:dndzone={{
items: [], // Empty array for placeholder
flipDurationMs,
dropTargetStyle: { outline: 'blue' }
}}
on:consider={handleDndConsiderCards(idx)}
on:finalize={handleDndFinalizeCards(idx)}
> >
<a <!-- Placeholder content -->
class="flex w-full items-center cursor-pointer text-black mb-2" <p class="text-gray-500 dark:text-gray-400 text-left p-2">
href={`${$page.url.pathname}?submissionId=${item.id}`} drop your {title} exercises here
>
<p class="dark:text-white ml-2 text-sm">
{item.student.username}
</p>
</a>
<a
class="text-primary-700 dark:text-white text-md font-bold"
href="{$page.url.pathname}?submissionId={item.id}"
>
{item.exercise.title}
</a>
<a
class="flex items-center no-underline hover:underline text-black my-2"
href="{$page.url?.pathname?.replace('submissions', 'lessons')}/{item.lesson
.id}/exercises/{item.exercise.id}"
>
<p class="dark:text-white text-grey text-sm">
#{item.lesson.title}
{`${item.tutor ? 'created by' + item.tutor.name : ''}`}
</p>
</a>
<p class="dark:text-white text-gray-500 text-xs">
{item.submittedAt}
</p> </p>
</div> </div>
{/each} {:else}
{/if} {#each items as item (item.id)}
<div
class=" w-full my-2 mx-0 rounded-md bg-white dark:bg-gray-600 py-3 px-3"
animate:flip={{ duration: flipDurationMs }}
>
<a
class="flex w-full items-center cursor-pointer text-black mb-2"
href={`${$page.url.pathname}?submissionId=${item.id}`}
>
<p class="dark:text-white ml-2 text-sm">
{item.student.username}
</p>
</a>
<a
class="text-primary-700 dark:text-white text-md font-bold"
href="{$page.url.pathname}?submissionId={item.id}"
>
{item.exercise.title}
</a>
<a
class="flex items-center no-underline hover:underline text-black my-2"
href="{$page.url?.pathname?.replace('submissions', 'lessons')}/{item.lesson
.id}/exercises/{item.exercise.id}"
>
<p class="dark:text-white text-grey text-sm">
#{item.lesson.title}
{`${item.tutor ? 'created by' + item.tutor.name : ''}`}
</p>
</a>
<p class="dark:text-white text-gray-500 text-xs">
{item.submittedAt}
</p>
</div>
{/each}
{/if}
</div>
</div> </div>
</div> {/each}
{/each} </div>
</div> </div>
</div> </div>
</section> </section>
+36 -7
View File
@@ -3,7 +3,10 @@
import Tabs from '$lib/components/Tabs/index.svelte'; import Tabs from '$lib/components/Tabs/index.svelte';
import TabContent from '$lib/components/TabContent/index.svelte'; import TabContent from '$lib/components/TabContent/index.svelte';
import Courses from '$lib/components/Courses/index.svelte'; import Courses from '$lib/components/Courses/index.svelte';
import { courses } from '$lib/components/Courses/store'; import { fetchCourses } from '$lib/components/Courses/api';
import { profile } from '$lib/utils/store/user';
import { currentOrg } from '$lib/utils/store/org';
import { courses, courseMetaDeta, coursesComplete } from '$lib/components/Courses/store';
const tabs = [ const tabs = [
{ {
@@ -16,11 +19,29 @@
} }
]; ];
let currentTab = tabs[0].value; let currentTab = tabs[0].value;
let hasFetched = false;
const onChange = function onChange(tab: 0) {
(tab = 0) => return () => (currentTab = tab);
() => }
(currentTab = tab);
async function getCourses(userId: string | null, orgId: string) {
if (hasFetched || !userId || !orgId) {
return;
}
$courseMetaDeta.isLoading = true;
const coursesResult = await fetchCourses(userId, orgId);
console.log(`coursesResult`, coursesResult);
$courseMetaDeta.isLoading = false;
if (!coursesResult) return;
courses.set(coursesResult.allCourses);
hasFetched = true;
}
$: getCourses($profile.id, $currentOrg.id);
</script> </script>
<section class="max-w-6xl mx-auto"> <section class="max-w-6xl mx-auto">
@@ -32,10 +53,18 @@
<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={tabs[0].value} index={currentTab}>
<Courses courses={$courses} /> <Courses
courses={$courses}
emptyTitle="No Course In progress"
emptyDescription="Any course that you start will be displayed here"
/>
</TabContent> </TabContent>
<TabContent value={tabs[1].value} index={currentTab}> <TabContent value={tabs[1].value} index={currentTab}>
<Courses courses={[]} /> <Courses
courses={$coursesComplete}
emptyTitle="No Course Completed"
emptyDescription="Any course that you complete will be displayed here"
/>
</TabContent> </TabContent>
</slot:fragment> </slot:fragment>
</Tabs> </Tabs>
+21 -15
View File
@@ -22,20 +22,26 @@
]; ];
</script> </script>
<section class="max-w-6xl mx-auto"> <section class="w-full max-w-6xl mx-auto">
<div class="m-2 md:m-5"> <div class="py-10 px-5">
<Tabs autoWidth bind:selected> <div class="flex items-center justify-between mb-10">
{#each tabs as tab} <h1 class="dark:text-white text-3xl font-bold">Settings</h1>
<Tab label={tab.label} href={tab.href} /> </div>
{/each}
<svelte:fragment slot="content"> <div class="">
<TabContent> <Tabs autoWidth bind:selected>
<Profile /> {#each tabs as tab}
</TabContent> <Tab label={tab.label} href={tab.href} />
<TabContent> {/each}
<Integrations /> <svelte:fragment slot="content">
</TabContent> <TabContent>
</svelte:fragment> <Profile />
</Tabs> </TabContent>
<TabContent>
<Integrations />
</TabContent>
</svelte:fragment>
</Tabs>
</div>
</div> </div>
</section> </section>
-2
View File
@@ -177,8 +177,6 @@
return dateLessonData; return dateLessonData;
} }
function goToSite() {}
onMount(() => { onMount(() => {
// Remove default shouldEnlargeDay=true when <DatePicker /> on mobile // Remove default shouldEnlargeDay=true when <DatePicker /> on mobile
if ($store && $store.shouldEnlargeDay) { if ($store && $store.shouldEnlargeDay) {