From 2f9cd43f44f8cdd4bdb16bb95a507b5f45c8e44d Mon Sep 17 00:00:00 2001 From: Mike Vink Date: Fri, 4 Jun 2021 17:08:53 +0200 Subject: server and response api --- client/index.html | 166 ++------------------ client/shoppingbasket.html | 46 +++++- client/src/functions.js | 20 +++ client/src/index.js | 285 ++++++++++++++++++++++++++++------ client/src/shoppingbasket.js | 82 +++++----- client/src/templateImplementations.js | 96 ++++++++++++ client/src/utils.js | 48 ++++++ client/style/main.css | 28 ++++ client/style/shoppingbasket.css | 2 +- main.js | 19 +++ 10 files changed, 538 insertions(+), 254 deletions(-) create mode 100644 client/src/functions.js create mode 100644 client/src/templateImplementations.js create mode 100644 client/src/utils.js diff --git a/client/index.html b/client/index.html index 0ca5740..e918f39 100644 --- a/client/index.html +++ b/client/index.html @@ -47,11 +47,14 @@
+ +
+
+ + -
-
Madurodam
-
- The Netherlands smallest theme park. -
-
-
-
Adults: 25,-
-
Kids: 20,-
-
- Family ticket: - Buy 1 adult tickets & 2 kid tickets for a 25% discount! -
-
- - -
0,-
- -
-
- -
-
Toverland
-
- Experience magic and wonder. -
-
-
-
Adults: 30,-
-
Kids: 30,-
-
- Family ticket: - Buy 2 adult tickets & 2 kid tickets for a 33% discount! -
-
- - -
0,-
- -
-
- -
-
Walibi Holland
-
- Need an Adrenaline Rush? -
-
-
-
Adults: 37,-
-
Kids: 37,-
-
- Family ticket: - Buy 4 adult tickets & 0 kid tickets for a 10% discount! -
-
- - -
0,-
- -
-
- -
-
Duinrell
-
- From the Kikkerbaan to the Tikibad. -
-
-
-
Adults: 22,-
-
Kids: 19,-
-
- Family ticket: - Buy 1 adult tickets & 3 kid tickets for a 20% discount! -
-
- - -
0,-
- -
-
- -
-
Slagharen
-
- Fun for the whole family in a true western style. -
-
-
-
Adults: 28,-
-
Kids: 20,-
-
- Family ticket: - Buy 2 adult tickets & 2 kid tickets for a 50% discount! -
-
- - -
0,-
- -
-
- -
-
Drievliet
-
- Come and experience our wonderful attractions. -
-
-
-
Adults: 26,-
-
Kids: 24,-
-
- Family ticket: - Buy 1 adult tickets & 2 kid tickets for a 25% discount! -
-
- - -
0,-
- -
-
- - - - + diff --git a/client/shoppingbasket.html b/client/shoppingbasket.html index 720c463..8464aaf 100644 --- a/client/shoppingbasket.html +++ b/client/shoppingbasket.html @@ -41,18 +41,52 @@
- +
- + + + + diff --git a/client/src/functions.js b/client/src/functions.js new file mode 100644 index 0000000..7f06c3b --- /dev/null +++ b/client/src/functions.js @@ -0,0 +1,20 @@ +// Dynamic article displaying +export async function fetchAttractions() { + + try { + + const response = await fetch("api/attractions"); + + if (!response.ok) { + const message = `An error has occured: ${response.status}`; + throw new Error(message); + } + + const attractions = response.json(); + return attractions; + + } catch(error) { + console.log("something went wrong when fetching attractions: ", error); + } + +} diff --git a/client/src/index.js b/client/src/index.js index 13d7219..018693f 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -1,75 +1,270 @@ +import { ParkArticle } from "./templateImplementations.js"; +import { displayNumberOfItemsInShoppingBasketWithBadge, dutchCurrencyFormat, dutchCurrencyFormatWithSign } from "./utils.js"; +import { fetchAttractions } from "./functions.js" + + + +function displayArticles(articles) { + + const parkArticleFunctionality = { + orderButtonClick: orderButtonClicked, + displayTotal: displayTotal, + disableButton: disableButton, + } + + for (var i = 0; i < articles.length; i++) { + var parkArticle = new ParkArticle(articles[i], document.querySelector("#parkarticle")); + parkArticle.addToNode(document.querySelector("#center-articles"), parkArticleFunctionality); + } + +} + +function disableButton(name, button) { + return function inputEventReceiver(event) { + const inputs = button.parentNode.querySelectorAll("input"); + var inputTickets = 0; + for (let i = 0; i < inputs.length; i++) { + if (inputs[i].value > 0) { + inputTickets = inputTickets + Number.parseInt(inputs[i].value); + } + } + + const shoppingBasketArray = JSON.parse(localStorage.getItem("shoppingBasketArray")); + var shoppingBasketTickets = 0; + + if (shoppingBasketArray) { + for (let i = 0; i < shoppingBasketArray.length; i++) { + if (shoppingBasketArray[i].name === name) { + shoppingBasketTickets = inputTickets + shoppingBasketArray[i].numberOfKids + shoppingBasketArray[i].numberOfAdults; + } + } + } + const totalTickets = inputTickets + shoppingBasketTickets; + fetchAttractions() + .then(attractions => { + + var attraction; + for (let i = 0; i < attractions.length; i++) { + if (attractions[i].name === name) { + attraction = attractions[i]; + } + } + + var front = button.querySelector(".front"); + if (attraction.available < totalTickets || attraction.available === 0 || attraction.available === shoppingBasketTickets) { + front.classList.add("disabled"); + button.removeEventListener("click", orderButtonClicked); + } else { + front.classList.remove("disabled"); + button.addEventListener("click", orderButtonClicked); + } + + }) + .catch(error => {console.error(error)}); + } +} + function orderButtonClicked(event) { + console.log("button click"); var button; + if (event.target.classList.contains("orderbutton")) { button = event.target; } else { button = event.target.parentNode; } - var node = button.previousElementSibling; - var adults; - var kids; - var parkName; - while (true) { - if (node.classList.contains("numberofkids")) { - kids = Number(node.value); + + const order = button.parentNode; + const parkArticle = order.parentNode; + + const orderClientSideInfo = { + name: parkArticle.querySelector(".parkname").textContent, + numberOfKids: Number(order.querySelector(".numberofkids").value), + numberOfAdults: Number(order.querySelector(".numberofadults").value), + } + + console.log(orderClientSideInfo); + + if ((orderClientSideInfo.numberOfKids > 0 && orderClientSideInfo.numberOfAdults >= 0) || (orderClientSideInfo.numberOfAdults > 0 && orderClientSideInfo.numberOfKids >= 0)) { + fetchAttractions() + .then(checkTicketAvailability(button, orderClientSideInfo)) + .then(saveOrderInShoppingBasket(orderClientSideInfo)) + .then(disableButton(orderClientSideInfo.name, button)) + .catch((error) => {console.log(error.message)}) + } +} + +class TicketsNotAvailableError extends Error { + constructor(message) { + super(message); + this.name = "TicketsNotAvailableError"; + } +} + + +export function checkTicketAvailability(button, orderClientSideInfo) { + + return function serverAttractionsAccepter(serverAttractionsArray) { + var attraction; + for (let i = 0; i < serverAttractionsArray.length; i++) { + if (serverAttractionsArray[i].name === orderClientSideInfo.name) { + attraction = serverAttractionsArray[i] + } } - if (node.classList.contains("numberofadults")) { - adults = Number(node.value) + + if (attraction.available < orderClientSideInfo.numberOfKids + orderClientSideInfo.numberOfAdults) { + throw new TicketsNotAvailableError("The tickets of the order exceed the available tickets!"); } - if (node.classList.contains("parkname")) { - parkName = node.innerText; - break + + return serverAttractionsArray; + } +} + +function saveOrderInShoppingBasket(orderClientSideInfo) { + console.log("saving in shopping basket"); + + return function serverAttractionsAccepter(serverAttractionsArray) { + // const orderClientSideInfo = this; + + var price; + for (let i = 0; i < serverAttractionsArray.length; i++) { + if (serverAttractionsArray[i].name === orderClientSideInfo.name) { + price = calulateTotal( + orderClientSideInfo.numberOfKids, + orderClientSideInfo.numberOfAdults, + serverAttractionsArray[i] + ); + } } - if (node.previousElementSibling == null) { - node = node.parentNode; + + console.log("totalprice: " + price.total); + console.log("discount: " + price.discount); + orderClientSideInfo.price = price; + + var shoppingBasketArray; + + if (localStorage.getItem("shoppingBasketArray") === null) { + shoppingBasketArray = []; + shoppingBasketArray.push(orderClientSideInfo); } else { - node = node.previousElementSibling; + shoppingBasketArray = JSON.parse(localStorage.getItem("shoppingBasketArray")); + console.log("before adding to array: " + shoppingBasketArray); + shoppingBasketArray.push(orderClientSideInfo); + console.log("after adding to array: " + shoppingBasketArray); } + + localStorage.setItem("shoppingBasketArray", JSON.stringify(shoppingBasketArray)); + displayNumberOfItemsInShoppingBasketWithBadge(); } +} - if (kids > 0 || adults > 0) { - saveOrderInShoppingBasket(parkName, adults, kids); +function calulateTotal(numberOfKids, numberOfAdults, serverSideAttraction) { + console.log("Calculating total !"); + console.log(serverSideAttraction); + + const adultPrice = serverSideAttraction.adultPrice; + const kidsPrice = serverSideAttraction.kidsPrice; + + const discountPercentage = serverSideAttraction.discount; + const minNumberkids = serverSideAttraction.minimumNumberOfKids; + const minNumberAdults = serverSideAttraction.minimumNumberOfAdults; + + var totalPrice = 0; + if (numberOfKids > 0) { + totalPrice = totalPrice + numberOfKids * kidsPrice; + } + if (numberOfAdults > 0) { + totalPrice = totalPrice + numberOfAdults * adultPrice; } -}; -function saveOrderInShoppingBasket(name, adults, kids) { - var order = { - name: name, - adults: adults, - children: kids, - }; - var orderString = JSON.stringify(order); - localStorage.setItem(localStorage.length + 1, orderString) - document.querySelector(".badge").innerText = localStorage.length; + if (numberOfKids >= minNumberkids && numberOfAdults >= minNumberAdults) { + var discount = totalPrice * discountPercentage / 100; + totalPrice = totalPrice - discount; + } + + if (discount) { + return {total:totalPrice, discount: discount} + } else { + return {total: totalPrice}; + } } -document.querySelector(".badge").innerText = localStorage.length; -var buttons = document.querySelectorAll(".orderbutton"); +function displayTotal(event) { + var order = event.target.parentNode; + var total = order.querySelector(".total"); + + var kids = order.querySelector(".numberofkids").value; + var adults = order.querySelector(".numberofadults").value; + + let re = /\d+/; + var kidsPrice = order.querySelector(".kidsprice") + .textContent + .match(re)[0]; + var adultPrice = order.querySelector(".adultprice") + .textContent + .match(re)[0]; + + var discountReq = order.querySelector(".discountrequirement") + var minNumberkids = discountReq.querySelector(".child") + .textContent + .match(re)[0] + var minNumberadults = discountReq.querySelector(".adults") + .textContent + .match(re)[0] + var discountPercentage = discountReq.querySelector(".percentage") + .textContent + .match(re)[0]; + // console.log(discountPercentage); + + var value = 0; + if (kids > 0) { + value = value + Number.parseInt(kids) * Number.parseFloat(kidsPrice); + } + if (adults > 0) { + value = value + Number.parseInt(adults) * Number.parseFloat(adultPrice); + } + var discount; + if (Number.parseInt(kids) >= Number.parseInt(minNumberkids) && Number.parseInt(adults) >= Number.parseInt(minNumberadults)) { + discount = value * Number.parseFloat(discountPercentage) / 100 + value = value - discount; + } + // console.log(value); -for (var i = 0; i < buttons.length; i++) { - console.log(buttons[i]); - buttons[i].addEventListener("click", orderButtonClicked); + var priceString = dutchCurrencyFormat(value); + if (!(discount === undefined)) { + priceString = priceString + " discount: " + dutchCurrencyFormatWithSign(discount); + } + total.querySelector(".price").textContent = priceString; } -// When the user scrolls the page, execute myFunction -window.onscroll = function() {myFunction()}; +function setStickyNavBar() { + // Get the header + var header = document.getElementById("sticky-header"); -// Get the header -var header = document.getElementById("sticky-header"); + // Get the offset position of the navbar + var sticky = header.offsetTop; -// Get the offset position of the navbar -var sticky = header.offsetTop; + // Add the sticky class to the header when you reach its scroll position. Remove "sticky" when you leave the scroll position + function makeHeaderStickyWhenScrolling() { + + if (window.pageYOffset > sticky) { + header.classList.add("sticky"); + } else { + header.classList.remove("sticky"); + } + } + // Sticky navigation bar stuff + // + // When the user scrolls the page, execute myFunction + window.onscroll = function() {makeHeaderStickyWhenScrolling()}; -// Add the sticky class to the header when you reach its scroll position. Remove "sticky" when you leave the scroll position -function myFunction() { - if (window.pageYOffset > sticky) { - header.classList.add("sticky"); - } else { - header.classList.remove("sticky"); - } } +displayNumberOfItemsInShoppingBasketWithBadge(); +setStickyNavBar(); +fetchAttractions() + .then(displayArticles); diff --git a/client/src/shoppingbasket.js b/client/src/shoppingbasket.js index c23c02c..7355a99 100644 --- a/client/src/shoppingbasket.js +++ b/client/src/shoppingbasket.js @@ -1,70 +1,62 @@ -document.querySelector(".badge").innerText = localStorage.length; +import { Order } from "./templateImplementations.js" +import { displayNumberOfItemsInShoppingBasketWithBadge, findParentWithTag, childKillerUsingTags } from "./utils.js"; + function getOrderArray() { - var orders = new Array; - for (let i = 0; i < localStorage.length; i++) { - var order = localStorage.getItem(i+1); - order = JSON.parse(order); - orders.push(order); - } - // console.log(orders); + var orders = JSON.parse(localStorage.getItem("shoppingBasketArray")); return orders; } -class Order { - constructor(orderJSON) { - for (const [key, value] of Object.entries(orderJSON)) { - this[key] = value; - } - - } - - addToMain() { - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template - var main = document.querySelector("main"); - var template = document.querySelector("#ticket"); +function displayOrders() { + displayNumberOfItemsInShoppingBasketWithBadge(); + const orderFunctionality = { + cancel: cancelOrder, + }; - var clone = template.content.cloneNode(true); - // console.log(clone); - var lines = clone.querySelectorAll("div"); + var orders = getOrderArray(); + if (orders === null) return; - for (var i = 0; i < lines.length; i++) { - var text = lines[i].textContent; + var main = document.querySelector("main"); - if (text === "Parkname") { - console.log(this.name); - lines[i].textContent = this.name; - } + for (let i = 0; i < orders.length; i++) { + var orderObj = new Order(orders[i], document.querySelector("#ticket")); + orderObj.addToNode(main, orderFunctionality); + } +} - if (text.toLowerCase().includes("adults")) { - lines[i].textContent = text + " " + this.adults; - } +function cancelOrder(event) { + //console.log(event.target); + const article = findParentWithTag.bind(event.target)("article"); - if (text.toLowerCase().includes("kids")) { - lines[i].textContent = text + " " + this.children; - } + var previous = article.previousSibling; + var i = 0; + while (previous) { + if (previous.tagName === "ARTICLE") { + i = i+1; } - - main.appendChild(clone); + previous = previous.previousSibling; } -} - -function displayOrders() { var orders = getOrderArray(); - for (let i = 0; i < orders.length; i++) { - orderObj = new Order(orders[i]); - orderObj.addToMain(); - } + orders.splice(i, 1); + localStorage.setItem("shoppingBasketArray", JSON.stringify(orders)); + var main = document.querySelector("main"); + childKillerUsingTags(main)(main.firstChild)("article"); + displayOrders(); } -displayOrders(); + + function finalizePayment(event) { console.log("finalizing payments"); localStorage.clear(); + window.location.replace("orderplaced.html"); } + document.querySelector("#finalizepaymentbutton").addEventListener("click", finalizePayment); + +displayOrders(); diff --git a/client/src/templateImplementations.js b/client/src/templateImplementations.js new file mode 100644 index 0000000..24b8c9a --- /dev/null +++ b/client/src/templateImplementations.js @@ -0,0 +1,96 @@ +import { dutchCurrencyFormat, dutchCurrencyFormatWithSign } from "./utils.js" +import { fetchAttractions } from "./functions.js" +/** + * Abstract class + * + * + * @class TemplatedNode + */ +class TemplatedNode { + constructor(json, template) { + // console.log(json); + for (const [key, value] of Object.entries(json)) { + this[key] = value; + } + + this.template = template; + } + + addToNode(node) { + throw new Error("Method 'addToNode' node be implemented."); + } +} + + +export class Order extends TemplatedNode { + + addToNode(node, orderFunctionality) { + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template + + var clone = this.template.content.cloneNode(true); + // console.log(clone); + clone.querySelector(".parkname").textContent = this.name; + clone.querySelector(".numberofkids").textContent = "Kids: " + this.numberOfKids; + clone.querySelector(".numberofadults").textContent = "Adults: " + this.numberOfAdults; + + + var priceString = "Total: " + dutchCurrencyFormat(this.price.total); + if (this.price.discount) { + priceString = priceString + " discount: " + dutchCurrencyFormatWithSign(this.price.discount); + } + clone.querySelector(".price").textContent = priceString; + + clone.querySelector("button").addEventListener("click", orderFunctionality.cancel); + + node.appendChild(clone); + } + +} + +export class ParkArticle extends TemplatedNode { + + addToNode(node, parkArticleFunctionality) { + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template + + var clone = this.template.content.cloneNode(true); + this.clone = clone; + var cloneChildren = clone.querySelectorAll("div"); + + for (var i = 0; i < cloneChildren.length; i++) { + // console.log(cloneChildren[i].className); + var element = cloneChildren[i]; + var className = element.className; + var text = element.textContent; + if (className === "parkname") { + element.textContent = this.name; + } + + if (className === "parkdescription") { + element.textContent = this.description; + } + + if (className === "adultprice") { + element.querySelector(".price").textContent = dutchCurrencyFormat(this.adultPrice); + } + + if (className === "kidsprice") { + element.querySelector(".price").textContent = dutchCurrencyFormat(this.kidsPrice); + } + + if (className === "discountrequirement") { + element.querySelector(".adults").textContent = this.minimumNumberOfAdults; + element.querySelector(".child").textContent = this.minimumNumberOfKids; + } + } + + var button = clone.querySelector(".orderbutton"); + parkArticleFunctionality.disableButton(this.name, button)(null); + + var inputElements = clone.querySelectorAll("input"); + for (let i = 0; i < inputElements.length; i++) { + inputElements[i].addEventListener("input", parkArticleFunctionality.displayTotal); + inputElements[i].addEventListener("input", parkArticleFunctionality.disableButton(this.name, button)); + } + node.appendChild(clone); + } +} diff --git a/client/src/utils.js b/client/src/utils.js new file mode 100644 index 0000000..03416f0 --- /dev/null +++ b/client/src/utils.js @@ -0,0 +1,48 @@ +export function displayNumberOfItemsInShoppingBasketWithBadge() { + var shoppingBasketArray = JSON.parse(localStorage.getItem("shoppingBasketArray")); + if (shoppingBasketArray === null) { + document.querySelector(".badge").innerText = 0; + } else { + document.querySelector(".badge").innerText = shoppingBasketArray.length; + } +} + +export function dutchCurrencyFormat(number) { + var decimal = (number * 10) % 10 + if (decimal === 0) return number + ",-"; + else { + return (number * 10 - decimal) / 10 + "," + decimal; + } +} + +export function dutchCurrencyFormatWithSign(number) { + return "\u20AC" + dutchCurrencyFormat(number); +} + + +export function findParentWithTag(tagName) { + if (this.tagName === tagName.toUpperCase()) { + return this; + } else { + return findParentWithTag.bind(this.parentNode)(tagName); + } +} + +export function childKillerUsingTags(parent) { + + return function oneOfMyChildren(child) { + + return function killChildrenWithTag(tag) { + if (child === null) { + return + } else if (child.tagName === tag.toUpperCase()) { + var next = child.nextSibling; + parent.removeChild(child); + return oneOfMyChildren(next)(tag); + } else { + return oneOfMyChildren(child.nextSibling)(tag); + } + } + + } +} diff --git a/client/style/main.css b/client/style/main.css index 7ec539a..c828d62 100644 --- a/client/style/main.css +++ b/client/style/main.css @@ -113,6 +113,7 @@ article { outline-offset: 4px; margin: 20px; } + .front { display: block; padding: 12px 42px; @@ -126,6 +127,16 @@ article { cubic-bezier(.3, .7, .4, 1); } +.disabled { + display: block; + padding: 12px 42px; + border-radius: 12px; + font-size: 1.25rem; + background: grey; + color: white; + transform: translateY(-2px); +} + .orderbutton:hover .front { transform: translateY(-6px); transition: @@ -134,6 +145,23 @@ article { cubic-bezier(.3, .7, .4, 1.5); } + .orderbutton:active .front { transform: translateY(-2px); } + +.orderbutton:active .disabled { + transform: translateY(-2px); +} + +.orderbutton:hover .disabled { + transform: translateY(-2px); +} + + + + +.cancel .front { + font-size: 1rem; + padding: 6px 15px; +} diff --git a/client/style/shoppingbasket.css b/client/style/shoppingbasket.css index 44990ad..871a380 100644 --- a/client/style/shoppingbasket.css +++ b/client/style/shoppingbasket.css @@ -1,3 +1,3 @@ article { height: auto; -} \ No newline at end of file +} diff --git a/main.js b/main.js index 6b942d7..4cc62ba 100644 --- a/main.js +++ b/main.js @@ -105,6 +105,8 @@ const attractions = [ }, ] +var ordersOnServer = []; + /** * A route is like a method call. It has a name, some parameters and some return value. * @@ -126,6 +128,23 @@ app.get("/api/attractions", function (request, response) { app.post("/api/placeorder", function (request, response) { console.log("Api call received for /placeorder"); + // console.log("hello"); + + var orders = request.body; + + for (let i = 0; i < orders.length; i++) { + for (let j = 0; j < attractions.length; j++) { + if (orders[i].name === attractions[j].name) { + attractions[j].available = attractions[j].available - orders[i].numberOfAdults - orders[i].numberOfKids; + } + } + } + + ordersOnServer = ordersOnServer.concat(orders); + + + console.log("orders serverside: "); + console.log(ordersOnServer); /** * Send the status code 200 back to the clients browser. -- cgit v1.2.3