Files
thrillwiki_django_no_react/backend/static/js/alpine-components.js
pacnpal b9063ff4f8 feat: Add detailed park and ride pages with HTMX integration
- Implemented park detail page with dynamic content loading for rides and weather.
- Created park list page with filters and search functionality.
- Developed ride detail page showcasing ride stats, reviews, and similar rides.
- Added ride list page with filtering options and dynamic loading.
- Introduced search results page with tabs for parks, rides, and users.
- Added HTMX tests for global search functionality.
2025-12-19 19:53:20 -05:00

546 lines
11 KiB
JavaScript

// Reduced Alpine components: keep only pure client-side UI state
document.addEventListener('alpine:init', () => {
Alpine.data('themeToggle', () => ({
theme: localStorage.getItem('theme') || 'system',
init() { this.updateTheme(); },
toggle() {
this.theme = this.theme === 'dark' ? 'light' : 'dark';
localStorage.setItem('theme', this.theme);
this.updateTheme();
},
updateTheme() {
if (this.theme === 'dark') document.documentElement.classList.add('dark');
else document.documentElement.classList.remove('dark');
}
}));
Alpine.data('mobileMenu', () => ({
open: false,
toggle() {
this.open = !this.open;
document.body.style.overflow = this.open ? 'hidden' : '';
}
}));
Alpine.data('dropdown', () => ({
open: false,
toggle() { this.open = !this.open; }
}));
});
/**
* Alpine.js Components for ThrillWiki
* Enhanced components matching React frontend functionality
*/
// 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 {
const response = await fetch(`/api/search/?q=${encodeURIComponent(this.query)}`);
const data = await response.json();
this.results = data.results || [];
this.showResults = this.results.length > 0;
} catch (error) {
console.error('Search error:', error);
this.results = [];
this.showResults = false;
} finally {
this.loading = false;
}
},
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;
}
}));
// Toast/Alert Component
Alpine.data('toast', () => ({
toasts: [],
show(message, type = 'info', duration = 5000) {
const id = Date.now();
const toast = { id, message, type, visible: true };
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); // Wait for animation
}
},
success(message, duration) {
return this.show(message, 'success', duration);
},
error(message, duration) {
return this.show(message, 'error', duration);
},
warning(message, duration) {
return this.show(message, 'warning', duration);
},
info(message, duration) {
return this.show(message, 'info', duration);
}
}));
// Enhanced Toast Component with Better UX
Alpine.data('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) {
// Animate progress bar
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);
}
}));
// 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);
}
});
// Initialize Alpine.js when DOM is ready
document.addEventListener('alpine:init', () => {
console.log('Alpine.js components initialized');
});