From ad421acf8dffdc96fe9ea2555655558c5fd8222d Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Wed, 14 Aug 2024 10:27:37 +0100 Subject: [PATCH] feat: lesson sections - v1 (#431) * add todo * update sidebar with sections * add course versioning * fix db reset issue * v1 of working sections feature * remove --- .../Lesson/DeleteLessonConfirmation.svelte | 4 +- .../components/Lesson/LessonList.svelte | 192 +++++++++++ .../Lesson/LessonSectionList.svelte | 209 ++++++++++++ .../components/Lesson/Materials/index.svelte | 2 +- .../components/Lesson/NewLessonModal.svelte | 101 +++++- .../Course/components/Lesson/store.ts | 7 + .../Course/components/Lesson/store/lessons.ts | 102 +++++- .../Navigation/NavExpandable.svelte | 48 ++- .../components/Navigation/NavIcons.svelte | 25 +- .../Course/components/Navigation/constants.ts | 12 + .../Course/components/Navigation/index.svelte | 88 ++++- .../Course/components/Navigation/store.js | 5 - .../src/lib/components/Course/function.ts | 4 +- .../src/lib/components/Course/store.ts | 36 +- .../components/NewCourseModal/index.svelte | 3 +- .../functions/{validator.js => validator.ts} | 7 +- .../src/lib/utils/services/courses/index.ts | 24 +- .../src/lib/utils/translations/de.json | 5 +- .../src/lib/utils/translations/en.json | 5 +- .../src/lib/utils/translations/es.json | 5 +- .../src/lib/utils/translations/fr.json | 5 +- .../src/lib/utils/translations/hi.json | 5 +- .../src/lib/utils/translations/pt.json | 5 +- .../src/lib/utils/translations/ru.json | 5 +- .../src/lib/utils/translations/vi.json | 5 +- apps/dashboard/src/lib/utils/types/index.ts | 18 + .../routes/courses/[id]/lessons/+page.svelte | 313 ++---------------- .../lessons/[...lessonParams]/+page.svelte | 3 +- .../20240811123634_lesson_section.sql | 127 +++++++ supabase/seed.sql | 2 +- 30 files changed, 979 insertions(+), 393 deletions(-) create mode 100644 apps/dashboard/src/lib/components/Course/components/Lesson/LessonList.svelte create mode 100644 apps/dashboard/src/lib/components/Course/components/Lesson/LessonSectionList.svelte create mode 100644 apps/dashboard/src/lib/components/Course/components/Lesson/store.ts create mode 100644 apps/dashboard/src/lib/components/Course/components/Navigation/constants.ts delete mode 100644 apps/dashboard/src/lib/components/Course/components/Navigation/store.js rename apps/dashboard/src/lib/utils/functions/{validator.js => validator.ts} (98%) create mode 100644 supabase/migrations/20240811123634_lesson_section.sql diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/DeleteLessonConfirmation.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/DeleteLessonConfirmation.svelte index 5143784d..f7e63473 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/DeleteLessonConfirmation.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/DeleteLessonConfirmation.svelte @@ -4,7 +4,6 @@ import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { t } from '$lib/utils/functions/translations'; - export let lessonTitle: string; export let openDeleteModal = false; export let deleteLesson = () => {}; @@ -18,11 +17,12 @@ onClose={() => (openDeleteModal = false)} bind:open={openDeleteModal} width="w-96" + containerClass="px-6 pt-2 pb-6" modalHeading={$t('delete_modal.label')} >

- {$t('delete_modal.content')} {lessonTitle}? + {$t('delete_modal.content')}?

diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/LessonList.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/LessonList.svelte new file mode 100644 index 00000000..9c77c7f9 --- /dev/null +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/LessonList.svelte @@ -0,0 +1,192 @@ + + +
+ {#each $lessons as lesson (lesson.id)} +
+ +
+ +
+ + +
+ + {#if lessonEditing === lesson.id} + + {:else} +

+ + {lesson.title} + +

+ {/if} + +
+ +
+ +

+ {lesson?.totalExercises ? lesson?.totalExercises?.map((c) => c.count) : 0} + {$t('exercises.heading')} +

+
+
+
+ + +
+ + + { + lesson.is_unlocked = !lesson.is_unlocked; + handleSaveLesson(lesson, $course.id); + }} + /> + { + if (lessonEditing === lesson.id) { + saveLesson(lesson, $course.id); + } else { + lessonEditing = lesson.id; + } + }} + /> + { + lessonToDelete = lesson; + openDeleteModal = true; + }} + /> + + +
+
+ {:else} + +
+ Lesson +

{$t('course.navItem.lessons.body_header')}

+

+ {$t('course.navItem.lessons.body_content')} +

+
+
+ {/each} +
diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/LessonSectionList.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/LessonSectionList.svelte new file mode 100644 index 00000000..e1650c11 --- /dev/null +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/LessonSectionList.svelte @@ -0,0 +1,209 @@ + + + + +
+
+ {#each $lessonSections as section (section.id)} +
+
+ {#if lessonEditing === section.id} + + {:else} +

{section.title}

+ {/if} + + +
+ {#if lessonEditing === section.id} + { + section.title = prevTitle ?? section.title; + resetEdit(); + }} + /> + onSave({ sectionId: section.id })} + /> + {:else} + handleAddLesson(section.id)} disabled={!!lessonEditing}> + + + onEdit({ sectionId: section.id, sectionTitle: section.title })} + disabled={!!lessonEditing} + > + + + onDelete({ sectionId: section.id })} + disabled={!!lessonEditing} + > + + + {/if} +
+
+
+ +
+ {#each section.lessons as lesson (lesson.id)} +
+ {#if lessonEditing === lesson.id} + + {:else} +

{lesson.title}

+ {/if} + + +
+ {#if lessonEditing === lesson.id} + { + lesson.title = prevTitle ?? lesson.title; + resetEdit(); + }} + /> + onSave({ lessonId: lesson.id }, lesson)} + /> + {:else} + onEdit({ lessonId: lesson.id, lessonTitle: lesson.title })} + disabled={!!lessonEditing} + > + + + onDelete({ lessonId: lesson.id })} + disabled={!!lessonEditing} + > + + + {/if} +
+
+
+ {:else} + {$t('course.navItem.lessons.no_lesson')} + {/each} +
+
+ {/each} +
+
diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/index.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/index.svelte index a2199378..0920ea3b 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/index.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/Materials/index.svelte @@ -300,7 +300,7 @@ $: initPlyr(player, $lesson.materials.videos); - $: lessonTitle = $lessons.find((les) => les.id === $lesson.id)?.title || ''; + $: lessonTitle = $lesson.title; $: editorValue = lessonFallbackNote( $lesson.materials.note, diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/NewLessonModal.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/NewLessonModal.svelte index 70174e75..a0e8cf3b 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/NewLessonModal.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/NewLessonModal.svelte @@ -1,15 +1,17 @@ ($handleAddLessonWidget.open = false)} + onClose={handleClose} bind:open={$handleAddLessonWidget.open} width="w-[80%] md:w-[65%]" maxWidth="max-w-2xl" containerClass="overflow-hidden" - modalHeading={$t('course.navItem.lessons.add_lesson.modal_heading')} + modalHeading={$t( + `course.navItem.lessons.add_lesson.${ + $handleAddLessonWidget.isSection ? 'modal_heading_section' : 'modal_heading' + }` + )} >
{#if $course.type == COURSE_TYPE.LIVE_CLASS} -
@@ -91,7 +157,7 @@
-
+
--> {/if}
@@ -100,4 +166,3 @@
- diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/store.ts b/apps/dashboard/src/lib/components/Course/components/Lesson/store.ts new file mode 100644 index 00000000..6ca2189e --- /dev/null +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/store.ts @@ -0,0 +1,7 @@ +import { writable } from 'svelte/store'; + +export const handleAddLessonWidget = writable({ + id: '', + open: false, + isSection: false +}); diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/store/lessons.ts b/apps/dashboard/src/lib/components/Course/components/Lesson/store/lessons.ts index 0498aebe..fa0862bb 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/store/lessons.ts +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/store/lessons.ts @@ -1,7 +1,14 @@ import { writable } from 'svelte/store'; import type { Writable, Updater } from 'svelte/store'; -import { createLesson, updateLesson, deleteLesson } from '$lib/utils/services/courses'; -import type { Lesson, Course, LessonPage, LessonComment } from '$lib/utils/types'; +import { + createLesson, + updateLesson, + deleteLesson, + createLessonSection, + updateLessonSection, + deleteLessonSection +} from '$lib/utils/services/courses'; +import type { Lesson, Course, LessonPage, LessonComment, LessonSection } from '$lib/utils/types'; import { LOCALE } from '$lib/utils/types'; import { snackbar } from '$lib/components/Snackbar/store'; import { lessonValidation } from '$lib/utils/functions/validator'; @@ -12,9 +19,12 @@ export const uploadCourseVideoStore = writable({ export const lessons: Writable = writable([]); +export const lessonSections: Writable = writable([]); + export const lesson = writable({ id: null, locale: LOCALE.EN, + title: '', totalExercises: 0, totalComments: 0, isSaving: false, @@ -54,23 +64,50 @@ export function handleAddLesson() { export async function handleDelete(lessonId: Lesson['id'] | undefined) { // Need to implement soft delete - if (lessonId) { - const { error } = await deleteLesson(lessonId); - - if (error) { - snackbar.error(error.message); - return console.error('Error deleting course', error); - } - - lessons.update((_lessons) => _lessons.filter((lesson) => lesson.id !== lessonId)); - - snackbar.success('snackbar.generic.success_delete'); + if (!lessonId) { + return; } + const { error } = await deleteLesson(lessonId); + + if (error) { + snackbar.error(error.message); + return console.error('Error deleting course', error); + } + + lessons.update((_lessons) => _lessons.filter((lesson) => lesson.id !== lessonId)); + lessonSections.update((_sections) => + _sections.map((section) => { + section.lessons = section.lessons.filter((lesson) => lesson.id !== lessonId); + return section; + }) + ); + + snackbar.success('snackbar.generic.success_delete'); console.log(`lessonId`, lessonId); } -export async function handleSaveLesson(lesson: Lesson, course_id: Course['id']) { +export async function handleDeleteSection(sectionId: LessonSection['id'] | undefined) { + // Need to implement soft delete + if (!sectionId) { + return; + } + const { error } = await deleteLessonSection(sectionId); + + if (error) { + snackbar.error(error.message); + return console.error('Error deleting course', error); + } + + lessonSections.update((_sections) => _sections.filter((section) => section.id !== sectionId)); + lessons.update((_lessons) => _lessons.filter((lesson) => lesson.section_id !== sectionId)); + + snackbar.success('snackbar.generic.success_delete'); + + console.log(`sectionId`, sectionId); +} + +export async function handleSaveLesson(lesson: Lesson, courseId: Course['id']) { const result = lessonValidation(lesson); if (Object.keys(result).length) { @@ -83,13 +120,14 @@ export async function handleSaveLesson(lesson: Lesson, course_id: Course['id']) lesson_at: lesson?.lesson_at, call_url: lesson?.call_url, teacher_id: lesson?.profile ? lesson?.profile.id : undefined, - course_id, - is_unlocked: lesson.is_unlocked + course_id: courseId, + is_unlocked: lesson.is_unlocked, + section_id: lesson.section_id }; let newLessonData: any[] | null = null; - if (!!lesson.id) { + if (lesson.id) { // No need to get the result of update cause we have all in local state await updateLesson(newLesson, lesson.id); } else { @@ -99,6 +137,36 @@ export async function handleSaveLesson(lesson: Lesson, course_id: Course['id']) } return newLessonData; } +export async function handleSaveLessonSection( + section: Partial, + courseId: Course['id'] +) { + const result = lessonValidation(section); + + if (Object.keys(result).length) { + return result; + } + console.log(`handleSaveLessonSection lesson`, section); + + const newSection: Partial = { + id: section.id, + title: section.title, + course_id: courseId + }; + + let newSectionData: any[] | null = null; + + if (newSection.id) { + // No need to get the result of update cause we have all in local state + await updateLessonSection(newSection, newSection.id); + } else { + const { data } = await createLessonSection(newSection); + + newSectionData = data; + } + + return newSectionData; +} export async function handleUpdateLessonMaterials(lesson: any, lessonId: Lesson['id']) { const materials = { diff --git a/apps/dashboard/src/lib/components/Course/components/Navigation/NavExpandable.svelte b/apps/dashboard/src/lib/components/Course/components/Navigation/NavExpandable.svelte index d46540bb..1899a5d4 100644 --- a/apps/dashboard/src/lib/components/Course/components/Navigation/NavExpandable.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Navigation/NavExpandable.svelte @@ -1,32 +1,46 @@ -
+
@@ -173,210 +108,18 @@

- {:else if $lessons.length} + {:else if lessonsLength > 0} {#if reorder}

{$t('course.navItem.lessons.drag')}

{/if} -
- {#each $lessons as lesson (lesson.id)} -
- -
- -
- -
- - {#if lessonEditing === lesson.id} - - {:else} -

- - {lesson.title} - -

- {/if} - - - {#if isLiveClass} -
-
- {#if lessonEditing === lesson.id} -