build out client page, edit functionality, create functionality, data massager

This commit is contained in:
Casey 2025-11-19 22:25:16 -06:00
parent f510645a31
commit 34f2c110d6
15 changed files with 1571 additions and 1681 deletions

View file

@ -0,0 +1,219 @@
<template>
<div class="map-container">
<div ref="mapElement" class="map" :style="{ height: mapHeight }"></div>
<div v-if="!latitude || !longitude" class="map-overlay">
<div class="no-coordinates">
<i
class="pi pi-map-marker"
style="font-size: 2rem; color: #64748b; margin-bottom: 0.5rem"
></i>
<p>No coordinates available</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from "vue";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
// Fix Leaflet default marker icons
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl:
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
});
const props = defineProps({
latitude: {
type: [Number, String],
required: false,
default: null,
},
longitude: {
type: [Number, String],
required: false,
default: null,
},
addressTitle: {
type: String,
default: "Location",
},
mapHeight: {
type: String,
default: "400px",
},
zoomLevel: {
type: Number,
default: 15,
},
interactive: {
type: Boolean,
default: true,
},
});
const mapElement = ref(null);
let map = null;
let marker = null;
const initializeMap = async () => {
if (!mapElement.value) return;
// Clean up existing map
if (map) {
map.remove();
map = null;
marker = null;
}
const lat = parseFloat(props.latitude);
const lng = parseFloat(props.longitude);
// Only create map if we have valid coordinates
if (!isNaN(lat) && !isNaN(lng)) {
await nextTick();
// Initialize map
map = L.map(mapElement.value, {
zoomControl: props.interactive,
dragging: props.interactive,
touchZoom: props.interactive,
scrollWheelZoom: props.interactive,
doubleClickZoom: props.interactive,
boxZoom: props.interactive,
keyboard: props.interactive,
}).setView([lat, lng], props.zoomLevel);
// Add tile layer
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: "© OpenStreetMap contributors",
}).addTo(map);
// Add marker
marker = L.marker([lat, lng])
.addTo(map)
.bindPopup(
`
<div style="text-align: center;">
<strong>${props.addressTitle}</strong><br>
<small>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</small>
</div>
`,
)
.openPopup();
}
};
const updateMap = () => {
const lat = parseFloat(props.latitude);
const lng = parseFloat(props.longitude);
if (map && !isNaN(lat) && !isNaN(lng)) {
// Update map view
map.setView([lat, lng], props.zoomLevel);
// Update marker
if (marker) {
marker.setLatLng([lat, lng]);
marker.setPopupContent(`
<div style="text-align: center;">
<strong>${props.addressTitle}</strong><br>
<small>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</small>
</div>
`);
} else {
marker = L.marker([lat, lng])
.addTo(map)
.bindPopup(
`
<div style="text-align: center;">
<strong>${props.addressTitle}</strong><br>
<small>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</small>
</div>
`,
)
.openPopup();
}
} else if (!isNaN(lat) && !isNaN(lng)) {
// Coordinates available but no map yet - initialize
initializeMap();
}
};
// Watch for coordinate changes
watch(
() => [props.latitude, props.longitude, props.addressTitle],
() => {
updateMap();
},
{ immediate: false },
);
onMounted(() => {
initializeMap();
});
onUnmounted(() => {
if (map) {
map.remove();
map = null;
marker = null;
}
});
</script>
<style scoped>
.map-container {
position: relative;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--surface-border);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.map {
width: 100%;
z-index: 1;
}
.map-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--surface-ground);
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.no-coordinates {
text-align: center;
color: var(--text-color-secondary);
padding: 2rem;
}
.no-coordinates p {
margin: 0;
font-size: 0.9rem;
}
/* Leaflet popup customization */
:deep(.leaflet-popup-content-wrapper) {
border-radius: 6px;
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
}
:deep(.leaflet-popup-tip) {
background: white;
border: none;
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
}
</style>