feat: course types - self paced and live class (#309)

* working on the course type

* trying to mark it persist

* almost done here

* fixed sendig emtpy options bug

* created migration file

* created migration file

* made updates based on PR review

* merge

* update course type card

* add redirect logic with classroomio.com/new

* make new course modal only open/close with url

* feat: page active state (#315)

* finished the active link state

* updated the active link state based on review

* simplify code

---------

Co-authored-by: Rotimi Best <best@classroomio.com>

* add type to course card

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
Em_Dev
2024-05-21 00:14:47 +01:00
committed by GitHub
co-authored by Rotimi Best
parent b9bb568f98
commit 49d8539be9
41 changed files with 894 additions and 393 deletions
@@ -225,10 +225,10 @@
}
function getPageRoles(org: CurrentOrg) {
const roles = [1, 2];
const roles: number[] = [1, 2];
if (org.customization.course.newsfeed) {
roles.push(3);
roles.concat([3]);
}
return roles;
@@ -14,7 +14,7 @@
} from '$lib/components/Course/components/Lesson/store/lessons';
import PageBody from '$lib/components/PageBody/index.svelte';
import AddLessonModal from '$lib/components/Course/components/Lesson/AddLessonModal.svelte';
import NewLessonModal from '$lib/components/Course/components/Lesson/NewLessonModal.svelte';
import DeleteLessonConfirmation from '$lib/components/Course/components/Lesson/DeleteLessonConfirmation.svelte';
import { handleAddLessonWidget } from '$lib/components/Course/components/Navigation/store';
import { Video, Calendar } from 'carbon-icons-svelte';
@@ -28,6 +28,7 @@
import type { Course, Lesson } from '$lib/utils/types';
import { t } from '$lib/utils/functions/translations.js';
import { goto } from '$app/navigation';
import { COURSE_TYPE } from '$lib/utils/types';
export let data;
@@ -126,7 +127,7 @@
</script>
{#if $handleAddLessonWidget}
<AddLessonModal {isStudent} />
<NewLessonModal {isStudent} />
{/if}
<DeleteLessonConfirmation
@@ -217,84 +218,86 @@
{/if}
<!-- Lesson Educator -->
<div
class="mt-2 flex w-4/5 flex-col items-start justify-between lg:flex-row lg:items-center"
>
<div class="lg:mb-0">
{#if lessonEditing === lesson.id}
<Select
bind:value={lesson.profile}
options={$group.tutors}
labelKey="fullname"
className="sm:my-1 w-[100%]"
/>
{:else if !lesson.profile}
<p class="dark:text-white ml-2 text-sm mb-3">
{$t('course.navItem.lessons.no_tutor')}
</p>
{:else}
<a href="." class="mb-2 flex items-center hover:underline">
<Avatar
className="w-6 h-6"
src={lesson.profile.avatar_url}
name="Avatar placeholder"
{#if $course.type === COURSE_TYPE.LIVE_CLASS}
<div
class="mt-2 flex w-4/5 flex-col items-start justify-between lg:flex-row lg:items-center"
>
<div class="lg:mb-0">
{#if lessonEditing === lesson.id}
<Select
bind:value={lesson.profile}
options={$group.tutors}
labelKey="fullname"
className="sm:my-1 w-[100%]"
/>
<p class="ml-2 text-sm dark:text-white">
{lesson.profile.fullname}
{:else if !lesson.profile}
<p class="dark:text-white ml-2 text-sm mb-3">
{$t('course.navItem.lessons.no_tutor')}
</p>
</a>
{/if}
</div>
<!-- Lesson Date -->
<div class="flex items-center lg:mb-0">
{#if lessonEditing === lesson.id}
<DateField
value={formatDate(lesson.lesson_at)}
className="p-2 my-2 rounded-md sm:w-[179px] dark:bg-neutral-800 dark:text-white"
onChange={(e) => (lesson.lesson_at = e.target.value)}
/>
{:else}
<div class="mb-2 flex">
<Calendar size={20} class="carbon-icon text-gray-400 dark:text-white" />
<p class="ml-2 text-sm dark:text-white">
{new Date(lesson.lesson_at || '').toDateString()}
</p>
</div>
{/if}
</div>
<!-- Lesson Call url -->
<div class="mb-3 flex items-center lg:mb-0">
{#if lessonEditing === lesson.id}
<TextField
bind:value={lesson.call_url}
autoFocus={true}
className="w-[179px]"
placeholder="https://meet.google.com/mga-dsjs-fmb"
/>
{:else}
<div class="flex">
<Video size={20} class="carbon-icon ml-0.5 text-gray-400 dark:text-white" />
<a
class="text-primary-600 ml-2 text-sm"
href={lesson.call_url || '#'}
target="_blank"
>
{lesson.call_url
? $t('course.navItem.lessons.join_lesson')
: $t('course.navItem.lessons.no_link')}
{:else}
<a href="." class="mb-2 flex items-center hover:underline">
<Avatar
className="w-6 h-6"
src={lesson.profile.avatar_url}
name="Avatar placeholder"
/>
<p class="ml-2 text-sm dark:text-white">
{lesson.profile.fullname}
</p>
</a>
</div>
{/if}
{/if}
</div>
<!-- Lesson Date -->
<div class="flex items-center lg:mb-0">
{#if lessonEditing === lesson.id}
<DateField
value={formatDate(lesson.lesson_at)}
className="p-2 my-2 rounded-md sm:w-[179px] dark:bg-neutral-800 dark:text-white"
onChange={(e) => (lesson.lesson_at = e.target.value)}
/>
{:else}
<div class="mb-2 flex">
<Calendar size={20} class="carbon-icon text-gray-400 dark:text-white" />
<p class="ml-2 text-sm dark:text-white">
{new Date(lesson.lesson_at || '').toDateString()}
</p>
</div>
{/if}
</div>
<!-- Lesson Call url -->
<div class="mb-3 flex items-center lg:mb-0">
{#if lessonEditing === lesson.id}
<TextField
bind:value={lesson.call_url}
autoFocus={true}
className="w-[179px]"
placeholder="https://meet.google.com/mga-dsjs-fmb"
/>
{:else}
<div class="flex">
<Video size={20} class="carbon-icon ml-0.5 text-gray-400 dark:text-white" />
<a
class="text-primary-600 ml-2 text-sm"
href={lesson.call_url || '#'}
target="_blank"
>
{lesson.call_url
? $t('course.navItem.lessons.join_lesson')
: $t('course.navItem.lessons.no_link')}
</a>
</div>
{/if}
</div>
</div>
</div>
{/if}
</div>
<!-- 3 dot edit -->
<div class="flex flex-row">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<OverflowMenu size="xl" class="absolute right-0 top-0">
<OverflowMenu size="xl" class={`absolute right-0 top-0`}>
<OverflowMenuItem
disabled={isStudent}
text={lesson.is_unlocked
@@ -83,6 +83,7 @@
}
});
}
// TODO WRITE A REDIRECT FUNCTION FOR THIS THIS PAGE
function getPageRoles(org: CurrentOrg) {
const roles = [1, 2];
@@ -245,7 +245,7 @@
const questionAnswer = questionAnswers.find(
(answer: { question_id: string }) => answer.question_id == questionId
);
console.log('this is the question answer in the admin side', questionAnswer);
const point = questionAnswerByPoint[questionId];
totalPoints += parseInt(point, 10);
@@ -6,24 +6,18 @@
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';
import {
courses,
courseMetaDeta,
coursesComplete,
coursesInProgress
} from '$lib/components/Courses/store';
import { browser } from '$app/environment';
import { t } from '$lib/utils/functions/translations';
const tabs = [
{
label: $t('my_learning.progress'),
value: 1
},
{
label: $t('my_learning.complete'),
value: 2
}
];
let currentTab = tabs[0].value;
let hasFetched = false;
function onChange(tab: 0) {
function onChange(tab) {
return () => (currentTab = tab);
}
@@ -51,6 +45,18 @@
$: if (browser && $profile.id && $currentOrg.id) {
getCourses($profile.id, $currentOrg.id);
}
$: tabs = [
{
label: `${$t('my_learning.progress')} (${$coursesInProgress.length})`,
value: '1'
},
{
label: `${$t('my_learning.complete')} (${$coursesComplete.length})`,
value: '2'
}
];
$: currentTab = tabs[0].value;
</script>
<section class="max-w-6xl mx-auto">
@@ -63,7 +69,7 @@
<slot:fragment slot="content">
<TabContent value={tabs[0].value} index={currentTab}>
<Courses
courses={$courses}
courses={$coursesInProgress}
emptyTitle={$t('my_learning.not_in_progress')}
emptyDescription={$t('my_learning.any_progress')}
/>
@@ -11,7 +11,8 @@
export let data;
$: if ($currentOrg.id && data.orgName === '*') {
goto(`/org/${$currentOrg.siteName}`);
const newUrl = $page.url.pathname.replace('*', $currentOrg.siteName);
goto(newUrl + $page.url.search);
}
</script>
@@ -1,13 +1,12 @@
<script lang="ts">
import { Search, Dropdown } from 'carbon-components-svelte';
import { page } from '$app/stores';
import { profile } from '$lib/utils/store/user';
import { fetchCourses } from '$lib/components/Courses/api';
import Courses from '$lib/components/Courses/index.svelte';
import NewCourseModal from '$lib/components/Courses/components/NewCourseModal/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { courses, createCourseModal, courseMetaDeta } from '$lib/components/Courses/store';
import { currentOrg } from '$lib/utils/store/org';
import { courses, courseMetaDeta } from '$lib/components/Courses/store';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import { Add } from 'carbon-icons-svelte';
import { isMobile } from '$lib/utils/store/useMobile';
import { isOrgAdmin } from '$lib/utils/store/org';
@@ -19,6 +18,7 @@
import Grid from 'carbon-icons-svelte/lib/Grid.svelte';
import List from 'carbon-icons-svelte/lib/List.svelte';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
export let data;
@@ -28,12 +28,6 @@
let filteredCourses: Course[];
let hasFetched = false;
const urlParams = new URLSearchParams($page.url.search);
if (urlParams.get('create') === 'true') {
$createCourseModal.open = true;
}
async function getCourses(userId: string | undefined, orgId: string) {
if (cantFetch && typeof cantFetch === 'boolean' && orgId && !hasFetched) {
// only show is loading when fetching for the first time
@@ -86,6 +80,10 @@
localStorage.setItem('courseView', preference);
};
const openNewCourseModal = () => {
goto($currentOrgPath + '/courses?create=true');
};
onMount(() => {
const courseView = localStorage.getItem('courseView') as 'grid' | 'list' | null;
@@ -107,7 +105,7 @@
<div class="flex items-center justify-between mb-5">
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">{$t('courses.heading')}</h1>
{#if $isMobile}
<PrimaryButton isDisabled={!$isOrgAdmin} onClick={() => ($createCourseModal.open = true)}>
<PrimaryButton isDisabled={!$isOrgAdmin} onClick={openNewCourseModal}>
<Add size={24} />
</PrimaryButton>
{:else}
@@ -115,7 +113,7 @@
label={$t('courses.heading_button')}
variant={VARIANTS.CONTAINED_DARK}
isDisabled={!$isOrgAdmin}
onClick={() => ($createCourseModal.open = true)}
onClick={openNewCourseModal}
/>
{/if}
</div>