Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,33 @@
.animate-fade-out {
animation: fadeOut 0.3s ease-out;
}
/* Mobile-only quick add controls. These supplement Tailwind output so the FAB stays mobile-only even when CSS is not rebuilt. */
#mobileQuickAddBtn {
display: none;
}

@media (max-width: 767px) {
#mobileQuickAddBtn {
display: flex;
left: 1.5rem;
}

#mobileQuickAddModal.flex {
align-items: flex-end;
padding: 0 0.75rem 0.75rem;
}

#mobileQuickAddModal > div {
max-height: 92vh;
}
}

@media (min-width: 768px) {
#mobileQuickAddModal,
#mobileQuickAddBtn {
display: none !important;
}
}
</style>
<script src="https://cdn.jsdelivr.net/npm/flatpickr"
integrity="sha384-5JqMv4L/Xa0hfvtF06qboNdhvuYXUku9ZrhZh3bSk8VXF0A/RuSLHpLsSV9Zqhl6"
Expand Down Expand Up @@ -1002,6 +1029,113 @@ <h3 class="text-lg font-bold text-slate-800 flex items-center gap-2">
</div>
</div>


<!-- Mobile Quick Add Button -->
<button id="mobileQuickAddBtn" type="button" aria-label="Quick add income or expense"
class="fixed bottom-6 left-6 z-40 bg-blue-600 hover:bg-blue-700 text-white rounded-full w-14 h-14 md:hidden flex items-center justify-center shadow-xl transition-all hover:scale-105 active:scale-95">
<i class="ph ph-plus text-2xl"></i>
</button>

<!-- Mobile Quick Add Modal -->
<div id="mobileQuickAddModal" class="fixed inset-0 bg-slate-900/60 hidden items-end justify-center z-50 md:hidden px-3 pb-3">
<div class="bg-white rounded-2xl w-full max-w-md shadow-2xl overflow-hidden animate-fade-in max-h-[92vh] flex flex-col">
<div class="p-5 border-b border-slate-100 flex items-center justify-between flex-shrink-0">
<div>
<h3 class="text-lg font-bold text-slate-800 flex items-center gap-2">
<i class="ph ph-plus-circle text-blue-500"></i> Quick Add
</h3>
<p class="text-xs text-slate-500 mt-0.5">Add income or expenses from any view.</p>
</div>
<button id="closeMobileQuickAddBtn" type="button" class="text-slate-400 hover:text-slate-600 transition-colors p-1" aria-label="Close quick add">
<i class="ph ph-x text-xl"></i>
</button>
</div>
<div class="p-5 overflow-y-auto custom-scrollbar">
<form id="mobileQuickAddForm" class="space-y-4">
<div class="flex p-1 bg-slate-100 rounded-lg">
<label class="flex-1 cursor-pointer" for="mobileQuickTypeExpense">
<input type="radio" id="mobileQuickTypeExpense" name="mobileQuickType" value="expense" class="peer sr-only" checked>
<div class="text-center py-2 rounded-md text-sm font-medium text-slate-500 peer-checked:bg-white peer-checked:text-rose-600 peer-checked:shadow-sm transition-all">Expense</div>
</label>
<label class="flex-1 cursor-pointer" for="mobileQuickTypeIncome">
<input type="radio" id="mobileQuickTypeIncome" name="mobileQuickType" value="income" class="peer sr-only">
<div class="text-center py-2 rounded-md text-sm font-medium text-slate-500 peer-checked:bg-white peer-checked:text-emerald-600 peer-checked:shadow-sm transition-all">Income</div>
</label>
</div>

<div>
<label for="mobileQuickAmount" class="block text-xs font-semibold text-slate-500 mb-1 uppercase tracking-wider">Amount</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span id="mobileQuickCurrencySymbol" class="text-slate-400 sm:text-sm">$</span>
</div>
<input type="number" id="mobileQuickAmount" step="0.01" min="0.01" required placeholder="0.00"
class="input-field w-full pl-8 pr-3 py-2 bg-slate-50 border border-slate-200 rounded-lg focus:outline-none text-slate-800 placeholder-slate-400">
</div>
</div>

<div>
<label for="mobileQuickCategory" class="block text-xs font-semibold text-slate-500 mb-1 uppercase tracking-wider">Category</label>
<div class="relative">
<select id="mobileQuickCategory" required class="input-field w-full pl-3 pr-8 py-2 bg-slate-50 border border-slate-200 rounded-lg focus:outline-none text-slate-800 appearance-none">
<option value="" disabled selected>Select category...</option>
<optgroup label="Expenses" id="mobileQuickExpenseCategories">
<option value="Food & Dining">Food & Dining</option>
<option value="Housing">Housing</option>
<option value="Transportation">Transportation</option>
<option value="Utilities">Utilities</option>
<option value="Entertainment">Entertainment</option>
<option value="Healthcare">Healthcare</option>
<option value="Shopping">Shopping</option>
<option value="Other">Other Expense</option>
</optgroup>
<optgroup label="Income" id="mobileQuickIncomeCategories">
<option value="Salary">Salary / Wages</option>
<option value="Freelance">Freelance</option>
<option value="Investments">Investments</option>
<option value="Gifts">Gifts</option>
<option value="Other">Other Income</option>
</optgroup>
</select>
<i class="ph ph-caret-down absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i>
</div>
</div>

<div>
<label for="mobileQuickDate" class="block text-xs font-semibold text-slate-500 mb-1 uppercase tracking-wider">Date</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="ph ph-calendar-blank text-slate-400"></i>
</div>
<input type="text" id="mobileQuickDate" required placeholder="Select date..."
class="input-field w-full pl-9 pr-3 py-2 bg-slate-50 border border-slate-200 rounded-lg focus:outline-none text-slate-800 placeholder-slate-400 cursor-pointer">
</div>
</div>

<div>
<label for="mobileQuickFamilyMember" class="block text-xs font-semibold text-slate-500 mb-1 uppercase tracking-wider">Family Member (Optional)</label>
<div class="relative">
<select id="mobileQuickFamilyMember" class="input-field w-full pl-3 pr-8 py-2 bg-slate-50 border border-slate-200 rounded-lg focus:outline-none text-slate-800 appearance-none">
<option value="" selected>None / Myself</option>
</select>
<i class="ph ph-caret-down absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i>
</div>
</div>

<div>
<label for="mobileQuickNote" class="block text-xs font-semibold text-slate-500 mb-1 uppercase tracking-wider">Note (Optional)</label>
<input type="text" id="mobileQuickNote" placeholder="What was this for?"
class="input-field w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg focus:outline-none text-slate-800 placeholder-slate-400">
</div>

<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2">
<i class="ph ph-check-circle"></i> Add Entry
</button>
</form>
</div>
</div>
</div>

<!-- Auth Modal -->
<div id="authModal" class="fixed inset-0 bg-slate-900/70 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-sm p-8">
Expand Down
164 changes: 127 additions & 37 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,16 @@ const managedMembersList = document.getElementById('managedMembersList');
const noMembersState = document.getElementById('noMembersState');
const familyMemberSelect = document.getElementById('familyMember');

// Mobile quick add elements
const mobileQuickAddBtn = document.getElementById('mobileQuickAddBtn');
const mobileQuickAddModal = document.getElementById('mobileQuickAddModal');
const closeMobileQuickAddBtn = document.getElementById('closeMobileQuickAddBtn');
const mobileQuickAddForm = document.getElementById('mobileQuickAddForm');
const mobileQuickCurrencySymbol = document.getElementById('mobileQuickCurrencySymbol');
const mobileQuickExpenseCategories = document.getElementById('mobileQuickExpenseCategories');
const mobileQuickIncomeCategories = document.getElementById('mobileQuickIncomeCategories');
const mobileQuickFamilyMemberSelect = document.getElementById('mobileQuickFamilyMember');

// --- INITIALIZATION ---
function init() {
// Initialize Currency Selector
Expand All @@ -375,13 +385,30 @@ function init() {
dateFormat: "Y-m-d",
disableMobile: true // Ensures the custom modern UI shows on mobile devices instead of native pickers
});
window.mobileQuickDatePicker = flatpickr("#mobileQuickDate", {
defaultDate: "today",
dateFormat: "Y-m-d",
disableMobile: true
});
updateMobileQuickCategoryOptions('expense');

// Event Listeners
currencyButton.addEventListener('click', () => {
openCurrencyModal();
});

form.addEventListener('submit', addExpense);
mobileQuickAddForm.addEventListener('submit', addMobileQuickEntry);
mobileQuickAddBtn.addEventListener('click', openMobileQuickAdd);
closeMobileQuickAddBtn.addEventListener('click', closeMobileQuickAdd);
mobileQuickAddModal.addEventListener('click', (e) => {
if (e.target === mobileQuickAddModal) {
closeMobileQuickAdd();
}
});
document.querySelectorAll('input[name="mobileQuickType"]').forEach(radio => {
radio.addEventListener('change', () => updateMobileQuickCategoryOptions(radio.value));
});

updateDarkModeToggle(document.documentElement.classList.contains('dark'));

Expand Down Expand Up @@ -593,6 +620,7 @@ function initializeCurrencySelector() {
// Initialize with default currency
currentCurrencyEl.textContent = currentCurrency;
currencySymbolSpan.textContent = getCurrencySymbol(currentCurrency);
mobileQuickCurrencySymbol.textContent = getCurrencySymbol(currentCurrency);

// Render the initial currency options
renderCurrencyOptions('');
Expand Down Expand Up @@ -633,6 +661,7 @@ function selectCurrency(currency) {
localStorage.setItem('sw_currency', currency);
currentCurrencyEl.textContent = currency;
currencySymbolSpan.textContent = getCurrencySymbol(currency);
mobileQuickCurrencySymbol.textContent = getCurrencySymbol(currency);

// Update all currency displays with conversion
updateSummary();
Expand Down Expand Up @@ -758,30 +787,23 @@ async function removeMember(name) {
}

function updateFamilyMemberSelect() {
familyMemberSelect.innerHTML = '<option value="" selected>None / Myself</option>';

familyMembers.forEach(member => {
const option = document.createElement('option');
option.value = member;
option.textContent = member;
familyMemberSelect.appendChild(option);
const selects = [familyMemberSelect, mobileQuickFamilyMemberSelect].filter(Boolean);

selects.forEach(select => {
select.innerHTML = '<option value="" selected>None / Myself</option>';

familyMembers.forEach(member => {
const option = document.createElement('option');
option.value = member;
option.textContent = member;
select.appendChild(option);
});
});
}

// --- EXPENSE HANDLING ---
async function addExpense(e) {
e.preventDefault();

const type = form.type.value;
const amount = parseFloat(form.amount.value);
const category = form.category.value;
const date = form.date.value;
const familyMember = form.familyMember.value || '';
const note = form.note.value || '';

if (!type || Number.isNaN(amount) || !category || !date) return;

let newEntry;
async function saveTransaction({ type, amount, category, date, familyMember = '', note = '' }) {
if (!type || Number.isNaN(amount) || !category || !date) return null;

if (isLoggedIn) {
try {
Expand All @@ -792,19 +814,51 @@ async function addExpense(e) {
if (!res.ok) {
const err = await res.json().catch(() => ({}));
console.error('Failed to save expense:', err.error ?? res.status);
return;
return null;
}
newEntry = mapServerExpense(await res.json());
return mapServerExpense(await res.json());
} catch (err) {
console.error('Network error saving expense:', err);
return;
return null;
}
} else {
newEntry = { id: String(Date.now()), type, amount, category, date, familyMember, note, currency: currentCurrency };
}

return { id: String(Date.now()), type, amount, category, date, familyMember, note, currency: currentCurrency };
}

async function refreshAfterTransactionAdded(newEntry) {
if (!newEntry) return;

expense.unshift(newEntry);

await updateSummary();
await renderExpenses();
await updateExpenseChart();

if (currentFilter === 'income') {
await renderIncomeView();
} else if (currentFilter === 'expense') {
await renderExpenseView();
} else if (currentFilter === 'history') {
await renderHistoryView();
Comment on lines +842 to +843

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh the account view after quick-add saves

When the FAB is used from the Account view (currentFilter === 'account'), the new entry is added to expense[] but this refresh helper never calls renderAccountView(). The account cards are computed only inside renderAccountView() from the in-memory transaction list, so after adding an income/expense from that visible view the transaction count, income, expense, and balance remain stale until the user navigates away and back.

Useful? React with 👍 / 👎.

}
}

async function addExpense(e) {
e.preventDefault();

const type = form.type.value;
const newEntry = await saveTransaction({
type,
amount: parseFloat(form.amount.value),
category: form.category.value,
date: form.date.value,
familyMember: form.familyMember.value || '',
note: form.note.value || '',
});

if (!newEntry) return;

// Reset form
if (type === 'expense') {
typeRadios[1].checked = true;
Expand All @@ -822,19 +876,26 @@ async function addExpense(e) {
// Reset date picker to today
window.datePicker.setDate("today");

// Update UI
updateSummary();
renderExpenses();
updateExpenseChart();
await refreshAfterTransactionAdded(newEntry);
}

// Update current view if needed
if (currentFilter === 'income') {
renderIncomeView();
} else if (currentFilter === 'expense') {
renderExpenseView();
} else if (currentFilter === 'history') {
renderHistoryView();
}
async function addMobileQuickEntry(e) {
e.preventDefault();

const newEntry = await saveTransaction({
type: mobileQuickAddForm.mobileQuickType.value,
amount: parseFloat(document.getElementById('mobileQuickAmount').value),
category: document.getElementById('mobileQuickCategory').value,
date: document.getElementById('mobileQuickDate').value,
familyMember: mobileQuickFamilyMemberSelect.value || '',
note: document.getElementById('mobileQuickNote').value || '',
});

if (!newEntry) return;

resetMobileQuickAddForm();
closeMobileQuickAdd();
await refreshAfterTransactionAdded(newEntry);
}

function updateCategoryOptions(type) {
Expand All @@ -847,6 +908,35 @@ function updateCategoryOptions(type) {
}
}

function updateMobileQuickCategoryOptions(type) {
if (type === 'expense') {
mobileQuickExpenseCategories.style.display = '';
mobileQuickIncomeCategories.style.display = 'none';
} else {
mobileQuickExpenseCategories.style.display = 'none';
mobileQuickIncomeCategories.style.display = '';
}

document.getElementById('mobileQuickCategory').value = '';
}

function openMobileQuickAdd() {
mobileQuickAddModal.classList.remove('hidden');
mobileQuickAddModal.classList.add('flex');
document.getElementById('mobileQuickAmount').focus();
}

function closeMobileQuickAdd() {
mobileQuickAddModal.classList.add('hidden');
mobileQuickAddModal.classList.remove('flex');
}

function resetMobileQuickAddForm() {
mobileQuickAddForm.reset();
updateMobileQuickCategoryOptions('expense');
window.mobileQuickDatePicker.setDate("today");
}

async function deleteExpense(id) {
if (isLoggedIn) {
try {
Expand Down
Loading