/** * Alpine.js Components for ThrillWiki * Enhanced components matching React frontend functionality */ // Debug logging to see what's happening console.log('Alpine components script is loading...'); console.log('Alpine available?', typeof window.Alpine !== 'undefined'); // Try multiple approaches to ensure components register function registerComponents() { console.log('Registering components - Alpine available:', typeof Alpine !== 'undefined'); if (typeof Alpine === 'undefined') { console.error('Alpine still not available when trying to register components!'); return; } // 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; } })); // Enhanced Authentication Modal Component Alpine.data('authModal', (defaultMode = 'login') => ({ open: false, mode: defaultMode, // 'login' or 'register' showPassword: false, socialProviders: [], socialLoading: true, // Login form data loginForm: { username: '', password: '' }, loginLoading: false, loginError: '', // Register form data registerForm: { first_name: '', last_name: '', email: '', username: '', password1: '', password2: '' }, registerLoading: false, registerError: '', init() { this.fetchSocialProviders(); // Listen for auth modal events this.$watch('open', (value) => { if (value) { document.body.style.overflow = 'hidden'; } else { document.body.style.overflow = ''; this.resetForms(); } }); // No need for event listeners since x-init handles global exposure }, async fetchSocialProviders() { try { const response = await fetch('/api/v1/auth/social-providers/'); const data = await response.json(); this.socialProviders = data.available_providers || []; } catch (error) { console.error('Failed to fetch social providers:', error); this.socialProviders = []; } finally { this.socialLoading = false; } }, show(mode = 'login') { this.mode = mode; this.open = true; }, close() { this.open = false; }, switchToLogin() { this.mode = 'login'; this.resetForms(); }, switchToRegister() { this.mode = 'register'; this.resetForms(); }, resetForms() { this.loginForm = { username: '', password: '' }; this.registerForm = { first_name: '', last_name: '', email: '', username: '', password1: '', password2: '' }; this.loginError = ''; this.registerError = ''; this.showPassword = false; }, async handleLogin() { if (!this.loginForm.username || !this.loginForm.password) { this.loginError = 'Please fill in all fields'; return; } this.loginLoading = true; this.loginError = ''; try { const response = await fetch('/accounts/login/', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': this.getCSRFToken(), 'X-Requested-With': 'XMLHttpRequest' }, body: new URLSearchParams({ login: this.loginForm.username, password: this.loginForm.password }) }); if (response.ok) { // Login successful - reload page to update auth state window.location.reload(); } else { const data = await response.json(); this.loginError = data.message || 'Login failed. Please check your credentials.'; } } catch (error) { console.error('Login error:', error); this.loginError = 'An error occurred. Please try again.'; } finally { this.loginLoading = false; } }, async handleRegister() { if (!this.registerForm.first_name || !this.registerForm.last_name || !this.registerForm.email || !this.registerForm.username || !this.registerForm.password1 || !this.registerForm.password2) { this.registerError = 'Please fill in all fields'; return; } if (this.registerForm.password1 !== this.registerForm.password2) { this.registerError = 'Passwords do not match'; return; } this.registerLoading = true; this.registerError = ''; try { const response = await fetch('/accounts/signup/', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': this.getCSRFToken(), 'X-Requested-With': 'XMLHttpRequest' }, body: new URLSearchParams({ first_name: this.registerForm.first_name, last_name: this.registerForm.last_name, email: this.registerForm.email, username: this.registerForm.username, password1: this.registerForm.password1, password2: this.registerForm.password2 }) }); if (response.ok) { // Registration successful this.close(); // Show success message or redirect Alpine.store('toast').success('Account created successfully! Please check your email to verify your account.'); } else { const data = await response.json(); this.registerError = data.message || 'Registration failed. Please try again.'; } } catch (error) { console.error('Registration error:', error); this.registerError = 'An error occurred. Please try again.'; } finally { this.registerLoading = false; } }, handleSocialLogin(providerId) { const provider = this.socialProviders.find(p => p.id === providerId); if (!provider) { Alpine.store('toast').error(`Social provider ${providerId} not found.`); return; } // Redirect to social auth URL window.location.href = provider.auth_url; }, getCSRFToken() { const token = document.querySelector('[name=csrfmiddlewaretoken]')?.value || document.querySelector('meta[name=csrf-token]')?.getAttribute('content') || document.cookie.split('; ').find(row => row.startsWith('csrftoken='))?.split('=')[1]; return token || ''; } })); // 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); } }); console.log('All Alpine.js components registered successfully'); // Ensure global authModal is available immediately if (typeof window !== 'undefined') { // Create a simple proxy that will find the authModal component when called window.authModal = { show: (mode = 'login') => { console.log('Attempting to show auth modal:', mode); // Find the authModal component in the DOM const modalEl = document.querySelector('[x-data*="authModal"]'); if (modalEl && modalEl._x_dataStack && modalEl._x_dataStack[0]) { const component = modalEl._x_dataStack[0]; if (component.show && typeof component.show === 'function') { component.show(mode); console.log('Auth modal opened successfully'); return; } } // Fallback: try to find any component with a show method const elements = document.querySelectorAll('[x-data]'); for (let el of elements) { if (el._x_dataStack) { for (let stack of el._x_dataStack) { if (stack.show && stack.mode !== undefined) { stack.show(mode); console.log('Auth modal opened via fallback method'); return; } } } } console.error('Could not find authModal component to open'); } }; console.log('Global authModal proxy created'); } } // Try multiple registration approaches document.addEventListener('alpine:init', registerComponents); document.addEventListener('DOMContentLoaded', registerComponents); // Fallback - try after a delay setTimeout(() => { if (typeof Alpine !== 'undefined') { console.log('Fallback registration triggered'); registerComponents(); } }, 100);