From 59853757551108aed855a4bec72eaab76a3e37fb Mon Sep 17 00:00:00 2001 From: chris Date: Thu, 6 Nov 2025 00:32:05 -0500 Subject: [PATCH] update mobile experiance --- app.js | 406 +++++++++++++++++++++++++---------- index.html | 608 ++++++++++++++++++++++++++++++++++------------------- 2 files changed, 696 insertions(+), 318 deletions(-) diff --git a/app.js b/app.js index fc9ec6a..a497b6f 100644 --- a/app.js +++ b/app.js @@ -184,7 +184,7 @@ list.innerHTML = ''; categories.sort((a,b) => a.name.localeCompare(b.name)).forEach(cat => { const el = document.createElement('div'); - el.className = 'flex justify-between items-center p-2 bg-white rounded-lg shadow-sm'; + el.className = 'flex items-center justify-between rounded-xl bg-white/80 px-4 py-3 text-sm shadow-sm backdrop-blur transition hover:-translate-y-0.5 hover:shadow-lg dark:bg-slate-800/70 dark:text-slate-100'; el.innerHTML = ` ${cat.name} (${cat.type}) @@ -213,7 +213,7 @@ list.innerHTML = ''; vendors.forEach(v => { const el = document.createElement('div'); - el.className = 'flex justify-between items-center p-2 bg-white rounded-lg shadow-sm'; + el.className = 'flex items-center justify-between rounded-xl bg-white/80 px-4 py-3 text-sm shadow-sm backdrop-blur transition hover:-translate-y-0.5 hover:shadow-lg dark:bg-slate-800/70 dark:text-slate-100'; el.innerHTML = ` ${v.name} @@ -242,7 +242,7 @@ list.innerHTML = ''; rules.forEach(rule => { const el = document.createElement('div'); - el.className = 'flex justify-between items-center p-2 bg-white rounded-lg shadow-sm'; + el.className = 'flex items-center justify-between gap-3 rounded-xl bg-white/80 px-4 py-3 text-sm shadow-sm backdrop-blur transition hover:-translate-y-0.5 hover:shadow-lg dark:bg-slate-800/70 dark:text-slate-100'; el.innerHTML = ` If description contains "${rule.keyword}", set category to ${rule.category} @@ -254,9 +254,15 @@ function renderTransactions() { const tbody = document.getElementById('transactionsTableBody'); - const filter = document.getElementById('transactionFilter').value; - const search = document.getElementById('transactionSearch').value.toLowerCase(); - if (!tbody) return; + const mobileList = document.getElementById('transactionsListMobile'); + const filterEl = document.getElementById('transactionFilter'); + const searchEl = document.getElementById('transactionSearch'); + const mobileSortSelect = document.getElementById('mobileSortSelect'); + + if (!tbody && !mobileList) return; + + const filter = filterEl ? filterEl.value : 'all'; + const search = searchEl ? searchEl.value.toLowerCase() : ''; let txsToRender = [...transactions]; @@ -296,75 +302,146 @@ return 0; }); - // 2. Filter by Category Status + // 3. Filter by Category Status if (filter === 'uncategorized') { txsToRender = txsToRender.filter(tx => tx.reconciliation_status === 'uncategorized'); } - // 3. Filter by Search + // 4. Filter by Search if (search) { txsToRender = txsToRender.filter(tx => tx.description.toLowerCase().includes(search)); } - // 4. Update Header styles + // 5. Update Header styles + mobile select document.querySelectorAll('.sortable-header').forEach(th => { th.classList.remove('sort-asc', 'sort-desc'); if (th.dataset.sort === transactionSort.column) { th.classList.add(transactionSort.direction === 'asc' ? 'sort-asc' : 'sort-desc'); } }); + if (mobileSortSelect) { + const desiredValue = `${transactionSort.column}-${transactionSort.direction}`; + if (Array.from(mobileSortSelect.options).some(opt => opt.value === desiredValue)) { + mobileSortSelect.value = desiredValue; + } + } - // 5. Render - tbody.innerHTML = ''; + // 6. Render + if (tbody) tbody.innerHTML = ''; + if (mobileList) mobileList.innerHTML = ''; + if (txsToRender.length === 0) { - tbody.innerHTML = 'No transactions found.'; + if (tbody) { + tbody.innerHTML = 'No transactions found.'; + } + if (mobileList) { + const emptyCard = document.createElement('div'); + emptyCard.className = 'card text-center text-sm text-slate-500 dark:text-slate-300'; + emptyCard.textContent = 'No transactions found.'; + mobileList.appendChild(emptyCard); + } return; } txsToRender.forEach(tx => { const txDate = new Date(tx.date); - const tr = document.createElement('tr'); - tr.className = 'border-b hover:bg-gray-50'; - - // Determine Category/Vendor display + let categoryDisplay = ''; + let categoryLabel = ''; let vendorDisplay = ''; + let vendorLabel = ''; + if (tx.reconciliation_status === 'uncategorized') { - categoryDisplay = 'Uncategorized'; + categoryDisplay = 'Uncategorized'; + categoryLabel = 'Uncategorized'; vendorDisplay = 'N/A'; + vendorLabel = 'N/A'; } else if (tx.split_count === 1) { categoryDisplay = tx.category || 'N/A'; + categoryLabel = tx.category || 'N/A'; vendorDisplay = tx.vendorName || 'N/A'; + vendorLabel = tx.vendorName || 'N/A'; } else if (tx.split_count > 1) { categoryDisplay = '— Split —'; + categoryLabel = 'Split'; vendorDisplay = '— Split —'; - } else { // split_count is 0 or null - categoryDisplay = 'Needs Attention'; + vendorLabel = 'Split'; + } else { + categoryDisplay = 'Needs Attention'; + categoryLabel = 'Needs attention'; vendorDisplay = 'N/A'; + vendorLabel = 'N/A'; } - tr.innerHTML = ` - ${txDate.toLocaleDateString()} - ${tx.description} - ${categoryDisplay} - ${vendorDisplay} - - ${tx.amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })} - - - - - - `; - tbody.appendChild(tr); + if (tbody) { + const tr = document.createElement('tr'); + tr.className = 'border-b border-slate-100 transition dark:border-slate-800 hover:bg-slate-50/80 dark:hover:bg-slate-800/60'; + tr.innerHTML = ` + ${txDate.toLocaleDateString()} + ${tx.description} + ${categoryDisplay} + ${vendorDisplay} + + ${tx.amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })} + + + + + + `; + tbody.appendChild(tr); + } + + if (mobileList) { + const card = document.createElement('div'); + card.className = 'card space-y-3'; + const amountColor = tx.amount >= 0 ? 'text-emerald-500' : 'text-rose-500'; + card.innerHTML = ` +
+
+

${txDate.toLocaleDateString()}

+

${tx.description}

+
+
+

${tx.amount >= 0 ? 'Incoming' : 'Outgoing'}

+

${tx.amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })}

+
+
+
+ ${categoryLabel} + Vendor: ${vendorLabel} + ${tx.reconciliation_status} +
+
+ + +
+ `; + mobileList.appendChild(card); + } }); - tbody.querySelectorAll('.edit-tx-btn').forEach(btn => { - btn.onclick = (e) => showSplitTransactionModal(e.target.dataset.id); - }); - tbody.querySelectorAll('.delete-tx-btn').forEach(btn => { - btn.onclick = (e) => deleteTransaction(e.target.dataset.id); - }); + if (tbody) { + tbody.querySelectorAll('.edit-tx-btn').forEach(btn => { + btn.onclick = (e) => showSplitTransactionModal(e.target.dataset.id); + }); + tbody.querySelectorAll('.delete-tx-btn').forEach(btn => { + btn.onclick = (e) => deleteTransaction(e.target.dataset.id); + }); + } + + if (mobileList) { + mobileList.querySelectorAll('.mobile-edit-tx-btn').forEach(btn => { + btn.onclick = (e) => showSplitTransactionModal(e.currentTarget.dataset.id); + }); + mobileList.querySelectorAll('.mobile-delete-tx-btn').forEach(btn => { + btn.onclick = (e) => deleteTransaction(e.currentTarget.dataset.id); + }); + } } function handleSort(e) { @@ -1026,18 +1103,20 @@ document.querySelectorAll('main > section').forEach(section => { section.classList.add('hidden'); }); - document.querySelectorAll('nav a').forEach(a => { - a.classList.remove('bg-blue-700', 'text-white'); - a.classList.add('text-blue-100', 'hover:bg-blue-600'); + document.querySelectorAll('#sidebar nav a').forEach(a => { + a.classList.remove('active-nav'); + a.removeAttribute('aria-current'); }); const activeView = document.getElementById(viewId); - const activeLink = document.querySelector(`nav a[data-view="${viewId}"]`); + const activeLink = document.querySelector(`#sidebar nav a[data-view="${viewId}"]`); - if (activeView) activeView.classList.remove('hidden'); + if (activeView) { + activeView.classList.remove('hidden'); + } if (activeLink) { - activeLink.classList.add('bg-blue-700', 'text-white'); - activeLink.classList.remove('text-blue-100', 'hover:bg-blue-600'); + activeLink.classList.add('active-nav'); + activeLink.setAttribute('aria-current', 'page'); } // Refresh dashboard data only when switching to it @@ -1059,6 +1138,79 @@ // --- Initial App Load --- document.addEventListener('DOMContentLoaded', () => { + const body = document.body; + const root = document.documentElement; + const darkModeToggle = document.getElementById('darkModeToggle'); + const sidebar = document.getElementById('sidebar'); + const overlay = document.getElementById('mobileOverlay'); + const menuToggle = document.getElementById('menuToggle'); + const closeSidebarBtn = document.getElementById('closeSidebar'); + + const applyTheme = (mode) => { + const isDark = mode === 'dark'; + body.classList.toggle('dark-mode', isDark); + root.classList.toggle('dark', isDark); + }; + + const savedTheme = localStorage.getItem('simpleLedgerTheme'); + if (savedTheme) { + applyTheme(savedTheme); + } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + applyTheme('dark'); + } + + if (menuToggle) { + menuToggle.setAttribute('aria-expanded', 'false'); + } + + if (darkModeToggle) { + darkModeToggle.addEventListener('click', () => { + const isDark = !body.classList.contains('dark-mode'); + applyTheme(isDark ? 'dark' : 'light'); + localStorage.setItem('simpleLedgerTheme', isDark ? 'dark' : 'light'); + }); + } + + const openSidebar = () => { + if (!sidebar) return; + sidebar.classList.remove('-translate-x-full'); + sidebar.classList.add('translate-x-0'); + overlay?.classList.remove('hidden'); + menuToggle?.setAttribute('aria-expanded', 'true'); + }; + + const hideSidebar = () => { + if (!sidebar) return; + sidebar.classList.add('-translate-x-full'); + sidebar.classList.remove('translate-x-0'); + overlay?.classList.add('hidden'); + menuToggle?.setAttribute('aria-expanded', 'false'); + }; + + const maybeHideSidebar = () => { + if (window.innerWidth < 1024) { + hideSidebar(); + } + }; + + menuToggle?.addEventListener('click', openSidebar); + closeSidebarBtn?.addEventListener('click', hideSidebar); + overlay?.addEventListener('click', hideSidebar); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && window.innerWidth < 1024) { + hideSidebar(); + } + }); + + window.addEventListener('resize', () => { + if (window.innerWidth >= 1024) { + sidebar?.classList.remove('-translate-x-full'); + sidebar?.classList.remove('translate-x-0'); + overlay?.classList.add('hidden'); + menuToggle?.setAttribute('aria-expanded', 'false'); + } + }); + // Auth form toggling document.getElementById('toggleAuth').onclick = (e) => { e.preventDefault(); @@ -1071,74 +1223,114 @@ document.getElementById('registerForm').onsubmit = handleRegister; // App Navigation - document.querySelectorAll('nav a').forEach(a => { - a.onclick = (e) => { + document.querySelectorAll('#sidebar nav a').forEach(link => { + link.onclick = (e) => { e.preventDefault(); - if(e.target.id === 'logoutButton') return logout(); - showView(e.target.dataset.view); - } - }); - - // --- Event Listeners --- - document.getElementById('uploadForm').onsubmit = handleFiles; - document.getElementById('categoryForm').onsubmit = addCategory; - document.getElementById('vendorForm').onsubmit = addVendor; - document.getElementById('ruleForm').onsubmit = addRule; - document.getElementById('transactionFilter').onchange = renderTransactions; - document.getElementById('transactionSearch').oninput = renderTransactions; - document.getElementById('generateReportBtn').onclick = generateReport; - document.getElementById('exportReportBtn').onclick = exportReport; - document.getElementById('applyAllRulesBtn').onclick = applyAllRules; - - document.getElementById('dateFilterSelect').onchange = (e) => { - const customPicker = document.getElementById('customDateRangePicker'); - if (e.target.value === 'custom') { - customPicker.classList.remove('hidden'); - customPicker.classList.add('flex'); - } else { - customPicker.classList.add('hidden'); - customPicker.classList.remove('flex'); - } - }; - - document.getElementById('filterByDateBtn').onclick = () => { - const filterType = document.getElementById('dateFilterSelect').value; - transactionDateFilter = filterType; - - if (filterType === 'custom') { - const startDate = document.getElementById('startDate').value; - const endDate = document.getElementById('endDate').value; - if (startDate && endDate) { - customDateRange = { start: new Date(startDate), end: new Date(endDate) }; - } else { - swal('Error', 'Please select a start and end date for the custom range.', 'error'); - return; + const targetView = e.currentTarget.dataset.view; + if (targetView) { + showView(targetView); + maybeHideSidebar(); } - } - renderTransactions(); - }; + }; + }); - // Mobile menu - const sidebar = document.getElementById('sidebar'); - const mobileMenuBtn = document.getElementById('mobileMenuBtn'); - - if (mobileMenuBtn) { - mobileMenuBtn.onclick = () => { - sidebar.classList.toggle('-translate-x-full'); - sidebar.classList.toggle('hidden'); + const logoutButton = document.getElementById('logoutButton'); + if (logoutButton) { + logoutButton.onclick = (e) => { + e.preventDefault(); + logout(); + maybeHideSidebar(); }; } - // Close sidebar when a nav link is clicked on mobile - sidebar.querySelectorAll('a').forEach(link => { - link.addEventListener('click', () => { - if (window.innerWidth < 768) { // md breakpoint - sidebar.classList.add('-translate-x-full'); - sidebar.classList.add('hidden'); - } - }); - }); + const quickTransactions = document.getElementById('quickTransactions'); + if (quickTransactions) { + quickTransactions.onclick = () => { + showView('transactions'); + maybeHideSidebar(); + }; + } + const quickUpload = document.getElementById('quickUpload'); + if (quickUpload) { + quickUpload.onclick = () => { + showView('upload'); + maybeHideSidebar(); + }; + } + + // --- Event Listeners --- + const uploadForm = document.getElementById('uploadForm'); + if (uploadForm) uploadForm.onsubmit = handleFiles; + + const categoryForm = document.getElementById('categoryForm'); + if (categoryForm) categoryForm.onsubmit = addCategory; + + const vendorForm = document.getElementById('vendorForm'); + if (vendorForm) vendorForm.onsubmit = addVendor; + + const ruleForm = document.getElementById('ruleForm'); + if (ruleForm) ruleForm.onsubmit = addRule; + + const transactionFilterEl = document.getElementById('transactionFilter'); + if (transactionFilterEl) transactionFilterEl.onchange = renderTransactions; + + const transactionSearchEl = document.getElementById('transactionSearch'); + if (transactionSearchEl) transactionSearchEl.oninput = renderTransactions; + + const generateReportBtn = document.getElementById('generateReportBtn'); + if (generateReportBtn) generateReportBtn.onclick = generateReport; + + const exportReportBtn = document.getElementById('exportReportBtn'); + if (exportReportBtn) exportReportBtn.onclick = exportReport; + + const applyAllRulesBtn = document.getElementById('applyAllRulesBtn'); + if (applyAllRulesBtn) applyAllRulesBtn.onclick = applyAllRules; + + const dateFilterSelect = document.getElementById('dateFilterSelect'); + if (dateFilterSelect) { + dateFilterSelect.onchange = (e) => { + const customPicker = document.getElementById('customDateRangePicker'); + if (!customPicker) return; + transactionDateFilter = e.target.value; + if (e.target.value === 'custom') { + customPicker.classList.remove('hidden'); + customPicker.classList.add('flex'); + handleDateChange(); + } else { + customPicker.classList.add('hidden'); + customPicker.classList.remove('flex'); + customDateRange = { start: null, end: null }; + renderTransactions(); + } + }; + } + renderTransactions(); + + const handleDateChange = () => { + if (transactionDateFilter !== 'custom') return; + const startDateVal = document.getElementById('startDate')?.value; + const endDateVal = document.getElementById('endDate')?.value; + if (startDateVal && endDateVal) { + customDateRange = { start: new Date(startDateVal), end: new Date(endDateVal) }; + renderTransactions(); + } + }; + + const startDateInput = document.getElementById('startDate'); + const endDateInput = document.getElementById('endDate'); + if (startDateInput) startDateInput.onchange = handleDateChange; + if (endDateInput) endDateInput.onchange = handleDateChange; + + const mobileSortSelectEl = document.getElementById('mobileSortSelect'); + if (mobileSortSelectEl) { + mobileSortSelectEl.onchange = (e) => { + const [column, direction] = e.target.value.split('-'); + transactionSort.column = column; + transactionSort.direction = direction; + renderTransactions(); + }; + } // Transaction table sorting document.querySelectorAll('.sortable-header').forEach(th => { th.onclick = handleSort; diff --git a/index.html b/index.html index b93d5eb..f7103dc 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,10 @@ SimpleLedger - Your Personal Bookkeeper + + + + @@ -15,7 +19,7 @@ } .spinner { border: 4px solid rgba(0, 0, 0, .1); - border-left-color: #2563eb; + border-left-color: #6366f1; border-radius: 50%; width: 40px; height: 40px; @@ -34,7 +38,12 @@ user-select: none; } .sortable-header:hover { - background-color: #f9fafb; + background-color: rgba(148, 163, 184, 0.16); + color: #2563eb; + } + body.dark-mode .sortable-header:hover { + background-color: rgba(30, 41, 59, 0.8); + color: #93c5fd; } .sortable-header.sort-asc::after { content: ' ▲'; @@ -46,87 +55,91 @@ } - + -
-
-

Login

- +
+
+
+ SimpleLedger +

Login

+

Track income, tame expenses, celebrate wins.

+
+ -
-
- - + +
+ +
-
- - +
+ +
- - -