mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-20 09:31:11 -05:00
Implement LocationDisplayComponent and LocationMapComponent for interactive map features; add event handling and state management
This commit is contained in:
190
resources/views/livewire/location/location-display.blade.php
Normal file
190
resources/views/livewire/location/location-display.blade.php
Normal file
@@ -0,0 +1,190 @@
|
||||
@push('styles')
|
||||
<style>
|
||||
/* Ensure map container and its elements stay below other UI elements */
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
z-index: 1 !important;
|
||||
}
|
||||
.leaflet-control {
|
||||
z-index: 2 !important;
|
||||
}
|
||||
|
||||
/* Custom marker cluster styling */
|
||||
.marker-cluster {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
border: 2px solid rgba(0, 120, 255, 0.5);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.marker-cluster div {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
padding-top: 5px;
|
||||
background-color: rgba(0, 120, 255, 0.6);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Custom info window styling */
|
||||
.location-info-window {
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.location-info-window h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
<div class="location-display-component">
|
||||
<div wire:ignore class="relative mb-4" style="z-index: 1;">
|
||||
<div id="locationMap" class="h-[400px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
|
||||
|
||||
@if($showInfoWindow && $activeMarker)
|
||||
<div class="location-info-window absolute top-4 right-4 z-10">
|
||||
<div class="flex justify-between items-start">
|
||||
<h3 class="text-lg font-semibold">{{ $activeMarker['name'] ?? 'Location' }}</h3>
|
||||
<button wire:click="closeInfoWindow" class="text-gray-500 hover:text-gray-700">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
@if(isset($activeMarker['address']))
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ $activeMarker['address']['street'] ?? '' }}
|
||||
{{ $activeMarker['address']['city'] ?? '' }}
|
||||
{{ $activeMarker['address']['state'] ?? '' }}
|
||||
{{ $activeMarker['address']['country'] ?? '' }}
|
||||
</p>
|
||||
@endif
|
||||
@if(isset($activeMarker['description']))
|
||||
<p class="mt-2 text-sm">{{ $activeMarker['description'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('livewire:initialized', function () {
|
||||
const map = L.map('locationMap');
|
||||
let markerClusterGroup;
|
||||
let markers = {};
|
||||
|
||||
// Initialize map with OSM tiles
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Initialize marker cluster group
|
||||
markerClusterGroup = L.markerClusterGroup({
|
||||
maxClusterRadius: @entangle('clusterRadius'),
|
||||
spiderfyOnMaxZoom: true,
|
||||
showCoverageOnHover: false,
|
||||
zoomToBoundsOnClick: true,
|
||||
removeOutsideVisibleBounds: true,
|
||||
iconCreateFunction: function(cluster) {
|
||||
const count = cluster.getChildCount();
|
||||
return L.divIcon({
|
||||
html: `<div><span>${count}</span></div>`,
|
||||
className: 'marker-cluster',
|
||||
iconSize: L.point(40, 40)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
map.addLayer(markerClusterGroup);
|
||||
|
||||
// Listen for marker updates from Livewire
|
||||
@this.on('updateMarkers', (markerData) => {
|
||||
// Clear existing markers
|
||||
markerClusterGroup.clearLayers();
|
||||
markers = {};
|
||||
|
||||
// Add new markers
|
||||
markerData.forEach(marker => {
|
||||
const leafletMarker = L.marker([marker.lat, marker.lng], {
|
||||
icon: getMarkerIcon(marker.category)
|
||||
});
|
||||
|
||||
leafletMarker.on('click', () => {
|
||||
@this.markerClicked(marker.id);
|
||||
});
|
||||
|
||||
markers[marker.id] = leafletMarker;
|
||||
markerClusterGroup.addLayer(leafletMarker);
|
||||
});
|
||||
|
||||
// Fit bounds if available
|
||||
if (@this.bounds) {
|
||||
map.fitBounds([
|
||||
[@this.bounds.south, @this.bounds.west],
|
||||
[@this.bounds.north, @this.bounds.east]
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle cluster clicks
|
||||
markerClusterGroup.on('clusterclick', (e) => {
|
||||
const clusterMarkers = e.layer.getAllChildMarkers().map(marker => {
|
||||
const markerId = Object.keys(markers).find(key => markers[key] === marker);
|
||||
return { id: markerId, lat: marker.getLatLng().lat, lng: marker.getLatLng().lng };
|
||||
});
|
||||
@this.clusterClicked(clusterMarkers);
|
||||
});
|
||||
|
||||
// Handle map bounds changes
|
||||
map.on('moveend', () => {
|
||||
const bounds = map.getBounds();
|
||||
@this.boundsChanged({
|
||||
north: bounds.getNorth(),
|
||||
south: bounds.getSouth(),
|
||||
east: bounds.getEast(),
|
||||
west: bounds.getWest()
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize markers
|
||||
@this.getMarkers().then(markerData => {
|
||||
@this.emit('updateMarkers', markerData);
|
||||
});
|
||||
|
||||
// Helper function to get marker icon based on category
|
||||
function getMarkerIcon(category) {
|
||||
// Customize icons based on category
|
||||
return L.divIcon({
|
||||
className: `marker-icon marker-${category || 'default'}`,
|
||||
html: `<div class="w-8 h-8 rounded-full bg-blue-500 border-2 border-white shadow-lg flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>`,
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 32]
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
207
resources/views/livewire/location/location-map.blade.php
Normal file
207
resources/views/livewire/location/location-map.blade.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<div
|
||||
wire:ignore
|
||||
x-data="mapComponent(@js($mapConfig))"
|
||||
x-init="initializeMap()"
|
||||
class="relative w-full h-[400px] rounded-lg overflow-hidden"
|
||||
>
|
||||
{{-- Map Container --}}
|
||||
<div id="map" class="absolute inset-0 w-full h-full"></div>
|
||||
|
||||
{{-- Loading Overlay --}}
|
||||
<div
|
||||
x-show="loading"
|
||||
class="absolute inset-0 bg-white/60 flex items-center justify-center"
|
||||
>
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
|
||||
{{-- Map Controls --}}
|
||||
@if($showControls)
|
||||
<div class="absolute top-2 right-2 flex flex-col gap-2 z-[1000]">
|
||||
{{-- Zoom Controls --}}
|
||||
<div class="flex flex-col bg-white rounded-lg shadow-lg">
|
||||
<button
|
||||
@click="zoomIn"
|
||||
class="p-2 hover:bg-gray-100 rounded-t-lg border-b"
|
||||
title="Zoom in"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v12m6-6H6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click="zoomOut"
|
||||
class="p-2 hover:bg-gray-100 rounded-b-lg"
|
||||
title="Zoom out"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Center Control --}}
|
||||
<button
|
||||
@click="centerOnMarkers"
|
||||
class="p-2 bg-white rounded-lg shadow-lg hover:bg-gray-100"
|
||||
title="Center map on markers"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Selected Location Info --}}
|
||||
@if($selectedLocation)
|
||||
<div
|
||||
x-show="selectedLocation"
|
||||
class="absolute bottom-4 left-4 right-4 bg-white rounded-lg shadow-lg p-4 z-[1000] max-w-sm"
|
||||
>
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900" x-text="selectedLocation.name || 'Selected Location'"></h3>
|
||||
<p class="text-sm text-gray-600 mt-1" x-text="selectedLocation.address || formatCoordinates(selectedLocation)"></p>
|
||||
</div>
|
||||
<button
|
||||
@click="clearSelection"
|
||||
class="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('mapComponent', (config) => ({
|
||||
map: null,
|
||||
markers: [],
|
||||
markerLayer: null,
|
||||
loading: true,
|
||||
selectedLocation: config.selectedLocation,
|
||||
|
||||
async initializeMap() {
|
||||
// Wait for Leaflet to be loaded
|
||||
if (typeof L === 'undefined') {
|
||||
await this.loadLeaflet();
|
||||
}
|
||||
|
||||
// Initialize the map
|
||||
this.map = L.map('map', {
|
||||
center: [config.center.lat || 0, config.center.lng || 0],
|
||||
zoom: config.zoom || 13,
|
||||
zoomControl: false,
|
||||
dragging: config.interactive,
|
||||
touchZoom: config.interactive,
|
||||
doubleClickZoom: config.interactive,
|
||||
scrollWheelZoom: config.interactive,
|
||||
boxZoom: config.interactive,
|
||||
tap: config.interactive
|
||||
});
|
||||
|
||||
// Add tile layer
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
maxZoom: 18
|
||||
}).addTo(this.map);
|
||||
|
||||
// Initialize marker layer
|
||||
this.markerLayer = L.layerGroup().addTo(this.map);
|
||||
|
||||
// Add markers
|
||||
this.updateMarkers(config.markers);
|
||||
|
||||
// Set up event listeners
|
||||
this.map.on('moveend', () => this.handleMapMoved());
|
||||
this.map.on('zoomend', () => this.handleZoomChanged());
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
async loadLeaflet() {
|
||||
// Load Leaflet CSS
|
||||
if (!document.querySelector('link[href*="leaflet.css"]')) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
// Load Leaflet JS
|
||||
if (typeof L === 'undefined') {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
|
||||
document.head.appendChild(script);
|
||||
|
||||
await new Promise(resolve => script.onload = resolve);
|
||||
}
|
||||
},
|
||||
|
||||
updateMarkers(markers) {
|
||||
this.markerLayer.clearLayers();
|
||||
this.markers = markers.map(marker => {
|
||||
const leafletMarker = L.marker([marker.latitude, marker.longitude], {
|
||||
title: marker.name || 'Location'
|
||||
});
|
||||
|
||||
leafletMarker.on('click', () => {
|
||||
this.$wire.handleMarkerClicked(marker);
|
||||
});
|
||||
|
||||
this.markerLayer.addLayer(leafletMarker);
|
||||
return leafletMarker;
|
||||
});
|
||||
|
||||
if (markers.length > 0) {
|
||||
this.centerOnMarkers();
|
||||
}
|
||||
},
|
||||
|
||||
handleMapMoved() {
|
||||
const center = this.map.getCenter();
|
||||
this.$wire.handleMapMoved(center.lat, center.lng);
|
||||
},
|
||||
|
||||
handleZoomChanged() {
|
||||
this.$wire.handleZoomChanged(this.map.getZoom());
|
||||
},
|
||||
|
||||
zoomIn() {
|
||||
this.map.zoomIn();
|
||||
},
|
||||
|
||||
zoomOut() {
|
||||
this.map.zoomOut();
|
||||
},
|
||||
|
||||
centerOnMarkers() {
|
||||
if (this.markers.length === 0) return;
|
||||
|
||||
if (this.markers.length === 1) {
|
||||
this.map.setView(this.markers[0].getLatLng(), this.map.getZoom());
|
||||
} else {
|
||||
const group = L.featureGroup(this.markers);
|
||||
this.map.fitBounds(group.getBounds().pad(0.1));
|
||||
}
|
||||
},
|
||||
|
||||
clearSelection() {
|
||||
this.selectedLocation = null;
|
||||
this.$wire.handleLocationSelected(null);
|
||||
},
|
||||
|
||||
formatCoordinates(location) {
|
||||
if (!location?.latitude || !location?.longitude) return '';
|
||||
return `${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}`;
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
206
resources/views/livewire/location/location-selector.blade.php
Normal file
206
resources/views/livewire/location/location-selector.blade.php
Normal file
@@ -0,0 +1,206 @@
|
||||
<div>
|
||||
<div class="space-y-4">
|
||||
<!-- Mode Switcher -->
|
||||
<div class="flex space-x-2 mb-4">
|
||||
<button
|
||||
wire:click="switchMode('search')"
|
||||
class="px-4 py-2 text-sm rounded-lg {{ $mode === 'search' ? 'bg-blue-600 text-white' : 'bg-gray-100 hover:bg-gray-200' }}"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
Search
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
wire:click="switchMode('coordinates')"
|
||||
class="px-4 py-2 text-sm rounded-lg {{ $mode === 'coordinates' ? 'bg-blue-600 text-white' : 'bg-gray-100 hover:bg-gray-200' }}"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
Coordinates
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
wire:click="detectCurrentLocation"
|
||||
class="px-4 py-2 text-sm rounded-lg {{ $mode === 'current' ? 'bg-blue-600 text-white' : 'bg-gray-100 hover:bg-gray-200' }}"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Current Location
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search Mode -->
|
||||
@if ($mode === 'search')
|
||||
<div class="relative">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
wire:model.live="searchQuery"
|
||||
placeholder="Search for a location..."
|
||||
class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
@if ($isSearching)
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<svg class="w-5 h-5 text-gray-400 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($showSearchResults && count($searchResults))
|
||||
<div class="absolute z-10 w-full mt-1 bg-white rounded-lg shadow-lg">
|
||||
<ul class="py-1">
|
||||
@foreach ($searchResults as $result)
|
||||
<li>
|
||||
<button
|
||||
wire:click="selectLocation({{ json_encode($result) }})"
|
||||
class="w-full px-4 py-2 text-left hover:bg-gray-100"
|
||||
>
|
||||
{{ $result['display_name'] }}
|
||||
</button>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Coordinate Mode -->
|
||||
@if ($mode === 'coordinates')
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Latitude</label>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
wire:model="latitude"
|
||||
class="w-full px-4 py-2 mt-1 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Enter latitude..."
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Longitude</label>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
wire:model="longitude"
|
||||
class="w-full px-4 py-2 mt-1 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Enter longitude..."
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Current Location Mode -->
|
||||
@if ($mode === 'current')
|
||||
<div class="p-4 text-center">
|
||||
@if ($isLoadingLocation)
|
||||
<div class="flex items-center justify-center space-x-2">
|
||||
<svg class="w-5 h-5 text-blue-600 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>Detecting your location...</span>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-600">Click the Current Location button above to detect your location</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Map Component -->
|
||||
@if ($latitude && $longitude)
|
||||
<div class="mt-4">
|
||||
<livewire:location.location-map-component
|
||||
:latitude="$latitude"
|
||||
:longitude="$longitude"
|
||||
:interactive="true"
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Error Messages -->
|
||||
@if ($validationError)
|
||||
<div class="p-4 mt-4 text-sm text-red-700 bg-red-100 rounded-lg">
|
||||
{{ $validationError }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Selected Location Display -->
|
||||
@if ($isValidLocation)
|
||||
<div class="p-4 mt-4 bg-green-50 rounded-lg">
|
||||
<h4 class="font-medium text-green-800">Selected Location</h4>
|
||||
@if ($formattedAddress)
|
||||
<p class="mt-1 text-sm text-green-600">{{ $formattedAddress }}</p>
|
||||
@endif
|
||||
<p class="mt-1 text-sm text-green-600">
|
||||
Coordinates: {{ number_format($latitude, 6) }}, {{ number_format($longitude, 6) }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Clear Selection Button -->
|
||||
@if ($isValidLocation)
|
||||
<div class="flex justify-end mt-4">
|
||||
<button
|
||||
wire:click="clearSelection"
|
||||
class="px-4 py-2 text-sm text-red-600 bg-red-100 rounded-lg hover:bg-red-200"
|
||||
>
|
||||
Clear Selection
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- JavaScript for Geolocation -->
|
||||
<script>
|
||||
document.addEventListener('livewire:initialized', () => {
|
||||
@this.on('requestCurrentLocation', () => {
|
||||
if (!navigator.geolocation) {
|
||||
@this.dispatch('validationFailed', {
|
||||
message: 'Geolocation is not supported by your browser'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
@this.setCoordinates(
|
||||
position.coords.latitude,
|
||||
position.coords.longitude
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
let errorMessage = 'Failed to get your location';
|
||||
switch (error.code) {
|
||||
case error.PERMISSION_DENIED:
|
||||
errorMessage = 'Location permission denied';
|
||||
break;
|
||||
case error.POSITION_UNAVAILABLE:
|
||||
errorMessage = 'Location information unavailable';
|
||||
break;
|
||||
case error.TIMEOUT:
|
||||
errorMessage = 'Location request timed out';
|
||||
break;
|
||||
}
|
||||
@this.dispatch('validationFailed', {
|
||||
message: errorMessage
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
Reference in New Issue
Block a user