mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 06:51:09 -05:00
Add standardized HTMX conventions, interaction patterns, and migration guide for ThrillWiki UX
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,111 +1,443 @@
|
||||
/**
|
||||
* 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() {
|
||||
// Initialize from server-rendered data
|
||||
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() {
|
||||
this.isDark = localStorage.getItem('theme') === 'dark' ||
|
||||
(!localStorage.getItem('theme') &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
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 logic would go here
|
||||
// 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/Notification Store
|
||||
|
||||
// =========================================================================
|
||||
// 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();
|
||||
this.toasts.push({ id, message, type, duration });
|
||||
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);
|
||||
setTimeout(() => {
|
||||
this.dismiss(id);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Dismiss a toast by ID
|
||||
* @param {number} id - Toast ID
|
||||
*/
|
||||
dismiss(id) {
|
||||
this.toasts = this.toasts.filter(t => t.id !== 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);
|
||||
}
|
||||
},
|
||||
|
||||
success(message) { return this.show(message, 'success'); },
|
||||
error(message) { return this.show(message, 'error'); },
|
||||
warning(message) { return this.show(message, 'warning'); },
|
||||
info(message) { return this.show(message, 'info'); }
|
||||
|
||||
/**
|
||||
* 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: [],
|
||||
|
||||
openModal(id) {
|
||||
if (!this.modalStack.includes(id)) {
|
||||
this.modalStack.push(id);
|
||||
}
|
||||
},
|
||||
closeModal(id) {
|
||||
this.modalStack = this.modalStack.filter(m => m !== id);
|
||||
},
|
||||
isModalOpen(id) { return this.modalStack.includes(id); },
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user