feat: lesson creation
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
<div class="text-lg font-semibold">
|
||||
{{ __(title) }}
|
||||
</div>
|
||||
<Button @click="openChapterModal()">
|
||||
<Button v-if="allowEdit" @click="openChapterModal()">
|
||||
{{ __('Add Chapter') }}
|
||||
</Button>
|
||||
<!-- <span class="font-medium cursor-pointer" @click="expandAllChapters()">
|
||||
@@ -25,7 +25,7 @@
|
||||
:key="chapter.name"
|
||||
: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
|
||||
:class="{
|
||||
'rotate-90 transform duration-200': open,
|
||||
@@ -34,20 +34,16 @@
|
||||
}"
|
||||
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 }}
|
||||
</div>
|
||||
<div class="ml-auto text-sm">
|
||||
{{ chapter.lessons.length }}
|
||||
{{ chapter.lessons.length == 1 ? __('lesson') : __('lessons') }}
|
||||
</div>
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel class="pb-2">
|
||||
<div v-for="lesson in chapter.lessons" :key="lesson.name">
|
||||
<div class="outline-lesson pl-8 py-2">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Lesson',
|
||||
name: allowEdit ? 'CreateLesson' : 'Lesson',
|
||||
params: {
|
||||
courseName: courseName,
|
||||
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
|
||||
v-if="lesson.icon === 'icon-youtube'"
|
||||
class="h-4 w-4 text-gray-900 stroke-1 mr-2"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Create a Batch'),
|
||||
size: 'xl',
|
||||
size: '3xl',
|
||||
actions: [
|
||||
{
|
||||
label: __('Save'),
|
||||
@@ -15,14 +15,19 @@
|
||||
>
|
||||
<template #body-content>
|
||||
<div>
|
||||
<FormControl v-model="batch.title" :label="__('Title')" class="mb-4" />
|
||||
<FormControl
|
||||
v-model="batch.published"
|
||||
type="checkbox"
|
||||
:label="__('Published')"
|
||||
class="mb-4"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<FormControl
|
||||
v-model="batch.title"
|
||||
:label="__('Title')"
|
||||
class="mb-4"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.published"
|
||||
type="checkbox"
|
||||
:label="__('Published')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormControl
|
||||
v-model="batch.start_date"
|
||||
@@ -52,19 +57,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2">
|
||||
<div>
|
||||
<FormControl
|
||||
v-model="batch.medium"
|
||||
:label="__('Medium')"
|
||||
class="mb-4"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.category"
|
||||
:label="__('Category')"
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4 mt-4 border-t pt-4">
|
||||
<div>
|
||||
<FormControl
|
||||
v-model="batch.seat_count"
|
||||
@@ -79,68 +72,103 @@
|
||||
class="mb-4"
|
||||
/>
|
||||
</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 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
|
||||
v-model="batch.description"
|
||||
:label="__('Description')"
|
||||
type="textarea"
|
||||
class="mb-4"
|
||||
/>
|
||||
<TextEditor
|
||||
v-model="batch.batch_details"
|
||||
:label="__('Batch Details')"
|
||||
class="mb-4"
|
||||
/>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">
|
||||
{{ __('Batch Details') }}
|
||||
</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
|
||||
v-model="batch_details.raw"
|
||||
:label="__('Batch Details')"
|
||||
v-model="batch.batch_details_raw"
|
||||
:label="__('Batch Details Raw')"
|
||||
type="textarea"
|
||||
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>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<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({
|
||||
title: '',
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</header>
|
||||
<div v-if="batch.data" class="grid grid-cols-[70%,30%] h-full">
|
||||
<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">
|
||||
<div>
|
||||
<button
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
<div class="grid grid-cols-[60%,20%] gap-20 mt-10">
|
||||
<div class="">
|
||||
<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"
|
||||
class="batch-description"
|
||||
></div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-semibold">
|
||||
<div class="text-2xl font-semibold mt-10">
|
||||
{{ __('Courses') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 mt-5">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:items="[{ label: __('All Batches'), route: { name: 'Batches' } }]"
|
||||
/>
|
||||
<div class="flex">
|
||||
<Button variant="solid">
|
||||
<Button variant="solid" @click="openBatchModal()">
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4" />
|
||||
</template>
|
||||
@@ -73,14 +73,18 @@
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
<BatchCreation v-model="showBatchModal" />
|
||||
</template>
|
||||
<script setup>
|
||||
import { createListResource, Breadcrumbs, Button, Tabs, Badge } from 'frappe-ui'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import BatchCard from '@/components/BatchCard.vue'
|
||||
import { inject, ref, computed } from 'vue'
|
||||
import BatchCreation from '@/components/Modals/BatchCreation.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const showBatchModal = ref(false)
|
||||
|
||||
const batches = createListResource({
|
||||
doctype: 'LMS Batch',
|
||||
url: 'lms.lms.utils.get_batches',
|
||||
@@ -116,4 +120,8 @@ if (user.data) {
|
||||
count: computed(() => batches.data?.enrolled?.length),
|
||||
})
|
||||
}
|
||||
|
||||
const openBatchModal = () => {
|
||||
showBatchModal.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -321,6 +321,7 @@ const courseCreationResource = createResource({
|
||||
})
|
||||
|
||||
const submitCourse = () => {
|
||||
console.log(courseResource.doc?.modified)
|
||||
if (courseResource.doc) {
|
||||
courseResource.setValue.submit(
|
||||
{
|
||||
@@ -331,8 +332,11 @@ const submitCourse = () => {
|
||||
validate() {
|
||||
return validateMandatoryFields()
|
||||
},
|
||||
onSuccess() {
|
||||
showToast('Success', 'Course updated successfully', 'check')
|
||||
},
|
||||
onError(err) {
|
||||
showToast(err)
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -341,6 +345,9 @@ const submitCourse = () => {
|
||||
validate() {
|
||||
return validateMandatoryFields()
|
||||
},
|
||||
onSuccess() {
|
||||
showToast('Success', 'Course created successfully', 'check')
|
||||
},
|
||||
onError(err) {
|
||||
showToast(err)
|
||||
},
|
||||
@@ -392,14 +399,17 @@ const removeTag = (tag) => {
|
||||
newTag.value = ''
|
||||
}
|
||||
|
||||
const showToast = (err) => {
|
||||
const showToast = (title, text, icon) => {
|
||||
createToast({
|
||||
title: 'Error',
|
||||
text: err.messages?.[0] || err,
|
||||
icon: 'x',
|
||||
iconClasses: 'bg-red-600 text-white rounded-md p-px',
|
||||
position: 'top-center',
|
||||
timeout: 10,
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,14 @@
|
||||
>
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
</header>
|
||||
<div class="w-7/12 mx-auto pt-5">
|
||||
<div class="text-lg font-semibold mb-5">
|
||||
{{ __('Lesson Details') }}
|
||||
<div class="w-7/12 mx-auto py-5">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div class="text-lg font-semibold">
|
||||
{{ __('Lesson Details') }}
|
||||
</div>
|
||||
<Button variant="solid" @click="saveLesson()">
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</div>
|
||||
<FormControl v-model="lesson.title" label="Title" class="mb-4" />
|
||||
<FormControl
|
||||
@@ -15,24 +20,40 @@
|
||||
type="checkbox"
|
||||
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">
|
||||
<label class="block text-xs text-gray-600 mb-1">
|
||||
{{ __('Content') }}
|
||||
</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>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Breadcrumbs, FormControl, createResource } from 'frappe-ui'
|
||||
import { computed, reactive, onMounted } from 'vue'
|
||||
import {
|
||||
Breadcrumbs,
|
||||
FormControl,
|
||||
createResource,
|
||||
Button,
|
||||
createDocumentResource,
|
||||
} from 'frappe-ui'
|
||||
import { computed, reactive, onMounted, onBeforeMount } from 'vue'
|
||||
import EditorJS from '@editorjs/editorjs'
|
||||
import Header from '@editorjs/header'
|
||||
import Paragraph from '@editorjs/paragraph'
|
||||
import List from '@editorjs/list'
|
||||
import Embed from '@editorjs/embed'
|
||||
import YouTubeVideo from '../utils/youtube.js'
|
||||
import { createToast } from '../utils'
|
||||
|
||||
let editor
|
||||
let editLessonResource
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
@@ -49,47 +70,56 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(
|
||||
() =>
|
||||
new EditorJS({
|
||||
holder: 'content',
|
||||
tools: {
|
||||
header: Header,
|
||||
youtube: YouTubeVideo,
|
||||
paragraph: {
|
||||
class: Paragraph,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
preserveBlank: true,
|
||||
},
|
||||
},
|
||||
list: List,
|
||||
embed: {
|
||||
class: Embed,
|
||||
config: {
|
||||
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>",
|
||||
},
|
||||
},
|
||||
onMounted(() => {
|
||||
editor = renderEditor('content')
|
||||
/* renderEditor('instructor-notes') */
|
||||
})
|
||||
|
||||
const renderEditor = (holder) => {
|
||||
return new EditorJS({
|
||||
holder: holder,
|
||||
tools: getEditorTools(),
|
||||
})
|
||||
}
|
||||
|
||||
const getEditorTools = () => {
|
||||
return {
|
||||
header: Header,
|
||||
youtube: YouTubeVideo,
|
||||
paragraph: {
|
||||
class: Paragraph,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
preserveBlank: true,
|
||||
},
|
||||
},
|
||||
list: List,
|
||||
embed: {
|
||||
class: Embed,
|
||||
config: {
|
||||
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({
|
||||
title: '',
|
||||
include_in_preview: false,
|
||||
body: '',
|
||||
body: 'Test',
|
||||
instructor_notes: '',
|
||||
content: '',
|
||||
})
|
||||
|
||||
const lessonDetails = createResource({
|
||||
@@ -100,8 +130,136 @@ const lessonDetails = createResource({
|
||||
lesson: props.lessonNumber,
|
||||
},
|
||||
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(() => {
|
||||
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({
|
||||
label: 'Create Lesson',
|
||||
label: editLessonResource?.doc ? 'Edit Lesson' : 'Create Lesson',
|
||||
route: {
|
||||
name: 'CreateLesson',
|
||||
params: {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="text-3xl font-semibold">
|
||||
{{ lesson.data.title }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<router-link
|
||||
v-if="lesson.data.prev"
|
||||
:to="{
|
||||
@@ -40,7 +40,27 @@
|
||||
}"
|
||||
>
|
||||
<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>
|
||||
</router-link>
|
||||
<router-link
|
||||
@@ -55,7 +75,12 @@
|
||||
}"
|
||||
>
|
||||
<Button>
|
||||
<ChevronRight class="w-4 h-4 stroke-1" />
|
||||
<template #suffix>
|
||||
<ChevronRight class="w-4 h-4 stroke-1" />
|
||||
</template>
|
||||
<span>
|
||||
{{ __('Next') }}
|
||||
</span>
|
||||
</Button>
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -86,6 +111,18 @@
|
||||
</span>
|
||||
</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"
|
||||
>
|
||||
<div v-if="lesson.data.youtube">
|
||||
@@ -346,6 +383,16 @@ const allowDiscussions = () => {
|
||||
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>
|
||||
<style>
|
||||
.avatar-group {
|
||||
|
||||
@@ -197,13 +197,13 @@ const courseCompletion = createResource({
|
||||
const signupChartOptions = () => {
|
||||
let options = chartOptions(false)
|
||||
options.plugins.title.text = 'New Signups'
|
||||
options.borderColor = '#278F5E'
|
||||
options.borderColor = '#4563f0'
|
||||
options.backgroundColor = (ctx) => {
|
||||
const canvas = ctx.chart.ctx
|
||||
const gradient = canvas.createLinearGradient(0, 0, 0, 160)
|
||||
gradient.addColorStop(0, '#B6DEC5')
|
||||
gradient.addColorStop(0.5, '#E4F5E9')
|
||||
gradient.addColorStop(1, '#F3FCF5')
|
||||
gradient.addColorStop(0, '#4563f0')
|
||||
gradient.addColorStop(0.5, '#e8ecfe')
|
||||
gradient.addColorStop(1, '#f6f7ff')
|
||||
|
||||
return gradient
|
||||
}
|
||||
@@ -213,13 +213,13 @@ const signupChartOptions = () => {
|
||||
const enrollmentChartOptions = () => {
|
||||
let options = chartOptions(false)
|
||||
options.plugins.title.text = 'Course Enrollments'
|
||||
options.borderColor = '#B52A2A'
|
||||
options.borderColor = '#4563f0'
|
||||
options.backgroundColor = (ctx) => {
|
||||
const canvas = ctx.chart.ctx
|
||||
const gradient = canvas.createLinearGradient(0, 0, 0, 160)
|
||||
gradient.addColorStop(0, '#FFC6A5')
|
||||
gradient.addColorStop(0.5, '#FFD8C5')
|
||||
gradient.addColorStop(1, '#FFE9E5')
|
||||
gradient.addColorStop(0, '#4563f0')
|
||||
gradient.addColorStop(0.5, '#e8ecfe')
|
||||
gradient.addColorStop(1, '#f6f7ff')
|
||||
|
||||
return gradient
|
||||
}
|
||||
@@ -229,13 +229,13 @@ const enrollmentChartOptions = () => {
|
||||
const lessonChartOptions = () => {
|
||||
let options = chartOptions(false)
|
||||
options.plugins.title.text = 'Lesson Completion'
|
||||
options.borderColor = '#0070CC'
|
||||
options.borderColor = '#4563f0'
|
||||
options.backgroundColor = (ctx) => {
|
||||
const canvas = ctx.chart.ctx
|
||||
const gradient = canvas.createLinearGradient(0, 0, 0, 160)
|
||||
gradient.addColorStop(0, '#B6DEC5')
|
||||
gradient.addColorStop(0.5, '#E4F5E9')
|
||||
gradient.addColorStop(1, '#F3FCF5')
|
||||
gradient.addColorStop(0.5, '#e8ecfe')
|
||||
gradient.addColorStop(1, '#f6f7ff')
|
||||
|
||||
return gradient
|
||||
}
|
||||
@@ -245,7 +245,7 @@ const lessonChartOptions = () => {
|
||||
const courseChartOptions = () => {
|
||||
let options = chartOptions(true)
|
||||
options.plugins.title.text = 'Course Completion'
|
||||
options.backgroundColor = ['#E4521B', '#FEEB65']
|
||||
options.backgroundColor = ['#4563f0', '#f683ae']
|
||||
return options
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ const routes = [
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
// Create a route for path /courses/inventory-management/learn/1.1
|
||||
path: '/courses/:courseName/learn/:chapterNumber-:lessonNumber',
|
||||
name: 'Lesson',
|
||||
component: () => import('@/pages/Lesson.vue'),
|
||||
|
||||
@@ -1338,6 +1338,7 @@ def get_lesson(course, chapter, lesson):
|
||||
"file_type",
|
||||
"instructor_notes",
|
||||
"course",
|
||||
"content",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
@@ -1756,16 +1757,10 @@ def get_lesson_creation_details(course, chapter, 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 {
|
||||
"course_title": frappe.db.get_value("LMS Course", course, "title"),
|
||||
"chapter_title": frappe.db.get_value("Course Chapter", chapter_name, "title"),
|
||||
"lesson_title": lesson_details if lesson_name else None,
|
||||
"chapter": frappe.db.get_value(
|
||||
"Course Chapter", chapter_name, ["title", "name"], as_dict=True
|
||||
),
|
||||
"lesson": lesson_name if lesson_name else None,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user