feat: chapter creation
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
<template>
|
||||
<div class="text-base">
|
||||
<div
|
||||
v-if="showHeader && outline.data?.length"
|
||||
class="flex justify-between mb-4"
|
||||
v-if="title && (outline.data?.length || allowEdit)"
|
||||
class="flex items-center justify-between mb-4"
|
||||
>
|
||||
<div class="text-2xl font-semibold">
|
||||
{{ __('Course Content') }}
|
||||
<div class="text-lg font-semibold">
|
||||
{{ __(title) }}
|
||||
</div>
|
||||
<Button @click="openChapterModal()">
|
||||
{{ __('Add Chapter') }}
|
||||
</Button>
|
||||
<!-- <span class="font-medium cursor-pointer" @click="expandAllChapters()">
|
||||
{{ expandAll ? __("Collapse all chapters") : __("Expand all chapters") }}
|
||||
</span> -->
|
||||
@@ -20,7 +23,7 @@
|
||||
v-slot="{ open }"
|
||||
v-for="(chapter, index) in outline.data"
|
||||
:key="chapter.name"
|
||||
:defaultOpen="openChapter(chapter.idx)"
|
||||
:defaultOpen="openChapterDetail(chapter.idx)"
|
||||
>
|
||||
<DisclosureButton ref="" class="flex w-full px-2 py-4">
|
||||
<ChevronRight
|
||||
@@ -41,7 +44,7 @@
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel class="pb-2">
|
||||
<div v-for="lesson in chapter.lessons" :key="lesson.name">
|
||||
<div class="outline-lesson py-2 pl-8">
|
||||
<div class="outline-lesson pl-8 py-2">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Lesson',
|
||||
@@ -70,13 +73,38 @@
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="allowEdit" class="flex mt-2 pl-8">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'CreateLesson',
|
||||
params: {
|
||||
courseName: courseName,
|
||||
chapterNumber: chapter.idx,
|
||||
lessonNumber: chapter.lessons.length + 1,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<Button>
|
||||
{{ __('Add Lesson') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
<Button class="ml-2" @click="openChapterModal(chapter)">
|
||||
{{ __('Edit Chapter') }}
|
||||
</Button>
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</div>
|
||||
</div>
|
||||
<ChapterModal
|
||||
v-model="showChapterModal"
|
||||
v-model:outline="outline"
|
||||
:course="courseName"
|
||||
:chapterDetail="getCurrentChapter()"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createResource } from 'frappe-ui'
|
||||
import { Button, createResource } from 'frappe-ui'
|
||||
import { ref } from 'vue'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
|
||||
import {
|
||||
@@ -86,9 +114,13 @@ import {
|
||||
FileText,
|
||||
} from 'lucide-vue-next'
|
||||
import { useRoute } from 'vue-router'
|
||||
import ChapterModal from '@/components/Modals/ChapterModal.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const expandAll = ref(true)
|
||||
const showChapterModal = ref(false)
|
||||
const currentChapter = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
type: String,
|
||||
@@ -98,7 +130,11 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showHeader: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
@@ -113,12 +149,17 @@ const outline = createResource({
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const openChapter = (index) => {
|
||||
const openChapterDetail = (index) => {
|
||||
return index == route.params.chapterNumber || index == 1
|
||||
}
|
||||
|
||||
const expandAllChapters = () => {
|
||||
expandAll.value = !expandAll.value
|
||||
const openChapterModal = (chapter = null) => {
|
||||
currentChapter.value = chapter
|
||||
showChapterModal.value = true
|
||||
}
|
||||
|
||||
const getCurrentChapter = () => {
|
||||
return currentChapter.value
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
30
frontend/src/components/CreateOutline.vue
Normal file
30
frontend/src/components/CreateOutline.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div v-if="course">
|
||||
<div class="text-xl font-semibold">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
<div v-if="course.chapters.length">
|
||||
{{ course.chapters }}
|
||||
</div>
|
||||
<div v-else class="border bg-white rounded-md p-5 text-center mt-4">
|
||||
<div>
|
||||
{{
|
||||
__(
|
||||
'There are no chapters in this course. Create and manage chapters from here.'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<Button class="mt-4">
|
||||
{{ __('Add Chapter') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
course: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -3,7 +3,6 @@
|
||||
<div class="h-full overflow-auto" id="scrollContainer">
|
||||
<slot />
|
||||
</div>
|
||||
{{ tabs }}
|
||||
<div
|
||||
v-if="tabs"
|
||||
class="grid grid-cols-5 border-t border-gray-300 standalone:pb-4"
|
||||
@@ -15,8 +14,7 @@
|
||||
class="flex flex-col items-center justify-center py-3 transition active:scale-95"
|
||||
@click="handleClick(tab)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
<component :is="tab.icon" class="h-6 w-6" />
|
||||
<component :is="tab.icon" class="h-6 w-6 stroke-1.5 text-gray-700" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,8 +29,6 @@ const tabs = computed(() => {
|
||||
return getSidebarLinks()
|
||||
})
|
||||
|
||||
console.log(tabs.value)
|
||||
|
||||
/* let isActive = computed((tab) => {
|
||||
console.log(tab);
|
||||
return router.currentRoute.value.name === tab.to
|
||||
|
||||
164
frontend/src/components/Modals/BatchCreation.vue
Normal file
164
frontend/src/components/Modals/BatchCreation.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Create a Batch'),
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: __('Save'),
|
||||
variant: 'solid',
|
||||
onClick: (close) => createBatch(close),
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<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>
|
||||
<FormControl
|
||||
v-model="batch.start_date"
|
||||
:label="__('Start Date')"
|
||||
type="date"
|
||||
class="mb-4"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.end_date"
|
||||
:label="__('End Date')"
|
||||
type="date"
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormControl
|
||||
v-model="batch.start_time"
|
||||
:label="__('Start Time')"
|
||||
type="time"
|
||||
class="mb-4"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.end_time"
|
||||
:label="__('End Time')"
|
||||
type="time"
|
||||
class="mb-4"
|
||||
/>
|
||||
</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>
|
||||
<FormControl
|
||||
v-model="batch.seat_count"
|
||||
:label="__('Seat Count')"
|
||||
type="number"
|
||||
class="mb-4"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.evaluation_end_date"
|
||||
:label="__('Evaluation End Date')"
|
||||
type="date"
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
</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"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch_details.raw"
|
||||
:label="__('Batch Details')"
|
||||
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'
|
||||
|
||||
const batch = reactive({
|
||||
title: '',
|
||||
published: false,
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
start_time: '',
|
||||
end_time: '',
|
||||
medium: '',
|
||||
category: '',
|
||||
seat_count: 0,
|
||||
evaluation_end_date: '',
|
||||
description: '',
|
||||
batch_details: '',
|
||||
batch_details_raw: '',
|
||||
meta_image: '',
|
||||
paid_batch: false,
|
||||
amount: 0,
|
||||
currency: '',
|
||||
})
|
||||
</script>
|
||||
161
frontend/src/components/Modals/ChapterModal.vue
Normal file
161
frontend/src/components/Modals/ChapterModal.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Add Chapter'),
|
||||
size: 'lg',
|
||||
actions: [
|
||||
{
|
||||
label: chapterDetail ? __('Edit Chapter') : __('Add Chapter'),
|
||||
variant: 'solid',
|
||||
onClick: (close) =>
|
||||
chapterDetail ? editChapter(close) : addChapter(close),
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<FormControl label="Title" v-model="chapter.title" class="mb-4" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, FormControl, createResource } from 'frappe-ui'
|
||||
import { defineModel, reactive, watch, inject } from 'vue'
|
||||
import { createToast, formatTime } from '@/utils/'
|
||||
|
||||
const show = defineModel()
|
||||
const outline = defineModel('outline')
|
||||
|
||||
const props = defineProps({
|
||||
course: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
chapterDetail: {
|
||||
type: Object,
|
||||
},
|
||||
})
|
||||
const chapter = reactive({
|
||||
title: '',
|
||||
})
|
||||
|
||||
const chapterResource = createResource({
|
||||
url: 'frappe.client.insert',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doc: {
|
||||
doctype: 'Course Chapter',
|
||||
title: chapter.title,
|
||||
description: chapter.description,
|
||||
course: props.course,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const chapterEditResource = createResource({
|
||||
url: 'frappe.client.set_value',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: 'Course Chapter',
|
||||
name: props.chapterDetail?.name,
|
||||
fieldname: 'title',
|
||||
value: chapter.title,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const chapterReference = createResource({
|
||||
url: 'frappe.client.insert',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doc: {
|
||||
doctype: 'Chapter Reference',
|
||||
chapter: values.name,
|
||||
parent: props.course,
|
||||
parenttype: 'LMS Course',
|
||||
parentfield: 'chapters',
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const addChapter = (close) => {
|
||||
chapterResource.submit(
|
||||
{},
|
||||
{
|
||||
validate() {
|
||||
if (!chapter.title) {
|
||||
return 'Title is required'
|
||||
}
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
chapterReference.submit(
|
||||
{ name: data.name },
|
||||
{
|
||||
onSuccess(data) {
|
||||
outline.value.reload()
|
||||
createToast({
|
||||
text: 'Chapter added successfully',
|
||||
icon: 'check',
|
||||
iconClasses: 'bg-green-600 text-white rounded-md p-px',
|
||||
})
|
||||
},
|
||||
onError(err) {
|
||||
showError(err)
|
||||
},
|
||||
}
|
||||
)
|
||||
close()
|
||||
},
|
||||
onError(err) {
|
||||
showError(err)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const editChapter = (close) => {
|
||||
chapterEditResource.submit(
|
||||
{},
|
||||
{
|
||||
validate() {
|
||||
if (!chapter.title) {
|
||||
return 'Title is required'
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
outline.value.reload()
|
||||
createToast({
|
||||
text: 'Chapter updated successfully',
|
||||
icon: 'check',
|
||||
iconClasses: 'bg-green-600 text-white rounded-md p-px',
|
||||
})
|
||||
close()
|
||||
},
|
||||
onError(err) {
|
||||
showError(err)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const showError = (err) => {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.chapterDetail,
|
||||
(newChapter) => {
|
||||
chapter.title = newChapter?.title
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -6,7 +6,7 @@
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: 'Submit',
|
||||
label: __('Submit'),
|
||||
variant: 'solid',
|
||||
onClick: (close) => submitEvaluation(close),
|
||||
},
|
||||
|
||||
59
frontend/src/components/Tags.vue
Normal file
59
frontend/src/components/Tags.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-1.5 text-sm text-gray-700">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
{{ tags }}
|
||||
<div
|
||||
v-for="tag in tags?.split(', ')"
|
||||
class="flex items-center bg-gray-100 p-2 rounded-md mr-2"
|
||||
>
|
||||
{{ tag }}
|
||||
<X
|
||||
class="stroke-1.5 w-3 h-3 ml-2 cursor-pointer"
|
||||
@click="removeTag(tag)"
|
||||
/>
|
||||
</div>
|
||||
<FormControl v-model="newTag" @keyup.enter="updateTags()" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { FormControl } from 'frappe-ui'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: 'Tags',
|
||||
},
|
||||
})
|
||||
console.log(props.modelValue)
|
||||
let tags = ref(props.modelValue)
|
||||
console.log(tags.value)
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
let newTag = ref('')
|
||||
|
||||
let emitChange = (value) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
const updateTags = () => {
|
||||
if (newTag) {
|
||||
tags.value = tags.value ? `${tags.value}, ${newTag}` : newTag
|
||||
newTag.value = ''
|
||||
emitChange(tags.value)
|
||||
}
|
||||
}
|
||||
|
||||
const removeTag = (tag) => {
|
||||
tags.value = tags.value.replace(tag, '').replace(', ,', ',')
|
||||
emitChange(tags.value)
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user