feat: timer in quiz
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
|
||||
<script setup>
|
||||
import { Star } from 'lucide-vue-next'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
<template>
|
||||
<div v-if="quiz.data">
|
||||
<div class="bg-blue-100 py-2 px-2 mb-4 rounded-md text-sm text-blue-800">
|
||||
<div class="leading-relaxed">
|
||||
<div
|
||||
class="bg-blue-100 space-y-2 py-2 px-2 rounded-md text-sm text-blue-800"
|
||||
>
|
||||
<div class="leading-5">
|
||||
{{
|
||||
__('This quiz consists of {0} questions.').format(questions.length)
|
||||
}}
|
||||
</div>
|
||||
<div class="leading-5">
|
||||
{{
|
||||
__(
|
||||
'Please ensure that you complete all the questions in {0} minutes.'
|
||||
).format(quiz.data.duration)
|
||||
}}
|
||||
</div>
|
||||
<div class="leading-5">
|
||||
{{
|
||||
__(
|
||||
'If you fail to do so, the quiz will be automatically submitted when the timer ends.'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div v-if="quiz.data.passing_percentage" class="leading-relaxed">
|
||||
{{
|
||||
__(
|
||||
@@ -22,14 +38,16 @@
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div v-if="quiz.data.time" class="leading-relaxed">
|
||||
{{
|
||||
__(
|
||||
'The quiz has a time limit. For each question you will be given {0} seconds.'
|
||||
).format(quiz.data.time)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="quiz.data.duration" class="flex items-center space-x-2 my-4">
|
||||
<span class="text-gray-600 text-xs"> {{ __('Time') }}: </span>
|
||||
<ProgressBar :progress="timerProgress" />
|
||||
<span class="font-semibold">
|
||||
{{ formatTimer(timer) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="activeQuestion == 0">
|
||||
<div class="border text-center p-20 rounded-md">
|
||||
<div class="font-semibold text-lg">
|
||||
@@ -63,7 +81,7 @@
|
||||
class="border rounded-md p-5"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<div class="text-sm">
|
||||
<div class="text-sm text-gray-600">
|
||||
<span class="mr-2">
|
||||
{{ __('Question {0}').format(activeQuestion) }}:
|
||||
</span>
|
||||
@@ -162,8 +180,8 @@
|
||||
editorClass="prose-sm max-w-none border-b border-x bg-gray-100 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-5">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mt-4">
|
||||
<div class="text-sm text-gray-600">
|
||||
{{
|
||||
__('Question {0} of {1}').format(
|
||||
activeQuestion,
|
||||
@@ -250,20 +268,29 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Badge, Button, createResource, ListView, TextEditor } from 'frappe-ui'
|
||||
import { ref, watch, reactive, inject } from 'vue'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
createResource,
|
||||
ListView,
|
||||
TextEditor,
|
||||
FormControl,
|
||||
} from 'frappe-ui'
|
||||
import { ref, watch, reactive, inject, computed } from 'vue'
|
||||
import { createToast } from '@/utils/'
|
||||
import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next'
|
||||
import { timeAgo } from '@/utils'
|
||||
import FormControl from 'frappe-ui/src/components/FormControl.vue'
|
||||
const user = inject('$user')
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const activeQuestion = ref(0)
|
||||
const currentQuestion = ref('')
|
||||
const selectedOptions = reactive([0, 0, 0, 0])
|
||||
const showAnswers = reactive([])
|
||||
let questions = reactive([])
|
||||
const possibleAnswer = ref(null)
|
||||
const timer = ref(0)
|
||||
let timerInterval = null
|
||||
|
||||
const props = defineProps({
|
||||
quizName: {
|
||||
@@ -284,6 +311,7 @@ const quiz = createResource({
|
||||
auto: true,
|
||||
onSuccess(data) {
|
||||
populateQuestions()
|
||||
setupTimer()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -299,6 +327,37 @@ const populateQuestions = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const setupTimer = () => {
|
||||
if (quiz.data.duration) {
|
||||
timer.value = quiz.data.duration * 60
|
||||
}
|
||||
}
|
||||
|
||||
const startTimer = () => {
|
||||
timerInterval = setInterval(() => {
|
||||
timer.value--
|
||||
if (timer.value == 0) {
|
||||
clearInterval(timerInterval)
|
||||
submitQuiz()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const formatTimer = (seconds) => {
|
||||
const hrs = Math.floor(seconds / 3600)
|
||||
.toString()
|
||||
.padStart(2, '0')
|
||||
const mins = Math.floor((seconds % 3600) / 60)
|
||||
.toString()
|
||||
.padStart(2, '0')
|
||||
const secs = (seconds % 60).toString().padStart(2, '0')
|
||||
return hrs != '00' ? `${hrs}:${mins}:${secs}` : `${mins}:${secs}`
|
||||
}
|
||||
|
||||
const timerProgress = computed(() => {
|
||||
return (timer.value / (quiz.data.duration * 60)) * 100
|
||||
})
|
||||
|
||||
const shuffleArray = (array) => {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
@@ -383,6 +442,7 @@ watch(
|
||||
const startQuiz = () => {
|
||||
activeQuestion.value = 1
|
||||
localStorage.removeItem(quiz.data.title)
|
||||
if (quiz.data.duration) startTimer()
|
||||
}
|
||||
|
||||
const markAnswer = (index) => {
|
||||
@@ -493,9 +553,15 @@ const submitQuiz = () => {
|
||||
}
|
||||
|
||||
const createSubmission = () => {
|
||||
quizSubmission.reload().then(() => {
|
||||
if (quiz.data && quiz.data.max_attempts) attempts.reload()
|
||||
})
|
||||
quizSubmission.submit(
|
||||
{},
|
||||
{
|
||||
onSuccess(data) {
|
||||
if (quiz.data && quiz.data.max_attempts) attempts.reload()
|
||||
if (quiz.data.duration) clearInterval(timerInterval)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const resetQuiz = () => {
|
||||
@@ -504,6 +570,7 @@ const resetQuiz = () => {
|
||||
showAnswers.length = 0
|
||||
quizSubmission.reset()
|
||||
populateQuestions()
|
||||
setupTimer()
|
||||
}
|
||||
|
||||
const getInstructions = (question) => {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="w-3/4 mx-auto py-5">
|
||||
<!-- Details -->
|
||||
<div class="mb-8">
|
||||
<div class="text-sm font-semibold mb-4">
|
||||
<div class="font-semibold mb-4">
|
||||
{{ __('Details') }}
|
||||
</div>
|
||||
<FormControl
|
||||
@@ -50,11 +50,17 @@
|
||||
"
|
||||
/>
|
||||
<div v-if="quizDetails.data?.name">
|
||||
<div class="grid grid-cols-3 gap-5 mt-4 mb-8">
|
||||
<div class="grid grid-cols-2 gap-5 mt-4 mb-8">
|
||||
<FormControl
|
||||
type="number"
|
||||
v-model="quiz.max_attempts"
|
||||
:label="__('Maximun Attempts')"
|
||||
/>
|
||||
<FormControl
|
||||
type="number"
|
||||
v-model="quiz.duration"
|
||||
:label="__('Duration (in minutes)')"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="quiz.total_marks"
|
||||
:label="__('Total Marks')"
|
||||
@@ -68,7 +74,7 @@
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="mb-8">
|
||||
<div class="text-sm font-semibold mb-4">
|
||||
<div class="font-semibold mb-4">
|
||||
{{ __('Settings') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-5 my-4">
|
||||
@@ -86,7 +92,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<div class="text-sm font-semibold mb-4">
|
||||
<div class="font-semibold mb-4">
|
||||
{{ __('Shuffle Settings') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3">
|
||||
@@ -106,7 +112,7 @@
|
||||
<!-- Questions -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="text-sm font-semibold">
|
||||
<div class="font-semibold">
|
||||
{{ __('Questions') }}
|
||||
</div>
|
||||
<Button @click="openQuestionModal()">
|
||||
@@ -226,6 +232,7 @@ const quiz = reactive({
|
||||
total_marks: 0,
|
||||
passing_percentage: 0,
|
||||
max_attempts: 0,
|
||||
duration: 0,
|
||||
limit_questions_to: 0,
|
||||
show_answers: true,
|
||||
show_submission_history: false,
|
||||
|
||||
Reference in New Issue
Block a user