Compare commits

..

5 commits

12 changed files with 440 additions and 163 deletions

BIN
.github/screenshots/preview.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 MiB

BIN
.github/screenshots/small_products.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

BIN
.github/screenshots/small_settings.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
.github/screenshots/wide_products.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

BIN
.github/screenshots/wide_settings.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

28
LICENSE Normal file
View file

@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2025, Dennis Heinrich
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -6,33 +6,24 @@ ist auch offline verfügbar und kann ohne Internetverbindung genutzt werden (Ser
Einrichtung wird eine Internetverbindung empfohlen (das einmalige Aufrufen der Seite im Browser mit einer Internetverbindung Einrichtung wird eine Internetverbindung empfohlen (das einmalige Aufrufen der Seite im Browser mit einer Internetverbindung
genügt). genügt).
## Installation ![preview.png](.github/screenshots/preview.png)
> Weitere Screenshots sind im Ordner `.github/screenshots` zu finden.
## Anleitung und Hinweise
- Die Anwendung ist unter <https://cloudmaker97.github.io/DurstRechner> erreichbar.
- Die Anwendung kann auch lokal genutzt werden. Dazu einfach die Startseite einmal im Browser öffnen.
- Neue Produkte können im Bereich 'Einstellungen' hinzugefügt werden, Produktbilder sollten im Format 1:1 sein und idealerweise 150x150px.
- Die angelegten Produkte können auf andere Geräte übertragen werden in den Einstellungen. Einfach den Text aus dem Feld Export/Import entweder herauskopieren oder einfügen.
## Entwicklung und Installation
```bash ```bash
# Installieren der Abhängigkeiten # Installieren der Abhängigkeiten
pnpm install pnpm install
```
## Entwicklung
```bash
# Startet den Entwicklungsserver # Startet den Entwicklungsserver
npx http-server -o . npx http-server -o .
``` ```
## Screenshots
<details>
<summary>Desktop Ansicht / Tablet</summary>
![2025-04-06 01_31_13-Durstrechner](https://github.com/user-attachments/assets/c3f120cf-8b7b-42ee-8be5-5a697fe57fcb)
</details>
<details>
<summary>Smartphone Ansicht</summary>
![2025-04-06 01_30_54-Durstrechner](https://github.com/user-attachments/assets/8eabc119-d776-43e5-ac14-2a38a2006aa6)
</details>

View file

@ -1,4 +1,3 @@
import './theme.js'
import './calculator.js' import './calculator.js'
import './../../node_modules/bootstrap/dist/js/bootstrap.min.js' import './../../node_modules/bootstrap/dist/js/bootstrap.min.js'

View file

@ -1,25 +1,116 @@
/**
* 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');
}
}
/**
* TemplateElement class provides static methods to get various templates from the DOM.
*/
class TemplateElement { class TemplateElement {
/**
* Get the product list element template
* @returns {HTMLElement}
*/
static getProductTemplate() { static getProductTemplate() {
const element = document.querySelector('[data-template=product]').cloneNode(true); const element = document.querySelector('[data-template=product]').cloneNode(true);
return this.removeTemplate(element); return this.removeTemplate(element);
} }
/**
* Get the cart line element template
* @returns {HTMLElement}
*/
static getCartLineTemplate() { static getCartLineTemplate() {
const element = document.querySelector('[data-template=product-line-item]').cloneNode(true); const element = document.querySelector('[data-template=product-line-item]').cloneNode(true);
return this.removeTemplate(element); return this.removeTemplate(element);
} }
/**
* Get the settings product element template
* @returns {HTMLElement}
*/
static getSettingsProductTemplate() { static getSettingsProductTemplate() {
const element = document.querySelector('[data-template=settings-product]').cloneNode(true); const element = document.querySelector('[data-template=settings-product]').cloneNode(true);
return this.removeTemplate(element); return this.removeTemplate(element);
} }
/**
* Remove the template attribute from the element
* @param element {HTMLElement}
* @returns {HTMLElement}
*/
static removeTemplate(element) { static removeTemplate(element) {
element.removeAttribute('data-template'); element.removeAttribute('data-template');
return element; return element;
} }
} }
/**
* Product class represents a product with an id, name, price, and image.
*/
class Product { 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) { constructor(name, price, image) {
this.id = Product.generateId(); this.id = Product.generateId();
this.name = name; this.name = name;
@ -27,48 +118,141 @@ class Product {
this.image = image; this.image = image;
} }
/**
* Generate a random id for the product
* @returns {number}
*/
static generateId() { static generateId() {
return Math.floor(Math.random() * 1000000000) * Math.floor(Math.random() * 1000000000); 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() { getProductElement() {
const productElement = TemplateElement.getProductTemplate(); const productElement = TemplateElement.getProductTemplate();
productElement.setAttribute('data-id', this.id); productElement.setAttribute('data-id', this.id);
productElement.querySelector('[data-attr=name]').textContent = this.name; productElement.querySelector('[data-attr=name]').textContent = this.name;
productElement.querySelector('[data-attr=image]').src = this.image; productElement.querySelector('[data-attr=image]').src = this.image;
productElement.addEventListener('click', () => { productElement.addEventListener('click', () => {
cart.addProduct(this); cartManager.addProduct(this);
}) })
return productElement; return productElement;
} }
/**
* Get the settings product html element by template and set the attributes by the product object
* @returns {HTMLElement}
*/
getSettingsProductElement() { getSettingsProductElement() {
const productElement = TemplateElement.getSettingsProductTemplate(); const productElement = TemplateElement.getSettingsProductTemplate();
productElement.setAttribute('data-id', this.id); productElement.setAttribute('data-id', this.id);
productElement.textContent = this.name; productElement.textContent = this.name;
productElement.addEventListener('click', () => { productElement.addEventListener('click', () => {
productList.removeProduct(this); productManager.removeProduct(this);
productList.setExportField(); productManager.setExportFieldJsonValue();
productElement.remove(); productElement.remove();
}) })
return productElement; return productElement;
} }
} }
/**
* CartLine class represents a line in the cart with a product and its quantity.
*/
class CartLine { class CartLine {
/**
* CartLine constructor
* @param product {Product}
* @param quantity {number}
*/
constructor(product, quantity) { constructor(product, quantity) {
this.product = product; this.product = product;
this.quantity = quantity; this.quantity = quantity;
} }
} }
class ProductList { /**
* 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<string>}
*/
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;
});
}
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<string>}
*/
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() { constructor() {
this.loadFromJson(localStorage.getItem('products') ?? []) this.loadFromJson(localStorage.getItem('products') ?? [])
this.setExportField(); this.setExportFieldJsonValue();
this.getImportExportTextarea().addEventListener('input', (e) => { this.registerSettingsFormEvents();
this.registerImportFormEvents();
}
/**
* Register events for the import/export textarea.
*/
registerImportFormEvents() {
Element.getImportExportTextarea().addEventListener('input', (e) => {
try { try {
const json = e.target.value; localStorage.setItem('products', e.target.value);
localStorage.setItem('products', json);
window.location = window.location; window.location = window.location;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -76,10 +260,17 @@ class ProductList {
}); });
} }
setExportField() { /**
this.getImportExportTextarea().textContent = JSON.stringify(this.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) { loadFromJson(json) {
let parse = []; let parse = [];
try { try {
@ -89,59 +280,111 @@ class ProductList {
this.renderProductList(); this.renderProductList();
} }
getImportExportTextarea() { /**
return document.querySelector('#input-export-import'); * 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;
} }
getProductListElement() { // Turn image into base64 or use placeholder image
return document.querySelector('.product-list'); 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();
}
});
} }
getSettingListElement() { /**
return document.querySelector('.settings-product-list'); * 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() { renderProductList() {
// Clear the product list element before rendering except the template // Clear the product list element before rendering except the template
this.getProductListElement().querySelectorAll('[data-id]').forEach(e => { Element.getProductListElement().querySelectorAll('[data-id]').forEach(e => {
if (e.getAttribute('data-template') === null) { if (e.getAttribute('data-template') === null) {
e.remove(); e.remove();
} }
}); });
this.products.forEach(e => { this.products.forEach(e => {
this.getProductListElement().appendChild(e.getProductElement()); Element.getProductListElement().appendChild(e.getProductElement());
this.getSettingListElement().appendChild(e.getSettingsProductElement()); Element.getSettingListElement().appendChild(e.getSettingsProductElement());
}) })
} }
/**
* Add a new product to the list and save it to local storage.
* @param product {Product}
*/
addProduct(product) { addProduct(product) {
this.products.push(product); this.products.push(product);
localStorage.setItem('products', JSON.stringify(this.products)); localStorage.setItem('products', JSON.stringify(this.products));
this.getProductListElement().appendChild(product.getProductElement()); Element.getProductListElement().appendChild(product.getProductElement());
this.getSettingListElement().appendChild(product.getSettingsProductElement()); Element.getSettingListElement().appendChild(product.getSettingsProductElement());
this.setExportField(); this.setExportFieldJsonValue();
} }
/**
* Remove a product from the list and local storage.
* @param product {Product}
*/
removeProduct(product) { removeProduct(product) {
this.products = this.products.filter(e => e.id !== product.id); this.products = this.products.filter(e => e.id !== product.id);
const productElement = this.getProductListElement().querySelector(`[data-id="${product.id}"]`); const productElement = Element.getProductListElement().querySelector(`[data-id="${product.id}"]`);
if (productElement) { if (productElement) {
productElement.remove(); productElement.remove();
} }
localStorage.setItem('products', JSON.stringify(this.products)); localStorage.setItem('products', JSON.stringify(this.products));
this.setExportField(); this.setExportFieldJsonValue();
} }
} }
class Cart { /**
* CartManager class manages the cart, including adding, removing products, and rendering the cart.
*/
class CartManager {
constructor() { constructor() {
this.cartLines = []; this.cartLines = [];
this.getCartButton().addEventListener('click', () => { this.registerCartResetEvent();
}
/**
* Register the cart reset event to clear the cart when the button is clicked.
*/
registerCartResetEvent() {
Element.getCartButton().addEventListener('click', () => {
this.cartLines = []; this.cartLines = [];
this.renderCart(); this.renderCart();
}); });
} }
/**
* Add a product to the cart. If the product already exists, increase the quantity.
* @param product {Product}
*/
addProduct(product) { addProduct(product) {
const cartLine = this.cartLines.find(e => e.product.id === product.id); const cartLine = this.cartLines.find(e => e.product.id === product.id);
if (cartLine) { if (cartLine) {
@ -152,6 +395,10 @@ class Cart {
this.renderCart(); this.renderCart();
} }
/**
* Remove a product from the cart. If the quantity is 0, remove the product from the cart.
* @param product {Product}
*/
removeProduct(product) { removeProduct(product) {
const cartLine = this.cartLines.find(e => e.product.id === product.id); const cartLine = this.cartLines.find(e => e.product.id === product.id);
if (cartLine) { if (cartLine) {
@ -163,33 +410,41 @@ class Cart {
this.renderCart(); this.renderCart();
} }
/**
* Render the cart by clearing the existing elements and appending new ones.
*/
renderCart() { renderCart() {
// Clear the cart element before rendering except the template // Clear the cart element before rendering except the template
this.getCartElement().querySelectorAll('[data-id]').forEach(e => { Element.getCartElement().querySelectorAll('[data-id]').forEach(e => {
if (e.getAttribute('data-template') === null) { if (e.getAttribute('data-template') === null) {
e.remove(); e.remove();
} }
}); });
if(this.cartLines.length === 0) { if(this.cartLines.length === 0) {
this.getAlertElement().classList.remove('d-none'); Element.getCartEmptyAlertElement().classList.remove('d-none');
} else { } else {
this.getAlertElement().classList.add('d-none'); Element.getCartEmptyAlertElement().classList.add('d-none');
} }
this.calculateCartValue(); this.calculateCartValue();
// Render each cart line // Render each cart line
this.cartLines.forEach(cartLine => { this.cartLines.forEach(cartLine => {
const cartLineElement = this.getCartLineElement(cartLine); const cartLineElement = this.getCartLineElement(cartLine);
this.getCartElement().appendChild(cartLineElement); Element.getCartElement().appendChild(cartLineElement);
}); });
} }
/**
* Get the cart line html element and set the attributes by the product object
* @param cartLine {CartLine}
* @returns {*}
*/
getCartLineElement(cartLine) { getCartLineElement(cartLine) {
const cartLineElement = TemplateElement.getCartLineTemplate(); const cartLineElement = TemplateElement.getCartLineTemplate();
cartLineElement.setAttribute('data-id', cartLine.product.id); cartLineElement.setAttribute('data-id', cartLine.product.id);
cartLineElement.querySelector('[data-attr=name]').textContent = cartLine.product.name; cartLineElement.querySelector('[data-attr=name]').textContent = cartLine.product.name;
cartLineElement.querySelector('[data-attr=value]').textContent = Cart.getNumberFormatter().format(cartLine.product.price); cartLineElement.querySelector('[data-attr=value]').textContent = CartManager.getNumberFormatter().format(cartLine.product.price);
cartLineElement.querySelector('[data-attr=quantity]').textContent = cartLine.quantity; cartLineElement.querySelector('[data-attr=quantity]').textContent = cartLine.quantity;
cartLineElement.addEventListener('click', () => { cartLineElement.addEventListener('click', () => {
this.removeProduct(cartLine.product); this.removeProduct(cartLine.product);
@ -197,42 +452,60 @@ class Cart {
return cartLineElement; return cartLineElement;
} }
getAlertElement() { /**
return document.querySelector('.alert.cart-empty'); * Calculate the total value of the cart by multiplying the product price with the quantity and setting it to the button.
} */
getCartElement() {
return document.querySelector('.cart-items');
}
calculateCartValue() { calculateCartValue() {
let cartValue = this.cartLines.reduce((acc, cartLine) => { let cartValue = this.cartLines.reduce((acc, cartLine) => {
return acc + (cartLine.product.price * cartLine.quantity); return acc + (cartLine.product.price * cartLine.quantity);
}, 0); }, 0);
this.getCartButton().querySelector('[data-total-value]').textContent = Cart.getNumberFormatter().format(cartValue); Element.getCartButton().querySelector('[data-total-value]').textContent = CartManager.getNumberFormatter().format(cartValue);
} }
/**
* Get the number formatter for the currency.
* @returns {Intl.NumberFormat}
*/
static getNumberFormatter() { static getNumberFormatter() {
return new Intl.NumberFormat('de-DE', { return new Intl.NumberFormat('de-DE', {
currency: 'EUR', currency: 'EUR',
minimumFractionDigits: 2 minimumFractionDigits: 2
}); });
} }
getCartButton() {
return document.querySelector('button.cart-value');
}
} }
class Tab { /**
* 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() { static toggleTab() {
if(!isSettingsTab) { if(!TabManager.isSettingsTabActive) {
this.switchTab('settings'); this.switchTab('settings');
} else { } else {
this.switchTab('products'); this.switchTab('products');
} }
isSettingsTab = !isSettingsTab; TabManager.isSettingsTabActive = !TabManager.isSettingsTabActive;
} }
/**
* Switch the active tab by adding/removing the d-none class.
* @param tab {string} The tab to switch to
*/
static switchTab(tab) { static switchTab(tab) {
const tabs = document.querySelectorAll('[data-tab]'); const tabs = document.querySelectorAll('[data-tab]');
tabs.forEach(e => { tabs.forEach(e => {
@ -243,41 +516,41 @@ class Tab {
} }
} }
let isSettingsTab = false; /**
const cart = new Cart(); * ThemeManager class manages the theme of the application.
const productList = new ProductList(); */
class ThemeManager {
document.querySelector('[data-toggle-tab]').addEventListener('click', (e) => { constructor() {
Tab.toggleTab(); this.setBootstrapTheme(localStorage.getItem('bootstrap-theme') || 'light');
}); this.registerThemeSwitchEvent();
document.querySelector('#create-product').addEventListener('click', (e) => {
e.preventDefault(); // Prevent form submission (if any)
const name = document.querySelector('#product-name').value;
let fieldPrice = document.querySelector('#product-price').value;
const image = document.querySelector('#product-image').files[0];
if(name.length === 0) {
alert('Name muss ausgefüllt sein');
return;
} }
if (image) { /**
const reader = new FileReader(); * Register events for the theme switch button.
reader.onloadend = function () { */
const imageBase64 = reader.result; registerThemeSwitchEvent() {
const imageSrcWithBase64 = `data:${image.type};base64,${imageBase64.split(',')[1]}`; document.querySelectorAll('[data-toggle-bs-theme]').forEach(element => {
productList.addProduct(new Product(name, fieldPrice, imageSrcWithBase64)); element.addEventListener('click', event => {
document.querySelector('#product-name').value = ''; const theme = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark';
document.querySelector('#product-price').value = '0,00'; this.setBootstrapTheme(theme);
document.querySelector('#product-image').value = ''; })
};
reader.readAsDataURL(image);
} else {
productList.addProduct(new Product(name, fieldPrice, `https://placehold.co/250x250?text={${name}}`));
document.querySelector('#product-name').value = '';
document.querySelector('#product-price').value = '';
document.querySelector('#product-image').value = '';
}
}); });
}
/**
* 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);
}
}
/**
* Main function to initialize the application.
*/
const cartManager = new CartManager();
const productManager = new ProductManager();
const tabManager = new TabManager();
const themeManager = new ThemeManager();

View file

@ -10,7 +10,6 @@ const FILES_TO_CACHE = [
`${BASE_PATH}/assets/style/stylesheet.css`, `${BASE_PATH}/assets/style/stylesheet.css`,
`${BASE_PATH}/assets/script/all.js`, `${BASE_PATH}/assets/script/all.js`,
`${BASE_PATH}/assets/script/calculator.js`, `${BASE_PATH}/assets/script/calculator.js`,
`${BASE_PATH}/assets/script/theme.js`,
`${BASE_PATH}/node_modules/bootstrap/dist/css/bootstrap.min.css`, `${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/dist/js/bootstrap.bundle.min.js`,
`${BASE_PATH}/node_modules/bootstrap-icons/font/bootstrap-icons.min.css`, `${BASE_PATH}/node_modules/bootstrap-icons/font/bootstrap-icons.min.css`,

View file

@ -1,15 +0,0 @@
function setBootstrapTheme(theme) {
document.documentElement.setAttribute('data-bs-theme', theme);
localStorage.setItem('bootstrap-theme', theme);
}
setBootstrapTheme(localStorage.getItem('bootstrap-theme') || 'light');
document.querySelectorAll('[data-toggle-bs-theme]').forEach(element => {
element.addEventListener('click', event => {
const theme = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark';
setBootstrapTheme(theme);
})
});

View file

@ -2,7 +2,7 @@
<html lang="de" data-bs-theme="dark"> <html lang="de" data-bs-theme="dark">
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<meta name="theme-color" content="#2196f3"/> <meta name="theme-color" content="#2196f3"/>
<link rel="manifest" href="assets/manifest.json"> <link rel="manifest" href="assets/manifest.json">
<link rel="apple-touch-icon" sizes="180x180" href="assets/favicon/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="assets/favicon/apple-touch-icon.png">
@ -18,15 +18,15 @@
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="#">Der Durstrechner</a> <a class="navbar-brand" href="#">Der Durstrechner</a>
<form class="d-flex" role="search" method="dialog"> <form class="d-flex" role="search" method="dialog">
<button class="btn" data-toggle-bs-theme><i class="bi bi-lightbulb-fill"></i> Tag / Nacht</button> <button class="btn" data-toggle-bs-theme><i class="bi bi-lightbulb-fill"></i> <span class="d-none d-md-inline-block">Tag / Nacht</span></button>
<button class="btn" data-toggle-tab><i class="bi bi-gear"></i> Einstellungen</button> <button class="btn" data-toggle-tab><i class="bi bi-gear"></i> <span class="d-none d-md-inline-block">Einstellungen</span></button>
</form> </form>
</div> </div>
</nav> </nav>
<main class="container-fluid flex-grow-1"> <main class="container-fluid flex-grow-1">
<div data-tab="products" class="row"> <div data-tab="products" class="row">
<div class="col-12 col-md-4"> <div class="col-12 col-md-4 products">
<li class="list-group-item d-flex justify-content-between" data-template="product-line-item"> <li class="list-group-item d-flex justify-content-between" data-template="product-line-item">
<span class="line-item-details"> <span class="line-item-details">
<span class="amount quantity-value" data-attr="quantity">5</span> <span class="amount quantity-value" data-attr="quantity">5</span>
@ -43,10 +43,10 @@
</button> </button>
</div> </div>
<div class="col-12 col-md-8"> <div class="col-12 col-md-8">
<div class="row row-cols-4 row-gap-3 product-list h-100 overflow-auto"> <div class="row row-cols-2 row-cols-md-5 row-gap-3 product-list h-100 overflow-auto">
<div class="col product-box" data-template="product" data-id="0"> <div class="col product-box" data-template="product" data-id="0">
<div class="card"> <div class="card">
<img data-attr="image" src="https://placehold.co/250x250" alt="Produktbild" class="card-img-top" loading="lazy"> <img data-attr="image" src="https://placehold.co/250x250" alt="Produktbild" class="card-img-top" loading="eager">
<div class="card-body"> <div class="card-body">
<span data-attr="name">Wasser mit Kohlensäure</span> <span data-attr="name">Wasser mit Kohlensäure</span>
</div> </div>
@ -56,9 +56,8 @@
</div> </div>
</div> </div>
<div data-tab="settings" class="row d-none"> <div data-tab="settings" class="row d-none">
<div class="row"> <div class="col-12 col-md-6">
<div class="col"> <form class="card mb-2" method="dialog" id="new-product">
<form class="card mb-3" method="dialog" id="new-product">
<h5 class="card-header">Neues Produkt</h5> <h5 class="card-header">Neues Produkt</h5>
<div class="card-body"> <div class="card-body">
<div class="mb-2"> <div class="mb-2">
@ -67,7 +66,10 @@
</div> </div>
<div class="mb-2"> <div class="mb-2">
<label for="product-price" class="form-label">Preis</label> <label for="product-price" class="form-label">Preis</label>
<input type="number" class="form-control" step="0.01" value="0.00" id="product-price" required> <div class="input-group">
<span class="input-group-text" id="basic-addon1"></span>
<input type="number" class="form-control" step="0.01" value="" id="product-price" required>
</div>
</div> </div>
<div class="mb-2"> <div class="mb-2">
<label for="product-image" class="form-label">Bild auswählen</label> <label for="product-image" class="form-label">Bild auswählen</label>
@ -82,15 +84,16 @@
<div class="card-body"> <div class="card-body">
<label class="w-100"> <label class="w-100">
Daten importieren oder exportieren (Kopieren und Einfügen) Daten importieren oder exportieren (Kopieren und Einfügen)
<textarea class="form-control mt-1" id="input-export-import"></textarea> <textarea class="form-control mt-1" id="input-export-import" spellcheck="false"></textarea>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<div class="col"> <div class="col-12 col-md-6 mt-2 mt-md-0">
<div class="card"> <div class="card">
<h5 class="card-header">Produkte entfernen</h5> <h5 class="card-header">Produkte entfernen</h5>
<div class="card-body"> <div class="card-body">
<p class="mb-2">Hier können Produkte entfernt werden, die nicht mehr benötigt werden.</p>
<li class="list-group-item" data-template="settings-product">Test</li> <li class="list-group-item" data-template="settings-product">Test</li>
<ul class="list-group settings-product-list"> <ul class="list-group settings-product-list">
</ul> </ul>
@ -98,7 +101,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</main> </main>
<script src="assets/script/all.js" type="module"></script> <script src="assets/script/all.js" type="module"></script>