/** * upaas - Global Utilities Store * * Shared formatting, status helpers, and clipboard utilities used across all pages. */ document.addEventListener("alpine:init", () => { Alpine.store("utils", { /** * Format a date string as relative time (e.g., "5 minutes ago") */ formatRelativeTime(dateStr) { if (!dateStr) return ""; const date = new Date(dateStr); const now = new Date(); const diffMs = now - date; const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHour / 24); if (diffSec < 60) return "just now"; if (diffMin < 60) return diffMin + (diffMin === 1 ? " minute ago" : " minutes ago"); if (diffHour < 24) return diffHour + (diffHour === 1 ? " hour ago" : " hours ago"); if (diffDay < 7) return diffDay + (diffDay === 1 ? " day ago" : " days ago"); return date.toLocaleDateString(); }, /** * Get the badge class for a given status */ statusBadgeClass(status) { if (status === "running" || status === "success") return "badge-success"; if (status === "building" || status === "deploying") return "badge-warning"; if (status === "failed" || status === "error") return "badge-error"; return "badge-neutral"; }, /** * Format status for display (capitalize first letter) */ statusLabel(status) { if (!status) return ""; return status.charAt(0).toUpperCase() + status.slice(1); }, /** * Check if status indicates active deployment */ isDeploying(status) { return status === "building" || status === "deploying"; }, /** * Scroll an element to the bottom */ scrollToBottom(el) { if (el) { requestAnimationFrame(() => { el.scrollTop = el.scrollHeight; }); } }, /** * Check if a scrollable element is at (or near) the bottom. * Tolerance of 30px accounts for rounding and partial lines. */ isScrolledToBottom(el, tolerance = 30) { if (!el) return true; return el.scrollHeight - el.scrollTop - el.clientHeight <= tolerance; }, /** * Copy text to clipboard */ async copyToClipboard(text, button) { try { await navigator.clipboard.writeText(text); return true; } catch (err) { // Fallback for older browsers const textArea = document.createElement("textarea"); textArea.value = text; textArea.style.position = "fixed"; textArea.style.left = "-9999px"; document.body.appendChild(textArea); textArea.select(); try { document.execCommand("copy"); document.body.removeChild(textArea); return true; } catch (e) { document.body.removeChild(textArea); return false; } } }, }); }); // ============================================ // Legacy support - expose utilities globally // ============================================ window.upaas = { // These are kept for backwards compatibility but templates should use Alpine.js formatRelativeTime(dateStr) { if (!dateStr) return ""; const date = new Date(dateStr); const now = new Date(); const diffMs = now - date; const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHour / 24); if (diffSec < 60) return "just now"; if (diffMin < 60) return diffMin + (diffMin === 1 ? " minute ago" : " minutes ago"); if (diffHour < 24) return diffHour + (diffHour === 1 ? " hour ago" : " hours ago"); if (diffDay < 7) return diffDay + (diffDay === 1 ? " day ago" : " days ago"); return date.toLocaleDateString(); }, // Placeholder functions - templates should migrate to Alpine.js initAppDetailPage() {}, initDeploymentsPage() {}, }; // Update relative times on page load for non-Alpine elements document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll(".relative-time[data-time]").forEach((el) => { const time = el.getAttribute("data-time"); if (time) { el.textContent = window.upaas.formatRelativeTime(time); } }); });