Homework: Online/Offline Budget Trackers
Add functionality to our existing Budget Tracker application to allow for offline access and functionality.
The user will be able to add expenses and deposits to their budget with or without a connection. When entering transactions offline, they should populate the total when brought back online.
Offline Functionality:
Enter deposits offline
Enter expenses offline
When brought back online:
- Offline entries should be added to tracker.
User Story
AS AN avid traveller I WANT to be able to track my withdrawals and deposits with or without a data/internet connection SO THAT my account balance is accurate when I am traveling
Business Context
Giving users a fast and easy way to track their money is important, but allowing them to access that information anytime is even more important. Having offline functionality is paramount to our applications success.
Acceptance Criteria
GIVEN a user is on Budget App without an internet connection WHEN the user inputs a withdrawal or deposit THEN that will be shown on the page, and added to their transaction history when their connection is back online.
Develop/models/transaction.js const mongoose = require("mongoose"); const Schema = mongoose.Schema; const transactionSchema = new Schema( { name: { type: String, trim: true, required: "Enter a name for transaction" }, value: { type: Number, required: "Enter an amount" }, date: { type: Date, default: Date.now } } ); const Transaction = mongoose.model("Transaction", transactionSchema); module.exports = Transaction; Develop/package.json { "name": "budget-app", "version": "1.0.0", "description": "", "main": "server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js", "lite-server": "lite-server" }, "repository": { "type": "git", "url": "git+https://github.com/coding-boot-camp/unit18hw.git" }, "author": "", "license": "ISC", "bugs": { "url": "https://github.com/coding-boot-camp/unit18hw/issues" }, "homepage": "https://github.com/coding-boot-camp/unit18hw#readme", "dependencies": { "compression": "^1.7.4", "express": "^4.17.1", "lite-server": "^2.5.3", "mongoose": "^5.5.15", "morgan": "^1.9.1" } } Develop/public/icons/icon-192x192.png Develop/public/icons/icon-512x512.png Develop/public/index.html Your total is: $0 Add Funds Subtract Funds TransactionAmount Develop/public/index.js let transactions = []; let myChart; fetch("/api/transaction") .then(response => { return response.json(); }) .then(data => { // save db data on global variable transactions = data; populateTotal(); populateTable(); populateChart(); }); function populateTotal() { // reduce transaction amounts to a single total value let total = transactions.reduce((total, t) => { return total + parseInt(t.value); }, 0); let totalEl = document.querySelector("#total"); totalEl.textContent = total; } function populateTable() { let tbody = document.querySelector("#tbody"); tbody.innerHTML = ""; transactions.forEach(transaction => { // create and populate a table row let tr = document.createElement("tr"); tr.innerHTML = ` ${transaction.name} ${transaction.value} `; tbody.appendChild(tr); }); } function populateChart() { // copy array and reverse it let reversed = transactions.slice().reverse(); let sum = 0; // create date labels for chart let labels = reversed.map(t => { let date = new Date(t.date); return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; }); // create incremental values for chart let data = reversed.map(t => { sum += parseInt(t.value); return sum; }); // remove old chart if it exists if (myChart) { myChart.destroy(); } let ctx = document.getElementById("myChart").getContext("2d"); myChart = new Chart(ctx, { type: 'line', data: { labels, datasets: [{ label: "Total Over Time", fill: true, backgroundColor: "#6666ff", data }] } }); } function sendTransaction(isAdding) { let nameEl = document.querySelector("#t-name"); let amountEl = document.querySelector("#t-amount"); let errorEl = document.querySelector(".form .error"); // validate form if (nameEl.value === "" || amountEl.value === "") { errorEl.textContent = "Missing Information"; return; } else { errorEl.textContent = ""; } // create record let transaction = { name: nameEl.value, value: amountEl.value, date: new Date().toISOString() }; // if subtracting funds, convert amount to negative number if (!isAdding) { transaction.value *= -1; } // add to beginning of current array of data transactions.unshift(transaction); // re-run logic to populate ui with new record populateChart(); populateTable(); populateTotal(); // also send to server fetch("/api/transaction", { method: "POST", body: JSON.stringify(transaction), headers: { Accept: "application/json, text/plain, */*", "Content-Type": "application/json" } }) .then(response => { return response.json(); }) .then(data => { if (data.errors) { errorEl.textContent = "Missing Information"; } else { // clear form nameEl.value = ""; amountEl.value = ""; } }) .catch(err => { // fetch failed, so save in indexed db saveRecord(transaction); // clear form nameEl.value = ""; amountEl.value = ""; }); } document.querySelector("#add-btn").onclick = function() { sendTransaction(true); }; document.querySelector("#sub-btn").onclick = function() { sendTransaction(false); }; Develop/public/styles.css body { font-size: 120%; font-family: Arial; } button, input { font-size: 100%; font-family: Arial; } table { width: 100%; } td, th { border: 1px solid #dddddd; text-align: left; padding: 8px; } tr:nth-child(even) { background-color: #dddddd; } .error { color: red; } .wrapper { margin: 0 auto; max-width: 825px; text-align: center; } .total { font-size: 150%; text-decoration: underline; } .form { margin-top: 25px; width: 100%; } .transactions { text-align: left; max-height: 300px; overflow: auto; width: 90%; margin: 0 auto; } Develop/routes/api.js const router = require("express").Router(); const Transaction = require("../models/transaction.js"); router.post("/api/transaction", ({body}, res) => { Transaction.create(body) .then(dbTransaction => { res.json(dbTransaction); }) .catch(err => { res.status(404).json(err); }); }); router.post("/api/transaction/bulk", ({body}, res) => { Transaction.insertMany(body) .then(dbTransaction => { res.json(dbTransaction); }) .catch(err => { res.status(404).json(err); }); }); router.get("/api/transaction", (req, res) => { Transaction.find({}).sort({date: -1}) .then(dbTransaction => { res.json(dbTransaction); }) .catch(err => { res.status(404).json(err); }); }); module.exports = router; Develop/server.js const express = require("express"); const logger = require("morgan"); const mongoose = require("mongoose"); const compression = require("compression"); const PORT = 3000; const app = express(); app.use(logger("dev")); app.use(compression()); app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use(express.static("public")); mongoose.connect("mongodb://localhost/budget", { useNewUrlParser: true, useFindAndModify: false }); // routes app.use(require("./routes/api.js")); app.listen(PORT, () => { console.log(`App running on port ${PORT}!`); });