diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/README.md b/README.md index 493995c1..6340ccfc 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,79 @@ +Thea Caplan +Link: https://a2-tcaplan.glitch.me + Assignment 2 - Short Stack: Basic Two-tier Web Application using HTML/CSS/JS and Node.js === - Due: September 11th, by 11:59 AM. +=== -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. +## College Schedule Maker +This application is a class schedule maker that allows users to input their classes and view a table representation of the class schedule by day. Users can add, remove, and modify classes as desired. It is a single-page application that shows the current status of the server (stored data) at all times. CSS grid positioning was used to format the application. -Baseline Requirements ---- +The application includes 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. + +My tabular dataset: +An object array of class information, where each class has: +1. Name (required) +2. Class Code (optional) +3. Start Time (required) +4. End Time (required) +5. Days the class meets (at least 1 required) +6. Length of the class (derived) + +## Technical Achievements +**Created a Single-Page App**: The application is a one page application where the dataset stored in the server is updated in real time whenever a change is made to the dataset (submitting, removing, or modifying data) + +**Modification Of Data Enabled**: Along with adding and removing data from the server, users are allowed to modify existing data through the modification form. -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. +## Design/Evaluation Achievements +**Think-Aloud Protocol**: Ran 2 think-alouds to test the design of the website. -Your application is required to implement the following functionalities: +Prompt: Add 2 of your classes to the schedule, then modify 1 class to have class on a Sunday. -- 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` +Student 1: +1. Last Name: Logan +2. Problems: Didn't see the classes in the schedule before adding, so having 2 classes with the same name was confusing. +3. Surprising Comments: Liked the words at the box line for labeling +4. Changes based on feedback: Expected the classes to be organized positionally by time rather than just sorted. (Would have implemented with more time :( ) + +Student 2: +1. Last Name: Chang +2. Problems: The dropdowns needed a second click to update the data shown. (Bug fixed in final submission) +3. Surprising Comments: Liked the labels, described the form layouts as intuitive +4. Changes based on feedback: Fix the dropdown second click bug. (Bug was only noticed when using arrow keys, fixed in final submission) + +## Baseline Requirements +--- +- a `Server` (ADDED) +- a `Results` functionality (ADDED) +- a `Form/Entry` functionality (ADDED) +- a `Server Logic` (ADDED) +- the `Derived field` (ADDED) 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 `
+ + + diff --git a/public/js/display.js b/public/js/display.js new file mode 100644 index 00000000..dd85dc27 --- /dev/null +++ b/public/js/display.js @@ -0,0 +1,128 @@ +function displaySchedule(data) { + // make the headers + const daysOfWeek = { + Su: "Sunday", + M: "Monday", + T: "Tuesday", + W: "Wednesday", + Th: "Thursday", + F: "Friday", + Sa: "Saturday" + }; + + var table = document.getElementById('dataView') + table.textContent = '' // clear the table + + // create the weekday headers + var row = table.insertRow(-1); + Object.keys(daysOfWeek).forEach(key => { + var headerCell = document.createElement("th"); + headerCell.innerHTML = daysOfWeek[key] + headerCell.name = key + headerCell.className = "displayHeader" + row.appendChild(headerCell) + }); + + // organize by day / time + const schedule = { + Su: [], + M: [], + T: [], + W: [], + Th: [], + F: [], + Sa: [] + }; + + + // organize the data by time and day + for(obj of data) { + Object.values(obj.Days).forEach( day => { + + // compare the times and then organize + let i = 0 + const start1 = new Date('1970-01-01T' + obj.StartTime + ":00") + for(obj2 of schedule[day]) { + const start2 = new Date('1970-01-01T' + obj2.StartTime + ":00") + if(start1 - start2 < 0) { + break; + } + i++ + } + const temp = schedule[day].slice(0, i) + temp.push(obj) + for(el of schedule[day].slice(i)) { + temp.push(el) + } + schedule[day] = temp + }); + } + + let day = 0 + Object.values(schedule).forEach( classes => { + let row = 1 + let r + for(c of classes) { + if(table.rows.length <= row) { + r = table.insertRow(-1) + Object.values(schedule).forEach(_ => r.insertCell(-1)) + } + table.rows[row].cells[day].appendChild(classDisplay(c)) + row++ + } + day++; + }) + + // console.log(schedule) +} + +// display a singular class cell +function classDisplay(classObj, showDay=false) { + const box = document.createElement('div'); + box.className = 'classDisplay' + + const add = (tag, inner) => { + if(inner === "") { + box.appendChild(document.createElement('br')) + return box + } + const el = document.createElement(tag) + el.innerHTML = inner + box.appendChild(el) + } + + add('h2', classObj.Name) + add('p', classObj.Code) + add('p', classObj.StartTime + " - " + classObj.EndTime) + if(showDay) { + add('p', Object.values(classObj.Days).toString()) + } + add('p', classObj.Length + ` hour${classObj.Length === 1 ? '' : 's'}`) + + return box +} + +const showClass = () => { + const data = document.getElementById("classSelect").value + const display = document.getElementById("singleClassDisplay") + display.innerText = "" + + // re-add the legend + const legend = document.createElement('legend') + legend.innerHTML = "Class to Remove" + display.appendChild(legend) + + if(typeof data === "undefined" || data === "") { + display.appendChild(document.createElement('br')) + let temp1 = document.createElement('p') + temp1.innerHTML = 'No Class' + display.appendChild(temp1) + let temp2 = document.createElement('p') + temp2.innerHTML = 'Selected' + display.appendChild(temp2) + display.appendChild(document.createElement('br')) + } else { + display.appendChild(classDisplay(JSON.parse(data), true)) + } + +} \ No newline at end of file diff --git a/public/js/main.js b/public/js/main.js index a569258f..56e1f048 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -7,8 +7,16 @@ const submit = async function( event ) { // remains to this day event.preventDefault() - const input = document.querySelector( '#yourname' ), - json = { yourname: input.value }, + const Name = document.getElementById( 'className' ).value, + Code = document.getElementById( 'classCode' ).value, + StartTime = document.getElementById('startTime').value, + EndTime = document.getElementById('endTime').value, + Days = getDays(''), + json = { Name, + Code, + StartTime, + EndTime, + Days }, body = JSON.stringify( json ) const response = await fetch( '/submit', { @@ -16,12 +24,215 @@ const submit = async function( event ) { body }) + const responseJSON = await response.json() + + // console.log( 'submit - received:', JSON.stringify(responseJSON) ) + if('errors' in responseJSON) { // there has been an error + // show warning messages + for (const [key, val] of Object.entries(responseJSON)) { + const el = document.getElementById(key + 'Warning'); + if(typeof el !== 'undefined' && el !== null) { + if (val !== true) { // there is an error + el.innerHTML = val + } else { // no error + el.innerHTML = '' + } + } + } + } else { // remove all warning labels + const warnings = document.getElementsByClassName('warning') + for ( element of warnings) { + element.innerHTML = '' + } + } + + getAll() +} + +const getAll = async function getAll( ) { + // event.preventDefault(); // TODO: check if we always need this? not sure what this does + + const response = await fetch( '/getAll', { + method:'POST' + }) + + const data = await response.json(); + + // console.log( 'getAll - received:', data ) + + displaySchedule(data) + + addClassesTo(document.getElementById("classSelect")).then(showClass) + addClassesTo(document.getElementById("classModifySelect")).then(loadClass) +} + +const remove = 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 body = JSON.stringify( document.getElementById('classSelect').value ) + + const response = await fetch( '/remove', { + method:'POST', + body + }) + const text = await response.text() - console.log( 'text:', text ) + if(text === 'success') { + console.log('sucessfully removed') + } else { + console.log('failed to remove') + } + + getAll() + +} + +const modify = 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 prev = document.getElementById('classModifySelect').value, + Name = document.getElementById( 'className2' ).value, + Code = document.getElementById( 'classCode2' ).value, + StartTime = document.getElementById('startTime2').value, + EndTime = document.getElementById('endTime2').value, + Days = getDays('2'), + json = { prev, + 'new': { Name, + Code, + StartTime, + EndTime, + Days }, + } + body = JSON.stringify( json ) + + const response = await fetch( '/modify', { + method:'POST', + body + }) + + const responseJSON = await response.json() + + if('errors' in responseJSON) { // there has been an error + // show warning messages + for (const [key, val] of Object.entries(responseJSON)) { + const el = document.getElementById(key + '2Warning'); + if(typeof el !== 'undefined' && el !== null) { + if (val !== true) { // there is an error + el.innerHTML = val + } else { // no error + el.innerHTML = '' + } + } + } + } else { // remove all warning labels + const warnings = document.getElementsByClassName('warning') + for ( element of warnings) { + element.innerHTML = '' + } + } + + getAll() + + getAll() +} + +const getDays = (id) => { + var days = ['monday' + id, + 'tuesday' + id, + 'wednesday' + id, + 'thursday' + id, + 'friday' + id, + 'saturday' + id, + 'sunday' + id] + var selected = {} + days = days.map(day => document.getElementById(day)) + counter = 0 + days.forEach( el => { + if(el.checked) { + selected['pos' + counter++] = el.value + } + }) + return selected +}; + +const addClassesTo = async function ( dropdown ) { + const response = await fetch( '/getAll', { + method:'POST' + }) + + const data = await response.json() + + // console.log('getAll - received: ', data) + + dropdown.textContent = '' // clears the dropdown + + selected = false + for(c of data) { + let opt = document.createElement('option') + opt.value = JSON.stringify(c) + opt.innerHTML = c.Name + if(!selected) { + opt.selected = "selected" + selected = true + } + dropdown.appendChild(opt) + } +} + +const loadClass = function ( ) { + + const dropdown = document.getElementById('classModifySelect') + if(dropdown.value == "") { + document.getElementById('className2').value = "" + document.getElementById('classCode2').value = "" + document.getElementById('startTime2').value = "" + document.getElementById('endTime2').value = "" + const days = ['monday2', 'tuesday2', 'wednesday2', 'thursday2', 'friday2', 'saturday2', 'sunday2'] + for(day of days) { + document.getElementById(day).checked = "" + } + } else { + const data = JSON.parse(dropdown.value) + document.getElementById('className2').value = data.Name + document.getElementById('classCode2').value = data.Code + document.getElementById('startTime2').value = data.StartTime + document.getElementById('endTime2').value = data.EndTime + const days = ['monday2', 'tuesday2', 'wednesday2', 'thursday2', 'friday2', 'saturday2', 'sunday2'] + for(day of days) { + document.getElementById(day).checked = "" + } + Object.values(data.Days).forEach( day => { + switch(day) { + case 'M': document.getElementById('monday2').checked = "checked"; break; + case 'T': document.getElementById('tuesday2').checked = "checked"; break; + case 'W': document.getElementById('wednesday2').checked = "checked"; break; + case 'Th': document.getElementById('thursday2').checked = "checked"; break; + case 'F': document.getElementById('friday2').checked = "checked"; break; + case 'Sa': document.getElementById('saturday2').checked = "checked"; break; + case 'Su': document.getElementById('sunday2').checked = "checked"; break; + } + }) + } } window.onload = function() { - const button = document.querySelector("button"); - button.onclick = submit; + document.getElementById("submitAdd").onclick = submit; + document.getElementById("submitRemove").onclick = remove; + document.getElementById("submitModify").onclick = modify; + const removeDropdown = document.getElementById("classSelect") + const modifyDropdown = document.getElementById("classModifySelect") + addClassesTo(removeDropdown) + removeDropdown.onchange = showClass + addClassesTo(modifyDropdown) + modifyDropdown.onchange = loadClass + getAll(); } \ No newline at end of file diff --git a/server.improved.js b/server.improved.js index 9ac27fb8..daf68c49 100644 --- a/server.improved.js +++ b/server.improved.js @@ -9,9 +9,12 @@ const http = require( 'http' ), port = 3000 const appdata = [ - { 'model': 'toyota', 'year': 1999, 'mpg': 23 }, - { 'model': 'honda', 'year': 2004, 'mpg': 30 }, - { 'model': 'ford', 'year': 1987, 'mpg': 14} + { 'Name': 'Webware', + 'Code': 'CS4241', + 'StartTime' : '12:00', + 'EndTime': '14:00', + 'Days': { 'pos0': 'M', 'pos1': 'Th' }, + 'Length': 2 }, ] const server = http.createServer( function( request,response ) { @@ -33,20 +36,172 @@ const handleGet = function( request, response ) { } const handlePost = function( request, response ) { - let dataString = '' - request.on( 'data', function( data ) { - dataString += data - }) - request.on( 'end', function() { - console.log( JSON.parse( dataString ) ) + if(request.url === '/submit') { + handleSubmit( request, response ) + } else if(request.url === '/getAll') { + handleGetAll( request, response ) + } else if(request.url === '/remove') { + handleRemove( request, response ) + } else if(request.url === '/modify') { + handleModify( request, response ) + } else { + console.log('FAIL - requested: ' + request.url) + } +} + +const handleGetAll = function(request, response) { + request.on( 'data', () => {}) + request.on( 'end', function() { + response.writeHead( 200, "OK", {'Content-Type': 'text/plain' }) + // console.log('getAll - sending: ' + JSON.stringify(appdata)) + response.end(JSON.stringify(appdata)) + }) +} + +const handleSubmit = function(request, response) { + let dataString = '' + + request.on( 'data', function( data ) { + dataString += data + }) + + request.on( 'end', function() { + // console.log( 'submit - received: ' + JSON.parse( dataString ) ) + + json = JSON.parse( dataString ) + + // add data + // check has name, start time, end time, and 1 day associated + const error = validate(json); + + if(error.errors === false) { // no errors + + // calculate the derived field (length of class) + json['Length'] = calcDerivedLength(json.StartTime, json.EndTime) - // ... do something with the data here!!! + // add to the server data + appdata.push( + json + ) - response.writeHead( 200, "OK", {'Content-Type': 'text/plain' }) - response.end('test') - }) + // console.log('submit - new data: ' + JSON.stringify(appdata)) + + response.writeHead( 200, "OK", {'Content-Type': 'text/plain' }) + response.end(JSON.stringify({})) + } else { // send back error message + // console.log('submit - error: ' + JSON.stringify(error)) + response.writeHead( 200, "OK", {'Content-Type': 'text/plain' }) + response.end( JSON.stringify(error) ) + } + }) +} + +const handleRemove = function(request, response) { + let dataString = '' + + request.on( 'data', function( data ) { + dataString += data + }) + + request.on( 'end', function() { + // console.log( 'remove - received: ' + JSON.parse( dataString ) ) + + json = JSON.parse( dataString ) + + // find the data to remove + let i = 0; + const max = appdata.length + for(obj of appdata) { + if(JSON.stringify(obj) === json) { + const front = appdata.slice(0, i) + const back = appdata.slice(i+1) + const temp = front.concat(back) + while(appdata.length > 0) { + appdata.pop() + } + for(let j = 0; j < temp.length; j++) { + appdata.push(temp[j]) + } + + response.writeHead( 200, "OK", {'Content-Type': 'text/plain' }) + response.end('success') + console.log('remove - success') + break + } else { + i++ + } + } + + if(i >= max) { + response.writeHead( 200, "OK", {'Content-Type': 'text/plain' }) + response.end('fail') + console.log('failed to remove') + } + }) +} + +const handleModify = function(request, response) { + let dataString = '' + + request.on( 'data', function( data ) { + dataString += data + }) + + request.on( 'end', function() { + // console.log( 'modify - received: ' + dataString ) + + json = JSON.parse( dataString ) + + prev = json.prev + data = json.new + + if(prev === "") { // no classes to modify + const error = { + 'errors': true, + 'classModifySelect': "No Classes To Modify", + } + // console.log('modify - error: ' + JSON.stringify(error)) + response.writeHead( 200, "OK", {'Content-Type': 'text/plain' }) + response.end( JSON.stringify(error) ) + } else { + // validate the new values + const error = validate(data); + + if(error.errors === false) { + // find the data to modify + let i = 0; + const max = appdata.length + for(obj of appdata) { + if(JSON.stringify(obj) === prev) { + + obj.Name = data.Name + obj.Code = data.Code + obj.StartTime = data.StartTime + obj.EndTime = data.EndTime + obj.Length = calcDerivedLength(data.StartTime, data.EndTime) + + response.writeHead( 200, "OK", {'Content-Type': 'text/plain' }) + response.end(JSON.stringify({})) + console.log('modify - success') + break + } else { + i++ + } + } + if(i >= max) { + response.writeHead( 200, "OK", {'Content-Type': 'text/plain' }) + response.end(JSON.stringify(error)) + console.log('failed to modify') + } + } else { // send back error message + // console.log('modify - error: ' + JSON.stringify(error)) + response.writeHead( 200, "OK", {'Content-Type': 'text/plain' }) + response.end( JSON.stringify(error) ) + } + } + }) } const sendFile = function( response, filename ) { @@ -71,4 +226,50 @@ const sendFile = function( response, filename ) { }) } -server.listen( process.env.PORT || port ) +const validate = data => { + error = { + errors: false, + className: true, + startTime: true, + endTime: true, + days: true + } + if(!(data.Name.length > 0) ) { + error.className = "Name required" + error.errors = true + } + if(!(data.StartTime.length > 0) ) { + error.startTime = "Start Time required" + error.errors = true + } + if(!(data.EndTime.length > 0) ) { + error.endTime = "End Time required" + error.errors = true + } + + const start = data.StartTime.split(':') + const end = data.EndTime.split(':') + if(start[0] > end[0]) { + error.startTime = "Start Time must be before End Time" + error.errors = true + } else if (start[0] == end[0] && start[1] > end[1]) { + error.startTime = "Start Time must be before End Time" + error.errors = true + } + + if(!(Object.keys(data.Days).length > 0)) { + error.days = "Must select at least one day" + error.errors = true + } + return error; +}; + +const calcDerivedLength = (start, end) => { + // calculate the derived field (length of class) + const s = new Date('1970-01-01T' + start + ":00") + const e = new Date('1970-01-01T' + end + ":00") + const msPerHour = 1000 * 60 * 60 + return Math.round(((e-s) / msPerHour) * 100) / 100 +} + +server.listen( process.env.PORT || port ) \ No newline at end of file