/* ============================================ Grubflip Admin — Core Application Module Sidebar, API client, toast, auth helpers ============================================ */ const AdminApp = (() => { 'use strict'; const API_BASE = '/api'; // --- Auth --- function getToken() { return localStorage.getItem('gf_token'); } function getUser() { try { return JSON.parse(localStorage.getItem('gf_user') || '{}'); } catch { return {}; } } function requireAuth() { if (!getToken()) { window.location.href = 'index.html'; return false; } return true; } function logout() { localStorage.removeItem('gf_token'); localStorage.removeItem('gf_user'); window.location.href = 'index.html'; } // --- API Client --- async function api(endpoint, options = {}) { const url = `${API_BASE}${endpoint}`; const headers = { 'Accept': 'application/json', ...options.headers, }; const token = getToken(); if (token) { headers['Authorization'] = `Bearer ${token}`; } if (options.body && !(options.body instanceof FormData)) { headers['Content-Type'] = 'application/json'; if (typeof options.body === 'object') { options.body = JSON.stringify(options.body); } } const resp = await fetch(url, { ...options, headers }); if (resp.status === 401) { logout(); return null; } if (!resp.ok) { const data = await resp.json().catch(() => ({})); throw new Error(data.message || `Request failed (${resp.status})`); } if (resp.status === 204) return null; return resp.json(); } // --- Sidebar --- function initSidebar() { const sidebar = document.getElementById('sidebar'); const overlay = document.getElementById('sidebarOverlay'); const toggle = document.getElementById('menuToggle'); if (!sidebar || !toggle) return; toggle.addEventListener('click', () => { sidebar.classList.toggle('open'); if (overlay) overlay.classList.toggle('visible'); }); if (overlay) { overlay.addEventListener('click', () => { sidebar.classList.remove('open'); overlay.classList.remove('visible'); }); } // Close on Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && sidebar.classList.contains('open')) { sidebar.classList.remove('open'); if (overlay) overlay.classList.remove('visible'); } }); } // --- Toast System --- function toast(message, type = 'info', duration = 4000) { const container = document.getElementById('toastContainer'); if (!container) return; const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }; const el = document.createElement('div'); el.className = `toast toast-${type}`; el.innerHTML = `${icons[type] || ''} ${escapeHtml(message)}`; container.appendChild(el); setTimeout(() => { el.classList.add('removing'); el.addEventListener('animationend', () => el.remove()); }, duration); } // --- Utilities --- function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function formatDate(dateStr) { if (!dateStr) return '--'; const d = new Date(dateStr); return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } function formatTime(dateStr) { if (!dateStr) return '--'; const d = new Date(dateStr); return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); } function timeAgo(dateStr) { if (!dateStr) return '--'; const now = Date.now(); const then = new Date(dateStr).getTime(); const diff = Math.floor((now - then) / 1000); if (diff < 60) return 'just now'; if (diff < 3600) return `${Math.floor(diff / 60)} min ago`; if (diff < 86400) return `${Math.floor(diff / 3600)} hr ago`; if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`; return formatDate(dateStr); } // --- Confirm Dialog --- function confirm(message) { return new Promise((resolve) => { // Use native confirm for now — can replace with modal later resolve(window.confirm(message)); }); } return { api, getToken, getUser, requireAuth, logout, initSidebar, toast, escapeHtml, formatDate, formatTime, timeAgo, confirm, }; })();