major DB overhaul, api changes, lint
This commit is contained in:
parent
74ba3a5023
commit
d500031f34
32 changed files with 2468 additions and 949 deletions
|
@ -4,17 +4,20 @@
|
||||||
|
|
||||||
This project utilizes `pnpm`, thus it is recommended
|
This project utilizes `pnpm`, thus it is recommended
|
||||||
|
|
||||||
|
Also use [db-prawo-jazdy](https://git.mandarynki.eu/netman/db-prawo-jazdy) for running this project
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm install
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
# To-do:
|
# To-do:
|
||||||
|
|
||||||
- [ ] re-forge database structure, script for processing, share appropriate files
|
- [x] re-forge database structure (good for now)
|
||||||
- [ ] choose category
|
- [ ] db: script for processing, share appropriate files
|
||||||
|
- [x] choose category (good for now)
|
||||||
- [ ] beautify website
|
- [ ] beautify website
|
||||||
- [ ] better answer click recognition
|
- [ ] better answer click recognition
|
||||||
- [ ] come up with how to show results appropriately
|
- [x] come up with how to show results appropriately
|
||||||
- [ ] i18n - pl, en, de, ua (not all questions are not available in ua, api handle)
|
- [ ] i18n - pl, en, de, ua (not all questions are not available in ua, api handle)
|
||||||
- [ ] exam (maybe also results?) warning leave message on exit (refresh)
|
- [ ] exam (maybe also results?) warning leave message on exit (refresh)
|
||||||
- [ ] lazy loading
|
- [ ] lazy loading
|
||||||
|
|
2
app.vue
2
app.vue
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import "~/assets/css/main.css";
|
import '~/assets/css/main.css';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
defineProps<{
|
|
||||||
question: AdvancedQuestion | undefined;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const abc_model = defineModel();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-5 border-t px-4 py-5 border-slate-300 bg-slate-100"
|
|
||||||
>
|
|
||||||
<div class="text-xl">{{ question?.pytanie }}</div>
|
|
||||||
<div>
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="abc"
|
|
||||||
id="odp_a"
|
|
||||||
v-model="abc_model"
|
|
||||||
value="a"
|
|
||||||
class="hidden"
|
|
||||||
/>
|
|
||||||
<label for="odp_a">
|
|
||||||
<div
|
|
||||||
:class="`btn btn-primary btn-lg ${
|
|
||||||
abc_model == 'a' ? ' !btn-secondary' : ''
|
|
||||||
}`"
|
|
||||||
>
|
|
||||||
A
|
|
||||||
</div>
|
|
||||||
<span class="block">{{ question?.odp_a }}</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="abc"
|
|
||||||
id="odp_b"
|
|
||||||
v-model="abc_model"
|
|
||||||
value="b"
|
|
||||||
class="hidden"
|
|
||||||
/>
|
|
||||||
<label for="odp_b">
|
|
||||||
<div
|
|
||||||
:class="`btn btn-primary btn-lg ${
|
|
||||||
abc_model == 'b' ? ' !btn-secondary' : ''
|
|
||||||
}`"
|
|
||||||
>
|
|
||||||
B
|
|
||||||
</div>
|
|
||||||
<span class="block">{{ question?.odp_b }}</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="abc"
|
|
||||||
id="odp_c"
|
|
||||||
v-model="abc_model"
|
|
||||||
value="c"
|
|
||||||
class="hidden"
|
|
||||||
/>
|
|
||||||
<label for="odp_c">
|
|
||||||
<div
|
|
||||||
:class="`btn btn-primary btn-lg ${
|
|
||||||
abc_model == 'c' ? ' !btn-secondary' : ''
|
|
||||||
}`"
|
|
||||||
>
|
|
||||||
C
|
|
||||||
</div>
|
|
||||||
<span class="block">{{ question?.odp_c }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
label {
|
|
||||||
@apply cursor-pointer flex flex-row gap-2 items-center;
|
|
||||||
&:hover {
|
|
||||||
@apply bg-slate-200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
@apply text-lg;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,31 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
defineProps<{
|
|
||||||
question: BasicQuestion | undefined;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const tak_nie_model = defineModel();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-6 border-t px-4 py-5 border-slate-300 bg-slate-100"
|
|
||||||
>
|
|
||||||
<div class="text-xl">{{ question?.pytanie }}</div>
|
|
||||||
<div>
|
|
||||||
<div class="flex flex-row justify-around">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="tak_nie"
|
|
||||||
v-for="tn in ['tak', 'nie']"
|
|
||||||
:id="`odp_${tn}`"
|
|
||||||
v-model="tak_nie_model"
|
|
||||||
:value="tn"
|
|
||||||
class="btn btn-primary btn-xl"
|
|
||||||
:aria-label="tn.toUpperCase()"
|
|
||||||
:class="`${tak_nie_model == tn ? ' !btn-secondary' : ''}`"
|
|
||||||
:checked="tak_nie_model == tn"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
10
components/CurrentQuestionCount.vue
Normal file
10
components/CurrentQuestionCount.vue
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="text-lg"><slot name="title" /></span>
|
||||||
|
<div class="info-little-box w-full text-center">
|
||||||
|
<slot name="count" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -2,7 +2,7 @@
|
||||||
<div
|
<div
|
||||||
class="flex min-h-dvh justify-center items-center text-5xl flex-col gap-10"
|
class="flex min-h-dvh justify-center items-center text-5xl flex-col gap-10"
|
||||||
>
|
>
|
||||||
<span class="loading loading-spinner loading-xl scale-[2.5] block"></span>
|
<span class="loading loading-spinner loading-xl scale-[2.5] block" />
|
||||||
<span class="block">Ładowanie</span>
|
<span class="block">Ładowanie</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,13 +2,15 @@
|
||||||
const runtimeConfig = useRuntimeConfig();
|
const runtimeConfig = useRuntimeConfig();
|
||||||
const cdnUrl = runtimeConfig.public.cdn_url;
|
const cdnUrl = runtimeConfig.public.cdn_url;
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
media: {
|
media: string | null | undefined;
|
||||||
fileName: string | undefined;
|
|
||||||
fileType: string | undefined;
|
|
||||||
ogName: string | null | undefined;
|
|
||||||
};
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const mediaSplit = computed(() => {
|
||||||
|
const dotSplit = props.media?.split('.');
|
||||||
|
const extension = dotSplit?.pop()?.toLowerCase();
|
||||||
|
return [dotSplit?.join('.'), extension];
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -18,20 +20,13 @@ defineProps<{
|
||||||
<!-- Reserved for getting to know the question (20s) in basic questions section
|
<!-- Reserved for getting to know the question (20s) in basic questions section
|
||||||
src="~/public/placeholder.svg" -->
|
src="~/public/placeholder.svg" -->
|
||||||
<img
|
<img
|
||||||
:src="cdnUrl + media.ogName"
|
v-if="mediaSplit[1] === 'jpg'"
|
||||||
|
:key="`${media}-photo`"
|
||||||
|
:src="cdnUrl + media"
|
||||||
alt=""
|
alt=""
|
||||||
v-if="media.fileType == 'jpg'"
|
|
||||||
:key="`${media.ogName}-photo`"
|
|
||||||
/>
|
/>
|
||||||
<video
|
<video v-else-if="mediaSplit[1] === 'wmv'" :key="`${media}-video`" autoplay>
|
||||||
v-else-if="media.fileType == 'wmv'"
|
<source :src="cdnUrl + mediaSplit[0] + '.mp4'" type="video/mp4" />
|
||||||
:key="`${media.ogName}-video`"
|
|
||||||
autoplay
|
|
||||||
>
|
|
||||||
<source
|
|
||||||
:src="cdnUrl + [media.fileName, 'mp4'].join('.')"
|
|
||||||
type="video/mp4"
|
|
||||||
/>
|
|
||||||
</video>
|
</video>
|
||||||
<span v-else class="text-4xl font-bold flex items-center justify-center"
|
<span v-else class="text-4xl font-bold flex items-center justify-center"
|
||||||
>Brak mediów</span
|
>Brak mediów</span
|
||||||
|
|
|
@ -1,40 +1,34 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { VueFinalModal } from "vue-final-modal";
|
const myModal = useTemplateRef('myModal');
|
||||||
|
|
||||||
defineProps<{
|
onMounted(() => {
|
||||||
title?: string;
|
myModal.value?.showModal();
|
||||||
}>();
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "homepage"): void;
|
|
||||||
(e: "newExam"): void;
|
|
||||||
(e: "close"): void;
|
|
||||||
}>();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VueFinalModal
|
<dialog
|
||||||
class="flex justify-center items-center backdrop-blur-sm"
|
ref="myModal"
|
||||||
content-class="flex flex-col p-3 bg-white rounded-md gap-3"
|
class="flex justify-center items-center backdrop-blur-sm modal"
|
||||||
overlay-transition="vfm-fade"
|
|
||||||
content-transition="vfm-fade"
|
|
||||||
:click-to-close="false"
|
|
||||||
:esc-to-close="false"
|
|
||||||
>
|
>
|
||||||
<h1 class="text-[1.5rem]">{{ title }}</h1>
|
<div
|
||||||
<div class="*:inline">Kategoria: <slot name="category" /></div>
|
class="flex flex-col p-3 bg-white rounded-md gap-3 modal-box min-w-fit"
|
||||||
<div class="*:inline">Punkty: <slot name="points" /> / 74</div>
|
>
|
||||||
<div class="*:inline">Wynik: <slot name="resultTrueFalse" /></div>
|
<h1 class="text-[1.5rem]">
|
||||||
<div class="flex flex-row gap-2">
|
<slot name="title" />
|
||||||
<button class="btn btn-soft" @click="emit('homepage')">
|
</h1>
|
||||||
Wróć na stronę główną
|
<div class="*:inline">Kategoria: <slot name="category" /></div>
|
||||||
</button>
|
<div class="*:inline">Punkty: <slot name="points" /> / 74</div>
|
||||||
<button class="btn btn-outline" @click="emit('newExam')">
|
<div class="*:inline">Wynik: <slot name="resultTrueFalse" /></div>
|
||||||
Rozpocznij jeszcze raz
|
<div class="flex flex-row gap-2">
|
||||||
</button>
|
<NuxtLink to="/" class="btn btn-soft">Wróć na stronę główną</NuxtLink>
|
||||||
<button class="btn btn-neutral" @click="emit('close')">
|
<NuxtLink to="/exam" class="btn btn-outline">
|
||||||
Przejrzyj odpowiedzi
|
Rozpocznij jeszcze raz
|
||||||
</button>
|
</NuxtLink>
|
||||||
|
<button class="btn btn-neutral" @click="myModal?.close()">
|
||||||
|
Przejrzyj odpowiedzi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</VueFinalModal>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { range } from "lodash";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
result: ResultEndType;
|
|
||||||
countBasic: number;
|
|
||||||
countAdvanced: number;
|
|
||||||
question: BasicQuestion | AdvancedQuestion | undefined;
|
|
||||||
questionBasic: BasicQuestion | undefined;
|
|
||||||
questionAdvanced: AdvancedQuestion | undefined;
|
|
||||||
now: string | null | undefined;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const isBasic = computed(() => props.now == "basic");
|
|
||||||
const isAdvanced = computed(() => props.now == "advanced");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex flex-col items-stretch p-4 gap-6 border-l border-slate-300 bg-slate-100"
|
|
||||||
>
|
|
||||||
<button @click="$emit('nav', '/')" class="btn btn-warning btn-xl">
|
|
||||||
Wróć na stronę główną
|
|
||||||
</button>
|
|
||||||
<div class="flex flex-row gap-4 *:flex-1 w-full flex-wrap">
|
|
||||||
<button class="btn btn-info btn-lg" @click="$emit('change-now', 'basic')">
|
|
||||||
Pytania podstawowe
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-info btn-lg"
|
|
||||||
@click="$emit('change-now', 'advanced')"
|
|
||||||
>
|
|
||||||
Pytania specjalistyczne
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-[repeat(auto-fit,50px)] gap-2 justify-around w-full"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
:aria-label="(num + 1).toString()"
|
|
||||||
class="btn btn-md"
|
|
||||||
name="chooser"
|
|
||||||
v-for="num in range(0, 20)"
|
|
||||||
@click="
|
|
||||||
$emit('change-now', 'basic');
|
|
||||||
$emit('change-count', num);
|
|
||||||
"
|
|
||||||
:class="`${
|
|
||||||
result.basic[num].question?.poprawna_odp.toLowerCase() ==
|
|
||||||
result.basic[num].chosen_answer
|
|
||||||
? 'btn-success'
|
|
||||||
: 'btn-error'
|
|
||||||
}`"
|
|
||||||
:checked="isBasic ? countBasic == num : false"
|
|
||||||
:key="`choose-${num}-basic`"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-[repeat(auto-fit,50px)] gap-2 justify-around w-full"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
:aria-label="`${num + 1}`"
|
|
||||||
class="btn btn-md"
|
|
||||||
name="chooser"
|
|
||||||
v-for="num in range(0, 12)"
|
|
||||||
@click="
|
|
||||||
$emit('change-now', 'advanced');
|
|
||||||
$emit('change-count', num);
|
|
||||||
"
|
|
||||||
:class="`${
|
|
||||||
result.advanced[num].question?.poprawna_odp.toLowerCase() ==
|
|
||||||
result.advanced[num].chosen_answer
|
|
||||||
? 'btn-success'
|
|
||||||
: 'btn-error'
|
|
||||||
}`"
|
|
||||||
:checked="isAdvanced ? countAdvanced == num : false"
|
|
||||||
:key="`choose-${num}-advanced`"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="text-center text-4xl flex flex-col gap-5">
|
|
||||||
<span class="block">Poprawna odpowiedź</span>
|
|
||||||
<span class="block">{{ question?.poprawna_odp }}</span>
|
|
||||||
<span class="block">Zaznaczona odpowiedź</span>
|
|
||||||
<span class="block">
|
|
||||||
{{
|
|
||||||
isBasic
|
|
||||||
? result.basic[countBasic].chosen_answer.trim() == ""
|
|
||||||
? "BrakBasic"
|
|
||||||
: result.basic[countBasic].chosen_answer
|
|
||||||
: isAdvanced
|
|
||||||
? result.advanced[countAdvanced].chosen_answer.trim() == ""
|
|
||||||
? "BrakAdvanced"
|
|
||||||
: result.advanced[countAdvanced].chosen_answer
|
|
||||||
: "Błąd"
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1"></div>
|
|
||||||
<button @click="$emit('nav', '/exam')" class="btn btn-warning btn-xl">
|
|
||||||
Rozpocznij jeszcze raz
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -1,19 +1,20 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Duration } from "date-fns";
|
import type { Duration } from 'date-fns';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
points: number | undefined;
|
points: number | null | undefined;
|
||||||
category: string | undefined;
|
category: string | undefined;
|
||||||
timeRemaining?: Duration | undefined;
|
timeRemaining?: Duration | undefined;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const timeRemainingFriendly = computed(() => {
|
const timeRemainingFriendly = computed(() => {
|
||||||
if (typeof props.timeRemaining !== "undefined") {
|
if (typeof props.timeRemaining !== 'undefined') {
|
||||||
let seconds = props.timeRemaining.seconds ?? 0;
|
const seconds = props.timeRemaining.seconds ?? 0;
|
||||||
return `${props.timeRemaining.minutes ?? 0}:${
|
return `${props.timeRemaining.minutes ?? 0}:${
|
||||||
seconds >= 0 && seconds < 10 ? 0 : ""
|
seconds >= 0 && seconds < 10 ? 0 : ''
|
||||||
}${seconds ?? 0}`;
|
}${seconds ?? 0}`;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -29,7 +30,9 @@ const timeRemainingFriendly = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="block">Aktualna kategoria</span>
|
<span class="block">Aktualna kategoria</span>
|
||||||
<div class="info-little-box">{{ category }}</div>
|
<div class="info-little-box">
|
||||||
|
{{ category }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="typeof timeRemaining !== 'undefined'">
|
<div v-if="typeof timeRemaining !== 'undefined'">
|
||||||
<span class="block">Czas do końca egzaminu</span>
|
<span class="block">Czas do końca egzaminu</span>
|
|
@ -1,45 +1,41 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
result: ResultEndType;
|
|
||||||
countBasic: number;
|
countBasic: number;
|
||||||
countAdvanced: number;
|
countAdvanced: number;
|
||||||
dataBasic: BasicQuestion[] | null;
|
|
||||||
dataAdvanced: AdvancedQuestion[] | null;
|
|
||||||
now: string | null | undefined;
|
now: string | null | undefined;
|
||||||
ending: boolean;
|
ending: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isBasic = computed(() => props.now == "basic");
|
const emit = defineEmits<{
|
||||||
const isAdvanced = computed(() => props.now == "advanced");
|
endExam: [];
|
||||||
|
nextQuestion: [];
|
||||||
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col items-stretch p-4 gap-10 border-l border-slate-300 bg-slate-100"
|
class="flex flex-col items-stretch p-4 gap-10 border-l border-slate-300 bg-slate-100"
|
||||||
>
|
>
|
||||||
<button @click="$emit('end-exam')" class="btn btn-warning btn-xl">
|
<button class="btn btn-warning btn-xl" @click="emit('endExam')">
|
||||||
Zakończ egzamin
|
Zakończ egzamin
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="flex flex-row gap-6 *:flex-1 w-full">
|
<div class="flex flex-row gap-6 *:flex-1 w-full">
|
||||||
<div class="flex flex-col gap-1" :class="isBasic ? '' : 'opacity-45'">
|
<CurrentQuestionCount
|
||||||
<span class="text-lg">Pytania podstawowe</span>
|
:class="now === 'basic' ? 'font-semibold' : 'opacity-45'"
|
||||||
<div
|
>
|
||||||
class="info-little-box w-full text-center"
|
<template #title> Pytania podstawowe </template>
|
||||||
:class="isBasic ? 'font-semibold' : ''"
|
<template #count> {{ countBasic + 1 }} / 20 </template>
|
||||||
>
|
</CurrentQuestionCount>
|
||||||
{{ countBasic + 1 }} / {{ dataBasic?.length }}
|
|
||||||
</div>
|
<CurrentQuestionCount
|
||||||
</div>
|
:class="now === 'advanced' ? 'font-semibold' : 'opacity-45'"
|
||||||
<div class="flex flex-col gap-1" :class="isAdvanced ? '' : 'opacity-45'">
|
>
|
||||||
<span class="text-lg">Pytania specjalistyczne</span>
|
<template #title> Pytania specjalistyczne </template>
|
||||||
<div
|
<template #count> {{ countAdvanced + 1 }} / 12 </template>
|
||||||
class="info-little-box w-full text-center"
|
</CurrentQuestionCount>
|
||||||
:class="isAdvanced ? 'font-semibold' : ''"
|
|
||||||
>
|
|
||||||
{{ countAdvanced + 1 }} / {{ dataAdvanced?.length }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center text-xl flex flex-col gap-2">
|
<div class="text-center text-xl flex flex-col gap-2">
|
||||||
<span>Czas na zapoznanie się z pytaniem</span>
|
<span>Czas na zapoznanie się z pytaniem</span>
|
||||||
<div class="flex flex-row items-stretch gap-2">
|
<div class="flex flex-row items-stretch gap-2">
|
||||||
|
@ -49,11 +45,12 @@ const isAdvanced = computed(() => props.now == "advanced");
|
||||||
class="progress progress-warning w-full h-full"
|
class="progress progress-warning w-full h-full"
|
||||||
value="50"
|
value="50"
|
||||||
max="100"
|
max="100"
|
||||||
></progress>
|
/>
|
||||||
<span class="block set-translate z-10 text-black text-2xl">20s</span>
|
<span class="block set-translate z-10 text-black text-2xl">20s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center text-xl flex flex-col gap-2">
|
<div class="text-center text-xl flex flex-col gap-2">
|
||||||
<span>Czas na udzielenie odpowiedzi</span>
|
<span>Czas na udzielenie odpowiedzi</span>
|
||||||
<div class="h-9 relative">
|
<div class="h-9 relative">
|
||||||
|
@ -61,18 +58,17 @@ const isAdvanced = computed(() => props.now == "advanced");
|
||||||
class="progress progress-warning w-full h-full"
|
class="progress progress-warning w-full h-full"
|
||||||
value="50"
|
value="50"
|
||||||
max="100"
|
max="100"
|
||||||
></progress>
|
/>
|
||||||
<span class="block set-translate z-10 text-black text-2xl">15s</span>
|
<span class="block set-translate z-10 text-black text-2xl">15s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-h-[150px] overflow-y-scroll">
|
|
||||||
{{ result }}
|
<div class="flex-1" />
|
||||||
</div>
|
|
||||||
<div class="flex-1"></div>
|
|
||||||
<button
|
<button
|
||||||
@click="$emit('next-question')"
|
|
||||||
class="btn btn-warning btn-xl"
|
class="btn btn-warning btn-xl"
|
||||||
:disabled="ending"
|
:disabled="ending"
|
||||||
|
@click="emit('nextQuestion')"
|
||||||
>
|
>
|
||||||
Następne pytanie
|
Następne pytanie
|
||||||
</button>
|
</button>
|
||||||
|
@ -80,7 +76,7 @@ const isAdvanced = computed(() => props.now == "advanced");
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.progressive {
|
/*.progressive {
|
||||||
animation: progressZapoznanie 20s linear;
|
animation: progressZapoznanie 20s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,5 +87,5 @@ const isAdvanced = computed(() => props.now == "advanced");
|
||||||
100% {
|
100% {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
</style>
|
</style>
|
85
components/bar/right/Result.vue
Normal file
85
components/bar/right/Result.vue
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { range } from 'lodash';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
result: ResultEndType;
|
||||||
|
countBasic: number;
|
||||||
|
countAdvanced: number;
|
||||||
|
now: string | null | undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
changeNow: [value: string];
|
||||||
|
changeCount: [num: number];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-stretch p-4 gap-6 border-l border-slate-300 bg-slate-100"
|
||||||
|
>
|
||||||
|
<NuxtLink to="/" class="btn btn-warning btn-xl">
|
||||||
|
Wróć na stronę główną
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<button class="btn btn-info btn-lg" @click="emit('changeNow', 'basic')">
|
||||||
|
Pytania podstawowe
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-[repeat(auto-fit,50px)] gap-2 justify-around w-full"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-for="num in range(0, 20)"
|
||||||
|
:key="`choose-${num}-basic`"
|
||||||
|
type="radio"
|
||||||
|
:aria-label="(num + 1).toString()"
|
||||||
|
class="btn btn-md"
|
||||||
|
name="chooser"
|
||||||
|
:class="`${
|
||||||
|
result.basic[num].question?.correct_answer ===
|
||||||
|
result.basic[num].chosen_answer
|
||||||
|
? 'btn-success'
|
||||||
|
: 'btn-error'
|
||||||
|
}`"
|
||||||
|
:checked="now === 'basic' ? countBasic === num : false"
|
||||||
|
@click="
|
||||||
|
emit('changeNow', 'basic');
|
||||||
|
emit('changeCount', num);
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-info btn-lg" @click="emit('changeNow', 'advanced')">
|
||||||
|
Pytania specjalistyczne
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-[repeat(auto-fit,50px)] gap-2 justify-around w-full"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-for="num in range(0, 12)"
|
||||||
|
:key="`choose-${num}-advanced`"
|
||||||
|
type="radio"
|
||||||
|
:aria-label="`${num + 1}`"
|
||||||
|
class="btn btn-md"
|
||||||
|
name="chooser"
|
||||||
|
:class="`${
|
||||||
|
result.advanced[num].question?.correct_answer ===
|
||||||
|
result.advanced[num].chosen_answer
|
||||||
|
? 'btn-success'
|
||||||
|
: 'btn-error'
|
||||||
|
}`"
|
||||||
|
:checked="now === 'advanced' ? countAdvanced === num : false"
|
||||||
|
@click="
|
||||||
|
emit('changeNow', 'advanced');
|
||||||
|
emit('changeCount', num);
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1" />
|
||||||
|
<NuxtLink to="/exam" class="btn btn-warning btn-xl">
|
||||||
|
Rozpocznij jeszcze raz
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
57
components/question/Advanced.vue
Normal file
57
components/question/Advanced.vue
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
question: AdvancedQuestion | undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const answer = defineModel<string | null | undefined>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-5 border-t px-4 py-5 border-slate-300 bg-slate-100"
|
||||||
|
>
|
||||||
|
<div class="text-xl">
|
||||||
|
{{ question?.text }}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div
|
||||||
|
v-for="[element, value] of Object.entries({
|
||||||
|
A: question?.answer_a,
|
||||||
|
B: question?.answer_b,
|
||||||
|
C: question?.answer_c,
|
||||||
|
})"
|
||||||
|
:key="`btn_answer_${element}_${value}`"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="`odp_${element}`"
|
||||||
|
v-model="answer"
|
||||||
|
type="radio"
|
||||||
|
name="abc"
|
||||||
|
:value="element"
|
||||||
|
class="hidden"
|
||||||
|
/>
|
||||||
|
<label :for="`odp_${element}`">
|
||||||
|
<div
|
||||||
|
:class="answer === element ? ' !btn-secondary' : ''"
|
||||||
|
class="btn btn-primary btn-lg"
|
||||||
|
>
|
||||||
|
{{ element }}
|
||||||
|
</div>
|
||||||
|
<span class="block">{{ value }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
label {
|
||||||
|
@apply cursor-pointer flex flex-row gap-2 items-center;
|
||||||
|
&:hover {
|
||||||
|
@apply bg-slate-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
@apply text-lg;
|
||||||
|
}
|
||||||
|
</style>
|
40
components/question/Basic.vue
Normal file
40
components/question/Basic.vue
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
question: BasicQuestion | undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const answer = defineModel<string | null | undefined>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-6 border-t px-4 py-5 border-slate-300 bg-slate-100"
|
||||||
|
>
|
||||||
|
<div class="text-xl">
|
||||||
|
{{ question?.text }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-row justify-around">
|
||||||
|
<input
|
||||||
|
v-for="[element, value] of Object.entries({ TAK: true, NIE: false })"
|
||||||
|
:id="`odp_${element}`"
|
||||||
|
:key="`btn_answer_${element}`"
|
||||||
|
v-model="answer"
|
||||||
|
type="radio"
|
||||||
|
name="tak_nie"
|
||||||
|
:value="value"
|
||||||
|
class="btn btn-primary btn-xl"
|
||||||
|
:aria-label="element"
|
||||||
|
:class="
|
||||||
|
answer == null
|
||||||
|
? false
|
||||||
|
: answer === value?.toString()
|
||||||
|
? '!btn-secondary'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
:checked="answer == null ? false : answer === value?.toString()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -1,11 +1,11 @@
|
||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: "postgresql",
|
dialect: 'postgresql',
|
||||||
schema: "./src/db/schema.ts",
|
schema: './src/db/schema.ts',
|
||||||
out: "./drizzle",
|
out: './drizzle',
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: process.env.DATABASE_URL!,
|
url: process.env.DATABASE_URL!,
|
||||||
}
|
},
|
||||||
});
|
});
|
8
eslint.config.mjs
Normal file
8
eslint.config.mjs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// @ts-check
|
||||||
|
import eslintConfigPrettier from 'eslint-config-prettier/flat';
|
||||||
|
import withNuxt from './.nuxt/eslint.config.mjs';
|
||||||
|
|
||||||
|
export default withNuxt(
|
||||||
|
// Your custom configs here
|
||||||
|
eslintConfigPrettier,
|
||||||
|
);
|
|
@ -1,10 +1,9 @@
|
||||||
export default defineNuxtRouteMiddleware((to, from) => {
|
export default defineNuxtRouteMiddleware(async () => {
|
||||||
const examStore = useExamStore();
|
const examStore = useExamStore();
|
||||||
|
|
||||||
if (examStore.category != "") {
|
if (examStore.category === '') {
|
||||||
examStore.mildReset();
|
|
||||||
} else {
|
|
||||||
examStore.resetExam();
|
examStore.resetExam();
|
||||||
return navigateTo("/");
|
return navigateTo('/');
|
||||||
}
|
}
|
||||||
|
examStore.mildReset();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
export default defineNuxtRouteMiddleware((to, from) => {
|
export default defineNuxtRouteMiddleware(() => {
|
||||||
const examStore = useExamStore();
|
const examStore = useExamStore();
|
||||||
|
|
||||||
if (!examStore.end) {
|
if (!examStore.end) {
|
||||||
examStore.resetExam();
|
examStore.resetExam();
|
||||||
return navigateTo("/");
|
return navigateTo('/');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
import "dotenv/config";
|
import 'dotenv/config';
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2024-11-01",
|
modules: [
|
||||||
devtools: { enabled: true },
|
'@nuxtjs/tailwindcss',
|
||||||
modules: ["@nuxtjs/tailwindcss", "@nuxt/fonts", "@pinia/nuxt"],
|
'@nuxt/fonts',
|
||||||
|
'@pinia/nuxt',
|
||||||
|
'@nuxt/eslint',
|
||||||
|
],
|
||||||
ssr: true,
|
ssr: true,
|
||||||
css: ["vue-final-modal/style.css"],
|
|
||||||
imports: {
|
imports: {
|
||||||
dirs: ["types/*.ts", "store/*.ts", "types/**/*.ts"],
|
dirs: ['types/*.ts', 'store/*.ts', 'types/**/*.ts'],
|
||||||
},
|
},
|
||||||
|
devtools: { enabled: true },
|
||||||
// Transition (later)
|
// Transition (later)
|
||||||
// app: {
|
// app: {
|
||||||
// pageTransition: { name: "page", mode: "out-in" },
|
// pageTransition: { name: "page", mode: "out-in" },
|
||||||
|
@ -20,6 +23,17 @@ export default defineNuxtConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
routeRules: {
|
routeRules: {
|
||||||
"/": { prerender: true },
|
'/': { prerender: true },
|
||||||
|
},
|
||||||
|
compatibilityDate: '2024-11-01',
|
||||||
|
eslint: {
|
||||||
|
config: {
|
||||||
|
stylistic: {
|
||||||
|
indent: 2,
|
||||||
|
semi: true,
|
||||||
|
quotes: 'single',
|
||||||
|
jsx: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
33
package.json
33
package.json
|
@ -7,30 +7,39 @@
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"pretty": "prettier --write \"./**/*.{js,mjs,ts,vue,json}\"",
|
||||||
|
"tsc": "nuxi typecheck"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/fonts": "0.11.0",
|
"@nuxt/fonts": "0.11.1",
|
||||||
"@nuxtjs/tailwindcss": "6.13.1",
|
"@nuxtjs/tailwindcss": "6.13.1",
|
||||||
"@pinia/nuxt": "0.10.1",
|
"@pinia/nuxt": "0.11.0",
|
||||||
"array-shuffle": "^3.0.0",
|
"daisyui": "^5.0.20",
|
||||||
"daisyui": "^5.0.0",
|
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.5.0",
|
||||||
"drizzle-orm": "^0.40.0",
|
"drizzle-orm": "^0.42.0",
|
||||||
|
"eslint": "^9.24.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nuxt": "~3.15.4",
|
"nuxt": "~3.15.4",
|
||||||
"pg": "^8.13.3",
|
"pg": "^8.14.1",
|
||||||
"pinia": "^3.0.1",
|
"pinia": "^3.0.2",
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
"vue-final-modal": "^4.5.5",
|
|
||||||
"vue-router": "latest"
|
"vue-router": "latest"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af",
|
"packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"array-shuffle": "^3.0.0",
|
||||||
|
"@nuxt/eslint": "1.3.0",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/pg": "^8.11.11",
|
"@types/pg": "^8.11.13",
|
||||||
"drizzle-kit": "^0.30.5",
|
"drizzle-kit": "^0.30.5",
|
||||||
"tsx": "^4.19.3"
|
"eslint-config-prettier": "^10.1.2",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"tsx": "^4.19.3",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vite-plugin-eslint2": "^5.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
189
pages/exam.vue
189
pages/exam.vue
|
@ -1,9 +1,12 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useExamStore } from "~/store/examStore";
|
import { intervalToDuration, addMinutes, addSeconds, isEqual } from 'date-fns';
|
||||||
|
import { useExamStore } from '~/store/examStore';
|
||||||
|
|
||||||
import { intervalToDuration, addMinutes, addSeconds, isEqual } from "date-fns";
|
definePageMeta({ middleware: ['exam'] });
|
||||||
|
|
||||||
definePageMeta({ middleware: ["exam"] });
|
useHead({
|
||||||
|
title: 'Pytanie 1/20',
|
||||||
|
});
|
||||||
|
|
||||||
const nowTime = ref(new Date());
|
const nowTime = ref(new Date());
|
||||||
const timeEnd = addMinutes(new Date(), 25);
|
const timeEnd = addMinutes(new Date(), 25);
|
||||||
|
@ -12,7 +15,7 @@ const timeRemainingTotal = computed(() =>
|
||||||
intervalToDuration({
|
intervalToDuration({
|
||||||
start: nowTime.value,
|
start: nowTime.value,
|
||||||
end: timeEnd,
|
end: timeEnd,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
// const timeRemainingQuestion - to implement
|
// const timeRemainingQuestion - to implement
|
||||||
|
|
||||||
|
@ -28,10 +31,6 @@ onMounted(() => {
|
||||||
|
|
||||||
const examStore = useExamStore();
|
const examStore = useExamStore();
|
||||||
|
|
||||||
useHead({
|
|
||||||
title: "Pytanie 1/20",
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: dataBasic,
|
data: dataBasic,
|
||||||
error: errorBasic,
|
error: errorBasic,
|
||||||
|
@ -55,147 +54,113 @@ const {
|
||||||
const countBasic = ref(0);
|
const countBasic = ref(0);
|
||||||
const countAdvanced = ref(-1);
|
const countAdvanced = ref(-1);
|
||||||
|
|
||||||
const now = ref("basic");
|
const now = ref('basic');
|
||||||
|
const answer = ref<string>('');
|
||||||
const tak_nie_model = ref();
|
|
||||||
const abc_model = ref();
|
|
||||||
|
|
||||||
const ending = ref(false);
|
const ending = ref(false);
|
||||||
|
|
||||||
async function next() {
|
|
||||||
if (countBasic.value + 1 < dataBasic.value?.length!) {
|
|
||||||
result.value.basic.push({
|
|
||||||
question: questionBasic.value,
|
|
||||||
chosen_answer: tak_nie_model.value ?? "",
|
|
||||||
chosen_is_correct:
|
|
||||||
tak_nie_model.value == questionBasic.value?.poprawna_odp?.toLowerCase(),
|
|
||||||
liczba_pkt: questionBasic.value?.liczba_pkt,
|
|
||||||
});
|
|
||||||
tak_nie_model.value = "";
|
|
||||||
countBasic.value++;
|
|
||||||
useHead({
|
|
||||||
title: `Pytanie ${countBasic.value + 1}/${dataBasic.value?.length}`,
|
|
||||||
});
|
|
||||||
} else if (countAdvanced.value + 1 <= dataAdvanced.value?.length!) {
|
|
||||||
if (countAdvanced.value != -1) {
|
|
||||||
result.value.advanced.push({
|
|
||||||
question: questionAdvanced.value,
|
|
||||||
chosen_answer: abc_model.value ?? "",
|
|
||||||
chosen_is_correct:
|
|
||||||
abc_model.value ==
|
|
||||||
questionAdvanced.value?.poprawna_odp?.toLowerCase(),
|
|
||||||
liczba_pkt: questionAdvanced.value?.liczba_pkt,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
now.value = "advanced";
|
|
||||||
|
|
||||||
result.value.basic.push({
|
|
||||||
question: questionBasic.value,
|
|
||||||
chosen_answer: tak_nie_model.value ?? "",
|
|
||||||
chosen_is_correct:
|
|
||||||
tak_nie_model.value ==
|
|
||||||
questionBasic.value?.poprawna_odp?.toLowerCase(),
|
|
||||||
liczba_pkt: questionBasic.value?.liczba_pkt,
|
|
||||||
});
|
|
||||||
tak_nie_model.value = "";
|
|
||||||
}
|
|
||||||
if (countAdvanced.value + 1 < dataAdvanced.value?.length!) {
|
|
||||||
countAdvanced.value++;
|
|
||||||
useHead({
|
|
||||||
title: `Pytanie ${countAdvanced.value + 1}/${
|
|
||||||
dataAdvanced.value?.length
|
|
||||||
}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (countAdvanced.value == dataAdvanced.value?.length! - 1) {
|
|
||||||
ending.value = true;
|
|
||||||
}
|
|
||||||
abc_model.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function endExam() {
|
|
||||||
loading.value = true;
|
|
||||||
while (!ending.value) {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
examStore.setResult(result.value);
|
|
||||||
examStore.setEnd(true);
|
|
||||||
while (true) {
|
|
||||||
if (examStore.result == result.value && examStore.end) {
|
|
||||||
return navigateTo("/result", { replace: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const questionBasic = computed<BasicQuestion | undefined>(() =>
|
const questionBasic = computed<BasicQuestion | undefined>(() =>
|
||||||
dataBasic.value?.at(countBasic.value)
|
dataBasic.value?.at(countBasic.value),
|
||||||
);
|
);
|
||||||
const questionAdvanced = computed<AdvancedQuestion | undefined>(() =>
|
const questionAdvanced = computed<AdvancedQuestion | undefined>(() =>
|
||||||
dataAdvanced.value?.at(countAdvanced.value)
|
dataAdvanced.value?.at(countAdvanced.value),
|
||||||
);
|
);
|
||||||
|
|
||||||
const question = computed(() => {
|
const question = computed(() => {
|
||||||
if (now.value == "basic") {
|
if (now.value === 'basic') return questionBasic.value;
|
||||||
return questionBasic.value;
|
if (now.value === 'advanced') return questionAdvanced.value;
|
||||||
} else if (now.value == "advanced") {
|
return null;
|
||||||
return questionAdvanced.value;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const media = computed(() => {
|
|
||||||
const mediaSplit = question.value?.media?.split(".");
|
|
||||||
return {
|
|
||||||
fileType: mediaSplit?.pop()?.toLowerCase(),
|
|
||||||
fileName: mediaSplit?.join("."),
|
|
||||||
ogName: question.value?.media,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: Ref<ResultEndType> = ref({
|
const result: Ref<ResultEndType> = ref({
|
||||||
basic: [],
|
basic: [],
|
||||||
advanced: [],
|
advanced: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function next() {
|
||||||
|
function pushVal() {
|
||||||
|
if (now.value === 'basic' || now.value === 'advanced') {
|
||||||
|
result.value[now.value].push({
|
||||||
|
question: question.value,
|
||||||
|
chosen_answer: answer.value,
|
||||||
|
chosen_is_correct: answer.value === question.value?.correct_answer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
answer.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now.value === 'basic') {
|
||||||
|
pushVal();
|
||||||
|
countBasic.value++;
|
||||||
|
useHead({
|
||||||
|
title: `Pytanie ${countBasic.value + 1}/20`,
|
||||||
|
});
|
||||||
|
if (countBasic.value >= 20) {
|
||||||
|
now.value = 'advanced';
|
||||||
|
countBasic.value--;
|
||||||
|
countAdvanced.value++;
|
||||||
|
}
|
||||||
|
} else if (now.value === 'advanced') {
|
||||||
|
pushVal();
|
||||||
|
countAdvanced.value++;
|
||||||
|
useHead({
|
||||||
|
title: `Pytanie ${countAdvanced.value + 1}/12`,
|
||||||
|
});
|
||||||
|
if (countAdvanced.value >= 11) {
|
||||||
|
ending.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function endExam() {
|
||||||
|
loading.value = true;
|
||||||
|
do {
|
||||||
|
next();
|
||||||
|
} while (!ending.value);
|
||||||
|
examStore.setResult(result.value);
|
||||||
|
examStore.setEnd(true);
|
||||||
|
while (true) {
|
||||||
|
if (examStore.result == result.value && examStore.end) {
|
||||||
|
return navigateTo('/result', { replace: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<!-- as in to transition to the next page -->
|
||||||
<Loading v-if="loading" />
|
<Loading v-if="loading" />
|
||||||
<div v-if="statusBasic === 'success' && statusAdvanced === 'success'">
|
<div v-if="statusBasic === 'success' && statusAdvanced === 'success'">
|
||||||
<div class="grid grid-cols-4 min-h-dvh">
|
<div class="grid grid-cols-4 min-h-dvh">
|
||||||
<div class="col-span-3 flex flex-col gap-4">
|
<div class="col-span-3 flex flex-col gap-4">
|
||||||
<TopBar
|
<BarTop
|
||||||
:points="question?.liczba_pkt"
|
:points="question?.weight"
|
||||||
:category="examStore.category"
|
:category="examStore.category"
|
||||||
:time-remaining="timeRemainingTotal"
|
:time-remaining="timeRemainingTotal"
|
||||||
/>
|
/>
|
||||||
<Media :media="media" />
|
<Media :media="question?.media_url" />
|
||||||
<BasicQuestionBlock
|
<QuestionBasic
|
||||||
v-if="countAdvanced < 0"
|
v-if="now === 'basic'"
|
||||||
|
v-model="answer"
|
||||||
:question="questionBasic"
|
:question="questionBasic"
|
||||||
v-model="tak_nie_model"
|
|
||||||
/>
|
/>
|
||||||
<AdvancedQuestionBlock
|
<QuestionAdvanced
|
||||||
v-else
|
v-else-if="now === 'advanced'"
|
||||||
|
v-model="answer"
|
||||||
:question="questionAdvanced"
|
:question="questionAdvanced"
|
||||||
v-model="abc_model"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RightBarExam
|
<BarRightExam
|
||||||
:result="result"
|
|
||||||
:data-basic="dataBasic"
|
|
||||||
:data-advanced="dataAdvanced"
|
|
||||||
:count-basic="countBasic"
|
:count-basic="countBasic"
|
||||||
:count-advanced="countAdvanced"
|
:count-advanced="countAdvanced"
|
||||||
@next-question="next()"
|
|
||||||
@end-exam="endExam()"
|
|
||||||
:now="now"
|
:now="now"
|
||||||
:ending="ending"
|
:ending="ending"
|
||||||
|
@next-question="next()"
|
||||||
|
@end-exam="endExam()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
useHead({
|
useHead({
|
||||||
title: "Test na prawo jazdy",
|
title: 'Test na prawo jazdy',
|
||||||
});
|
});
|
||||||
|
|
||||||
const categories = [
|
const categories = [
|
||||||
"A",
|
'A',
|
||||||
"B",
|
'B',
|
||||||
"C",
|
'C',
|
||||||
"D",
|
'D',
|
||||||
"T",
|
'T',
|
||||||
"AM",
|
'AM',
|
||||||
"A1",
|
'A1',
|
||||||
"A2",
|
'A2',
|
||||||
"B1",
|
'B1',
|
||||||
"C1",
|
'C1',
|
||||||
"D1",
|
'D1',
|
||||||
"PT",
|
'PT',
|
||||||
];
|
];
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
@ -26,8 +26,8 @@ function setAndGo(category: string) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
examStore.setCategory(category);
|
examStore.setCategory(category);
|
||||||
while (true) {
|
while (true) {
|
||||||
if (examStore.category == category) {
|
if (examStore.category === category) {
|
||||||
return navigateTo("/exam");
|
return navigateTo('/exam');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,8 +44,9 @@ function setAndGo(category: string) {
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-row flex-wrap gap-2">
|
<div class="flex flex-row flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-xl btn-secondary"
|
|
||||||
v-for="category in categories"
|
v-for="category in categories"
|
||||||
|
:key="`btn-${category}`"
|
||||||
|
class="btn btn-xl btn-secondary"
|
||||||
@click="setAndGo(category)"
|
@click="setAndGo(category)"
|
||||||
>
|
>
|
||||||
{{ category }}
|
{{ category }}
|
||||||
|
|
109
pages/result.vue
109
pages/result.vue
|
@ -1,8 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ModalsContainer, useModal } from "vue-final-modal";
|
import ResultModal from '~/components/ResultModal.vue';
|
||||||
import ResultModal from "~/components/ResultModal.vue";
|
|
||||||
|
|
||||||
definePageMeta({ middleware: ["result"] });
|
definePageMeta({ middleware: ['result'] });
|
||||||
|
|
||||||
const examStore = useExamStore();
|
const examStore = useExamStore();
|
||||||
|
|
||||||
|
@ -10,16 +9,16 @@ const points = ref<number>(0);
|
||||||
|
|
||||||
examStore.result.basic.forEach((answer) => {
|
examStore.result.basic.forEach((answer) => {
|
||||||
if (answer.chosen_is_correct) {
|
if (answer.chosen_is_correct) {
|
||||||
points.value += answer.question?.liczba_pkt ?? 0;
|
points.value += answer.question?.weight ?? 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
examStore.result.advanced.forEach((answer) => {
|
examStore.result.advanced.forEach((answer) => {
|
||||||
if (answer.chosen_is_correct) {
|
if (answer.chosen_is_correct) {
|
||||||
points.value += answer.question?.liczba_pkt ?? 0;
|
points.value += answer.question?.weight ?? 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const resultTrueFalse = ref(points.value >= 68 ? "pozytywny" : "negatywny");
|
const resultTrueFalse = ref(points.value >= 68 ? 'pozytywny' : 'negatywny');
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: `${
|
title: `${
|
||||||
|
@ -32,122 +31,90 @@ const countBasic = ref(0);
|
||||||
const countAdvanced = ref(0);
|
const countAdvanced = ref(0);
|
||||||
|
|
||||||
const resultQuestionBasic = computed<ResultType<BasicQuestion> | undefined>(
|
const resultQuestionBasic = computed<ResultType<BasicQuestion> | undefined>(
|
||||||
() => examStore.result.basic.at(countBasic.value)
|
() => examStore.result.basic.at(countBasic.value),
|
||||||
);
|
);
|
||||||
const resultQuestionAdvanced = computed<
|
const resultQuestionAdvanced = computed<
|
||||||
ResultType<AdvancedQuestion> | undefined
|
ResultType<AdvancedQuestion> | undefined
|
||||||
>(() => examStore.result.advanced.at(countAdvanced.value));
|
>(() => examStore.result.advanced.at(countAdvanced.value));
|
||||||
|
|
||||||
const questionBasic = computed<BasicQuestion | undefined>(
|
const questionBasic = computed<BasicQuestion | undefined>(
|
||||||
() => resultQuestionBasic.value?.question
|
() => resultQuestionBasic.value?.question,
|
||||||
);
|
);
|
||||||
const questionAdvanced = computed<AdvancedQuestion | undefined>(
|
const questionAdvanced = computed<AdvancedQuestion | undefined>(
|
||||||
() => resultQuestionAdvanced.value?.question
|
() => resultQuestionAdvanced.value?.question,
|
||||||
);
|
);
|
||||||
|
|
||||||
const now = ref("basic");
|
const now = ref('basic');
|
||||||
|
|
||||||
const question = computed(() => {
|
const question = computed(() => {
|
||||||
if (now.value == "basic") {
|
if (now.value === 'basic') {
|
||||||
return questionBasic.value;
|
return questionBasic.value;
|
||||||
} else if (now.value == "advanced") {
|
} else if (now.value === 'advanced') {
|
||||||
return questionAdvanced.value;
|
return questionAdvanced.value;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const tak_nie_model = computed(() =>
|
const resultQuestion = computed(() => {
|
||||||
resultQuestionBasic.value?.chosen_answer.toLowerCase()
|
if (now.value === 'basic') {
|
||||||
);
|
return resultQuestionBasic.value;
|
||||||
const abc_model = computed(() =>
|
} else if (now.value === 'advanced') {
|
||||||
resultQuestionAdvanced.value?.chosen_answer.toLowerCase()
|
return resultQuestionAdvanced.value;
|
||||||
);
|
} else {
|
||||||
|
return null;
|
||||||
const media = computed(() => {
|
}
|
||||||
const mediaSplit = question.value?.media?.split(".");
|
|
||||||
return {
|
|
||||||
fileType: mediaSplit?.pop()?.toLowerCase(),
|
|
||||||
fileName: mediaSplit?.join("."),
|
|
||||||
ogName: question.value?.media,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { open, close } = useModal({
|
const answer = computed(() => resultQuestion.value?.chosen_answer);
|
||||||
component: ResultModal,
|
|
||||||
attrs: {
|
|
||||||
title: "Egzamin teorytyczny",
|
|
||||||
onClose() {
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
onHomepage() {
|
|
||||||
return navigateTo("/");
|
|
||||||
},
|
|
||||||
onNewExam() {
|
|
||||||
return navigateTo("/exam");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
slots: {
|
|
||||||
category: examStore.category,
|
|
||||||
points: `${points.value}`,
|
|
||||||
resultTrueFalse: resultTrueFalse,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
open();
|
|
||||||
|
|
||||||
function changeNow(to: string) {
|
function changeNow(to: string) {
|
||||||
now.value = to;
|
now.value = to;
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeCount(num: number) {
|
function changeCount(num: number) {
|
||||||
if (now.value == "basic") {
|
if (now.value === 'basic') {
|
||||||
countBasic.value = num;
|
countBasic.value = num;
|
||||||
} else if (now.value == "advanced") {
|
} else if (now.value === 'advanced') {
|
||||||
countAdvanced.value = num;
|
countAdvanced.value = num;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function nav(route: string) {
|
|
||||||
return navigateTo(route);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ModalsContainer />
|
<ResultModal>
|
||||||
|
<template #title>Egzamin teorytyczny</template>
|
||||||
|
<template #category>{{ examStore.category }}</template>
|
||||||
|
<template #points>{{ points }}</template>
|
||||||
|
<template #resultTrueFalse>{{ resultTrueFalse }}</template>
|
||||||
|
</ResultModal>
|
||||||
<div>
|
<div>
|
||||||
<div class="grid grid-cols-4 min-h-dvh">
|
<div class="grid grid-cols-4 min-h-dvh">
|
||||||
<div class="col-span-3 flex flex-col gap-4">
|
<div class="col-span-3 flex flex-col gap-4">
|
||||||
<TopBar
|
<BarTop :points="question?.weight" :category="examStore.category" />
|
||||||
:points="question?.liczba_pkt"
|
<Media :media="question?.media_url" />
|
||||||
:category="examStore.category"
|
<QuestionBasic
|
||||||
/>
|
v-if="now === 'basic'"
|
||||||
<Media :media="media" />
|
v-model="answer"
|
||||||
<BasicQuestionBlock
|
|
||||||
v-if="now == 'basic'"
|
|
||||||
:question="questionBasic"
|
:question="questionBasic"
|
||||||
v-model="tak_nie_model"
|
|
||||||
class="select-none z-[-1]"
|
class="select-none z-[-1]"
|
||||||
/>
|
/>
|
||||||
<AdvancedQuestionBlock
|
<QuestionAdvanced
|
||||||
v-else-if="now == 'advanced'"
|
v-else-if="now === 'advanced'"
|
||||||
|
v-model="answer"
|
||||||
:question="questionAdvanced"
|
:question="questionAdvanced"
|
||||||
v-model="abc_model"
|
|
||||||
class="select-none z-[-1]"
|
class="select-none z-[-1]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RightBarResult
|
<BarRightResult
|
||||||
:result="examStore.result"
|
:result="examStore.result"
|
||||||
:question="question"
|
|
||||||
:question-basic="questionBasic"
|
|
||||||
:question-advanced="questionAdvanced"
|
|
||||||
:count-basic="countBasic"
|
:count-basic="countBasic"
|
||||||
:count-advanced="countAdvanced"
|
:count-advanced="countAdvanced"
|
||||||
:now="now"
|
:now="now"
|
||||||
@change-now="changeNow"
|
@change-now="changeNow"
|
||||||
@change-count="changeCount"
|
@change-count="changeCount"
|
||||||
@nav="nav"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { createVfm } from "vue-final-modal";
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
|
||||||
const vfm = createVfm() as any;
|
|
||||||
|
|
||||||
nuxtApp.vueApp.use(vfm);
|
|
||||||
});
|
|
1981
pnpm-lock.yaml
generated
1981
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
12
prettier.config.mjs
Normal file
12
prettier.config.mjs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* @see https://prettier.io/docs/configuration
|
||||||
|
* @type {import("prettier").Config}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
trailingComma: 'all',
|
||||||
|
tabWidth: 2,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
|
@ -1,111 +1,79 @@
|
||||||
import "dotenv/config";
|
import 'dotenv/config';
|
||||||
import { drizzle } from "drizzle-orm/node-postgres";
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
import { dane, punkty } from "@/src/db/schema";
|
import { sql, eq, and } from 'drizzle-orm';
|
||||||
import { sql, eq, and, or, isNotNull } from "drizzle-orm";
|
import arrayShuffle from 'array-shuffle';
|
||||||
import { AdvancedQuestion } from "~/types";
|
import {
|
||||||
import arrayShuffle from "array-shuffle";
|
tasks_advanced,
|
||||||
|
questions_advanced,
|
||||||
|
categories_db,
|
||||||
|
} from '@/src/db/schema';
|
||||||
|
import type { AdvancedQuestion } from '~/types';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
async function getFromDb(points: number | string) {
|
|
||||||
return await db
|
|
||||||
.select({
|
|
||||||
id: dane.id,
|
|
||||||
nr_pytania: dane.nr_pytania,
|
|
||||||
pytanie: dane.pytanie,
|
|
||||||
poprawna_odp: dane.poprawna_odp,
|
|
||||||
media: dane.media,
|
|
||||||
kategorie: dane.kategorie,
|
|
||||||
nazwa_media_pjm_tresc_pyt: dane.nazwa_media_pjm_tresc_pyt,
|
|
||||||
pytanie_eng: dane.pytanie_eng,
|
|
||||||
pytanie_de: dane.pytanie_de,
|
|
||||||
pytanie_ua: dane.pytanie_ua,
|
|
||||||
liczba_pkt: punkty.liczba_pkt,
|
|
||||||
|
|
||||||
odp_a: dane.odp_a,
|
|
||||||
odp_b: dane.odp_b,
|
|
||||||
odp_c: dane.odp_c,
|
|
||||||
nazwa_media_pjm_tresc_odp_a: dane.nazwa_media_pjm_tresc_odp_a,
|
|
||||||
nazwa_media_pjm_tresc_odp_b: dane.nazwa_media_pjm_tresc_odp_b,
|
|
||||||
nazwa_media_pjm_tresc_odp_c: dane.nazwa_media_pjm_tresc_odp_c,
|
|
||||||
odp_a_eng: dane.odp_a_eng,
|
|
||||||
odp_b_eng: dane.odp_b_eng,
|
|
||||||
odp_c_eng: dane.odp_c_eng,
|
|
||||||
odp_a_de: dane.odp_a_de,
|
|
||||||
odp_b_de: dane.odp_b_de,
|
|
||||||
odp_c_de: dane.odp_c_de,
|
|
||||||
odp_a_ua: dane.odp_a_ua,
|
|
||||||
odp_b_ua: dane.odp_b_ua,
|
|
||||||
odp_c_ua: dane.odp_c_ua,
|
|
||||||
})
|
|
||||||
.from(dane)
|
|
||||||
.innerJoin(punkty, eq(dane.nr_pytania, punkty.nr_pytania))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
isNotNull(dane.odp_a),
|
|
||||||
isNotNull(dane.odp_b),
|
|
||||||
isNotNull(dane.odp_c),
|
|
||||||
isNotNull(dane.odp_a_eng),
|
|
||||||
isNotNull(dane.odp_b_eng),
|
|
||||||
isNotNull(dane.odp_c_eng),
|
|
||||||
isNotNull(dane.odp_a_de),
|
|
||||||
isNotNull(dane.odp_b_de),
|
|
||||||
isNotNull(dane.odp_c_de),
|
|
||||||
eq(punkty.liczba_pkt, +points)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const query = getQuery(event);
|
const query = getQuery(event);
|
||||||
const category = query.category;
|
const category = query.category;
|
||||||
const categories = [
|
const categories = [
|
||||||
"A",
|
'A',
|
||||||
"B",
|
'B',
|
||||||
"C",
|
'C',
|
||||||
"D",
|
'D',
|
||||||
"T",
|
'T',
|
||||||
"AM",
|
'AM',
|
||||||
"A1",
|
'A1',
|
||||||
"A2",
|
'A2',
|
||||||
"B1",
|
'B1',
|
||||||
"C1",
|
'C1',
|
||||||
"D1",
|
'D1',
|
||||||
"PT",
|
'PT',
|
||||||
];
|
];
|
||||||
if (category == null || category == "") {
|
if (category === '' || typeof category !== 'string') {
|
||||||
throw new Error(
|
throw createError({
|
||||||
"category argument has to be string (or not to be defined at all)"
|
statusCode: 400,
|
||||||
);
|
statusMessage:
|
||||||
|
'category argument has to be string (or not to be defined at all)',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!categories.includes(`${category}`)) {
|
if (!categories.includes(`${category.toUpperCase()}`)) {
|
||||||
throw new Error(`category argument has to be equal to: ${categories}`);
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: `category argument has to be equal to: ${categories}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFromDb(points: number, limit: number, category: string) {
|
||||||
|
return await db
|
||||||
|
.select({
|
||||||
|
id: tasks_advanced.id,
|
||||||
|
correct_answer: tasks_advanced.correct_answer,
|
||||||
|
media_url: tasks_advanced.media_url,
|
||||||
|
weight: tasks_advanced.weight,
|
||||||
|
text: questions_advanced.text,
|
||||||
|
answer_a: questions_advanced.answer_a,
|
||||||
|
answer_b: questions_advanced.answer_b,
|
||||||
|
answer_c: questions_advanced.answer_c,
|
||||||
|
})
|
||||||
|
.from(tasks_advanced)
|
||||||
|
.leftJoin(
|
||||||
|
questions_advanced,
|
||||||
|
eq(questions_advanced.task_id, tasks_advanced.id),
|
||||||
|
)
|
||||||
|
.leftJoin(categories_db, eq(categories_db.task_id, tasks_advanced.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(categories_db.name, category.toUpperCase()),
|
||||||
|
eq(questions_advanced.lang, 'PL'),
|
||||||
|
eq(tasks_advanced.weight, points),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(sql`RANDOM()`)
|
||||||
|
.limit(limit);
|
||||||
}
|
}
|
||||||
const db = drizzle(process.env.DATABASE_URL!);
|
const db = drizzle(process.env.DATABASE_URL!);
|
||||||
|
|
||||||
const randomizedQuestions: AdvancedQuestion[] = [];
|
const randomizedQuestions: AdvancedQuestion[] = [];
|
||||||
|
|
||||||
for (let [key, value] of Object.entries({ 1: 2, 2: 4, 3: 6 })) {
|
for (const [key, value] of Object.entries({ 1: 2, 2: 4, 3: 6 })) {
|
||||||
const questionsKeyPoints: AdvancedQuestion[] = await getFromDb(key);
|
randomizedQuestions.push(...(await getFromDb(+key, value, category)));
|
||||||
const chosenRandomQuestions: AdvancedQuestion[] = [];
|
|
||||||
|
|
||||||
const randoms: Array<number> = [];
|
|
||||||
for (let j = 0; j < value; j++) {
|
|
||||||
let randomized =
|
|
||||||
Math.floor(Math.random() * (questionsKeyPoints.length - 1 + 1)) + 0;
|
|
||||||
while (randoms.includes(randomized)) {
|
|
||||||
randomized =
|
|
||||||
Math.floor(Math.random() * (questionsKeyPoints.length - 1 + 1)) + 0;
|
|
||||||
}
|
|
||||||
randoms.push(randomized);
|
|
||||||
if (
|
|
||||||
questionsKeyPoints[randomized].kategorie
|
|
||||||
.split(",")
|
|
||||||
.includes(`${category}`)
|
|
||||||
) {
|
|
||||||
chosenRandomQuestions.push(questionsKeyPoints[randomized]);
|
|
||||||
} else {
|
|
||||||
j--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chosenRandomQuestions.forEach((q) => randomizedQuestions.push(q));
|
|
||||||
}
|
}
|
||||||
return arrayShuffle(randomizedQuestions);
|
return arrayShuffle(randomizedQuestions);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,90 +1,70 @@
|
||||||
import "dotenv/config";
|
import 'dotenv/config';
|
||||||
import { drizzle } from "drizzle-orm/node-postgres";
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
import { dane, punkty } from "@/src/db/schema";
|
import { sql, eq, and } from 'drizzle-orm';
|
||||||
import { sql, eq, and, or } from "drizzle-orm";
|
import arrayShuffle from 'array-shuffle';
|
||||||
import { BasicQuestion } from "~/types";
|
import { tasks, questions, categories_db } from '@/src/db/schema';
|
||||||
import arrayShuffle from "array-shuffle";
|
import type { BasicQuestion } from '~/types';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
async function getFromDb(points: number | string) {
|
|
||||||
return await db
|
|
||||||
.select({
|
|
||||||
id: dane.id,
|
|
||||||
nr_pytania: dane.nr_pytania,
|
|
||||||
pytanie: dane.pytanie,
|
|
||||||
poprawna_odp: dane.poprawna_odp,
|
|
||||||
media: dane.media,
|
|
||||||
kategorie: dane.kategorie,
|
|
||||||
nazwa_media_pjm_tresc_pyt: dane.nazwa_media_pjm_tresc_pyt,
|
|
||||||
pytanie_eng: dane.pytanie_eng,
|
|
||||||
pytanie_de: dane.pytanie_de,
|
|
||||||
pytanie_ua: dane.pytanie_ua,
|
|
||||||
liczba_pkt: punkty.liczba_pkt,
|
|
||||||
})
|
|
||||||
.from(dane)
|
|
||||||
.innerJoin(punkty, eq(dane.nr_pytania, punkty.nr_pytania))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(
|
|
||||||
sql`lower(${dane.poprawna_odp})='tak'`,
|
|
||||||
sql`lower(${dane.poprawna_odp})='nie'`
|
|
||||||
),
|
|
||||||
eq(punkty.liczba_pkt, +points)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const query = getQuery(event);
|
const query = getQuery(event);
|
||||||
const category = query.category;
|
const category = query.category;
|
||||||
const categories = [
|
const categories = [
|
||||||
"A",
|
'A',
|
||||||
"B",
|
'B',
|
||||||
"C",
|
'C',
|
||||||
"D",
|
'D',
|
||||||
"T",
|
'T',
|
||||||
"AM",
|
'AM',
|
||||||
"A1",
|
'A1',
|
||||||
"A2",
|
'A2',
|
||||||
"B1",
|
'B1',
|
||||||
"C1",
|
'C1',
|
||||||
"D1",
|
'D1',
|
||||||
"PT",
|
'PT',
|
||||||
];
|
];
|
||||||
if (category == null || category == "") {
|
|
||||||
throw new Error(
|
if (category === '' || typeof category !== 'string') {
|
||||||
"category argument has to be string (or not to be defined at all)"
|
throw createError({
|
||||||
);
|
statusCode: 400,
|
||||||
|
statusMessage:
|
||||||
|
'category argument has to be string (or not to be defined at all)',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!categories.includes(`${category}`)) {
|
if (!categories.includes(`${category.toUpperCase()}`)) {
|
||||||
throw new Error(`category argument has to be equal to: ${categories}`);
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: `category argument has to be equal to: ${categories}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFromDb(points: number, limit: number, category: string) {
|
||||||
|
return await db
|
||||||
|
.select({
|
||||||
|
id: tasks.id,
|
||||||
|
correct_answer: tasks.correct_answer,
|
||||||
|
media_url: tasks.media_url,
|
||||||
|
weight: tasks.weight,
|
||||||
|
text: questions.text,
|
||||||
|
})
|
||||||
|
.from(tasks)
|
||||||
|
.leftJoin(questions, eq(questions.task_id, tasks.id))
|
||||||
|
.leftJoin(categories_db, eq(categories_db.task_id, tasks.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(categories_db.name, category.toUpperCase()),
|
||||||
|
eq(questions.lang, 'PL'),
|
||||||
|
eq(tasks.weight, points),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(sql`RANDOM()`)
|
||||||
|
.limit(limit);
|
||||||
}
|
}
|
||||||
const db = drizzle(process.env.DATABASE_URL!);
|
const db = drizzle(process.env.DATABASE_URL!);
|
||||||
|
|
||||||
const randomizedQuestions: BasicQuestion[] = [];
|
const randomizedQuestions: BasicQuestion[] = [];
|
||||||
|
|
||||||
for (let [key, value] of Object.entries({ 1: 4, 2: 6, 3: 10 })) {
|
for (const [key, value] of Object.entries({ 1: 4, 2: 6, 3: 10 })) {
|
||||||
const questionsKeyPoints: BasicQuestion[] = await getFromDb(key);
|
randomizedQuestions.push(...(await getFromDb(+key, value, category)));
|
||||||
const chosenRandomQuestions: BasicQuestion[] = [];
|
|
||||||
|
|
||||||
const randoms: Array<number> = [];
|
|
||||||
for (let j = 0; j < value; j++) {
|
|
||||||
let randomized =
|
|
||||||
Math.floor(Math.random() * (questionsKeyPoints.length - 1 + 1)) + 0;
|
|
||||||
while (randoms.includes(randomized)) {
|
|
||||||
randomized =
|
|
||||||
Math.floor(Math.random() * (questionsKeyPoints.length - 1 + 1)) + 0;
|
|
||||||
}
|
|
||||||
randoms.push(randomized);
|
|
||||||
if (
|
|
||||||
questionsKeyPoints[randomized].kategorie
|
|
||||||
.split(",")
|
|
||||||
.includes(`${category}`)
|
|
||||||
) {
|
|
||||||
chosenRandomQuestions.push(questionsKeyPoints[randomized]);
|
|
||||||
} else {
|
|
||||||
j--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chosenRandomQuestions.forEach((q) => randomizedQuestions.push(q));
|
|
||||||
}
|
}
|
||||||
return arrayShuffle(randomizedQuestions);
|
return arrayShuffle(randomizedQuestions);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,34 +1,35 @@
|
||||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
import { integer, pgTable, text, smallint, char } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
export const dane = pgTable("dane", {
|
export const tasks = pgTable('tasks', {
|
||||||
id: integer().primaryKey().notNull(),
|
id: integer().notNull(),
|
||||||
nr_pytania: integer().notNull(),
|
correct_answer: text(),
|
||||||
pytanie: text().notNull(),
|
media_url: text(),
|
||||||
odp_a: text(),
|
weight: smallint(),
|
||||||
odp_b: text(),
|
|
||||||
odp_c: text(),
|
|
||||||
poprawna_odp: text().notNull(),
|
|
||||||
media: text(),
|
|
||||||
kategorie: text().notNull(),
|
|
||||||
nazwa_media_pjm_tresc_pyt: text(),
|
|
||||||
nazwa_media_pjm_tresc_odp_a: text(),
|
|
||||||
nazwa_media_pjm_tresc_odp_b: text(),
|
|
||||||
nazwa_media_pjm_tresc_odp_c: text(),
|
|
||||||
pytanie_eng: text().notNull(),
|
|
||||||
odp_a_eng: text(),
|
|
||||||
odp_b_eng: text(),
|
|
||||||
odp_c_eng: text(),
|
|
||||||
pytanie_de: text().notNull(),
|
|
||||||
odp_a_de: text(),
|
|
||||||
odp_b_de: text(),
|
|
||||||
odp_c_de: text(),
|
|
||||||
pytanie_ua: text(),
|
|
||||||
odp_a_ua: text(),
|
|
||||||
odp_b_ua: text(),
|
|
||||||
odp_c_ua: text(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const punkty = pgTable("punkty", {
|
export const questions = pgTable('questions', {
|
||||||
nr_pytania: integer().primaryKey().notNull(),
|
task_id: integer(),
|
||||||
liczba_pkt: integer().notNull(),
|
lang: char({ length: 2 }),
|
||||||
|
text: text(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tasks_advanced = pgTable('tasks_advanced', {
|
||||||
|
id: integer().notNull(),
|
||||||
|
correct_answer: char({ length: 1 }),
|
||||||
|
media_url: text(),
|
||||||
|
weight: smallint(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const questions_advanced = pgTable('questions_advanced', {
|
||||||
|
task_id: integer(),
|
||||||
|
lang: char({ length: 2 }),
|
||||||
|
text: text(),
|
||||||
|
answer_a: text(),
|
||||||
|
answer_b: text(),
|
||||||
|
answer_c: text(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const categories_db = pgTable('categories', {
|
||||||
|
name: text(),
|
||||||
|
task_id: integer(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
export const useExamStore = defineStore("exam-store", () => {
|
export const useExamStore = defineStore('exam-store', () => {
|
||||||
const category = ref("");
|
const category = ref('');
|
||||||
const end = ref(false);
|
const end = ref(false);
|
||||||
const result: Ref<ResultEndType> = ref({
|
const result: Ref<ResultEndType> = ref({
|
||||||
basic: [],
|
basic: [],
|
||||||
advanced: [],
|
advanced: [],
|
||||||
});
|
});
|
||||||
function resetExam() {
|
function resetExam() {
|
||||||
category.value = "";
|
category.value = '';
|
||||||
mildReset();
|
mildReset();
|
||||||
}
|
}
|
||||||
function mildReset() {
|
function mildReset() {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [require("daisyui")],
|
plugins: [require('daisyui')],
|
||||||
daisyui: {
|
daisyui: {
|
||||||
themes: ["light", "dark"],
|
themes: ['light', 'dark'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,40 +1,21 @@
|
||||||
export interface BasicQuestion {
|
export interface BasicQuestion {
|
||||||
id: number;
|
id: number | null;
|
||||||
nr_pytania: number;
|
correct_answer: string | null;
|
||||||
pytanie: string;
|
media_url: string | null;
|
||||||
poprawna_odp: string;
|
weight: number | null;
|
||||||
media: string | null;
|
text: string | null;
|
||||||
kategorie: string;
|
|
||||||
nazwa_media_pjm_tresc_pyt: string | null;
|
|
||||||
pytanie_eng: string;
|
|
||||||
pytanie_de: string;
|
|
||||||
pytanie_ua: string | null;
|
|
||||||
liczba_pkt: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdvancedQuestion extends BasicQuestion {
|
export interface AdvancedQuestion extends BasicQuestion {
|
||||||
odp_a: string;
|
answer_a: string | null;
|
||||||
odp_b: string;
|
answer_b: string | null;
|
||||||
odp_c: string;
|
answer_c: string | null;
|
||||||
nazwa_media_pjm_tresc_odp_a: string | null;
|
|
||||||
nazwa_media_pjm_tresc_odp_b: string | null;
|
|
||||||
nazwa_media_pjm_tresc_odp_c: string | null;
|
|
||||||
odp_a_eng: string;
|
|
||||||
odp_b_eng: string;
|
|
||||||
odp_c_eng: string;
|
|
||||||
odp_a_de: string;
|
|
||||||
odp_b_de: string;
|
|
||||||
odp_c_de: string;
|
|
||||||
odp_a_ua: string | null;
|
|
||||||
odp_b_ua: string | null;
|
|
||||||
odp_c_ua: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultType<T> {
|
export interface ResultType<T> {
|
||||||
question: T | undefined;
|
question: T | undefined;
|
||||||
chosen_answer: string;
|
chosen_answer: string;
|
||||||
chosen_is_correct: boolean | undefined;
|
chosen_is_correct: boolean | undefined;
|
||||||
liczba_pkt: number | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultEndType {
|
export interface ResultEndType {
|
||||||
|
|
Loading…
Add table
Reference in a new issue