mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 10:31:08 -05:00
444 lines
12 KiB
JavaScript
444 lines
12 KiB
JavaScript
/**
|
|
* 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:
|
|
* <button @click="$store.theme.toggle()">Toggle Theme</button>
|
|
* <span x-text="$store.auth.user?.username"></span>
|
|
* <button @click="$store.toast.success('Saved!')">Save</button>
|
|
*/
|
|
|
|
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');
|
|
});
|