diff --git a/.eslintrc.json b/.eslintrc.json index dd5dbdc7..17f92484 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -25,7 +25,7 @@ } ], "no-console": 1, - "quotes": ["error", "single", { "allowTemplateLiterals": true }], + "quotes": ["error", "double", { "allowTemplateLiterals": true }], "func-names": 0, "space-unary-ops": 2, "space-in-parens": "error", diff --git a/FelixGuzman.txt b/FelixGuzman.txt new file mode 100644 index 00000000..f3ac7311 --- /dev/null +++ b/FelixGuzman.txt @@ -0,0 +1 @@ +Felix Guzman \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9ff6702c..fc11712d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "outside", - "version": "1.1.0", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "outside", - "version": "1.1.0", + "version": "2.0.0", "license": "ISC", "devDependencies": { "eslint": "^8.26.0", diff --git a/package.json b/package.json index 829e444f..bc53e011 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "build": "vite build --emptyOutDir", "lint": "eslint *.js src/**/*.js", "format": "prettier --ignore-path ./.gitignore --write \"./**/*.{html,json,js,ts,css}\"", - "test": "jest" - + "test": "jest", + "preview": "vite preview" }, "author": "Shane Thompson", "license": "ISC", diff --git a/src/cart/index.html b/src/cart/index.html index 659c2bb4..246e00d0 100644 --- a/src/cart/index.html +++ b/src/cart/index.html @@ -1,9 +1,7 @@ - - @@ -13,76 +11,19 @@ - + - -
- - - -
- - - - - - - - - - - - - - - - - - - - - - - -
- +
+
-
- +

My Cart

- -

My Cart

-
    - - + -->
-
- + +
+ +
- - + - - diff --git a/src/checkout/index.html b/src/checkout/index.html index 87645865..e6ebe787 100644 --- a/src/checkout/index.html +++ b/src/checkout/index.html @@ -1,88 +1,67 @@ - - - - - - - - - Sleep Outside | Checkout - - - - - - - -
- - - - - -
- -
- -
- -

Review & Place your Order

- -
- -
- - - - + + + + Sleep Outside | Cedar Ridge Rimrock 2-person tent + + + + + +
+

Review & Place your Order

+
+
+ Shipping + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+
+ Payment Information + + +
+ + +
+ + +
+
+ +
+ Order Summary + +

+ +

+ +

+ +

+
+
+ +
+
+ - diff --git a/src/css/style.css b/src/css/style.css index dee10f62..815d3150 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -15,20 +15,25 @@ } * { box-sizing: border-box; + max-width: 100%; } body { margin: 0; font-family: var(--font-body); font-size: var(--font-base); color: var(--dark-grey); + max-width: 100%; } img { max-width: 100%; } header { + padding: 0.5em; display: flex; + align-items: center; justify-content: space-between; - padding: 0 10px; + position: relative; + max-width: 100%; } .logo { line-height: 60px; @@ -50,6 +55,10 @@ header { .hero { position: relative; } +.hero img { + width: 100%; + height: auto; +} .hero .logo { position: absolute; left: calc(50% - 60px); @@ -104,55 +113,164 @@ button { } .cart svg { - width: 25px; + width: 2rem; + position: relative; + transition: all 0.3s ease-in-out; } .cart:hover svg { fill: gray; + transform: scale(1.1); + transition: all 0.3s ease-in-out; +} + +.numberCartItems { + color: var(--dark-grey); + position: absolute; + right: 1.8rem; + top: 2.8rem; + font-size: 0.8em; + font-weight: bold; + background-color: var(--primary-color); + border-radius: 50%; + width: 1.5em; + height: 1.5em; + line-height: 1.5em; + text-align: center; } +span.rise-shake { + color: white; + padding: 1.5rem; + font-size: 1rem; + display: inline-block; +} + +/* span.rise-shake { + animation: jump-shaking 0.83s infinite; +} */ +@keyframes jump-shaking { + 0% { + transform: translateX(0); + } + 25% { + transform: translateY(-9px); + } + 35% { + transform: translateY(-9px) rotate(17deg); + } + 55% { + transform: translateY(-9px) rotate(-17deg); + } + 65% { + transform: translateY(-9px) rotate(17deg); + } + 75% { + transform: translateY(-9px) rotate(-17deg); + } + 100% { + transform: translateY(0) rotate(0); + } +} /* End cart icon styles */ .mission { - padding: 0 0.5em; - /* margin: 0.5em 0; */ + font-size: 1.2em; + padding: 1.5rem; line-height: 1.3; - max-width: 600px; - margin: auto; +} +.mission p { + text-indent: 1rem; } .products { + grid-template-columns: auto; padding: 0.5em; + display: flex; + flex-direction: column; + text-align: center; + max-width: 100%; } .product-list { + display: grid; + grid-template-columns: auto; + grid-gap: 1em; + padding: 0.5em; + max-width: 100%; + list-style: none; +} +.product-list li { display: flex; - flex-flow: row wrap; - justify-content: space-between; - list-style-type: none; - padding: 0; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0.5em; + border: 1px solid var(--light-grey); + border-radius: 1rem; + box-shadow: 1px 1px 4px var(--light-grey); + max-width: 100%; + transition: all 0.3s ease-in-out; +} +.product-list li:hover { + box-shadow: 1px 1px 4px var(--primary-color); + transform: scale(1.01); + transition: all 0.3s ease-in-out; } .product-list a { text-decoration: none; color: var(--dark-grey); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +.product-list a img { + transition: all 0.3s ease-in-out; +} +.product-list a img:hover { + transform: scale(1.09); + transition: all 0.3s ease-in-out; } /* Styles for product lists */ .product-card { - flex: 1 1 45%; - margin: 0.25em; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; padding: 0.5em; border: 1px solid var(--light-grey); - max-width: 250px; + border-radius: 5px; + box-shadow: 1px 1px 4px var(--light-grey); + max-width: 100%; } .product-card h2, .product-card h3, .product-card p { - margin: 0.5em 0; + text-align: center; +} + +.product-card__price { + font-size: 20px; + font-weight: bold; + text-align: center; +} + +.product-card__retail_price { + text-decoration: line-through; + color: red; + font-size: 0.8em; + margin: 0; + margin-bottom: 0.5rem; + text-align: center; } .card__brand { font-size: var(--small-font); + text-indent: 1rem; } .card__name { font-size: 1em; + padding-left: 0.5rem; + text-indent: 1rem; } /* End product list card */ @@ -171,10 +289,9 @@ button { display: grid; grid-template-columns: 25% auto 15%; font-size: var(--small-font); - /* max-height: 120px; */ align-items: center; + max-width: 100%; } - .cart-card__image { grid-row: 1/3; grid-column: 1; @@ -195,28 +312,304 @@ button { .cart-card__quantity { grid-row: 1; grid-column: 3; + text-align: center; } .cart-card__price { grid-row: 2; grid-column: 3; + font-weight: bold; + text-align: center; +} + +.cart-total { + display: none; + font-size: 1.3rem; + display: flex; + text-align: center; + margin: 0 3rem 1rem 0; + font-weight: bold; +} + +/* NEWSLETTER SUBSCRIPTION */ +#newsletter-signup { + padding: 1rem 1rem 0.1rem 1rem; + margin: 1rem; + border: 1px solid #ccc; + box-shadow: 1px 1px 1px 1px #ccc; + display: flex; + flex-direction: column; + max-width: 15rem; + min-height: 20rem; + justify-content: space-around; + border-radius: 2rem; + transition: all ease-in-out 0.3s; +} +#newsletter-signup:hover { + box-shadow: 1px 1px 1px 1px #525b0f; + transition: all ease-in-out 0.3s; +} +#newsletter-signup h2 { + text-align: center; + font-size: 1.5rem; + font-weight: bold; + margin: 0.5rem; + margin-bottom: 1rem; +} +.news-letter { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 0 0 2.5rem 0; +} +h2 { + margin-bottom: 20px; } -@media screen and (min-width: 500px) { - body { - max-width: 1080px; - margin: auto; +form { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 20px; +} + +input { + width: 100%; + padding: 10px; + margin-bottom: 20px; + border: 1px solid #ccc; + border-radius: 1rem; + font-size: 16px; +} +input#name, +input#email { + text-align: center; +} + +#response { + margin-top: 20px; + font-size: 18px; + font-weight: bold; +} + +.modal { + display: block; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: white; + padding: 20px; + box-shadow: 1px 12px 58px 1200px rgb(0 0 0 / 50%); + text-align: center; +} + +.modal img { + padding: 1em 0; +} +.close-pop-up { + display: none; +} + +/* ========== modified by ZB cart Buttons============ */ +.quantity-buttons { + display: flex; + align-items: center; + margin: 0; + margin-bottom: 0.5rem; + padding: 0; + grid-column: 2 / 3; + display: flex; + justify-content: center; + align-items: center; +} +.quantity-buttons button { + margin: 0; + padding: 1rem 0.5rem; + font-size: 1rem; +} +.quantity-buttons button:hover, +#sortName:hover, +#sortPrice:hover, +#addToCart:hover { + background-color: #525b0fc9; + transition: all ease-in-out 0.3s; +} +.increase-button { + border-radius: 0 1rem 1rem 0; +} +.decrease-button { + border-radius: 1rem 0 0 1rem; +} +.delete-button { + font-size: 1rem; +} +.discount { + color: red; + font-size: 0.8em; + margin: 0; + margin-bottom: 0.5rem; + grid-column: 2 / 3; +} +.saved { + color: green; + font-size: 1em; + margin: 0; + margin-bottom: 0.5rem; + grid-column: 3 / 4; + text-align: center; +} +.product-card__price { + font-size: 20px; + font-weight: bold; + margin: 0; +} +.show-pop-up { + font-size: 1rem; + border-radius: 1rem; +} +.show-pop-up:hover { + background-color: #525b0fc9; + transition: all ease-in-out 0.3s; +} +#submit-button { + font-size: 1rem; + border-radius: 1rem; + padding: 1rem 2rem; + font-size: 1rem; + background-color: #525b0f; + color: white; + border: none; + margin-top: 1rem; +} +#submit-button:hover { + background-color: #525b0fc9; + transition: all ease-in-out 0.3s; +} +footer { + font-size: var(--small-font); + padding: 2rem; + text-align: center; + font-size: 1.3rem; +} +.product-grid div { + padding: 1rem; + margin: 1rem; + border: 1px solid #ccc; + box-shadow: 1px 1px 1px 1px #ccc; + display: flex; + flex-direction: column; + min-width: 10rem; + min-height: 20rem; + justify-content: space-around; + border-radius: 2rem; + transition: all ease-in-out 0.3s; +} +.product-grid div:hover { + transform: scale(1.008); + box-shadow: 1px 1px 1px 1px #525b0f; + transition: all ease-in-out 0.3s; +} +.product-grid div h3 { + text-align: center; + font-size: 2rem; +} +.product-grid div a { + display: flex; + align-items: center; + width: 140px; + margin: auto; + transition: all ease-in-out 0.3s; +} +.product-grid div img { + transition: all ease-in-out 0.3s; +} +.product-grid div img:hover { + transform: scale(1.1); + transition: all ease-in-out 0.3s; +} + +.myCartTitle { + text-align: center; +} + + +/* ---------------------------- */ +/* -- SORT by Price and Name -- */ +/* ---------------------------- */ + +.sort { + display: flex; + column-gap: 2rem; +} + + +/* ========================medium view================================ */ +@media (min-width: 35rem) { + .product-grid { + display: grid; + grid-template-columns: auto auto; + grid-gap: 1rem; + } + .product-grid div h3 { + font-size: 2.5rem; } - .mission { - font-size: 1.2em; + .product-grid div a { + width: 180px; } - .cart-card { - font-size: inherit; - grid-template-columns: 150px auto 15%; + div#newsletter-signup { + width: 90%; + max-width: 37rem; + } + .product-list { + display: grid; + grid-template-columns: auto auto; + grid-gap: 1rem; + } + + /* ==============cart section =================*/ + .products { + display: grid; + grid-template-columns: auto; + } } -footer { +/* ========================large view================================ */ +@media (min-width: 63rem) { + .product-grid { + display: grid; + grid-template-columns: auto auto auto auto; + grid-gap: 1rem; + } + .product-grid div h3 { + text-align: center; + font-size: 1.5rem; + } + div#newsletter-signup { + max-width: 30rem; + } + .product-list { + display: grid; + grid-template-columns: auto auto auto; + grid-gap: 1rem; + } + .topProductsTitle { + font-size: 2rem; + } +} + +/*Style for the breadcrumb*/ +.breadcrumb { + padding: 0.5em; font-size: var(--small-font); - padding: 1em; + color: var(--dark-grey); +} +.breadcrumb a { + color: var(--dark-grey); + text-decoration: none; +} +.breadcrumb a:hover { + text-decoration: underline; } diff --git a/src/index.html b/src/index.html index 35ac63ce..df742429 100644 --- a/src/index.html +++ b/src/index.html @@ -7,28 +7,10 @@ -
- - +
+
+
image of a high mountain lake
@@ -51,56 +33,53 @@

-

Top Products

- + + + +
+
+ + Tent Icon from Noun Project: Mustofa Bayu + +

Tents

+
+
+ + Backpack Icon from Noun Project: Mustofa Bayu + +

Backpacks

+
+
+ + Sleeping Bag Icon from Noun Project: Mustofa Bayu + +

Sleeping Bags

+
+
+ + Hammock Icon from Noun Project: Paul Richard + +

Hammocks

+
+
+ + +
-
©NOT a real business
+
+ diff --git a/src/js/Alert.js b/src/js/Alert.js new file mode 100644 index 00000000..e69de29b diff --git a/src/js/Breadcrumbs.mjs b/src/js/Breadcrumbs.mjs new file mode 100644 index 00000000..e69de29b diff --git a/src/js/CheckoutProcess.mjs b/src/js/CheckoutProcess.mjs new file mode 100644 index 00000000..b9cc95d3 --- /dev/null +++ b/src/js/CheckoutProcess.mjs @@ -0,0 +1,86 @@ +import { sumTotal } from "./ShoppingCart.mjs"; +import { getLocalStorage, numberItems, calculateShippingCost } from "./utils.mjs"; +import ExternalServices from "./ExternalServices.mjs"; + +// Takes a form element and returns an object where the key is the "name" of the form input. +const services = new ExternalServices(); +function formDataToJSON(formElement) { + const formData = new FormData(formElement), + convertedJSON = {}; + + formData.forEach(function (value, key) { + convertedJSON[key] = value; + }); + + return convertedJSON; +} + +// Takes the items currently stored in the cart (localstorage) and returns them in a simplified form. +function packageItems(items) { + // Convert the list of products from localStorage to the simpler form required for the checkout process. Array.map would be perfect for this. + const simplifiedItems = items.map((item) => { + console.log(item); + return { + id: item.Id, + price: item.FinalPrice, + name: item.Name, + quantity: item.quantity, + }; + }); + return simplifiedItems; +} + +export default class CheckoutProcess { + constructor(key, outputSelector) { + this.key = key; + this.outputSelector = outputSelector; + this.list = []; + this.itemTotal = 0; + this.shipping = 0; + this.tax = 0; + this.orderTotal = 0; + } + init() { + this.list = getLocalStorage(this.key) || []; + this.calculateItemSummary(); + } + calculateItemSummary() { + // calculate and display the total amount of the items in the cart, and the number of items. + // console.log(this.list) + this.itemTotal = `${sumTotal(this.list).toFixed(2)}` + document.querySelector("#cartTotal").innerHTML += `${this.itemTotal}` + numberItems(this.key, "#num-items") + // console.log(numberItems(this.key)) + } + calculateOrdertotal() { + // calculate the shipping and tax amounts. Then use them to along with the cart total to figure out the order total + this.tax = this.itemTotal * 0.06 + this.shipping = calculateShippingCost(numberItems(this.key)) + this.orderTotal = +this.itemTotal + +this.shipping + +this.tax; + // display the totals. + this.displayOrderTotals(); + } + displayOrderTotals() { + // once the totals are all calculated display them in the order summary page + document.getElementById("shipping").innerHTML += `${this.shipping }` + document.getElementById("tax").innerHTML += `${this.tax}` + document.getElementById("orderTotal").innerHTML += `${this.orderTotal.toFixed(2)}` + } + async checkout() { + const formElement = document.forms["checkout"]; + const json = formDataToJSON(formElement); + // Add totals, and item details + json.orderDate = new Date(); + json.orderTotal = this.orderTotal; + json.tax = this.tax; + json.shipping = this.shipping; + json.items = packageItems(this.list); + console.log("json", json); + try { + const res = await services.checkout(json); + console.log(res); + } catch (err) { + console.log(err); + } + } +} diff --git a/src/js/ExternalServices.mjs b/src/js/ExternalServices.mjs new file mode 100644 index 00000000..834e2204 --- /dev/null +++ b/src/js/ExternalServices.mjs @@ -0,0 +1,58 @@ +//const baseURL = "http://server-nodejs.cit.byui.edu:3000/"; +const baseURL = "https://wdd330-backend.onrender.com/"; + +function convertToJson(res) { + if (res.ok) { + return res.json(); + } else { + throw new Error("Bad Response"); + } +} + +export default class ExternalServices { + constructor(category) { + // // this.category = category; + // // this.path = `../json/${this.category}.json`; + } + + async getData(category) { + + // http://server-nodejs.cit.byui.edu:3000/products/search/tents + + const response = await fetch(baseURL + `products/search/${category}`); + const data = await convertToJson(response); + return data.Result; + + } + // getData() { + // return fetch(this.path) + // .then(convertToJson) + // .then((data) => data); + // } + + async findProductById(id) { + + // http://server-nodejs.cit.byui.edu:3000/product/989CH + + const response = await fetch(baseURL + `product/${id}`); + const data = await convertToJson(response); + return data.Result; + + // const products = await this.getData(); + // return products.find((item) => item.Id === id); + } + + async checkout(payload) { + const options = { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) + } + // const response = await fetch(baseURL + "checkout/", options); + // const data = await convertToJson(response); + // return data.Result; + return await fetch(baseURL + "checkout/", options).then(convertToJson); + } +} diff --git a/src/js/NewsLetter.mjs b/src/js/NewsLetter.mjs new file mode 100644 index 00000000..f3d4d870 --- /dev/null +++ b/src/js/NewsLetter.mjs @@ -0,0 +1,23 @@ +export function newsLetterTemplate(){ + document.querySelector(".news-letter").innerHTML = `
+

Subscribe to Our Newsletter

+
+ + + +
+
+
` +} + +export function responseToSubmission(event){ + event.preventDefault(); + const name = document.getElementById("name").value; + const email = document.getElementById("email").value; + if (name && email) { + document.getElementById("response").innerHTML = "Thank you for subscribing, " + name + "!"; + // Add code here to store the name and email on your server or add them to a mailing list + } else { + document.getElementById("response").innerHTML = "Please enter your name and email."; + } +} diff --git a/src/js/ProductData.mjs b/src/js/ProductData.mjs deleted file mode 100644 index 2361ff21..00000000 --- a/src/js/ProductData.mjs +++ /dev/null @@ -1,23 +0,0 @@ -function convertToJson(res) { - if (res.ok) { - return res.json(); - } else { - throw new Error("Bad Response"); - } -} - -export default class ProductData { - constructor(category) { - this.category = category; - this.path = `../json/${this.category}.json`; - } - getData() { - return fetch(this.path) - .then(convertToJson) - .then((data) => data); - } - async findProductById(id) { - const products = await this.getData(); - return products.find((item) => item.Id === id); - } -} diff --git a/src/js/ProductDetails.mjs b/src/js/ProductDetails.mjs new file mode 100644 index 00000000..b6120be3 --- /dev/null +++ b/src/js/ProductDetails.mjs @@ -0,0 +1,81 @@ +import { getLocalStorage, setLocalStorage, numberItems } from "./utils.mjs"; + +function productDetailsTemplate(product) { + let final_price = Number(product.FinalPrice) + let suggested_retail_price = Number(product.SuggestedRetailPrice) + let discount = Math.abs(final_price - suggested_retail_price).toFixed(2) + + return `

${product.Brand.Name}

+

${product.NameWithoutBrand}

+ ${product.NameWithoutBrand} +

Now! $${final_price}

+

Before: $${suggested_retail_price}

+

You're Saving $${discount}

+

${product.Colors[0].ColorName}

+

+ ${product.DescriptionHtmlSimple} +

+
+ +
`; +} + +export default class ProductDetails { + constructor(productId, dataSource) { + this.productId = productId; + this.product = {}; + this.dataSource = dataSource; + } + async init() { + // use our datasource to get the details for the current product. findProductById will return a promise! use await or .then() to process it + this.product = await this.dataSource.findProductById(this.productId); + // once we have the product details we can render out the HTML + this.renderProductDetails("main"); + // once the HTML is rendered we can add a listener to Add to Cart button + // Notice the .bind(this). Our callback will not work if we don't include that line. Review the readings from this week on 'this' to understand why. + document + .getElementById("addToCart") + .addEventListener("click", this.addToCart.bind(this)); + } + addToCart() { + + numberItems("so-cart", ".numberCartItems"); + window.location.reload(); + + document.querySelector(".rise-shake").style.animation = "jump-shaking 0.83s" + + let Data = getLocalStorage("so-cart"); + if (Data) { + let tent = 1; + for (let i = 0; i < Data.length; i++) { + if (Data[i].Id == this.product.Id) { + this.product.quantity = Data[i].quantity++; + tent = 0; + } + } + + if (tent == 1) { + this.product.quantity = 1; + Data.push(this.product); + } + + } else { + Data = []; + this.product.quantity = 1; + Data.push(this.product); + } + setLocalStorage("so-cart", Data); + } + + renderProductDetails(selector) { + const element = document.querySelector(selector); + element.insertAdjacentHTML( + "afterBegin", + productDetailsTemplate(this.product) + ); + } +} diff --git a/src/js/ProductList.mjs b/src/js/ProductList.mjs new file mode 100644 index 00000000..08584b9c --- /dev/null +++ b/src/js/ProductList.mjs @@ -0,0 +1,77 @@ +import { renderListWithTemplate } from "./utils.mjs"; + +export function productCardTemplate(product) { + let final_price = Number(product.FinalPrice) + let suggested_retail_price = Number(product.SuggestedRetailPrice) + let discount = Math.abs(final_price - suggested_retail_price).toFixed(2) + let { Id, Images, Brand, Name, FinalPrice} = product + return `
  • + + + Image of ${Name} +

    ${Brand.Name}

    +

    ${Name}

    +

    Now! $${FinalPrice}

    +

    Save: $${discount} $${suggested_retail_price}

    + +

  • ` +} + +export default class ProductListing { + constructor(category, dataSource, listElement) { + // We passed in this information to make our class as reusable as possible. + // Being able to define these things when we use the class will make it very flexible + this.category = category; + this.dataSource = dataSource; + this.listElement = listElement; + } + async init() { + // our dataSource will return a Promise...so we can use await to resolve it. + + // OLD ----- const list = await this.dataSource.getData(); + const list = await this.dataSource.getData(this.category); + + // Title + document.querySelector("#nameCategory").textContent = this.category.charAt(0).toUpperCase() + this.category.slice(1); + + + document.getElementById("sortName") + .addEventListener("click", () => { + document.querySelector(".product-list").textContent = ""; + list.sort((nameA, nameB) => { + if(nameA.Name < nameB.Name) { return -1; } + if(nameA.Name > nameB.Name) { return 1; } + return 0; + }) + this.renderList(list) + }); + + document.getElementById("sortPrice") + .addEventListener("click", () => { + document.querySelector(".product-list").textContent = ""; + list.sort((priceA, priceB) => priceA.FinalPrice - priceB.FinalPrice + ) + this.renderList(list) + }); + + + // render the list + this.renderList(list) + } + + // render after doing the first stretch + renderList(list) { + renderListWithTemplate(productCardTemplate, this.listElement, list); + } + } diff --git a/src/js/QuickLook.mjs b/src/js/QuickLook.mjs new file mode 100644 index 00000000..6db040a6 --- /dev/null +++ b/src/js/QuickLook.mjs @@ -0,0 +1,53 @@ +export async function logProductCard() { + return new Promise((resolve) => { + let productCard = document.querySelector(".product-card"); + if (productCard) { + const showPopUpButton = document.querySelectorAll(".show-pop-up"); + const closePopUpButton = document.querySelectorAll(".close-pop-up"); + + + + /* + 1. get the id of the button + 2. change the styling only of the product that has the same id + */ + showPopUpButton.forEach(function (item) { + document.getElementById(`${item.id}`).addEventListener("click", function () { + document.getElementById(`${item.id}`).style.display = "none"; + document.getElementById(`close-${item.id}`).style.display = "block"; + const product = document.getElementById(`product-${item.id}`); + product.classList.remove("product-card"); + product.classList.add("modal"); + document.addEventListener("click", function (event) { + const popUp = document.getElementById(`product-${item.id}`); + if (!popUp.contains(event.target)) { + document.getElementById(`${item.id}`).style.display = "block"; + document.getElementById(`close-${item.id}`).style.display = "none"; + product.classList.remove("modal"); + product.classList.add("product-card"); + + } + }) + }); + + }); + + closePopUpButton.forEach(function (item) { + document.getElementById(`${item.id}`).addEventListener("click", function () { + let productId = item.id.slice(6, 11) + document.getElementById(`${productId}`).style.display = "block"; + document.getElementById(`close-${productId}`).style.display = "none"; + const product = document.getElementById(`product-${productId}`); + product.classList.remove("modal"); + product.classList.add("product-card"); + + }); + + }); + + resolve(productCard); + } else { + setTimeout(() => resolve(logProductCard()), 100); + } + }); +} diff --git a/src/js/ShoppingCart.mjs b/src/js/ShoppingCart.mjs new file mode 100644 index 00000000..3d4d9f7e --- /dev/null +++ b/src/js/ShoppingCart.mjs @@ -0,0 +1,71 @@ +import { getLocalStorage } from "./utils.mjs"; +function cartItemTemplate(item) { + let final_price = Number(item.FinalPrice) + let suggested_retail_price = Number(item.SuggestedRetailPrice) + let discount = Math.abs(final_price - suggested_retail_price).toFixed(2) + let quantity = Number(item.quantity) + let total_discount = (discount * quantity).toFixed(2) + let { Images, Name } = item + let total_price = Number(final_price * quantity).toFixed(2) + + console.log("item", item) + console.log("image",item.Images.PrimarySmall) + + const newItem = `
  • + + Image of ${Name} + + +

    ${item.Name}

    +
    +

    ${item.Colors[0].ColorName}

    +

    Quantity: ${item.quantity}

    +

    Unit Price: $${item.FinalPrice}

    +

    Total: $${total_price}

    +

    Saved: $${total_discount}

    +

  • + `; + return newItem; +} + +export default class ShoppingCart { + constructor(key, parentSelector) { + this.key = key; + this.parentSelector = parentSelector; + } + // renderCartContents() { + // const cartItems = getLocalStorage(this.key); + // const htmlItems = cartItems.map((item) => cartItemTemplate(item)); + // document.querySelector(this.parentSelector).innerHTML = htmlItems.join(""); + // } + renderCartContents() { + const cartItems = getLocalStorage(this.key) || []; + let cartTotal = document.querySelector(".cart-total") + + /* If there's something in the Cart, display the items and the total sum of them. */ + if (cartItems.length != 0) { + const htmlItems = cartItems.map((item) => cartItemTemplate(item)); + document.querySelector(this.parentSelector).innerHTML = htmlItems.join(""); + + cartTotal.style.display = "block"; // Make appear the total paragraph that is hidden by default + cartTotal.innerHTML = `Total: $${sumTotal(cartItems).toFixed(2)}` + } + } +} + +export function sumTotal(cart) { + let total = 0; + cart.forEach(item => total += (item.FinalPrice * item.quantity)); + return total; +} diff --git a/src/js/cart.js b/src/js/cart.js index a2fb3d8e..97dc3bc4 100644 --- a/src/js/cart.js +++ b/src/js/cart.js @@ -1,28 +1,52 @@ -import { getLocalStorage } from "./utils.mjs"; - -function renderCartContents() { - const cartItems = getLocalStorage("so-cart"); - const htmlItems = cartItems.map((item) => cartItemTemplate(item)); - document.querySelector(".product-list").innerHTML = htmlItems.join(""); -} - -function cartItemTemplate(item) { - const newItem = `
  • - - ${item.Name} - - -

    ${item.Name}

    -
    -

    ${item.Colors[0].ColorName}

    -

    qty: 1

    -

    $${item.FinalPrice}

    -
  • `; - - return newItem; -} - -renderCartContents(); +import { loadHeaderFooter, numberItems } from "./utils.mjs"; +import ShoppingCart from "./ShoppingCart.mjs"; + +loadHeaderFooter(); +numberItems("so-cart", ".numberCartItems"); + +const shoppingBag = new ShoppingCart("so-cart", ".product-list"); + +shoppingBag.renderCartContents(); + + + +// function renderCartContents() { +// const cartItems = getLocalStorage("so-cart") || []; +// let cartTotal = document.querySelector(".cart-total") + +// /* If there's something in the Cart, display the items and the total sum of them. */ +// if (cartItems.length != 0) { +// console.log(cartItems) +// const htmlItems = cartItems.map((item) => cartItemTemplate(item)); +// document.querySelector(".product-list").innerHTML = htmlItems.join(""); + +// cartTotal.style.display = "block"; // Make appear the total paragraph that is hidden by default +// cartTotal.innerHTML = `Total: ${sumTotal(cartItems).toFixed(2)}` +// } +// } + +// function cartItemTemplate(item) { +// const newItem = `
  • +// +// ${item.Name} +// +// +//

    ${item.Name}

    +//
    +//

    ${item.Colors[0].ColorName}

    +//

    qty: ${item.quantity}

    +//

    $${item.FinalPrice}

    +//
  • `; +// return newItem; +// } + +// function sumTotal(cart) { +// let total = 0; +// cart.forEach(item => total += (item.FinalPrice * item.quantity)); +// return total; +// } + +// renderCartContents(); diff --git a/src/js/checkout.js b/src/js/checkout.js new file mode 100644 index 00000000..0944986e --- /dev/null +++ b/src/js/checkout.js @@ -0,0 +1,14 @@ +//import { loadHeaderFooter } from "./utils.mjs"; +import CheckoutProcess from "./CheckoutProcess.mjs"; + +// loadHeaderFooter(); + +let myCheckout = new CheckoutProcess("so-cart", ".checkout-summary"); +myCheckout.init() + +document.querySelector("#zip").addEventListener("blur", myCheckout.calculateOrdertotal.bind(myCheckout)); +document.querySelector("#checkoutSubmit").addEventListener("click", (e) => { + e.preventDefault(); + + myCheckout.checkout(); +}); diff --git a/src/js/deleteButtonCart.js b/src/js/deleteButtonCart.js new file mode 100644 index 00000000..25033fe3 --- /dev/null +++ b/src/js/deleteButtonCart.js @@ -0,0 +1,128 @@ +document.addEventListener("DOMContentLoaded", function () { + //select the cart container + const myCartContainer = document.querySelector(".products"); + let check = JSON.parse(localStorage.getItem("so-cart")); + //check if the localstorage is empty + if (check == null) { + const messageEmptyCart = document.createElement("p"); + messageEmptyCart.textContent = "Your cart is empty!"; + messageEmptyCart.style.fontSize = "2rem"; + messageEmptyCart.style.fontWeight = "bold"; + myCartContainer.appendChild(messageEmptyCart); + messageEmptyCart.style.textAlign = "center"; + } + //if the localstorage is not empty + else { + const cards = [...document.querySelectorAll(".cart-card")]; + cards.forEach((card) => { + //create delete button + const deleteButton = document.createElement("button"); + deleteButton.textContent = "Delete"; + deleteButton.classList.add("delete-button"); + + //add event listener to delete button + deleteButton.addEventListener("click", deleteProduct); + //create buttons to increase and decrease quantities + const quantityButtons = document.createElement("div"); + quantityButtons.classList.add("quantity-buttons"); + card.appendChild(quantityButtons); + //create decrease button + const decreaseButton = document.createElement("button"); + decreaseButton.textContent = "-"; + decreaseButton.classList.add("decrease-button"); + quantityButtons.appendChild(decreaseButton); + //add delete button just between quantitties modifiers + quantityButtons.appendChild(deleteButton); + //create increase button + const increaseButton = document.createElement("button"); + increaseButton.textContent = "+"; + increaseButton.classList.add("increase-button"); + quantityButtons.appendChild(increaseButton); + //add event listener to all increase button + increaseButton.addEventListener("click", increaseQuantity); + //add event listener to all decrease button + decreaseButton.addEventListener("click", decreaseQuantity); + }); + } + + function deleteProduct(event) { + //get the parent element of the button that was clicked + const productCard = event.target.closest(".cart-card"); + //get the name of the product + const nameProduct = productCard.querySelector(".card__name").textContent; + //remove the product from the localstorage + let cart = JSON.parse(localStorage.getItem("so-cart")); + cart = cart.filter((item) => item.Name !== nameProduct); + //set the localstorage + localStorage.setItem("so-cart", JSON.stringify(cart)); + //remove the product from the DOM + productCard.remove(); + //if the cart is empty, display a message + if (cart.length === 0) { + const messageEmptyCart = document.createElement("p"); + document.querySelector(".cart-total").style.display = "none;"; + messageEmptyCart.textContent = "Your cart is empty!"; + messageEmptyCart.style.fontSize = "2rem"; + messageEmptyCart.style.fontWeight = "bold"; + myCartContainer.appendChild(messageEmptyCart); + messageEmptyCart.style.textAlign = "center"; + } + window.location.reload(); + } + + function increaseQuantity(event) { + //get the parent element of the button that was clicked + const productCard = event.target.closest(".cart-card"); + //get the name of the product + const nameProduct = productCard.querySelector(".card__name").textContent; + //get the quantity of the product + const quantityProduct = productCard.querySelector(".cart-card__quantity"); + //get the price of the product + const priceProduct = productCard.querySelector(".cart-card__price"); + //get the cart + let cart = JSON.parse(localStorage.getItem("so-cart")); + //find the product in the cart + const product = cart.find((item) => item.Name === nameProduct); + //increase the quantity of the product + product.quantity++; + //update the quantity on the DOM + quantityProduct.textContent = product.quantity; + //update the total price of the product + priceProduct.textContent = (product.quantity * product.Price).toFixed(2); + //set the localstorage + localStorage.setItem("so-cart", JSON.stringify(cart)); + //reload + location.reload(); + } + + function decreaseQuantity(event) { + //get the parent element of the button that was clicked + const productCard = event.target.closest(".cart-card"); + //get the name of the product + const nameProduct = productCard.querySelector(".card__name").textContent; + //get the quantity of the product + const quantityProduct = productCard.querySelector(".cart-card__quantity"); + //get the price of the product + const priceProduct = productCard.querySelector(".cart-card__price"); + //get the cart + let cart = JSON.parse(localStorage.getItem("so-cart")); + //find the product in the cart + const product = cart.find((item) => item.Name === nameProduct); + //decrease the quantity of the product + product.quantity--; + //if the quantity is less than 1 delete product + if (product.quantity < 1) { + deleteProduct(event); + document.querySelector(".cart-total").style.display = "none;"; + return; + } + //update the quantity on the DOM + quantityProduct.textContent = product.quantity; + //update the total price of the product + priceProduct.textContent = (product.quantity * product.Price).toFixed(2); + //set the localstorage + localStorage.setItem("so-cart", JSON.stringify(cart)); + //reload + window.location.reload(); + } +}); diff --git a/src/js/main.js b/src/js/main.js index e69de29b..339dfd80 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -0,0 +1,23 @@ +// import ProductListing from "./ProductList.mjs"; +// import ExternalServices from "./ExternalServices.mjs"; + +import { loadHeaderFooter, numberItems } from "./utils.mjs"; +import { newsLetterTemplate, responseToSubmission } from "./NewsLetter.mjs"; +import { logProductCard } from "./QuickLook.mjs"; + +loadHeaderFooter(); +numberItems("so-cart", ".numberCartItems"); + +// const dataSource = new ExternalServices("tents"); +// const element = document.querySelector(".product-list"); +// const listing = new ProductListing("Tents", dataSource, element); + +/* Function that Renders the newsletter form and say "Thanks when submitted" */ +newsLetterTemplate(); +document + .getElementById("submit-button") + .addEventListener("click", responseToSubmission); + +logProductCard(); + +// listing.init(); diff --git a/src/js/product-listing.js b/src/js/product-listing.js new file mode 100644 index 00000000..67fa133b --- /dev/null +++ b/src/js/product-listing.js @@ -0,0 +1,17 @@ +import ProductListing from "./ProductList.mjs"; +import ExternalServices from "./ExternalServices.mjs"; +import { loadHeaderFooter, numberItems, getParam } from "./utils.mjs"; +import { logProductCard } from "./QuickLook.mjs"; + +loadHeaderFooter(); +numberItems("so-cart", ".numberCartItems"); + +const category = getParam("category"); + +const dataSource = new ExternalServices(); +const element = document.querySelector(".product-list"); +const listing = new ProductListing(category, dataSource, element); + +logProductCard(); + +listing.init(); diff --git a/src/js/product.js b/src/js/product.js index 0b8d0aa6..23b035bb 100644 --- a/src/js/product.js +++ b/src/js/product.js @@ -1,18 +1,49 @@ -import { setLocalStorage } from "./utils.mjs"; -import ProductData from "./ProductData.mjs"; - -const dataSource = new ProductData("tents"); - -function addProductToCart(product) { - setLocalStorage("so-cart", product); -} -// add to cart button event handler -async function addToCartHandler(e) { - const product = await dataSource.findProductById(e.target.dataset.id); - addProductToCart(product); -} - -// add listener to Add to Cart button -document - .getElementById("addToCart") - .addEventListener("click", addToCartHandler); +import { getParam, loadHeaderFooter, numberItems } from "./utils.mjs"; +import ExternalServices from "./ExternalServices.mjs"; +import ProductDetails from "./ProductDetails.mjs"; + +loadHeaderFooter(); +numberItems("so-cart", ".numberCartItems"); + +const productId = getParam("product"); +const dataSource = new ExternalServices("tents"); +const product = new ProductDetails(productId, dataSource); + +product.init(); + +// console.log(dataSource.findProductById(productId)); + +// function addProductToCart(product) { +// let cart = getLocalStorage("so-cart"); +// if (cart) { +// let tent = 1; +// for (let i = 0; i < cart.length; i++) { +// if (cart[i].Id == product.Id) { +// product.quantity = cart[i].quantity++; +// tent = 0; +// } +// } + +// if (tent == 1) { +// product.quantity = 1; +// cart.push(product); +// } +// } else { +// cart = []; +// product.quantity = 1; +// cart.push(product); +// } + +// setLocalStorage("so-cart", cart); +// } + +// // add to cart button event handler +// async function addToCartHandler(e) { +// const product = await dataSource.findProductById(e.target.dataset.id); +// addProductToCart(product); +// } + +// // add listener to Add to Cart button +// document +// .getElementById("addToCart") +// .addEventListener("click", addToCartHandler); diff --git a/src/js/utils.mjs b/src/js/utils.mjs index 1a04d87f..4784bc92 100644 --- a/src/js/utils.mjs +++ b/src/js/utils.mjs @@ -21,3 +21,89 @@ export function setClick(selector, callback) { }); qs(selector).addEventListener("click", callback); } + + +export function getParam(param) { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + const product = urlParams.get(param); + return product; +} + +export function renderListWithTemplate( + templateFn, + parentElement, + list, + position = "afterbegin", + clear = false +) { + const htmlStrings = list.map(templateFn); //data to html + // if clear is true we need to clear out the contents of the parent. + if (clear) { + parentElement.innerHTML = ""; + } + parentElement.insertAdjacentHTML(position, htmlStrings.join("")); +} + +// To load templates. +export function renderWithTemplate( + template, + parentElement, //
    ,