diff --git a/README.md b/README.md index 3248143..aa9549f 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,31 @@ -Assignment 2 - Short Stack: Basic Two-tier Web Application using HTML/CSS/JS and Node.js -=== +## Maxwell Jeronimo +## Link: https://a2-maxwelljeronimo-a25-2.onrender.com -Due: Monday, September 8, 2025, by 11:59 PM. +## Vinyl Collection System +This project is a Two-Tier Node.js and a HTML/JS web app that keeps track of a collection of vinyls. A user can add +vinyls that they currently own, or ones that they are looking to get, as well as view their current collection +of both owned or wanted vinyls, as well as see purchase links if they do not own them. I used Flexbot positioning +and card-styled forms, tables, and textboxes, utilized by a css file. -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. +In order to use the application, you would navigate throught the two links in the nav bar, one being the Add Vinyl tab, +and the other being the View Collection tab. All data is stored dynamically through the data.json file. -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 (4 pts each, total 20 pts): - -- 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 (4 pts each, total 16 pts): -- 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 ` + + + + + ` +} + + +function escapeHtml(s) { + return String(s) + .replaceAll('&','&') + .replaceAll('<','<') + .replaceAll('>','>') +} + +function escapeAttr(s) { + return String(s) + .replaceAll('&','&') + .replaceAll('"','"') + .replaceAll("'","'") + .replaceAll('<','<') + .replaceAll('>','>') +} + +let currentlyEditing = null + +const displayResults = async function () { + try { + const response = await fetch("/results") + const data = await response.json() + + const tbody = document.querySelector("#results-table tbody") + if (!tbody) return + + tbody.innerHTML = "" + data.forEach(item => { + const tr = document.createElement("tr") + tr.dataset.slug = item.slug + tr.innerHTML = renderHTMLRow(item) + tr.dataset.original = JSON.stringify(item) + tbody.appendChild(tr) + }) + + + tbody.onclick = async function(e) { + const btn = e.target + const tr = btn.closest("tr") + if (!tr) return + + + if (btn.matches(".editButton")) { + if (currentlyEditing && currentlyEditing !== tr) { + alert("Finish or cancel the other edit first.") + return + } + currentlyEditing = tr + + const item = JSON.parse(tr.dataset.original) + tr.innerHTML = ` + + + + + + + ` + tr.querySelector(".input-vinyl").focus() + + const ownedInput = tr.querySelector(".input-owned") + const linkField = tr.querySelector(".link-edit-field") + const linkInput = tr.querySelector(".input-link") + + ownedInput.addEventListener("change", () => { + if (ownedInput.checked) { + linkField.style.display = "none" + linkInput.value = "" + } else { + linkField.style.display = "block" + } + }) + + + return + } + + + if (btn.matches(".saveButton")) { + const slug = btn.dataset.slug + const vinylVal = tr.querySelector(".input-vinyl").value.trim() + const artistVal = tr.querySelector(".input-artist").value.trim() + const ownedChecked = tr.querySelector(".input-owned").checked + const linkVal = tr.querySelector(".input-link").value.trim() + + const original = JSON.parse(tr.dataset.original) + + const updated = { + slug: slug, + vinyl: vinylVal !== "" ? vinylVal : original.vinyl, + artist: artistVal !== "" ? artistVal : original.artist, + owned: ownedChecked, + link: linkVal !== "" ? linkVal : original.link + } + + await fetch("/update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(updated) + }) + + currentlyEditing = null + displayResults() + return + } + + if (btn.matches(".cancelButton")) { + const original = JSON.parse(tr.dataset.original) + tr.innerHTML = renderHTMLRow(original) + tr.dataset.original = JSON.stringify(original) + currentlyEditing = null + return + } + + if (btn.matches(".deleteButton")) { + const slug = btn.dataset.slug + const actionCell = tr.querySelector(".cellActions") + actionCell.innerHTML = ` + Delete "${escapeHtml(tr.querySelector(".cellVinyl").textContent)}"? + + + ` + return + } + + if (btn.matches(".confirmDelete")) { + const slug = btn.dataset.slug + await fetch("/delete", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ slug }) + }) + displayResults() + return + } + + if (btn.matches(".cancelDelete")) { + const original = JSON.parse(tr.dataset.original) + tr.innerHTML = renderHTMLRow(original) + tr.dataset.original = JSON.stringify(original) + } + } + + } catch (err) { + console.error("Error in getting the results from the server:", err) + } +} + + 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 ) - - const response = await fetch( "/submit", { - method:"POST", - body + + + const vinylInput = document.querySelector("#vinyl") + const artistInput = document.querySelector("#artist") + const ownedInput = document.querySelector("#owned") + const linkInput = document.querySelector("#link") + + const json = { + vinyl: vinylInput.value, + artist: artistInput.value, + owned: ownedInput.checked, + link: ownedInput.checked ? "" : linkInput.value + } + + const response = await fetch("/submit", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(json) + }) - const text = await response.text() + const updatedData = await response.json() + console.log("Updated data:", updatedData) + + displayResults() + - console.log( "text:", text ) + vinylInput.value = '' + artistInput.value = '' + ownedInput.checked = false + linkInput.value = '' } + +const ownedCheckbox = document.getElementById("owned"); +const linkField = document.getElementById("link-field"); +const linkInput = document.getElementById("link"); + +if (ownedCheckbox && linkField && linkInput) { + ownedCheckbox.addEventListener("change", () => { + if (ownedCheckbox.checked) { + linkField.style.display = "none"; + linkInput.value = ""; + } else { + linkField.style.display = "block"; + } + }); +} + + window.onload = function() { - const button = document.querySelector("button"); - button.onclick = submit; + const form = document.querySelector("#vinylForm"); + if (form) { + form.onsubmit = submit + } + const resultsTable = document.querySelector("#results-table") + if (resultsTable) { + displayResults() + } } \ No newline at end of file diff --git a/public/results.html b/public/results.html new file mode 100644 index 0000000..b3a31b6 --- /dev/null +++ b/public/results.html @@ -0,0 +1,34 @@ + + + + CS4241 Assignment 2 + + + + + + + + + +

Vinyl Collection

+
${escapeHtml(item.vinyl)}${escapeHtml(item.artist)}${item.owned ? "✅" : "❌"} + + + + + + + +
+ + + + + + + + + + + + +
VinylArtistOwned?LinksActions
+ + + \ No newline at end of file diff --git a/server.improved.js b/server.improved.js index 0f63012..765d2f8 100644 --- a/server.improved.js +++ b/server.improved.js @@ -1,33 +1,64 @@ const http = require( "http" ), fs = require( "fs" ), + path = require( "path") + +const DATA_FILE = path.join(__dirname, "data.json") // 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/", + 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} -] +let appdata = [] +try { + if (fs.existsSync(DATA_FILE)) { + appdata = JSON.parse(fs.readFileSync(DATA_FILE, "utf-8")) + } +} catch (err) { + console.error("Error loading data file:", err) + appdata = [] +} + +function saveData() { + try { + fs.writeFileSync(DATA_FILE, JSON.stringify(appdata, null, 2)) + } catch (err) { + console.error("Error saving data file:", err) + } +} + +function makeSlug(vinyl, artist) { + return `${vinyl.toLowerCase().replace(/\s+/g, "-")}-${artist.toLowerCase().replace(/\s+/g, "-")}` +} const server = http.createServer( function( request,response ) { if( request.method === "GET" ) { handleGet( request, response ) - }else if( request.method === "POST" ){ - handlePost( request, response ) + }else if( request.method === "POST" && request.url === "/submit" ){ + handlePost( request, response ) + } else if (request.method === "POST" && request.url === "/update") { + handleUpdate(request, response) + } else if (request.method === "POST" && request.url === "/delete") { + handleDelete(request, response) + } else { + response.writeHead(404) + response.end("404 Error: Not Found") } }) const handleGet = function( request, response ) { const filename = dir + request.url.slice( 1 ) - if( request.url === "/" ) { + if (request.url === "/results") { + response.writeHead(200, { "Content-Type": "application/json" }) + response.end(JSON.stringify(appdata)) + } + else if( request.url === "/" ) { sendFile( response, "public/index.html" ) - }else{ + } + else{ sendFile( response, filename ) } } @@ -35,17 +66,69 @@ const handleGet = function( request, response ) { const handlePost = function( request, response ) { let dataString = "" - request.on( "data", function( data ) { - dataString += data + request.on( "data", chunk => dataString += chunk ) + + request.on("end", () => { + const newVinyl = JSON.parse(dataString) + const slug = makeSlug(newVinyl.vinyl, newVinyl.artist) + + if (appdata.find (v => v.slug === slug)) { + response.writeHead(400, {"Content-Type": "application/json"}) + response.end(JSON.stringify({error: "This Vinyl already exists"})) + return + } + + const record = { + vinyl: newVinyl.vinyl, + artist: newVinyl.artist, + owned: Boolean(newVinyl.owned), + link: newVinyl.owned ? "": (newVinyl.link || ""), + slug, + dateAdded: new Date().toISOString() + } + appdata.push(record) + saveData() + + response.writeHead( 200, {"Content-Type": "application/json" }) + response.end(JSON.stringify(appdata)) + }) +} + +const handleUpdate = function( request, response ) { + let dataString = "" + + request.on("data", chunk => dataString += chunk) + + request.on("end", () => { + const updated = JSON.parse(dataString) + const index = appdata.findIndex(v => v.slug === updated.slug) + + if (index !== -1) { + appdata[index].vinyl = updated.vinyl + appdata[index].artist = updated.artist + appdata[index].owned = updated.owned + appdata[index].link = updated.owned ? "" : updated.link + appdata[index].slug = makeSlug(updated.vinyl, updated.artist) + saveData() + } + + response.writeHead (200, {"Content-Type": "application/json"}) + response.end(JSON.stringify(appdata)) }) +} + +const handleDelete = function ( request, response ) { + let dataString = "" - request.on( "end", function() { - console.log( JSON.parse( dataString ) ) + request.on("data", chunk => dataString += chunk) - // ... do something with the data here!!! + request.on("end", () => { + const { slug } = JSON.parse(dataString) + appdata = appdata.filter (v => v.slug !== slug) + saveData() - response.writeHead( 200, "OK", {"Content-Type": "text/plain" }) - response.end("test") + response.writeHead(200, { "Content-Type": "application/json" }) + response.end(JSON.stringify(appdata)) }) }