feat: lesson creation

This commit is contained in:
Jannat Patel
2024-03-05 23:07:58 +05:30
parent b9f6a23412
commit 0ce7c74778
11 changed files with 409 additions and 155 deletions

View File

@@ -7,7 +7,7 @@
<div class="text-lg font-semibold"> <div class="text-lg font-semibold">
{{ __(title) }} {{ __(title) }}
</div> </div>
<Button @click="openChapterModal()"> <Button v-if="allowEdit" @click="openChapterModal()">
{{ __('Add Chapter') }} {{ __('Add Chapter') }}
</Button> </Button>
<!-- <span class="font-medium cursor-pointer" @click="expandAllChapters()"> <!-- <span class="font-medium cursor-pointer" @click="expandAllChapters()">
@@ -25,7 +25,7 @@
:key="chapter.name" :key="chapter.name"
:defaultOpen="openChapterDetail(chapter.idx)" :defaultOpen="openChapterDetail(chapter.idx)"
> >
<DisclosureButton ref="" class="flex w-full px-2 py-4"> <DisclosureButton ref="" class="flex w-full px-2 py-3">
<ChevronRight <ChevronRight
:class="{ :class="{
'rotate-90 transform duration-200': open, 'rotate-90 transform duration-200': open,
@@ -34,20 +34,16 @@
}" }"
class="h-4 w-4 text-gray-900 stroke-1 mr-2" class="h-4 w-4 text-gray-900 stroke-1 mr-2"
/> />
<div class="text-base text-left font-medium"> <div class="text-base text-left font-medium leading-5">
{{ chapter.title }} {{ chapter.title }}
</div> </div>
<div class="ml-auto text-sm">
{{ chapter.lessons.length }}
{{ chapter.lessons.length == 1 ? __('lesson') : __('lessons') }}
</div>
</DisclosureButton> </DisclosureButton>
<DisclosurePanel class="pb-2"> <DisclosurePanel class="pb-2">
<div v-for="lesson in chapter.lessons" :key="lesson.name"> <div v-for="lesson in chapter.lessons" :key="lesson.name">
<div class="outline-lesson pl-8 py-2"> <div class="outline-lesson pl-8 py-2">
<router-link <router-link
:to="{ :to="{
name: 'Lesson', name: allowEdit ? 'CreateLesson' : 'Lesson',
params: { params: {
courseName: courseName, courseName: courseName,
chapterNumber: lesson.number.split('.')[0], chapterNumber: lesson.number.split('.')[0],
@@ -55,7 +51,7 @@
}, },
}" }"
> >
<div class="flex items-center text-sm"> <div class="flex items-center text-sm leading-5">
<MonitorPlay <MonitorPlay
v-if="lesson.icon === 'icon-youtube'" v-if="lesson.icon === 'icon-youtube'"
class="h-4 w-4 text-gray-900 stroke-1 mr-2" class="h-4 w-4 text-gray-900 stroke-1 mr-2"

View File

@@ -3,7 +3,7 @@
v-model="show" v-model="show"
:options="{ :options="{
title: __('Create a Batch'), title: __('Create a Batch'),
size: 'xl', size: '3xl',
actions: [ actions: [
{ {
label: __('Save'), label: __('Save'),
@@ -15,14 +15,19 @@
> >
<template #body-content> <template #body-content>
<div> <div>
<FormControl v-model="batch.title" :label="__('Title')" class="mb-4" /> <div class="grid grid-cols-3 gap-4">
<FormControl <div>
v-model="batch.published" <FormControl
type="checkbox" v-model="batch.title"
:label="__('Published')" :label="__('Title')"
class="mb-4" class="mb-4"
/> />
<div class="grid grid-cols-2 gap-4"> <FormControl
v-model="batch.published"
type="checkbox"
:label="__('Published')"
/>
</div>
<div> <div>
<FormControl <FormControl
v-model="batch.start_date" v-model="batch.start_date"
@@ -52,19 +57,7 @@
/> />
</div> </div>
</div> </div>
<div class="grid grid-cols-2"> <div class="grid grid-cols-3 gap-4 mt-4 border-t pt-4">
<div>
<FormControl
v-model="batch.medium"
:label="__('Medium')"
class="mb-4"
/>
<FormControl
v-model="batch.category"
:label="__('Category')"
class="mb-4"
/>
</div>
<div> <div>
<FormControl <FormControl
v-model="batch.seat_count" v-model="batch.seat_count"
@@ -79,68 +72,103 @@
class="mb-4" class="mb-4"
/> />
</div> </div>
<div>
<FormControl
v-model="batch.medium"
:label="__('Medium')"
class="mb-4"
/>
<FormControl
v-model="batch.category"
:label="__('Category')"
class="mb-4"
/>
</div>
<div>
<FileUploader
v-if="!batch.meta_image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="
(file) => {
batch.meta_image.value = file
}
"
>
<template
v-slot="{ file, progress, uploading, openFileSelector }"
>
<div class="mb-4">
<Button @click="openFileSelector" :loading="uploading">
{{
uploading ? `Uploading ${progress}%` : 'Upload an image'
}}
</Button>
</div>
</template>
</FileUploader>
</div>
</div> </div>
<div class="border-t pt-4 mb-4">
<FormControl
v-model="batch.paid_batch"
type="checkbox"
:label="__('Paid Batch')"
/>
<FormControl
v-model="batch.amount"
:label="__('Amount')"
type="number"
class="my-4"
/>
<Link
doctype="Currency"
v-model="batch.currency"
:filters="{ enabled: 1 }"
:label="__('Currency')"
/>
</div>
<div class="grid grid-cols-2 gap-4 border-y pt-4 mb-4"></div>
<FormControl <FormControl
v-model="batch.description" v-model="batch.description"
:label="__('Description')" :label="__('Description')"
type="textarea" type="textarea"
class="mb-4" class="mb-4"
/> />
<TextEditor <div>
v-model="batch.batch_details" <label class="block text-sm text-gray-600 mb-1">
:label="__('Batch Details')" {{ __('Batch Details') }}
class="mb-4" </label>
/> <TextEditor
:content="batch.batch_details"
@change="(val) => (batch.batch_details = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-gray-100 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
/>
</div>
<FormControl <FormControl
v-model="batch_details.raw" v-model="batch.batch_details_raw"
:label="__('Batch Details')" :label="__('Batch Details Raw')"
type="textarea" type="textarea"
class="mb-4" class="mb-4"
/> />
<FileUploader
v-if="!image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="
(file) => {
image = file
}
"
>
<template v-slot="{ file, progress, uploading, openFileSelector }">
<div class="mb-4">
<Button @click="openFileSelector" :loading="uploading">
{{ uploading ? `Uploading ${progress}%` : 'Upload an image' }}
</Button>
</div>
</template>
</FileUploader>
<div>
<FormControl
v-model="batch.paid_batch"
type="checkbox"
:label="__('Paid Batch')"
class="mb-4"
/>
<FormControl
v-model="batch.amount"
:label="__('Amount')"
type="number"
class="mb-4"
/>
<Link
doctype="Currency"
v-model="course.currency"
:filters="{ enabled: 1 }"
:label="__('Currency')"
/>
</div>
</div> </div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, FormControl, TextEditor, FileUploader, Link } from 'frappe-ui' import {
Dialog,
FormControl,
TextEditor,
FileUploader,
Button,
} from 'frappe-ui'
import { reactive, defineModel } from 'vue'
import Link from '@/components/Controls/Link.vue'
const show = defineModel()
const batch = reactive({ const batch = reactive({
title: '', title: '',

View File

@@ -15,7 +15,7 @@
</header> </header>
<div v-if="batch.data" class="grid grid-cols-[70%,30%] h-full"> <div v-if="batch.data" class="grid grid-cols-[70%,30%] h-full">
<div class="border-r-2"> <div class="border-r-2">
<Tabs class="overflow-hidden" v-model="tabIndex" :tabs="tabs"> <Tabs v-model="tabIndex" :tabs="tabs" tablistClass="overflow-x-visible">
<template #tab="{ tab, selected }" class="overflow-x-hidden"> <template #tab="{ tab, selected }" class="overflow-x-hidden">
<div> <div>
<button <button

View File

@@ -37,8 +37,8 @@
<div class="grid grid-cols-[60%,20%] gap-20 mt-10"> <div class="grid grid-cols-[60%,20%] gap-20 mt-10">
<div class=""> <div class="">
<div <div
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none !whitespace-normal mt-6"
v-html="batch.data.batch_details" v-html="batch.data.batch_details"
class="batch-description"
></div> ></div>
</div> </div>
<div> <div>
@@ -46,7 +46,7 @@
</div> </div>
</div> </div>
<div> <div>
<div class="text-2xl font-semibold"> <div class="text-2xl font-semibold mt-10">
{{ __('Courses') }} {{ __('Courses') }}
</div> </div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 mt-5"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 mt-5">

View File

@@ -8,7 +8,7 @@
:items="[{ label: __('All Batches'), route: { name: 'Batches' } }]" :items="[{ label: __('All Batches'), route: { name: 'Batches' } }]"
/> />
<div class="flex"> <div class="flex">
<Button variant="solid"> <Button variant="solid" @click="openBatchModal()">
<template #prefix> <template #prefix>
<Plus class="h-4 w-4" /> <Plus class="h-4 w-4" />
</template> </template>
@@ -73,14 +73,18 @@
</Tabs> </Tabs>
</div> </div>
</div> </div>
<BatchCreation v-model="showBatchModal" />
</template> </template>
<script setup> <script setup>
import { createListResource, Breadcrumbs, Button, Tabs, Badge } from 'frappe-ui' import { createListResource, Breadcrumbs, Button, Tabs, Badge } from 'frappe-ui'
import { Plus } from 'lucide-vue-next' import { Plus } from 'lucide-vue-next'
import BatchCard from '@/components/BatchCard.vue' import BatchCard from '@/components/BatchCard.vue'
import { inject, ref, computed } from 'vue' import { inject, ref, computed } from 'vue'
import BatchCreation from '@/components/Modals/BatchCreation.vue'
const user = inject('$user') const user = inject('$user')
const showBatchModal = ref(false)
const batches = createListResource({ const batches = createListResource({
doctype: 'LMS Batch', doctype: 'LMS Batch',
url: 'lms.lms.utils.get_batches', url: 'lms.lms.utils.get_batches',
@@ -116,4 +120,8 @@ if (user.data) {
count: computed(() => batches.data?.enrolled?.length), count: computed(() => batches.data?.enrolled?.length),
}) })
} }
const openBatchModal = () => {
showBatchModal.value = true
}
</script> </script>

View File

@@ -321,6 +321,7 @@ const courseCreationResource = createResource({
}) })
const submitCourse = () => { const submitCourse = () => {
console.log(courseResource.doc?.modified)
if (courseResource.doc) { if (courseResource.doc) {
courseResource.setValue.submit( courseResource.setValue.submit(
{ {
@@ -331,8 +332,11 @@ const submitCourse = () => {
validate() { validate() {
return validateMandatoryFields() return validateMandatoryFields()
}, },
onSuccess() {
showToast('Success', 'Course updated successfully', 'check')
},
onError(err) { onError(err) {
showToast(err) showToast('Error', err.messages?.[0] || err, 'x')
}, },
} }
) )
@@ -341,6 +345,9 @@ const submitCourse = () => {
validate() { validate() {
return validateMandatoryFields() return validateMandatoryFields()
}, },
onSuccess() {
showToast('Success', 'Course created successfully', 'check')
},
onError(err) { onError(err) {
showToast(err) showToast(err)
}, },
@@ -392,14 +399,17 @@ const removeTag = (tag) => {
newTag.value = '' newTag.value = ''
} }
const showToast = (err) => { const showToast = (title, text, icon) => {
createToast({ createToast({
title: 'Error', title: title,
text: err.messages?.[0] || err, text: text,
icon: 'x', icon: icon,
iconClasses: 'bg-red-600 text-white rounded-md p-px', iconClasses:
position: 'top-center', icon == 'check'
timeout: 10, ? 'bg-green-600 text-white rounded-md p-px'
: 'bg-red-600 text-white rounded-md p-px',
position: icon == 'check' ? 'bottom-right' : 'top-center',
timeout: icon == 'check' ? 5 : 10,
}) })
} }

View File

@@ -5,9 +5,14 @@
> >
<Breadcrumbs :items="breadcrumbs" /> <Breadcrumbs :items="breadcrumbs" />
</header> </header>
<div class="w-7/12 mx-auto pt-5"> <div class="w-7/12 mx-auto py-5">
<div class="text-lg font-semibold mb-5"> <div class="flex items-center justify-between mb-5">
{{ __('Lesson Details') }} <div class="text-lg font-semibold">
{{ __('Lesson Details') }}
</div>
<Button variant="solid" @click="saveLesson()">
{{ __('Save') }}
</Button>
</div> </div>
<FormControl v-model="lesson.title" label="Title" class="mb-4" /> <FormControl v-model="lesson.title" label="Title" class="mb-4" />
<FormControl <FormControl
@@ -15,24 +20,40 @@
type="checkbox" type="checkbox"
label="Include in Preview" label="Include in Preview"
/> />
<div class="mt-4">
<label class="block text-xs text-gray-600 mb-1">
{{ __('Instructor Notes') }}
</label>
<div id="instructor-notes" class="border rounded-md px-10 py-3"></div>
</div>
<div class="mt-4"> <div class="mt-4">
<label class="block text-xs text-gray-600 mb-1"> <label class="block text-xs text-gray-600 mb-1">
{{ __('Content') }} {{ __('Content') }}
</label> </label>
<div id="content" class="border rounded-md px-10 py-3"></div> <div id="content" class="border rounded-md py-3"></div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { Breadcrumbs, FormControl, createResource } from 'frappe-ui' import {
import { computed, reactive, onMounted } from 'vue' Breadcrumbs,
FormControl,
createResource,
Button,
createDocumentResource,
} from 'frappe-ui'
import { computed, reactive, onMounted, onBeforeMount } from 'vue'
import EditorJS from '@editorjs/editorjs' import EditorJS from '@editorjs/editorjs'
import Header from '@editorjs/header' import Header from '@editorjs/header'
import Paragraph from '@editorjs/paragraph' import Paragraph from '@editorjs/paragraph'
import List from '@editorjs/list' import List from '@editorjs/list'
import Embed from '@editorjs/embed' import Embed from '@editorjs/embed'
import YouTubeVideo from '../utils/youtube.js' import YouTubeVideo from '../utils/youtube.js'
import { createToast } from '../utils'
let editor
let editLessonResource
const props = defineProps({ const props = defineProps({
courseName: { courseName: {
@@ -49,47 +70,56 @@ const props = defineProps({
}, },
}) })
onMounted( onMounted(() => {
() => editor = renderEditor('content')
new EditorJS({ /* renderEditor('instructor-notes') */
holder: 'content', })
tools: {
header: Header, const renderEditor = (holder) => {
youtube: YouTubeVideo, return new EditorJS({
paragraph: { holder: holder,
class: Paragraph, tools: getEditorTools(),
inlineToolbar: true, })
config: { }
preserveBlank: true,
}, const getEditorTools = () => {
}, return {
list: List, header: Header,
embed: { youtube: YouTubeVideo,
class: Embed, paragraph: {
config: { class: Paragraph,
services: { inlineToolbar: true,
youtube: true, config: {
vimeo: true, preserveBlank: true,
codepen: true, },
slides: { },
regex: list: List,
/https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/, embed: {
embedUrl: class: Embed,
'https://docs.google.com/presentation/d/e/<%= remote_id %>/embed', config: {
html: "<iframe width='100%' height='300' frameborder='0' allowfullscreen='true'></iframe>", services: {
}, youtube: true,
}, vimeo: true,
codepen: true,
slides: {
regex:
/https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/,
embedUrl:
'https://docs.google.com/presentation/d/e/<%= remote_id %>/embed',
html: "<iframe width='100%' height='300' frameborder='0' allowfullscreen='true'></iframe>",
}, },
}, },
}, },
}) },
) }
}
const lesson = reactive({ const lesson = reactive({
title: '', title: '',
include_in_preview: false, include_in_preview: false,
body: '', body: 'Test',
instructor_notes: '', instructor_notes: '',
content: '',
}) })
const lessonDetails = createResource({ const lessonDetails = createResource({
@@ -100,8 +130,136 @@ const lessonDetails = createResource({
lesson: props.lessonNumber, lesson: props.lessonNumber,
}, },
auto: true, auto: true,
onSuccess(data) {
if (data.lesson) {
createEditResource(data)
}
},
}) })
const newLessonResource = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'Course Lesson',
course: props.courseName,
chapter: lessonDetails.data?.chapter.name,
...lesson,
},
}
},
})
const createEditResource = (data) => {
editLessonResource = createDocumentResource({
doctype: 'Course Lesson',
name: data.lesson,
auto: true,
onSuccess(data) {
Object.keys(data).forEach((key) => {
lesson[key] = data[key]
})
lesson.include_in_preview = data.include_in_preview ? true : false
console.log(editor)
console.log(editor.isReady)
editor.isReady.then(() => {
editor.render(JSON.parse(data.content))
})
},
})
}
const lessonReference = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'Lesson Reference',
parent: lessonDetails.data?.chapter.name,
parenttype: 'Course Chapter',
parentfield: 'lessons',
lesson: values.lesson,
idx: props.lessonNumber,
},
}
},
})
const saveLesson = () => {
editor.save().then((outputData) => {
lesson.content = JSON.stringify(outputData)
console.log(editLessonResource?.doc?.modified)
if (editLessonResource?.doc) {
editLessonResource.setValue.submit(
{
...lesson,
},
{
validate() {
return validateLesson()
},
onSuccess() {
showToast('Success', 'Lesson updated successfully', 'check')
},
onError(err) {
showToast('Error', err.message, 'x')
},
}
)
} else {
createNewLesson()
}
})
}
const createNewLesson = () => {
newLessonResource.submit(
{},
{
validate() {
return validateLesson()
},
onSuccess(data) {
lessonReference.submit(
{ lesson: data.name },
{
onSuccess() {
showToast('Success', 'Lesson created successfully', 'check')
},
}
)
},
onError(err) {
showToast('Error', err.message, 'x')
},
}
)
}
const validateLesson = () => {
if (!lesson.title) {
return 'Title is required'
}
if (!lesson.content) {
return 'Content is required'
}
}
const showToast = (title, text, icon) => {
createToast({
title: title,
text: text,
icon: icon,
iconClasses:
icon == 'check'
? 'bg-green-600 text-white rounded-md p-px'
: 'bg-red-600 text-white rounded-md p-px',
position: icon == 'check' ? 'bottom-right' : 'top-center',
timeout: icon == 'check' ? 5 : 10,
})
}
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
let crumbs = [ let crumbs = [
{ {
@@ -114,8 +272,21 @@ const breadcrumbs = computed(() => {
}, },
] ]
if (editLessonResource?.doc) {
crumbs.push({
label: editLessonResource.doc.title,
route: {
name: 'Lesson',
params: {
courseName: props.courseName,
chapterNumber: props.chapterNumber,
lessonNumber: props.lessonNumber,
},
},
})
}
crumbs.push({ crumbs.push({
label: 'Create Lesson', label: editLessonResource?.doc ? 'Edit Lesson' : 'Create Lesson',
route: { route: {
name: 'CreateLesson', name: 'CreateLesson',
params: { params: {

View File

@@ -27,7 +27,7 @@
<div class="text-3xl font-semibold"> <div class="text-3xl font-semibold">
{{ lesson.data.title }} {{ lesson.data.title }}
</div> </div>
<div> <div class="flex items-center">
<router-link <router-link
v-if="lesson.data.prev" v-if="lesson.data.prev"
:to="{ :to="{
@@ -40,7 +40,27 @@
}" }"
> >
<Button class="mr-2"> <Button class="mr-2">
<ChevronLeft class="w-4 h-4 stroke-1" /> <template #prefix>
<ChevronLeft class="w-4 h-4 stroke-1" />
</template>
<span>
{{ __('Previous') }}
</span>
</Button>
</router-link>
<router-link
v-if="allowEdit()"
:to="{
name: 'CreateLesson',
params: {
courseName: courseName,
chapterNumber: props.chapterNumber,
lessonNumber: props.lessonNumber,
},
}"
>
<Button class="mr-2">
{{ __('Edit') }}
</Button> </Button>
</router-link> </router-link>
<router-link <router-link
@@ -55,7 +75,12 @@
}" }"
> >
<Button> <Button>
<ChevronRight class="w-4 h-4 stroke-1" /> <template #suffix>
<ChevronRight class="w-4 h-4 stroke-1" />
</template>
<span>
{{ __('Next') }}
</span>
</Button> </Button>
</router-link> </router-link>
</div> </div>
@@ -86,6 +111,18 @@
</span> </span>
</div> </div>
<div <div
v-if="lesson.data.content"
v-for="content in JSON.parse(lesson.data.content).blocks"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none !whitespace-normal mt-6"
>
<div v-if="content.type == 'paragraph'">
<div>
{{ content.data.text }}
</div>
</div>
</div>
<div
v-else
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none !whitespace-normal mt-6" class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none !whitespace-normal mt-6"
> >
<div v-if="lesson.data.youtube"> <div v-if="lesson.data.youtube">
@@ -346,6 +383,16 @@ const allowDiscussions = () => {
user.data?.is_instructor user.data?.is_instructor
) )
} }
const allowEdit = () => {
if (user.data?.is_instructor) {
return true
}
if (lesson.data?.instructor.includes(user.data?.name)) {
return true
}
return false
}
</script> </script>
<style> <style>
.avatar-group { .avatar-group {

View File

@@ -197,13 +197,13 @@ const courseCompletion = createResource({
const signupChartOptions = () => { const signupChartOptions = () => {
let options = chartOptions(false) let options = chartOptions(false)
options.plugins.title.text = 'New Signups' options.plugins.title.text = 'New Signups'
options.borderColor = '#278F5E' options.borderColor = '#4563f0'
options.backgroundColor = (ctx) => { options.backgroundColor = (ctx) => {
const canvas = ctx.chart.ctx const canvas = ctx.chart.ctx
const gradient = canvas.createLinearGradient(0, 0, 0, 160) const gradient = canvas.createLinearGradient(0, 0, 0, 160)
gradient.addColorStop(0, '#B6DEC5') gradient.addColorStop(0, '#4563f0')
gradient.addColorStop(0.5, '#E4F5E9') gradient.addColorStop(0.5, '#e8ecfe')
gradient.addColorStop(1, '#F3FCF5') gradient.addColorStop(1, '#f6f7ff')
return gradient return gradient
} }
@@ -213,13 +213,13 @@ const signupChartOptions = () => {
const enrollmentChartOptions = () => { const enrollmentChartOptions = () => {
let options = chartOptions(false) let options = chartOptions(false)
options.plugins.title.text = 'Course Enrollments' options.plugins.title.text = 'Course Enrollments'
options.borderColor = '#B52A2A' options.borderColor = '#4563f0'
options.backgroundColor = (ctx) => { options.backgroundColor = (ctx) => {
const canvas = ctx.chart.ctx const canvas = ctx.chart.ctx
const gradient = canvas.createLinearGradient(0, 0, 0, 160) const gradient = canvas.createLinearGradient(0, 0, 0, 160)
gradient.addColorStop(0, '#FFC6A5') gradient.addColorStop(0, '#4563f0')
gradient.addColorStop(0.5, '#FFD8C5') gradient.addColorStop(0.5, '#e8ecfe')
gradient.addColorStop(1, '#FFE9E5') gradient.addColorStop(1, '#f6f7ff')
return gradient return gradient
} }
@@ -229,13 +229,13 @@ const enrollmentChartOptions = () => {
const lessonChartOptions = () => { const lessonChartOptions = () => {
let options = chartOptions(false) let options = chartOptions(false)
options.plugins.title.text = 'Lesson Completion' options.plugins.title.text = 'Lesson Completion'
options.borderColor = '#0070CC' options.borderColor = '#4563f0'
options.backgroundColor = (ctx) => { options.backgroundColor = (ctx) => {
const canvas = ctx.chart.ctx const canvas = ctx.chart.ctx
const gradient = canvas.createLinearGradient(0, 0, 0, 160) const gradient = canvas.createLinearGradient(0, 0, 0, 160)
gradient.addColorStop(0, '#B6DEC5') gradient.addColorStop(0, '#B6DEC5')
gradient.addColorStop(0.5, '#E4F5E9') gradient.addColorStop(0.5, '#e8ecfe')
gradient.addColorStop(1, '#F3FCF5') gradient.addColorStop(1, '#f6f7ff')
return gradient return gradient
} }
@@ -245,7 +245,7 @@ const lessonChartOptions = () => {
const courseChartOptions = () => { const courseChartOptions = () => {
let options = chartOptions(true) let options = chartOptions(true)
options.plugins.title.text = 'Course Completion' options.plugins.title.text = 'Course Completion'
options.backgroundColor = ['#E4521B', '#FEEB65'] options.backgroundColor = ['#4563f0', '#f683ae']
return options return options
} }

View File

@@ -20,7 +20,6 @@ const routes = [
props: true, props: true,
}, },
{ {
// Create a route for path /courses/inventory-management/learn/1.1
path: '/courses/:courseName/learn/:chapterNumber-:lessonNumber', path: '/courses/:courseName/learn/:chapterNumber-:lessonNumber',
name: 'Lesson', name: 'Lesson',
component: () => import('@/pages/Lesson.vue'), component: () => import('@/pages/Lesson.vue'),

View File

@@ -1338,6 +1338,7 @@ def get_lesson(course, chapter, lesson):
"file_type", "file_type",
"instructor_notes", "instructor_notes",
"course", "course",
"content",
], ],
as_dict=True, as_dict=True,
) )
@@ -1756,16 +1757,10 @@ def get_lesson_creation_details(course, chapter, lesson):
"Lesson Reference", {"parent": chapter_name, "idx": lesson}, "lesson" "Lesson Reference", {"parent": chapter_name, "idx": lesson}, "lesson"
) )
if lesson_name:
lesson_details = frappe.db.get_value(
"Course Lesson",
lesson_name,
["name", "title", "body", "instructor_notes", "include_in_preview"],
as_dict=True,
)
return { return {
"course_title": frappe.db.get_value("LMS Course", course, "title"), "course_title": frappe.db.get_value("LMS Course", course, "title"),
"chapter_title": frappe.db.get_value("Course Chapter", chapter_name, "title"), "chapter": frappe.db.get_value(
"lesson_title": lesson_details if lesson_name else None, "Course Chapter", chapter_name, ["title", "name"], as_dict=True
),
"lesson": lesson_name if lesson_name else None,
} }