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
@@ -14,6 +14,11 @@
import Share from 'carbon-icons-svelte/lib/Share.svelte';
import UserFollow from 'carbon-icons-svelte/lib/UserFollow.svelte';
import { t } from '$lib/utils/functions/translations';
import { COURSE_TYPE } from '$lib/utils/types';
import RadioButtonChecked from 'carbon-icons-svelte/lib/RadioButtonChecked.svelte';
import GrowthIcon from 'carbon-icons-svelte/lib/Growth.svelte';
import CourseIcon from 'carbon-icons-svelte/lib/Course.svelte';
import UserProfileIcon from 'carbon-icons-svelte/lib/UserProfile.svelte';
export let bannerImage: string | undefined;
export let id = '';
@@ -28,6 +33,7 @@
export let isOnLandingPage = false;
export let isLMS = false;
export let progressRate = 45;
export let type: COURSE_TYPE;
let target: any;
@@ -58,6 +64,34 @@
? `/course/${slug}`
: `/courses/${id}${isLMS ? '/lessons?next=true' : ''}`;
}
const COURSE_TAG: Record<
string,
{
style: string;
label: string;
icon: any;
iconStyle?: string;
}
> = {
[COURSE_TYPE.LIVE_CLASS]: {
style: '',
label: $t('course.navItem.settings.live_class'),
icon: RadioButtonChecked,
iconStyle: 'text-red-700'
},
[COURSE_TYPE.SELF_PACED]: {
style: '',
label: $t('course.navItem.settings.self_paced'),
icon: UserProfileIcon,
iconStyle: 'text-primary-700'
},
SPECIALIZATION: {
style: '',
label: $t('specialization.course_tag'),
icon: GrowthIcon
}
};
</script>
{#if !isLMS && !isOnLandingPage}
@@ -96,17 +130,26 @@
class="text-black border border-gray dark:border-neutral-600 rounded w-full max-w-[320px] relative hover:scale-95 transition-all ease-in-out"
>
<div class="p-4">
<div class=" mb-5">
<div class="relative mb-5">
<ImageLoader
src={bannerImage}
alt="Course Logo"
class="h-[200px] w-full rounded dark:border dark:border-neutral-600"
class="h-[200px] w-full rounded dark:border dark:border-neutral-600 relative"
>
<svelte:fragment slot="loading">
<SkeletonPlaceholder style="width: 100%; height: 200px;" />
</svelte:fragment>
<svelte:fragment slot="error">{$t('courses.course_card.error_message')}</svelte:fragment>
</ImageLoader>
{#if type}
{@const tag = COURSE_TAG[type]}
<span
class="absolute bottom-2 left-2 z-10 text-xs capitalize bg-primary-50 rounded-sm p-1 flex items-center gap-1 font-mono"
>
<svelte:component this={tag.icon} size={16} class={tag.iconStyle} />
{tag.label}
</span>
{/if}
</div>
<h3 class="text-xl dark:text-white title">{title}</h3>
@@ -12,6 +12,7 @@
export let id = '';
export let title = '';
export let type = '';
export let description = '';
export let isPublished = false;
export let totalLessons = 0;
@@ -48,6 +49,7 @@
<p class="line-clamp-2">{description}</p>
</StructuredListCell>
{#if !$isMobile}
<StructuredListCell>{type}</StructuredListCell>
<StructuredListCell>{totalLessons}</StructuredListCell>
<StructuredListCell>{totalStudents}</StructuredListCell>
<StructuredListCell>
@@ -1,6 +1,10 @@
<script>
<script lang="ts">
import { page } from '$app/stores';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import Modal from '$lib/components/Modal/index.svelte';
import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import TextArea from '$lib/components/Form/TextArea.svelte';
import { courses, createCourseModal } from '$lib/components/Courses/store';
@@ -13,20 +17,41 @@
import { goto } from '$app/navigation';
import { capturePosthogEvent } from '$lib/utils/services/posthog';
import { t } from '$lib/utils/functions/translations';
import { COURSE_TYPE } from '$lib/utils/types';
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
let isLoading = false;
let errors = {
title: '',
description: ''
};
let step = 0;
let userPrompt = '';
const options = [
{
title: 'Live Class',
subtitle:
'This course type is ideal for bootcamps where lessons are time based and you need attendance and grading',
type: COURSE_TYPE.LIVE_CLASS,
isDisabled: false
},
{
title: 'Self Paced',
subtitle:
'This course type is ideal for courses where students can take lessons on their own pace without a teacher',
type: COURSE_TYPE.SELF_PACED,
isDisabled: false
}
];
let type = options[0].type;
function onClose() {
goto($page.url.pathname);
createCourseModal.update(() => ({
open: false,
title: '',
description: '',
type: '',
emails: '',
tutors: '',
students: ''
@@ -53,15 +78,13 @@
const { id: group_id } = newGroup[0];
// Set userPrompt to the description
userPrompt = description;
// 2. Create course with group_id
const { data: newCourse } = await supabase
.from('course')
.insert({
title,
description,
type: type,
group_id
})
.select();
@@ -109,6 +132,8 @@
onClose();
isLoading = false;
}
$: open = new URLSearchParams($page.url.search).get('create') === 'true';
</script>
<svelte:head>
@@ -117,41 +142,103 @@
<Modal
{onClose}
bind:open={$createCourseModal.open}
width="w-4/5 md:w-2/5"
bind:open
width="w-4/5 md:w-2/5 md:min-w-[600px]"
modalHeading={$t('courses.new_course_modal.heading')}
>
<form on:submit|preventDefault={createCourse}>
<TextField
label={$t('courses.new_course_modal.course_name')}
bind:value={$createCourseModal.title}
placeholder={$t('courses.new_course_modal.course_name_placeholder')}
className="mb-4"
isRequired={true}
errorMessage={errors.title}
autoComplete={false}
/>
{#if step === 0}
<div>
<h2 class="text-xl font-medium my-5">
{$t('courses.new_course_modal.type_selector_title')}
</h2>
<TextArea
label={$t('courses.new_course_modal.short_description')}
bind:value={$createCourseModal.description}
rows={4}
placeholder={$t('courses.new_course_modal.short_description_placeholder')}
className="mb-4"
isRequired={true}
errorMessage={errors.description}
isAIEnabled={true}
initAIPrompt="Write a 30 word description for a course titled: {$createCourseModal.title}"
/>
<div class="flex flex-col md:flex-row gap-2 justify-between items-center my-8">
{#each options as option}
<button
class="w-11/12 md:w-[261px] md:h-[240px] p-5 rounded-md dark:bg-neutral-700 border-2 {option.type ===
type
? 'border-primary-400'
: `border-gray-200 dark:border-neutral-600 ${
!option.isDisabled && 'hover:scale-95'
}`} flex flex-col {option.isDisabled &&
'cursor-not-allowed opacity-60'} transition-all ease-in-out"
type="button"
on:click={!option.isDisabled ? () => (type = option.type) : undefined}
>
<div class="w-full flex flex-row-reverse h-[70%]">
{#if option.type === type}
<CheckmarkFilledIcon
size={16}
class="carbon-icon text-primary-600 dark:text-primary-200"
/>
{:else if !option.isDisabled}
<CheckmarkOutlineIcon size={16} class="carbon-icon" />
{/if}
</div>
<div class="mt-5 flex items-center">
<PrimaryButton
className="px-6 py-3"
label={$t('courses.new_course_modal.button')}
type="submit"
isDisabled={isLoading}
{isLoading}
/>
<div>
<p class="font-bold text-start flex items-center">
<span class="mr-2 text-sm">{option.title}</span>
{#if option.isDisabled}
<ComingSoon />
{/if}
</p>
<p class="text-xs font-light text-start">{option.subtitle}</p>
</div>
</button>
{/each}
</div>
<div class="mt-8 flex items-center flex-row-reverse">
<PrimaryButton
className="px-6 py-3"
label={$t('courses.new_course_modal.next')}
onClick={() => (step = 1)}
isDisabled={!type}
/>
</div>
</div>
</form>
{:else}
<form on:submit|preventDefault={createCourse}>
<div class="flex items-end space-x-2 mb-4">
<TextField
label={$t('courses.new_course_modal.course_name')}
bind:value={$createCourseModal.title}
placeholder={$t('courses.new_course_modal.course_name_placeholder')}
className="w-full "
isRequired={true}
errorMessage={errors.title}
autoComplete={false}
/>
</div>
<TextArea
label={$t('courses.new_course_modal.short_description')}
bind:value={$createCourseModal.description}
rows={4}
placeholder={$t('courses.new_course_modal.short_description_placeholder')}
className="mb-4"
isRequired={true}
errorMessage={errors.description}
isAIEnabled={true}
initAIPrompt="Write a 30 word description for a course titled: {$createCourseModal.title}"
/>
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-3"
label={$t('courses.new_course_modal.back')}
variant={VARIANTS.OUTLINED}
onClick={() => (step = 0)}
/>
<PrimaryButton
className="px-6 py-3"
label={$t('courses.new_course_modal.button')}
type="submit"
isDisabled={isLoading}
{isLoading}
/>
</div>
</form>
{/if}
</Modal>
@@ -48,6 +48,9 @@
<StructuredListCell head>
{$t('courses.course_card.list_view.description')}
</StructuredListCell>
<StructuredListCell head>
{$t('courses.course_card.list_view.type')}
</StructuredListCell>
<StructuredListCell head>
{$t('courses.course_card.list_view.lessons')}
</StructuredListCell>
@@ -65,6 +68,7 @@
<List
id={courseData.id}
title={courseData.title}
type={$t(`course.navItem.settings.${courseData.type.toLowerCase()}`)}
description={courseData.description}
isPublished={courseData.is_published}
totalLessons={courseData.total_lessons}
@@ -84,6 +88,7 @@
description={courseData.description}
isPublished={courseData.is_published}
cost={courseData.cost}
type={courseData.type}
currency={courseData.currency}
totalLessons={courseData.total_lessons}
totalStudents={courseData.total_students}
@@ -2,9 +2,14 @@ import { writable, derived } from 'svelte/store';
import type { Course } from '$lib/utils/types';
export const courses = writable<Course[]>([]);
export const view = writable('grid');
export const coursesInProgress = derived(courses, ($courses) =>
$courses.length > 0 ? $courses.slice(0, 3) : []
$courses.length > 0
? $courses.filter((course) => {
return course.total_lessons !== course.progress_rate;
})
: []
);
export const coursesComplete = derived(courses, ($courses) =>
$courses.length > 0
@@ -22,8 +27,8 @@ export const courseMetaDeta = writable<{
});
export const createCourseModal = writable({
open: false,
title: '',
type: '',
description: '',
emails: '',
tutors: '',