From 3480e68322cbb3384c4584dad5f2276433910dd5 Mon Sep 17 00:00:00 2001 From: "Glitch (a2-javieira)" Date: Tue, 12 Sep 2023 09:50:41 +0000 Subject: [PATCH] A2 Shopping List --- .glitch-assets | 0 README.md | 118 +++++------------------ package.json | 8 +- public/css/main.css | 195 ++++++++++++++++++++++++++++++++++++- public/index.html | 137 +++++++++++++++++++++++--- public/js/main.js | 230 +++++++++++++++++++++++++++++++++++++++----- server.improved.js | 153 ++++++++++++++++------------- shrinkwrap.yaml | 15 +++ 8 files changed, 657 insertions(+), 199 deletions(-) create mode 100644 .glitch-assets create mode 100644 shrinkwrap.yaml diff --git a/.glitch-assets b/.glitch-assets new file mode 100644 index 00000000..e69de29b diff --git a/README.md b/README.md index 493995c1..63192ab5 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,32 @@ -Assignment 2 - Short Stack: Basic Two-tier Web Application using HTML/CSS/JS and Node.js -=== +## Shopping List -Due: September 11th, by 11:59 AM. +Website link: https://a2-javieira.glitch.me/ -This assignment aims to introduce you to creating a prototype two-tiered web application. -Your application will include the use of HTML, CSS, JavaScript, and Node.js functionality, with active communication between the client and the server over the life of a user session. +This Shopping List Application was designed to help users keep track of the items they would like to purchase and how much the total cost is compared to their budget. The user begins by filling out a budget if they would like. This budget is updated to the right of where they enter this figure as “Total Budget.” Alongside this label are the “Total Cost” and “Remaining Budget” labels which display how much all the items cost and the difference between the budget and total cost. If the user does not enter a budget, the budget and remaining budget labels will remain at $0.00, which is also the case if a NaN is entered. Below this section is where they can add in items. The sections that are required are the item, quantity, price, and product type. Each of these fields are marked with an aria label, the * symbol, so that the user knows they are required fields. In addition, there is CSS to outline the required fields in a red glow when they are not properly filled in. The user may also choose to write down some notes if they want to keep track of their purchase for each item, however this field is not required. If the user does not fill out a required field, a banner will appear at the top stating which field still needs to be filled out. Once the item is successfully entered into the server, the table is automatically updated with the new item. The derived field in this project is the cost for a quantity of items. This value is stored for each item and summed to find the total cost. This is found and added to the server data by multiplying the quantity and the price per item that the user enters. If the user accidentally reloads the page, the table will also repopulate so they are always aware of which items they have stored. The table displays all the information the user provided and in each row, there is a remove button which will take out the unwanted item. Further, when the item is submitted, the total cost and remaining budget are adjusted accordingly at the top of the screen. To format this project using CSS, multiple flex boxes were used. A flex box was used for the table so that the width could be set to 100% without running off the page as there is also padding. Another flexbox contains the budget information at the top of the screen. Inside this flexbox, a secondary flexbox sits so that the labels and input area could be formatted side by side while the labels, button, and input area themselves remained in columns. A similar technique was used in the add item section since the quantity and price fields were situated in columns for their respective labels and input boxes and then side by side. Then the entire add item section was encapsulated into another flexbox for further styling.The margins, padding, fonts, and colors were also manipulated using the classes defined by the flexboxes. In addition, individual elements, such as the buttons, and sections, such as the table rows and columns, were called separate from the flexboxes for CSS styling. I would also like to make a note about the validation baseline requirement. I went through and fixed all the errors that were given, but I was still given a couple warnings about unnecessary / in the HTML. This was a recurring problem as using the “prettier” function in Glitch adds in these extra slashes. In addition, there was another warning about the aria label which marked the label as a required field with the asterisk *. This is something I pulled directly from the forms link that was included in this assignment. I also collected all my fonts from Google fonts. These fonts made up the button, label, table, and title fonts. -Baseline Requirements ---- - -There is a large range of application areas and possibilities that meet these baseline requirements. -Try to make your application do something useful! A todo list, storing / retrieving high scores for a very simple game... have a little fun with it. - -Your application is required to implement the following functionalities: - -- a `Server` which not only serves files, but also maintains a tabular dataset with 3 or more fields related to your application -- a `Results` functionality which shows the entire dataset residing in the server's memory -- a `Form/Entry` functionality which allows a user to add or delete data items residing in the server's memory -- a `Server Logic` which, upon receiving new or modified "incoming" data, includes and uses a function that adds at least one additional derived field to this incoming data before integrating it with the existing dataset -- the `Derived field` for a new row of data must be computed based on fields already existing in the row. -For example, a `todo` dataset with `task`, `priority`, and `creation_date` may generate a new field `deadline` by looking at `creation_date` and `priority` - -Your application is required to demonstrate the use of the following concepts: - -HTML: -- One or more [HTML Forms](https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms), with any combination of form tags appropriate for the user input portion of the application -- A results page displaying all data currently available on the server. You will most likely use a `` tag for this, but `
+ + + + + + + + + +
ItemQuantityPriceCostProduct TypeProduct DetailsRemove Item
+ diff --git a/public/js/main.js b/public/js/main.js index a569258f..1dfbd3c2 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -1,27 +1,213 @@ -// FRONT-END (CLIENT) JAVASCRIPT HERE - -const submit = async function( event ) { - // stop form submission from trying to load - // a new .html page for displaying results... - // this was the original browser behavior and still - // remains to this day - event.preventDefault() - - const input = document.querySelector( '#yourname' ), - json = { yourname: input.value }, - body = JSON.stringify( json ) +/* +Constants used to keep track of user's total grocery budget and +what they are spending +defaultValue: fixed so prices formatted with two decimal places +totalCost: total cost of groceries +budget: user input budget +remaining: difference between budget and totalCost +*/ +let defaultValue = 0; +defaultValue = defaultValue.toFixed(2); +let totalCost = defaultValue; +let budget = defaultValue; +let remaining = defaultValue; + +/* +This is a function to handle the submit action in the addItem button +Variables item, quantity, price, productType, and productDetails are +collected from user input +If any of the input values is missing, a validation error will pop +up at the top of the screen stating which field is missing +Json object created using this data and showResults function is called +*/ +const submit = async function (event) { + event.preventDefault(); + const item = document.querySelector("#item"); + const quantity = document.querySelector("#quantity"); + const price = document.querySelector("#price"); + const productType = document.querySelector("#productType"); + const productDetails = document.querySelector("#productDetails"); + + //Validation error messages + if (item.value === "") { + window.alert("Please fill out item"); + return; + } else if (quantity.value === "") { + window.alert("Please fill out quantity"); + return; + } else if (price.value === "") { + window.alert("Please fill out price"); + return; + } else if (productType.value === "") { + window.alert("Please fill out product type"); + return; + } - const response = await fetch( '/submit', { - method:'POST', - body + //Create json object with user input + const json = { + item: item.value, + quantity: quantity.value, + price: price.value, + productType: productType.value, + productDetails: productDetails.value, + }; + const body = JSON.stringify(json); + const response = await fetch("/submit", { + method: "POST", + body, + }); + let data = await response.json(); + + //Display table of data + showResults(data, false); +}; + +/* +This is a function to handle the remove action in the removeItem button +When an item is removed, the table is automatically updated along +with the total cost label and remaining budget label +When all items are removed, the table should just show the header +*/ +const remove = async function (item) { + let json = { removeItem: item }; + let body = JSON.stringify(json); + fetch("/remove", { + method: "DELETE", + body, }) + .then((response) => response.json()) + .then((json) => { + showResults(json, true); + }); + return false; +}; + +/* +Constantly updates the table each time an item is added or removed +If the page is reloaded, all items in the server will still display +Each time an item is added, a new row in the table will appear +When an item is added, the remaining budget and total cost are +adjusted accordingly +*/ +const showResults = function (data, pageReloaded) { + let cost = 0.0; // initial cost for new item + let resultsTable = document.querySelector("#resultsTable"); // table + // when page reloaded or item is removed, all items in server + // must be added back to table + if (pageReloaded) { + // add header in table + resultsTable.innerHTML = + "ItemQuantity PriceCostProduct TypeProduct DetailsRemove Item"; + // for each item in server, add back data + data.forEach((grocery) => { + formatTable(grocery, resultsTable); + }); + } + + // if a new item is added + else { + // get the last added element in json Object + const update = data.at(-1); + // update table + formatTable(update, resultsTable); + } + + totalCost = defaultValue; // reset cost + // for each item in server, add its cost to totalCost + data.forEach((item) => { + cost = parseFloat(item.cost); + totalCost = (parseFloat(totalCost) + parseFloat(cost)).toFixed(2); + }); + + // update budgets and cost labels + updateBudgetCostLabels(); +}; - const text = await response.text() +/* +Updates table +Adds row each time an item is added +Gets the correct attribute value from the json Object element +*/ +const formatTable = function (grocery, resultsTable) { + console.log(resultsTable); + // get item price and format it to have 2 decimals + let price = parseFloat(grocery.price).toFixed(2); + // format strings to have dollar sign + let priceText = toPriceString(price); + let costText = toPriceString(grocery.cost); + // create new row in table + const row = resultsTable.insertRow(); + const cellItem = row.insertCell(); // create new cell + cellItem.innerHTML = grocery.item; // add data to cell + const cellQuantity = row.insertCell(); // create new cell + cellQuantity.innerHTML = grocery.quantity; // add data to cell + const cellPrice = row.insertCell(); // create new cell + cellPrice.innerHTML = priceText; // add data to cell + const cellCost = row.insertCell(); // create new cell + cellCost.innerHTML = costText; // add data to cell + const cellProductType = row.insertCell(); // create new cell + cellProductType.innerHTML = grocery.productType; // add data to cell + const cellProductDetails = row.insertCell(); // create new cell + cellProductDetails.innerHTML = grocery.productDetails; // add data to cell + // in last column, create a button to remove items + row.innerHTML += ``; +}; - console.log( 'text:', text ) -} +/* +Formats strings so they have a dollar sign, negative sign if +applicable, and two decimal places +*/ +const toPriceString = function (price) { + if (price >= 0) { + return "$" + `${price.toString()}`; + } else { + return "-$" + `${(price * -1).toString()}`; + } +}; -window.onload = function() { - const button = document.querySelector("button"); - button.onclick = submit; -} \ No newline at end of file +/* +Allows user to create or modify budget for entire shopping trip +*/ +const addBudget = function () { + // get budget input by user + let budgetInput = document.querySelector("#budgetInput").value; + // format budget so it is a float with 2 decimal places + budget = parseFloat(budgetInput).toFixed(2); + // if budget is not a number, set to 0; otherwise set to budget + budget = isNaN(budget) ? defaultValue : budget; + // update labels + updateBudgetCostLabels(); +}; + +/* +Update the labels for the total budget, total cost, and remaining +budget for the grocery trip +*/ +const updateBudgetCostLabels = function () { + // get labels + let totalBudgetLabel = document.querySelector("#totalBudget"); + let totalCostLabel = document.querySelector("#totalCost"); + let remainingBudgetLabel = document.querySelector("#remainingBudget"); + // if budget is not 0, update the remaining budget + if (budget != 0) { + remaining = isNaN(budget) || isNaN(totalCost) ? defaultValue : budget - totalCost; + remaining = remaining.toFixed(2); + } + // update labels + totalBudgetLabel.innerHTML = "Total Budget: " + toPriceString(budget); + totalCostLabel.innerHTML = "Total Cost: " + toPriceString(totalCost); + remainingBudgetLabel.innerHTML = "Remaining Budget: " + toPriceString(remaining); +}; + +window.onload = function () { + const addItem = document.querySelector("#addItem"); + const removeItem = document.querySelector("#removeItem"); + addItem.onclick = submit; + fetch("/data", { + method: "GET", + }) + .then((response) => response.json()) + .then((json) => { + showResults(json, true); + }); +}; diff --git a/server.improved.js b/server.improved.js index 9ac27fb8..3d4533c3 100644 --- a/server.improved.js +++ b/server.improved.js @@ -1,74 +1,95 @@ -const http = require( 'http' ), - fs = require( 'fs' ), - // IMPORTANT: you must run `npm install` in the directory for this assignment - // to install the mime library if you're testing this on your local machine. - // However, Glitch will install it automatically by looking in your package.json - // file. - mime = require( 'mime' ), - dir = 'public/', - port = 3000 - -const appdata = [ - { 'model': 'toyota', 'year': 1999, 'mpg': 23 }, - { 'model': 'honda', 'year': 2004, 'mpg': 30 }, - { 'model': 'ford', 'year': 1987, 'mpg': 14} -] - -const server = http.createServer( function( request,response ) { - if( request.method === 'GET' ) { - handleGet( request, response ) - }else if( request.method === 'POST' ){ - handlePost( request, response ) +const http = require("http"), + fs = require("fs"), + mime = require("mime"), + dir = "public/", + port = 3000; + +let groceryList = []; + +const server = http.createServer(function (request, response) { + if (request.method === "GET") { + handleGet(request, response); + } else if (request.method === "POST") { + handlePost(request, response); } -}) - -const handleGet = function( request, response ) { - const filename = dir + request.url.slice( 1 ) - - if( request.url === '/' ) { - sendFile( response, 'public/index.html' ) - }else{ - sendFile( response, filename ) + else if (request.method === 'DELETE') { + handleDelete(request, response) } -} - -const handlePost = function( request, response ) { +}); +const handleGet = function (request, response) { + const filename = dir + request.url.slice(1); + if (request.url === "/") { + sendFile(response, "public/index.html"); + } else if (request.url === "/data") { + console.log(JSON.stringify(groceryList)); + response.writeHeader(200, { "Content-type": "text/json" }); + response.end(JSON.stringify(groceryList)); + } else { + sendFile(response, filename); + } +}; +const handlePost = function (request, response) { + console.log("request URL" + request.url); + let dataString = ""; + request.on("data", function (data) { + dataString += data; + }); + request.on("end", function () { + let item = JSON.parse(dataString); + let cost = getCost(item.price, item.quantity); + groceryList.push({ + item: item.item, + quantity: item.quantity, + price: item.price, + cost: cost, + productType: item.productType, + productDetails: item.productDetails, + }); + response.writeHead(200, "OK", { "Content-Type": "text/json" }); + response.end(JSON.stringify(groceryList)); + }); +}; +const handleDelete = function (request, response) { let dataString = '' - - request.on( 'data', function( data ) { - dataString += data + request.on('data', function (data) { + dataString += data }) - - request.on( 'end', function() { - console.log( JSON.parse( dataString ) ) - - // ... do something with the data here!!! - - response.writeHead( 200, "OK", {'Content-Type': 'text/plain' }) - response.end('test') + request.on('end', function () { + let removeItem = JSON.parse(dataString).removeItem + + let index = -1; + for(let i = 0; i < groceryList.length; i++){ + if(groceryList[i].item === removeItem){ + index = i; + } + } + if(index != -1){ + groceryList.splice(index, 1); + } + + response.writeHead(200, "OK", { 'Content-Type': 'text/plain' }) + response.end(JSON.stringify(groceryList)) }) } - -const sendFile = function( response, filename ) { - const type = mime.getType( filename ) - - fs.readFile( filename, function( err, content ) { - - // if the error = null, then we've loaded the file successfully - if( err === null ) { - - // status code: https://httpstatuses.com - response.writeHeader( 200, { 'Content-Type': type }) - response.end( content ) - - }else{ - - // file not found, error code 404 - response.writeHeader( 404 ) - response.end( '404 Error: File Not Found' ) - - } - }) +const sendFile = function (response, filename) { + const type = mime.getType(filename); + fs.readFile(filename, function (err, content) { + // if the error = null, then we've loaded the file successfully + if (err === null) { + // status code: https://httpstatuses.com + response.writeHeader(200, { "Content-Type": type }); + response.end(content); + } else { + // file not found, error code 404 + response.writeHeader(404); + response.end("404 Error: File Not Found"); + } + }); +}; + +const getCost = function (price, quantity){ + let priceItem = parseFloat(price).toFixed(2); + return parseFloat(quantity * priceItem).toFixed(2).toString(); } -server.listen( process.env.PORT || port ) +server.listen(process.env.PORT || port); diff --git a/shrinkwrap.yaml b/shrinkwrap.yaml new file mode 100644 index 00000000..7cc43d7c --- /dev/null +++ b/shrinkwrap.yaml @@ -0,0 +1,15 @@ +dependencies: + mime: 3.0.0 +packages: + /mime/3.0.0: + dev: false + engines: + node: '>=10.0.0' + hasBin: true + resolution: + integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== +registry: 'https://registry.npmjs.org/' +shrinkwrapMinorVersion: 9 +shrinkwrapVersion: 3 +specifiers: + mime: ^3.0.0