feat: display quiz when time is reached

This commit is contained in:
Jannat Patel
2025-05-30 18:55:26 +05:30
parent 60334ca04a
commit 622a2ff072
6 changed files with 184 additions and 70 deletions

View File

@@ -3,23 +3,14 @@
v-model="show"
:options="{
title: __('Add quiz to this video'),
size: 'xl',
actions: [
{
label: __('Save'),
variant: 'solid',
onClick({ close }) {
addQuizToVideo(close)
},
},
],
size: '2xl',
}"
>
<template #body-content>
<div class="text-base">
<div class="flex items-end gap-4">
<FormControl
:label="__('Time in Video (seconds)')"
:label="__('Time in Video (minutes)')"
v-model="quiz.time"
type="number"
placeholder="Time"
@@ -31,7 +22,7 @@
doctype="LMS Quiz"
class="flex-1"
/>
<Button @click="addQuiz()">
<Button @click="addQuiz()" variant="solid">
<template #prefix>
<Plus class="w-4 h-4 stroke-1.5" />
</template>
@@ -39,43 +30,63 @@
</Button>
</div>
<ListView
v-if="quizzes.length"
class="mt-10"
:columns="columns"
:rows="allQuizzes"
row-key="name"
:options="{
showTooltip: false,
selectable: false,
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
<div class="mt-10 mb-5">
<div class="font-medium mb-4">
{{ __('Quizzes in this video') }}
</div>
<ListView
v-if="allQuizzes.length"
:columns="columns"
:rows="allQuizzes"
row-key="quiz"
:options="{
showTooltip: false,
}"
>
<ListHeaderItem :item="item" v-for="item in columns">
<template #prefix="{ item }">
<component
v-if="item.icon"
:is="item.icon"
class="h-4 w-4 stroke-1.5 ml-4"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem :item="item" v-for="item in columns">
<template #prefix="{ item }">
<component
v-if="item.icon"
:is="item.icon"
class="h-4 w-4 stroke-1.5 ml-4"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow :row="row" v-for="row in allQuizzes">
<template #default="{ column, item }">
<ListRowItem :item="row[column.key]" :align="column.align">
<div class="leading-5 text-sm">
{{ row[column.key] }}
</div>
</ListRowItem>
<ListRows>
<ListRow :row="row" v-for="row in allQuizzes">
<template #default="{ column, item }">
<ListRowItem :item="row[column.key]" :align="column.align">
<div class="leading-5 text-sm">
{{ row[column.key] }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
<ListSelectBanner>
<template #actions="{ unselectAll, selections }">
<div class="flex gap-2">
<Button
variant="ghost"
@click="removeQuiz(selections, unselectAll)"
>
<Trash2 class="h-4 w-4 stroke-1.5" />
</Button>
</div>
</template>
</ListRow>
</ListRows>
</ListView>
</ListSelectBanner>
</ListView>
<div v-else class="text-ink-gray-5 italic text-xs">
{{ __('No quizzes added yet.') }}
</div>
</div>
</div>
</template>
</Dialog>
@@ -91,10 +102,11 @@ import {
ListRows,
ListRow,
ListRowItem,
ListSelectBanner,
toast,
} from 'frappe-ui'
import { computed, reactive, ref, watch } from 'vue'
import { Plus } from 'lucide-vue-next'
import { Plus, Trash2 } from 'lucide-vue-next'
import Link from '@/components/Controls/Link.vue'
type Quiz = {
@@ -124,11 +136,6 @@ const props = defineProps({
},
})
const addQuizToVideo = (close: () => void) => {
props.saveQuizzes(allQuizzes.value)
close()
}
const addQuiz = () => {
if (quiz.time > props.duration) {
toast.error(__('Time in video exceeds the total duration of the video.'))
@@ -139,10 +146,23 @@ const addQuiz = () => {
quiz: quiz.quiz,
})
props.saveQuizzes(allQuizzes.value)
quiz.time = 0
quiz.quiz = ''
}
const removeQuiz = (selections: string, unselectAll: () => void) => {
Array.from(selections).forEach((selection) => {
const index = allQuizzes.value.findIndex((q) => q.quiz === selection)
if (index !== -1) {
allQuizzes.value.splice(index, 1)
}
unselectAll()
})
props.saveQuizzes(allQuizzes.value)
}
watch(
() => props.quizzes,
(newQuizzes) => {
@@ -159,7 +179,7 @@ const columns = computed(() => {
},
{
key: 'time',
label: __('Time in Video (seconds)'),
label: __('Time in Video (minutes)'),
align: 'center',
},
]

View File

@@ -1,8 +1,11 @@
<template>
<div v-if="quiz.data">
<div
class="bg-surface-blue-2 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-ink-blue-3"
class="bg-surface-blue-2 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-ink-blue-3 leading-5"
>
<div v-if="inVideo">
{{ __('You will have to complete the quiz to continue the video') }}
</div>
<div class="leading-5">
{{
__('This quiz consists of {0} questions.').format(questions.length)
@@ -247,18 +250,23 @@
)
}}
</div>
<Button
@click="resetQuiz()"
class="mt-2"
v-if="
!quiz.data.max_attempts ||
attempts?.data.length < quiz.data.max_attempts
"
>
<span>
{{ __('Try Again') }}
</span>
</Button>
<div class="space-x-2">
<Button
@click="resetQuiz()"
class="mt-2"
v-if="
!quiz.data.max_attempts ||
attempts?.data.length < quiz.data.max_attempts
"
>
<span>
{{ __('Try Again') }}
</span>
</Button>
<Button v-if="inVideo" @click="props.onSubmit()">
{{ __('Resume Video') }}
</Button>
</div>
</div>
<div
v-if="
@@ -315,6 +323,14 @@ const props = defineProps({
type: String,
required: true,
},
inVideo: {
type: Boolean,
default: false,
},
onSubmit: {
type: Function,
default: () => {},
},
})
const quiz = createResource({

View File

@@ -293,7 +293,7 @@ const tabsStructure = computed(() => {
type: 'checkbox',
},
{
label: 'Certified Participants',
label: 'Certified Members',
name: 'certified_participants',
type: 'checkbox',
},

View File

@@ -1,6 +1,26 @@
<template>
<div>
<div ref="videoContainer" class="video-block relative group">
<div
v-if="quizzes.length && !showQuiz"
class="bg-surface-blue-2 space-y-1 py-3 px-4 rounded-md text-sm text-ink-blue-3 leading-5"
>
{{
__('This video has {0} {1}:').format(
quizzes.length,
quizzes.length == 1 ? 'quiz' : 'quizzes'
)
}}
<div v-for="(quiz, index) in quizzes" class="pl-3 mt-1">
<span> {{ index + 1 }}. {{ quiz.quiz }} </span>
{{ __('at {0} minutes').format(quiz.time) }}
</div>
</div>
<div
v-if="!showQuiz"
ref="videoContainer"
class="video-block relative group"
>
<video
@timeupdate="updateTime"
@ended="videoEnded"
@@ -78,6 +98,12 @@
</Button>
</div>
</div>
<Quiz
v-if="showQuiz"
:quizName="currentQuiz"
:inVideo="true"
:onSubmit="resumeVideo"
/>
<div v-if="!readOnly" @click="showQuizModal = true">
<Button>
{{ __('Add Quiz to Video') }}
@@ -106,6 +132,9 @@ let currentTime = ref(0)
let duration = ref(0)
let muted = ref(false)
const showQuizModal = ref(false)
const showQuiz = ref(false)
const currentQuiz = ref(null)
const nextQuiz = ref({})
const props = defineProps({
file: {
@@ -130,15 +159,54 @@ const props = defineProps({
})
onMounted(() => {
updateCurrentTime()
updateNextQuiz()
})
const updateCurrentTime = () => {
setTimeout(() => {
videoRef.value.onloadedmetadata = () => {
duration.value = videoRef.value.duration
}
videoRef.value.ontimeupdate = () => {
currentTime.value = videoRef.value.currentTime
currentTime.value = videoRef.value?.currentTime || currentTime.value
if (currentTime.value >= nextQuiz.value.time * 60) {
videoRef.value.pause()
playing.value = false
videoRef.value.onTimeupdate = null
currentQuiz.value = nextQuiz.value.quiz
showQuiz.value = true
updateNextQuiz()
}
}
}, 0)
})
}
const resumeVideo = () => {
showQuiz.value = false
currentQuiz.value = null
updateCurrentTime()
setTimeout(() => {
videoRef.value.currentTime = currentTime.value
videoRef.value.play()
playing.value = true
}, 0)
}
const updateNextQuiz = () => {
if (!props.quizzes.length) return
props.quizzes.sort((a, b) => a.time - b.time)
const nextQuizIndex = props.quizzes.findIndex(
(quiz) => quiz.time * 60 > currentTime.value
)
if (nextQuizIndex !== -1) {
nextQuiz.value = props.quizzes[nextQuizIndex]
} else {
nextQuiz.value = {}
}
}
const fileURL = computed(() => {
if (isYoutube) {
@@ -156,6 +224,7 @@ const isYoutube = computed(() => {
})
const playVideo = () => {
console.log(currentTime.value)
videoRef.value.play()
playing.value = true
}
@@ -166,6 +235,7 @@ const pauseVideo = () => {
}
const togglePlay = () => {
console.log(currentTime.value)
if (playing.value) {
pauseVideo()
} else {
@@ -184,6 +254,8 @@ const toggleMute = () => {
const changeCurrentTime = () => {
videoRef.value.currentTime = currentTime.value
console.log('Current Time:', currentTime.value)
updateNextQuiz()
}
const toggleFullscreen = () => {

View File

@@ -47,6 +47,7 @@ export class Upload {
const app = createApp(VideoBlock, {
file: file.file_url,
readOnly: this.readOnly,
quizzes: file.quizzes || [],
saveQuizzes: (quizzes) => {
if (this.readOnly) return
this.data.quizzes = quizzes