/** * Utilities Module * Contains helper functions used across the application */ const Utils = (() => { /** * Format a date string (YYYY-MM-DD) to a more readable format * @param {string} dateString - Date in YYYY-MM-DD format * @returns {string} - Formatted date (e.g., "May 26, 2025") */ const formatDate = (dateString) => { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); }; /** * Get today's date in YYYY-MM-DD format * @returns {string} - Today's date */ const getTodayDate = () => { const today = new Date(); return today.toISOString().split('T')[0]; }; /** * Truncate text to a specific length * @param {string} text - Text to truncate * @param {number} maxLength - Maximum length * @returns {string} - Truncated text with ellipsis if needed */ const truncateText = (text, maxLength = 50) => { if (!text) return ''; if (text.length <= maxLength) return text; return text.substring(0, maxLength) + '...'; }; /** * Show a notification/toast message * @param {string} message - Message to display * @param {string} type - Type of message (success, error, info) */ const showNotification = (message, type = 'info') => { // Create notification element const notification = document.createElement('div'); notification.classList.add('notification', `notification-${type}`); notification.textContent = message; // Add to DOM document.body.appendChild(notification); // Trigger animation setTimeout(() => { notification.classList.add('show'); }, 10); // Remove after delay setTimeout(() => { notification.classList.remove('show'); setTimeout(() => { notification.remove(); }, 300); }, 3000); }; /** * Validate a weight entry * @param {Object} entry - Weight entry to validate * @returns {Object} - Validation result {valid, message} */ const validateWeightEntry = (entry) => { if (!entry.date) { return { valid: false, message: 'Date is required' }; } if (!entry.weight) { return { valid: false, message: 'Weight value is required' }; } const weightValue = parseFloat(entry.weight); if (isNaN(weightValue) || weightValue <= 0) { return { valid: false, message: 'Weight must be a positive number' }; } return { valid: true, message: 'Valid entry' }; }; /** * Calculate weight change and trend * @param {Array} weights - Array of weight entries * @returns {Object} - Statistics {totalChange, averageChange, trend} */ const calculateWeightStats = (weights) => { if (!weights || weights.length < 2) { return { totalChange: 0, averageChange: 0, trend: 'neutral' }; } // Sort by date (oldest first) const sortedWeights = [...weights].sort((a, b) => new Date(a.date) - new Date(b.date)); const firstWeight = sortedWeights[0].weight; const lastWeight = sortedWeights[sortedWeights.length - 1].weight; const totalChange = lastWeight - firstWeight; // Calculate average change per day const daysDiff = Math.max(1, (new Date(sortedWeights[sortedWeights.length - 1].date) - new Date(sortedWeights[0].date)) / (1000 * 60 * 60 * 24)); const averageChange = totalChange / daysDiff; // Determine trend let trend = 'neutral'; if (totalChange < 0) { trend = 'down'; } else if (totalChange > 0) { trend = 'up'; } return { totalChange: totalChange.toFixed(2), averageChange: averageChange.toFixed(2), trend }; }; /** * Copy text to clipboard * @param {string} text - Text to copy * @returns {boolean} - Success status */ const copyToClipboard = (text) => { try { // Use the modern navigator.clipboard API if available if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text); return true; } // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); const successful = document.execCommand('copy'); document.body.removeChild(textArea); return successful; } catch (error) { console.error('Error copying to clipboard:', error); return false; } }; /** * Sanitize HTML to prevent XSS attacks. * Replaces special characters with their HTML entities. * @param {string} text - The text to sanitize. * @returns {string} - The sanitized text. */ const sanitizeHTML = (text) => { if (typeof text !== 'string') return ''; return text.replace(/[&<>"']/g, function (match) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[match]; }); }; // Return public API /** * Convert a date string from UI format (DD/MM/YYYY) or existing ISO format (YYYY-MM-DD) to ISO format (YYYY-MM-DD). * @param {string} dateStr - The date string to convert. * @returns {string} - The date string in YYYY-MM-DD format, or original if conversion fails. */ const uiDateToISO = (dateStr) => { if (!dateStr) return ''; // Check if already YYYY-MM-DD if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { // Potentially validate if it's a real date, but for now, assume format implies intent return dateStr; } // Try to parse as DD/MM/YYYY const parts = dateStr.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); if (parts) { const day = String(parts[1]).padStart(2, '0'); const month = String(parts[2]).padStart(2, '0'); const year = parts[3]; return `${year}-${month}-${day}`; } console.warn(`uiDateToISO: Unrecognized date format "${dateStr}". Returning as is.`); return dateStr; // Fallback: return original if not recognized }; /** * Convert a date string from DD/MM/YYYY to YYYY-MM-DD (ISO). * @param {string} uiDate - Date in DD/MM/YYYY * @returns {string} - Date in YYYY-MM-DD */ const toISODate = (uiDate) => { if (!uiDate) return ''; const parts = uiDate.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); if (parts) { const day = String(parts[1]).padStart(2, '0'); const month = String(parts[2]).padStart(2, '0'); const year = parts[3]; return `${year}-${month}-${day}`; } // If already in ISO format, return as is if (/^\d{4}-\d{2}-\d{2}$/.test(uiDate)) return uiDate; return uiDate; }; /** * Convert a date string from YYYY-MM-DD (ISO) to DD/MM/YYYY for UI. * @param {string} isoDate - Date in YYYY-MM-DD * @returns {string} - Date in DD/MM/YYYY */ const toUIDate = (isoDate) => { if (!isoDate) return ''; const parts = isoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/); if (parts) { return `${parts[3]}/${parts[2]}/${parts[1]}`; } // If already in UI format, return as is if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(isoDate)) return isoDate; return isoDate; }; return { formatDate, getTodayDate, truncateText, showNotification, validateWeightEntry, calculateWeightStats, copyToClipboard, sanitizeHTML, toISODate, toUIDate }; })();