Files
lms/frontend/src/pages/Courses.vue
2025-07-25 17:29:48 +05:30

343 lines
7.6 KiB
Vue

<template>
<header
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs :items="breadcrumbs" />
<router-link
v-if="canCreateCourse()"
:to="{
name: 'CourseForm',
params: { courseName: 'new' },
}"
>
<Button variant="solid">
<template #prefix>
<Plus class="h-4 w-4 stroke-1.5" />
</template>
{{ __('Create') }}
</Button>
</router-link>
</header>
<div class="p-5 pb-10">
<div
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5"
>
<div class="text-lg text-ink-gray-9 font-semibold">
{{ __('All Courses') }}
</div>
<div
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4"
>
<TabButtons :buttons="courseTabs" v-model="currentTab" />
<FormControl
v-model="certification"
:label="__('Certification')"
type="checkbox"
@change="updateCourses()"
/>
<div class="grid grid-cols-2 gap-2">
<FormControl
v-model="title"
:placeholder="__('Search by Title')"
type="text"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
@input="updateCourses()"
/>
<div class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40">
<Select
v-if="categories.length"
v-model="currentCategory"
:options="categories"
:placeholder="__('Category')"
@change="updateCourses()"
/>
</div>
</div>
</div>
</div>
<div
v-if="courses.data?.length"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-8"
>
<router-link
v-for="course in courses.data"
:to="{ name: 'CourseDetail', params: { courseName: course.name } }"
>
<CourseCard :course="course" />
</router-link>
</div>
<EmptyState v-else-if="!courses.list.loading" type="Courses" />
<div
v-if="!courses.list.loading && courses.hasNextPage"
class="flex justify-center mt-5"
>
<Button @click="courses.next()">
{{ __('Load More') }}
</Button>
</div>
</div>
</template>
<script setup>
import {
Breadcrumbs,
Button,
call,
createListResource,
FormControl,
Select,
TabButtons,
usePageMeta,
} from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue'
import { Plus } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { canCreateCourse } from '@/utils'
import CourseCard from '@/components/CourseCard.vue'
import EmptyState from '@/components/EmptyState.vue'
import router from '../router'
const user = inject('$user')
const dayjs = inject('$dayjs')
const start = ref(0)
const pageLength = ref(30)
const categories = ref([])
const currentCategory = ref(null)
const title = ref('')
const certification = ref(false)
const filters = ref({})
const currentTab = ref('Live')
const { brand } = sessionStore()
const courseCount = ref(0)
onMounted(() => {
setFiltersFromQuery()
updateCourses()
getCourseCount()
categories.value = [
{
label: '',
value: null,
},
]
})
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 courses = createListResource({
doctype: 'LMS Course',
url: 'lms.lms.utils.get_courses',
cache: ['courses', user.data?.name],
pageLength: pageLength.value,
start: start.value,
onSuccess(data) {
setCategories(data)
},
})
const setCategories = (data) => {
let allCategories = data.map((course) => course.category)
allCategories = allCategories.filter(
(category, index) => allCategories.indexOf(category) === index && category
)
if (categories.value.length <= allCategories.length) {
updateCategories(data)
}
}
const isPersonaCaptured = async () => {
let persona = await call('frappe.client.get_single_value', {
doctype: 'LMS Settings',
field: 'persona_captured',
})
return persona
}
const identifyUserPersona = async () => {
if (user.data?.is_system_manager && !user.data?.developer_mode) {
let personaCaptured = await isPersonaCaptured()
if (personaCaptured) return
if (!courseCount.value) {
router.push({
name: 'PersonaForm',
})
}
}
}
const getCourseCount = () => {
if (!user.data) return
call('frappe.client.get_count', {
doctype: 'LMS Course',
}).then((data) => {
courseCount.value = data
identifyUserPersona()
})
}
const updateCourses = () => {
updateFilters()
courses.update({
filters: filters.value,
})
courses.reload()
}
const updateFilters = () => {
updateCategoryFilter()
updateTitleFilter()
updateCertificationFilter()
updateTabFilter()
updateStudentFilter()
setQueryParams()
}
const updateCategoryFilter = () => {
if (currentCategory.value) {
filters.value['category'] = currentCategory.value
} else {
delete filters.value['category']
}
}
const updateTitleFilter = () => {
if (title.value) {
filters.value['title'] = ['like', `%${title.value}%`]
} else {
delete filters.value['title']
}
}
const updateCertificationFilter = () => {
if (certification.value) {
filters.value['certification'] = 1
} else {
delete filters.value['certification']
}
}
const updateTabFilter = () => {
delete filters.value['live']
delete filters.value['created']
delete filters.value['published_on']
delete filters.value['upcoming']
if (currentTab.value == 'Enrolled' && user.data?.is_student) {
filters.value['enrolled'] = 1
delete filters.value['published']
} else {
delete filters.value['published']
delete filters.value['enrolled']
if (currentTab.value == 'Live') {
filters.value['published'] = 1
filters.value['upcoming'] = 0
filters.value['live'] = 1
} else if (currentTab.value == 'Upcoming') {
filters.value['upcoming'] = 1
} else if (currentTab.value == 'New') {
filters.value['published'] = 1
filters.value['published_on'] = [
'>=',
dayjs().add(-3, 'month').format('YYYY-MM-DD'),
]
} else if (currentTab.value == 'Created') {
filters.value['created'] = 1
} else if (currentTab.value == 'Unpublished') {
filters.value['published'] = 0
}
}
}
const updateStudentFilter = () => {
if (!user.data || (user.data?.is_student && currentTab.value != 'Enrolled')) {
filters.value['published'] = 1
}
}
const setQueryParams = () => {
let queries = new URLSearchParams(location.search)
let filterKeys = {
title: title.value,
category: currentCategory.value,
certification: certification.value,
}
Object.keys(filterKeys).forEach((key) => {
if (filterKeys[key]) {
queries.set(key, filterKeys[key])
} else {
queries.delete(key)
}
})
let queryString = ''
if (queries.toString()) {
queryString = `?${queries.toString()}`
}
history.replaceState({}, '', `${location.pathname}${queryString}`)
}
const updateCategories = (data) => {
data.forEach((course) => {
if (
course.category &&
!categories.value.find((category) => category.value === course.category)
)
categories.value.push({
label: course.category,
value: course.category,
})
})
}
watch(currentTab, () => {
updateCourses()
})
const courseTabs = computed(() => {
let tabs = [
{
label: __('Live'),
},
{
label: __('New'),
},
{
label: __('Upcoming'),
},
]
if (
user.data?.is_moderator ||
user.data?.is_instructor ||
user.data?.is_evaluator
) {
tabs.push({ label: __('Created') })
tabs.push({ label: __('Unpublished') })
} else if (user.data) {
tabs.push({ label: __('Enrolled') })
}
return tabs
})
const breadcrumbs = computed(() => [
{
label: __('Courses'),
route: { name: 'Courses' },
},
])
usePageMeta(() => {
return {
title: __('Courses'),
icon: brand.favicon,
}
})
</script>