Compare commits
No commits in common. "main" and "main" have entirely different histories.
40 changed files with 2894 additions and 4861 deletions
|
@ -1,2 +1 @@
|
|||
DATABASE_URL="postgres://USERNAME:PASSWORD@HOST:PORT/DATABASE"
|
||||
CDN_URL="http://DOMAIN.TLD/FOLDER"
|
26
README.md
26
README.md
|
@ -8,31 +8,7 @@ This project utilizes `pnpm`, thus it is recommended
|
|||
pnpm install
|
||||
```
|
||||
|
||||
## Required
|
||||
|
||||
The [db-prawo-jazdy](https://git.mandarynki.eu/netman/db-prawo-jazdy) project is designed for this one in mind, so use it in conjunction with this - visit it for more details
|
||||
|
||||
You also need the exam media files from the (Ministry of Infrasture)[https://www.gov.pl/web/infrastruktura/prawo-jazdy]. The newest at the moment of me writing this (19th of April 2025) are the (visualisations for questions from the 18th of January of 2024)[https://www.gov.pl/pliki/mi/wizualizacje_do_pytan_18_01_2024.zip]
|
||||
|
||||
# To-do:
|
||||
|
||||
- [x] re-forge database structure (good for now)
|
||||
- [x] choose category (good for now)
|
||||
- [x] come up with how to show results appropriately
|
||||
- [x] db: script for processing, share appropriate files
|
||||
- [x] better answer click recognition
|
||||
- [x] beautify website (good for now)
|
||||
- [ ] <b>fix pinia middleware between pages, MAJOR ISSUE - finishing exam sometimes redirects to homepage instead of results, help appreciated</b>
|
||||
- [ ] exam (& results?) warning leave message on exit and timer end (and definitely on refresh)
|
||||
- [ ] question timers
|
||||
- [ ] lazy loading
|
||||
- [ ] i18n - pl, en, de, ua (not all questions are not available in ua, api handle)
|
||||
|
||||
## Some info
|
||||
|
||||
My intention is, to share access to test exams free of charge - all data is free of charge and is already available as public information, either on the gov website, or by writing to the MI
|
||||
|
||||
This project is an SSR website mimicking an official driver's license exam (for different categories) with a seperate CDN for media, connected using an ORM to a postgres DB
|
||||
More information about setting up database will come here later
|
||||
|
||||
## Development Server
|
||||
|
||||
|
|
4
app.vue
4
app.vue
|
@ -1,5 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<NuxtPage />
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
.btn {
|
||||
height: initial !important;
|
||||
min-height: var(--size);
|
||||
}
|
||||
.set-translate {
|
||||
@apply absolute top-[50%] left-[50%];
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.info-little-box {
|
||||
@apply inline-block px-[15px] py-[8px] bg-blue-500 text-white font-bold rounded-md;
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
export default [
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'T',
|
||||
'AM',
|
||||
'A1',
|
||||
'A2',
|
||||
'B1',
|
||||
'C1',
|
||||
'D1',
|
||||
'PT',
|
||||
];
|
82
components/AdvancedQuestionBlock.vue
Normal file
82
components/AdvancedQuestionBlock.vue
Normal file
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
question: AdvancedQuestion | undefined;
|
||||
}>();
|
||||
|
||||
const abc_model = defineModel();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="text-xl">{{ question?.pytanie }}</div>
|
||||
<div>
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
type="radio"
|
||||
name="abc"
|
||||
id="odp_a"
|
||||
v-model="abc_model"
|
||||
value="a"
|
||||
class="hidden"
|
||||
/>
|
||||
<label for="odp_a">
|
||||
<div
|
||||
:class="`btn-answer ${abc_model == 'a' ? ' !bg-fuchsia-500' : ''}`"
|
||||
>
|
||||
A
|
||||
</div>
|
||||
{{ question?.odp_a }}
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="abc"
|
||||
id="odp_b"
|
||||
v-model="abc_model"
|
||||
value="b"
|
||||
class="hidden"
|
||||
/>
|
||||
<label for="odp_b">
|
||||
<div
|
||||
:class="`btn-answer ${abc_model == 'b' ? ' !bg-fuchsia-500' : ''}`"
|
||||
>
|
||||
B
|
||||
</div>
|
||||
{{ question?.odp_b }}
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="abc"
|
||||
id="odp_c"
|
||||
v-model="abc_model"
|
||||
value="c"
|
||||
class="hidden"
|
||||
/>
|
||||
<label for="odp_c">
|
||||
<div
|
||||
:class="`btn-answer ${abc_model == 'c' ? ' !bg-fuchsia-500' : ''}`"
|
||||
>
|
||||
C
|
||||
</div>
|
||||
{{ question?.odp_c }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.btn-answer {
|
||||
display: inline-block;
|
||||
}
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label:hover .btn-answer {
|
||||
@apply bg-blue-300;
|
||||
}
|
||||
|
||||
label:hover {
|
||||
@apply bg-slate-200;
|
||||
}
|
||||
</style>
|
51
components/BasicQuestionBlock.vue
Normal file
51
components/BasicQuestionBlock.vue
Normal file
|
@ -0,0 +1,51 @@
|
|||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
question: BasicQuestion | undefined;
|
||||
}>();
|
||||
|
||||
const tak_nie_model = defineModel();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="text-xl">{{ question?.pytanie }}</div>
|
||||
<div>
|
||||
<div class="flex flex-row justify-around">
|
||||
<input
|
||||
type="radio"
|
||||
name="tak_nie"
|
||||
id="odp_tak"
|
||||
v-model="tak_nie_model"
|
||||
value="tak"
|
||||
class="hidden"
|
||||
/>
|
||||
<label for="odp_tak">
|
||||
<div
|
||||
:class="`btn-answer ${
|
||||
tak_nie_model == 'tak' ? ' !bg-fuchsia-500' : ''
|
||||
}`"
|
||||
>
|
||||
TAK
|
||||
</div>
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="tak_nie"
|
||||
id="odp_nie"
|
||||
v-model="tak_nie_model"
|
||||
value="nie"
|
||||
class="hidden"
|
||||
/>
|
||||
<label for="odp_nie">
|
||||
<div
|
||||
:class="`btn-answer ${
|
||||
tak_nie_model == 'nie' ? ' !bg-fuchsia-500' : ''
|
||||
}`"
|
||||
>
|
||||
NIE
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,10 +0,0 @@
|
|||
<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>
|
|
@ -1,8 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
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 class="block">Ładowanie</span>
|
||||
</div>
|
||||
</template>
|
31
components/Media.vue
Normal file
31
components/Media.vue
Normal file
|
@ -0,0 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const cdnUrl = runtimeConfig.public.cdn_url;
|
||||
|
||||
defineProps<{
|
||||
media: {
|
||||
fileName: string | undefined;
|
||||
fileType: string | undefined;
|
||||
ogName: string | null | undefined;
|
||||
};
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="select-none z-[-1] flex-1 flex items-stretch w-full justify-center *:object-contain"
|
||||
>
|
||||
<!-- OLD -->
|
||||
<!-- + [media.fileName, media.fileType].join('.') -->
|
||||
<img :src="cdnUrl + media.ogName" alt="" v-if="media.fileType == 'jpg'" />
|
||||
<video v-else-if="media.fileType == 'wmv'" :key="media.fileName" autoplay>
|
||||
<source
|
||||
:src="cdnUrl + [media.fileName, 'mp4'].join('.')"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
<span v-else class="text-4xl font-bold flex items-center justify-center"
|
||||
>Brak mediów</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
|
@ -1,50 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { joinURL } from 'ufo';
|
||||
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const cdnUrl = runtimeConfig.public.cdn_url;
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const props = defineProps<{
|
||||
mediaPath: string | null | undefined;
|
||||
}>();
|
||||
|
||||
const media = computed(() => {
|
||||
const dotSplit = props.mediaPath?.split('.');
|
||||
const extension = dotSplit?.pop()?.toLowerCase();
|
||||
let type = null;
|
||||
if (extension === 'jpg') {
|
||||
type = 'image';
|
||||
} else if (extension === 'wmv') {
|
||||
type = 'video';
|
||||
}
|
||||
return { name: dotSplit?.join('.'), type };
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="select-none flex-auto w-full *:object-contain *:w-full *:h-full *:max-h-full relative *:absolute"
|
||||
:class="route.path === '/exam' ? 'z-[-1]' : ''"
|
||||
>
|
||||
<NuxtImg
|
||||
v-if="media.type === 'image'"
|
||||
:key="`${mediaPath}-image`"
|
||||
provider="selfhost"
|
||||
:src="'/' + mediaPath"
|
||||
:alt="mediaPath ?? ''"
|
||||
/>
|
||||
<video
|
||||
v-else-if="media.type === 'video'"
|
||||
:key="`${mediaPath}-video`"
|
||||
:autoplay="route.path === '/exam'"
|
||||
:controls="route.path === '/result'"
|
||||
>
|
||||
<source :src="joinURL(cdnUrl, media.name + '.mp4')" type="video/mp4" />
|
||||
</video>
|
||||
<span v-else class="text-5xl font-bold flex items-center justify-center"
|
||||
>Brak mediów</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
|
@ -1,32 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
const myModal = useTemplateRef('myModal');
|
||||
|
||||
onMounted(() => {
|
||||
myModal.value?.showModal();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog
|
||||
ref="myModal"
|
||||
class="flex justify-center items-center backdrop-blur-sm modal"
|
||||
>
|
||||
<div class="flex flex-col p-3 bg-base rounded-md gap-3 modal-box min-w-fit">
|
||||
<h1 class="text-[1.5rem]">
|
||||
<slot name="title" />
|
||||
</h1>
|
||||
<div class="*:inline">Kategoria: <slot name="category" /></div>
|
||||
<div class="*:inline">Punkty: <slot name="points" /> / 74</div>
|
||||
<div class="*:inline">Wynik: <slot name="resultTrueFalse" /></div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<NuxtLink to="/" class="btn btn-soft">Wróć na stronę główną</NuxtLink>
|
||||
<NuxtLink to="/exam" class="btn btn-outline">
|
||||
Rozpocznij jeszcze raz
|
||||
</NuxtLink>
|
||||
<button class="btn btn-neutral" @click="myModal?.close()">
|
||||
Przejrzyj odpowiedzi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
111
components/RightBar.vue
Normal file
111
components/RightBar.vue
Normal file
|
@ -0,0 +1,111 @@
|
|||
<script setup lang="ts">
|
||||
import "7.css/dist/7.scoped.css";
|
||||
|
||||
const props = defineProps<{
|
||||
questionaries: BasicQuestion[] | null;
|
||||
countBasic: number;
|
||||
countAdvanced: number;
|
||||
dataBasic: BasicQuestion[] | null;
|
||||
dataAdvanced: AdvancedQuestion[] | null;
|
||||
now: string | null | undefined;
|
||||
}>();
|
||||
|
||||
const isBasic = computed(() => props.now == "basic");
|
||||
const isAdvanced = computed(() => props.now == "advanced");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center p-4 gap-10">
|
||||
<div>
|
||||
<button class="btn-major">Zakończ egzamin</button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-6">
|
||||
<div :class="isBasic ? '' : 'opacity-45'">
|
||||
Pytania podstawowe
|
||||
<div class="win7 *:!h-10 *:*:!h-10 *:*:*:h-10 text-lg">
|
||||
<div
|
||||
role="progressbar"
|
||||
class="relative"
|
||||
:class="isBasic ? 'animate' : ''"
|
||||
>
|
||||
<div :class="`w-full`">
|
||||
<div
|
||||
class="set-translate w-full text-center bg-blue-500 bg-opacity-60"
|
||||
:class="isBasic ? 'font-semibold' : ''"
|
||||
>
|
||||
<span class="block set-translate w-full text-center"
|
||||
>{{ countBasic + 1 }} / {{ dataBasic?.length }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="isAdvanced ? '' : 'opacity-45'">
|
||||
Pytania specjalistyczne
|
||||
<div class="win7 *:!h-10 *:*:!h-10 *:*:*:h-10 text-lg">
|
||||
<div
|
||||
role="progressbar"
|
||||
class="relative"
|
||||
:class="isAdvanced ? 'animate' : ''"
|
||||
>
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="set-translate w-full text-center bg-blue-500 bg-opacity-60"
|
||||
:class="isAdvanced ? 'font-semibold' : ''"
|
||||
>
|
||||
<span class="block set-translate w-full text-center">
|
||||
{{ countAdvanced + 1 }} / {{ dataAdvanced?.length }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center text-xl">
|
||||
Czas na zapoznanie się z pytaniem<br />
|
||||
Czas na udzielenie odpowiedzi
|
||||
|
||||
<div class="flex flex-row items-stretch">
|
||||
<div class="btn-major">START</div>
|
||||
<div class="win7 flex-1 *:!h-[100%] *:*:!h-[100%]">
|
||||
<div role="progressbar" class="relative min-h-6">
|
||||
<div class="progressive !bg-orange-500">
|
||||
<div class="set-translate w-full text-center text-3xl">10 s</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="$emit('next-question')" class="btn-major">
|
||||
Następne pytanie
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div class="max-h-[150px] overflow-y-scroll">{{ questionaries }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.set-translate {
|
||||
@apply absolute top-[50%] left-[50%];
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.progressive {
|
||||
animation: progressZapoznanie 20s linear;
|
||||
}
|
||||
|
||||
@keyframes progressZapoznanie {
|
||||
0% {
|
||||
width: 0;
|
||||
}
|
||||
100% {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
26
components/TopBar.vue
Normal file
26
components/TopBar.vue
Normal file
|
@ -0,0 +1,26 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
points: number | undefined;
|
||||
category: string | undefined;
|
||||
timeRemaining: string | undefined;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row gap-4 *:flex *:items-center *:gap-3">
|
||||
<div>
|
||||
<span class="block">Wartość punktowa</span>
|
||||
<div class="info-little-box">
|
||||
{{ points }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="block">Aktualna kategoria (implement)</span>
|
||||
<div class="info-little-box">{{ category }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="block">Czas do końca egzaminu (implement)</span>
|
||||
<div class="info-little-box">{{ timeRemaining }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,44 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import type { Duration } from 'date-fns';
|
||||
|
||||
const props = defineProps<{
|
||||
points: number | null | undefined;
|
||||
category: string | undefined;
|
||||
timeRemaining?: Duration | undefined;
|
||||
}>();
|
||||
|
||||
const timeRemainingFriendly = computed(() => {
|
||||
if (typeof props.timeRemaining !== 'undefined') {
|
||||
const seconds = props.timeRemaining.seconds ?? 0;
|
||||
return `${props.timeRemaining.minutes ?? 0}:${
|
||||
seconds >= 0 && seconds < 10 ? 0 : ''
|
||||
}${seconds ?? 0}`;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-none flex-row gap-4 *:flex *:items-center *:gap-3 border-b p-4 border-base-300 bg-base-100"
|
||||
>
|
||||
<div>
|
||||
<span class="block">Wartość punktowa</span>
|
||||
<div class="info-little-box">
|
||||
{{ points }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="block">Aktualna kategoria</span>
|
||||
<div class="info-little-box">
|
||||
{{ category }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="typeof timeRemaining !== 'undefined'">
|
||||
<span class="block">Czas do końca egzaminu</span>
|
||||
<div class="info-little-box w-20 text-center">
|
||||
{{ timeRemainingFriendly }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,91 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
countBasic: number;
|
||||
countAdvanced: number;
|
||||
now: string | null | undefined;
|
||||
ending: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
endExam: [];
|
||||
nextQuestion: [];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-stretch p-4 gap-10 border-l border-base-300 bg-base-100"
|
||||
>
|
||||
<button class="btn btn-warning btn-xl" @click="emit('endExam')">
|
||||
Zakończ egzamin
|
||||
</button>
|
||||
|
||||
<div class="flex flex-row gap-6 *:flex-1 w-full">
|
||||
<CurrentQuestionCount
|
||||
:class="now === 'basic' ? 'font-semibold' : 'opacity-45'"
|
||||
>
|
||||
<template #title> Pytania podstawowe </template>
|
||||
<template #count> {{ countBasic + 1 }} / 20 </template>
|
||||
</CurrentQuestionCount>
|
||||
|
||||
<CurrentQuestionCount
|
||||
:class="now === 'advanced' ? 'font-semibold' : 'opacity-45'"
|
||||
>
|
||||
<template #title> Pytania specjalistyczne </template>
|
||||
<template #count> {{ countAdvanced + 1 }} / 12 </template>
|
||||
</CurrentQuestionCount>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-xl flex flex-col gap-2">
|
||||
<span>Czas na zapoznanie się z pytaniem</span>
|
||||
<div class="flex flex-row items-stretch gap-2">
|
||||
<div class="btn btn-primary">START</div>
|
||||
<div class="h-full flex-1 relative">
|
||||
<progress
|
||||
class="progress progress-warning w-full h-full"
|
||||
value="50"
|
||||
max="100"
|
||||
/>
|
||||
<span class="block set-translate z-10 text-black text-2xl">20s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-xl flex flex-col gap-2">
|
||||
<span>Czas na udzielenie odpowiedzi</span>
|
||||
<div class="h-9 relative">
|
||||
<progress
|
||||
class="progress progress-warning w-full h-full"
|
||||
value="50"
|
||||
max="100"
|
||||
/>
|
||||
<span class="block set-translate z-10 text-black text-2xl">15s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1" />
|
||||
|
||||
<button
|
||||
class="btn btn-warning btn-xl"
|
||||
:disabled="ending"
|
||||
@click="emit('nextQuestion')"
|
||||
>
|
||||
Następne pytanie
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/*.progressive {
|
||||
animation: progressZapoznanie 20s linear;
|
||||
}
|
||||
|
||||
@keyframes progressZapoznanie {
|
||||
0% {
|
||||
width: 0;
|
||||
}
|
||||
100% {
|
||||
width: 100%;
|
||||
}
|
||||
}*/
|
||||
</style>
|
|
@ -1,94 +0,0 @@
|
|||
<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-base-300 bg-base-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'
|
||||
} ${now === 'basic' && countBasic === num ? 'outline-set-solid outline-2' : ''}`"
|
||||
: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'
|
||||
} ${now === 'advanced' && countAdvanced === num ? 'outline-set-solid outline-2' : ''}`"
|
||||
:checked="now === 'advanced' ? countAdvanced === num : false"
|
||||
@click="
|
||||
emit('changeNow', 'advanced');
|
||||
emit('changeCount', num);
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="*:inline">Punkty: <slot name="points" /> / 74</div>
|
||||
<div class="*:inline">Wynik: <slot name="resultTrueFalse" /></div>
|
||||
</div>
|
||||
<NuxtLink to="/exam" class="btn btn-warning btn-xl">
|
||||
Rozpocznij jeszcze raz
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.outline-set-solid {
|
||||
outline-style: solid;
|
||||
}
|
||||
</style>
|
|
@ -1,57 +0,0 @@
|
|||
<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-base-300 bg-base-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-base-200;
|
||||
}
|
||||
}
|
||||
span {
|
||||
@apply text-lg;
|
||||
}
|
||||
</style>
|
|
@ -1,40 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
question: BasicQuestion | undefined;
|
||||
}>();
|
||||
|
||||
const answer = defineModel<string | null | undefined>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-none flex-col gap-6 border-t px-4 py-5 border-base-300 bg-base-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.toString()"
|
||||
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 { defineConfig } from 'drizzle-kit';
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
dialect: 'postgresql',
|
||||
schema: './src/db/schema.ts',
|
||||
out: './drizzle',
|
||||
dialect: "postgresql",
|
||||
schema: "./src/db/schema.ts",
|
||||
out: "./drizzle",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
}
|
||||
});
|
|
@ -1,8 +0,0 @@
|
|||
// @ts-check
|
||||
import eslintConfigPrettier from 'eslint-config-prettier/flat';
|
||||
import withNuxt from './.nuxt/eslint.config.mjs';
|
||||
|
||||
export default withNuxt(
|
||||
// Your custom configs here
|
||||
eslintConfigPrettier,
|
||||
);
|
3
layouts/default.vue
Normal file
3
layouts/default.vue
Normal file
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<div><slot /></div>
|
||||
</template>
|
|
@ -1,9 +0,0 @@
|
|||
export default defineNuxtRouteMiddleware(async () => {
|
||||
const examStore = useExamStore();
|
||||
|
||||
if (examStore.category === '') {
|
||||
examStore.resetExam();
|
||||
return navigateTo('/');
|
||||
}
|
||||
examStore.mildReset();
|
||||
});
|
|
@ -1,8 +0,0 @@
|
|||
export default defineNuxtRouteMiddleware(() => {
|
||||
const examStore = useExamStore();
|
||||
|
||||
if (!examStore.end) {
|
||||
examStore.resetExam();
|
||||
return navigateTo('/');
|
||||
}
|
||||
});
|
|
@ -1,49 +1,15 @@
|
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
import 'dotenv/config';
|
||||
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
'@nuxtjs/tailwindcss',
|
||||
'@nuxt/fonts',
|
||||
'@pinia/nuxt',
|
||||
'@nuxt/eslint',
|
||||
'@nuxt/image',
|
||||
],
|
||||
compatibilityDate: "2024-11-01",
|
||||
devtools: { enabled: true },
|
||||
modules: ["@nuxtjs/tailwindcss", "@nuxt/fonts"],
|
||||
ssr: true,
|
||||
imports: {
|
||||
dirs: ['types/*.ts', 'store/*.ts', 'types/**/*.ts'],
|
||||
dirs: ["types/*.ts", "store/*.ts", "types/**/*.ts"],
|
||||
},
|
||||
devtools: { enabled: true },
|
||||
css: ['~/assets/main.css'],
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
cdn_url: process.env.CDN_URL,
|
||||
cdn_url: "http://pj.netman.ovh/",
|
||||
},
|
||||
},
|
||||
routeRules: {
|
||||
'/': { prerender: true },
|
||||
},
|
||||
compatibilityDate: '2024-11-01',
|
||||
eslint: {
|
||||
config: {
|
||||
stylistic: {
|
||||
indent: 2,
|
||||
semi: true,
|
||||
quotes: 'single',
|
||||
jsx: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
image: {
|
||||
providers: {
|
||||
selfhost: {
|
||||
name: 'selfhost',
|
||||
provider: '~/providers/selfhost.ts',
|
||||
options: {
|
||||
baseUrl: process.env.CDN_URL,
|
||||
},
|
||||
},
|
||||
},
|
||||
provider: 'selfhost',
|
||||
},
|
||||
});
|
||||
|
|
41
package.json
41
package.json
|
@ -7,41 +7,28 @@
|
|||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"pretty": "prettier --write \"./**/*.{js,mjs,ts,vue,json}\"",
|
||||
"tsc": "nuxi typecheck"
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/fonts": "0.11.1",
|
||||
"@nuxt/image": "1.10.0",
|
||||
"@nuxtjs/tailwindcss": "6.13.2",
|
||||
"@pinia/nuxt": "0.11.0",
|
||||
"7.css": "^0.17.0",
|
||||
"@nuxt/fonts": "0.10.3",
|
||||
"@nuxtjs/tailwindcss": "6.13.1",
|
||||
"array-shuffle": "^3.0.0",
|
||||
"daisyui": "^5.0.27",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"drizzle-kit": "^0.31.0",
|
||||
"drizzle-orm": "^0.42.0",
|
||||
"eslint": "^9.24.0",
|
||||
"lodash": "^4.17.21",
|
||||
"nuxt": "~3.16.2",
|
||||
"pg": "^8.14.1",
|
||||
"pinia": "^3.0.2",
|
||||
"ufo": "^1.6.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-orm": "^0.40.0",
|
||||
"nuxt": "^3.15.4",
|
||||
"pg": "^8.13.3",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest"
|
||||
},
|
||||
"packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af",
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint": "1.3.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/pg": "^8.11.13",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"prettier": "^3.5.3",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-plugin-eslint2": "^5.0.3"
|
||||
"@types/pg": "^8.11.11",
|
||||
"drizzle-kit": "^0.30.5",
|
||||
"tsx": "^4.19.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"nitropack": "2.8.1"
|
||||
}
|
||||
}
|
||||
|
|
242
pages/exam.vue
242
pages/exam.vue
|
@ -1,172 +1,178 @@
|
|||
<script lang="ts" setup>
|
||||
import { intervalToDuration, addMinutes, addSeconds, isEqual } from 'date-fns';
|
||||
import { useExamStore } from '~/store/examStore';
|
||||
|
||||
definePageMeta({ middleware: ['exam'] });
|
||||
import "7.css/dist/7.scoped.css";
|
||||
|
||||
useHead({
|
||||
title: 'Pytanie 1/20',
|
||||
title: "Pytanie 1/20",
|
||||
});
|
||||
|
||||
const nowTime = ref(new Date());
|
||||
const timeEnd = addMinutes(new Date(), 25);
|
||||
|
||||
const timeRemainingTotal = computed(() =>
|
||||
intervalToDuration({
|
||||
start: nowTime.value,
|
||||
end: timeEnd,
|
||||
}),
|
||||
);
|
||||
// const timeRemainingQuestion - to implement
|
||||
|
||||
onMounted(() => {
|
||||
const endInterval = setInterval(() => {
|
||||
nowTime.value = addSeconds(nowTime.value, 1);
|
||||
if (isEqual(nowTime.value, timeEnd)) {
|
||||
clearInterval(endInterval);
|
||||
endExam();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
watchEffect(() => {
|
||||
if (now.value === 'basic')
|
||||
useHead({ title: `Pytanie ${countBasic.value + 1}/20` });
|
||||
if (now.value === 'advanced')
|
||||
useHead({ title: `Pytanie ${countAdvanced.value + 1}/12` });
|
||||
});
|
||||
});
|
||||
|
||||
const examStore = useExamStore();
|
||||
|
||||
const {
|
||||
data: dataBasic,
|
||||
error: errorBasic,
|
||||
status: statusBasic,
|
||||
} = await useLazyFetch<BasicQuestion[]>(`/api/basic`, {
|
||||
query: {
|
||||
category: examStore.category,
|
||||
},
|
||||
});
|
||||
} = await useFetch<BasicQuestion[]>("/api/basic");
|
||||
|
||||
const {
|
||||
data: dataAdvanced,
|
||||
error: errorAdvanced,
|
||||
status: statusAdvanced,
|
||||
} = await useLazyFetch<AdvancedQuestion[]>(`/api/advanced`, {
|
||||
query: {
|
||||
category: examStore.category,
|
||||
},
|
||||
});
|
||||
} = await useFetch<AdvancedQuestion[]>("/api/advanced");
|
||||
|
||||
const countBasic = ref(0);
|
||||
const countAdvanced = ref(-1);
|
||||
|
||||
const now = ref('basic');
|
||||
const answer = ref<string>('');
|
||||
const now = ref("basic");
|
||||
|
||||
const ending = ref(false);
|
||||
const tak_nie_model = ref();
|
||||
const abc_model = ref();
|
||||
|
||||
async function next() {
|
||||
if (countBasic.value + 1 < dataBasic.value?.length!) {
|
||||
questionaries.value.push({
|
||||
question: question.value,
|
||||
chosen_answer: tak_nie_model.value ?? "",
|
||||
chosen_is_correct:
|
||||
tak_nie_model.value == question.value?.poprawna_odp?.toLowerCase(),
|
||||
liczba_pkt: question.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) {
|
||||
questionaries.value.push({
|
||||
question: question.value,
|
||||
chosen_answer: abc_model.value ?? "",
|
||||
chosen_is_correct:
|
||||
abc_model.value == question.value?.poprawna_odp?.toLowerCase(),
|
||||
liczba_pkt: question.value?.liczba_pkt,
|
||||
});
|
||||
} else {
|
||||
now.value = "advanced";
|
||||
}
|
||||
countAdvanced.value++;
|
||||
abc_model.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function endExam() {
|
||||
return;
|
||||
}
|
||||
|
||||
const questionBasic = computed<BasicQuestion | undefined>(() =>
|
||||
dataBasic.value?.at(countBasic.value),
|
||||
dataBasic.value?.at(countBasic.value)
|
||||
);
|
||||
const questionAdvanced = computed<AdvancedQuestion | undefined>(() =>
|
||||
dataAdvanced.value?.at(countAdvanced.value),
|
||||
dataAdvanced.value?.at(countAdvanced.value)
|
||||
);
|
||||
|
||||
const question = computed(() => {
|
||||
if (now.value === 'basic') return questionBasic.value;
|
||||
if (now.value === 'advanced') return questionAdvanced.value;
|
||||
return null;
|
||||
if (now.value == "basic") {
|
||||
return questionBasic.value;
|
||||
} else if (now.value == "advanced") {
|
||||
return questionAdvanced.value;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const result: Ref<ResultEndType> = ref({
|
||||
basic: [],
|
||||
advanced: [],
|
||||
const media = computed(() => {
|
||||
const mediaSplit = question.value?.media?.split(".");
|
||||
return {
|
||||
fileType: mediaSplit?.pop()?.toLowerCase(),
|
||||
fileName: mediaSplit?.join("."),
|
||||
ogName: question.value?.media,
|
||||
};
|
||||
});
|
||||
|
||||
async function next() {
|
||||
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 = '';
|
||||
const questionaries: Ref<Array<any>> = ref([]);
|
||||
|
||||
if (now.value === 'basic') {
|
||||
if (countBasic.value < 19) {
|
||||
countBasic.value++;
|
||||
} else {
|
||||
now.value = 'advanced';
|
||||
countAdvanced.value++;
|
||||
}
|
||||
} else if (now.value === 'advanced') {
|
||||
if (countAdvanced.value < 11) {
|
||||
countAdvanced.value++;
|
||||
}
|
||||
if (countAdvanced.value >= 11) {
|
||||
ending.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 loading = ref(false);
|
||||
// onMounted(() => {
|
||||
// const progresInterval = setInterval(() => {
|
||||
// progres.value.value = +progres.value.value + 1;
|
||||
// if (progres.value.value >= 100) {
|
||||
// clearInterval(progresInterval);
|
||||
// }
|
||||
// }, 100);
|
||||
// });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- as in to transition to the next page -->
|
||||
<LoadingScreen v-if="loading" />
|
||||
<div v-if="statusBasic === 'success' && statusAdvanced === 'success'">
|
||||
<div v-if="statusBasic === '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="timeRemainingTotal"
|
||||
<div class="col-span-3 flex flex-col gap-4 p-4">
|
||||
<TopBar
|
||||
:points="question?.liczba_pkt"
|
||||
:category="`B`"
|
||||
:time-remaining="`25:00`"
|
||||
/>
|
||||
<MediaBox :media-path="question?.media_url" />
|
||||
<QuestionBasic
|
||||
v-if="now === 'basic'"
|
||||
v-model="answer"
|
||||
<Media :media="media" />
|
||||
<BasicQuestionBlock
|
||||
v-if="countAdvanced < 0"
|
||||
:question="questionBasic"
|
||||
v-model="tak_nie_model"
|
||||
/>
|
||||
<QuestionAdvanced
|
||||
v-else-if="now === 'advanced'"
|
||||
v-model="answer"
|
||||
<AdvancedQuestionBlock
|
||||
v-else
|
||||
:question="questionAdvanced"
|
||||
v-model="abc_model"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BarRightExam
|
||||
<RightBar
|
||||
:questionaries="questionaries"
|
||||
:data-basic="dataBasic"
|
||||
:data-advanced="dataAdvanced"
|
||||
:count-basic="countBasic"
|
||||
:count-advanced="countAdvanced"
|
||||
:now="now"
|
||||
:ending="ending"
|
||||
@next-question="next()"
|
||||
@end-exam="endExam()"
|
||||
:now="now"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="statusBasic === 'error' || statusAdvanced === 'error'">
|
||||
An API error occurred: {{ errorBasic }} {{ errorAdvanced }}
|
||||
</div>
|
||||
<LoadingScreen v-else />
|
||||
<div v-else>Loading...</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.btn {
|
||||
@apply box-border text-white font-bold p-3 rounded-md w-fit cursor-pointer border-[4px] transition duration-100;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
@apply duration-0;
|
||||
}
|
||||
|
||||
.btn-answer {
|
||||
@apply btn bg-blue-500 text-xl;
|
||||
}
|
||||
|
||||
.btn-answer:hover {
|
||||
@apply bg-blue-300;
|
||||
}
|
||||
|
||||
.btn-answer:active {
|
||||
@apply bg-blue-400;
|
||||
}
|
||||
|
||||
.btn-major {
|
||||
@apply btn bg-orange-400 text-base;
|
||||
}
|
||||
|
||||
.btn-major:hover {
|
||||
@apply bg-orange-200;
|
||||
}
|
||||
|
||||
.btn-major:active {
|
||||
@apply bg-orange-300;
|
||||
}
|
||||
|
||||
.info-little-box {
|
||||
@apply inline-block px-[15px] py-[8px] bg-blue-500 text-white font-bold;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,47 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import categories from '~/categories';
|
||||
|
||||
onMounted(() => {
|
||||
useHead({
|
||||
title: 'Test na prawo jazdy',
|
||||
});
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const examStore = useExamStore();
|
||||
|
||||
function setAndGo(category: string) {
|
||||
loading.value = true;
|
||||
examStore.setCategory(category);
|
||||
while (true) {
|
||||
if (examStore.category === category) {
|
||||
return navigateTo('/exam');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!loading" class="text-3xl">
|
||||
<span>Test na prawo jazdy</span>
|
||||
<p>
|
||||
Witaj w teście na prawo jazdy, aby rozpocząć, naciśnij jeden z
|
||||
poniższych przycisków:
|
||||
<br />
|
||||
</p>
|
||||
<div class="flex flex-row flex-wrap gap-2">
|
||||
<button
|
||||
v-for="category in categories"
|
||||
:key="`btn-${category}`"
|
||||
class="btn btn-xl btn-secondary"
|
||||
@click="setAndGo(category)"
|
||||
>
|
||||
{{ category }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<LoadingScreen v-else />
|
||||
<h1>Test na prawo jazdy kat.B</h1>
|
||||
<p>
|
||||
Witaj w teście na prawo jazdy kat.B, aby rozpocząć, naciśnij poniższy
|
||||
przycisk:
|
||||
</p>
|
||||
<NuxtLink to="/exam" class="text-4xl font-bold bg-fuchsia-200"
|
||||
>START!</NuxtLink
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
|
127
pages/result.vue
127
pages/result.vue
|
@ -1,127 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import ResultModal from '~/components/ResultModal.vue';
|
||||
|
||||
definePageMeta({ middleware: ['result'] });
|
||||
|
||||
const examStore = useExamStore();
|
||||
|
||||
const points = ref<number>(0);
|
||||
|
||||
examStore.result.basic.forEach((answer) => {
|
||||
if (answer.chosen_is_correct) {
|
||||
points.value += answer.question?.weight ?? 0;
|
||||
}
|
||||
});
|
||||
examStore.result.advanced.forEach((answer) => {
|
||||
if (answer.chosen_is_correct) {
|
||||
points.value += answer.question?.weight ?? 0;
|
||||
}
|
||||
});
|
||||
|
||||
const resultTrueFalse = ref(points.value >= 68 ? 'pozytywny' : 'negatywny');
|
||||
|
||||
onMounted(() => {
|
||||
useHead({
|
||||
title: `${
|
||||
String(resultTrueFalse.value[0]).toUpperCase() +
|
||||
String(resultTrueFalse.value).slice(1)
|
||||
} (${points.value}/74)`,
|
||||
});
|
||||
});
|
||||
|
||||
const countBasic = ref(0);
|
||||
const countAdvanced = ref(0);
|
||||
|
||||
const resultQuestionBasic = computed<ResultType<BasicQuestion> | undefined>(
|
||||
() => examStore.result.basic.at(countBasic.value),
|
||||
);
|
||||
const resultQuestionAdvanced = computed<
|
||||
ResultType<AdvancedQuestion> | undefined
|
||||
>(() => examStore.result.advanced.at(countAdvanced.value));
|
||||
|
||||
const questionBasic = computed<BasicQuestion | undefined>(
|
||||
() => resultQuestionBasic.value?.question,
|
||||
);
|
||||
const questionAdvanced = computed<AdvancedQuestion | undefined>(
|
||||
() => resultQuestionAdvanced.value?.question,
|
||||
);
|
||||
|
||||
const now = ref('basic');
|
||||
|
||||
const question = computed(() => {
|
||||
if (now.value === 'basic') {
|
||||
return questionBasic.value;
|
||||
} else if (now.value === 'advanced') {
|
||||
return questionAdvanced.value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const resultQuestion = computed(() => {
|
||||
if (now.value === 'basic') {
|
||||
return resultQuestionBasic.value;
|
||||
} else if (now.value === 'advanced') {
|
||||
return resultQuestionAdvanced.value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const answer = computed(() => resultQuestion.value?.chosen_answer);
|
||||
|
||||
function changeNow(to: string) {
|
||||
now.value = to;
|
||||
}
|
||||
|
||||
function changeCount(num: number) {
|
||||
if (now.value === 'basic') {
|
||||
countBasic.value = num;
|
||||
} else if (now.value === 'advanced') {
|
||||
countAdvanced.value = num;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ResultModal>
|
||||
<template #title>Egzamin teorytyczny</template>
|
||||
<template #category>{{ examStore.category }}</template>
|
||||
<template #points>{{ points }}</template>
|
||||
<template #resultTrueFalse>{{ resultTrueFalse }}</template>
|
||||
</ResultModal>
|
||||
<div>
|
||||
<div class="grid grid-cols-4 min-h-dvh">
|
||||
<div class="col-span-3 flex flex-col">
|
||||
<BarTop :points="question?.weight" :category="examStore.category" />
|
||||
<MediaBox :media-path="question?.media_url" />
|
||||
<QuestionBasic
|
||||
v-if="now === 'basic'"
|
||||
v-model="answer"
|
||||
:question="questionBasic"
|
||||
class="select-none z-[-1]"
|
||||
/>
|
||||
<QuestionAdvanced
|
||||
v-else-if="now === 'advanced'"
|
||||
v-model="answer"
|
||||
:question="questionAdvanced"
|
||||
class="select-none z-[-1]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BarRightResult
|
||||
:result="examStore.result"
|
||||
:count-basic="countBasic"
|
||||
:count-advanced="countAdvanced"
|
||||
:now="now"
|
||||
@change-now="changeNow"
|
||||
@change-count="changeCount"
|
||||
>
|
||||
<template #points>{{ points }}</template>
|
||||
<template #resultTrueFalse>{{ resultTrueFalse }}</template>
|
||||
</BarRightResult>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
5985
pnpm-lock.yaml
generated
5985
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,12 +0,0 @@
|
|||
/**
|
||||
* @see https://prettier.io/docs/configuration
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
const config = {
|
||||
trailingComma: 'all',
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,18 +0,0 @@
|
|||
import { joinURL } from 'ufo';
|
||||
import type { ProviderGetImage } from '@nuxt/image';
|
||||
import { createOperationsGenerator } from '#image';
|
||||
|
||||
const operationsGenerator = createOperationsGenerator();
|
||||
|
||||
export const getImage: ProviderGetImage = (
|
||||
src,
|
||||
{ modifiers = {}, baseUrl } = {},
|
||||
) => {
|
||||
baseUrl ??= '';
|
||||
|
||||
const operations = operationsGenerator(modifiers);
|
||||
|
||||
return {
|
||||
url: joinURL(baseUrl, src + (operations ? '?' + operations : '')),
|
||||
};
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="680.764" height="528.354" viewBox="0 0 180.119 139.794"><g transform="translate(-13.59 -66.639)" paint-order="fill markers stroke"><path fill="#d0d0d0" d="M13.591 66.639H193.71v139.794H13.591z"/><path d="m118.507 133.514-34.249 34.249-15.968-15.968-41.938 41.937H178.726z" opacity=".675" fill="#fff"/><circle cx="58.217" cy="108.555" r="11.773" opacity=".675" fill="#fff"/><path fill="none" d="M26.111 77.634h152.614v116.099H26.111z"/></g></svg>
|
Before Width: | Height: | Size: 492 B |
|
@ -1,67 +1,73 @@
|
|||
import 'dotenv/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { sql, eq, and } from 'drizzle-orm';
|
||||
import arrayShuffle from 'array-shuffle';
|
||||
import {
|
||||
tasks_advanced,
|
||||
questions_advanced,
|
||||
categories_db,
|
||||
} from '@/src/db/schema';
|
||||
import type { AdvancedQuestion } from '~/types';
|
||||
import categories from '~/categories';
|
||||
import "dotenv/config";
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import { dane, punkty } from "@/src/db/schema";
|
||||
import { sql, eq, isNotNull, and } from "drizzle-orm";
|
||||
import { AdvancedQuestion } from "~/types/basic";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
const category = query.category;
|
||||
|
||||
if (category === '' || typeof category !== 'string') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage:
|
||||
'category argument has to be string (or not to be defined at all)',
|
||||
});
|
||||
}
|
||||
if (!categories.includes(`${category.toUpperCase()}`)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `category argument has to be equal to either: ${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 amount = query?.amount;
|
||||
const db = drizzle(process.env.DATABASE_URL!);
|
||||
const questions: AdvancedQuestion[] = 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,
|
||||
|
||||
const randomizedQuestions: AdvancedQuestion[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries({ 1: 2, 2: 4, 3: 6 })) {
|
||||
randomizedQuestions.push(...(await getFromDb(+key, value, category)));
|
||||
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)
|
||||
)
|
||||
// sql`lower(${dane.poprawna_odp})='a' or lower(${dane.poprawna_odp})='b' or lower(${dane.poprawna_odp})='c'`
|
||||
// sql`${dane.odp_a} is not null or ${dane.odp_b} is not null or ${dane.odp_c} is not null`
|
||||
);
|
||||
const randoms: Array<number> = [];
|
||||
const randomizedQuestions = [];
|
||||
for (let i = 0; i < +(amount ?? 12); i++) {
|
||||
let randomized = Math.floor(Math.random() * (questions.length - 1 + 1)) + 0;
|
||||
while (randoms.includes(randomized)) {
|
||||
randomized = Math.floor(Math.random() * (questions.length - 1 + 1)) + 0;
|
||||
}
|
||||
randoms.push(randomized);
|
||||
if (questions[randomized].kategorie?.split(",").includes("B")) {
|
||||
randomizedQuestions.push(questions[randomized]);
|
||||
} else {
|
||||
i--;
|
||||
}
|
||||
}
|
||||
return arrayShuffle(randomizedQuestions);
|
||||
return randomizedQuestions;
|
||||
});
|
||||
|
|
|
@ -1,57 +1,73 @@
|
|||
import 'dotenv/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { sql, eq, and } from 'drizzle-orm';
|
||||
import arrayShuffle from 'array-shuffle';
|
||||
import { tasks, questions, categories_db } from '@/src/db/schema';
|
||||
import type { BasicQuestion } from '~/types';
|
||||
import categories from '~/categories';
|
||||
import "dotenv/config";
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import { dane, punkty } from "@/src/db/schema";
|
||||
import { sql, eq, and, or } from "drizzle-orm";
|
||||
import { BasicQuestion } from "~/types/basic";
|
||||
import arrayShuffle from "array-shuffle";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
const category = query.category;
|
||||
|
||||
if (category === '' || typeof category !== 'string') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage:
|
||||
'category argument has to be string (or not to be defined at all)',
|
||||
});
|
||||
}
|
||||
if (!categories.includes(`${category.toUpperCase()}`)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `category argument has to be equal to either: ${categories}`,
|
||||
});
|
||||
}
|
||||
|
||||
async function getFromDb(points: number, limit: number, category: string) {
|
||||
async function getFromDb(points: number | string) {
|
||||
return await db
|
||||
.select({
|
||||
id: tasks.id,
|
||||
correct_answer: tasks.correct_answer,
|
||||
media_url: tasks.media_url,
|
||||
weight: tasks.weight,
|
||||
text: questions.text,
|
||||
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(tasks)
|
||||
.leftJoin(questions, eq(questions.task_id, tasks.id))
|
||||
.leftJoin(categories_db, eq(categories_db.task_id, tasks.id))
|
||||
.from(dane)
|
||||
.innerJoin(punkty, eq(dane.nr_pytania, punkty.nr_pytania))
|
||||
.where(
|
||||
and(
|
||||
eq(categories_db.name, category.toUpperCase()),
|
||||
eq(questions.lang, 'PL'),
|
||||
eq(tasks.weight, points),
|
||||
),
|
||||
)
|
||||
.orderBy(sql`RANDOM()`)
|
||||
.limit(limit);
|
||||
or(
|
||||
sql`lower(${dane.poprawna_odp})='tak'`,
|
||||
sql`lower(${dane.poprawna_odp})='nie'`
|
||||
),
|
||||
eq(punkty.liczba_pkt, +points)
|
||||
)
|
||||
);
|
||||
}
|
||||
const query = getQuery(event);
|
||||
const category = query.category;
|
||||
if (typeof category != "undefined" && typeof category != "string") {
|
||||
throw new Error(
|
||||
"category argument has to be string (or not to be defined at all)"
|
||||
);
|
||||
}
|
||||
const db = drizzle(process.env.DATABASE_URL!);
|
||||
|
||||
const randomizedQuestions: BasicQuestion[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries({ 1: 4, 2: 6, 3: 10 })) {
|
||||
randomizedQuestions.push(...(await getFromDb(+key, value, category)));
|
||||
for (let [key, value] of Object.entries({ 1: 4, 2: 6, 3: 10 })) {
|
||||
const questionsKeyPoints: BasicQuestion[] = await getFromDb(key);
|
||||
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 ?? "B")
|
||||
) {
|
||||
chosenRandomQuestions.push(questionsKeyPoints[randomized]);
|
||||
} else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
chosenRandomQuestions.forEach((q) => randomizedQuestions.push(q));
|
||||
}
|
||||
return arrayShuffle(randomizedQuestions);
|
||||
});
|
||||
|
|
|
@ -1,35 +1,34 @@
|
|||
import { integer, pgTable, text, smallint, char } from 'drizzle-orm/pg-core';
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
|
||||
export const tasks = pgTable('tasks', {
|
||||
id: integer().notNull(),
|
||||
correct_answer: text(),
|
||||
media_url: text(),
|
||||
weight: smallint(),
|
||||
export const dane = pgTable("dane", {
|
||||
id: integer().primaryKey().notNull(),
|
||||
nr_pytania: integer().notNull(),
|
||||
pytanie: text().notNull(),
|
||||
odp_a: text(),
|
||||
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 questions = pgTable('questions', {
|
||||
task_id: integer(),
|
||||
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(),
|
||||
export const punkty = pgTable("punkty", {
|
||||
nr_pytania: integer().primaryKey().notNull(),
|
||||
liczba_pkt: integer().notNull(),
|
||||
});
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useExamStore = defineStore('exam-store', () => {
|
||||
const category = ref('');
|
||||
const end = ref(false);
|
||||
const result: Ref<ResultEndType> = ref({
|
||||
basic: [],
|
||||
advanced: [],
|
||||
});
|
||||
function resetExam() {
|
||||
category.value = '';
|
||||
mildReset();
|
||||
}
|
||||
function mildReset() {
|
||||
end.value = false;
|
||||
result.value = {
|
||||
basic: [],
|
||||
advanced: [],
|
||||
};
|
||||
}
|
||||
function setEnd(value: boolean) {
|
||||
end.value = value;
|
||||
return end.value;
|
||||
}
|
||||
function setCategory(value: string) {
|
||||
category.value = value;
|
||||
return category.value;
|
||||
}
|
||||
function setResult(value: ResultEndType) {
|
||||
result.value = value;
|
||||
return result.value;
|
||||
}
|
||||
return {
|
||||
category,
|
||||
end,
|
||||
result,
|
||||
resetExam,
|
||||
mildReset,
|
||||
setEnd,
|
||||
setCategory,
|
||||
setResult,
|
||||
};
|
||||
});
|
|
@ -1,8 +0,0 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
module.exports = {
|
||||
plugins: [require('daisyui')],
|
||||
daisyui: {
|
||||
themes: ['light', 'dark'],
|
||||
},
|
||||
};
|
46
types/basic.ts
Normal file
46
types/basic.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
export interface BasicQuestion {
|
||||
id: number;
|
||||
nr_pytania: number;
|
||||
pytanie: string;
|
||||
// odp_a: null;
|
||||
// odp_b: null;
|
||||
// odp_c: null;
|
||||
poprawna_odp: string;
|
||||
media: string | null;
|
||||
kategorie: string;
|
||||
nazwa_media_pjm_tresc_pyt: 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;
|
||||
pytanie_eng: string;
|
||||
// odp_a_eng: string | null;
|
||||
// odp_b_eng: string | null;
|
||||
// odp_c_eng: string | null;
|
||||
pytanie_de: string;
|
||||
// odp_a_de: string | null;
|
||||
// odp_b_de: string | null;
|
||||
// odp_c_de: string | null;
|
||||
pytanie_ua: string | null;
|
||||
// odp_a_ua: string | null;
|
||||
// odp_b_ua: string | null;
|
||||
// odp_c_ua: string | null;
|
||||
liczba_pkt: number;
|
||||
}
|
||||
|
||||
export interface AdvancedQuestion extends BasicQuestion {
|
||||
odp_a: string;
|
||||
odp_b: string;
|
||||
odp_c: string;
|
||||
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;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
export interface BasicQuestion {
|
||||
id: number | null;
|
||||
correct_answer: string | null;
|
||||
media_url: string | null;
|
||||
weight: number | null;
|
||||
text: string | null;
|
||||
}
|
||||
|
||||
export interface AdvancedQuestion extends BasicQuestion {
|
||||
answer_a: string | null;
|
||||
answer_b: string | null;
|
||||
answer_c: string | null;
|
||||
}
|
||||
|
||||
export interface ResultType<T> {
|
||||
question: T | undefined;
|
||||
chosen_answer: string;
|
||||
chosen_is_correct: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface ResultEndType {
|
||||
basic: ResultType<BasicQuestion>[];
|
||||
advanced: ResultType<AdvancedQuestion>[];
|
||||
}
|
Loading…
Add table
Reference in a new issue