Compare commits
17 commits
358fcc2745
...
eb1229d859
| Author | SHA1 | Date | |
|---|---|---|---|
| eb1229d859 | |||
| 37b068396e | |||
| 05f58406d0 | |||
| 87d0169a65 | |||
| 474682252f | |||
| f3cb932654 | |||
| b0cff2c55f | |||
| 2b3796445e | |||
| d6c8c1d600 | |||
| 0092d527a6 | |||
| cc529e7da9 | |||
| 52cbaaf978 | |||
| 6e41c5920f | |||
| 8b1b9a6b11 | |||
| 47482b40e6 | |||
| 4dc591896f | |||
| 9296f456fc |
42
.github/workflows/static.yml
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
name: Deploy static content to Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pnpm install
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: '.'
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
3
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
node_modules
|
node_modules
|
||||||
|
.idea
|
||||||
8
.idea/.gitignore
vendored
|
|
@ -1,8 +0,0 @@
|
||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="de.php_perfect.intellij.ddev.settings.DdevSettingsState">
|
|
||||||
<option name="ddevBinary" value="C:\Program Files\DDEV\ddev.exe" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
|
|
||||||
<serverData>
|
|
||||||
<paths name="Streckenkunde Digital (DDEV)">
|
|
||||||
<serverdata>
|
|
||||||
<mappings>
|
|
||||||
<mapping local="$PROJECT_DIR$" web="/" />
|
|
||||||
</mappings>
|
|
||||||
</serverdata>
|
|
||||||
</paths>
|
|
||||||
</serverData>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="JavaScriptSettings">
|
|
||||||
<option name="languageLevel" value="FLOW" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/durst-rechner.iml" filepath="$PROJECT_DIR$/.idea/durst-rechner.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="MessDetectorOptionsConfiguration">
|
|
||||||
<option name="transferred" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PHPCSFixerOptionsConfiguration">
|
|
||||||
<option name="transferred" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PHPCodeSnifferOptionsConfiguration">
|
|
||||||
<option name="highlightLevel" value="WARNING" />
|
|
||||||
<option name="transferred" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PhpStanOptionsConfiguration">
|
|
||||||
<option name="transferred" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PsalmOptionsConfiguration">
|
|
||||||
<option name="transferred" value="true" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
26
README.md
|
|
@ -6,9 +6,33 @@ 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Installieren der Abhängigkeiten
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
## Entwicklung
|
## Entwicklung
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Startet den Entwicklungsserver
|
# Startet den Entwicklungsserver
|
||||||
npx http-server -o .
|
npx http-server -o .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Desktop Ansicht / Tablet</summary>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Smartphone Ansicht</summary>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 527 B After Width: | Height: | Size: 527 B |
|
Before Width: | Height: | Size: 966 B After Width: | Height: | Size: 966 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
11
assets/script/all.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import './theme.js'
|
||||||
|
import './calculator.js'
|
||||||
|
import './../../node_modules/bootstrap/dist/js/bootstrap.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));
|
||||||
|
});
|
||||||
|
}
|
||||||
283
assets/script/calculator.js
Normal file
|
|
@ -0,0 +1,283 @@
|
||||||
|
class TemplateElement {
|
||||||
|
static getProductTemplate() {
|
||||||
|
const element = document.querySelector('[data-template=product]').cloneNode(true);
|
||||||
|
return this.removeTemplate(element);
|
||||||
|
}
|
||||||
|
static getCartLineTemplate() {
|
||||||
|
const element = document.querySelector('[data-template=product-line-item]').cloneNode(true);
|
||||||
|
return this.removeTemplate(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getSettingsProductTemplate() {
|
||||||
|
const element = document.querySelector('[data-template=settings-product]').cloneNode(true);
|
||||||
|
return this.removeTemplate(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeTemplate(element) {
|
||||||
|
element.removeAttribute('data-template');
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Product {
|
||||||
|
constructor(name, price, image) {
|
||||||
|
this.id = Product.generateId();
|
||||||
|
this.name = name;
|
||||||
|
this.price = price;
|
||||||
|
this.image = image;
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateId() {
|
||||||
|
return Math.floor(Math.random() * 1000000000) * Math.floor(Math.random() * 1000000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductElement() {
|
||||||
|
const productElement = TemplateElement.getProductTemplate();
|
||||||
|
productElement.setAttribute('data-id', this.id);
|
||||||
|
productElement.querySelector('[data-attr=name]').textContent = this.name;
|
||||||
|
productElement.querySelector('[data-attr=image]').src = this.image;
|
||||||
|
productElement.addEventListener('click', () => {
|
||||||
|
cart.addProduct(this);
|
||||||
|
})
|
||||||
|
return productElement;
|
||||||
|
}
|
||||||
|
getSettingsProductElement() {
|
||||||
|
const productElement = TemplateElement.getSettingsProductTemplate();
|
||||||
|
productElement.setAttribute('data-id', this.id);
|
||||||
|
productElement.textContent = this.name;
|
||||||
|
productElement.addEventListener('click', () => {
|
||||||
|
productList.removeProduct(this);
|
||||||
|
productList.setExportField();
|
||||||
|
productElement.remove();
|
||||||
|
})
|
||||||
|
return productElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CartLine {
|
||||||
|
constructor(product, quantity) {
|
||||||
|
this.product = product;
|
||||||
|
this.quantity = quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProductList {
|
||||||
|
constructor() {
|
||||||
|
this.loadFromJson(localStorage.getItem('products') ?? [])
|
||||||
|
this.setExportField();
|
||||||
|
this.getImportExportTextarea().addEventListener('input', (e) => {
|
||||||
|
try {
|
||||||
|
const json = e.target.value;
|
||||||
|
localStorage.setItem('products', json);
|
||||||
|
window.location = window.location;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setExportField() {
|
||||||
|
this.getImportExportTextarea().textContent = JSON.stringify(this.products);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
getImportExportTextarea() {
|
||||||
|
return document.querySelector('#input-export-import');
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductListElement() {
|
||||||
|
return document.querySelector('.product-list');
|
||||||
|
}
|
||||||
|
|
||||||
|
getSettingListElement() {
|
||||||
|
return document.querySelector('.settings-product-list');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderProductList() {
|
||||||
|
// Clear the product list element before rendering except the template
|
||||||
|
this.getProductListElement().querySelectorAll('[data-id]').forEach(e => {
|
||||||
|
if (e.getAttribute('data-template') === null) {
|
||||||
|
e.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.products.forEach(e => {
|
||||||
|
this.getProductListElement().appendChild(e.getProductElement());
|
||||||
|
this.getSettingListElement().appendChild(e.getSettingsProductElement());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addProduct(product) {
|
||||||
|
this.products.push(product);
|
||||||
|
localStorage.setItem('products', JSON.stringify(this.products));
|
||||||
|
this.getProductListElement().appendChild(product.getProductElement());
|
||||||
|
this.getSettingListElement().appendChild(product.getSettingsProductElement());
|
||||||
|
this.setExportField();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeProduct(product) {
|
||||||
|
this.products = this.products.filter(e => e.id !== product.id);
|
||||||
|
const productElement = this.getProductListElement().querySelector(`[data-id="${product.id}"]`);
|
||||||
|
if (productElement) {
|
||||||
|
productElement.remove();
|
||||||
|
}
|
||||||
|
localStorage.setItem('products', JSON.stringify(this.products));
|
||||||
|
this.setExportField();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Cart {
|
||||||
|
constructor() {
|
||||||
|
this.cartLines = [];
|
||||||
|
this.getCartButton().addEventListener('click', () => {
|
||||||
|
this.cartLines = [];
|
||||||
|
this.renderCart();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCart() {
|
||||||
|
// Clear the cart element before rendering except the template
|
||||||
|
this.getCartElement().querySelectorAll('[data-id]').forEach(e => {
|
||||||
|
if (e.getAttribute('data-template') === null) {
|
||||||
|
e.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(this.cartLines.length === 0) {
|
||||||
|
this.getAlertElement().classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
this.getAlertElement().classList.add('d-none');
|
||||||
|
}
|
||||||
|
this.calculateCartValue();
|
||||||
|
|
||||||
|
// Render each cart line
|
||||||
|
this.cartLines.forEach(cartLine => {
|
||||||
|
const cartLineElement = this.getCartLineElement(cartLine);
|
||||||
|
this.getCartElement().appendChild(cartLineElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = Cart.getNumberFormatter().format(cartLine.product.price);
|
||||||
|
cartLineElement.querySelector('[data-attr=quantity]').textContent = cartLine.quantity;
|
||||||
|
cartLineElement.addEventListener('click', () => {
|
||||||
|
this.removeProduct(cartLine.product);
|
||||||
|
})
|
||||||
|
return cartLineElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAlertElement() {
|
||||||
|
return document.querySelector('.alert.cart-empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
getCartElement() {
|
||||||
|
return document.querySelector('.cart-items');
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateCartValue() {
|
||||||
|
let cartValue = this.cartLines.reduce((acc, cartLine) => {
|
||||||
|
return acc + (cartLine.product.price * cartLine.quantity);
|
||||||
|
}, 0);
|
||||||
|
this.getCartButton().querySelector('[data-total-value]').textContent = Cart.getNumberFormatter().format(cartValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getNumberFormatter() {
|
||||||
|
return new Intl.NumberFormat('de-DE', {
|
||||||
|
currency: 'EUR',
|
||||||
|
minimumFractionDigits: 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getCartButton() {
|
||||||
|
return document.querySelector('button.cart-value');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tab {
|
||||||
|
static toggleTab() {
|
||||||
|
if(!isSettingsTab) {
|
||||||
|
this.switchTab('settings');
|
||||||
|
} else {
|
||||||
|
this.switchTab('products');
|
||||||
|
}
|
||||||
|
isSettingsTab = !isSettingsTab;
|
||||||
|
}
|
||||||
|
static switchTab(tab) {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let isSettingsTab = false;
|
||||||
|
const cart = new Cart();
|
||||||
|
const productList = new ProductList();
|
||||||
|
|
||||||
|
document.querySelector('[data-toggle-tab]').addEventListener('click', (e) => {
|
||||||
|
Tab.toggleTab();
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
reader.onloadend = function () {
|
||||||
|
const imageBase64 = reader.result;
|
||||||
|
const imageSrcWithBase64 = `data:${image.type};base64,${imageBase64.split(',')[1]}`;
|
||||||
|
productList.addProduct(new Product(name, fieldPrice, imageSrcWithBase64));
|
||||||
|
document.querySelector('#product-name').value = '';
|
||||||
|
document.querySelector('#product-price').value = '0,00';
|
||||||
|
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 = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
70
assets/script/service-worker.js
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
const CACHE_NAME = 'durst-rechner-v2';
|
||||||
|
|
||||||
|
const BASE_PATH = self.location.pathname.includes('DurstRechner') ? '/DurstRechner' : ''; // Handle GitHub Pages vs. Localhost
|
||||||
|
|
||||||
|
const FILES_TO_CACHE = [
|
||||||
|
`${BASE_PATH}/`,
|
||||||
|
`${BASE_PATH}/index.html`,
|
||||||
|
`${BASE_PATH}/assets/manifest.json`,
|
||||||
|
`${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}/assets/script/theme.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`,
|
||||||
|
`${BASE_PATH}/node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2`,
|
||||||
|
`${BASE_PATH}/node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff`,
|
||||||
|
`${BASE_PATH}/assets/favicon/android-chrome-192x192.png`,
|
||||||
|
`${BASE_PATH}/assets/favicon/android-chrome-512x512.png`,
|
||||||
|
`${BASE_PATH}/assets/favicon/apple-touch-icon.png`,
|
||||||
|
`${BASE_PATH}/assets/favicon/favicon-16x16.png`,
|
||||||
|
`${BASE_PATH}/assets/favicon/favicon-32x32.png`,
|
||||||
|
`${BASE_PATH}/assets/favicon/favicon.ico`,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Cache files with the correct base path
|
||||||
|
const FILES_TO_CACHE_PREFIXED = FILES_TO_CACHE;
|
||||||
|
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then(cache => {
|
||||||
|
const cachePromises = FILES_TO_CACHE_PREFIXED.map(url => {
|
||||||
|
return fetch(url).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
return cache.put(url, response);
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to fetch: ${url}`);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(`Failed to fetch (network error): ${url}`, err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Promise.all(cachePromises);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then(response => {
|
||||||
|
return response || fetch(event.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', event => {
|
||||||
|
const cacheWhitelist = [CACHE_NAME];
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then(cacheNames => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map(cacheName => {
|
||||||
|
if (!cacheWhitelist.includes(cacheName)) {
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
39
assets/style/stylesheet.css
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
.line-item-details {
|
||||||
|
.amount {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 1.5em;
|
||||||
|
}
|
||||||
|
.price {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.product-list {
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
.currency-value::after {
|
||||||
|
content: ' €';
|
||||||
|
}
|
||||||
|
.quantity-value::after {
|
||||||
|
content: 'x ';
|
||||||
|
}
|
||||||
|
.name-value {
|
||||||
|
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 {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
.product-box, .cart-items, .settings-product-list {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
108
index.html
|
|
@ -4,65 +4,103 @@
|
||||||
<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"/>
|
||||||
<meta name="theme-color" content="#2196f3"/>
|
<meta name="theme-color" content="#2196f3"/>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="assets/manifest.json">
|
||||||
<link rel="stylesheet" href="stylesheet.css">
|
<link rel="apple-touch-icon" sizes="180x180" href="assets/favicon/apple-touch-icon.png">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon/favicon-16x16.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon/favicon-16x16.png">
|
|
||||||
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
|
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="node_modules/bootstrap-icons/font/bootstrap-icons.min.css">
|
<link rel="stylesheet" href="node_modules/bootstrap-icons/font/bootstrap-icons.min.css">
|
||||||
<title>Der Durstrechner</title>
|
<link rel="stylesheet" href="assets/style/stylesheet.css">
|
||||||
|
<title>Durstrechner</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="d-flex flex-column vh-100">
|
<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">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="#">Durstrechner</a>
|
<a class="navbar-brand" href="#">Der Durstrechner</a>
|
||||||
<form class="d-flex" role="search">
|
<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> Tag / Nacht</button>
|
||||||
<button class="btn">Einstellungen</button>
|
<button class="btn" data-toggle-tab><i class="bi bi-gear"></i> Einstellungen</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<main class="container-fluid flex-grow-1">
|
||||||
<div class="row">
|
<div data-tab="products" class="row">
|
||||||
<div class="col-3">
|
<div class="col-12 col-md-4">
|
||||||
<ul class="list-group d-none">
|
<li class="list-group-item d-flex justify-content-between" data-template="product-line-item">
|
||||||
<li class="list-group-item">Test</li>
|
<span class="line-item-details">
|
||||||
</ul>
|
<span class="amount quantity-value" data-attr="quantity">5</span>
|
||||||
<div class="alert alert-info">Keine Produkte ausgewählt</div>
|
<span class="price currency-value" data-attr="value">2,50</span>
|
||||||
|
<span class="name name-value" data-attr="name">Wasser mit Kohlensäure</span>
|
||||||
|
</span>
|
||||||
|
<i class="bi bi-dash-circle"></i>
|
||||||
|
</li>
|
||||||
|
<ul class="list-group cart-items"></ul>
|
||||||
|
<div class="alert alert-info cart-empty">Keine Produkte ausgewählt</div>
|
||||||
<hr>
|
<hr>
|
||||||
<button class="btn btn-secondary btn-lg w-100">
|
<button class="btn btn-secondary btn-lg w-100 cart-value mb-3">
|
||||||
<span class="currency-value" data-total-value>0,00</span>
|
<span class="currency-value" data-total-value>0,00</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9">
|
<div class="col-12 col-md-8">
|
||||||
<div class="row row-cols-6 row-gap-3">
|
<div class="row row-cols-4 row-gap-3 product-list h-100 overflow-auto">
|
||||||
<div class="col product-box">
|
<div class="col product-box" data-template="product" data-id="0">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<img src="https://placehold.co/250x250" alt="Produktbild" class="card-img-top">
|
<img data-attr="image" src="https://placehold.co/250x250" alt="Produktbild" class="card-img-top" loading="lazy">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<span>Wasser mit Kohlensäure</span>
|
<span data-attr="name">Wasser mit Kohlensäure</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div data-tab="settings" class="row d-none">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<form class="card mb-3" method="dialog" id="new-product">
|
||||||
|
<h5 class="card-header">Neues Produkt</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="product-name" class="form-label">Name des Produkts</label>
|
||||||
|
<input type="text" class="form-control" placeholder="Tolles Produkt" id="product-name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<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>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="product-image" class="form-label">Bild auswählen</label>
|
||||||
|
<input type="file" class="form-control" placeholder="Tolles Produkt" id="product-image">
|
||||||
|
</div>
|
||||||
|
<input type="submit" class="btn btn-success" value="Speichern" id="create-product">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<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)
|
||||||
|
<textarea class="form-control mt-1" id="input-export-import"></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Produkte entfernen</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<li class="list-group-item" data-template="settings-product">Test</li>
|
||||||
|
<ul class="list-group settings-product-list">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
<script src="node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="assets/script/all.js" type="module"></script>
|
||||||
<script src="script/all.js" type="module"></script>
|
|
||||||
<script>
|
|
||||||
// Install the service worker for offline capabilities
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
navigator.serviceWorker.register('service-worker.js')
|
|
||||||
.then(reg => console.log('Service Worker registered', reg))
|
|
||||||
.catch(err => console.error('SW registration failed', err));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
import './theme.js'
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
const CACHE_NAME = 'durst-rechner-v2';
|
|
||||||
|
|
||||||
const FILES_TO_CACHE = [
|
|
||||||
'/',
|
|
||||||
'/index.html',
|
|
||||||
'/manifest.json',
|
|
||||||
'/service-worker.js',
|
|
||||||
'/stylesheet.css',
|
|
||||||
'/script/all.js',
|
|
||||||
'/script/calculator.js',
|
|
||||||
'/script/theme.js',
|
|
||||||
'/node_modules/bootstrap/dist/css/bootstrap.min.css',
|
|
||||||
'/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js',
|
|
||||||
'/node_modules/bootstrap-icons/font/bootstrap-icons.min.css',
|
|
||||||
'/node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2',
|
|
||||||
'/node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff',
|
|
||||||
'/favicon/android-chrome-192x192.png',
|
|
||||||
'/favicon/android-chrome-512x512.png',
|
|
||||||
'/favicon/apple-touch-icon.png',
|
|
||||||
'/favicon/favicon-16x16.png',
|
|
||||||
'/favicon/favicon-32x32.png',
|
|
||||||
'/favicon/favicon.ico',
|
|
||||||
];
|
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
|
||||||
event.waitUntil(
|
|
||||||
caches.open(CACHE_NAME).then(cache => {
|
|
||||||
return cache.addAll(FILES_TO_CACHE);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('fetch', event => {
|
|
||||||
event.respondWith(
|
|
||||||
caches.match(event.request).then(response => {
|
|
||||||
return response || fetch(event.request);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
.currency-value::after {
|
|
||||||
content: ' €';
|
|
||||||
}
|
|
||||||