Online Secure Private Life Milestone Logger

Secure Private Life Milestone Logger

All data is stored securely in your browser. Nothing is ever uploaded.

${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(); });
Scroll to Top