/** * Verde admin web — shared client helpers, exposed on window.Verde so * inline page scripts can use them without ESM imports. */ import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; const TOKEN_KEY = 'verde:token'; const USER_KEY = 'verde:user'; window.Pusher = Pusher; 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); } /** * Lazy Echo init. Only spins up when a page actually subscribes — saves * a WebSocket connection on pages that don't need real-time. Returns * null when Reverb isn't configured (callers fall back to polling). */ let _echo = null; function getEcho() { if (_echo) return _echo; const cfg = window.VERDE_BROADCAST ?? {}; if (! cfg.key || ! cfg.host) return null; _echo = new Echo({ broadcaster: 'reverb', key: cfg.key, wsHost: cfg.host, wsPort: Number(cfg.port) || 80, wssPort: Number(cfg.port) || 443, forceTLS: cfg.scheme === 'https', enabledTransports: ['ws', 'wss'], authorizer: (channel) => ({ authorize: (socketId, callback) => { fetch('/broadcasting/auth', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': `Bearer ${getToken() ?? ''}`, 'X-CSRF-TOKEN': csrfToken(), 'X-Requested-With': 'XMLHttpRequest', }, body: JSON.stringify({ socket_id: socketId, channel_name: channel.name }), }) .then(r => r.ok ? r.json() : Promise.reject(r)) .then(data => callback(false, data)) .catch(err => callback(true, err)); }, }), }); return _echo; } Object.assign(window.Verde, { apiFetch, requireAuth, logout, setSession, getToken, getUser, toast, escapeHtml, formatDate, getEcho, });