472 lines
18 KiB
JavaScript

/**
* UI Module
* Handles all user interface interactions and rendering
*/
const UI = (() => {
// Tab elements
const tabs = {
weight: { btn: document.getElementById('tab-weight'), content: document.getElementById('weight-log-content') },
meals: { btn: document.getElementById('tab-meals'), content: document.getElementById('meals-log-content') },
charts: { btn: document.getElementById('tab-charts'), content: document.getElementById('charts-content') },
settings: { btn: document.getElementById('tab-settings'), content: document.getElementById('settings-content') }
};
// Form elements
const forms = {
weight: document.getElementById('weight-form'),
meal: document.getElementById('meal-form')
};
// Table elements
const tables = {
weight: document.getElementById('weight-entries'),
meal: document.getElementById('meal-entries')
};
// Initialize the UI
const init = () => {
// Set today's date as default for forms
document.getElementById('weight-date').value = Utils.getTodayDate();
document.getElementById('meal-date').value = Utils.getTodayDate();
// Initialize tab navigation
initTabs();
// Initialize form submissions
initForms();
// Initialize data import/export
initDataManagement();
// Add notifications styles
addNotificationsCSS();
};
/**
* Initialize tab navigation
*/
const initTabs = () => {
// Add click event to each tab button
for (const tabKey in tabs) {
const tab = tabs[tabKey];
tab.btn.addEventListener('click', () => switchTab(tabKey));
}
};
/**
* Switch active tab
* @param {string} tabKey - Key of the tab to activate
*/
const switchTab = (tabKey) => {
// Deactivate all tabs
for (const key in tabs) {
tabs[key].btn.classList.remove('active');
tabs[key].content.classList.remove('active');
}
// Activate the selected tab
tabs[tabKey].btn.classList.add('active');
tabs[tabKey].content.classList.add('active');
};
/**
* Initialize form submissions
*/
const initForms = () => {
// Weight form submission
forms.weight.addEventListener('submit', (e) => {
e.preventDefault();
const weightEntry = {
date: document.getElementById('weight-date').value,
weight: document.getElementById('weight-value').value,
notes: document.getElementById('weight-notes').value
};
// Validate entry
const validation = Utils.validateWeightEntry(weightEntry);
if (!validation.valid) {
Utils.showNotification(validation.message, 'error');
return;
}
// Add entry to data
if (DataManager.addWeight(weightEntry)) {
Utils.showNotification('Weight entry saved successfully', 'success');
// Reset form
document.getElementById('weight-date').value = Utils.getTodayDate();
document.getElementById('weight-value').value = '';
document.getElementById('weight-notes').value = '';
// Refresh table
renderWeightTable();
// Update charts if on chart tab
if (tabs.charts.content.classList.contains('active')) {
Charts.renderWeightChart();
}
} else {
Utils.showNotification('Error saving weight entry', 'error');
}
});
// Meal form submission
forms.meal.addEventListener('submit', (e) => {
e.preventDefault();
const mealEntry = {
date: document.getElementById('meal-date').value,
breakfast: document.getElementById('breakfast').value,
lunch: document.getElementById('lunch').value,
dinner: document.getElementById('dinner').value,
otherMeals: document.getElementById('other-meals').value
};
// Add/update entry
if (DataManager.addMeal(mealEntry)) {
Utils.showNotification('Meal entries saved successfully', 'success');
// Reset form except for date
document.getElementById('breakfast').value = '';
document.getElementById('lunch').value = '';
document.getElementById('dinner').value = '';
document.getElementById('other-meals').value = '';
// Refresh table
renderMealTable();
} else {
Utils.showNotification('Error saving meal entries', 'error');
}
});
};
/**
* Initialize data import/export functionality
*/
const initDataManagement = () => {
// Export data as JSON
document.getElementById('export-data').addEventListener('click', () => {
DataManager.exportData();
Utils.showNotification('Data exported successfully', 'success');
});
// Import data file selection
document.getElementById('import-data-file').addEventListener('change', (e) => {
const fileInput = e.target;
const importButton = document.getElementById('import-data');
if (fileInput.files.length > 0) {
document.getElementById('file-name-display').textContent = fileInput.files[0].name;
importButton.disabled = false;
} else {
document.getElementById('file-name-display').textContent = 'No file selected';
importButton.disabled = true;
}
});
// Import data from JSON
document.getElementById('import-data').addEventListener('click', () => {
const fileInput = document.getElementById('import-data-file');
const importMode = document.querySelector('input[name="import-mode"]:checked').value;
if (fileInput.files.length === 0) {
Utils.showNotification('Please select a file to import', 'error');
return;
}
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = (event) => {
try {
// First, validate that we can parse the JSON
let importedData;
try {
importedData = JSON.parse(event.target.result);
} catch (parseError) {
console.error('JSON parsing error:', parseError);
Utils.showNotification('Invalid JSON format. Please check the file contents.', 'error');
return;
}
// Then validate the structure before importing
if (!importedData || typeof importedData !== 'object') {
Utils.showNotification('Invalid data: File does not contain a valid data object', 'error');
return;
}
// Check for weights and meals arrays
if (!importedData.weights || !Array.isArray(importedData.weights)) {
Utils.showNotification('Invalid data: Missing weights array', 'error');
return;
}
if (!importedData.meals || !Array.isArray(importedData.meals)) {
Utils.showNotification('Invalid data: Missing meals array', 'error');
return;
}
// Try to import the data
if (DataManager.importData(importedData, importMode)) {
Utils.showNotification('Data imported successfully', 'success');
// Refresh tables and charts
renderWeightTable();
renderMealTable();
Charts.renderWeightChart();
// Reset file input
fileInput.value = '';
document.getElementById('file-name-display').textContent = 'No file selected';
document.getElementById('import-data').disabled = true;
} else {
Utils.showNotification('Error importing data. Please check console for details.', 'error');
}
} catch (error) {
console.error('Error processing imported data:', error);
Utils.showNotification('Error processing data: ' + (error.message || 'Unknown error'), 'error');
}
};
reader.onerror = () => {
Utils.showNotification('Error reading file', 'error');
};
reader.readAsText(file);
});
// Export as CSV
document.getElementById('export-csv').addEventListener('click', () => {
DataManager.exportCSV('weights');
Utils.showNotification('Weight data exported as CSV', 'success');
});
// Copy table data to clipboard
document.getElementById('copy-table-data').addEventListener('click', () => {
const weights = DataManager.getWeights();
if (weights.length === 0) {
Utils.showNotification('No weight data to copy', 'error');
return;
}
let csvContent = 'Date,Weight (kg),Notes\n';
weights.forEach(entry => {
const notes = entry.notes ? `"${entry.notes.replace(/"/g, '""')}"` : '';
csvContent += `${entry.date},${entry.weight},${notes}\n`;
});
if (Utils.copyToClipboard(csvContent)) {
Utils.showNotification('Weight data copied to clipboard', 'success');
} else {
Utils.showNotification('Failed to copy data to clipboard', 'error');
}
});
// Date filter for chart
document.getElementById('apply-date-filter').addEventListener('click', () => {
Charts.updateDateFilter(
document.getElementById('chart-start-date').value,
document.getElementById('chart-end-date').value
);
});
// Reset date filter
document.getElementById('reset-date-filter').addEventListener('click', () => {
document.getElementById('chart-start-date').value = '';
document.getElementById('chart-end-date').value = '';
Charts.updateDateFilter(null, null);
});
};
/**
* Render the weight entries table
*/
const renderWeightTable = () => {
const weights = DataManager.getWeights();
tables.weight.innerHTML = '';
if (weights.length === 0) {
// Show empty state
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = `
<td colspan="4" class="empty-state">No weight entries found. Use the form above to add your first entry.</td>
`;
tables.weight.appendChild(emptyRow);
return;
}
// Render each weight entry
weights.forEach(entry => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${Utils.formatDate(entry.date)}</td>
<td>${entry.weight} kg</td>
<td>${Utils.sanitizeHTML(entry.notes)}</td>
<td>
<button class="btn text-btn delete-weight" data-id="${entry.id}">Delete</button>
</td>
`;
tables.weight.appendChild(row);
});
// Add event listeners for delete buttons
document.querySelectorAll('.delete-weight').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = e.target.getAttribute('data-id');
if (confirm('Are you sure you want to delete this weight entry?')) {
if (DataManager.deleteWeight(id)) {
Utils.showNotification('Weight entry deleted', 'success');
renderWeightTable();
Charts.renderWeightChart();
} else {
Utils.showNotification('Error deleting weight entry', 'error');
}
}
});
});
};
/**
* Render the meal entries table
*/
const renderMealTable = () => {
const meals = DataManager.getMeals();
tables.meal.innerHTML = '';
if (meals.length === 0) {
// Show empty state
const emptyRow = document.createElement('tr');
emptyRow.innerHTML = `
<td colspan="6" class="empty-state">No meal entries found. Use the form above to add your first entry.</td>
`;
tables.meal.appendChild(emptyRow);
return;
}
// Render each meal entry
meals.forEach(entry => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${Utils.formatDate(entry.date)}</td>
<td>${Utils.sanitizeHTML(Utils.truncateText(entry.breakfast, 30))}</td>
<td>${Utils.sanitizeHTML(Utils.truncateText(entry.lunch, 30))}</td>
<td>${Utils.sanitizeHTML(Utils.truncateText(entry.dinner, 30))}</td>
<td>${Utils.sanitizeHTML(Utils.truncateText(entry.otherMeals, 30))}</td>
<td>
<button class="btn text-btn edit-meal" data-id="${entry.id}">Edit</button>
<button class="btn text-btn delete-meal" data-id="${entry.id}">Delete</button>
</td>
`;
tables.meal.appendChild(row);
});
// Add event listeners for delete buttons
document.querySelectorAll('.delete-meal').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = e.target.getAttribute('data-id');
if (confirm('Are you sure you want to delete this meal entry?')) {
if (DataManager.deleteMeal(id)) {
Utils.showNotification('Meal entry deleted', 'success');
renderMealTable();
} else {
Utils.showNotification('Error deleting meal entry', 'error');
}
}
});
});
// Add event listeners for edit buttons
document.querySelectorAll('.edit-meal').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = e.target.getAttribute('data-id');
const meal = DataManager.getMeals().find(m => m.id === id);
if (meal) {
document.getElementById('meal-date').value = meal.date;
document.getElementById('breakfast').value = meal.breakfast || '';
document.getElementById('lunch').value = meal.lunch || '';
document.getElementById('dinner').value = meal.dinner || '';
document.getElementById('other-meals').value = meal.otherMeals || '';
// Scroll to form
document.getElementById('meal-form').scrollIntoView({ behavior: 'smooth' });
Utils.showNotification('Edit meal entry and click Save to update', 'info');
}
});
});
};
/**
* Add CSS for notifications
*/
const addNotificationsCSS = () => {
// Create style element
const style = document.createElement('style');
style.textContent = `
.notification {
position: fixed;
bottom: -60px;
left: 50%;
transform: translateX(-50%);
padding: 12px 25px;
border-radius: var(--border-radius);
background-color: #333;
color: white;
font-weight: 500;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
transition: bottom 0.3s ease-in-out;
}
.notification.show {
bottom: 30px;
}
.notification-success {
background-color: var(--success-color);
}
.notification-error {
background-color: var(--error-color);
}
.notification-info {
background-color: var(--primary-color);
}
.empty-state {
text-align: center;
padding: 20px;
color: var(--text-secondary);
font-style: italic;
}
@media (max-width: 768px) {
.notification {
width: 90%;
padding: 10px 15px;
font-size: 0.9rem;
}
.notification.show {
bottom: 20px;
}
}
`;
// Add to document head
document.head.appendChild(style);
};
// Return public API
return {
init,
renderWeightTable,
renderMealTable,
switchTab
};
})();