Files
thrillwiki_django_no_react/static/js/backup/thrillwiki-enhanced.js
pacnpal b1c369c1bb Add park and ride card components with advanced search functionality
- 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.
2025-09-24 23:10:48 -04:00

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;
}