import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; // Utility function for combining class names export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } // Date formatting utilities export const formatDate = (dateString: string): string => { const date = new Date(dateString); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); }; export const formatRelativeTime = (dateString: string): string => { if (!dateString) return 'Never'; let date: Date; try { // Handle various timestamp formats if (dateString.includes('T') && dateString.includes('Z')) { // ISO 8601 format date = new Date(dateString); } else if (dateString.includes(' ')) { // Database format like "2025-01-15 10:30:00" date = new Date(dateString.replace(' ', 'T') + 'Z'); } else { // Try direct parsing date = new Date(dateString); } // Check if date is invalid if (isNaN(date.getTime())) { console.warn('Invalid date string:', dateString); return 'Invalid Date'; } } catch (error) { console.warn('Error parsing date:', dateString, error); return 'Invalid Date'; } const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffMins < 1) { return 'Just now'; } else if (diffMins < 60) { return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`; } else if (diffHours < 24) { return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`; } else if (diffDays < 7) { return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`; } else { return formatDate(date.toISOString()); } }; export const isOnline = (lastCheckin: string): boolean => { const lastCheck = new Date(lastCheckin); const now = new Date(); const diffMs = now.getTime() - lastCheck.getTime(); const diffMins = Math.floor(diffMs / 60000); return diffMins < 15; // Consider online if checked in within 15 minutes (allows for 5min check-in + buffer) }; // Size formatting utilities export const formatBytes = (bytes: number): string => { if (bytes === 0) return '0 B'; 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]; }; // Version comparison utilities export const versionCompare = (v1: string, v2: string): number => { const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); const maxLength = Math.max(parts1.length, parts2.length); for (let i = 0; i < maxLength; i++) { const part1 = parts1[i] || 0; const part2 = parts2[i] || 0; if (part1 > part2) return 1; if (part1 < part2) return -1; } return 0; }; // Status and severity utilities export const getStatusColor = (status: string): string => { switch (status) { case 'online': return 'text-success-600 bg-success-100'; case 'offline': return 'text-danger-600 bg-danger-100'; case 'pending': return 'text-warning-600 bg-warning-100'; case 'approved': case 'scheduled': return 'text-blue-600 bg-blue-100'; case 'installing': return 'text-indigo-600 bg-indigo-100'; case 'installed': return 'text-success-600 bg-success-100'; case 'failed': return 'text-danger-600 bg-danger-100'; default: return 'text-gray-600 bg-gray-100'; } }; export const getSeverityColor = (severity: string): string => { switch (severity) { case 'critical': return 'text-danger-600 bg-danger-100'; case 'important': case 'high': return 'text-warning-600 bg-warning-100'; case 'moderate': case 'medium': return 'text-blue-600 bg-blue-100'; case 'low': case 'none': return 'text-gray-600 bg-gray-100'; default: return 'text-gray-600 bg-gray-100'; } }; export const getPackageTypeIcon = (type: string): string => { switch (type) { case 'apt': return '📦'; case 'docker': return '🐳'; case 'yum': case 'dnf': return '🐧'; case 'windows': return '🪟'; case 'winget': return '📱'; default: return '📋'; } }; // Filter and search utilities export const filterUpdates = ( updates: any[], filters: { status: string[]; severity: string[]; type: string[]; search: string; } ): any[] => { return updates.filter(update => { // Status filter if (filters.status.length > 0 && !filters.status.includes(update.status)) { return false; } // Severity filter if (filters.severity.length > 0 && !filters.severity.includes(update.severity)) { return false; } // Type filter if (filters.type.length > 0 && !filters.type.includes(update.package_type)) { return false; } // Search filter if (filters.search) { const searchLower = filters.search.toLowerCase(); return ( update.package_name.toLowerCase().includes(searchLower) || update.current_version.toLowerCase().includes(searchLower) || update.available_version.toLowerCase().includes(searchLower) ); } return true; }); }; // Error handling utilities export const getErrorMessage = (error: any): string => { if (typeof error === 'string') { return error; } if (error?.message) { return error.message; } if (error?.response?.data?.message) { return error.response.data.message; } return 'An unexpected error occurred'; }; // Debounce utility export const debounce = any>( func: T, wait: number ): ((...args: Parameters) => void) => { let timeout: ReturnType; return (...args: Parameters) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; }; // Local storage utilities export const storage = { get: (key: string): string | null => { try { return localStorage.getItem(key); } catch { return null; } }, set: (key: string, value: string): void => { try { localStorage.setItem(key, value); } catch { // Silent fail for storage issues } }, remove: (key: string): void => { try { localStorage.removeItem(key); } catch { // Silent fail for storage issues } }, getJSON: (key: string): T | null => { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : null; } catch { return null; } }, setJSON: (key: string, value: any): void => { try { localStorage.setItem(key, JSON.stringify(value)); } catch { // Silent fail for storage issues } }, };