feat: featured courses
This commit is contained in:
@@ -10,6 +10,15 @@
|
|||||||
:style="{ backgroundImage: 'url(\'' + encodeURI(course.image) + '\')' }"
|
:style="{ backgroundImage: 'url(\'' + encodeURI(course.image) + '\')' }"
|
||||||
>
|
>
|
||||||
<div class="flex relative top-4 left-4 w-fit flex-wrap">
|
<div class="flex relative top-4 left-4 w-fit flex-wrap">
|
||||||
|
<Badge
|
||||||
|
v-if="course.featured"
|
||||||
|
variant="subtle"
|
||||||
|
theme="green"
|
||||||
|
size="md"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
{{ __('Featured') }}
|
||||||
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
theme="gray"
|
theme="gray"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="text-base">
|
<div class="text-base">
|
||||||
<div
|
<div
|
||||||
v-if="title && (outline.data?.length || allowEdit)"
|
v-if="title && (outline.data?.length || allowEdit)"
|
||||||
class="grid grid-cols-[70%,30%] mb-4"
|
class="grid grid-cols-[70%,30%] mb-4 px-2"
|
||||||
>
|
>
|
||||||
<div class="font-semibold text-lg">
|
<div class="font-semibold text-lg">
|
||||||
{{ __(title) }}
|
{{ __(title) }}
|
||||||
|
|||||||
@@ -38,9 +38,6 @@
|
|||||||
{{ participant.full_name }}
|
{{ participant.full_name }}
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="font-medium text-gray-700 text-xs mb-1">
|
|
||||||
{{ __('is certified for') }}
|
|
||||||
</div>
|
|
||||||
<div class="leading-5" v-for="course in participant.courses">
|
<div class="leading-5" v-for="course in participant.courses">
|
||||||
{{ course }}
|
{{ course }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,14 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="">
|
<div class="">
|
||||||
<div
|
|
||||||
v-if="courses.data.length == 0 && courses.list.loading"
|
|
||||||
class="p-5 text-base text-gray-700"
|
|
||||||
>
|
|
||||||
{{ __('Loading Courses...') }}
|
|
||||||
</div>
|
|
||||||
<Tabs
|
<Tabs
|
||||||
v-else
|
|
||||||
v-model="tabIndex"
|
v-model="tabIndex"
|
||||||
tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap"
|
tablistClass="overflow-x-visible flex-wrap !gap-3 md:flex-nowrap"
|
||||||
:tabs="tabs"
|
:tabs="tabs"
|
||||||
|
|||||||
@@ -126,32 +126,41 @@
|
|||||||
<div class="text-lg font-semibold mt-5 mb-4">
|
<div class="text-lg font-semibold mt-5 mb-4">
|
||||||
{{ __('Settings') }}
|
{{ __('Settings') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="grid grid-cols-2 gap-10 mb-4">
|
||||||
v-if="user.data?.is_moderator"
|
<div
|
||||||
class="flex items-center justify-between mb-4"
|
v-if="user.data?.is_moderator"
|
||||||
>
|
class="flex flex-col space-y-3"
|
||||||
<FormControl
|
>
|
||||||
type="checkbox"
|
<FormControl
|
||||||
v-model="course.published"
|
type="checkbox"
|
||||||
:label="__('Published')"
|
v-model="course.published"
|
||||||
/>
|
:label="__('Published')"
|
||||||
<FormControl
|
/>
|
||||||
type="checkbox"
|
<FormControl
|
||||||
v-model="course.upcoming"
|
v-model="course.published_on"
|
||||||
:label="__('Upcoming')"
|
:label="__('Published On')"
|
||||||
/>
|
type="date"
|
||||||
<FormControl
|
class="mb-5"
|
||||||
type="checkbox"
|
/>
|
||||||
v-model="course.disable_self_learning"
|
</div>
|
||||||
:label="__('Disable Self Enrollment')"
|
<div class="flex flex-col space-y-3">
|
||||||
/>
|
<FormControl
|
||||||
|
type="checkbox"
|
||||||
|
v-model="course.upcoming"
|
||||||
|
:label="__('Upcoming')"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="checkbox"
|
||||||
|
v-model="course.featured"
|
||||||
|
:label="__('Featured')"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
type="checkbox"
|
||||||
|
v-model="course.disable_self_learning"
|
||||||
|
:label="__('Disable Self Enrollment')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
|
||||||
v-model="course.published_on"
|
|
||||||
:label="__('Published On')"
|
|
||||||
type="date"
|
|
||||||
class="mb-5"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="container border-t">
|
<div class="container border-t">
|
||||||
<div class="text-lg font-semibold mt-5 mb-4">
|
<div class="text-lg font-semibold mt-5 mb-4">
|
||||||
@@ -196,11 +205,18 @@ import {
|
|||||||
TextEditor,
|
TextEditor,
|
||||||
Button,
|
Button,
|
||||||
createResource,
|
createResource,
|
||||||
createDocumentResource,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FileUploader,
|
FileUploader,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { inject, onMounted, computed, ref, reactive, watch } from 'vue'
|
import {
|
||||||
|
inject,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
computed,
|
||||||
|
ref,
|
||||||
|
reactive,
|
||||||
|
watch,
|
||||||
|
} from 'vue'
|
||||||
import { convertToTitleCase, showToast, getFileSize } from '../utils'
|
import { convertToTitleCase, showToast, getFileSize } 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, X } from 'lucide-vue-next'
|
||||||
@@ -227,6 +243,7 @@ const course = reactive({
|
|||||||
tags: '',
|
tags: '',
|
||||||
published: false,
|
published: false,
|
||||||
published_on: '',
|
published_on: '',
|
||||||
|
featured: false,
|
||||||
upcoming: false,
|
upcoming: false,
|
||||||
disable_self_learning: false,
|
disable_self_learning: false,
|
||||||
paid_course: false,
|
paid_course: false,
|
||||||
@@ -246,6 +263,22 @@ onMounted(() => {
|
|||||||
if (props.courseName !== 'new') {
|
if (props.courseName !== 'new') {
|
||||||
courseResource.reload()
|
courseResource.reload()
|
||||||
}
|
}
|
||||||
|
window.addEventListener('keydown', keyboardShortcut)
|
||||||
|
})
|
||||||
|
|
||||||
|
const keyboardShortcut = (e) => {
|
||||||
|
if (
|
||||||
|
e.key === 's' &&
|
||||||
|
(e.ctrlKey || e.metaKey) &&
|
||||||
|
!e.target.classList.contains('ProseMirror')
|
||||||
|
) {
|
||||||
|
submitCourse()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('keydown', keyboardShortcut)
|
||||||
})
|
})
|
||||||
|
|
||||||
const courseCreationResource = createResource({
|
const courseCreationResource = createResource({
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xl font-semibold mb-1">
|
<div class="text-xl font-semibold mb-1">
|
||||||
{{ chartDetails.data.courses }}
|
{{ formatNumber(chartDetails.data.courses) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-gray-700">
|
<div class="text-gray-700">
|
||||||
{{ __('Published Courses') }}
|
{{ __('Published Courses') }}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xl font-semibold mb-1">
|
<div class="text-xl font-semibold mb-1">
|
||||||
{{ chartDetails.data.users }}
|
{{ formatNumber(chartDetails.data.users) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-gray-700">
|
<div class="text-gray-700">
|
||||||
{{ __('Total Signups') }}
|
{{ __('Total Signups') }}
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xl font-semibold mb-1">
|
<div class="text-xl font-semibold mb-1">
|
||||||
{{ chartDetails.data.enrollments }}
|
{{ formatNumber(chartDetails.data.enrollments) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-gray-700">
|
<div class="text-gray-700">
|
||||||
{{ __('Enrolled Users') }}
|
{{ __('Enrolled Users') }}
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xl font-semibold mb-1">
|
<div class="text-xl font-semibold mb-1">
|
||||||
{{ chartDetails.data.completions }}
|
{{ formatNumber(chartDetails.data.completions) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-gray-700">
|
<div class="text-gray-700">
|
||||||
{{ __('Courses Completed') }}
|
{{ __('Courses Completed') }}
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xl font-semibold mb-1">
|
<div class="text-xl font-semibold mb-1">
|
||||||
{{ chartDetails.data.lesson_completions }}
|
{{ formatNumber(chartDetails.data.lesson_completions) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-gray-700">
|
<div class="text-gray-700">
|
||||||
{{ __('Lessons Completed') }}
|
{{ __('Lessons Completed') }}
|
||||||
@@ -109,6 +109,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { createResource, Breadcrumbs } from 'frappe-ui'
|
import { createResource, Breadcrumbs } from 'frappe-ui'
|
||||||
import { computed, inject } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
|
import { formatNumber } from '@/utils'
|
||||||
import { Line, Pie } from 'vue-chartjs'
|
import { Line, Pie } from 'vue-chartjs'
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ export function formatTime(timeString) {
|
|||||||
return formattedTime
|
return formattedTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatNumber(number) {
|
||||||
|
return number.toLocaleString('en-IN', {
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function formatNumberIntoCurrency(number, currency) {
|
export function formatNumberIntoCurrency(number, currency) {
|
||||||
if (number) {
|
if (number) {
|
||||||
return number.toLocaleString('en-IN', {
|
return number.toLocaleString('en-IN', {
|
||||||
|
|||||||
@@ -377,6 +377,7 @@ def get_assigned_badges(member):
|
|||||||
return assigned_badges
|
return assigned_badges
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
def get_certificates(member):
|
def get_certificates(member):
|
||||||
"""Get certificates for a member."""
|
"""Get certificates for a member."""
|
||||||
return frappe.get_all(
|
return frappe.get_all(
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"published_on",
|
"published_on",
|
||||||
"column_break_10",
|
"column_break_10",
|
||||||
"upcoming",
|
"upcoming",
|
||||||
"column_break_12",
|
"featured",
|
||||||
"disable_self_learning",
|
"disable_self_learning",
|
||||||
"section_break_18",
|
"section_break_18",
|
||||||
"short_introduction",
|
"short_introduction",
|
||||||
@@ -169,10 +169,6 @@
|
|||||||
"fieldname": "column_break_10",
|
"fieldname": "column_break_10",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_12",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"depends_on": "enable_certification",
|
"depends_on": "enable_certification",
|
||||||
"fieldname": "grant_certificate_after",
|
"fieldname": "grant_certificate_after",
|
||||||
@@ -247,6 +243,12 @@
|
|||||||
"fieldname": "published_on",
|
"fieldname": "published_on",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Published On"
|
"label": "Published On"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "featured",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Featured"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_published_field": "published",
|
"is_published_field": "published",
|
||||||
@@ -273,7 +275,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2024-05-08 15:11:07.833094",
|
"modified": "2024-05-24 18:03:38.330443",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Course",
|
"name": "LMS Course",
|
||||||
|
|||||||
@@ -1266,6 +1266,7 @@ def get_course_details(course):
|
|||||||
"short_introduction",
|
"short_introduction",
|
||||||
"published",
|
"published",
|
||||||
"upcoming",
|
"upcoming",
|
||||||
|
"featured",
|
||||||
"disable_self_learning",
|
"disable_self_learning",
|
||||||
"published_on",
|
"published_on",
|
||||||
"status",
|
"status",
|
||||||
@@ -1347,6 +1348,8 @@ def get_categorized_courses(courses):
|
|||||||
for category in categories:
|
for category in categories:
|
||||||
category.sort(key=lambda x: x.enrollment_count, reverse=True)
|
category.sort(key=lambda x: x.enrollment_count, reverse=True)
|
||||||
|
|
||||||
|
live.sort(key=lambda x: x.featured, reverse=True)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"live": live,
|
"live": live,
|
||||||
"new": new,
|
"new": new,
|
||||||
|
|||||||
@@ -15,10 +15,10 @@
|
|||||||
<meta name="twitter:title" content="{{ meta.title }}" />
|
<meta name="twitter:title" content="{{ meta.title }}" />
|
||||||
<meta name="twitter:image" content="{{ meta.image }}" />
|
<meta name="twitter:image" content="{{ meta.image }}" />
|
||||||
<meta name="twitter:description" content="{{ meta.description }}" />
|
<meta name="twitter:description" content="{{ meta.description }}" />
|
||||||
<script type="module" crossorigin src="/assets/lms/frontend/assets/index-C-DogOtg.js"></script>
|
<script type="module" crossorigin src="/assets/lms/frontend/assets/index-DH4LPAyv.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/lms/frontend/assets/frappe-ui-CGsuCsfq.js">
|
<link rel="modulepreload" crossorigin href="/assets/lms/frontend/assets/frappe-ui-Cdm1MNHD.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/frappe-ui-B1gEXx4C.css">
|
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/frappe-ui-B1gEXx4C.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/index-C1pDkvO9.css">
|
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/index-CfA8Gbx1.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
|||||||
@@ -112,3 +112,21 @@ def get_meta(app_path):
|
|||||||
"keywords": "Enrollment Count, Completion, Signups",
|
"keywords": "Enrollment Count, Completion, Signups",
|
||||||
"link": "/statistics",
|
"link": "/statistics",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if re.match(r"^user/.*$", app_path):
|
||||||
|
username = app_path.split("/")[1]
|
||||||
|
user = frappe.db.get_value(
|
||||||
|
"User",
|
||||||
|
{
|
||||||
|
"username": username,
|
||||||
|
},
|
||||||
|
["full_name", "user_image", "bio"],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"title": user.full_name,
|
||||||
|
"image": user.user_image,
|
||||||
|
"description": user.bio,
|
||||||
|
"keywords": f"{user.full_name}, {user.bio}",
|
||||||
|
"link": f"/user/{username}",
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user