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(`
-
- `);
-
- // 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 ?
- `
` :
- ``;
- 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 = `
-
- `;
-
- 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(`
-
- `);
- }
-
- 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 `
-
- `;
- }
-
- /**
- * 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 = `
-
-
-
-
-
-
-
-
Your Route (0/${this.options.maxParks})
-
-
-
-
Search and select parks to build your road trip route
-
-
-
-
-
-
Trip Summary
-
-
- Total Distance:
- -
-
-
- Driving Time:
- -
-
-
- Parks:
- 0
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- 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(`
-
- `);
-
- 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 ? `
-

-
` : ''}
-
- `;
- },
-
- 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 `
-
- `;
- },
-
- 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 %}