feat: lesson sections - v1 (#431)
* add todo * update sidebar with sections * add course versioning * fix db reset issue * v1 of working sections feature * remove
This commit is contained in:
+2
-2
@@ -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')}
|
||||
>
|
||||
<div>
|
||||
<h1 class="dark:text-white text-lg">
|
||||
{$t('delete_modal.content')} <strong>{lessonTitle}</strong>?
|
||||
{$t('delete_modal.content')}?
|
||||
</h1>
|
||||
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { dndzone } from 'svelte-dnd-action';
|
||||
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import ScreenMap from 'carbon-icons-svelte/lib/ScreenMap.svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import {
|
||||
lessons,
|
||||
handleSaveLesson
|
||||
} from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { updateLesson } from '$lib/utils/services/courses';
|
||||
import type { Course, Lesson } from '$lib/utils/types';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
|
||||
const flipDurationMs = 300;
|
||||
|
||||
export let reorder = false;
|
||||
export let lessonEditing: string | undefined;
|
||||
export let lessonToDelete: Lesson | undefined;
|
||||
export let openDeleteModal = false;
|
||||
|
||||
let errors: Record<string, string> = {};
|
||||
|
||||
async function saveLesson(lesson: Lesson, courseId: Course['id']) {
|
||||
const validationRes = await handleSaveLesson(lesson, courseId);
|
||||
|
||||
if (validationRes && Object.keys(validationRes).length) {
|
||||
// @ts-ignore
|
||||
errors = validationRes;
|
||||
} else {
|
||||
errors = {};
|
||||
lessonEditing = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDndConsider(e: any) {
|
||||
$lessons = e.detail.items;
|
||||
}
|
||||
|
||||
function handleDndFinalize(e: any) {
|
||||
const prevLessonsByOrder = $lessons.reduce((acc, l) => ({ ...acc, [l.id]: l.order }), {});
|
||||
$lessons = e.detail.items;
|
||||
|
||||
// Only update the lesson order that changed
|
||||
e.detail.items.forEach((item: { id: string }, index: number) => {
|
||||
const order = index + 1;
|
||||
|
||||
if (order !== prevLessonsByOrder[item.id]) {
|
||||
$lessons[index].order = order;
|
||||
updateLesson({ order }, item.id).then((update) =>
|
||||
console.log(`updated lesson order`, update)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getLessonOrder(id: string) {
|
||||
const index = $lessons.findIndex((lesson) => lesson.id === id);
|
||||
|
||||
if (index < 9) {
|
||||
return '0' + (index + 1);
|
||||
} else {
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="m-auto w-full p-3 lg:w-11/12 lg:px-4"
|
||||
use:dndzone={{
|
||||
items: $lessons,
|
||||
flipDurationMs,
|
||||
dragDisabled: $globalStore.isStudent || !reorder,
|
||||
dropTargetStyle: {
|
||||
border: '2px #1d4ed8 solid',
|
||||
'border-style': 'dashed'
|
||||
}
|
||||
}}
|
||||
on:consider={handleDndConsider}
|
||||
on:finalize={handleDndFinalize}
|
||||
>
|
||||
{#each $lessons as lesson (lesson.id)}
|
||||
<div
|
||||
class={`relative m-auto mb-4 flex max-w-xl items-center rounded-md border-2 ${
|
||||
reorder ? 'border-primary-400' : 'border-gray-200'
|
||||
} border-gray-200 dark:border-neutral-600 p-5 dark:bg-neutral-800`}
|
||||
>
|
||||
<!-- Number Chip -->
|
||||
<div class="mr-5">
|
||||
<TextChip
|
||||
value={getLessonOrder(lesson.id)}
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
className="bg-primary-200 text-primary-600 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Lesson Content -->
|
||||
<div class="w-4/5">
|
||||
<!-- Lesson Title -->
|
||||
{#if lessonEditing === lesson.id}
|
||||
<TextField
|
||||
bind:value={lesson.title}
|
||||
autoFocus={true}
|
||||
className="max-w-lg"
|
||||
errorMessage={errors?.title}
|
||||
/>
|
||||
{:else}
|
||||
<h3 class="m-0 flex items-center text-lg dark:text-white">
|
||||
<a
|
||||
href={$globalStore.isStudent && !lesson.is_unlocked
|
||||
? $page.url.pathname
|
||||
: '/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'
|
||||
: ''}"
|
||||
>
|
||||
{lesson.title}
|
||||
</a>
|
||||
</h3>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="mt-2 flex w-4/5 flex-col items-start justify-between lg:flex-row lg:items-center"
|
||||
>
|
||||
<!-- Lesson Length -->
|
||||
<div class="mb-3 flex items-center lg:mb-0">
|
||||
<ScreenMap size={20} class="carbon-icon dark:text-white" />
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{lesson?.totalExercises ? lesson?.totalExercises?.map((c) => c.count) : 0}
|
||||
{$t('exercises.heading')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3 dot edit -->
|
||||
<div class="flex flex-row">
|
||||
<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')}
|
||||
on:click={() => {
|
||||
lesson.is_unlocked = !lesson.is_unlocked;
|
||||
handleSaveLesson(lesson, $course.id);
|
||||
}}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
text={lessonEditing === lesson.id
|
||||
? $t('course.navItem.lessons.add_lesson.save')
|
||||
: $t('course.navItem.lessons.add_lesson.edit')}
|
||||
on:click={() => {
|
||||
if (lessonEditing === lesson.id) {
|
||||
saveLesson(lesson, $course.id);
|
||||
} else {
|
||||
lessonEditing = lesson.id;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
danger
|
||||
text={$t('course.navItem.lessons.add_lesson.delete')}
|
||||
on:click={() => {
|
||||
lessonToDelete = lesson;
|
||||
openDeleteModal = true;
|
||||
}}
|
||||
/>
|
||||
</OverflowMenu>
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Box>
|
||||
<div class="flex justify-between flex-col items-center w-[90%] md:w-96">
|
||||
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
|
||||
<h2 class="text-xl my-1.5 font-normal">{$t('course.navItem.lessons.body_header')}</h2>
|
||||
<p class="text-sm text-center text-slate-500">
|
||||
{$t('course.navItem.lessons.body_content')}
|
||||
</p>
|
||||
</div>
|
||||
</Box>
|
||||
{/each}
|
||||
</section>
|
||||
@@ -0,0 +1,209 @@
|
||||
<script lang="ts">
|
||||
import { Edit, TrashCan, Add } from 'carbon-icons-svelte';
|
||||
import TextField from '$lib/components/Form/TextField.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 { handleAddLessonWidget } from '$lib/components/Course/components/Lesson/store';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import {
|
||||
lessonSections,
|
||||
handleDelete,
|
||||
handleSaveLesson,
|
||||
handleSaveLessonSection,
|
||||
handleDeleteSection
|
||||
} from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
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';
|
||||
|
||||
type CrudParam = {
|
||||
sectionId?: string;
|
||||
lessonId?: string;
|
||||
sectionTitle?: string;
|
||||
lessonTitle?: string;
|
||||
};
|
||||
|
||||
// export let reorder = false;
|
||||
export let lessonEditing: string | undefined;
|
||||
|
||||
let prevTitle: string | undefined;
|
||||
let errors: Record<string, string>;
|
||||
let openDeleteModal = false;
|
||||
let deletingData = {
|
||||
id: '',
|
||||
isSection: false
|
||||
};
|
||||
|
||||
function onEdit(params: CrudParam) {
|
||||
lessonEditing = params.sectionId || params.lessonId;
|
||||
prevTitle = params.sectionTitle || params.lessonTitle;
|
||||
}
|
||||
|
||||
async function onSave(params: CrudParam, lesson?: Lesson) {
|
||||
if (params.sectionId) {
|
||||
const section = $lessonSections.find((s) => s.id === params.sectionId);
|
||||
if (!section) return;
|
||||
|
||||
const validationRes = await handleSaveLessonSection(section, section.course_id);
|
||||
|
||||
if (validationRes && Object.keys(validationRes).length) {
|
||||
errors = {
|
||||
// @ts-ignore
|
||||
title: validationRes?.title
|
||||
};
|
||||
}
|
||||
} else if (lesson) {
|
||||
const validationRes = await handleSaveLesson(lesson, lesson.course_id);
|
||||
|
||||
if (validationRes && Object.keys(validationRes).length) {
|
||||
errors = {
|
||||
// @ts-ignore
|
||||
title: validationRes?.title
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors?.title) {
|
||||
resetEdit();
|
||||
} else {
|
||||
snackbar.success('snackbar.success_update');
|
||||
}
|
||||
}
|
||||
|
||||
function onDelete(params: CrudParam) {
|
||||
deletingData.id = params.sectionId || params.lessonId || '';
|
||||
deletingData.isSection = params.sectionId ? true : false;
|
||||
openDeleteModal = true;
|
||||
}
|
||||
|
||||
function deleteLesson() {
|
||||
if (deletingData.isSection) {
|
||||
handleDeleteSection(deletingData.id);
|
||||
} else {
|
||||
handleDelete(deletingData.id);
|
||||
}
|
||||
}
|
||||
|
||||
function resetEdit() {
|
||||
lessonEditing = undefined;
|
||||
prevTitle = undefined;
|
||||
errors = {};
|
||||
}
|
||||
|
||||
function handleAddLesson(id) {
|
||||
handleAddLessonWidget.update(() => ({
|
||||
open: true,
|
||||
isSection: false,
|
||||
id
|
||||
}));
|
||||
}
|
||||
</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 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}
|
||||
|
||||
<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 })}
|
||||
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}
|
||||
/>
|
||||
{:else}
|
||||
<p class="w-4/6 text-md">{lesson.title}</p>
|
||||
{/if}
|
||||
|
||||
<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();
|
||||
}}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.save')}
|
||||
onClick={() => onSave({ lessonId: lesson.id }, lesson)}
|
||||
/>
|
||||
{: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>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
@@ -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,
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<script lang="ts">
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import Select from '$lib/components/Form/Select.svelte';
|
||||
// import Select from '$lib/components/Form/Select.svelte';
|
||||
import {
|
||||
lessons,
|
||||
handleSaveLesson
|
||||
lessonSections,
|
||||
handleSaveLesson,
|
||||
handleSaveLessonSection
|
||||
} from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { course, group } from '$lib/components/Course/store';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { handleAddLessonWidget } from '../Navigation/store';
|
||||
import { handleAddLessonWidget } from './store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { COURSE_TYPE } from '$lib/utils/types';
|
||||
import type { Lesson } from '$lib/utils/types';
|
||||
@@ -25,7 +27,8 @@
|
||||
call_url: undefined,
|
||||
lesson_at: new Date().toDateString(),
|
||||
is_unlocked: true,
|
||||
lesson_completion: []
|
||||
lesson_completion: [],
|
||||
created_at: ''
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -33,26 +36,85 @@
|
||||
errors.title = 'title cannot be empty';
|
||||
return;
|
||||
}
|
||||
const savedLesson = await handleSaveLesson(lesson, $course.id);
|
||||
|
||||
if (Array.isArray(savedLesson) && savedLesson[0]) {
|
||||
const newLesson = savedLesson[0];
|
||||
lesson.id = newLesson.id;
|
||||
$lessons = [...$lessons, lesson];
|
||||
goto('/courses/' + $course.id + '/lessons/' + lesson.id);
|
||||
if ($handleAddLessonWidget.isSection) {
|
||||
const savedSection = await handleSaveLessonSection(
|
||||
{
|
||||
title: lesson.title,
|
||||
order: $lessonSections.length
|
||||
},
|
||||
$course.id
|
||||
);
|
||||
|
||||
if (Array.isArray(savedSection) && savedSection[0]) {
|
||||
const newLessonSection = savedSection[0];
|
||||
|
||||
lessonSections.update((sections) => {
|
||||
return [
|
||||
...sections,
|
||||
{
|
||||
id: newLessonSection.id,
|
||||
title: lesson.title,
|
||||
order: newLessonSection.order,
|
||||
course_id: newLessonSection.course_id,
|
||||
lessons: []
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
} else {
|
||||
lesson.section_id = $handleAddLessonWidget.id;
|
||||
const savedLesson = await handleSaveLesson(lesson, $course.id);
|
||||
|
||||
if (Array.isArray(savedLesson) && savedLesson[0]) {
|
||||
const newLesson = savedLesson[0];
|
||||
lesson.id = newLesson.id;
|
||||
$lessons = [...$lessons, lesson];
|
||||
|
||||
lessonSections.update((sections) =>
|
||||
sections.map((s) => {
|
||||
if (s.id === newLesson.section_id) {
|
||||
s.lessons = [...s.lessons, lesson];
|
||||
}
|
||||
|
||||
return s;
|
||||
})
|
||||
);
|
||||
goto('/courses/' + $course.id + '/lessons/' + lesson.id);
|
||||
}
|
||||
}
|
||||
|
||||
$handleAddLessonWidget.open = false;
|
||||
handleClose();
|
||||
};
|
||||
|
||||
function handleClose() {
|
||||
$handleAddLessonWidget.open = false;
|
||||
|
||||
lesson = {
|
||||
id: '',
|
||||
course_id: $course.id || '',
|
||||
title: '',
|
||||
profile: undefined,
|
||||
call_url: undefined,
|
||||
lesson_at: new Date().toDateString(),
|
||||
is_unlocked: true,
|
||||
lesson_completion: [],
|
||||
created_at: ''
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={() => ($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'
|
||||
}`
|
||||
)}
|
||||
>
|
||||
<form
|
||||
on:submit|preventDefault={handleSave}
|
||||
@@ -60,7 +122,11 @@
|
||||
>
|
||||
<div class="w-full">
|
||||
<TextField
|
||||
label={$t('course.navItem.lessons.add_lesson.lesson_title')}
|
||||
label={$t(
|
||||
`course.navItem.lessons.add_lesson.${
|
||||
$handleAddLessonWidget.isSection ? 'lesson_section_title' : 'lesson_title'
|
||||
}`
|
||||
)}
|
||||
bind:value={lesson.title}
|
||||
autoFocus={true}
|
||||
className="flex-1 min-w-lg max-w-lg"
|
||||
@@ -68,7 +134,7 @@
|
||||
errorMessage={errors.title}
|
||||
/>
|
||||
{#if $course.type == COURSE_TYPE.LIVE_CLASS}
|
||||
<div
|
||||
<!-- <div
|
||||
class="flex items-start justify-evenly gap-1 flex-col lg:flex-row lg:items-center mt-2 w-4/5"
|
||||
>
|
||||
<div class="lg:mb-0">
|
||||
@@ -91,7 +157,7 @@
|
||||
<div class="flex items-center mb-3 lg:mb-0">
|
||||
<TextField className="w-[179px]" placeholder="https://meet.google.com/mga-dsjs-fmb" />
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
@@ -100,4 +166,3 @@
|
||||
<PrimaryButton label={$t('course.navItem.lessons.add_lesson.save')} onClick={handleSave} />
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const handleAddLessonWidget = writable({
|
||||
id: '',
|
||||
open: false,
|
||||
isSection: false
|
||||
});
|
||||
@@ -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<Lesson[]> = writable([]);
|
||||
|
||||
export const lessonSections: Writable<LessonSection[]> = writable([]);
|
||||
|
||||
export const lesson = writable<LessonPage>({
|
||||
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<LessonSection>,
|
||||
courseId: Course['id']
|
||||
) {
|
||||
const result = lessonValidation(section);
|
||||
|
||||
if (Object.keys(result).length) {
|
||||
return result;
|
||||
}
|
||||
console.log(`handleSaveLessonSection lesson`, section);
|
||||
|
||||
const newSection: Partial<LessonSection> = {
|
||||
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 = {
|
||||
|
||||
+30
-18
@@ -1,32 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import ChevronDownIcon from 'carbon-icons-svelte/lib/ChevronDown.svelte';
|
||||
import ChevronUpIcon from 'carbon-icons-svelte/lib/ChevronUp.svelte';
|
||||
import Add from 'carbon-icons-svelte/lib/Add.svelte';
|
||||
import { ChevronDown, ChevronUp, Add, FlashFilled } from 'carbon-icons-svelte';
|
||||
import { SkeletonText } from 'carbon-components-svelte';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte';
|
||||
import { handleAddLessonWidget } from './store';
|
||||
import { handleAddLessonWidget } from '../Lesson/store';
|
||||
import NavIcons from './NavIcons.svelte';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import { goto } from '$app/navigation';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
|
||||
export let handleClick = () => {};
|
||||
export let id = '';
|
||||
export let name = '';
|
||||
export let label = '';
|
||||
export let isGroupActive = false;
|
||||
export let isExpanded: boolean | undefined;
|
||||
export let total = 0;
|
||||
export let isLoading = true;
|
||||
export let isLesson = false;
|
||||
export let isSection = false;
|
||||
export let isStudent = true;
|
||||
export let isPaidFeature = false;
|
||||
// export let subMenuItems = [];
|
||||
export let className = '';
|
||||
export let btnPadding = 'py-3 px-4';
|
||||
|
||||
function addLesson() {
|
||||
goto('/courses/' + $course.id + '/lessons');
|
||||
$handleAddLessonWidget.open = true;
|
||||
// If it is section, then add lesson to section
|
||||
$handleAddLessonWidget.isSection = isSection ? false : true;
|
||||
|
||||
if (isSection) {
|
||||
$handleAddLessonWidget.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
if (isSection) {
|
||||
toggleIsExpanded();
|
||||
}
|
||||
|
||||
handleClick();
|
||||
}
|
||||
|
||||
@@ -35,22 +49,22 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="">
|
||||
<div class={className}>
|
||||
<button
|
||||
class="item relative flex items-center py-3 px-4 ml-2 mb-1 {NavClasses.item} {isGroupActive &&
|
||||
class="item relative flex items-center {btnPadding} ml-2 mb-1 {NavClasses.item} {isGroupActive &&
|
||||
!isLoading &&
|
||||
NavClasses.active} w-[95%]"
|
||||
tabindex="0"
|
||||
on:click={onClick}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<NavIcons name={label} />
|
||||
<NavIcons {name} />
|
||||
{#if isLoading}
|
||||
<div class="w-11/12 mx-auto">
|
||||
<SkeletonText class="rounded-md" style="margin: 0px; height: 30px;" />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="font-bold text-md">{label}</span>
|
||||
<span class="font-bold text-md text-start leading-4 line-clamp-2">{label}</span>
|
||||
{#if total}
|
||||
<span class="ml-1">({total})</span>
|
||||
{/if}
|
||||
@@ -60,17 +74,15 @@
|
||||
{#if isPaidFeature && $isFreePlan}
|
||||
<FlashFilled size={20} class="text-blue-700" />
|
||||
{/if}
|
||||
{#if isLesson && !isLoading}
|
||||
{#if !isStudent}
|
||||
<IconButton onClick={() => addLesson()} size="small">
|
||||
<Add />
|
||||
</IconButton>
|
||||
{/if}
|
||||
{#if (isLesson || isSection) && !isLoading && !isStudent}
|
||||
<IconButton stopPropagation={true} onClick={addLesson} size="small">
|
||||
<Add />
|
||||
</IconButton>
|
||||
<IconButton size="small" stopPropagation={true} onClick={toggleIsExpanded}>
|
||||
{#if isExpanded}
|
||||
<ChevronUpIcon class="carbon-icon dark:text-white" />
|
||||
<ChevronUp class="carbon-icon dark:text-white" />
|
||||
{:else}
|
||||
<ChevronDownIcon class="carbon-icon dark:text-white" />
|
||||
<ChevronDown class="carbon-icon dark:text-white" />
|
||||
{/if}
|
||||
</IconButton>
|
||||
{/if}
|
||||
|
||||
@@ -9,28 +9,29 @@
|
||||
import IbmWatsonKnowledgeCatalog from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeCatalog.svelte';
|
||||
import Settings from 'carbon-icons-svelte/lib/Settings.svelte';
|
||||
import Bullhorn from 'carbon-icons-svelte/lib/Bullhorn.svelte';
|
||||
import { NAV_IDS } from './constants';
|
||||
|
||||
export let name = '';
|
||||
</script>
|
||||
|
||||
{#if name === 'Overview'}
|
||||
<TableOfContents size={20} class="carbon-icon mr-2" />
|
||||
{:else if name === 'News Feed'}
|
||||
<Bullhorn size={20} class="carbon-icon mr-2" />
|
||||
{:else if name === 'Lessons'}
|
||||
{#if name === NAV_IDS.SECTION}
|
||||
<TableOfContents size={20} class="carbon-icon mr-2" style="min-width: 20px;" />
|
||||
{:else if name === NAV_IDS.NEWS_FEED}
|
||||
<Bullhorn size={20} class="carbon-icon r-2" />
|
||||
{:else if name === NAV_IDS.LESSONS}
|
||||
<IbmWatsonKnowledgeCatalog size={20} class="carbon-icon mr-2" />
|
||||
{:else if name === 'Attendance'}
|
||||
{:else if name === NAV_IDS.ATTENDANCE}
|
||||
<ListChecked size={20} class="carbon-icon mr-2" />
|
||||
{:else if name === 'Submissions'}
|
||||
{:else if name === NAV_IDS.SUBMISSIONS}
|
||||
<Workspace size={20} class="carbon-icon mr-2" />
|
||||
{:else if name === 'Marks'}
|
||||
{:else if name === NAV_IDS.MARKS}
|
||||
<Result size={20} class="carbon-icon mr-2" />
|
||||
{:else if name === 'People'}
|
||||
{:else if name === NAV_IDS.PEOPLE}
|
||||
<Group size={20} class="carbon-icon mr-2" />
|
||||
{:else if name === 'Landing Page'}
|
||||
{:else if name === NAV_IDS.LANDING_PAGE}
|
||||
<ApplicationWeb size={20} class="carbon-icon mr-2" />
|
||||
{:else if name === 'Certificates'}
|
||||
{:else if name === NAV_IDS.CERTIFICATES}
|
||||
<Badge size={20} class="carbon-icon mr-2" />
|
||||
{:else if name === 'Settings'}
|
||||
{:else if name === NAV_IDS.SETTINGS}
|
||||
<Settings size={20} class="carbon-icon mr-2" />
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export const NAV_IDS = {
|
||||
NEWS_FEED: 'NEWS_FEED',
|
||||
LESSONS: 'LESSONS',
|
||||
ATTENDANCE: 'ATTENDANCE',
|
||||
SUBMISSIONS: 'SUBMISSIONS',
|
||||
MARKS: 'MARKS',
|
||||
CERTIFICATES: 'CERTIFICATES',
|
||||
LANDING_PAGE: 'LANDING_PAGE',
|
||||
PEOPLE: 'PEOPLE',
|
||||
SETTINGS: 'SETTINGS',
|
||||
SECTION: 'SECTION'
|
||||
};
|
||||
@@ -3,12 +3,12 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import LockedIcon from 'carbon-icons-svelte/lib/Locked.svelte';
|
||||
import CheckmarkFilled from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
|
||||
import NavExpandable from './NavExpandable.svelte';
|
||||
import { getNavItemRoute, getLessonsRoute, getLectureNo } from '$lib/components/Course/function';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import { lessons } from '../Lesson/store/lessons';
|
||||
import { lessons, lessonSections } from '../Lesson/store/lessons';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
@@ -17,18 +17,28 @@
|
||||
import { getIsLessonComplete } from '../Lesson/functions';
|
||||
import { currentOrg, isFreePlan } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { COURSE_TYPE } from '$lib/utils/types';
|
||||
import { COURSE_TYPE, COURSE_VERSION, type Lesson } from '$lib/utils/types';
|
||||
import { NAV_IDS } from './constants';
|
||||
|
||||
export let path: string;
|
||||
export let isStudent: boolean = false;
|
||||
|
||||
interface Section {
|
||||
id: string;
|
||||
order: number;
|
||||
label: string;
|
||||
lessons: Lesson[];
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
interface NavItem {
|
||||
id: string;
|
||||
label: string;
|
||||
to: string;
|
||||
hideSortIcon: boolean;
|
||||
isLesson?: boolean;
|
||||
isPaidFeature: boolean;
|
||||
isExpanded?: boolean;
|
||||
sections?: Section[];
|
||||
show?: () => boolean;
|
||||
}
|
||||
|
||||
@@ -139,6 +149,7 @@
|
||||
$: {
|
||||
navItems = [
|
||||
{
|
||||
id: NAV_IDS.NEWS_FEED,
|
||||
label: $t('course.navItems.nav_news_feed'),
|
||||
to: getNavItemRoute($course.id),
|
||||
hideSortIcon: true,
|
||||
@@ -148,14 +159,21 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.LESSONS,
|
||||
label: $t('course.navItems.nav_lessons'),
|
||||
to: getLessonsRoute($course.id),
|
||||
hideSortIcon: false,
|
||||
isPaidFeature: false,
|
||||
isLesson: true,
|
||||
sections: $lessonSections.map((section) => ({
|
||||
...section,
|
||||
label: section.title,
|
||||
isExpanded: true
|
||||
})),
|
||||
isExpanded: isStudent ? true : $page.url.pathname.includes('/lessons')
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.ATTENDANCE,
|
||||
label: $t('course.navItems.nav_attendance'),
|
||||
to: getNavItemRoute($course.id, 'attendance'),
|
||||
isPaidFeature: false,
|
||||
@@ -167,6 +185,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.SUBMISSIONS,
|
||||
label: $t('course.navItems.nav_submissions'),
|
||||
to: getNavItemRoute($course.id, 'submissions'),
|
||||
hideSortIcon: true,
|
||||
@@ -178,6 +197,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.MARKS,
|
||||
label: $t('course.navItems.nav_marks'),
|
||||
to: getNavItemRoute($course.id, 'marks'),
|
||||
isPaidFeature: false,
|
||||
@@ -191,6 +211,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.CERTIFICATES,
|
||||
label: $t('course.navItems.nav_certificates'),
|
||||
to: getNavItemRoute($course.id, 'certificates'),
|
||||
hideSortIcon: true,
|
||||
@@ -204,6 +225,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.LANDING_PAGE,
|
||||
label: $t('course.navItems.nav_landing_page'),
|
||||
to: getNavItemRoute($course.id, 'landingpage'),
|
||||
hideSortIcon: true,
|
||||
@@ -213,6 +235,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.PEOPLE,
|
||||
label: $t('course.navItems.nav_people'),
|
||||
to: getNavItemRoute($course.id, 'people'),
|
||||
isPaidFeature: false,
|
||||
@@ -222,6 +245,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.SETTINGS,
|
||||
label: $t('course.navItems.nav_settings'),
|
||||
to: getNavItemRoute($course.id, 'settings'),
|
||||
hideSortIcon: true,
|
||||
@@ -258,6 +282,8 @@
|
||||
{#each navItems as navItem}
|
||||
{#if !navItem.show || (typeof navItem.show === 'function' && navItem.show())}
|
||||
<NavExpandable
|
||||
id={navItem.id}
|
||||
name={navItem.id}
|
||||
label={navItem.label}
|
||||
handleClick={handleMainGroupClick(navItem.to)}
|
||||
isGroupActive={(path || $page.url.pathname) === navItem.to}
|
||||
@@ -268,10 +294,10 @@
|
||||
isExpanded={navItem.isExpanded}
|
||||
{isStudent}
|
||||
>
|
||||
{#if navItem.isLesson}
|
||||
{#if $course.version === COURSE_VERSION.V1}
|
||||
{#each $lessons as item, index}
|
||||
<a
|
||||
class="pl-7 w-[95%] text-[0.80rem] mb-1 text-black dark:text-white {isStudent &&
|
||||
class="pl-7 w-[95%] text-[0.80rem] mb-2 text-black dark:text-white {isStudent &&
|
||||
!item.is_unlocked
|
||||
? 'cursor-not-allowed'
|
||||
: ''}"
|
||||
@@ -280,9 +306,10 @@
|
||||
: getLessonsRoute($course.id, item.id)}
|
||||
on:click={toggleSidebarOnMobile}
|
||||
aria-disabled={!item.is_unlocked}
|
||||
title={item.title}
|
||||
>
|
||||
<div
|
||||
class="flex items-center py-3 px-4 {NavClasses.item} {(
|
||||
class="flex items-center py-2 px-4 {NavClasses.item} {(
|
||||
path || $page.url.pathname
|
||||
).includes(item.id) && NavClasses.active}"
|
||||
>
|
||||
@@ -292,7 +319,7 @@
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
/>
|
||||
<span class="w-[70%] text-ellipsis line-clamp-2">{item.title}</span>
|
||||
<span class="w-[85%] text-ellipsis line-clamp-2">{item.title}</span>
|
||||
<span class="grow" />
|
||||
{#if !item.is_unlocked}
|
||||
<span class="text-md ml-2" title="This lesson is locked.">
|
||||
@@ -306,6 +333,53 @@
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{:else if navItem.sections}
|
||||
{#each navItem.sections as section}
|
||||
<NavExpandable
|
||||
id={section.id}
|
||||
name={NAV_IDS.SECTION}
|
||||
label={section.label}
|
||||
isLoading={!$course.id}
|
||||
isSection={true}
|
||||
isExpanded={section.isExpanded}
|
||||
className="ml-4"
|
||||
btnPadding="py-2 px-3"
|
||||
{isStudent}
|
||||
>
|
||||
{#each section.lessons as item}
|
||||
<a
|
||||
class="pl-7 w-[95%] text-[0.80rem] mb-2 text-black dark:text-white {isStudent &&
|
||||
!item.is_unlocked
|
||||
? 'cursor-not-allowed'
|
||||
: ''}"
|
||||
href={isStudent && !item.is_unlocked
|
||||
? $page.url.pathname
|
||||
: getLessonsRoute($course.id, item.id)}
|
||||
on:click={toggleSidebarOnMobile}
|
||||
aria-disabled={!item.is_unlocked}
|
||||
title={item.title}
|
||||
>
|
||||
<div
|
||||
class="flex items-center py-2 px-4 {NavClasses.item} {(
|
||||
path || $page.url.pathname
|
||||
).includes(item.id) && NavClasses.active}"
|
||||
>
|
||||
<span class="w-[85%] text-ellipsis line-clamp-2">{item.title}</span>
|
||||
<span class="grow" />
|
||||
{#if !item.is_unlocked}
|
||||
<span class="text-md ml-2" title="This lesson is locked.">
|
||||
<LockedIcon class="carbon-icon dark:text-white" />
|
||||
</span>
|
||||
{:else if getIsLessonComplete(item.lesson_completion, $profile.id)}
|
||||
<span class="ml-2" title="You have completed this lesson">
|
||||
<CheckmarkFilled class="carbon-icon dark:text-white" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</NavExpandable>
|
||||
{/each}
|
||||
{/if}
|
||||
</NavExpandable>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let handleAddLessonWidget = writable({
|
||||
open: false
|
||||
});
|
||||
@@ -6,7 +6,7 @@ export function getGroupMemberId(people, profileId) {
|
||||
return groupMember ? groupMember.id : null;
|
||||
}
|
||||
|
||||
export function getNavItemRoute(courseId, routeId) {
|
||||
export function getNavItemRoute(courseId, routeId?: string) {
|
||||
const path = `/${ROUTES.COURSES}/${courseId}`;
|
||||
|
||||
if (!routeId) {
|
||||
@@ -16,7 +16,7 @@ export function getNavItemRoute(courseId, routeId) {
|
||||
return `${path}/${routeId}`;
|
||||
}
|
||||
|
||||
export function getLessonsRoute(courseId, lessonId) {
|
||||
export function getLessonsRoute(courseId, lessonId?: string) {
|
||||
const path = getNavItemRoute(courseId, ROUTES.LESSONS);
|
||||
|
||||
if (!lessonId) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { lessons } from './components/Lesson/store/lessons';
|
||||
import { lessons, lessonSections } from './components/Lesson/store/lessons';
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
import type { Course, GroupPerson } from '$lib/utils/types';
|
||||
import type { Course, GroupPerson, Lesson, LessonSection } from '$lib/utils/types';
|
||||
import { COURSE_TYPE } from '$lib/utils/types';
|
||||
|
||||
export const defaultCourse: Course = {
|
||||
@@ -116,17 +116,23 @@ export async function setCourse(data: Course, setLesson = true) {
|
||||
}
|
||||
|
||||
if (setLesson) {
|
||||
const orderedLessons = (data.lessons || [])
|
||||
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
// .map((lesson) => ({
|
||||
// ...lesson,
|
||||
// profile: lesson.profile && tutorsById[lesson.profile.id],
|
||||
// }));
|
||||
const orderedLessons = sortLesson(data.lessons);
|
||||
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);
|
||||
return section;
|
||||
});
|
||||
|
||||
lessonSections.set(sortLessonSection(sections));
|
||||
}
|
||||
}
|
||||
|
||||
delete data.lessons;
|
||||
delete data?.lesson_section;
|
||||
|
||||
if (data.metadata && !Object.values(data.metadata)) {
|
||||
data.metadata = {
|
||||
@@ -156,3 +162,15 @@ export async function setCourse(data: Course, setLesson = true) {
|
||||
}
|
||||
course.set(data);
|
||||
}
|
||||
|
||||
function sortLesson(lessons: Lesson[] = []) {
|
||||
return lessons
|
||||
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
}
|
||||
|
||||
function sortLessonSection(sections: LessonSection[] = []) {
|
||||
return sections
|
||||
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
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 { COURSE_TYPE, COURSE_VERSION } from '$lib/utils/types';
|
||||
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
||||
|
||||
let isLoading = false;
|
||||
@@ -85,6 +85,7 @@
|
||||
title,
|
||||
description,
|
||||
type: type,
|
||||
version: COURSE_VERSION.V2,
|
||||
group_id
|
||||
})
|
||||
.select();
|
||||
|
||||
+3
-4
@@ -26,7 +26,7 @@ const lessonSchema = z.object({
|
||||
title: z.string().nonempty({ message: `${t.get('validations.lesson_schema.empty_title')}` }),
|
||||
lesson_at: z.string().optional(),
|
||||
call_url: z.string().nullable().optional(),
|
||||
is_unlocked: z.boolean()
|
||||
is_unlocked: z.boolean().optional()
|
||||
});
|
||||
|
||||
const createQuizValidationSchema = z.object({
|
||||
@@ -148,8 +148,8 @@ export const getConfirmPasswordError = ({ password, confirmPassword }) => {
|
||||
: undefined;
|
||||
};
|
||||
|
||||
export const processErrors = (error, mapToId) => {
|
||||
const errors = {};
|
||||
export const processErrors = (error, mapToId?: boolean) => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (Array.isArray(error?.issues)) {
|
||||
for (const issue of error.issues) {
|
||||
@@ -162,7 +162,6 @@ export const processErrors = (error, mapToId) => {
|
||||
if (mapToId) {
|
||||
let value = '';
|
||||
path.forEach((p, i) => {
|
||||
const isLast = !path[i + 1];
|
||||
const formatP = isNumber(p) ? `[${p}]` : p;
|
||||
|
||||
value += !value ? formatP : `.${formatP}`;
|
||||
@@ -9,7 +9,8 @@ import type {
|
||||
Groupmember,
|
||||
Exercise,
|
||||
LessonCompletion,
|
||||
ExerciseTemplate
|
||||
ExerciseTemplate,
|
||||
LessonSection
|
||||
} from '$lib/utils/types';
|
||||
import { STATUS } from '$lib/utils/constants/course';
|
||||
import type { PostgrestSingleResponse, PostgrestError } from '@supabase/supabase-js';
|
||||
@@ -70,10 +71,12 @@ const SLUG_QUERY = `
|
||||
is_published,
|
||||
slug,
|
||||
cost,
|
||||
version,
|
||||
currency,
|
||||
metadata,
|
||||
is_certificate_downloadable,
|
||||
certificate_theme,
|
||||
lesson_section(id, title, order),
|
||||
lessons:lesson(
|
||||
id, title, order
|
||||
)
|
||||
@@ -87,6 +90,7 @@ const ID_QUERY = `
|
||||
overview,
|
||||
logo,
|
||||
is_published,
|
||||
version,
|
||||
group(*,
|
||||
members:groupmember(*,
|
||||
profile(*)
|
||||
@@ -98,8 +102,9 @@ const ID_QUERY = `
|
||||
metadata,
|
||||
is_certificate_downloadable,
|
||||
certificate_theme,
|
||||
lesson_section(id, title, order, created_at),
|
||||
lessons:lesson(
|
||||
id, title,public, lesson_at, is_unlocked, order, created_at,
|
||||
id, title, public, lesson_at, is_unlocked, order, created_at, section_id,
|
||||
note, videos, slide_url, call_url, totalExercises:exercise(count), totalComments:lesson_comment(count),
|
||||
profile:teacher_id(id, avatar_url, fullname),
|
||||
lesson_completion(id, profile_id, is_complete)
|
||||
@@ -244,6 +249,7 @@ export function fetchLesson(lessonId: Lesson['id']) {
|
||||
.from('lesson')
|
||||
.select(
|
||||
`id,
|
||||
title,
|
||||
note,
|
||||
videos,
|
||||
slide_url,
|
||||
@@ -271,6 +277,15 @@ export function fetchLesssonLanguageHistory(lessonId: string, locale: string, en
|
||||
export function createLesson(lesson: any) {
|
||||
return supabase.from('lesson').insert(lesson).select();
|
||||
}
|
||||
export function createLessonSection(section: any) {
|
||||
return supabase.from('lesson_section').insert(section).select();
|
||||
}
|
||||
export function updateLessonSection(section: any, sectionId: LessonSection['id']) {
|
||||
return supabase
|
||||
.from('lesson_section')
|
||||
.update({ ...section, id: undefined })
|
||||
.match({ id: sectionId });
|
||||
}
|
||||
|
||||
export function updateLesson(lesson: any, lessonId: Lesson['id']) {
|
||||
return supabase
|
||||
@@ -297,6 +312,11 @@ export function deleteLesson(lessonId: Lesson['id']) {
|
||||
return supabase.from('lesson').delete().match({ id: lessonId });
|
||||
}
|
||||
|
||||
export function deleteLessonSection(id: LessonSection['id']) {
|
||||
// Need to implement soft delete
|
||||
return supabase.from('lesson_section').delete().match({ id });
|
||||
}
|
||||
|
||||
export function createExercise(exercise: any) {
|
||||
return supabase.from('exercise').insert(exercise).select();
|
||||
}
|
||||
|
||||
@@ -754,7 +754,10 @@
|
||||
"lock": "Sperren",
|
||||
"unlock": "Freischalten",
|
||||
"start_reorder": "Neu anordnen",
|
||||
"end_reorder": "Nachbestellung beenden"
|
||||
"end_reorder": "Nachbestellung beenden",
|
||||
"modal_heading_section": "Neuen Abschnitt hinzufügen",
|
||||
"lesson_section_title": "Abschnittstitel",
|
||||
"cancel": "Stornieren"
|
||||
},
|
||||
"disabled": "Funktion ist deaktiviert",
|
||||
"next": "Nächste",
|
||||
|
||||
@@ -512,14 +512,17 @@
|
||||
"start_reorder": "Reorder",
|
||||
"end_reorder": "End Reorder",
|
||||
"modal_heading": "Add New Lesson",
|
||||
"modal_heading_section": "Add New Section",
|
||||
"lesson_title": "Lesson Title",
|
||||
"lesson_section_title": "Section Title",
|
||||
"click_to": "Click to",
|
||||
"lock": "Lock",
|
||||
"unlock": "Unlock",
|
||||
"fullname": "fullname",
|
||||
"save": "Save",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete"
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"join_lesson": "Join lesson",
|
||||
"no_link": "No link",
|
||||
|
||||
@@ -754,7 +754,10 @@
|
||||
"lock": "Cerrar",
|
||||
"unlock": "desbloquear",
|
||||
"start_reorder": "Reordenar",
|
||||
"end_reorder": "Finalizar reorden"
|
||||
"end_reorder": "Finalizar reorden",
|
||||
"modal_heading_section": "Agregar nueva sección",
|
||||
"lesson_section_title": "Título de la sección",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"disabled": "La característica está deshabilitada",
|
||||
"next": "Próximo",
|
||||
|
||||
@@ -754,7 +754,10 @@
|
||||
"lock": "Verrouillage",
|
||||
"unlock": "Ouvrir",
|
||||
"start_reorder": "Réorganiser",
|
||||
"end_reorder": "Terminer la réapprovisionnement"
|
||||
"end_reorder": "Terminer la réapprovisionnement",
|
||||
"modal_heading_section": "Ajouter une nouvelle section",
|
||||
"lesson_section_title": "Titre de la section",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"disabled": "La fonctionnalité est désactivée",
|
||||
"next": "Suivant",
|
||||
|
||||
@@ -754,7 +754,10 @@
|
||||
"lock": "ताला",
|
||||
"unlock": "अनलॉक",
|
||||
"start_reorder": "पुन: व्यवस्थित करें",
|
||||
"end_reorder": "पुनः क्रम समाप्त करें"
|
||||
"end_reorder": "पुनः क्रम समाप्त करें",
|
||||
"modal_heading_section": "नया अनुभाग जोड़ें",
|
||||
"lesson_section_title": "अनुभाग शीर्षक",
|
||||
"cancel": "रद्द करना"
|
||||
},
|
||||
"disabled": "सुविधा अक्षम है",
|
||||
"next": "अगला",
|
||||
|
||||
@@ -754,7 +754,10 @@
|
||||
"lock": "Trancar",
|
||||
"unlock": "Desbloquear",
|
||||
"start_reorder": "Reordenar",
|
||||
"end_reorder": "Finalizar novo pedido"
|
||||
"end_reorder": "Finalizar novo pedido",
|
||||
"modal_heading_section": "Adicionar nova seção",
|
||||
"lesson_section_title": "Título da seção",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"disabled": "O recurso está desativado",
|
||||
"next": "Próximo",
|
||||
|
||||
@@ -754,7 +754,10 @@
|
||||
"lock": "Замок",
|
||||
"unlock": "Разблокировать",
|
||||
"start_reorder": "Изменение порядка",
|
||||
"end_reorder": "Завершить изменение порядка"
|
||||
"end_reorder": "Завершить изменение порядка",
|
||||
"modal_heading_section": "Добавить новый раздел",
|
||||
"lesson_section_title": "Название раздела",
|
||||
"cancel": "Отмена"
|
||||
},
|
||||
"disabled": "Функция отключена",
|
||||
"next": "Следующий",
|
||||
|
||||
@@ -754,7 +754,10 @@
|
||||
"lock": "Khóa",
|
||||
"unlock": "Mở khóa",
|
||||
"start_reorder": "Sắp xếp lại",
|
||||
"end_reorder": "Kết thúc sắp xếp lại"
|
||||
"end_reorder": "Kết thúc sắp xếp lại",
|
||||
"modal_heading_section": "Thêm phần mới",
|
||||
"lesson_section_title": "Tiêu đề phần",
|
||||
"cancel": "Hủy bỏ"
|
||||
},
|
||||
"disabled": "Tính năng bị vô hiệu hóa",
|
||||
"next": "Kế tiếp",
|
||||
|
||||
@@ -194,10 +194,15 @@ export enum COURSE_TYPE {
|
||||
SELF_PACED = 'SELF_PACED',
|
||||
LIVE_CLASS = 'LIVE_CLASS'
|
||||
}
|
||||
export enum COURSE_VERSION {
|
||||
V1 = 'V1', // with only lesson
|
||||
V2 = 'V2' // lessons are grouped into sections
|
||||
}
|
||||
export interface Course {
|
||||
title?: any; // type unknown;
|
||||
description: string; // type unknown;
|
||||
type: COURSE_TYPE;
|
||||
version: COURSE_VERSION;
|
||||
overview?: any; // type unknown;
|
||||
id?: string /* primary key */;
|
||||
created_at: string;
|
||||
@@ -225,6 +230,7 @@ export interface Course {
|
||||
is_present: boolean;
|
||||
id: number;
|
||||
}[];
|
||||
lesson_section?: LessonSection[];
|
||||
lessons?: Lesson[];
|
||||
polls: { status: string }[];
|
||||
}
|
||||
@@ -271,6 +277,7 @@ export enum VideoType {
|
||||
|
||||
export interface LessonPage {
|
||||
id?: string | null;
|
||||
title: '';
|
||||
totalExercises: number;
|
||||
totalComments: number;
|
||||
locale: LOCALE;
|
||||
@@ -305,6 +312,7 @@ export interface Lesson {
|
||||
videos?: []; // type unknown;
|
||||
slide_url?: any; // type unknown;
|
||||
course_id: string /* foreign key to course.id */;
|
||||
section_id?: string /* foreign key to course.id */;
|
||||
id: string /* primary key */;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
@@ -318,6 +326,16 @@ export interface Lesson {
|
||||
course?: Course;
|
||||
profile?: Profile;
|
||||
lesson_completion: LessonCompletion[];
|
||||
totalExercises?: { count: number }[];
|
||||
}
|
||||
|
||||
export interface LessonSection {
|
||||
id: string;
|
||||
title: string;
|
||||
order: number;
|
||||
course_id: string /* foreign key to course.id */;
|
||||
lessons: Lesson[];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Exercise {
|
||||
|
||||
@@ -1,37 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { dndzone } from 'svelte-dnd-action';
|
||||
import PageNav from '$lib/components/PageNav/index.svelte';
|
||||
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 { updateLesson } from '$lib/utils/services/courses';
|
||||
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
|
||||
import {
|
||||
lessons,
|
||||
handleDelete,
|
||||
handleSaveLesson
|
||||
lessonSections
|
||||
} from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import PageBody from '$lib/components/PageBody/index.svelte';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import ScreenMap from 'carbon-icons-svelte/lib/ScreenMap.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';
|
||||
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import Select from '$lib/components/Form/Select.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import Avatar from '$lib/components/Apps/components/Poll/components/Avatar.svelte';
|
||||
import { course, group } from '$lib/components/Course/store';
|
||||
import DateField from '$lib/components/Form/Date.svelte';
|
||||
import type { Course, Lesson } from '$lib/utils/types';
|
||||
import { handleAddLessonWidget } from '$lib/components/Course/components/Lesson/store';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import type { Lesson } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { goto } from '$app/navigation';
|
||||
import { COURSE_TYPE } from '$lib/utils/types';
|
||||
import { COURSE_VERSION } from '$lib/utils/types';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants.js';
|
||||
import { profile } from '$lib/utils/store/user.js';
|
||||
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';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -43,65 +34,9 @@
|
||||
let isFetching: boolean = false;
|
||||
let reorder = false;
|
||||
|
||||
const flipDurationMs = 300;
|
||||
|
||||
let errors: Record<string, string> = {};
|
||||
|
||||
async function saveLesson(lesson: Lesson, courseId: Course['id']) {
|
||||
const validationRes = await handleSaveLesson(lesson, courseId);
|
||||
|
||||
if (validationRes && Object.keys(validationRes).length) {
|
||||
errors = validationRes;
|
||||
} else {
|
||||
errors = {};
|
||||
lessonEditing = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function addLesson() {
|
||||
$handleAddLessonWidget.open = true;
|
||||
}
|
||||
|
||||
function handleDndConsider(e: any) {
|
||||
$lessons = e.detail.items;
|
||||
}
|
||||
|
||||
function handleDndFinalize(e: any) {
|
||||
const prevLessonsByOrder = $lessons.reduce((acc, l) => ({ ...acc, [l.id]: l.order }), {});
|
||||
$lessons = e.detail.items;
|
||||
|
||||
// Only update the lesson order that changed
|
||||
e.detail.items.forEach((item: { id: string }, index: number) => {
|
||||
const order = index + 1;
|
||||
|
||||
if (order !== prevLessonsByOrder[item.id]) {
|
||||
$lessons[index].order = order;
|
||||
updateLesson({ order }, item.id).then((update) =>
|
||||
console.log(`updated lesson order`, update)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatDate(date: string | undefined) {
|
||||
if (!date) return '';
|
||||
const d = new Date(date);
|
||||
|
||||
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(d);
|
||||
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(d);
|
||||
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(d);
|
||||
|
||||
return `${ye}-${mo}-${da}`;
|
||||
}
|
||||
|
||||
function getLessonOrder(id: string) {
|
||||
const index = $lessons.findIndex((lesson) => lesson.id === id);
|
||||
|
||||
if (index < 9) {
|
||||
return '0' + (index + 1);
|
||||
} else {
|
||||
return index + 1;
|
||||
}
|
||||
$handleAddLessonWidget.isSection = true;
|
||||
}
|
||||
|
||||
function hasUserCompletedLesson(completion) {
|
||||
@@ -128,16 +63,14 @@
|
||||
|
||||
$: shouldGoToNextLesson = query.get('next') === 'true';
|
||||
$: !isFetching && shouldGoToNextLesson && onNextQuery($lessons);
|
||||
$: isLiveClass = $course.type === COURSE_TYPE.LIVE_CLASS;
|
||||
$: lessonsLength =
|
||||
$course.version === COURSE_VERSION.V1 ? $lessons.length : $lessonSections.length;
|
||||
</script>
|
||||
|
||||
{#if $handleAddLessonWidget}
|
||||
<NewLessonModal />
|
||||
{/if}
|
||||
<NewLessonModal />
|
||||
|
||||
<DeleteLessonConfirmation
|
||||
bind:openDeleteModal
|
||||
lessonTitle={lessonToDelete?.title || ''}
|
||||
deleteLesson={() => handleDelete(lessonToDelete?.id)}
|
||||
/>
|
||||
|
||||
@@ -150,14 +83,16 @@
|
||||
onClick={addLesson}
|
||||
isDisabled={!!lessonEditing}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t(
|
||||
`course.navItem.lessons.add_lesson.${reorder ? 'end_reorder' : 'start_reorder'}`
|
||||
)}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => (reorder = !reorder)}
|
||||
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>
|
||||
@@ -173,210 +108,18 @@
|
||||
</p>
|
||||
</div>
|
||||
</Box>
|
||||
{:else if $lessons.length}
|
||||
{:else if lessonsLength > 0}
|
||||
{#if reorder}
|
||||
<p class="text-gray-400 text-center dark:text-white italic text-xs">
|
||||
{$t('course.navItem.lessons.drag')}
|
||||
</p>
|
||||
{/if}
|
||||
<section
|
||||
class="m-auto w-full p-3 lg:w-11/12 lg:px-4"
|
||||
use:dndzone={{
|
||||
items: $lessons,
|
||||
flipDurationMs,
|
||||
dragDisabled: $globalStore.isStudent || !reorder,
|
||||
dropTargetStyle: {
|
||||
border: '2px #1d4ed8 solid',
|
||||
'border-style': 'dashed'
|
||||
}
|
||||
}}
|
||||
on:consider={handleDndConsider}
|
||||
on:finalize={handleDndFinalize}
|
||||
>
|
||||
{#each $lessons as lesson (lesson.id)}
|
||||
<div
|
||||
class={`relative m-auto mb-4 flex max-w-xl items-center rounded-md border-2 ${
|
||||
reorder ? 'border-primary-400' : 'border-gray-200'
|
||||
} border-gray-200 dark:border-neutral-600 p-5 dark:bg-neutral-800`}
|
||||
>
|
||||
<!-- Number Chip -->
|
||||
<div class="mr-5">
|
||||
<TextChip
|
||||
value={getLessonOrder(lesson.id)}
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
className="bg-primary-200 text-primary-600 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Lesson Content -->
|
||||
<div class="w-4/5">
|
||||
<!-- Lesson Title -->
|
||||
{#if lessonEditing === lesson.id}
|
||||
<TextField
|
||||
bind:value={lesson.title}
|
||||
autoFocus={true}
|
||||
className="max-w-lg"
|
||||
errorMessage={errors?.title}
|
||||
/>
|
||||
{:else}
|
||||
<h3 class="m-0 flex items-center text-lg dark:text-white">
|
||||
<a
|
||||
href={$globalStore.isStudent && !lesson.is_unlocked
|
||||
? $page.url.pathname
|
||||
: '/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'
|
||||
: ''}"
|
||||
>
|
||||
{lesson.title}
|
||||
</a>
|
||||
</h3>
|
||||
{/if}
|
||||
|
||||
<!-- Lesson Educator -->
|
||||
{#if isLiveClass}
|
||||
<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"
|
||||
/>
|
||||
<p class="ml-2 text-sm dark:text-white">
|
||||
{lesson.profile.fullname}
|
||||
</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')}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="mt-2 flex w-4/5 flex-col items-start justify-between lg:flex-row lg:items-center"
|
||||
>
|
||||
<!-- Lesson Length -->
|
||||
<div class="mb-3 flex items-center lg:mb-0">
|
||||
<ScreenMap size={20} class="carbon-icon dark:text-white" />
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{lesson?.totalExercises ? lesson?.totalExercises?.map((c) => c.count) : 0}
|
||||
{$t('exercises.heading')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- 3 dot edit -->
|
||||
<div class="flex flex-row">
|
||||
<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')}
|
||||
on:click={() => {
|
||||
lesson.is_unlocked = !lesson.is_unlocked;
|
||||
handleSaveLesson(lesson, $course.id);
|
||||
}}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
text={lessonEditing === lesson.id
|
||||
? $t('course.navItem.lessons.add_lesson.save')
|
||||
: $t('course.navItem.lessons.add_lesson.edit')}
|
||||
on:click={() => {
|
||||
if (lessonEditing === lesson.id) {
|
||||
saveLesson(lesson, $course.id);
|
||||
} else {
|
||||
lessonEditing = lesson.id;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
danger
|
||||
text={$t('course.navItem.lessons.add_lesson.delete')}
|
||||
on:click={() => {
|
||||
lessonToDelete = lesson;
|
||||
openDeleteModal = true;
|
||||
}}
|
||||
/>
|
||||
</OverflowMenu>
|
||||
</RoleBasedSecurity>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Box>
|
||||
<div class="flex justify-between flex-col items-center w-[90%] md:w-96">
|
||||
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
|
||||
<h2 class="text-xl my-1.5 font-normal">{$t('course.navItem.lessons.body_header')}</h2>
|
||||
<p class="text-sm text-center text-slate-500">
|
||||
{$t('course.navItem.lessons.body_content')}
|
||||
</p>
|
||||
</div>
|
||||
</Box>
|
||||
{/each}
|
||||
</section>
|
||||
{#if $course.version === COURSE_VERSION.V1}
|
||||
<LessonList {reorder} {lessonEditing} bind:lessonToDelete bind:openDeleteModal />
|
||||
{:else if $course.version === COURSE_VERSION.V2}
|
||||
<LessonSectionList {lessonEditing} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Box className="w-full lg:w-11/12 lg:px-4 m-auto">
|
||||
<div class="flex justify-between flex-col items-center">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { Dropdown, Loading } from 'carbon-components-svelte';
|
||||
import { Dropdown } from 'carbon-components-svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import PageNav from '$lib/components/PageNav/index.svelte';
|
||||
import PageBody from '$lib/components/PageBody/index.svelte';
|
||||
@@ -198,6 +198,7 @@
|
||||
lesson.update((l) => ({
|
||||
...l,
|
||||
id: data.lessonId,
|
||||
title: lessonData.title,
|
||||
totalExercises,
|
||||
totalComments: totalComments,
|
||||
materials: {
|
||||
|
||||
Reference in New Issue
Block a user