const createFetchError = async (response) => { let payload = {}; try { payload = await response.json(); } catch (error) { payload = {}; } const message = payload.message || payload.error || `Request failed with status ${response.status}`; const error = new Error(message); error.status = response.status; error.code = payload.error; error.details = payload.details; error.payload = payload; return error; }; let csrfToken = null; const setCsrfToken = (token) => { csrfToken = typeof token === 'string' ? token : null; }; const getCsrfToken = () => csrfToken; const fetchJson = async (url, options = {}) => { const method = (options.method || 'GET').toUpperCase(); const response = await fetch(url, { credentials: 'include', headers: { 'Content-Type': 'application/json', ...(csrfToken && method !== 'GET' && method !== 'HEAD' ? {'X-CSRF-Token': csrfToken} : {}), ...(options.headers || {}), }, ...options, }); if (!response.ok) { throw await createFetchError(response); } return response.json().catch(() => ({})); }; const showToast = (message, type = 'success') => { const container = document.getElementById('toastContainer'); if (!container) { return; } const toast = document.createElement('div'); toast.className = `toast toast--${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.classList.add('toast--visible'); }, 10); setTimeout(() => { toast.classList.remove('toast--visible'); setTimeout(() => toast.remove(), 200); }, 3200); }; const setErrorBanner = (bannerEl, message, details) => { if (!bannerEl) { return; } const messageEl = bannerEl.querySelector('[data-role="error-message"]'); const copyButton = bannerEl.querySelector('[data-role="error-copy"]'); if (messageEl) { messageEl.textContent = message; } if (copyButton) { copyButton.onclick = async () => { const payload = details || {message}; try { await navigator.clipboard.writeText(JSON.stringify(payload, null, 2)); showToast('Error details copied.', 'success'); } catch (error) { showToast('Unable to copy details.', 'error'); } }; } bannerEl.hidden = false; }; const clearErrorBanner = (bannerEl) => { if (!bannerEl) { return; } bannerEl.hidden = true; }; const formatUserDisplay = (user) => { if (!user) { return 'Not signed in'; } const tag = user.discriminator && user.discriminator !== '0' ? `#${user.discriminator}` : ''; return `${user.username}${tag}`; }; const debounce = (callback, wait = 300) => { let timeoutId; return (...args) => { if (timeoutId) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => callback(...args), wait); }; }; const initIndexPage = async () => { const sessionStatus = document.getElementById('sessionStatus'); const loginButton = document.getElementById('loginButton'); const dashboardButton = document.getElementById('dashboardButton'); const logoutButton = document.getElementById('logoutButton'); const banner = document.getElementById('banner'); if (!sessionStatus || !loginButton) { return; } try { const data = await fetchJson('/api/me'); setCsrfToken(data.csrfToken); sessionStatus.textContent = formatUserDisplay(data.user); loginButton.hidden = true; dashboardButton.hidden = false; logoutButton.hidden = false; if (banner) { banner.textContent = ''; banner.style.display = 'none'; } } catch (error) { sessionStatus.textContent = ''; loginButton.hidden = false; dashboardButton.hidden = true; logoutButton.hidden = true; if (banner) { const sessionExpired = error.status === 401 || error.status === 403; banner.textContent = sessionExpired ? '' : 'Unable to check your session. Try again.'; banner.style.display = sessionExpired ? 'none' : 'block'; } } logoutButton?.addEventListener('click', async () => { try { await fetchJson('/auth/logout', { method: 'POST', credentials: 'include', headers: { ...(csrfToken ? {'X-CSRF-Token': csrfToken} : {}), }, }); sessionStatus.textContent = ''; loginButton.hidden = false; dashboardButton.hidden = true; logoutButton.hidden = true; } catch (error) { if (banner) { banner.textContent = error.message || 'Failed to log out.'; banner.style.display = 'block'; } } }); }; document.addEventListener('DOMContentLoaded', () => { initIndexPage(); }); window.dashboardApi = { fetchJson, setCsrfToken, getCsrfToken, showToast, setErrorBanner, clearErrorBanner, formatUserDisplay, debounce, };