Demo-Templates, Designanpassungen, Importverhalten geändert
62
assets/example/example.json
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
[
|
||||
{
|
||||
"id": 75086086956949000,
|
||||
"name": "Wasser",
|
||||
"price": "0.5",
|
||||
"image": "./assets/example/images/wasser.webp"
|
||||
},
|
||||
{
|
||||
"id": 403980494034923100,
|
||||
"name": "Cola",
|
||||
"price": "1",
|
||||
"image": "./assets/example/images/cola.webp"
|
||||
},
|
||||
{
|
||||
"id": 174111295271372860,
|
||||
"name": "Fanta",
|
||||
"price": "1",
|
||||
"image": "./assets/example/images/fanta.webp"
|
||||
},
|
||||
{
|
||||
"id": 13086165444583086,
|
||||
"name": "Bier",
|
||||
"price": "1.5",
|
||||
"image": "./assets/example/images/bier.webp"
|
||||
},
|
||||
{
|
||||
"id": 227920082494117630,
|
||||
"name": "Helles Bier",
|
||||
"price": "1.2",
|
||||
"image": "./assets/example/images/bier_helles.webp"
|
||||
},
|
||||
{
|
||||
"id": 27617349687000040,
|
||||
"name": "Apfelsaft",
|
||||
"price": "1.2",
|
||||
"image": "./assets/example/images/apfelsaft.webp"
|
||||
},
|
||||
{
|
||||
"id": 191783545996520450,
|
||||
"name": "Eistee",
|
||||
"price": "1.5",
|
||||
"image": "./assets/example/images/eistee.webp"
|
||||
},
|
||||
{
|
||||
"id": 203321763516746460,
|
||||
"name": "Cider",
|
||||
"price": "1.8",
|
||||
"image": "./assets/example/images/cider.webp"
|
||||
},
|
||||
{
|
||||
"id": 615216202376277900,
|
||||
"name": "Zitronenlimonade",
|
||||
"price": "1",
|
||||
"image": "./assets/example/images/limonade.webp"
|
||||
},
|
||||
{
|
||||
"id": 363150314621734500,
|
||||
"name": "Wodka Lemon",
|
||||
"price": "3",
|
||||
"image": "./assets/example/images/wodka_lemon.webp"
|
||||
}
|
||||
]
|
||||
62
assets/example/example_urls.json
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
[
|
||||
{
|
||||
"id": 75086086956949000,
|
||||
"name": "Wasser",
|
||||
"price": "0.5",
|
||||
"image": "https://placehold.co/250x250?text=1"
|
||||
},
|
||||
{
|
||||
"id": 403980494034923100,
|
||||
"name": "Cola",
|
||||
"price": "1",
|
||||
"image": "https://placehold.co/250x250?text=2"
|
||||
},
|
||||
{
|
||||
"id": 174111295271372860,
|
||||
"name": "Fanta",
|
||||
"price": "1",
|
||||
"image": "https://placehold.co/250x250?text=3"
|
||||
},
|
||||
{
|
||||
"id": 13086165444583086,
|
||||
"name": "Bier",
|
||||
"price": "1.5",
|
||||
"image": "https://placehold.co/250x250?text=4"
|
||||
},
|
||||
{
|
||||
"id": 227920082494117630,
|
||||
"name": "Helles Bier",
|
||||
"price": "1.2",
|
||||
"image": "https://placehold.co/250x250?text=5"
|
||||
},
|
||||
{
|
||||
"id": 27617349687000040,
|
||||
"name": "Apfelsaft",
|
||||
"price": "1.2",
|
||||
"image": "https://placehold.co/250x250?text=6"
|
||||
},
|
||||
{
|
||||
"id": 191783545996520450,
|
||||
"name": "Eistee",
|
||||
"price": "1.5",
|
||||
"image": "https://placehold.co/250x250?text=7"
|
||||
},
|
||||
{
|
||||
"id": 203321763516746460,
|
||||
"name": "Cider",
|
||||
"price": "1.8",
|
||||
"image": "https://placehold.co/250x250?text=8"
|
||||
},
|
||||
{
|
||||
"id": 615216202376277900,
|
||||
"name": "Zitronenlimonade",
|
||||
"price": "1",
|
||||
"image": "https://placehold.co/250x250?text=9"
|
||||
},
|
||||
{
|
||||
"id": 363150314621734500,
|
||||
"name": "Wodka Lemon",
|
||||
"price": "3",
|
||||
"image": "https://placehold.co/250x250?text=10"
|
||||
}
|
||||
]
|
||||
BIN
assets/example/images/apfelsaft.webp
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
assets/example/images/bier.webp
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
assets/example/images/bier_helles.webp
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
assets/example/images/cider.webp
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
assets/example/images/cola.webp
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
assets/example/images/eistee.webp
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
assets/example/images/fanta.webp
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
assets/example/images/limonade.webp
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
assets/example/images/wasser.webp
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
assets/example/images/wodka_lemon.webp
Normal file
|
After Width: | Height: | Size: 124 KiB |
|
|
@ -3,7 +3,6 @@
|
|||
"short_name": "Durstrechner",
|
||||
"description": "Der Durstrechner ist ein einfaches Tool zur Berechnung von Verkaufspreisen bei Veranstaltungen.",
|
||||
"start_url": "../",
|
||||
"scope": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#2196f3",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import './calculator.js'
|
||||
import './../../node_modules/bootstrap/dist/js/bootstrap.min.js'
|
||||
import './../../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js'
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,20 @@ class Element {
|
|||
* @returns {Element}
|
||||
*/
|
||||
static getNavbarBrand() {
|
||||
return document.querySelector('nav a.navbar-brand');
|
||||
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]');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,6 +155,7 @@ class Product {
|
|||
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);
|
||||
|
|
@ -273,16 +287,41 @@ class ProductManager {
|
|||
* Register events for the import/export textarea.
|
||||
*/
|
||||
registerImportFormEvents() {
|
||||
Element.getImportExportTextarea().addEventListener('input', (e) => {
|
||||
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 {
|
||||
localStorage.setItem('products', e.target.value);
|
||||
window.location = window.location;
|
||||
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<void>}
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
@ -553,6 +592,16 @@ 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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,40 @@
|
|||
.line-item-details {
|
||||
/**
|
||||
* Here are some variables that are used in the whole application.
|
||||
*/
|
||||
:root {
|
||||
--currency-symbol: '€';
|
||||
--separator: '|';
|
||||
--price-color-light-mode: var(--bs-primary-rgb);
|
||||
--price-color-dark-mode: var(--bs-info-rgb);
|
||||
}
|
||||
/**
|
||||
* Let the user never select the text in the product list, cart items, alert and navbar brand.
|
||||
* This is used to prevent the user from selecting the text in the product list and cart items. (Usability)
|
||||
*/
|
||||
.cart-items, .product-list, .alert, .navbar-brand, .settings-product-list {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/**
|
||||
* Make product boxes, cart items and settings product list clickable
|
||||
* This is generally used for product list and cart items
|
||||
*/
|
||||
.product-box, .cart-items, .settings-product-list, .navbar-brand {
|
||||
cursor: pointer;
|
||||
}
|
||||
/**
|
||||
* This is a dummy element, that can be copied via JavaScript
|
||||
* and will be filled dynamically with some data.
|
||||
*/
|
||||
[data-template] {
|
||||
display: none !important;
|
||||
}
|
||||
/**
|
||||
* This is the cart with its line items
|
||||
*/
|
||||
.cart {
|
||||
.line-item-details {
|
||||
.amount {
|
||||
display: inline-block;
|
||||
min-width: 1.5em;
|
||||
|
|
@ -7,45 +43,68 @@
|
|||
display: inline-block;
|
||||
min-width: 3em;
|
||||
}
|
||||
}
|
||||
.product-list {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
.currency-value::after {
|
||||
content: ' €';
|
||||
}
|
||||
.quantity-value::after {
|
||||
content: 'x ';
|
||||
}
|
||||
.name-value {
|
||||
.name {
|
||||
margin-left: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-template] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.cart-items, .product-list, .alert, .navbar-brand, .settings-product-list {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
img {
|
||||
-webkit-user-drag: none;
|
||||
-khtml-user-drag: none;
|
||||
user-drag: none;
|
||||
}
|
||||
.product-box, .cart-items, .settings-product-list {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Product list contains all available products,
|
||||
* which are displayed in a grid layout. The grid scaling is done by bootstrap.
|
||||
*/
|
||||
.product-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 4fr));
|
||||
gap: .4em;
|
||||
}
|
||||
padding-bottom: 1em;
|
||||
|
||||
.product-box .card-body span {
|
||||
/**
|
||||
* Product box for product list
|
||||
* @param {string} data-attr - product attribute
|
||||
*/
|
||||
.product-box {
|
||||
/* Product box should be clickable */
|
||||
cursor: pointer;
|
||||
|
||||
/* Images shouldn't drag in user interface */
|
||||
img {
|
||||
-webkit-user-drag: none;
|
||||
-khtml-user-drag: none;
|
||||
user-drag: none;
|
||||
}
|
||||
|
||||
/* Break the words on the card body */
|
||||
.card-body span {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
/* Slightly bold font for the price attribute */
|
||||
.product-box small[data-attr=price] {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* When using dark mode, use info color for price */
|
||||
[data-bs-theme="dark"] {
|
||||
.product-box small[data-attr=price] {
|
||||
color: rgba(var(--price-color-dark-mode));
|
||||
}
|
||||
}
|
||||
/* When using light mode, use primary color for price */
|
||||
[data-bs-theme="light"] {
|
||||
.product-box small[data-attr=price] {
|
||||
color: rgba(var(--price-color-light-mode));
|
||||
}
|
||||
}
|
||||
/* Insert currency symbol after each value */
|
||||
.currency-value::after {
|
||||
content: ' ' var(--currency-symbol);
|
||||
}
|
||||
/* Insert separator */
|
||||
.separator::after {
|
||||
content: ' ' var(--separator) ' ';
|
||||
}
|
||||
/* Insert times x symbol after an amount */
|
||||
.quantity-value::after {
|
||||
content: 'x ';
|
||||
}
|
||||
76
index.html
|
|
@ -14,19 +14,29 @@
|
|||
<title>Durstrechner</title>
|
||||
</head>
|
||||
<body class="d-flex flex-column vh-100">
|
||||
<nav class="navbar navbar-expand-lg border-bottom mb-3">
|
||||
<nav class="navbar navbar-expand-lg border-bottom mb-3">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Der Durstrechner</a>
|
||||
<span class="navbar-brand d-flex gap-2" href="#">
|
||||
<span>Der Durstrechner</span>
|
||||
<a href="https://github.com/cloudmaker97/DurstRechner" class="text-white visually-hidden" title="Projekt auf GitHub" data-github-ref style="width: 20px; height: 20px;">
|
||||
<i class="bi bi-github"></i>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<form class="d-flex" role="search" method="dialog">
|
||||
<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> <span class="d-none d-md-inline-block">Einstellungen</span></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> <span class="d-none d-md-inline-block">Einstellungen</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
|
||||
<main class="container-fluid flex-grow-1">
|
||||
<main class="container-fluid flex-grow-1">
|
||||
<div data-tab="products" class="row">
|
||||
<div class="col-12 col-md-4">
|
||||
<aside class="col-12 col-md-4 cart">
|
||||
<li class="list-group-item d-flex justify-content-between" data-template="product-line-item">
|
||||
<span class="line-item-details">
|
||||
<span class="amount quantity-value" data-attr="quantity">5</span>
|
||||
|
|
@ -41,13 +51,17 @@
|
|||
<button class="btn btn-secondary btn-lg w-100 cart-value mb-3">
|
||||
<span class="currency-value" data-total-value>0,00</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="col-12 col-md-8">
|
||||
<div class="product-list">
|
||||
<div class="col product-box" data-template="product" data-id="0">
|
||||
<div class="card">
|
||||
<img data-attr="image" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+BCQAHBQICJmhD1AAAAABJRU5ErkJggg==" alt="Produktbild" class="card-img-top" loading="eager">
|
||||
<img data-attr="image"
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+BCQAHBQICJmhD1AAAAABJRU5ErkJggg=="
|
||||
alt="Produktbild" class="card-img-top" loading="eager">
|
||||
<div class="card-body">
|
||||
<small data-attr="price" class="currency-value">0,00</small>
|
||||
<i class="separator"></i>
|
||||
<small data-attr="name">Wasser mit Kohlensäure</small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -55,9 +69,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-tab="settings" class="row d-none">
|
||||
<div class="col-12 col-md-6">
|
||||
<aside class="col-12 col-md-6">
|
||||
<form class="card mb-2" method="dialog" id="new-product">
|
||||
<h5 class="card-header">Neues Produkt</h5>
|
||||
<div class="card-body">
|
||||
|
|
@ -83,13 +96,44 @@
|
|||
<div class="card">
|
||||
<h5 class="card-header">Export / Import</h5>
|
||||
<div class="card-body">
|
||||
<label class="w-100">
|
||||
Daten importieren oder exportieren (Kopieren und Einfügen)
|
||||
<details class="mb-2">
|
||||
<summary>Exportieren und Importieren</summary>
|
||||
<p>
|
||||
Die Daten können im JSON-Format exportiert und importiert werden.
|
||||
Um Speicherplatz zu sparen, sollte das Bild im Format 1:1 vorliegen.
|
||||
Ideal ist eine Bildgröße von 250x250 Pixel. Wenn ein Bild über eine URL bereitgestellt wird,
|
||||
lädt die Software das Bild herunter und speichert es in der JSON-Datei als Base64-kodierte
|
||||
Version. Die ursprüngliche URL wird dabei nicht mehr gespeichert.
|
||||
</p>
|
||||
<label class="w-100 form-label">
|
||||
<b>Text zum Kopieren und Einfügen:</b>
|
||||
<textarea class="form-control mt-1" id="input-export-import" spellcheck="false"></textarea>
|
||||
</label>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Testdaten installieren</summary>
|
||||
<p>
|
||||
Über den Knopf "Testdaten einspielen" können Testdaten in die Software importiert werden
|
||||
oder auch angezeigt werden. Die Daten können nach eigenen Belieben bearbeitet werden. Die
|
||||
Testdaten könnten einen kleinen Augenblick laden, da sie erst heruntergeladen werden müssen.
|
||||
</p>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-action="import-testdata">
|
||||
Testdaten einspielen
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown" aria-expanded="false" data-bs-reference="parent">
|
||||
<span class="visually-hidden">Testdaten einspielen</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="/assets/example/example.json">Import-Datei anzeigen</a></li>
|
||||
<li><a class="dropdown-item" href="/assets/example/example_urls.json">Import-Datei mit URLs anzeigen</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="col-12 col-md-6 mt-2 mt-md-0">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Produkte entfernen</h5>
|
||||
|
|
@ -102,8 +146,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
|
||||
<script src="assets/script/all.js" type="module"></script>
|
||||
<script src="assets/script/all.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,17 @@
|
|||
"name": "durst-rechner",
|
||||
"version": "1.0.0",
|
||||
"scripts": {},
|
||||
"keywords": ["calculator", "feuerwehr", "firefighter", "pos", "thirst"],
|
||||
"keywords": [
|
||||
"calculator",
|
||||
"feuerwehr",
|
||||
"firefighter",
|
||||
"pos",
|
||||
"thirst"
|
||||
],
|
||||
"author": "Dennis Heinrich",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "5.3.5",
|
||||
"bootstrap-icons": "^1.11.3"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ 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)
|
||||
|
|
|
|||