feat: batch details

This commit is contained in:
Jannat Patel
2024-01-05 18:22:03 +05:30
parent 10cdd712d2
commit 3a33f047f5
15 changed files with 1215 additions and 662 deletions

View File

@@ -1,23 +1,37 @@
<template>
<div class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out bg-gray-50"
:class="isSidebarCollapsed ? 'w-12' : 'w-56'">
<div class="flex flex-col overflow-hidden">
<UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
<div class="flex flex-col overflow-y-auto">
<SidebarLink v-for="link in links" :icon="link.icon" :label="link.label" :to="link.to"
:isCollapsed="isSidebarCollapsed" class="mx-2 my-0.5" />
</div>
</div>
<SidebarLink :label="isSidebarCollapsed ? 'Expand' : 'Collapse'" :isCollapsed="isSidebarCollapsed"
@click="isSidebarCollapsed = !isSidebarCollapsed" class="m-2">
<template #icon>
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
<CollapseSidebar class="h-4.5 w-4.5 text-gray-700 duration-300 ease-in-out"
:class="{ '[transform:rotateY(180deg)]': isSidebarCollapsed }" />
</span>
</template>
</SidebarLink>
</div>
<div
class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out bg-gray-50"
:class="isSidebarCollapsed ? 'w-12' : 'w-56'"
>
<div class="flex flex-col overflow-hidden">
<UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
<div class="flex flex-col overflow-y-auto">
<SidebarLink
v-for="link in links"
:icon="link.icon"
:label="link.label"
:to="link.to"
:isCollapsed="isSidebarCollapsed"
class="mx-2 my-0.5"
/>
</div>
</div>
<SidebarLink
:label="isSidebarCollapsed ? 'Expand' : 'Collapse'"
:isCollapsed="isSidebarCollapsed"
@click="isSidebarCollapsed = !isSidebarCollapsed"
class="m-2"
>
<template #icon>
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
<CollapseSidebar
class="h-4.5 w-4.5 text-gray-700 duration-300 ease-in-out"
:class="{ '[transform:rotateY(180deg)]': isSidebarCollapsed }"
/>
</span>
</template>
</SidebarLink>
</div>
</template>
<script setup>
@@ -26,30 +40,33 @@ import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
import SidebarLink from '@/components/SidebarLink.vue'
import { useStorage } from '@vueuse/core'
import { BookOpen, Users, TrendingUp, Briefcase } from 'lucide-vue-next'
import { ref } from 'vue'
import { ref, watch } from 'vue'
const links = [
{
label: 'Courses',
icon: BookOpen,
to: 'Courses',
},
{
label: "Batches",
icon: Users,
to: 'Batches',
},
{
label: "Statistics",
icon: TrendingUp,
to: 'Statistics',
},
{
label: "Jobs",
icon: Briefcase,
to: 'Jobs',
},
{
label: 'Courses',
icon: BookOpen,
to: 'Courses',
},
{
label: 'Batches',
icon: Users,
to: 'Batches',
},
{
label: 'Statistics',
icon: TrendingUp,
to: 'Statistics',
},
{
label: 'Jobs',
icon: Briefcase,
to: 'Jobs',
},
]
const getSidebarFromStorage = () => {
return useStorage('sidebar_is_collapsed', false)
}
let isSidebarCollapsed = ref(useStorage("sidebar_is_collapsed", false))
let isSidebarCollapsed = ref(getSidebarFromStorage())
</script>

View File

@@ -1,71 +1,65 @@
<template>
<div class="flex flex-col border border-gray-200 rounded-md p-4 h-full" style="min-height: 150px;">
<Badge v-if="batch.seat_count && batch.seats_left > 0" theme="green" class="self-start mb-2">
{{ batch.seats_left }} {{ __("Seat Left") }}
</Badge>
<Badge v-else-if="batch.seat_count && batch.seats_left <= 0" theme="red" class="self-start mb-2">
{{ __("Sold Out") }}
</Badge>
<div class="text-xl font-semibold mb-1">
{{ batch.title }}
</div>
<div class="short-introduction">
{{ batch.description }}
</div>
<div class="mt-auto">
<div v-if="batch.amount" class="font-semibold text-lg mb-4">
{{ batch.price }}
</div>
<div class="flex items-center mb-3">
<BookOpen class="h-4 w-4 stroke-1.5 mr-2 text-gray-700"/>
<span>
{{ batch.courses }} {{ __("Courses") }}
</span>
</div>
<div class="flex items-center mb-3">
<Calendar class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ dayjs(batch.start_date).format("DD MMM YYYY") }} - {{ dayjs(batch.end_date).format("DD MMM YYYY") }}
</span>
</div>
<div class="flex items-center">
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ formatTime(batch.start_time) }} - {{ formatTime(batch.end_time) }}
</span>
</div>
</div>
</div>
<div
class="flex flex-col border border-gray-200 rounded-md p-4 h-full"
style="min-height: 150px"
>
<Badge
v-if="batch.seat_count && batch.seats_left > 0"
theme="green"
class="self-start mb-2"
>
{{ batch.seats_left }} {{ __('Seat Left') }}
</Badge>
<Badge
v-else-if="batch.seat_count && batch.seats_left <= 0"
theme="red"
class="self-start mb-2"
>
{{ __('Sold Out') }}
</Badge>
<div class="text-xl font-semibold mb-1">
{{ batch.title }}
</div>
<div class="short-introduction">
{{ batch.description }}
</div>
<div class="mt-auto">
<div v-if="batch.amount" class="font-semibold text-lg mb-4">
{{ batch.price }}
</div>
<div class="flex items-center mb-3">
<BookOpen class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span> {{ batch.courses }} {{ __('Courses') }} </span>
</div>
<div class="flex items-center mb-3">
<Calendar class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ dayjs(batch.start_date).format('DD MMM YYYY') }} -
{{ dayjs(batch.end_date).format('DD MMM YYYY') }}
</span>
</div>
<div class="flex items-center">
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ formatTime(batch.start_time) }} - {{ formatTime(batch.end_time) }}
</span>
</div>
</div>
</div>
</template>
<script setup>
import { Calendar, Clock, BookOpen } from "lucide-vue-next"
import { inject } from "vue"
import { Badge } from "frappe-ui"
import { Calendar, Clock, BookOpen } from 'lucide-vue-next'
import { inject } from 'vue'
import { Badge } from 'frappe-ui'
import { formatTime } from '../utils'
const dayjs = inject("$dayjs")
const dayjs = inject('$dayjs')
const props = defineProps({
batch: {
type: Object,
default: null,
},
});
function formatTime(timeString) {
if (!timeString) return "";
const [hour, minute] = timeString.split(":").map(Number);
// Create a Date object with dummy values for day, month, and year
const dummyDate = new Date(0, 0, 0, hour, minute);
// Use Intl.DateTimeFormat to format the time in 12-hour format
const formattedTime = new Intl.DateTimeFormat("en-US", {
hour: "numeric",
minute: "numeric",
hour12: true,
}).format(dummyDate);
return formattedTime;
}
batch: {
type: Object,
default: null,
},
})
</script>
<style>
.short-introduction {
@@ -76,6 +70,6 @@ function formatTime(timeString) {
width: 100%;
overflow: hidden;
margin: 0.25rem 0 1.25rem;
line-height: 1.5;
line-height: 1.5;
}
</style>
</style>

View File

@@ -0,0 +1,81 @@
<template>
<div class="shadow rounded-md p-5" style="width: 300px">
<Badge
v-if="batch.doc.seat_count && seats_left > 0"
theme="green"
class="self-start mb-2 float-right"
>
{{ seats_left }} {{ __('Seat Left') }}
</Badge>
<Badge
v-else-if="batch.doc.seat_count && seats_left <= 0"
theme="red"
class="self-start mb-2 float-right"
>
{{ __('Sold Out') }}
</Badge>
<div v-if="batch.doc.amount" class="text-lg font-semibold mb-3">
{{ formatNumberIntoCurrency(batch.doc.amount, batch.doc.currency) }}
</div>
<div class="flex items-center mb-3">
<BookOpen class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span> {{ batch.doc.courses.length }} {{ __('Courses') }} </span>
</div>
<div class="flex items-center mb-3">
<Calendar class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ dayjs(batch.doc.start_date).format('DD MMM YYYY') }} -
{{ dayjs(batch.doc.end_date).format('DD MMM YYYY') }}
</span>
</div>
<div class="flex items-center">
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ formatTime(batch.doc.start_time) }} -
{{ formatTime(batch.doc.end_time) }}
</span>
</div>
<Button v-if="user?.data?.is_moderator" class="w-full mt-4">
<span>
{{ __('Manage Batch') }}
</span>
</Button>
<Button
v-else-if="batch.doc.paid_batch"
class="w-full mt-4"
variant="solid"
>
<span>
{{ __('Register Now') }}
</span>
</Button>
<Button v-if="user?.data?.is_moderator" class="w-full mt-2">
<span>
{{ __('Edit') }}
</span>
</Button>
</div>
</template>
<script setup>
import { formatNumberIntoCurrency, formatTime } from '@/utils'
import { BookOpen, Calendar, Clock } from 'lucide-vue-next'
import { inject, computed } from 'vue'
import { Badge, Button } from 'frappe-ui'
const dayjs = inject('$dayjs')
const user = inject('$user')
const props = defineProps({
batch: {
type: Object,
default: null,
},
})
const seats_left = computed(() => {
if (props.batch.doc.seat_count) {
return props.batch.doc.seat_count - props.batch.doc.students.length
}
return null
})
</script>

View File

@@ -1,97 +1,156 @@
<template>
<div class="flex flex-col border border-gray-200 h-full rounded-md shadow-sm text-base overflow-auto" style="min-height: 320px;">
<div class="course-image" :class="{ 'default-image': !course.image }" :style="{ backgroundImage: 'url(' + encodeURI(course.image) + ')' }">
<div class="flex relative top-4 left-4 w-fit">
<div class="course-card-pills rounded-md border border-gray-200" v-for="tag in course.tags">
{{ tag }}
</div>
</div>
<div v-if="!course.image" class="image-placeholder">{{ course.title[0] }}</div>
</div>
<div class="flex flex-col flex-auto p-4">
<div class="flex items-center justify-between mb-2">
<div v-if="course.lesson_count" class="flex items-center space-x-1 py-1">
<BookOpen class="h-4 w-4 stroke-1.5 text-gray-700" />
<span> {{ course.lesson_count }} </span>
</div>
<div
v-if="course.title"
class="flex flex-col border border-gray-200 h-full rounded-md shadow-sm text-base overflow-auto"
style="min-height: 320px"
>
<div
class="course-image"
:class="{ 'default-image': !course.image }"
:style="{ backgroundImage: 'url(' + encodeURI(course.image) + ')' }"
>
<div class="flex relative top-4 left-4 w-fit">
<div
class="course-card-pills rounded-md border border-gray-200"
v-for="tag in course.tags"
>
{{ tag }}
</div>
</div>
<div v-if="!course.image" class="image-placeholder">
{{ course.title[0] }}
</div>
</div>
<div class="flex flex-col flex-auto p-4">
<div class="flex items-center justify-between mb-2">
<div
v-if="course.lesson_count"
class="flex items-center space-x-1 py-1"
>
<BookOpen class="h-4 w-4 stroke-1.5 text-gray-700" />
<span> {{ course.lesson_count }} </span>
</div>
<div v-if="course.enrollment_count" class="flex items-center space-x-1 py-1">
<Users class="h-4 w-4 stroke-1.5 text-gray-700" />
<span> {{ course.enrollment_count }} </span>
</div>
<div
v-if="course.enrollment_count"
class="flex items-center space-x-1 py-1"
>
<Users class="h-4 w-4 stroke-1.5 text-gray-700" />
<span> {{ course.enrollment_count }} </span>
</div>
<div v-if="course.avg_rating" class="flex items-center space-x-1 py-1">
<Star class="h-4 w-4 stroke-1.5 text-gray-700" />
<span> {{ course.avg_rating }} </span>
</div>
<div v-if="course.avg_rating" class="flex items-center space-x-1 py-1">
<Star class="h-4 w-4 stroke-1.5 text-gray-700" />
<span> {{ course.avg_rating }} </span>
</div>
<div v-if="course.status != 'Approved'">
<Badge variant="solid" :theme="course.status === 'Under Review' ? 'orange' : 'blue'" size="sm">
{{ course.status }}
</Badge>
</div>
</div>
<div class="text-xl font-semibold">
{{ course.title }}
</div>
<div v-if="course.status != 'Approved'">
<Badge
variant="solid"
:theme="course.status === 'Under Review' ? 'orange' : 'blue'"
size="sm"
>
{{ course.status }}
</Badge>
</div>
</div>
<div class="short-introduction">
{{ course.short_introduction }}
</div>
<div v-if="user && course.membership" class="w-full bg-gray-200 rounded-full h-1 mb-2">
<div class="bg-gray-900 h-1 rounded-full" :style="{ width: Math.ceil(course.membership.progress) + '%' }"></div>
</div>
<div v-if="user && course.membership" class="text-sm mb-4">
{{ Math.ceil(course.membership.progress) }}% completed
</div>
<div class="text-xl font-semibold">
{{ course.title }}
</div>
<div class="flex items-center justify-between mt-auto">
<div class="flex avatar-group overlap">
<div class="mr-1" :class="{ 'avatar-group overlap': course.instructors.length > 1 }">
<UserAvatar v-for="instructor in course.instructors" :user="instructor"/>
</div>
<span v-if="course.instructors.length == 1">
{{ course.instructors[0].full_name }}
</span>
<span v-if="course.instructors.length == 2">
{{ course.instructors[0].first_name }} and {{ course.instructors[1].first_name }}
</span>
<span v-if="course.instructors.length > 2">
{{ course.instructors[0].first_name }} and {{ course.instructors.length - 1 }} others
</span>
</div>
<div class="short-introduction">
{{ course.short_introduction }}
</div>
<div
v-if="user && course.membership"
class="w-full bg-gray-200 rounded-full h-1 mb-2"
>
<div
class="bg-gray-900 h-1 rounded-full"
:style="{ width: Math.ceil(course.membership.progress) + '%' }"
></div>
</div>
<div v-if="user && course.membership" class="text-sm mb-4">
{{ Math.ceil(course.membership.progress) }}% completed
</div>
<div class="font-semibold">
{{ course.price }}
</div>
</div>
</div>
</div>
<div class="flex items-center justify-between mt-auto">
<div class="flex avatar-group overlap">
<div
class="mr-1"
:class="{ 'avatar-group overlap': course.instructors.length > 1 }"
>
<UserAvatar
v-for="instructor in course.instructors"
:user="instructor"
/>
</div>
<span v-if="course.instructors.length == 1">
{{ course.instructors[0].full_name }}
</span>
<span v-if="course.instructors.length == 2">
{{ course.instructors[0].first_name }} and
{{ course.instructors[1].first_name }}
</span>
<span v-if="course.instructors.length > 2">
{{ course.instructors[0].first_name }} and
{{ course.instructors.length - 1 }} others
</span>
</div>
<div class="font-semibold">
{{ course.price }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { BookOpen, Users, Star } from 'lucide-vue-next'
import UserAvatar from '@/components/UserAvatar.vue'
import { sessionStore } from '@/stores/session'
import { Badge } from "frappe-ui"
import { Badge, createResource } from 'frappe-ui'
import { ref, watchEffect } from 'vue'
const { isLoggedIn, user } = sessionStore()
const { user } = sessionStore()
let course = ref({})
const props = defineProps({
course: {
type: Object,
default: null,
},
});
course: {
type: [Object, String],
default: null,
},
})
const courseDetails = createResource({
url: 'lms.lms.utils.get_course_details',
cache: ['course', props.courseName],
makeParams() {
return {
course: props.course,
}
},
transform(data) {
course.value = data
},
})
/* watchEffect(() => {
if (props.course && typeof props.course === "object") {
course.value = props.course;
} else {
courseDetails.reload();
}
}); */
</script>
<style>
.course-image {
height: 168px;
width: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
height: 168px;
width: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.course-card-pills {
@@ -108,31 +167,31 @@ const props = defineProps({
}
.default-image {
display: flex;
flex-direction: column;
align-items: center;
background-color: theme('colors.gray.200');
color: theme('colors.gray.700');
display: flex;
flex-direction: column;
align-items: center;
background-color: theme('colors.gray.200');
color: theme('colors.gray.700');
}
.avatar-group {
display: inline-flex;
align-items: center;
display: inline-flex;
align-items: center;
}
.avatar-group .avatar {
transition: margin 0.1s ease-in-out;
}
.image-placeholder {
display: flex;
align-items: center;
flex: 1;
font-size: 5rem;
color: theme('colors.gray.700');
font-weight: 600;
display: flex;
align-items: center;
flex: 1;
font-size: 5rem;
color: theme('colors.gray.700');
font-weight: 600;
}
.avatar-group.overlap .avatar + .avatar {
margin-left: calc(-8px);
margin-left: calc(-8px);
}
.short-introduction {
@@ -143,6 +202,6 @@ const props = defineProps({
width: 100%;
overflow: hidden;
margin: 0.25rem 0 1.25rem;
line-height: 1.5;
line-height: 1.5;
}
</style>
</style>

View File

@@ -1,101 +1,132 @@
<template>
<div class="shadow rounded-md" style="width: 300px;">
<iframe v-if="course.data.video_link" :src="video_link" class="rounded-t-md" />
<div class="p-5">
<router-link v-if="course.data.membership && course.data.current_lesson"
:to="{name: 'Lesson', params: {
courseName: course.name,
chapterNumber: course.data.current_lesson.split('.')[0],
lessonNumber: course.data.current_lesson.split('.')[1]
}}">
<Button variant="solid" class="w-full mb-3">
<span>
{{ __("Continue Learning") }}
</span>
</Button>
</router-link>
<Button v-else @click="enrollStudent()" variant="solid" class="w-full mb-3">
<span>
{{ __("Start Learning") }}
</span>
</Button>
<Button v-if="user?.data?.is_moderator" variant="subtle" class="w-full mb-3">
<span>
{{ __("Edit") }}
</span>
</Button>
<div class="flex items-center mb-3">
<Users class="h-4 w-4 text-gray-700"/>
<span class="ml-1">
{{ course.data.enrollment_count_formatted }} {{ __("Enrolled") }}
</span>
</div>
<div class="flex items-center mb-3">
<BookOpen class="h-4 w-4 text-gray-700"/>
<span class="ml-1">
{{ course.data.lesson_count }} {{ __("Lessons") }}
</span>
</div>
<div class="flex items-center">
<Star class="h-4 w-4 fill-orange-500 text-gray-100"/>
<span class="ml-1">
{{ course.data.avg_rating }} {{ __("Rating") }}
</span>
</div>
</div>
</div>
<div class="shadow rounded-md" style="width: 300px">
<iframe
v-if="course.data.video_link"
:src="video_link"
class="rounded-t-md"
/>
<div class="p-5">
<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" class="w-full mb-3">
<span>
{{ __('Continue Learning') }}
</span>
</Button>
</router-link>
<Button
v-else
@click="enrollStudent()"
variant="solid"
class="w-full mb-3"
>
<span>
{{ __('Start Learning') }}
</span>
</Button>
<Button
v-if="user?.data?.is_moderator"
variant="subtle"
class="w-full mb-3"
>
<span>
{{ __('Edit') }}
</span>
</Button>
<div class="flex items-center mb-3">
<Users class="h-4 w-4 text-gray-700" />
<span class="ml-1">
{{ course.data.enrollment_count_formatted }} {{ __('Enrolled') }}
</span>
</div>
<div class="flex items-center mb-3">
<BookOpen class="h-4 w-4 text-gray-700" />
<span class="ml-1">
{{ course.data.lesson_count }} {{ __('Lessons') }}
</span>
</div>
<div class="flex items-center">
<Star class="h-4 w-4 fill-orange-500 text-gray-100" />
<span class="ml-1">
{{ course.data.avg_rating }} {{ __('Rating') }}
</span>
</div>
</div>
</div>
</template>
<script setup>
import { BookOpen, Users, Star } from 'lucide-vue-next'
import { computed, inject } from 'vue'
import { Button, createResource } from "frappe-ui"
import { createToast } from "@/utils/"
import { Button, createResource } from 'frappe-ui'
import { createToast } from '@/utils/'
import { useRouter } from 'vue-router'
const router = useRouter()
const user = inject("$user");
const user = inject('$user')
const props = defineProps({
course: {
type: Object,
default: null,
},
});
course: {
type: Object,
default: null,
},
})
const video_link = computed(() => {
if (props.course.data.video_link) {
return "https://www.youtube.com/embed/" + props.course.data.video_link;
}
return null;
});
if (props.course.data.video_link) {
return 'https://www.youtube.com/embed/' + props.course.data.video_link
}
return null
})
function enrollStudent() {
if (!user.data) {
createToast({
title: "Please Login",
icon: 'alert-circle',
iconClasses: 'text-yellow-600 bg-yellow-100',
})
setTimeout(() => {
window.location.href = `/login?redirect-to=${window.location.pathname}`
}, 3000)
} else {
const enrollStudentResource = createResource({
url: "lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership"
})
console.log(props.course)
enrollStudentResource.submit({
course: props.course.data.name
}).then(() => {
createToast({
title: "Enrolled Successfully",
icon: 'check',
iconClasses: 'text-green-600 bg-green-100',
})
setTimeout(() => {
router.push({ name: 'Lesson', params: { courseName: props.course.data.name, chapterNumber: 1, lessonNumber: 1 } })
}, 3000)
})
}
if (!user.data) {
createToast({
title: 'Please Login',
icon: 'alert-circle',
iconClasses: 'text-yellow-600 bg-yellow-100',
})
setTimeout(() => {
window.location.href = `/login?redirect-to=${window.location.pathname}`
}, 3000)
} else {
const enrollStudentResource = createResource({
url: 'lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership',
})
console.log(props.course)
enrollStudentResource
.submit({
course: props.course.data.name,
})
.then(() => {
createToast({
title: 'Enrolled Successfully',
icon: 'check',
iconClasses: 'text-green-600 bg-green-100',
})
setTimeout(() => {
router.push({
name: 'Lesson',
params: {
courseName: props.course.data.name,
chapterNumber: 1,
lessonNumber: 1,
},
})
}, 3000)
})
}
}
</script>
</script>

View File

@@ -1,67 +1,96 @@
<template>
<div class="course-outline text-base">
<div class="mt-4">
<Disclosure v-slot="{ open }" v-for="(chapter, index) in outline.data" :key="chapter.name" :defaultOpen="chapter.idx == route.params.chapterNumber">
<DisclosureButton class="flex w-full px-2 pt-2 pb-3">
<ChevronRight
:class="{'rotate-90 transform duration-200' : open, 'duration-200' : !open, 'open': index == 1}"
class="h-5 w-5 text-gray-900 stroke-1 mr-2"
/>
<div class="text-base font-medium">
{{ chapter.title }}
</div>
</DisclosureButton>
<DisclosurePanel class="pb-2">
<div v-for="lesson in chapter.lessons" :key="lesson.name">
<div class="outline-lesson mb-2 pl-9">
<router-link :to='{
name: "Lesson",
params: {
courseName: courseName,
chapterNumber: lesson.number.split(".")[0],
lessonNumber: lesson.number.split(".")[1],
}
}'>
<div class="flex items-center text-sm">
<MonitorPlay v-if="lesson.icon === 'icon-youtube'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
<HelpCircle v-else-if="lesson.icon === 'icon-quiz'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
<FileText v-else-if="lesson.icon === 'icon-list'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
{{ lesson.title }}
</div>
</router-link>
</div>
</div>
</DisclosurePanel>
</Disclosure>
</div>
</div>
<div class="course-outline text-base">
<div class="mt-4">
<Disclosure
v-slot="{ open }"
v-for="(chapter, index) in outline.data"
:key="chapter.name"
:defaultOpen="openChapter(chapter.idx)"
>
<DisclosureButton class="flex w-full px-2 pt-2 pb-3">
<ChevronRight
:class="{
'rotate-90 transform duration-200': open,
'duration-200': !open,
open: index == 1,
}"
class="h-5 w-5 text-gray-900 stroke-1 mr-2"
/>
<div class="text-base">
{{ chapter.title }}
</div>
</DisclosureButton>
<DisclosurePanel class="pb-2">
<div v-for="lesson in chapter.lessons" :key="lesson.name">
<div class="outline-lesson mb-2 pl-9">
<router-link
:to="{
name: 'Lesson',
params: {
courseName: courseName,
chapterNumber: lesson.number.split('.')[0],
lessonNumber: lesson.number.split('.')[1],
},
}"
>
<div class="flex items-center text-sm">
<MonitorPlay
v-if="lesson.icon === 'icon-youtube'"
class="h-4 w-4 text-gray-900 stroke-1 mr-2"
/>
<HelpCircle
v-else-if="lesson.icon === 'icon-quiz'"
class="h-4 w-4 text-gray-900 stroke-1 mr-2"
/>
<FileText
v-else-if="lesson.icon === 'icon-list'"
class="h-4 w-4 text-gray-900 stroke-1 mr-2"
/>
{{ lesson.title }}
</div>
</router-link>
</div>
</div>
</DisclosurePanel>
</Disclosure>
</div>
</div>
</template>
<script setup>
import { createResource } from "frappe-ui";
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
import { ChevronRight, MonitorPlay, HelpCircle, FileText } from 'lucide-vue-next';
import { useRoute } from "vue-router";
import { createResource } from 'frappe-ui'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
import {
ChevronRight,
MonitorPlay,
HelpCircle,
FileText,
} from 'lucide-vue-next'
import { useRoute } from 'vue-router'
const route = useRoute();
const route = useRoute()
const props = defineProps({
courseName: {
type: String,
required: true,
},
});
courseName: {
type: String,
required: true,
},
})
const outline = createResource({
url: "lms.lms.utils.get_course_outline",
cache: ["course_outline", props.courseName],
params: {
course: props.courseName
},
auto: true,
});
url: 'lms.lms.utils.get_course_outline',
cache: ['course_outline', props.courseName],
params: {
course: props.courseName,
},
auto: true,
})
const openChapter = (index) => {
return index == route.params.chapterNumber || index == 1
}
</script>
<style>
.outline-lesson:has(.router-link-active) {
background-color: theme('colors.gray.100');
padding: 0.5rem 0 0.5rem 2rem;
background-color: theme('colors.gray.100');
padding: 0.5rem 0 0.5rem 2rem;
}
</style>
</style>