Add Road Trip Planner template with interactive map and trip management features

- Implemented a new HTML template for the Road Trip Planner.
- Integrated Leaflet.js for interactive mapping and routing.
- Added functionality for searching and selecting parks to include in a trip.
- Enabled drag-and-drop reordering of selected parks.
- Included trip optimization and route calculation features.
- Created a summary display for trip statistics.
- Added functionality to save trips and manage saved trips.
- Enhanced UI with responsive design and dark mode support.
This commit is contained in:
pacnpal
2025-08-15 20:53:00 -04:00
parent da7c7e3381
commit b5bae44cb8
99 changed files with 18697 additions and 4010 deletions

665
static/js/dark-mode-maps.js Normal file
View File

@@ -0,0 +1,665 @@
/**
* ThrillWiki Dark Mode Maps - Dark Mode Integration for Maps
*
* This module provides comprehensive dark mode support for maps,
* including automatic theme switching, dark tile layers, and consistent styling
*/
class DarkModeMaps {
constructor(options = {}) {
this.options = {
enableAutoThemeDetection: true,
enableSystemPreference: true,
enableStoredPreference: true,
storageKey: 'thrillwiki_dark_mode',
transitionDuration: 300,
tileProviders: {
light: {
osm: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
cartodb: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
},
dark: {
osm: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
cartodb: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
}
},
...options
};
this.currentTheme = 'light';
this.mapInstances = new Set();
this.tileLayers = new Map();
this.observer = null;
this.mediaQuery = null;
this.init();
}
/**
* Initialize dark mode support
*/
init() {
this.detectCurrentTheme();
this.setupThemeObserver();
this.setupSystemPreferenceDetection();
this.setupStorageSync();
this.setupMapThemeStyles();
this.bindEventHandlers();
console.log('Dark mode maps initialized with theme:', this.currentTheme);
}
/**
* Detect current theme from DOM
*/
detectCurrentTheme() {
if (document.documentElement.classList.contains('dark')) {
this.currentTheme = 'dark';
} else {
this.currentTheme = 'light';
}
// Check stored preference
if (this.options.enableStoredPreference) {
const stored = localStorage.getItem(this.options.storageKey);
if (stored && ['light', 'dark', 'auto'].includes(stored)) {
this.applyStoredPreference(stored);
}
}
// Check system preference if auto
if (this.options.enableSystemPreference && this.getStoredPreference() === 'auto') {
this.applySystemPreference();
}
}
/**
* Setup theme observer to watch for changes
*/
setupThemeObserver() {
this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
const newTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
if (newTheme !== this.currentTheme) {
this.handleThemeChange(newTheme);
}
}
});
});
this.observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
}
/**
* Setup system preference detection
*/
setupSystemPreferenceDetection() {
if (!this.options.enableSystemPreference) return;
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemChange = (e) => {
if (this.getStoredPreference() === 'auto') {
const newTheme = e.matches ? 'dark' : 'light';
this.setTheme(newTheme);
}
};
// Modern browsers
if (this.mediaQuery.addEventListener) {
this.mediaQuery.addEventListener('change', handleSystemChange);
} else {
// Fallback for older browsers
this.mediaQuery.addListener(handleSystemChange);
}
}
/**
* Setup storage synchronization
*/
setupStorageSync() {
// Listen for storage changes from other tabs
window.addEventListener('storage', (e) => {
if (e.key === this.options.storageKey) {
const newPreference = e.newValue;
if (newPreference) {
this.applyStoredPreference(newPreference);
}
}
});
}
/**
* Setup map theme styles
*/
setupMapThemeStyles() {
if (document.getElementById('dark-mode-map-styles')) return;
const styles = `
<style id="dark-mode-map-styles">
/* Light theme map styles */
.map-container {
transition: filter ${this.options.transitionDuration}ms ease;
}
/* Dark theme map styles */
.dark .map-container {
filter: brightness(0.9) contrast(1.1);
}
/* Dark theme popup styles */
.dark .leaflet-popup-content-wrapper {
background: #1F2937;
color: #F9FAFB;
border: 1px solid #374151;
}
.dark .leaflet-popup-tip {
background: #1F2937;
border: 1px solid #374151;
}
.dark .leaflet-popup-close-button {
color: #D1D5DB;
}
.dark .leaflet-popup-close-button:hover {
color: #F9FAFB;
}
/* Dark theme control styles */
.dark .leaflet-control-zoom a {
background-color: #374151;
border-color: #4B5563;
color: #F9FAFB;
}
.dark .leaflet-control-zoom a:hover {
background-color: #4B5563;
}
.dark .leaflet-control-attribution {
background: rgba(31, 41, 55, 0.8);
color: #D1D5DB;
}
/* Dark theme marker cluster styles */
.dark .cluster-marker-inner {
background: #1E40AF;
border-color: #1F2937;
}
.dark .cluster-marker-medium .cluster-marker-inner {
background: #059669;
}
.dark .cluster-marker-large .cluster-marker-inner {
background: #DC2626;
}
/* Dark theme location marker styles */
.dark .location-marker-inner {
border-color: #1F2937;
box-shadow: 0 2px 8px rgba(0,0,0,0.6);
}
/* Dark theme filter panel styles */
.dark .filter-chip.active {
background: #1E40AF;
color: #F9FAFB;
}
.dark .filter-chip.inactive {
background: #374151;
color: #D1D5DB;
}
.dark .filter-chip.inactive:hover {
background: #4B5563;
}
/* Dark theme road trip styles */
.dark .park-item {
background: #374151;
border-color: #4B5563;
}
.dark .park-item:hover {
background: #4B5563;
}
/* Dark theme search results */
.dark .search-result-item {
background: #374151;
border-color: #4B5563;
}
.dark .search-result-item:hover {
background: #4B5563;
}
/* Dark theme loading indicators */
.dark .htmx-indicator {
color: #D1D5DB;
}
/* Theme transition effects */
.theme-transition {
transition: background-color ${this.options.transitionDuration}ms ease,
color ${this.options.transitionDuration}ms ease,
border-color ${this.options.transitionDuration}ms ease;
}
/* Dark theme toggle button */
.theme-toggle {
position: relative;
width: 48px;
height: 24px;
background: #E5E7EB;
border-radius: 12px;
border: none;
cursor: pointer;
transition: background-color ${this.options.transitionDuration}ms ease;
}
.dark .theme-toggle {
background: #374151;
}
.theme-toggle::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transition: transform ${this.options.transitionDuration}ms ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.dark .theme-toggle::after {
transform: translateX(24px);
background: #F9FAFB;
}
/* Theme icons */
.theme-icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
transition: opacity ${this.options.transitionDuration}ms ease;
}
.theme-icon.sun {
left: 4px;
color: #F59E0B;
opacity: 1;
}
.theme-icon.moon {
right: 4px;
color: #6366F1;
opacity: 0;
}
.dark .theme-icon.sun {
opacity: 0;
}
.dark .theme-icon.moon {
opacity: 1;
}
/* System preference indicator */
.theme-auto-indicator {
font-size: 10px;
opacity: 0.6;
margin-left: 4px;
}
</style>
`;
document.head.insertAdjacentHTML('beforeend', styles);
}
/**
* Bind event handlers
*/
bindEventHandlers() {
// Handle theme toggle buttons
const themeToggleButtons = document.querySelectorAll('[data-theme-toggle]');
themeToggleButtons.forEach(button => {
button.addEventListener('click', () => {
this.toggleTheme();
});
});
// Handle theme selection
const themeSelectors = document.querySelectorAll('[data-theme-select]');
themeSelectors.forEach(selector => {
selector.addEventListener('change', (e) => {
this.setThemePreference(e.target.value);
});
});
}
/**
* Handle theme change
*/
handleThemeChange(newTheme) {
const oldTheme = this.currentTheme;
this.currentTheme = newTheme;
// Update map tile layers
this.updateMapTileLayers(newTheme);
// Update marker themes
this.updateMarkerThemes(newTheme);
// Emit theme change event
const event = new CustomEvent('themeChanged', {
detail: {
oldTheme,
newTheme,
isSystemPreference: this.getStoredPreference() === 'auto'
}
});
document.dispatchEvent(event);
console.log(`Theme changed from ${oldTheme} to ${newTheme}`);
}
/**
* Update map tile layers for theme
*/
updateMapTileLayers(theme) {
this.mapInstances.forEach(mapInstance => {
const currentTileLayer = this.tileLayers.get(mapInstance);
if (currentTileLayer) {
mapInstance.removeLayer(currentTileLayer);
}
// Create new tile layer for theme
const tileUrl = this.options.tileProviders[theme].osm;
const newTileLayer = L.tileLayer(tileUrl, {
attribution: '© OpenStreetMap contributors' + (theme === 'dark' ? ', © CARTO' : ''),
className: `map-tiles-${theme}`
});
newTileLayer.addTo(mapInstance);
this.tileLayers.set(mapInstance, newTileLayer);
});
}
/**
* Update marker themes
*/
updateMarkerThemes(theme) {
if (window.mapMarkers) {
// Clear marker caches to force re-render with new theme
window.mapMarkers.clearIconCache();
window.mapMarkers.clearPopupCache();
}
}
/**
* Toggle between light and dark themes
*/
toggleTheme() {
const newTheme = this.currentTheme === 'light' ? 'dark' : 'light';
this.setTheme(newTheme);
this.setStoredPreference(newTheme);
}
/**
* Set specific theme
*/
setTheme(theme) {
if (!['light', 'dark'].includes(theme)) return;
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Update theme toggle states
this.updateThemeToggleStates(theme);
}
/**
* Set theme preference (light, dark, auto)
*/
setThemePreference(preference) {
if (!['light', 'dark', 'auto'].includes(preference)) return;
this.setStoredPreference(preference);
if (preference === 'auto') {
this.applySystemPreference();
} else {
this.setTheme(preference);
}
}
/**
* Apply system preference
*/
applySystemPreference() {
if (this.mediaQuery) {
const systemPrefersDark = this.mediaQuery.matches;
this.setTheme(systemPrefersDark ? 'dark' : 'light');
}
}
/**
* Apply stored preference
*/
applyStoredPreference(preference) {
if (preference === 'auto') {
this.applySystemPreference();
} else {
this.setTheme(preference);
}
}
/**
* Get stored theme preference
*/
getStoredPreference() {
return localStorage.getItem(this.options.storageKey) || 'auto';
}
/**
* Set stored theme preference
*/
setStoredPreference(preference) {
localStorage.setItem(this.options.storageKey, preference);
}
/**
* Update theme toggle button states
*/
updateThemeToggleStates(theme) {
const toggleButtons = document.querySelectorAll('[data-theme-toggle]');
toggleButtons.forEach(button => {
button.setAttribute('data-theme', theme);
button.setAttribute('aria-label', `Switch to ${theme === 'light' ? 'dark' : 'light'} theme`);
});
const themeSelectors = document.querySelectorAll('[data-theme-select]');
themeSelectors.forEach(selector => {
selector.value = this.getStoredPreference();
});
}
/**
* Register map instance for theme management
*/
registerMapInstance(mapInstance) {
this.mapInstances.add(mapInstance);
// Apply current theme immediately
setTimeout(() => {
this.updateMapTileLayers(this.currentTheme);
}, 100);
}
/**
* Unregister map instance
*/
unregisterMapInstance(mapInstance) {
this.mapInstances.delete(mapInstance);
this.tileLayers.delete(mapInstance);
}
/**
* Create theme toggle button
*/
createThemeToggle() {
const toggle = document.createElement('button');
toggle.className = 'theme-toggle';
toggle.setAttribute('data-theme-toggle', 'true');
toggle.setAttribute('aria-label', 'Toggle theme');
toggle.innerHTML = `
<i class="theme-icon sun fas fa-sun"></i>
<i class="theme-icon moon fas fa-moon"></i>
`;
toggle.addEventListener('click', () => {
this.toggleTheme();
});
return toggle;
}
/**
* Create theme selector dropdown
*/
createThemeSelector() {
const selector = document.createElement('select');
selector.className = 'theme-selector';
selector.setAttribute('data-theme-select', 'true');
selector.innerHTML = `
<option value="auto">Auto</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
`;
selector.value = this.getStoredPreference();
selector.addEventListener('change', (e) => {
this.setThemePreference(e.target.value);
});
return selector;
}
/**
* Get current theme
*/
getCurrentTheme() {
return this.currentTheme;
}
/**
* Check if dark mode is active
*/
isDarkMode() {
return this.currentTheme === 'dark';
}
/**
* Check if system preference is supported
*/
isSystemPreferenceSupported() {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').media !== 'not all';
}
/**
* Get system preference
*/
getSystemPreference() {
if (this.isSystemPreferenceSupported() && this.mediaQuery) {
return this.mediaQuery.matches ? 'dark' : 'light';
}
return 'light';
}
/**
* Add theme transition classes
*/
addThemeTransitions() {
const elements = document.querySelectorAll('.filter-chip, .park-item, .search-result-item, .popup-btn');
elements.forEach(element => {
element.classList.add('theme-transition');
});
}
/**
* Remove theme transition classes
*/
removeThemeTransitions() {
const elements = document.querySelectorAll('.theme-transition');
elements.forEach(element => {
element.classList.remove('theme-transition');
});
}
/**
* Destroy dark mode instance
*/
destroy() {
if (this.observer) {
this.observer.disconnect();
}
if (this.mediaQuery) {
if (this.mediaQuery.removeEventListener) {
this.mediaQuery.removeEventListener('change', this.applySystemPreference);
} else {
this.mediaQuery.removeListener(this.applySystemPreference);
}
}
this.mapInstances.clear();
this.tileLayers.clear();
}
}
// Auto-initialize dark mode support
document.addEventListener('DOMContentLoaded', function() {
window.darkModeMaps = new DarkModeMaps();
// Register existing map instances
if (window.thrillwikiMap) {
window.darkModeMaps.registerMapInstance(window.thrillwikiMap);
}
// Add theme transitions
window.darkModeMaps.addThemeTransitions();
// Add theme toggle to navigation if it doesn't exist
const nav = document.querySelector('nav, .navbar, .header-nav');
if (nav && !nav.querySelector('[data-theme-toggle]')) {
const themeToggle = window.darkModeMaps.createThemeToggle();
themeToggle.style.marginLeft = 'auto';
nav.appendChild(themeToggle);
}
});
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = DarkModeMaps;
} else {
window.DarkModeMaps = DarkModeMaps;
}