nuxt-prawo-jazdy/pages/exam.vue
NetMan 59497e8b01 exam: warning on refresh and end-exam
warning on end-exam if it isn't the last question - otherwise exam ends without warning
2025-12-15 21:05:54 +01:00

269 lines
6.8 KiB
Vue

<script lang="ts" setup>
import { intervalToDuration, addMinutes, addSeconds, isAfter } from 'date-fns';
import { useNow } from '@vueuse/core';
import { useExamStore } from '~/store/examStore';
definePageMeta({ middleware: 'exam' });
const examStore = useExamStore();
const basicStore = useBasicStore();
const advancedStore = useAdvancedStore();
await callOnce(() => examStore.mildReset(), { mode: 'navigation' });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function preventRefresh(e: any) {
e.preventDefault();
e.returnValue = ''; // polyfill for older chrome
}
const now = ref('basic');
const nowTime = useNow();
const timeEnd = addMinutes(new Date(), 25);
const questionEnd = ref(addSeconds(new Date(), 20));
const mediaLoaded = ref(false);
const time = ref({
total: computed(() =>
intervalToDuration({
start: nowTime.value,
end: timeEnd,
}),
),
question: computed(() => {
const interval = intervalToDuration({
start: nowTime.value,
end: questionEnd.value,
});
if (Object.hasOwn(interval, 'seconds') && interval.seconds != undefined) {
return interval.seconds;
} else {
return 0;
}
}),
phase: 'set-basic',
});
function changeQuestionTimeAfterNext() {
if (time.value.phase == 'set-basic') {
time.value.phase = 'start-basic';
questionEnd.value = addSeconds(new Date(), 15);
} else if (time.value.phase == 'start-basic') {
if (now.value == 'basic') {
time.value.phase = 'set-basic';
mediaLoaded.value = false;
questionEnd.value = addSeconds(new Date(), 20);
} else {
time.value.phase = 'set-advanced';
questionEnd.value = addSeconds(new Date(), 50);
}
} else if (time.value.phase == 'set-advanced') {
questionEnd.value = addSeconds(new Date(), 50);
}
}
function onMediaLoad() {
mediaLoaded.value = true;
}
function clickNext() {
if (ending.value) {
endExam();
}
if (time.value.phase != 'set-basic') {
next();
}
changeQuestionTimeAfterNext();
}
onMounted(() => {
useHead({
title: 'Pytanie 1/20',
});
window.addEventListener('beforeunload', preventRefresh);
watchEffect(() => {
if (isAfter(nowTime.value, timeEnd)) endExam();
});
watchEffect(() => {
if (now.value === 'basic')
useHead({ title: `Pytanie ${countBasic.value + 1}/20` });
if (now.value === 'advanced')
useHead({ title: `Pytanie ${countAdvanced.value + 1}/12` });
});
watchEffect(() => {
if (mediaLoaded.value == false && time.value.phase == 'start-basic') {
questionEnd.value = addSeconds(new Date(), 15);
}
if (time.value.question < 0 && ending.value == false) {
clickNext();
}
});
});
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', preventRefresh);
});
const {
data: dataBasic,
error: errorBasic,
status: statusBasic,
} = await useLazyFetch<BasicQuestion[]>(`/api/basic`, {
query: {
category: examStore.category,
},
});
const {
data: dataAdvanced,
error: errorAdvanced,
status: statusAdvanced,
} = await useLazyFetch<AdvancedQuestion[]>(`/api/advanced`, {
query: {
category: examStore.category,
},
});
const countBasic = ref(0);
const countAdvanced = ref(-1);
const answer = ref<string>('');
const ending = ref(false);
const questionBasic = computed<BasicQuestion | undefined>(() =>
dataBasic.value?.at(countBasic.value),
);
const questionAdvanced = computed<AdvancedQuestion | undefined>(() =>
dataAdvanced.value?.at(countAdvanced.value),
);
const question = computed(() => {
if (now.value === 'basic') return questionBasic.value;
if (now.value === 'advanced') return questionAdvanced.value;
return null;
});
const result: Ref<ResultEndType> = ref({
basic: [],
advanced: [],
});
function next() {
if (examStore.end == false) {
result.value[now.value as keyof ResultEndType].push({
question: question.value,
chosen_answer: answer.value,
correct_answer: question.value?.correct_answer ?? null,
chosen_is_correct: answer.value == question.value?.correct_answer,
});
answer.value = '';
} else {
console.error('next() incorrectly executed - exam has already ended.');
}
if (now.value === 'basic') {
if (countBasic.value + 1 <= 19) {
countBasic.value++;
} else {
now.value = 'advanced';
countAdvanced.value++;
}
} else if (now.value === 'advanced') {
if (countAdvanced.value + 1 <= 11) {
countAdvanced.value++;
}
if (countAdvanced.value + 1 >= 12) {
ending.value = true;
}
}
}
function endExam() {
loading.value = true;
while (ending.value == false) {
if (time.value.phase != 'set-basic') {
next();
}
changeQuestionTimeAfterNext();
}
next();
basicStore.set(result.value.basic);
advancedStore.set(result.value.advanced);
examStore.setEnd(true);
if (
basicStore.basic == result.value.basic &&
advancedStore.advanced == result.value.advanced &&
examStore.end
) {
return navigateTo(`/result`, { replace: true });
} else {
return navigateTo(`/anomaly`);
}
}
const loading = ref(false);
const showEndModal = ref(false);
</script>
<template>
<div>
<!-- as in to transition to the next page -->
<LoadingScreen v-if="loading" />
<div v-if="statusBasic === 'success' && statusAdvanced === 'success'">
<div class="grid grid-cols-4 min-h-dvh">
<div class="col-span-3 flex flex-col">
<BarTop
:points="question?.weight"
:category="examStore.category"
:time-remaining="time.total"
/>
<MediaBox
:media-path="question?.media_url"
:phase="time.phase"
@mediaload="onMediaLoad()"
/>
<QuestionBasic
v-if="now === 'basic'"
v-model="answer"
phase="exam"
:question="questionBasic"
/>
<QuestionAdvanced
v-else-if="now === 'advanced'"
v-model="answer"
phase="exam"
:question="questionAdvanced"
/>
</div>
<BarRightExam
:count-basic="countBasic"
:count-advanced="countAdvanced"
:now="now"
:ending="ending"
:set-basic="time.phase == 'set-basic'"
:time="time.question"
:phase="time.phase"
@show-end-modal="showEndModal = true"
@next-question="clickNext()"
@end-exam="endExam()"
@next-time="changeQuestionTimeAfterNext()"
/>
</div>
</div>
<div v-else-if="statusBasic === 'error' || statusAdvanced === 'error'">
An API error occurred: {{ errorBasic }} {{ errorAdvanced }}
</div>
<LoadingScreen v-else />
<EndModal
:show-modal="showEndModal"
@hide-end-modal="showEndModal = false"
@end-exam="endExam()"
/>
</div>
</template>