4 Commits

Author SHA1 Message Date
Alexis
cd5e821308 Added flyTo on geoloc click 2026-04-10 22:03:40 +02:00
Alexis
a6a5fe892d Added geoloc marker to map 2026-04-10 21:06:18 +02:00
Alexis
2b1ed5c09e Added geoloc button to filters 2026-04-06 22:26:51 +02:00
Alexis
d8ee9a20e0 Merge branch 'ux/map-loading' into dev 2026-04-04 22:45:43 +02:00
3 changed files with 88 additions and 11 deletions

View File

@@ -1,21 +1,72 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useMap } from '@/stores/map' import { useMap } from '@/stores/map'
import { PhMapPin, PhSpinner } from '@phosphor-icons/vue'
import { useGeolocation, usePermission } from '@vueuse/core'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { computed, ref, watch } from 'vue'
import ParkingSpaceRange from './filters/ParkingSpaceRange.vue'
import Checkbox from './forms/Checkbox.vue' import Checkbox from './forms/Checkbox.vue'
import ParkingSpaceRange from './filters/ParkingSpaceRange.vue';
const { filterCovered, filterUncovered, filterKorrigo, filterCargoOnly, filterStdOnly } = storeToRefs(useMap()) const { userCoords, filterCovered, filterUncovered, filterKorrigo, filterCargoOnly, filterStdOnly } = storeToRefs(useMap())
defineProps<{ defineProps<{
nbUncovered?: number nbUncovered?: number
nbCovered?: number nbCovered?: number
nbKorrigo?: number nbKorrigo?: number
}>() }>()
// Geolocation features
const { coords, error, resume, pause, locatedAt } = useGeolocation({ immediate: false })
const geolocationPermission = usePermission('geolocation')
const isRequesting = ref(false)
const hasLocation = computed(() => locatedAt.value !== null && !error.value)
// Auto-start if permission was already granted
watch(geolocationPermission, (permission) => {
if (permission === 'granted' && !hasLocation.value) {
resume()
}
}, { immediate: true })
function toggleGeolocation() {
if (hasLocation.value) {
pause()
isRequesting.value = false
emit('click-geoloc')
} else {
isRequesting.value = true
resume()
}
}
watch(locatedAt, () => { isRequesting.value = false })
watch(error, () => { isRequesting.value = false })
watch(coords, () => {
if (coords.value.latitude && coords.value.longitude) {
userCoords.value = coords.value
}
})
const emit = defineEmits(['click-geoloc'])
</script> </script>
<template> <template>
<aside class="max-w-xs absolute top-5 left-5 z-10 p-2 bg-slate-50 text-slate-950 rounded-xs"> <div class="max-w-xs absolute top-5 left-5 z-10 grid gap-2">
<form class="grid grid-cols-1 gap-3"> <button @click="toggleGeolocation" :title="error?.message ?? (hasLocation ? 'Aller à ma position' : 'Me localiser')"
:class="[
'size-9 grid place-items-center border shadow rounded-full cursor-pointer transition-colors',
hasLocation
? 'bg-primary text-primary-foreground border-primary'
: 'bg-slate-50 text-slate-950 border-transparent hover:bg-background hover:text-primary hover:border-primary',
error ? 'text-destructive border-destructive hover:text-destructive hover:border-destructive' : ''
]">
<PhSpinner v-if="isRequesting" :size="20" class="animate-spin" />
<PhMapPin v-else :size="20" weight="fill" />
</button>
<form class="p-2 bg-slate-50 text-slate-950 rounded-xs grid grid-cols-1 gap-3">
<fieldset> <fieldset>
<legend <legend
class="mb-1.5 text-sm font-medium relative isolate before:contents-[''] before:block before:w-full before:bg-amber-500/15 before:h-2 before:absolute before:bottom-0.5 before:-z-10"> class="mb-1.5 text-sm font-medium relative isolate before:contents-[''] before:block before:w-full before:bg-amber-500/15 before:h-2 before:absolute before:bottom-0.5 before:-z-10">
@@ -43,5 +94,5 @@ defineProps<{
</div> </div>
</fieldset> </fieldset>
</form> </form>
</aside> </div>
</template> </template>

View File

@@ -2,7 +2,7 @@
import 'leaflet/dist/leaflet.css' import 'leaflet/dist/leaflet.css'
import 'vue-leaflet-markercluster/dist/style.css' import 'vue-leaflet-markercluster/dist/style.css'
import L from 'leaflet' import L, { Map } from 'leaflet'
globalThis.L = L globalThis.L = L
import { useMap } from '@/stores/map' import { useMap } from '@/stores/map'
@@ -10,13 +10,17 @@ import type { ApiResponse } from '@/types/Api'
import type { BikeParking } from '@/types/Bikes' import type { BikeParking } from '@/types/Bikes'
import { API_BASE_URL, API_LIMIT, MAP_TILELAYER_URL, SpotAccess, SpotType } from '@/utils/const' import { API_BASE_URL, API_LIMIT, MAP_TILELAYER_URL, SpotAccess, SpotType } from '@/utils/const'
import { useQuery } from '@pinia/colada' import { useQuery } from '@pinia/colada'
import { LControlZoom, LMap, LTileLayer } from '@vue-leaflet/vue-leaflet' import { LControlZoom, LIcon, LMap, LMarker, LTileLayer } from '@vue-leaflet/vue-leaflet'
import BikeFilters from './BikeFilters.vue' import BikeFilters from './BikeFilters.vue'
import { computed, ref } from 'vue' import { computed, ref, useTemplateRef } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import BikeClusterLayer from './BikeClusterLayer.vue' import BikeClusterLayer from './BikeClusterLayer.vue'
import { PhCircleNotch } from '@phosphor-icons/vue' import { PhCircleNotch, PhMapPin } from '@phosphor-icons/vue'
// Map setup
const mapRef = useTemplateRef<{ leafletObject: Map }>('map')
// Data fetching
const fetchedPages = ref(0) const fetchedPages = ref(0)
const totalPages = ref(1) const totalPages = ref(1)
const progress = computed(() => Math.round((fetchedPages.value / totalPages.value) * 100)) const progress = computed(() => Math.round((fetchedPages.value / totalPages.value) * 100))
@@ -78,13 +82,22 @@ const premiumParkingsSpots = computed(() =>
const { zoom, minZoom, center, maxBounds, maxBoundsViscosity, maxClusterRadius, disableClusteringAtZoom, extraOptions } = useMap() const { zoom, minZoom, center, maxBounds, maxBoundsViscosity, maxClusterRadius, disableClusteringAtZoom, extraOptions } = useMap()
const { filterUncovered, filterCovered, filterKorrigo } = storeToRefs(useMap()) const { filterUncovered, filterCovered, filterKorrigo } = storeToRefs(useMap())
// Geolocation
const { userCoords } = storeToRefs(useMap())
function handleClickGeoloc() {
if (userCoords.value) {
mapRef.value?.leafletObject.flyTo([userCoords.value.latitude, userCoords.value.longitude], 12)
}
}
</script> </script>
<template> <template>
<Transition enter-from-class="opacity-0 translate-y-2" <Transition enter-from-class="opacity-0 translate-y-2"
enter-active-class="transition-all duration-300 ease-out delay-500" enter-to-class="opacity-100 translate-y-0"> enter-active-class="transition-all duration-300 ease-out delay-500" enter-to-class="opacity-100 translate-y-0">
<BikeFilters v-if="state.status === 'success'" :nb-uncovered="nonCoveredParkingsSpots" <BikeFilters v-if="state.status === 'success'" :nb-uncovered="nonCoveredParkingsSpots"
:nb-covered="coveredParkingsSpots" :nb-korrigo="premiumParkingsSpots" /> :nb-covered="coveredParkingsSpots" :nb-korrigo="premiumParkingsSpots" @click-geoloc="handleClickGeoloc" />
</Transition> </Transition>
<main class="relative z-0 h-screen w-screen grid place-items-center"> <main class="relative z-0 h-screen w-screen grid place-items-center">
@@ -103,6 +116,12 @@ const { filterUncovered, filterCovered, filterKorrigo } = storeToRefs(useMap())
<LControlZoom position="bottomright" /> <LControlZoom position="bottomright" />
<LTileLayer v-once :url="MAP_TILELAYER_URL" layer-type="base" /> <LTileLayer v-once :url="MAP_TILELAYER_URL" layer-type="base" />
<LMarker v-if="userCoords" :lat-lng="[userCoords.latitude, userCoords.longitude]">
<LIcon>
<PhMapPin size="24" weight="fill" class="text-amber-400" />
</LIcon>
</LMarker>
<BikeClusterLayer :parkings="coveredParkings" group="covered" :visible="filterCovered" :max-cluster-radius <BikeClusterLayer :parkings="coveredParkings" group="covered" :visible="filterCovered" :max-cluster-radius
:disable-clustering-at-zoom /> :disable-clustering-at-zoom />
<BikeClusterLayer :parkings="nonCoveredParkings" group="non-covered" :visible="filterUncovered" <BikeClusterLayer :parkings="nonCoveredParkings" group="non-covered" :visible="filterUncovered"

View File

@@ -6,6 +6,7 @@ export const SPOTS_MIN = 1
export const SPOTS_MAX = 99 export const SPOTS_MAX = 99
export const useMap = defineStore('map', () => { export const useMap = defineStore('map', () => {
// Map config and options
const zoom = ref(10) const zoom = ref(10)
const minZoom = 8 const minZoom = 8
const center = ref<PointTuple>([48.11180645878813, -1.6637869497745246]) const center = ref<PointTuple>([48.11180645878813, -1.6637869497745246])
@@ -18,9 +19,14 @@ export const useMap = defineStore('map', () => {
zoomControl: false, zoomControl: false,
} }
// Cluster layers options
const maxClusterRadius = 40 const maxClusterRadius = 40
const disableClusteringAtZoom = 17 const disableClusteringAtZoom = 17
// Geolocation
const userCoords = ref<Omit<GeolocationCoordinates, 'toJSON'> | null>(null)
// Active filters
const filterUncovered = ref(true) const filterUncovered = ref(true)
const filterCovered = ref(true) const filterCovered = ref(true)
const filterKorrigo = ref(true) const filterKorrigo = ref(true)
@@ -36,9 +42,10 @@ export const useMap = defineStore('map', () => {
center, center,
maxBounds, maxBounds,
maxBoundsViscosity, maxBoundsViscosity,
extraOptions,
maxClusterRadius, maxClusterRadius,
disableClusteringAtZoom, disableClusteringAtZoom,
extraOptions, userCoords,
filterUncovered, filterUncovered,
filterCovered, filterCovered,
filterKorrigo, filterKorrigo,