10 Commits

Author SHA1 Message Date
Alexis
0bb00c7623 Changed tilelayer 2026-07-09 19:45:41 +02:00
Alexis
94074d6d77 Updated package 2026-06-05 20:01:59 +02:00
Alexis
a700fe4325 Refactored bike user marker 2026-06-05 19:59:03 +02:00
Alexis
1409e44d4b Changed bg map 2026-06-05 17:06:56 +02:00
Alexis
01ceb46835 Updated favicon and head 2026-06-05 16:46:28 +02:00
Alexis
b26969d682 Merge branch 'feature/map-geoloc' into dev 2026-04-10 22:03:49 +02:00
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
67 changed files with 1692 additions and 1329 deletions

View File

@@ -6,7 +6,58 @@
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/src/assets/css/main.css" rel="stylesheet"> <link href="/src/assets/css/main.css" rel="stylesheet">
<title>Vite App</title>
<title>Vélo Park — AlexisNP</title>
<meta property="og:title" content="Vélo Park — AlexisNP" />
<meta name="description" content="Cartes des emplacements de parcs à vélos de l'agglomération rennaise." />
<meta property="og:description" content="Cartes des emplacements de parcs à vélos de l'agglomération rennaise." />
<meta name="twitter:description" content="Cartes des emplacements de parcs à vélos de l'agglomération rennaise." />
<meta property="author" content="Alexis Pelé" />
<meta property="og:author" content="Alexis Pelé" />
<meta property="og:author:username" content="AlexisNP" />
<link rel="apple-touch-icon" sizes="57x57" href="/favicon/light/apple-icon-57x57.png" media="(prefers-color-scheme: light)">
<link rel="apple-touch-icon" sizes="60x60" href="/favicon/light/apple-icon-60x60.png" media="(prefers-color-scheme: light)">
<link rel="apple-touch-icon" sizes="72x72" href="/favicon/light/apple-icon-72x72.png" media="(prefers-color-scheme: light)">
<link rel="apple-touch-icon" sizes="76x76" href="/favicon/light/apple-icon-76x76.png" media="(prefers-color-scheme: light)">
<link rel="apple-touch-icon" sizes="114x114" href="/favicon/light/apple-icon-114x114.png" media="(prefers-color-scheme: light)">
<link rel="apple-touch-icon" sizes="120x120" href="/favicon/light/apple-icon-120x120.png" media="(prefers-color-scheme: light)">
<link rel="apple-touch-icon" sizes="144x144" href="/favicon/light/apple-icon-144x144.png" media="(prefers-color-scheme: light)">
<link rel="apple-touch-icon" sizes="152x152" href="/favicon/light/apple-icon-152x152.png" media="(prefers-color-scheme: light)">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/light/apple-icon-180x180.png" media="(prefers-color-scheme: light)">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/light/android-icon-192x192.png" media="(prefers-color-scheme: light)">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/light/favicon-32x32.png" media="(prefers-color-scheme: light)">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon/light/favicon-96x96.png" media="(prefers-color-scheme: light)">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/light/favicon-16x16.png" media="(prefers-color-scheme: light)">
<link rel="manifest" href="/favicon/light/manifest.json" media="(prefers-color-scheme: light)">
<meta name="msapplication-TileImage" content="/favicon/light/ms-icon-144x144.png" media="(prefers-color-scheme: light)">
<link rel="apple-touch-icon" sizes="57x57" href="/favicon/dark/apple-icon-57x57.png" media="(prefers-color-scheme: dark)">
<link rel="apple-touch-icon" sizes="60x60" href="/favicon/dark/apple-icon-60x60.png" media="(prefers-color-scheme: dark)">
<link rel="apple-touch-icon" sizes="72x72" href="/favicon/dark/apple-icon-72x72.png" media="(prefers-color-scheme: dark)">
<link rel="apple-touch-icon" sizes="76x76" href="/favicon/dark/apple-icon-76x76.png" media="(prefers-color-scheme: dark)">
<link rel="apple-touch-icon" sizes="114x114" href="/favicon/dark/apple-icon-114x114.png" media="(prefers-color-scheme: dark)">
<link rel="apple-touch-icon" sizes="120x120" href="/favicon/dark/apple-icon-120x120.png" media="(prefers-color-scheme: dark)">
<link rel="apple-touch-icon" sizes="144x144" href="/favicon/dark/apple-icon-144x144.png" media="(prefers-color-scheme: dark)">
<link rel="apple-touch-icon" sizes="152x152" href="/favicon/dark/apple-icon-152x152.png" media="(prefers-color-scheme: dark)">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/dark/apple-icon-180x180.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/dark/android-icon-192x192.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/dark/favicon-32x32.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon/dark/favicon-96x96.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/dark/favicon-16x16.png" media="(prefers-color-scheme: dark)">
<link rel="manifest" href="/favicon/dark/manifest.json" media="(prefers-color-scheme: dark)">
<meta name="msapplication-TileImage" content="/favicon/dark/ms-icon-144x144.png" media="(prefers-color-scheme: dark)">
<meta name="msapplication-TileColor" content="#06df72">
<meta name="theme-color" content="#06df72">
<meta name="color-scheme" content="light" />
<meta property="og:site_name" content="bikes.alexcreates.fr" />
<meta name="twitter:domain" content="bikes.alexcreates.fr" />
<meta property="og:type" content="website" />
</head> </head>
<body> <body>

View File

@@ -16,40 +16,40 @@
}, },
"dependencies": { "dependencies": {
"@phosphor-icons/vue": "^2.2.1", "@phosphor-icons/vue": "^2.2.1",
"@pinia/colada": "^1.0.0", "@pinia/colada": "^1.3.1",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.3.0",
"@vue-leaflet/vue-leaflet": "^0.10.1", "@vue-leaflet/vue-leaflet": "^0.10.1",
"@vueuse/core": "^14.2.1", "@vueuse/core": "^14.3.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"reka-ui": "^2.9.2", "reka-ui": "^2.9.9",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.3.0",
"vue": "^3.5.30", "vue": "^3.5.35",
"vue-leaflet-markercluster": "^0.6.1", "vue-leaflet-markercluster": "^0.6.1",
"vue-router": "^5.0.3" "vue-router": "^5.1.0"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node24": "^24.0.4", "@tsconfig/node24": "^24.0.4",
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"@types/node": "^24.12.0", "@types/node": "^25.9.1",
"@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue": "^6.0.7",
"@vue/eslint-config-typescript": "^14.7.0", "@vue/eslint-config-typescript": "^14.8.0",
"@vue/tsconfig": "^0.9.0", "@vue/tsconfig": "^0.9.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint": "^10.0.3", "eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-oxlint": "~1.51.0", "eslint-plugin-oxlint": "~1.68.0",
"eslint-plugin-vue": "~10.8.0", "eslint-plugin-vue": "~10.9.2",
"jiti": "^2.6.1", "jiti": "^2.7.0",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^9.0.1",
"oxfmt": "^0.37.0", "oxfmt": "^0.53.0",
"oxlint": "~1.51.0", "oxlint": "~1.68.0",
"sass-embedded": "^1.98.0", "sass-embedded": "^1.100.0",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.6.0",
"typescript": "~5.9.3", "typescript": "~6.0.3",
"vite": "^7.3.1", "vite": "^8.0.16",
"vite-plugin-vue-devtools": "^8.0.7", "vite-plugin-vue-devtools": "^8.1.2",
"vue-tsc": "^3.2.5" "vue-tsc": "^3.3.3"
}, },
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"

2622
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
onlyBuiltDependencies: onlyBuiltDependencies:
- '@parcel/watcher'
- esbuild - esbuild
- vue-demi - vue-demi

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#065f46</TileColor></tile></msapplication></browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,46 @@
{
"name": "alexcreates.fr",
"theme_color": "#065f46",
"background_color": "#065f46",
"display": "browser",
"scope": "/",
"start_url": "https://alexcreates.fr",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#065f46</TileColor></tile></msapplication></browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,46 @@
{
"name": "alexcreates.fr",
"theme_color": "#065f46",
"background_color": "#065f46",
"display": "browser",
"scope": "/",
"start_url": "https://alexcreates.fr",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

49
public/robots.txt Normal file
View File

@@ -0,0 +1,49 @@
User-agent: GPTBot
Disallow: /
User-agent: ChatGPT-User
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: PerplexityBot
Disallow: /
User-agent: Amazonbot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: Omgilibot
Disallow: /
User-Agent: FacebookBot
Disallow: /
User-Agent: Applebot
Disallow: /
User-agent: anthropic-ai
Disallow: /
User-agent: Bytespider
Disallow: /
User-agent: Claude-Web
Disallow: /
User-agent: Diffbot
Disallow: /
User-agent: ImagesiftBot
Disallow: /
User-agent: Omgilibot
Disallow: /
User-agent: Omgili
Disallow: /
User-agent: YouBot
Disallow: /
User-agent: AhrefsBot
Disallow: /
User-agent: AhrefsSiteAudit
Disallow: /
User-agent: SemrushBot
Disallow: /
User-agent: YandexMarket
Disallow: /
User-agent: YandexBot
Disallow: /
User-agent: DataForSeoBot
Disallow: /
Sitemap: https://alexcreates.fr/sitemap-index.xml

View File

@@ -21,19 +21,19 @@ defineProps<{
<style lang="scss"> <style lang="scss">
.marker-cluster div { .marker-cluster div {
font-family: var(--default-font-family); font-family: var(--default-font-family);
font-size: .9em; font-size: .95em;
} }
.marker-cluster span { .marker-cluster span {
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
opacity: .85; opacity: .5;
} }
.marker-cluster-small { .marker-cluster-small {
background-color: color-mix(in srgb, var(--color-green-400) 10%, transparent); background-color: color-mix(in srgb, var(--color-green-400) 7.5%, transparent);
div { div {
background-color: color-mix(in srgb, var(--color-green-400) 15%, transparent); background-color: color-mix(in srgb, var(--color-green-400) 10%, transparent);
} }
} }
@@ -41,7 +41,7 @@ defineProps<{
background-color: color-mix(in srgb, var(--color-amber-500) 12.5%, transparent); background-color: color-mix(in srgb, var(--color-amber-500) 12.5%, transparent);
div { div {
background-color: color-mix(in srgb, var(--color-amber-500) 20%, transparent); background-color: color-mix(in srgb, var(--color-amber-500) 15%, transparent);
} }
} }
@@ -49,7 +49,7 @@ defineProps<{
background-color: color-mix(in srgb, var(--color-rose-500) 15%, transparent); background-color: color-mix(in srgb, var(--color-rose-500) 15%, transparent);
div { div {
background-color: color-mix(in srgb, var(--color-rose-500) 25%, transparent); background-color: color-mix(in srgb, var(--color-rose-500) 20%, transparent);
} }
} }
</style> </style>

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,21 +2,32 @@
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'
import type { ApiResponse } from '@/types/Api' 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_DARK, MAP_TILELAYER_URL_LIGHT, 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, LMap, 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 } from '@phosphor-icons/vue'
import { useDark } from '@vueuse/core'
import BikeUserMarker from './BikeUserMarker.vue'
// Map setup
const mapRef = useTemplateRef<{ leafletObject: Map }>('map')
const isDark = useDark({ storageKey: "theme" })
const mapTilelayer = computed(() => {
return isDark.value ? MAP_TILELAYER_URL_DARK : MAP_TILELAYER_URL_LIGHT
})
// 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 +89,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">
@@ -101,7 +121,9 @@ const { filterUncovered, filterCovered, filterKorrigo } = storeToRefs(useMap())
<LMap ref="map" :min-zoom v-model:zoom="zoom" :center="center" :max-bounds :max-bounds-viscosity <LMap ref="map" :min-zoom v-model:zoom="zoom" :center="center" :max-bounds :max-bounds-viscosity
:options="extraOptions" :useGlobalLeaflet="true"> :options="extraOptions" :useGlobalLeaflet="true">
<LControlZoom position="bottomright" /> <LControlZoom position="bottomright" />
<LTileLayer v-once :url="MAP_TILELAYER_URL" layer-type="base" /> <LTileLayer v-once :url="mapTilelayer" layer-type="base" />
<BikeUserMarker v-if="userCoords" />
<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 />

View File

@@ -84,7 +84,7 @@ const isMarkerVisible = computed(() => isMarkerVisibleLayers.value && isMarkerVi
{ 'korrigo': park.condition_acces === KORRIGO_KEY }, { 'korrigo': park.condition_acces === KORRIGO_KEY },
{ 'highlight': park.type === BOXED_KEY || park.type === COVERED_KEY }, { 'highlight': park.type === BOXED_KEY || park.type === COVERED_KEY },
)"> )">
<PhBicycle v-if="park.type === UNCOVERED_KEY" size="18" /> <PhBicycle v-if="park.type === UNCOVERED_KEY" size="18" weight="fill" />
<PhWarehouse v-else-if="park.type === COVERED_KEY" size="18" weight="light" /> <PhWarehouse v-else-if="park.type === COVERED_KEY" size="18" weight="light" />
<PhSquareHalf v-else-if="park.type === BOXED_KEY" size="18" weight="light" /> <PhSquareHalf v-else-if="park.type === BOXED_KEY" size="18" weight="light" />
<PhLockKey v-else-if="park.condition_acces === KORRIGO_KEY" size="18" weight="fill" /> <PhLockKey v-else-if="park.condition_acces === KORRIGO_KEY" size="18" weight="fill" />
@@ -95,8 +95,10 @@ const isMarkerVisible = computed(() => isMarkerVisibleLayers.value && isMarkerVi
<style lang="scss"> <style lang="scss">
.leaflet-marker-icon:not(.marker-cluster) { .leaflet-marker-icon:not(.marker-cluster) {
border-radius: 50%; border-radius: 50%;
color: var(--color-muted-foreground); color: color-mix(in srgb, var(--color-foreground) 80%, var(--color-background));
background-color: var(--color-background); background-color: color-mix(in srgb, var(--color-background) 85%, var(--color-foreground));
border: 1px solid color-mix(in srgb, var(--color-muted-foreground) 50%, var(--color-background));
box-shadow: var(--shadow);
display: grid; display: grid;
place-items: center; place-items: center;

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import { useMap } from '@/stores/map';
import { PhMapPin } from '@phosphor-icons/vue';
import { LIcon, LMarker } from '@vue-leaflet/vue-leaflet';
import type { PointTuple } from 'leaflet';
import { storeToRefs } from 'pinia';
import { computed, ref } from 'vue';
const normalIconSize: PointTuple = [30, 30]
const iconSize = ref<PointTuple>(normalIconSize)
const iconAnchor = computed<PointTuple>(() => [iconSize.value[0] / 2, iconSize.value[1]])
// Geolocation
const { userCoords } = storeToRefs(useMap())
</script>
<template>
<LMarker v-if="userCoords" :lat-lng="[userCoords.latitude, userCoords.longitude]" :icon-size :icon-anchor>
<LIcon>
<PhMapPin size="20" weight="fill" class="text-amber-400" />
</LIcon>
</LMarker>
</template>

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,
} }
const maxClusterRadius = 40 // Cluster layers options
const maxClusterRadius = 60
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,

View File

@@ -1,5 +1,5 @@
export const MAP_TILELAYER_URL = export const MAP_TILELAYER_URL_LIGHT = 'http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png'
'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png' export const MAP_TILELAYER_URL_DARK = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png'
export const API_BASE_URL = export const API_BASE_URL =
'https://data.rennesmetropole.fr/api/explore/v2.1/catalog/datasets/parkings_velos_sur_rennes_metropole/records' 'https://data.rennesmetropole.fr/api/explore/v2.1/catalog/datasets/parkings_velos_sur_rennes_metropole/records'