Migrate grateWizard from React/Next.js to native Nuxt integration
- Port all React components to Vue 3 (GwTabs, GwMN, GwCRA, GwCRS, GwMap, GwRelations, GwPerimeterList) - Port hooks to Vue composables (useCesiumProfiles, useSavedPerimeters) - Copy pure TS services and utils (duniter/, ss58, gratewizard utils) - Add Leaflet + Geoman + MarkerCluster dependencies - Serve grateWizard as popup via /gratewizard?popup (layout: false) and info page on /gratewizard (with Librodrome layout) - Remove public/gratewizard-app/ static Next.js export - Refine UI: compact tabs, buttons, inputs, cards, perimeter list - Use Ğ1 breve everywhere, French locale for all dates and amounts - Rename roles: vendeur→offre / acheteur→reçoit le produit ou service - Rename prix→évaluation in all visible text - Add calculated result column in CRA and CRS relation tables - DU/Ğ1 selector uses toggle switch (same as role toggle) - Auto-scroll to monetary data card on polygon selection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
146
app/components/gratewizard/GwMap.client.vue
Normal file
146
app/components/gratewizard/GwMap.client.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div ref="containerRef" class="gw-map" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import 'leaflet.markercluster';
|
||||
import 'leaflet.markercluster/dist/MarkerCluster.css';
|
||||
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
|
||||
import '@geoman-io/leaflet-geoman-free';
|
||||
import '@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css';
|
||||
import type { GeoMember } from '~/composables/useCesiumProfiles';
|
||||
|
||||
type LoadPolygon = { coords: [number, number][]; name: string } | null;
|
||||
|
||||
const props = defineProps<{
|
||||
members: GeoMember[];
|
||||
clearTrigger?: number;
|
||||
loadPolygon?: LoadPolygon;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
polygonChange: [polygon: [number, number][] | null, name?: string];
|
||||
}>();
|
||||
|
||||
const containerRef = ref<HTMLDivElement | null>(null);
|
||||
let map: L.Map | null = null;
|
||||
let cluster: any = null;
|
||||
let polygonLayer: L.Layer | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
if (!containerRef.value) return;
|
||||
|
||||
map = L.map(containerRef.value).setView([46.5, 2.5], 6);
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
}).addTo(map);
|
||||
|
||||
cluster = (L as any).markerClusterGroup();
|
||||
map.addLayer(cluster);
|
||||
|
||||
// Geoman controls
|
||||
if (map.pm) {
|
||||
map.pm.addControls({
|
||||
position: 'topleft',
|
||||
drawCircle: false,
|
||||
drawCircleMarker: false,
|
||||
drawMarker: false,
|
||||
drawPolyline: false,
|
||||
drawText: false,
|
||||
editMode: false,
|
||||
dragMode: false,
|
||||
cutPolygon: false,
|
||||
rotateMode: false,
|
||||
removalMode: false,
|
||||
});
|
||||
map.pm.setLang('fr');
|
||||
}
|
||||
|
||||
map.on('pm:create', (e: any) => {
|
||||
if (polygonLayer) {
|
||||
map!.removeLayer(polygonLayer);
|
||||
}
|
||||
polygonLayer = e.layer;
|
||||
const latlngs = (e.layer as L.Polygon).getLatLngs()[0] as L.LatLng[];
|
||||
emit('polygonChange', latlngs.map((ll) => [ll.lat, ll.lng]));
|
||||
});
|
||||
|
||||
map.on('pm:remove', () => {
|
||||
polygonLayer = null;
|
||||
emit('polygonChange', null);
|
||||
});
|
||||
|
||||
// Populate initial markers
|
||||
updateMarkers(props.members);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (map) {
|
||||
map.remove();
|
||||
map = null;
|
||||
cluster = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Clear polygon when clearTrigger changes
|
||||
watch(() => props.clearTrigger, () => {
|
||||
if (!props.clearTrigger || !map) return;
|
||||
if (polygonLayer) {
|
||||
const center = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
map.removeLayer(polygonLayer);
|
||||
polygonLayer = null;
|
||||
map.setView(center, zoom, { animate: false });
|
||||
}
|
||||
if (map.pm) {
|
||||
map.pm.disableDraw();
|
||||
}
|
||||
});
|
||||
|
||||
// Load a saved polygon
|
||||
watch(() => props.loadPolygon, (loadPolygon) => {
|
||||
if (!map || !loadPolygon) return;
|
||||
|
||||
if (polygonLayer) {
|
||||
map.removeLayer(polygonLayer);
|
||||
polygonLayer = null;
|
||||
}
|
||||
|
||||
const latlngs = loadPolygon.coords.map(([lat, lng]) => L.latLng(lat, lng));
|
||||
const poly = L.polygon(latlngs, { color: '#3388ff' }).addTo(map);
|
||||
polygonLayer = poly;
|
||||
map.fitBounds(poly.getBounds(), { padding: [20, 20] });
|
||||
emit('polygonChange', loadPolygon.coords, loadPolygon.name);
|
||||
});
|
||||
|
||||
// Update markers when members change
|
||||
function updateMarkers(members: GeoMember[]) {
|
||||
if (!cluster) return;
|
||||
cluster.clearLayers();
|
||||
const markers = members.map((m) =>
|
||||
L.circleMarker([m.lat, m.lon], {
|
||||
radius: 6,
|
||||
color: '#f59e0b',
|
||||
fillColor: '#f59e0b',
|
||||
fillOpacity: 0.7,
|
||||
}).bindPopup(
|
||||
`<strong>${m.title || m.pubkey.slice(0, 8)}</strong>${m.city ? '<br>' + m.city : ''}`
|
||||
)
|
||||
);
|
||||
cluster.addLayers(markers);
|
||||
}
|
||||
|
||||
watch(() => props.members, (members) => {
|
||||
updateMarkers(members);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gw-map {
|
||||
height: 500px;
|
||||
width: 100%;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user