feat: featured courses

This commit is contained in:
Jannat Patel
2024-05-27 15:30:15 +05:30
parent cb9125632a
commit b7dd488886
12 changed files with 115 additions and 52 deletions

View File

@@ -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"

View File

@@ -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) }}

View File

@@ -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>

View File

@@ -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"

View File

@@ -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({

View File

@@ -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,

View File

@@ -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', {

View File

@@ -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(

View File

@@ -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",

View File

@@ -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,

View File

@@ -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">

View File

@@ -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}",
}