diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a3f2401435f5b1cdc824f469ea3ede34585def42..882e9d439faaf0972ef2db46de56777442805079 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -44,7 +44,8 @@ build: when: on_success policy: pull script: - - npm run build + - npm run build || + (echo "Build failed, trying npm ci..." && npm ci && npm run build) # run npm ci if build fails artifacts: paths: - .nuxt/ @@ -75,9 +76,11 @@ lint: check-updates: stage: test script: - - UPDATES=$(npx npm-check-updates | tail -n +2) # Ignorer la première ligne de sortie - - if [ ! -z "$UPDATES" ]; then exit 1; fi # Retourner une erreur si des mises à jour sont disponibles - allow_failure: true # Permet de remonter un warning et non une erreur + - UPDATES=$(npx npm-check-updates | tail -n +4) # Ignorer la première ligne de sortie + - if [ ! -z "$UPDATES" ]; then echo $UPDATES; exit 1; fi # Retourner une erreur si des mises à jour sont disponibles + - UPDATES=$(npx ncu @nuxt/ui --target @next | tail -n +4) # Ignorer la première ligne de sortie + - if [ ! -z "$UPDATES" ]; then echo $UPDATES; exit 2; fi # Retourner une erreur si des mises à jour sont disponibles + allow_failure: true # Permet de remonter un warning et non une erreur build-image: stage: docker diff --git a/README.md b/README.md index 1f3157336ae49f456ae425a4bc6ad0f010574357..e1b5ebdf3b11a48c416aa24738ddaf41f8640ebf 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Be sure there are no new dependency updates: ```bash npx npm-check-updates -u +npx ncu @nuxt/ui --target @next npm install ``` diff --git a/app.config.ts b/app.config.ts index ae7894b38d5ec79342d25944a2a8d62d4e779333..2d3c146fb3568ed219370b4d07fe0239a6b9c590 100644 --- a/app.config.ts +++ b/app.config.ts @@ -5,8 +5,8 @@ export default defineAppConfig({ dark: { dark: true, colors: { - primary: '#F8FAE5', - secondary: '#14C78B', + primary: '#14C78B', + secondary: '#F8FAE5', background: '#1C3238', accent: '#FF5A33', info: '#0080aa', diff --git a/components/Parcours/Details.vue b/components/Parcours/Details.vue index 2f0662c9b6c4f12cf39c67bc376c736e755ecbfa..854505e96a76e4fcda987da5e7fd98366ff6d07f 100644 --- a/components/Parcours/Details.vue +++ b/components/Parcours/Details.vue @@ -1,17 +1,20 @@ <script setup lang="ts"> -import _L, {type PointTuple} from "leaflet"; +import {type LatLngExpression} from "leaflet"; import 'leaflet.markercluster'; import * as v from 'valibot'; -import {setNumberedIcon} from "~/utils/MapUtils"; +import {calculateCenter, calculateMaxBounds, createMarkerNumberedIcon} from "~/utils/MapUtils"; import type {FormSubmitEvent} from '@nuxt/ui'; -import type {Exercise} from "~/types/Exercise"; -import type {Step} from "~/types/Step"; +import type {Exercise, SelectedExercise} from "~/types/Exercise"; import type {MarkerProps} from "~/types/Leaflet"; +import type {Route} from "~/types/Route"; const props = defineProps<{ - selectedExercises: {step: Step, exercise: Exercise}[] + selectedExercises: SelectedExercise[], + paths: LatLngExpression[] }>(); +const {$api} = useNuxtApp(); +const app = useAppConfig(); const toast = useToast(); const schema = v.object({ @@ -30,31 +33,47 @@ const state = reactive({ }); const zoom = ref(17); -// get min max bounds from all the selected exercises -const maxBounds = props.selectedExercises.reduce((acc, se) => { - const step = se.step; - return [ - [Math.min(acc[0][0], step.latitude), Math.min(acc[0][1], step.longitude)], - [Math.max(acc[1][0], step.latitude), Math.max(acc[1][1], step.longitude)] - ]; -}, [[Infinity, Infinity], [-Infinity, -Infinity]]); -const center: PointTuple = props.selectedExercises.reduce((acc, se) => { - const step = se.step; - return [acc[0] + step.latitude, acc[1] + step.longitude]; -}, [0, 0]).map(c => c / props.selectedExercises.length) as PointTuple; - -async function onSubmit(event: FormSubmitEvent<Schema>) { - toast.add({title: 'Success', description: 'The form has been submitted.', color: 'success'}); - state.parcoursTitle = ''; - state.localization = ''; - state.duration = '00:30'; - // TODO : Send the data to the API - // $api.parcours.createParcours(state) - console.log(event.data); +const maxBounds = calculateMaxBounds(props.selectedExercises); +const center = calculateCenter(props.selectedExercises); + +async function onSubmit(_: FormSubmitEvent<Schema>) { + const route: Route = { + id: -1, + name: state.parcoursTitle, + location: state.localization, + estimatedTime: +state.duration.split(':')[0] * 60 + +state.duration.split(':')[1], + steps: props.selectedExercises.map(se => se.step), + path: props.paths + }; + + const res = await $api.route.saveRoute(route, props.selectedExercises); + switch (res.statusCode) { + case 200: + toast.add({title: 'Success', description: 'Votre parcours a bien été édité !', color: 'success'}); + break; + case 201: + toast.add({title: 'Success', description: 'Votre parcours a bien été crée !', color: 'success'}); + break; + case 401: + toast.add( + {title: 'Error', description: 'Vous n\'êtes pas autorisé à effectuer cette action', color: 'error'}); + break; + case 403: + toast.add({ + title: 'Error', + description: 'Vous devez vous identifier avant d\'effectuer cette action.', + color: 'error' + }); + break; + case 400: + default: + toast.add({title: 'Error', description: 'Votre parcours n\'a pas été crée :(', color: 'error'}); + break; + } } async function onMapReady() { - await useLMarkerCluster({ + await useLMarkerCluster({ leafletObject: map?.value?.leafletObject, options: { disableClusteringAtZoom: zoom, @@ -66,7 +85,7 @@ async function onMapReady() { lat: se.step.latitude, lng: se.step.longitude, options: { - icon: setNumberedIcon((se.step.stepOrder + 1).toString(), [30, 30]) + icon: createMarkerNumberedIcon((se.step.stepOrder + 1).toString(), [30, 30]) } }; }) as MarkerProps[] @@ -100,6 +119,10 @@ async function onMapReady() { layer-type="base" name="OpenStreetMap" /> + <LPolyline + :lat-lngs="paths" + :color="app.theme.light.colors.primary" + /> </LMap> <UFormField label="Localisation" name="localization"> @@ -114,7 +137,8 @@ async function onMapReady() { <div class="flex flex-col h-full overflow-auto gap-1 col-span-2 p-1"> <ParcoursExerciceItem - v-for="exercise in props.selectedExercises.map(se => se.exercise)" :id="'exercise-item-'+exercise.relatedStep+'-'+exercise.id" + v-for="exercise in props.selectedExercises.map(se => se.exercise)" + :id="'exercise-item-'+exercise.relatedStep+'-'+exercise.id" :key="exercise.id" ref="list" :exercise="exercise as Exercise" class="skeletton gap-4"/> diff --git a/components/Parcours/Order.vue b/components/Parcours/Order.vue index 7d58e7cc850ce84cdbceb8056c4b369bcc95b077..1c08f5cd8e3c768c0682702fba16be67393d64b2 100644 --- a/components/Parcours/Order.vue +++ b/components/Parcours/Order.vue @@ -1,37 +1,29 @@ <script setup lang="ts"> -import type {LeafletMouseEvent, Marker, PointTuple} from "leaflet"; +import type {LatLngExpression, LeafletMouseEvent, Marker} from "leaflet"; import type {DropResult} from "vue3-smooth-dnd"; -import {setNumberedIcon} from "~/utils/MapUtils"; +import {calculateCenter, calculateMaxBounds, createMarkerNumberedIcon} from "~/utils/MapUtils"; import type {Exercise} from "~/types/Exercise"; import type {Step} from "~/types/Step"; import type {MarkerCluster, MarkerProps} from "~/types/Leaflet"; const props = defineProps<{ - selectedExercises: {step: Step, exercise: Exercise}[] + selectedExercises: { step: Step, exercise: Exercise }[], + paths: LatLngExpression[] }>(); -const emit = defineEmits(['update:selectedExercises']); +const emit = defineEmits(['update:selectedExercises', "update:paths"]); + +const app = useAppConfig(); const map = ref<any>(null); const exerciseSearch = ref(''); const zoom = ref(17); -// get min max bounds from all the selected exercises -const maxBounds = props.selectedExercises.reduce((acc, se) => { - const step = se.step; - return [ - [Math.min(acc[0][0], step.latitude), Math.min(acc[0][1], step.longitude)], - [Math.max(acc[1][0], step.latitude), Math.max(acc[1][1], step.longitude)] - ]; -}, [[Infinity, Infinity], [-Infinity, -Infinity]]); -const center: PointTuple = props.selectedExercises.reduce((acc, se) => { - const step = se.step; - return [acc[0] + step.latitude, acc[1] + step.longitude]; -}, [0, 0]).map(c => c / props.selectedExercises.length) as PointTuple; +const maxBounds = calculateMaxBounds(props.selectedExercises); +const center = calculateCenter(props.selectedExercises); let markerCluster: MarkerCluster; - // Map utilities async function onDrop(dropResult: DropResult) { - const { removedIndex, addedIndex, payload } = dropResult; + const {removedIndex, addedIndex, payload} = dropResult; if (removedIndex === null && addedIndex === null) return; const updatedExercises = [...props.selectedExercises]; @@ -59,18 +51,35 @@ async function createCluster() { lat: se.step.latitude, lng: se.step.longitude, options: { - icon: setNumberedIcon((se.step.stepOrder + 1).toString(), [30, 30]) + icon: createMarkerNumberedIcon((se.step.stepOrder + 1).toString(), [30, 30]) } }; }) as MarkerProps[] }); + calculatePaths(); + markerCluster.markers.forEach((marker: Marker) => { marker.on('mouseover', onMouseoverMarker); marker.on('mouseout', onMouseoutMarker); }); } -function filterExercise(): {step: Step, exercise: Exercise}[] { +function calculatePaths() { + const paths = props.selectedExercises.map((se, index) => { + const step = se.step; + const nextStep = props.selectedExercises[index + 1]?.step; + if (nextStep) { + return [ + [step.latitude, step.longitude], + [nextStep.latitude, nextStep.longitude] + ]; + } + }).filter((path) => { + return path !== undefined; + }); + emit('update:paths', paths); +} +function filterExercise(): { step: Step, exercise: Exercise }[] { return props.selectedExercises.filter((se) => { return se.exercise.name.toLowerCase().includes(exerciseSearch.value.toLowerCase()); }); @@ -85,23 +94,23 @@ function onMouseoutMarker(e: LeafletMouseEvent): void { } // Events - Exercises -function onMouseoverExercise(se: {step: Step, exercise: Exercise}): void { +function onMouseoverExercise(se: { step: Step, exercise: Exercise }): void { markerCluster.markers.forEach((marker: Marker) => { if (marker.getLatLng().lat === se.step.latitude && marker.getLatLng().lng === se.step.longitude) { - marker.setIcon(setNumberedIcon((se.step.stepOrder + 1).toString(), [45, 45])); + marker.setIcon(createMarkerNumberedIcon((se.step.stepOrder + 1).toString(), [45, 45])); } }); } -function onMouseoutExercise(se: {step: Step, exercise: Exercise}): void { +function onMouseoutExercise(se: { step: Step, exercise: Exercise }): void { markerCluster.markers.forEach((marker: Marker) => { if (marker.getLatLng().lat === se.step.latitude && marker.getLatLng().lng === se.step.longitude) { - marker.setIcon(setNumberedIcon((se.step.stepOrder + 1).toString(), [30, 30])); + marker.setIcon(createMarkerNumberedIcon((se.step.stepOrder + 1).toString(), [30, 30])); } }); } async function onMapReady() { - await createCluster(); + await createCluster(); } </script> @@ -155,6 +164,10 @@ async function onMapReady() { layer-type="base" name="OpenStreetMap" /> + <LPolyline + :lat-lngs="paths" + :color="app.theme.light.colors.primary" + /> </LMap> </div> </template> @@ -166,6 +179,7 @@ async function onMapReady() { height: 65vh; gap: 1px; } + .exercise-hoovered { background-color: color-mix(in oklab, var(--color-background-500) 50%, transparent); border-radius: 5px; diff --git a/components/Parcours/Step.vue b/components/Parcours/Step.vue index 15550f6b766c207f151d3ad7c43ca0cd687dcb85..82884e3e49fcb1ff52a0fc3c591780279490242d 100644 --- a/components/Parcours/Step.vue +++ b/components/Parcours/Step.vue @@ -1,13 +1,14 @@ <script setup lang="ts"> -import L, {type LeafletMouseEvent, Marker, type PointTuple} from 'leaflet'; +import _L, {type LeafletMouseEvent, Marker, type PointTuple} from 'leaflet'; import 'leaflet.markercluster'; import type {Exercise} from "~/types/Exercise"; import type {Step} from "~/types/Step"; import type {MarkerCluster, MarkerProps} from "~/types/Leaflet"; +import {createMarkerIcon} from "~/utils/MapUtils"; // Parent props const props = defineProps<{ - selectedExercises: {step: Step, exercise: Exercise}[] + selectedExercises: { step: Step, exercise: Exercise }[] }>(); const emit = defineEmits(['update:selectedExercises']); @@ -24,6 +25,7 @@ let createCluster: MarkerCluster; const exercises = ref<Exercise[]>([]); const steps = ref<Step[]>([]); +let selectedStep: Step; let counter = props.selectedExercises.length; // TODO : Make it dynamic @@ -44,25 +46,27 @@ const contextMenuItems = ref([ async function updateLocations(): Promise<void> { locations.value = steps.value.map((step) => { + const isStepSelected = selectedStep?.id === step.id; + console.log(isStepSelected); return { name: step.id.toString(), lat: step.latitude, lng: step.longitude, options: { - icon: L.icon({ - iconUrl: '/svg/pin.svg', - iconSize: [30, 30], - }) + icon: createMarkerIcon(isStepSelected ? '/svg/pinselected.svg' : '/svg/pin.svg', [30, 30]) } }; }); markerCluster?.markerCluster?.clearLayers(); + // Merge selected exercises with the steps steps.value.map((step) => { step.specificExercises.map((exo) => { - exo.selected = props.selectedExercises.find((se) => se.step.id === step.id && se.exercise.id === exo.id) !== undefined; + exo.selected = props.selectedExercises.find( + (se) => se.step.id === step.id && se.exercise.id === exo.id) !== undefined; }); step.terrainType.genericExercises.map((exo) => { - exo.selected = props.selectedExercises.find((se) => se.step.id === step.id && se.exercise.id === exo.id) !== undefined; + exo.selected = props.selectedExercises.find( + (se) => se.step.id === step.id && se.exercise.id === exo.id) !== undefined; }); }); @@ -92,30 +96,30 @@ function deleteMarker(): void { // Events - Markers function onMarkerSelect(e: LeafletMouseEvent): void { const stepId = (e.target as Marker).options.title; + exercises.value = steps.value .filter((step: Step) => step.id.toString() === stepId) .map((step: Step) => { return step.specificExercises .concat(step.terrainType.genericExercises); }).flat(); + + selectedStep = steps.value.find((step: Step) => step.id.toString() === stepId) as Step; + e.target.setIcon(createMarkerIcon('/svg/pinselected.svg', e.target.getIcon().options.iconSize ?? [30, 30])); + updateLocations(); } function onMouseoverMarker(e: LeafletMouseEvent): void { - e.target.setIcon(L.icon({ - iconUrl: '/svg/pin.svg', - iconSize: [45, 45], - })); + e.target.setIcon(createMarkerIcon(e.target.getIcon().options.iconUrl ?? '/svg/pin.svg', [45, 45])); } function onMouseoutMarker(e: LeafletMouseEvent): void { - e.target.setIcon(L.icon({ - iconUrl: '/svg/pin.svg', - iconSize: [30, 30], - })); + e.target.setIcon(createMarkerIcon(e.target.getIcon().options.iconUrl ?? '/svg/pin.svg', [30, 30])); } // Events - Exercises function onExerciceSelect(exo: Exercise): void { if (exo.selected) { - const index = props.selectedExercises.findIndex((se) => se.step.id === exo.relatedStep && se.exercise.id === exo.id); + const index = props.selectedExercises.findIndex( + (se) => se.step.id === exo.relatedStep && se.exercise.id === exo.id); if (index !== -1) { const updatedExercises = [ ...props.selectedExercises.slice(0, index), @@ -176,10 +180,7 @@ function onMapClick(e: LeafletMouseEvent): void { title: 'New marker', draggable: true, }); - newMarker.setIcon(L.icon({ - iconUrl: '/svg/pinadd.svg', - iconSize: [30, 30], - })); + newMarker.setIcon(createMarkerIcon('/svg/pinadd.svg', [30, 30])); createCluster.markerCluster.addLayer(newMarker); } async function onMapReady(): Promise<void> { @@ -249,13 +250,18 @@ async function onMapReady(): Promise<void> { </UInput> <USeparator class="m-1 w-full"/> <div id="exercices-list" class="flex flex-col w-full h-full overflow-auto gap-1"> + <div v-if="inCreation"> + <USeparator label="Création d'étape"/> + </div> + <USeparator v-if="exercises.length > 0" label="Liste d'exercices disponible pour l'étape sélectionnée"/> <ParcoursExerciceItem v-for="exercise in filterExercise()" :id="'exercise-item-'+exercise.relatedStep+'-'+exercise.id" :key="exercise.id" ref="list" :exercise="exercise as Exercise" class="skeletton gap-4" @click="onExerciceSelect(exercise)" - @mouseover="onMouseoverExercise(exercise as Exercise)" @mouseleave="onMouseoutExercise(exercise as Exercise)"/> + @mouseover="onMouseoverExercise(exercise as Exercise)" + @mouseleave="onMouseoutExercise(exercise as Exercise)"/> <div class="skeletton gap-4"> <USkeleton class="h-12 w-12 rounded-full"/> <div class="grid gap-2 w-full"> diff --git a/package-lock.json b/package-lock.json index 17d94eb771a6a3f6bc502f45d21b9c19073d5046..cd1a661bbad9de3ed67c39c9518665b5405890b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,10 @@ "@nuxt/eslint": "^1.1.0", "@nuxt/ui": "^3.0.0-alpha.13", "@nuxtjs/leaflet": "^1.2.6", - "eslint": "^9.20.1", + "eslint": "^9.21.0", "leaflet.markercluster": "^1.5.3", "nuxt": "^3.15.4", - "valibot": "^1.0.0-rc.1", + "valibot": "^1.0.0-rc.2", "vue": "latest", "vue-router": "latest", "vue3-smooth-dnd": "^0.0.6" @@ -21,11 +21,11 @@ "devDependencies": { "@iconify-json/lucide": "^1.2.26", "@iconify-json/material-symbols": "^1.2.14", - "@nuxt/test-utils": "^3.15.4", + "@nuxt/test-utils": "^3.17.0", "@vue/test-utils": "^2.4.6", - "happy-dom": "^17.1.1", + "happy-dom": "^17.1.8", "typescript": "^5.7.3", - "vitest": "^3.0.6" + "vitest": "^3.0.7" } }, "node_modules/@alloc/quick-lru": { @@ -1090,12 +1090,12 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -1159,9 +1159,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -1171,9 +1171,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -1237,30 +1237,30 @@ } }, "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", + "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.12.0", "levn": "^0.4.1" }, "engines": { @@ -1378,9 +1378,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -2502,27 +2502,27 @@ } }, "node_modules/@nuxt/test-utils": { - "version": "3.15.4", - "resolved": "https://registry.npmjs.org/@nuxt/test-utils/-/test-utils-3.15.4.tgz", - "integrity": "sha512-R5eNXILsB5GCTMgoKdW3rN9rNBQCVBqxw4+tcujNplcivbJp7lQrRMHlbR9ijAJ1jEMkDNXdOQGbM1RnWvDuuQ==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@nuxt/test-utils/-/test-utils-3.17.0.tgz", + "integrity": "sha512-NfKES2wGXxV8bNec30W0+rjegy7azFxXT7TJZ3SIcg6CxWQiO8Q+Xh743UnoqPw2WF03GNEndovf6AqwIRjNJQ==", "dev": true, "license": "MIT", "dependencies": { - "@nuxt/kit": "^3.15.1", - "@nuxt/schema": "^3.15.1", - "c12": "^2.0.1", - "consola": "^3.3.3", + "@nuxt/kit": "^3.15.4", + "@nuxt/schema": "^3.15.4", + "c12": "^2.0.2", + "consola": "^3.4.0", "defu": "^6.1.4", "destr": "^2.0.3", "estree-walker": "^3.0.3", "fake-indexeddb": "^6.0.0", "get-port-please": "^3.1.2", - "h3": "^1.13.1", + "h3": "^1.15.0", "local-pkg": "^1.0.0", "magic-string": "^0.30.17", - "node-fetch-native": "^1.6.4", + "node-fetch-native": "^1.6.5", "ofetch": "^1.4.1", - "pathe": "^2.0.1", + "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "radix3": "^1.1.2", "scule": "^1.3.0", @@ -2530,8 +2530,8 @@ "tinyexec": "^0.3.2", "ufo": "^1.5.4", "unenv": "^1.10.0", - "unplugin": "^2.1.2", - "vite": "^6.0.7", + "unplugin": "^2.2.0", + "vite": "^6.1.1", "vitest-environment-nuxt": "^1.0.1", "vue": "^3.5.13" }, @@ -2544,10 +2544,10 @@ "@playwright/test": "^1.43.1", "@testing-library/vue": "^7.0.0 || ^8.0.1", "@vue/test-utils": "^2.4.2", - "happy-dom": "^9.10.9 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "happy-dom": "^9.10.9 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "jsdom": "^22.0.0 || ^23.0.0 || ^24.0.0 || ^25.0.0 || ^26.0.0", "playwright-core": "^1.43.1", - "vitest": "^0.34.6 || ^1.0.0 || ^2.0.0 || ^3.0.0-beta.3" + "vitest": "^0.34.6 || ^1.0.0 || ^2.0.0 || ^3.0.0" }, "peerDependenciesMeta": { "@cucumber/cucumber": { @@ -2582,6 +2582,35 @@ } } }, + "node_modules/@nuxt/test-utils/node_modules/c12": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-2.0.4.tgz", + "integrity": "sha512-3DbbhnFt0fKJHxU4tEUPmD1ahWE4PWPMomqfYsTJdrhpmEnRKJi3qSC4rO5U6E6zN1+pjBY7+z8fUmNRMaVKLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.1.8", + "defu": "^6.1.4", + "dotenv": "^16.4.7", + "giget": "^1.2.4", + "jiti": "^2.4.2", + "mlly": "^1.7.4", + "ohash": "^2.0.4", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.3.1", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/@nuxt/test-utils/node_modules/local-pkg": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.0.0.tgz", @@ -2599,6 +2628,27 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@nuxt/test-utils/node_modules/ohash": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.5.tgz", + "integrity": "sha512-3k3APZwRRPYyohdIDmPTpe5i0AY5lm7gvu/Oip7tZrTaEGfSlKX+7kXUoWLd9sHX0GDRVwVvlW18yEcD7qS1zw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nuxt/test-utils/node_modules/unplugin": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.2.0.tgz", + "integrity": "sha512-m1ekpSwuOT5hxkJeZGRxO7gXbXT3gF26NjQ7GdVHoLoF8/nopLcd/QfPigpCy7i51oFHiRJg/CyHhj4vs2+KGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, "node_modules/@nuxt/ui": { "version": "3.0.0-alpha.13", "resolved": "https://registry.npmjs.org/@nuxt/ui/-/ui-3.0.0-alpha.13.tgz", @@ -4556,14 +4606,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.6.tgz", - "integrity": "sha512-zBduHf/ja7/QRX4HdP1DSq5XrPgdN+jzLOwaTq/0qZjYfgETNFCKf9nOAp2j3hmom3oTbczuUzrzg9Hafh7hNg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz", + "integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.6", - "@vitest/utils": "3.0.6", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -4572,13 +4622,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.6.tgz", - "integrity": "sha512-KPztr4/tn7qDGZfqlSPQoF2VgJcKxnDNhmfR3VgZ6Fy1bO8T9Fc1stUiTXtqz0yG24VpD00pZP5f8EOFknjNuQ==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz", + "integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.6", + "@vitest/spy": "3.0.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -4599,9 +4649,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.6.tgz", - "integrity": "sha512-Zyctv3dbNL+67qtHfRnUE/k8qxduOamRfAL1BurEIQSyOEFffoMvx2pnDSSbKAAVxY0Ej2J/GH2dQKI0W2JyVg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz", + "integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==", "dev": true, "license": "MIT", "dependencies": { @@ -4612,13 +4662,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.6.tgz", - "integrity": "sha512-JopP4m/jGoaG1+CBqubV/5VMbi7L+NQCJTu1J1Pf6YaUbk7bZtaq5CX7p+8sY64Sjn1UQ1XJparHfcvTTdu9cA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz", + "integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.6", + "@vitest/utils": "3.0.7", "pathe": "^2.0.3" }, "funding": { @@ -4626,13 +4676,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.6.tgz", - "integrity": "sha512-qKSmxNQwT60kNwwJHMVwavvZsMGXWmngD023OHSgn873pV0lylK7dwBTfYP7e4URy5NiBCHHiQGA9DHkYkqRqg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz", + "integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.6", + "@vitest/pretty-format": "3.0.7", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -4641,9 +4691,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.6.tgz", - "integrity": "sha512-HfOGx/bXtjy24fDlTOpgiAEJbRfFxoX3zIGagCqACkFKKZ/TTOE6gYMKXlqecvxEndKFuNHcHqP081ggZ2yM0Q==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz", + "integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==", "dev": true, "license": "MIT", "dependencies": { @@ -4654,13 +4704,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.6.tgz", - "integrity": "sha512-18ktZpf4GQFTbf9jK543uspU03Q2qya7ZGya5yiZ0Gx0nnnalBvd5ZBislbl2EhLjM8A8rt4OilqKG7QwcGkvQ==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz", + "integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.6", + "@vitest/pretty-format": "3.0.7", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -6229,9 +6279,9 @@ } }, "node_modules/crossws": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.2.tgz", - "integrity": "sha512-S2PpQHRcgYABOS2465b34wqTOn5dbLL+iSvyweJYGGFLDsKq88xrjDXUiEhfYkhWZq1HuS6of3okRHILbkrqxw==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.4.tgz", + "integrity": "sha512-uj0O1ETYX1Bh6uSgktfPvwDiPYGQ3aI4qVsaC/LWpkIzGj1nUYm5FK3K+t11oOlpN01lGbprFCH4wBlKdJjVgw==", "license": "MIT", "dependencies": { "uncrypto": "^0.1.3" @@ -6972,21 +7022,21 @@ } }, "node_modules/eslint": { - "version": "9.20.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", - "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", + "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.19.2", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.21.0", + "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -7313,18 +7363,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -8264,27 +8302,26 @@ } }, "node_modules/h3": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.14.0.tgz", - "integrity": "sha512-ao22eiONdgelqcnknw0iD645qW0s9NnrJHr5OBz4WOMdBdycfSas1EQf1wXRsm+PcB2Yoj43pjBPwqIpJQTeWg==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.1.tgz", + "integrity": "sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA==", "license": "MIT", "dependencies": { "cookie-es": "^1.2.2", - "crossws": "^0.3.2", + "crossws": "^0.3.3", "defu": "^6.1.4", "destr": "^2.0.3", "iron-webcrypto": "^1.2.1", - "ohash": "^1.1.4", + "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.5.4", - "uncrypto": "^0.1.3", - "unenv": "^1.10.0" + "uncrypto": "^0.1.3" } }, "node_modules/happy-dom": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.1.1.tgz", - "integrity": "sha512-OSTkBlmD/6Do7gCd7nZB5iFq1bF9VQg/iFmjHmxvVX2S1UiOpo6sT+aFNnu3XUsB8hCZb9+GZ0G1g1TaMiAggw==", + "version": "17.1.8", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.1.8.tgz", + "integrity": "sha512-Yxbq/FG79z1rhAf/iB6YM8wO2JB/JDQBy99RiLSs+2siEAi5J05x9eW1nnASHZJbpldjJE2KuFLsLZ+AzX/IxA==", "dev": true, "license": "MIT", "dependencies": { @@ -8442,9 +8479,9 @@ "license": "MIT" }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -9959,6 +9996,12 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-mock-http": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.0.tgz", + "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -10592,9 +10635,9 @@ } }, "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -13658,9 +13701,9 @@ "license": "MIT" }, "node_modules/valibot": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.0.0-rc.1.tgz", - "integrity": "sha512-bTHNpeeQ403xS7qGHF/tw3EC/zkZOU5VdkfIsmRDu1Sp+BJNTNCm6m5HlwOgyW/03lofP+uQiq3R+Poo9wiCEg==", + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.0.0-rc.2.tgz", + "integrity": "sha512-Tnnp7dydpihvoUbJiaxuYfsCAgAFKuFMex7PTaI25XSjRWkU70DmJPlAO1W6sF1/WUx4RNWyM2hdmBSMIUSZFA==", "license": "MIT", "peerDependencies": { "typescript": ">=5" @@ -13794,14 +13837,14 @@ } }, "node_modules/vite": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", - "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", "license": "MIT", "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.4.49", - "rollup": "^4.23.0" + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" }, "bin": { "vite": "bin/vite.js" @@ -13877,9 +13920,9 @@ } }, "node_modules/vite-node": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.6.tgz", - "integrity": "sha512-s51RzrTkXKJrhNbUzQRsarjmAae7VmMPAsRT7lppVpIg6mK3zGthP9Hgz0YQQKuNcF+Ii7DfYk3Fxz40jRmePw==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", + "integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==", "license": "MIT", "dependencies": { "cac": "^6.7.14", @@ -14161,19 +14204,19 @@ } }, "node_modules/vitest": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.6.tgz", - "integrity": "sha512-/iL1Sc5VeDZKPDe58oGK4HUFLhw6b5XdY1MYawjuSaDA4sEfYlY9HnS6aCEG26fX+MgUi7MwlduTBHHAI/OvMA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz", + "integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.6", - "@vitest/mocker": "3.0.6", - "@vitest/pretty-format": "^3.0.6", - "@vitest/runner": "3.0.6", - "@vitest/snapshot": "3.0.6", - "@vitest/spy": "3.0.6", - "@vitest/utils": "3.0.6", + "@vitest/expect": "3.0.7", + "@vitest/mocker": "3.0.7", + "@vitest/pretty-format": "^3.0.7", + "@vitest/runner": "3.0.7", + "@vitest/snapshot": "3.0.7", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", @@ -14185,7 +14228,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.6", + "vite-node": "3.0.7", "why-is-node-running": "^2.3.0" }, "bin": { @@ -14201,8 +14244,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.6", - "@vitest/ui": "3.0.6", + "@vitest/browser": "3.0.7", + "@vitest/ui": "3.0.7", "happy-dom": "*", "jsdom": "*" }, diff --git a/package.json b/package.json index f3bc4954d352d230058b13e6fe996ca4a0d54f67..913db2310f8f22634804ed1a31069d6ce27a8237 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,10 @@ "@nuxt/eslint": "^1.1.0", "@nuxt/ui": "^3.0.0-alpha.13", "@nuxtjs/leaflet": "^1.2.6", - "eslint": "^9.20.1", + "eslint": "^9.21.0", "leaflet.markercluster": "^1.5.3", "nuxt": "^3.15.4", - "valibot": "^1.0.0-rc.1", + "valibot": "^1.0.0-rc.2", "vue": "latest", "vue-router": "latest", "vue3-smooth-dnd": "^0.0.6" @@ -28,15 +28,14 @@ "devDependencies": { "@iconify-json/lucide": "^1.2.26", "@iconify-json/material-symbols": "^1.2.14", - "@nuxt/test-utils": "^3.15.4", + "@nuxt/test-utils": "^3.17.0", "@vue/test-utils": "^2.4.6", - "happy-dom": "^17.1.1", + "happy-dom": "^17.1.8", "typescript": "^5.7.3", - "vitest": "^3.0.6" + "vitest": "^3.0.7" }, "overrides": { "glob": "^11.0.1", - "happy-dom": "^17.1.1", "esbuild": "^0.25.0" } } diff --git a/package.json.md b/package.json.md index 9a05fcc9eec3141c96dbc0256e1cddcadd19bd9d..88b8fff48273bc6d2e26af956d6ea6a8aa5e5d2b 100644 --- a/package.json.md +++ b/package.json.md @@ -2,9 +2,6 @@ Dépendances outrepassées : - `glob` with version "^11.0.1", waiting update from these packages: - @vue/test-utils@2.4.6 - nuxt@3.15.4 -- `happy-dom` with version "^17.1.0", waiting update from these packages: - - @nuxt/test-utils@3.15.4 - - vitest@3.0.5 - `esbuild` with version "^0.25.0", waiting update from these packages: - @nuxt@3.15.4 - @nuxt/test-utils@3.15.4 diff --git a/pages/parcours.vue b/pages/parcours.vue index 963edc5d0d527c9d520514ef158b17bf0c4dcb0d..e9fcb75c609f87b89194ff268f5d9a6837a51d94 100644 --- a/pages/parcours.vue +++ b/pages/parcours.vue @@ -1,5 +1,5 @@ <script setup lang="ts"> - +import type {LatLngExpression} from "leaflet"; import type {Step} from "~/types/Step"; import type {Exercise} from "~/types/Exercise"; @@ -23,16 +23,20 @@ const items = [ } ]; const selectedExercises = ref<{step: Step, exercise: Exercise}[]>([]); +const paths = ref<LatLngExpression[]>([]); const stepper = useTemplateRef('stepper'); function updateSelectedExercises(newExercises: {step: Step, exercise: Exercise}[]) { selectedExercises.value = newExercises; } +function updatePaths(newPaths: LatLngExpression[]) { + paths.value = newPaths; +} </script> <template> <div id="parcours" class="w-full flex flex-col justify-between"> - <UStepper ref="stepper" :items="items"> + <UStepper ref="stepper" :items="items" :disabled="selectedExercises.length === 0"> <template #step> <ParcoursStep :selected-exercises="selectedExercises" @@ -43,6 +47,8 @@ function updateSelectedExercises(newExercises: {step: Step, exercise: Exercise}[ <template #parcours> <ParcoursOrder :selected-exercises="selectedExercises" + :paths="paths" + @update:paths="updatePaths" @update:selected-exercises="updateSelectedExercises" /> </template> @@ -50,6 +56,7 @@ function updateSelectedExercises(newExercises: {step: Step, exercise: Exercise}[ <template #details> <ParcoursDetails :selected-exercises="selectedExercises" + :paths="paths" /> </template> </UStepper> diff --git a/plugins/api.ts b/plugins/api.ts index 774cb8882b795880cb2333aa44f51d3937376c7b..4f811399b39ffd0b0a8870cbfba87dec1e88276e 100644 --- a/plugins/api.ts +++ b/plugins/api.ts @@ -1,9 +1,11 @@ import ExerciseModule from "~/repository/module/exercise"; import UserModule from '~/repository/module/user'; import StepModule from "~/repository/module/step"; +import RouteModule from "~/repository/module/route"; interface IApiInstance { exercises: ExerciseModule; + route: RouteModule; step: StepModule; users: UserModule; } @@ -11,6 +13,7 @@ interface IApiInstance { export default defineNuxtPlugin(() => { const modules: IApiInstance = { exercises: new ExerciseModule(), + route: new RouteModule(), step: new StepModule(), users: new UserModule() }; diff --git a/public/svg/pinselected.svg b/public/svg/pinselected.svg new file mode 100644 index 0000000000000000000000000000000000000000..0cca2615b87f46b194bf231881bd0fac14747233 --- /dev/null +++ b/public/svg/pinselected.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"> + <path fill="#14C78B" + d="M12 12q.825 0 1.413-.587T14 10t-.587-1.412T12 8t-1.412.588T10 10t.588 1.413T12 12m0 10q-4.025-3.425-6.012-6.362T4 10.2q0-3.75 2.413-5.975T12 2t5.588 2.225T20 10.2q0 2.5-1.987 5.438T12 22"/> +</svg> \ No newline at end of file diff --git a/repository/module/route.ts b/repository/module/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..31cb992cc67f45b2ca94e3eba7047a5b165d8381 --- /dev/null +++ b/repository/module/route.ts @@ -0,0 +1,114 @@ +import {type SelectedExercise} from "~/types/Exercise"; +import type {ResponseBody} from "~/types/ResponseBody"; +import type {Route} from "~/types/Route"; + +class RouteModule { + private readonly RESSOURCE: string = '/route'; + private routes: Route[] = []; + + /** + * Get an exercise by its id + * @param id the id of the exercise + * @return the exercise + */ + // async getExercise(id: number): Promise<Exercise> { + // if (!this.exercises.length || !this.exercises.find((e) => e.id === id)) { + // await this.fetchExercises(); + // } + // const ex = this.exercises.find((e) => e.id === id); + // if (!ex) { + // return new ExerciseClass(this.getNewId()); + // } + // return ex; + // } + + /** + * Get all exercises. If there is no exercises in local, it will get them from the server. + * @return a list of exercises + */ + // async getExercises(): Promise<Exercise[]> { + // if (!this.exercises.length) { + // await this.fetchExercises(); + // } + // return this.exercises.map((e) => e); + // } + + /** + * Save an exercise. Could be a new one or an existing one. + * @param route the route to save + * @param selectedExercises a list of selected exercises, containing the step and the exercise + * @return either a response as a string or the saved route with their associated status code + */ + async saveRoute(route: Route, selectedExercises: SelectedExercise[]): Promise<ResponseBody<string | number>> { + const method = this.routes.find((e) => e.id === route.id) ? 'PATCH' : 'POST'; + + const data: ResponseBody<string | number> = (await $fetch(this.RESSOURCE, { + method: method, + body: JSON.stringify(RouteToPostBody(route, selectedExercises)) + })); + + if (data.statusCode == 200) { + this.routes = this.routes.map((e) => e.id === route.id ? route : e); + } else if (data.statusCode == 201) { + route.id = data.message as number; + this.routes.push(route); + } + return data; + } + + /** + * Delete an exercise by its id + * @param id the id of the exercise to delete + * @return the response from the server + */ + // async deleteExercise(id: number): Promise<ResponseBody<string>> { + // const data: ResponseBody<string> = (await $fetch(`${this.RESSOURCE}?id=${id}`, { + // method: 'DELETE' + // })); + // + // if (data.statusCode == 200) { + // this.exercises = this.exercises.filter((e) => e.id !== id); + // } + // return data; + // } + + /** + * Fetch all sub-categories from the server + * @return a list of sub-categories + */ + // async fetchSubCategories(): Promise<string[]> { + // return (await $fetch(`${this.RESSOURCE}/fetch-sub-categories`, { + // method: 'GET' + // })).message ?? []; + // } + + /** + * Fetch all exercises from the server + * @private + */ + // private async fetchExercises(): Promise<void> { + // this.exercises = (await $fetch(this.RESSOURCE, { + // method: 'GET' + // })).message ?? []; + // } + +} + +export default RouteModule; + + +function RouteToPostBody(route: Route, selectedExercises: SelectedExercise[]) { + return JSON.stringify({ + name: route.name, + location: route.location, + estimatedTime: route.estimatedTime, + steps: selectedExercises.map(se => { + return { + stepId: se.step.id, + preferredExerciseId: se.exercise.id + }; + }), + // TODO : Add path points between steps + path: JSON.stringify(route.steps.map((s: Step) => [s.latitude, s.longitude])) + }); +} \ No newline at end of file diff --git a/server/routes/route/.post.ts b/server/routes/route/.post.ts new file mode 100644 index 0000000000000000000000000000000000000000..bbe374befe8e402aedab99b0c2cd41bc022f5f74 --- /dev/null +++ b/server/routes/route/.post.ts @@ -0,0 +1,29 @@ +import {defineEventHandler, readBody} from "h3"; +import {useRuntimeConfig} from "#imports"; +import type {ResponseBody} from "../../../types/ResponseBody"; + + +// TODO : TU des différents retours de l'api +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(event); + + const headers = new Headers(); + headers.set('Content-Type', 'application/json'); + headers.set('accept', '*/*'); + headers.set('Cookie', event.headers.get('Cookie') as string); + + const res = await fetch(`${config.public.apiBase}${event.path}`, { + method: 'POST', + headers: headers, + body: await readBody(event) + }); + + if (res.ok) { + return { + statusCode: res.status, + message: (await res.json()).id as number + } as ResponseBody<number>; + } else { + return await res.json() as ResponseBody<string>; + } +}); \ No newline at end of file diff --git a/types/Exercise.ts b/types/Exercise.ts index 46d8099b2474a7064767d359e76b9251b2f7b08a..c6b9c153ff32b182bcc425f249d21fc10bcb6737 100644 --- a/types/Exercise.ts +++ b/types/Exercise.ts @@ -1,4 +1,10 @@ import {ExerciseCategory} from "~/types/enum/ExerciseCategory"; +import type {Step} from "~/types/Step"; + +export type SelectedExercise = { + step: Step, + exercise: Exercise +} export type Exercise = { id: number; diff --git a/types/Route.ts b/types/Route.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f06bd3f86ff8fa2838745582af64fac3e3d16c3 --- /dev/null +++ b/types/Route.ts @@ -0,0 +1,10 @@ +import type {LatLngExpression} from "leaflet"; + +export type Route = { + id: number; + name: string; + location: string; + estimatedTime: number; + steps: Step[]; + path: LatLngExpression[]; +} \ No newline at end of file diff --git a/utils/MapUtils.ts b/utils/MapUtils.ts index a893988c8e83e0e8abc3e013d1e1f7dfdda92944..182fffb2ba2714cffd52fd2e73749fb59b1dc211 100644 --- a/utils/MapUtils.ts +++ b/utils/MapUtils.ts @@ -1,9 +1,9 @@ -import L from "leaflet"; +import L, {type LatLngExpression, type PointTuple} from "leaflet"; -export function setNumberedIcon(text: string, iconSize: [number, number]): L.DivIcon { +export function createMarkerNumberedIcon(text: string, iconSize: [number, number]): L.DivIcon { return L.divIcon({ iconSize: iconSize, - iconAnchor: [iconSize[0]/2, iconSize[1]], + iconAnchor: [iconSize[0] / 2, iconSize[1]], popupAnchor: [0, -iconSize[1]], className: '', html: ` @@ -15,3 +15,28 @@ export function setNumberedIcon(text: string, iconSize: [number, number]): L.Div ` }); } + +export function createMarkerIcon(icon: string, iconSize: [number, number]): L.DivIcon { + return L.icon({ + iconUrl: icon, + iconSize: iconSize, + iconAnchor: [iconSize[0] / 2, iconSize[1]], + popupAnchor: [0, -iconSize[1]], + }); +} + +export function calculateMaxBounds(selectedExercises: SelectedExercise[]): LatLngExpression[] { + return selectedExercises.reduce((acc, se) => { + const step = se.step; + return [ + [Math.min(acc[0][0], step.latitude), Math.min(acc[0][1], step.longitude)], + [Math.max(acc[1][0], step.latitude), Math.max(acc[1][1], step.longitude)] + ]; + }, [[Infinity, Infinity], [-Infinity, -Infinity]]); +} +export function calculateCenter(selectedExercises: SelectedExercise[]): PointTuple { + return selectedExercises.reduce((acc, se) => { + const step = se.step; + return [acc[0] + step.latitude, acc[1] + step.longitude]; + }, [0, 0]).map(c => c / selectedExercises.length) as PointTuple; +}