mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 10:11:09 -05:00
618 lines
22 KiB
HTML
618 lines
22 KiB
HTML
{% extends 'base/base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}{{ page_title }} - ThrillWiki{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<!-- Leaflet CSS -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<!-- Leaflet MarkerCluster CSS -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css" />
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css" />
|
|
|
|
<style>
|
|
.map-container {
|
|
height: 75vh;
|
|
min-height: 600px;
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.park-status-badge {
|
|
@apply inline-flex items-center px-2 py-1 rounded-full text-xs font-medium;
|
|
}
|
|
|
|
.park-status-operating {
|
|
@apply bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100;
|
|
}
|
|
|
|
.park-status-closed {
|
|
@apply bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100;
|
|
}
|
|
|
|
.park-status-construction {
|
|
@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100;
|
|
}
|
|
|
|
.park-status-demolished {
|
|
@apply bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-100;
|
|
}
|
|
|
|
.park-marker {
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
|
|
.park-marker-inner {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-weight: bold;
|
|
font-size: 18px;
|
|
border: 3px solid white;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.park-marker-operating {
|
|
background: #10b981;
|
|
}
|
|
|
|
.park-marker-closed {
|
|
background: #ef4444;
|
|
}
|
|
|
|
.park-marker-construction {
|
|
background: #f59e0b;
|
|
}
|
|
|
|
.park-marker-demolished {
|
|
background: #6b7280;
|
|
}
|
|
|
|
.park-info-popup {
|
|
max-width: 350px;
|
|
}
|
|
|
|
.park-info-popup h3 {
|
|
margin: 0 0 0.5rem 0;
|
|
font-size: 1.2rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.park-info-popup .park-meta {
|
|
margin: 0.25rem 0;
|
|
font-size: 0.9rem;
|
|
color: #666;
|
|
}
|
|
|
|
.dark .park-info-popup .park-meta {
|
|
color: #ccc;
|
|
}
|
|
|
|
.quick-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.quick-stat-card {
|
|
@apply bg-white dark:bg-gray-800 rounded-lg p-4 text-center shadow-sm;
|
|
}
|
|
|
|
.quick-stat-value {
|
|
@apply text-2xl font-bold text-blue-600 dark:text-blue-400;
|
|
}
|
|
|
|
.quick-stat-label {
|
|
@apply text-sm text-gray-600 dark:text-gray-400 mt-1;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container px-4 mx-auto">
|
|
<!-- Header -->
|
|
<div class="flex flex-col items-start justify-between gap-4 mb-6 md:flex-row md:items-center">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ page_title }}</h1>
|
|
<p class="mt-2 text-gray-600 dark:text-gray-400">
|
|
Discover theme parks and amusement parks worldwide
|
|
</p>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<a href="{% url 'maps:universal_map' %}"
|
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 transition-colors">
|
|
<i class="mr-2 fas fa-globe"></i>All Locations
|
|
</a>
|
|
<a href="{% url 'parks:roadtrip_planner' %}"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors">
|
|
<i class="mr-2 fas fa-route"></i>Plan Road Trip
|
|
</a>
|
|
<a href="{% url 'parks:park_list' %}"
|
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 transition-colors">
|
|
<i class="mr-2 fas fa-list"></i>List View
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Stats -->
|
|
<div class="quick-stats mb-6" id="park-stats">
|
|
<div class="quick-stat-card">
|
|
<div class="quick-stat-value" id="total-parks">-</div>
|
|
<div class="quick-stat-label">Total Parks</div>
|
|
</div>
|
|
<div class="quick-stat-card">
|
|
<div class="quick-stat-value" id="operating-parks">-</div>
|
|
<div class="quick-stat-label">Operating</div>
|
|
</div>
|
|
<div class="quick-stat-card">
|
|
<div class="quick-stat-value" id="countries-count">-</div>
|
|
<div class="quick-stat-label">Countries</div>
|
|
</div>
|
|
<div class="quick-stat-card">
|
|
<div class="quick-stat-value" id="total-rides">-</div>
|
|
<div class="quick-stat-label">Total Rides</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters Panel -->
|
|
<div class="p-4 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
|
|
<form id="park-filters"
|
|
hx-get="{% url 'maps:htmx_filter' %}"
|
|
hx-trigger="change, submit"
|
|
hx-target="#map-container"
|
|
hx-swap="none"
|
|
hx-push-url="false">
|
|
|
|
<!-- Hidden input to specify park-only filtering -->
|
|
<input type="hidden" name="types" value="park">
|
|
|
|
<div class="grid grid-cols-1 gap-4 mb-4 md:grid-cols-2 lg:grid-cols-4">
|
|
<!-- Search -->
|
|
<div>
|
|
<label for="search" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Search Parks</label>
|
|
<input type="text" name="q" id="search"
|
|
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
|
placeholder="Search park names..."
|
|
hx-get="{% url 'maps:htmx_search' %}"
|
|
hx-trigger="input changed delay:500ms"
|
|
hx-target="#search-results"
|
|
hx-indicator="#search-loading">
|
|
</div>
|
|
|
|
<!-- Country -->
|
|
<div>
|
|
<label for="country" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">Country</label>
|
|
<input type="text" name="country" id="country"
|
|
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
|
placeholder="Filter by country...">
|
|
</div>
|
|
|
|
<!-- State/Region -->
|
|
<div>
|
|
<label for="state" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">State/Region</label>
|
|
<input type="text" name="state" id="state"
|
|
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
|
placeholder="Filter by state...">
|
|
</div>
|
|
|
|
<!-- Clustering Toggle -->
|
|
<div class="flex items-end">
|
|
<label class="flex items-center cursor-pointer">
|
|
<input type="checkbox" name="cluster" value="true" id="cluster-toggle"
|
|
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
|
checked>
|
|
<span class="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">Group Nearby Parks</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Park Status Filters -->
|
|
<div class="mb-4">
|
|
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">Park Status</label>
|
|
<div class="flex flex-wrap gap-2">
|
|
<label class="park-status-badge park-status-operating cursor-pointer">
|
|
<input type="checkbox" name="park_status" value="OPERATING" class="hidden status-checkbox" checked>
|
|
<i class="mr-1 fas fa-check-circle"></i>Operating
|
|
</label>
|
|
<label class="park-status-badge park-status-closed cursor-pointer">
|
|
<input type="checkbox" name="park_status" value="CLOSED_TEMP" class="hidden status-checkbox">
|
|
<i class="mr-1 fas fa-clock"></i>Temporarily Closed
|
|
</label>
|
|
<label class="park-status-badge park-status-closed cursor-pointer">
|
|
<input type="checkbox" name="park_status" value="CLOSED_PERM" class="hidden status-checkbox">
|
|
<i class="mr-1 fas fa-times-circle"></i>Permanently Closed
|
|
</label>
|
|
<label class="park-status-badge park-status-construction cursor-pointer">
|
|
<input type="checkbox" name="park_status" value="UNDER_CONSTRUCTION" class="hidden status-checkbox">
|
|
<i class="mr-1 fas fa-hard-hat"></i>Under Construction
|
|
</label>
|
|
<label class="park-status-badge park-status-demolished cursor-pointer">
|
|
<input type="checkbox" name="park_status" value="DEMOLISHED" class="hidden status-checkbox">
|
|
<i class="mr-1 fas fa-ban"></i>Demolished
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Search Results -->
|
|
<div id="search-results" class="mt-4"></div>
|
|
<div id="search-loading" class="htmx-indicator">
|
|
<div class="flex items-center justify-center p-4">
|
|
<div class="w-6 h-6 border-2 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
|
|
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Searching parks...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Map Container -->
|
|
<div class="relative">
|
|
<div id="map-container" class="map-container"></div>
|
|
|
|
<!-- Map Loading Indicator -->
|
|
<div id="map-loading" class="htmx-indicator absolute inset-0 flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded-lg">
|
|
<div class="text-center">
|
|
<div class="w-8 h-8 mx-auto mb-4 border-4 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">Loading park data...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Location Details Modal -->
|
|
<div id="location-modal" class="fixed inset-0 z-50 hidden">
|
|
<!-- Modal content will be loaded here via HTMX -->
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<!-- Leaflet JS -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<!-- Leaflet MarkerCluster JS -->
|
|
<script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script>
|
|
|
|
<script>
|
|
// Park-specific map class
|
|
class ParkMap {
|
|
constructor(containerId, options = {}) {
|
|
this.containerId = containerId;
|
|
this.options = {
|
|
center: [39.8283, -98.5795],
|
|
zoom: 4,
|
|
enableClustering: true,
|
|
...options
|
|
};
|
|
this.map = null;
|
|
this.markers = new L.MarkerClusterGroup({
|
|
maxClusterRadius: 50,
|
|
spiderfyOnMaxZoom: true,
|
|
showCoverageOnHover: false
|
|
});
|
|
this.currentData = [];
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
// Initialize the map
|
|
this.map = L.map(this.containerId, {
|
|
center: this.options.center,
|
|
zoom: this.options.zoom,
|
|
zoomControl: false
|
|
});
|
|
|
|
// Add custom zoom control
|
|
L.control.zoom({
|
|
position: 'bottomright'
|
|
}).addTo(this.map);
|
|
|
|
// Add tile layers with dark mode support
|
|
const lightTiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors'
|
|
});
|
|
|
|
const darkTiles = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
|
attribution: '© OpenStreetMap contributors, © CARTO'
|
|
});
|
|
|
|
// Set initial tiles based on theme
|
|
if (document.documentElement.classList.contains('dark')) {
|
|
darkTiles.addTo(this.map);
|
|
} else {
|
|
lightTiles.addTo(this.map);
|
|
}
|
|
|
|
// Listen for theme changes
|
|
this.observeThemeChanges(lightTiles, darkTiles);
|
|
|
|
// Add markers cluster group
|
|
this.map.addLayer(this.markers);
|
|
|
|
// Bind map events
|
|
this.bindEvents();
|
|
|
|
// Load initial data
|
|
this.loadMapData();
|
|
}
|
|
|
|
observeThemeChanges(lightTiles, darkTiles) {
|
|
const observer = new MutationObserver((mutations) => {
|
|
mutations.forEach((mutation) => {
|
|
if (mutation.attributeName === 'class') {
|
|
if (document.documentElement.classList.contains('dark')) {
|
|
this.map.removeLayer(lightTiles);
|
|
this.map.addLayer(darkTiles);
|
|
} else {
|
|
this.map.removeLayer(darkTiles);
|
|
this.map.addLayer(lightTiles);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
observer.observe(document.documentElement, {
|
|
attributes: true,
|
|
attributeFilter: ['class']
|
|
});
|
|
}
|
|
|
|
bindEvents() {
|
|
// Update map when bounds change
|
|
this.map.on('moveend zoomend', () => {
|
|
this.updateMapBounds();
|
|
});
|
|
|
|
// Handle filter form changes
|
|
document.getElementById('park-filters').addEventListener('htmx:afterRequest', (event) => {
|
|
if (event.detail.successful) {
|
|
this.loadMapData();
|
|
}
|
|
});
|
|
}
|
|
|
|
async loadMapData() {
|
|
try {
|
|
document.getElementById('map-loading').style.display = 'flex';
|
|
|
|
const formData = new FormData(document.getElementById('park-filters'));
|
|
const params = new URLSearchParams();
|
|
|
|
// Add form data to params
|
|
for (let [key, value] of formData.entries()) {
|
|
params.append(key, value);
|
|
}
|
|
|
|
// Add map bounds
|
|
const bounds = this.map.getBounds();
|
|
params.append('north', bounds.getNorth());
|
|
params.append('south', bounds.getSouth());
|
|
params.append('east', bounds.getEast());
|
|
params.append('west', bounds.getWest());
|
|
params.append('zoom', this.map.getZoom());
|
|
|
|
const response = await fetch(`{{ map_api_urls.locations }}?${params}`);
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
this.updateMarkers(data.data);
|
|
this.updateStats(data.data);
|
|
} else {
|
|
console.error('Park data error:', data.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load park data:', error);
|
|
} finally {
|
|
document.getElementById('map-loading').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
updateStats(data) {
|
|
// Update quick stats
|
|
const totalParks = (data.locations || []).length + (data.clusters || []).reduce((sum, cluster) => sum + cluster.count, 0);
|
|
const operatingParks = (data.locations || []).filter(park => park.status === 'OPERATING').length;
|
|
const countries = new Set((data.locations || []).map(park => park.country).filter(Boolean)).size;
|
|
const totalRides = (data.locations || []).reduce((sum, park) => sum + (park.ride_count || 0), 0);
|
|
|
|
document.getElementById('total-parks').textContent = totalParks.toLocaleString();
|
|
document.getElementById('operating-parks').textContent = operatingParks.toLocaleString();
|
|
document.getElementById('countries-count').textContent = countries.toLocaleString();
|
|
document.getElementById('total-rides').textContent = totalRides.toLocaleString();
|
|
}
|
|
|
|
updateMarkers(data) {
|
|
// Clear existing markers
|
|
this.markers.clearLayers();
|
|
|
|
// Add park markers
|
|
if (data.locations) {
|
|
data.locations.forEach(park => {
|
|
this.addParkMarker(park);
|
|
});
|
|
}
|
|
|
|
// Add cluster markers
|
|
if (data.clusters) {
|
|
data.clusters.forEach(cluster => {
|
|
this.addClusterMarker(cluster);
|
|
});
|
|
}
|
|
}
|
|
|
|
addParkMarker(park) {
|
|
const icon = this.getParkIcon(park.status);
|
|
const marker = L.marker([park.latitude, park.longitude], { icon });
|
|
|
|
// Create popup content
|
|
const popupContent = this.createParkPopupContent(park);
|
|
marker.bindPopup(popupContent, { maxWidth: 350 });
|
|
|
|
// Add click handler for detailed view
|
|
marker.on('click', () => {
|
|
this.showParkDetails(park.id);
|
|
});
|
|
|
|
this.markers.addLayer(marker);
|
|
}
|
|
|
|
addClusterMarker(cluster) {
|
|
const marker = L.marker([cluster.latitude, cluster.longitude], {
|
|
icon: L.divIcon({
|
|
className: 'cluster-marker',
|
|
html: `<div class="cluster-marker-inner">${cluster.count}</div>`,
|
|
iconSize: [40, 40]
|
|
})
|
|
});
|
|
|
|
marker.bindPopup(`${cluster.count} parks in this area`);
|
|
this.markers.addLayer(marker);
|
|
}
|
|
|
|
getParkIcon(status) {
|
|
const statusClass = {
|
|
'OPERATING': 'park-marker-operating',
|
|
'CLOSED_TEMP': 'park-marker-closed',
|
|
'CLOSED_PERM': 'park-marker-closed',
|
|
'UNDER_CONSTRUCTION': 'park-marker-construction',
|
|
'DEMOLISHED': 'park-marker-demolished'
|
|
}[status] || 'park-marker-operating';
|
|
|
|
return L.divIcon({
|
|
className: 'park-marker',
|
|
html: `<div class="park-marker-inner ${statusClass}">🎢</div>`,
|
|
iconSize: [32, 32],
|
|
iconAnchor: [16, 16]
|
|
});
|
|
}
|
|
|
|
createParkPopupContent(park) {
|
|
const statusClass = {
|
|
'OPERATING': 'park-status-operating',
|
|
'CLOSED_TEMP': 'park-status-closed',
|
|
'CLOSED_PERM': 'park-status-closed',
|
|
'UNDER_CONSTRUCTION': 'park-status-construction',
|
|
'DEMOLISHED': 'park-status-demolished'
|
|
}[park.status] || 'park-status-operating';
|
|
|
|
return `
|
|
<div class="park-info-popup">
|
|
<h3>${park.name}</h3>
|
|
<div class="park-meta">
|
|
<span class="park-status-badge ${statusClass}">
|
|
${this.getStatusDisplayName(park.status)}
|
|
</span>
|
|
</div>
|
|
${park.formatted_location ? `<div class="park-meta"><i class="fas fa-map-marker-alt mr-1"></i>${park.formatted_location}</div>` : ''}
|
|
${park.operator ? `<div class="park-meta"><i class="fas fa-building mr-1"></i>${park.operator}</div>` : ''}
|
|
${park.ride_count ? `<div class="park-meta"><i class="fas fa-rocket mr-1"></i>${park.ride_count} rides</div>` : ''}
|
|
${park.average_rating ? `<div class="park-meta"><i class="fas fa-star mr-1"></i>${park.average_rating}/10 rating</div>` : ''}
|
|
<div class="mt-3 flex gap-2">
|
|
<button onclick="parkMap.showParkDetails(${park.id})"
|
|
class="px-3 py-1 text-sm text-white bg-blue-600 rounded hover:bg-blue-700">
|
|
View Details
|
|
</button>
|
|
<a href="/parks/${park.slug}/"
|
|
class="px-3 py-1 text-sm text-blue-600 border border-blue-600 rounded hover:bg-blue-50">
|
|
Visit Page
|
|
</a>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
getStatusDisplayName(status) {
|
|
const statusMap = {
|
|
'OPERATING': 'Operating',
|
|
'CLOSED_TEMP': 'Temporarily Closed',
|
|
'CLOSED_PERM': 'Permanently Closed',
|
|
'UNDER_CONSTRUCTION': 'Under Construction',
|
|
'DEMOLISHED': 'Demolished'
|
|
};
|
|
return statusMap[status] || 'Unknown';
|
|
}
|
|
|
|
showParkDetails(parkId) {
|
|
htmx.ajax('GET', `{% url 'maps:htmx_location_detail' 'park' 0 %}`.replace('0', parkId), {
|
|
target: '#location-modal',
|
|
swap: 'innerHTML'
|
|
}).then(() => {
|
|
document.getElementById('location-modal').classList.remove('hidden');
|
|
});
|
|
}
|
|
|
|
updateMapBounds() {
|
|
// Reload data when the map moves significantly
|
|
clearTimeout(this.boundsUpdateTimeout);
|
|
this.boundsUpdateTimeout = setTimeout(() => {
|
|
this.loadMapData();
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
// Initialize map when page loads
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
window.parkMap = new ParkMap('map-container', {
|
|
enableClustering: {{ enable_clustering|yesno:"true,false" }}
|
|
});
|
|
|
|
// Handle status filter toggles
|
|
document.querySelectorAll('.park-status-badge').forEach(badge => {
|
|
const checkbox = badge.querySelector('input[type="checkbox"]');
|
|
|
|
// Set initial state
|
|
if (checkbox && checkbox.checked) {
|
|
badge.style.opacity = '1';
|
|
} else if (checkbox) {
|
|
badge.style.opacity = '0.5';
|
|
}
|
|
|
|
badge.addEventListener('click', () => {
|
|
if (checkbox) {
|
|
checkbox.checked = !checkbox.checked;
|
|
badge.style.opacity = checkbox.checked ? '1' : '0.5';
|
|
|
|
// Trigger form change
|
|
document.getElementById('park-filters').dispatchEvent(new Event('change'));
|
|
}
|
|
});
|
|
});
|
|
|
|
// Close modal handler
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.id === 'location-modal') {
|
|
document.getElementById('location-modal').classList.add('hidden');
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.cluster-marker {
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
|
|
.cluster-marker-inner {
|
|
background: #3b82f6;
|
|
color: white;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
border: 3px solid white;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.dark .cluster-marker-inner {
|
|
border-color: #374151;
|
|
}
|
|
</style>
|
|
{% endblock %} |