diff options
| author | Leo Goetz <dev@leogtz.de> | 2026-02-02 11:47:42 +0100 |
|---|---|---|
| committer | Leo Goetz <dev@leogtz.de> | 2026-02-02 11:47:42 +0100 |
| commit | bc09ac8989d5d7cc5e89bca7036b6010815dbee9 (patch) | |
| tree | d5cc8a1f4e99910870589539e5fe86809795e314 | |
| parent | d5a420a8135537c9fc36f9dd81ec7c9fc0500e66 (diff) | |
| -rw-r--r-- | app.js | 15 | ||||
| -rw-r--r-- | components/CartItem.js | 27 | ||||
| -rw-r--r-- | components/DetailsPage.js | 46 | ||||
| -rw-r--r-- | components/MenuPage.js | 54 | ||||
| -rw-r--r-- | components/OrderPage.js | 100 | ||||
| -rw-r--r-- | components/ProductItem.js | 30 | ||||
| -rw-r--r-- | services/Menu.js | 16 | ||||
| -rw-r--r-- | services/Order.js | 20 | ||||
| -rw-r--r-- | services/Router.js | 11 | ||||
| -rw-r--r-- | services/Store.js | 17 |
10 files changed, 326 insertions, 10 deletions
@@ -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; |
