summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app.js15
-rw-r--r--components/CartItem.js27
-rw-r--r--components/DetailsPage.js46
-rw-r--r--components/MenuPage.js54
-rw-r--r--components/OrderPage.js100
-rw-r--r--components/ProductItem.js30
-rw-r--r--services/Menu.js16
-rw-r--r--services/Order.js20
-rw-r--r--services/Router.js11
-rw-r--r--services/Store.js17
10 files changed, 326 insertions, 10 deletions
diff --git a/app.js b/app.js
index 57bddfe..9c8c319 100644
--- a/app.js
+++ b/app.js
@@ -2,6 +2,13 @@ import Store from "./services/Store.js";
import { loadData } from "./services/Menu.js";
import Router from "./services/Router.js";
+// Link Webcomponents
+import { MenuPage } from "./components/MenuPage.js";
+import { DetailsPage } from "./components/DetailsPage.js";
+import { OrderPage } from "./components/OrderPage.js";
+import ProductItem from "./components/ProductItem.js";
+import CartItem from "./components/CartItem.js";
+
window.app = {
store: Store,
router: Router,
@@ -11,3 +18,11 @@ window.addEventListener("DOMContentLoaded", () => {
loadData();
app.router.init();
});
+
+window.addEventListener("appcartchange", (event) => {
+ const badge = document.getElementById("badge");
+ const qty = app.store.cart.reduce((acc, item) => acc + item.quantity, 0);
+
+ badge.textContent = qty;
+ badge.hidden = qty == 0;
+});
diff --git a/components/CartItem.js b/components/CartItem.js
new file mode 100644
index 0000000..dd3fcef
--- /dev/null
+++ b/components/CartItem.js
@@ -0,0 +1,27 @@
+import { removeFromCart } from "../services/Order.js";
+
+export default class CartItem extends HTMLElement {
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ const item = JSON.parse(this.dataset.item);
+ this.innerHTML = ""; // Clear the element
+
+ const template = document.getElementById("cart-item-template");
+ const content = template.content.cloneNode(true);
+
+ this.appendChild(content);
+
+ this.querySelector(".qty").textContent = `${item.quantity}x`;
+ this.querySelector(".name").textContent = item.product.name;
+ this.querySelector(".price").textContent =
+ `$${item.product.price.toFixed(2)}`;
+ this.querySelector("a.delete-button").addEventListener("click", (event) => {
+ removeFromCart(item.product.id);
+ });
+ }
+}
+
+customElements.define("cart-item", CartItem);
diff --git a/components/DetailsPage.js b/components/DetailsPage.js
new file mode 100644
index 0000000..4f8eb87
--- /dev/null
+++ b/components/DetailsPage.js
@@ -0,0 +1,46 @@
+import { getProductById } from "../services/Menu.js";
+import { addToCart } from "../services/Order.js";
+
+export class DetailsPage extends HTMLElement {
+ constructor() {
+ super();
+
+ this.root = this.attachShadow({ mode: "open" });
+
+ const template = document.getElementById("details-page-template");
+ const content = template.content.cloneNode(true);
+ const styles = document.createElement("style");
+ this.root.appendChild(content);
+ this.root.appendChild(styles);
+
+ async function loadCSS() {
+ const request = await fetch("/components/DetailsPage.css");
+ styles.textContent = await request.text();
+ }
+ loadCSS();
+ }
+
+ async renderData() {
+ if (this.dataset.productId) {
+ this.product = await getProductById(this.dataset.productId);
+ this.root.querySelector("h2").textContent = this.product.name;
+ this.root.querySelector("img").src = `/data/images/${this.product.image}`;
+ this.root.querySelector(".description").textContent =
+ this.product.description;
+ this.root.querySelector(".price").textContent =
+ `$ ${this.product.price.toFixed(2)} ea`;
+ this.root.querySelector("button").addEventListener("click", () => {
+ addToCart(this.product.id);
+ app.router.go("/order");
+ });
+ } else {
+ alert("Invalid Product ID");
+ }
+ }
+
+ connectedCallback() {
+ this.renderData();
+ }
+}
+
+customElements.define("details-page", DetailsPage);
diff --git a/components/MenuPage.js b/components/MenuPage.js
new file mode 100644
index 0000000..cd593fc
--- /dev/null
+++ b/components/MenuPage.js
@@ -0,0 +1,54 @@
+export class MenuPage extends HTMLElement {
+ constructor() {
+ super();
+
+ this.root = this.attachShadow({ mode: "open" });
+
+ const styles = document.createElement("style");
+ this.root.appendChild(styles);
+
+ async function loadCSS() {
+ const request = await fetch("/components/MenuPage.css");
+ const css = await request.text();
+ styles.textContent = css;
+ }
+
+ loadCSS();
+ }
+
+ // when the component is attached to the DOM
+ connectedCallback() {
+ const template = document.getElementById("menu-page-template");
+ const content = template.content.cloneNode(true);
+ this.root.appendChild(content);
+
+ window.addEventListener("appmenuchange", () => {
+ this.render();
+ });
+ this.render();
+ }
+
+ render() {
+ if (app.store.menu) {
+ this.root.querySelector("#menu").innerHTML = "";
+ for (let category of app.store.menu) {
+ const liCategory = document.createElement("li");
+ liCategory.innerHTML = `
+ <h3>${category.name}</h3>
+ <ul class='category'></ul>
+ `;
+ this.root.querySelector("#menu").appendChild(liCategory);
+
+ category.products.forEach((product) => {
+ const item = document.createElement("product-item");
+ item.dataset.product = JSON.stringify(product);
+ liCategory.querySelector("ul").appendChild(item);
+ });
+ }
+ } else {
+ this.root.querySelector("#menu").innerHTML = "Loading...";
+ }
+ }
+}
+
+customElements.define("menu-page", MenuPage);
diff --git a/components/OrderPage.js b/components/OrderPage.js
new file mode 100644
index 0000000..adb6427
--- /dev/null
+++ b/components/OrderPage.js
@@ -0,0 +1,100 @@
+export class OrderPage extends HTMLElement {
+ // the hash defines a private property in js
+ #user = {
+ name: "",
+ phone: "",
+ email: "",
+ };
+
+ constructor() {
+ super();
+
+ this.root = this.attachShadow({ mode: "open" });
+ const styles = document.createElement("style");
+ this.root.appendChild(styles);
+ const section = document.createElement("section");
+ this.root.appendChild(section);
+
+ async function loadCSS() {
+ const request = await fetch("/components/OrderPage.css");
+ styles.textContent = await request.text();
+ }
+ loadCSS();
+ }
+
+ connectedCallback() {
+ window.addEventListener("appcartchange", () => {
+ this.render();
+ });
+ this.render();
+ }
+
+ render() {
+ let section = this.root.querySelector("section");
+ if (app.store.cart.length == 0) {
+ section.innerHTML = `
+ <p class="empty">Your order is empty</p>
+ `;
+ } else {
+ let html = `
+ <h2>Your Order</h2>
+ <ul>
+ </ul>
+ `;
+ section.innerHTML = html;
+
+ const template = document.getElementById("order-form-template");
+ const content = template.content.cloneNode(true);
+ section.appendChild(content);
+
+ let total = 0;
+ for (let prodInCart of app.store.cart) {
+ const item = document.createElement("cart-item");
+ item.dataset.item = JSON.stringify(prodInCart);
+ this.root.querySelector("ul").appendChild(item);
+
+ total += prodInCart.quantity * prodInCart.product.price;
+ }
+ this.root.querySelector("ul").innerHTML += `
+ <li>
+ <p class='total'>Total</p>
+ <p class='price-total'>$${total.toFixed(2)}</p>
+ </li>
+ `;
+ }
+
+ // use the shadow dom to select the form otherwise it doesnt work
+ this.setFormBindings(this.root.querySelector("form"));
+ }
+
+ setFormBindings(form) {
+ // use the submit event so the enter key and the button works
+ form.addEventListener("submit", (event) => {
+ event.preventDefault();
+ alert(`Thanks for your order ${this.#user.name}.`);
+ this.#user.name = "";
+ this.#user.email = "";
+ this.#user.phone = "";
+ // TODO: Send the data to the Server
+ });
+
+ // set double data binding
+ this.#user = new Proxy(this.#user, {
+ set(target, property, value) {
+ target[property] = value;
+ form.elements[property].value = value;
+ // needed to indicate that the modification was successful
+ return true;
+ },
+ });
+ Array.from(form.elements).forEach((element) => {
+ // the change event does only get triggered when the user changes the
+ // element via the ui
+ element.addEventListener("change", (event) => {
+ this.#user[element.name] = element.value;
+ });
+ });
+ }
+}
+
+customElements.define("order-page", OrderPage);
diff --git a/components/ProductItem.js b/components/ProductItem.js
new file mode 100644
index 0000000..ac9253b
--- /dev/null
+++ b/components/ProductItem.js
@@ -0,0 +1,30 @@
+import { addToCart } from "../services/Order.js";
+
+export default class ProductItem extends HTMLElement {
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ const template = document.getElementById("product-item-template");
+ const content = template.content.cloneNode(true);
+
+ this.appendChild(content);
+
+ const product = JSON.parse(this.dataset.product);
+ this.querySelector("h4").textContent = product.name;
+ this.querySelector("p.price").textContent = `$${product.price.toFixed(2)}`;
+ this.querySelector("img").src = `data/images/${product.image}`;
+ this.querySelector("a").addEventListener("click", (event) => {
+ console.log(event.target.tagName);
+ if (event.target.tagName.toLowerCase() == "button") {
+ addToCart(product.id);
+ } else {
+ app.router.go(`/product-${product.id}`);
+ }
+ event.preventDefault();
+ });
+ }
+}
+
+customElements.define("product-item", ProductItem);
diff --git a/services/Menu.js b/services/Menu.js
index 88e3f35..b8e0ebd 100644
--- a/services/Menu.js
+++ b/services/Menu.js
@@ -3,3 +3,19 @@ import API from "./API.js";
export async function loadData() {
app.store.menu = await API.fetchMenu();
}
+
+export async function getProductById(id) {
+ if (app.store.menu == null) {
+ await loadData();
+ }
+
+ for (let c of app.store.menu) {
+ for (let p of c.products) {
+ if (p.id == id) {
+ return p;
+ }
+ }
+ }
+
+ return null;
+}
diff --git a/services/Order.js b/services/Order.js
new file mode 100644
index 0000000..1c6e131
--- /dev/null
+++ b/services/Order.js
@@ -0,0 +1,20 @@
+import { getProductById } from "./Menu.js";
+
+export async function addToCart(id) {
+ const product = await getProductById(id);
+ const results = app.store.cart.filter(
+ (prodInCart) => prodInCart.product.id == id,
+ );
+
+ if (results.length == 1) {
+ app.store.cart = app.store.cart.map((p) =>
+ p.product.id == id ? { ...p, quantity: p.quantity + 1 } : p,
+ );
+ } else {
+ app.store.cart = [...app.store.cart, { product, quantity: 1 }];
+ }
+}
+
+export function removeFromCart(id) {
+ app.store.cart = app.store.cart.filter((p) => p.product.id != id);
+}
diff --git a/services/Router.js b/services/Router.js
index 13ef58e..4742e24 100644
--- a/services/Router.js
+++ b/services/Router.js
@@ -27,21 +27,18 @@ const Router = {
switch (route) {
case "/":
- pageElement = document.createElement("h1");
- pageElement.textContent = "Menu";
+ pageElement = document.createElement("menu-page");
break;
case "/order":
- pageElement = document.createElement("h1");
- pageElement.textContent = "Your Order";
+ pageElement = document.createElement("order-page");
break;
default:
if (route.startsWith("/product-")) {
- pageElement = document.createElement("h1");
- pageElement.textContent = "Your Order";
+ pageElement = document.createElement("details-page");
const paramId = route.substring(route.lastIndexOf("-") + 1);
// Dataset is great for storing costum data because it doesnt get
// parsed by the browser
- pageElement.dataset.id = paramId;
+ pageElement.dataset.productId = paramId;
}
}
diff --git a/services/Store.js b/services/Store.js
index 979a5a6..d16f81b 100644
--- a/services/Store.js
+++ b/services/Store.js
@@ -1,8 +1,19 @@
-import API from "./API.js";
-
const Store = {
menu: null,
cart: [],
};
-export default Store;
+const proxiedStore = new Proxy(Store, {
+ set(target, property, value) {
+ target[property] = value;
+ if (property == "menu") {
+ window.dispatchEvent(new Event("appmenuchange"));
+ }
+ if (property == "cart") {
+ window.dispatchEvent(new Event("appcartchange"));
+ }
+ return true;
+ },
+});
+
+export default proxiedStore;