feat: onboarding
This commit is contained in:
@@ -56,6 +56,23 @@
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
class="flex items-center text-sm font-medium space-x-2 cursor-pointer"
|
||||
>
|
||||
<span>
|
||||
{{ __('What does include in preview mean?') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mb-1 leading-5">
|
||||
{{
|
||||
__(
|
||||
'If Include in Preview is enabled for a lesson then the lesson will also be accessible to non logged in users.'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ExplanationVideos v-model="showExplanation" :type="type" />
|
||||
</template>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Add Chapter'),
|
||||
title: chapterDetail ? __('Edit Chapter') : __('Add Chapter'),
|
||||
size: 'lg',
|
||||
actions: [
|
||||
{
|
||||
label: chapterDetail ? __('Edit Chapter') : __('Add Chapter'),
|
||||
label: chapterDetail ? __('Edit') : __('Create'),
|
||||
variant: 'solid',
|
||||
onClick: (close) =>
|
||||
chapterDetail ? editChapter(close) : addChapter(close),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="quiz.data">
|
||||
<div
|
||||
class="bg-blue-100 space-y-1 py-2 px-2 rounded-md text-sm text-blue-800"
|
||||
class="bg-blue-100 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-blue-800"
|
||||
>
|
||||
<div class="leading-5">
|
||||
{{
|
||||
|
||||
@@ -27,10 +27,11 @@
|
||||
<FormControl
|
||||
v-model="course.short_introduction"
|
||||
:label="__('Short Introduction')"
|
||||
:placeholder="__('A one line introduction to the course that appears on the course card')"
|
||||
class="mb-4"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
<div class="mb-1.5 text-sm text-gray-700">
|
||||
<div class="mb-1.5 text-sm text-gray-600">
|
||||
{{ __('Course Description') }}
|
||||
</div>
|
||||
<TextEditor
|
||||
@@ -41,49 +42,69 @@
|
||||
editorClass="prose-sm max-w-none border-b border-x bg-gray-100 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||
/>
|
||||
</div>
|
||||
<FileUploader
|
||||
v-if="!course.course_image"
|
||||
:fileTypes="['image/*']"
|
||||
:validateFile="validateFile"
|
||||
@success="(file) => saveImage(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 v-else class="mb-4">
|
||||
<div class="text-xs text-gray-600 mb-1">
|
||||
<div class="mb-4">
|
||||
<div class="text-xs text-gray-600 mb-2">
|
||||
{{ __('Course Image') }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="border rounded-md p-2 mr-2">
|
||||
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" />
|
||||
<FileUploader
|
||||
v-if="!course.course_image"
|
||||
:fileTypes="['image/*']"
|
||||
:validateFile="validateFile"
|
||||
@success="(file) => saveImage(file)"
|
||||
>
|
||||
<template
|
||||
v-slot="{ file, progress, uploading, openFileSelector }"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="border rounded-md w-fit py-5 px-20">
|
||||
<Image class="size-5 stroke-1 text-gray-700" />
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<Button @click="openFileSelector">
|
||||
{{ __("Upload") }}
|
||||
</Button>
|
||||
<div class="mt-2 text-gray-600 text-sm">
|
||||
{{ __("Appears on the course card in the course list") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FileUploader>
|
||||
<div v-else class="mb-4">
|
||||
<div class="flex items-center">
|
||||
<img :src="course.course_image.file_url" class="border rounded-md w-40" />
|
||||
<div class="ml-4">
|
||||
<Button @click="removeImage()">
|
||||
{{ __("Remove") }}
|
||||
</Button>
|
||||
<div class="mt-2 text-gray-600 text-sm">
|
||||
{{ __("Appears on the course card in the course list") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span>
|
||||
{{ course.course_image.file_name }}
|
||||
</span>
|
||||
<span class="text-sm text-gray-500 mt-1">
|
||||
{{ getFileSize(course.course_image.file_size) }}
|
||||
</span>
|
||||
</div>
|
||||
<X
|
||||
@click="removeImage()"
|
||||
class="bg-gray-200 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
|
||||
/>
|
||||
<!-- <div class="flex items-center">
|
||||
<div class="border rounded-md p-2 mr-2">
|
||||
<img :src="course.course_image.file_url" class="border rounded-md" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span>
|
||||
{{ course.course_image.file_name }}
|
||||
</span>
|
||||
<span class="text-sm text-gray-500 mt-1">
|
||||
{{ getFileSize(course.course_image.file_size) }}
|
||||
</span>
|
||||
</div>
|
||||
<X
|
||||
@click="removeImage()"
|
||||
class="bg-gray-200 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
|
||||
/>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="course.video_link"
|
||||
:label="__('Preview Video')"
|
||||
:placeholder="__('Paste the youtube link of a short video introducing the course')"
|
||||
class="mb-4"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
@@ -104,6 +125,8 @@
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="newTag"
|
||||
:placeholder="__('Keywords for the course')"
|
||||
class="w-52"
|
||||
@keyup.enter="updateTags()"
|
||||
id="tags"
|
||||
/>
|
||||
@@ -130,7 +153,7 @@
|
||||
<div class="grid grid-cols-3 gap-10 mb-4">
|
||||
<div
|
||||
v-if="user.data?.is_moderator"
|
||||
class="flex flex-col space-y-3"
|
||||
class="flex flex-col space-y-4"
|
||||
>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
@@ -231,7 +254,7 @@ import {
|
||||
updateDocumentTitle,
|
||||
} from '@/utils'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { FileText, X } from 'lucide-vue-next'
|
||||
import { FileText, Image, X } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||
|
||||
@@ -101,14 +101,41 @@
|
||||
<CourseCard :course="course" />
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-else-if="user.data?.is_moderator || user.data?.is_instructor" class="grid grid-cols-3 p-5">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'CourseForm',
|
||||
params: {
|
||||
courseName: 'new',
|
||||
},
|
||||
}"
|
||||
>
|
||||
<div class="bg-gray-50 py-32 px-5 rounded-md">
|
||||
<div class="flex flex-col items-center text-center space-y-2">
|
||||
<Plus
|
||||
class="size-10 stroke-1 text-gray-800 p-1 rounded-full border bg-white"
|
||||
/>
|
||||
<div class="font-medium">
|
||||
{{ __("Create a Course") }}
|
||||
</div>
|
||||
<span class="text-gray-700 text-sm leading-4">
|
||||
{{ __("You can add chapters and lessons to it.") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="grid flex-1 place-items-center text-xl font-medium text-gray-500"
|
||||
class="text-center p-5 text-gray-600 mt-20 w-1/2 mx-auto space-y-2"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center mt-4">
|
||||
<div>
|
||||
{{ __('No {0} courses found').format(tab.label.toLowerCase()) }}
|
||||
</div>
|
||||
<BookOpen class="size-10 mx-auto stroke-1 text-gray-500" />
|
||||
<div class="text-xl font-medium">
|
||||
{{ __("No courses found") }}
|
||||
</div>
|
||||
<div>
|
||||
{{ __("There are no courses available at the moment. Keep an eye out, fresh learning experiences are on the way soon!") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -127,13 +154,14 @@ import {
|
||||
createResource,
|
||||
} from 'frappe-ui'
|
||||
import CourseCard from '@/components/CourseCard.vue'
|
||||
import { Plus, Search } from 'lucide-vue-next'
|
||||
import { BookOpen, Plus, Search } from 'lucide-vue-next'
|
||||
import { ref, computed, inject, onMounted, watch } from 'vue'
|
||||
import { updateDocumentTitle } from '@/utils'
|
||||
|
||||
const user = inject('$user')
|
||||
const searchQuery = ref('')
|
||||
const currentCategory = ref(null)
|
||||
const noCoursesFound = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
let queries = new URLSearchParams(location.search)
|
||||
@@ -145,7 +173,7 @@ onMounted(() => {
|
||||
const courses = createResource({
|
||||
url: 'lms.lms.utils.get_courses',
|
||||
cache: ['courses', user.data?.email],
|
||||
auto: true,
|
||||
auto: true
|
||||
})
|
||||
|
||||
const tabIndex = ref(0)
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Breadcrumbs, FormControl, createResource, Button } from 'frappe-ui'
|
||||
import { Breadcrumbs, Button, createResource, FormControl } from 'frappe-ui'
|
||||
import {
|
||||
computed,
|
||||
reactive,
|
||||
|
||||
@@ -51,7 +51,7 @@ export class Quiz {
|
||||
app.mount(this.wrapper)
|
||||
return
|
||||
}
|
||||
this.wrapper.innerHTML = `<div class='border rounded-md p-10 text-center mb-2'>
|
||||
this.wrapper.innerHTML = `<div class='border rounded-md p-10 text-center bg-gray-50 mb-2'>
|
||||
<span class="font-medium">
|
||||
Quiz: ${quiz}
|
||||
</span>
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"course",
|
||||
"title",
|
||||
"column_break_3",
|
||||
"description",
|
||||
"title",
|
||||
"section_break_5",
|
||||
"lessons"
|
||||
],
|
||||
@@ -35,11 +34,6 @@
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -59,7 +53,7 @@
|
||||
"link_fieldname": "chapter"
|
||||
}
|
||||
],
|
||||
"modified": "2023-09-29 17:03:58.013819",
|
||||
"modified": "2024-10-29 16:54:20.904683",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Course Chapter",
|
||||
|
||||
@@ -157,11 +157,12 @@ def get_lesson_details(chapter, progress=False):
|
||||
"file_type",
|
||||
"instructor_notes",
|
||||
"course",
|
||||
"content"
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
lesson_details.number = f"{chapter.idx}.{row.idx}"
|
||||
lesson_details.icon = get_lesson_icon(lesson_details.body)
|
||||
lesson_details.icon = get_lesson_icon(lesson_details.body, lesson_details.content)
|
||||
|
||||
if progress:
|
||||
lesson_details.is_complete = get_progress(lesson_details.course, lesson_details.name)
|
||||
@@ -170,20 +171,31 @@ def get_lesson_details(chapter, progress=False):
|
||||
return lessons
|
||||
|
||||
|
||||
def get_lesson_icon(content):
|
||||
icon = None
|
||||
macros = find_macros(content)
|
||||
def get_lesson_icon(body, content):
|
||||
if content:
|
||||
content = json.loads(content)
|
||||
|
||||
for block in content.get("blocks"):
|
||||
if block.get("type") == "upload" and block.get("data").get("file_type").lower() in ["mp4", "webm", "ogg", "mov"]:
|
||||
return "icon-youtube"
|
||||
|
||||
if block.get("type") == "embed" and block.get("data").get("service") in ["youtube", "vimeo"]:
|
||||
return "icon-youtube"
|
||||
|
||||
if block.get("type") == "quiz":
|
||||
return "icon-quiz"
|
||||
|
||||
return "icon-list"
|
||||
|
||||
macros = find_macros(body)
|
||||
for macro in macros:
|
||||
if macro[0] == "YouTubeVideo" or macro[0] == "Video":
|
||||
icon = "icon-youtube"
|
||||
return "icon-youtube"
|
||||
elif macro[0] == "Quiz":
|
||||
icon = "icon-quiz"
|
||||
return "icon-quiz"
|
||||
|
||||
if not icon:
|
||||
icon = "icon-list"
|
||||
return "icon-list"
|
||||
|
||||
return icon
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
|
||||
Reference in New Issue
Block a user