diff --git a/static/js/backup/alerts.js b/static/js/backup/alerts.js deleted file mode 100644 index fc054c16..00000000 --- a/static/js/backup/alerts.js +++ /dev/null @@ -1,18 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - // Get all alert elements - const alerts = document.querySelectorAll('.alert'); - - // For each alert - alerts.forEach(alert => { - // After 5 seconds - setTimeout(() => { - // Add slideOut animation - alert.style.animation = 'slideOut 0.5s ease-out forwards'; - - // Remove the alert after animation completes - setTimeout(() => { - alert.remove(); - }, 500); - }, 5000); - }); -}); diff --git a/static/js/backup/alpine-components.js b/static/js/backup/alpine-components.js deleted file mode 100644 index 04ab9803..00000000 --- a/static/js/backup/alpine-components.js +++ /dev/null @@ -1,746 +0,0 @@ -/** - * Alpine.js Components for ThrillWiki - * Enhanced components matching React frontend functionality - */ - -// Flag to prevent duplicate component registration -let componentsRegistered = false; - -// Debug logging to see what's happening -console.log('Alpine components script is loading...'); - -// Try multiple approaches to ensure components register -function registerComponents() { - // Prevent duplicate registration - if (componentsRegistered) { - return; - } - - if (typeof Alpine === 'undefined') { - console.warn('Alpine.js not available yet, registration will retry'); - return; - } - - console.log('Registering Alpine.js components...'); - - // Mark as registered at the start to prevent race conditions - componentsRegistered = true; - -// Theme Toggle Component -Alpine.data('themeToggle', () => ({ - theme: localStorage.getItem('theme') || 'system', - - init() { - this.updateTheme(); - - // Watch for system theme changes - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { - if (this.theme === 'system') { - this.updateTheme(); - } - }); - }, - - toggleTheme() { - const themes = ['light', 'dark', 'system']; - const currentIndex = themes.indexOf(this.theme); - this.theme = themes[(currentIndex + 1) % themes.length]; - localStorage.setItem('theme', this.theme); - this.updateTheme(); - }, - - updateTheme() { - const root = document.documentElement; - - if (this.theme === 'dark' || - (this.theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { - root.classList.add('dark'); - } else { - root.classList.remove('dark'); - } - } -})); - -// Search Component -Alpine.data('searchComponent', () => ({ - query: '', - results: [], - loading: false, - showResults: false, - - async search() { - if (this.query.length < 2) { - this.results = []; - this.showResults = false; - return; - } - - this.loading = true; - - try { - // Use the same search endpoint as HTMX in the template - const response = await fetch(`/search/parks/?q=${encodeURIComponent(this.query)}`, { - headers: { - 'Accept': 'application/json', - 'X-Requested-With': 'XMLHttpRequest' - } - }); - - if (response.ok) { - // Try to parse as JSON first, fallback to extracting from HTML - const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('application/json')) { - const data = await response.json(); - this.results = data.results || []; - } else { - // Parse HTML response to extract search results - const html = await response.text(); - this.results = this.parseSearchResults(html); - } - this.showResults = this.results.length > 0; - } else { - this.results = []; - this.showResults = false; - } - } catch (error) { - console.error('Search error:', error); - this.results = []; - this.showResults = false; - } finally { - this.loading = false; - } - }, - - parseSearchResults(html) { - // Helper method to extract search results from HTML response - try { - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - const results = []; - - // Look for search result items in the HTML - const resultElements = doc.querySelectorAll('[data-search-result], .search-result-item, .park-item'); - resultElements.forEach(element => { - const title = element.querySelector('h3, .title, [data-title]')?.textContent?.trim(); - const url = element.querySelector('a')?.getAttribute('href'); - const description = element.querySelector('.description, .excerpt, p')?.textContent?.trim(); - - if (title && url) { - results.push({ - title, - url, - description: description || '' - }); - } - }); - - return results.slice(0, 10); // Limit to 10 results - } catch (error) { - console.error('Error parsing search results:', error); - return []; - } - }, - - selectResult(result) { - window.location.href = result.url; - this.showResults = false; - this.query = ''; - }, - - clearSearch() { - this.query = ''; - this.results = []; - this.showResults = false; - } -})); - -// Browse Menu Component -Alpine.data('browseMenu', () => ({ - open: false, - - toggle() { - this.open = !this.open; - }, - - close() { - this.open = false; - } -})); - -// Mobile Menu Component -Alpine.data('mobileMenu', () => ({ - open: false, - - toggle() { - this.open = !this.open; - - // Prevent body scroll when menu is open - if (this.open) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = ''; - } - }, - - close() { - this.open = false; - document.body.style.overflow = ''; - } -})); - -// User Menu Component -Alpine.data('userMenu', () => ({ - open: false, - - toggle() { - this.open = !this.open; - }, - - close() { - this.open = false; - } -})); - -// Modal Component -Alpine.data('modal', (initialOpen = false) => ({ - open: initialOpen, - - show() { - this.open = true; - document.body.style.overflow = 'hidden'; - }, - - hide() { - this.open = false; - document.body.style.overflow = ''; - }, - - toggle() { - if (this.open) { - this.hide(); - } else { - this.show(); - } - } -})); - -// Dropdown Component -Alpine.data('dropdown', (initialOpen = false) => ({ - open: initialOpen, - - toggle() { - this.open = !this.open; - }, - - close() { - this.open = false; - }, - - show() { - this.open = true; - } -})); - -// Tabs Component -Alpine.data('tabs', (defaultTab = 0) => ({ - activeTab: defaultTab, - - setTab(index) { - this.activeTab = index; - }, - - isActive(index) { - return this.activeTab === index; - } -})); - -// Accordion Component -Alpine.data('accordion', (allowMultiple = false) => ({ - openItems: [], - - toggle(index) { - if (this.isOpen(index)) { - this.openItems = this.openItems.filter(item => item !== index); - } else { - if (allowMultiple) { - this.openItems.push(index); - } else { - this.openItems = [index]; - } - } - }, - - isOpen(index) { - return this.openItems.includes(index); - }, - - open(index) { - if (!this.isOpen(index)) { - if (allowMultiple) { - this.openItems.push(index); - } else { - this.openItems = [index]; - } - } - }, - - close(index) { - this.openItems = this.openItems.filter(item => item !== index); - } -})); - -// Form Component with Validation -Alpine.data('form', (initialData = {}) => ({ - data: initialData, - errors: {}, - loading: false, - - setField(field, value) { - this.data[field] = value; - // Clear error when user starts typing - if (this.errors[field]) { - delete this.errors[field]; - } - }, - - setError(field, message) { - this.errors[field] = message; - }, - - clearErrors() { - this.errors = {}; - }, - - hasError(field) { - return !!this.errors[field]; - }, - - getError(field) { - return this.errors[field] || ''; - }, - - async submit(url, options = {}) { - this.loading = true; - this.clearErrors(); - - try { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || '', - ...options.headers - }, - body: JSON.stringify(this.data), - ...options - }); - - const result = await response.json(); - - if (!response.ok) { - if (result.errors) { - this.errors = result.errors; - } - throw new Error(result.message || 'Form submission failed'); - } - - return result; - } catch (error) { - console.error('Form submission error:', error); - throw error; - } finally { - this.loading = false; - } - } -})); - -// Pagination Component -Alpine.data('pagination', (initialPage = 1, totalPages = 1) => ({ - currentPage: initialPage, - totalPages: totalPages, - - goToPage(page) { - if (page >= 1 && page <= this.totalPages) { - this.currentPage = page; - } - }, - - nextPage() { - this.goToPage(this.currentPage + 1); - }, - - prevPage() { - this.goToPage(this.currentPage - 1); - }, - - hasNext() { - return this.currentPage < this.totalPages; - }, - - hasPrev() { - return this.currentPage > 1; - }, - - getPages() { - const pages = []; - const start = Math.max(1, this.currentPage - 2); - const end = Math.min(this.totalPages, this.currentPage + 2); - - for (let i = start; i <= end; i++) { - pages.push(i); - } - - return pages; - } -})); - - -// Enhanced Authentication Modal Component -Alpine.data('authModal', (defaultMode = 'login') => ({ - open: false, - mode: defaultMode, // 'login' or 'register' - showPassword: false, - socialProviders: [ - {id: 'google', name: 'Google', auth_url: '/accounts/google/login/'}, - {id: 'discord', name: 'Discord', auth_url: '/accounts/discord/login/'} - ], - socialLoading: false, - - // Login form data - loginForm: { - username: '', - password: '' - }, - loginLoading: false, - loginError: '', - - // Register form data - registerForm: { - first_name: '', - last_name: '', - email: '', - username: '', - password1: '', - password2: '' - }, - registerLoading: false, - registerError: '', - - init() { - // Listen for auth modal events - this.$watch('open', (value) => { - if (value) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = ''; - this.resetForms(); - } - }); - }, - - - show(mode = 'login') { - this.mode = mode; - this.open = true; - }, - - close() { - this.open = false; - }, - - switchToLogin() { - this.mode = 'login'; - this.resetForms(); - }, - - switchToRegister() { - this.mode = 'register'; - this.resetForms(); - }, - - resetForms() { - this.loginForm = { username: '', password: '' }; - this.registerForm = { - first_name: '', - last_name: '', - email: '', - username: '', - password1: '', - password2: '' - }; - this.loginError = ''; - this.registerError = ''; - this.showPassword = false; - }, - - async handleLogin() { - if (!this.loginForm.username || !this.loginForm.password) { - this.loginError = 'Please fill in all fields'; - return; - } - - this.loginLoading = true; - this.loginError = ''; - - try { - const response = await fetch('/accounts/login/', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': this.getCSRFToken(), - 'X-Requested-With': 'XMLHttpRequest' - }, - body: new URLSearchParams({ - login: this.loginForm.username, - password: this.loginForm.password, - next: window.location.pathname - }), - redirect: 'manual' // Handle redirects manually - }); - - // Django allauth returns 302 redirect on successful login - if (response.status === 302 || (response.ok && response.status === 200)) { - // Check if login was successful by trying to parse response - try { - const html = await response.text(); - // If response contains error messages, login failed - if (html.includes('errorlist') || html.includes('alert-danger') || html.includes('invalid')) { - this.loginError = this.extractErrorFromHTML(html) || 'Login failed. Please check your credentials.'; - } else { - // Login successful - reload page to update auth state - window.location.reload(); - } - } catch { - // If we can't parse response, assume success and reload - window.location.reload(); - } - } else if (response.status === 200) { - // Form validation errors - parse HTML response for error messages - const html = await response.text(); - this.loginError = this.extractErrorFromHTML(html) || 'Login failed. Please check your credentials.'; - } else { - this.loginError = 'Login failed. Please check your credentials.'; - } - } catch (error) { - console.error('Login error:', error); - this.loginError = 'An error occurred. Please try again.'; - } finally { - this.loginLoading = false; - } - }, - - async handleRegister() { - if (!this.registerForm.first_name || !this.registerForm.last_name || - !this.registerForm.email || !this.registerForm.username || - !this.registerForm.password1 || !this.registerForm.password2) { - this.registerError = 'Please fill in all fields'; - return; - } - - if (this.registerForm.password1 !== this.registerForm.password2) { - this.registerError = 'Passwords do not match'; - return; - } - - this.registerLoading = true; - this.registerError = ''; - - try { - const response = await fetch('/accounts/signup/', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': this.getCSRFToken(), - 'X-Requested-With': 'XMLHttpRequest' - }, - body: new URLSearchParams({ - first_name: this.registerForm.first_name, - last_name: this.registerForm.last_name, - email: this.registerForm.email, - username: this.registerForm.username, - password1: this.registerForm.password1, - password2: this.registerForm.password2 - }), - redirect: 'manual' - }); - - if (response.status === 302 || response.ok) { - try { - const html = await response.text(); - // Check if registration was successful - if (html.includes('errorlist') || html.includes('alert-danger') || html.includes('invalid')) { - this.registerError = this.extractErrorFromHTML(html) || 'Registration failed. Please try again.'; - } else { - // Registration successful - this.close(); - Alpine.store('toast').success('Account created successfully! Please check your email to verify your account.'); - } - } catch { - // Assume success if we can't parse response - this.close(); - Alpine.store('toast').success('Account created successfully! Please check your email to verify your account.'); - } - } else if (response.status === 200) { - // Form validation errors - const html = await response.text(); - this.registerError = this.extractErrorFromHTML(html) || 'Registration failed. Please try again.'; - } else { - this.registerError = 'Registration failed. Please try again.'; - } - } catch (error) { - console.error('Registration error:', error); - this.registerError = 'An error occurred. Please try again.'; - } finally { - this.registerLoading = false; - } - }, - - handleSocialLogin(providerId) { - const provider = this.socialProviders.find(p => p.id === providerId); - if (!provider) { - Alpine.store('toast').error(`Social provider ${providerId} not found.`); - return; - } - - // Redirect to social auth URL - window.location.href = provider.auth_url; - }, - - extractErrorFromHTML(html) { - try { - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - - // Look for error messages in various formats - const errorSelectors = [ - '.errorlist li', - '.alert-danger', - '.invalid-feedback', - '.form-error', - '[class*="error"]', - '.field-error' - ]; - - for (const selector of errorSelectors) { - const errorElements = doc.querySelectorAll(selector); - if (errorElements.length > 0) { - return Array.from(errorElements) - .map(el => el.textContent.trim()) - .filter(text => text.length > 0) - .join(' '); - } - } - - return null; - } catch (error) { - console.error('Error parsing HTML for error messages:', error); - return null; - } - }, - - getCSRFToken() { - const token = document.querySelector('[name=csrfmiddlewaretoken]')?.value || - document.querySelector('meta[name=csrf-token]')?.getAttribute('content') || - document.cookie.split('; ').find(row => row.startsWith('csrftoken='))?.split('=')[1]; - return token || ''; - } -})); - - -// Global Store for App State -Alpine.store('app', { - user: null, - theme: 'system', - searchQuery: '', - notifications: [], - - setUser(user) { - this.user = user; - }, - - setTheme(theme) { - this.theme = theme; - localStorage.setItem('theme', theme); - }, - - addNotification(notification) { - this.notifications.push({ - id: Date.now(), - ...notification - }); - }, - - removeNotification(id) { - this.notifications = this.notifications.filter(n => n.id !== id); - } -}); - -// Global Toast Store -Alpine.store('toast', { - toasts: [], - - show(message, type = 'info', duration = 5000) { - const id = Date.now() + Math.random(); - const toast = { - id, - message, - type, - visible: true, - progress: 100 - }; - - this.toasts.push(toast); - - if (duration > 0) { - const interval = setInterval(() => { - toast.progress -= (100 / (duration / 100)); - if (toast.progress <= 0) { - clearInterval(interval); - this.hide(id); - } - }, 100); - } - - return id; - }, - - hide(id) { - const toast = this.toasts.find(t => t.id === id); - if (toast) { - toast.visible = false; - setTimeout(() => { - this.toasts = this.toasts.filter(t => t.id !== id); - }, 300); - } - }, - - success(message, duration = 5000) { - return this.show(message, 'success', duration); - }, - - error(message, duration = 7000) { - return this.show(message, 'error', duration); - }, - - warning(message, duration = 6000) { - return this.show(message, 'warning', duration); - }, - - info(message, duration = 5000) { - return this.show(message, 'info', duration); - } -}); - - console.log('Alpine.js components registered successfully'); -} - -// Try multiple registration approaches -document.addEventListener('alpine:init', registerComponents); -document.addEventListener('DOMContentLoaded', registerComponents); - -// Fallback - try after a delay -setTimeout(() => { - if (typeof Alpine !== 'undefined' && !componentsRegistered) { - registerComponents(); - } -}, 100); diff --git a/static/js/backup/dark-mode-maps.js b/static/js/backup/dark-mode-maps.js deleted file mode 100644 index 99cfaf3c..00000000 --- a/static/js/backup/dark-mode-maps.js +++ /dev/null @@ -1,665 +0,0 @@ -/** - * 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 = ` - - `; - - 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 = ` - - - `; - - 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 = ` - - - - `; - - 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; -} \ No newline at end of file diff --git a/static/js/backup/geolocation.js b/static/js/backup/geolocation.js deleted file mode 100644 index 521cf6fe..00000000 --- a/static/js/backup/geolocation.js +++ /dev/null @@ -1,720 +0,0 @@ -/** - * ThrillWiki Geolocation - User Location and "Near Me" Functionality - * - * This module handles browser geolocation API integration with privacy-conscious - * permission handling, distance calculations, and "near me" functionality - */ - -class UserLocation { - constructor(options = {}) { - this.options = { - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 300000, // 5 minutes - watchPosition: false, - autoShowOnMap: true, - showAccuracyCircle: true, - enableCaching: true, - cacheKey: 'thrillwiki_user_location', - apiEndpoints: { - nearby: '/api/map/nearby/', - distance: '/api/map/distance/' - }, - defaultRadius: 50, // miles - maxRadius: 500, - ...options - }; - - this.currentPosition = null; - this.watchId = null; - this.mapInstance = null; - this.locationMarker = null; - this.accuracyCircle = null; - this.permissionState = 'unknown'; - this.lastLocationTime = null; - - // Event handlers - this.eventHandlers = { - locationFound: [], - locationError: [], - permissionChanged: [] - }; - - this.init(); - } - - /** - * Initialize the geolocation component - */ - init() { - this.checkGeolocationSupport(); - this.loadCachedLocation(); - this.setupLocationButtons(); - this.checkPermissionState(); - } - - /** - * Check if geolocation is supported by the browser - */ - checkGeolocationSupport() { - if (!navigator.geolocation) { - console.warn('Geolocation is not supported by this browser'); - this.hideLocationButtons(); - return false; - } - return true; - } - - /** - * Setup location-related buttons and controls - */ - setupLocationButtons() { - // Find all "locate me" buttons - const locateButtons = document.querySelectorAll('[data-action="locate-user"], .locate-user-btn'); - - locateButtons.forEach(button => { - button.addEventListener('click', (e) => { - e.preventDefault(); - this.requestLocation(); - }); - }); - - // Find "near me" buttons - const nearMeButtons = document.querySelectorAll('[data-action="near-me"], .near-me-btn'); - - nearMeButtons.forEach(button => { - button.addEventListener('click', (e) => { - e.preventDefault(); - this.showNearbyLocations(); - }); - }); - - // Distance calculator buttons - const distanceButtons = document.querySelectorAll('[data-action="calculate-distance"]'); - - distanceButtons.forEach(button => { - button.addEventListener('click', (e) => { - e.preventDefault(); - const targetLat = parseFloat(button.dataset.lat); - const targetLng = parseFloat(button.dataset.lng); - this.calculateDistance(targetLat, targetLng); - }); - }); - } - - /** - * Hide location buttons when geolocation is not supported - */ - hideLocationButtons() { - const locationElements = document.querySelectorAll('.geolocation-feature'); - locationElements.forEach(el => { - el.style.display = 'none'; - }); - } - - /** - * Check current permission state - */ - async checkPermissionState() { - if ('permissions' in navigator) { - try { - const permission = await navigator.permissions.query({ name: 'geolocation' }); - this.permissionState = permission.state; - this.updateLocationButtonStates(); - - // Listen for permission changes - permission.addEventListener('change', () => { - this.permissionState = permission.state; - this.updateLocationButtonStates(); - this.triggerEvent('permissionChanged', this.permissionState); - }); - } catch (error) { - console.warn('Could not check geolocation permission:', error); - } - } - } - - /** - * Update location button states based on permission - */ - updateLocationButtonStates() { - const locateButtons = document.querySelectorAll('[data-action="locate-user"], .locate-user-btn'); - - locateButtons.forEach(button => { - const icon = button.querySelector('i') || button; - - switch (this.permissionState) { - case 'granted': - button.disabled = false; - button.title = 'Find my location'; - icon.className = 'fas fa-crosshairs'; - break; - case 'denied': - button.disabled = true; - button.title = 'Location access denied'; - icon.className = 'fas fa-times-circle'; - break; - case 'prompt': - default: - button.disabled = false; - button.title = 'Find my location (permission required)'; - icon.className = 'fas fa-crosshairs'; - break; - } - }); - } - - /** - * Request user location - */ - requestLocation(options = {}) { - if (!navigator.geolocation) { - this.handleLocationError(new Error('Geolocation not supported')); - return; - } - - const requestOptions = { - ...this.options, - ...options - }; - - // Show loading state - this.setLocationButtonLoading(true); - - navigator.geolocation.getCurrentPosition( - (position) => this.handleLocationSuccess(position), - (error) => this.handleLocationError(error), - { - enableHighAccuracy: requestOptions.enableHighAccuracy, - timeout: requestOptions.timeout, - maximumAge: requestOptions.maximumAge - } - ); - } - - /** - * Start watching user position - */ - startWatching() { - if (!navigator.geolocation || this.watchId) return; - - this.watchId = navigator.geolocation.watchPosition( - (position) => this.handleLocationSuccess(position), - (error) => this.handleLocationError(error), - { - enableHighAccuracy: this.options.enableHighAccuracy, - timeout: this.options.timeout, - maximumAge: this.options.maximumAge - } - ); - } - - /** - * Stop watching user position - */ - stopWatching() { - if (this.watchId) { - navigator.geolocation.clearWatch(this.watchId); - this.watchId = null; - } - } - - /** - * Handle successful location acquisition - */ - handleLocationSuccess(position) { - this.currentPosition = { - lat: position.coords.latitude, - lng: position.coords.longitude, - accuracy: position.coords.accuracy, - timestamp: position.timestamp - }; - - this.lastLocationTime = Date.now(); - - // Cache location - if (this.options.enableCaching) { - this.cacheLocation(this.currentPosition); - } - - // Show on map if enabled - if (this.options.autoShowOnMap && this.mapInstance) { - this.showLocationOnMap(); - } - - // Update button states - this.setLocationButtonLoading(false); - this.updateLocationButtonStates(); - - // Trigger event - this.triggerEvent('locationFound', this.currentPosition); - - console.log('Location found:', this.currentPosition); - } - - /** - * Handle location errors - */ - handleLocationError(error) { - this.setLocationButtonLoading(false); - - let message = 'Unable to get your location'; - - switch (error.code) { - case error.PERMISSION_DENIED: - message = 'Location access denied. Please enable location services.'; - this.permissionState = 'denied'; - break; - case error.POSITION_UNAVAILABLE: - message = 'Location information is unavailable.'; - break; - case error.TIMEOUT: - message = 'Location request timed out.'; - break; - default: - message = 'An unknown error occurred while retrieving location.'; - break; - } - - this.showLocationMessage(message, 'error'); - this.updateLocationButtonStates(); - - // Trigger event - this.triggerEvent('locationError', { error, message }); - - console.error('Location error:', error); - } - - /** - * Show user location on map - */ - showLocationOnMap() { - if (!this.mapInstance || !this.currentPosition) return; - - const { lat, lng, accuracy } = this.currentPosition; - - // Remove existing location marker and circle - this.clearLocationDisplay(); - - // Add location marker - this.locationMarker = L.marker([lat, lng], { - icon: this.createUserLocationIcon() - }).addTo(this.mapInstance); - - this.locationMarker.bindPopup(` -
-

Your Location

-

Accuracy: ±${Math.round(accuracy)}m

-
- -
-
- `); - - // Add accuracy circle if enabled and accuracy is reasonable - if (this.options.showAccuracyCircle && accuracy < 1000) { - this.accuracyCircle = L.circle([lat, lng], { - radius: accuracy, - fillColor: '#3388ff', - fillOpacity: 0.2, - color: '#3388ff', - weight: 2, - opacity: 0.5 - }).addTo(this.mapInstance); - } - - // Center map on user location - this.mapInstance.setView([lat, lng], 13); - } - - /** - * Create custom icon for user location - */ - createUserLocationIcon() { - return L.divIcon({ - className: 'user-location-marker', - html: ` -
- -
- `, - iconSize: [24, 24], - iconAnchor: [12, 12] - }); - } - - /** - * Clear location display from map - */ - clearLocationDisplay() { - if (this.locationMarker && this.mapInstance) { - this.mapInstance.removeLayer(this.locationMarker); - this.locationMarker = null; - } - - if (this.accuracyCircle && this.mapInstance) { - this.mapInstance.removeLayer(this.accuracyCircle); - this.accuracyCircle = null; - } - } - - /** - * Show nearby locations - */ - async showNearbyLocations(radius = null) { - if (!this.currentPosition) { - this.requestLocation(); - return; - } - - try { - const searchRadius = radius || this.options.defaultRadius; - const { lat, lng } = this.currentPosition; - - const params = new URLSearchParams({ - lat: lat, - lng: lng, - radius: searchRadius, - unit: 'miles' - }); - - const response = await fetch(`${this.options.apiEndpoints.nearby}?${params}`); - const data = await response.json(); - - if (data.status === 'success') { - this.displayNearbyResults(data.data); - } else { - this.showLocationMessage('No nearby locations found', 'info'); - } - } catch (error) { - console.error('Failed to find nearby locations:', error); - this.showLocationMessage('Failed to find nearby locations', 'error'); - } - } - - /** - * Display nearby search results - */ - displayNearbyResults(results) { - // Find or create results container - let resultsContainer = document.getElementById('nearby-results'); - - if (!resultsContainer) { - resultsContainer = document.createElement('div'); - resultsContainer.id = 'nearby-results'; - resultsContainer.className = 'nearby-results-container'; - - // Try to insert after a logical element - const mapContainer = document.getElementById('map-container'); - if (mapContainer && mapContainer.parentNode) { - mapContainer.parentNode.insertBefore(resultsContainer, mapContainer.nextSibling); - } else { - document.body.appendChild(resultsContainer); - } - } - - const html = ` -
-

- - Nearby Parks (${results.length} found) -

-
- ${results.map(location => ` -
-
-

${location.name}

-

${location.formatted_location || ''}

-

- - ${location.distance} away -

-
-
- -
-
- `).join('')} -
-
- `; - - resultsContainer.innerHTML = html; - - // Scroll to results - resultsContainer.scrollIntoView({ behavior: 'smooth' }); - } - - /** - * Calculate distance to a specific location - */ - async calculateDistance(targetLat, targetLng) { - if (!this.currentPosition) { - this.showLocationMessage('Please enable location services first', 'warning'); - return null; - } - - try { - const { lat, lng } = this.currentPosition; - - const params = new URLSearchParams({ - from_lat: lat, - from_lng: lng, - to_lat: targetLat, - to_lng: targetLng - }); - - const response = await fetch(`${this.options.apiEndpoints.distance}?${params}`); - const data = await response.json(); - - if (data.status === 'success') { - return data.data; - } - } catch (error) { - console.error('Failed to calculate distance:', error); - } - - // Fallback to Haversine formula - return this.calculateHaversineDistance( - this.currentPosition.lat, - this.currentPosition.lng, - targetLat, - targetLng - ); - } - - /** - * Calculate distance using Haversine formula - */ - calculateHaversineDistance(lat1, lng1, lat2, lng2) { - const R = 3959; // Earth's radius in miles - const dLat = this.toRadians(lat2 - lat1); - const dLng = this.toRadians(lng2 - lng1); - - const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * - Math.sin(dLng / 2) * Math.sin(dLng / 2); - - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - const distance = R * c; - - return { - distance: Math.round(distance * 10) / 10, - unit: 'miles' - }; - } - - /** - * Convert degrees to radians - */ - toRadians(degrees) { - return degrees * (Math.PI / 180); - } - - /** - * Center map on specific location - */ - centerOnLocation(lat, lng, zoom = 15) { - if (this.mapInstance) { - this.mapInstance.setView([lat, lng], zoom); - } - } - - /** - * Cache user location - */ - cacheLocation(position) { - try { - const cacheData = { - position: position, - timestamp: Date.now() - }; - localStorage.setItem(this.options.cacheKey, JSON.stringify(cacheData)); - } catch (error) { - console.warn('Failed to cache location:', error); - } - } - - /** - * Load cached location - */ - loadCachedLocation() { - if (!this.options.enableCaching) return null; - - try { - const cached = localStorage.getItem(this.options.cacheKey); - if (!cached) return null; - - const cacheData = JSON.parse(cached); - const age = Date.now() - cacheData.timestamp; - - // Check if cache is still valid (5 minutes) - if (age < this.options.maximumAge) { - this.currentPosition = cacheData.position; - this.lastLocationTime = cacheData.timestamp; - return this.currentPosition; - } else { - // Remove expired cache - localStorage.removeItem(this.options.cacheKey); - } - } catch (error) { - console.warn('Failed to load cached location:', error); - } - - return null; - } - - /** - * Set loading state for location buttons - */ - setLocationButtonLoading(loading) { - const locateButtons = document.querySelectorAll('[data-action="locate-user"], .locate-user-btn'); - - locateButtons.forEach(button => { - const icon = button.querySelector('i') || button; - - if (loading) { - button.disabled = true; - icon.className = 'fas fa-spinner fa-spin'; - } else { - button.disabled = false; - // Icon will be updated by updateLocationButtonStates - } - }); - } - - /** - * Show location-related message - */ - showLocationMessage(message, type = 'info') { - // Create or update message element - let messageEl = document.getElementById('location-message'); - - if (!messageEl) { - messageEl = document.createElement('div'); - messageEl.id = 'location-message'; - messageEl.className = 'location-message'; - - // Insert at top of page or after header - const header = document.querySelector('header, .header'); - if (header) { - header.parentNode.insertBefore(messageEl, header.nextSibling); - } else { - document.body.insertBefore(messageEl, document.body.firstChild); - } - } - - messageEl.textContent = message; - messageEl.className = `location-message location-message-${type}`; - messageEl.style.display = 'block'; - - // Auto-hide after delay - setTimeout(() => { - if (messageEl.parentNode) { - messageEl.style.display = 'none'; - } - }, 5000); - } - - /** - * Connect to a map instance - */ - connectToMap(mapInstance) { - this.mapInstance = mapInstance; - - // Show cached location on map if available - if (this.currentPosition && this.options.autoShowOnMap) { - this.showLocationOnMap(); - } - } - - /** - * Get current position - */ - getCurrentPosition() { - return this.currentPosition; - } - - /** - * Check if location is available - */ - hasLocation() { - return this.currentPosition !== null; - } - - /** - * Check if location is recent - */ - isLocationRecent(maxAge = 300000) { // 5 minutes default - if (!this.lastLocationTime) return false; - return (Date.now() - this.lastLocationTime) < maxAge; - } - - /** - * Add event listener - */ - on(event, handler) { - if (!this.eventHandlers[event]) { - this.eventHandlers[event] = []; - } - this.eventHandlers[event].push(handler); - } - - /** - * Remove event listener - */ - off(event, handler) { - if (this.eventHandlers[event]) { - const index = this.eventHandlers[event].indexOf(handler); - if (index > -1) { - this.eventHandlers[event].splice(index, 1); - } - } - } - - /** - * Trigger event - */ - triggerEvent(event, data) { - if (this.eventHandlers[event]) { - this.eventHandlers[event].forEach(handler => { - try { - handler(data); - } catch (error) { - console.error(`Error in ${event} handler:`, error); - } - }); - } - } - - /** - * Destroy the geolocation instance - */ - destroy() { - this.stopWatching(); - this.clearLocationDisplay(); - this.eventHandlers = {}; - } -} - -// Auto-initialize user location -document.addEventListener('DOMContentLoaded', function() { - window.userLocation = new UserLocation(); - - // Connect to map instance if available - if (window.thrillwikiMap) { - window.userLocation.connectToMap(window.thrillwikiMap); - } -}); - -// Export for use in other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = UserLocation; -} else { - window.UserLocation = UserLocation; -} \ No newline at end of file diff --git a/static/js/backup/htmx-maps.js b/static/js/backup/htmx-maps.js deleted file mode 100644 index 3d849936..00000000 --- a/static/js/backup/htmx-maps.js +++ /dev/null @@ -1,725 +0,0 @@ -/** - * ThrillWiki HTMX Maps Integration - Dynamic Map Updates via HTMX - * - * This module handles HTMX events for map updates, manages loading states - * during API calls, updates map content based on HTMX responses, and provides - * error handling for failed requests - */ - -class HTMXMapIntegration { - constructor(options = {}) { - this.options = { - mapInstance: null, - filterInstance: null, - defaultTarget: '#map-container', - loadingClass: 'htmx-loading', - errorClass: 'htmx-error', - successClass: 'htmx-success', - loadingTimeout: 30000, // 30 seconds - retryAttempts: 3, - retryDelay: 1000, - ...options - }; - - this.loadingElements = new Set(); - this.activeRequests = new Map(); - this.requestQueue = []; - this.retryCount = new Map(); - - this.init(); - } - - /** - * Initialize HTMX integration - */ - init() { - if (typeof htmx === 'undefined') { - console.warn('HTMX not found, map integration disabled'); - return; - } - - this.setupEventHandlers(); - this.setupCustomEvents(); - this.setupErrorHandling(); - this.enhanceExistingElements(); - } - - /** - * Setup HTMX event handlers - */ - setupEventHandlers() { - // Before request - show loading states - document.addEventListener('htmx:beforeRequest', (e) => { - this.handleBeforeRequest(e); - }); - - // After request - handle response and update maps - document.addEventListener('htmx:afterRequest', (e) => { - this.handleAfterRequest(e); - }); - - // Response error - handle failed requests - document.addEventListener('htmx:responseError', (e) => { - this.handleResponseError(e); - }); - - // Send error - handle network errors - document.addEventListener('htmx:sendError', (e) => { - this.handleSendError(e); - }); - - // Timeout - handle request timeouts - document.addEventListener('htmx:timeout', (e) => { - this.handleTimeout(e); - }); - - // Before swap - prepare for content updates - document.addEventListener('htmx:beforeSwap', (e) => { - this.handleBeforeSwap(e); - }); - - // After swap - update maps with new content - document.addEventListener('htmx:afterSwap', (e) => { - this.handleAfterSwap(e); - }); - - // Config request - modify requests before sending - document.addEventListener('htmx:configRequest', (e) => { - this.handleConfigRequest(e); - }); - } - - /** - * Setup custom map-specific events - */ - setupCustomEvents() { - // Custom event for map data updates - document.addEventListener('map:dataUpdate', (e) => { - this.handleMapDataUpdate(e); - }); - - // Custom event for filter changes - document.addEventListener('filter:changed', (e) => { - this.handleFilterChange(e); - }); - - // Custom event for search updates - document.addEventListener('search:results', (e) => { - this.handleSearchResults(e); - }); - } - - /** - * Setup global error handling - */ - setupErrorHandling() { - // Global error handler - window.addEventListener('error', (e) => { - if (e.filename && e.filename.includes('htmx')) { - console.error('HTMX error:', e.error); - this.showErrorMessage('An error occurred while updating the map'); - } - }); - - // Unhandled promise rejection handler - window.addEventListener('unhandledrejection', (e) => { - if (e.reason && e.reason.toString().includes('htmx')) { - console.error('HTMX promise rejection:', e.reason); - this.showErrorMessage('Failed to complete map request'); - } - }); - } - - /** - * Enhance existing elements with HTMX map functionality - */ - enhanceExistingElements() { - // Add map-specific attributes to filter forms - const filterForms = document.querySelectorAll('[data-map-filter]'); - filterForms.forEach(form => { - if (!form.hasAttribute('hx-get')) { - form.setAttribute('hx-get', form.getAttribute('data-map-filter')); - form.setAttribute('hx-trigger', 'change, submit'); - form.setAttribute('hx-target', '#map-container'); - form.setAttribute('hx-swap', 'none'); - } - }); - - // Add map update attributes to search inputs - const searchInputs = document.querySelectorAll('[data-map-search]'); - searchInputs.forEach(input => { - if (!input.hasAttribute('hx-get')) { - input.setAttribute('hx-get', input.getAttribute('data-map-search')); - input.setAttribute('hx-trigger', 'input changed delay:500ms'); - input.setAttribute('hx-target', '#search-results'); - } - }); - } - - /** - * Handle before request event - */ - handleBeforeRequest(e) { - const element = e.target; - const requestId = this.generateRequestId(); - - // Store request information - this.activeRequests.set(requestId, { - element: element, - startTime: Date.now(), - url: e.detail.requestConfig.path - }); - - // Show loading state - this.showLoadingState(element, true); - - // Add request ID to detail for tracking - e.detail.requestId = requestId; - - // Set timeout - setTimeout(() => { - if (this.activeRequests.has(requestId)) { - this.handleTimeout({ detail: { requestId } }); - } - }, this.options.loadingTimeout); - - console.log('HTMX request started:', e.detail.requestConfig.path); - } - - /** - * Handle after request event - */ - handleAfterRequest(e) { - const requestId = e.detail.requestId; - const request = this.activeRequests.get(requestId); - - if (request) { - const duration = Date.now() - request.startTime; - console.log(`HTMX request completed in ${duration}ms:`, request.url); - - this.activeRequests.delete(requestId); - this.showLoadingState(request.element, false); - } - - if (e.detail.successful) { - this.handleSuccessfulResponse(e); - } else { - this.handleFailedResponse(e); - } - } - - /** - * Handle successful response - */ - handleSuccessfulResponse(e) { - const element = e.target; - - // Add success class temporarily - element.classList.add(this.options.successClass); - setTimeout(() => { - element.classList.remove(this.options.successClass); - }, 2000); - - // Reset retry count - this.retryCount.delete(element); - - // Check if this is a map-related request - if (this.isMapRequest(e)) { - this.updateMapFromResponse(e); - } - } - - /** - * Handle failed response - */ - handleFailedResponse(e) { - const element = e.target; - - // Add error class - element.classList.add(this.options.errorClass); - setTimeout(() => { - element.classList.remove(this.options.errorClass); - }, 5000); - - // Check if we should retry - if (this.shouldRetry(element)) { - this.scheduleRetry(element, e.detail); - } else { - this.showErrorMessage('Failed to update map data'); - } - } - - /** - * Handle response error - */ - handleResponseError(e) { - console.error('HTMX response error:', e.detail); - - const element = e.target; - const status = e.detail.xhr.status; - - let message = 'An error occurred while updating the map'; - - switch (status) { - case 400: - message = 'Invalid request parameters'; - break; - case 401: - message = 'Authentication required'; - break; - case 403: - message = 'Access denied'; - break; - case 404: - message = 'Map data not found'; - break; - case 429: - message = 'Too many requests. Please wait a moment.'; - break; - case 500: - message = 'Server error. Please try again later.'; - break; - } - - this.showErrorMessage(message); - this.showLoadingState(element, false); - } - - /** - * Handle send error - */ - handleSendError(e) { - console.error('HTMX send error:', e.detail); - this.showErrorMessage('Network error. Please check your connection.'); - this.showLoadingState(e.target, false); - } - - /** - * Handle timeout - */ - handleTimeout(e) { - console.warn('HTMX request timeout'); - - if (e.detail.requestId) { - const request = this.activeRequests.get(e.detail.requestId); - if (request) { - this.showLoadingState(request.element, false); - this.activeRequests.delete(e.detail.requestId); - } - } - - this.showErrorMessage('Request timed out. Please try again.'); - } - - /** - * Handle before swap - */ - handleBeforeSwap(e) { - // Prepare map for content update - if (this.isMapRequest(e)) { - console.log('Preparing map for content swap'); - } - } - - /** - * Handle after swap - */ - handleAfterSwap(e) { - // Re-initialize any new HTMX elements - this.enhanceExistingElements(); - - // Update maps if needed - if (this.isMapRequest(e)) { - this.reinitializeMapComponents(); - } - } - - /** - * Handle config request - */ - handleConfigRequest(e) { - const config = e.detail; - - // Add CSRF token if available - const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]'); - if (csrfToken && (config.verb === 'post' || config.verb === 'put' || config.verb === 'patch')) { - config.headers['X-CSRFToken'] = csrfToken.value; - } - - // Add map-specific headers - if (this.isMapRequest(e)) { - config.headers['X-Map-Request'] = 'true'; - - // Add current map bounds if available - if (this.options.mapInstance) { - const bounds = this.options.mapInstance.getBounds(); - if (bounds) { - config.headers['X-Map-Bounds'] = JSON.stringify({ - north: bounds.getNorth(), - south: bounds.getSouth(), - east: bounds.getEast(), - west: bounds.getWest(), - zoom: this.options.mapInstance.getZoom() - }); - } - } - } - } - - /** - * Handle map data updates - */ - handleMapDataUpdate(e) { - if (this.options.mapInstance) { - const data = e.detail; - this.options.mapInstance.updateMarkers(data); - } - } - - /** - * Handle filter changes - */ - handleFilterChange(e) { - if (this.options.filterInstance) { - const filters = e.detail; - - // Trigger HTMX request for filter update - const filterForm = document.getElementById('map-filters'); - if (filterForm && filterForm.hasAttribute('hx-get')) { - htmx.trigger(filterForm, 'change'); - } - } - } - - /** - * Handle search results - */ - handleSearchResults(e) { - const results = e.detail; - - // Update map with search results if applicable - if (results.locations && this.options.mapInstance) { - this.options.mapInstance.updateMarkers({ locations: results.locations }); - } - } - - /** - * Show/hide loading state - */ - showLoadingState(element, show) { - if (show) { - element.classList.add(this.options.loadingClass); - this.loadingElements.add(element); - - // Show loading indicators - const indicators = element.querySelectorAll('.htmx-indicator'); - indicators.forEach(indicator => { - indicator.style.display = 'block'; - }); - - // Disable form elements - const inputs = element.querySelectorAll('input, button, select'); - inputs.forEach(input => { - input.disabled = true; - }); - } else { - element.classList.remove(this.options.loadingClass); - this.loadingElements.delete(element); - - // Hide loading indicators - const indicators = element.querySelectorAll('.htmx-indicator'); - indicators.forEach(indicator => { - indicator.style.display = 'none'; - }); - - // Re-enable form elements - const inputs = element.querySelectorAll('input, button, select'); - inputs.forEach(input => { - input.disabled = false; - }); - } - } - - /** - * Check if request is map-related - */ - isMapRequest(e) { - const element = e.target; - const url = e.detail.requestConfig ? e.detail.requestConfig.path : ''; - - return element.hasAttribute('data-map-filter') || - element.hasAttribute('data-map-search') || - element.closest('[data-map-target]') || - url.includes('/api/map/') || - url.includes('/maps/'); - } - - /** - * Update map from HTMX response - */ - updateMapFromResponse(e) { - if (!this.options.mapInstance) return; - - try { - // Try to extract map data from response - const responseText = e.detail.xhr.responseText; - - // If response is JSON, update map directly - try { - const data = JSON.parse(responseText); - if (data.status === 'success' && data.data) { - this.options.mapInstance.updateMarkers(data.data); - } - } catch (jsonError) { - // If not JSON, look for data attributes in HTML - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = responseText; - - const mapData = tempDiv.querySelector('[data-map-data]'); - if (mapData) { - const data = JSON.parse(mapData.getAttribute('data-map-data')); - this.options.mapInstance.updateMarkers(data); - } - } - } catch (error) { - console.error('Failed to update map from response:', error); - } - } - - /** - * Check if element should be retried - */ - shouldRetry(element) { - const retryCount = this.retryCount.get(element) || 0; - return retryCount < this.options.retryAttempts; - } - - /** - * Schedule retry for failed request - */ - scheduleRetry(element, detail) { - const retryCount = (this.retryCount.get(element) || 0) + 1; - this.retryCount.set(element, retryCount); - - const delay = this.options.retryDelay * Math.pow(2, retryCount - 1); // Exponential backoff - - setTimeout(() => { - console.log(`Retrying HTMX request (attempt ${retryCount})`); - htmx.trigger(element, 'retry'); - }, delay); - } - - /** - * Show error message to user - */ - showErrorMessage(message) { - // Create or update error message element - let errorEl = document.getElementById('htmx-error-message'); - - if (!errorEl) { - errorEl = document.createElement('div'); - errorEl.id = 'htmx-error-message'; - errorEl.className = 'htmx-error-message'; - - // Insert at top of page - document.body.insertBefore(errorEl, document.body.firstChild); - } - - errorEl.innerHTML = ` -
- - ${message} - -
- `; - - errorEl.style.display = 'block'; - - // Auto-hide after 10 seconds - setTimeout(() => { - if (errorEl.parentNode) { - errorEl.remove(); - } - }, 10000); - } - - /** - * Reinitialize map components after content swap - */ - reinitializeMapComponents() { - // Reinitialize filter components - if (this.options.filterInstance) { - this.options.filterInstance.init(); - } - - // Reinitialize any new map containers - const newMapContainers = document.querySelectorAll('[data-map="auto"]:not([data-initialized])'); - newMapContainers.forEach(container => { - container.setAttribute('data-initialized', 'true'); - // Initialize new map instance if needed - }); - } - - /** - * Generate unique request ID - */ - generateRequestId() { - return `htmx-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - } - - /** - * Connect to map instance - */ - connectToMap(mapInstance) { - this.options.mapInstance = mapInstance; - } - - /** - * Connect to filter instance - */ - connectToFilter(filterInstance) { - this.options.filterInstance = filterInstance; - } - - /** - * Get active request count - */ - getActiveRequestCount() { - return this.activeRequests.size; - } - - /** - * Cancel all active requests - */ - cancelAllRequests() { - this.activeRequests.forEach((request, id) => { - this.showLoadingState(request.element, false); - }); - this.activeRequests.clear(); - } - - /** - * Get loading elements - */ - getLoadingElements() { - return Array.from(this.loadingElements); - } -} - -// Auto-initialize HTMX integration -document.addEventListener('DOMContentLoaded', function() { - window.htmxMapIntegration = new HTMXMapIntegration(); - - // Connect to existing instances - if (window.thrillwikiMap) { - window.htmxMapIntegration.connectToMap(window.thrillwikiMap); - } - - if (window.mapFilters) { - window.htmxMapIntegration.connectToFilter(window.mapFilters); - } -}); - -// Add styles for HTMX integration -document.addEventListener('DOMContentLoaded', function() { - if (document.getElementById('htmx-map-styles')) return; - - const styles = ` - - `; - - document.head.insertAdjacentHTML('beforeend', styles); -}); - -// Export for use in other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = HTMXMapIntegration; -} else { - window.HTMXMapIntegration = HTMXMapIntegration; -} \ No newline at end of file diff --git a/static/js/backup/location-autocomplete.js b/static/js/backup/location-autocomplete.js deleted file mode 100644 index 7bc90cd6..00000000 --- a/static/js/backup/location-autocomplete.js +++ /dev/null @@ -1,81 +0,0 @@ -function locationAutocomplete(field, filterParks = false) { - return { - query: '', - suggestions: [], - fetchSuggestions() { - let url; - const params = new URLSearchParams({ - q: this.query, - filter_parks: filterParks - }); - - switch (field) { - case 'country': - url = '/parks/ajax/countries/'; - break; - case 'region': - url = '/parks/ajax/regions/'; - // Add country parameter if we're fetching regions - const countryInput = document.getElementById(filterParks ? 'country' : 'id_country_name'); - if (countryInput && countryInput.value) { - params.append('country', countryInput.value); - } - break; - case 'city': - url = '/parks/ajax/cities/'; - // Add country and region parameters if we're fetching cities - const regionInput = document.getElementById(filterParks ? 'region' : 'id_region_name'); - const cityCountryInput = document.getElementById(filterParks ? 'country' : 'id_country_name'); - if (regionInput && regionInput.value && cityCountryInput && cityCountryInput.value) { - params.append('country', cityCountryInput.value); - params.append('region', regionInput.value); - } - break; - } - - if (url) { - fetch(`${url}?${params}`) - .then(response => response.json()) - .then(data => { - this.suggestions = data; - }); - } - }, - selectSuggestion(suggestion) { - this.query = suggestion.name; - this.suggestions = []; - - // If this is a form field (not filter), update hidden fields - if (!filterParks) { - const hiddenField = document.getElementById(`id_${field}`); - if (hiddenField) { - hiddenField.value = suggestion.id; - } - - // Clear dependent fields when parent field changes - if (field === 'country') { - const regionInput = document.getElementById('id_region_name'); - const cityInput = document.getElementById('id_city_name'); - const regionHidden = document.getElementById('id_region'); - const cityHidden = document.getElementById('id_city'); - - if (regionInput) regionInput.value = ''; - if (cityInput) cityInput.value = ''; - if (regionHidden) regionHidden.value = ''; - if (cityHidden) cityHidden.value = ''; - } else if (field === 'region') { - const cityInput = document.getElementById('id_city_name'); - const cityHidden = document.getElementById('id_city'); - - if (cityInput) cityInput.value = ''; - if (cityHidden) cityHidden.value = ''; - } - } - - // Trigger form submission for filters - if (filterParks) { - htmx.trigger('#park-filters', 'change'); - } - } - }; -} diff --git a/static/js/backup/location-search.js b/static/js/backup/location-search.js deleted file mode 100644 index 610bb96b..00000000 --- a/static/js/backup/location-search.js +++ /dev/null @@ -1,54 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - const useLocationBtn = document.getElementById('use-my-location'); - const latInput = document.getElementById('lat-input'); - const lngInput = document.getElementById('lng-input'); - const locationInput = document.getElementById('location-input'); - - if (useLocationBtn && 'geolocation' in navigator) { - useLocationBtn.addEventListener('click', function() { - this.textContent = '📍 Getting location...'; - this.disabled = true; - - navigator.geolocation.getCurrentPosition( - function(position) { - latInput.value = position.coords.latitude; - lngInput.value = position.coords.longitude; - locationInput.value = `${position.coords.latitude.toFixed(6)}, ${position.coords.longitude.toFixed(6)}`; - useLocationBtn.textContent = '✅ Location set'; - setTimeout(() => { - useLocationBtn.textContent = '📍 Use My Location'; - useLocationBtn.disabled = false; - }, 2000); - }, - function(error) { - useLocationBtn.textContent = '❌ Location failed'; - console.error('Geolocation error:', error); - setTimeout(() => { - useLocationBtn.textContent = '📍 Use My Location'; - useLocationBtn.disabled = false; - }, 2000); - } - ); - }); - } else if (useLocationBtn) { - useLocationBtn.style.display = 'none'; - } - - // Autocomplete for location search - if (locationInput) { - locationInput.addEventListener('input', function() { - const query = this.value; - if (query.length < 3) { - return; - } - - fetch(`/search/location/suggestions/?q=${query}`) - .then(response => response.json()) - .then(data => { - // This is a simplified example. A more robust solution would use a library like Awesomplete or build a custom dropdown. - console.log('Suggestions:', data.suggestions); - }) - .catch(error => console.error('Error fetching suggestions:', error)); - }); - } -}); \ No newline at end of file diff --git a/static/js/backup/main.js b/static/js/backup/main.js deleted file mode 100644 index 088434a3..00000000 --- a/static/js/backup/main.js +++ /dev/null @@ -1,121 +0,0 @@ -// Theme handling -document.addEventListener('DOMContentLoaded', () => { - const themeToggle = document.getElementById('theme-toggle'); - const html = document.documentElement; - - // Initialize toggle state based on current theme - if (themeToggle) { - themeToggle.checked = html.classList.contains('dark'); - - // Handle toggle changes - themeToggle.addEventListener('change', function() { - const isDark = this.checked; - html.classList.toggle('dark', isDark); - localStorage.setItem('theme', isDark ? 'dark' : 'light'); - }); - - // Listen for system theme changes - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - mediaQuery.addEventListener('change', (e) => { - if (!localStorage.getItem('theme')) { - const isDark = e.matches; - html.classList.toggle('dark', isDark); - themeToggle.checked = isDark; - } - }); - } -}); - -// Handle search form submission -document.addEventListener('submit', (e) => { - if (e.target.matches('form[action*="search"]')) { - const searchInput = e.target.querySelector('input[name="q"]'); - if (!searchInput.value.trim()) { - e.preventDefault(); - } - } -}); - -// Mobile menu toggle with transitions -document.addEventListener('DOMContentLoaded', () => { - const mobileMenuBtn = document.getElementById('mobileMenuBtn'); - const mobileMenu = document.getElementById('mobileMenu'); - - if (mobileMenuBtn && mobileMenu) { - let isMenuOpen = false; - - const toggleMenu = () => { - isMenuOpen = !isMenuOpen; - mobileMenu.classList.toggle('show', isMenuOpen); - mobileMenuBtn.setAttribute('aria-expanded', isMenuOpen.toString()); - - // Update icon - const icon = mobileMenuBtn.querySelector('i'); - if (icon) { - icon.classList.remove(isMenuOpen ? 'fa-bars' : 'fa-times'); - icon.classList.add(isMenuOpen ? 'fa-times' : 'fa-bars'); - } - }; - - mobileMenuBtn.addEventListener('click', toggleMenu); - - // Close menu when clicking outside - document.addEventListener('click', (e) => { - if (isMenuOpen && !mobileMenu.contains(e.target) && !mobileMenuBtn.contains(e.target)) { - toggleMenu(); - } - }); - - // Close menu when pressing escape - document.addEventListener('keydown', (e) => { - if (isMenuOpen && e.key === 'Escape') { - toggleMenu(); - } - }); - - // Handle viewport changes - const mediaQuery = window.matchMedia('(min-width: 1024px)'); - mediaQuery.addEventListener('change', (e) => { - if (e.matches && isMenuOpen) { - toggleMenu(); - } - }); - } -}); - -// User dropdown functionality is handled by Alpine.js in the template -// No additional JavaScript needed for dropdown functionality - -// Handle flash messages -document.addEventListener('DOMContentLoaded', () => { - const alerts = document.querySelectorAll('.alert'); - alerts.forEach(alert => { - setTimeout(() => { - alert.style.opacity = '0'; - setTimeout(() => alert.remove(), 300); - }, 5000); - }); -}); - -// Initialize tooltips -document.addEventListener('DOMContentLoaded', () => { - const tooltips = document.querySelectorAll('[data-tooltip]'); - tooltips.forEach(tooltip => { - tooltip.addEventListener('mouseenter', (e) => { - const text = e.target.getAttribute('data-tooltip'); - const tooltipEl = document.createElement('div'); - tooltipEl.className = 'absolute z-50 px-2 py-1 text-sm text-white bg-gray-900 rounded tooltip'; - tooltipEl.textContent = text; - document.body.appendChild(tooltipEl); - - const rect = e.target.getBoundingClientRect(); - tooltipEl.style.top = rect.bottom + 5 + 'px'; - tooltipEl.style.left = rect.left + (rect.width - tooltipEl.offsetWidth) / 2 + 'px'; - }); - - tooltip.addEventListener('mouseleave', () => { - const tooltips = document.querySelectorAll('.tooltip'); - tooltips.forEach(t => t.remove()); - }); - }); -}); diff --git a/static/js/backup/map-filters.js b/static/js/backup/map-filters.js deleted file mode 100644 index 024fe7e3..00000000 --- a/static/js/backup/map-filters.js +++ /dev/null @@ -1,573 +0,0 @@ -/** - * ThrillWiki Map Filters - Location Filtering Component - * - * This module handles filter panel interactions and updates maps via HTMX - * Supports location type filtering, geographic filtering, and real-time search - */ - -class MapFilters { - constructor(formId, options = {}) { - this.formId = formId; - this.options = { - autoSubmit: true, - searchDelay: 500, - enableLocalStorage: true, - storageKey: 'thrillwiki_map_filters', - mapInstance: null, - htmxTarget: '#map-container', - htmxUrl: null, - ...options - }; - - this.form = null; - this.searchTimeout = null; - this.currentFilters = {}; - this.filterChips = []; - - this.init(); - } - - /** - * Initialize the filter component - */ - init() { - this.form = document.getElementById(this.formId); - if (!this.form) { - console.error(`Filter form with ID '${this.formId}' not found`); - return; - } - - this.setupFilterChips(); - this.bindEvents(); - this.loadSavedFilters(); - this.initializeFilters(); - } - - /** - * Setup filter chip interactions - */ - setupFilterChips() { - this.filterChips = this.form.querySelectorAll('.filter-chip, .filter-pill'); - - this.filterChips.forEach(chip => { - const checkbox = chip.querySelector('input[type="checkbox"]'); - - if (checkbox) { - // Set initial state - this.updateChipState(chip, checkbox.checked); - - // Bind click handler - chip.addEventListener('click', (e) => { - e.preventDefault(); - this.toggleChip(chip, checkbox); - }); - } - }); - } - - /** - * Toggle filter chip state - */ - toggleChip(chip, checkbox) { - checkbox.checked = !checkbox.checked; - this.updateChipState(chip, checkbox.checked); - - // Trigger change event - this.handleFilterChange(); - } - - /** - * Update visual state of filter chip - */ - updateChipState(chip, isActive) { - if (isActive) { - chip.classList.add('active'); - chip.classList.remove('inactive'); - } else { - chip.classList.remove('active'); - chip.classList.add('inactive'); - } - } - - /** - * Bind event handlers - */ - bindEvents() { - // Form submission - this.form.addEventListener('submit', (e) => { - if (this.options.autoSubmit) { - e.preventDefault(); - this.submitFilters(); - } - }); - - // Input changes (excluding search) - this.form.addEventListener('change', (e) => { - if (e.target.name !== 'q' && !e.target.closest('.no-auto-submit')) { - this.handleFilterChange(); - } - }); - - // Search input with debouncing - const searchInput = this.form.querySelector('input[name="q"]'); - if (searchInput) { - searchInput.addEventListener('input', () => { - this.handleSearchInput(); - }); - } - - // Range inputs - const rangeInputs = this.form.querySelectorAll('input[type="range"]'); - rangeInputs.forEach(input => { - input.addEventListener('input', (e) => { - this.updateRangeDisplay(e.target); - }); - }); - - // Clear filters button - const clearButton = this.form.querySelector('[data-action="clear-filters"]'); - if (clearButton) { - clearButton.addEventListener('click', (e) => { - e.preventDefault(); - this.clearAllFilters(); - }); - } - - // HTMX events - if (typeof htmx !== 'undefined') { - this.form.addEventListener('htmx:beforeRequest', () => { - this.showLoadingState(true); - }); - - this.form.addEventListener('htmx:afterRequest', (e) => { - this.showLoadingState(false); - if (e.detail.successful) { - this.onFiltersApplied(this.getCurrentFilters()); - } - }); - - this.form.addEventListener('htmx:responseError', (e) => { - this.showLoadingState(false); - this.handleError(e.detail); - }); - } - } - - /** - * Handle search input with debouncing - */ - handleSearchInput() { - clearTimeout(this.searchTimeout); - this.searchTimeout = setTimeout(() => { - this.handleFilterChange(); - }, this.options.searchDelay); - } - - /** - * Handle filter changes - */ - handleFilterChange() { - const filters = this.getCurrentFilters(); - this.currentFilters = filters; - - if (this.options.autoSubmit) { - this.submitFilters(); - } - - // Save filters to localStorage - if (this.options.enableLocalStorage) { - this.saveFilters(filters); - } - - // Update map if connected - if (this.options.mapInstance && this.options.mapInstance.updateFilters) { - this.options.mapInstance.updateFilters(filters); - } - - // Trigger custom event - this.triggerFilterEvent('filterChange', filters); - } - - /** - * Submit filters via HTMX or form submission - */ - submitFilters() { - if (typeof htmx !== 'undefined' && this.options.htmxUrl) { - // Use HTMX - const formData = new FormData(this.form); - const params = new URLSearchParams(formData); - - htmx.ajax('GET', `${this.options.htmxUrl}?${params}`, { - target: this.options.htmxTarget, - swap: 'none' - }); - } else { - // Regular form submission - this.form.submit(); - } - } - - /** - * Get current filter values - */ - getCurrentFilters() { - const formData = new FormData(this.form); - const filters = {}; - - for (let [key, value] of formData.entries()) { - if (value.trim() === '') continue; - - if (filters[key]) { - if (Array.isArray(filters[key])) { - filters[key].push(value); - } else { - filters[key] = [filters[key], value]; - } - } else { - filters[key] = value; - } - } - - return filters; - } - - /** - * Set filter values - */ - setFilters(filters) { - Object.entries(filters).forEach(([key, value]) => { - const elements = this.form.querySelectorAll(`[name="${key}"]`); - - elements.forEach(element => { - if (element.type === 'checkbox' || element.type === 'radio') { - if (Array.isArray(value)) { - element.checked = value.includes(element.value); - } else { - element.checked = element.value === value; - } - - // Update chip state if applicable - const chip = element.closest('.filter-chip, .filter-pill'); - if (chip) { - this.updateChipState(chip, element.checked); - } - } else { - element.value = Array.isArray(value) ? value[0] : value; - - // Update range display if applicable - if (element.type === 'range') { - this.updateRangeDisplay(element); - } - } - }); - }); - - this.currentFilters = filters; - } - - /** - * Clear all filters - */ - clearAllFilters() { - // Reset form - this.form.reset(); - - // Update all chip states - this.filterChips.forEach(chip => { - const checkbox = chip.querySelector('input[type="checkbox"]'); - if (checkbox) { - this.updateChipState(chip, false); - } - }); - - // Update range displays - const rangeInputs = this.form.querySelectorAll('input[type="range"]'); - rangeInputs.forEach(input => { - this.updateRangeDisplay(input); - }); - - // Clear saved filters - if (this.options.enableLocalStorage) { - localStorage.removeItem(this.options.storageKey); - } - - // Submit cleared filters - this.handleFilterChange(); - } - - /** - * Update range input display - */ - updateRangeDisplay(rangeInput) { - const valueDisplay = document.getElementById(`${rangeInput.id}-value`) || - document.getElementById(`${rangeInput.name}-value`); - - if (valueDisplay) { - valueDisplay.textContent = rangeInput.value; - } - } - - /** - * Load saved filters from localStorage - */ - loadSavedFilters() { - if (!this.options.enableLocalStorage) return; - - try { - const saved = localStorage.getItem(this.options.storageKey); - if (saved) { - const filters = JSON.parse(saved); - this.setFilters(filters); - } - } catch (error) { - console.warn('Failed to load saved filters:', error); - } - } - - /** - * Save filters to localStorage - */ - saveFilters(filters) { - if (!this.options.enableLocalStorage) return; - - try { - localStorage.setItem(this.options.storageKey, JSON.stringify(filters)); - } catch (error) { - console.warn('Failed to save filters:', error); - } - } - - /** - * Initialize filters from URL parameters or defaults - */ - initializeFilters() { - // Check for URL parameters - const urlParams = new URLSearchParams(window.location.search); - const urlFilters = {}; - - for (let [key, value] of urlParams.entries()) { - if (urlFilters[key]) { - if (Array.isArray(urlFilters[key])) { - urlFilters[key].push(value); - } else { - urlFilters[key] = [urlFilters[key], value]; - } - } else { - urlFilters[key] = value; - } - } - - if (Object.keys(urlFilters).length > 0) { - this.setFilters(urlFilters); - } - - // Emit initial filter state - this.triggerFilterEvent('filterInit', this.getCurrentFilters()); - } - - /** - * Show/hide loading state - */ - showLoadingState(show) { - const loadingIndicators = this.form.querySelectorAll('.filter-loading, .htmx-indicator'); - loadingIndicators.forEach(indicator => { - indicator.style.display = show ? 'block' : 'none'; - }); - - // Disable form during loading - const inputs = this.form.querySelectorAll('input, select, button'); - inputs.forEach(input => { - input.disabled = show; - }); - } - - /** - * Handle errors - */ - handleError(detail) { - console.error('Filter request failed:', detail); - - // Show user-friendly error message - this.showMessage('Failed to apply filters. Please try again.', 'error'); - } - - /** - * Show message to user - */ - showMessage(message, type = 'info') { - // Create or update message element - let messageEl = this.form.querySelector('.filter-message'); - if (!messageEl) { - messageEl = document.createElement('div'); - messageEl.className = 'filter-message'; - this.form.insertBefore(messageEl, this.form.firstChild); - } - - messageEl.textContent = message; - messageEl.className = `filter-message filter-message-${type}`; - - // Auto-hide after delay - setTimeout(() => { - if (messageEl.parentNode) { - messageEl.remove(); - } - }, 5000); - } - - /** - * Callback for when filters are successfully applied - */ - onFiltersApplied(filters) { - this.triggerFilterEvent('filterApplied', filters); - } - - /** - * Trigger custom events - */ - triggerFilterEvent(eventName, data) { - const event = new CustomEvent(eventName, { - detail: data - }); - this.form.dispatchEvent(event); - } - - /** - * Connect to a map instance - */ - connectToMap(mapInstance) { - this.options.mapInstance = mapInstance; - - // Listen to map events - if (mapInstance.on) { - mapInstance.on('boundsChange', (bounds) => { - // Could update location-based filters here - }); - } - } - - /** - * Export current filters as URL parameters - */ - getFilterUrl(baseUrl = window.location.pathname) { - const filters = this.getCurrentFilters(); - const params = new URLSearchParams(); - - Object.entries(filters).forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach(v => params.append(key, v)); - } else { - params.append(key, value); - } - }); - - return params.toString() ? `${baseUrl}?${params}` : baseUrl; - } - - /** - * Update URL with current filters (without page reload) - */ - updateUrl() { - const url = this.getFilterUrl(); - if (window.history && window.history.pushState) { - window.history.pushState(null, '', url); - } - } - - /** - * Get filter summary for display - */ - getFilterSummary() { - const filters = this.getCurrentFilters(); - const summary = []; - - // Location types - if (filters.types) { - const types = Array.isArray(filters.types) ? filters.types : [filters.types]; - summary.push(`Types: ${types.join(', ')}`); - } - - // Geographic filters - if (filters.country) summary.push(`Country: ${filters.country}`); - if (filters.state) summary.push(`State: ${filters.state}`); - if (filters.city) summary.push(`City: ${filters.city}`); - - // Search query - if (filters.q) summary.push(`Search: "${filters.q}"`); - - // Radius - if (filters.radius) summary.push(`Within ${filters.radius} miles`); - - return summary.length > 0 ? summary.join(' • ') : 'No filters applied'; - } - - /** - * Reset to default filters - */ - resetToDefaults() { - const defaults = { - types: ['park'], - cluster: 'true' - }; - - this.setFilters(defaults); - this.handleFilterChange(); - } - - /** - * Destroy the filter component - */ - destroy() { - if (this.searchTimeout) { - clearTimeout(this.searchTimeout); - } - - // Remove event listeners would go here if we stored references - // For now, rely on garbage collection - } -} - -// Auto-initialize filter forms -document.addEventListener('DOMContentLoaded', function() { - // Initialize map filters form - const mapFiltersForm = document.getElementById('map-filters'); - if (mapFiltersForm) { - window.mapFilters = new MapFilters('map-filters', { - htmxUrl: mapFiltersForm.getAttribute('hx-get'), - htmxTarget: mapFiltersForm.getAttribute('hx-target') || '#map-container' - }); - - // Connect to map instance if available - if (window.thrillwikiMap) { - window.mapFilters.connectToMap(window.thrillwikiMap); - } - } - - // Initialize other filter forms with data attributes - const filterForms = document.querySelectorAll('[data-filter-form]'); - filterForms.forEach(form => { - const options = {}; - - // Parse data attributes - Object.keys(form.dataset).forEach(key => { - if (key.startsWith('filter')) { - const optionKey = key.replace('filter', '').toLowerCase(); - let value = form.dataset[key]; - - // Parse boolean values - if (value === 'true') value = true; - else if (value === 'false') value = false; - - options[optionKey] = value; - } - }); - - new MapFilters(form.id, options); - }); -}); - -// Export for use in other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = MapFilters; -} else { - window.MapFilters = MapFilters; -} \ No newline at end of file diff --git a/static/js/backup/map-integration.js b/static/js/backup/map-integration.js deleted file mode 100644 index 23c8b318..00000000 --- a/static/js/backup/map-integration.js +++ /dev/null @@ -1,553 +0,0 @@ -/** - * ThrillWiki Map Integration - Master Integration Script - * - * This module coordinates all map components, handles initialization order, - * manages component communication, and provides a unified API - */ - -class MapIntegration { - constructor(options = {}) { - this.options = { - autoInit: true, - enableLogging: true, - enablePerformanceMonitoring: true, - initTimeout: 10000, - retryAttempts: 3, - components: { - maps: true, - filters: true, - roadtrip: true, - geolocation: true, - markers: true, - htmx: true, - mobileTouch: true, - darkMode: true - }, - ...options - }; - - this.components = {}; - this.initOrder = [ - 'darkMode', - 'mobileTouch', - 'maps', - 'markers', - 'filters', - 'geolocation', - 'htmx', - 'roadtrip' - ]; - this.initialized = false; - this.initStartTime = null; - this.errors = []; - - if (this.options.autoInit) { - this.init(); - } - } - - /** - * Initialize all map components - */ - async init() { - this.initStartTime = performance.now(); - this.log('Starting map integration initialization...'); - - try { - // Wait for DOM to be ready - await this.waitForDOM(); - - // Initialize components in order - await this.initializeComponents(); - - // Connect components - this.connectComponents(); - - // Setup global event handlers - this.setupGlobalHandlers(); - - // Verify integration - this.verifyIntegration(); - - this.initialized = true; - this.logPerformance(); - this.log('Map integration initialized successfully'); - - // Emit ready event - this.emitEvent('mapIntegrationReady', { - components: Object.keys(this.components), - initTime: performance.now() - this.initStartTime - }); - - } catch (error) { - this.handleInitError(error); - } - } - - /** - * Wait for DOM to be ready - */ - waitForDOM() { - return new Promise((resolve) => { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', resolve); - } else { - resolve(); - } - }); - } - - /** - * Initialize components in the correct order - */ - async initializeComponents() { - for (const componentName of this.initOrder) { - if (!this.options.components[componentName]) { - this.log(`Skipping ${componentName} (disabled)`); - continue; - } - - try { - await this.initializeComponent(componentName); - this.log(`✓ ${componentName} initialized`); - } catch (error) { - this.error(`✗ Failed to initialize ${componentName}:`, error); - this.errors.push({ component: componentName, error }); - } - } - } - - /** - * Initialize individual component - */ - async initializeComponent(componentName) { - switch (componentName) { - case 'darkMode': - if (window.DarkModeMaps) { - this.components.darkMode = window.darkModeMaps || new DarkModeMaps(); - } - break; - - case 'mobileTouch': - if (window.MobileTouchSupport) { - this.components.mobileTouch = window.mobileTouchSupport || new MobileTouchSupport(); - } - break; - - case 'maps': - // Look for existing map instances or create new ones - if (window.thrillwikiMap) { - this.components.maps = window.thrillwikiMap; - } else if (window.ThrillWikiMap) { - const mapContainer = document.getElementById('map-container'); - if (mapContainer) { - this.components.maps = new ThrillWikiMap('map-container'); - window.thrillwikiMap = this.components.maps; - } - } - break; - - case 'markers': - if (window.MapMarkers && this.components.maps) { - this.components.markers = window.mapMarkers || new MapMarkers(this.components.maps); - } - break; - - case 'filters': - if (window.MapFilters) { - const filterForm = document.getElementById('map-filters'); - if (filterForm) { - this.components.filters = window.mapFilters || new MapFilters('map-filters'); - } - } - break; - - case 'geolocation': - if (window.UserLocation) { - this.components.geolocation = window.userLocation || new UserLocation(); - } - break; - - case 'htmx': - if (window.HTMXMapIntegration && typeof htmx !== 'undefined') { - this.components.htmx = window.htmxMapIntegration || new HTMXMapIntegration(); - } - break; - - case 'roadtrip': - if (window.RoadTripPlanner) { - const roadtripContainer = document.getElementById('roadtrip-planner'); - if (roadtripContainer) { - this.components.roadtrip = window.roadTripPlanner || new RoadTripPlanner('roadtrip-planner'); - } - } - break; - } - } - - /** - * Connect components together - */ - connectComponents() { - this.log('Connecting components...'); - - // Connect maps to other components - if (this.components.maps) { - // Connect to dark mode - if (this.components.darkMode) { - this.components.darkMode.registerMapInstance(this.components.maps); - } - - // Connect to mobile touch - if (this.components.mobileTouch) { - this.components.mobileTouch.registerMapInstance(this.components.maps); - } - - // Connect to geolocation - if (this.components.geolocation) { - this.components.geolocation.connectToMap(this.components.maps); - } - - // Connect to road trip planner - if (this.components.roadtrip) { - this.components.roadtrip.connectToMap(this.components.maps); - } - } - - // Connect filters to other components - if (this.components.filters) { - // Connect to maps - if (this.components.maps) { - this.components.filters.connectToMap(this.components.maps); - } - - // Connect to HTMX - if (this.components.htmx) { - this.components.htmx.connectToFilter(this.components.filters); - } - } - - // Connect HTMX to maps - if (this.components.htmx && this.components.maps) { - this.components.htmx.connectToMap(this.components.maps); - } - } - - /** - * Setup global event handlers - */ - setupGlobalHandlers() { - // Handle global map events - document.addEventListener('mapDataUpdate', (e) => { - this.handleMapDataUpdate(e.detail); - }); - - // Handle filter changes - document.addEventListener('filterChange', (e) => { - this.handleFilterChange(e.detail); - }); - - // Handle theme changes - document.addEventListener('themeChanged', (e) => { - this.handleThemeChange(e.detail); - }); - - // Handle orientation changes - document.addEventListener('orientationChanged', (e) => { - this.handleOrientationChange(e.detail); - }); - - // Handle visibility changes for performance - document.addEventListener('visibilitychange', () => { - this.handleVisibilityChange(); - }); - - // Handle errors - window.addEventListener('error', (e) => { - if (this.isMapRelatedError(e)) { - this.handleGlobalError(e); - } - }); - } - - /** - * Handle map data updates - */ - handleMapDataUpdate(data) { - if (this.components.maps) { - this.components.maps.updateMarkers(data); - } - } - - /** - * Handle filter changes - */ - handleFilterChange(filters) { - if (this.components.maps) { - this.components.maps.updateFilters(filters); - } - } - - /** - * Handle theme changes - */ - handleThemeChange(themeData) { - // All components should already be listening for this - // Just log for monitoring - this.log(`Theme changed to ${themeData.newTheme}`); - } - - /** - * Handle orientation changes - */ - handleOrientationChange(orientationData) { - // Invalidate map sizes after orientation change - if (this.components.maps) { - setTimeout(() => { - this.components.maps.invalidateSize(); - }, 300); - } - } - - /** - * Handle visibility changes - */ - handleVisibilityChange() { - const isHidden = document.hidden; - - // Pause/resume location watching - if (this.components.geolocation) { - if (isHidden) { - this.components.geolocation.stopWatching(); - } else if (this.components.geolocation.options.watchPosition) { - this.components.geolocation.startWatching(); - } - } - } - - /** - * Check if error is map-related - */ - isMapRelatedError(error) { - const mapKeywords = ['leaflet', 'map', 'marker', 'tile', 'geolocation', 'htmx']; - const errorMessage = error.message ? error.message.toLowerCase() : ''; - const errorStack = error.error && error.error.stack ? error.error.stack.toLowerCase() : ''; - - return mapKeywords.some(keyword => - errorMessage.includes(keyword) || errorStack.includes(keyword) - ); - } - - /** - * Handle global errors - */ - handleGlobalError(error) { - this.error('Global map error:', error); - this.errors.push({ type: 'global', error }); - - // Emit error event - this.emitEvent('mapError', { error, timestamp: Date.now() }); - } - - /** - * Verify integration is working - */ - verifyIntegration() { - const issues = []; - - // Check required components - if (this.options.components.maps && !this.components.maps) { - issues.push('Maps component not initialized'); - } - - // Check component connections - if (this.components.maps && this.components.darkMode) { - if (!this.components.darkMode.mapInstances.has(this.components.maps)) { - issues.push('Maps not connected to dark mode'); - } - } - - // Check DOM elements - const mapContainer = document.getElementById('map-container'); - if (this.components.maps && !mapContainer) { - issues.push('Map container not found in DOM'); - } - - if (issues.length > 0) { - this.warn('Integration issues found:', issues); - } - - return issues.length === 0; - } - - /** - * Handle initialization errors - */ - handleInitError(error) { - this.error('Map integration initialization failed:', error); - - // Emit error event - this.emitEvent('mapIntegrationError', { - error, - errors: this.errors, - timestamp: Date.now() - }); - - // Try to initialize what we can - this.attemptPartialInit(); - } - - /** - * Attempt partial initialization - */ - attemptPartialInit() { - this.log('Attempting partial initialization...'); - - // Try to initialize at least the core map - if (!this.components.maps && window.ThrillWikiMap) { - try { - const mapContainer = document.getElementById('map-container'); - if (mapContainer) { - this.components.maps = new ThrillWikiMap('map-container'); - this.log('✓ Core map initialized in fallback mode'); - } - } catch (error) { - this.error('✗ Fallback map initialization failed:', error); - } - } - } - - /** - * Get component by name - */ - getComponent(name) { - return this.components[name] || null; - } - - /** - * Get all components - */ - getAllComponents() { - return { ...this.components }; - } - - /** - * Check if integration is ready - */ - isReady() { - return this.initialized; - } - - /** - * Get initialization status - */ - getStatus() { - return { - initialized: this.initialized, - components: Object.keys(this.components), - errors: this.errors, - initTime: this.initStartTime ? performance.now() - this.initStartTime : null - }; - } - - /** - * Emit custom event - */ - emitEvent(eventName, detail) { - const event = new CustomEvent(eventName, { detail }); - document.dispatchEvent(event); - } - - /** - * Log performance metrics - */ - logPerformance() { - if (!this.options.enablePerformanceMonitoring) return; - - const initTime = performance.now() - this.initStartTime; - const componentCount = Object.keys(this.components).length; - - this.log(`Performance: ${initTime.toFixed(2)}ms to initialize ${componentCount} components`); - - // Send to analytics if available - if (typeof gtag !== 'undefined') { - gtag('event', 'map_integration_performance', { - event_category: 'performance', - value: Math.round(initTime), - custom_map: { - component_count: componentCount, - errors: this.errors.length - } - }); - } - } - - /** - * Logging methods - */ - log(message, ...args) { - if (this.options.enableLogging) { - console.log(`[MapIntegration] ${message}`, ...args); - } - } - - warn(message, ...args) { - if (this.options.enableLogging) { - console.warn(`[MapIntegration] ${message}`, ...args); - } - } - - error(message, ...args) { - if (this.options.enableLogging) { - console.error(`[MapIntegration] ${message}`, ...args); - } - } - - /** - * Destroy integration - */ - destroy() { - // Destroy all components - Object.values(this.components).forEach(component => { - if (component && typeof component.destroy === 'function') { - try { - component.destroy(); - } catch (error) { - this.error('Error destroying component:', error); - } - } - }); - - this.components = {}; - this.initialized = false; - this.log('Map integration destroyed'); - } -} - -// Auto-initialize map integration -let mapIntegration; - -document.addEventListener('DOMContentLoaded', function() { - // Only initialize if we have map-related elements - const hasMapElements = document.querySelector('#map-container, .map-container, [data-map], [data-roadtrip]'); - - if (hasMapElements) { - mapIntegration = new MapIntegration(); - window.mapIntegration = mapIntegration; - } -}); - -// Global API for external access -window.ThrillWikiMaps = { - getIntegration: () => mapIntegration, - isReady: () => mapIntegration && mapIntegration.isReady(), - getComponent: (name) => mapIntegration ? mapIntegration.getComponent(name) : null, - getStatus: () => mapIntegration ? mapIntegration.getStatus() : { initialized: false } -}; - -// Export for module systems -if (typeof module !== 'undefined' && module.exports) { - module.exports = MapIntegration; -} else { - window.MapIntegration = MapIntegration; -} \ No newline at end of file diff --git a/static/js/backup/map-markers.js b/static/js/backup/map-markers.js deleted file mode 100644 index 45dbf348..00000000 --- a/static/js/backup/map-markers.js +++ /dev/null @@ -1,850 +0,0 @@ -/** - * ThrillWiki Map Markers - Custom Marker Icons and Rich Popup System - * - * This module handles custom marker icons for different location types, - * rich popup content with location details, and performance optimization - */ - -class MapMarkers { - constructor(mapInstance, options = {}) { - this.mapInstance = mapInstance; - this.options = { - enableClustering: true, - clusterDistance: 50, - enableCustomIcons: true, - enableRichPopups: true, - enableMarkerAnimation: true, - popupMaxWidth: 300, - iconTheme: 'modern', // 'modern', 'classic', 'emoji' - apiEndpoints: { - details: '/api/map/location-detail/', - media: '/api/media/' - }, - ...options - }; - - this.markerStyles = this.initializeMarkerStyles(); - this.iconCache = new Map(); - this.popupCache = new Map(); - this.activePopup = null; - - this.init(); - } - - /** - * Initialize the marker system - */ - init() { - this.setupMarkerStyles(); - this.setupClusterStyles(); - } - - /** - * Initialize marker style definitions - */ - initializeMarkerStyles() { - return { - park: { - operating: { - color: '#10B981', - emoji: '🎢', - icon: 'fas fa-tree', - size: 'large' - }, - closed_temp: { - color: '#F59E0B', - emoji: '🚧', - icon: 'fas fa-clock', - size: 'medium' - }, - closed_perm: { - color: '#EF4444', - emoji: '❌', - icon: 'fas fa-times-circle', - size: 'medium' - }, - under_construction: { - color: '#8B5CF6', - emoji: '🏗️', - icon: 'fas fa-hard-hat', - size: 'medium' - }, - demolished: { - color: '#6B7280', - emoji: '🏚️', - icon: 'fas fa-ban', - size: 'small' - } - }, - ride: { - operating: { - color: '#3B82F6', - emoji: '🎠', - icon: 'fas fa-rocket', - size: 'medium' - }, - closed_temp: { - color: '#F59E0B', - emoji: '⏸️', - icon: 'fas fa-pause-circle', - size: 'small' - }, - closed_perm: { - color: '#EF4444', - emoji: '❌', - icon: 'fas fa-times-circle', - size: 'small' - }, - under_construction: { - color: '#8B5CF6', - emoji: '🔨', - icon: 'fas fa-tools', - size: 'small' - }, - removed: { - color: '#6B7280', - emoji: '💔', - icon: 'fas fa-trash', - size: 'small' - } - }, - company: { - manufacturer: { - color: '#8B5CF6', - emoji: '🏭', - icon: 'fas fa-industry', - size: 'medium' - }, - operator: { - color: '#059669', - emoji: '🏢', - icon: 'fas fa-building', - size: 'medium' - }, - designer: { - color: '#DC2626', - emoji: '🎨', - icon: 'fas fa-pencil-ruler', - size: 'medium' - } - }, - user: { - current: { - color: '#3B82F6', - emoji: '📍', - icon: 'fas fa-crosshairs', - size: 'medium' - } - } - }; - } - - /** - * Setup marker styles in CSS - */ - setupMarkerStyles() { - if (document.getElementById('map-marker-styles')) return; - - const styles = ` - - `; - - document.head.insertAdjacentHTML('beforeend', styles); - } - - /** - * Setup cluster marker styles - */ - setupClusterStyles() { - // Additional cluster-specific styles if needed - } - - /** - * Create a location marker - */ - createLocationMarker(location) { - const iconData = this.getMarkerIconData(location); - const icon = this.createCustomIcon(iconData, location); - - const marker = L.marker([location.latitude, location.longitude], { - icon: icon, - locationData: location, - riseOnHover: true - }); - - // Create popup - if (this.options.enableRichPopups) { - const popupContent = this.createPopupContent(location); - marker.bindPopup(popupContent, { - maxWidth: this.options.popupMaxWidth, - className: 'location-popup-container' - }); - } - - // Add click handler - marker.on('click', (e) => { - this.handleMarkerClick(marker, location); - }); - - // Add hover effects if animation is enabled - if (this.options.enableMarkerAnimation) { - marker.on('mouseover', () => { - const iconElement = marker.getElement(); - if (iconElement) { - iconElement.style.transform = 'scale(1.1)'; - iconElement.style.zIndex = '1000'; - } - }); - - marker.on('mouseout', () => { - const iconElement = marker.getElement(); - if (iconElement) { - iconElement.style.transform = 'scale(1)'; - iconElement.style.zIndex = ''; - } - }); - } - - return marker; - } - - /** - * Get marker icon data based on location type and status - */ - getMarkerIconData(location) { - const type = location.type || 'generic'; - const status = location.status || 'operating'; - - // Get style data - const typeStyles = this.markerStyles[type]; - if (!typeStyles) { - return this.markerStyles.park.operating; - } - - const statusStyle = typeStyles[status.toLowerCase()]; - if (!statusStyle) { - // Fallback to first available status for this type - const firstStatus = Object.keys(typeStyles)[0]; - return typeStyles[firstStatus]; - } - - return statusStyle; - } - - /** - * Create custom icon - */ - createCustomIcon(iconData, location) { - const cacheKey = `${location.type}-${location.status}-${this.options.iconTheme}`; - - if (this.iconCache.has(cacheKey)) { - return this.iconCache.get(cacheKey); - } - - let iconHtml; - - switch (this.options.iconTheme) { - case 'emoji': - iconHtml = `${iconData.emoji}`; - break; - case 'classic': - iconHtml = ``; - break; - case 'modern': - default: - iconHtml = location.featured_image ? - `${location.name}` : - ``; - break; - } - - const sizeClass = iconData.size || 'medium'; - const size = sizeClass === 'small' ? 24 : sizeClass === 'large' ? 40 : 32; - - const icon = L.divIcon({ - className: `location-marker size-${sizeClass}`, - html: `
${iconHtml}
`, - iconSize: [size, size], - iconAnchor: [size / 2, size / 2], - popupAnchor: [0, -(size / 2) - 8] - }); - - this.iconCache.set(cacheKey, icon); - return icon; - } - - /** - * Create rich popup content - */ - createPopupContent(location) { - const cacheKey = `popup-${location.type}-${location.id}`; - - if (this.popupCache.has(cacheKey)) { - return this.popupCache.get(cacheKey); - } - - const statusClass = this.getStatusClass(location.status); - - const content = ` -
- ${location.featured_image ? ` - ${location.name} - ` : ''} - - - - - - -
- `; - - this.popupCache.set(cacheKey, content); - return content; - } - - /** - * Create popup detail items - */ - createPopupDetails(location) { - const details = []; - - if (location.formatted_location) { - details.push(` - - `); - } - - if (location.operator) { - details.push(` - - `); - } - - if (location.ride_count && location.ride_count > 0) { - details.push(` - - `); - } - - if (location.opened_date) { - details.push(` - - `); - } - - if (location.manufacturer) { - details.push(` - - `); - } - - if (location.designer) { - details.push(` - - `); - } - - return details.join(''); - } - - /** - * Create popup action buttons - */ - createPopupActions(location) { - const actions = []; - - // View details button - actions.push(` - - `); - - // Add to road trip (for parks) - if (location.type === 'park' && window.roadTripPlanner) { - actions.push(` - - `); - } - - // Get directions - if (location.latitude && location.longitude) { - const mapsUrl = `https://www.google.com/maps/dir/?api=1&destination=${location.latitude},${location.longitude}`; - actions.push(` - - - Directions - - `); - } - - return actions.join(''); - } - - /** - * Handle marker click events - */ - handleMarkerClick(marker, location) { - this.activePopup = marker.getPopup(); - - // Load additional data if needed - this.loadLocationDetails(location); - - // Track click event - if (typeof gtag !== 'undefined') { - gtag('event', 'marker_click', { - event_category: 'map', - event_label: `${location.type}:${location.id}`, - custom_map: { - location_type: location.type, - location_name: location.name - } - }); - } - } - - /** - * Load additional location details - */ - async loadLocationDetails(location) { - try { - const response = await fetch(`${this.options.apiEndpoints.details}${location.type}/${location.id}/`); - const data = await response.json(); - - if (data.status === 'success') { - // Update popup with additional details if popup is still open - if (this.activePopup && this.activePopup.isOpen()) { - const updatedContent = this.createPopupContent(data.data); - this.activePopup.setContent(updatedContent); - } - } - } catch (error) { - console.error('Failed to load location details:', error); - } - } - - /** - * Show location details modal/page - */ - showLocationDetails(type, id) { - const url = `/${type}/${id}/`; - - if (typeof htmx !== 'undefined') { - htmx.ajax('GET', url, { - target: '#location-modal', - swap: 'innerHTML' - }).then(() => { - const modal = document.getElementById('location-modal'); - if (modal) { - modal.classList.remove('hidden'); - } - }); - } else { - window.location.href = url; - } - } - - /** - * Get CSS class for status - */ - getStatusClass(status) { - if (!status) return ''; - - const statusLower = status.toLowerCase(); - - if (statusLower.includes('operating') || statusLower.includes('open')) { - return 'operating'; - } else if (statusLower.includes('closed') || statusLower.includes('temp')) { - return 'closed'; - } else if (statusLower.includes('construction') || statusLower.includes('building')) { - return 'construction'; - } - - return ''; - } - - /** - * Format status for display - */ - formatStatus(status) { - return status.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase()); - } - - /** - * Format date for display - */ - formatDate(dateString) { - try { - const date = new Date(dateString); - return date.getFullYear(); - } catch (error) { - return dateString; - } - } - - /** - * Capitalize first letter - */ - capitalizeFirst(str) { - return str.charAt(0).toUpperCase() + str.slice(1); - } - - /** - * Escape HTML - */ - escapeHtml(text) { - const map = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' - }; - return text.replace(/[&<>"']/g, m => map[m]); - } - - /** - * Create cluster marker - */ - createClusterMarker(cluster) { - const count = cluster.getChildCount(); - let sizeClass = 'small'; - - if (count > 100) sizeClass = 'large'; - else if (count > 10) sizeClass = 'medium'; - - return L.divIcon({ - html: `
${count}
`, - className: `cluster-marker cluster-marker-${sizeClass}`, - iconSize: L.point(sizeClass === 'small' ? 32 : sizeClass === 'medium' ? 40 : 48, - sizeClass === 'small' ? 32 : sizeClass === 'medium' ? 40 : 48) - }); - } - - /** - * Update marker theme - */ - setIconTheme(theme) { - this.options.iconTheme = theme; - this.iconCache.clear(); - - // Re-render all markers if map instance is available - if (this.mapInstance && this.mapInstance.markers) { - // This would need to be implemented in the main map class - console.log(`Icon theme changed to: ${theme}`); - } - } - - /** - * Clear popup cache - */ - clearPopupCache() { - this.popupCache.clear(); - } - - /** - * Clear icon cache - */ - clearIconCache() { - this.iconCache.clear(); - } - - /** - * Get marker statistics - */ - getMarkerStats() { - return { - iconCacheSize: this.iconCache.size, - popupCacheSize: this.popupCache.size, - iconTheme: this.options.iconTheme - }; - } -} - -// Auto-initialize with map instance if available -document.addEventListener('DOMContentLoaded', function() { - if (window.thrillwikiMap) { - window.mapMarkers = new MapMarkers(window.thrillwikiMap); - } -}); - -// Export for use in other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = MapMarkers; -} else { - window.MapMarkers = MapMarkers; -} \ No newline at end of file diff --git a/static/js/backup/maps.js b/static/js/backup/maps.js deleted file mode 100644 index 1969dcf1..00000000 --- a/static/js/backup/maps.js +++ /dev/null @@ -1,656 +0,0 @@ -/** - * ThrillWiki Maps - Core Map Functionality - * - * This module provides the main map functionality for ThrillWiki using Leaflet.js - * Includes clustering, filtering, dark mode support, and HTMX integration - */ - -class ThrillWikiMap { - constructor(containerId, options = {}) { - this.containerId = containerId; - this.options = { - center: [39.8283, -98.5795], // Center of USA - zoom: 4, - minZoom: 2, - maxZoom: 18, - enableClustering: true, - enableDarkMode: true, - enableGeolocation: false, - apiEndpoints: { - locations: '/api/map/locations/', - details: '/api/map/location-detail/' - }, - ...options - }; - - this.map = null; - this.markers = null; - this.currentData = []; - this.userLocation = null; - this.currentTileLayer = null; - this.boundsUpdateTimeout = null; - - // Event handlers - this.eventHandlers = { - locationClick: [], - boundsChange: [], - dataLoad: [] - }; - - this.init(); - } - - /** - * Initialize the map - */ - init() { - const container = document.getElementById(this.containerId); - if (!container) { - console.error(`Map container with ID '${this.containerId}' not found`); - return; - } - - try { - this.initializeMap(); - this.setupTileLayers(); - this.setupClustering(); - this.bindEvents(); - this.loadInitialData(); - } catch (error) { - console.error('Failed to initialize map:', error); - } - } - - /** - * Initialize the Leaflet map instance - */ - initializeMap() { - this.map = L.map(this.containerId, { - center: this.options.center, - zoom: this.options.zoom, - minZoom: this.options.minZoom, - maxZoom: this.options.maxZoom, - zoomControl: false, - attributionControl: false - }); - - // Add custom zoom control - L.control.zoom({ - position: 'bottomright' - }).addTo(this.map); - - // Add attribution control - L.control.attribution({ - position: 'bottomleft', - prefix: false - }).addTo(this.map); - } - - /** - * Setup tile layers with dark mode support - */ - setupTileLayers() { - this.tileLayers = { - light: L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors', - className: 'map-tiles-light' - }), - dark: L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { - attribution: '© OpenStreetMap contributors, © CARTO', - className: 'map-tiles-dark' - }), - satellite: L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { - attribution: '© Esri, DigitalGlobe, GeoEye, Earthstar Geographics, CNES/Airbus DS, USDA, USGS, AeroGRID, IGN, and the GIS User Community', - className: 'map-tiles-satellite' - }) - }; - - // Set initial tile layer based on theme - this.updateTileLayer(); - - // Listen for theme changes if dark mode is enabled - if (this.options.enableDarkMode) { - this.observeThemeChanges(); - } - } - - /** - * Setup marker clustering - */ - setupClustering() { - if (this.options.enableClustering) { - this.markers = L.markerClusterGroup({ - chunkedLoading: true, - maxClusterRadius: 50, - spiderfyOnMaxZoom: true, - showCoverageOnHover: false, - zoomToBoundsOnClick: true, - iconCreateFunction: (cluster) => { - const count = cluster.getChildCount(); - let className = 'cluster-marker-small'; - - if (count > 100) className = 'cluster-marker-large'; - else if (count > 10) className = 'cluster-marker-medium'; - - return L.divIcon({ - html: `
${count}
`, - className: `cluster-marker ${className}`, - iconSize: L.point(40, 40) - }); - } - }); - } else { - this.markers = L.layerGroup(); - } - - this.map.addLayer(this.markers); - } - - /** - * Bind map events - */ - bindEvents() { - // Map movement events - this.map.on('moveend zoomend', () => { - this.handleBoundsChange(); - }); - - // Marker click events - this.markers.on('click', (e) => { - if (e.layer.options && e.layer.options.locationData) { - this.handleLocationClick(e.layer.options.locationData); - } - }); - - // Custom event handlers - this.map.on('locationfound', (e) => { - this.handleLocationFound(e); - }); - - this.map.on('locationerror', (e) => { - this.handleLocationError(e); - }); - } - - /** - * Observe theme changes for automatic tile layer switching - */ - observeThemeChanges() { - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.attributeName === 'class') { - this.updateTileLayer(); - } - }); - }); - - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['class'] - }); - } - - /** - * Update tile layer based on current theme and settings - */ - updateTileLayer() { - // Remove current tile layer - if (this.currentTileLayer) { - this.map.removeLayer(this.currentTileLayer); - } - - // Determine which layer to use - let layerType = 'light'; - - if (document.documentElement.classList.contains('dark')) { - layerType = 'dark'; - } - - // Check for satellite mode toggle - const satelliteToggle = document.querySelector('input[name="satellite"]'); - if (satelliteToggle && satelliteToggle.checked) { - layerType = 'satellite'; - } - - // Add the appropriate tile layer - this.currentTileLayer = this.tileLayers[layerType]; - this.map.addLayer(this.currentTileLayer); - } - - /** - * Load initial map data - */ - async loadInitialData() { - const bounds = this.map.getBounds(); - await this.loadLocations(bounds, {}); - } - - /** - * Load locations with optional bounds and filters - */ - async loadLocations(bounds = null, filters = {}) { - try { - this.showLoading(true); - - const params = new URLSearchParams(); - - // Add bounds if provided - if (bounds) { - params.append('north', bounds.getNorth()); - params.append('south', bounds.getSouth()); - params.append('east', bounds.getEast()); - params.append('west', bounds.getWest()); - } - - // Add zoom level - params.append('zoom', this.map.getZoom()); - - // Add filters - Object.entries(filters).forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach(v => params.append(key, v)); - } else if (value !== null && value !== undefined && value !== '') { - params.append(key, value); - } - }); - - const response = await fetch(`${this.options.apiEndpoints.locations}?${params}`); - const data = await response.json(); - - if (data.status === 'success') { - this.updateMarkers(data.data); - this.triggerEvent('dataLoad', data.data); - } else { - console.error('Map data error:', data.message); - } - } catch (error) { - console.error('Failed to load map data:', error); - } finally { - this.showLoading(false); - } - } - - /** - * Update map markers with new data - */ - updateMarkers(data) { - // Clear existing markers - this.markers.clearLayers(); - this.currentData = data; - - // Add location markers - if (data.locations) { - data.locations.forEach(location => { - this.addLocationMarker(location); - }); - } - - // Add cluster markers (if not using Leaflet clustering) - if (data.clusters && !this.options.enableClustering) { - data.clusters.forEach(cluster => { - this.addClusterMarker(cluster); - }); - } - } - - /** - * Add a location marker to the map - */ - addLocationMarker(location) { - const icon = this.createLocationIcon(location); - const marker = L.marker([location.latitude, location.longitude], { - icon: icon, - locationData: location - }); - - // Create popup content - const popupContent = this.createPopupContent(location); - marker.bindPopup(popupContent, { - maxWidth: 300, - className: 'location-popup' - }); - - this.markers.addLayer(marker); - } - - /** - * Add a cluster marker (for server-side clustering) - */ - addClusterMarker(cluster) { - const marker = L.marker([cluster.latitude, cluster.longitude], { - icon: L.divIcon({ - className: 'cluster-marker server-cluster', - html: `
${cluster.count}
`, - iconSize: [40, 40] - }) - }); - - marker.bindPopup(`${cluster.count} locations in this area`); - this.markers.addLayer(marker); - } - - /** - * Create location icon based on type - */ - createLocationIcon(location) { - const iconMap = { - 'park': { emoji: '🎢', color: '#10B981' }, - 'ride': { emoji: '🎠', color: '#3B82F6' }, - 'company': { emoji: '🏢', color: '#8B5CF6' }, - 'generic': { emoji: '📍', color: '#6B7280' } - }; - - const iconData = iconMap[location.type] || iconMap.generic; - - return L.divIcon({ - className: 'location-marker', - html: ` -
- ${iconData.emoji} -
- `, - iconSize: [30, 30], - iconAnchor: [15, 15], - popupAnchor: [0, -15] - }); - } - - /** - * Create popup content for a location - */ - createPopupContent(location) { - return ` -
- - ${location.formatted_location ? `` : ''} - ${location.operator ? `` : ''} - ${location.ride_count ? `` : ''} - ${location.status ? `` : ''} - -
- `; - } - - /** - * Show/hide loading indicator - */ - showLoading(show) { - const loadingElement = document.getElementById(`${this.containerId}-loading`) || - document.getElementById('map-loading'); - - if (loadingElement) { - loadingElement.style.display = show ? 'flex' : 'none'; - } - } - - /** - * Handle map bounds change - */ - handleBoundsChange() { - clearTimeout(this.boundsUpdateTimeout); - this.boundsUpdateTimeout = setTimeout(() => { - const bounds = this.map.getBounds(); - this.triggerEvent('boundsChange', bounds); - - // Auto-reload data on significant bounds change - if (this.shouldReloadData()) { - this.loadLocations(bounds, this.getCurrentFilters()); - } - }, 1000); - } - - /** - * Handle location click - */ - handleLocationClick(location) { - this.triggerEvent('locationClick', location); - } - - /** - * Show location details (integrate with HTMX) - */ - showLocationDetails(type, id) { - const url = `${this.options.apiEndpoints.details}${type}/${id}/`; - - if (typeof htmx !== 'undefined') { - htmx.ajax('GET', url, { - target: '#location-modal', - swap: 'innerHTML' - }).then(() => { - const modal = document.getElementById('location-modal'); - if (modal) { - modal.classList.remove('hidden'); - } - }); - } else { - // Fallback to regular navigation - window.location.href = url; - } - } - - /** - * Get current filters from form - */ - getCurrentFilters() { - const form = document.getElementById('map-filters'); - if (!form) return {}; - - const formData = new FormData(form); - const filters = {}; - - for (let [key, value] of formData.entries()) { - if (filters[key]) { - if (Array.isArray(filters[key])) { - filters[key].push(value); - } else { - filters[key] = [filters[key], value]; - } - } else { - filters[key] = value; - } - } - - return filters; - } - - /** - * Update filters and reload data - */ - updateFilters(filters) { - const bounds = this.map.getBounds(); - this.loadLocations(bounds, filters); - } - - /** - * Enable user location features - */ - enableGeolocation() { - this.options.enableGeolocation = true; - this.map.locate({ setView: false, maxZoom: 16 }); - } - - /** - * Handle location found - */ - handleLocationFound(e) { - if (this.userLocation) { - this.map.removeLayer(this.userLocation); - } - - this.userLocation = L.marker(e.latlng, { - icon: L.divIcon({ - className: 'user-location-marker', - html: '
', - iconSize: [24, 24], - iconAnchor: [12, 12] - }) - }).addTo(this.map); - - this.userLocation.bindPopup('Your Location'); - } - - /** - * Handle location error - */ - handleLocationError(e) { - console.warn('Location access denied or unavailable:', e.message); - } - - /** - * Determine if data should be reloaded based on map movement - */ - shouldReloadData() { - // Simple heuristic: reload if zoom changed or moved significantly - return true; // For now, always reload - } - - /** - * Add event listener - */ - on(event, handler) { - if (!this.eventHandlers[event]) { - this.eventHandlers[event] = []; - } - this.eventHandlers[event].push(handler); - } - - /** - * Remove event listener - */ - off(event, handler) { - if (this.eventHandlers[event]) { - const index = this.eventHandlers[event].indexOf(handler); - if (index > -1) { - this.eventHandlers[event].splice(index, 1); - } - } - } - - /** - * Trigger event - */ - triggerEvent(event, data) { - if (this.eventHandlers[event]) { - this.eventHandlers[event].forEach(handler => { - try { - handler(data); - } catch (error) { - console.error(`Error in ${event} handler:`, error); - } - }); - } - } - - /** - * Export map view as image (requires html2canvas) - */ - async exportMap() { - if (typeof html2canvas === 'undefined') { - console.warn('html2canvas library not loaded, cannot export map'); - return null; - } - - try { - const canvas = await html2canvas(document.getElementById(this.containerId)); - return canvas.toDataURL('image/png'); - } catch (error) { - console.error('Failed to export map:', error); - return null; - } - } - - /** - * Resize map (call when container size changes) - */ - invalidateSize() { - if (this.map) { - this.map.invalidateSize(); - } - } - - /** - * Get map bounds - */ - getBounds() { - return this.map ? this.map.getBounds() : null; - } - - /** - * Set map view - */ - setView(latlng, zoom) { - if (this.map) { - this.map.setView(latlng, zoom); - } - } - - /** - * Fit map to bounds - */ - fitBounds(bounds, options = {}) { - if (this.map) { - this.map.fitBounds(bounds, options); - } - } - - /** - * Destroy map instance - */ - destroy() { - if (this.map) { - this.map.remove(); - this.map = null; - } - - // Clear timeouts - if (this.boundsUpdateTimeout) { - clearTimeout(this.boundsUpdateTimeout); - } - - // Clear event handlers - this.eventHandlers = {}; - } -} - -// Auto-initialize maps with data attributes -document.addEventListener('DOMContentLoaded', function() { - // Find all elements with map-container class - const mapContainers = document.querySelectorAll('[data-map="auto"]'); - - mapContainers.forEach(container => { - const mapId = container.id; - const options = {}; - - // Parse data attributes for configuration - Object.keys(container.dataset).forEach(key => { - if (key.startsWith('map')) { - const optionKey = key.replace('map', '').toLowerCase(); - let value = container.dataset[key]; - - // Try to parse as JSON for complex values - try { - value = JSON.parse(value); - } catch (e) { - // Keep as string if not valid JSON - } - - options[optionKey] = value; - } - }); - - // Create map instance - window[`${mapId}Instance`] = new ThrillWikiMap(mapId, options); - }); -}); - -// Export for use in other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = ThrillWikiMap; -} else { - window.ThrillWikiMap = ThrillWikiMap; -} \ No newline at end of file diff --git a/static/js/backup/mobile-touch.js b/static/js/backup/mobile-touch.js deleted file mode 100644 index ebb2d988..00000000 --- a/static/js/backup/mobile-touch.js +++ /dev/null @@ -1,881 +0,0 @@ -/** - * ThrillWiki Mobile Touch Support - Enhanced Mobile and Touch Experience - * - * This module provides mobile-optimized interactions, touch-friendly controls, - * responsive map sizing, and battery-conscious features for mobile devices - */ - -class MobileTouchSupport { - constructor(options = {}) { - this.options = { - enableTouchOptimizations: true, - enableSwipeGestures: true, - enablePinchZoom: true, - enableResponsiveResize: true, - enableBatteryOptimization: true, - touchDebounceDelay: 150, - swipeThreshold: 50, - swipeVelocityThreshold: 0.3, - maxTouchPoints: 2, - orientationChangeDelay: 300, - ...options - }; - - this.isMobile = this.detectMobileDevice(); - this.isTouch = this.detectTouchSupport(); - this.orientation = this.getOrientation(); - this.mapInstances = new Set(); - this.touchHandlers = new Map(); - this.gestureState = { - isActive: false, - startDistance: 0, - startCenter: null, - lastTouchTime: 0 - }; - - this.init(); - } - - /** - * Initialize mobile touch support - */ - init() { - if (!this.isTouch && !this.isMobile) { - console.log('Mobile touch support not needed for this device'); - return; - } - - this.setupTouchOptimizations(); - this.setupSwipeGestures(); - this.setupResponsiveHandling(); - this.setupBatteryOptimization(); - this.setupAccessibilityEnhancements(); - this.bindEventHandlers(); - - console.log('Mobile touch support initialized'); - } - - /** - * Detect if device is mobile - */ - detectMobileDevice() { - const userAgent = navigator.userAgent.toLowerCase(); - const mobileKeywords = ['android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone']; - - return mobileKeywords.some(keyword => userAgent.includes(keyword)) || - window.innerWidth <= 768 || - (typeof window.orientation !== 'undefined'); - } - - /** - * Detect touch support - */ - detectTouchSupport() { - return 'ontouchstart' in window || - navigator.maxTouchPoints > 0 || - navigator.msMaxTouchPoints > 0; - } - - /** - * Get current orientation - */ - getOrientation() { - if (screen.orientation) { - return screen.orientation.angle; - } else if (window.orientation !== undefined) { - return window.orientation; - } - return window.innerWidth > window.innerHeight ? 90 : 0; - } - - /** - * Setup touch optimizations - */ - setupTouchOptimizations() { - if (!this.options.enableTouchOptimizations) return; - - // Add touch-optimized styles - this.addTouchStyles(); - - // Enhance touch targets - this.enhanceTouchTargets(); - - // Optimize scroll behavior - this.optimizeScrollBehavior(); - - // Setup touch feedback - this.setupTouchFeedback(); - } - - /** - * Add touch-optimized CSS styles - */ - addTouchStyles() { - if (document.getElementById('mobile-touch-styles')) return; - - const styles = ` - - `; - - document.head.insertAdjacentHTML('beforeend', styles); - } - - /** - * Enhance touch targets for better accessibility - */ - enhanceTouchTargets() { - const smallTargets = document.querySelectorAll('button, .btn, a, input[type="checkbox"], input[type="radio"]'); - - smallTargets.forEach(target => { - const rect = target.getBoundingClientRect(); - - // If target is smaller than 44px (Apple's recommended minimum), enhance it - if (rect.width < 44 || rect.height < 44) { - target.classList.add('touch-enhanced'); - target.style.minWidth = '44px'; - target.style.minHeight = '44px'; - target.style.display = 'inline-flex'; - target.style.alignItems = 'center'; - target.style.justifyContent = 'center'; - } - }); - } - - /** - * Optimize scroll behavior for mobile - */ - optimizeScrollBehavior() { - // Add momentum scrolling to scrollable elements - const scrollableElements = document.querySelectorAll('.scrollable, .overflow-auto, .overflow-y-auto'); - - scrollableElements.forEach(element => { - element.classList.add('touch-scroll'); - element.style.webkitOverflowScrolling = 'touch'; - }); - - // Prevent body scroll when interacting with maps - document.addEventListener('touchstart', (e) => { - if (e.target.closest('.leaflet-container')) { - e.preventDefault(); - } - }, { passive: false }); - } - - /** - * Setup touch feedback for interactive elements - */ - setupTouchFeedback() { - const interactiveElements = document.querySelectorAll('button, .btn, .filter-chip, .filter-pill, .park-item'); - - interactiveElements.forEach(element => { - element.classList.add('touch-feedback'); - - element.addEventListener('touchstart', (e) => { - element.classList.add('active'); - - setTimeout(() => { - element.classList.remove('active'); - }, 300); - }, { passive: true }); - }); - } - - /** - * Setup swipe gesture support - */ - setupSwipeGestures() { - if (!this.options.enableSwipeGestures) return; - - let touchStartX = 0; - let touchStartY = 0; - let touchStartTime = 0; - - document.addEventListener('touchstart', (e) => { - if (e.touches.length === 1) { - touchStartX = e.touches[0].clientX; - touchStartY = e.touches[0].clientY; - touchStartTime = Date.now(); - } - }, { passive: true }); - - document.addEventListener('touchend', (e) => { - if (e.changedTouches.length === 1) { - const touchEndX = e.changedTouches[0].clientX; - const touchEndY = e.changedTouches[0].clientY; - const touchEndTime = Date.now(); - - const deltaX = touchEndX - touchStartX; - const deltaY = touchEndY - touchStartY; - const deltaTime = touchEndTime - touchStartTime; - const velocity = Math.abs(deltaX) / deltaTime; - - // Check if this is a swipe gesture - if (Math.abs(deltaX) > this.options.swipeThreshold && - Math.abs(deltaY) < Math.abs(deltaX) && - velocity > this.options.swipeVelocityThreshold) { - - const direction = deltaX > 0 ? 'right' : 'left'; - this.handleSwipeGesture(direction, e.target); - } - } - }, { passive: true }); - } - - /** - * Handle swipe gestures - */ - handleSwipeGesture(direction, target) { - // Handle swipe on filter panels - if (target.closest('.filter-panel')) { - if (direction === 'down' || direction === 'up') { - this.toggleFilterPanel(); - } - } - - // Handle swipe on road trip list - if (target.closest('.parks-list')) { - if (direction === 'left') { - this.showParkActions(target); - } else if (direction === 'right') { - this.hideParkActions(target); - } - } - - // Emit custom swipe event - const swipeEvent = new CustomEvent('swipe', { - detail: { direction, target } - }); - document.dispatchEvent(swipeEvent); - } - - /** - * Setup responsive handling for orientation changes - */ - setupResponsiveHandling() { - if (!this.options.enableResponsiveResize) return; - - // Handle orientation changes - window.addEventListener('orientationchange', () => { - setTimeout(() => { - this.handleOrientationChange(); - }, this.options.orientationChangeDelay); - }); - - // Handle window resize - let resizeTimeout; - window.addEventListener('resize', () => { - clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(() => { - this.handleWindowResize(); - }, 250); - }); - - // Handle viewport changes (for mobile browsers with dynamic toolbars) - this.setupViewportHandler(); - } - - /** - * Handle orientation change - */ - handleOrientationChange() { - const newOrientation = this.getOrientation(); - - if (newOrientation !== this.orientation) { - this.orientation = newOrientation; - - // Resize all map instances - this.mapInstances.forEach(mapInstance => { - if (mapInstance.invalidateSize) { - mapInstance.invalidateSize(); - } - }); - - // Emit orientation change event - const orientationEvent = new CustomEvent('orientationChanged', { - detail: { orientation: this.orientation } - }); - document.dispatchEvent(orientationEvent); - } - } - - /** - * Handle window resize - */ - handleWindowResize() { - // Update mobile detection - this.isMobile = this.detectMobileDevice(); - - // Resize map instances - this.mapInstances.forEach(mapInstance => { - if (mapInstance.invalidateSize) { - mapInstance.invalidateSize(); - } - }); - - // Update touch targets - this.enhanceTouchTargets(); - } - - /** - * Setup viewport handler for dynamic mobile toolbars - */ - setupViewportHandler() { - // Use visual viewport API if available - if (window.visualViewport) { - window.visualViewport.addEventListener('resize', () => { - this.handleViewportChange(); - }); - } - - // Fallback for older browsers - let lastHeight = window.innerHeight; - - const checkViewportChange = () => { - if (Math.abs(window.innerHeight - lastHeight) > 100) { - lastHeight = window.innerHeight; - this.handleViewportChange(); - } - }; - - window.addEventListener('resize', checkViewportChange); - document.addEventListener('focusin', checkViewportChange); - document.addEventListener('focusout', checkViewportChange); - } - - /** - * Handle viewport changes - */ - handleViewportChange() { - // Adjust map container heights - const mapContainers = document.querySelectorAll('.map-container'); - mapContainers.forEach(container => { - const viewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight; - - if (viewportHeight < 500) { - container.style.height = '40vh'; - } else { - container.style.height = ''; // Reset to CSS default - } - }); - } - - /** - * Setup battery optimization - */ - setupBatteryOptimization() { - if (!this.options.enableBatteryOptimization) return; - - // Reduce update frequency when battery is low - if ('getBattery' in navigator) { - navigator.getBattery().then(battery => { - const optimizeBattery = () => { - if (battery.level < 0.2) { // Battery below 20% - this.enableBatterySaveMode(); - } else { - this.disableBatterySaveMode(); - } - }; - - battery.addEventListener('levelchange', optimizeBattery); - optimizeBattery(); - }); - } - - // Reduce activity when page is not visible - document.addEventListener('visibilitychange', () => { - if (document.hidden) { - this.pauseNonEssentialFeatures(); - } else { - this.resumeNonEssentialFeatures(); - } - }); - } - - /** - * Enable battery save mode - */ - enableBatterySaveMode() { - console.log('Enabling battery save mode'); - - // Reduce map update frequency - this.mapInstances.forEach(mapInstance => { - if (mapInstance.options) { - mapInstance.options.updateInterval = 5000; // Increase to 5 seconds - } - }); - - // Disable animations - document.body.classList.add('battery-save-mode'); - } - - /** - * Disable battery save mode - */ - disableBatterySaveMode() { - console.log('Disabling battery save mode'); - - // Restore normal update frequency - this.mapInstances.forEach(mapInstance => { - if (mapInstance.options) { - mapInstance.options.updateInterval = 1000; // Restore to 1 second - } - }); - - // Re-enable animations - document.body.classList.remove('battery-save-mode'); - } - - /** - * Pause non-essential features - */ - pauseNonEssentialFeatures() { - // Pause location watching - if (window.userLocation && window.userLocation.stopWatching) { - window.userLocation.stopWatching(); - } - - // Reduce map updates - this.mapInstances.forEach(mapInstance => { - if (mapInstance.pauseUpdates) { - mapInstance.pauseUpdates(); - } - }); - } - - /** - * Resume non-essential features - */ - resumeNonEssentialFeatures() { - // Resume location watching if it was active - if (window.userLocation && window.userLocation.options.watchPosition) { - window.userLocation.startWatching(); - } - - // Resume map updates - this.mapInstances.forEach(mapInstance => { - if (mapInstance.resumeUpdates) { - mapInstance.resumeUpdates(); - } - }); - } - - /** - * Setup accessibility enhancements for mobile - */ - setupAccessibilityEnhancements() { - // Add focus indicators for touch navigation - const focusableElements = document.querySelectorAll('button, a, input, select, textarea, [tabindex]'); - - focusableElements.forEach(element => { - element.addEventListener('focus', () => { - element.classList.add('touch-focused'); - }); - - element.addEventListener('blur', () => { - element.classList.remove('touch-focused'); - }); - }); - - // Enhance keyboard navigation - document.addEventListener('keydown', (e) => { - if (e.key === 'Tab') { - document.body.classList.add('keyboard-navigation'); - } - }); - - document.addEventListener('mousedown', () => { - document.body.classList.remove('keyboard-navigation'); - }); - } - - /** - * Bind event handlers - */ - bindEventHandlers() { - // Handle double-tap to zoom - this.setupDoubleTapZoom(); - - // Handle long press - this.setupLongPress(); - - // Handle pinch gestures - if (this.options.enablePinchZoom) { - this.setupPinchZoom(); - } - } - - /** - * Setup double-tap to zoom - */ - setupDoubleTapZoom() { - let lastTapTime = 0; - - document.addEventListener('touchend', (e) => { - const currentTime = Date.now(); - - if (currentTime - lastTapTime < 300) { - // Double tap detected - const target = e.target; - if (target.closest('.leaflet-container')) { - this.handleDoubleTapZoom(e); - } - } - - lastTapTime = currentTime; - }, { passive: true }); - } - - /** - * Handle double-tap zoom - */ - handleDoubleTapZoom(e) { - const mapContainer = e.target.closest('.leaflet-container'); - if (!mapContainer) return; - - // Find associated map instance - this.mapInstances.forEach(mapInstance => { - if (mapInstance.getContainer() === mapContainer) { - const currentZoom = mapInstance.getZoom(); - const newZoom = currentZoom < mapInstance.getMaxZoom() ? currentZoom + 2 : mapInstance.getMinZoom(); - - mapInstance.setZoom(newZoom, { - animate: true, - duration: 0.3 - }); - } - }); - } - - /** - * Setup long press detection - */ - setupLongPress() { - let pressTimer; - - document.addEventListener('touchstart', (e) => { - pressTimer = setTimeout(() => { - this.handleLongPress(e); - }, 750); // 750ms for long press - }, { passive: true }); - - document.addEventListener('touchend', () => { - clearTimeout(pressTimer); - }, { passive: true }); - - document.addEventListener('touchmove', () => { - clearTimeout(pressTimer); - }, { passive: true }); - } - - /** - * Handle long press - */ - handleLongPress(e) { - const target = e.target; - - // Emit long press event - const longPressEvent = new CustomEvent('longPress', { - detail: { target, touches: e.touches } - }); - target.dispatchEvent(longPressEvent); - - // Provide haptic feedback if available - if (navigator.vibrate) { - navigator.vibrate(50); - } - } - - /** - * Setup pinch zoom for maps - */ - setupPinchZoom() { - document.addEventListener('touchstart', (e) => { - if (e.touches.length === 2) { - this.gestureState.isActive = true; - this.gestureState.startDistance = this.getDistance(e.touches[0], e.touches[1]); - this.gestureState.startCenter = this.getCenter(e.touches[0], e.touches[1]); - } - }, { passive: true }); - - document.addEventListener('touchmove', (e) => { - if (this.gestureState.isActive && e.touches.length === 2) { - this.handlePinchZoom(e); - } - }, { passive: false }); - - document.addEventListener('touchend', () => { - this.gestureState.isActive = false; - }, { passive: true }); - } - - /** - * Handle pinch zoom gesture - */ - handlePinchZoom(e) { - if (!e.target.closest('.leaflet-container')) return; - - const currentDistance = this.getDistance(e.touches[0], e.touches[1]); - const scale = currentDistance / this.gestureState.startDistance; - - // Emit pinch event - const pinchEvent = new CustomEvent('pinch', { - detail: { scale, center: this.gestureState.startCenter } - }); - e.target.dispatchEvent(pinchEvent); - } - - /** - * Get distance between two touch points - */ - getDistance(touch1, touch2) { - const dx = touch1.clientX - touch2.clientX; - const dy = touch1.clientY - touch2.clientY; - return Math.sqrt(dx * dx + dy * dy); - } - - /** - * Get center point between two touches - */ - getCenter(touch1, touch2) { - return { - x: (touch1.clientX + touch2.clientX) / 2, - y: (touch1.clientY + touch2.clientY) / 2 - }; - } - - /** - * Register map instance for mobile optimizations - */ - registerMapInstance(mapInstance) { - this.mapInstances.add(mapInstance); - - // Apply mobile-specific map options - if (this.isMobile && mapInstance.options) { - mapInstance.options.zoomControl = false; // Use custom larger controls - mapInstance.options.attributionControl = false; // Save space - } - } - - /** - * Unregister map instance - */ - unregisterMapInstance(mapInstance) { - this.mapInstances.delete(mapInstance); - } - - /** - * Toggle filter panel for mobile - */ - toggleFilterPanel() { - const filterPanel = document.querySelector('.filter-panel'); - if (filterPanel) { - filterPanel.classList.toggle('mobile-expanded'); - } - } - - /** - * Show park actions on swipe - */ - showParkActions(target) { - const parkItem = target.closest('.park-item'); - if (parkItem) { - parkItem.classList.add('actions-visible'); - } - } - - /** - * Hide park actions - */ - hideParkActions(target) { - const parkItem = target.closest('.park-item'); - if (parkItem) { - parkItem.classList.remove('actions-visible'); - } - } - - /** - * Check if device is mobile - */ - isMobileDevice() { - return this.isMobile; - } - - /** - * Check if device supports touch - */ - isTouchDevice() { - return this.isTouch; - } - - /** - * Get device info - */ - getDeviceInfo() { - return { - isMobile: this.isMobile, - isTouch: this.isTouch, - orientation: this.orientation, - viewportWidth: window.innerWidth, - viewportHeight: window.innerHeight, - pixelRatio: window.devicePixelRatio || 1 - }; - } -} - -// Auto-initialize mobile touch support -document.addEventListener('DOMContentLoaded', function() { - window.mobileTouchSupport = new MobileTouchSupport(); - - // Register existing map instances - if (window.thrillwikiMap) { - window.mobileTouchSupport.registerMapInstance(window.thrillwikiMap); - } -}); - -// Export for use in other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = MobileTouchSupport; -} else { - window.MobileTouchSupport = MobileTouchSupport; -} \ No newline at end of file diff --git a/static/js/backup/park-map.js b/static/js/backup/park-map.js deleted file mode 100644 index cb7f399e..00000000 --- a/static/js/backup/park-map.js +++ /dev/null @@ -1,29 +0,0 @@ -// Only declare parkMap if it doesn't exist -window.parkMap = window.parkMap || null; - -function initParkMap(latitude, longitude, name) { - const mapContainer = document.getElementById('park-map'); - - // Only initialize if container exists and map hasn't been initialized - if (mapContainer && !window.parkMap) { - const width = mapContainer.offsetWidth; - mapContainer.style.height = width + 'px'; - - window.parkMap = L.map('park-map').setView([latitude, longitude], 13); - - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors' - }).addTo(window.parkMap); - - L.marker([latitude, longitude]) - .addTo(window.parkMap) - .bindPopup(name); - - // Update map size when window is resized - window.addEventListener('resize', function() { - const width = mapContainer.offsetWidth; - mapContainer.style.height = width + 'px'; - window.parkMap.invalidateSize(); - }); - } -} diff --git a/static/js/backup/photo-gallery.js b/static/js/backup/photo-gallery.js deleted file mode 100644 index 2705dcab..00000000 --- a/static/js/backup/photo-gallery.js +++ /dev/null @@ -1,91 +0,0 @@ -document.addEventListener('alpine:init', () => { - Alpine.data('photoDisplay', ({ photos, contentType, objectId, csrfToken, uploadUrl }) => ({ - photos, - fullscreenPhoto: null, - uploading: false, - uploadProgress: 0, - error: null, - showSuccess: false, - - showFullscreen(photo) { - this.fullscreenPhoto = photo; - }, - - async handleFileSelect(event) { - const files = Array.from(event.target.files); - if (!files.length) { - return; - } - - this.uploading = true; - this.uploadProgress = 0; - this.error = null; - this.showSuccess = false; - - const totalFiles = files.length; - let completedFiles = 0; - - for (const file of files) { - const formData = new FormData(); - formData.append('image', file); - formData.append('app_label', contentType.split('.')[0]); - formData.append('model', contentType.split('.')[1]); - formData.append('object_id', objectId); - - try { - const response = await fetch(uploadUrl, { - method: 'POST', - headers: { - 'X-CSRFToken': csrfToken, - }, - body: formData - }); - - if (!response.ok) { - const data = await response.json(); - throw new Error(data.error || 'Upload failed'); - } - - const photo = await response.json(); - this.photos.push(photo); - completedFiles++; - this.uploadProgress = (completedFiles / totalFiles) * 100; - } catch (err) { - this.error = err.message || 'Failed to upload photo. Please try again.'; - console.error('Upload error:', err); - break; - } - } - - this.uploading = false; - event.target.value = ''; // Reset file input - - if (!this.error) { - this.showSuccess = true; - setTimeout(() => { - this.showSuccess = false; - }, 3000); - } - }, - - async sharePhoto(photo) { - if (navigator.share) { - try { - await navigator.share({ - title: photo.caption || 'Shared photo', - url: photo.url - }); - } catch (err) { - if (err.name !== 'AbortError') { - console.error('Error sharing:', err); - } - } - } else { - // Fallback: copy URL to clipboard - navigator.clipboard.writeText(photo.url) - .then(() => alert('Photo URL copied to clipboard!')) - .catch(err => console.error('Error copying to clipboard:', err)); - } - } - })); -}); diff --git a/static/js/backup/roadtrip.js b/static/js/backup/roadtrip.js deleted file mode 100644 index b7e15610..00000000 --- a/static/js/backup/roadtrip.js +++ /dev/null @@ -1,774 +0,0 @@ -/** - * ThrillWiki Road Trip Planner - Multi-park Route Planning - * - * This module provides road trip planning functionality with multi-park selection, - * route visualization, distance calculations, and export capabilities - */ - -class RoadTripPlanner { - constructor(containerId, options = {}) { - this.containerId = containerId; - this.options = { - mapInstance: null, - maxParks: 20, - enableOptimization: true, - enableExport: true, - apiEndpoints: { - parks: '/api/parks/', - route: '/api/roadtrip/route/', - optimize: '/api/roadtrip/optimize/', - export: '/api/roadtrip/export/' - }, - routeOptions: { - color: '#3B82F6', - weight: 4, - opacity: 0.8 - }, - ...options - }; - - this.container = null; - this.mapInstance = null; - this.selectedParks = []; - this.routeLayer = null; - this.parkMarkers = new Map(); - this.routePolyline = null; - this.routeData = null; - - this.init(); - } - - /** - * Initialize the road trip planner - */ - init() { - this.container = document.getElementById(this.containerId); - if (!this.container) { - console.error(`Road trip container with ID '${this.containerId}' not found`); - return; - } - - this.setupUI(); - this.bindEvents(); - - // Connect to map instance if provided - if (this.options.mapInstance) { - this.connectToMap(this.options.mapInstance); - } - - this.loadInitialData(); - } - - /** - * Setup the UI components - */ - setupUI() { - const html = ` -
-
-

- - Road Trip Planner -

-
- - -
-
- -
-
-
- -
-
-
- -
-

Your Route (0/${this.options.maxParks})

-
-
- -

Search and select parks to build your road trip route

-
-
-
- - -
-
- `; - - this.container.innerHTML = html; - } - - /** - * Bind event handlers - */ - bindEvents() { - // Park search - const searchInput = document.getElementById('park-search'); - if (searchInput) { - let searchTimeout; - searchInput.addEventListener('input', (e) => { - clearTimeout(searchTimeout); - searchTimeout = setTimeout(() => { - this.searchParks(e.target.value); - }, 300); - }); - } - - // Route controls - const optimizeBtn = document.getElementById('optimize-route'); - if (optimizeBtn) { - optimizeBtn.addEventListener('click', () => this.optimizeRoute()); - } - - const clearBtn = document.getElementById('clear-route'); - if (clearBtn) { - clearBtn.addEventListener('click', () => this.clearRoute()); - } - - // Export buttons - const exportGpxBtn = document.getElementById('export-gpx'); - if (exportGpxBtn) { - exportGpxBtn.addEventListener('click', () => this.exportRoute('gpx')); - } - - const exportKmlBtn = document.getElementById('export-kml'); - if (exportKmlBtn) { - exportKmlBtn.addEventListener('click', () => this.exportRoute('kml')); - } - - const shareBtn = document.getElementById('share-route'); - if (shareBtn) { - shareBtn.addEventListener('click', () => this.shareRoute()); - } - - // Make parks list sortable - this.initializeSortable(); - } - - /** - * Initialize drag-and-drop sorting for parks list - */ - initializeSortable() { - const parksList = document.getElementById('parks-list'); - if (!parksList) return; - - // Simple drag and drop implementation - let draggedElement = null; - - parksList.addEventListener('dragstart', (e) => { - if (e.target.classList.contains('park-item')) { - draggedElement = e.target; - e.target.style.opacity = '0.5'; - } - }); - - parksList.addEventListener('dragend', (e) => { - if (e.target.classList.contains('park-item')) { - e.target.style.opacity = '1'; - draggedElement = null; - } - }); - - parksList.addEventListener('dragover', (e) => { - e.preventDefault(); - }); - - parksList.addEventListener('drop', (e) => { - e.preventDefault(); - - if (draggedElement && e.target.classList.contains('park-item')) { - const afterElement = this.getDragAfterElement(parksList, e.clientY); - - if (afterElement == null) { - parksList.appendChild(draggedElement); - } else { - parksList.insertBefore(draggedElement, afterElement); - } - - this.reorderParks(); - } - }); - } - - /** - * Get the element to insert after during drag and drop - */ - getDragAfterElement(container, y) { - const draggableElements = [...container.querySelectorAll('.park-item:not(.dragging)')]; - - return draggableElements.reduce((closest, child) => { - const box = child.getBoundingClientRect(); - const offset = y - box.top - box.height / 2; - - if (offset < 0 && offset > closest.offset) { - return { offset: offset, element: child }; - } else { - return closest; - } - }, { offset: Number.NEGATIVE_INFINITY }).element; - } - - /** - * Search for parks - */ - async searchParks(query) { - if (!query.trim()) { - document.getElementById('park-search-results').innerHTML = ''; - return; - } - - try { - const response = await fetch(`${this.options.apiEndpoints.parks}?q=${encodeURIComponent(query)}&limit=10`); - const data = await response.json(); - - if (data.status === 'success') { - this.displaySearchResults(data.data); - } - } catch (error) { - console.error('Failed to search parks:', error); - } - } - - /** - * Display park search results - */ - displaySearchResults(parks) { - const resultsContainer = document.getElementById('park-search-results'); - - if (parks.length === 0) { - resultsContainer.innerHTML = '
No parks found
'; - return; - } - - const html = parks - .filter(park => !this.isParkSelected(park.id)) - .map(park => ` -
-
-
${park.name}
-
${park.formatted_location || ''}
-
- -
- `).join(''); - - resultsContainer.innerHTML = html; - } - - /** - * Check if a park is already selected - */ - isParkSelected(parkId) { - return this.selectedParks.some(park => park.id === parkId); - } - - /** - * Add a park to the route - */ - async addPark(parkId) { - if (this.selectedParks.length >= this.options.maxParks) { - this.showMessage(`Maximum ${this.options.maxParks} parks allowed`, 'warning'); - return; - } - - try { - const response = await fetch(`${this.options.apiEndpoints.parks}${parkId}/`); - const data = await response.json(); - - if (data.status === 'success') { - const park = data.data; - this.selectedParks.push(park); - this.updateParksDisplay(); - this.addParkMarker(park); - this.updateRoute(); - - // Clear search - document.getElementById('park-search').value = ''; - document.getElementById('park-search-results').innerHTML = ''; - } - } catch (error) { - console.error('Failed to add park:', error); - } - } - - /** - * Remove a park from the route - */ - removePark(parkId) { - const index = this.selectedParks.findIndex(park => park.id === parkId); - if (index > -1) { - this.selectedParks.splice(index, 1); - this.updateParksDisplay(); - this.removeParkMarker(parkId); - this.updateRoute(); - } - } - - /** - * Update the parks display - */ - updateParksDisplay() { - const parksList = document.getElementById('parks-list'); - const parkCount = document.getElementById('park-count'); - - parkCount.textContent = this.selectedParks.length; - - if (this.selectedParks.length === 0) { - parksList.innerHTML = ` -
- -

Search and select parks to build your road trip route

-
- `; - this.updateControls(); - return; - } - - const html = this.selectedParks.map((park, index) => ` -
-
${index + 1}
-
-
${park.name}
-
${park.formatted_location || ''}
- ${park.distance_from_previous ? `
${park.distance_from_previous}
` : ''} -
-
- -
-
- `).join(''); - - parksList.innerHTML = html; - this.updateControls(); - } - - /** - * Update control buttons state - */ - updateControls() { - const optimizeBtn = document.getElementById('optimize-route'); - const clearBtn = document.getElementById('clear-route'); - - const hasParks = this.selectedParks.length > 0; - const canOptimize = this.selectedParks.length > 2; - - if (optimizeBtn) optimizeBtn.disabled = !canOptimize; - if (clearBtn) clearBtn.disabled = !hasParks; - } - - /** - * Reorder parks after drag and drop - */ - reorderParks() { - const parkItems = document.querySelectorAll('.park-item'); - const newOrder = []; - - parkItems.forEach(item => { - const parkId = parseInt(item.dataset.parkId); - const park = this.selectedParks.find(p => p.id === parkId); - if (park) { - newOrder.push(park); - } - }); - - this.selectedParks = newOrder; - this.updateRoute(); - } - - /** - * Update the route visualization - */ - async updateRoute() { - if (this.selectedParks.length < 2) { - this.clearRouteVisualization(); - this.updateRouteSummary(null); - return; - } - - try { - const parkIds = this.selectedParks.map(park => park.id); - const response = await fetch(`${this.options.apiEndpoints.route}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - }, - body: JSON.stringify({ parks: parkIds }) - }); - - const data = await response.json(); - - if (data.status === 'success') { - this.routeData = data.data; - this.visualizeRoute(data.data); - this.updateRouteSummary(data.data); - } - } catch (error) { - console.error('Failed to calculate route:', error); - } - } - - /** - * Visualize the route on the map - */ - visualizeRoute(routeData) { - if (!this.mapInstance) return; - - // Clear existing route - this.clearRouteVisualization(); - - if (routeData.coordinates) { - // Create polyline from coordinates - this.routePolyline = L.polyline(routeData.coordinates, this.options.routeOptions); - this.routePolyline.addTo(this.mapInstance); - - // Fit map to route bounds - if (routeData.coordinates.length > 0) { - this.mapInstance.fitBounds(this.routePolyline.getBounds(), { padding: [20, 20] }); - } - } - } - - /** - * Clear route visualization - */ - clearRouteVisualization() { - if (this.routePolyline && this.mapInstance) { - this.mapInstance.removeLayer(this.routePolyline); - this.routePolyline = null; - } - } - - /** - * Update route summary display - */ - updateRouteSummary(routeData) { - const summarySection = document.getElementById('route-summary'); - - if (!routeData || this.selectedParks.length < 2) { - summarySection.style.display = 'none'; - return; - } - - summarySection.style.display = 'block'; - - document.getElementById('total-distance').textContent = routeData.total_distance || '-'; - document.getElementById('total-time').textContent = routeData.total_time || '-'; - document.getElementById('total-parks').textContent = this.selectedParks.length; - } - - /** - * Optimize the route order - */ - async optimizeRoute() { - if (this.selectedParks.length < 3) return; - - try { - const parkIds = this.selectedParks.map(park => park.id); - const response = await fetch(`${this.options.apiEndpoints.optimize}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - }, - body: JSON.stringify({ parks: parkIds }) - }); - - const data = await response.json(); - - if (data.status === 'success') { - // Reorder parks based on optimization - const optimizedOrder = data.data.optimized_order; - this.selectedParks = optimizedOrder.map(id => - this.selectedParks.find(park => park.id === id) - ).filter(Boolean); - - this.updateParksDisplay(); - this.updateRoute(); - this.showMessage('Route optimized for shortest distance', 'success'); - } - } catch (error) { - console.error('Failed to optimize route:', error); - this.showMessage('Failed to optimize route', 'error'); - } - } - - /** - * Clear the entire route - */ - clearRoute() { - this.selectedParks = []; - this.clearAllParkMarkers(); - this.clearRouteVisualization(); - this.updateParksDisplay(); - this.updateRouteSummary(null); - } - - /** - * Export route in specified format - */ - async exportRoute(format) { - if (!this.routeData) { - this.showMessage('No route to export', 'warning'); - return; - } - - try { - const response = await fetch(`${this.options.apiEndpoints.export}${format}/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCsrfToken() - }, - body: JSON.stringify({ - parks: this.selectedParks.map(p => p.id), - route_data: this.routeData - }) - }); - - if (response.ok) { - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `thrillwiki-roadtrip.${format}`; - a.click(); - window.URL.revokeObjectURL(url); - } - } catch (error) { - console.error('Failed to export route:', error); - this.showMessage('Failed to export route', 'error'); - } - } - - /** - * Share the route - */ - shareRoute() { - if (this.selectedParks.length === 0) { - this.showMessage('No route to share', 'warning'); - return; - } - - const parkIds = this.selectedParks.map(p => p.id).join(','); - const url = `${window.location.origin}/roadtrip/?parks=${parkIds}`; - - if (navigator.share) { - navigator.share({ - title: 'ThrillWiki Road Trip', - text: `Check out this ${this.selectedParks.length}-park road trip!`, - url: url - }); - } else { - // Fallback to clipboard - navigator.clipboard.writeText(url).then(() => { - this.showMessage('Route URL copied to clipboard', 'success'); - }).catch(() => { - // Manual selection fallback - const textarea = document.createElement('textarea'); - textarea.value = url; - document.body.appendChild(textarea); - textarea.select(); - document.execCommand('copy'); - document.body.removeChild(textarea); - this.showMessage('Route URL copied to clipboard', 'success'); - }); - } - } - - /** - * Add park marker to map - */ - addParkMarker(park) { - if (!this.mapInstance) return; - - const marker = L.marker([park.latitude, park.longitude], { - icon: this.createParkIcon(park) - }); - - marker.bindPopup(` -
-

${park.name}

-

${park.formatted_location || ''}

- -
- `); - - marker.addTo(this.mapInstance); - this.parkMarkers.set(park.id, marker); - } - - /** - * Remove park marker from map - */ - removeParkMarker(parkId) { - if (this.parkMarkers.has(parkId) && this.mapInstance) { - this.mapInstance.removeLayer(this.parkMarkers.get(parkId)); - this.parkMarkers.delete(parkId); - } - } - - /** - * Clear all park markers - */ - clearAllParkMarkers() { - this.parkMarkers.forEach(marker => { - if (this.mapInstance) { - this.mapInstance.removeLayer(marker); - } - }); - this.parkMarkers.clear(); - } - - /** - * Create custom icon for park marker - */ - createParkIcon(park) { - const index = this.selectedParks.findIndex(p => p.id === park.id) + 1; - - return L.divIcon({ - className: 'roadtrip-park-marker', - html: `
${index}
`, - iconSize: [30, 30], - iconAnchor: [15, 15] - }); - } - - /** - * Connect to a map instance - */ - connectToMap(mapInstance) { - this.mapInstance = mapInstance; - this.options.mapInstance = mapInstance; - } - - /** - * Load initial data (from URL parameters) - */ - loadInitialData() { - const urlParams = new URLSearchParams(window.location.search); - const parkIds = urlParams.get('parks'); - - if (parkIds) { - const ids = parkIds.split(',').map(id => parseInt(id)).filter(id => !isNaN(id)); - this.loadParksById(ids); - } - } - - /** - * Load parks by IDs - */ - async loadParksById(parkIds) { - try { - const promises = parkIds.map(id => - fetch(`${this.options.apiEndpoints.parks}${id}/`) - .then(res => res.json()) - .then(data => data.status === 'success' ? data.data : null) - ); - - const parks = (await Promise.all(promises)).filter(Boolean); - - this.selectedParks = parks; - this.updateParksDisplay(); - - // Add markers and update route - parks.forEach(park => this.addParkMarker(park)); - this.updateRoute(); - - } catch (error) { - console.error('Failed to load parks:', error); - } - } - - /** - * Get CSRF token for POST requests - */ - getCsrfToken() { - const token = document.querySelector('[name=csrfmiddlewaretoken]'); - return token ? token.value : ''; - } - - /** - * Show message to user - */ - showMessage(message, type = 'info') { - // Create or update message element - let messageEl = this.container.querySelector('.roadtrip-message'); - if (!messageEl) { - messageEl = document.createElement('div'); - messageEl.className = 'roadtrip-message'; - this.container.insertBefore(messageEl, this.container.firstChild); - } - - messageEl.textContent = message; - messageEl.className = `roadtrip-message roadtrip-message-${type}`; - - // Auto-hide after delay - setTimeout(() => { - if (messageEl.parentNode) { - messageEl.remove(); - } - }, 5000); - } -} - -// Auto-initialize road trip planner -document.addEventListener('DOMContentLoaded', function() { - const roadtripContainer = document.getElementById('roadtrip-planner'); - if (roadtripContainer) { - window.roadTripPlanner = new RoadTripPlanner('roadtrip-planner', { - mapInstance: window.thrillwikiMap || null - }); - } -}); - -// Export for use in other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = RoadTripPlanner; -} else { - window.RoadTripPlanner = RoadTripPlanner; -} \ No newline at end of file diff --git a/static/js/backup/search.js b/static/js/backup/search.js deleted file mode 100644 index b6589dbd..00000000 --- a/static/js/backup/search.js +++ /dev/null @@ -1,42 +0,0 @@ -function parkSearch() { - return { - query: '', - results: [], - loading: false, - selectedId: null, - - async search() { - if (!this.query.trim()) { - this.results = []; - return; - } - - this.loading = true; - try { - const response = await fetch(`/parks/suggest_parks/?search=${encodeURIComponent(this.query)}`); - const data = await response.json(); - this.results = data.results; - } catch (error) { - console.error('Search failed:', error); - this.results = []; - } finally { - this.loading = false; - } - }, - - clear() { - this.query = ''; - this.results = []; - this.selectedId = null; - }, - - selectPark(park) { - this.query = park.name; - this.selectedId = park.id; - this.results = []; - - // Trigger filter update - document.getElementById('park-filters').dispatchEvent(new Event('change')); - } - }; -} \ No newline at end of file diff --git a/static/js/backup/theme.js b/static/js/backup/theme.js deleted file mode 100644 index 44fa008b..00000000 --- a/static/js/backup/theme.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Theme management script - * Prevents flash of wrong theme by setting theme class immediately - */ -(function() { - let theme = localStorage.getItem("theme"); - if (!theme) { - theme = window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light"; - localStorage.setItem("theme", theme); - } - if (theme === "dark") { - document.documentElement.classList.add("dark"); - } -})(); \ No newline at end of file diff --git a/static/js/backup/thrillwiki-enhanced.js b/static/js/backup/thrillwiki-enhanced.js deleted file mode 100644 index 4e8390b5..00000000 --- a/static/js/backup/thrillwiki-enhanced.js +++ /dev/null @@ -1,799 +0,0 @@ -/** - * ThrillWiki Enhanced JavaScript - * Advanced interactions, animations, and UI enhancements - * Last Updated: 2025-01-15 - */ - -// Global ThrillWiki namespace -window.ThrillWiki = window.ThrillWiki || {}; - -(function(TW) { - 'use strict'; - - // Configuration - TW.config = { - animationDuration: 300, - scrollOffset: 80, - debounceDelay: 300, - apiEndpoints: { - search: '/api/search/', - favorites: '/api/favorites/', - notifications: '/api/notifications/' - } - }; - - // Utility functions - TW.utils = { - // Debounce function for performance - debounce: function(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - }, - - // Throttle function for scroll events - throttle: function(func, limit) { - let inThrottle; - return function() { - const args = arguments; - const context = this; - if (!inThrottle) { - func.apply(context, args); - inThrottle = true; - setTimeout(() => inThrottle = false, limit); - } - }; - }, - - // Smooth scroll to element - scrollTo: function(element, offset = TW.config.scrollOffset) { - const targetPosition = element.offsetTop - offset; - window.scrollTo({ - top: targetPosition, - behavior: 'smooth' - }); - }, - - // Check if element is in viewport - isInViewport: function(element) { - const rect = element.getBoundingClientRect(); - return ( - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && - rect.right <= (window.innerWidth || document.documentElement.clientWidth) - ); - }, - - // Format numbers with commas - formatNumber: function(num) { - return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); - }, - - // Generate unique ID - generateId: function() { - return 'tw-' + Math.random().toString(36).substr(2, 9); - } - }; - - // Animation system - TW.animations = { - // Fade in animation - fadeIn: function(element, duration = TW.config.animationDuration) { - element.style.opacity = '0'; - element.style.display = 'block'; - - const fadeEffect = setInterval(() => { - if (!element.style.opacity) { - element.style.opacity = 0; - } - if (element.style.opacity < 1) { - element.style.opacity = parseFloat(element.style.opacity) + 0.1; - } else { - clearInterval(fadeEffect); - } - }, duration / 10); - }, - - // Slide in from bottom - slideInUp: function(element, duration = TW.config.animationDuration) { - element.style.transform = 'translateY(30px)'; - element.style.opacity = '0'; - element.style.transition = `all ${duration}ms cubic-bezier(0.16, 1, 0.3, 1)`; - - setTimeout(() => { - element.style.transform = 'translateY(0)'; - element.style.opacity = '1'; - }, 10); - }, - - // Pulse effect - pulse: function(element, intensity = 1.05) { - element.style.transition = 'transform 0.15s ease-out'; - element.style.transform = `scale(${intensity})`; - - setTimeout(() => { - element.style.transform = 'scale(1)'; - }, 150); - }, - - // Shake effect for errors - shake: function(element) { - element.style.animation = 'shake 0.5s ease-in-out'; - setTimeout(() => { - element.style.animation = ''; - }, 500); - } - }; - - // Enhanced search functionality - TW.search = { - init: function() { - this.setupQuickSearch(); - this.setupAdvancedSearch(); - this.setupSearchSuggestions(); - }, - - setupQuickSearch: function() { - const quickSearchInputs = document.querySelectorAll('[data-quick-search]'); - - quickSearchInputs.forEach(input => { - const debouncedSearch = TW.utils.debounce(this.performQuickSearch.bind(this), TW.config.debounceDelay); - - input.addEventListener('input', (e) => { - const query = e.target.value.trim(); - if (query.length >= 2) { - debouncedSearch(query, e.target); - } else { - this.clearSearchResults(e.target); - } - }); - - // Handle keyboard navigation - input.addEventListener('keydown', this.handleSearchKeyboard.bind(this)); - }); - }, - - performQuickSearch: function(query, inputElement) { - const resultsContainer = document.getElementById(inputElement.dataset.quickSearch); - if (!resultsContainer) return; - - // Show loading state - resultsContainer.innerHTML = this.getLoadingHTML(); - resultsContainer.classList.remove('hidden'); - - // Perform search - fetch(`${TW.config.apiEndpoints.search}?q=${encodeURIComponent(query)}`) - .then(response => response.json()) - .then(data => { - this.displaySearchResults(data, resultsContainer); - }) - .catch(error => { - console.error('Search error:', error); - resultsContainer.innerHTML = this.getErrorHTML(); - }); - }, - - displaySearchResults: function(data, container) { - if (!data.results || data.results.length === 0) { - container.innerHTML = this.getNoResultsHTML(); - return; - } - - let html = '
'; - - // Group results by type - const groupedResults = this.groupResultsByType(data.results); - - Object.keys(groupedResults).forEach(type => { - if (groupedResults[type].length > 0) { - html += `
-

${this.getTypeTitle(type)}

-
`; - - groupedResults[type].forEach(result => { - html += this.getResultItemHTML(result); - }); - - html += '
'; - } - }); - - html += '
'; - container.innerHTML = html; - - // Add click handlers - this.attachResultClickHandlers(container); - }, - - getResultItemHTML: function(result) { - return ` -
-
- -
-
-
${result.name}
-
${result.subtitle || ''}
-
- ${result.image ? `
- ${result.name} -
` : ''} -
- `; - }, - - groupResultsByType: function(results) { - return results.reduce((groups, result) => { - const type = result.type || 'other'; - if (!groups[type]) groups[type] = []; - groups[type].push(result); - return groups; - }, {}); - }, - - getTypeTitle: function(type) { - const titles = { - 'park': 'Theme Parks', - 'ride': 'Rides & Attractions', - 'location': 'Locations', - 'other': 'Other Results' - }; - return titles[type] || 'Results'; - }, - - getTypeIcon: function(type) { - const icons = { - 'park': 'map-marked-alt', - 'ride': 'rocket', - 'location': 'map-marker-alt', - 'other': 'search' - }; - return icons[type] || 'search'; - }, - - getLoadingHTML: function() { - return ` -
-
- -
- Searching... -
- `; - }, - - getNoResultsHTML: function() { - return ` -
- - No results found -
- `; - }, - - getErrorHTML: function() { - return ` -
- - Search error. Please try again. -
- `; - }, - - attachResultClickHandlers: function(container) { - const resultItems = container.querySelectorAll('.search-result-item'); - - resultItems.forEach(item => { - item.addEventListener('click', (e) => { - const url = item.dataset.url; - if (url) { - // Use HTMX if available, otherwise navigate normally - if (window.htmx) { - htmx.ajax('GET', url, { - target: '#main-content', - swap: 'innerHTML transition:true' - }); - } else { - window.location.href = url; - } - - // Clear search - this.clearSearchResults(container.previousElementSibling); - } - }); - }); - }, - - clearSearchResults: function(inputElement) { - const resultsContainer = document.getElementById(inputElement.dataset.quickSearch); - if (resultsContainer) { - resultsContainer.classList.add('hidden'); - resultsContainer.innerHTML = ''; - } - }, - - handleSearchKeyboard: function(e) { - // Handle escape key to close results - if (e.key === 'Escape') { - this.clearSearchResults(e.target); - } - } - }; - - // Enhanced card interactions - TW.cards = { - init: function() { - this.setupCardHovers(); - this.setupFavoriteButtons(); - this.setupCardAnimations(); - }, - - setupCardHovers: function() { - const cards = document.querySelectorAll('.card-park, .card-ride, .card-feature'); - - cards.forEach(card => { - card.addEventListener('mouseenter', () => { - this.onCardHover(card); - }); - - card.addEventListener('mouseleave', () => { - this.onCardLeave(card); - }); - }); - }, - - onCardHover: function(card) { - // Add subtle glow effect - card.style.boxShadow = '0 20px 40px rgba(99, 102, 241, 0.15)'; - - // Animate card image if present - const image = card.querySelector('.card-park-image, .card-ride-image'); - if (image) { - image.style.transform = 'scale(1.05)'; - } - - // Show hidden elements - const hiddenElements = card.querySelectorAll('.opacity-0'); - hiddenElements.forEach(el => { - el.style.opacity = '1'; - el.style.transform = 'translateY(0)'; - }); - }, - - onCardLeave: function(card) { - // Reset styles - card.style.boxShadow = ''; - - const image = card.querySelector('.card-park-image, .card-ride-image'); - if (image) { - image.style.transform = ''; - } - }, - - setupFavoriteButtons: function() { - document.addEventListener('click', (e) => { - if (e.target.closest('[data-favorite-toggle]')) { - e.preventDefault(); - e.stopPropagation(); - - const button = e.target.closest('[data-favorite-toggle]'); - this.toggleFavorite(button); - } - }); - }, - - toggleFavorite: function(button) { - const itemId = button.dataset.favoriteToggle; - const itemType = button.dataset.favoriteType || 'park'; - - // Optimistic UI update - const icon = button.querySelector('i'); - const isFavorited = icon.classList.contains('fas'); - - if (isFavorited) { - icon.classList.remove('fas', 'text-red-500'); - icon.classList.add('far', 'text-neutral-600', 'dark:text-neutral-400'); - } else { - icon.classList.remove('far', 'text-neutral-600', 'dark:text-neutral-400'); - icon.classList.add('fas', 'text-red-500'); - } - - // Animate button - TW.animations.pulse(button, 1.2); - - // Send request to server - fetch(`${TW.config.apiEndpoints.favorites}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': this.getCSRFToken() - }, - body: JSON.stringify({ - item_id: itemId, - item_type: itemType, - action: isFavorited ? 'remove' : 'add' - }) - }) - .then(response => response.json()) - .then(data => { - if (!data.success) { - // Revert optimistic update - if (isFavorited) { - icon.classList.remove('far', 'text-neutral-600', 'dark:text-neutral-400'); - icon.classList.add('fas', 'text-red-500'); - } else { - icon.classList.remove('fas', 'text-red-500'); - icon.classList.add('far', 'text-neutral-600', 'dark:text-neutral-400'); - } - - TW.notifications.show('Error updating favorite', 'error'); - } - }) - .catch(error => { - console.error('Favorite toggle error:', error); - TW.notifications.show('Error updating favorite', 'error'); - }); - }, - - getCSRFToken: function() { - const token = document.querySelector('[name=csrfmiddlewaretoken]'); - return token ? token.value : ''; - } - }; - - // Enhanced notifications system - TW.notifications = { - container: null, - - init: function() { - this.createContainer(); - this.setupAutoHide(); - }, - - createContainer: function() { - if (!this.container) { - this.container = document.createElement('div'); - this.container.id = 'tw-notifications'; - this.container.className = 'fixed top-4 right-4 z-50 space-y-4'; - document.body.appendChild(this.container); - } - }, - - show: function(message, type = 'info', duration = 5000) { - const notification = this.createNotification(message, type); - this.container.appendChild(notification); - - // Animate in - setTimeout(() => { - notification.classList.add('show'); - }, 10); - - // Auto hide - if (duration > 0) { - setTimeout(() => { - this.hide(notification); - }, duration); - } - - return notification; - }, - - createNotification: function(message, type) { - const notification = document.createElement('div'); - notification.className = `notification notification-${type}`; - - const typeIcons = { - 'success': 'check-circle', - 'error': 'exclamation-circle', - 'warning': 'exclamation-triangle', - 'info': 'info-circle' - }; - - notification.innerHTML = ` -
- - ${message} - -
- `; - - return notification; - }, - - hide: function(notification) { - notification.classList.add('hide'); - setTimeout(() => { - if (notification.parentNode) { - notification.parentNode.removeChild(notification); - } - }, 300); - }, - - setupAutoHide: function() { - // Auto-hide notifications on page navigation - if (window.htmx) { - document.addEventListener('htmx:beforeRequest', () => { - const notifications = this.container.querySelectorAll('.notification'); - notifications.forEach(notification => { - this.hide(notification); - }); - }); - } - } - }; - - // Enhanced scroll effects - TW.scroll = { - init: function() { - this.setupParallax(); - this.setupRevealAnimations(); - this.setupScrollToTop(); - }, - - setupParallax: function() { - const parallaxElements = document.querySelectorAll('[data-parallax]'); - - if (parallaxElements.length > 0) { - const handleScroll = TW.utils.throttle(() => { - const scrolled = window.pageYOffset; - - parallaxElements.forEach(element => { - const speed = parseFloat(element.dataset.parallax) || 0.5; - const yPos = -(scrolled * speed); - element.style.transform = `translateY(${yPos}px)`; - }); - }, 16); // ~60fps - - window.addEventListener('scroll', handleScroll); - } - }, - - setupRevealAnimations: function() { - const revealElements = document.querySelectorAll('[data-reveal]'); - - if (revealElements.length > 0) { - const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - const element = entry.target; - const animationType = element.dataset.reveal || 'fadeIn'; - const delay = parseInt(element.dataset.revealDelay) || 0; - - setTimeout(() => { - element.classList.add('revealed'); - - if (TW.animations[animationType]) { - TW.animations[animationType](element); - } - }, delay); - - observer.unobserve(element); - } - }); - }, { - threshold: 0.1, - rootMargin: '0px 0px -50px 0px' - }); - - revealElements.forEach(element => { - observer.observe(element); - }); - } - }, - - setupScrollToTop: function() { - const scrollToTopBtn = document.createElement('button'); - scrollToTopBtn.id = 'scroll-to-top'; - scrollToTopBtn.className = 'fixed bottom-8 right-8 w-12 h-12 bg-thrill-primary text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-300 opacity-0 pointer-events-none z-40'; - scrollToTopBtn.innerHTML = ''; - scrollToTopBtn.setAttribute('aria-label', 'Scroll to top'); - - document.body.appendChild(scrollToTopBtn); - - const handleScroll = TW.utils.throttle(() => { - if (window.pageYOffset > 300) { - scrollToTopBtn.classList.remove('opacity-0', 'pointer-events-none'); - scrollToTopBtn.classList.add('opacity-100'); - } else { - scrollToTopBtn.classList.add('opacity-0', 'pointer-events-none'); - scrollToTopBtn.classList.remove('opacity-100'); - } - }, 100); - - window.addEventListener('scroll', handleScroll); - - scrollToTopBtn.addEventListener('click', () => { - window.scrollTo({ - top: 0, - behavior: 'smooth' - }); - }); - } - }; - - // Enhanced form handling - TW.forms = { - init: function() { - this.setupFormValidation(); - this.setupFormAnimations(); - this.setupFileUploads(); - }, - - setupFormValidation: function() { - const forms = document.querySelectorAll('form[data-validate]'); - - forms.forEach(form => { - form.addEventListener('submit', (e) => { - if (!this.validateForm(form)) { - e.preventDefault(); - TW.animations.shake(form); - } - }); - - // Real-time validation - const inputs = form.querySelectorAll('input, textarea, select'); - inputs.forEach(input => { - input.addEventListener('blur', () => { - this.validateField(input); - }); - }); - }); - }, - - validateForm: function(form) { - let isValid = true; - const inputs = form.querySelectorAll('input[required], textarea[required], select[required]'); - - inputs.forEach(input => { - if (!this.validateField(input)) { - isValid = false; - } - }); - - return isValid; - }, - - validateField: function(field) { - const value = field.value.trim(); - const isRequired = field.hasAttribute('required'); - const type = field.type; - - let isValid = true; - let errorMessage = ''; - - // Required validation - if (isRequired && !value) { - isValid = false; - errorMessage = 'This field is required'; - } - - // Type-specific validation - if (value && type === 'email') { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(value)) { - isValid = false; - errorMessage = 'Please enter a valid email address'; - } - } - - // Update field appearance - this.updateFieldValidation(field, isValid, errorMessage); - - return isValid; - }, - - updateFieldValidation: function(field, isValid, errorMessage) { - const fieldGroup = field.closest('.form-group'); - if (!fieldGroup) return; - - // Remove existing error states - field.classList.remove('form-input-error'); - const existingError = fieldGroup.querySelector('.form-error'); - if (existingError) { - existingError.remove(); - } - - if (!isValid) { - field.classList.add('form-input-error'); - - const errorElement = document.createElement('div'); - errorElement.className = 'form-error'; - errorElement.textContent = errorMessage; - - fieldGroup.appendChild(errorElement); - TW.animations.slideInUp(errorElement, 200); - } - } - }; - - // Initialize all modules - TW.init = function() { - // Wait for DOM to be ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', this.initModules.bind(this)); - } else { - this.initModules(); - } - }; - - TW.initModules = function() { - console.log('🎢 ThrillWiki Enhanced JavaScript initialized'); - - // Initialize all modules - TW.search.init(); - TW.cards.init(); - TW.notifications.init(); - TW.scroll.init(); - TW.forms.init(); - - // Setup HTMX enhancements - if (window.htmx) { - this.setupHTMXEnhancements(); - } - - // Setup global error handling - this.setupErrorHandling(); - }; - - TW.setupHTMXEnhancements = function() { - // Global HTMX configuration - htmx.config.globalViewTransitions = true; - htmx.config.scrollBehavior = 'smooth'; - - // Enhanced loading states - document.addEventListener('htmx:beforeRequest', (e) => { - const target = e.target; - target.classList.add('htmx-request'); - }); - - document.addEventListener('htmx:afterRequest', (e) => { - const target = e.target; - target.classList.remove('htmx-request'); - }); - - // Re-initialize components after HTMX swaps - document.addEventListener('htmx:afterSwap', (e) => { - // Re-initialize cards in the swapped content - const newCards = e.detail.target.querySelectorAll('.card-park, .card-ride, .card-feature'); - if (newCards.length > 0) { - TW.cards.setupCardHovers(); - } - - // Re-initialize forms - const newForms = e.detail.target.querySelectorAll('form[data-validate]'); - if (newForms.length > 0) { - TW.forms.setupFormValidation(); - } - }); - }; - - TW.setupErrorHandling = function() { - window.addEventListener('error', (e) => { - console.error('ThrillWiki Error:', e.error); - // Could send to error tracking service here - }); - - window.addEventListener('unhandledrejection', (e) => { - console.error('ThrillWiki Promise Rejection:', e.reason); - // Could send to error tracking service here - }); - }; - - // Auto-initialize - TW.init(); - -})(window.ThrillWiki); - -// Export for module systems -if (typeof module !== 'undefined' && module.exports) { - module.exports = window.ThrillWiki; -} diff --git a/templates/base/base.html b/templates/base/base.html index 2a6a910e..0ad145ab 100644 --- a/templates/base/base.html +++ b/templates/base/base.html @@ -172,347 +172,41 @@ - - + + Alpine.store('toast', { + toasts: [], + show(message, type = 'info', duration = 5000) { + const id = Date.now() + Math.random(); + const toast = { id, message, type, visible: true, progress: 100 }; + this.toasts.push(toast); + if (duration > 0) { + setTimeout(() => this.hide(id), duration); + } + return id; + }, + hide(id) { + const toast = this.toasts.find(t => t.id === id); + if (toast) { + toast.visible = false; + setTimeout(() => { + this.toasts = this.toasts.filter(t => t.id !== id); + }, 300); + } + } + }); + " style="display: none;"> {% block extra_js %}{% endblock %} diff --git a/templates/components/layout/enhanced_header.html b/templates/components/layout/enhanced_header.html index 4b3172c0..b3c73ec5 100644 --- a/templates/components/layout/enhanced_header.html +++ b/templates/components/layout/enhanced_header.html @@ -41,9 +41,9 @@