From 23bcdacbd36be1cc07ed6d04c7350962d8ffa29d Mon Sep 17 00:00:00 2001 From: Pierre-Alexandre Martin <pierre-alexandre.martin@imt-atlantique.net> Date: Wed, 19 Feb 2025 17:50:53 +0100 Subject: [PATCH] =?UTF-8?q?Ajout=20du=20choix=20des=20exercices=20et=20des?= =?UTF-8?q?=20=C3=A9tapes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/Parcours/ExerciceItem.vue | 32 ++- .../Parcours/{Exercices.vue => Step.vue} | 184 ++++++++++-------- pages/parcours.vue | 10 +- server/utils/mapper/StepMapper.ts | 6 +- types/Exercise.ts | 1 + 5 files changed, 141 insertions(+), 92 deletions(-) rename components/Parcours/{Exercices.vue => Step.vue} (68%) diff --git a/components/Parcours/ExerciceItem.vue b/components/Parcours/ExerciceItem.vue index 8134feb..76927cf 100644 --- a/components/Parcours/ExerciceItem.vue +++ b/components/Parcours/ExerciceItem.vue @@ -5,13 +5,28 @@ import {getExerciseCategoryIcon, translateExerciseCategoryToFrench} from "~/type const props = defineProps<{ exercise: Exercise }>() + +function parseExerciseType(): string { + if (props.exercise.type === 'specific') { + return 'Exercice spécifique' + } else { + return 'Terrain ' + (props.exercise.type?.replace('generic', '').replace(/\s+/g, ' ') ?? '') + } +} </script> <template> <div class="exercise-item" :class="{ 'exercise-selected' : exercise.selected }"> <UIcon :name="getExerciseCategoryIcon(exercise.category)" class="h-9 w-9 m-1.5"/> - <div class="grid gap-2 w-full"> + <div class="grid grid-cols-2 gap-2 w-full"> <p>{{ props.exercise.name }}</p> + <UBadge + variant="subtle" + class="w-fit h-fit place-self-center justify-self-end row-span-2" + :class="props.exercise.type === 'specific' ? 'gold-badge': 'silver-badge'" + > + {{parseExerciseType()}} + </UBadge> <small> {{ translateExerciseCategoryToFrench(props.exercise.category) }} {{ props.exercise.subCategory ? '> ' + props.exercise.subCategory : '' }} @@ -26,8 +41,8 @@ div.exercise-item { } div.exercise-item:hover { - background-color: var(--color-background-800); - border-radius: 25px; + background-color: color-mix(in oklab, var(--color-background-500) 50%, transparent); + border-radius: 5px; } .exercise-item > span { @@ -37,4 +52,15 @@ div.exercise-item:hover { .exercise-selected > span { color: var(--color-3spa_success-500) } + +.gold-badge { + background-color: color-mix(in oklab, #DAA520 10%, transparent); + --tw-ring-color: color-mix(in oklab, #DAA520 50%, transparent); + color: #DAA520; +} +.silver-badge { + background-color: color-mix(in oklab, #C0C0C0 10%, transparent); + --tw-ring-color: color-mix(in oklab, #C0C0C0 50%, transparent); + color: #C0C0C0; +} </style> \ No newline at end of file diff --git a/components/Parcours/Exercices.vue b/components/Parcours/Step.vue similarity index 68% rename from components/Parcours/Exercices.vue rename to components/Parcours/Step.vue index 1734ac1..ea55872 100644 --- a/components/Parcours/Exercices.vue +++ b/components/Parcours/Step.vue @@ -1,6 +1,6 @@ <script setup lang="ts"> -import L, {type LeafletMouseEvent, Marker, type MarkerOptions, 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"; @@ -9,7 +9,6 @@ interface MarkerCluster { markerCluster: L.LayerGroup; markers: Marker[]; } - interface MarkerProps { name?: string, lat: number, @@ -19,6 +18,7 @@ interface MarkerProps { } const {$api} = useNuxtApp(); +const toast = useToast(); // Map utilities const exerciseSearch = ref(''); @@ -29,14 +29,26 @@ let markerCluster: MarkerCluster; let createCluster: MarkerCluster; const exercises = ref<Exercise[]>([]); -const selectedExercises: Exercise[] = []; +const selectedExercises: {step: Step, exercise: Exercise}[] = []; const steps = ref<Step[]>([]); // TODO : Make it dynamic const zoom = ref(17) const center: PointTuple = [47.287, -1.524] -async function updateLocations() { +// TODO : maybe move to a separate file +const contextMenuItems = ref([ + [ + { + label: 'Delete the point', + onSelect() { + deleteMarker(); + } + }, + ], +]) + +async function updateLocations(): Promise<void> { locations.value = steps.value.map((step) => { return { name: step.id.toString(), @@ -51,49 +63,29 @@ async function updateLocations() { } }) 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; + }); + step.terrainType.genericExercises.map((exo) => { + exo.selected = 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', () => { - marker.setIcon(L.icon({ - iconUrl: '/svg/pin.svg', - iconSize: [45, 45], - })); - }); - marker.on('mouseout', () => { - marker.setIcon(L.icon({ - iconUrl: '/svg/pin.svg', - iconSize: [30, 30], - })); - }); + marker.on('mouseover', onMouseoverMarker); + marker.on('mouseout', onMouseoutMarker); }) } -const items = ref([ - [ - { - label: 'Delete the point', - onSelect() { - deleteMarker(); - } - }, - ], -]) - -async function onMapReady() { - createCluster = await useLMarkerCluster({ - leafletObject: map?.value?.leafletObject, - markers: [], - }); - steps.value = (await $api.step.postArea(map.value.leafletObject.getBounds())).message as Step[]; - await updateLocations(); - - map.value.leafletObject.on('moveend', async () => { - steps.value = (await $api.step.postArea(map.value.leafletObject.getBounds())).message as Step[]; - await updateLocations(); +function filterExercise(): Exercise[] { + return exercises.value.filter((exo: Exercise) => { + return exo.name.toLowerCase().includes(exerciseSearch.value.toLowerCase()); }); } @@ -103,46 +95,50 @@ function deleteMarker(): void { inCreation.value = false; } -function MapEvents(e: LeafletMouseEvent): void { - if (inCreation.value) return; - inCreation.value = true; - const newMarker: Marker = new Marker(e.latlng, { - title: 'New marker', - draggable: true, - }); - newMarker.setIcon(L.icon({ - iconUrl: '/svg/pinadd.svg', - iconSize: [30, 30], - })); - createCluster.markerCluster.addLayer(newMarker); -} - -// TODO : reimplement these ? => see git history -function mouseoverExercise(_: Exercise): void { - return +// 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(); } -function mouseoutExercise(_: Exercise): void { - return +function onMouseoverMarker(e: LeafletMouseEvent): void { + e.target.setIcon(L.icon({ + iconUrl: '/svg/pin.svg', + iconSize: [45, 45], + })); } - -function filterExercise(): Exercise[] { - return exercises.value.filter((exo: Exercise) => { - return exo.name.toLowerCase().includes(exerciseSearch.value.toLowerCase()); - }); +function onMouseoutMarker(e: LeafletMouseEvent): void { + e.target.setIcon(L.icon({ + iconUrl: '/svg/pin.svg', + iconSize: [30, 30], + })); } -function onExerciceClick(exo: Exercise): void { +// Events - Exercises +function onExerciceSelect(exo: Exercise): void { if (exo.selected) { - selectedExercises.splice(selectedExercises.indexOf(exo), 1); - document.getElementById(`exercise-item-${exo.id}`)?.classList.remove('exercise-selected'); + selectedExercises.find((se) => se.step.id === exo.relatedStep && se.exercise.id === exo.id); + document.getElementById(`exercise-item-${exo.relatedStep}-${exo.id}`)?.classList.remove('exercise-selected'); exercises.value.find((exercise: Exercise) => { if (exercise.id === exo.id) { exercise.selected = false; } }); } else { - selectedExercises.push(exo); - document.getElementById(`exercise-item-${exo.id}`)?.classList.add('exercise-selected'); + // 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; + } + selectedExercises.push({ + step: steps.value.find((step: Step) => step.id === exo.relatedStep) as Step, + exercise: exo + }); + 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; @@ -150,23 +146,47 @@ function onExerciceClick(exo: Exercise): void { }) } } +// TODO : reimplement these ? => see git history +function onMouseoverExercise(_: Exercise): void { + return +} +function onMouseoutExercise(_: Exercise): void { + return +} -function onMarkerSelect(e: PointerEvent): void { - if (e.target instanceof Marker) { - 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(); - } +// Events - Map +function onMapClick(e: LeafletMouseEvent): void { + if (inCreation.value) return; + inCreation.value = true; + const newMarker: Marker = new Marker(e.latlng, { + title: 'New marker', + draggable: true, + }); + newMarker.setIcon(L.icon({ + iconUrl: '/svg/pinadd.svg', + iconSize: [30, 30], + })); + createCluster.markerCluster.addLayer(newMarker); +} +async function onMapReady(): Promise<void> { + createCluster = await useLMarkerCluster({ + leafletObject: map?.value?.leafletObject, + markers: [], + }); + steps.value = (await $api.step.postArea(map.value.leafletObject.getBounds())).message as Step[]; + await updateLocations(); + + map.value.leafletObject.on('moveend', async () => { + steps.value = (await $api.step.postArea(map.value.leafletObject.getBounds())).message as Step[]; + await updateLocations(); + }); } + </script> <template> <div id="exercices"> - <UContextMenu :items="items" :ui="{ content: 'w-48' }"> + <UContextMenu :items="contextMenuItems" :ui="{ content: 'w-48' }"> <div> <LMap ref="map" @@ -176,7 +196,7 @@ function onMarkerSelect(e: PointerEvent): void { :use-global-leaflet="true" :options="{ zoomSnap: 0.75 }" @ready="onMapReady" - @click="MapEvents" + @click="onMapClick" > <LTileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" @@ -205,12 +225,12 @@ function onMarkerSelect(e: PointerEvent): void { <USeparator class="m-1 w-full"/> <div id="exercices-list" class="flex flex-col w-full h-full overflow-auto gap-1"> <ParcoursExerciceItem - v-for="exercise in filterExercise()" :id="'exercise-item-'+exercise.id" + 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="onExerciceClick(exercise)" - @mouseover="mouseoverExercise(exercise as Exercise)" @mouseleave="mouseoutExercise(exercise as Exercise)"/> + @click="onExerciceSelect(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/pages/parcours.vue b/pages/parcours.vue index 10aacf3..595739f 100644 --- a/pages/parcours.vue +++ b/pages/parcours.vue @@ -2,9 +2,9 @@ const items = [ { - slot: 'exercises', - title: 'Exercises', - description: 'Choix des exercices', + slot: 'step', + title: 'Étapes', + description: 'Choix des étapes', icon: 'material-symbols:exercise-outline' }, { slot: 'parcours', @@ -25,8 +25,8 @@ const stepper = useTemplateRef('stepper') <template> <div id="parcours" class="w-full flex flex-col justify-between"> <UStepper ref="stepper" :items="items"> - <template #exercises> - <ParcoursExercices/> + <template #step> + <ParcoursStep/> </template> <template #parcours> diff --git a/server/utils/mapper/StepMapper.ts b/server/utils/mapper/StepMapper.ts index 0a5c44a..3604c4a 100644 --- a/server/utils/mapper/StepMapper.ts +++ b/server/utils/mapper/StepMapper.ts @@ -11,7 +11,8 @@ export function StepMapperFrom(iStep: IStep): Step { specificExercises: iStep.specificExercise.map((se) => { return { ...ExerciseMapperFrom(se.exercise), - type: 'specific' + type: 'specific', + relatedStep: iStep.id } }), terrainType: { @@ -21,7 +22,8 @@ export function StepMapperFrom(iStep: IStep): Step { genericExercises: iStep.terrainType.genericExercises.map((ge) => { return { ...ExerciseMapperFrom(ge.exercise), - type: 'generic ' + iStep.terrainType.name + type: 'generic ' + iStep.terrainType.name, + relatedStep: iStep.id } }) } diff --git a/types/Exercise.ts b/types/Exercise.ts index 8cf060d..46d8099 100644 --- a/types/Exercise.ts +++ b/types/Exercise.ts @@ -12,6 +12,7 @@ export type Exercise = { video?: string; type?: string; + relatedStep?: number selected?: boolean; }; -- GitLab