feat: new course modal

This commit is contained in:
Jannat Patel
2026-01-28 10:57:58 +05:30
parent 49ed082831
commit b50d584a5b
13 changed files with 1991 additions and 313 deletions

View File

@@ -19,9 +19,14 @@
showOptions = true
}
"
@click="(e) => {
showOptions = true
nextTick(() => {
setFocus()
})
}"
@focus="
() => {
showOptions = true
if (!filterOptions.data || filterOptions.data.length === 0) {
reload('')
}
@@ -115,7 +120,7 @@ import {
} from '@headlessui/vue'
import { createResource, Popover, Button } from 'frappe-ui'
import { ref, computed, nextTick, useAttrs } from 'vue'
import { watchDebounced } from '@vueuse/core'
import { set, watchDebounced } from '@vueuse/core'
import { X, Plus } from 'lucide-vue-next'
const props = defineProps({
@@ -149,7 +154,6 @@ const props = defineProps({
const values = defineModel()
const attrs = useAttrs()
const emails = ref([])
const search = ref(null)
const error = ref(null)
const query = ref('')
@@ -161,6 +165,7 @@ const selectedValue = computed({
set: (val) => {
query.value = ''
val?.value && addValue(val.value)
showOptions.value = false
},
})

View File

@@ -34,7 +34,7 @@
<img
v-if="type == 'image'"
:src="modelValue"
class="border rounded-md w-44 h-auto"
class="border rounded-md w-44 h-auto min-h-20 object-cover"
/>
<video v-else controls class="border rounded-md w-44 h-auto">
<source :src="modelValue" />

View File

@@ -88,29 +88,11 @@
</template>
{{ __('Get Certificate') }}
</Button>
<router-link
v-if="user?.data?.is_moderator || is_instructor()"
:to="{
name: 'CourseForm',
params: {
courseName: course.data.name,
},
}"
>
<Button variant="subtle" class="w-full mt-2" size="md">
<template #prefix>
<Pencil class="size-4 stroke-1.5" />
</template>
<span>
{{ __('Edit') }}
</span>
</Button>
</router-link>
</div>
<div class="space-y-4">
<div
class="font-medium text-ink-gray-9"
:class="{ 'mt-8': !readOnlyMode }"
:class="{ 'mt-8': course.data.membership && !readOnlyMode }"
>
{{ __('This course has:') }}
</div>

View File

@@ -1,5 +1,5 @@
<template>
<div :class="$attrs.class">
<div class="p-5">
<div class="grid grid-cols-3 gap-5 mb-5">
<NumberChartGraph :title="__('Enrolled')" :value="memberCount || 0" />
<NumberChartGraph

View File

@@ -4,31 +4,42 @@
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs class="h-7" :items="breadcrumbs" />
<div v-if="tabIndex == 2" class="flex items-center space-x-2">
<Button>
<template #icon>
<Trash2 class="w-4 h-4 stroke-1.5" />
</template>
</Button>
<Button variant="solid">
{{ __("Save") }}
</Button>
</div>
</header>
<CourseOverview v-if="!isAdmin" :course="course" class="p-5" />
<CourseOverview v-if="!isAdmin" :course="course" />
<div v-else>
<Tabs :tabs="tabs" v-model="tabIndex">
<template #tab-panel="{ tab }">
<component :is="tab.component" :course="course" class="p-5" />
<component :is="tab.component" :course="course" />
</template>
</Tabs>
</div>
</div>
</template>
<script setup>
import { createResource, Breadcrumbs, Tabs, usePageMeta } from 'frappe-ui'
import { computed, inject, markRaw, ref, watch } from 'vue'
import { Button, createResource, Breadcrumbs, Tabs, usePageMeta } from 'frappe-ui'
import { computed, inject, markRaw, onMounted, ref, watch } from 'vue'
import { sessionStore } from '@/stores/session'
import { useRouter } from 'vue-router'
import { List, Settings2, TrendingUp } from 'lucide-vue-next'
import { useRouter, useRoute } from 'vue-router'
import { List, Settings2, Trash2, TrendingUp } from 'lucide-vue-next'
import CourseOverview from '@/pages/Courses/CourseOverview.vue'
import CourseDashboard from '@/pages/Courses/CourseDashboard.vue'
import CourseForm from '@/pages/Courses/CourseForm.vue'
const { brand } = sessionStore()
const router = useRouter()
const route = useRoute()
const user = inject('$user')
const tabIndex = ref(1)
const tabIndex = ref(0)
const props = defineProps({
courseName: {
@@ -37,6 +48,28 @@ const props = defineProps({
},
})
onMounted(() => {
updateTabIndex()
})
const updateTabIndex = () => {
const hash = route.hash
if (hash) {
tabs.value.forEach((tab, index) => {
if (tab.label?.toLowerCase() === hash.replace('#', '')) {
tabIndex.value = index
}
})
}
}
watch(tabIndex, () => {
const tab = tabs.value[tabIndex.value]
if (tab.label != route.hash.replace('#', '')) {
router.push({ ...route, hash: `#${tab.label.toLowerCase()}` })
}
})
const course = createResource({
url: 'lms.lms.utils.get_course_details',
cache: ['course', props.courseName],

View File

@@ -19,6 +19,13 @@
placeholder=" "
v-model="student"
:required="true"
:allowCreate="true"
@create="
() => {
openSettings('Members')
show = false
}
"
/>
<Link
v-if="purchasedCertificate"

View File

@@ -1,38 +1,21 @@
<template>
<div class="h-full">
<div class="grid grid-cols-1 md:grid-cols-[70%,30%] h-full">
<div>
<header
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs class="h-7" :items="breadcrumbs" />
<div class="flex items-center mt-3 md:mt-0">
<Button v-if="courseResource.data?.name" @click="trashCourse()">
<template #icon>
<Trash2 class="w-4 h-4 stroke-1.5" />
</template>
</Button>
<Button variant="solid" @click="submitCourse()" class="ml-2">
<span>
{{ __('Save') }}
</span>
</Button>
</div>
</header>
<div class="mt-5 mb-5">
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
<div class="pl-5">
<div class="grid grid-cols-1 md:grid-cols-[70%,30%] ">
<div v-if="courseResource.doc" class="max-h-[88vh] overflow-y-auto">
<div class="my-5">
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold mb-4 text-ink-gray-9">
{{ __('Details') }}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<FormControl
v-model="course.title"
v-model="courseResource.doc.title"
:label="__('Title')"
:required="true"
/>
<Link
doctype="LMS Category"
v-model="course.category"
v-model="courseResource.doc.category"
:label="__('Category')"
:onCreate="(value, close) => openSettings('Categories', close)"
/>
@@ -60,8 +43,8 @@
<div>
<div class="flex items-center flex-wrap gap-2">
<div
v-if="course.tags"
v-for="tag in course.tags?.split(', ')"
v-if="courseResource.doc.tags"
v-for="tag in courseResource.doc.tags?.split(', ')"
class="flex items-center bg-surface-gray-2 text-ink-gray-7 p-2 rounded-md"
>
{{ tag }}
@@ -76,13 +59,13 @@
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<Uploader
v-model="course.image"
v-model="courseResource.doc.image"
:label="__('Course Image')"
:required="false"
/>
<ColorSwatches
v-model="course.card_gradient"
v-model="courseResource.doc.card_gradient"
:label="__('Color')"
:description="__('Choose a color for the course card')"
class="w-full"
@@ -90,7 +73,7 @@
</div>
</div>
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('Settings') }}
</div>
@@ -101,11 +84,11 @@
>
<FormControl
type="checkbox"
v-model="course.published"
v-model="courseResource.doc.published"
:label="__('Published')"
/>
<FormControl
v-model="course.published_on"
v-model="courseResource.doc.published_on"
:label="__('Published On')"
type="date"
/>
@@ -113,29 +96,29 @@
<div class="flex flex-col space-y-5">
<FormControl
type="checkbox"
v-model="course.upcoming"
v-model="courseResource.doc.upcoming"
:label="__('Upcoming')"
/>
<FormControl
type="checkbox"
v-model="course.featured"
v-model="courseResource.doc.featured"
:label="__('Featured')"
/>
<FormControl
type="checkbox"
v-model="course.disable_self_learning"
v-model="courseResource.doc.disable_self_learning"
:label="__('Disable Self Enrollment')"
/>
</div>
</div>
</div>
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('About the Course') }}
</div>
<FormControl
v-model="course.short_introduction"
v-model="courseResource.doc.short_introduction"
type="textarea"
:rows="5"
:label="__('Short Introduction')"
@@ -152,8 +135,8 @@
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:content="course.description"
@change="(val) => (course.description = val)"
:content="courseResource.doc.description"
@change="(val) => (courseResource.doc.description = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
@@ -161,7 +144,7 @@
</div>
<FormControl
v-model="course.video_link"
v-model="courseResource.doc.video_link"
:label="__('Preview Video')"
:placeholder="
__(
@@ -174,7 +157,7 @@
v-model="related_courses"
doctype="LMS Course"
:label="__('Related Courses')"
:filters="{ name: ['!=', courseResource.data?.name] }"
:filters="{ name: ['!=', courseResource.doc?.name] }"
:onCreate="
(close) => {
router.push({
@@ -186,41 +169,41 @@
/>
</div>
<div class="px-5 md:px-10 pb-5 space-y-5 border-b">
<div class="pr-5 md:pr-10 pb-5 space-y-5 border-b">
<div class="text-lg font-semibold mt-5 text-ink-gray-9">
{{ __('Pricing and Certification') }}
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
<FormControl
type="checkbox"
v-model="course.paid_course"
v-model="courseResource.doc.paid_course"
:label="__('Paid Course')"
/>
<FormControl
type="checkbox"
v-model="course.enable_certification"
v-model="courseResource.doc.enable_certification"
:label="__('Completion Certificate')"
/>
<FormControl
type="checkbox"
v-model="course.paid_certificate"
v-model="courseResource.doc.paid_certificate"
:label="__('Paid Certificate')"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div class="space-y-5">
<FormControl
v-if="course.paid_course || course.paid_certificate"
v-model="course.course_price"
v-if="courseResource.doc.paid_course || courseResource.doc.paid_certificate"
v-model="courseResource.doc.course_price"
:label="__('Amount')"
:required="course.paid_course || course.paid_certificate"
:required="courseResource.doc.paid_course || courseResource.doc.paid_certificate"
/>
<Link
v-if="course.paid_certificate"
v-if="courseResource.doc.paid_certificate"
doctype="Course Evaluator"
v-model="course.evaluator"
v-model="courseResource.doc.evaluator"
:label="__('Evaluator')"
:required="course.paid_certificate"
:required="courseResource.doc.paid_certificate"
:onCreate="
(value, close) => openSettings('Evaluators', close)
"
@@ -228,25 +211,25 @@
</div>
<div class="space-y-5">
<Link
v-if="course.paid_course || course.paid_certificate"
v-if="courseResource.doc.paid_course || courseResource.doc.paid_certificate"
doctype="Currency"
v-model="course.currency"
v-model="courseResource.doc.currency"
:filters="{ enabled: 1 }"
:label="__('Currency')"
:required="course.paid_course || course.paid_certificate"
:required="courseResource.doc.paid_course || courseResource.doc.paid_certificate"
/>
<FormControl
v-if="course.paid_certificate"
v-model="course.timezone"
v-if="courseResource.doc.paid_certificate"
v-model="courseResource.doc.timezone"
:label="__('Timezone')"
:required="course.paid_certificate"
:required="courseResource.doc.paid_certificate"
:placeholder="__('e.g. IST, UTC, GMT...')"
/>
</div>
</div>
</div>
<div class="px-5 md:px-10 pb-5 space-y-5">
<div class="pr-5 md:pr-10 pb-5 space-y-5">
<div class="text-lg font-semibold mt-5 text-ink-gray-9">
{{ __('Meta Tags') }}
</div>
@@ -270,9 +253,9 @@
</div>
<div class="border-l">
<CourseOutline
v-if="courseResource.data"
:courseName="courseResource.data.name"
:title="__('Course Outline')"
v-if="courseResource.doc"
:courseName="courseResource.doc.name"
:title="__('Chapters')"
:allowEdit="true"
/>
</div>
@@ -281,10 +264,10 @@
</template>
<script setup>
import {
Breadcrumbs,
TextEditor,
Button,
createResource,
createDocumentResource,
FormControl,
usePageMeta,
toast,
@@ -308,7 +291,6 @@ import {
} from '@/utils'
import { Trash2, X } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
import { sessionStore } from '../../stores/session'
import Link from '@/components/Controls/Link.vue'
import CourseOutline from '@/components/CourseOutline.vue'
@@ -323,39 +305,14 @@ const router = useRouter()
const instructors = ref([])
const related_courses = ref([])
const app = getCurrentInstance()
const { capture } = useTelemetry()
const { updateOnboardingStep } = useOnboarding('learning')
const { $dialog } = app.appContext.config.globalProperties
const props = defineProps({
courseName: {
type: String,
course: {
type: Object,
},
})
const course = reactive({
title: '',
short_introduction: '',
description: '',
video_link: '',
image: null,
card_gradient: '',
tags: '',
category: '',
published: false,
published_on: '',
featured: false,
upcoming: false,
disable_self_learning: false,
enable_certification: false,
paid_course: false,
paid_certificate: false,
course_price: '',
currency: '',
evaluator: '',
timezone: '',
})
const meta = reactive({
description: '',
keywords: '',
@@ -365,18 +322,86 @@ onMounted(() => {
if (!user.data?.is_moderator && !user.data?.is_instructor) {
router.push({ name: 'Courses' })
}
if (props.courseName !== 'new') {
fetchCourseInfo()
} else {
capture('course_form_opened')
}
window.addEventListener('keydown', keyboardShortcut)
})
const fetchCourseInfo = () => {
courseResource.reload()
getMetaInfo('courses', props.courseName, meta)
const courseResource = createDocumentResource({
doctype: 'LMS Course',
name: props.course.data?.name,
auto: true,
transform(data) {
updateCourseData(data)
},
onSuccess(data) {
check_permission()
getMetaInfo('courses', data.name, meta)
},
})
const updateCourseData = (data) => {
Object.keys(data).forEach((key) => {
if (key == 'instructors') {
instructors.value = []
data.instructors.forEach((instructor) => {
instructors.value.push(instructor.instructor)
})
} else if (key == 'related_courses') {
related_courses.value = []
data.related_courses.forEach((course) => {
related_courses.value.push(course.course)
})
}
})
let checkboxes = [
'published',
'upcoming',
'disable_self_learning',
'paid_course',
'featured',
'enable_certification',
'paid_certificate',
]
for (let idx in checkboxes) {
let key = checkboxes[idx]
data[key] = data[key] ? true : false
}
}
const submitCourse = () => {
validateFields()
updateCourse()
}
const validateFields = () => {
courseResource.doc.description = sanitizeHTML(courseResource.doc.description)
Object.keys(courseResource.doc).forEach((key) => {
if (key != 'description' && typeof courseResource.doc[key] === 'string') {
courseResource.doc[key] = escapeHTML(courseResource.doc[key])
}
})
}
const updateCourse = () => {
courseResource.setValue.submit({
...courseResource.doc,
instructors: instructors.value.map((instructor) => ({
instructor: instructor,
})),
related_courses: related_courses.value.map((course) => ({
course: course,
})),
}, {
onSuccess() {
updateMetaInfo('courses', courseResource.doc?.name, meta)
toast.success(__('Course updated successfully'))
},
onError(err) {
toast.error(err.messages?.[0] || err)
console.error(err)
},
})
}
const keyboardShortcut = (e) => {
@@ -394,151 +419,11 @@ onBeforeUnmount(() => {
window.removeEventListener('keydown', keyboardShortcut)
})
const courseCreationResource = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'LMS Course',
image: course.image,
instructors: instructors.value.map((instructor) => ({
instructor: instructor,
})),
related_courses: related_courses.value.map((course) => ({
course: course,
})),
...values,
},
}
},
})
const courseEditResource = createResource({
url: 'frappe.client.set_value',
auto: false,
makeParams(values) {
return {
doctype: 'LMS Course',
name: values.course,
fieldname: {
image: course.image,
instructors: instructors.value.map((instructor) => ({
instructor: instructor,
})),
related_courses: related_courses.value.map((course) => ({
course: course,
})),
...course,
},
}
},
})
const courseResource = createResource({
url: 'frappe.client.get',
makeParams(values) {
return {
doctype: 'LMS Course',
name: props.courseName,
}
},
auto: false,
onSuccess(data) {
Object.keys(data).forEach((key) => {
if (key == 'instructors') {
instructors.value = []
data.instructors.forEach((instructor) => {
instructors.value.push(instructor.instructor)
})
} else if (key == 'related_courses') {
related_courses.value = []
data.related_courses.forEach((course) => {
related_courses.value.push(course.course)
})
} else if (Object.hasOwn(course, key)) course[key] = data[key]
})
let checkboxes = [
'published',
'upcoming',
'disable_self_learning',
'paid_course',
'featured',
'enable_certification',
'paid_certificate',
]
for (let idx in checkboxes) {
let key = checkboxes[idx]
course[key] = course[key] ? true : false
}
check_permission()
},
})
const validateFields = () => {
course.description = sanitizeHTML(course.description)
Object.keys(course).forEach((key) => {
if (key != 'description' && typeof course[key] === 'string') {
course[key] = escapeHTML(course[key])
}
})
}
const submitCourse = () => {
validateFields()
if (courseResource.data) {
editCourse()
} else {
createCourse()
}
}
const createCourse = () => {
courseCreationResource.submit(course, {
onSuccess(data) {
updateMetaInfo('courses', data.name, meta)
if (user.data?.is_system_manager) {
updateOnboardingStep('create_first_course', true, false, () => {
localStorage.setItem('firstCourse', data.name)
})
}
capture('course_created')
toast.success(__('Course created successfully'))
router.push({
name: 'CourseForm',
params: { courseName: data.name },
})
},
onError(err) {
toast.error(err.messages?.[0] || err)
},
})
}
const editCourse = () => {
courseEditResource.submit(
{
course: courseResource.data.name,
},
{
onSuccess() {
updateMetaInfo('courses', props.courseName, meta)
toast.success(__('Course updated successfully'))
},
onError(err) {
toast.error(err.messages?.[0] || err)
},
}
)
}
const deleteCourse = createResource({
url: 'lms.lms.api.delete_course',
makeParams(values) {
return {
course: props.courseName,
course: courseResource.doc?.name,
}
},
onSuccess() {
@@ -567,24 +452,17 @@ const trashCourse = () => {
})
}
watch(
() => props.courseName !== 'new',
(newVal) => {
if (newVal) {
fetchCourseInfo()
}
}
)
const updateTags = () => {
if (newTag.value) {
course.tags = course.tags ? `${course.tags}, ${newTag.value}` : newTag.value
courseResource.doc.tags = courseResource.doc.tags
? `${courseResource.doc.tags}, ${newTag.value}`
: newTag.value
newTag.value = ''
}
}
const removeTag = (tag) => {
course.tags = course.tags
courseResource.doc.tags = courseResource.doc.tags
?.split(', ')
.filter((t) => t !== tag)
.join(', ')
@@ -606,29 +484,9 @@ const check_permission = () => {
}
}
const breadcrumbs = computed(() => {
let crumbs = [
{
label: 'Courses',
route: { name: 'Courses' },
},
]
if (courseResource.data) {
crumbs.push({
label: course.title,
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
})
}
crumbs.push({
label: props.courseName == 'new' ? 'New Course' : 'Edit Course',
route: { name: 'CourseForm', params: { courseName: props.courseName } },
})
return crumbs
})
usePageMeta(() => {
return {
title: courseResource.data?.title || __('New Course'),
title: courseResource.doc?.title,
icon: brand.favicon,
}
})

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="p-5">
<div class="flex justify-between w-full space-x-5">
<div class="md:w-2/3">
<div class="text-3xl font-semibold text-ink-gray-9">

View File

@@ -13,10 +13,7 @@
label: __('New Course'),
icon: 'book-open',
onClick() {
router.push({
name: 'CourseForm',
params: { courseName: 'new' },
})
showCourseModal = true
},
},
{
@@ -109,6 +106,7 @@
</Button>
</div>
</div>
<NewCourseModal v-if="showCourseModal" v-model="showCourseModal" :courses="courses" />
</template>
<script setup>
import {
@@ -129,6 +127,7 @@ import { canCreateCourse } from '@/utils'
import CourseCard from '@/components/CourseCard.vue'
import EmptyState from '@/components/EmptyState.vue'
import { useRouter } from 'vue-router'
import NewCourseModal from '@/pages/Courses/NewCourseModal.vue'
const user = inject('$user')
const dayjs = inject('$dayjs')
@@ -143,6 +142,7 @@ const currentTab = ref('Live')
const { brand } = sessionStore()
const courseCount = ref(0)
const router = useRouter()
const showCourseModal = ref(false)
onMounted(() => {
setFiltersFromQuery()

View File

@@ -0,0 +1,150 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Create Course'),
size: '3xl',
}"
>
<template #body-content>
<div class="text-base">
<div class="grid grid-cols-2 gap-5 border-b mb-5">
<FormControl
v-model="course.title"
:label="__('Title')"
:required="true"
/>
<Link
doctype="LMS Category"
v-model="course.category"
:label="__('Category')"
:allowCreate="true"
@create="
() => {
openSettings('Categories')
show = false
}
"
/>
<MultiSelect
v-model="course.instructors"
doctype="User"
:label="__('Instructors')"
:filters="{ ignore_user_type: 1 }"
:onCreate="(close: () => void) => openSettings('Members', close)"
:required="true"
/>
<Uploader
v-model="course.image"
:label="__('Course Image')"
:required="false"
/>
</div>
<div class="space-y-4">
<FormControl
v-model="course.short_introduction"
:label="__('Short Introduction')"
type="textarea"
:required="true"
:rows="4"
/>
<div class="">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Course Description') }}
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:content="course.description"
@change="(val: string) => (course.description = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem]"
/>
</div>
</div>
</div>
</template>
<template #actions="{ close }">
<div class="text-right">
<Button variant="solid" @click="saveCourse(close)">
{{ __('Create') }}
</Button>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { Link, useOnboarding, useTelemetry } from "frappe-ui/frappe"
import { onMounted, onBeforeUnmount, ref, watch } from 'vue';
import { useRouter } from 'vue-router'
import { openSettings } from '@/utils'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import Uploader from '@/components/Controls/Uploader.vue'
const show = defineModel<boolean>({ required: true, default: false })
const router = useRouter()
const { capture } = useTelemetry()
const { updateOnboardingStep } = useOnboarding('learning')
const props = defineProps<{
courses: any
}>()
const course = ref({
title: '',
short_introduction: '',
description: '',
instructors: [],
category: null,
image: null,
})
const saveCourse = (close: () => void = () => {}) => {
props.courses.insert.submit({
...course.value,
instructors: course.value.instructors.map((instructor) => ({
instructor: instructor,
})),
}, {
onSuccess(data: any) {
toast.success(__('Course created successfully'))
close()
capture('course_created')
updateOnboardingStep('create_first_course', true, false, () => {
localStorage.setItem('firstCourse', data.name)
})
router.push({
name: 'CourseDetail',
params: { courseName: data.name },
hash: '#settings',
})
},
})
}
const keyboardShortcut = (e: KeyboardEvent) => {
if (
e.key === 's' &&
(e.ctrlKey || e.metaKey) &&
e.target &&
e.target instanceof HTMLElement &&
!e.target.classList.contains('ProseMirror')
) {
saveCourse()
e.preventDefault()
}
}
onMounted(() => {
window.addEventListener('keydown', keyboardShortcut)
})
onBeforeUnmount(() => {
window.removeEventListener('keydown', keyboardShortcut)
})
watch(show, () => {
capture('course_form_opened')
})
</script>

View File

@@ -118,12 +118,6 @@ const routes = [
component: () => import('@/pages/JobApplications.vue'),
props: true,
},
{
path: '/courses/:courseName/edit',
name: 'CourseForm',
component: () => import('@/pages/Courses/CourseForm.vue'),
props: true,
},
{
path: '/courses/:courseName/learn/:chapterNumber-:lessonNumber/edit',
name: 'LessonForm',