Compare commits

..

4 commits

Author SHA1 Message Date
NetMan
c05db22b6b add keybinds to btns in exam 2025-12-15 21:51:54 +01:00
NetMan
59497e8b01 exam: warning on refresh and end-exam
warning on end-exam if it isn't the last question - otherwise exam ends without warning
2025-12-15 21:05:54 +01:00
NetMan
97b8d5dab9 question timers, little fixes to nav/store 2025-12-15 20:14:54 +01:00
NetMan
63121da4b7 postgres -> sqlite, pinia/middleware fix?
other smaller: little type fixes, little changes to ui - more readable category chooser
- result screen with correct, incorrect and chosen answers
little changes to readme
2025-12-13 16:07:23 +01:00
28 changed files with 1052 additions and 338 deletions

View file

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

View file

@ -1,5 +1,48 @@
# nuxt-prawo-jazdy
## 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 latest files should be there
The newest at the moment of me writing this (December 13th 2025) are the (visualisations for questions from November 2025)[https://www.gov.pl/pliki/mi/pytania_egzaminacyjne_na_prawo_jazdy_11_2025.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] better answer click recognition
- [x] beautify website (good for now)
- [x] <b>Fixed?</b> Needs testing, but should be fine question-mark? - fix pinia middleware between pages, MAJOR ISSUE - finishing exam sometimes redirects to homepage instead of results
- [x] question timers
- [x] exam (& results?) warning leave message on exit and timer end (and definitely on refresh)
- [x] add keybinds:
- S - start
- D - nast.pyt
- X - koniec egzaminu (na pewno chcesz zakonczyc egzamin?)
- T - Tak
- N - Nie
- A - A
- B - B
- C - C
- [ ] i18n - pl, en, de, ua (not all questions are available in ua, api handle)
- UI i18n
- db: examstore add language field, api handle languages
- [ ] db: (revise) script for processing, (revise and) share appropriate files
- [ ] clean up js code in exam.vue and result.vue (currently a little bit of a mess)
## Some information about the project
My intention is, to share access to test exams free of charge, you don't have to pay me - although you can, I greatly appreciate if you donate!
In the future I will host this project publicly `aaS`, and will probably put non-invasive, privacy friendly ads if it gains enough traction
All data used by this software is public information by definition provided in the Polish Constitution - (article 61.)[https://www.sejm.gov.pl/prawo/konst/polski/kon1.htm], and can be acquired by either checking above links on the gov website, or by writing to the Ministry ((if something happened to be missing))[placeholder_for_post_about_missing_points_column]
This project is a website mimicking an official driver's license theoritical exam (for different license categories) with a seperate media server, connected using drizzle ORM to a SQLite database
## Setup
This project utilizes `pnpm`, thus it is recommended
@ -8,32 +51,6 @@ 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
## Development Server
Start the development server on `http://localhost:3000`:

View file

@ -3,3 +3,9 @@
<NuxtPage />
</div>
</template>
<style>
.outline-set-solid {
outline-style: solid;
}
</style>

View file

@ -12,3 +12,33 @@ export default [
'D1',
'PT',
];
export const opis = [
'motocykle bez ograniczeń mocy',
'⭐ samochody osobowe do 3,5 t',
'pojazdy ciężarowe powyżej 3,5 t',
'autobusy',
'ciągniki rolnicze i pojazdy wolnobieżne',
'motorowery i lekkie czterokołowce',
'motocykle do 125 cm³ i 11 kW',
'motocykle do 35 kW',
'czterokołowce (np. quady)',
'pojazdy od 3,5 t do 7,5 t',
'autobusy do 16 pasażerów',
'tramwaje',
];
export const wiek = [
'(24 lata; lub 20 lat jeśli masz kat. A2 min. 2 lata)',
'(18 lat)',
'(21 lat; lub 18 lat z kwalifikacją wstępną)',
'(24 lata; lub 21 lat z kwalifikacją wstępną)',
'(16 lat)',
'(14 lat)',
'(16 lat)',
'(18 lat)',
'(16 lat)',
'(18 lat)',
'(21 lat; lub 18 lat z kwalifikacją wstępną)',
'(21 lat)',
];

29
components/EndModal.vue Normal file
View file

@ -0,0 +1,29 @@
<script setup lang="ts">
const props = defineProps<{ showModal: boolean }>();
const endModal = useTemplateRef('endModal');
const emit = defineEmits(['hideEndModal', 'endExam']);
watchEffect(() => {
if (props.showModal && endModal.value?.open == false) {
endModal.value?.showModal();
emit('hideEndModal');
}
});
</script>
<template>
<dialog
ref="endModal"
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]">Koniec egzaminu</h1>
<div class="*:inline">Czy na pewno chcesz zakończyć egzamin?</div>
<div class="flex flex-row gap-2 justify-around">
<div class="btn btn-lg btn-success" @click="emit('endExam')">Tak</div>
<div class="btn btn-lg btn-error" @click="endModal?.close()">Nie</div>
</div>
</div>
</dialog>
</template>

View file

@ -6,8 +6,11 @@ const cdnUrl = runtimeConfig.public.cdn_url;
const route = useRoute();
const emit = defineEmits(['mediaload']);
const props = defineProps<{
mediaPath: string | null | undefined;
phase: string;
}>();
const media = computed(() => {
@ -21,6 +24,21 @@ const media = computed(() => {
}
return { name: dotSplit?.join('.'), type };
});
const video = ref();
function onVideoLoad() {
if (route.path === '/exam') {
video.value.play();
let duration = video.value.duration;
if (isNaN(duration) || duration == Infinity) {
duration = 0;
}
setTimeout(() => {
emit('mediaload');
}, duration * 1000);
}
}
</script>
<template>
@ -28,23 +46,26 @@ const media = computed(() => {
class="select-none flex-auto w-full *:object-contain *:w-full *:h-full *:max-h-full relative *:absolute"
:class="route.path === '/exam' ? 'z-[-1]' : ''"
>
<img v-if="phase == 'set-basic'" src="/placeholder.svg" alt="placeholder" />
<NuxtImg
v-if="media.type === 'image'"
v-else-if="media.type === 'image'"
:key="`${mediaPath}-image`"
provider="selfhost"
:src="'/' + mediaPath"
:alt="mediaPath ?? ''"
@load="emit('mediaload')"
/>
<video
v-else-if="media.type === 'video'"
:key="`${mediaPath}-video`"
:autoplay="route.path === '/exam'"
ref="video"
:controls="route.path === '/result'"
@canplaythrough="onVideoLoad()"
>
<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
>Pytanie bez wizualizacji</span
>
</div>
</template>

View file

@ -4,6 +4,8 @@ const myModal = useTemplateRef('myModal');
onMounted(() => {
myModal.value?.showModal();
});
defineEmits(['again', 'home']);
</script>
<template>
@ -19,10 +21,12 @@ onMounted(() => {
<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">
<div class="btn btn-soft" @click="$emit('home')">
Wróć na stronę główną
</div>
<div class="btn btn-outline" @click="$emit('again')">
Rozpocznij jeszcze raz
</NuxtLink>
</div>
<button class="btn btn-neutral" @click="myModal?.close()">
Przejrzyj odpowiedzi
</button>

View file

@ -1,22 +1,59 @@
<script setup lang="ts">
defineProps<{
import { onKeyStroke } from '@vueuse/core';
const props = defineProps<{
countBasic: number;
countAdvanced: number;
now: string | null | undefined;
ending: boolean;
setBasic: boolean;
time: number;
phase: string;
}>();
const emit = defineEmits<{
endExam: [];
nextQuestion: [];
nextTime: [];
showEndModal: [];
}>();
function tryEndExam() {
if (props.ending == false) {
emit('showEndModal');
} else {
emit('endExam');
}
}
const startButton = useTemplateRef('start-button');
onKeyStroke(['S', 's'], () => {
startButton.value?.click();
});
const nextButton = useTemplateRef('next-button');
onKeyStroke(['D', 'd'], () => {
nextButton.value?.click();
});
const endButton = useTemplateRef('end-button');
onKeyStroke(['X', 'x'], () => {
endButton.value?.click();
});
</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')">
<button
ref="end-button"
class="btn btn-warning btn-xl"
@click="tryEndExam()"
>
Zakończ egzamin
</button>
@ -36,38 +73,90 @@ const emit = defineEmits<{
</CurrentQuestionCount>
</div>
<div class="text-center text-xl flex flex-col gap-2">
<div
v-if="phase == 'set-basic'"
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
ref="start-button"
class="btn btn-primary"
@click="emit('nextTime')"
>
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>
:value="time"
max="20"
></progress>
<span class="block set-translate z-10 text-black text-2xl">
{{ time >= 0 ? time : 0 }}s
</span>
</div>
</div>
</div>
<div class="text-center text-xl flex flex-col gap-2">
<div v-else 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>
:value="time"
:max="phase == 'start-basic' ? 15 : 45"
></progress>
<span class="block set-translate z-10 text-black text-2xl">
{{ time >= 0 ? time : 0 }}s
</span>
</div>
</div>
<div class="flex-1" />
<div class="flex-1">
<span class="text-xl">Skróty klawiszowe</span>
<table class="table table-sm">
<tbody>
<tr>
<td>S</td>
<td>niebieski przycisk start</td>
</tr>
<tr>
<td>D</td>
<td>następne pytanie</td>
</tr>
<tr>
<td>X</td>
<td>zakończ egzamin</td>
</tr>
<tr>
<td>T / Y</td>
<td>Tak</td>
</tr>
<tr>
<td>N</td>
<td>Nie</td>
</tr>
<tr>
<td>A</td>
<td>A</td>
</tr>
<tr>
<td>B</td>
<td>B</td>
</tr>
<tr>
<td>C</td>
<td>C</td>
</tr>
</tbody>
</table>
</div>
<button
ref="next-button"
class="btn btn-warning btn-xl"
:disabled="ending"
:disabled="ending || setBasic"
@click="emit('nextQuestion')"
>
Następne pytanie
@ -76,16 +165,12 @@ const emit = defineEmits<{
</template>
<style>
/*.progressive {
animation: progressZapoznanie 20s linear;
.progressive {
width: calc(100% / v-bind('time'));
transition: all 1s linear;
}
@keyframes progressZapoznanie {
0% {
width: 0;
}
100% {
width: 100%;
}
}*/
progress[value]::-webkit-progress-value {
transition: width 0.5s;
}
</style>

View file

@ -11,6 +11,8 @@ defineProps<{
const emit = defineEmits<{
changeNow: [value: string];
changeCount: [num: number];
again: [];
home: [];
}>();
</script>
@ -18,9 +20,9 @@ const emit = defineEmits<{
<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">
<div class="btn btn-warning btn-xl" @click="$emit('home')">
Wróć na stronę główną
</NuxtLink>
</div>
<button class="btn btn-info btn-lg" @click="emit('changeNow', 'basic')">
Pytania podstawowe
@ -37,10 +39,12 @@ const emit = defineEmits<{
class="btn btn-md"
name="chooser"
:class="`${
result.basic[num].question?.correct_answer ===
result.basic[num].chosen_answer
? 'btn-success'
: 'btn-error'
result.basic[num].chosen_answer == ''
? 'btn-warning'
: 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="
@ -65,10 +69,12 @@ const emit = defineEmits<{
class="btn btn-md"
name="chooser"
:class="`${
result.advanced[num].question?.correct_answer ===
result.advanced[num].chosen_answer
? 'btn-success'
: 'btn-error'
result.advanced[num].chosen_answer == ''
? 'btn-warning'
: 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="
@ -81,14 +87,8 @@ const emit = defineEmits<{
<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">
<div class="btn btn-warning btn-xl" @click="$emit('again')">
Rozpocznij jeszcze raz
</NuxtLink>
</div>
</div>
</template>
<style scoped>
.outline-set-solid {
outline-style: solid;
}
</style>

View file

@ -1,9 +1,36 @@
<script lang="ts" setup>
defineProps<{
question: AdvancedQuestion | undefined;
import { onKeyStroke } from '@vueuse/core';
const props = defineProps<{
question: Question;
phase: string;
}>();
const answer = defineModel<string | null | undefined>();
const aButton = useTemplateRef('A-button');
onKeyStroke(['A', 'a'], () => {
if (props.phase != 'result') {
aButton.value[0].click();
}
});
const bButton = useTemplateRef('B-button');
onKeyStroke(['B', 'b'], () => {
if (props.phase != 'result') {
bButton.value[0].click();
}
});
const cButton = useTemplateRef('C-button');
onKeyStroke(['C', 'c'], () => {
if (props.phase != 'result') {
cButton.value[0].click();
}
});
</script>
<template>
@ -16,14 +43,15 @@ const answer = defineModel<string | null | undefined>();
<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,
A: (question as AdvancedQuestion)?.answer_a,
B: (question as AdvancedQuestion)?.answer_b,
C: (question as AdvancedQuestion)?.answer_c,
})"
:key="`btn_answer_${element}_${value}`"
>
<input
:id="`odp_${element}`"
:ref="`${element}-button`"
v-model="answer"
type="radio"
name="abc"
@ -32,7 +60,13 @@ const answer = defineModel<string | null | undefined>();
/>
<label :for="`odp_${element}`">
<div
:class="answer === element ? ' !btn-secondary' : ''"
:class="
phase == 'exam'
? answer === element
? ' !btn-secondary'
: ''
: `${answer === element ? 'outline-set-solid outline-2' : ''} ${element == question?.correct_answer ? 'btn-success' : 'btn-error'}`
"
class="btn btn-primary btn-lg"
>
{{ element }}

View file

@ -1,9 +1,28 @@
<script lang="ts" setup>
defineProps<{
question: BasicQuestion | undefined;
import { onKeyStroke } from '@vueuse/core';
const props = defineProps<{
question: Question;
phase: string;
}>();
const answer = defineModel<string | null | undefined>();
const yesButton = useTemplateRef('true-button');
onKeyStroke(['T', 't', 'Y', 'y'], () => {
if (props.phase != 'result') {
yesButton.value[0].click();
}
});
const noButton = useTemplateRef('false-button');
onKeyStroke(['N', 'n'], () => {
if (props.phase != 'result') {
noButton.value[0].click();
}
});
</script>
<template>
@ -18,6 +37,7 @@ const answer = defineModel<string | null | undefined>();
<input
v-for="[element, value] of Object.entries({ TAK: true, NIE: false })"
:id="`odp_${element}`"
:ref="`${value}-button`"
:key="`btn_answer_${element}`"
v-model="answer"
type="radio"
@ -26,11 +46,21 @@ const answer = defineModel<string | null | undefined>();
class="btn btn-primary btn-xl"
:aria-label="element"
:class="
answer == null
? false
: answer === value.toString()
? '!btn-secondary'
: ''
phase == 'exam'
? answer == null
? false
: answer === value.toString()
? '!btn-secondary'
: ''
: `${
answer === value.toString()
? 'outline-set-solid outline-2'
: ''
} ${
question?.correct_answer?.toString() == value.toString()
? ' btn-success'
: ' btn-error'
}`
"
:checked="answer == null ? false : answer === value.toString()"
/>

BIN
db/database.db Normal file

Binary file not shown.

36
db/schema.ts Normal file
View file

@ -0,0 +1,36 @@
import { sqliteTable, text, int } from 'drizzle-orm/sqlite-core';
export const tasks = sqliteTable('tasks', {
id: int().notNull(),
correct_answer: text(),
media_url: text(),
weight: int(),
});
export const questions = sqliteTable('questions', {
task_id: int(),
lang: text(),
text: text(),
});
export const tasks_advanced = sqliteTable('tasks_advanced', {
id: int().notNull(),
correct_answer: text(),
media_url: text(),
weight: int(),
});
export const questions_advanced = sqliteTable('questions_advanced', {
task_id: int(),
lang: text(),
text: text(),
answer_a: text(),
answer_b: text(),
answer_c: text(),
});
export const categories_db = sqliteTable('categories', {
name: text(),
task_id: int(),
});

View file

@ -2,9 +2,9 @@ import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './src/db/schema.ts',
out: './drizzle',
schema: './db/schema.ts',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DATABASE_URL!,
},

View file

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

View file

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

View file

@ -6,10 +6,11 @@ export default defineNuxtConfig({
'@nuxtjs/tailwindcss',
'@nuxt/fonts',
'@pinia/nuxt',
'pinia-plugin-persistedstate/nuxt',
'@nuxt/eslint',
'@nuxt/image',
],
ssr: true,
ssr: false,
imports: {
dirs: ['types/*.ts', 'store/*.ts', 'types/**/*.ts'],
},
@ -46,4 +47,7 @@ export default defineNuxtConfig({
},
provider: 'selfhost',
},
pinia: {
storesDirs: ['./store/**'],
},
});

View file

@ -14,10 +14,12 @@
"tsc": "nuxi typecheck"
},
"dependencies": {
"@libsql/client": "^0.15.15",
"@nuxt/fonts": "0.11.1",
"@nuxt/image": "1.10.0",
"@nuxtjs/tailwindcss": "6.13.2",
"@pinia/nuxt": "0.11.0",
"@vueuse/core": "^14.1.0",
"array-shuffle": "^3.0.0",
"daisyui": "^5.0.27",
"date-fns": "^4.1.0",
@ -27,8 +29,8 @@
"eslint": "^9.24.0",
"lodash": "^4.17.21",
"nuxt": "~3.16.2",
"pg": "^8.14.1",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.7.1",
"ufo": "^1.6.1",
"vue": "latest",
"vue-router": "latest"
@ -37,7 +39,6 @@
"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",

22
pages/anomaly.vue Normal file
View file

@ -0,0 +1,22 @@
<script setup lang="ts">
const examStore = useExamStore();
const basicStore = useBasicStore();
const advancedStore = useAdvancedStore();
</script>
<template>
<div class="flex flex-col gap-2 items-start m-2">
<h1 class="text-2xl">Nastąpiła anomalia</h1>
<br />
Przekierowanie z: {{ useRoute().redirectedFrom ?? '""' }} <br />
Kategoria:
{{ examStore.category != '' ? examStore.category : '""' }}
<br />
Koniec: {{ examStore.end }} <br />
Pytania podstawowe: <code class="text-xs">{{ basicStore.basic }}</code>
<br />
Pytania specjalistyczne:
<code class="text-xs">{{ advancedStore.advanced }}</code> <br />
<NuxtLink to="/" class="btn btn-primary"> Wróć na stronę główną </NuxtLink>
</div>
</template>

View file

@ -1,32 +1,92 @@
<script lang="ts" setup>
import { intervalToDuration, addMinutes, addSeconds, isEqual } from 'date-fns';
import { intervalToDuration, addMinutes, addSeconds, isAfter } from 'date-fns';
import { useNow } from '@vueuse/core';
import { useExamStore } from '~/store/examStore';
definePageMeta({ middleware: ['exam'] });
definePageMeta({ middleware: 'exam' });
useHead({
title: 'Pytanie 1/20',
});
const examStore = useExamStore();
const basicStore = useBasicStore();
const advancedStore = useAdvancedStore();
await callOnce(() => examStore.mildReset(), { mode: 'navigation' });
const nowTime = ref(new Date());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function preventRefresh(e: any) {
e.preventDefault();
e.returnValue = ''; // polyfill for older chrome
}
const now = ref('basic');
const nowTime = useNow();
const timeEnd = addMinutes(new Date(), 25);
const timeRemainingTotal = computed(() =>
intervalToDuration({
start: nowTime.value,
end: timeEnd,
const questionEnd = ref(addSeconds(new Date(), 20));
const mediaLoaded = ref(false);
const time = ref({
total: computed(() =>
intervalToDuration({
start: nowTime.value,
end: timeEnd,
}),
),
question: computed(() => {
const interval = intervalToDuration({
start: nowTime.value,
end: questionEnd.value,
});
if (Object.hasOwn(interval, 'seconds') && interval.seconds != undefined) {
return interval.seconds;
} else {
return 0;
}
}),
);
// const timeRemainingQuestion - to implement
phase: 'set-basic',
});
function changeQuestionTimeAfterNext() {
if (time.value.phase == 'set-basic') {
time.value.phase = 'start-basic';
questionEnd.value = addSeconds(new Date(), 15);
} else if (time.value.phase == 'start-basic') {
if (now.value == 'basic') {
time.value.phase = 'set-basic';
mediaLoaded.value = false;
questionEnd.value = addSeconds(new Date(), 20);
} else {
time.value.phase = 'set-advanced';
questionEnd.value = addSeconds(new Date(), 50);
}
} else if (time.value.phase == 'set-advanced') {
questionEnd.value = addSeconds(new Date(), 50);
}
}
function onMediaLoad() {
mediaLoaded.value = true;
}
function clickNext() {
if (ending.value) {
endExam();
}
if (time.value.phase != 'set-basic') {
next();
}
changeQuestionTimeAfterNext();
}
onMounted(() => {
const endInterval = setInterval(() => {
nowTime.value = addSeconds(nowTime.value, 1);
if (isEqual(nowTime.value, timeEnd)) {
clearInterval(endInterval);
endExam();
}
}, 1000);
useHead({
title: 'Pytanie 1/20',
});
window.addEventListener('beforeunload', preventRefresh);
watchEffect(() => {
if (isAfter(nowTime.value, timeEnd)) endExam();
});
watchEffect(() => {
if (now.value === 'basic')
@ -34,9 +94,20 @@ onMounted(() => {
if (now.value === 'advanced')
useHead({ title: `Pytanie ${countAdvanced.value + 1}/12` });
});
watchEffect(() => {
if (mediaLoaded.value == false && time.value.phase == 'start-basic') {
questionEnd.value = addSeconds(new Date(), 15);
}
if (time.value.question < 0 && ending.value == false) {
clickNext();
}
});
});
const examStore = useExamStore();
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', preventRefresh);
});
const {
data: dataBasic,
@ -61,7 +132,6 @@ const {
const countBasic = ref(0);
const countAdvanced = ref(-1);
const now = ref('basic');
const answer = ref<string>('');
const ending = ref(false);
@ -84,28 +154,30 @@ const result: Ref<ResultEndType> = ref({
advanced: [],
});
async function next() {
if (now.value === 'basic' || now.value === 'advanced') {
result.value[now.value].push({
function next() {
if (examStore.end == false) {
result.value[now.value as keyof ResultEndType].push({
question: question.value,
chosen_answer: answer.value,
chosen_is_correct: answer.value === question.value?.correct_answer,
correct_answer: question.value?.correct_answer ?? null,
chosen_is_correct: answer.value == question.value?.correct_answer,
});
answer.value = '';
} else {
console.error('next() incorrectly executed - exam has already ended.');
}
answer.value = '';
if (now.value === 'basic') {
if (countBasic.value < 19) {
if (countBasic.value + 1 <= 19) {
countBasic.value++;
} else {
now.value = 'advanced';
countAdvanced.value++;
}
} else if (now.value === 'advanced') {
if (countAdvanced.value < 11) {
if (countAdvanced.value + 1 <= 11) {
countAdvanced.value++;
}
if (countAdvanced.value >= 11) {
if (countAdvanced.value + 1 >= 12) {
ending.value = true;
}
}
@ -113,20 +185,30 @@ async function next() {
function endExam() {
loading.value = true;
while (!ending.value) {
next();
while (ending.value == false) {
if (time.value.phase != 'set-basic') {
next();
}
changeQuestionTimeAfterNext();
}
next();
examStore.setResult(result.value);
basicStore.set(result.value.basic);
advancedStore.set(result.value.advanced);
examStore.setEnd(true);
while (true) {
if (examStore.result == result.value && examStore.end) {
return navigateTo('/result', { replace: true });
}
if (
basicStore.basic == result.value.basic &&
advancedStore.advanced == result.value.advanced &&
examStore.end
) {
return navigateTo(`/result`, { replace: true });
} else {
return navigateTo(`/anomaly`);
}
}
const loading = ref(false);
const showEndModal = ref(false);
</script>
<template>
@ -139,28 +221,38 @@ const loading = ref(false);
<BarTop
:points="question?.weight"
:category="examStore.category"
:time-remaining="timeRemainingTotal"
:time-remaining="time.total"
/>
<MediaBox
:media-path="question?.media_url"
:phase="time.phase"
@mediaload="onMediaLoad()"
/>
<MediaBox :media-path="question?.media_url" />
<QuestionBasic
v-if="now === 'basic'"
v-model="answer"
phase="exam"
:question="questionBasic"
/>
<QuestionAdvanced
v-else-if="now === 'advanced'"
v-model="answer"
phase="exam"
:question="questionAdvanced"
/>
</div>
<BarRightExam
:count-basic="countBasic"
:count-advanced="countAdvanced"
:now="now"
:ending="ending"
@next-question="next()"
:set-basic="time.phase == 'set-basic'"
:time="time.question"
:phase="time.phase"
@show-end-modal="showEndModal = true"
@next-question="clickNext()"
@end-exam="endExam()"
@next-time="changeQuestionTimeAfterNext()"
/>
</div>
</div>
@ -168,5 +260,10 @@ const loading = ref(false);
An API error occurred: {{ errorBasic }} {{ errorAdvanced }}
</div>
<LoadingScreen v-else />
<EndModal
:show-modal="showEndModal"
@hide-end-modal="showEndModal = false"
@end-exam="endExam()"
/>
</div>
</template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import categories from '~/categories';
import categories, { opis, wiek } from '~/categories';
onMounted(() => {
useHead({
@ -10,6 +10,7 @@ onMounted(() => {
const loading = ref(false);
const examStore = useExamStore();
await callOnce(() => examStore.resetExam(), { mode: 'navigation' });
function setAndGo(category: string) {
loading.value = true;
@ -24,22 +25,24 @@ function setAndGo(category: string) {
<template>
<div>
<div v-if="!loading" class="text-3xl">
<div v-if="!loading" class="text-3xl m-2 flex flex-col gap-2">
<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"
<div
class="flex flex-col flex-wrap gap-2 items-start p-4 bg-slate-100 border-1 border-slate-500 rounded-xl w-fit"
>
<div
v-for="[index, category] of categories.entries()"
:key="`btn-${category}`"
class="btn btn-xl btn-secondary"
@click="setAndGo(category)"
class="flex flex-row gap-3 items-center"
>
{{ category }}
</button>
<button class="btn btn-xl btn-secondary" @click="setAndGo(category)">
{{ category }}
</button>
<div class="flex flex-col text-sm">
<div>{{ opis[index] }}</div>
<div>{{ wiek[index] }}</div>
</div>
</div>
</div>
</div>
<LoadingScreen v-else />

View file

@ -5,14 +5,19 @@ definePageMeta({ middleware: ['result'] });
const examStore = useExamStore();
const basicStore = useBasicStore();
const advancedStore = useAdvancedStore();
const loading = ref(false);
const points = ref<number>(0);
examStore.result.basic.forEach((answer) => {
basicStore.basic.forEach((answer) => {
if (answer.chosen_is_correct) {
points.value += answer.question?.weight ?? 0;
}
});
examStore.result.advanced.forEach((answer) => {
advancedStore.advanced.forEach((answer) => {
if (answer.chosen_is_correct) {
points.value += answer.question?.weight ?? 0;
}
@ -32,17 +37,17 @@ onMounted(() => {
const countBasic = ref(0);
const countAdvanced = ref(0);
const resultQuestionBasic = computed<ResultType<BasicQuestion> | undefined>(
() => examStore.result.basic.at(countBasic.value),
const resultQuestionBasic = computed<ResultType | undefined>(() =>
basicStore.basic.at(countBasic.value),
);
const resultQuestionAdvanced = computed<ResultType | undefined>(() =>
advancedStore.advanced.at(countAdvanced.value),
);
const resultQuestionAdvanced = computed<
ResultType<AdvancedQuestion> | undefined
>(() => examStore.result.advanced.at(countAdvanced.value));
const questionBasic = computed<BasicQuestion | undefined>(
const questionBasic = computed<Question>(
() => resultQuestionBasic.value?.question,
);
const questionAdvanced = computed<AdvancedQuestion | undefined>(
const questionAdvanced = computed<Question>(
() => resultQuestionAdvanced.value?.question,
);
@ -81,46 +86,67 @@ function changeCount(num: number) {
countAdvanced.value = num;
}
}
async function again() {
loading.value = true;
await examStore.mildReset();
return await navigateTo('/exam');
}
async function home() {
loading.value = true;
await examStore.resetExam();
return await navigateTo('/');
}
</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]"
/>
<LoadingScreen v-if="loading" />
<div v-else>
<ResultModal @again="again" @home="home">
<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" phase="" />
<QuestionBasic
v-if="now === 'basic'"
v-model="answer"
:question="questionBasic"
phase="result"
class="select-none z-[-1]"
/>
<QuestionAdvanced
v-else-if="now === 'advanced'"
v-model="answer"
:question="questionAdvanced"
phase="result"
class="select-none z-[-1]"
/>
</div>
<BarRightResult
:result="{
basic: basicStore.basic,
advanced: advancedStore.advanced,
}"
:count-basic="countBasic"
:count-advanced="countAdvanced"
:now="now"
@change-now="changeNow"
@change-count="changeCount"
@home="home"
@again="again"
>
<template #points>{{ points }}</template>
<template #resultTrueFalse>{{ resultTrueFalse }}</template>
</BarRightResult>
</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>

388
pnpm-lock.yaml generated
View file

@ -8,24 +8,30 @@ importers:
.:
dependencies:
'@libsql/client':
specifier: ^0.15.15
version: 0.15.15
'@nuxt/fonts':
specifier: 0.11.1
version: 0.11.1(db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)(magicast@0.3.5)(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))
version: 0.11.1(db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)(magicast@0.3.5)(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))
'@nuxt/image':
specifier: 1.10.0
version: 1.10.0(db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)(magicast@0.3.5)
version: 1.10.0(db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)(magicast@0.3.5)
'@nuxtjs/tailwindcss':
specifier: 6.13.2
version: 6.13.2(magicast@0.3.5)
'@pinia/nuxt':
specifier: 0.11.0
version: 0.11.0(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)))
'@vueuse/core':
specifier: ^14.1.0
version: 14.1.0(vue@3.5.13(typescript@5.8.3))
array-shuffle:
specifier: ^3.0.0
version: 3.0.0
daisyui:
specifier: ^5.0.23
version: 5.0.23
specifier: ^5.0.27
version: 5.2.3
date-fns:
specifier: ^4.1.0
version: 4.1.0
@ -37,7 +43,7 @@ importers:
version: 0.31.0
drizzle-orm:
specifier: ^0.42.0
version: 0.42.0(@types/pg@8.11.13)(pg@8.14.1)
version: 0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)
eslint:
specifier: ^9.24.0
version: 9.24.0(jiti@2.4.2)
@ -46,13 +52,13 @@ importers:
version: 4.17.21
nuxt:
specifier: ~3.16.2
version: 3.16.2(@parcel/watcher@2.5.1)(@types/node@22.14.1)(db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)))(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1))(eslint@9.24.0(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.29.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.40.0)(terser@5.39.0)(tsx@4.19.3)(typescript@5.8.3)(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(yaml@2.7.1)
pg:
specifier: ^8.14.1
version: 8.14.1
version: 3.16.2(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@22.14.1)(db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)))(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1))(eslint@9.24.0(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.29.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.40.0)(terser@5.39.0)(tsx@4.19.3)(typescript@5.8.3)(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(yaml@2.7.1)
pinia:
specifier: ^3.0.2
version: 3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
pinia-plugin-persistedstate:
specifier: ^4.7.1
version: 4.7.1(@nuxt/kit@3.16.2(magicast@0.3.5))(@pinia/nuxt@0.11.0(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))))(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)))
ufo:
specifier: ^1.6.1
version: 1.6.1
@ -69,9 +75,6 @@ importers:
'@types/lodash':
specifier: ^4.17.16
version: 4.17.16
'@types/pg':
specifier: ^8.11.13
version: 8.11.13
eslint-config-prettier:
specifier: ^10.1.2
version: 10.1.2(eslint@9.24.0(jiti@2.4.2))
@ -678,6 +681,67 @@ packages:
'@kwsites/promise-deferred@1.1.1':
resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==}
'@libsql/client@0.15.15':
resolution: {integrity: sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w==}
'@libsql/core@0.15.15':
resolution: {integrity: sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA==}
'@libsql/darwin-arm64@0.5.22':
resolution: {integrity: sha512-4B8ZlX3nIDPndfct7GNe0nI3Yw6ibocEicWdC4fvQbSs/jdq/RC2oCsoJxJ4NzXkvktX70C1J4FcmmoBy069UA==}
cpu: [arm64]
os: [darwin]
'@libsql/darwin-x64@0.5.22':
resolution: {integrity: sha512-ny2HYWt6lFSIdNFzUFIJ04uiW6finXfMNJ7wypkAD8Pqdm6nAByO+Fdqu8t7sD0sqJGeUCiOg480icjyQ2/8VA==}
cpu: [x64]
os: [darwin]
'@libsql/hrana-client@0.7.0':
resolution: {integrity: sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==}
'@libsql/isomorphic-fetch@0.3.1':
resolution: {integrity: sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==}
engines: {node: '>=18.0.0'}
'@libsql/isomorphic-ws@0.1.5':
resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==}
'@libsql/linux-arm-gnueabihf@0.5.22':
resolution: {integrity: sha512-3Uo3SoDPJe/zBnyZKosziRGtszXaEtv57raWrZIahtQDsjxBVjuzYQinCm9LRCJCUT5t2r5Z5nLDPJi2CwZVoA==}
cpu: [arm]
os: [linux]
'@libsql/linux-arm-musleabihf@0.5.22':
resolution: {integrity: sha512-LCsXh07jvSojTNJptT9CowOzwITznD+YFGGW+1XxUr7fS+7/ydUrpDfsMX7UqTqjm7xG17eq86VkWJgHJfvpNg==}
cpu: [arm]
os: [linux]
'@libsql/linux-arm64-gnu@0.5.22':
resolution: {integrity: sha512-KSdnOMy88c9mpOFKUEzPskSaF3VLflfSUCBwas/pn1/sV3pEhtMF6H8VUCd2rsedwoukeeCSEONqX7LLnQwRMA==}
cpu: [arm64]
os: [linux]
'@libsql/linux-arm64-musl@0.5.22':
resolution: {integrity: sha512-mCHSMAsDTLK5YH//lcV3eFEgiR23Ym0U9oEvgZA0667gqRZg/2px+7LshDvErEKv2XZ8ixzw3p1IrBzLQHGSsw==}
cpu: [arm64]
os: [linux]
'@libsql/linux-x64-gnu@0.5.22':
resolution: {integrity: sha512-kNBHaIkSg78Y4BqAdgjcR2mBilZXs4HYkAmi58J+4GRwDQZh5fIUWbnQvB9f95DkWUIGVeenqLRFY2pcTmlsew==}
cpu: [x64]
os: [linux]
'@libsql/linux-x64-musl@0.5.22':
resolution: {integrity: sha512-UZ4Xdxm4pu3pQXjvfJiyCzZop/9j/eA2JjmhMaAhe3EVLH2g11Fy4fwyUp9sT1QJYR1kpc2JLuybPM0kuXv/Tg==}
cpu: [x64]
os: [linux]
'@libsql/win32-x64-msvc@0.5.22':
resolution: {integrity: sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA==}
cpu: [x64]
os: [win32]
'@mapbox/node-pre-gyp@2.0.0':
resolution: {integrity: sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==}
engines: {node: '>=18'}
@ -686,6 +750,9 @@ packages:
'@napi-rs/wasm-runtime@0.2.8':
resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==}
'@neon-rs/load@0.0.4':
resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==}
'@netlify/functions@3.0.4':
resolution: {integrity: sha512-Ox8+ABI+nsLK+c4/oC5dpquXuEIjzfTlJrdQKgQijCsDQoje7inXFAtKDLvvaGvuvE+PVpMLwQcIUL6P9Ob1hQ==}
engines: {node: '>=18.0.0'}
@ -1209,6 +1276,12 @@ packages:
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@typescript-eslint/eslint-plugin@8.30.1':
resolution: {integrity: sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -1431,6 +1504,19 @@ packages:
'@vue/shared@3.5.13':
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
'@vueuse/core@14.1.0':
resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==}
peerDependencies:
vue: ^3.5.0
'@vueuse/metadata@14.1.0':
resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==}
'@vueuse/shared@14.1.0':
resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==}
peerDependencies:
vue: ^3.5.0
abbrev@3.0.1:
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
engines: {node: ^18.17.0 || >=20.5.0}
@ -1911,8 +1997,12 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
daisyui@5.0.23:
resolution: {integrity: sha512-SIR8yAneeNxvrpaR5kREG1DrPK8XFyfmyvqyZEUTJ2e6tv4Pp56/w+52Vdv34hmSmtAyDldTCEPWx+GvPyp0Yg==}
daisyui@5.2.3:
resolution: {integrity: sha512-sldBQUIFCsSPoF4LvoHhIi9GnvBX/3aZD9NoTOvpTSX8sDjO484wQx7yEvRyREMpn4rZMvQSKKskHAHdM8+B4Q==}
data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
@ -2021,6 +2111,10 @@ packages:
engines: {node: '>=0.10'}
hasBin: true
detect-libc@2.0.2:
resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
engines: {node: '>=8'}
detect-libc@2.0.3:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'}
@ -2423,6 +2517,10 @@ packages:
picomatch:
optional: true
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@ -2463,6 +2561,10 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
@ -2819,6 +2921,9 @@ packages:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
js-base64@3.7.8:
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -2915,6 +3020,10 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
libsql@0.5.22:
resolution: {integrity: sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA==}
os: [darwin, linux, win32]
lightningcss-darwin-arm64@1.29.2:
resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==}
engines: {node: '>= 12.0.0'}
@ -3209,6 +3318,11 @@ packages:
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
node-fetch-native@1.6.6:
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
@ -3221,6 +3335,10 @@ packages:
encoding:
optional: true
node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
node-forge@1.3.1:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
engines: {node: '>= 6.13.0'}
@ -3490,6 +3608,20 @@ packages:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
pinia-plugin-persistedstate@4.7.1:
resolution: {integrity: sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==}
peerDependencies:
'@nuxt/kit': '>=3.0.0'
'@pinia/nuxt': '>=0.10.0'
pinia: '>=3.0.0'
peerDependenciesMeta:
'@nuxt/kit':
optional: true
'@pinia/nuxt':
optional: true
pinia:
optional: true
pinia@3.0.2:
resolution: {integrity: sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==}
peerDependencies:
@ -3790,6 +3922,9 @@ packages:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
promise-limit@2.7.0:
resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==}
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
@ -4625,6 +4760,10 @@ packages:
typescript:
optional: true
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@ -5273,6 +5412,68 @@ snapshots:
'@kwsites/promise-deferred@1.1.1': {}
'@libsql/client@0.15.15':
dependencies:
'@libsql/core': 0.15.15
'@libsql/hrana-client': 0.7.0
js-base64: 3.7.8
libsql: 0.5.22
promise-limit: 2.7.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@libsql/core@0.15.15':
dependencies:
js-base64: 3.7.8
'@libsql/darwin-arm64@0.5.22':
optional: true
'@libsql/darwin-x64@0.5.22':
optional: true
'@libsql/hrana-client@0.7.0':
dependencies:
'@libsql/isomorphic-fetch': 0.3.1
'@libsql/isomorphic-ws': 0.1.5
js-base64: 3.7.8
node-fetch: 3.3.2
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@libsql/isomorphic-fetch@0.3.1': {}
'@libsql/isomorphic-ws@0.1.5':
dependencies:
'@types/ws': 8.18.1
ws: 8.18.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@libsql/linux-arm-gnueabihf@0.5.22':
optional: true
'@libsql/linux-arm-musleabihf@0.5.22':
optional: true
'@libsql/linux-arm64-gnu@0.5.22':
optional: true
'@libsql/linux-arm64-musl@0.5.22':
optional: true
'@libsql/linux-x64-gnu@0.5.22':
optional: true
'@libsql/linux-x64-musl@0.5.22':
optional: true
'@libsql/win32-x64-msvc@0.5.22':
optional: true
'@mapbox/node-pre-gyp@2.0.0':
dependencies:
consola: 3.4.2
@ -5293,6 +5494,8 @@ snapshots:
'@tybys/wasm-util': 0.9.0
optional: true
'@neon-rs/load@0.0.4': {}
'@netlify/functions@3.0.4':
dependencies:
'@netlify/serverless-functions-api': 1.36.0
@ -5480,7 +5683,7 @@ snapshots:
- utf-8-validate
- vite
'@nuxt/fonts@0.11.1(db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)(magicast@0.3.5)(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))':
'@nuxt/fonts@0.11.1(db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)(magicast@0.3.5)(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))':
dependencies:
'@nuxt/devtools-kit': 2.4.0(magicast@0.3.5)(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))
'@nuxt/kit': 3.16.2(magicast@0.3.5)
@ -5501,7 +5704,7 @@ snapshots:
ufo: 1.6.1
unifont: 0.1.7
unplugin: 2.3.2
unstorage: 1.15.0(db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)
unstorage: 1.15.0(db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@ -5525,7 +5728,7 @@ snapshots:
- uploadthing
- vite
'@nuxt/image@1.10.0(db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)(magicast@0.3.5)':
'@nuxt/image@1.10.0(db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)(magicast@0.3.5)':
dependencies:
'@nuxt/kit': 3.16.2(magicast@0.3.5)
consola: 3.4.2
@ -5538,7 +5741,7 @@ snapshots:
std-env: 3.9.0
ufo: 1.6.1
optionalDependencies:
ipx: 2.1.0(db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)
ipx: 2.1.0(db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@ -5999,9 +6202,16 @@ snapshots:
'@types/node': 22.14.1
pg-protocol: 1.8.0
pg-types: 4.0.2
optional: true
'@types/resolve@1.20.2': {}
'@types/web-bluetooth@0.0.21': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 22.14.1
'@typescript-eslint/eslint-plugin@8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
@ -6295,6 +6505,19 @@ snapshots:
'@vue/shared@3.5.13': {}
'@vueuse/core@14.1.0(vue@3.5.13(typescript@5.8.3))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 14.1.0
'@vueuse/shared': 14.1.0(vue@3.5.13(typescript@5.8.3))
vue: 3.5.13(typescript@5.8.3)
'@vueuse/metadata@14.1.0': {}
'@vueuse/shared@14.1.0(vue@3.5.13(typescript@5.8.3))':
dependencies:
vue: 3.5.13(typescript@5.8.3)
abbrev@3.0.1: {}
abort-controller@3.0.0:
@ -6791,13 +7014,16 @@ snapshots:
csstype@3.1.3: {}
daisyui@5.0.23: {}
daisyui@5.2.3: {}
data-uri-to-buffer@4.0.1: {}
date-fns@4.1.0: {}
db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)):
db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)):
optionalDependencies:
drizzle-orm: 0.42.0(@types/pg@8.11.13)(pg@8.14.1)
'@libsql/client': 0.15.15
drizzle-orm: 0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)
debug@3.2.7:
dependencies:
@ -6848,6 +7074,8 @@ snapshots:
detect-libc@1.0.3: {}
detect-libc@2.0.2: {}
detect-libc@2.0.3: {}
devalue@5.1.1: {}
@ -6897,8 +7125,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1):
drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1):
optionalDependencies:
'@libsql/client': 0.15.15
'@types/pg': 8.11.13
pg: 8.14.1
@ -7267,6 +7496,11 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@ -7326,6 +7560,10 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
fraction.js@4.3.7: {}
fresh@0.5.2: {}
@ -7585,7 +7823,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
ipx@2.1.0(db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1):
ipx@2.1.0(db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1):
dependencies:
'@fastify/accept-negotiator': 1.1.0
citty: 0.1.6
@ -7601,7 +7839,7 @@ snapshots:
sharp: 0.32.6
svgo: 3.3.2
ufo: 1.6.1
unstorage: 1.15.0(db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)
unstorage: 1.15.0(db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)
xss: 1.0.15
transitivePeerDependencies:
- '@azure/app-configuration'
@ -7725,6 +7963,8 @@ snapshots:
jiti@2.4.2: {}
js-base64@3.7.8: {}
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
@ -7838,6 +8078,21 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
libsql@0.5.22:
dependencies:
'@neon-rs/load': 0.0.4
detect-libc: 2.0.2
optionalDependencies:
'@libsql/darwin-arm64': 0.5.22
'@libsql/darwin-x64': 0.5.22
'@libsql/linux-arm-gnueabihf': 0.5.22
'@libsql/linux-arm-musleabihf': 0.5.22
'@libsql/linux-arm64-gnu': 0.5.22
'@libsql/linux-arm64-musl': 0.5.22
'@libsql/linux-x64-gnu': 0.5.22
'@libsql/linux-x64-musl': 0.5.22
'@libsql/win32-x64-msvc': 0.5.22
lightningcss-darwin-arm64@1.29.2:
optional: true
@ -8075,7 +8330,7 @@ snapshots:
negotiator@0.6.3: {}
nitropack@2.11.9(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)):
nitropack@2.11.9(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.0
'@netlify/functions': 3.0.4
@ -8097,7 +8352,7 @@ snapshots:
cookie-es: 2.0.0
croner: 9.0.0
crossws: 0.3.4
db0: 0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1))
db0: 0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1))
defu: 6.1.4
destr: 2.0.5
dot-prop: 9.0.0
@ -8143,7 +8398,7 @@ snapshots:
unenv: 2.0.0-rc.15
unimport: 5.0.0
unplugin-utils: 0.2.4
unstorage: 1.15.0(db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)
unstorage: 1.15.0(db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)
untyped: 2.0.0
unwasm: 0.3.9
youch: 4.1.0-beta.7
@ -8185,12 +8440,20 @@ snapshots:
node-addon-api@7.1.1: {}
node-domexception@1.0.0: {}
node-fetch-native@1.6.6: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-fetch@3.3.2:
dependencies:
data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
node-forge@1.3.1: {}
node-gyp-build@4.8.4: {}
@ -8226,7 +8489,7 @@ snapshots:
dependencies:
boolbase: 1.0.0
nuxt@3.16.2(@parcel/watcher@2.5.1)(@types/node@22.14.1)(db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)))(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1))(eslint@9.24.0(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.29.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.40.0)(terser@5.39.0)(tsx@4.19.3)(typescript@5.8.3)(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(yaml@2.7.1):
nuxt@3.16.2(@libsql/client@0.15.15)(@parcel/watcher@2.5.1)(@types/node@22.14.1)(db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)))(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1))(eslint@9.24.0(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.29.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.40.0)(terser@5.39.0)(tsx@4.19.3)(typescript@5.8.3)(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.1))(yaml@2.7.1):
dependencies:
'@nuxt/cli': 3.24.1(magicast@0.3.5)
'@nuxt/devalue': 2.0.2
@ -8263,7 +8526,7 @@ snapshots:
mlly: 1.7.4
mocked-exports: 0.1.1
nanotar: 0.2.0
nitropack: 2.11.9(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1))
nitropack: 2.11.9(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1))
nypm: 0.6.0
ofetch: 1.4.1
ohash: 2.0.11
@ -8285,7 +8548,7 @@ snapshots:
unimport: 4.2.0
unplugin: 2.3.2
unplugin-vue-router: 0.12.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
unstorage: 1.15.0(db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)
unstorage: 1.15.0(db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1)
untyped: 2.0.0
vue: 3.5.13(typescript@5.8.3)
vue-bundle-renderer: 2.1.1
@ -8359,7 +8622,8 @@ snapshots:
object-hash@3.0.0: {}
obuf@1.1.2: {}
obuf@1.1.2:
optional: true
ofetch@1.4.1:
dependencies:
@ -8511,17 +8775,22 @@ snapshots:
pg-cloudflare@1.1.1:
optional: true
pg-connection-string@2.7.0: {}
pg-connection-string@2.7.0:
optional: true
pg-int8@1.0.1: {}
pg-int8@1.0.1:
optional: true
pg-numeric@1.0.2: {}
pg-numeric@1.0.2:
optional: true
pg-pool@3.8.0(pg@8.14.1):
dependencies:
pg: 8.14.1
optional: true
pg-protocol@1.8.0: {}
pg-protocol@1.8.0:
optional: true
pg-types@2.2.0:
dependencies:
@ -8530,6 +8799,7 @@ snapshots:
postgres-bytea: 1.0.0
postgres-date: 1.0.7
postgres-interval: 1.2.0
optional: true
pg-types@4.0.2:
dependencies:
@ -8540,6 +8810,7 @@ snapshots:
postgres-date: 2.1.0
postgres-interval: 3.0.0
postgres-range: 1.1.4
optional: true
pg@8.14.1:
dependencies:
@ -8550,10 +8821,12 @@ snapshots:
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.1.1
optional: true
pgpass@1.0.5:
dependencies:
split2: 4.2.0
optional: true
picocolors@1.1.1: {}
@ -8563,6 +8836,14 @@ snapshots:
pify@2.3.0: {}
pinia-plugin-persistedstate@4.7.1(@nuxt/kit@3.16.2(magicast@0.3.5))(@pinia/nuxt@0.11.0(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))))(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))):
dependencies:
defu: 6.1.4
optionalDependencies:
'@nuxt/kit': 3.16.2(magicast@0.3.5)
'@pinia/nuxt': 0.11.0(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)))
pinia: 3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)):
dependencies:
'@vue/devtools-api': 7.7.2
@ -8791,27 +9072,36 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postgres-array@2.0.0: {}
postgres-array@2.0.0:
optional: true
postgres-array@3.0.4: {}
postgres-array@3.0.4:
optional: true
postgres-bytea@1.0.0: {}
postgres-bytea@1.0.0:
optional: true
postgres-bytea@3.0.0:
dependencies:
obuf: 1.1.2
optional: true
postgres-date@1.0.7: {}
postgres-date@1.0.7:
optional: true
postgres-date@2.1.0: {}
postgres-date@2.1.0:
optional: true
postgres-interval@1.2.0:
dependencies:
xtend: 4.0.2
optional: true
postgres-interval@3.0.0: {}
postgres-interval@3.0.0:
optional: true
postgres-range@1.1.4: {}
postgres-range@1.1.4:
optional: true
prebuild-install@7.1.3:
dependencies:
@ -8839,6 +9129,8 @@ snapshots:
process@0.11.10: {}
promise-limit@2.7.0: {}
prompts@2.4.2:
dependencies:
kleur: 3.0.3
@ -9177,7 +9469,8 @@ snapshots:
speakingurl@14.0.1: {}
split2@4.2.0: {}
split2@4.2.0:
optional: true
stable-hash@0.0.5: {}
@ -9590,7 +9883,7 @@ snapshots:
'@unrs/resolver-binding-win32-ia32-msvc': 1.5.0
'@unrs/resolver-binding-win32-x64-msvc': 1.5.0
unstorage@1.15.0(db0@0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1):
unstorage@1.15.0(db0@0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1)))(ioredis@5.6.1):
dependencies:
anymatch: 3.1.3
chokidar: 4.0.3
@ -9601,7 +9894,7 @@ snapshots:
ofetch: 1.4.1
ufo: 1.6.1
optionalDependencies:
db0: 0.3.1(drizzle-orm@0.42.0(@types/pg@8.11.13)(pg@8.14.1))
db0: 0.3.1(@libsql/client@0.15.15)(drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.11.13)(pg@8.14.1))
ioredis: 5.6.1
untun@0.1.3:
@ -9788,6 +10081,8 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
web-streams-polyfill@3.3.3: {}
webidl-conversions@3.0.1: {}
webpack-virtual-modules@0.6.2: {}
@ -9831,7 +10126,8 @@ snapshots:
cssfilter: 0.0.10
optional: true
xtend@4.0.2: {}
xtend@4.0.2:
optional: true
y18n@5.0.8: {}

View file

@ -1,12 +1,8 @@
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { drizzle } from 'drizzle-orm/libsql';
import { sql, eq, and } from 'drizzle-orm';
import arrayShuffle from 'array-shuffle';
import {
tasks_advanced,
questions_advanced,
categories_db,
} from '@/src/db/schema';
import { tasks_advanced, questions_advanced, categories_db } from '~/db/schema';
import type { AdvancedQuestion } from '~/types';
import categories from '~/categories';

View file

@ -1,8 +1,8 @@
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { drizzle } from 'drizzle-orm/libsql';
import { sql, eq, and } from 'drizzle-orm';
import arrayShuffle from 'array-shuffle';
import { tasks, questions, categories_db } from '@/src/db/schema';
import { tasks, questions, categories_db } from '~/db/schema';
import type { BasicQuestion } from '~/types';
import categories from '~/categories';

View file

@ -1,35 +0,0 @@
import { integer, pgTable, text, smallint, char } from 'drizzle-orm/pg-core';
export const tasks = pgTable('tasks', {
id: integer().notNull(),
correct_answer: text(),
media_url: text(),
weight: smallint(),
});
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(),
});

View file

@ -1,43 +1,54 @@
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,
};
export const useBasicStore = defineStore('basicStore', {
state: () => ({
basic: [] as ResultType[],
}),
actions: {
async set(basic: ResultType[]) {
this.basic = basic;
},
},
persist: {
storage: piniaPluginPersistedstate.localStorage(),
},
});
export const useAdvancedStore = defineStore('advancedStore', {
state: () => ({
advanced: [] as ResultType[],
}),
actions: {
async set(advanced: ResultType[]) {
this.advanced = advanced;
},
},
persist: {
storage: piniaPluginPersistedstate.localStorage(),
},
});
export const useExamStore = defineStore('examStore', {
state: () => ({
category: '',
end: false,
}),
actions: {
async setCategory(category: string) {
this.category = category;
},
async setEnd(end: boolean) {
this.end = end;
},
async mildReset() {
this.end = false;
useBasicStore().set([]);
useAdvancedStore().set([]);
},
async resetExam() {
this.category = '';
this.mildReset();
},
},
persist: {
storage: piniaPluginPersistedstate.cookies(),
},
});

View file

@ -12,13 +12,16 @@ export interface AdvancedQuestion extends BasicQuestion {
answer_c: string | null;
}
export interface ResultType<T> {
question: T | undefined;
export type Question = BasicQuestion | AdvancedQuestion | null | undefined;
export interface ResultType {
question: Question;
chosen_answer: string;
correct_answer: string | null;
chosen_is_correct: boolean | undefined;
}
export interface ResultEndType {
basic: ResultType<BasicQuestion>[];
advanced: ResultType<AdvancedQuestion>[];
basic: ResultType[];
advanced: ResultType[];
}