Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
coverage/
246 changes: 193 additions & 53 deletions budget.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
//SELECT ELEMENTS
// =====================
// SELECT DOM ELEMENTS
// =====================
const balanceEl = document.querySelector(".balance .value");
const incomeTotalEl = document.querySelector(".income-total");
const outcomeTotalEl = document.querySelector(".outcome-total");

const incomeEl = document.querySelector("#income");
const expenseEl = document.querySelector("#expense");
const allEl = document.querySelector("#all");

const incomeList = document.querySelector("#income .list");
const expenseList = document.querySelector("#expense .list");
const allList = document.querySelector("#all .list");

//SELECT BUTTONS
// =====================
// SELECT BUTTONS
// =====================
const expenseBtn = document.querySelector(".first-tab");
const incomeBtn = document.querySelector(".second-tab");
const allBtn = document.querySelector(".third-tab");

//INPUT BTS
// =====================
// INPUT ELEMENTS
// =====================
const addExpense = document.querySelector(".add-expense");
const expenseTitle = document.getElementById("expense-title-input");
const expenseAmount = document.getElementById("expense-amount-input");
Expand All @@ -23,180 +31,312 @@ const addIncome = document.querySelector(".add-income");
const incomeTitle = document.getElementById("income-title-input");
const incomeAmount = document.getElementById("income-amount-input");

//VARIABLES
// =====================
// STATE VARIABLES
// =====================
let ENTRY_LIST;
let balance = 0,
income = 0,
outcome = 0;
const DELETE = "delete",
EDIT = "edit";

// LOOK IF THERE IS DATA IN LOCAL STORAGE
ENTRY_LIST = JSON.parse(localStorage.getItem("entry_list")) || [];
let balance = 0;
let income = 0;
let outcome = 0;

// Action identifiers
const DELETE = "delete";
const EDIT = "edit";

// Validation message
const AMOUNT_ERROR = "Please enter a valid positive amount.";

// =====================
// LOCAL STORAGE HANDLING
// =====================

// Load entries safely from localStorage
function loadEntries() {
try {
const savedData = localStorage.getItem("entry_list");

if (!savedData) return [];

const parsedData = JSON.parse(savedData);

if (!Array.isArray(parsedData)) {
throw new Error("Invalid entry list format");
}

return parsedData;
} catch (error) {
console.error("Failed to load entries:", error);

// Reset corrupted data
localStorage.removeItem("entry_list");
alert("Saved data was corrupted and has been reset.");

return [];
}
}

// Save entries safely to localStorage
function saveEntries() {
try {
localStorage.setItem("entry_list", JSON.stringify(ENTRY_LIST));
} catch (error) {
console.error("Failed to save entries:", error);
alert("Unable to save budget data.");
}
}

// Initialize data
ENTRY_LIST = loadEntries();
updateUI();

//EVENT LISTENERS
expenseBtn.addEventListener("click", function () {
// =====================
// EVENT LISTENERS
// =====================

// Tab switching
expenseBtn.addEventListener("click", () => {
show(expenseEl);
hide([incomeEl, allEl]);
active(expenseBtn);
inactive([incomeBtn, allBtn]);
});
incomeBtn.addEventListener("click", function () {

incomeBtn.addEventListener("click", () => {
show(incomeEl);
hide([expenseEl, allEl]);
active(incomeBtn);
inactive([expenseBtn, allBtn]);
});
allBtn.addEventListener("click", function () {

allBtn.addEventListener("click", () => {
show(allEl);
hide([incomeEl, expenseEl]);
active(allBtn);
inactive([incomeBtn, expenseBtn]);
});

addExpense.addEventListener("click", function () {
// CHECK IF ONE OF THE INPUT IS EMPTY => EXIT
if (!expenseTitle.value || !expenseAmount.value) return;
// Add expense
addExpense.addEventListener("click", () => {
if (!expenseTitle.value) return;

const amount = getPositiveAmount(expenseAmount);
if (amount === null) return;

// ADD INPUTs TO ENTRY_LIST
let expense = {
const expense = {
type: "expense",
title: expenseTitle.value,
amount: +expenseAmount.value,
amount: amount,
};
ENTRY_LIST.push(expense);

ENTRY_LIST.push(expense);
updateUI();
clearInput([expenseTitle, expenseAmount]);
});

addIncome.addEventListener("click", function () {
// CHECK IF ONE OF THE INPUT IS EMPTY => EXIT
if (!incomeTitle.value || !incomeAmount.value) return;
// Add income
addIncome.addEventListener("click", () => {
if (!incomeTitle.value) return;

// ADD INPUTs TO ENTRY_LIST
let income = {
const amount = getPositiveAmount(incomeAmount);
if (amount === null) return;

const incomeEntry = {
type: "income",
title: incomeTitle.value,
amount: +incomeAmount.value,
amount: amount,
};
ENTRY_LIST.push(income);

ENTRY_LIST.push(incomeEntry);
updateUI();
clearInput([incomeTitle, incomeAmount]);
});

// Handle delete/edit clicks
incomeList.addEventListener("click", deleteOrEdit);
expenseList.addEventListener("click", deleteOrEdit);
allList.addEventListener("click", deleteOrEdit);

// HELEPER FUNCS
// =====================
// CORE FUNCTIONS
// =====================

// Decide whether to delete or edit an entry
function deleteOrEdit(event) {
const targetBtn = event.target;
const entry = targetBtn.parentNode;

if (targetBtn.id == EDIT) {
if (targetBtn.id === EDIT) {
editEntry(entry);
} else if (targetBtn.id == DELETE) {
} else if (targetBtn.id === DELETE) {
deleteEntry(entry);
}
}

// Remove entry from list
function deleteEntry(entry) {
ENTRY_LIST.splice(entry.id, 1);
updateUI();
}

// Load entry data into inputs for editing
function editEntry(entry) {
const ENTRY = ENTRY_LIST[entry.id];

if (ENTRY.type == "income") {
if (ENTRY.type === "income") {
incomeTitle.value = ENTRY.title;
incomeAmount.value = ENTRY.amount;
} else if (ENTRY.type == "expense") {
} else {
expenseTitle.value = ENTRY.title;
expenseAmount.value = ENTRY.amount;
}

deleteEntry(entry);
}

// Update UI and recalculate values
function updateUI() {
income = calculateTotal("income", ENTRY_LIST);
outcome = calculateTotal("expense", ENTRY_LIST);
balance = Math.abs(calculateBalance(income, outcome));

let sign = income >= outcome ? "$" : "-$";
const sign = income >= outcome ? "$" : "-$";

//UPDATE UI
balanceEl.innerHTML = `<small>${sign}</small>${balance}`;
outcomeTotalEl.innerHTML = `<small>$</small>${outcome}`;
incomeTotalEl.innerHTML = `<small>$</small>${income}`;

clearElement([expenseList, incomeList, allList]);

ENTRY_LIST.forEach((entry, index) => {
if (entry.type == "expense") {
if (entry.type === "expense") {
showEntry(expenseList, entry.type, entry.title, entry.amount, index);
} else if (entry.type == "income") {
} else {
showEntry(incomeList, entry.type, entry.title, entry.amount, index);
}

showEntry(allList, entry.type, entry.title, entry.amount, index);
});

updateChart(income, outcome);
localStorage.setItem("entry_list", JSON.stringify(ENTRY_LIST));

// Persist data
saveEntries();
}

// Render a single entry
function showEntry(list, type, title, amount, id) {
const entry = `<li id="${id}" class="${type}">
<div class="entry">${title} : $${amount}</div>
<div id="edit"></div>
<div id="delete"></div>
</li>`;
const position = "afterbegin";
list.insertAdjacentHTML(position, entry);
}
const li = document.createElement("li");
li.id = id;
li.className = type;

function clearElement(elements) {
elements.forEach((element) => {
element.innerHTML = "";
});
const entryDiv = document.createElement("div");
entryDiv.className = "entry";
entryDiv.textContent = `${title} : $${amount}`;

const editDiv = document.createElement("div");
editDiv.id = EDIT;

const deleteDiv = document.createElement("div");
deleteDiv.id = DELETE;

li.appendChild(entryDiv);
li.appendChild(editDiv);
li.appendChild(deleteDiv);

list.insertBefore(li, list.firstChild);
}

// =====================
// BUSINESS LOGIC
// =====================

// Calculate total income or expense
function calculateTotal(type, list) {
let sum = 0;

list.forEach((entry) => {
if (entry.type == type) {
if (entry.type === type) {
sum += entry.amount;
}
});

return sum;
}

// Calculate balance
function calculateBalance(income, outcome) {
return income - outcome;
}

// Validate positive numeric input
function getPositiveAmount(input) {
const amount = Number(input.value);

if (!Number.isFinite(amount) || amount <= 0) {
alert(AMOUNT_ERROR);
input.value = "";
input.focus();
return null;
}

return amount;
}

// =====================
// HELPER FUNCTIONS
// =====================

// Clear list elements
function clearElement(elements) {
elements.forEach((element) => {
element.innerHTML = "";
});
}

// Clear input fields
function clearInput(inputs) {
inputs.forEach((input) => {
input.value = "";
});
}

// Show element
function show(element) {
element.classList.remove("hide");
}

// Hide elements
function hide(elements) {
elements.forEach((element) => {
element.classList.add("hide");
});
}

// Activate tab
function active(element) {
element.classList.add("focus");
}

// Deactivate tabs
function inactive(elements) {
elements.forEach((element) => {
element.classList.remove("focus");
});
}

// =====================
// EXPORT FOR TESTING
// =====================
if (typeof module !== "undefined" && module.exports) {
module.exports = {
calculateTotal,
calculateBalance,
getPositiveAmount,
show,
hide,
active,
inactive,
clearInput,
clearElement,
};
}
Loading