Merge branch 'frappe:develop' into feat/scorm-progress

This commit is contained in:
Fahid Latheef A
2025-03-02 14:17:26 +05:30
committed by GitHub
64 changed files with 7589 additions and 4285 deletions

View File

@@ -19,6 +19,7 @@
"@editorjs/paragraph": "^2.11.3",
"@editorjs/simple-image": "^1.6.0",
"@editorjs/table": "^2.4.2",
"@vueuse/router": "^12.7.0",
"ace-builds": "^1.36.2",
"apexcharts": "^4.3.0",
"chart.js": "^4.4.1",

View File

@@ -69,7 +69,11 @@
name: batch.data.name,
},
}"
v-else-if="batch.data.paid_batch && batch.data.seats_left"
v-else-if="
batch.data.paid_batch &&
batch.data.seats_left > 0 &&
batch.data.accept_enrollments
"
>
<Button v-if="!isStudent" class="w-full mt-4" variant="solid">
<span>
@@ -80,7 +84,11 @@
<Button
variant="solid"
class="w-full mt-2"
v-else-if="batch.data.allow_self_enrollment && batch.data.seats_left"
v-else-if="
batch.data.allow_self_enrollment &&
batch.data.seats_left &&
batch.data.accept_enrollments
"
@click="enrollInBatch()"
>
{{ __('Enroll Now') }}
@@ -112,6 +120,7 @@ import { useRouter } from 'vue-router'
const router = useRouter()
const user = inject('$user')
const dayjs = inject('$dayjs')
const props = defineProps({
batch: {

View File

@@ -0,0 +1,67 @@
<template>
<div
v-if="
certification.data &&
certification.data.membership &&
certification.data.paid_certificate &&
user.data?.is_student
"
>
<router-link
v-if="!certification.data.membership.purchased_certificate"
:to="{
name: 'Billing',
params: {
type: 'certificate',
name: courseName,
},
}"
>
<Button class="w-full">
<template #prefix>
<GraduationCap class="size-4 stroke-1.5" />
</template>
{{ __('Get Certified') }}
</Button>
</router-link>
<router-link
v-else-if="!certification.data.membership.certficate"
:to="{
name: 'CourseCertification',
params: {
courseName: courseName,
},
}"
>
<Button class="w-full">
<template #prefix>
<GraduationCap class="size-4 stroke-1.5" />
</template>
{{ __('Get Certified') }}
</Button>
</router-link>
</div>
</template>
<script setup>
import { Button, createResource } from 'frappe-ui'
import { inject } from 'vue'
import { GraduationCap } from 'lucide-vue-next'
const user = inject('$user')
const props = defineProps({
courseName: {
type: String,
required: true,
},
})
const certification = createResource({
url: 'lms.lms.api.get_certification_details',
params: {
course: props.courseName,
},
auto: true,
cache: ['certificationData', user.data?.name],
})
</script>

View File

@@ -100,9 +100,15 @@
<CourseInstructors :instructors="course.instructors" />
</div>
<div class="font-semibold">
<div v-if="course.paid_course" class="font-semibold">
{{ course.price }}
</div>
<div
v-if="course.paid_certificate || course.enable_certification"
class="text-xs text-ink-blue-3 bg-surface-blue-1 py-0.5 px-1 rounded-md"
>
{{ __('Certification') }}
</div>
</div>
</div>
</div>

View File

@@ -6,30 +6,32 @@
class="rounded-t-md min-h-56 w-full"
/>
<div class="p-5">
<div v-if="course.data.price" class="text-2xl font-semibold mb-3">
<div v-if="course.data.paid_course" class="text-2xl font-semibold mb-3">
{{ course.data.price }}
</div>
<router-link
v-if="course.data.membership"
:to="{
name: 'Lesson',
params: {
courseName: course.name,
chapterNumber: course.data.current_lesson
? course.data.current_lesson.split('-')[0]
: 1,
lessonNumber: course.data.current_lesson
? course.data.current_lesson.split('-')[1]
: 1,
},
}"
>
<Button variant="solid" size="md" class="w-full">
<span>
{{ __('Continue Learning') }}
</span>
</Button>
</router-link>
<div v-if="course.data.membership" class="space-y-2">
<router-link
:to="{
name: 'Lesson',
params: {
courseName: course.name,
chapterNumber: course.data.current_lesson
? course.data.current_lesson.split('-')[0]
: 1,
lessonNumber: course.data.current_lesson
? course.data.current_lesson.split('-')[1]
: 1,
},
}"
>
<Button variant="solid" size="md" class="w-full">
<span>
{{ __('Continue Learning') }}
</span>
</Button>
</router-link>
<CertificationLinks :courseName="course.data.name" />
</div>
<router-link
v-else-if="course.data.paid_course"
:to="{
@@ -113,17 +115,36 @@
{{ course.data.rating }} {{ __('Rating') }}
</span>
</div>
<div
v-if="course.data.enable_certification"
class="flex items-center font-semibold text-ink-gray-9"
>
<GraduationCap class="h-4 w-4 stroke-2" />
<span class="ml-2">
{{ __('Certificate of Completion') }}
</span>
</div>
<div
v-if="course.data.paid_certificate"
class="flex items-center font-semibold text-ink-gray-9"
>
<GraduationCap class="h-4 w-4 stroke-2" />
<span class="ml-2">
{{ __('Paid Certificate after Evaluation') }}
</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { BookOpen, Users, Star } from 'lucide-vue-next'
import { BookOpen, Users, Star, GraduationCap } from 'lucide-vue-next'
import { computed, inject } from 'vue'
import { Button, createResource } from 'frappe-ui'
import { Button, createResource, Tooltip } from 'frappe-ui'
import { showToast, formatAmount } from '@/utils/'
import { capture } from '@/telemetry'
import { useRouter } from 'vue-router'
import CertificationLinks from '@/components/CertificationLinks.vue'
const router = useRouter()
const user = inject('$user')

View File

@@ -38,7 +38,7 @@
<div class="flex mt-2">
<Star
v-for="index in 5"
class="h-5 w-5 text-ink-gray-2 rounded-sm mr-2"
class="h-5 w-5 text-ink-gray-1 rounded-sm mr-2"
:class="
index <= Math.ceil(review.rating)
? 'fill-orange-500'

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex space-x-4 border rounded-md p-2">
<Avatar :image="job.company_logo" :label="job.job_title" size="2xl" />
<img :src="job.company_logo" class="size-10 rounded-full object-contain" />
<div class="flex flex-col space-y-2 flex-1">
<div class="flex items-center justify-between">
<span class="font-semibold text-ink-gray-9">

View File

@@ -36,7 +36,7 @@
<FormControl
v-model="member.first_name"
:placeholder="__('First Name')"
type="test"
type="text"
class="w-full"
/>
<Button @click="addMember()" variant="subtle">

View File

@@ -31,7 +31,7 @@
</template>
<script setup>
import { Dialog, createResource } from 'frappe-ui'
import { ref, defineModel } from 'vue'
import { ref } from 'vue'
import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils'

View File

@@ -77,7 +77,7 @@ import {
FormControl,
Switch,
} from 'frappe-ui'
import { defineModel, reactive, watch } from 'vue'
import { reactive, watch } from 'vue'
import { showToast, getFileSize } from '@/utils/'
import { capture } from '@/telemetry'
import { FileText, X } from 'lucide-vue-next'

View File

@@ -35,7 +35,7 @@
</template>
<script setup>
import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui'
import { reactive, defineModel } from 'vue'
import { reactive } from 'vue'
import { showToast, singularize } from '@/utils'
const topics = defineModel('reloadTopics')

View File

@@ -94,7 +94,7 @@ import {
createResource,
TextEditor,
} from 'frappe-ui'
import { reactive, watch, defineModel } from 'vue'
import { reactive, watch } from 'vue'
import { FileText, X } from 'lucide-vue-next'
import { getFileSize, showToast, escapeHTML } from '@/utils'

View File

@@ -25,7 +25,15 @@
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Date') }}
</div>
<FormControl type="date" v-model="evaluation.date" />
<FormControl
type="date"
v-model="evaluation.date"
:min="
dayjs()
.add(dayjs.duration({ days: 1 }))
.format('YYYY-MM-DD')
"
/>
</div>
<div v-if="slots.data?.length">
<div class="mb-1.5 text-sm text-ink-gray-5">
@@ -58,7 +66,7 @@
</template>
<script setup>
import { Dialog, createResource, Select, FormControl } from 'frappe-ui'
import { defineModel, reactive, watch, inject } from 'vue'
import { reactive, watch, inject } from 'vue'
import { createToast, formatTime } from '@/utils/'
const user = inject('$user')
@@ -161,6 +169,11 @@ const getCourses = () => {
})
}
}
if (courses.length == 1) {
evaluation.course = courses[0].value
}
return courses
}

View File

@@ -66,7 +66,7 @@
<script setup>
import { Dialog, FileUploader, Button, createResource } from 'frappe-ui'
import { FileText } from 'lucide-vue-next'
import { ref, inject, defineModel } from 'vue'
import { ref, inject } from 'vue'
import { createToast, getFileSize } from '@/utils/'
const resume = ref(null)

View File

@@ -33,7 +33,7 @@
</template>
<script setup>
import { Dialog, Textarea, createResource } from 'frappe-ui'
import { defineModel, reactive } from 'vue'
import { reactive } from 'vue'
import Rating from '@/components/Controls/Rating.vue'
import { createToast } from '@/utils/'

View File

@@ -4,12 +4,18 @@
<div class="text-lg font-semibold">
{{ __('Upcoming Evaluations') }}
</div>
<Button @click="openEvalModal">
<Button
v-if="
!upcoming_evals.data?.length ||
upcoming_evals.length == courses.length
"
@click="openEvalModal"
>
{{ __('Schedule Evaluation') }}
</Button>
</div>
<div v-if="upcoming_evals.data?.length">
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-3 gap-4">
<div v-for="evl in upcoming_evals.data">
<div class="border rounded-md p-3">
<div class="font-semibold mb-3">
@@ -28,17 +34,39 @@
</span>
</div>
<div class="flex items-center">
<UserCog2 class="w-4 h-4 stroke-1.5" />
<span class="ml-2 font-medium">
<GraduationCap class="w-4 h-4 stroke-1.5" />
<span class="ml-2">
{{ evl.evaluator_name }}
</span>
</div>
<div class="flex items-center justify-between space-x-2 mt-4">
<Button
v-if="evl.google_meet_link"
@click="openEvalCall(evl)"
class="w-full"
>
<template #prefix>
<HeadsetIcon class="w-4 h-4 stroke-1.5" />
</template>
{{ __('Join Call') }}
</Button>
<Button
v-if="evl.date > dayjs().format()"
@click="cancelEvaluation(evl)"
class="w-full"
>
<template #prefix>
<Ban class="w-4 h-4 stroke-1.5" />
</template>
{{ __('Cancel') }}
</Button>
</div>
</div>
</div>
</div>
</div>
<div v-else class="text-sm italic text-ink-gray-5">
{{ __('No upcoming evaluations.') }}
{{ __('Please schedule an evaluation to get certified.') }}
</div>
</div>
<EvaluationModal
@@ -50,15 +78,23 @@
/>
</template>
<script setup>
import { Calendar, Clock, UserCog2 } from 'lucide-vue-next'
import { inject, ref } from 'vue'
import {
Ban,
Calendar,
Clock,
GraduationCap,
HeadsetIcon,
} from 'lucide-vue-next'
import { inject, ref, getCurrentInstance } from 'vue'
import { formatTime } from '../utils'
import { Button, createResource } from 'frappe-ui'
import { Button, createResource, call } from 'frappe-ui'
import EvaluationModal from '@/components/Modals/EvaluationModal.vue'
const dayjs = inject('$dayjs')
const user = inject('$user')
const showEvalModal = ref(false)
const app = getCurrentInstance()
const { $dialog } = app.appContext.config.globalProperties
const props = defineProps({
batch: {
@@ -77,10 +113,10 @@ const props = defineProps({
const upcoming_evals = createResource({
url: 'lms.lms.utils.get_upcoming_evals',
cache: ['upcoming_evals', user.data.name],
params: {
student: user.data.name,
courses: props.courses.map((course) => course.course),
batch: props.batch,
},
auto: true,
})
@@ -88,4 +124,32 @@ const upcoming_evals = createResource({
function openEvalModal() {
showEvalModal.value = true
}
const openEvalCall = (evl) => {
window.open(evl.google_meet_link, '_blank')
}
const cancelEvaluation = (evl) => {
$dialog({
title: __('Cancel this evaluation?'),
message: __(
'Are you sure you want to cancel this evaluation? This action cannot be undone.'
),
actions: [
{
label: __('Cancel'),
theme: 'red',
variant: 'solid',
onClick(close) {
call('lms.lms.api.cancel_evaluation', { evaluation: evl }).then(
() => {
upcoming_evals.reload()
}
)
close()
},
},
],
})
}
</script>

View File

@@ -6,7 +6,7 @@
<Breadcrumbs class="h-7" :items="breadcrumbs" />
<div class="flex items-center space-x-2">
<Button
v-if="user.data?.is_moderator"
v-if="user.data?.is_moderator && batch.data?.certification"
@click="openCertificateDialog = true"
>
{{ __('Generate Certificates') }}
@@ -193,8 +193,9 @@
<BulkCertificates v-model="openCertificateDialog" :batch="batch.data" />
</template>
<script setup>
import { Breadcrumbs, Button, createResource, Tabs, Badge } from 'frappe-ui'
import { computed, inject, ref } from 'vue'
import { useRouteQuery } from '@vueuse/router'
import { Breadcrumbs, Button, createResource, Tabs, Badge } from 'frappe-ui'
import CourseInstructors from '@/components/CourseInstructors.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import {
@@ -270,7 +271,7 @@ const isStudent = computed(() => {
)
})
const tabIndex = ref(0)
const tabIndex = useRouteQuery('tab', 0)
const tabs = computed(() => {
let batchTabs = []
batchTabs.push({
@@ -313,7 +314,7 @@ const tabs = computed(() => {
})
const redirectToLogin = () => {
window.location.href = `/login?redirect-to=/batches`
window.location.href = `/login?redirect-to=/lms/batches/${props.batchName}`
}
const openAnnouncementModal = () => {

View File

@@ -31,6 +31,11 @@
type="checkbox"
:label="__('Allow self enrollment')"
/>
<FormControl
v-model="batch.certification"
type="checkbox"
:label="__('Certification')"
/>
</div>
</div>
</div>
@@ -293,6 +298,7 @@ const batch = reactive({
medium: '',
category: '',
allow_self_enrollment: false,
certification: false,
image: null,
paid_batch: false,
currency: '',
@@ -362,7 +368,12 @@ const batchDetail = createResource({
batch[key] = `${hours}:${minutes}`
} else if (Object.hasOwn(batch, key)) batch[key] = data[key]
})
let checkboxes = ['published', 'paid_batch', 'allow_self_enrollment']
let checkboxes = [
'published',
'paid_batch',
'allow_self_enrollment',
'certification',
]
for (let idx in checkboxes) {
let key = checkboxes[idx]
batch[key] = batch[key] ? true : false

View File

@@ -26,13 +26,19 @@
{{ __('All Batches') }}
</div>
<div
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-2"
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4"
>
<TabButtons
v-if="user.data"
:buttons="batchTabs"
v-model="currentTab"
/>
<FormControl
v-model="certification"
:label="__('Certification')"
type="checkbox"
@change="updateBatches()"
/>
<div class="grid grid-cols-2 gap-2">
<FormControl
v-model="title"
@@ -111,6 +117,7 @@ const pageLength = ref(20)
const categories = ref([])
const currentCategory = ref(null)
const title = ref('')
const certification = ref(false)
const filters = ref({})
const currentTab = ref(user.data?.is_student ? 'All' : 'Upcoming')
const orderBy = ref('start_date')
@@ -130,6 +137,7 @@ const setFiltersFromQuery = () => {
let queries = new URLSearchParams(location.search)
title.value = queries.get('title') || ''
currentCategory.value = queries.get('category') || null
certification.value = queries.get('certification') || false
}
const batches = createListResource({
@@ -161,6 +169,7 @@ const updateBatches = () => {
const updateFilters = () => {
updateCategoryFilter()
updateTitleFilter()
updateCertificationFilter()
updateTabFilter()
updateStudentFilter()
setQueryParams()
@@ -182,6 +191,14 @@ const updateTitleFilter = () => {
}
}
const updateCertificationFilter = () => {
if (certification.value) {
filters.value['certification'] = 1
} else {
delete filters.value['certification']
}
}
const updateTabFilter = () => {
orderBy.value = 'start_date'
if (!user.data) {
@@ -222,6 +239,7 @@ const setQueryParams = () => {
let filterKeys = {
title: title.value,
category: currentCategory.value,
certification: certification.value,
}
Object.keys(filterKeys).forEach((key) => {

View File

@@ -12,20 +12,15 @@
v-if="access.data?.access && orderSummary.data"
class="pt-5 pb-10 mx-5"
>
<!-- <div class="mb-5">
<div class="text-lg font-semibold">
{{ __('Address') }}
</div>
</div> -->
<div class="flex flex-col lg:flex-row justify-between">
<div
class="h-fit bg-surface-gray-2 rounded-md p-5 space-y-4 lg:order-last mb-10 lg:mt-10 text-sm font-medium lg:w-1/4"
class="h-fit bg-surface-gray-2 rounded-md p-5 space-y-4 lg:order-last mb-10 lg:mt-10 font-medium lg:w-1/3"
>
<div class="flex items-center justify-between space-x-2">
<div class="flex items-baseline justify-between space-y-2">
<div class="text-ink-gray-5">
{{ __('Ordered Item') }}
{{ __('Payment for ') }} {{ type }}:
</div>
<div class="">
<div class="leading-5">
{{ orderSummary.data.title }}
</div>
</div>
@@ -126,7 +121,7 @@
<p class="text-ink-gray-5">
{{
__(
'Make sure to enter the right billing name as the same will be used in your invoice.'
'Make sure to enter the correct billing name as the same will be used in your invoice.'
)
}}
</p>
@@ -140,10 +135,10 @@
<div v-else-if="access.data?.message">
<NotPermitted
:text="access.data.message"
:buttonLabel="
type == 'course' ? 'Checkout Courses' : 'Checkout Batches'
:buttonLabel="type == 'course' ? 'Checkout Course' : 'Checkout Batch'"
:buttonLink="
type == 'course' ? `/lms/courses/${name}` : `/lms/batches/${name}`
"
:buttonLink="type == 'course' ? '/lms/courses' : '/lms/batches'"
/>
</div>
<div v-else-if="!user.data?.name">
@@ -163,7 +158,7 @@ import {
Breadcrumbs,
Tooltip,
} from 'frappe-ui'
import { reactive, inject, onMounted, ref } from 'vue'
import { reactive, inject, onMounted, computed } from 'vue'
import Link from '@/components/Controls/Link.vue'
import NotPermitted from '@/components/NotPermitted.vue'
import { showToast } from '@/utils/'
@@ -193,7 +188,7 @@ const props = defineProps({
const access = createResource({
url: 'lms.lms.api.validate_billing_access',
params: {
type: props.type,
billing_type: props.type,
name: props.name,
},
onSuccess(data) {
@@ -206,7 +201,7 @@ const orderSummary = createResource({
url: 'lms.lms.utils.get_order_summary',
makeParams(values) {
return {
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
docname: props.name,
country: billingDetails.country,
}
@@ -236,22 +231,26 @@ const paymentLink = createResource({
url: 'lms.lms.payments.get_payment_link',
makeParams(values) {
return {
doctype: props.type == 'course' ? 'LMS Course' : 'LMS Batch',
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
docname: props.name,
title: orderSummary.data.title,
amount: orderSummary.data.original_amount,
total_amount: orderSummary.data.amount,
currency: orderSummary.data.currency,
address: billingDetails,
redirect_to: redirectTo.value,
payment_for_certificate: props.type == 'certificate',
}
},
})
const generatePaymentLink = () => {
console.log('called')
paymentLink.submit(
{},
{
validate() {
console.log('validation start')
if (!billingDetails.source) {
return __('Please let us know where you heard about us from.')
}
@@ -330,6 +329,8 @@ const validateAddress = () => {
!states.includes(billingDetails.state)
)
return 'Please enter a valid state with correct spelling and the first letter capitalized.'
console.log('validation address')
}
const showError = (err) => {
@@ -347,4 +348,14 @@ const changeCurrency = (country) => {
billingDetails.country = country
orderSummary.reload()
}
const redirectTo = computed(() => {
if (props.type == 'course') {
return `/lms/courses/${props.name}`
} else if (props.type == 'batch') {
return `/lms/batches/${props.name}`
} else if (props.type == 'certificate') {
return `/lms/courses/${props.name}/certification`
}
})
</script>

View File

@@ -0,0 +1,117 @@
<template>
<header
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" />
</header>
<div class="p-5">
<div v-if="certificate.data && Object.keys(certificate.data).length">
<div class="text-lg text-ink-gray-9 font-semibold mb-1">
{{ __('Certification') }}
</div>
<div class="text-ink-gray-9 text-sm">
{{
__(
'You are already certified for this course. Click on the card below to open your certificate.'
)
}}
</div>
<div
class="border p-3 w-fit min-w-60 rounded-md space-y-2 hover:bg-surface-gray-1 cursor-pointer mt-5"
@click="openCertificate(certificate.data)"
>
<div class="text-ink-gray-9 font-semibold">
{{ courseTitle }}
</div>
<div class="text-sm text-ink-gray-7 font-medium">
{{ __('Issued On') }}:
{{ dayjs(certificate.data.issue_date).format('DD MMM YYYY') }}
</div>
</div>
</div>
<div v-else>
<UpcomingEvaluations v-if="courses.length" :courses="courses" />
</div>
</div>
</template>
<script setup>
import { computed, inject, onMounted, ref } from 'vue'
import { Breadcrumbs, call, createResource } from 'frappe-ui'
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
const courseTitle = ref(null)
const evaluator = ref(null)
const courses = ref([])
const user = inject('$user')
const dayjs = inject('$dayjs')
const props = defineProps({
courseName: {
type: String,
required: true,
},
})
onMounted(() => {
fetchCourseDetails()
})
const certificate = createResource({
url: 'frappe.client.get_value',
params: {
doctype: 'LMS Certificate',
filters: {
member: user.data?.name,
course: props.courseName,
},
fieldname: ['name', 'template', 'issue_date'],
},
auto: true,
cache: [user.data?.name, props.courseName],
})
const fetchCourseDetails = () => {
call('frappe.client.get_value', {
doctype: 'LMS Course',
filters: { name: props.courseName },
fieldname: ['title', 'evaluator'],
}).then((data) => {
courseTitle.value = data.title
evaluator.value = data.evaluator
populateCourses()
})
}
const populateCourses = () => {
courses.value = [
{
course: props.courseName,
title: courseTitle.value,
evaluator: evaluator.value,
},
]
}
const openCertificate = (certificate) => {
window.open(
`/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name=${
certificate.name
}&format=${encodeURIComponent(certificate.template)}`,
'_blank'
)
}
const breadcrumbs = computed(() => [
{
label: __('Courses'),
route: { name: 'Courses' },
},
{
label: courseTitle.value,
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
},
{
label: __('Certification'),
},
])
</script>

View File

@@ -160,7 +160,7 @@
<div class="text-lg font-semibold mt-5 mb-4">
{{ __('Settings') }}
</div>
<div class="grid grid-cols-3 gap-10 mb-4">
<div class="grid grid-cols-2 gap-10 mb-4">
<div
v-if="user.data?.is_moderator"
class="flex flex-col space-y-4"
@@ -188,43 +188,48 @@
v-model="course.featured"
:label="__('Featured')"
/>
</div>
<div class="flex flex-col space-y-3">
<FormControl
type="checkbox"
v-model="course.disable_self_learning"
:label="__('Disable Self Enrollment')"
/>
<FormControl
type="checkbox"
v-model="course.enable_certification"
:label="__('Completion Certificate')"
/>
</div>
</div>
</div>
<div class="container border-t">
<div class="text-lg font-semibold mt-5 mb-4">
{{ __('Pricing') }}
<div class="container border-t space-y-4">
<div class="text-lg font-semibold mt-5">
{{ __('Pricing and Certification') }}
</div>
<div class="mb-4">
<div class="grid grid-cols-3">
<FormControl
type="checkbox"
v-model="course.paid_course"
:label="__('Paid Course')"
/>
<FormControl
type="checkbox"
v-model="course.enable_certification"
:label="__('Completion Certificate')"
/>
<FormControl
type="checkbox"
v-model="course.paid_certificate"
:label="__('Paid Certificate')"
/>
</div>
<FormControl
v-model="course.course_price"
:label="__('Course Price')"
class="mb-4"
/>
<FormControl v-model="course.course_price" :label="__('Amount')" />
<Link
doctype="Currency"
v-model="course.currency"
:filters="{ enabled: 1 }"
:label="__('Currency')"
/>
<Link
v-if="course.paid_certificate"
doctype="Course Evaluator"
v-model="course.evaluator"
:label="__('Evaluator')"
/>
</div>
</div>
</div>
@@ -296,8 +301,10 @@ const course = reactive({
disable_self_learning: false,
enable_certification: false,
paid_course: false,
paid_certificate: false,
course_price: '',
currency: '',
evaluator: '',
})
onMounted(() => {
@@ -391,6 +398,7 @@ const courseResource = createResource({
'paid_course',
'featured',
'enable_certification',
'paid_certifiate',
]
for (let idx in checkboxes) {
let key = checkboxes[idx]

View File

@@ -4,6 +4,7 @@
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" />
<CertificationLinks :courseName="courseName" />
</header>
<div class="grid md:grid-cols-[70%,30%] h-screen">
<div
@@ -197,13 +198,14 @@ import { computed, watch, inject, ref, onMounted, onBeforeUnmount } from 'vue'
import CourseOutline from '@/components/CourseOutline.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import { useRouter, useRoute } from 'vue-router'
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
import { ChevronLeft, ChevronRight, GraduationCap } from 'lucide-vue-next'
import Discussions from '@/components/Discussions.vue'
import { getEditorTools, updateDocumentTitle } from '../utils'
import EditorJS from '@editorjs/editorjs'
import LessonContent from '@/components/LessonContent.vue'
import CourseInstructors from '@/components/CourseInstructors.vue'
import ProgressBar from '@/components/ProgressBar.vue'
import CertificationLinks from '@/components/CertificationLinks.vue'
const user = inject('$user')
const router = useRouter()

View File

@@ -139,7 +139,7 @@ const renderEditor = (holder) => {
const lesson = reactive({
title: '',
include_in_preview: false,
body: 'Test',
body: '',
instructor_notes: '',
content: '',
})
@@ -294,7 +294,7 @@ const convertToJSON = (lessonData) => {
type: 'upload',
data: {
file_url: video,
file_type: 'video',
file_type: video.split('.').pop(),
},
})
} else if (block.includes('{{ Audio')) {
@@ -303,7 +303,7 @@ const convertToJSON = (lessonData) => {
type: 'upload',
data: {
file_url: audio,
file_type: 'audio',
file_type: audio.split('.').pop(),
},
})
} else if (block.includes('{{ PDF')) {

View File

@@ -28,6 +28,12 @@ const routes = [
component: () => import('@/pages/Lesson.vue'),
props: true,
},
{
path: '/courses/:courseName/certification',
name: 'CourseCertification',
component: () => import('@/pages/CourseCertification.vue'),
props: true,
},
{
path: '/courses/:courseName/learn/:chapterName',
name: 'SCORMChapter',

View File

@@ -50,10 +50,18 @@ export class Markdown {
this.wrapper.innerHTML = this.text
this.wrapper.addEventListener('keydown', (event) => {
const value = event.target.textContent
let value = event.target.textContent
if (event.keyCode === 32 && value.startsWith('#')) {
this.convertToHeader(event, value)
} else if (event.keyCode === 13) {
} else if (event.keyCode == 189) {
this.convertBlock('list', {
style: 'unordered',
})
} else if (/^[a-zA-Z]/.test(event.key)) {
this.convertBlock('paragraph', {
text: value,
})
} else if (event.keyCode === 13 || event.keyCode === 190) {
this.parseContent(event)
}
})
@@ -75,7 +83,11 @@ export class Markdown {
parseContent(event) {
event.preventDefault()
const previousLine = this.wrapper.textContent
let previousLine = this.wrapper.textContent
if (event.keyCode === 190) {
previousLine = previousLine + '.'
}
if (previousLine && this.hasImage(previousLine)) {
this.wrapper.textContent = ''
this.convertBlock('image')
@@ -94,12 +106,12 @@ export class Markdown {
},
],
})
} else if (previousLine && previousLine.startsWith('1. ')) {
} else if (previousLine && previousLine.startsWith('1.')) {
this.convertBlock('list', {
style: 'ordered',
items: [
{
content: previousLine.replace('1. ', ''),
content: previousLine.replace('1.', ''),
},
],
})
@@ -108,6 +120,10 @@ export class Markdown {
this.convertBlock('embed', {
source: previousLine,
})
} else {
this.convertBlock('paragraph', {
text: previousLine,
})
}
}