mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 16:51:07 -05:00
- Add complete backend/ directory with full Django application - Add frontend/ directory with Vite + TypeScript setup ready for Next.js - Add comprehensive shared/ directory with: - Complete documentation and memory-bank archives - Media files and avatars (letters, park/ride images) - Deployment scripts and automation tools - Shared types and utilities - Add architecture/ directory with migration guides - Configure pnpm workspace for monorepo development - Update .gitignore to exclude .django_tailwind_cli/ build artifacts - Preserve all historical documentation in shared/docs/memory-bank/ - Set up proper structure for full-stack development with shared resources
665 lines
20 KiB
JavaScript
665 lines
20 KiB
JavaScript
/**
|
|
* 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;
|
|
} |