/** * 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 return { formatDate, getTodayDate, truncateText, showNotification, validateWeightEntry, calculateWeightStats, copyToClipboard, sanitizeHTML }; })();