+16
i18n/en-us/forms.ftl
+16
i18n/en-us/forms.ftl
···
153
153
button-back = Back
154
154
button-add-location = Add location
155
155
156
+
# Map modal
157
+
map-modal-default-title = Location
158
+
button-open-in-openstreetmap = Open in OpenStreetMap
159
+
160
+
# Map picker modal
161
+
map-picker-modal-title = Choose Location on Map
162
+
map-picker-instructions = Click on the map to select a location
163
+
button-use-location = Use This Location
164
+
165
+
# Geolocation for map picker
166
+
button-locate-me = Locate Me
167
+
locating-user = Locating your position...
168
+
location-error = Unable to access your location
169
+
location-permission-denied = Location access denied
170
+
171
+
156
172
+16
-1
i18n/fr-ca/forms.ftl
+16
-1
i18n/fr-ca/forms.ftl
···
154
154
title-manual-location-entry = Saisie manuelle du lieu
155
155
select-country = Sélectionner un pays
156
156
button-back = Retour
157
-
button-add-location = Ajouter un lieu
157
+
button-add-location = Ajouter un lieu
158
+
159
+
# Map modal
160
+
map-modal-default-title = Lieu
161
+
button-open-in-openstreetmap = Ouvrir dans OpenStreetMap
162
+
163
+
# Map picker modal
164
+
map-picker-modal-title = Choisir un lieu sur la carte
165
+
map-picker-instructions = Cliquez sur la carte pour sélectionner un lieu
166
+
button-use-location = Utiliser ce lieu
167
+
168
+
# Geolocation for map picker
169
+
button-locate-me = Me localiser
170
+
locating-user = Localisation en cours...
171
+
location-error = Impossible d'accéder à votre position
172
+
location-permission-denied = Accès à la localisation refusé
+439
-3
static/location-map-viewer.js
+439
-3
static/location-map-viewer.js
···
413
413
}
414
414
415
415
openFullMap(lat, lng, venueName) {
416
-
// Open in external map application
417
-
const url = `https://maps.google.com/?q=${lat},${lng}`;
418
-
window.open(url, '_blank', 'noopener');
416
+
// Get translations from body data attributes
417
+
const defaultTitle = document.body.dataset.i18nMapDefaultTitle || 'Location';
418
+
const closeText = document.body.dataset.i18nClose || 'Close';
419
+
const openMapText = document.body.dataset.i18nOpenOpenstreetmap || 'Open in OpenStreetMap';
420
+
421
+
// Create modal for full map view
422
+
const modal = document.createElement('div');
423
+
modal.className = 'modal is-active';
424
+
modal.innerHTML = `
425
+
<div class="modal-background" onclick="this.parentElement.remove()"></div>
426
+
<div class="modal-content">
427
+
<div class="box">
428
+
<h3 class="title is-4">${venueName || defaultTitle}</h3>
429
+
<div id="full-map-${Date.now()}" style="height: 400px; width: 100%;"></div>
430
+
<div class="mt-4">
431
+
<button class="button" onclick="this.closest('.modal').remove()">${closeText}</button>
432
+
<a href="https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}&zoom=16"
433
+
target="_blank" class="button is-info">
434
+
<span class="icon"><i class="fas fa-external-link-alt"></i></span>
435
+
<span>${openMapText}</span>
436
+
</a>
437
+
</div>
438
+
</div>
439
+
</div>
440
+
<button class="modal-close is-large" onclick="this.parentElement.remove()"></button>
441
+
`;
442
+
443
+
document.body.appendChild(modal);
444
+
445
+
// Initialize full map
446
+
const mapId = modal.querySelector('[id^="full-map-"]').id;
447
+
setTimeout(() => {
448
+
if (typeof maplibregl !== 'undefined') {
449
+
try {
450
+
const fullMap = new maplibregl.Map({
451
+
container: mapId,
452
+
style: {
453
+
version: 8,
454
+
sources: {
455
+
'osm': {
456
+
type: 'raster',
457
+
tiles: [
458
+
'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png',
459
+
'https://b.tile.openstreetmap.org/{z}/{x}/{y}.png',
460
+
'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png'
461
+
],
462
+
tileSize: 256,
463
+
attribution: '© OpenStreetMap contributors'
464
+
}
465
+
},
466
+
layers: [
467
+
{
468
+
id: 'osm',
469
+
type: 'raster',
470
+
source: 'osm'
471
+
}
472
+
]
473
+
},
474
+
center: [lng, lat],
475
+
zoom: 16,
476
+
attributionControl: true
477
+
});
478
+
479
+
new maplibregl.Marker({
480
+
color: '#7877c6'
481
+
})
482
+
.setLngLat([lng, lat])
483
+
.addTo(fullMap);
484
+
485
+
// Apply midnight glass theme
486
+
fullMap.on('load', () => {
487
+
this.applyMidnightGlassTheme(fullMap);
488
+
});
489
+
} catch (error) {
490
+
console.error('Error creating full map:', error);
491
+
}
492
+
}
493
+
}, 100);
419
494
}
420
495
421
496
hideLoading() {
···
528
603
}
529
604
});
530
605
}
606
+
}
607
+
608
+
// Export for global access
609
+
window.MiniMapViewer = MiniMapViewer;
610
+
611
+
// Global map picker function for location forms
612
+
window.openMapPicker = function() {
613
+
// Get translations from body data attributes
614
+
const pickerTitle = document.body.dataset.i18nMapPickerTitle || 'Choose Location on Map';
615
+
const pickerInstructions = document.body.dataset.i18nMapPickerInstructions || 'Click on the map to select a location';
616
+
const useLocationText = document.body.dataset.i18nUseLocation || 'Use This Location';
617
+
const closeText = document.body.dataset.i18nClose || 'Close';
618
+
const locateMeText = document.body.dataset.i18nLocateMe || 'Locate Me';
619
+
const locatingUserText = document.body.dataset.i18nLocatingUser || 'Locating your position...';
620
+
const locationErrorText = document.body.dataset.i18nLocationError || 'Unable to access your location';
621
+
const locationPermissionDeniedText = document.body.dataset.i18nLocationPermissionDenied || 'Location access denied';
622
+
623
+
// Create modal for map picker
624
+
const modal = document.createElement('div');
625
+
modal.className = 'modal is-active';
626
+
modal.id = 'map-picker-modal';
627
+
628
+
const mapId = 'map-picker-' + Date.now();
629
+
modal.innerHTML = `
630
+
<div class="modal-background" onclick="closeMapPicker()"></div>
631
+
<div class="modal-content">
632
+
<div class="box">
633
+
<h3 class="title is-4">${pickerTitle}</h3>
634
+
<p class="subtitle is-6">${pickerInstructions}</p>
635
+
<div class="mb-3">
636
+
<button class="button is-info is-small" id="locate-me-btn" onclick="locateUser()">
637
+
<span class="icon">
638
+
<i class="fas fa-location-arrow"></i>
639
+
</span>
640
+
<span>${locateMeText}</span>
641
+
</button>
642
+
<div id="location-status" class="help" style="display: none;"></div>
643
+
</div>
644
+
<div id="${mapId}" style="height: 400px; width: 100%; cursor: crosshair;"></div>
645
+
<div class="mt-4 is-flex is-justify-content-space-between">
646
+
<button class="button" onclick="closeMapPicker()">${closeText}</button>
647
+
<button class="button is-primary" id="use-location-btn" onclick="useSelectedLocation()" disabled>
648
+
${useLocationText}
649
+
</button>
650
+
</div>
651
+
</div>
652
+
</div>
653
+
<button class="modal-close is-large" onclick="closeMapPicker()"></button>
654
+
`;
655
+
656
+
document.body.appendChild(modal);
657
+
658
+
let selectedLocation = null;
659
+
let marker = null;
660
+
let map = null;
661
+
662
+
// Initialize map
663
+
setTimeout(() => {
664
+
if (typeof maplibregl !== 'undefined') {
665
+
try {
666
+
map = new maplibregl.Map({
667
+
container: mapId,
668
+
style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
669
+
center: [-73.5673, 45.5017], // Default to Montreal
670
+
zoom: 10,
671
+
attributionControl: true
672
+
});
673
+
674
+
// Apply midnight glass theme
675
+
map.on('load', () => {
676
+
if (window.MiniMapViewer && typeof window.MiniMapViewer.prototype.applyMidnightGlassTheme === 'function') {
677
+
window.MiniMapViewer.prototype.applyMidnightGlassTheme.call({ map }, map);
678
+
}
679
+
});
680
+
681
+
// Add click handler for location selection
682
+
map.on('click', async (e) => {
683
+
await selectLocation(e.lngLat.lng, e.lngLat.lat);
684
+
});
685
+
686
+
// Store map reference for cleanup
687
+
window.mapPickerInstance = {
688
+
map,
689
+
marker,
690
+
selectedLocation,
691
+
selectLocation,
692
+
translations: {
693
+
locatingUserText,
694
+
locationErrorText,
695
+
locationPermissionDeniedText
696
+
}
697
+
};
698
+
699
+
} catch (error) {
700
+
console.error('Error creating map picker:', error);
701
+
document.getElementById(mapId).innerHTML = `
702
+
<div class="notification is-warning">
703
+
<p>Map could not be loaded. Please enter the location manually.</p>
704
+
</div>
705
+
`;
706
+
}
707
+
}
708
+
}, 100);
709
+
710
+
// Function to select a location (used by both click and geolocation)
711
+
async function selectLocation(lng, lat) {
712
+
// Remove existing marker
713
+
if (marker) {
714
+
marker.remove();
715
+
}
716
+
717
+
// Add new marker
718
+
marker = new maplibregl.Marker({
719
+
color: '#7877c6'
720
+
})
721
+
.setLngLat([lng, lat])
722
+
.addTo(map);
723
+
724
+
// Store selected location
725
+
selectedLocation = { lat, lng };
726
+
727
+
// Enable use location button
728
+
document.getElementById('use-location-btn').disabled = false;
729
+
730
+
// Try to reverse geocode for address
731
+
try {
732
+
const response = await fetch(`http://localhost:8080/reverse?format=json&lat=${lat}&lon=${lng}&zoom=16&addressdetails=1`);
733
+
const data = await response.json();
734
+
selectedLocation.address = data.display_name || '';
735
+
selectedLocation.components = data.address || {};
736
+
} catch (error) {
737
+
console.warn('Reverse geocoding failed:', error);
738
+
selectedLocation.address = `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
739
+
selectedLocation.components = {};
740
+
}
741
+
742
+
// Update instance
743
+
if (window.mapPickerInstance) {
744
+
window.mapPickerInstance.selectedLocation = selectedLocation;
745
+
window.mapPickerInstance.marker = marker;
746
+
}
747
+
}
748
+
};
749
+
750
+
// Function to locate the user when requested
751
+
window.locateUser = function() {
752
+
const statusDiv = document.getElementById('location-status');
753
+
const locateBtn = document.getElementById('locate-me-btn');
754
+
755
+
if (!window.mapPickerInstance || !window.mapPickerInstance.map) {
756
+
return;
757
+
}
758
+
759
+
const { map, translations } = window.mapPickerInstance;
760
+
761
+
// Show loading state
762
+
locateBtn.disabled = true;
763
+
locateBtn.innerHTML = `
764
+
<span class="icon">
765
+
<i class="fas fa-spinner fa-spin"></i>
766
+
</span>
767
+
<span>${translations.locatingUserText}</span>
768
+
`;
769
+
statusDiv.style.display = 'block';
770
+
statusDiv.textContent = translations.locatingUserText;
771
+
statusDiv.className = 'help has-text-info';
772
+
773
+
// Check if geolocation is supported
774
+
if (!navigator.geolocation) {
775
+
showLocationError(translations.locationErrorText);
776
+
return;
777
+
}
778
+
779
+
// Get user's location
780
+
navigator.geolocation.getCurrentPosition(
781
+
// Success callback
782
+
async (position) => {
783
+
const { latitude, longitude } = position.coords;
784
+
785
+
// Center map on user's location
786
+
map.flyTo({
787
+
center: [longitude, latitude],
788
+
zoom: 15,
789
+
duration: 2000
790
+
});
791
+
792
+
// Select this location
793
+
await window.mapPickerInstance.selectLocation(longitude, latitude);
794
+
795
+
// Reset button state
796
+
resetLocateButton();
797
+
statusDiv.style.display = 'none';
798
+
},
799
+
// Error callback
800
+
(error) => {
801
+
let errorMessage;
802
+
switch (error.code) {
803
+
case error.PERMISSION_DENIED:
804
+
errorMessage = translations.locationPermissionDeniedText;
805
+
break;
806
+
case error.POSITION_UNAVAILABLE:
807
+
case error.TIMEOUT:
808
+
default:
809
+
errorMessage = translations.locationErrorText;
810
+
break;
811
+
}
812
+
showLocationError(errorMessage);
813
+
},
814
+
// Options
815
+
{
816
+
enableHighAccuracy: true,
817
+
timeout: 10000,
818
+
maximumAge: 300000 // 5 minutes
819
+
}
820
+
);
821
+
822
+
function showLocationError(message) {
823
+
statusDiv.style.display = 'block';
824
+
statusDiv.textContent = message;
825
+
statusDiv.className = 'help has-text-danger';
826
+
resetLocateButton();
827
+
}
828
+
829
+
function resetLocateButton() {
830
+
const locateMeText = document.body.dataset.i18nLocateMe || 'Locate Me';
831
+
locateBtn.disabled = false;
832
+
locateBtn.innerHTML = `
833
+
<span class="icon">
834
+
<i class="fas fa-location-arrow"></i>
835
+
</span>
836
+
<span>${locateMeText}</span>
837
+
`;
838
+
}
839
+
};
840
+
841
+
window.closeMapPicker = function() {
842
+
const modal = document.getElementById('map-picker-modal');
843
+
if (modal) {
844
+
// Clean up map instance
845
+
if (window.mapPickerInstance && window.mapPickerInstance.map) {
846
+
window.mapPickerInstance.map.remove();
847
+
}
848
+
if (window.mapPickerInstance && window.mapPickerInstance.marker) {
849
+
window.mapPickerInstance.marker.remove();
850
+
}
851
+
window.mapPickerInstance = null;
852
+
853
+
modal.remove();
854
+
}
855
+
};
856
+
857
+
window.useSelectedLocation = function() {
858
+
if (window.mapPickerInstance && window.mapPickerInstance.selectedLocation) {
859
+
const location = window.mapPickerInstance.selectedLocation;
860
+
861
+
// Create form data for HTMX request
862
+
const formData = new FormData();
863
+
formData.append('build_state', 'Selected');
864
+
865
+
// Add location data
866
+
formData.append('latitude', location.lat.toString());
867
+
formData.append('longitude', location.lng.toString());
868
+
869
+
// Parse address components from reverse geocoding
870
+
if (location.address) {
871
+
formData.append('location_name', location.address.split(',')[0] || '');
872
+
}
873
+
874
+
if (location.components) {
875
+
// Map Nominatim address components to form fields
876
+
if (location.components.house_number && location.components.road) {
877
+
formData.append('location_street', `${location.components.house_number} ${location.components.road}`);
878
+
} else if (location.components.road) {
879
+
formData.append('location_street', location.components.road);
880
+
}
881
+
882
+
if (location.components.city || location.components.town || location.components.village) {
883
+
formData.append('location_locality', location.components.city || location.components.town || location.components.village);
884
+
}
885
+
886
+
if (location.components.state || location.components.province) {
887
+
formData.append('location_region', location.components.state || location.components.province);
888
+
}
889
+
890
+
if (location.components.postcode) {
891
+
formData.append('location_postal_code', location.components.postcode);
892
+
}
893
+
894
+
if (location.components.country) {
895
+
formData.append('location_country', location.components.country);
896
+
}
897
+
}
898
+
899
+
// Trigger HTMX request to update the form
900
+
if (typeof htmx !== 'undefined') {
901
+
htmx.ajax('POST', '/event/location', {
902
+
values: Object.fromEntries(formData),
903
+
target: '#locationGroup',
904
+
swap: 'outerHTML'
905
+
});
906
+
} else {
907
+
// Fallback: try to fill form fields directly
908
+
fillFormFields(location);
909
+
}
910
+
911
+
// Close modal
912
+
closeMapPicker();
913
+
}
914
+
};
915
+
916
+
// Fallback function to fill form fields directly
917
+
function fillFormFields(location) {
918
+
const latField = document.querySelector('input[name="latitude"]');
919
+
const lngField = document.querySelector('input[name="longitude"]');
920
+
const nameField = document.querySelector('input[name="location_name"]');
921
+
const streetField = document.querySelector('input[name="location_street"]');
922
+
const localityField = document.querySelector('input[name="location_locality"]');
923
+
const regionField = document.querySelector('input[name="location_region"]');
924
+
const postalField = document.querySelector('input[name="location_postal_code"]');
925
+
const countryField = document.querySelector('select[name="location_country"]');
926
+
927
+
if (latField) latField.value = location.lat.toString();
928
+
if (lngField) lngField.value = location.lng.toString();
929
+
930
+
if (location.address && nameField) {
931
+
nameField.value = location.address.split(',')[0] || '';
932
+
}
933
+
934
+
if (location.components) {
935
+
if (streetField && location.components.road) {
936
+
if (location.components.house_number) {
937
+
streetField.value = `${location.components.house_number} ${location.components.road}`;
938
+
} else {
939
+
streetField.value = location.components.road;
940
+
}
941
+
}
942
+
943
+
if (localityField && (location.components.city || location.components.town || location.components.village)) {
944
+
localityField.value = location.components.city || location.components.town || location.components.village;
945
+
}
946
+
947
+
if (regionField && (location.components.state || location.components.province)) {
948
+
regionField.value = location.components.state || location.components.province;
949
+
}
950
+
951
+
if (postalField && location.components.postcode) {
952
+
postalField.value = location.components.postcode;
953
+
}
954
+
955
+
if (countryField && location.components.country) {
956
+
countryField.value = location.components.country;
957
+
}
958
+
}
959
+
960
+
// Trigger events to notify form of changes
961
+
[latField, lngField, nameField, streetField, localityField, regionField, postalField, countryField].forEach(field => {
962
+
if (field) {
963
+
field.dispatchEvent(new Event('input', { bubbles: true }));
964
+
field.dispatchEvent(new Event('change', { bubbles: true }));
965
+
}
966
+
});
531
967
}
532
968
533
969
// Initialize maps when DOM is ready
-504
static/map-integration.js
-504
static/map-integration.js
···
1
-
/**
2
-
* Map Integration Component
3
-
* Handles MapLibreGL integration for venue location picking and display
4
-
*/
5
-
class MapIntegration {
6
-
constructor() {
7
-
this.map = null;
8
-
this.marker = null;
9
-
this.selectedLocation = null;
10
-
11
-
this.init();
12
-
}
13
-
14
-
init() {
15
-
// Try to initialize maps when DOM is ready
16
-
if (document.readyState === 'loading') {
17
-
document.addEventListener('DOMContentLoaded', () => {
18
-
this.initializeMiniMaps();
19
-
this.setupHTMXListener();
20
-
});
21
-
} else {
22
-
this.initializeMiniMaps();
23
-
this.setupHTMXListener();
24
-
}
25
-
}
26
-
27
-
setupHTMXListener() {
28
-
// Re-initialize after HTMX swaps
29
-
if (document.body) {
30
-
document.body.addEventListener('htmx:afterSwap', (e) => {
31
-
this.initializeMiniMaps(e.target);
32
-
});
33
-
}
34
-
}
35
-
36
-
initializeMiniMaps(container = document) {
37
-
// Initialize mini maps for event display
38
-
const miniMaps = container.querySelectorAll('.venue-map-preview[data-lat][data-lng]');
39
-
miniMaps.forEach(mapContainer => {
40
-
const lat = parseFloat(mapContainer.dataset.lat);
41
-
const lng = parseFloat(mapContainer.dataset.lng);
42
-
const venueName = mapContainer.dataset.venueName || 'Event Location';
43
-
44
-
if (lat && lng && !mapContainer.querySelector('.maplibregl-map')) {
45
-
this.createMiniMap(mapContainer, lat, lng, venueName);
46
-
}
47
-
});
48
-
49
-
// Initialize mini maps for event listings
50
-
const eventMiniMaps = container.querySelectorAll('.event-mini-map[data-lat][data-lng]');
51
-
eventMiniMaps.forEach(mapContainer => {
52
-
const lat = parseFloat(mapContainer.dataset.lat);
53
-
const lng = parseFloat(mapContainer.dataset.lng);
54
-
const venueName = mapContainer.dataset.venueName || 'Event Location';
55
-
56
-
if (lat && lng && !mapContainer.querySelector('.maplibregl-map')) {
57
-
this.createMiniMap(mapContainer, lat, lng, venueName);
58
-
}
59
-
});
60
-
}
61
-
62
-
createMiniMap(container, lat, lng, title) {
63
-
// Use MapLibreGL for mini maps
64
-
if (typeof maplibregl === 'undefined') {
65
-
// Fallback if MapLibreGL is not loaded
66
-
container.innerHTML = `
67
-
<div class="map-fallback">
68
-
<p class="has-text-centered">
69
-
<span class="icon"><i class="fas fa-map-marker-alt"></i></span><br>
70
-
<strong>${title}</strong><br>
71
-
<small>${lat.toFixed(4)}, ${lng.toFixed(4)}</small>
72
-
</p>
73
-
</div>
74
-
`;
75
-
return;
76
-
}
77
-
78
-
try {
79
-
// Set container dimensions
80
-
container.style.height = '200px';
81
-
container.style.width = '100%';
82
-
83
-
// Create MapLibreGL map with midnight glass theme
84
-
const map = new maplibregl.Map({
85
-
container: container,
86
-
style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
87
-
center: [lng, lat],
88
-
zoom: 14,
89
-
interactive: false, // Disable interaction for mini maps
90
-
attributionControl: false
91
-
});
92
-
93
-
// Add marker with midnight glass theme
94
-
new maplibregl.Marker({
95
-
color: '#7877c6'
96
-
})
97
-
.setLngLat([lng, lat])
98
-
.setPopup(new maplibregl.Popup().setText(title))
99
-
.addTo(map);
100
-
101
-
// Apply midnight glass theme
102
-
map.on('load', () => {
103
-
this.applyMidnightGlassTheme(map);
104
-
});
105
-
106
-
// Add click handler to open full map
107
-
container.style.cursor = 'pointer';
108
-
container.addEventListener('click', () => {
109
-
this.showFullMap(lat, lng, title);
110
-
});
111
-
112
-
} catch (error) {
113
-
console.error('Error creating mini map:', error);
114
-
// Fallback to text display
115
-
container.innerHTML = `
116
-
<div class="map-fallback">
117
-
<p class="has-text-centered">
118
-
<span class="icon"><i class="fas fa-map-marker-alt"></i></span><br>
119
-
<strong>${title}</strong><br>
120
-
<small>${lat.toFixed(4)}, ${lng.toFixed(4)}</small>
121
-
</p>
122
-
</div>
123
-
`;
124
-
}
125
-
}
126
-
127
-
showFullMap(lat, lng, title) {
128
-
// Create modal for full map view
129
-
const modal = document.createElement('div');
130
-
modal.className = 'modal is-active';
131
-
modal.innerHTML = `
132
-
<div class="modal-background" onclick="this.parentElement.remove()"></div>
133
-
<div class="modal-content">
134
-
<div class="box">
135
-
<h3 class="title is-4">${title}</h3>
136
-
<div id="full-map-${Date.now()}" style="height: 400px; width: 100%;"></div>
137
-
<div class="mt-4">
138
-
<button class="button" onclick="this.closest('.modal').remove()">Close</button>
139
-
<a href="https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}&zoom=16"
140
-
target="_blank" class="button is-info">
141
-
<span class="icon"><i class="fas fa-external-link-alt"></i></span>
142
-
<span>Open in OpenStreetMap</span>
143
-
</a>
144
-
<a href="https://maps.google.com/?q=${lat},${lng}"
145
-
target="_blank" class="button is-link">
146
-
<span class="icon"><i class="fas fa-external-link-alt"></i></span>
147
-
<span>Open in Google Maps</span>
148
-
</a>
149
-
</div>
150
-
</div>
151
-
</div>
152
-
<button class="modal-close is-large" onclick="this.parentElement.remove()"></button>
153
-
`;
154
-
155
-
document.body.appendChild(modal);
156
-
157
-
// Initialize full map
158
-
const mapId = modal.querySelector('[id^="full-map-"]').id;
159
-
setTimeout(() => {
160
-
if (typeof maplibregl !== 'undefined') {
161
-
const fullMap = new maplibregl.Map({
162
-
container: mapId,
163
-
style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
164
-
center: [lng, lat],
165
-
zoom: 15
166
-
});
167
-
168
-
new maplibregl.Marker({
169
-
color: '#7877c6'
170
-
})
171
-
.setLngLat([lng, lat])
172
-
.setPopup(new maplibregl.Popup().setText(title))
173
-
.addTo(fullMap);
174
-
175
-
// Add navigation controls (zoom buttons)
176
-
fullMap.addControl(new maplibregl.NavigationControl(), 'top-right');
177
-
178
-
// Apply midnight glass theme
179
-
fullMap.on('load', () => {
180
-
window.mapIntegration.applyMidnightGlassTheme(fullMap);
181
-
});
182
-
} else {
183
-
// Fallback for when MapLibreGL is not available
184
-
document.getElementById(mapId).innerHTML = `
185
-
<div class="map-fallback" style="height: 400px; display: flex; align-items: center; justify-content: center; background: #f5f5f5; border: 1px solid #ddd;">
186
-
<div class="has-text-centered">
187
-
<span class="icon is-large"><i class="fas fa-map-marker-alt fa-2x"></i></span><br>
188
-
<strong>${title}</strong><br>
189
-
<small>${lat.toFixed(4)}, ${lng.toFixed(4)}</small>
190
-
</div>
191
-
</div>
192
-
`;
193
-
}
194
-
}, 100);
195
-
}
196
-
197
-
applyMidnightGlassTheme(map) {
198
-
// Fond et terre
199
-
if(map.getLayer('background')) {
200
-
map.setPaintProperty('background', 'background-color', '#1a1b2a');
201
-
}
202
-
if(map.getLayer('landcover')) {
203
-
map.setPaintProperty('landcover', 'fill-color', '#2e2f49');
204
-
}
205
-
if(map.getLayer('landuse_residential')) {
206
-
map.setPaintProperty('landuse_residential', 'fill-color', '#3a3b5e');
207
-
map.setPaintProperty('landuse_residential', 'fill-opacity', 0.4);
208
-
}
209
-
if(map.getLayer('landuse')) {
210
-
map.setPaintProperty('landuse', 'fill-color', '#3a3b5e');
211
-
map.setPaintProperty('landuse', 'fill-opacity', 0.3);
212
-
}
213
-
214
-
// Eau et ombres
215
-
if(map.getLayer('water')) {
216
-
map.setPaintProperty('water', 'fill-color', [
217
-
'interpolate', ['linear'], ['zoom'],
218
-
0, '#2b2f66',
219
-
10, '#5b5ea6',
220
-
15, '#8779c3'
221
-
]);
222
-
map.setPaintProperty('water', 'fill-opacity', 0.85);
223
-
}
224
-
if(map.getLayer('water_shadow')) {
225
-
map.setPaintProperty('water_shadow', 'fill-color', '#555a9a');
226
-
map.setPaintProperty('water_shadow', 'fill-opacity', 0.3);
227
-
}
228
-
229
-
// Parcs
230
-
['park_national_park', 'park_nature_reserve'].forEach(id => {
231
-
if(map.getLayer(id)) {
232
-
map.setPaintProperty(id, 'fill-color', '#50537a');
233
-
map.setPaintProperty(id, 'fill-opacity', 0.3);
234
-
}
235
-
});
236
-
237
-
// Routes principales et secondaires
238
-
const roadsPrimary = [
239
-
'road_pri_case_noramp', 'road_pri_fill_noramp',
240
-
'road_pri_case_ramp', 'road_pri_fill_ramp'
241
-
];
242
-
roadsPrimary.forEach(id => {
243
-
if(map.getLayer(id)) {
244
-
map.setPaintProperty(id, 'line-color', '#9389b8');
245
-
if(id.includes('fill')) {
246
-
map.setPaintProperty(id, 'line-width', 2);
247
-
}
248
-
}
249
-
});
250
-
251
-
const roadsSecondary = [
252
-
'road_sec_case_noramp', 'road_sec_fill_noramp'
253
-
];
254
-
roadsSecondary.forEach(id => {
255
-
if(map.getLayer(id)) {
256
-
map.setPaintProperty(id, 'line-color', '#6d6ea1');
257
-
if(id.includes('fill')) {
258
-
map.setPaintProperty(id, 'line-width', 1.5);
259
-
}
260
-
}
261
-
});
262
-
263
-
// Bâtiments
264
-
['building', 'building-top'].forEach(id => {
265
-
if(map.getLayer(id)) {
266
-
map.setPaintProperty(id, 'fill-color', '#9a92bc');
267
-
map.setPaintProperty(id, 'fill-opacity', 0.35);
268
-
}
269
-
});
270
-
271
-
// Ponts
272
-
['bridge_pri_case', 'bridge_pri_fill', 'bridge_sec_case', 'bridge_sec_fill'].forEach(id => {
273
-
if(map.getLayer(id)) {
274
-
map.setPaintProperty(id, 'line-color', '#7a75aa');
275
-
}
276
-
});
277
-
}
278
-
}
279
-
280
-
// Map Picker functionality for location selection
281
-
window.openMapPicker = function() {
282
-
const modal = document.createElement('div');
283
-
modal.id = 'map-picker-modal';
284
-
modal.className = 'modal is-active';
285
-
modal.innerHTML = `
286
-
<div class="modal-background" onclick="closeMapPicker()"></div>
287
-
<div class="modal-card">
288
-
<header class="modal-card-head">
289
-
<p class="modal-card-title">Pick Location on Map</p>
290
-
<button class="delete" onclick="closeMapPicker()"></button>
291
-
</header>
292
-
<section class="modal-card-body">
293
-
<div class="notification is-info is-light">
294
-
<span class="icon"><i class="fas fa-mouse-pointer"></i></span>
295
-
<strong>Click anywhere on the map to select a location.</strong>
296
-
<p>The address will be automatically filled in the form.</p>
297
-
</div>
298
-
<div id="location-picker-map" style="height: 400px; width: 100%;"></div>
299
-
<div id="selected-location-info" class="is-hidden">
300
-
<div class="notification is-success is-light mt-3">
301
-
<h6 class="title is-6">Selected Location:</h6>
302
-
<p><strong>Coordinates:</strong> <span id="selected-coords"></span></p>
303
-
<p><strong>Address:</strong> <span id="selected-address">Loading...</span></p>
304
-
</div>
305
-
</div>
306
-
</section>
307
-
<footer class="modal-card-foot">
308
-
<button id="use-location-btn" class="button is-primary" onclick="useSelectedLocation()" disabled>
309
-
Use This Location
310
-
</button>
311
-
<button class="button" onclick="closeMapPicker()">Cancel</button>
312
-
</footer>
313
-
</div>
314
-
`;
315
-
316
-
document.body.appendChild(modal);
317
-
318
-
// Initialize map picker
319
-
setTimeout(() => {
320
-
initializeMapPicker();
321
-
}, 100);
322
-
};
323
-
324
-
window.closeMapPicker = function() {
325
-
const modal = document.getElementById('map-picker-modal');
326
-
if (modal) {
327
-
modal.remove();
328
-
}
329
-
};
330
-
331
-
function initializeMapPicker() {
332
-
if (typeof maplibregl === 'undefined') {
333
-
console.error('MapLibreGL is not loaded');
334
-
// Show fallback message
335
-
document.getElementById('location-picker-map').innerHTML = `
336
-
<div class="map-fallback" style="height: 400px; display: flex; align-items: center; justify-content: center; background: #f5f5f5; border: 1px solid #ddd;">
337
-
<div class="has-text-centered">
338
-
<span class="icon is-large"><i class="fas fa-exclamation-triangle fa-2x"></i></span><br>
339
-
<strong>Map not available</strong><br>
340
-
<small>Please enter coordinates manually</small>
341
-
</div>
342
-
</div>
343
-
`;
344
-
return;
345
-
}
346
-
347
-
// Default to Montreal coordinates
348
-
let defaultLat = 45.5088;
349
-
let defaultLng = -73.5878;
350
-
351
-
// Try to get user's location
352
-
if (navigator.geolocation) {
353
-
navigator.geolocation.getCurrentPosition(
354
-
position => {
355
-
defaultLat = position.coords.latitude;
356
-
defaultLng = position.coords.longitude;
357
-
initMap(defaultLat, defaultLng);
358
-
},
359
-
() => {
360
-
// Fallback to default location
361
-
initMap(defaultLat, defaultLng);
362
-
}
363
-
);
364
-
} else {
365
-
initMap(defaultLat, defaultLng);
366
-
}
367
-
368
-
function initMap(lat, lng) {
369
-
const map = new maplibregl.Map({
370
-
container: 'location-picker-map',
371
-
style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
372
-
center: [lng, lat],
373
-
zoom: 12
374
-
});
375
-
376
-
let selectedMarker = null;
377
-
window.mapPickerSelectedLocation = null;
378
-
379
-
// Add navigation controls (zoom buttons)
380
-
map.addControl(new maplibregl.NavigationControl(), 'top-right');
381
-
382
-
// Apply midnight glass theme
383
-
map.on('load', () => {
384
-
window.mapIntegration.applyMidnightGlassTheme(map);
385
-
});
386
-
387
-
// Handle map clicks
388
-
map.on('click', function(e) {
389
-
const { lat, lng } = e.lngLat;
390
-
391
-
// Remove existing marker
392
-
if (selectedMarker) {
393
-
selectedMarker.remove();
394
-
}
395
-
396
-
// Add new marker with midnight glass theme
397
-
selectedMarker = new maplibregl.Marker({
398
-
color: '#7877c6'
399
-
})
400
-
.setLngLat([lng, lat])
401
-
.addTo(map);
402
-
403
-
// Store selected location
404
-
window.mapPickerSelectedLocation = { lat, lng };
405
-
406
-
// Update UI
407
-
document.getElementById('selected-coords').textContent = `${lat.toFixed(4)}, ${lng.toFixed(4)}`;
408
-
document.getElementById('selected-location-info').classList.remove('is-hidden');
409
-
document.getElementById('use-location-btn').disabled = false;
410
-
411
-
// Reverse geocode to get address
412
-
reverseGeocode(lat, lng);
413
-
});
414
-
415
-
// Add geolocation control
416
-
map.addControl(new maplibregl.GeolocateControl({
417
-
positionOptions: {
418
-
enableHighAccuracy: true
419
-
},
420
-
trackUserLocation: true,
421
-
showUserHeading: true
422
-
}));
423
-
}
424
-
}
425
-
426
-
function reverseGeocode(lat, lng) {
427
-
// Use Nominatim for reverse geocoding
428
-
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&addressdetails=1`;
429
-
430
-
fetch(url)
431
-
.then(response => response.json())
432
-
.then(data => {
433
-
const address = data.display_name || `${lat.toFixed(4)}, ${lng.toFixed(4)}`;
434
-
document.getElementById('selected-address').textContent = address;
435
-
436
-
// Store address components for form population
437
-
if (data.address) {
438
-
window.mapPickerSelectedLocation.addressComponents = data.address;
439
-
window.mapPickerSelectedLocation.displayName = data.display_name;
440
-
}
441
-
})
442
-
.catch(error => {
443
-
console.error('Reverse geocoding error:', error);
444
-
document.getElementById('selected-address').textContent = 'Address lookup failed';
445
-
});
446
-
}
447
-
448
-
window.useSelectedLocation = function() {
449
-
if (!window.mapPickerSelectedLocation) {
450
-
return;
451
-
}
452
-
453
-
const location = window.mapPickerSelectedLocation;
454
-
const address = location.addressComponents || {};
455
-
456
-
// Populate form fields
457
-
const fields = {
458
-
'latitude': location.lat,
459
-
'longitude': location.lng,
460
-
'location_name': address.building || address.house_number && address.road ? `${address.house_number} ${address.road}` : '',
461
-
'location_street': address.road || '',
462
-
'location_locality': address.city || address.town || address.village || '',
463
-
'location_region': address.state || address.province || '',
464
-
'location_postal_code': address.postcode || '',
465
-
'location_country': address.country_code ? address.country_code.toUpperCase() : ''
466
-
};
467
-
468
-
// Update form fields
469
-
Object.keys(fields).forEach(fieldName => {
470
-
const field = document.querySelector(`input[name="${fieldName}"], select[name="${fieldName}"]`);
471
-
if (field && fields[fieldName]) {
472
-
field.value = fields[fieldName];
473
-
} else if (fields[fieldName]) {
474
-
// Create hidden field
475
-
const hiddenField = document.createElement('input');
476
-
hiddenField.type = 'hidden';
477
-
hiddenField.name = fieldName;
478
-
hiddenField.value = fields[fieldName];
479
-
document.querySelector('#locationGroup').appendChild(hiddenField);
480
-
}
481
-
});
482
-
483
-
// Trigger form update via HTMX
484
-
htmx.ajax('POST', '/event/location', {
485
-
values: {
486
-
build_state: 'Selected',
487
-
...fields
488
-
},
489
-
target: '#locationGroup',
490
-
swap: 'outerHTML'
491
-
});
492
-
493
-
// Close modal
494
-
closeMapPicker();
495
-
};
496
-
497
-
// Initialize map integration when DOM is ready
498
-
if (document.readyState === 'loading') {
499
-
document.addEventListener('DOMContentLoaded', () => {
500
-
window.mapIntegration = new MapIntegration();
501
-
});
502
-
} else {
503
-
window.mapIntegration = new MapIntegration();
504
-
}
+12
-3
templates/base.en-us.html
+12
-3
templates/base.en-us.html
···
20
20
<script src="/static/sse.js"></script>
21
21
<script src="/static/site.js"></script>
22
22
<script src="/static/venue-search.js"></script>
23
-
<script src="/static/map-integration.js"></script>
23
+
<script src="/static/location-map-viewer.js"></script>
24
24
<script src="/static/form-enhancement.js"></script>
25
-
<script src="/static/location-map-viewer.js"></script>
26
25
27
26
<!-- MapLibreGL JS -->
28
27
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
···
78
77
{% endblock %}
79
78
<meta name="theme-color" content="#00d1b2">
80
79
</head>
81
-
<body hx-ext="loading-states">
80
+
<body hx-ext="loading-states"
81
+
data-i18n-map-default-title="{{ t('map-modal-default-title') }}"
82
+
data-i18n-close="{{ t('close') }}"
83
+
data-i18n-open-openstreetmap="{{ t('button-open-in-openstreetmap') }}"
84
+
data-i18n-map-picker-title="{{ t('map-picker-modal-title') }}"
85
+
data-i18n-map-picker-instructions="{{ t('map-picker-instructions') }}"
86
+
data-i18n-use-location="{{ t('button-use-location') }}"
87
+
data-i18n-locate-me="{{ t('button-locate-me') }}"
88
+
data-i18n-locating-user="{{ t('locating-user') }}"
89
+
data-i18n-location-error="{{ t('location-error') }}"
90
+
data-i18n-location-permission-denied="{{ t('location-permission-denied') }}">
82
91
{% include 'nav.' + current_locale + '.html' %}
83
92
{% block content %}{% endblock %}
84
93
{% include 'footer.' + current_locale + '.html' %}
+12
-3
templates/base.fr-ca.html
+12
-3
templates/base.fr-ca.html
···
20
20
<script src="/static/sse.js"></script>
21
21
<script src="/static/site.js"></script>
22
22
<script src="/static/venue-search.js"></script>
23
-
<script src="/static/map-integration.js"></script>
23
+
<script src="/static/location-map-viewer.js"></script>
24
24
<script src="/static/form-enhancement.js"></script>
25
-
<script src="/static/location-map-viewer.js"></script>
26
25
27
26
<!-- MapLibreGL JS -->
28
27
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
···
195
194
}
196
195
</style>
197
196
</head>
198
-
<body hx-ext="loading-states">
197
+
<body hx-ext="loading-states"
198
+
data-i18n-map-default-title="{{ t('map-modal-default-title') }}"
199
+
data-i18n-close="{{ t('close') }}"
200
+
data-i18n-open-openstreetmap="{{ t('button-open-in-openstreetmap') }}"
201
+
data-i18n-map-picker-title="{{ t('map-picker-modal-title') }}"
202
+
data-i18n-map-picker-instructions="{{ t('map-picker-instructions') }}"
203
+
data-i18n-use-location="{{ t('button-use-location') }}"
204
+
data-i18n-locate-me="{{ t('button-locate-me') }}"
205
+
data-i18n-locating-user="{{ t('locating-user') }}"
206
+
data-i18n-location-error="{{ t('location-error') }}"
207
+
data-i18n-location-permission-denied="{{ t('location-permission-denied') }}">
199
208
{% include 'nav.' + current_locale + '.html' %}
200
209
{% block content %}{% endblock %}
201
210
{% include 'footer.' + current_locale + '.html' %}
+1
-1
templates/create_event.en-us.html
+1
-1
templates/create_event.en-us.html
+1
-1
templates/edit_event.en-us.html
+1
-1
templates/edit_event.en-us.html