// ====================================================== // FILE: static/script.js (Netlify/Static Version) // ====================================================== // --- 1. The "Life Brain" Definitions --- const CATEGORIES = { "brain-dump": { name: "๐Ÿง  Quick Brain Dump", description: "Get it out of your head. Sort it later.", emoji: "๐Ÿ“ฅ", steps: [ { title: "What's on your mind?", fields: [ { id: "title", label: "Short Title", type: "text", required: true }, { id: "event_date", label: "Date/Time (Optional)", type: "datetime-local" }, { id: "description", label: "Details (#tags)", type: "textarea", required: true }, { id: "frequency", label: "Is this recurring?", type: "select", options: ["One-Time", "Daily", "Weekly", "Monthly", "Yearly"] }, ], }, ], }, chore: { name: "๐Ÿงน Routine / Chore", description: "Cleaning, maintenance, bills.", emoji: "๐Ÿ”„", steps: [ { title: "The Routine", fields: [ { id: "title", label: "What needs doing?", type: "text", placeholder: "e.g. Take out Trash", required: true }, { id: "event_date", label: "Start Date (for recurring calc)", type: "datetime-local" }, { id: "frequency", label: "How often?", type: "select", options: ["One-Time", "Daily", "Weekly", "Monthly", "Yearly"] }, { id: "assignee", label: "Who does this? (Optional)", type: "text", placeholder: "e.g. Dad, Kids" }, ], }, { title: "Details", fields: [ { id: "description", label: "Instructions / Notes (Use #tags)", type: "textarea" }, { id: "tools", label: "Tools/Supplies needed?", type: "text" }, ], }, ], }, project: { name: "๐Ÿ› ๏ธ Project / Fix", description: "Something broken or a goal.", emoji: "๐Ÿšง", steps: [ { title: "The Project", fields: [ { id: "title", label: "Project Name", type: "text", required: true }, { id: "event_date", label: "Deadline / Date", type: "datetime-local" }, { id: "frequency", label: "Timeline", type: "select", options: ["One-Time", "Daily", "Weekly", "Monthly", "Yearly"] }, ], }, { title: "The Plan", fields: [ { id: "description", label: "Desired Outcome (#tags)", type: "textarea" }, { id: "budget", label: "Estimated Cost ($)", type: "number" }, { id: "blockers", label: "What is stopping us?", type: "text" }, ], }, ], }, inventory: { name: "๐Ÿ“ฆ Life / Appointments", description: "Car, Health, House, etc..", emoji: "๐Ÿ“‹", steps: [ { title: "System Info", fields: [ { id: "title", label: "Item Name", type: "text", placeholder: "e.g. Honda Civic", required: true }, { id: "event_date", label: "Next Maintenance/Appt Date", type: "datetime-local" }, { id: "frequency", label: "Maintenance/Appt Cycle", type: "select", options: ["One-Time", "Daily", "Weekly", "Monthly", "Yearly"] }, ], }, { title: "Reference Data", fields: [ { id: "description", label: "Key Data (VIN, Names, ID #) - Use #tags", type: "textarea" }, { id: "location", label: "Where is it happening/stored?", type: "text" }, ], }, ], }, }; // --- State --- let allEntries = []; let editingId = null; let currentStep = 0; let currentCatKey = null; let wizardData = {}; // NEW: OS-Site Style State let activeTags = new Set(); let searchQuery = ""; // --- Elements --- const grid = document.getElementById("grid"); const modal = document.getElementById("modal"); const modalBody = document.getElementById("wizard-body"); const prevBtn = document.getElementById("prev-btn"); const nextBtn = document.getElementById("next-btn"); const searchInput = document.getElementById("search-input"); document.addEventListener("DOMContentLoaded", () => { fetchEntries(); searchInput.addEventListener("input", (e) => { searchQuery = e.target.value.trim().toLowerCase(); refreshViews(); }); }); // ========================================================== // --- DATA LAYER (REPLACED RUST BACKEND WITH LOCALSTORAGE) --- // ========================================================== const DB_KEY = "lifeman_data"; // Helper: Generate UUID (Replaces Rust uuid::Uuid::new_v4) function uuidv4() { return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ); } async function fetchEntries() { // OLD: const res = await fetch("/api/entries"); allEntries = await res.json(); // NEW: Load from LocalStorage const raw = localStorage.getItem(DB_KEY); if (raw) { allEntries = JSON.parse(raw); } else { // Initialize with empty if nothing exists allEntries = []; } // Sort logic (ported from Rust handlers.rs) // Sort: Pinned/Daily first (logic not in JSON but implied), then by created_at desc allEntries.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); renderTagBar(); refreshViews(); } async function saveData() { // Prepare the payload const payload = { template_type: currentCatKey, title: wizardData.title || "Untitled", description: wizardData.description || "", frequency: wizardData.frequency || "One-Time", event_date: wizardData.event_date ? new Date(wizardData.event_date).toISOString() : null, tags: extractTags(wizardData.description || ""), details: wizardData, updated_at: new Date().toISOString() }; if (editingId) { // --- UPDATE LOGIC --- const index = allEntries.findIndex(e => e.id === editingId); if (index !== -1) { // Merge existing data with new payload allEntries[index] = { ...allEntries[index], ...payload }; } } else { // --- CREATE LOGIC --- const newEntry = { id: uuidv4(), status: "Active", created_at: new Date().toISOString(), ...payload }; allEntries.push(newEntry); } // Persist to LocalStorage localStorage.setItem(DB_KEY, JSON.stringify(allEntries)); closeModal(); fetchEntries(); } async function deleteEntry(id) { if (confirm("Are you sure you want to remove this?")) { // Filter out the deleted item allEntries = allEntries.filter(e => e.id !== id); // Save localStorage.setItem(DB_KEY, JSON.stringify(allEntries)); fetchEntries(); } } // ========================================================== // --- END DATA LAYER --- // ========================================================== function refreshViews() { if (currentView === "list") renderGrid(); else renderCalendar(); } // --- Tag System --- function renderTagBar() { const container = document.getElementById("tag-filter-bar"); const tags = new Set(); allEntries.forEach((entry) => { if (entry.tags) entry.tags.forEach((t) => tags.add(t)); const matches = entry.description ? entry.description.match(/#\w+/g) : []; if (matches) matches.forEach((m) => tags.add(m.substring(1))); }); container.innerHTML = ""; const allBtn = document.createElement("button"); allBtn.className = `chip ${activeTags.size === 0 ? "active" : ""}`; allBtn.textContent = "All"; allBtn.onclick = () => { activeTags.clear(); renderTagBar(); refreshViews(); }; container.appendChild(allBtn); Array.from(tags).sort().forEach((tag) => { const btn = document.createElement("button"); btn.className = `chip ${activeTags.has(tag) ? "active" : ""}`; btn.textContent = `#${tag}`; btn.onclick = () => toggleTag(tag); container.appendChild(btn); }); } function toggleTag(tag) { if (activeTags.has(tag)) { activeTags.delete(tag); } else { activeTags.add(tag); } renderTagBar(); refreshViews(); } // --- Filter Logic --- function entryMatchesFilter(entry) { const textMatch = !searchQuery || entry.title.toLowerCase().includes(searchQuery) || (entry.description && entry.description.toLowerCase().includes(searchQuery)); if (!textMatch) return false; if (activeTags.size === 0) return true; const entryTags = new Set(entry.tags || []); const descTags = entry.description ? entry.description.match(/#\w+/g) : []; if (descTags) descTags.forEach(t => entryTags.add(t.substring(1))); for (let tag of activeTags) { if (entryTags.has(tag)) return true; } return false; } function renderGrid() { grid.innerHTML = ""; const visibleEntries = allEntries.filter(entry => entryMatchesFilter(entry)); if (visibleEntries.length === 0) { grid.innerHTML = `

No entries found.

`; return; } visibleEntries.forEach((entry) => { const descWithTags = parseTags(entry.description || ""); const card = document.createElement("div"); card.className = "card"; card.innerHTML = `
${CATEGORIES[entry.template_type]?.emoji || "๐Ÿ“„"} ${entry.frequency || "One-Time"}

${entry.title}

${descWithTags}

`; grid.appendChild(card); }); } function parseTags(text) { if (!text) return ""; return text.replace( /#(\w+)/g, '#$1', ); } function filterByTag(tag) { // Simple wrapper to support the inline onclick in parseTags if (!activeTags.has(tag)) { activeTags.clear(); activeTags.add(tag); renderTagBar(); refreshViews(); } } // --- Wizard / Editor Logic --- function openNew() { editingId = null; wizardData = {}; showCategorySelect(); modal.classList.add("open"); } async function openEdit(id) { const entry = allEntries.find((e) => e.id === id); if (!entry) return; editingId = id; currentCatKey = entry.template_type; let formattedDate = ""; if (entry.event_date) { const d = new Date(entry.event_date); const pad = (n) => (n < 10 ? "0" + n : n); formattedDate = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; } wizardData = { title: entry.title, description: entry.description, frequency: entry.frequency, event_date: formattedDate, ...entry.details, }; startWizard(currentCatKey); modal.classList.add("open"); } function showCategorySelect() { modalBody.innerHTML = `

What are we organizing?

${Object.entries(CATEGORIES) .map( ([key, cat]) => `
${cat.emoji}
${cat.name}
${cat.description}
`, ) .join("")}
`; prevBtn.classList.add("hidden"); nextBtn.classList.add("hidden"); } function startWizard(key) { currentCatKey = key; currentStep = 0; renderStep(); } function renderStep() { const template = CATEGORIES[currentCatKey]; const step = template.steps[currentStep]; let html = `

${step.title}

`; step.fields.forEach((field) => { const val = wizardData[field.id] || ""; html += `
`; if (field.type === "textarea") { html += ``; } else if (field.type === "select") { html += ``; } else { html += ``; } html += `
`; }); modalBody.innerHTML = html; prevBtn.classList.remove("hidden"); nextBtn.classList.remove("hidden"); nextBtn.innerText = currentStep === template.steps.length - 1 ? editingId ? "Update" : "Save" : "Next"; } window.moveWizard = async (dir) => { const inputs = modalBody.querySelectorAll("input, select, textarea"); inputs.forEach((i) => { const key = i.id.replace("inp_", ""); wizardData[key] = i.value; }); const template = CATEGORIES[currentCatKey]; const nextIdx = currentStep + dir; if (dir === -1 && currentStep === 0) { if (editingId) closeModal(); else showCategorySelect(); return; } if (nextIdx >= template.steps.length) { await saveData(); return; } currentStep = nextIdx; renderStep(); }; function extractTags(text) { const match = text.match(/#\w+/g); return match ? match.map((t) => t.substring(1)) : []; } function closeModal() { modal.classList.remove("open"); } // --- Calendar Logic --- let currentDate = new Date(); let currentView = "list"; function switchView(view) { currentView = view; document.getElementById("view-list").classList.toggle("active", view === "list"); document.getElementById("view-cal").classList.toggle("active", view === "calendar"); if (view === "calendar") { document.getElementById("grid").classList.add("hidden"); document.getElementById("calendar-view").classList.remove("hidden"); renderCalendar(); } else { document.getElementById("grid").classList.remove("hidden"); document.getElementById("calendar-view").classList.add("hidden"); } } function changeMonth(dir) { currentDate.setMonth(currentDate.getMonth() + dir); renderCalendar(); } function renderCalendar() { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const monthNames = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; document.getElementById("cal-month-name").innerText = `${monthNames[month]} ${year}`; const daysContainer = document.getElementById("cal-days"); daysContainer.innerHTML = ""; const firstDayIndex = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const today = new Date(); for (let i = 0; i < firstDayIndex; i++) { const div = document.createElement("div"); div.className = "cal-day"; div.style.background = "#111"; daysContainer.appendChild(div); } for (let i = 1; i <= daysInMonth; i++) { const dayDiv = document.createElement("div"); dayDiv.className = "cal-day"; if (i === today.getDate() && month === today.getMonth() && year === today.getFullYear()) { dayDiv.classList.add("today"); } dayDiv.innerHTML = `${i}`; const dayEvents = getEventsForDay(year, month, i); dayEvents.forEach((evt) => { const el = document.createElement("div"); el.className = `cal-event type-${evt.type}`; el.innerText = evt.title; el.onclick = (e) => { if(window.innerWidth > 768) { e.stopPropagation(); openEdit(evt.id); } }; dayDiv.appendChild(el); }); dayDiv.onclick = () => { selectMobileDay(dayDiv, dayEvents, i, year, month); }; daysContainer.appendChild(dayDiv); } } function getEventsForDay(year, month, day) { const targetDate = new Date(year, month, day); const dayOfWeek = targetDate.getDay(); const dayOfMonth = day; const targetMonth = month; let events = []; allEntries.forEach((entry) => { if (!entryMatchesFilter(entry)) return; const freq = entry.frequency || "One-Time"; const title = entry.title; const id = entry.id; const baseDateStr = entry.event_date || entry.created_at; const baseDate = new Date(baseDateStr); if (freq === "One-Time") { if (baseDate.getFullYear() === year && baseDate.getMonth() === month && baseDate.getDate() === day) { events.push({ id, title, type: "one-time", category: entry.template_type }); } } if (freq === "Daily") { if (targetDate >= new Date(baseDate).setHours(0, 0, 0, 0)) { events.push({ id, title, type: "daily", category: entry.template_type }); } } if (freq === "Weekly") { if (baseDate.getDay() === dayOfWeek && targetDate >= new Date(baseDate).setHours(0, 0, 0, 0)) { events.push({ id, title, type: "weekly", category: entry.template_type }); } } if (freq === "Monthly") { if (baseDate.getDate() === dayOfMonth && targetDate >= new Date(baseDate).setHours(0, 0, 0, 0)) { events.push({ id, title, type: "monthly", category: entry.template_type }); } } if (freq === "Yearly") { if (baseDate.getMonth() === targetMonth && baseDate.getDate() === dayOfMonth && targetDate >= new Date(baseDate).setHours(0, 0, 0, 0)) { events.push({ id, title, type: "yearly", category: entry.template_type }); } } }); return events; } function selectMobileDay(el, events, day, year, month) { document.querySelectorAll('.cal-day').forEach(d => d.classList.remove('selected')); el.classList.add('selected'); const detailBox = document.getElementById('mobile-day-details'); const titleEl = document.getElementById('mobile-selected-date-title'); const listEl = document.getElementById('mobile-event-list'); detailBox.classList.remove('hidden'); const dateObj = new Date(year, month, day); titleEl.innerText = dateObj.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' }); if (events.length === 0) { listEl.innerHTML = `
No events planned.
`; return; } listEl.innerHTML = events.map(evt => `
${evt.title} ${evt.category}
`).join(''); if(window.innerWidth < 600) { detailBox.scrollIntoView({ behavior: "smooth" }); } } function getCategoryColor(type) { if(type === 'daily') return 'var(--success)'; if(type === 'weekly') return 'var(--warning)'; if(type === 'monthly') return '#2979ff'; if(type === 'yearly') return '#ff4081'; return 'var(--accent)'; } // ====================================================== // NEW: Data Import / Export Logic // ====================================================== function downloadData() { if (allEntries.length === 0) { alert("Nothing to save!"); return; } // 1. Convert data to JSON string const dataStr = JSON.stringify(allEntries, null, 2); // 2. Create a Blob (Binary Large Object) const blob = new Blob([dataStr], { type: "application/json" }); // 3. Create a temporary download link const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; // 4. Name the file with today's date const date = new Date().toISOString().split('T')[0]; a.download = `lifeman_backup_${date}.json`; // 5. Trigger download and cleanup document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function triggerImport() { // Click the hidden file input programmatically document.getElementById('import-file').click(); } function handleFileImport(input) { const file = input.files[0]; if (!file) return; const reader = new FileReader(); // When the file is read... reader.onload = function(e) { try { const json = JSON.parse(e.target.result); // Basic validation if (!Array.isArray(json)) { throw new Error("File content is not a valid list."); } // Ask user for strategy const strategy = confirm( "Click OK to MERGE with existing data.\nClick Cancel to REPLACE all data." ); if (strategy === false) { // REPLACE STRATEGY allEntries = json; localStorage.setItem(DB_KEY, JSON.stringify(allEntries)); alert("Database replaced successfully!"); } else { // MERGE STRATEGY // We map existing items by ID for easy lookup let count = 0; json.forEach(newItem => { const index = allEntries.findIndex(old => old.id === newItem.id); if (index !== -1) { // Update existing allEntries[index] = newItem; } else { // Add new allEntries.push(newItem); } count++; }); localStorage.setItem(DB_KEY, JSON.stringify(allEntries)); alert(`Processed ${count} items.`); } // Refresh UI renderTagBar(); refreshViews(); } catch (err) { console.error(err); alert("Error reading file: " + err.message); } // Reset input so you can load the same file again if needed input.value = ''; }; reader.readAsText(file); }