/** * ThrillWiki Alpine.js Global Stores * * This file is the single source of truth for all Alpine.js global stores. * Stores provide shared state management across components. * * Available Stores: * - $store.auth: User authentication state * - $store.theme: Theme (dark/light mode) management * - $store.search: Global search state * - $store.toast: Toast notification system * - $store.ui: UI state (sidebar, modals, etc.) * * Usage in templates: * * * */ document.addEventListener('alpine:init', () => { // ========================================================================= // Authentication Store // ========================================================================= Alpine.store('auth', { user: null, isAuthenticated: false, permissions: [], /** * Initialize auth state from server-rendered data * The server should render window.__AUTH_USER__ and window.__AUTH_PERMISSIONS__ */ init() { if (window.__AUTH_USER__) { this.user = window.__AUTH_USER__; this.isAuthenticated = true; this.permissions = window.__AUTH_PERMISSIONS__ || []; } }, /** * Check if user has a specific permission * @param {string} permission - Permission string to check * @returns {boolean} */ hasPermission(permission) { return this.permissions.includes(permission); }, /** * Check if user has any of the specified permissions * @param {string[]} permissions - Array of permission strings * @returns {boolean} */ hasAnyPermission(permissions) { return permissions.some(p => this.permissions.includes(p)); }, /** * Update user data (after login/profile update) * @param {Object} userData - User data object */ setUser(userData) { this.user = userData; this.isAuthenticated = !!userData; }, /** * Clear auth state (on logout) */ logout() { this.user = null; this.isAuthenticated = false; this.permissions = []; } }); // ========================================================================= // Theme Store // ========================================================================= Alpine.store('theme', { isDark: false, /** * Initialize theme from localStorage or system preference */ init() { const savedTheme = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; this.isDark = savedTheme === 'dark' || (!savedTheme && prefersDark); this.apply(); // Watch for system theme changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { if (!localStorage.getItem('theme')) { this.isDark = e.matches; this.apply(); } }); }, /** * Toggle between dark and light mode */ toggle() { this.isDark = !this.isDark; this.apply(); }, /** * Set a specific theme * @param {string} mode - 'dark', 'light', or 'system' */ setMode(mode) { if (mode === 'system') { localStorage.removeItem('theme'); this.isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; } else { this.isDark = mode === 'dark'; localStorage.setItem('theme', this.isDark ? 'dark' : 'light'); } this.apply(); }, /** * Apply the current theme to the document */ apply() { document.documentElement.classList.toggle('dark', this.isDark); localStorage.setItem('theme', this.isDark ? 'dark' : 'light'); } }); // ========================================================================= // Search Store // ========================================================================= Alpine.store('search', { query: '', results: [], isOpen: false, isLoading: false, filters: {}, /** * Toggle the search modal/dropdown */ toggle() { this.isOpen = !this.isOpen; if (this.isOpen) { // Focus search input when opening this.$nextTick(() => { const input = document.querySelector('[data-search-input]'); if (input) input.focus(); }); } }, /** * Open the search modal */ open() { this.isOpen = true; }, /** * Close the search modal */ close() { this.isOpen = false; }, /** * Clear the search query and results */ clearSearch() { this.query = ''; this.results = []; }, /** * Set a filter value * @param {string} key - Filter key * @param {*} value - Filter value */ setFilter(key, value) { this.filters[key] = value; }, /** * Clear all filters */ clearFilters() { this.filters = {}; } }); // ========================================================================= // Toast Store // ========================================================================= Alpine.store('toast', { toasts: [], /** * Show a toast notification * @param {string} message - Toast message * @param {string} type - Toast type (success, error, warning, info) * @param {number} duration - Duration in ms (0 for persistent) * @returns {number} - Toast ID for programmatic dismissal */ show(message, type = 'info', duration = 5000) { const id = Date.now() + Math.random(); const toast = { id, message, type, visible: true, duration }; this.toasts.push(toast); if (duration > 0) { setTimeout(() => { this.dismiss(id); }, duration); } return id; }, /** * Dismiss a toast by ID * @param {number} id - Toast ID */ dismiss(id) { const toast = this.toasts.find(t => t.id === id); if (toast) { toast.visible = false; // Remove from array after animation setTimeout(() => { this.toasts = this.toasts.filter(t => t.id !== id); }, 300); } }, /** * Show a success toast * @param {string} message * @param {number} duration */ success(message, duration = 5000) { return this.show(message, 'success', duration); }, /** * Show an error toast * @param {string} message * @param {number} duration */ error(message, duration = 7000) { return this.show(message, 'error', duration); }, /** * Show a warning toast * @param {string} message * @param {number} duration */ warning(message, duration = 6000) { return this.show(message, 'warning', duration); }, /** * Show an info toast * @param {string} message * @param {number} duration */ info(message, duration = 5000) { return this.show(message, 'info', duration); }, /** * Clear all toasts */ clearAll() { this.toasts = []; } }); // ========================================================================= // UI State Store // ========================================================================= Alpine.store('ui', { sidebarOpen: false, modalStack: [], activeDropdown: null, /** * Toggle the sidebar */ toggleSidebar() { this.sidebarOpen = !this.sidebarOpen; document.body.style.overflow = this.sidebarOpen ? 'hidden' : ''; }, /** * Open a modal by ID * @param {string} id - Modal ID */ openModal(id) { if (!this.modalStack.includes(id)) { this.modalStack.push(id); document.body.style.overflow = 'hidden'; } }, /** * Close a modal by ID * @param {string} id - Modal ID */ closeModal(id) { this.modalStack = this.modalStack.filter(m => m !== id); if (this.modalStack.length === 0) { document.body.style.overflow = ''; } }, /** * Close the topmost modal */ closeTopModal() { if (this.modalStack.length > 0) { this.closeModal(this.modalStack[this.modalStack.length - 1]); } }, /** * Check if a modal is open * @param {string} id - Modal ID * @returns {boolean} */ isModalOpen(id) { return this.modalStack.includes(id); }, /** * Check if any modal is open * @returns {boolean} */ hasOpenModal() { return this.modalStack.length > 0; }, /** * Set the active dropdown (closes others) * @param {string|null} id - Dropdown ID or null to close all */ setActiveDropdown(id) { this.activeDropdown = id; }, /** * Check if a dropdown is active * @param {string} id - Dropdown ID * @returns {boolean} */ isDropdownActive(id) { return this.activeDropdown === id; } }); // ========================================================================= // App Store (General application state) // ========================================================================= Alpine.store('app', { loading: false, notifications: [], /** * Set global loading state * @param {boolean} state */ setLoading(state) { this.loading = state; }, /** * Add a notification * @param {Object} notification */ addNotification(notification) { this.notifications.push({ id: Date.now(), read: false, timestamp: new Date(), ...notification }); }, /** * Mark a notification as read * @param {number} id */ markAsRead(id) { const notification = this.notifications.find(n => n.id === id); if (notification) { notification.read = true; } }, /** * Mark all notifications as read */ markAllAsRead() { this.notifications.forEach(n => n.read = true); }, /** * Remove a notification * @param {number} id */ removeNotification(id) { this.notifications = this.notifications.filter(n => n.id !== id); }, /** * Get unread notification count * @returns {number} */ get unreadCount() { return this.notifications.filter(n => !n.read).length; } }); console.log('Alpine.js stores initialized'); });