/** * ThrillWiki Main JavaScript * * This file contains core functionality including: * - CSRF token handling for HTMX and AJAX requests * - Theme management * - Mobile menu functionality * - Flash message handling * - Tooltip initialization */ // ============================================================================= // CSRF Token Handling // ============================================================================= /** * Get CSRF token from cookies. * Django sets the CSRF token in a cookie named 'csrftoken'. * * @returns {string|null} The CSRF token or null if not found */ function getCookie(name) { let cookieValue = null; if (document.cookie && document.cookie !== '') { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim(); if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } /** * Configure HTMX to include CSRF token in all requests. * This handler runs before every HTMX request and adds the X-CSRFToken header. */ document.body.addEventListener('htmx:configRequest', (event) => { // Only add CSRF token for state-changing methods const method = event.detail.verb.toUpperCase(); if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) { // Try to get token from cookie first const csrfToken = getCookie('csrftoken'); if (csrfToken) { event.detail.headers['X-CSRFToken'] = csrfToken; } else { // Fallback: try to get from meta tag or hidden input const metaToken = document.querySelector('meta[name="csrf-token"]'); const inputToken = document.querySelector('input[name="csrfmiddlewaretoken"]'); if (metaToken) { event.detail.headers['X-CSRFToken'] = metaToken.getAttribute('content'); } else if (inputToken) { event.detail.headers['X-CSRFToken'] = inputToken.value; } } } }); // ============================================================================= // Theme Handling // ============================================================================= // Theme handling document.addEventListener('DOMContentLoaded', () => { const themeToggle = document.getElementById('theme-toggle'); const html = document.documentElement; // Initialize toggle state based on current theme if (themeToggle) { themeToggle.checked = html.classList.contains('dark'); // Handle toggle changes themeToggle.addEventListener('change', function() { const isDark = this.checked; html.classList.toggle('dark', isDark); localStorage.setItem('theme', isDark ? 'dark' : 'light'); }); // Listen for system theme changes const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', (e) => { if (!localStorage.getItem('theme')) { const isDark = e.matches; html.classList.toggle('dark', isDark); themeToggle.checked = isDark; } }); } }); // Handle search form submission document.addEventListener('submit', (e) => { if (e.target.matches('form[action*="search"]')) { const searchInput = e.target.querySelector('input[name="q"]'); if (!searchInput.value.trim()) { e.preventDefault(); } } }); // Mobile menu toggle with transitions document.addEventListener('DOMContentLoaded', () => { const mobileMenuBtn = document.getElementById('mobileMenuBtn'); const mobileMenu = document.getElementById('mobileMenu'); if (mobileMenuBtn && mobileMenu) { let isMenuOpen = false; const toggleMenu = () => { isMenuOpen = !isMenuOpen; mobileMenu.classList.toggle('show', isMenuOpen); mobileMenuBtn.setAttribute('aria-expanded', isMenuOpen.toString()); // Update icon const icon = mobileMenuBtn.querySelector('i'); icon.classList.remove(isMenuOpen ? 'fa-bars' : 'fa-times'); icon.classList.add(isMenuOpen ? 'fa-times' : 'fa-bars'); }; mobileMenuBtn.addEventListener('click', toggleMenu); // Close menu when clicking outside document.addEventListener('click', (e) => { if (isMenuOpen && !mobileMenu.contains(e.target) && !mobileMenuBtn.contains(e.target)) { toggleMenu(); } }); // Close menu when pressing escape document.addEventListener('keydown', (e) => { if (isMenuOpen && e.key === 'Escape') { toggleMenu(); } }); // Handle viewport changes const mediaQuery = window.matchMedia('(min-width: 1024px)'); mediaQuery.addEventListener('change', (e) => { if (e.matches && isMenuOpen) { toggleMenu(); } }); } }); // User dropdown functionality is handled by Alpine.js in the template // No additional JavaScript needed for dropdown functionality // Handle flash messages document.addEventListener('DOMContentLoaded', () => { const alerts = document.querySelectorAll('.alert'); alerts.forEach(alert => { setTimeout(() => { alert.style.opacity = '0'; setTimeout(() => alert.remove(), 300); }, 5000); }); }); // Initialize tooltips document.addEventListener('DOMContentLoaded', () => { const tooltips = document.querySelectorAll('[data-tooltip]'); tooltips.forEach(tooltip => { tooltip.addEventListener('mouseenter', (e) => { const text = e.target.getAttribute('data-tooltip'); const tooltipEl = document.createElement('div'); tooltipEl.className = 'absolute z-50 px-2 py-1 text-sm text-white bg-gray-900 rounded tooltip'; tooltipEl.textContent = text; document.body.appendChild(tooltipEl); const rect = e.target.getBoundingClientRect(); tooltipEl.style.top = rect.bottom + 5 + 'px'; tooltipEl.style.left = rect.left + (rect.width - tooltipEl.offsetWidth) / 2 + 'px'; }); tooltip.addEventListener('mouseleave', () => { const tooltips = document.querySelectorAll('.tooltip'); tooltips.forEach(t => t.remove()); }); }); });