From eddf7cd50b1d38f55f9d0a4a407aa3a8a042553d Mon Sep 17 00:00:00 2001 From: Dennis Heinrich Date: Mon, 7 Apr 2025 17:52:27 +0200 Subject: [PATCH] =?UTF-8?q?Nicht=20verwendete=20Abh=C3=A4ngigkeit=20entfer?= =?UTF-8?q?nt=20und=20Zusammenf=C3=BChrung=20der=20JavaScript-Dateien?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/script/all.js | 661 +++++++++++++++++++++++++++++++- assets/script/calculator.js | 654 ------------------------------- assets/script/service-worker.js | 1 - package.json | 1 - pnpm-lock.yaml | 3 - 5 files changed, 658 insertions(+), 662 deletions(-) delete mode 100644 assets/script/calculator.js diff --git a/assets/script/all.js b/assets/script/all.js index 7b89d9f..bf3fa7d 100644 --- a/assets/script/all.js +++ b/assets/script/all.js @@ -1,10 +1,665 @@ -import './calculator.js' +/** + * This imports the necessary files and registers the service worker. + */ import './../../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js' - if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('./assets/script/service-worker.js') .then(reg => console.log('Service Worker installiert', reg)) .catch(err => console.error('Service Worker fehlgeschlagen', err)); }); -} \ No newline at end of file +} +/** + * Element class provides static methods to get various elements from the DOM. + */ +class Element { + /** + * Get the import/export textarea element + * @returns {HTMLTextAreaElement} + */ + static getImportExportTextarea() { + return document.querySelector('#input-export-import'); + } + + /** + * Get the product list element + * @returns {HTMLElement} + */ + static getProductListElement() { + return document.querySelector('.product-list'); + } + + /** + * Get the settings product list element + * @returns {HTMLElement} + */ + static getSettingListElement() { + return document.querySelector('.settings-product-list'); + } + + /** + * Get the cart element + * @returns {HTMLElement} + */ + static getCartElement() { + return document.querySelector('.cart-items'); + } + + /** + * Get the cart empty alert element + * @returns {HTMLElement} + */ + static getCartEmptyAlertElement() { + return document.querySelector('.alert.cart-empty'); + } + + /** + * Get the create new product button element + * @returns {HTMLButtonElement} + */ + static getCreateNewProductButton() { + return document.querySelector('#create-product'); + } + + /** + * Get the cart button element + * @returns {HTMLButtonElement} + */ + static getCartButton() { + return document.querySelector('button.cart-value'); + } + + /** + * Get the navbar brand element + * @returns {Element} + */ + static getNavbarBrand() { + return document.querySelector('.navbar-brand'); + } + + static getButtonsImportTestdata() { + return document.querySelectorAll('[data-action=import-testdata]'); + } + static getButtonShowTestdata() { + return document.querySelectorAll('[data-action=import-show-testdata]'); + } + static getButtonClearTestdata() { + return document.querySelectorAll('[data-action=import-clear]'); + } + static getGitHubReferenceLink() { + return document.querySelector('[data-github-ref]'); + } +} + +/** + * TemplateElement class provides static methods to get various templates from the DOM. + */ +class TemplateElement { + /** + * Get the product list element template + * @returns {HTMLElement} + */ + static getProductTemplate() { + const element = document.querySelector('[data-template=product]').cloneNode(true); + return this.removeTemplate(element); + } + + /** + * Get the cart line element template + * @returns {HTMLElement} + */ + static getCartLineTemplate() { + const element = document.querySelector('[data-template=product-line-item]').cloneNode(true); + return this.removeTemplate(element); + } + + /** + * Get the settings product element template + * @returns {HTMLElement} + */ + static getSettingsProductTemplate() { + const element = document.querySelector('[data-template=settings-product]').cloneNode(true); + return this.removeTemplate(element); + } + + /** + * Remove the template attribute from the element + * @param element {HTMLElement} + * @returns {HTMLElement} + */ + static removeTemplate(element) { + element.removeAttribute('data-template'); + return element; + } +} + +/** + * Product class represents a product with an id, name, price, and image. + */ +class Product { + /** + * Product constructor + * @param name {string} Name of the product + * @param price {number} Price of the product + * @param image {string} Image of the product (source url or base64) + */ + constructor(name, price, image) { + this.id = Product.generateId(); + this.name = name; + this.price = price; + this.image = image; + } + + /** + * Generate a random id for the product + * @returns {number} + */ + static generateId() { + return Math.floor(Math.random() * 1000000000) * Math.floor(Math.random() * 1000000000); + } + + /** + * Get the product html element by template and set the attributes by the product object + * @returns {HTMLElement} + */ + getProductElement() { + const productElement = TemplateElement.getProductTemplate(); + productElement.setAttribute('data-id', this.id); + productElement.querySelector('[data-attr=name]').textContent = this.name; + productElement.querySelector('[data-attr=price]').textContent = CartManager.getNumberFormatter().format(this.price); + productElement.querySelector('[data-attr=image]').src = this.image; + productElement.addEventListener('click', () => { + cartManager.addProduct(this); + }) + return productElement; + } + + /** + * Get the settings product html element by template and set the attributes by the product object + * @returns {HTMLElement} + */ + getSettingsProductElement() { + const productElement = TemplateElement.getSettingsProductTemplate(); + productElement.setAttribute('data-id', this.id); + productElement.textContent = this.name; + productElement.addEventListener('click', () => { + productManager.removeProduct(this); + productManager.setExportFieldJsonValue(); + productElement.remove(); + }) + return productElement; + } +} + +/** + * CartLine class represents a line in the cart with a product and its quantity. + */ +class CartLine { + /** + * CartLine constructor + * @param product {Product} + * @param quantity {number} + */ + constructor(product, quantity) { + this.product = product; + this.quantity = quantity; + } +} + +/** + * Base64Image class provides static methods to convert an image URL or file to a base64 string. + */ +class Base64Image { + /** + * Convert an image URL to a base64 string + * @param url + * @returns {Promise} + */ + static fromImageUrl(url) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = 'Anonymous'; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + const dataUrl = canvas.toDataURL(); + resolve(dataUrl); + }; + img.onerror = reject; + img.src = url; + }); + } + + /** + * Resize an image from a base64 string to a smaller size + * @param base64 + * @returns {Promise} + */ + static imageResize(base64) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = 'Anonymous'; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = 250; + canvas.height = 250; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + const dataUrl = canvas.toDataURL(); + resolve(dataUrl); + }; + img.onerror = reject; + img.src = base64; + }); + } + + /** + * Convert a file to a base64 string + * @param file + * @returns {Promise} + */ + static fromFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result); + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } +} + +/** + * ProductManager class manages the product list, including loading from JSON, adding, and removing products. + */ +class ProductManager { + constructor() { + this.loadFromJson(localStorage.getItem('products') ?? []) + this.setExportFieldJsonValue(); + this.registerSettingsFormEvents(); + this.registerImportFormEvents(); + this.registerNavbarBrandToProductEvent(); + } + + /** + * Register the event for the navbar brand to switch to the products tab when clicked. + */ + registerNavbarBrandToProductEvent() { + Element.getNavbarBrand().addEventListener('click', () => { + TabManager.switchTab('products'); + }) + } + + /** + * Register events for the import/export textarea. + */ + registerImportFormEvents() { + Element.getButtonsImportTestdata().forEach(button => { + button.addEventListener('click', async () => { + await fetch('assets/example/example.json').then(r => r.json()).then(async json => { + await this.convertAndSaveProductImages(json); + location.reload(); + }) + }); + }) + + Element.getImportExportTextarea().addEventListener('input', async (e) => { + try { + const json = JSON.parse(e.target.value); + await this.convertAndSaveProductImages(json); + location.reload(); + } catch (e) { + console.error(e); + } + }); + } + + /** + * Convert and save product images from JSON string to base64 and save it to local storage. + * @param json + * @returns {Promise} + */ + async convertAndSaveProductImages(json) { + const products = json.map(e => new Product(e.name, e.price, e.image)); + for (const product of products) { + if (!product.image.toString().startsWith('data:image/')) { + product.image = await Base64Image.imageResize(await Base64Image.fromImageUrl(product.image)) + } + } + localStorage.setItem('products', JSON.stringify(products)); + } + + /** + * Set the JSON value of the export field to the current products. + */ + setExportFieldJsonValue() { + Element.getImportExportTextarea().textContent = JSON.stringify(this.products); + } + + /** + * Load products from JSON string and parse it into Product objects, then render the product list. + * @param json + */ + loadFromJson(json) { + let parse = []; + try { + parse = JSON.parse(json); + } catch (e) {} + this.products = parse.map(e => new Product(e.name, e.price, e.image)); + this.renderProductList(); + } + + /** + * Register events for the create new product button. + */ + registerSettingsFormEvents() { + Element.getCreateNewProductButton().addEventListener('click', async (e) => { + e.preventDefault(); + + const name = document.querySelector('#product-name').value; + const price = document.querySelector('#product-price').value; + const image = document.querySelector('#product-image').files[0]; + + // Validate price + if (name.length === 0) { + alert('Name muss ausgefüllt sein'); + return; + } + + // Turn image into base64 or use placeholder image + if (image) { + const imageSrcWithBase64 = Base64Image.fromFile(image); + imageSrcWithBase64.then(async imageSrcWithBase64 => { + productManager.addProduct(new Product(name, price, await Base64Image.imageResize(imageSrcWithBase64))); + ProductManager.resetProductSettingsForm(); + }); + } else { + let image1 = await Base64Image.imageResize(await Base64Image.fromImageUrl(`https://placehold.co/250x250?text=${name}`)); + productManager.addProduct(new Product(name, price, image1)); + ProductManager.resetProductSettingsForm(); + } + }); + } + + /** + * Reset the product settings form by clearing all input fields. + */ + static resetProductSettingsForm() { + document.querySelectorAll('form input:not([type=submit])').forEach(inputField => inputField.value = ''); + } + + /** + * Render the product list by clearing the existing elements and appending new ones. + */ + renderProductList() { + // Clear the product list element before rendering except the template + Element.getProductListElement().querySelectorAll('[data-id]').forEach(e => { + if (e.getAttribute('data-template') === null) { + e.remove(); + } + }); + this.products.forEach(e => { + Element.getProductListElement().appendChild(e.getProductElement()); + Element.getSettingListElement().appendChild(e.getSettingsProductElement()); + }) + } + + /** + * Add a new product to the list and save it to local storage. + * @param product {Product} + */ + addProduct(product) { + this.products.push(product); + localStorage.setItem('products', JSON.stringify(this.products)); + Element.getProductListElement().appendChild(product.getProductElement()); + Element.getSettingListElement().appendChild(product.getSettingsProductElement()); + this.setExportFieldJsonValue(); + } + + /** + * Remove a product from the list and local storage. + * @param product {Product} + */ + removeProduct(product) { + this.products = this.products.filter(e => e.id !== product.id); + const productElement = Element.getProductListElement().querySelector(`[data-id="${product.id}"]`); + if (productElement) { + productElement.remove(); + } + localStorage.setItem('products', JSON.stringify(this.products)); + this.setExportFieldJsonValue(); + } +} + +/** + * CartManager class manages the cart, including adding, removing products, and rendering the cart. + */ +class CartManager { + constructor() { + this.cartLines = []; + this.registerCartResetEvent(); + } + + /** + * Register the cart reset event to clear the cart when the button is clicked. + */ + registerCartResetEvent() { + Element.getCartButton().addEventListener('click', () => { + CartHistoryManager.addToTotal(this.cartLines.reduce((acc, cartLine) => { + return acc + (cartLine.product.price * cartLine.quantity); + }, 0)); + this.cartLines = []; + this.renderCart(); + }); + } + + /** + * Add a product to the cart. If the product already exists, increase the quantity. + * @param product {Product} + */ + addProduct(product) { + const cartLine = this.cartLines.find(e => e.product.id === product.id); + if (cartLine) { + cartLine.quantity++; + } else { + this.cartLines.push(new CartLine(product, 1)); + } + this.renderCart(); + } + + /** + * Remove a product from the cart. If the quantity is 0, remove the product from the cart. + * @param product {Product} + */ + removeProduct(product) { + const cartLine = this.cartLines.find(e => e.product.id === product.id); + if (cartLine) { + cartLine.quantity--; + if (cartLine.quantity <= 0) { + this.cartLines = this.cartLines.filter(e => e.product.id !== product.id); + } + } + this.renderCart(); + } + + /** + * Render the cart by clearing the existing elements and appending new ones. + */ + renderCart() { + // Clear the cart element before rendering except the template + Element.getCartElement().querySelectorAll('[data-id]').forEach(e => { + if (e.getAttribute('data-template') === null) { + e.remove(); + } + }); + + if(this.cartLines.length === 0) { + Element.getCartEmptyAlertElement().classList.remove('d-none'); + } else { + Element.getCartEmptyAlertElement().classList.add('d-none'); + } + this.calculateCartValue(); + + // Render each cart line + this.cartLines.forEach(cartLine => { + const cartLineElement = this.getCartLineElement(cartLine); + Element.getCartElement().appendChild(cartLineElement); + }); + } + + /** + * Get the cart line html element and set the attributes by the product object + * @param cartLine {CartLine} + * @returns {*} + */ + getCartLineElement(cartLine) { + const cartLineElement = TemplateElement.getCartLineTemplate(); + cartLineElement.setAttribute('data-id', cartLine.product.id); + cartLineElement.querySelector('[data-attr=name]').textContent = cartLine.product.name; + cartLineElement.querySelector('[data-attr=value]').textContent = CartManager.getNumberFormatter().format(cartLine.product.price); + cartLineElement.querySelector('[data-attr=quantity]').textContent = cartLine.quantity; + cartLineElement.addEventListener('click', () => { + this.removeProduct(cartLine.product); + }) + return cartLineElement; + } + + /** + * Calculate the total value of the cart by multiplying the product price with the quantity and setting it to the button. + */ + calculateCartValue() { + let cartValue = this.cartLines.reduce((acc, cartLine) => { + return acc + (cartLine.product.price * cartLine.quantity); + }, 0); + Element.getCartButton().querySelector('[data-total-value]').textContent = CartManager.getNumberFormatter().format(cartValue); + } + + /** + * Get the number formatter for the currency. + * @returns {Intl.NumberFormat} + */ + static getNumberFormatter() { + return new Intl.NumberFormat('de-DE', { + currency: 'EUR', + minimumFractionDigits: 2 + }); + } +} + +/** + * TabManager class manages the tabs in the settings page. + */ +class TabManager { + /** + * @type {boolean} If the settings tab is active or not + */ + static isSettingsTabActive = false; + + constructor() { + TabManager.isSettingsTabActive = false; + document.querySelector('[data-toggle-tab]').addEventListener('click', (e) => { + TabManager.toggleTab(); + }); + } + + /** + * Toggle the active tab between settings and products. + */ + static toggleTab() { + if(!TabManager.isSettingsTabActive) { + TabManager.isSettingsTabActive = true; + this.switchTab('settings'); + } else { + this.switchTab('products'); + TabManager.isSettingsTabActive = false; + } + } + + /** + * Switch the active tab by adding/removing the d-none class. + * @param tab {string} The tab to switch to + */ + static switchTab(tab) { + if(tab !== 'settings') { + TabManager.isSettingsTabActive = false; + } + const tabs = document.querySelectorAll('[data-tab]'); + tabs.forEach(e => { + e.classList.add('d-none'); + }); + const activeTab = document.querySelector(`[data-tab=${tab}]`); + activeTab.classList.remove('d-none'); + } +} + +/** + * ThemeManager class manages the theme of the application. + */ +class ThemeManager { + constructor() { + this.setBootstrapTheme(localStorage.getItem('bootstrap-theme') || 'light'); + this.registerThemeSwitchEvent(); + this.showGitHubIconOnAlternateInstallation(); + } + + /** + * Show the GitHub icon on alternate installation if the URL does not start with the specified string. + */ + showGitHubIconOnAlternateInstallation() { + if (!location.href.startsWith('https://cloudmaker97.github.io/DurstRechner')) { + Element.getGitHubReferenceLink().classList.remove('visually-hidden'); + } + } + + /** + * Register events for the theme switch button. + */ + registerThemeSwitchEvent() { + document.querySelectorAll('[data-toggle-bs-theme]').forEach(element => { + element.addEventListener('click', event => { + const theme = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark'; + this.setBootstrapTheme(theme); + }) + }); + } + + /** + * Set the bootstrap theme by setting the data-bs-theme attribute on the html element and saving it to local storage. + * @param theme {string} The theme to set (light or dark) + */ + setBootstrapTheme(theme) { + document.documentElement.setAttribute('data-bs-theme', theme); + localStorage.setItem('bootstrap-theme', theme); + } +} + +/** + * Define the CartHistoryManager class to manage the cart history. + */ +class CartHistoryManager { + static getTotal() { + return localStorage.getItem('cart-total-value') || 0; + } + + static setTotal(value) { + localStorage.setItem('cart-total-value', value); + } + + static addToTotal(value) { + const total = parseFloat(CartHistoryManager.getTotal()) + value; + CartHistoryManager.setTotal(total); + } +} + +/** + * Main function to initialize the application. + */ +const cartManager = new CartManager(); +const productManager = new ProductManager(); +const tabManager = new TabManager(); +const themeManager = new ThemeManager(); +const cartHistoryManager = new CartHistoryManager(); \ No newline at end of file diff --git a/assets/script/calculator.js b/assets/script/calculator.js deleted file mode 100644 index c9c9f13..0000000 --- a/assets/script/calculator.js +++ /dev/null @@ -1,654 +0,0 @@ -/** - * Element class provides static methods to get various elements from the DOM. - */ -class Element { - /** - * Get the import/export textarea element - * @returns {HTMLTextAreaElement} - */ - static getImportExportTextarea() { - return document.querySelector('#input-export-import'); - } - - /** - * Get the product list element - * @returns {HTMLElement} - */ - static getProductListElement() { - return document.querySelector('.product-list'); - } - - /** - * Get the settings product list element - * @returns {HTMLElement} - */ - static getSettingListElement() { - return document.querySelector('.settings-product-list'); - } - - /** - * Get the cart element - * @returns {HTMLElement} - */ - static getCartElement() { - return document.querySelector('.cart-items'); - } - - /** - * Get the cart empty alert element - * @returns {HTMLElement} - */ - static getCartEmptyAlertElement() { - return document.querySelector('.alert.cart-empty'); - } - - /** - * Get the create new product button element - * @returns {HTMLButtonElement} - */ - static getCreateNewProductButton() { - return document.querySelector('#create-product'); - } - - /** - * Get the cart button element - * @returns {HTMLButtonElement} - */ - static getCartButton() { - return document.querySelector('button.cart-value'); - } - - /** - * Get the navbar brand element - * @returns {Element} - */ - static getNavbarBrand() { - return document.querySelector('.navbar-brand'); - } - - static getButtonsImportTestdata() { - return document.querySelectorAll('[data-action=import-testdata]'); - } - static getButtonShowTestdata() { - return document.querySelectorAll('[data-action=import-show-testdata]'); - } - static getButtonClearTestdata() { - return document.querySelectorAll('[data-action=import-clear]'); - } - static getGitHubReferenceLink() { - return document.querySelector('[data-github-ref]'); - } -} - -/** - * TemplateElement class provides static methods to get various templates from the DOM. - */ -class TemplateElement { - /** - * Get the product list element template - * @returns {HTMLElement} - */ - static getProductTemplate() { - const element = document.querySelector('[data-template=product]').cloneNode(true); - return this.removeTemplate(element); - } - - /** - * Get the cart line element template - * @returns {HTMLElement} - */ - static getCartLineTemplate() { - const element = document.querySelector('[data-template=product-line-item]').cloneNode(true); - return this.removeTemplate(element); - } - - /** - * Get the settings product element template - * @returns {HTMLElement} - */ - static getSettingsProductTemplate() { - const element = document.querySelector('[data-template=settings-product]').cloneNode(true); - return this.removeTemplate(element); - } - - /** - * Remove the template attribute from the element - * @param element {HTMLElement} - * @returns {HTMLElement} - */ - static removeTemplate(element) { - element.removeAttribute('data-template'); - return element; - } -} - -/** - * Product class represents a product with an id, name, price, and image. - */ -class Product { - /** - * Product constructor - * @param name {string} Name of the product - * @param price {number} Price of the product - * @param image {string} Image of the product (source url or base64) - */ - constructor(name, price, image) { - this.id = Product.generateId(); - this.name = name; - this.price = price; - this.image = image; - } - - /** - * Generate a random id for the product - * @returns {number} - */ - static generateId() { - return Math.floor(Math.random() * 1000000000) * Math.floor(Math.random() * 1000000000); - } - - /** - * Get the product html element by template and set the attributes by the product object - * @returns {HTMLElement} - */ - getProductElement() { - const productElement = TemplateElement.getProductTemplate(); - productElement.setAttribute('data-id', this.id); - productElement.querySelector('[data-attr=name]').textContent = this.name; - productElement.querySelector('[data-attr=price]').textContent = CartManager.getNumberFormatter().format(this.price); - productElement.querySelector('[data-attr=image]').src = this.image; - productElement.addEventListener('click', () => { - cartManager.addProduct(this); - }) - return productElement; - } - - /** - * Get the settings product html element by template and set the attributes by the product object - * @returns {HTMLElement} - */ - getSettingsProductElement() { - const productElement = TemplateElement.getSettingsProductTemplate(); - productElement.setAttribute('data-id', this.id); - productElement.textContent = this.name; - productElement.addEventListener('click', () => { - productManager.removeProduct(this); - productManager.setExportFieldJsonValue(); - productElement.remove(); - }) - return productElement; - } -} - -/** - * CartLine class represents a line in the cart with a product and its quantity. - */ -class CartLine { - /** - * CartLine constructor - * @param product {Product} - * @param quantity {number} - */ - constructor(product, quantity) { - this.product = product; - this.quantity = quantity; - } -} - -/** - * Base64Image class provides static methods to convert an image URL or file to a base64 string. - */ -class Base64Image { - /** - * Convert an image URL to a base64 string - * @param url - * @returns {Promise} - */ - static fromImageUrl(url) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.crossOrigin = 'Anonymous'; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0); - const dataUrl = canvas.toDataURL(); - resolve(dataUrl); - }; - img.onerror = reject; - img.src = url; - }); - } - - /** - * Resize an image from a base64 string to a smaller size - * @param base64 - * @returns {Promise} - */ - static imageResize(base64) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.crossOrigin = 'Anonymous'; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = 250; - canvas.height = 250; - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); - const dataUrl = canvas.toDataURL(); - resolve(dataUrl); - }; - img.onerror = reject; - img.src = base64; - }); - } - - /** - * Convert a file to a base64 string - * @param file - * @returns {Promise} - */ - static fromFile(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - resolve(reader.result); - }; - reader.onerror = reject; - reader.readAsDataURL(file); - }); - } -} - -/** - * ProductManager class manages the product list, including loading from JSON, adding, and removing products. - */ -class ProductManager { - constructor() { - this.loadFromJson(localStorage.getItem('products') ?? []) - this.setExportFieldJsonValue(); - this.registerSettingsFormEvents(); - this.registerImportFormEvents(); - this.registerNavbarBrandToProductEvent(); - } - - /** - * Register the event for the navbar brand to switch to the products tab when clicked. - */ - registerNavbarBrandToProductEvent() { - Element.getNavbarBrand().addEventListener('click', () => { - TabManager.switchTab('products'); - }) - } - - /** - * Register events for the import/export textarea. - */ - registerImportFormEvents() { - Element.getButtonsImportTestdata().forEach(button => { - button.addEventListener('click', async () => { - await fetch('assets/example/example.json').then(r => r.json()).then(async json => { - await this.convertAndSaveProductImages(json); - location.reload(); - }) - }); - }) - - Element.getImportExportTextarea().addEventListener('input', async (e) => { - try { - const json = JSON.parse(e.target.value); - await this.convertAndSaveProductImages(json); - location.reload(); - } catch (e) { - console.error(e); - } - }); - } - - /** - * Convert and save product images from JSON string to base64 and save it to local storage. - * @param json - * @returns {Promise} - */ - async convertAndSaveProductImages(json) { - const products = json.map(e => new Product(e.name, e.price, e.image)); - for (const product of products) { - if (!product.image.toString().startsWith('data:image/')) { - product.image = await Base64Image.imageResize(await Base64Image.fromImageUrl(product.image)) - } - } - localStorage.setItem('products', JSON.stringify(products)); - } - - /** - * Set the JSON value of the export field to the current products. - */ - setExportFieldJsonValue() { - Element.getImportExportTextarea().textContent = JSON.stringify(this.products); - } - - /** - * Load products from JSON string and parse it into Product objects, then render the product list. - * @param json - */ - loadFromJson(json) { - let parse = []; - try { - parse = JSON.parse(json); - } catch (e) {} - this.products = parse.map(e => new Product(e.name, e.price, e.image)); - this.renderProductList(); - } - - /** - * Register events for the create new product button. - */ - registerSettingsFormEvents() { - Element.getCreateNewProductButton().addEventListener('click', async (e) => { - e.preventDefault(); - - const name = document.querySelector('#product-name').value; - const price = document.querySelector('#product-price').value; - const image = document.querySelector('#product-image').files[0]; - - // Validate price - if (name.length === 0) { - alert('Name muss ausgefüllt sein'); - return; - } - - // Turn image into base64 or use placeholder image - if (image) { - const imageSrcWithBase64 = Base64Image.fromFile(image); - imageSrcWithBase64.then(async imageSrcWithBase64 => { - productManager.addProduct(new Product(name, price, await Base64Image.imageResize(imageSrcWithBase64))); - ProductManager.resetProductSettingsForm(); - }); - } else { - let image1 = await Base64Image.imageResize(await Base64Image.fromImageUrl(`https://placehold.co/250x250?text=${name}`)); - productManager.addProduct(new Product(name, price, image1)); - ProductManager.resetProductSettingsForm(); - } - }); - } - - /** - * Reset the product settings form by clearing all input fields. - */ - static resetProductSettingsForm() { - document.querySelectorAll('form input:not([type=submit])').forEach(inputField => inputField.value = ''); - } - - /** - * Render the product list by clearing the existing elements and appending new ones. - */ - renderProductList() { - // Clear the product list element before rendering except the template - Element.getProductListElement().querySelectorAll('[data-id]').forEach(e => { - if (e.getAttribute('data-template') === null) { - e.remove(); - } - }); - this.products.forEach(e => { - Element.getProductListElement().appendChild(e.getProductElement()); - Element.getSettingListElement().appendChild(e.getSettingsProductElement()); - }) - } - - /** - * Add a new product to the list and save it to local storage. - * @param product {Product} - */ - addProduct(product) { - this.products.push(product); - localStorage.setItem('products', JSON.stringify(this.products)); - Element.getProductListElement().appendChild(product.getProductElement()); - Element.getSettingListElement().appendChild(product.getSettingsProductElement()); - this.setExportFieldJsonValue(); - } - - /** - * Remove a product from the list and local storage. - * @param product {Product} - */ - removeProduct(product) { - this.products = this.products.filter(e => e.id !== product.id); - const productElement = Element.getProductListElement().querySelector(`[data-id="${product.id}"]`); - if (productElement) { - productElement.remove(); - } - localStorage.setItem('products', JSON.stringify(this.products)); - this.setExportFieldJsonValue(); - } -} - -/** - * CartManager class manages the cart, including adding, removing products, and rendering the cart. - */ -class CartManager { - constructor() { - this.cartLines = []; - this.registerCartResetEvent(); - } - - /** - * Register the cart reset event to clear the cart when the button is clicked. - */ - registerCartResetEvent() { - Element.getCartButton().addEventListener('click', () => { - CartHistoryManager.addToTotal(this.cartLines.reduce((acc, cartLine) => { - return acc + (cartLine.product.price * cartLine.quantity); - }, 0)); - this.cartLines = []; - this.renderCart(); - }); - } - - /** - * Add a product to the cart. If the product already exists, increase the quantity. - * @param product {Product} - */ - addProduct(product) { - const cartLine = this.cartLines.find(e => e.product.id === product.id); - if (cartLine) { - cartLine.quantity++; - } else { - this.cartLines.push(new CartLine(product, 1)); - } - this.renderCart(); - } - - /** - * Remove a product from the cart. If the quantity is 0, remove the product from the cart. - * @param product {Product} - */ - removeProduct(product) { - const cartLine = this.cartLines.find(e => e.product.id === product.id); - if (cartLine) { - cartLine.quantity--; - if (cartLine.quantity <= 0) { - this.cartLines = this.cartLines.filter(e => e.product.id !== product.id); - } - } - this.renderCart(); - } - - /** - * Render the cart by clearing the existing elements and appending new ones. - */ - renderCart() { - // Clear the cart element before rendering except the template - Element.getCartElement().querySelectorAll('[data-id]').forEach(e => { - if (e.getAttribute('data-template') === null) { - e.remove(); - } - }); - - if(this.cartLines.length === 0) { - Element.getCartEmptyAlertElement().classList.remove('d-none'); - } else { - Element.getCartEmptyAlertElement().classList.add('d-none'); - } - this.calculateCartValue(); - - // Render each cart line - this.cartLines.forEach(cartLine => { - const cartLineElement = this.getCartLineElement(cartLine); - Element.getCartElement().appendChild(cartLineElement); - }); - } - - /** - * Get the cart line html element and set the attributes by the product object - * @param cartLine {CartLine} - * @returns {*} - */ - getCartLineElement(cartLine) { - const cartLineElement = TemplateElement.getCartLineTemplate(); - cartLineElement.setAttribute('data-id', cartLine.product.id); - cartLineElement.querySelector('[data-attr=name]').textContent = cartLine.product.name; - cartLineElement.querySelector('[data-attr=value]').textContent = CartManager.getNumberFormatter().format(cartLine.product.price); - cartLineElement.querySelector('[data-attr=quantity]').textContent = cartLine.quantity; - cartLineElement.addEventListener('click', () => { - this.removeProduct(cartLine.product); - }) - return cartLineElement; - } - - /** - * Calculate the total value of the cart by multiplying the product price with the quantity and setting it to the button. - */ - calculateCartValue() { - let cartValue = this.cartLines.reduce((acc, cartLine) => { - return acc + (cartLine.product.price * cartLine.quantity); - }, 0); - Element.getCartButton().querySelector('[data-total-value]').textContent = CartManager.getNumberFormatter().format(cartValue); - } - - /** - * Get the number formatter for the currency. - * @returns {Intl.NumberFormat} - */ - static getNumberFormatter() { - return new Intl.NumberFormat('de-DE', { - currency: 'EUR', - minimumFractionDigits: 2 - }); - } -} - -/** - * TabManager class manages the tabs in the settings page. - */ -class TabManager { - /** - * @type {boolean} If the settings tab is active or not - */ - static isSettingsTabActive = false; - - constructor() { - TabManager.isSettingsTabActive = false; - document.querySelector('[data-toggle-tab]').addEventListener('click', (e) => { - TabManager.toggleTab(); - }); - } - - /** - * Toggle the active tab between settings and products. - */ - static toggleTab() { - if(!TabManager.isSettingsTabActive) { - TabManager.isSettingsTabActive = true; - this.switchTab('settings'); - } else { - this.switchTab('products'); - TabManager.isSettingsTabActive = false; - } - } - - /** - * Switch the active tab by adding/removing the d-none class. - * @param tab {string} The tab to switch to - */ - static switchTab(tab) { - if(tab !== 'settings') { - TabManager.isSettingsTabActive = false; - } - const tabs = document.querySelectorAll('[data-tab]'); - tabs.forEach(e => { - e.classList.add('d-none'); - }); - const activeTab = document.querySelector(`[data-tab=${tab}]`); - activeTab.classList.remove('d-none'); - } -} - -/** - * ThemeManager class manages the theme of the application. - */ -class ThemeManager { - constructor() { - this.setBootstrapTheme(localStorage.getItem('bootstrap-theme') || 'light'); - this.registerThemeSwitchEvent(); - this.showGitHubIconOnAlternateInstallation(); - } - - /** - * Show the GitHub icon on alternate installation if the URL does not start with the specified string. - */ - showGitHubIconOnAlternateInstallation() { - if (!location.href.startsWith('https://cloudmaker97.github.io/DurstRechner')) { - Element.getGitHubReferenceLink().classList.remove('visually-hidden'); - } - } - - /** - * Register events for the theme switch button. - */ - registerThemeSwitchEvent() { - document.querySelectorAll('[data-toggle-bs-theme]').forEach(element => { - element.addEventListener('click', event => { - const theme = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark'; - this.setBootstrapTheme(theme); - }) - }); - } - - /** - * Set the bootstrap theme by setting the data-bs-theme attribute on the html element and saving it to local storage. - * @param theme {string} The theme to set (light or dark) - */ - setBootstrapTheme(theme) { - document.documentElement.setAttribute('data-bs-theme', theme); - localStorage.setItem('bootstrap-theme', theme); - } -} - -/** - * Define the CartHistoryManager class to manage the cart history. - */ -class CartHistoryManager { - static getTotal() { - return localStorage.getItem('cart-total-value') || 0; - } - - static setTotal(value) { - localStorage.setItem('cart-total-value', value); - } - - static addToTotal(value) { - const total = parseFloat(CartHistoryManager.getTotal()) + value; - CartHistoryManager.setTotal(total); - } -} - -/** - * Main function to initialize the application. - */ -const cartManager = new CartManager(); -const productManager = new ProductManager(); -const tabManager = new TabManager(); -const themeManager = new ThemeManager(); -const cartHistoryManager = new CartHistoryManager(); \ No newline at end of file diff --git a/assets/script/service-worker.js b/assets/script/service-worker.js index 3743992..485980d 100644 --- a/assets/script/service-worker.js +++ b/assets/script/service-worker.js @@ -9,7 +9,6 @@ const FILES_TO_CACHE = [ `${BASE_PATH}/assets/script/service-worker.js`, `${BASE_PATH}/assets/style/stylesheet.css`, `${BASE_PATH}/assets/script/all.js`, - `${BASE_PATH}/assets/script/calculator.js`, `${BASE_PATH}/node_modules/bootstrap/dist/css/bootstrap.min.css`, `${BASE_PATH}/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js`, `${BASE_PATH}/node_modules/bootstrap-icons/font/bootstrap-icons.min.css`, diff --git a/package.json b/package.json index 66e9bee..5e5c938 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "author": "Dennis Heinrich", "license": "BSD-3-Clause", "dependencies": { - "@popperjs/core": "^2.11.8", "bootstrap": "5.3.5", "bootstrap-icons": "^1.11.3" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e38b292..3021a59 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@popperjs/core': - specifier: ^2.11.8 - version: 2.11.8 bootstrap: specifier: 5.3.5 version: 5.3.5(@popperjs/core@2.11.8)