Compare commits

..

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

29 changed files with 791 additions and 2161 deletions

View file

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

View file

@ -8,17 +8,7 @@ This project utilizes `pnpm`, thus it is recommended
pnpm install pnpm install
``` ```
# To-do: More information about setting up database will come here later
- [ ] re-forge database structure, script for processing, share appropriate files
- [ ] choose category
- [ ] beautify website
- [ ] better answer click recognition
- [ ] come up with how to show results appropriately
- [ ] i18n - pl, en, de, ua (not all questions are not available in ua, api handle)
- [ ] exam (maybe also results?) warning leave message on exit (refresh)
- [ ] lazy loading
- [ ] question timers, and at end of total timer show a message for a while before immediatly navigating to results (maybe sth similar also when normally ending exam)
## Development Server ## Development Server

View file

@ -1,9 +1,7 @@
<script setup lang="ts">
import "~/assets/css/main.css";
</script>
<template> <template>
<div> <div>
<NuxtLayout>
<NuxtPage /> <NuxtPage />
</NuxtLayout>
</div> </div>
</template> </template>

View file

@ -1,23 +0,0 @@
.btn {
height: initial !important;
min-height: var(--size);
}
.set-translate {
@apply absolute top-[50%] left-[50%];
transform: translate(-50%, -50%);
}
/* Transition (later)
.page-enter-active,
.page-leave-active {
transition: all 0.4s;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
filter: blur(1rem);
} */
.info-little-box {
@apply inline-block px-[15px] py-[8px] bg-blue-500 text-white font-bold rounded-md;
}

View file

@ -7,12 +7,10 @@ const abc_model = defineModel();
</script> </script>
<template> <template>
<div <div class="flex flex-col gap-3">
class="flex flex-col gap-5 border-t px-4 py-5 border-slate-300 bg-slate-100"
>
<div class="text-xl">{{ question?.pytanie }}</div> <div class="text-xl">{{ question?.pytanie }}</div>
<div> <div>
<div class="flex flex-col gap-3"> <div class="flex flex-col">
<input <input
type="radio" type="radio"
name="abc" name="abc"
@ -23,13 +21,11 @@ const abc_model = defineModel();
/> />
<label for="odp_a"> <label for="odp_a">
<div <div
:class="`btn btn-primary btn-lg ${ :class="`btn-answer ${abc_model == 'a' ? ' !bg-fuchsia-500' : ''}`"
abc_model == 'a' ? ' !btn-secondary' : ''
}`"
> >
A A
</div> </div>
<span class="block">{{ question?.odp_a }}</span> {{ question?.odp_a }}
</label> </label>
<input <input
type="radio" type="radio"
@ -41,13 +37,11 @@ const abc_model = defineModel();
/> />
<label for="odp_b"> <label for="odp_b">
<div <div
:class="`btn btn-primary btn-lg ${ :class="`btn-answer ${abc_model == 'b' ? ' !bg-fuchsia-500' : ''}`"
abc_model == 'b' ? ' !btn-secondary' : ''
}`"
> >
B B
</div> </div>
<span class="block">{{ question?.odp_b }}</span> {{ question?.odp_b }}
</label> </label>
<input <input
type="radio" type="radio"
@ -59,13 +53,11 @@ const abc_model = defineModel();
/> />
<label for="odp_c"> <label for="odp_c">
<div <div
:class="`btn btn-primary btn-lg ${ :class="`btn-answer ${abc_model == 'c' ? ' !bg-fuchsia-500' : ''}`"
abc_model == 'c' ? ' !btn-secondary' : ''
}`"
> >
C C
</div> </div>
<span class="block">{{ question?.odp_c }}</span> {{ question?.odp_c }}
</label> </label>
</div> </div>
</div> </div>
@ -73,13 +65,18 @@ const abc_model = defineModel();
</template> </template>
<style scoped> <style scoped>
label { .btn-answer {
@apply cursor-pointer flex flex-row gap-2 items-center; display: inline-block;
&:hover {
@apply bg-slate-200;
}
} }
span { label {
@apply text-lg; cursor: pointer;
}
label:hover .btn-answer {
@apply bg-blue-300;
}
label:hover {
@apply bg-slate-200;
} }
</style> </style>

View file

@ -7,24 +7,44 @@ const tak_nie_model = defineModel();
</script> </script>
<template> <template>
<div <div class="flex flex-col gap-3">
class="flex flex-col gap-6 border-t px-4 py-5 border-slate-300 bg-slate-100"
>
<div class="text-xl">{{ question?.pytanie }}</div> <div class="text-xl">{{ question?.pytanie }}</div>
<div> <div>
<div class="flex flex-row justify-around"> <div class="flex flex-row justify-around">
<input <input
type="radio" type="radio"
name="tak_nie" name="tak_nie"
v-for="tn in ['tak', 'nie']" id="odp_tak"
:id="`odp_${tn}`"
v-model="tak_nie_model" v-model="tak_nie_model"
:value="tn" value="tak"
class="btn btn-primary btn-xl" class="hidden"
:aria-label="tn.toUpperCase()"
:class="`${tak_nie_model == tn ? ' !btn-secondary' : ''}`"
:checked="tak_nie_model == tn"
/> />
<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> </div>
</div> </div>

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>
<span class="block">Ładowanie</span>
</div>
</template>

View file

@ -15,19 +15,10 @@ defineProps<{
<div <div
class="select-none z-[-1] flex-1 flex items-stretch w-full justify-center *:object-contain" class="select-none z-[-1] flex-1 flex items-stretch w-full justify-center *:object-contain"
> >
<!-- Reserved for getting to know the question (20s) in basic questions section <!-- OLD -->
src="~/public/placeholder.svg" --> <!-- + [media.fileName, media.fileType].join('.') -->
<img <img :src="cdnUrl + media.ogName" alt="" v-if="media.fileType == 'jpg'" />
:src="cdnUrl + media.ogName" <video v-else-if="media.fileType == 'wmv'" :key="media.fileName" autoplay>
alt=""
v-if="media.fileType == 'jpg'"
:key="`${media.ogName}-photo`"
/>
<video
v-else-if="media.fileType == 'wmv'"
:key="`${media.ogName}-video`"
autoplay
>
<source <source
:src="cdnUrl + [media.fileName, 'mp4'].join('.')" :src="cdnUrl + [media.fileName, 'mp4'].join('.')"
type="video/mp4" type="video/mp4"

View file

@ -1,39 +0,0 @@
<script setup lang="ts">
import { VueFinalModal } from "vue-final-modal";
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: "homepage"): void;
(e: "newExam"): void;
(e: "close"): void;
}>();
</script>
<template>
<VueFinalModal
class="flex justify-center items-center backdrop-blur-sm"
content-class="flex flex-col p-3 bg-white rounded-md gap-3"
overlay-transition="vfm-fade"
content-transition="vfm-fade"
:click-to-close="false"
:esc-to-close="false"
>
<h1 class="text-[1.5rem]">{{ title }}</h1>
<div class="*:inline">Punkty: <slot name="points" /> / 74</div>
<div class="*:inline">Wynik: <slot name="resultTrueFalse" /></div>
<div class="flex flex-row gap-2">
<button class="btn btn-soft" @click="emit('homepage')">
Wróć na stronę główną
</button>
<button class="btn btn-outline" @click="emit('newExam')">
Rozpocznij jeszcze raz
</button>
<button class="btn btn-neutral" @click="emit('close')">
Przejrzyj odpowiedzi
</button>
</div>
</VueFinalModal>
</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>

View file

@ -1,95 +0,0 @@
<script setup lang="ts">
const props = defineProps<{
result: ResultEndType;
countBasic: number;
countAdvanced: number;
dataBasic: BasicQuestion[] | null;
dataAdvanced: AdvancedQuestion[] | null;
now: string | null | undefined;
ending: boolean;
}>();
const isBasic = computed(() => props.now == "basic");
const isAdvanced = computed(() => props.now == "advanced");
</script>
<template>
<div
class="flex flex-col items-stretch p-4 gap-10 border-l border-slate-300 bg-slate-100"
>
<button @click="$emit('end-exam')" class="btn btn-warning btn-xl">
Zakończ egzamin
</button>
<div class="flex flex-row gap-6 *:flex-1 w-full">
<div class="flex flex-col gap-1" :class="isBasic ? '' : 'opacity-45'">
<span class="text-lg">Pytania podstawowe</span>
<div
class="info-little-box w-full text-center"
:class="isBasic ? 'font-semibold' : ''"
>
{{ countBasic + 1 }} / {{ dataBasic?.length }}
</div>
</div>
<div class="flex flex-col gap-1" :class="isAdvanced ? '' : 'opacity-45'">
<span class="text-lg">Pytania specjalistyczne</span>
<div
class="info-little-box w-full text-center"
:class="isAdvanced ? 'font-semibold' : ''"
>
{{ countAdvanced + 1 }} / {{ dataAdvanced?.length }}
</div>
</div>
</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"
></progress>
<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"
></progress>
<span class="block set-translate z-10 text-black text-2xl">15s</span>
</div>
</div>
<div class="max-h-[150px] overflow-y-scroll">
{{ result }}
</div>
<div class="flex-1"></div>
<button
@click="$emit('next-question')"
class="btn btn-warning btn-xl"
:disabled="ending"
>
Następne pytanie
</button>
</div>
</template>
<style>
.progressive {
animation: progressZapoznanie 20s linear;
}
@keyframes progressZapoznanie {
0% {
width: 0;
}
100% {
width: 100%;
}
}
</style>

View file

@ -1,108 +0,0 @@
<script setup lang="ts">
import { range } from "lodash";
const props = defineProps<{
result: ResultEndType;
countBasic: number;
countAdvanced: number;
question: BasicQuestion | AdvancedQuestion | undefined;
questionBasic: BasicQuestion | undefined;
questionAdvanced: AdvancedQuestion | undefined;
now: string | null | undefined;
}>();
const isBasic = computed(() => props.now == "basic");
const isAdvanced = computed(() => props.now == "advanced");
const boxesAmount = computed(() => {
if (isBasic.value) {
return 20;
} else if (isAdvanced.value) {
return 12;
} else {
return 0;
}
});
const countSwitchable = computed(() => {
if (isBasic.value) {
return props.countBasic;
} else if (isAdvanced.value) {
return props.countAdvanced;
} else {
return 0;
}
});
</script>
<template>
<div
class="flex flex-col items-stretch p-4 gap-6 border-l border-slate-300 bg-slate-100"
>
<button @click="$emit('nav', '/')" class="btn btn-warning btn-xl">
Wróć na stronę główną
</button>
<div class="flex flex-row gap-4 *:flex-1 w-full flex-wrap">
<button class="btn btn-info btn-lg" @click="$emit('change-now', 'basic')">
Pytania podstawowe
</button>
<button
class="btn btn-info btn-lg"
@click="$emit('change-now', 'advanced')"
>
Pytania specjalistyczne
</button>
</div>
<div
class="grid grid-cols-[repeat(auto-fit,50px)] gap-2 justify-around w-full"
>
<input
type="radio"
:aria-label="(num + 1).toString()"
class="btn btn-md"
:name="`${now}-chooser`"
v-for="num in range(0, boxesAmount)"
@click="$emit('change-count', num)"
:class="`${
isBasic
? result.basic[num].question?.poprawna_odp.toLowerCase() ==
result.basic[num].chosen_answer
? 'btn-success'
: 'btn-error'
: ''
}${
isAdvanced
? result.advanced[num].question?.poprawna_odp.toLowerCase() ==
result.advanced[num].chosen_answer
? 'btn-success'
: 'btn-error'
: ''
}`"
:checked="countSwitchable == num"
:key="`choose-${num}-${now}`"
/>
</div>
<div class="text-center text-4xl flex flex-col gap-5">
<span class="block">Poprawna odpowiedź</span>
<span class="block">{{ question?.poprawna_odp }}</span>
<span class="block">Zaznaczona odpowiedź</span>
<span class="block">
{{
isBasic
? result.basic[countBasic].chosen_answer.trim() == ""
? "BrakBasic"
: result.basic[countBasic].chosen_answer
: isAdvanced
? result.advanced[countAdvanced].chosen_answer.trim() == ""
? "BrakAdvanced"
: result.advanced[countAdvanced].chosen_answer
: "Błąd"
}}
</span>
</div>
<div class="flex-1"></div>
<button @click="$emit('nav', '/exam')" class="btn btn-warning btn-xl">
Rozpocznij jeszcze raz
</button>
</div>
</template>

View file

@ -1,26 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Duration } from "date-fns"; defineProps<{
const props = defineProps<{
points: number | undefined; points: number | undefined;
category: string | undefined; category: string | undefined;
timeRemaining?: Duration | undefined; timeRemaining: string | undefined;
}>(); }>();
const timeRemainingFriendly = computed(() => {
if (typeof props.timeRemaining !== "undefined") {
let seconds = props.timeRemaining.seconds ?? 0;
return `${props.timeRemaining.minutes ?? 0}:${
seconds >= 0 && seconds < 10 ? 0 : ""
}${seconds ?? 0}`;
}
});
</script> </script>
<template> <template>
<div <div class="flex flex-row gap-4 *:flex *:items-center *:gap-3">
class="flex flex-row gap-4 *:flex *:items-center *:gap-3 border-b p-4 border-slate-300 bg-slate-100"
>
<div> <div>
<span class="block">Wartość punktowa</span> <span class="block">Wartość punktowa</span>
<div class="info-little-box"> <div class="info-little-box">
@ -28,14 +15,12 @@ const timeRemainingFriendly = computed(() => {
</div> </div>
</div> </div>
<div> <div>
<span class="block">Aktualna kategoria</span> <span class="block">Aktualna kategoria (implement)</span>
<div class="info-little-box">{{ category }}</div> <div class="info-little-box">{{ category }}</div>
</div> </div>
<div v-if="typeof timeRemaining !== 'undefined'"> <div>
<span class="block">Czas do końca egzaminu</span> <span class="block">Czas do końca egzaminu (implement)</span>
<div class="info-little-box w-20 text-center"> <div class="info-little-box">{{ timeRemaining }}</div>
{{ timeRemainingFriendly }}
</div>
</div> </div>
</div> </div>
</template> </template>

3
layouts/default.vue Normal file
View file

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

View file

@ -1,10 +0,0 @@
export default defineNuxtRouteMiddleware((to, from) => {
const examStore = useExamStore();
if (examStore.category != "") {
examStore.mildReset();
} else {
examStore.resetExam();
return navigateTo("/");
}
});

View file

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

View file

@ -1,25 +1,15 @@
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
import "dotenv/config";
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: "2024-11-01", compatibilityDate: "2024-11-01",
devtools: { enabled: true }, devtools: { enabled: true },
modules: ["@nuxtjs/tailwindcss", "@nuxt/fonts", "@pinia/nuxt"], modules: ["@nuxtjs/tailwindcss", "@nuxt/fonts"],
ssr: true, ssr: true,
css: ["vue-final-modal/style.css"],
imports: { imports: {
dirs: ["types/*.ts", "store/*.ts", "types/**/*.ts"], dirs: ["types/*.ts", "store/*.ts", "types/**/*.ts"],
}, },
// Transition (later)
// app: {
// pageTransition: { name: "page", mode: "out-in" },
// },
runtimeConfig: { runtimeConfig: {
public: { public: {
cdn_url: process.env.CDN_URL, cdn_url: "http://pj.netman.ovh/",
}, },
}, },
routeRules: {
"/": { prerender: true },
},
}); });

View file

@ -10,27 +10,25 @@
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"dependencies": { "dependencies": {
"@nuxt/fonts": "0.11.0", "7.css": "^0.17.0",
"@nuxt/fonts": "0.10.3",
"@nuxtjs/tailwindcss": "6.13.1", "@nuxtjs/tailwindcss": "6.13.1",
"@pinia/nuxt": "0.10.1",
"array-shuffle": "^3.0.0", "array-shuffle": "^3.0.0",
"daisyui": "^5.0.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"drizzle-orm": "^0.40.0", "drizzle-orm": "^0.40.0",
"lodash": "^4.17.21", "nuxt": "^3.15.4",
"nuxt": "~3.15.4",
"pg": "^8.13.3", "pg": "^8.13.3",
"pinia": "^3.0.1",
"vue": "latest", "vue": "latest",
"vue-final-modal": "^4.5.5",
"vue-router": "latest" "vue-router": "latest"
}, },
"packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af", "packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af",
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.17.16",
"@types/pg": "^8.11.11", "@types/pg": "^8.11.11",
"drizzle-kit": "^0.30.5", "drizzle-kit": "^0.30.5",
"tsx": "^4.19.3" "tsx": "^4.19.3"
},
"resolutions": {
"nitropack": "2.8.1"
} }
} }

View file

@ -1,32 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useExamStore } from "~/store/examStore"; import "7.css/dist/7.scoped.css";
import { intervalToDuration, addMinutes, addSeconds, isEqual } from "date-fns";
definePageMeta({ middleware: ["exam"] });
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);
});
const examStore = useExamStore();
useHead({ useHead({
title: "Pytanie 1/20", title: "Pytanie 1/20",
@ -36,21 +9,13 @@ const {
data: dataBasic, data: dataBasic,
error: errorBasic, error: errorBasic,
status: statusBasic, status: statusBasic,
} = await useLazyFetch<BasicQuestion[]>(`/api/basic`, { } = await useFetch<BasicQuestion[]>("/api/basic");
query: {
category: examStore.category,
},
});
const { const {
data: dataAdvanced, data: dataAdvanced,
error: errorAdvanced, error: errorAdvanced,
status: statusAdvanced, status: statusAdvanced,
} = await useLazyFetch<AdvancedQuestion[]>(`/api/advanced`, { } = await useFetch<AdvancedQuestion[]>("/api/advanced");
query: {
category: examStore.category,
},
});
const countBasic = ref(0); const countBasic = ref(0);
const countAdvanced = ref(-1); const countAdvanced = ref(-1);
@ -60,73 +25,39 @@ const now = ref("basic");
const tak_nie_model = ref(); const tak_nie_model = ref();
const abc_model = ref(); const abc_model = ref();
const ending = ref(false);
async function next() { async function next() {
if (countBasic.value + 1 < dataBasic.value?.length!) { if (countBasic.value + 1 < dataBasic.value?.length!) {
result.value.basic.push({ questionaries.value.push({
question: questionBasic.value, question: question.value,
chosen_answer: tak_nie_model.value ?? "", chosen_answer: tak_nie_model.value ?? "",
chosen_is_correct: chosen_is_correct:
tak_nie_model.value == questionBasic.value?.poprawna_odp?.toLowerCase(), tak_nie_model.value == question.value?.poprawna_odp?.toLowerCase(),
liczba_pkt: questionBasic.value?.liczba_pkt, liczba_pkt: question.value?.liczba_pkt,
}); });
tak_nie_model.value = ""; tak_nie_model.value = "";
countBasic.value++; countBasic.value++;
useHead({ useHead({
title: `Pytanie ${countBasic.value + 1}/${dataBasic.value?.length}`, title: `Pytanie ${countBasic.value + 1}/${dataBasic.value?.length}`,
}); });
} else if (countAdvanced.value + 1 <= dataAdvanced.value?.length!) { } else if (countAdvanced.value + 1 < dataAdvanced.value?.length!) {
if (countAdvanced.value != -1) { if (countAdvanced.value != -1) {
result.value.advanced.push({ questionaries.value.push({
question: questionAdvanced.value, question: question.value,
chosen_answer: abc_model.value ?? "", chosen_answer: abc_model.value ?? "",
chosen_is_correct: chosen_is_correct:
abc_model.value == abc_model.value == question.value?.poprawna_odp?.toLowerCase(),
questionAdvanced.value?.poprawna_odp?.toLowerCase(), liczba_pkt: question.value?.liczba_pkt,
liczba_pkt: questionAdvanced.value?.liczba_pkt,
}); });
} else { } else {
now.value = "advanced"; now.value = "advanced";
result.value.basic.push({
question: questionBasic.value,
chosen_answer: tak_nie_model.value ?? "",
chosen_is_correct:
tak_nie_model.value ==
questionBasic.value?.poprawna_odp?.toLowerCase(),
liczba_pkt: questionBasic.value?.liczba_pkt,
});
tak_nie_model.value = "";
} }
if (countAdvanced.value + 1 < dataAdvanced.value?.length!) {
countAdvanced.value++; countAdvanced.value++;
useHead({
title: `Pytanie ${countAdvanced.value + 1}/${
dataAdvanced.value?.length
}`,
});
}
if (countAdvanced.value == dataAdvanced.value?.length! - 1) {
ending.value = true;
}
abc_model.value = ""; abc_model.value = "";
} }
} }
function endExam() { async function endExam() {
loading.value = true; return;
while (!ending.value) {
next();
}
next();
examStore.setResult(result.value);
examStore.setEnd(true);
while (true) {
if (examStore.result == result.value && examStore.end) {
return navigateTo("/result", { replace: true });
}
}
} }
const questionBasic = computed<BasicQuestion | undefined>(() => const questionBasic = computed<BasicQuestion | undefined>(() =>
@ -155,23 +86,27 @@ const media = computed(() => {
}; };
}); });
const result: Ref<ResultEndType> = ref({ const questionaries: Ref<Array<any>> = ref([]);
basic: [],
advanced: [], // onMounted(() => {
}); // const progresInterval = setInterval(() => {
const loading = ref(false); // progres.value.value = +progres.value.value + 1;
// if (progres.value.value >= 100) {
// clearInterval(progresInterval);
// }
// }, 100);
// });
</script> </script>
<template> <template>
<div> <div>
<Loading v-if="loading" /> <div v-if="statusBasic === 'success'">
<div v-if="statusBasic === 'success' && statusAdvanced === 'success'">
<div class="grid grid-cols-4 min-h-dvh"> <div class="grid grid-cols-4 min-h-dvh">
<div class="col-span-3 flex flex-col gap-4"> <div class="col-span-3 flex flex-col gap-4 p-4">
<TopBar <TopBar
:points="question?.liczba_pkt" :points="question?.liczba_pkt"
:category="examStore.category" :category="`B`"
:time-remaining="timeRemainingTotal" :time-remaining="`25:00`"
/> />
<Media :media="media" /> <Media :media="media" />
<BasicQuestionBlock <BasicQuestionBlock
@ -186,22 +121,58 @@ const loading = ref(false);
/> />
</div> </div>
<RightBarExam <RightBar
:result="result" :questionaries="questionaries"
:data-basic="dataBasic" :data-basic="dataBasic"
:data-advanced="dataAdvanced" :data-advanced="dataAdvanced"
:count-basic="countBasic" :count-basic="countBasic"
:count-advanced="countAdvanced" :count-advanced="countAdvanced"
@next-question="next()" @next-question="next()"
@end-exam="endExam()"
:now="now" :now="now"
:ending="ending"
/> />
</div> </div>
</div> </div>
<div v-else-if="statusBasic === 'error' || statusAdvanced === 'error'"> <div v-else-if="statusBasic === 'error' || statusAdvanced === 'error'">
An API error occurred: {{ errorBasic }} {{ errorAdvanced }} An API error occurred: {{ errorBasic }} {{ errorAdvanced }}
</div> </div>
<Loading v-else /> <div v-else>Loading...</div>
</div> </div>
</template> </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,57 +1,12 @@
<script setup lang="ts">
useHead({
title: "Test na prawo jazdy",
});
const categories = [
"A",
"B",
"C",
"D",
"T",
"AM",
"A1",
"A2",
"B1",
"C1",
"D1",
"PT",
];
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> <template>
<div> <div>
<div v-if="!loading" class="text-3xl"> <h1>Test na prawo jazdy kat.B</h1>
<span>Test na prawo jazdy</span>
<p> <p>
Witaj w teście na prawo jazdy, aby rozpocząć, naciśnij jeden z Witaj w teście na prawo jazdy kat.B, aby rozpocząć, naciśnij poniższy
poniższych przycisków: przycisk:
<br />
</p> </p>
<div class="flex flex-row flex-wrap gap-2"> <NuxtLink to="/exam" class="text-4xl font-bold bg-fuchsia-200"
<button >START!</NuxtLink
class="btn btn-xl btn-secondary"
v-for="category in categories"
@click="setAndGo(category)"
> >
{{ category }}
</button>
</div>
</div>
<Loading v-else />
</div> </div>
</template> </template>

View file

@ -1,147 +0,0 @@
<script setup lang="ts">
import { ModalsContainer, useModal } from "vue-final-modal";
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?.liczba_pkt ?? 0;
}
});
examStore.result.advanced.forEach((answer) => {
if (answer.chosen_is_correct) {
points.value += answer.question?.liczba_pkt ?? 0;
}
});
const resultTrueFalse = ref(points.value >= 68 ? "pozytywny" : "negatywny");
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;
}
});
const tak_nie_model = computed(() =>
resultQuestionBasic.value?.chosen_answer.toLowerCase()
);
const abc_model = computed(() =>
resultQuestionAdvanced.value?.chosen_answer.toLowerCase()
);
const media = computed(() => {
const mediaSplit = question.value?.media?.split(".");
return {
fileType: mediaSplit?.pop()?.toLowerCase(),
fileName: mediaSplit?.join("."),
ogName: question.value?.media,
};
});
const { open, close } = useModal({
component: ResultModal,
attrs: {
title: "Egzamin teorytyczny",
onClose() {
close();
},
onHomepage() {
return navigateTo("/");
},
onNewExam() {
return navigateTo("/exam");
},
},
slots: {
points: `${points.value}`,
resultTrueFalse: resultTrueFalse,
},
});
open();
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;
}
}
function nav(route: string) {
return navigateTo(route);
}
</script>
<template>
<div>
<ModalsContainer />
<div>
<div class="grid grid-cols-4 min-h-dvh">
<div class="col-span-3 flex flex-col gap-4">
<TopBar
:points="question?.liczba_pkt"
:category="examStore.category"
/>
<Media :media="media" />
<BasicQuestionBlock
v-if="now == 'basic'"
:question="questionBasic"
v-model="tak_nie_model"
class="select-none z-[-1]"
/>
<AdvancedQuestionBlock
v-else-if="now == 'advanced'"
:question="questionAdvanced"
v-model="abc_model"
class="select-none z-[-1]"
/>
</div>
<RightBarResult
:result="examStore.result"
:question="question"
:question-basic="questionBasic"
:question-advanced="questionAdvanced"
:count-basic="countBasic"
:count-advanced="countAdvanced"
:now="now"
@change-now="changeNow"
@change-count="changeCount"
@nav="nav"
/>
</div>
</div>
</div>
</template>

View file

@ -1,7 +0,0 @@
import { createVfm } from "vue-final-modal";
export default defineNuxtPlugin((nuxtApp) => {
const vfm = createVfm() as any;
nuxtApp.vueApp.use(vfm);
});

1720
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

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,13 +1,14 @@
import "dotenv/config"; import "dotenv/config";
import { drizzle } from "drizzle-orm/node-postgres"; import { drizzle } from "drizzle-orm/node-postgres";
import { dane, punkty } from "@/src/db/schema"; import { dane, punkty } from "@/src/db/schema";
import { sql, eq, and, or, isNotNull } from "drizzle-orm"; import { sql, eq, isNotNull, and } from "drizzle-orm";
import { AdvancedQuestion } from "~/types"; import { AdvancedQuestion } from "~/types/basic";
import arrayShuffle from "array-shuffle";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
async function getFromDb(points: number | string) { const query = getQuery(event);
return await db const amount = query?.amount;
const db = drizzle(process.env.DATABASE_URL!);
const questions: AdvancedQuestion[] = await db
.select({ .select({
id: dane.id, id: dane.id,
nr_pytania: dane.nr_pytania, nr_pytania: dane.nr_pytania,
@ -49,63 +50,24 @@ export default defineEventHandler(async (event) => {
isNotNull(dane.odp_c_eng), isNotNull(dane.odp_c_eng),
isNotNull(dane.odp_a_de), isNotNull(dane.odp_a_de),
isNotNull(dane.odp_b_de), isNotNull(dane.odp_b_de),
isNotNull(dane.odp_c_de), isNotNull(dane.odp_c_de)
eq(punkty.liczba_pkt, +points)
) )
// 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 query = getQuery(event);
const category = query.category;
const categories = [
"A",
"B",
"C",
"D",
"T",
"AM",
"A1",
"A2",
"B1",
"C1",
"D1",
"PT",
];
if (category == null || category == "") {
throw new Error(
"category argument has to be string (or not to be defined at all)"
);
}
if (!categories.includes(`${category}`)) {
throw new Error(`category argument has to be equal to: ${categories}`);
}
const db = drizzle(process.env.DATABASE_URL!);
const randomizedQuestions: AdvancedQuestion[] = [];
for (let [key, value] of Object.entries({ 1: 2, 2: 4, 3: 6 })) {
const questionsKeyPoints: AdvancedQuestion[] = await getFromDb(key);
const chosenRandomQuestions: AdvancedQuestion[] = [];
const randoms: Array<number> = []; const randoms: Array<number> = [];
for (let j = 0; j < value; j++) { const randomizedQuestions = [];
let randomized = for (let i = 0; i < +(amount ?? 12); i++) {
Math.floor(Math.random() * (questionsKeyPoints.length - 1 + 1)) + 0; let randomized = Math.floor(Math.random() * (questions.length - 1 + 1)) + 0;
while (randoms.includes(randomized)) { while (randoms.includes(randomized)) {
randomized = randomized = Math.floor(Math.random() * (questions.length - 1 + 1)) + 0;
Math.floor(Math.random() * (questionsKeyPoints.length - 1 + 1)) + 0;
} }
randoms.push(randomized); randoms.push(randomized);
if ( if (questions[randomized].kategorie?.split(",").includes("B")) {
questionsKeyPoints[randomized].kategorie randomizedQuestions.push(questions[randomized]);
.split(",")
.includes(`${category}`)
) {
chosenRandomQuestions.push(questionsKeyPoints[randomized]);
} else { } else {
j--; i--;
} }
} }
chosenRandomQuestions.forEach((q) => randomizedQuestions.push(q)); return randomizedQuestions;
}
return arrayShuffle(randomizedQuestions);
}); });

View file

@ -2,7 +2,7 @@ import "dotenv/config";
import { drizzle } from "drizzle-orm/node-postgres"; import { drizzle } from "drizzle-orm/node-postgres";
import { dane, punkty } from "@/src/db/schema"; import { dane, punkty } from "@/src/db/schema";
import { sql, eq, and, or } from "drizzle-orm"; import { sql, eq, and, or } from "drizzle-orm";
import { BasicQuestion } from "~/types"; import { BasicQuestion } from "~/types/basic";
import arrayShuffle from "array-shuffle"; import arrayShuffle from "array-shuffle";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
@ -35,28 +35,11 @@ export default defineEventHandler(async (event) => {
} }
const query = getQuery(event); const query = getQuery(event);
const category = query.category; const category = query.category;
const categories = [ if (typeof category != "undefined" && typeof category != "string") {
"A",
"B",
"C",
"D",
"T",
"AM",
"A1",
"A2",
"B1",
"C1",
"D1",
"PT",
];
if (category == null || category == "") {
throw new Error( throw new Error(
"category argument has to be string (or not to be defined at all)" "category argument has to be string (or not to be defined at all)"
); );
} }
if (!categories.includes(`${category}`)) {
throw new Error(`category argument has to be equal to: ${categories}`);
}
const db = drizzle(process.env.DATABASE_URL!); const db = drizzle(process.env.DATABASE_URL!);
const randomizedQuestions: BasicQuestion[] = []; const randomizedQuestions: BasicQuestion[] = [];
@ -77,7 +60,7 @@ export default defineEventHandler(async (event) => {
if ( if (
questionsKeyPoints[randomized].kategorie questionsKeyPoints[randomized].kategorie
.split(",") .split(",")
.includes(`${category}`) .includes(category ?? "B")
) { ) {
chosenRandomQuestions.push(questionsKeyPoints[randomized]); chosenRandomQuestions.push(questionsKeyPoints[randomized]);
} else { } else {

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,7 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
plugins: [require("daisyui")],
daisyui: {
themes: ["light", "dark"],
},
};

View file

@ -2,13 +2,28 @@ export interface BasicQuestion {
id: number; id: number;
nr_pytania: number; nr_pytania: number;
pytanie: string; pytanie: string;
// odp_a: null;
// odp_b: null;
// odp_c: null;
poprawna_odp: string; poprawna_odp: string;
media: string | null; media: string | null;
kategorie: string; kategorie: string;
nazwa_media_pjm_tresc_pyt: string | null; 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; pytanie_eng: string;
// odp_a_eng: string | null;
// odp_b_eng: string | null;
// odp_c_eng: string | null;
pytanie_de: string; pytanie_de: string;
// odp_a_de: string | null;
// odp_b_de: string | null;
// odp_c_de: string | null;
pytanie_ua: string | null; pytanie_ua: string | null;
// odp_a_ua: string | null;
// odp_b_ua: string | null;
// odp_c_ua: string | null;
liczba_pkt: number; liczba_pkt: number;
} }
@ -29,15 +44,3 @@ export interface AdvancedQuestion extends BasicQuestion {
odp_b_ua: string | null; odp_b_ua: string | null;
odp_c_ua: string | null; odp_c_ua: string | null;
} }
export interface ResultType<T> {
question: T | undefined;
chosen_answer: string;
chosen_is_correct: boolean | undefined;
liczba_pkt: number | undefined;
}
export interface ResultEndType {
basic: ResultType<BasicQuestion>[];
advanced: ResultType<AdvancedQuestion>[];
}