From e260ab122dd7dcf3eb63bba1f8f3b210c65bd416 Mon Sep 17 00:00:00 2001
From: Pierre-Alexandre Martin <pierre-alexandre.martin@imt-atlantique.net>
Date: Tue, 25 Feb 2025 16:02:30 +0100
Subject: [PATCH] =?UTF-8?q?Ajout=20des=20;=20au=20linter.=20Ajout=20d'une?=
 =?UTF-8?q?=20premi=C3=A8re=20version=20d'arrangement=20des=20=C3=A9tapes?=
 =?UTF-8?q?=20Mise=20=C3=A0=20jour=20des=20mod=C3=A8les=20de=20l'API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .gitlab-ci.yml                                |   7 +
 app.config.ts                                 |   2 +-
 app.vue                                       |   2 +-
 components/AccountPlaceholder.vue             |   1 +
 components/AppHeader.vue                      |   4 +-
 components/ColorModeButton.vue                |   8 +-
 components/Parcours/Details.vue               |  87 ++++++---
 components/Parcours/ExerciceItem.vue          |   6 +-
 components/Parcours/Order.vue                 | 174 ++++++++++++++++++
 components/Parcours/Step.vue                  |  93 ++++++----
 eslint.config.mjs                             |   7 +-
 middleware/auth.ts                            |   7 +-
 nuxt.config.ts                                |   2 +-
 package-lock.json                             |  21 ++-
 package.json                                  |   3 +-
 pages/about.vue                               |   4 +-
 pages/exercise/[id].vue                       |  24 +--
 pages/exercises.vue                           |  36 ++--
 pages/index.vue                               |   4 +-
 pages/login.vue                               |  14 +-
 pages/parcours.vue                            |  32 +++-
 plugins/vue3-smooth-dnd.client.ts             |   7 +
 public/svg/pinfull.svg                        |  10 +
 repository/module/exercise.ts                 |  13 +-
 repository/module/step.ts                     |   4 +-
 repository/module/user.ts                     |   2 +-
 server/model/IExercise.ts                     |   8 +-
 server/model/IStep.ts                         |  16 +-
 server/routes/auth/login.post.ts              |   6 +-
 server/routes/exercise/.delete.ts             |   8 +-
 server/routes/exercise/.get.ts                |   4 +-
 server/routes/exercise/.patch.ts              |  17 +-
 server/routes/exercise/.post.ts               |  12 +-
 .../exercise/fetch-sub-categories.get.ts      |   4 +-
 server/routes/step/fetch-in-area.post.ts      |   8 +-
 server/utils/mapper/ExerciseMapper.test.ts    |  64 +++----
 server/utils/mapper/ExerciseMapper.ts         |  17 +-
 server/utils/mapper/StepMapper.test.ts        |  72 ++++----
 server/utils/mapper/StepMapper.ts             |  28 +--
 server/utils/mapper/UserMapper.test.ts        |   4 +-
 server/utils/mapper/UserMapper.ts             |   6 +-
 types/Leaflet.ts                              |  15 ++
 types/Step.ts                                 |   1 +
 types/enum/ExerciseCategory.ts                |  14 +-
 types/vue3-smooth-dnd.d.ts                    |  42 +++++
 utils/MapUtils.ts                             |  17 ++
 vitest.config.ts                              |   4 +-
 47 files changed, 644 insertions(+), 297 deletions(-)
 create mode 100644 components/Parcours/Order.vue
 create mode 100644 plugins/vue3-smooth-dnd.client.ts
 create mode 100644 public/svg/pinfull.svg
 create mode 100644 types/Leaflet.ts
 create mode 100644 types/vue3-smooth-dnd.d.ts
 create mode 100644 utils/MapUtils.ts

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 81e4d16..a3f2401 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -72,6 +72,13 @@ lint:
   script:
     - npm run 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
+
 build-image:
   stage: docker
   rules:
diff --git a/app.config.ts b/app.config.ts
index c7a91d6..ae7894b 100644
--- a/app.config.ts
+++ b/app.config.ts
@@ -43,5 +43,5 @@ export default defineAppConfig({
             accent: 'accent',
         }
     }
-})
+});
 
diff --git a/app.vue b/app.vue
index 90d2c9e..3f6bfd1 100644
--- a/app.vue
+++ b/app.vue
@@ -5,7 +5,7 @@ useHead({
     titleTemplate: (titleChunk) => {
         return titleChunk ? `3SPA | ${titleChunk}` : '3SPA';
     }
-})
+});
 </script>
 
 <template>
diff --git a/components/AccountPlaceholder.vue b/components/AccountPlaceholder.vue
index a155f14..67d5282 100644
--- a/components/AccountPlaceholder.vue
+++ b/components/AccountPlaceholder.vue
@@ -2,6 +2,7 @@
 import userData from '~/data/mock/user.json';
 import {UserMapper} from "~/server/utils/mapper/UserMapper";
 
+// FIXME : The APi should be able to return these data, currently only mocked
 const user = ref<User>(UserMapper.from(userData));
 
 function getStatus(): "error" | "success" | "warning" | "neutral" | "primary" | "secondary" | "info" | undefined {
diff --git a/components/AppHeader.vue b/components/AppHeader.vue
index 15d612b..a91851d 100644
--- a/components/AppHeader.vue
+++ b/components/AppHeader.vue
@@ -1,6 +1,6 @@
 <script setup lang="ts">
-const appConfig = useAppConfig()
-const open = ref(false)
+const appConfig = useAppConfig();
+const open = ref(false);
 </script>
 
 <template>
diff --git a/components/ColorModeButton.vue b/components/ColorModeButton.vue
index 5018ac3..e2b94dc 100644
--- a/components/ColorModeButton.vue
+++ b/components/ColorModeButton.vue
@@ -1,13 +1,13 @@
 <script setup>
-const colorMode = useColorMode()
+const colorMode = useColorMode();
 const isDark = computed({
     get() {
-        return colorMode.value === 'dark'
+        return colorMode.value === 'dark';
     },
     set() {
-        colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
+        colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark';
     }
-})
+});
 </script>
 
 <template>
diff --git a/components/Parcours/Details.vue b/components/Parcours/Details.vue
index 8a456cf..2f0662c 100644
--- a/components/Parcours/Details.vue
+++ b/components/Parcours/Details.vue
@@ -1,42 +1,81 @@
 <script setup lang="ts">
-import type {PointTuple} from "leaflet";
-import * as v from 'valibot'
-import type {FormSubmitEvent} from '@nuxt/ui'
+import _L, {type PointTuple} from "leaflet";
+import 'leaflet.markercluster';
+import * as v from 'valibot';
+import {setNumberedIcon} from "~/utils/MapUtils";
+import type {FormSubmitEvent} from '@nuxt/ui';
+import type {Exercise} from "~/types/Exercise";
+import type {Step} from "~/types/Step";
+import type {MarkerProps} from "~/types/Leaflet";
 
-const toast = useToast()
+const props = defineProps<{
+    selectedExercises: {step: Step, exercise: Exercise}[]
+}>();
+
+const toast = useToast();
 
 const schema = v.object({
     parcoursTitle: v.pipe(v.string(), v.nonEmpty('Titre obligatoire')),
     localization: v.pipe(v.string(), v.nonEmpty('Localisation obligatoire')),
     duration: v.pipe(v.string(), v.isoTime('Invalid time format'))
-})
+});
 
 type Schema = v.InferOutput<typeof schema>
 
-const map = ref(null) as any;
+const map = ref<any>(null);
 const state = reactive({
     parcoursTitle: '',
     localization: '',
     duration: '00:30'
-})
-// TODO : calculate these datas from the data
-const zoom = ref(16.5)
-const maxBounds = [[47.283, -1.53], [47.29, -1.52]]
-const center: PointTuple = [47.287, -1.524]
+});
+
+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'})
+    toast.add({title: 'Success', description: 'The form has been submitted.', color: 'success'});
     state.parcoursTitle = '';
     state.localization = '';
-    state.duration = '00:30'
+    state.duration = '00:30';
     // TODO : Send the data to the API
     // $api.parcours.createParcours(state)
-    console.log(event.data)
+    console.log(event.data);
+}
+
+async function onMapReady() {
+     await useLMarkerCluster({
+        leafletObject: map?.value?.leafletObject,
+        options: {
+            disableClusteringAtZoom: zoom,
+        },
+        markers: props.selectedExercises.map(se => {
+            return {
+                name: `exercise-item-${se.step.id}-${se.exercise.id}`,
+                popup: se.exercise.name,
+                lat: se.step.latitude,
+                lng: se.step.longitude,
+                options: {
+                    icon: setNumberedIcon((se.step.stepOrder + 1).toString(), [30, 30])
+                }
+            };
+        }) as MarkerProps[]
+    });
 }
 </script>
 
 <template>
-    <UForm :schema="v.safeParser(schema)" :state="state" class="space-y-4 grid-container" @submit="onSubmit">
+    <UForm id="details-grid" :schema="v.safeParser(schema)" :state="state" class="space-y-4" @submit="onSubmit">
         <UFormField label="Titre du parcours" name="parcoursTitle" class="col-span-2">
             <UInput
                 v-model="state.parcoursTitle" placeholder="Titre du parcours" class="w-full"
@@ -53,7 +92,7 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
             :center="center"
             :max-bounds="maxBounds"
             :use-global-leaflet="true"
-            :options="{ zoomSnap: 0.75 }"
+            @ready="onMapReady"
         >
             <LTileLayer
                 url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
@@ -73,9 +112,12 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
             <UInput v-model="state.duration" type="time" class="w-full" style="margin-right: 20px"/>
         </UFormField>
 
-        <div class="grid-item col-span-2">
-            <a href="https://github.com/kutlugsahin/vue-smooth-dnd?tab=readme-ov-file">Implements drag and drop from
-                this library</a>
+        <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"
+                :key="exercise.id" ref="list"
+                :exercise="exercise as Exercise"
+                class="skeletton gap-4"/>
         </div>
 
         <UButton type="submit" class="col-span-3 w-fit justify-self-center">
@@ -85,16 +127,11 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
 </template>
 
 <style>
-.grid-container {
+#details-grid {
     display: grid;
     grid-template-columns: 3fr 1fr 4fr;
     grid-template-rows: 1fr 1fr 6fr auto;
     height: 65vh;
     gap: 1px;
 }
-
-.grid-item {
-    background-color: lightgray;
-    border: 1px solid gray;
-}
 </style>
\ No newline at end of file
diff --git a/components/Parcours/ExerciceItem.vue b/components/Parcours/ExerciceItem.vue
index 76927cf..a7b01ab 100644
--- a/components/Parcours/ExerciceItem.vue
+++ b/components/Parcours/ExerciceItem.vue
@@ -4,13 +4,13 @@ import {getExerciseCategoryIcon, translateExerciseCategoryToFrench} from "~/type
 
 const props = defineProps<{
     exercise: Exercise
-}>()
+}>();
 
 function parseExerciseType(): string {
     if (props.exercise.type === 'specific') {
-        return 'Exercice spécifique'
+        return 'Exercice spécifique';
     } else {
-        return 'Terrain ' + (props.exercise.type?.replace('generic', '').replace(/\s+/g, ' ') ?? '')
+        return 'Terrain ' + (props.exercise.type?.replace('generic', '').replace(/\s+/g, ' ') ?? '');
     }
 }
 </script>
diff --git a/components/Parcours/Order.vue b/components/Parcours/Order.vue
new file mode 100644
index 0000000..7d58e7c
--- /dev/null
+++ b/components/Parcours/Order.vue
@@ -0,0 +1,174 @@
+<script setup lang="ts">
+import type {LeafletMouseEvent, Marker, PointTuple} from "leaflet";
+import type {DropResult} from "vue3-smooth-dnd";
+import {setNumberedIcon} 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}[]
+}>();
+const emit = defineEmits(['update:selectedExercises']);
+
+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;
+let markerCluster: MarkerCluster;
+
+
+// Map utilities
+async function onDrop(dropResult: DropResult) {
+    const { removedIndex, addedIndex, payload } = dropResult;
+    if (removedIndex === null && addedIndex === null) return;
+
+    const updatedExercises = [...props.selectedExercises];
+    const itemToMove = removedIndex !== null ? updatedExercises.splice(removedIndex, 1)[0] : payload;
+
+    if (addedIndex !== null) {
+        updatedExercises.splice(addedIndex, 0, itemToMove);
+        updatedExercises.forEach((se, index) => se.step.stepOrder = index);
+    }
+
+    emit('update:selectedExercises', updatedExercises);
+    await createCluster();
+}
+async function createCluster() {
+    markerCluster?.markerCluster.clearLayers();
+    markerCluster = await useLMarkerCluster({
+        leafletObject: map?.value?.leafletObject,
+        options: {
+            disableClusteringAtZoom: zoom,
+        },
+        markers: props.selectedExercises.map(se => {
+            return {
+                name: `exercise-item-${se.step.id}-${se.exercise.id}`,
+                popup: se.exercise.name,
+                lat: se.step.latitude,
+                lng: se.step.longitude,
+                options: {
+                    icon: setNumberedIcon((se.step.stepOrder + 1).toString(), [30, 30])
+                }
+            };
+        }) as MarkerProps[]
+    });
+
+    markerCluster.markers.forEach((marker: Marker) => {
+        marker.on('mouseover', onMouseoverMarker);
+        marker.on('mouseout', onMouseoutMarker);
+    });
+}
+function filterExercise(): {step: Step, exercise: Exercise}[] {
+    return props.selectedExercises.filter((se) => {
+        return se.exercise.name.toLowerCase().includes(exerciseSearch.value.toLowerCase());
+    });
+}
+
+// Events - Markers
+function onMouseoverMarker(e: LeafletMouseEvent): void {
+    document.getElementById(e.target.options.title)?.classList.add('exercise-hoovered');
+}
+function onMouseoutMarker(e: LeafletMouseEvent): void {
+    document.getElementById(e.target.options.title)?.classList.remove('exercise-hoovered');
+}
+
+// Events - Exercises
+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]));
+        }
+    });
+}
+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]));
+        }
+    });
+}
+
+async function onMapReady() {
+        await createCluster();
+}
+</script>
+
+<template>
+    <div id="order-grid">
+        <div id="side" class="flex flex-col w-full h-full p-2 overflow-hidden">
+            <UInput
+                id="search-input"
+                v-model="exerciseSearch"
+                icon="material-symbols:search"
+                size="md" variant="outline" placeholder=""
+                :ui="{ base: 'peer' }">
+                <label
+                    for="search-input"
+                    class="pointer-events-none absolute left-6 peer-focus:left-1 -top-2.5 text-[var(--ui-text-highlighted)] text-xs font-medium px-1.5 transition-all
+                    peer-focus:-top-2.5 peer-focus:text-[var(--ui-text-highlighted)] peer-focus:text-xs peer-focus:font-medium
+                    peer-placeholder-shown:text-sm peer-placeholder-shown:text-[var(--ui-text-dimmed)] peer-placeholder-shown:top-1.5 peer-placeholder-shown:font-normal">
+                    <span class="inline-flex bg-[var(--ui-bg)] px-1">Rechercher un exercice</span>
+                </label>
+            </UInput>
+            <USeparator class="m-1 w-full"/>
+            <div id="exercices-list" class="flex flex-col w-full h-full overflow-auto gap-3">
+                <Container @drop="onDrop">
+                    <Draggable v-for="item in filterExercise()" :key="item.step.id *100 + item.exercise.id">
+                        <ParcoursExerciceItem
+                            :id="`exercise-item-${item.step.id}-${item.exercise.id}`"
+                            :exercise="item.exercise as Exercise"
+                            class="skeletton gap-4 draggable-item"
+                            @mouseover="onMouseoverExercise(item)" @mouseleave="onMouseoutExercise(item)"
+                        />
+                    </Draggable>
+                </Container>
+
+            </div>
+        </div>
+        <LMap
+            ref="map"
+            class="row-span-3"
+            style="height: auto;"
+            :zoom="zoom"
+            :min-zoom="zoom"
+            :max-zoom="zoom"
+            :center="center"
+            :max-bounds="maxBounds"
+            :use-global-leaflet="true"
+            @ready="onMapReady"
+        >
+            <LTileLayer
+                url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
+                attribution="&amp;copy; <a href=&quot;https://www.openstreetmap.org/&quot;>OpenStreetMap</a>"
+                layer-type="base"
+                name="OpenStreetMap"
+            />
+        </LMap>
+    </div>
+</template>
+
+<style scoped>
+#order-grid {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    height: 65vh;
+    gap: 1px;
+}
+.exercise-hoovered {
+    background-color: color-mix(in oklab, var(--color-background-500) 50%, transparent);
+    border-radius: 5px;
+    transform: scale(1.05);
+}
+</style>
\ No newline at end of file
diff --git a/components/Parcours/Step.vue b/components/Parcours/Step.vue
index ea55872..15550f6 100644
--- a/components/Parcours/Step.vue
+++ b/components/Parcours/Step.vue
@@ -1,21 +1,15 @@
 <script setup lang="ts">
+import L, {type LeafletMouseEvent, Marker, type PointTuple} from 'leaflet';
 import 'leaflet.markercluster';
-import L, {type LeafletMouseEvent, Marker, type MarkerOptions, type PointTuple} from 'leaflet';
 import type {Exercise} from "~/types/Exercise";
 import type {Step} from "~/types/Step";
+import type {MarkerCluster, MarkerProps} from "~/types/Leaflet";
 
-// TODO : Move to a separate file
-interface MarkerCluster {
-    markerCluster: L.LayerGroup;
-    markers: Marker[];
-}
-interface MarkerProps {
-    name?: string,
-    lat: number,
-    lng: number,
-    options: MarkerOptions,
-    popup?: string
-}
+// Parent props
+const props = defineProps<{
+    selectedExercises: {step: Step, exercise: Exercise}[]
+}>();
+const emit = defineEmits(['update:selectedExercises']);
 
 const {$api} = useNuxtApp();
 const toast = useToast();
@@ -29,12 +23,12 @@ let markerCluster: MarkerCluster;
 let createCluster: MarkerCluster;
 
 const exercises = ref<Exercise[]>([]);
-const selectedExercises: {step: Step, exercise: Exercise}[] = [];
 const steps = ref<Step[]>([]);
+let counter = props.selectedExercises.length;
 
 // TODO : Make it dynamic
-const zoom = ref(17)
-const center: PointTuple = [47.287, -1.524]
+const zoom = ref(17);
+const center: PointTuple = [47.287, -1.524];
 
 // TODO : maybe move to a separate file
 const contextMenuItems = ref([
@@ -46,7 +40,7 @@ const contextMenuItems = ref([
             }
         },
     ],
-])
+]);
 
 async function updateLocations(): Promise<void> {
     locations.value = steps.value.map((step) => {
@@ -60,27 +54,27 @@ async function updateLocations(): Promise<void> {
                     iconSize: [30, 30],
                 })
             }
-        }
-    })
+        };
+    });
     markerCluster?.markerCluster?.clearLayers();
     steps.value.map((step) => {
         step.specificExercises.map((exo) => {
-            exo.selected = 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 = 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;
         });
     });
 
     markerCluster = await useLMarkerCluster({
         leafletObject: map?.value?.leafletObject,
         markers: locations.value as MarkerProps[]
-    })
+    });
     markerCluster.markers.forEach((marker: Marker) => {
         marker.on('click', onMarkerSelect);
         marker.on('mouseover', onMouseoverMarker);
         marker.on('mouseout', onMouseoutMarker);
-    })
+    });
 }
 
 function filterExercise(): Exercise[] {
@@ -121,7 +115,19 @@ function onMouseoutMarker(e: LeafletMouseEvent): void {
 // Events - Exercises
 function onExerciceSelect(exo: Exercise): void {
     if (exo.selected) {
-        selectedExercises.find((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),
+                ...props.selectedExercises.slice(index + 1)
+            ];
+            counter = 0;
+            updatedExercises.forEach((se) => {
+                se.step.stepOrder = counter++;
+            });
+            emit('update:selectedExercises', updatedExercises);
+        }
+
         document.getElementById(`exercise-item-${exo.relatedStep}-${exo.id}`)?.classList.remove('exercise-selected');
         exercises.value.find((exercise: Exercise) => {
             if (exercise.id === exo.id) {
@@ -130,28 +136,36 @@ function onExerciceSelect(exo: Exercise): void {
         });
     } else {
         // check if there is already an exercise selected for this step
-        if (selectedExercises.find((se) => se.step.id === exo.relatedStep) !== undefined) {
-           toast.add({title: 'Erreur', description: 'Un seul exercice par étape est autorisé', color:'warning'});
-           return;
+        if (props.selectedExercises.find((se) => se.step.id === exo.relatedStep) !== undefined) {
+            toast.add({title: 'Erreur', description: 'Un seul exercice par étape est autorisé', color: 'warning'});
+            return;
         }
-        selectedExercises.push({
-            step: steps.value.find((step: Step) => step.id === exo.relatedStep) as Step,
-            exercise: exo
-        });
+        const updatedExercises = [
+            ...props.selectedExercises,
+            {
+                step: {
+                    ...steps.value.find((step: Step) => step.id === exo.relatedStep) as Step,
+                    stepOrder: counter++
+                },
+                exercise: exo
+            }
+        ];
+        emit('update:selectedExercises', updatedExercises);
+
         document.getElementById(`exercise-item-${exo.relatedStep}-${exo.id}`)?.classList.add('exercise-selected');
         exercises.value.find((exercise: Exercise) => {
             if (exercise.id === exo.id) {
                 exercise.selected = true;
             }
-        })
+        });
     }
 }
 // TODO : reimplement these ? => see git history
 function onMouseoverExercise(_: Exercise): void {
-    return
+    return;
 }
 function onMouseoutExercise(_: Exercise): void {
-    return
+    return;
 }
 
 // Events - Map
@@ -204,6 +218,17 @@ async function onMapReady(): Promise<void> {
                         layer-type="base"
                         name="OpenStreetMap"
                     />
+                    <LControl position="topleft">
+                        <UButton
+                            icon="material-symbols:my-location"
+                            color="neutral"
+                            variant="outline"
+                            style="
+                                border: 1px solid rgba(0, 0, 0, 0.2);
+                                border-radius: 4px;
+                                box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);"
+                            @click="map.leafletObject.flyTo(center)"/>
+                    </LControl>
                 </LMap>
             </div>
         </UContextMenu>
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 65e3c51..525307b 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,10 +1,11 @@
 // @ts-check
-import withNuxt from './.nuxt/eslint.config.mjs'
+import withNuxt from './.nuxt/eslint.config.mjs';
 
 export default withNuxt(
     {
         rules: {
-            "@typescript-eslint/no-explicit-any": "off"
+            "@typescript-eslint/no-explicit-any": "off",
+            "semi": [1, "always"]
         }
     },
-)
+);
diff --git a/middleware/auth.ts b/middleware/auth.ts
index 5b041b9..efb2d73 100644
--- a/middleware/auth.ts
+++ b/middleware/auth.ts
@@ -1,9 +1,10 @@
+// TODO : récupérer l'état de connexion de l'utilisateur
 function isAuthenticated(): boolean {
-    return true
+    return true;
 }
 
 export default defineNuxtRouteMiddleware((_, __) => {
     if (!isAuthenticated()) {
-        return navigateTo('/login')
+        return navigateTo('/login');
     }
-})
+});
diff --git a/nuxt.config.ts b/nuxt.config.ts
index f7b4a86..b183b45 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -80,4 +80,4 @@ export default defineNuxtConfig({
             collections: ['material-symbols']
         }
     }
-})
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 8fb3542..17d94eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,8 @@
         "nuxt": "^3.15.4",
         "valibot": "^1.0.0-rc.1",
         "vue": "latest",
-        "vue-router": "latest"
+        "vue-router": "latest",
+        "vue3-smooth-dnd": "^0.0.6"
       },
       "devDependencies": {
         "@iconify-json/lucide": "^1.2.26",
@@ -12026,6 +12027,12 @@
       "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==",
       "license": "MIT"
     },
+    "node_modules/smooth-dnd": {
+      "version": "0.12.1",
+      "resolved": "https://registry.npmjs.org/smooth-dnd/-/smooth-dnd-0.12.1.tgz",
+      "integrity": "sha512-Dndj/MOG7VP83mvzfGCLGzV2HuK1lWachMtWl/Iuk6zV7noDycIBnflwaPuDzoaapEl3Pc4+ybJArkkx9sxPZg==",
+      "license": "MIT"
+    },
     "node_modules/source-map": {
       "version": "0.7.4",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
@@ -14445,6 +14452,18 @@
         "vue": "^3.2.0"
       }
     },
+    "node_modules/vue3-smooth-dnd": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/vue3-smooth-dnd/-/vue3-smooth-dnd-0.0.6.tgz",
+      "integrity": "sha512-CH9ZZhEfE7qU1ef2rlfgBG+nZtQX8PnWlspB2HDDz1uVGU7fXM0Pr65DftBMz4X81S+edw2H+ZFG6Dyb5J81KA==",
+      "license": "MIT",
+      "dependencies": {
+        "smooth-dnd": "^0.12.1"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.11"
+      }
+    },
     "node_modules/webidl-conversions": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
diff --git a/package.json b/package.json
index 3ae1712..f3bc495 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,8 @@
     "nuxt": "^3.15.4",
     "valibot": "^1.0.0-rc.1",
     "vue": "latest",
-    "vue-router": "latest"
+    "vue-router": "latest",
+    "vue3-smooth-dnd": "^0.0.6"
   },
   "devDependencies": {
     "@iconify-json/lucide": "^1.2.26",
diff --git a/pages/about.vue b/pages/about.vue
index 96c894a..9898ad3 100644
--- a/pages/about.vue
+++ b/pages/about.vue
@@ -4,12 +4,12 @@ useHead({
     meta: [
         {name: 'description', content: 'Page d\'accueil de l\'interface administrateur du projet.'}
     ]
-})
+});
 definePageMeta({
     pageTransition: {
         name: 'rotate'
     }
-})
+});
 </script>
 
 <template>
diff --git a/pages/exercise/[id].vue b/pages/exercise/[id].vue
index 8dea90f..4b31f68 100644
--- a/pages/exercise/[id].vue
+++ b/pages/exercise/[id].vue
@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import * as v from 'valibot'
+import * as v from 'valibot';
 import {ModalConfirmation} from "#components";
 import {getExerciseCategoryIcon, translateExerciseCategoryToFrench} from "~/types/enum/ExerciseCategory";
 import type {ResponseBody} from "~/types/ResponseBody";
@@ -20,7 +20,7 @@ const categories = Object.values(ExerciseCategory).map((category) => {
         label: translateExerciseCategoryToFrench(category),
         value: category,
         icon: getExerciseCategoryIcon(category)
-    }
+    };
 });
 const subCategories = ref<string[]>(await $api.exercises.fetchSubCategories());
 
@@ -35,27 +35,27 @@ const schema = v.object({
 });
 
 function onCreate(item: string) {
-    subCategories.value.push(item)
+    subCategories.value.push(item);
 
     // TODO : Need api update
     // Gestion d'une nouvelle sous catégorie ?
     // affichage d'une pop-up ?
-    exercise.subCategory = item
+    exercise.subCategory = item;
 }
 
 async function onSubmit() {
     const data = await $api.exercises.saveExercise(exercise) as ResponseBody<string | IExercise>;
 
     if (data.statusCode === 200 || data.statusCode === 201) {
-        toast.add({title: 'Success', description: 'Votre exercice a bien été enregistré.', color: 'success'})
+        toast.add({title: 'Success', description: 'Votre exercice a bien été enregistré.', color: 'success'});
         await router.push('/exercises');
     } else if (data.statusCode === 401) {
-        toast.add({title: 'Error', description: 'Vous n\'êtes pas autorisé à effectuer cette action.' , color: 'error'})
+        toast.add({title: 'Error', description: 'Vous n\'êtes pas autorisé à effectuer cette action.' , color: 'error'});
     } else if (data.statusCode === 403) {
-        toast.add({title: 'Error', description: 'Vous devez vous identifier avant d\'effectuer cette action.', color: 'error'})
+        toast.add({title: 'Error', description: 'Vous devez vous identifier avant d\'effectuer cette action.', color: 'error'});
     } else {
         console.error('Error saving exercise', data);
-        toast.add({title: 'Error', description: 'Error updating exercise', color: 'error'})
+        toast.add({title: 'Error', description: 'Error updating exercise', color: 'error'});
     }
 }
 
@@ -66,12 +66,12 @@ function onCancel() {
             description: 'Toutes vos modifications seront perdues.',
             confirmIcon: 'material-symbols:googler-travel',
             onSuccess() {
-                modal.close()
-                router.push('/exercises')
+                modal.close();
+                router.push('/exercises');
             }
-        })
+        });
     } else {
-        router.push('/exercises')
+        router.push('/exercises');
     }
 }
 </script>
diff --git a/pages/exercises.vue b/pages/exercises.vue
index d994b31..941e3ba 100644
--- a/pages/exercises.vue
+++ b/pages/exercises.vue
@@ -38,7 +38,7 @@ const columns: TableColumn<Exercise>[] = [
                 icon: getSortIcon(column.getIsSorted()),
                 class: '-mx-2.5',
                 onClick: () => column.toggleSorting(column.getIsSorted() === 'asc')
-            })
+            });
         },
         cell: ({row}) => `#${row.getValue('id')}`
     },
@@ -52,10 +52,10 @@ const columns: TableColumn<Exercise>[] = [
                 icon: getSortIcon(column.getIsSorted()),
                 class: '-mx-2.5',
                 onClick: () => column.toggleSorting(column.getIsSorted() === 'asc')
-            })
+            });
         },
         cell: ({row}) => {
-            return h('div', {class: 'font-medium'}, row.getValue('name'))
+            return h('div', {class: 'font-medium'}, row.getValue('name'));
         }
     },
     {
@@ -66,7 +66,7 @@ const columns: TableColumn<Exercise>[] = [
                 UBadge,
                 {class: 'capitalize justify-center', variant: 'subtle', color:'neutral', icon: getExerciseCategoryIcon(row.getValue('category'))},
                 () => translateExerciseCategoryToFrench(row.getValue('category'))
-            )
+            );
         }
     },
     {
@@ -91,15 +91,15 @@ const columns: TableColumn<Exercise>[] = [
                             class: 'ml-auto'
                         })
                 )
-            )
+            );
         }
     }
 ];
 
 // Get the icon for the sorting direction
 function getSortIcon(isSorted:  false | SortDirection) {
-    if (!isSorted) return 'i-lucide-arrow-up-down'
-    return isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow'
+    if (!isSorted) return 'i-lucide-arrow-up-down';
+    return isSorted === 'asc' ? 'i-lucide-arrow-up-narrow-wide' : 'i-lucide-arrow-down-wide-narrow';
 }
 
 // Calculate the number of displayed elements
@@ -117,12 +117,12 @@ function getDropdownActions(exercise: Exercise): DropdownMenuItem[][] {
                 label: 'Copier l\'identifiant',
                 icon: 'material-symbols:content-copy-outline',
                 onSelect: () => {
-                    navigator.clipboard.writeText(exercise.id.toString())
+                    navigator.clipboard.writeText(exercise.id.toString());
                     toast.add({
                         title: 'Identifiant de l\'exercice copié dans le presse-papier.',
                         color: 'success',
                         icon: 'material-symbols:check-circle-outline'
-                    })
+                    });
                 }
             }
         ],
@@ -131,14 +131,14 @@ function getDropdownActions(exercise: Exercise): DropdownMenuItem[][] {
                 label: 'Afficher',
                 icon: 'material-symbols:visibility',
                 onSelect: () => {
-                    router.push(`/exercise/${exercise.id}`)
+                    router.push(`/exercise/${exercise.id}`);
                 }
             },
             {
                 label: 'Modifier',
                 icon: 'material-symbols:edit',
                 onSelect: () => {
-                    router.push(`/exercise/${exercise.id}?mode=edit`)
+                    router.push(`/exercise/${exercise.id}?mode=edit`);
                 }
             },
             {
@@ -146,23 +146,23 @@ function getDropdownActions(exercise: Exercise): DropdownMenuItem[][] {
                 icon: 'material-symbols:delete-forever-outline',
                 color: 'error',
                 onSelect: async () => {
-                    const response = await $api.exercises.deleteExercise(exercise.id)
+                    const response = await $api.exercises.deleteExercise(exercise.id);
                     if (response.statusCode === 200) {
-                        data.value = await $api.exercises.getExercises()
-                        toast.add({title: 'Success', description: 'Votre exercice a bien été enregistré.', color: 'success'})
+                        data.value = await $api.exercises.getExercises();
+                        toast.add({title: 'Success', description: 'Votre exercice a bien été enregistré.', color: 'success'});
                         await router.push('/exercises');
                     } else if (response.statusCode === 401) {
-                        toast.add({title: 'Error', description: 'Vous n\'êtes pas autorisé à effectuer cette action.' , color: 'error'})
+                        toast.add({title: 'Error', description: 'Vous n\'êtes pas autorisé à effectuer cette action.' , color: 'error'});
                     } else if (response.statusCode === 403) {
-                        toast.add({title: 'Error', description: 'Vous devez vous identifier avant d\'effectuer cette action.', color: 'error'})
+                        toast.add({title: 'Error', description: 'Vous devez vous identifier avant d\'effectuer cette action.', color: 'error'});
                     } else {
                         console.error('Error saving exercise', response);
-                        toast.add({title: 'Error', description: 'Une erreur est survenue.', color: 'error'})
+                        toast.add({title: 'Error', description: 'Une erreur est survenue.', color: 'error'});
                     }
                 }
             }
         ]
-    ]
+    ];
 }
 
 </script>
diff --git a/pages/index.vue b/pages/index.vue
index c9a6593..19c5f99 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -3,10 +3,10 @@ useHead({
     meta: [
         {name: 'description', content: 'Page d\'accueil de l\'interface administrateur du projet.'}
     ]
-})
+});
 definePageMeta({
     middleware: 'auth'
-})
+});
 </script>
 
 <template>
diff --git a/pages/login.vue b/pages/login.vue
index 848e148..5dfdc45 100644
--- a/pages/login.vue
+++ b/pages/login.vue
@@ -28,7 +28,7 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
     const body: LoginBody = {
         username: event.data.email,
         password: event.data.password
-    }
+    };
     const data = await $api.users.postLogin(body);
     if (data.message === 'Login successful') {
         toast.add({
@@ -37,14 +37,14 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
             color: 'success',
             duration: 2000,
             icon: 'material-symbols:check'
-        })
-        await router.push('/')
+        });
+        await router.push('/');
     } else if (data.error  === 'Invalid username or password') {
-        toast.add({title: 'Error', description: data.error, color: 'error'})
-        state.password = ''
-        return new Promise<void>(res => setTimeout(res, 1000))
+        toast.add({title: 'Error', description: data.error, color: 'error'});
+        state.password = '';
+        return new Promise<void>(res => setTimeout(res, 1000));
     } else {
-        toast.add({title: 'Error', description: 'Une erreur est survenue.', color: 'error'})
+        toast.add({title: 'Error', description: 'Une erreur est survenue.', color: 'error'});
     }
 }
 </script>
diff --git a/pages/parcours.vue b/pages/parcours.vue
index 595739f..963edc5 100644
--- a/pages/parcours.vue
+++ b/pages/parcours.vue
@@ -1,5 +1,9 @@
 <script setup lang="ts">
 
+import type {Step} from "~/types/Step";
+import type {Exercise} from "~/types/Exercise";
+
+// TODO : move to separate file ?
 const items = [
     {
         slot: 'step',
@@ -17,24 +21,36 @@ const items = [
         description: 'Détails du parcours',
         icon: 'material-symbols:data-info-alert'
     }
-]
+];
+const selectedExercises = ref<{step: Step, exercise: Exercise}[]>([]);
+const stepper = useTemplateRef('stepper');
 
-const stepper = useTemplateRef('stepper')
+function updateSelectedExercises(newExercises: {step: Step, exercise: Exercise}[]) {
+    selectedExercises.value = newExercises;
+}
 </script>
 
 <template>
     <div id="parcours" class="w-full flex flex-col justify-between">
         <UStepper ref="stepper" :items="items">
             <template #step>
-                <ParcoursStep/>
+                <ParcoursStep
+                    :selected-exercises="selectedExercises"
+                    @update:selected-exercises="updateSelectedExercises"
+                />
             </template>
 
             <template #parcours>
-                Parcours
+                <ParcoursOrder
+                    :selected-exercises="selectedExercises"
+                    @update:selected-exercises="updateSelectedExercises"
+                />
             </template>
 
             <template #details>
-                <ParcoursDetails/>
+                <ParcoursDetails
+                    :selected-exercises="selectedExercises"
+                />
             </template>
         </UStepper>
 
@@ -44,15 +60,15 @@ const stepper = useTemplateRef('stepper')
                 :disabled="!stepper?.hasPrev"
                 @click="stepper?.prev()"
             >
-                Prev
+                Précédent
             </UButton>
 
             <UButton
                 trailing-icon="i-lucide-arrow-right"
-                :disabled="!stepper?.hasNext"
+                :disabled="!stepper?.hasNext || selectedExercises.length === 0"
                 @click="stepper?.next()"
             >
-                Next
+                Suivant
             </UButton>
         </div>
     </div>
diff --git a/plugins/vue3-smooth-dnd.client.ts b/plugins/vue3-smooth-dnd.client.ts
new file mode 100644
index 0000000..5f9e4e9
--- /dev/null
+++ b/plugins/vue3-smooth-dnd.client.ts
@@ -0,0 +1,7 @@
+import { defineNuxtPlugin } from '#app';
+import { Container, Draggable } from 'vue3-smooth-dnd';
+
+export default defineNuxtPlugin((nuxtApp) => {
+    nuxtApp.vueApp.component('Container', Container);
+    nuxtApp.vueApp.component('Draggable', Draggable);
+});
diff --git a/public/svg/pinfull.svg b/public/svg/pinfull.svg
new file mode 100644
index 0000000..c353e8e
--- /dev/null
+++ b/public/svg/pinfull.svg
@@ -0,0 +1,10 @@
+<svg
+        xmlns="http://www.w3.org/2000/svg"
+        width="32"
+        height="32"
+        viewBox="0 0 24 24"
+>
+    <path
+            fill="#1C3238"
+            d="M12 12q.825 0 1.413-.587T14 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/exercise.ts b/repository/module/exercise.ts
index b7b7296..6a391df 100644
--- a/repository/module/exercise.ts
+++ b/repository/module/exercise.ts
@@ -2,7 +2,7 @@ import {type Exercise, ExerciseClass} from "~/types/Exercise";
 import type {ResponseBody} from "~/types/ResponseBody";
 
 class ExerciseModule {
-    private readonly RESSOURCE: string = '/exercise'
+    private readonly RESSOURCE: string = '/exercise';
     private exercises: Exercise[] = [];
 
     /**
@@ -14,7 +14,7 @@ class ExerciseModule {
         if (!this.exercises.length || !this.exercises.find((e) => e.id === id)) {
             await this.fetchExercises();
         }
-        const ex = this.exercises.find((e) => e.id === id)
+        const ex = this.exercises.find((e) => e.id === id);
         if (!ex) {
             return new ExerciseClass(this.getNewId());
         }
@@ -37,18 +37,19 @@ class ExerciseModule {
      * @param exercise the exercise to save
      * @return either a response as a string or the saved exercise with their associated status code
      */
-    async saveExercise(exercise: Exercise): Promise<ResponseBody<string | Exercise>> {
+    async saveExercise(exercise: Exercise): Promise<ResponseBody<string | number>> {
         const method = this.exercises.find((e) => e.id === exercise.id) ? 'PATCH' : 'POST';
 
-        const data: ResponseBody<string | Exercise> = (await $fetch(this.RESSOURCE, {
+        const data: ResponseBody<string | number> = (await $fetch(this.RESSOURCE, {
             method: method,
             body: JSON.stringify(exercise)
         }));
 
         if (data.statusCode == 200) {
-            this.exercises = this.exercises.map((e) => e.id === exercise.id ? data.message as Exercise: e);
+            this.exercises = this.exercises.map((e) => e.id === exercise.id ? exercise : e);
         } else if (data.statusCode == 201) {
-            this.exercises.push(data.message as Exercise);
+            exercise.id = data.message as number;
+            this.exercises.push(exercise);
         }
         return data;
     }
diff --git a/repository/module/step.ts b/repository/module/step.ts
index 26c9dc5..b2a2877 100644
--- a/repository/module/step.ts
+++ b/repository/module/step.ts
@@ -21,7 +21,7 @@ class StepModule {
         return await $fetch(`${this.RESOURCE}/fetch-in-area`, {
             method: 'POST',
             body: JSON.stringify(boundsToAreaBody(body))
-        })
+        });
     }
 }
 
@@ -33,5 +33,5 @@ function boundsToAreaBody(bounds: LatLngBounds): AreaBody {
         latMax: bounds?.getNorthEast().lat,
         longMin: bounds?.getSouthWest().lng,
         longMax: bounds?.getNorthEast().lng
-    }
+    };
 }
\ No newline at end of file
diff --git a/repository/module/user.ts b/repository/module/user.ts
index 338a8ce..65bbe00 100644
--- a/repository/module/user.ts
+++ b/repository/module/user.ts
@@ -14,7 +14,7 @@ class UserModule {
         return await $fetch(`${this.RESOURCE}/login`, {
             method: 'POST',
             body: JSON.stringify(body)
-        })
+        });
     }
 }
 
diff --git a/server/model/IExercise.ts b/server/model/IExercise.ts
index 913059b..cd5f526 100644
--- a/server/model/IExercise.ts
+++ b/server/model/IExercise.ts
@@ -6,10 +6,6 @@ export type IExercise = {
     generalInstructions: string;
     diagram: string;
     video: string;
-    category: {
-        name: string
-    },
-    subCategory: {
-        name: string
-    }
+    category: string,
+    subCategory: string
 }
\ No newline at end of file
diff --git a/server/model/IStep.ts b/server/model/IStep.ts
index 98231a6..2c547ce 100644
--- a/server/model/IStep.ts
+++ b/server/model/IStep.ts
@@ -1,20 +1,10 @@
-import type {IExercise} from "~/repository/model/IExercise";
-
-export type ISpecificExercise = {
-    id: number;
-    exercise: IExercise;
-}
-
-export type IGenericExercise = {
-    id: number;
-    exercise: IExercise;
-}
+import type {IExercise} from "./IExercise";
 
 export type ITerrainType = {
     id: number;
     name: string;
     description: string;
-    genericExercises: IGenericExercise[];
+    exercises: IExercise[];
 }
 
 export type IStep = {
@@ -22,6 +12,6 @@ export type IStep = {
     latitude: number;
     longitude: number;
     radius: number;
-    specificExercise: ISpecificExercise[];
+    specificExercise: IExercise[];
     terrainType: ITerrainType;
 }
diff --git a/server/routes/auth/login.post.ts b/server/routes/auth/login.post.ts
index 3602675..01360e2 100644
--- a/server/routes/auth/login.post.ts
+++ b/server/routes/auth/login.post.ts
@@ -12,8 +12,8 @@ export default defineEventHandler(async (event) => {
         method: 'POST',
         headers: headers,
         body: JSON.stringify(await readBody(event))
-    })
+    });
 
-    appendResponseHeader(event, 'set-cookie', res.headers.getSetCookie())
+    appendResponseHeader(event, 'set-cookie', res.headers.getSetCookie());
     return await res.json();
-})
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/server/routes/exercise/.delete.ts b/server/routes/exercise/.delete.ts
index fbcd077..ef28c75 100644
--- a/server/routes/exercise/.delete.ts
+++ b/server/routes/exercise/.delete.ts
@@ -13,14 +13,14 @@ export default defineEventHandler(async (event) => {
     const res = await fetch(`${config.public.apiBase}${event.path}`, {
         method: 'DELETE',
         headers: headers
-    })
+    });
 
     if (res.ok) {
         return {
             statusCode: res.status,
             message: await res.text()
-        } as ResponseBody<string>
+        } as ResponseBody<string>;
     } else {
-        return await res.json() as ResponseBody<string>
+        return await res.json() as ResponseBody<string>;
     }
-})
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/server/routes/exercise/.get.ts b/server/routes/exercise/.get.ts
index 90ce48a..b5fcd24 100644
--- a/server/routes/exercise/.get.ts
+++ b/server/routes/exercise/.get.ts
@@ -15,10 +15,10 @@ export default defineEventHandler(async (event) => {
     const res = await fetch(`${config.public.apiBase}${event.path}`, {
         method: 'GET',
         headers: headers
-    })
+    });
 
     return {
         statusCode: res.status,
         message: (await res.json() as IExercise[]).map((e) => ExerciseMapperFrom(e))
     } as ResponseBody<Exercise[]>;
-})
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/server/routes/exercise/.patch.ts b/server/routes/exercise/.patch.ts
index fbe5b8f..2d26831 100644
--- a/server/routes/exercise/.patch.ts
+++ b/server/routes/exercise/.patch.ts
@@ -1,8 +1,6 @@
 import {defineEventHandler, readBody} from "h3";
 import {useRuntimeConfig} from "#imports";
-import {ExerciseMapperFrom} from "~/server/utils/mapper/ExerciseMapper";
-import type {IExercise} from "~/server/model/IExercise";
-import type {ResponseBody} from "~/types/ResponseBody";
+import type {ResponseBody} from "../../../types/ResponseBody";
 
 // TODO : TU des différents retours de l'api
 export default defineEventHandler(async (event) => {
@@ -13,21 +11,22 @@ export default defineEventHandler(async (event) => {
     headers.set('accept', '*/*');
     headers.set('Cookie', event.headers.get('Cookie') as string);
 
+    const body = await readBody(event);
     const res = await fetch(`${config.public.apiBase}${event.path}`, {
         method: 'PATCH',
         headers: headers,
-        body: ExerciseToPostBody(await readBody(event))
-    })
+        body: ExerciseToPostBody(body)
+    });
 
     if(res.ok) {
         return {
             statusCode: res.status,
-            message: ExerciseMapperFrom(await res.json() as IExercise)
-        } as ResponseBody<Exercise>
+            message: body.id
+        } as ResponseBody<number>;
     } else {
-        return await res.json() as ResponseBody<string>
+        return await res.json() as ResponseBody<string>;
     }
-})
+});
 
 // TODO : TU
 function ExerciseToPostBody(exercise: Exercise) {
diff --git a/server/routes/exercise/.post.ts b/server/routes/exercise/.post.ts
index 8874e8a..0cda968 100644
--- a/server/routes/exercise/.post.ts
+++ b/server/routes/exercise/.post.ts
@@ -1,8 +1,6 @@
 import {defineEventHandler, readBody} from "h3";
 import {useRuntimeConfig} from "#imports";
-import {ExerciseMapperFrom} from "~/server/utils/mapper/ExerciseMapper";
 import type {Exercise} from "~/types/Exercise";
-import type {IExercise} from "~/server/model/IExercise";
 import type {ResponseBody} from "~/types/ResponseBody";
 
 // TODO : TU des différents retours de l'api
@@ -18,17 +16,17 @@ export default defineEventHandler(async (event) => {
         method: 'POST',
         headers: headers,
         body: ExerciseToPostBody(await readBody(event))
-    })
+    });
 
     if(res.ok) {
         return {
             statusCode: res.status,
-            message: ExerciseMapperFrom(await res.json() as IExercise)
-        } as ResponseBody<Exercise>
+            message: (await res.json()).id as number
+        } as ResponseBody<number>;
     } else {
-        return await res.json() as ResponseBody<string>
+        return await res.json() as ResponseBody<string>;
     }
-})
+});
 
 // TODO : TU
 function ExerciseToPostBody(exercise: Exercise) {
diff --git a/server/routes/exercise/fetch-sub-categories.get.ts b/server/routes/exercise/fetch-sub-categories.get.ts
index 725df11..c017ec9 100644
--- a/server/routes/exercise/fetch-sub-categories.get.ts
+++ b/server/routes/exercise/fetch-sub-categories.get.ts
@@ -12,13 +12,13 @@ export default defineEventHandler(async (event) => {
     const res = await fetch(`${config.public.apiBase}${event.path}`, {
         method: 'GET',
         headers: headers
-    })
+    });
 
     return {
         statusCode: res.status,
         message: mapSubCategoryToString(await res.json())
     } as ResponseBody<string[]>;
-})
+});
 
 type ISubCategory = {
     name: string;
diff --git a/server/routes/step/fetch-in-area.post.ts b/server/routes/step/fetch-in-area.post.ts
index ad382bd..97de982 100644
--- a/server/routes/step/fetch-in-area.post.ts
+++ b/server/routes/step/fetch-in-area.post.ts
@@ -18,14 +18,14 @@ export default defineEventHandler(async (event) => {
         method: 'POST',
         headers: headers,
         body: JSON.stringify(await readBody(event))
-    })
+    });
 
     if(res.ok) {
         return {
             statusCode: res.status,
             message: (await res.json() as IStep[]).map((s) => StepMapperFrom(s))
-        } as ResponseBody<Step[]>
+        } as ResponseBody<Step[]>;
     } else {
-        return await res.json() as ResponseBody<string>
+        return await res.json() as ResponseBody<string>;
     }
-})
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/server/utils/mapper/ExerciseMapper.test.ts b/server/utils/mapper/ExerciseMapper.test.ts
index 60d1e6a..8e594eb 100644
--- a/server/utils/mapper/ExerciseMapper.test.ts
+++ b/server/utils/mapper/ExerciseMapper.test.ts
@@ -1,10 +1,10 @@
-import {expect, test} from 'vitest'
-import {ExerciseMapperFrom, ExerciseMapperTo} from './ExerciseMapper'
+import {expect, test} from 'vitest';
+import {ExerciseMapperFrom, ExerciseMapperTo} from './ExerciseMapper';
 import {ExerciseCategory} from "../../../types/enum/ExerciseCategory";
 import type {Exercise} from "../../../types/Exercise";
 import type {IExercise} from "../../model/IExercise";
 
-const iExercise: IExercise = {
+export const iExercise: IExercise = {
     id: 1,
     name: 'name',
     description: 'description',
@@ -12,15 +12,11 @@ const iExercise: IExercise = {
     generalInstructions: 'generalInstructions',
     diagram: 'diagram',
     video: 'video',
-    category: {
-        name: 'Cardio'
-    },
-    subCategory: {
-        name: 'subCategory'
-    }
-}
+    category: 'Cardio',
+    subCategory: 'subCategory'
+};
 
-const exercise: Exercise = {
+export const exercise: Exercise = {
     id: 1,
     name: 'name',
     description: 'description',
@@ -31,36 +27,36 @@ const exercise: Exercise = {
     category: ExerciseCategory.CARDIO,
     subCategory: 'subCategory',
     selected: false
-}
+};
 
 // ExerciseMapperFrom
 test('ExerciseMapperFrom', () => {
-    const mappedExercise = ExerciseMapperFrom(iExercise)
+    const mappedExercise = ExerciseMapperFrom(iExercise);
 
-    expect(mappedExercise).toStrictEqual(exercise)
-})
+    expect(mappedExercise).toStrictEqual(exercise);
+});
 
 // ExerciseMapperTo
 test('ExerciseMapperTo', () => {
-    const mappedIExercise = ExerciseMapperTo(exercise)
+    const mappedIExercise = ExerciseMapperTo(exercise);
 
-    expect(mappedIExercise).toStrictEqual(iExercise)
-})
+    expect(mappedIExercise).toStrictEqual(iExercise);
+});
 
 // ExerciseMapperFlipFlop
 test('ExerciseMapperFlipFlop', () => {
-    const mappedIExercise = ExerciseMapperTo(ExerciseMapperFrom(iExercise))
-    const mappedExercise = ExerciseMapperFrom(ExerciseMapperTo(exercise))
+    const mappedIExercise = ExerciseMapperTo(ExerciseMapperFrom(iExercise));
+    const mappedExercise = ExerciseMapperFrom(ExerciseMapperTo(exercise));
 
-    expect(mappedIExercise).toStrictEqual(iExercise)
-    expect(mappedExercise).toStrictEqual(exercise)
-})
+    expect(mappedIExercise).toStrictEqual(iExercise);
+    expect(mappedExercise).toStrictEqual(exercise);
+});
 
 // ExerciseMapperFromNull, should throw an error
 test('ExerciseMapperNull', () => {
-    expect(() => ExerciseMapperFrom(null)).toThrow(TypeError)
-    expect(() => ExerciseMapperTo(null)).toThrow(TypeError)
-})
+    expect(() => ExerciseMapperFrom(null)).toThrow(TypeError);
+    expect(() => ExerciseMapperTo(null)).toThrow(TypeError);
+});
 
 // ExerciseMapperFromEmptyOptional
 test('ExerciseMapperEmptyOptional', () => {
@@ -78,11 +74,11 @@ test('ExerciseMapperEmptyOptional', () => {
         generalInstructions: '',
         diagram: '',
         video: ''
-    }
+    };
 
-    const mappedExercise = ExerciseMapperFrom(toMap)
-    expect(mappedExercise).toStrictEqual(expected)
-})
+    const mappedExercise = ExerciseMapperFrom(toMap);
+    expect(mappedExercise).toStrictEqual(expected);
+});
 
 // ExerciseMapperToEmptyOptional
 test('ExerciseMapperToEmptyOptional', () => {
@@ -100,8 +96,8 @@ test('ExerciseMapperToEmptyOptional', () => {
         generalInstructions: '',
         diagram: '',
         video: ''
-    }
+    };
 
-    const mappedIExercise = ExerciseMapperTo(toMap)
-    expect(mappedIExercise).toStrictEqual(expected)
-})
\ No newline at end of file
+    const mappedIExercise = ExerciseMapperTo(toMap);
+    expect(mappedIExercise).toStrictEqual(expected);
+});
\ No newline at end of file
diff --git a/server/utils/mapper/ExerciseMapper.ts b/server/utils/mapper/ExerciseMapper.ts
index cdf02a5..c34d1d5 100644
--- a/server/utils/mapper/ExerciseMapper.ts
+++ b/server/utils/mapper/ExerciseMapper.ts
@@ -1,6 +1,5 @@
 import type {Exercise} from "../../../types/Exercise";
 import type {ExerciseCategory} from "../../../types/enum/ExerciseCategory";
-import type {ExerciseSubCategory} from "../../../types/enum/ExerciseSubCategory";
 import type {IExercise} from "../../model/IExercise";
 
 export function ExerciseMapperFrom(iExercise: IExercise): Exercise {
@@ -12,10 +11,10 @@ export function ExerciseMapperFrom(iExercise: IExercise): Exercise {
         generalInstructions: iExercise.generalInstructions,
         diagram: iExercise.diagram,
         video: iExercise.video,
-        category: iExercise.category?.name as ExerciseCategory,
-        subCategory: iExercise.subCategory?.name as ExerciseSubCategory,
+        category: iExercise.category as ExerciseCategory,
+        subCategory: iExercise.subCategory,
         selected: false
-    }
+    };
 }
 
 export function ExerciseMapperTo(exercise: Exercise): IExercise {
@@ -27,11 +26,7 @@ export function ExerciseMapperTo(exercise: Exercise): IExercise {
         generalInstructions: exercise.generalInstructions ?? '',
         diagram: exercise.diagram ?? '',
         video: exercise.video ?? '',
-        category: {
-            name: exercise.category
-        },
-        subCategory: {
-            name: exercise.subCategory ?? ''
-        }
-    }
+        category: exercise.category,
+        subCategory: exercise.subCategory
+    };
 }
diff --git a/server/utils/mapper/StepMapper.test.ts b/server/utils/mapper/StepMapper.test.ts
index 6a10364..9490ee1 100644
--- a/server/utils/mapper/StepMapper.test.ts
+++ b/server/utils/mapper/StepMapper.test.ts
@@ -1,62 +1,70 @@
-import {expect, test, describe} from 'vitest'
+import {expect, test} from 'vitest';
 import {StepMapperFrom, StepMapperTo} from "./StepMapper";
+import {exercise, iExercise} from "./ExerciseMapper.test";
 import type {IStep} from "../../model/IStep";
 import type {Step} from "../../../types/Step";
 
 const iStep: IStep = {
-    id: 0, 
-    latitude: 0, 
-    longitude: 0, 
-    radius: 0, 
-    specificExercise: [],
+    id: 15,
+    latitude: 1.123456789,
+    longitude: -987654321,
+    radius: 10,
+    specificExercise: Array(5).fill(iExercise),
     terrainType: {
-        id: 0,
+        id: 1,
         name: 'name',
         description: 'description',
-        genericExercises: []
+        exercises: [iExercise]
     }
-}
+};
 
 const step: Step = {
-    id: 0, 
-    latitude: 0, 
-    longitude: 0, 
-    radius: 0, 
-    specificExercises: [],
+    id: 15,
+    latitude: 1.123456789,
+    longitude: -987654321,
+    radius: 10,
+    specificExercises: Array(5).fill({
+        ...exercise,
+        relatedStep: 15,
+        type: 'specific'
+    }),
     terrainType: {
-        id: 0,
+        id: 1,
         name: 'name',
         description: 'description',
-        genericExercises: []
+        genericExercises: [{
+            ...exercise,
+            relatedStep: 15,
+            type: 'generic name'
+        }]
     }
-}
+};
 
-describe.todo('unimplemented suite')
 // StepMapperFrom
 test('StepMapperFrom', () => {
-    const mappedStep = StepMapperFrom(iStep)
+    const mappedStep = StepMapperFrom(iStep);
 
-    expect(mappedStep).toStrictEqual(step)
-})
+    expect(mappedStep).toStrictEqual(step);
+});
 
 // StepMapperTo
 test('StepMapperTo', () => {
-    const mappedIStep = StepMapperTo(step)
+    const mappedIStep = StepMapperTo(step);
 
-    expect(mappedIStep).toStrictEqual(iStep)
-})
+    expect(mappedIStep).toStrictEqual(iStep);
+});
 
 // StepMapperFlipFlop
 test('StepMapperFlipFlop', () => {
-    const mappedIStep = StepMapperTo(StepMapperFrom(iStep))
-    const mappedStep = StepMapperFrom(StepMapperTo(step))
+    const mappedIStep = StepMapperTo(StepMapperFrom(iStep));
+    const mappedStep = StepMapperFrom(StepMapperTo(step));
 
-    expect(mappedIStep).toStrictEqual(iStep)
-    expect(mappedStep).toStrictEqual(step)
-})
+    expect(mappedIStep).toStrictEqual(iStep);
+    expect(mappedStep).toStrictEqual(step);
+});
 
 // StepMapperFromNull, should throw an error
 test('StepMapperNull', () => {
-    expect(() => StepMapperFrom(null)).toThrow(TypeError)
-    expect(() => StepMapperTo(null)).toThrow(TypeError)
-})
\ No newline at end of file
+    expect(() => StepMapperFrom(null)).toThrow(TypeError);
+    expect(() => StepMapperTo(null)).toThrow(TypeError);
+});
\ No newline at end of file
diff --git a/server/utils/mapper/StepMapper.ts b/server/utils/mapper/StepMapper.ts
index 3604c4a..a53fbea 100644
--- a/server/utils/mapper/StepMapper.ts
+++ b/server/utils/mapper/StepMapper.ts
@@ -10,24 +10,24 @@ export function StepMapperFrom(iStep: IStep): Step {
         radius: iStep.radius,
         specificExercises: iStep.specificExercise.map((se) => {
             return {
-                ...ExerciseMapperFrom(se.exercise),
+                ...ExerciseMapperFrom(se),
                 type: 'specific',
                 relatedStep: iStep.id
-            }
+            };
         }),
         terrainType: {
             id: iStep.terrainType.id,
             name: iStep.terrainType.name,
             description: iStep.terrainType.description,
-            genericExercises: iStep.terrainType.genericExercises.map((ge) => {
+            genericExercises: iStep.terrainType.exercises.map((ge) => {
                 return {
-                    ...ExerciseMapperFrom(ge.exercise),
+                    ...ExerciseMapperFrom(ge),
                     type: 'generic ' + iStep.terrainType.name,
                     relatedStep: iStep.id
-                }
+                };
             })
         }
-    }
+    };
 }
 
 export function StepMapperTo(step: Step): IStep {
@@ -36,22 +36,12 @@ export function StepMapperTo(step: Step): IStep {
         latitude: step.latitude,
         longitude: step.longitude,
         radius: step.radius,
-        specificExercise: step.specificExercises.map((se) => {
-            return {
-                id: se.id,
-                exercise: ExerciseMapperTo(se.exercise)
-            }
-        }),
+        specificExercise: step.specificExercises.map((se) => ExerciseMapperTo(se)),
         terrainType: {
             id: step.terrainType.id,
             name: step.terrainType.name,
             description: step.terrainType.description,
-            genericExercises: step.terrainType.genericExercises.map((ge) => {
-                return {
-                    id: ge.id,
-                    exercise: ExerciseMapperTo(ge.exercise)
-                }
-            })
+            exercises: step.terrainType.genericExercises.map((ge) => ExerciseMapperTo(ge))
         }
-    }
+    };
 }
diff --git a/server/utils/mapper/UserMapper.test.ts b/server/utils/mapper/UserMapper.test.ts
index 7d78937..fe86ec0 100644
--- a/server/utils/mapper/UserMapper.test.ts
+++ b/server/utils/mapper/UserMapper.test.ts
@@ -1,5 +1,5 @@
-import {test} from 'vitest'
+import {test} from 'vitest';
 
 test('my test', () => {
     // ... test with Nuxt environment!
-})
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/server/utils/mapper/UserMapper.ts b/server/utils/mapper/UserMapper.ts
index 7d91f70..b446aa4 100644
--- a/server/utils/mapper/UserMapper.ts
+++ b/server/utils/mapper/UserMapper.ts
@@ -1,7 +1,7 @@
-import {Role} from "~/types/enum/Role";
-import {Status} from "~/types/enum/Status";
+import type {User} from "../../../types/User";
+import {Role} from "../../../types/enum/Role";
+import {Status} from "../../../types/enum/Status";
 
-// FIXME : The APi doesn't return these data, currently only mocked
 interface UserDTO {
     id: number;
     lastname: string;
diff --git a/types/Leaflet.ts b/types/Leaflet.ts
new file mode 100644
index 0000000..7702256
--- /dev/null
+++ b/types/Leaflet.ts
@@ -0,0 +1,15 @@
+import type L from "leaflet";
+import type {Marker, MarkerOptions} from "leaflet";
+
+export interface MarkerCluster {
+    markerCluster: L.LayerGroup;
+    markers: Marker[];
+}
+
+export interface MarkerProps {
+    name?: string,
+    lat: number,
+    lng: number,
+    options: MarkerOptions,
+    popup?: string
+}
\ No newline at end of file
diff --git a/types/Step.ts b/types/Step.ts
index 829ac2e..3f5c131 100644
--- a/types/Step.ts
+++ b/types/Step.ts
@@ -12,4 +12,5 @@ export type Step = {
     radius: number;
     specificExercises: Exercise[];
     terrainType: TerrainType;
+    stepOrder: number;
 }
diff --git a/types/enum/ExerciseCategory.ts b/types/enum/ExerciseCategory.ts
index 0e2f636..415d74e 100644
--- a/types/enum/ExerciseCategory.ts
+++ b/types/enum/ExerciseCategory.ts
@@ -1,7 +1,7 @@
 export enum ExerciseCategory {
-    REINFORCEMENT = 'Reinforcement',
+    RENFORCEMENT = 'Renforcement',
     CARDIO = 'Cardio',
-    STRENGTHENING = 'Strengthening',
+    ETIREMENT = 'Étirement',
 }
 
 /**
@@ -11,11 +11,11 @@ export enum ExerciseCategory {
  */
 export function getExerciseCategoryIcon(category: ExerciseCategory): string {
     switch (category) {
-        case ExerciseCategory.REINFORCEMENT:
+        case ExerciseCategory.RENFORCEMENT:
             return 'material-symbols:exercise-outline';
         case ExerciseCategory.CARDIO:
             return 'material-symbols:cardiology-outline';
-        case ExerciseCategory.STRENGTHENING:
+        case ExerciseCategory.ETIREMENT:
             return 'material-symbols:transfer-within-a-station';
         default:
             return 'material-symbols:question-mark';
@@ -29,11 +29,11 @@ export function getExerciseCategoryIcon(category: ExerciseCategory): string {
  */
 export function translateExerciseCategoryToFrench(category: ExerciseCategory): string {
     switch (category) {
-        case ExerciseCategory.REINFORCEMENT:
-            return 'Renforcement musculaire';
+        case ExerciseCategory.RENFORCEMENT:
+            return 'Renforcement';
         case ExerciseCategory.CARDIO:
             return 'Cardio';
-        case ExerciseCategory.STRENGTHENING:
+        case ExerciseCategory.ETIREMENT:
             return 'Étirement';
         default:
             return 'Inconnu';
diff --git a/types/vue3-smooth-dnd.d.ts b/types/vue3-smooth-dnd.d.ts
new file mode 100644
index 0000000..ce847c8
--- /dev/null
+++ b/types/vue3-smooth-dnd.d.ts
@@ -0,0 +1,42 @@
+declare module 'vue3-smooth-dnd' {
+    import type { DefineComponent } from 'vue';
+
+    // Définition des props pour le Container
+    interface ContainerProps {
+        orientation?: 'vertical' | 'horizontal';
+        behaviour?: 'move' | 'copy' | 'drag';
+        groupName?: string;
+        dragHandleSelector?: string;
+        nonDragAreaSelector?: string;
+        lockAxis?: 'x' | 'y';
+        dragClass?: string;
+        dropClass?: string;
+        removeOnDropOut?: boolean;
+        autoScrollEnabled?: boolean;
+        animationDuration?: number;
+        dragBeginDelay?: number;
+        getChildPayload?: (index: number) => any;
+        shouldAnimateDrop?: (sourceContainer: any, payload: any) => boolean;
+        shouldAcceptDrop?: (sourceContainer: any, payload: any) => boolean;
+        getGhostParent?: () => HTMLElement;
+        dropPlaceholder?: object | boolean;
+        tag?: string;
+    }
+
+    // Définition des props pour Draggable (aucune prop spécifique, il agit juste comme un wrapper)
+    interface DraggableProps {
+        [key: string]: unknown;
+    }
+
+    // Extracted from node_modules/smooth-dnd/dist/src/exportTypes.d.ts:14
+    export interface DropResult {
+        removedIndex: number | null;
+        addedIndex: number | null;
+        payload?: any;
+        element?: HTMLElement;
+    }
+
+    // Déclaration des composants Vue avec leurs props
+    export const Container: DefineComponent<ContainerProps>;
+    export const Draggable: DefineComponent<DraggableProps>;
+}
diff --git a/utils/MapUtils.ts b/utils/MapUtils.ts
new file mode 100644
index 0000000..a893988
--- /dev/null
+++ b/utils/MapUtils.ts
@@ -0,0 +1,17 @@
+import L from "leaflet";
+
+export function setNumberedIcon(text: string, iconSize: [number, number]): L.DivIcon {
+    return L.divIcon({
+        iconSize: iconSize,
+        iconAnchor: [iconSize[0]/2, iconSize[1]],
+        popupAnchor: [0, -iconSize[1]],
+        className: '',
+        html: `
+        <div style="position: relative; width: 100%; height: 100%;">
+            <img src='/svg/pinfull.svg' class='w-full h-full' alt=""/>
+            <p style="position: absolute; top: 45%; left: 50%; transform: translate(-50%, -50%);
+                      color: white; text-align: center; width: 100%;">${text}</p>
+        </div>
+    `
+    });
+}
diff --git a/vitest.config.ts b/vitest.config.ts
index 4e5a30b..5a6bbe9 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,4 +1,4 @@
-import {defineVitestConfig} from '@nuxt/test-utils/config'
+import {defineVitestConfig} from '@nuxt/test-utils/config';
 
 export default defineVitestConfig({
     test: {
@@ -8,4 +8,4 @@ export default defineVitestConfig({
             junit: './.nuxt/tests/vitest-junit-report.xml'
         }
     }
-})
\ No newline at end of file
+});
\ No newline at end of file
-- 
GitLab