diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/ActivateSectionsModal.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/ActivateSectionsModal.svelte new file mode 100644 index 00000000..46f5ecbf --- /dev/null +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/ActivateSectionsModal.svelte @@ -0,0 +1,63 @@ + + + +
+
+

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

+

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

+
+ +
+ + +
+
+
diff --git a/apps/dashboard/src/lib/components/Course/components/Lesson/LessonList.svelte b/apps/dashboard/src/lib/components/Course/components/Lesson/LessonList.svelte index 9c77c7f9..00c4635e 100644 --- a/apps/dashboard/src/lib/components/Course/components/Lesson/LessonList.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Lesson/LessonList.svelte @@ -75,7 +75,7 @@ use:dndzone={{ items: $lessons, flipDurationMs, - dragDisabled: $globalStore.isStudent || !reorder, + dragDisabled: !reorder, dropTargetStyle: { border: '2px #1d4ed8 solid', 'border-style': 'dashed' @@ -115,7 +115,7 @@ - import { Edit, TrashCan, Add } from 'carbon-icons-svelte'; + import { page } from '$app/stores'; + import { flip } from 'svelte/animate'; + import { dndzone } from 'svelte-dnd-action'; + import { Add, ScreenMap } from 'carbon-icons-svelte'; + import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte'; import TextField from '$lib/components/Form/TextField.svelte'; + import TextChip from '$lib/components/Chip/Text.svelte'; import IconButton from '$lib/components/IconButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { t } from '$lib/utils/functions/translations'; + import { globalStore } from '$lib/utils/store/app'; + import { course } from '$lib/components/Course/store'; import { handleAddLessonWidget } from '$lib/components/Course/components/Lesson/store'; import { snackbar } from '$lib/components/Snackbar/store'; import { @@ -14,6 +21,7 @@ handleSaveLessonSection, handleDeleteSection } from '$lib/components/Course/components/Lesson/store/lessons'; + import { updateLesson, updateLessonSection } from '$lib/utils/services/courses'; import type { Lesson } from '$lib/utils/types'; import DeleteLessonConfirmation from '$lib/components/Course/components/Lesson/DeleteLessonConfirmation.svelte'; import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte'; @@ -25,7 +33,7 @@ lessonTitle?: string; }; - // export let reorder = false; + export let reorder = false; export let lessonEditing: string | undefined; let prevTitle: string | undefined; @@ -67,7 +75,7 @@ if (!errors?.title) { resetEdit(); - } else { + snackbar.success('snackbar.success_update'); } } @@ -99,111 +107,247 @@ id })); } + + /** + * Drag functionality + */ + const flipDurationMs = 200; + + function handleDndConsiderColumns(e) { + $lessonSections = e.detail.items; + } + function handleDndFinalizeColumns(e) { + const prevSectionsOrder = $lessonSections.reduce((acc, l) => ({ ...acc, [l.id]: l.order }), {}); + + $lessonSections = e.detail.items; + + e.detail.items.forEach((section, index) => { + const order = index + 1; + + if (order !== prevSectionsOrder[section.id]) { + $lessonSections[index].order = order; + updateLessonSection({ order }, section.id).then((update) => + console.log(`updated section order`, update) + ); + } + }); + } + function handleDndConsiderCards(cid, e) { + const colIdx = $lessonSections.findIndex((c) => c.id === cid); + $lessonSections[colIdx].lessons = e.detail.items; + $lessonSections = [...$lessonSections]; + } + function handleDndFinalizeCards(cid, e) { + const colIdx = $lessonSections.findIndex((c) => c.id === cid); + + const section = $lessonSections[colIdx]; + const prevLessonOrder = section.lessons.reduce((acc, l) => ({ ...acc, [l.id]: l.order }), {}); + + $lessonSections[colIdx].lessons = e.detail.items; + $lessonSections = [...$lessonSections]; + + e.detail.items.forEach((lesson: Lesson, index: number) => { + const order = index + 1; + + if (order !== prevLessonOrder[lesson.id] || lesson.section_id !== section.id) { + section.lessons[index].order = order; + + const lessonUpdate = { + order, + section_id: $lessonSections[colIdx].id + }; + updateLesson(lessonUpdate, lesson.id).then((update) => + console.log(`updated lesson order`, update) + ); + } + }); + } + + function getLessonOrder(lessons, id) { + const index = lessons.findIndex((lesson) => lesson.id === id); + + if (index < 9) { + return '0' + (index + 1); + } else { + return index + 1; + } + } -
-
- {#each $lessonSections as section (section.id)} -
-
- {#if lessonEditing === section.id} - - {:else} -

{section.title}

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

{lesson.title}

- {/if} + onDelete({ sectionId: section.id })} + /> + + {/if} +
+ +
- -
+ +
+ {:else} + {$t('course.navItem.lessons.no_lesson')} + {/each}
- {/each} -
+
+ {/each}
diff --git a/apps/dashboard/src/lib/components/Course/components/Navigation/index.svelte b/apps/dashboard/src/lib/components/Course/components/Navigation/index.svelte index f684f0f3..82a2bf84 100644 --- a/apps/dashboard/src/lib/components/Course/components/Navigation/index.svelte +++ b/apps/dashboard/src/lib/components/Course/components/Navigation/index.svelte @@ -160,7 +160,7 @@ }, { id: NAV_IDS.LESSONS, - label: $t('course.navItems.nav_lessons'), + label: $t('course.navItems.nav_content'), to: getLessonsRoute($course.id), hideSortIcon: false, isPaidFeature: false, diff --git a/apps/dashboard/src/lib/components/Course/store.ts b/apps/dashboard/src/lib/components/Course/store.ts index a40d0510..6c5a753a 100644 --- a/apps/dashboard/src/lib/components/Course/store.ts +++ b/apps/dashboard/src/lib/components/Course/store.ts @@ -3,7 +3,7 @@ import type { Writable } from 'svelte/store'; import { lessons, lessonSections } from './components/Lesson/store/lessons'; import { ROLE } from '$lib/utils/constants/roles'; import type { Course, GroupPerson, Lesson, LessonSection } from '$lib/utils/types'; -import { COURSE_TYPE } from '$lib/utils/types'; +import { COURSE_TYPE, COURSE_VERSION } from '$lib/utils/types'; export const defaultCourse: Course = { id: '', @@ -20,6 +20,7 @@ export const defaultCourse: Course = { updated_at: new Date().toDateString(), attendance: [], polls: [], + version: COURSE_VERSION.V2, metadata: { requirements: '', description: '', @@ -120,7 +121,6 @@ export async function setCourse(data: Course, setLesson = true) { lessons.set(orderedLessons); if (data.lesson_section) { - console.log('Sections', data.lesson_section); const sections = data.lesson_section?.map((section) => { const lessons = (data.lessons || []).filter((lesson) => lesson.section_id === section.id); section.lessons = sortLesson(lessons); diff --git a/apps/dashboard/src/lib/utils/services/courses/index.ts b/apps/dashboard/src/lib/utils/services/courses/index.ts index bbb40ec6..261f924b 100644 --- a/apps/dashboard/src/lib/utils/services/courses/index.ts +++ b/apps/dashboard/src/lib/utils/services/courses/index.ts @@ -287,7 +287,7 @@ export function updateLessonSection(section: any, sectionId: LessonSection['id'] .match({ id: sectionId }); } -export function updateLesson(lesson: any, lessonId: Lesson['id']) { +export async function updateLesson(lesson: any, lessonId: Lesson['id']) { return supabase .from('lesson') .update({ ...lesson, id: undefined }) @@ -307,6 +307,14 @@ export function updateLessonCompletion(completion: LessonCompletion, shouldUpdat } } +export const upsertLessons = (data: { id: string; order: number }[]) => { + return supabase.from('lesson').upsert(data); +}; + +export const upsertLessonSections = (data: { id: string; order: number }[]) => { + return supabase.from('lesson_section').upsert(data); +}; + export function deleteLesson(lessonId: Lesson['id']) { // Need to implement soft delete return supabase.from('lesson').delete().match({ id: lessonId }); diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json index a494893d..1001762d 100644 --- a/apps/dashboard/src/lib/utils/translations/de.json +++ b/apps/dashboard/src/lib/utils/translations/de.json @@ -801,14 +801,14 @@ }, "navItems": { "nav_news_feed": "Neuigkeiten", - "nav_lessons": "Unterricht", "nav_attendance": "Teilnahme", "nav_submissions": "Einsendungen", "nav_marks": "Markierungen", "nav_people": "Menschen", "nav_certificates": "Zertifikate", "nav_landing_page": "Landingpage", - "nav_settings": "Einstellungen" + "nav_settings": "Einstellungen", + "nav_content": "Inhalt" }, "not_permitted": { "header": "Keine Erlaubnis", diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json index cfe89fef..fdfb259d 100644 --- a/apps/dashboard/src/lib/utils/translations/en.json +++ b/apps/dashboard/src/lib/utils/translations/en.json @@ -464,7 +464,7 @@ "course": { "navItems": { "nav_news_feed": "News Feed", - "nav_lessons": "Lessons", + "nav_content": "Content", "nav_attendance": "Attendance", "nav_submissions": "Submissions", "nav_marks": "Marks", @@ -504,9 +504,18 @@ "lessons": { "heading": "Lessons", + "heading_v2": "Content", "drag": "drag a card to reorder", "no_lesson": "No lessons yet", "share_your_knowledge": "Share your knowledge with the world by creating engaging lessons. Start by clicking on the Add button.", + "section_prompt": { + "cta": "Enable Sections", + "header": "Enable Sections", + "title": "We Launched Course Sections", + "description": "You can now group lessons into sections to organize your content and make learning smoother for your students.", + "activate": "Activate", + "cancel": "Cancel" + }, "add_lesson": { "button_title": "Add", "start_reorder": "Reorder", diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json index bd1f3459..9c703bf8 100644 --- a/apps/dashboard/src/lib/utils/translations/es.json +++ b/apps/dashboard/src/lib/utils/translations/es.json @@ -801,14 +801,14 @@ }, "navItems": { "nav_news_feed": "Noticias", - "nav_lessons": "Lecciones", "nav_attendance": "Asistencia", "nav_submissions": "Envíos", "nav_marks": "Marcas", "nav_people": "Gente", "nav_certificates": "Certificados", "nav_landing_page": "Página de destino", - "nav_settings": "Ajustes" + "nav_settings": "Ajustes", + "nav_content": "Contenido" }, "not_permitted": { "header": "Sin autorización", diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json index 8e162667..889d5478 100644 --- a/apps/dashboard/src/lib/utils/translations/fr.json +++ b/apps/dashboard/src/lib/utils/translations/fr.json @@ -801,14 +801,14 @@ }, "navItems": { "nav_news_feed": "Fil d'actualité", - "nav_lessons": "Cours", "nav_attendance": "Présence", "nav_submissions": "Soumissions", "nav_marks": "Des marques", "nav_people": "Personnes", "nav_certificates": "Certificats", "nav_landing_page": "Page de destination", - "nav_settings": "Paramètres" + "nav_settings": "Paramètres", + "nav_content": "Contenu" }, "not_permitted": { "header": "Aucune autorisation", diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json index 81c07054..1a5fbbee 100644 --- a/apps/dashboard/src/lib/utils/translations/hi.json +++ b/apps/dashboard/src/lib/utils/translations/hi.json @@ -801,14 +801,14 @@ }, "navItems": { "nav_news_feed": "समाचार फ़ीड", - "nav_lessons": "पाठ", "nav_attendance": "उपस्थिति", "nav_submissions": "प्रविष्टियों", "nav_marks": "निशान", "nav_people": "लोग", "nav_certificates": "प्रमाण पत्र", "nav_landing_page": "लैंडिंग पृष्ठ", - "nav_settings": "समायोजन" + "nav_settings": "समायोजन", + "nav_content": "सामग्री" }, "not_permitted": { "header": "अनुमति नहीं", diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json index 855bfb24..9a57797f 100644 --- a/apps/dashboard/src/lib/utils/translations/pt.json +++ b/apps/dashboard/src/lib/utils/translations/pt.json @@ -801,14 +801,14 @@ }, "navItems": { "nav_news_feed": "Notícias", - "nav_lessons": "Lições", "nav_attendance": "Comparecimento", "nav_submissions": "Envios", "nav_marks": "Marcas", "nav_people": "Pessoas", "nav_certificates": "Certificados", "nav_landing_page": "Página inicial", - "nav_settings": "Configurações" + "nav_settings": "Configurações", + "nav_content": "Contente" }, "not_permitted": { "header": "Sem permissão", diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json index 8b2b5315..d66897fb 100644 --- a/apps/dashboard/src/lib/utils/translations/ru.json +++ b/apps/dashboard/src/lib/utils/translations/ru.json @@ -801,14 +801,14 @@ }, "navItems": { "nav_news_feed": "Новостная лента", - "nav_lessons": "Уроки", "nav_attendance": "посещаемость", "nav_submissions": "Материалы", "nav_marks": "Метки", "nav_people": "Люди", "nav_certificates": "Сертификаты", "nav_landing_page": "Целевая страница", - "nav_settings": "Настройки" + "nav_settings": "Настройки", + "nav_content": "Содержание" }, "not_permitted": { "header": "Нет разрешения", diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json index ee9a379b..32776e22 100644 --- a/apps/dashboard/src/lib/utils/translations/vi.json +++ b/apps/dashboard/src/lib/utils/translations/vi.json @@ -801,14 +801,14 @@ }, "navItems": { "nav_news_feed": "Nguồn cấp tin tức", - "nav_lessons": "Những bài học", "nav_attendance": "Sự tham dự", "nav_submissions": "bài nộp", "nav_marks": "Điểm", "nav_people": "Mọi người", "nav_certificates": "Chứng chỉ", "nav_landing_page": "Trang đích", - "nav_settings": "Cài đặt" + "nav_settings": "Cài đặt", + "nav_content": "Nội dung" }, "not_permitted": { "header": "Không cho phép", diff --git a/apps/dashboard/src/routes/courses/[id]/lessons/+page.svelte b/apps/dashboard/src/routes/courses/[id]/lessons/+page.svelte index 6940b9ac..8304b7a7 100644 --- a/apps/dashboard/src/routes/courses/[id]/lessons/+page.svelte +++ b/apps/dashboard/src/routes/courses/[id]/lessons/+page.svelte @@ -4,6 +4,7 @@ import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte'; import Box from '$lib/components/Box/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; + import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import CourseContainer from '$lib/components/CourseContainer/index.svelte'; import { lessons, @@ -18,11 +19,11 @@ import { t } from '$lib/utils/functions/translations'; import { goto } from '$app/navigation'; import { COURSE_VERSION } from '$lib/utils/types'; - import { VARIANTS } from '$lib/components/PrimaryButton/constants.js'; - import { profile } from '$lib/utils/store/user.js'; + import { profile } from '$lib/utils/store/user'; import LessonList from '$lib/components/Course/components/Lesson/LessonList.svelte'; import LessonSectionList from '$lib/components/Course/components/Lesson/LessonSectionList.svelte'; import NewLessonModal from '$lib/components/Course/components/Lesson/NewLessonModal.svelte'; + import ActivateSectionsModal from '$lib/components/Course/components/Lesson/ActivateSectionsModal.svelte'; export let data; @@ -33,6 +34,7 @@ let openDeleteModal: boolean = false; let isFetching: boolean = false; let reorder = false; + let activateSections = false; function addLesson() { $handleAddLessonWidget.open = true; @@ -44,8 +46,22 @@ return completion?.find((c) => c.profile_id === $profile.id); } + const getLessons = () => { + if ($course.version === COURSE_VERSION.V1) { + return $lessons; + } else { + const _lessons: Lesson[] = []; + + $lessonSections.forEach((section) => { + _lessons.push(...section.lessons); + }); + + return _lessons; + } + }; + function findFirstIncompleteLesson() { - return $lessons.find( + return getLessons().find( (lesson) => !hasUserCompletedLesson(lesson.lesson_completion) && lesson.is_unlocked === true ); } @@ -70,30 +86,38 @@ + + handleDelete(lessonToDelete?.id)} /> - +
+ {#if $course.version === COURSE_VERSION.V1} + (activateSections = !activateSections)} + isDisabled={!!lessonEditing} + /> + {/if} + (reorder = !reorder)} + isDisabled={!!lessonEditing} + /> - {#if $course.version === COURSE_VERSION.V1} - (reorder = !reorder)} - isDisabled={!!lessonEditing} - /> - {/if}
@@ -119,7 +143,7 @@ {#if $course.version === COURSE_VERSION.V1} {:else if $course.version === COURSE_VERSION.V2} - + {/if} {:else} diff --git a/apps/dashboard/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte b/apps/dashboard/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte index bb26a056..8e20c98a 100644 --- a/apps/dashboard/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte +++ b/apps/dashboard/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte @@ -31,12 +31,13 @@ import { lesson, lessons, - lessonByTranslation + lessonByTranslation, + lessonSections } from '$lib/components/Course/components/Lesson/store/lessons'; import { browser } from '$app/environment'; import { currentOrg } from '$lib/utils/store/org'; import { snackbar } from '$lib/components/Snackbar/store'; - import type { LessonCompletion } from '$lib/utils/types'; + import { COURSE_VERSION, type LessonCompletion, type Lesson } from '$lib/utils/types'; import { profile } from '$lib/utils/store/user'; import { getIsLessonComplete } from '$lib/components/Course/components/Lesson/functions'; import { t } from '$lib/utils/functions/translations'; @@ -249,20 +250,36 @@ $apps.open = true; } - const isNextOrPrevDisabled = (lessonId: string, isPrev: boolean) => { - const index = $lessons.findIndex((lesson) => lesson.id === lessonId); + const getLessons = () => { + if ($course.version === COURSE_VERSION.V1) { + return $lessons; + } else { + const _lessons: Lesson[] = []; - return isPrev ? !$lessons[index - 1] : !$lessons[index + 1]; + $lessonSections.forEach((section) => { + _lessons.push(...section.lessons); + }); + + return _lessons; + } + }; + + const isNextOrPrevDisabled = (lessonId: string, isPrev: boolean) => { + const _lessons = getLessons(); + const index = _lessons.findIndex((lesson) => lesson.id === lessonId); + + return isPrev ? !_lessons[index - 1] : !_lessons[index + 1]; }; const goToNextOrPrevLesson = (lessonId: string, isPrev: boolean) => { + const _lessons = getLessons(); const isDisabled = isNextOrPrevDisabled(lessonId, isPrev); // Always use early return if (isDisabled) return; - const index = $lessons.findIndex((lesson) => lesson.id === lessonId); - const nextOrPrevLesson = isPrev ? $lessons[index - 1] : $lessons[index + 1]; + const index = _lessons.findIndex((lesson) => lesson.id === lessonId); + const nextOrPrevLesson = isPrev ? _lessons[index - 1] : _lessons[index + 1]; const isLocked = $globalStore.isStudent && !nextOrPrevLesson.is_unlocked; diff --git a/supabase/migrations/20240811123634_lesson_section.sql b/supabase/migrations/20240811123634_lesson_section.sql index c988ba84..f601fd14 100644 --- a/supabase/migrations/20240811123634_lesson_section.sql +++ b/supabase/migrations/20240811123634_lesson_section.sql @@ -124,4 +124,22 @@ using (is_user_in_group_with_role(( SELECT course.group_id with check (is_user_in_group_with_role(( SELECT course.group_id FROM course WHERE (course.id = lesson_section.course_id) - LIMIT 1))); \ No newline at end of file + LIMIT 1))); + +CREATE +OR REPLACE FUNCTION convert_course_to_v2 (course_id uuid) RETURNS void AS $$ +DECLARE + new_section_id bigint; +BEGIN + UPDATE course + SET version = 'V2' + WHERE id = course_id; + + INSERT INTO lesson_section (title, course_id) VALUES ('First Section [edit me]', course_id) RETURNING id INTO new_section_id; + + UPDATE lesson + SET section_id = new_section_id + WHERE course_id = course_id; +END; +$$ LANGUAGE plpgsql; +