feat: onboarding

This commit is contained in:
Jannat Patel
2024-10-29 23:00:38 +05:30
parent df3bca6405
commit 19b759e9fb
9 changed files with 141 additions and 67 deletions

View File

@@ -56,6 +56,23 @@
}} }}
</div> </div>
</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> </div>
<ExplanationVideos v-model="showExplanation" :type="type" /> <ExplanationVideos v-model="showExplanation" :type="type" />
</template> </template>

View File

@@ -2,11 +2,11 @@
<Dialog <Dialog
v-model="show" v-model="show"
:options="{ :options="{
title: __('Add Chapter'), title: chapterDetail ? __('Edit Chapter') : __('Add Chapter'),
size: 'lg', size: 'lg',
actions: [ actions: [
{ {
label: chapterDetail ? __('Edit Chapter') : __('Add Chapter'), label: chapterDetail ? __('Edit') : __('Create'),
variant: 'solid', variant: 'solid',
onClick: (close) => onClick: (close) =>
chapterDetail ? editChapter(close) : addChapter(close), chapterDetail ? editChapter(close) : addChapter(close),

View File

@@ -1,7 +1,7 @@
<template> <template>
<div v-if="quiz.data"> <div v-if="quiz.data">
<div <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"> <div class="leading-5">
{{ {{

View File

@@ -27,10 +27,11 @@
<FormControl <FormControl
v-model="course.short_introduction" v-model="course.short_introduction"
:label="__('Short Introduction')" :label="__('Short Introduction')"
:placeholder="__('A one line introduction to the course that appears on the course card')"
class="mb-4" class="mb-4"
/> />
<div 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') }} {{ __('Course Description') }}
</div> </div>
<TextEditor <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]" editorClass="prose-sm max-w-none border-b border-x bg-gray-100 rounded-b-md py-1 px-2 min-h-[7rem]"
/> />
</div> </div>
<FileUploader <div class="mb-4">
v-if="!course.course_image" <div class="text-xs text-gray-600 mb-2">
: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">
{{ __('Course Image') }} {{ __('Course Image') }}
</div> </div>
<div class="flex items-center"> <FileUploader
<div class="border rounded-md p-2 mr-2"> v-if="!course.course_image"
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" /> :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>
<div class="flex flex-col"> <!-- <div class="flex items-center">
<span> <div class="border rounded-md p-2 mr-2">
{{ course.course_image.file_name }} <img :src="course.course_image.file_url" class="border rounded-md" />
</span> </div>
<span class="text-sm text-gray-500 mt-1"> <div class="flex flex-col">
{{ getFileSize(course.course_image.file_size) }} <span>
</span> {{ course.course_image.file_name }}
</div> </span>
<X <span class="text-sm text-gray-500 mt-1">
@click="removeImage()" {{ getFileSize(course.course_image.file_size) }}
class="bg-gray-200 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4" </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>
</div> </div>
<FormControl <FormControl
v-model="course.video_link" v-model="course.video_link"
:label="__('Preview Video')" :label="__('Preview Video')"
:placeholder="__('Paste the youtube link of a short video introducing the course')"
class="mb-4" class="mb-4"
/> />
<div class="mb-4"> <div class="mb-4">
@@ -104,6 +125,8 @@
</div> </div>
<FormControl <FormControl
v-model="newTag" v-model="newTag"
:placeholder="__('Keywords for the course')"
class="w-52"
@keyup.enter="updateTags()" @keyup.enter="updateTags()"
id="tags" id="tags"
/> />
@@ -130,7 +153,7 @@
<div class="grid grid-cols-3 gap-10 mb-4"> <div class="grid grid-cols-3 gap-10 mb-4">
<div <div
v-if="user.data?.is_moderator" v-if="user.data?.is_moderator"
class="flex flex-col space-y-3" class="flex flex-col space-y-4"
> >
<FormControl <FormControl
type="checkbox" type="checkbox"
@@ -231,7 +254,7 @@ import {
updateDocumentTitle, updateDocumentTitle,
} from '@/utils' } from '@/utils'
import Link from '@/components/Controls/Link.vue' 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 { useRouter } from 'vue-router'
import CourseOutline from '@/components/CourseOutline.vue' import CourseOutline from '@/components/CourseOutline.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue' import MultiSelect from '@/components/Controls/MultiSelect.vue'

View File

@@ -101,14 +101,41 @@
<CourseCard :course="course" /> <CourseCard :course="course" />
</router-link> </router-link>
</div> </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 <div
v-else 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"> <BookOpen class="size-10 mx-auto stroke-1 text-gray-500" />
<div> <div class="text-xl font-medium">
{{ __('No {0} courses found').format(tab.label.toLowerCase()) }} {{ __("No courses found") }}
</div> </div>
<div>
{{ __("There are no courses available at the moment. Keep an eye out, fresh learning experiences are on the way soon!") }}
</div> </div>
</div> </div>
</template> </template>
@@ -127,13 +154,14 @@ import {
createResource, createResource,
} from 'frappe-ui' } from 'frappe-ui'
import CourseCard from '@/components/CourseCard.vue' 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 { ref, computed, inject, onMounted, watch } from 'vue'
import { updateDocumentTitle } from '@/utils' import { updateDocumentTitle } from '@/utils'
const user = inject('$user') const user = inject('$user')
const searchQuery = ref('') const searchQuery = ref('')
const currentCategory = ref(null) const currentCategory = ref(null)
const noCoursesFound = ref(false)
onMounted(() => { onMounted(() => {
let queries = new URLSearchParams(location.search) let queries = new URLSearchParams(location.search)
@@ -145,7 +173,7 @@ onMounted(() => {
const courses = createResource({ const courses = createResource({
url: 'lms.lms.utils.get_courses', url: 'lms.lms.utils.get_courses',
cache: ['courses', user.data?.email], cache: ['courses', user.data?.email],
auto: true, auto: true
}) })
const tabIndex = ref(0) const tabIndex = ref(0)

View File

@@ -69,7 +69,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { Breadcrumbs, FormControl, createResource, Button } from 'frappe-ui' import { Breadcrumbs, Button, createResource, FormControl } from 'frappe-ui'
import { import {
computed, computed,
reactive, reactive,

View File

@@ -51,7 +51,7 @@ export class Quiz {
app.mount(this.wrapper) app.mount(this.wrapper)
return 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"> <span class="font-medium">
Quiz: ${quiz} Quiz: ${quiz}
</span> </span>

View File

@@ -9,9 +9,8 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"course", "course",
"title",
"column_break_3", "column_break_3",
"description", "title",
"section_break_5", "section_break_5",
"lessons" "lessons"
], ],
@@ -35,11 +34,6 @@
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{ {
"fieldname": "section_break_5", "fieldname": "section_break_5",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@@ -59,7 +53,7 @@
"link_fieldname": "chapter" "link_fieldname": "chapter"
} }
], ],
"modified": "2023-09-29 17:03:58.013819", "modified": "2024-10-29 16:54:20.904683",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Course Chapter", "name": "Course Chapter",

View File

@@ -157,11 +157,12 @@ def get_lesson_details(chapter, progress=False):
"file_type", "file_type",
"instructor_notes", "instructor_notes",
"course", "course",
"content"
], ],
as_dict=True, as_dict=True,
) )
lesson_details.number = f"{chapter.idx}.{row.idx}" 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: if progress:
lesson_details.is_complete = get_progress(lesson_details.course, lesson_details.name) 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 return lessons
def get_lesson_icon(content): def get_lesson_icon(body, content):
icon = None if content:
macros = find_macros(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: for macro in macros:
if macro[0] == "YouTubeVideo" or macro[0] == "Video": if macro[0] == "YouTubeVideo" or macro[0] == "Video":
icon = "icon-youtube" return "icon-youtube"
elif macro[0] == "Quiz": elif macro[0] == "Quiz":
icon = "icon-quiz" return "icon-quiz"
if not icon: return "icon-list"
icon = "icon-list"
return icon
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)