feat: home page

This commit is contained in:
Jannat Patel
2025-08-21 21:16:21 +05:30
parent 0725714144
commit e0601c7b38
10 changed files with 626 additions and 25 deletions

View File

@@ -365,7 +365,7 @@ const addPrograms = async () => {
let canAddProgram = await checkIfCanAddProgram()
if (!canAddProgram) return
let activeFor = ['Programs', 'ProgramDetail']
let index = 1
let index = 2
sidebarLinks.value.splice(index, 0, {
label: 'Programs',

View File

@@ -52,7 +52,7 @@
<Clock class="w-4 h-4 stroke-1.5" />
<span>
{{ formatTime(cls.time) }} -
{{ dayjs(getClassEnd(cls)).format('HH:mm') }}
{{ dayjs(getClassEnd(cls)).format('HH:mm A') }}
</span>
</div>
<div

View File

@@ -11,7 +11,7 @@
class="flex items-center w-full duration-300 ease-in-out group"
:class="isCollapsed ? 'p-1 relative' : 'px-2 py-1'"
>
<Tooltip :text="link.label" placement="right">
<Tooltip :text="__(link.label)" placement="right">
<slot name="icon">
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
<component

View File

@@ -1,22 +1,24 @@
<template>
<div>
<div v-if="!forHome || (forHome && upcoming_evals.data?.length)">
<div class="flex items-center justify-between mb-4">
<div class="text-lg text-ink-gray-9 font-semibold">
{{ __('Upcoming Evaluations') }}
</div>
<Button
v-if="upcoming_evals.data?.length != evaluationCourses.length"
v-if="
upcoming_evals.data?.length != evaluationCourses.length && !forHome
"
@click="openEvalModal"
>
{{ __('Schedule Evaluation') }}
</Button>
</div>
<div v-if="upcoming_evals.data?.length">
<div class="grid grid-cols-3 gap-4">
<div class="grid gap-4" :class="forHome ? 'grid-cols-2' : 'grid-cols-3'">
<div v-for="evl in upcoming_evals.data">
<div class="border text-ink-gray-7 rounded-md p-3">
<div class="flex justify-between mb-3">
<span class="font-semibold text-ink-gray-9 leading-5">
<span class="text-lg font-semibold text-ink-gray-9 leading-5">
{{ evl.course_title }}
</span>
<Menu
@@ -94,8 +96,8 @@
</div>
</div>
</div>
<div v-else class="text-sm italic text-ink-gray-5">
{{ __('Please schedule an evaluation to get certified.') }}
<div v-else class="text-ink-gray-5">
{{ __('Schedule an evaluation to get certified.') }}
</div>
</div>
<EvaluationModal
@@ -122,7 +124,6 @@ import EvaluationModal from '@/components/Modals/EvaluationModal.vue'
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
const dayjs = inject('$dayjs')
const user = inject('$user')
const showEvalModal = ref(false)
const app = getCurrentInstance()
const { $dialog } = app.appContext.config.globalProperties
@@ -140,12 +141,15 @@ const props = defineProps({
type: String,
default: null,
},
forHome: {
type: Boolean,
default: false,
},
})
const upcoming_evals = createResource({
url: 'lms.lms.utils.get_upcoming_evals',
params: {
student: user.data.name,
courses: props.courses.map((course) => course.course),
batch: props.batch,
},

View File

@@ -0,0 +1,75 @@
<template>
<div>
<div v-if="createdCourses.data?.length" class="mt-10">
<div class="flex items-center justify-between mb-3">
<span class="font-semibold text-lg">
{{ __('Courses by me') }}
</span>
<router-link
:to="{
name: 'Courses',
}"
>
<span class="flex items-center space-x-1 text-ink-gray-5 text-xs">
<span>
{{ __('See all') }}
</span>
<MoveRight class="size-3 stroke-1.5" />
</span>
</router-link>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
<router-link
v-for="course in createdCourses.data"
:to="{ name: 'CourseDetail', params: { courseName: course.name } }"
>
<CourseCard :course="course" />
</router-link>
</div>
</div>
<div v-if="createdBatches.data?.length" class="mt-10">
<div class="flex items-center justify-between mb-3">
<span class="font-semibold text-lg">
{{ __('Batches by me') }}
</span>
<router-link
:to="{
name: 'Batches',
}"
>
<span class="flex items-center space-x-1 text-ink-gray-5 text-xs">
<span>
{{ __('See all') }}
</span>
<MoveRight class="size-3 stroke-1.5" />
</span>
</router-link>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
<router-link
v-for="batch in createdBatches.data"
:to="{ name: 'BatchDetail', params: { batchName: batch.name } }"
>
<BatchCard :batch="batch" />
</router-link>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { createResource } from 'frappe-ui'
import { MoveRight } from 'lucide-vue-next'
import CourseCard from '@/components/CourseCard.vue'
import BatchCard from '@/components/BatchCard.vue'
const createdCourses = createResource({
url: 'lms.lms.utils.get_created_courses',
auto: true,
})
const createdBatches = createResource({
url: 'lms.lms.utils.get_created_batches',
auto: true,
})
</script>

View File

@@ -0,0 +1,104 @@
<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="[{ label: __('Home'), route: { name: 'Home' } }]" />
</header> -->
<div class="w-full px-5 pt-10 pb-10">
<div class="flex items-center justify-between">
<div class="space-y-2">
<div class="text-xl font-bold">
{{ __('Hey') }}, {{ user.data?.full_name }} 👋
</div>
<div class="text-lg text-ink-gray-6">
<span v-if="isAdmin">
{{ __('Manage your courses and batches at a glance') }}
</span>
<span v-else-if="myLiveClasses.data?.length > 0 || evalCount > 0">
<span v-if="myLiveClasses.data?.length > 0">
{{
__('You have {0} upcoming live classes').format(
myLiveClasses.data.length
)
}}
</span>
<span v-if="evalCount > 0">
{{ __(' and {0} evaluation').format(evalCount) }}
</span>
<span>
{{ __(' scheduled.') }}
</span>
</span>
<span v-else-if="myLiveClasses.data?.length > 0">
{{
__('You have {0} upcoming live classes.').format(
myLiveClasses.data.length
)
}}
</span>
<span v-else-if="evalCount > 0">
{{ __('You have {0} evaluations scheduled.').format(evalCount) }}
</span>
<span v-else>
{{ __('Resume where you left off') }}
</span>
</div>
</div>
<div>
<TabButtons v-if="isAdmin" v-model="currentTab" :buttons="tabs" />
</div>
</div>
<AdminHome v-if="isAdmin && currentTab === 'instructor'" />
<StudentHome v-else :myLiveClasses="myLiveClasses" />
</div>
</template>
<script setup lang="ts">
import { computed, inject, onMounted, ref } from 'vue'
import {
Breadcrumbs,
call,
createResource,
TabButtons,
usePageMeta,
} from 'frappe-ui'
import { sessionStore } from '@/stores/session'
import StudentHome from '@/pages/Home/StudentHome.vue'
import AdminHome from '@/pages/Home/AdminHome.vue'
const user = inject<any>('$user')
const { brand } = sessionStore()
const evalCount = ref(0)
const currentTab = ref<'student' | 'instructor'>('instructor')
onMounted(() => {
call('lms.lms.utils.get_upcoming_evals').then((data: any) => {
evalCount.value = data.length
})
})
const myLiveClasses = createResource({
url: 'lms.lms.utils.get_my_live_classes',
auto: true,
})
const tabs = [
{ label: __('Student'), value: 'student' },
{ label: __('Instructor'), value: 'instructor' },
]
const isAdmin = computed(() => {
return (
user.data?.is_moderator ||
user.data?.is_instructor ||
user.data?.is_evaluator
)
})
usePageMeta(() => {
return {
title: __('Home'),
icon: brand.favicon,
}
})
</script>

View File

@@ -0,0 +1,192 @@
<template>
<div>
<div v-if="myCourses.data?.length" class="mt-10">
<div class="flex items-center justify-between mb-3">
<span class="font-semibold text-lg">
{{ __('My Courses') }}
</span>
<router-link
:to="{
name: 'Courses',
}"
>
<span class="flex items-center space-x-1 text-ink-gray-5 text-xs">
<span>
{{ __('See all') }}
</span>
<MoveRight class="size-3 stroke-1.5" />
</span>
</router-link>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
<router-link
v-for="course in myCourses.data"
:to="{ name: 'CourseDetail', params: { courseName: course.name } }"
>
<CourseCard :course="course" />
</router-link>
</div>
</div>
<div v-if="myBatches.data?.length" class="mt-10">
<div class="flex items-center justify-between mb-3">
<span class="font-semibold text-lg">
{{ __('My Batches') }}
</span>
<router-link
:to="{
name: 'Batches',
}"
>
<span class="flex items-center space-x- 1 text-ink-gray-5 text-xs">
<span>
{{ __('See all') }}
</span>
<MoveRight class="size-3 stroke-1.5" />
</span>
</router-link>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
<router-link
v-for="batch in myBatches.data"
:to="{ name: 'BatchDetail', params: { batchName: batch.name } }"
>
<BatchCard :batch="batch" />
</router-link>
</div>
</div>
<div class="grid grid-cols-2 gap-5 mt-10">
<UpcomingEvaluations :forHome="true" />
<div v-if="myLiveClasses.data?.length">
<div class="font-semibold text-lg mb-3">
{{ __('Upcoming Live Classes') }}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div v-for="cls in myLiveClasses.data" class="border rounded-md p-2">
<div class="font-semibold text-ink-gray-9 text-lg mb-1">
{{ cls.title }}
</div>
<div class="text-ink-gray-7 text-sm leading-5 mb-4">
{{ cls.description }}
</div>
<div class="mt-auto space-y-3 text-ink-gray-7 text-sm">
<div class="flex items-center space-x-2">
<Calendar class="w-4 h-4 stroke-1.5" />
<span>
{{ dayjs(cls.date).format('DD MMMM YYYY') }}
</span>
</div>
<div class="flex items-center space-x-2">
<Clock class="w-4 h-4 stroke-1.5" />
<span>
{{ formatTime(cls.time) }} -
{{ dayjs(getClassEnd(cls)).format('HH:mm A') }}
</span>
</div>
<div
v-if="canAccessClass(cls)"
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
>
<a
v-if="user.data?.is_moderator || user.data?.is_evaluator"
:href="cls.start_url"
target="_blank"
class="cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
:class="cls.join_url ? 'w-full' : 'w-1/2'"
>
<Monitor class="h-4 w-4 stroke-1.5" />
{{ __('Start') }}
</a>
<a
:href="cls.join_url"
target="_blank"
class="w-full cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
>
<Video class="h-4 w-4 stroke-1.5" />
{{ __('Join') }}
</a>
</div>
<Tooltip
v-else-if="hasClassEnded(cls)"
:text="__('This class has ended')"
placement="right"
>
<div class="flex items-center space-x-2 text-ink-amber-3 w-fit">
<Info class="w-4 h-4 stroke-1.5" />
<span>
{{ __('Ended') }}
</span>
</div>
</Tooltip>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue'
import { createResource, Tooltip } from 'frappe-ui'
import { formatTime } from '@/utils'
import {
Calendar,
Clock,
Info,
Monitor,
MoveRight,
Video,
} from 'lucide-vue-next'
import CourseCard from '@/components/CourseCard.vue'
import BatchCard from '@/components/BatchCard.vue'
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
const dayjs = inject<any>('$dayjs')
const user = inject<any>('$user')
const props = defineProps<{
myLiveClasses: any
}>()
const myCourses = createResource({
url: 'lms.lms.utils.get_my_courses',
auto: true,
})
const myBatches = createResource({
url: 'lms.lms.utils.get_my_batches',
auto: true,
})
const streakInfo = createResource({
url: 'lms.lms.utils.get_streak_info',
auto: true,
})
const getClassEnd = (cls: { date: string; time: string; duration: number }) => {
const classStart = new Date(`${cls.date}T${cls.time}`)
return new Date(classStart.getTime() + cls.duration * 60000)
}
const canAccessClass = (cls: {
date: string
time: string
duration: number
}) => {
if (cls.date < dayjs().format('YYYY-MM-DD')) return false
if (cls.date > dayjs().format('YYYY-MM-DD')) return false
if (hasClassEnded(cls)) return false
return true
}
const hasClassEnded = (cls: {
date: string
time: string
duration: number
}) => {
const classEnd = getClassEnd(cls)
const now = new Date()
return now > classEnd
}
</script>

View File

@@ -3,13 +3,11 @@ import { usersStore } from './stores/user'
import { sessionStore } from './stores/session'
import { useSettings } from './stores/settings'
let defaultRoute = '/courses'
const routes = [
{
path: '/',
redirect: {
name: 'Courses',
},
name: 'Home',
component: () => import('@/pages/Home/Home.vue'),
},
{
path: '/courses',

View File

@@ -403,6 +403,12 @@ export function getUserTimezone() {
export function getSidebarLinks() {
return [
{
label: 'Home',
icon: 'Home',
to: 'Home',
activeFor: ['Home'],
},
{
label: 'Courses',
icon: 'BookOpen',