feat: zen mode

This commit is contained in:
Jannat Patel
2025-04-23 11:44:39 +05:30
parent 4b80fbe5eb
commit 93b5cb6161
2 changed files with 190 additions and 139 deletions

View File

@@ -4,7 +4,16 @@
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5" class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs class="h-7" :items="breadcrumbs" /> <Breadcrumbs class="h-7" :items="breadcrumbs" />
<CertificationLinks :courseName="courseName" /> <div class="flex items-center space-x-2">
<Tooltip :text="__('Zen Mode')">
<Button @click="goFullScreen()">
<template #icon>
<Focus class="w-4 h-4 stroke-2" />
</template>
</Button>
</Tooltip>
<CertificationLinks :courseName="courseName" />
</div>
</header> </header>
<div class="grid md:grid-cols-[70%,30%] h-screen"> <div class="grid md:grid-cols-[70%,30%] h-screen">
<div v-if="lesson.data.no_preview" class="border-r"> <div v-if="lesson.data.no_preview" class="border-r">
@@ -33,146 +42,154 @@
</Button> </Button>
</div> </div>
</div> </div>
<div v-else ref="lessonContainer" class="bg-surface-white">
<div v-else class="border-r container pt-5 pb-10 px-5">
<div class="flex flex-col md:flex-row md:items-center justify-between">
<div class="text-3xl font-semibold text-ink-gray-9">
{{ lesson.data.title }}
</div>
<div class="flex items-center mt-2 md:mt-0">
<router-link
v-if="lesson.data.prev"
:to="{
name: 'Lesson',
params: {
courseName: courseName,
chapterNumber: lesson.data.prev.split('.')[0],
lessonNumber: lesson.data.prev.split('.')[1],
},
}"
>
<Button class="mr-2">
<template #prefix>
<ChevronLeft class="w-4 h-4 stroke-1" />
</template>
<span>
{{ __('Previous') }}
</span>
</Button>
</router-link>
<router-link
v-if="allowEdit()"
:to="{
name: 'LessonForm',
params: {
courseName: courseName,
chapterNumber: props.chapterNumber,
lessonNumber: props.lessonNumber,
},
}"
>
<Button class="mr-2">
{{ __('Edit') }}
</Button>
</router-link>
<router-link
v-if="lesson.data.next"
:to="{
name: 'Lesson',
params: {
courseName: courseName,
chapterNumber: lesson.data.next.split('.')[0],
lessonNumber: lesson.data.next.split('.')[1],
},
}"
>
<Button>
<template #suffix>
<ChevronRight class="w-4 h-4 stroke-1" />
</template>
<span>
{{ __('Next') }}
</span>
</Button>
</router-link>
<router-link
v-else
:to="{
name: 'CourseDetail',
params: { courseName: courseName },
}"
>
<Button>
{{ __('Back to Course') }}
</Button>
</router-link>
</div>
</div>
<div class="flex items-center mt-2">
<span
class="h-6 mr-1"
:class="{
'avatar-group overlap': lesson.data.instructors?.length > 1,
}"
>
<UserAvatar
v-for="instructor in lesson.data.instructors"
:user="instructor"
/>
</span>
<CourseInstructors
v-if="lesson.data?.instructors"
:instructors="lesson.data.instructors"
/>
</div>
<div <div
v-if=" class="border-r container pt-5 pb-10 px-5"
lesson.data.instructor_content && :class="{
JSON.parse(lesson.data.instructor_content)?.blocks?.length > 1 && 'w-1/2 mx-auto border-none': zenModeEnabled,
allowInstructorContent() }"
"
class="bg-surface-gray-2 p-3 rounded-md mt-6"
> >
<div class="text-ink-gray-5 font-medium"> <div
{{ __('Instructor Notes') }} class="flex flex-col md:flex-row md:items-center justify-between"
>
<div class="text-3xl font-semibold text-ink-gray-9">
{{ lesson.data.title }}
</div>
<div class="flex items-center mt-2 md:mt-0">
<router-link
v-if="lesson.data.prev"
:to="{
name: 'Lesson',
params: {
courseName: courseName,
chapterNumber: lesson.data.prev.split('.')[0],
lessonNumber: lesson.data.prev.split('.')[1],
},
}"
>
<Button class="mr-2">
<template #prefix>
<ChevronLeft class="w-4 h-4 stroke-1" />
</template>
<span>
{{ __('Previous') }}
</span>
</Button>
</router-link>
<router-link
v-if="allowEdit()"
:to="{
name: 'LessonForm',
params: {
courseName: courseName,
chapterNumber: props.chapterNumber,
lessonNumber: props.lessonNumber,
},
}"
>
<Button class="mr-2">
{{ __('Edit') }}
</Button>
</router-link>
<router-link
v-if="lesson.data.next"
:to="{
name: 'Lesson',
params: {
courseName: courseName,
chapterNumber: lesson.data.next.split('.')[0],
lessonNumber: lesson.data.next.split('.')[1],
},
}"
>
<Button>
<template #suffix>
<ChevronRight class="w-4 h-4 stroke-1" />
</template>
<span>
{{ __('Next') }}
</span>
</Button>
</router-link>
<router-link
v-else
:to="{
name: 'CourseDetail',
params: { courseName: courseName },
}"
>
<Button>
{{ __('Back to Course') }}
</Button>
</router-link>
</div>
</div>
<div class="flex items-center mt-2">
<span
class="h-6 mr-1"
:class="{
'avatar-group overlap': lesson.data.instructors?.length > 1,
}"
>
<UserAvatar
v-for="instructor in lesson.data.instructors"
:user="instructor"
/>
</span>
<CourseInstructors
v-if="lesson.data?.instructors"
:instructors="lesson.data.instructors"
/>
</div> </div>
<div <div
id="instructor-content" v-if="
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal" lesson.data.instructor_content &&
></div> JSON.parse(lesson.data.instructor_content)?.blocks?.length > 1 &&
</div> allowInstructorContent()
<div "
v-else-if="lesson.data.instructor_notes" class="bg-surface-gray-2 p-3 rounded-md mt-6"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-6" >
> <div class="text-ink-gray-5 font-medium">
<LessonContent :content="lesson.data.instructor_notes" /> {{ __('Instructor Notes') }}
</div> </div>
<div <div
v-if="lesson.data.content" id="instructor-content"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-5" class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal"
> ></div>
<div id="editor"></div> </div>
</div> <div
<div v-else-if="lesson.data.instructor_notes"
v-else class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-6"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-5" >
> <LessonContent :content="lesson.data.instructor_notes" />
<LessonContent </div>
v-if="lesson.data?.body" <div
:content="lesson.data.body" v-if="lesson.data.content"
:youtube="lesson.data.youtube" class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-5"
:quizId="lesson.data.quiz_id" >
/> <div id="editor"></div>
</div> </div>
<div class="mt-20"> <div
<Discussions v-else
v-if="allowDiscussions" class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-5"
:title="'Questions'" >
:doctype="'Course Lesson'" <LessonContent
:docname="lesson.data.name" v-if="lesson.data?.body"
:key="lesson.data.name" :content="lesson.data.body"
/> :youtube="lesson.data.youtube"
:quizId="lesson.data.quiz_id"
/>
</div>
<div class="mt-20">
<Discussions
v-if="allowDiscussions"
:title="'Questions'"
:doctype="'Course Lesson'"
:docname="lesson.data.name"
:key="lesson.data.name"
/>
</div>
</div> </div>
</div> </div>
<div class="sticky top-10"> <div class="sticky top-10">
@@ -202,7 +219,13 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { createResource, Breadcrumbs, Button, usePageMeta } from 'frappe-ui' import {
createResource,
Breadcrumbs,
Button,
Tooltip,
usePageMeta,
} from 'frappe-ui'
import { computed, watch, inject, ref, onMounted, onBeforeUnmount } from 'vue' import { computed, watch, inject, ref, onMounted, onBeforeUnmount } from 'vue'
import CourseOutline from '@/components/CourseOutline.vue' import CourseOutline from '@/components/CourseOutline.vue'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
@@ -212,6 +235,7 @@ import {
ChevronRight, ChevronRight,
LockKeyholeIcon, LockKeyholeIcon,
LogIn, LogIn,
Focus,
} from 'lucide-vue-next' } from 'lucide-vue-next'
import Discussions from '@/components/Discussions.vue' import Discussions from '@/components/Discussions.vue'
import { getEditorTools } from '../utils' import { getEditorTools } from '../utils'
@@ -229,6 +253,8 @@ const allowDiscussions = ref(false)
const editor = ref(null) const editor = ref(null)
const instructorEditor = ref(null) const instructorEditor = ref(null)
const lessonProgress = ref(0) const lessonProgress = ref(0)
const lessonContainer = ref(null)
const zenModeEnabled = ref(false)
const timer = ref(0) const timer = ref(0)
const { brand } = sessionStore() const { brand } = sessionStore()
let timerInterval let timerInterval
@@ -250,6 +276,19 @@ const props = defineProps({
onMounted(() => { onMounted(() => {
startTimer() startTimer()
document.addEventListener('fullscreenchange', attachFullscreenEvent)
})
const attachFullscreenEvent = () => {
if (document.fullscreenElement) {
zenModeEnabled.value = true
} else {
zenModeEnabled.value = false
}
}
onBeforeUnmount(() => {
document.removeEventListener('fullscreenchange', attachFullscreenEvent)
}) })
const lesson = createResource({ const lesson = createResource({
@@ -431,6 +470,18 @@ const enrollStudent = () => {
) )
} }
const goFullScreen = () => {
if (lessonContainer.value.requestFullscreen) {
lessonContainer.value.requestFullscreen()
} else if (lessonContainer.value.mozRequestFullScreen) {
lessonContainer.value.mozRequestFullScreen()
} else if (lessonContainer.value.webkitRequestFullscreen) {
lessonContainer.value.webkitRequestFullscreen()
} else if (lessonContainer.value.msRequestFullscreen) {
lessonContainer.value.msRequestFullscreen()
}
}
const redirectToLogin = () => { const redirectToLogin = () => {
window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}` window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}`
} }

View File

@@ -200,7 +200,7 @@ export function getEditorTools() {
youtube: { youtube: {
regex: /(?:https?:\/\/)?(?:www\.)?(?:(?:youtu\.be\/)|(?:youtube\.com)\/(?:v\/|u\/\w\/|embed\/|watch))(?:(?:\?v=)?([^#&?=]*))?((?:[?&]\w*=\w*)*)/, regex: /(?:https?:\/\/)?(?:www\.)?(?:(?:youtu\.be\/)|(?:youtube\.com)\/(?:v\/|u\/\w\/|embed\/|watch))(?:(?:\?v=)?([^#&?=]*))?((?:[?&]\w*=\w*)*)/,
embedUrl: embedUrl:
'https://www.youtube.com/embed/<%= remote_id %>', 'https://www.youtube.com/embed/<%= remote_id %>?modestbranding=1&enablejsapi=1&widgetid=3&iv_load_policy=3&fs=0',
html: `<iframe style="width:100%; height: ${ html: `<iframe style="width:100%; height: ${
window.innerWidth < 640 ? '15rem' : '30rem' window.innerWidth < 640 ? '15rem' : '30rem'
};" frameborder="0" allowfullscreen></iframe>`, };" frameborder="0" allowfullscreen></iframe>`,