Secure Private Life Milestone Logger
All data is stored securely in your browser. Nothing is ever uploaded.
Milestones by Category
Summary
Manage Categories
Data Management
Download your data for backup or to transfer to another device. No data is stored on our servers.
Add New Milestone
${name}: ${count}
`).join(''); // Chart const chartCanvas = document.getElementById('category-chart'); if (categoryChart) categoryChart.destroy(); categoryChart = new Chart(chartCanvas, { type: 'pie', data: { labels: Object.keys(countByCategory), datasets: [{ data: Object.values(countByCategory), backgroundColor: categories.map(c => c.color), hoverOffset: 4 }] }, options: { responsive: true, maintainAspectRatio: false } }); }; // --- MODAL HANDLING --- const openModal = (milestoneId = null) => { if (milestoneId) { const m = milestones.find(m => m.id === milestoneId); modalTitle.textContent = 'Edit Milestone'; modalMilestoneId.value = m.id; modalTitleInput.value = m.title; modalDateInput.value = m.date; modalCategorySelect.value = m.category; modalDescriptionTextarea.value = m.description; modalDeleteBtn.classList.remove('hidden'); } else { modalTitle.textContent = 'Add New Milestone'; modalMilestoneId.value = ''; modalTitleInput.value = ''; modalDateInput.valueAsDate = new Date(); modalCategorySelect.selectedIndex = 0; modalDescriptionTextarea.value = ''; modalDeleteBtn.classList.add('hidden'); } modal.style.display = 'block'; }; const closeModal = () => modal.style.display = 'none'; const saveMilestone = () => { const id = modalMilestoneId.value; const milestoneData = { title: modalTitleInput.value.trim(), date: modalDateInput.value, category: modalCategorySelect.value, description: modalDescriptionTextarea.value.trim() }; if (!milestoneData.title || !milestoneData.date) { alert("Title and date are required."); return; } if (id) { // Update const index = milestones.findIndex(m => m.id == id); milestones[index] = { ...milestones[index], ...milestoneData }; } else { // Create milestones.push({ id: Date.now(), ...milestoneData }); } saveData(); renderTimeline(); closeModal(); }; const deleteMilestone = () => { if (confirm("Are you sure you want to delete this milestone?")) { milestones = milestones.filter(m => m.id != modalMilestoneId.value); saveData(); renderTimeline(); closeModal(); } }; // --- CATEGORY & DATA MANAGEMENT --- const addCategory = () => { const name = newCategoryNameInput.value.trim(); if (name && !categories.some(c => c.name === name)) { categories.push({ name, color: newCategoryColorInput.value }); saveData(); renderCategories(); newCategoryNameInput.value = ''; } }; const deleteCategory = (name) => { if (confirm(`Delete category "${name}"? Existing milestones in this category will remain.`)) { categories = categories.filter(c => c.name !== name); saveData(); renderCategories(); } }; const exportData = () => { const dataStr = JSON.stringify({ milestones, categories }); const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr); const link = document.createElement('a'); link.setAttribute('href', dataUri); link.setAttribute('download', 'milestone_data.json'); document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const importData = (event) => { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { try { const data = JSON.parse(e.target.result); if (data.milestones && data.categories && confirm("This will overwrite your current data. Are you sure?")) { milestones = data.milestones; categories = data.categories; saveData(); renderAll(); } else { alert("Invalid data file."); } } catch (err) { alert("Error reading file."); } }; reader.readAsText(file); }; // --- TAB MANAGEMENT & PDF --- const renderAll = () => { renderTimeline(); renderCategories(); if (currentTab === 'tab2') renderDashboard(); }; window.changeTab = (tabId) => { currentTab = tabId; ['tab1', 'tab2', 'tab3'].forEach(id => { document.getElementById(id).classList.toggle('hidden', id !== tabId); document.getElementById(`${id}-button`).classList.toggle('active', id === tabId); }); if (tabId === 'tab2') renderDashboard(); updateButtonVisibility(); }; const updateButtonVisibility = () => { const prevBtn = document.getElementById('prev-button'); const nextBtn = document.getElementById('next-button'); const downloadBtn = document.getElementById('download-pdf-button'); prevBtn.classList.toggle('hidden', currentTab === 'tab1'); nextBtn.classList.toggle('hidden', currentTab === 'tab3'); downloadBtn.classList.toggle('hidden', !['tab1', 'tab2'].includes(currentTab)); }; window.handleNext = () => changeTab(['tab1', 'tab2', 'tab3'][['tab1', 'tab2', 'tab3'].indexOf(currentTab) + 1]); window.handlePrev = () => changeTab(['tab1', 'tab2', 'tab3'][['tab1', 'tab2', 'tab3'].indexOf(currentTab) - 1]); window.downloadPDF = async () => { const el = currentTab === 'tab1' ? document.getElementById('timeline-container') : document.getElementById('dashboard-content'); const filename = currentTab === 'tab1' ? 'milestone-timeline.pdf' : 'milestone-dashboard.pdf'; try { const canvas = await html2canvas(el, { scale: 2 }); const imgData = canvas.toDataURL('image/png'); const pdf = new jsPDF({ orientation: 'portrait', unit: 'px', format: 'a4' }); const pdfWidth = pdf.internal.pageSize.getWidth(); const newWidth = pdfWidth - 80; const newHeight = (canvas.height * newWidth) / canvas.width; pdf.addImage(imgData, 'PNG', 40, 40, newWidth, newHeight); pdf.save(filename); } catch (e) { alert("Error generating PDF."); } }; // --- EVENT LISTENERS --- document.getElementById('add-milestone-btn').addEventListener('click', () => openModal()); timelineContainer.addEventListener('click', (e) => { if (e.target.tagName === 'BUTTON' && e.target.dataset.id) { openModal(parseInt(e.target.dataset.id)); } }); modalSaveBtn.addEventListener('click', saveMilestone); modalCancelBtn.addEventListener('click', closeModal); modalDeleteBtn.addEventListener('click', deleteMilestone); addCategoryBtn.addEventListener('click', addCategory); categoryList.addEventListener('click', (e) => { if (e.target.tagName === 'BUTTON' && e.target.dataset.name) { deleteCategory(e.target.dataset.name); } }); exportBtn.addEventListener('click', exportData); importFile.addEventListener('change', importData); // --- INITIALIZATION --- loadData(); renderAll(); updateButtonVisibility(); });