/** * Verde admin web — shared client helpers, exposed on window.Verde so * inline page scripts can use them without ESM imports. */ const TOKEN_KEY = 'verde:token'; const USER_KEY = 'verde:user'; function csrfToken() { return document.querySelector('meta[name="csrf-token"]')?.content ?? ''; } function getToken() { return localStorage.getItem(TOKEN_KEY); } function getUser() { try { return JSON.parse(localStorage.getItem(USER_KEY) || 'null'); } catch { return null; } } function setSession(token, user) { localStorage.setItem(TOKEN_KEY, token); if (user) localStorage.setItem(USER_KEY, JSON.stringify(user)); } function clearSession() { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(USER_KEY); } async function apiFetch(path, opts = {}) { const token = getToken(); const method = (opts.method || 'GET').toUpperCase(); const isFormData = opts.body instanceof FormData; const headers = { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken(), 'X-Requested-With': 'XMLHttpRequest', ...(opts.headers || {}), }; if (token) headers['Authorization'] = `Bearer ${token}`; if (!isFormData && method !== 'GET' && method !== 'HEAD' && opts.body !== undefined) { headers['Content-Type'] = headers['Content-Type'] || 'application/json'; } const res = await fetch(path, { ...opts, method, credentials: 'same-origin', headers, }); if (res.status === 401) { clearSession(); window.location.href = '/login'; throw new Error('unauthenticated'); } let body = null; try { body = await res.json(); } catch { /* ignore */ } return { ok: res.ok, status: res.status, body }; } function requireAuth() { if (!getToken()) { window.location.href = '/login'; return false; } return true; } async function logout() { try { await apiFetch('/api/v1/auth/logout', { method: 'POST' }); } catch { /* ignore */ } clearSession(); window.location.href = '/login'; } function escapeHtml(value) { if (value === null || value === undefined) return ''; const div = document.createElement('div'); div.textContent = String(value); return div.innerHTML; } function formatDate(iso, options = { dateStyle: 'medium', timeStyle: 'short' }) { if (!iso) return '—'; try { return new Intl.DateTimeFormat(undefined, options).format(new Date(iso)); } catch { return iso; } } function toast(message, type = 'info') { let host = document.getElementById('verde-toasts'); if (!host) { host = document.createElement('div'); host.id = 'verde-toasts'; host.className = 'pointer-events-none fixed top-4 right-4 z-50 flex flex-col gap-2'; document.body.appendChild(host); } const colors = { success: 'border-verde-200 bg-verde-50 text-verde-800', error: 'border-red-200 bg-red-50 text-red-700', info: 'border-neutral-200 bg-white text-neutral-800', }; const el = document.createElement('div'); el.className = `pointer-events-auto rounded-md border ${colors[type] || colors.info} px-4 py-3 text-sm shadow-md transition-opacity`; el.textContent = message; host.appendChild(el); setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 300); }, 3500); } window.Verde = { apiFetch, requireAuth, logout, setSession, getUser, toast, escapeHtml, formatDate, };