mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 17:11:09 -05:00
- Implemented park card component with image, status badge, favorite button, and quick stats overlay. - Developed ride card component featuring thrill level badge, status badge, favorite button, and detailed stats. - Created advanced search page with filters for parks and rides, including location, type, status, and thrill level. - Added dynamic quick search functionality with results display. - Enhanced user experience with JavaScript for filter toggling, range slider updates, and view switching. - Included custom CSS for improved styling of checkboxes and search results layout.
800 lines
28 KiB
JavaScript
800 lines
28 KiB
JavaScript
/**
|
|
* 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 = '<div class="search-results-dropdown">';
|
|
|
|
// Group results by type
|
|
const groupedResults = this.groupResultsByType(data.results);
|
|
|
|
Object.keys(groupedResults).forEach(type => {
|
|
if (groupedResults[type].length > 0) {
|
|
html += `<div class="search-group">
|
|
<h4 class="search-group-title">${this.getTypeTitle(type)}</h4>
|
|
<div class="search-group-items">`;
|
|
|
|
groupedResults[type].forEach(result => {
|
|
html += this.getResultItemHTML(result);
|
|
});
|
|
|
|
html += '</div></div>';
|
|
}
|
|
});
|
|
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
|
|
// Add click handlers
|
|
this.attachResultClickHandlers(container);
|
|
},
|
|
|
|
getResultItemHTML: function(result) {
|
|
return `
|
|
<div class="search-result-item" data-url="${result.url}" data-type="${result.type}">
|
|
<div class="search-result-icon">
|
|
<i class="fas fa-${this.getTypeIcon(result.type)}"></i>
|
|
</div>
|
|
<div class="search-result-content">
|
|
<div class="search-result-title">${result.name}</div>
|
|
<div class="search-result-subtitle">${result.subtitle || ''}</div>
|
|
</div>
|
|
${result.image ? `<div class="search-result-image">
|
|
<img src="${result.image}" alt="${result.name}" loading="lazy">
|
|
</div>` : ''}
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
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 `
|
|
<div class="search-loading">
|
|
<div class="loading-spinner opacity-100">
|
|
<i class="fas fa-spinner text-thrill-primary"></i>
|
|
</div>
|
|
<span>Searching...</span>
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
getNoResultsHTML: function() {
|
|
return `
|
|
<div class="search-no-results">
|
|
<i class="fas fa-search text-neutral-400"></i>
|
|
<span>No results found</span>
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
getErrorHTML: function() {
|
|
return `
|
|
<div class="search-error">
|
|
<i class="fas fa-exclamation-triangle text-red-500"></i>
|
|
<span>Search error. Please try again.</span>
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
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 = `
|
|
<div class="notification-content">
|
|
<i class="fas fa-${typeIcons[type] || 'info-circle'} notification-icon"></i>
|
|
<span class="notification-message">${message}</span>
|
|
<button class="notification-close" onclick="ThrillWiki.notifications.hide(this.closest('.notification'))">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
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 = '<i class="fas fa-arrow-up"></i>';
|
|
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;
|
|
}
|