mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 05:11:09 -05:00
- 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.
546 lines
11 KiB
JavaScript
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');
|
|
});
|