Initial commit: Coffee Map PWA

Vue 3 + Vite PWA backed by ATProto PDS (coffee.apoena.dev).
Stores coffee spots as dev.apoena.coffeespot records with name,
geolocation, note, and status. Map via MapLibre + OpenFreeMap,
auth via ATProto OAuth, deploy via Docker + Nginx on Coolify.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Julien Calixte
2026-03-28 23:01:17 +01:00
commit 645f93069c
26 changed files with 7241 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
<template>
<div ref="mapEl" class="w-full h-full" />
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import maplibregl from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css'
import type { Shop } from '@/stores/shops'
const props = defineProps<{ shops: Shop[] }>()
const emit = defineEmits<{ select: [shop: Shop] }>()
const mapEl = ref<HTMLElement | null>(null)
let map: maplibregl.Map | null = null
const markers: maplibregl.Marker[] = []
onMounted(() => {
if (!mapEl.value) return
map = new maplibregl.Map({
container: mapEl.value,
// OpenFreeMap — free, no API key, Google-free
style: 'https://tiles.openfreemap.org/styles/liberty',
center: [2.3522, 48.8566], // Paris
zoom: 13,
})
map.addControl(new maplibregl.NavigationControl(), 'top-right')
map.addControl(new maplibregl.GeolocateControl({ trackUserLocation: true }), 'top-right')
map.on('load', () => renderMarkers())
})
onUnmounted(() => {
markers.forEach((m) => m.remove())
map?.remove()
})
watch(() => props.shops, renderMarkers, { deep: true })
function renderMarkers() {
if (!map) return
markers.forEach((m) => m.remove())
markers.length = 0
for (const shop of props.shops) {
if (shop.record.lat == null || shop.record.lng == null) continue
const el = document.createElement('div')
el.className = 'coffee-marker'
el.textContent = shop.record.status === 'visited' ? '✅' : '☕'
el.title = shop.record.name
el.style.cssText = 'cursor:pointer;font-size:1.5rem;filter:drop-shadow(0 1px 2px rgba(0,0,0,.4))'
const marker = new maplibregl.Marker({ element: el })
.setLngLat([shop.record.lng, shop.record.lat])
.addTo(map!)
el.addEventListener('click', () => emit('select', shop))
markers.push(marker)
}
}
</script>