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