feat: batch details
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
81
frontend/src/components/BatchOverlay.vue
Normal file
81
frontend/src/components/BatchOverlay.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user