/** * component-helpers.js * Shared utilities for panel components * Provides standardized loading, error, and empty states */ export class ComponentHelpers { /** * Render a loading spinner with optional message * @param {string} message - Optional loading message * @returns {string} HTML string for loading state */ static renderLoading(message = 'Loading...') { return `
${message}
`; } /** * Render an error state with message * @param {string} message - Error message to display * @param {Error} error - Optional error object for details * @returns {string} HTML string for error state */ static renderError(message, error = null) { const details = error ? `
${error.message || error.toString()}
` : ''; return `
⚠️
${message}
${details}
`; } /** * Render an empty state with message * @param {string} message - Empty state message * @param {string} icon - Optional icon/emoji * @returns {string} HTML string for empty state */ static renderEmpty(message = 'No data available', icon = '📭') { return `
${icon}
${message}
`; } /** * Format timestamp for display * @param {Date|string|number} date - Date to format * @param {boolean} includeTime - Include time in output * @returns {string} Formatted date string */ static formatTimestamp(date, includeTime = true) { if (!date) return 'N/A'; const d = date instanceof Date ? date : new Date(date); if (isNaN(d.getTime())) return 'Invalid Date'; const dateStr = d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); if (!includeTime) return dateStr; const timeStr = d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); return `${dateStr} ${timeStr}`; } /** * Format relative time (e.g., "2 minutes ago") * @param {Date|string|number} date - Date to format * @returns {string} Relative time string */ static formatRelativeTime(date) { if (!date) return 'N/A'; const d = date instanceof Date ? date : new Date(date); if (isNaN(d.getTime())) return 'Invalid Date'; const now = new Date(); const diffMs = now - d; 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} minute${diffMin !== 1 ? 's' : ''} ago`; if (diffHour < 24) return `${diffHour} hour${diffHour !== 1 ? 's' : ''} ago`; if (diffDay < 7) return `${diffDay} day${diffDay !== 1 ? 's' : ''} ago`; return this.formatTimestamp(d, false); } /** * Truncate text to max length with ellipsis * @param {string} text - Text to truncate * @param {number} maxLength - Maximum length * @returns {string} Truncated text */ static truncateText(text, maxLength = 100) { if (!text || text.length <= maxLength) return text; return text.substring(0, maxLength - 3) + '...'; } /** * Escape HTML to prevent XSS * @param {string} text - Text to escape * @returns {string} Escaped HTML */ static escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Format file size in human-readable format * @param {number} bytes - Size in bytes * @returns {string} Formatted size string */ static formatFileSize(bytes) { if (bytes === 0) return '0 B'; if (!bytes) return 'N/A'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } /** * Format duration in human-readable format * @param {number} ms - Duration in milliseconds * @returns {string} Formatted duration string */ static formatDuration(ms) { if (ms === 0) return '0ms'; if (!ms) return 'N/A'; if (ms < 1000) return `${Math.round(ms)}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`; if (ms < 3600000) return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; const hours = Math.floor(ms / 3600000); const minutes = Math.floor((ms % 3600000) / 60000); return `${hours}h ${minutes}m`; } /** * Create a badge element * @param {string} label - Badge label * @param {string} type - Badge type (success, warning, error, info) * @returns {string} HTML string for badge */ static createBadge(label, type = 'info') { const colors = { success: '#89d185', warning: '#dbb765', error: '#f48771', info: '#75beff' }; const color = colors[type] || colors.info; return ` ${label} `; } /** * Convert hex color to RGB values * @param {string} hex - Hex color * @returns {string} RGB values (e.g., "255, 0, 0") */ static hexToRgb(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}` : '128, 128, 128'; } /** * Debounce function execution * @param {Function} func - Function to debounce * @param {number} wait - Wait time in milliseconds * @returns {Function} Debounced function */ static debounce(func, wait = 300) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * Parse JSON safely with fallback * @param {string} jsonString - JSON string to parse * @param {*} fallback - Fallback value if parse fails * @returns {*} Parsed object or fallback */ static safeJsonParse(jsonString, fallback = null) { try { return JSON.parse(jsonString); } catch (e) { console.warn('Failed to parse JSON:', e); return fallback; } } /** * Create a table from array of objects * @param {Array} data - Array of objects * @param {Array} columns - Column names to display * @returns {string} HTML string for table */ static createTable(data, columns = null) { if (!data || data.length === 0) { return this.renderEmpty('No data to display', '📋'); } const cols = columns || Object.keys(data[0]); const headerRow = cols.map(col => `${col}` ).join(''); const dataRows = data.map(row => ` ${cols.map(col => ` ${this.escapeHtml(String(row[col] ?? ''))} `).join('')} `).join(''); return `
${headerRow} ${dataRows}
`; } }