diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-02-20 16:11:50 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-02-20 16:15:08 +0100 |
| commit | 73a5c7fae9ffbe9ada721148c8c454a643aceebe (patch) | |
| tree | c8fad013ed9b5dd589add87f8d45cf02bbfc6e91 /public/projects/js-small-apps/budget-app | |
| parent | b01239fbdcc5bbc5921f73ec0e8fee7bedd5c8e8 (diff) | |
chore!: restructure repo
I separated public files from the config/dev files. It improves repo
readability.
I also moved dotenv helper to public/inc directory and extract the
Matomo tracker in the same directory.
Diffstat (limited to 'public/projects/js-small-apps/budget-app')
11 files changed, 1442 insertions, 0 deletions
diff --git a/public/projects/js-small-apps/budget-app/README.md b/public/projects/js-small-apps/budget-app/README.md new file mode 100644 index 0000000..fe8f09f --- /dev/null +++ b/public/projects/js-small-apps/budget-app/README.md @@ -0,0 +1,17 @@ +# Budget app + +A simple budget app. + +## Description + +You can define some categories for your expenses and incomes. Then you can add/edit/remove each type of transaction. + +The app will show you the remaining budget and the total expenses based on the initial budget. + +## Preview + +You can see a live preview here: https://demo.armandphilippot.com/#budget-app + +## License + +This project is open-source and available under the [MIT License](../LICENSE). diff --git a/public/projects/js-small-apps/budget-app/app.js b/public/projects/js-small-apps/budget-app/app.js new file mode 100644 index 0000000..2fb0cba --- /dev/null +++ b/public/projects/js-small-apps/budget-app/app.js @@ -0,0 +1,311 @@ +import BudgetApp from "./lib/class-budget-app.js"; +import Notification from "./lib/class-notification.js"; +import getCurrencyFormat from "./lib/utils/currency.js"; + +const app = new BudgetApp("Budget", "Anonymous"); +const ui = { + budget: { + remaining: document.getElementById("budget-remaining"), + spent: document.getElementById("budget-spent"), + }, + buttons: { + categories: { + add: document.querySelector(".manage-categories .btn--add"), + delete: document.querySelector(".manage-categories .btn--delete"), + rename: document.querySelector(".manage-categories .btn--rename"), + }, + register: document.querySelector(".register .btn--register"), + reset: document.querySelector(".footer .btn--reset"), + transactions: { + update: document.querySelector(".manage-transactions .btn--update"), + }, + }, + form: { + categories: { + add: document.getElementById("add-category"), + rename: document.getElementById("rename-category"), + select: document.getElementById("select-category"), + }, + register: { + budget: document.getElementById("register-budget"), + language: document.getElementById("register-locale"), + username: document.getElementById("register-username"), + }, + transactions: { + amount: document.getElementById("transaction-amount"), + category: document.getElementById("transaction-category"), + date: document.getElementById("transaction-date"), + id: document.getElementById("transaction-id"), + name: document.getElementById("transaction-name"), + type: document.getElementById("transaction-type"), + }, + }, + history: { + body: document.querySelector(".app__history .table__body"), + }, + title: document.querySelector(".branding__title"), +}; + +function initApp() { + const register = document.querySelector(".register"); + const application = document.querySelector(".app"); + const budget = ui.form.register.budget.value; + const locale = ui.form.register.language.value; + const username = ui.form.register.username.value; + + if (budget && locale && username) { + app.user.username = username; + app.user.budget = budget; + app.user.locale = locale; + register.style.display = "none"; + application.style.display = "block"; + } else { + notify("You must complete all fields!", "error", 3000); + } +} + +function notify(message, type, duration, position = "bottom") { + const notification = new Notification(message, type); + notification.duration = duration; + notification.position = position; + return notification.notify(); +} + +function getSelectOptions(select, options) { + select.innerHTML = ""; + options.forEach((option) => { + select.add(new Option(option.name, option.id)); + }); + + return select; +} + +function findName(id, array) { + const object = array.find((item) => item.id === Number(id)); + if (object) { + return object.name; + } else { + return "(deleted)"; + } +} + +function resetTransactionForm() { + ui.form.transactions.amount.value = ""; + ui.form.transactions.category.value = ""; + ui.form.transactions.date.value = ""; + ui.form.transactions.id.value = ""; + ui.form.transactions.name.value = ""; + ui.form.transactions.type.value = ""; +} + +function setTransactionForm(transaction) { + ui.form.transactions.amount.value = transaction.amount; + ui.form.transactions.category.value = transaction.category; + ui.form.transactions.date.valueAsDate = transaction.date; + ui.form.transactions.id.value = transaction.id; + ui.form.transactions.name.value = transaction.name; + ui.form.transactions.type.value = transaction.type; +} + +function manageHistory(target) { + const tr = target.parentElement.parentElement; + const transactionId = Number(tr.id.replace("transaction-", "")); + const array = tr.classList.contains("table__row--expense") + ? app.expenses + : app.incomes; + if (target.classList.contains("btn--delete")) { + const tbody = tr.parentElement; + app.remove(transactionId, array); + updateBudget(); + updateHistory(tbody); + } else if (target.classList.contains("btn--edit")) { + const index = array.findIndex( + (transaction) => transaction.id === transactionId + ); + setTransactionForm(array[index]); + } +} + +function getTransactionButton(type) { + const btn = document.createElement("button"); + let text = ""; + + switch (type) { + case "delete": + text = "Delete"; + break; + case "edit": + text = "Edit"; + break; + default: + break; + } + + btn.textContent = text; + btn.classList.add("btn", `btn--${type}`); + btn.addEventListener("click", (event) => manageHistory(event.target)); + + return btn; +} + +function getTransactionCell(data = "") { + const td = document.createElement("td"); + td.classList.add("table__item"); + td.textContent = data; + + return td; +} + +function getTransactionRow(transaction) { + const tr = document.createElement("tr"); + const amount = + transaction.type === "expense" + ? transaction.amount * -1 + : transaction.amount; + const localizedAmount = getCurrencyFormat(amount, app.user.locale); + const categoryName = findName(transaction.category, app.categories); + const date = transaction.date.toLocaleDateString(app.user.locale); + + const dateCell = getTransactionCell(date); + const nameCell = getTransactionCell(transaction.name); + const categoryCell = getTransactionCell(categoryName); + const typeCell = getTransactionCell(transaction.type); + const amountCell = getTransactionCell(localizedAmount); + const manageCell = getTransactionCell(); + const editButton = getTransactionButton("edit"); + const deleteButton = getTransactionButton("delete"); + + manageCell.append(editButton, deleteButton); + tr.classList.add("table__row", `table__row--${transaction.type}`); + tr.id = `transaction-${transaction.id}`; + tr.append(dateCell, nameCell, categoryCell, typeCell, amountCell, manageCell); + + return tr; +} + +function getHistory(tbody) { + const transactions = app.getOrderedTransactions("oldest"); + transactions.forEach((transaction) => { + tbody.appendChild(getTransactionRow(transaction)); + }); +} + +function updateBudget() { + app.updateUserBudget(); + ui.budget.remaining.textContent = getCurrencyFormat( + app.user.budget.remaining(), + app.user.locale + ); + ui.budget.spent.textContent = getCurrencyFormat( + app.user.budget.spent, + app.user.locale + ); +} + +function updateCategories() { + getSelectOptions(ui.form.categories.select, app.categories); + getSelectOptions(ui.form.transactions.category, app.categories); +} + +function updateHistory() { + ui.history.body.innerHTML = ""; + getHistory(ui.history.body, app); +} + +function updateAll() { + ui.title.textContent = `${app.user.username} ${app.title}`; + updateBudget(); + updateCategories(); + updateHistory(); +} + +function listen() { + for (const [name, element] of Object.entries(ui.buttons.categories)) { + element.addEventListener("click", (event) => { + event.preventDefault(); + const id = Number(ui.form.categories.select.value); + + switch (name) { + case "add": + if (ui.form.categories.add.value) { + app.addCategory(ui.form.categories.add.value); + ui.form.categories.add.value = ""; + } else { + notify("Category name must be filled!", "error", 3000); + } + break; + case "delete": + if (id) { + app.remove(id, app.categories); + updateHistory(); + } else { + notify("A category must be selected!", "error", 3000); + } + break; + case "rename": + const newName = ui.form.categories.rename.value; + if (newName && id) { + app.renameCategory(id, newName); + updateHistory(); + ui.form.categories.rename.value = ""; + } else { + notify( + "You need to select a category and enter a new name first!", + "error", + 3000 + ); + } + break; + default: + break; + } + + updateCategories(); + }); + } + + ui.buttons.transactions.update.addEventListener("click", (event) => { + event.preventDefault(); + const transactionId = Number(ui.form.transactions.id.value); + const transaction = { + date: ui.form.transactions.date.value, + name: ui.form.transactions.name.value, + type: ui.form.transactions.type.value, + category: ui.form.transactions.category.value, + amount: ui.form.transactions.amount.value, + }; + const error = []; + for (const value in transaction) { + const element = transaction[value]; + !element ? error.push(value) : ""; + } + if (error.length === 0) { + transactionId + ? app.editTransaction({ id: transactionId, ...transaction }) + : app.addTransaction(transaction); + updateBudget(); + updateHistory(); + resetTransactionForm(); + } else { + const errorMsg = `These fields (${error.join(", ")}) must be filled!`; + notify(errorMsg, "error", 3000); + } + }); + + ui.buttons.register.addEventListener("click", (event) => { + event.preventDefault(); + initApp(); + updateAll(); + }); + + ui.buttons.reset.addEventListener("click", (event) => { + event.preventDefault(); + if (confirm("Are you sure?")) { + notify("Reset!", "warning", 2000); + app.reset(); + } + updateAll(); + }); +} + +listen(); diff --git a/public/projects/js-small-apps/budget-app/index.html b/public/projects/js-small-apps/budget-app/index.html new file mode 100644 index 0000000..90f5d12 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/index.html @@ -0,0 +1,232 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Budget App</title> + <link rel="stylesheet" href="style.css" /> + </head> + <body> + <header class="branding"> + <h1 class="branding__title">Budget App</h1> + </header> + <main class="main"> + <div class="register"> + <form action="#" class="register__form form"> + <p class="form__item"> + <label for="register-username" class="form__label" + >Enter a username:</label + > + <input + type="text" + id="register-username" + name="register-username" + class="form__field" + required + /> + </p> + <p class="form__item"> + <label for="register-budget" class="form__label" + >Enter a budget:</label + > + <input + type="number" + id="register-budget" + name="register-budget" + class="form__field" + required + /> + </p> + <p class="form__item"> + <label for="register-locale" class="form__label" + >Select a language:</label + > + <select + name="register-locale" + id="register-locale" + class="form__select" + required + > + <option value="en-US">Anglais</option> + <option value="fr-FR">Français</option> + </select> + </p> + <button type="submit" class="btn btn--register">Register</button> + </form> + </div> + <div class="app"> + <div class="app__management"> + <div class="app__categories"> + <h2 class="app__title">Categories</h2> + <form action="#" class="manage-categories form"> + <fieldset class="form__fieldset"> + <legend class="form__legend">Add a new category</legend> + <p class="form__item"> + <label for="add-category" class="form__label" + >Category name:</label + > + <input + type="text" + name="" + id="add-category" + class="form__field" + required + /> + <button type="submit" class="form__submit btn btn--add"> + Add + </button> + </p> + </fieldset> + <fieldset class="form__fieldset"> + <legend class="form__legend">Manage existing categories</legend> + <p class="form__item"> + <label for="select-category" class="form__label" + >Select a category:</label + > + <select + name="select-category" + id="select-category" + class="form__select" + > + <option value="undefined" class="form__option"> + Undefined + </option> + </select> + <button type="submit" class="form__submit btn btn--delete"> + Delete + </button> + </p> + <p class="form__item"> + <label for="rename-category" class="form__label" + >New name:</label + > + <input + type="text" + name="rename-category" + id="rename-category" + class="form__field" + /> + <button type="submit" class="form__submit btn btn--rename"> + Rename + </button> + </p> + </fieldset> + </form> + </div> + <div class="app__transactions"> + <h2 class="app__title">Transactions</h2> + <form action="#" class="manage-transactions form"> + <fieldset class="form__fieldset"> + <legend class="form__legend">Manage transactions</legend> + <p class="form__item"> + <label for="transaction-date" class="form__label" + >Select a date:</label + > + <input + type="date" + name="transaction-date" + id="transaction-date" + class="form__field" + required + /> + </p> + <p class="form__item"> + <label for="" class="form__label">Choose a type:</label> + <select + name="transaction-type" + id="transaction-type" + class="form__select" + > + <option value=""></option> + <option value="expense">Expense</option> + <option value="income">Income</option> + </select> + </p> + <p class="form__item"> + <label for="" class="form__label">Choose a category:</label> + <select + name="transaction-category" + id="transaction-category" + class="form__select" + > + <option value="undefined">Undefined</option> + </select> + </p> + <p class="form__item"> + <label for="transaction-name" class="form__label" + >Name:</label + > + <input + type="text" + name="transaction-name" + id="transaction-name" + class="form__field" + required + /> + </p> + <p class="form__item"> + <label for="transaction-amount" class="form__label" + >Amount:</label + > + <input + type="number" + name="transaction-amount" + id="transaction-amount" + step="0.01" + class="form__field" + required + /> + </p> + <input + type="hidden" + name="transaction-id" + id="transaction-id" + /> + <button type="submit" class="form__submit btn btn--update"> + Update + </button> + </fieldset> + </form> + </div> + <div class="app__budget"> + <h2 class="app__title">Budget state</h2> + <table class="table"> + <thead class="table__header"> + <tr> + <td class="table__item">Spent</td> + <td class="table__item">Remaining</td> + </tr> + </thead> + <tbody class="table__body"> + <tr> + <td id="budget-spent" class="table__item">0</td> + <td id="budget-remaining" class="table__item">0</td> + </tr> + </tbody> + </table> + </div> + </div> + <div class="app__history"> + <h2 class="app__title">History</h2> + <table class="table"> + <thead class="table__header"> + <td class="table__item">Date</td> + <td class="table__item">Name</td> + <td class="table__item">Category</td> + <td class="table__item">Type</td> + <td class="table__item">Amount</td> + <td class="table__item">Manage</td> + </thead> + <tbody class="table__body"></tbody> + </table> + </div> + </div> + </main> + <footer class="footer container"> + <button type="button" class="btn btn--reset">Reset</button> + <p class="footer__copyright">Budget App. MIT 2021. Armand Philippot.</p> + </footer> + <script src="app.js" type="module"></script> + </body> +</html> diff --git a/public/projects/js-small-apps/budget-app/lib/class-budget-app.js b/public/projects/js-small-apps/budget-app/lib/class-budget-app.js new file mode 100644 index 0000000..45097a0 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/class-budget-app.js @@ -0,0 +1,144 @@ +import Category from "./class-category.js"; +import Transaction from "./class-transaction.js"; +import User from "./class-user.js"; + +class BudgetApp { + #title = "Budget App"; + #categoryId = 1; + #categories = []; + #transactionId = 1; + #incomes = []; + #expenses = []; + #userId = 1; + #user = {}; + + constructor(title, username) { + this.#title = title; + this.#user = new User(this.#userId++, username); + } + + set title(string) { + this.#title = string; + } + + get title() { + return this.#title; + } + + set categories(array) { + this.#categories = array; + } + + get categories() { + return this.#categories; + } + + set incomes(array) { + this.#incomes = array; + } + + get incomes() { + return this.#incomes; + } + + set expenses(array) { + this.#expenses = array; + } + + get expenses() { + return this.#expenses; + } + + set user(username) { + this.#user = new User(this.#userId++, username); + } + + get user() { + return this.#user; + } + + remove(id, from) { + const index = from.findIndex((object) => object.id === id); + from.splice(index, 1); + } + + addCategory(name) { + this.#categories.push(new Category(this.#categoryId++, name)); + } + + renameCategory(id, newName) { + const index = this.categories.findIndex((object) => object.id === id); + this.categories[index].name = newName; + } + + addTransaction(transaction) { + const array = + transaction.type === "income" ? this.#incomes : this.#expenses; + array.push( + new Transaction( + this.#transactionId++, + transaction.date, + transaction.name, + transaction.type, + transaction.category, + transaction.amount + ) + ); + } + + editTransaction(transaction) { + const array = transaction.type === "income" ? this.incomes : this.expenses; + const index = array.findIndex((object) => { + return object.id === Number(transaction.id); + }); + if (index !== -1) { + array[index] = new Transaction(...Object.values(transaction)); + } else { + const oldArray = array === this.incomes ? this.expenses : this.incomes; + array.push(new Transaction(...Object.values(transaction))); + this.remove(transaction.id, oldArray); + } + } + + getOrderedTransactions(order) { + const transactions = [...this.expenses, ...this.incomes]; + + switch (order) { + case "newest": + transactions.sort((a, b) => b.date - a.date); + break; + case "oldest": + transactions.sort((a, b) => a.date - b.date); + break; + default: + break; + } + + return transactions; + } + + total(transaction) { + const array = transaction === "expense" ? this.#expenses : this.#incomes; + let total = 0; + array.forEach((item) => { + total += item.amount; + }); + return total; + } + + updateUserBudget() { + this.user.budget.spent = this.total("expense"); + this.user.budget.profit = this.total("income"); + } + + reset() { + this.#categoryId = 1; + this.#transactionId = 1; + this.#categories = []; + this.#incomes = []; + this.#expenses = []; + this.updateUserBudget(); + } +} + +export default BudgetApp; diff --git a/public/projects/js-small-apps/budget-app/lib/class-budget.js b/public/projects/js-small-apps/budget-app/lib/class-budget.js new file mode 100644 index 0000000..cde22fd --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/class-budget.js @@ -0,0 +1,44 @@ +/** + * Budget class + * + * Create a new budget. + */ +class Budget { + #initial = 0; + #spent = 0; + #profit = 0; + + constructor(initial) { + this.#initial = Number.parseFloat(initial); + } + + set initial(number) { + this.#initial = Number.parseFloat(number); + } + + get initial() { + return this.#initial; + } + + set spent(number) { + this.#spent = Number.parseFloat(number); + } + + get spent() { + return this.#spent; + } + + set profit(number) { + this.#profit = Number.parseFloat(number); + } + + get profit() { + return this.#profit; + } + + remaining() { + return this.initial + this.profit - this.spent; + } +} + +export default Budget; diff --git a/public/projects/js-small-apps/budget-app/lib/class-category.js b/public/projects/js-small-apps/budget-app/lib/class-category.js new file mode 100644 index 0000000..7b2f1b2 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/class-category.js @@ -0,0 +1,37 @@ +/** + * Category class. + * + * Create a new category with id, name and attachments. + */ +class Category { + #id = 0; + #name = ""; + #attachments = []; + + constructor(id, name) { + this.#id = Number(id); + this.#name = name; + } + + set name(name) { + this.#name = name; + } + + get name() { + return this.#name; + } + + get id() { + return this.#id; + } + + set attachments(attachment) { + this.#attachments.push(Number(attachment)); + } + + get attachments() { + return this.#attachments; + } +} + +export default Category; diff --git a/public/projects/js-small-apps/budget-app/lib/class-notification.js b/public/projects/js-small-apps/budget-app/lib/class-notification.js new file mode 100644 index 0000000..17a32a0 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/class-notification.js @@ -0,0 +1,89 @@ +class Notification { + #id = 0; + #title = ""; + #message = ""; + #type = ""; + #duration = 0; + #position = ""; + + constructor(message, type) { + this.#message = message; + this.#type = type; + } + + get id() { + return this.#id; + } + + set title(string) { + this.#title = string; + } + + get title() { + return this.#title; + } + + set message(text) { + this.#message = text; + } + + get message() { + return this.#message; + } + + set type(string) { + this.#type = string; + } + + get type() { + return this.#type; + } + + set duration(number) { + this.#duration = number; + } + + get duration() { + return this.#duration; + } + + set position(string) { + this.#position = string; + } + + get position() { + return this.#position; + } + + #getWrapper() { + let wrapper = document.getElementById("notifications-center"); + + if (!wrapper) { + wrapper = document.createElement("div"); + wrapper.id = "notifications-center"; + wrapper.classList = "notifications"; + document.body.appendChild(wrapper); + } + + return wrapper; + } + + notify() { + const notification = document.createElement("div"); + notification.textContent = this.message; + notification.classList.add("notification", `notification--${this.type}`); + + const wrapper = this.#getWrapper(); + document.body.style.position = "relative"; + wrapper.style.cssText = `position: fixed;${this.position}: 1rem;right: 1rem;display: flex;flex-flow: column wrap;gap: 1rem;`; + wrapper.appendChild(notification); + + if (this.duration && Number(this.duration) !== 0) { + setTimeout(() => { + notification.remove(); + }, this.duration); + } + } +} + +export default Notification; diff --git a/public/projects/js-small-apps/budget-app/lib/class-transaction.js b/public/projects/js-small-apps/budget-app/lib/class-transaction.js new file mode 100644 index 0000000..6807372 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/class-transaction.js @@ -0,0 +1,76 @@ +/** + * Transaction class + * + * Create a new transaction with id, date, name, type, category and amount. + */ +class Transaction { + #id = 0; + #date = new Date(); + #name = ""; + #type = ""; + #category = 0; + #amount = 0; + + constructor(id, date, name, type, category, amount) { + this.#id = Number(id); + this.#date = new Date(date); + this.#name = name; + this.#type = type; + this.#category = Number(category); + this.#amount = Number.parseFloat(amount); + } + + get id() { + return this.#id; + } + + set date(datetime) { + this.#date = datetime; + } + + get date() { + return this.#date; + } + + set name(string) { + this.#name = string; + } + + get name() { + return this.#name; + } + + set type(string) { + this.#type = string; + } + + get type() { + return this.#type; + } + + set category(string) { + this.#category = string; + } + + get category() { + return this.#category; + } + + set amount(number) { + this.#amount = number; + } + + get amount() { + return this.#amount; + } + + update(date, name, type, category, amount) { + this.date = date; + this.name = name; + this.type = type; + this.category = category; + this.amount = amount; + } +} + +export default Transaction; diff --git a/public/projects/js-small-apps/budget-app/lib/class-user.js b/public/projects/js-small-apps/budget-app/lib/class-user.js new file mode 100644 index 0000000..e0e137b --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/class-user.js @@ -0,0 +1,79 @@ +import Budget from "./class-budget.js"; + +class User { + #id = 0; + #username = "Anonymous"; + #firstName = "John"; + #lastName = "Doe"; + #role = "admin"; + #locale = "en-US"; + #accountCreation = new Date(); + #budget = 0; + + constructor(id, username) { + this.#id = id; + this.#username = username; + } + + get id() { + return this.#id; + } + + set username(name) { + this.#username = name; + } + + get username() { + return this.#username; + } + + set firstName(name) { + this.#firstName = name; + } + + get firstName() { + return this.#firstName; + } + + set lastName(name) { + this.#lastName = name; + } + + get lastName() { + return this.#lastName; + } + + set role(string) { + this.#role = string; + } + + get role() { + return this.#role; + } + + set locale(code) { + this.#locale = code; + } + + get locale() { + return this.#locale; + } + + get accountCreation() { + return this.#accountCreation; + } + + set budget(number) { + this.#budget = new Budget(number); + } + + get budget() { + return this.#budget; + } + + name() { + return `${this.#firstName} ${this.#lastName}`; + } +} + +export default User; diff --git a/public/projects/js-small-apps/budget-app/lib/utils/currency.js b/public/projects/js-small-apps/budget-app/lib/utils/currency.js new file mode 100644 index 0000000..04d3ad0 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/lib/utils/currency.js @@ -0,0 +1,47 @@ +/** + * Convert a number to fr_FR locale. + * @param {Number} number A number to format. + * @returns A number formatted with fr_FR locale. + */ +const getCurrencyFR = (number) => { + const formatted = + Number.parseFloat(number) + .toFixed(2) + .replace(".", ",") + .replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1 ") + " €"; + return formatted; +}; + +/** + * Convert a number to en_US locale. + * @param {Number} number A number to format. + * @returns A number formatted with en_US locale. + */ +const getCurrencyUS = (number) => { + const formatted = + "$" + + Number.parseFloat(number) + .toFixed(2) + .replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,"); + return formatted; +}; + +/** + * Get a number formatted based on a locale. + * @param {Number} number A number to format. + * @param {String} format A language code. + * @returns A formatted number. + */ +const getCurrencyFormat = (number, format) => { + switch (format) { + case "fr-FR": + return getCurrencyFR(number); + case "en-US": + return getCurrencyUS(number); + default: + console.log("Not supported!"); + break; + } +}; + +export default getCurrencyFormat; diff --git a/public/projects/js-small-apps/budget-app/style.css b/public/projects/js-small-apps/budget-app/style.css new file mode 100644 index 0000000..009fbb7 --- /dev/null +++ b/public/projects/js-small-apps/budget-app/style.css @@ -0,0 +1,366 @@ +/* + * Base + */ + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: hsl(207, 52%, 95%); + color: #000; + font-family: Arial, Helvetica, sans-serif; + font-size: 1rem; + line-height: 1.618; + display: flex; + flex-flow: column nowrap; + min-height: 100vh; + max-width: 100vw; +} + +button { + display: block; + font-family: inherit; + font-size: 100%; + line-height: 1.15; + cursor: pointer; +} + +select { + cursor: pointer; +} + +table { + width: 100%; +} + +/* + * Layout + */ + +.branding { + background: hsl(207, 35%, 90%); + border-bottom: 1px solid hsl(207, 35%, 80%); + box-shadow: 0 2px 0 1px hsl(207, 25%, 70%); + padding: 2rem 0; + margin-bottom: clamp(2rem, 3vw, 4rem); +} + +.branding__title { + color: hsl(207, 85%, 27%); + font-size: clamp(1.9rem, 5vw, 2.2rem); + text-align: center; + text-shadow: 0 0 1px hsl(207, 100%, 50%); + text-transform: uppercase; +} + +.footer { + background: hsl(207, 35%, 90%); + border-top: 1px solid hsl(207, 35%, 80%); + margin-top: clamp(3rem, 5vw, 4rem); + padding: 2rem 0 1.5rem; +} + +.footer .btn--reset { + margin-bottom: 2rem; +} + +.footer__copyright { + font-size: 0.9rem; + text-align: center; +} + +.main { + flex: 1; + padding: 0 clamp(1rem, 3vw, 2rem); +} + +.register { + display: flex; + flex-flow: column nowrap; + justify-content: center; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background: hsl(202, 19%, 89%); +} + +.register__form { + margin: auto; +} + +.app { + display: none; +} + +.app__title { + background: hsl(207, 25%, 92%); + border: 1px solid hsl(207, 25%, 70%); + box-shadow: 0 2px 0 1px hsl(207, 25%, 70%); + grid-column: 1 / -1; + color: hsl(207, 85%, 27%); + font-size: clamp(1.4rem, 4vw, 1.7rem); + margin-bottom: 1rem; + padding: 0.5rem 1rem; +} + +.app__management { + display: grid; + grid-template-columns: repeat(auto-fill, min(calc(100vw - 2 * 1rem), 21rem)); + justify-content: center; + column-gap: 2rem; +} + +.app__budget { + margin-bottom: clamp(1rem, 3vw, 2rem); +} + +.form__fieldset { + border: 2px solid hsl(207, 85%, 27%); + margin-bottom: clamp(1rem, 3vw, 2rem); + padding: 0 1rem 1rem; +} + +.form__legend { + background: hsl(207, 85%, 37%); + border: 2px solid hsl(207, 85%, 27%); + color: #fff; + font-weight: 600; + width: 100%; + margin-bottom: 1rem; + padding: 0.5rem 1rem; +} + +.form__item:not(:last-child) { + margin-bottom: 1rem; +} + +.form__label { + display: block; + cursor: pointer; + font-size: 0.8rem; + font-weight: 600; + letter-spacing: 1px; + text-transform: uppercase; + margin-bottom: 0.1rem; +} + +.form__field, +.form__select { + border: 1px solid hsl(207, 85%, 27%); + box-shadow: 2px 2px 0 0 hsl(0, 0%, 20%); + font-size: inherit; + padding: 0.5rem clamp(0.5rem, 3vw, 0.8rem); + width: 100%; + transition: all 0.3s ease-in-out 0s; +} + +.form__field:hover, +.form__field:focus, +.form__select:hover, +.form__select:focus { + transform: scaleX(1.05); +} + +.form__field:focus, +.form__select:focus { + outline: none; + box-shadow: 2px 2px 0 0 hsl(0, 0%, 20%), 1px 1px 0 4px hsl(207, 85%, 60%); +} + +.manage-categories .form__item { + display: grid; + grid-template-columns: minmax(0, 1fr) max-content; + column-gap: 1rem; +} + +.manage-categories .form__label { + grid-column: 1 / -1; +} + +.manage-categories .form__select, +.manage-categories .form__field { + grid-column: 1; +} + +.manage-categories .form__submit { + grid-column: 2; +} + +.table { + border: 2px solid hsl(207, 85%, 27%); + border-collapse: collapse; + width: 100%; + overflow-x: auto; +} + +.table__header { + background: hsl(207, 85%, 37%); + color: #fff; + font-weight: 600; +} + +.table__item { + border: 1px solid hsl(207, 85%, 27%); + padding: 0.5rem 0.7rem; + width: max-content; +} + +.table__item .btn { + display: inline-flex; + margin: 0.5rem; +} + +.app__budget .table { + table-layout: fixed; +} + +.app__budget .table__item { + font-weight: 600; + text-align: center; +} + +.app__budget .table__body { + font-size: 1.3rem; +} + +.app__history { + max-width: 100%; + overflow-x: auto; +} + +.app__history .app__title { + position: sticky; + left: 0; +} + +/* + * Components + */ + +.btn { + border-radius: 5px; + font-weight: 600; + margin: auto; + padding: 0.6rem 0.8rem; + transition: all 0.35s ease-in-out 0s; +} + +.btn:hover, +.btn:focus { + transform: scaleX(1.2) scaleY(1.1); +} + +.btn:focus { + outline: none; +} + +.btn:active { + transform: scale(0.95); +} + +.btn--register { + background: hsl(202, 67%, 84%); + border: 2px solid hsl(202, 67%, 34%); + box-shadow: 2px 2px 0 0 hsl(202, 67%, 24%); +} + +.btn--add, +.btn--update { + background: hsl(120, 33%, 84%); + border: 2px solid hsl(120, 33%, 34%); + box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%); +} + +.btn--add:focus, +.btn--update:focus { + box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%), 1px 1px 0 4px hsl(120, 33%, 81%); +} + +.btn--add:active, +.btn--update:active { + box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%); +} + +.btn--delete, +.btn--reset { + background: hsl(0, 33%, 84%); + border: 2px solid hsl(0, 33%, 34%); + box-shadow: 2px 2px 0 0 hsl(0, 33%, 24%); +} + +.btn--delete:focus, +.btn--reset:focus { + box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%), 1px 1px 0 4px hsl(0, 33%, 81%); +} + +.btn--delete:active, +.btn--reset:active { + box-shadow: 2px 2px 0 0 hsl(0, 33%, 24%); +} + +.btn--rename, +.btn--edit { + background: hsl(193, 33%, 84%); + border: 2px solid hsl(193, 33%, 34%); + box-shadow: 2px 2px 0 0 hsl(193, 33%, 24%); +} + +.btn--rename:focus, +.btn--edit:focus { + box-shadow: 2px 2px 0 0 hsl(120, 33%, 24%), 1px 1px 0 4px hsl(193, 33%, 81%); +} + +.btn--delete:active, +.btn--edit:active { + box-shadow: 2px 2px 0 0 hsl(193, 33%, 24%); +} + +.notification { + background: #fff; + border: 3px solid hsl(207, 85%, 27%); + border-radius: 3px; + box-shadow: 1px 1px 3px 0 hsla(0, 0%, 0%, 0.7); + font-weight: 600; + padding: 1rem; +} + +.notification--error { + color: hsl(0, 49%, 39%); + border-color: hsl(0, 49%, 39%); +} + +.notification--info { + color: hsl(212, 76%, 38%); + border-color: hsl(212, 76%, 38%); +} + +.notification--warning { + color: hsl(32, 76%, 38%); + border-color: hsl(32, 76%, 38%); +} + +/* + * Media Queries + */ +@media screen and (min-width: 1200px) { + .main { + margin: auto; + max-width: 1200px; + } + .app__management { + grid-template-columns: repeat(3, min(calc(100vw - 2 * 1rem), 21rem)); + } + + .app__budget { + position: sticky; + top: 1rem; + height: max-content; + } +} |
