Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

40 changed files with 2894 additions and 4861 deletions

View file

@ -1,2 +1 @@
DATABASE_URL="postgres://USERNAME:PASSWORD@HOST:PORT/DATABASE"
CDN_URL="http://DOMAIN.TLD/FOLDER"

View file

@ -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

View file

@ -1,5 +1,7 @@
<template>
<div>
<NuxtPage />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>

View file

@ -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;
}

View file

@ -1,14 +0,0 @@
export default [
'A',
'B',
'C',
'D',
'T',
'AM',
'A1',
'A2',
'B1',
'C1',
'D1',
'PT',
];

View 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>

View 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>

View file

@ -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>

View file

@ -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
View 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>

View file

@ -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>

View file

@ -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
View 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
View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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!,
},
}
});

View file

@ -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
View file

@ -0,0 +1,3 @@
<template>
<div><slot /></div>
</template>

View file

@ -1,9 +0,0 @@
export default defineNuxtRouteMiddleware(async () => {
const examStore = useExamStore();
if (examStore.category === '') {
examStore.resetExam();
return navigateTo('/');
}
examStore.mildReset();
});

View file

@ -1,8 +0,0 @@
export default defineNuxtRouteMiddleware(() => {
const examStore = useExamStore();
if (!examStore.end) {
examStore.resetExam();
return navigateTo('/');
}
});

View file

@ -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',
},
});

View file

@ -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"
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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;

View file

@ -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 : '')),
};
};

View file

@ -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

View file

@ -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;
});

View file

@ -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);
});

View file

@ -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(),
});

View file

@ -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,
};
});

View file

@ -1,8 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
plugins: [require('daisyui')],
daisyui: {
themes: ['light', 'dark'],
},
};

46
types/basic.ts Normal file
View 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;
}

View file

@ -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>[];
}