Files
Verde-Web/resources/js/admin.js

181 lines
5.1 KiB
JavaScript

/**
* 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;
}
window.Verde = {
apiFetch,
requireAuth,
logout,
setSession,
getToken,
getUser,
toast,
escapeHtml,
formatDate,
getEcho,
};