/**
* 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 `
`;
}
/**
* 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 `
`;
}
/**
* 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 `
`;
}
/**
* 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