mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 10:51:09 -05:00
712 lines
16 KiB
JavaScript
712 lines
16 KiB
JavaScript
/**
|
|
* 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 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();
|
|
}
|
|
});
|
|
},
|
|
|
|
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 || '';
|
|
}
|
|
}));
|
|
|
|
// 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');
|
|
});
|