feat: batch announcements
This commit is contained in:
@@ -1,13 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<DesktopLayout>
|
<Layout>
|
||||||
<router-view />
|
<router-view />
|
||||||
</DesktopLayout>
|
</Layout>
|
||||||
<Dialogs />
|
<Dialogs />
|
||||||
<Toasts />
|
<Toasts />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import DesktopLayout from '@/components/DesktopLayout.vue'
|
import { Toasts } from 'frappe-ui'
|
||||||
import { Toasts } from 'frappe-ui'
|
import { Dialogs } from '@/utils/dialogs'
|
||||||
import { Dialogs } from '@/utils/dialogs'
|
import { computed, defineAsyncComponent } from 'vue'
|
||||||
|
import { useScreenSize } from './utils/composables'
|
||||||
|
|
||||||
|
const screenSize = useScreenSize()
|
||||||
|
const MobileLayout = defineAsyncComponent(() =>
|
||||||
|
import('@/components/MobileLayout.vue')
|
||||||
|
)
|
||||||
|
const DesktopLayout = defineAsyncComponent(() =>
|
||||||
|
import('@/components/DesktopLayout.vue')
|
||||||
|
)
|
||||||
|
|
||||||
|
const Layout = computed(() => {
|
||||||
|
if (screenSize.width < 640) {
|
||||||
|
return MobileLayout
|
||||||
|
} else {
|
||||||
|
return DesktopLayout
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
59
frontend/src/components/Annoucements.vue
Normal file
59
frontend/src/components/Annoucements.vue
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="communications.data">
|
||||||
|
<div v-for="comm in communications.data">
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Avatar :label="comm.sender_full_name" size="lg" />
|
||||||
|
<div class="ml-2">
|
||||||
|
{{ comm.sender_full_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
{{ timeAgo(comm.communication_date) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="prose prose-sm bg-gray-50 !min-w-full px-4 py-2 rounded-md"
|
||||||
|
v-html="comm.content"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { createListResource, Avatar } from 'frappe-ui'
|
||||||
|
import { timeAgo } from '@/utils'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
batch: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const communications = createListResource({
|
||||||
|
doctype: 'Communication',
|
||||||
|
fields: [
|
||||||
|
'subject',
|
||||||
|
'content',
|
||||||
|
'recipients',
|
||||||
|
'cc',
|
||||||
|
'communication_date',
|
||||||
|
'sender',
|
||||||
|
'sender_full_name',
|
||||||
|
],
|
||||||
|
filters: {
|
||||||
|
reference_doctype: 'LMS Batch',
|
||||||
|
reference_name: props.batch,
|
||||||
|
},
|
||||||
|
orderBy: 'communication_date desc',
|
||||||
|
auto: true,
|
||||||
|
cache: ['batch', props.batch],
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.prose-sm p {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -39,31 +39,11 @@ import UserDropdown from '@/components/UserDropdown.vue'
|
|||||||
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
|
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
|
||||||
import SidebarLink from '@/components/SidebarLink.vue'
|
import SidebarLink from '@/components/SidebarLink.vue'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { BookOpen, Users, TrendingUp, Briefcase } from 'lucide-vue-next'
|
import { ref } from 'vue'
|
||||||
import { ref, watch } from 'vue'
|
import { getSidebarLinks } from '../utils'
|
||||||
|
|
||||||
|
const links = getSidebarLinks()
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const getSidebarFromStorage = () => {
|
const getSidebarFromStorage = () => {
|
||||||
return useStorage('sidebar_is_collapsed', false)
|
return useStorage('sidebar_is_collapsed', false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,9 +84,9 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { Settings, Trash2, Plus } from 'lucide-vue-next'
|
import { Trash2, Plus } from 'lucide-vue-next'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import StudentModal from '@/components/StudentModal.vue'
|
import StudentModal from '@/components/Modals/StudentModal.vue'
|
||||||
|
|
||||||
const showStudentModal = ref(false)
|
const showStudentModal = ref(false)
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ import { Star } from 'lucide-vue-next'
|
|||||||
import { createResource, Button } from 'frappe-ui'
|
import { createResource, Button } from 'frappe-ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import ReviewModal from '@/components/ReviewModal.vue'
|
import ReviewModal from '@/components/Modals/ReviewModal.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
courseName: {
|
courseName: {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
import { createListResource, Button } from 'frappe-ui'
|
import { createListResource, Button } from 'frappe-ui'
|
||||||
import { Plus, Clock, Calendar, Video, Monitor } from 'lucide-vue-next'
|
import { Plus, Clock, Calendar, Video, Monitor } from 'lucide-vue-next'
|
||||||
import { inject } from 'vue'
|
import { inject } from 'vue'
|
||||||
import LiveClassModal from '@/components/LiveClassModal.vue'
|
import LiveClassModal from '@/components/Modals/LiveClassModal.vue'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { formatTime } from '@/utils/'
|
import { formatTime } from '@/utils/'
|
||||||
|
|
||||||
|
|||||||
40
frontend/src/components/MobileLayout.vue
Normal file
40
frontend/src/components/MobileLayout.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full flex-col">
|
||||||
|
<div class="h-full overflow-auto" id="scrollContainer">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-5 border-t border-gray-300 standalone:pb-4"
|
||||||
|
:style="{ gridTemplateColumns: `repeat(${tabs.length}, minmax(0, 1fr))` }"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.label"
|
||||||
|
class="flex flex-col items-center justify-center py-3 transition active:scale-95"
|
||||||
|
@click="handleClick(tab)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="tab.icon"
|
||||||
|
class="h-6 w-6"
|
||||||
|
:class="[isActive(tab) ? 'text-gray-900' : 'text-gray-600']"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { scrollTo } from '@/utils/scrollContainer'
|
||||||
|
import { getSidebarLinks } from '../utils'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const tabs = getSidebarLinks()
|
||||||
|
|
||||||
|
let isActive = computed((tab) => {
|
||||||
|
return router.currentRoute.value.name === tab.to
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClick = (tab) => {
|
||||||
|
router.push({ name: tab.to })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
117
frontend/src/components/Modals/AnnouncementModal.vue
Normal file
117
frontend/src/components/Modals/AnnouncementModal.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
title: __('Make an Announcement'),
|
||||||
|
size: 'xl',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: 'Submit',
|
||||||
|
variant: 'solid',
|
||||||
|
onClick: (close) => makeAnnouncement(close),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-600">
|
||||||
|
{{ __('Subject') }}
|
||||||
|
</div>
|
||||||
|
<Input type="text" v-model="announcement.subject" />
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-600">
|
||||||
|
{{ __('Reply To') }}
|
||||||
|
</div>
|
||||||
|
<Input type="text" v-model="announcement.replyTo" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="mb-1.5 text-sm text-gray-600">
|
||||||
|
{{ __('Announcement') }}
|
||||||
|
</div>
|
||||||
|
<TextEditor
|
||||||
|
:bubbleMenu="true"
|
||||||
|
@change="(val) => (announcement.announcement = val)"
|
||||||
|
editorClass="prose-sm py-2 px-2 min-h-[200px] border-gray-300 hover:border-gray-400 rounded-md bg-gray-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Dialog, Input, TextEditor, createResource } from 'frappe-ui'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
import { createToast } from '@/utils/'
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
batch: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
students: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const announcement = reactive({
|
||||||
|
subject: '',
|
||||||
|
replyTo: '',
|
||||||
|
announcement: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const announcementResource = createResource({
|
||||||
|
url: 'frappe.core.doctype.communication.email.make',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
recipients: props.students.join(', '),
|
||||||
|
cc: announcement.replyTo,
|
||||||
|
subject: announcement.subject,
|
||||||
|
content: announcement.announcement,
|
||||||
|
doctype: 'LMS Batch',
|
||||||
|
name: props.batch,
|
||||||
|
send_email: 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const makeAnnouncement = (close) => {
|
||||||
|
announcementResource.submit(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
validate() {
|
||||||
|
if (!props.students.length) {
|
||||||
|
return 'No students in this batch'
|
||||||
|
}
|
||||||
|
if (!announcement.subject) {
|
||||||
|
return 'Subject is required'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
close()
|
||||||
|
createToast({
|
||||||
|
title: 'Success',
|
||||||
|
text: 'Announcement has been sent successfully',
|
||||||
|
icon: 'Check',
|
||||||
|
iconClasses: 'bg-green-600 text-white rounded-md p-px',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
createToast({
|
||||||
|
title: 'Error',
|
||||||
|
text: err.messages?.[0] || err,
|
||||||
|
icon: 'x',
|
||||||
|
iconClasses: 'bg-red-600 text-white rounded-md p-px',
|
||||||
|
position: 'top-center',
|
||||||
|
timeout: 10,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -130,7 +130,6 @@ let liveClass = reactive({
|
|||||||
batch: props.batch,
|
batch: props.batch,
|
||||||
host: user.data.name,
|
host: user.data.name,
|
||||||
})
|
})
|
||||||
console.log(liveClass)
|
|
||||||
|
|
||||||
const getTimezoneOptions = () => {
|
const getTimezoneOptions = () => {
|
||||||
return getTimezones().map((timezone) => {
|
return getTimezones().map((timezone) => {
|
||||||
@@ -52,7 +52,7 @@ import { Calendar, Clock, UserCog2 } from 'lucide-vue-next'
|
|||||||
import { inject, ref } from 'vue'
|
import { inject, ref } from 'vue'
|
||||||
import { formatTime } from '../utils'
|
import { formatTime } from '../utils'
|
||||||
import { Button, createResource } from 'frappe-ui'
|
import { Button, createResource } from 'frappe-ui'
|
||||||
import EvaluationModal from '@/components/EvaluationModal.vue'
|
import EvaluationModal from '@/components/Modals/EvaluationModal.vue'
|
||||||
|
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
|||||||
@@ -4,97 +4,111 @@
|
|||||||
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
|
||||||
>
|
>
|
||||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||||
|
<Button @click="openAnnouncementModal()">
|
||||||
|
<span>
|
||||||
|
{{ __('Make an Announcement') }}
|
||||||
|
</span>
|
||||||
|
<template #suffix>
|
||||||
|
<SendIcon class="h-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
<div v-if="batch.data">
|
<div v-if="batch.data" class="grid grid-cols-[70%,30%] h-full">
|
||||||
<div class="grid grid-cols-[70%,30%] h-full">
|
<div class="border-r-2">
|
||||||
<div class="border-r-2">
|
<Tabs class="overflow-hidden" v-model="tabIndex" :tabs="tabs">
|
||||||
<Tabs class="overflow-hidden" v-model="tabIndex" :tabs="tabs">
|
<template #tab="{ tab, selected }">
|
||||||
<template #tab="{ tab, selected }">
|
<div>
|
||||||
<div>
|
<button
|
||||||
<button
|
class="group -mb-px flex items-center gap-1 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
||||||
class="group -mb-px flex items-center gap-1 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
:class="{ 'text-gray-900': selected }"
|
||||||
:class="{ 'text-gray-900': selected }"
|
>
|
||||||
|
<component
|
||||||
|
v-if="tab.icon"
|
||||||
|
:is="tab.icon"
|
||||||
|
class="h-4 stroke-1.5"
|
||||||
|
/>
|
||||||
|
{{ __(tab.label) }}
|
||||||
|
<Badge
|
||||||
|
v-if="tab.count"
|
||||||
|
:class="{
|
||||||
|
'text-gray-900 border border-gray-900': selected,
|
||||||
|
}"
|
||||||
|
variant="subtle"
|
||||||
|
theme="gray"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
<component
|
{{ tab.count }}
|
||||||
v-if="tab.icon"
|
</Badge>
|
||||||
:is="tab.icon"
|
</button>
|
||||||
class="h-4 stroke-1.5"
|
</div>
|
||||||
/>
|
</template>
|
||||||
{{ __(tab.label) }}
|
<template #default="{ tab }">
|
||||||
<Badge
|
<div class="pt-5 px-10 pb-10">
|
||||||
v-if="tab.count"
|
<div v-if="tab.label == 'Courses'">
|
||||||
:class="{
|
<div class="text-xl font-semibold">
|
||||||
'text-gray-900 border border-gray-900': selected,
|
{{ __('Courses') }}
|
||||||
}"
|
</div>
|
||||||
variant="subtle"
|
<div
|
||||||
theme="gray"
|
class="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 gap-8 mt-5"
|
||||||
size="sm"
|
>
|
||||||
>
|
<div v-for="course in courses.data">
|
||||||
{{ tab.count }}
|
<router-link
|
||||||
</Badge>
|
:to="{
|
||||||
</button>
|
name: 'CourseDetail',
|
||||||
</div>
|
params: {
|
||||||
</template>
|
courseName: course.name,
|
||||||
<template #default="{ tab }">
|
},
|
||||||
<div class="pt-5 px-10 pb-10">
|
}"
|
||||||
<div v-if="tab.label == 'Courses'">
|
>
|
||||||
<div class="text-xl font-semibold">
|
<CourseCard :key="course.name" :course="course" />
|
||||||
{{ __('Courses') }}
|
</router-link>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 gap-8 mt-5"
|
|
||||||
>
|
|
||||||
<div v-for="course in courses.data">
|
|
||||||
<router-link
|
|
||||||
:to="{
|
|
||||||
name: 'CourseDetail',
|
|
||||||
params: {
|
|
||||||
courseName: course.name,
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<CourseCard :key="course.name" :course="course" />
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab.label == 'Dashboard'">
|
|
||||||
<BatchDashboard :batch="batch" :isStudent="isStudent" />
|
|
||||||
</div>
|
|
||||||
<div v-else-if="tab.label == 'Live Class'">
|
|
||||||
<LiveClass :batch="batch.data.name" />
|
|
||||||
</div>
|
|
||||||
<div v-else-if="tab.label == 'Students'">
|
|
||||||
<BatchStudents :batch="batch.data.name" />
|
|
||||||
</div>
|
|
||||||
<div v-else-if="tab.label == 'Assessments'">
|
|
||||||
<Assessments :batch="batch.data.name" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div v-else-if="tab.label == 'Dashboard'">
|
||||||
</Tabs>
|
<BatchDashboard :batch="batch" :isStudent="isStudent" />
|
||||||
</div>
|
</div>
|
||||||
<div class="p-5">
|
<div v-else-if="tab.label == 'Live Class'">
|
||||||
<div class="text-2xl font-semibold mb-3">
|
<LiveClass :batch="batch.data.name" />
|
||||||
{{ batch.data.title }}
|
</div>
|
||||||
</div>
|
<div v-else-if="tab.label == 'Students'">
|
||||||
<div class="flex items-center mb-3">
|
<BatchStudents :batch="batch.data.name" />
|
||||||
<Calendar class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
|
</div>
|
||||||
<span>
|
<div v-else-if="tab.label == 'Assessments'">
|
||||||
{{ dayjs(batch.data.start_date).format('DD MMMM YYYY') }} -
|
<Assessments :batch="batch.data.name" />
|
||||||
{{ dayjs(batch.data.end_date).format('DD MMMM YYYY') }}
|
</div>
|
||||||
</span>
|
<div v-else-if="tab.label == 'Announcements'">
|
||||||
</div>
|
<Announcements :batch="batch.data.name" />
|
||||||
<div class="flex items-center mb-6">
|
</div>
|
||||||
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
|
</div>
|
||||||
<span>
|
</template>
|
||||||
{{ formatTime(batch.data.start_time) }} -
|
</Tabs>
|
||||||
{{ formatTime(batch.data.end_time) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-html="batch.data.description"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="text-2xl font-semibold mb-3">
|
||||||
|
{{ batch.data.title }}
|
||||||
|
</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.data.start_date).format('DD MMMM YYYY') }} -
|
||||||
|
{{ dayjs(batch.data.end_date).format('DD MMMM YYYY') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-6">
|
||||||
|
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
|
||||||
|
<span>
|
||||||
|
{{ formatTime(batch.data.start_time) }} -
|
||||||
|
{{ formatTime(batch.data.end_time) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-html="batch.data.description"></div>
|
||||||
|
</div>
|
||||||
|
<AnnouncementModal
|
||||||
|
v-model="showAnnouncementModal"
|
||||||
|
:batch="batch.data.name"
|
||||||
|
:students="batch.data.students"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="h-screen">
|
<div v-else class="h-screen">
|
||||||
@@ -152,6 +166,8 @@ import {
|
|||||||
Laptop,
|
Laptop,
|
||||||
BookOpenCheck,
|
BookOpenCheck,
|
||||||
Contact2,
|
Contact2,
|
||||||
|
Mail,
|
||||||
|
SendIcon,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { formatTime } from '@/utils'
|
import { formatTime } from '@/utils'
|
||||||
import CourseCard from '@/components/CourseCard.vue'
|
import CourseCard from '@/components/CourseCard.vue'
|
||||||
@@ -159,9 +175,12 @@ import BatchDashboard from '@/components/BatchDashboard.vue'
|
|||||||
import LiveClass from '@/components/LiveClass.vue'
|
import LiveClass from '@/components/LiveClass.vue'
|
||||||
import BatchStudents from '@/components/BatchStudents.vue'
|
import BatchStudents from '@/components/BatchStudents.vue'
|
||||||
import Assessments from '@/components/Assessments.vue'
|
import Assessments from '@/components/Assessments.vue'
|
||||||
|
import Announcements from '@/components/Annoucements.vue'
|
||||||
|
import AnnouncementModal from '@/components/Modals/AnnouncementModal.vue'
|
||||||
|
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
const showAnnouncementModal = ref(false)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
batchName: {
|
batchName: {
|
||||||
@@ -232,6 +251,11 @@ tabs.push({
|
|||||||
icon: BookOpen,
|
icon: BookOpen,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tabs.push({
|
||||||
|
label: 'Announcements',
|
||||||
|
icon: Mail,
|
||||||
|
})
|
||||||
|
|
||||||
const courses = createResource({
|
const courses = createResource({
|
||||||
url: 'lms.lms.utils.get_batch_courses',
|
url: 'lms.lms.utils.get_batch_courses',
|
||||||
params: {
|
params: {
|
||||||
@@ -244,4 +268,8 @@ const courses = createResource({
|
|||||||
const redirectToLogin = () => {
|
const redirectToLogin = () => {
|
||||||
window.location.href = `/login?redirect-to=/batches`
|
window.location.href = `/login?redirect-to=/batches`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openAnnouncementModal = () => {
|
||||||
|
showAnnouncementModal.value = true
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
100
frontend/src/utils/composables.js
Normal file
100
frontend/src/utils/composables.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
export function useScreenSize() {
|
||||||
|
const size = reactive({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onResize = () => {
|
||||||
|
size.width = window.innerWidth
|
||||||
|
size.height = window.innerHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', onResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', onResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
// write a composable for detecting swipe gestures in mobile devices
|
||||||
|
export function useSwipe() {
|
||||||
|
const swipe = reactive({
|
||||||
|
initialX: null,
|
||||||
|
initialY: null,
|
||||||
|
currentX: null,
|
||||||
|
currentY: null,
|
||||||
|
diffX: null,
|
||||||
|
diffY: null,
|
||||||
|
absDiffX: null,
|
||||||
|
absDiffY: null,
|
||||||
|
direction: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onTouchStart = (e) => {
|
||||||
|
swipe.initialX = e.touches[0].clientX
|
||||||
|
swipe.initialY = e.touches[0].clientY
|
||||||
|
swipe.direction = null
|
||||||
|
swipe.diffX = null
|
||||||
|
swipe.diffY = null
|
||||||
|
swipe.absDiffX = null
|
||||||
|
swipe.absDiffY = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTouchMove = (e) => {
|
||||||
|
swipe.currentX = e.touches[0].clientX
|
||||||
|
swipe.currentY = e.touches[0].clientY
|
||||||
|
|
||||||
|
swipe.diffX = swipe.initialX - swipe.currentX
|
||||||
|
swipe.diffY = swipe.initialY - swipe.currentY
|
||||||
|
|
||||||
|
swipe.absDiffX = Math.abs(swipe.diffX)
|
||||||
|
swipe.absDiffY = Math.abs(swipe.diffY)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTouchEnd = (e) => {
|
||||||
|
let { diffX, diffY, absDiffX, absDiffY } = swipe
|
||||||
|
if (absDiffX > absDiffY) {
|
||||||
|
if (diffX > 0) {
|
||||||
|
swipe.direction = 'left'
|
||||||
|
} else {
|
||||||
|
swipe.direction = 'right'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (diffY > 0) {
|
||||||
|
swipe.direction = 'up'
|
||||||
|
} else {
|
||||||
|
swipe.direction = 'down'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('touchstart', onTouchStart)
|
||||||
|
window.addEventListener('touchend', onTouchEnd)
|
||||||
|
window.addEventListener('touchmove', onTouchMove)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('touchstart', onTouchStart)
|
||||||
|
window.removeEventListener('touchend', onTouchEnd)
|
||||||
|
window.removeEventListener('touchmove', onTouchMove)
|
||||||
|
})
|
||||||
|
|
||||||
|
return swipe
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLocalStorage(key, initialValue) {
|
||||||
|
let value = ref(null)
|
||||||
|
let storedValue = localStorage.getItem(key)
|
||||||
|
value.value = storedValue ? JSON.parse(storedValue) : initialValue
|
||||||
|
|
||||||
|
watch(value, (newValue) => {
|
||||||
|
localStorage.setItem(key, JSON.stringify(newValue))
|
||||||
|
})
|
||||||
|
return value
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { toast } from 'frappe-ui'
|
import { toast } from 'frappe-ui'
|
||||||
import { useDateFormat, useTimeAgo } from '@vueuse/core'
|
import { useDateFormat, useTimeAgo } from '@vueuse/core'
|
||||||
|
import { BookOpen, Users, TrendingUp, Briefcase } from 'lucide-vue-next'
|
||||||
|
|
||||||
export function createToast(options) {
|
export function createToast(options) {
|
||||||
toast({
|
toast({
|
||||||
@@ -179,3 +180,28 @@ export function getTimezones() {
|
|||||||
'Pacific/Apia',
|
'Pacific/Apia',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSidebarLinks() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Courses',
|
||||||
|
icon: BookOpen,
|
||||||
|
to: 'Courses',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Batches',
|
||||||
|
icon: Users,
|
||||||
|
to: 'Batches',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Statistics',
|
||||||
|
icon: TrendingUp,
|
||||||
|
to: 'Statistics',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Jobs',
|
||||||
|
icon: Briefcase,
|
||||||
|
to: 'Jobs',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user