217 lines
7.0 KiB
JavaScript
217 lines
7.0 KiB
JavaScript
/**
|
|
* 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
|
|
};
|
|
|
|
return {
|
|
formatDate,
|
|
getTodayDate,
|
|
truncateText,
|
|
showNotification,
|
|
validateWeightEntry,
|
|
calculateWeightStats,
|
|
copyToClipboard,
|
|
sanitizeHTML,
|
|
uiDateToISO // Export the new function
|
|
};
|
|
})();
|