498 lines
19 KiB
JavaScript
498 lines
19 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.toUIDate(Utils.getTodayDate());
|
|
document.getElementById('meal-date').value = Utils.getTodayDate();
|
|
|
|
// Initialize tab navigation
|
|
initTabs();
|
|
|
|
// Initialize form submissions
|
|
initForms();
|
|
|
|
// Populate meal form for the initial date (today)
|
|
populateMealFormForDate(document.getElementById('meal-date').value);
|
|
|
|
// 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');
|
|
|
|
// If switching to meals tab, always reset to today and autofill
|
|
if (tabKey === 'meals') {
|
|
const todayISODate = Utils.getTodayDate();
|
|
document.getElementById('meal-date').value = todayISODate;
|
|
populateMealFormForDate(todayISODate);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Initialize form submissions
|
|
*/
|
|
/**
|
|
* Populate the meal form fields based on data for the given date.
|
|
* @param {string} dateString - The date (YYYY-MM-DD) to load data for.
|
|
*/
|
|
const populateMealFormForDate = (dateStringFromInput) => {
|
|
const isoDateString = Utils.toISODate(dateStringFromInput);
|
|
const mealData = DataManager.getMealByDate(isoDateString);
|
|
document.getElementById('breakfast').value = mealData?.breakfast || '';
|
|
document.getElementById('lunch').value = mealData?.lunch || '';
|
|
document.getElementById('dinner').value = mealData?.dinner || '';
|
|
document.getElementById('other-meals').value = mealData?.otherMeals || '';
|
|
};
|
|
|
|
|
|
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
|
|
document.getElementById('meal-date').addEventListener('change', (e) => {
|
|
populateMealFormForDate(e.target.value);
|
|
});
|
|
|
|
forms.meal.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
|
|
const mealEntry = {
|
|
date: Utils.toISODate(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');
|
|
|
|
// Refresh form with current date's data (which might have just been updated)
|
|
populateMealFormForDate(mealEntry.date);
|
|
|
|
// 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
|
|
};
|
|
})();
|