feat: course section reordering & migration to course v2 (#437)
* feat: reorder sections and lessons gracefully * convert course into version 2 * feat: get next and prev working
This commit is contained in:
+63
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { course } from '../../store';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
|
||||
export let open = false;
|
||||
|
||||
let isActivating = false;
|
||||
|
||||
const activate = async () => {
|
||||
isActivating = true;
|
||||
const { error } = await supabase.rpc('convert_course_to_v2', {
|
||||
course_id_arg: $course.id
|
||||
});
|
||||
|
||||
if (error) {
|
||||
snackbar.error('snackbar.something');
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
function handleClose() {
|
||||
open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
bind:open
|
||||
width="w-[80%] md:w-[65%]"
|
||||
maxWidth="max-w-xl"
|
||||
modalHeading={$t(`course.navItem.lessons.section_prompt.header`)}
|
||||
>
|
||||
<div class="flex flex-col w-full items-center">
|
||||
<div class="mb-8">
|
||||
<h3 class="text-center text-2xl">
|
||||
{$t('course.navItem.lessons.section_prompt.title')}
|
||||
</h3>
|
||||
<p class="text-center max-w-md">
|
||||
{$t('course.navItem.lessons.section_prompt.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('course.navItem.lessons.section_prompt.cancel')}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.section_prompt.activate')}
|
||||
onClick={activate}
|
||||
isLoading={isActivating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -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 @@
|
||||
<a
|
||||
href={$globalStore.isStudent && !lesson.is_unlocked
|
||||
? $page.url.pathname
|
||||
: '/courses/' + $course.id + '/lessons/' + lesson.id}
|
||||
: `/courses/${$course.id}/lessons/${lesson.id}`}
|
||||
class="font-medium text-black no-underline hover:underline dark:text-white {$globalStore.isStudent &&
|
||||
!lesson.is_unlocked
|
||||
? 'cursor-not-allowed'
|
||||
@@ -145,7 +145,6 @@
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<OverflowMenu size="xl">
|
||||
<OverflowMenuItem
|
||||
disabled={$globalStore.isStudent}
|
||||
text={lesson.is_unlocked
|
||||
? $t('course.navItem.lessons.add_lesson.lock')
|
||||
: $t('course.navItem.lessons.add_lesson.unlock')}
|
||||
|
||||
+242
-98
@@ -1,10 +1,17 @@
|
||||
<script lang="ts">
|
||||
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;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DeleteLessonConfirmation bind:openDeleteModal {deleteLesson} />
|
||||
|
||||
<section class="w-full p-3 lg:w-11/12 lg:px-4">
|
||||
<div class="max-w-xl mx-auto">
|
||||
{#each $lessonSections as section (section.id)}
|
||||
<div class="mb-3 border border-gray-200 rounded-md">
|
||||
<div
|
||||
class="mb-2 px-3 py-1 min-h-[50px] border-b bg-gray-50 rounded-tl-md rounded-tr-md flex justify-between items-center"
|
||||
>
|
||||
{#if lessonEditing === section.id}
|
||||
<TextField className="w-4/6" bind:value={section.title} errorMessage={errors?.title} />
|
||||
{:else}
|
||||
<p class="w-4/6 font-semibold">{section.title}</p>
|
||||
{/if}
|
||||
<section
|
||||
class="m-auto w-full p-3 lg:w-11/12 lg:px-4"
|
||||
use:dndzone={{
|
||||
items: $lessonSections,
|
||||
flipDurationMs,
|
||||
type: 'rows',
|
||||
dropTargetStyle: {
|
||||
border: '2px #1d4ed8 solid',
|
||||
'border-style': 'dashed'
|
||||
},
|
||||
dragDisabled: !reorder
|
||||
}}
|
||||
on:consider={handleDndConsiderColumns}
|
||||
on:finalize={handleDndFinalizeColumns}
|
||||
>
|
||||
{#each $lessonSections as section (section.id)}
|
||||
<div
|
||||
class="mb-3 m-auto max-w-xl border-2 {reorder
|
||||
? 'border-primary-400'
|
||||
: 'border-gray-200'} dark:border-neutral-600 dark:bg-neutral-800 rounded-md"
|
||||
>
|
||||
<div
|
||||
class="mb-2 px-3 py-1 min-h-[50px] border-b bg-gray-50 dark:bg-neutral-700 rounded-tl-md rounded-tr-md flex justify-between items-center"
|
||||
>
|
||||
{#if lessonEditing === section.id}
|
||||
<TextField className="w-4/6" bind:value={section.title} errorMessage={errors?.title} />
|
||||
{:else}
|
||||
<p class="w-4/6 font-semibold">{section.title}</p>
|
||||
{/if}
|
||||
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<div class="flex items-center">
|
||||
{#if lessonEditing === section.id}
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.cancel')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => {
|
||||
section.title = prevTitle ?? section.title;
|
||||
resetEdit();
|
||||
}}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.save')}
|
||||
onClick={() => onSave({ sectionId: section.id })}
|
||||
/>
|
||||
{:else}
|
||||
<IconButton onClick={() => handleAddLesson(section.id)} disabled={!!lessonEditing}>
|
||||
<Add size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => onEdit({ sectionId: section.id, sectionTitle: section.title })}
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<div class="flex items-center">
|
||||
{#if lessonEditing === section.id}
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.cancel')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => {
|
||||
section.title = prevTitle ?? section.title;
|
||||
resetEdit();
|
||||
}}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.save')}
|
||||
onClick={() => onSave({ sectionId: section.id })}
|
||||
/>
|
||||
{:else}
|
||||
<IconButton
|
||||
contained
|
||||
size="small"
|
||||
onClick={() => handleAddLesson(section.id)}
|
||||
disabled={!!lessonEditing}
|
||||
>
|
||||
<Add size={20} />
|
||||
</IconButton>
|
||||
<OverflowMenu size="xl" flipped>
|
||||
<OverflowMenuItem
|
||||
text={$t('course.navItem.lessons.add_lesson.edit')}
|
||||
disabled={!!lessonEditing}
|
||||
>
|
||||
<Edit size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => onDelete({ sectionId: section.id })}
|
||||
disabled={!!lessonEditing}
|
||||
>
|
||||
<TrashCan size={20} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</div>
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
|
||||
<div class="mx-3 py-1">
|
||||
{#each section.lessons as lesson (lesson.id)}
|
||||
<div
|
||||
class="border max-w-xl border-gray-200 px-3 py-1 min-h-[50px] rounded-md mb-2 flex items-center justify-between"
|
||||
>
|
||||
{#if lessonEditing === lesson.id}
|
||||
<TextField
|
||||
className="w-4/6"
|
||||
bind:value={lesson.title}
|
||||
errorMessage={errors?.title}
|
||||
on:click={() => onEdit({ sectionId: section.id, sectionTitle: section.title })}
|
||||
/>
|
||||
{:else}
|
||||
<p class="w-4/6 text-md">{lesson.title}</p>
|
||||
{/if}
|
||||
<OverflowMenuItem
|
||||
danger
|
||||
text={$t('course.navItem.lessons.add_lesson.delete')}
|
||||
on:click={() => onDelete({ sectionId: section.id })}
|
||||
/>
|
||||
</OverflowMenu>
|
||||
{/if}
|
||||
</div>
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<div class="flex items-center gap-1">
|
||||
{#if lessonEditing === lesson.id}
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.cancel')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => {
|
||||
lesson.title = prevTitle ?? lesson.title;
|
||||
resetEdit();
|
||||
<div
|
||||
class="mx-3 py-1"
|
||||
use:dndzone={{
|
||||
items: section.lessons,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {
|
||||
border: '2px #1d4ed8 solid',
|
||||
'border-style': 'dashed'
|
||||
},
|
||||
dragDisabled: !reorder
|
||||
}}
|
||||
on:consider={(e) => handleDndConsiderCards(section.id, e)}
|
||||
on:finalize={(e) => handleDndFinalizeCards(section.id, e)}
|
||||
>
|
||||
{#each section.lessons as lesson (lesson.id)}
|
||||
<div
|
||||
class="border max-w-xl {reorder
|
||||
? 'border-primary-400'
|
||||
: 'border-gray-200'} px-3 py-1 min-h-[50px] rounded-md mb-2 flex items-center justify-between"
|
||||
animate:flip={{ duration: flipDurationMs }}
|
||||
>
|
||||
{#if lessonEditing === lesson.id}
|
||||
<TextField className="w-4/6" bind:value={lesson.title} errorMessage={errors?.title} />
|
||||
{:else}
|
||||
<div class="w-4/5 flex items-center gap-2">
|
||||
<TextChip
|
||||
value={getLessonOrder(section.lessons, lesson.id)}
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
className="bg-primary-200 text-primary-600 text-xs"
|
||||
/>
|
||||
<div>
|
||||
<a
|
||||
href={$globalStore.isStudent && !lesson.is_unlocked
|
||||
? $page.url.pathname
|
||||
: `/courses/${$course.id}/lessons/${lesson.id}`}
|
||||
class=" text-black dark:text-white {$globalStore.isStudent &&
|
||||
!lesson.is_unlocked
|
||||
? 'cursor-not-allowed'
|
||||
: ''}"
|
||||
data-sveltekit-preload-data="off"
|
||||
>
|
||||
{lesson.title}
|
||||
</a>
|
||||
|
||||
<div class="mt-1 mb-3 flex items-center lg:mb-0">
|
||||
<ScreenMap size={16} class="carbon-icon dark:text-white" />
|
||||
<p class="ml-2 text-xs text-gray-500 dark:text-white">
|
||||
{lesson?.totalExercises ? lesson?.totalExercises?.map((c) => c.count) : 0}
|
||||
{$t('exercises.heading')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- IS EDITING -->
|
||||
{#if lessonEditing === lesson.id}
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.cancel')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => {
|
||||
lesson.title = prevTitle ?? lesson.title;
|
||||
resetEdit();
|
||||
}}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.save')}
|
||||
onClick={() => onSave({ lessonId: lesson.id }, lesson)}
|
||||
/>
|
||||
{:else}
|
||||
<OverflowMenu size="sm" flipped>
|
||||
<OverflowMenuItem
|
||||
text={lesson.is_unlocked
|
||||
? $t('course.navItem.lessons.add_lesson.lock')
|
||||
: $t('course.navItem.lessons.add_lesson.unlock')}
|
||||
on:click={() => {
|
||||
lesson.is_unlocked = !lesson.is_unlocked;
|
||||
handleSaveLesson(lesson, $course.id);
|
||||
}}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.save')}
|
||||
onClick={() => onSave({ lessonId: lesson.id }, lesson)}
|
||||
<OverflowMenuItem
|
||||
text={$t('course.navItem.lessons.add_lesson.edit')}
|
||||
on:click={() => onEdit({ lessonId: lesson.id, lessonTitle: lesson.title })}
|
||||
/>
|
||||
{:else}
|
||||
<IconButton
|
||||
onClick={() => onEdit({ lessonId: lesson.id, lessonTitle: lesson.title })}
|
||||
disabled={!!lessonEditing}
|
||||
>
|
||||
<Edit size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => onDelete({ lessonId: lesson.id })}
|
||||
disabled={!!lessonEditing}
|
||||
>
|
||||
<TrashCan size={20} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</div>
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
{:else}
|
||||
{$t('course.navItem.lessons.no_lesson')}
|
||||
{/each}
|
||||
</div>
|
||||
<OverflowMenuItem
|
||||
danger
|
||||
text={$t('course.navItem.lessons.add_lesson.delete')}
|
||||
on:click={() => onDelete({ lessonId: lesson.id })}
|
||||
/>
|
||||
</OverflowMenu>
|
||||
{/if}
|
||||
</div>
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
{:else}
|
||||
{$t('course.navItem.lessons.no_lesson')}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "अनुमति नहीं",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Нет разрешения",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
<NewLessonModal />
|
||||
|
||||
<ActivateSectionsModal bind:open={activateSections} />
|
||||
|
||||
<DeleteLessonConfirmation
|
||||
bind:openDeleteModal
|
||||
deleteLesson={() => handleDelete(lessonToDelete?.id)}
|
||||
/>
|
||||
|
||||
<CourseContainer bind:isFetching bind:courseId={data.courseId}>
|
||||
<PageNav title={$t('course.navItem.lessons.heading')}>
|
||||
<PageNav title={$t('course.navItem.lessons.heading_v2')}>
|
||||
<div slot="widget" class="flex w-full justify-end gap-2">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
{#if $course.version === COURSE_VERSION.V1}
|
||||
<PrimaryButton
|
||||
label={$t(`course.navItem.lessons.section_prompt.cta`)}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => (activateSections = !activateSections)}
|
||||
isDisabled={!!lessonEditing}
|
||||
/>
|
||||
{/if}
|
||||
<PrimaryButton
|
||||
label={$t(
|
||||
`course.navItem.lessons.add_lesson.${reorder ? 'end_reorder' : 'start_reorder'}`
|
||||
)}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => (reorder = !reorder)}
|
||||
isDisabled={!!lessonEditing}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.button_title')}
|
||||
onClick={addLesson}
|
||||
isDisabled={!!lessonEditing}
|
||||
/>
|
||||
{#if $course.version === COURSE_VERSION.V1}
|
||||
<PrimaryButton
|
||||
label={$t(
|
||||
`course.navItem.lessons.add_lesson.${reorder ? 'end_reorder' : 'start_reorder'}`
|
||||
)}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => (reorder = !reorder)}
|
||||
isDisabled={!!lessonEditing}
|
||||
/>
|
||||
{/if}
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
</PageNav>
|
||||
@@ -119,7 +143,7 @@
|
||||
{#if $course.version === COURSE_VERSION.V1}
|
||||
<LessonList {reorder} {lessonEditing} bind:lessonToDelete bind:openDeleteModal />
|
||||
{:else if $course.version === COURSE_VERSION.V2}
|
||||
<LessonSectionList {lessonEditing} />
|
||||
<LessonSectionList {reorder} {lessonEditing} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Box className="w-full lg:w-11/12 lg:px-4 m-auto">
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)));
|
||||
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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user