// 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'); });