feat: lesson creation
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user