Compare commits

...

17 commits

Author SHA1 Message Date
eb1229d859
Update der README.md
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
2025-04-06 01:33:49 +02:00
37b068396e Base-Paths für Root-Pfad 2025-04-06 01:22:51 +02:00
05f58406d0 Test für Base-Paths, einzelne Pfade 2025-04-06 01:20:42 +02:00
87d0169a65 Test für Base-Paths 2025-04-06 01:18:00 +02:00
474682252f Test 2025-04-06 01:14:10 +02:00
f3cb932654 Replace index.html 2025-04-06 01:10:11 +02:00
b0cff2c55f Service-Worker mit Prefix Origin 2025-04-06 01:08:03 +02:00
2b3796445e Lokale Verzeichnisse für Service Worker 2025-04-06 01:01:21 +02:00
d6c8c1d600 Test to remove / 2025-04-06 00:59:16 +02:00
0092d527a6 PNPM installieren mit Version 2025-04-06 00:54:50 +02:00
cc529e7da9 PNPM installieren 2025-04-06 00:54:05 +02:00
52cbaaf978
Workflow erstellt 2025-04-06 00:52:30 +02:00
6e41c5920f IDEA-Ordner entfernt 2025-04-06 00:49:12 +02:00
8b1b9a6b11 Bearbeiten der Produkte in den Einstellungen ermöglicht 2025-04-06 00:26:32 +02:00
47482b40e6 Import & Export Textfeld, Einstellungs Tab implementiert und Formular vorbereitet 2025-04-05 23:34:54 +02:00
4dc591896f Auslagerung der Dateien in den Assets-Folder 2025-04-05 22:50:42 +02:00
9296f456fc Berechnen von Gesamtpreisen nach Hinzufügen von Elementen; JavaScript Grundfunktionalität 2025-04-05 22:22:29 +02:00
28 changed files with 545 additions and 154 deletions

42
.github/workflows/static.yml vendored Normal file
View 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

1
.gitignore vendored
View file

@ -1 +1,2 @@
node_modules node_modules
.idea

8
.idea/.gitignore vendored
View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="FLOW" />
</component>
</project>

View file

@ -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>

View file

@ -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>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View file

@ -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>
![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

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View file

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View file

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 527 B

View file

Before

Width:  |  Height:  |  Size: 966 B

After

Width:  |  Height:  |  Size: 966 B

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

11
assets/script/all.js Normal file
View 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
View 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 = '';
}
});

View 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);
}
})
);
})
);
});

View 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;
}

View file

@ -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 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>
<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>

View file

@ -1 +0,0 @@
import './theme.js'

View file

View file

@ -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);
})
);
});

View file

@ -1,3 +0,0 @@
.currency-value::after {
content: ' €';
}