mirror of
https://github.com/trevor1969/teatracker.git
synced 2026-08-09 10:41:59 +00:00
Compare commits
5 Commits
vibe/versi
...
23c83ef4d1
| Author | SHA1 | Date | |
|---|---|---|---|
| 23c83ef4d1 | |||
| b2f632c07f | |||
| b586eb3804 | |||
| 2544a102d9 | |||
| b47e2bd7d0 |
38
.htaccess
Normal file
38
.htaccess
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# TeeTracker .htaccess Configuration
|
||||||
|
# Enable PUT and DELETE methods for the data directory
|
||||||
|
# Required for file-based storage to work on Apache servers
|
||||||
|
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Allow PUT and DELETE for data directory
|
||||||
|
RewriteCond %{REQUEST_METHOD} ^(PUT|DELETE)$
|
||||||
|
RewriteCond %{REQUEST_URI} ^/data/ [NC]
|
||||||
|
RewriteRule ^ - [L]
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Enable all HTTP methods for the data directory
|
||||||
|
<Limit GET POST PUT DELETE OPTIONS>
|
||||||
|
Require all granted
|
||||||
|
</Limit>
|
||||||
|
|
||||||
|
<LimitExcept GET POST PUT DELETE OPTIONS>
|
||||||
|
Require all denied
|
||||||
|
</LimitExcept>
|
||||||
|
|
||||||
|
# Set proper MIME types for JSON files
|
||||||
|
<IfModule mod_mime.c>
|
||||||
|
AddType application/json .json
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Disable caching for data files
|
||||||
|
<FilesMatch "\.(json|txt)$">
|
||||||
|
<IfModule mod_expires.c>
|
||||||
|
ExpiresActive Off
|
||||||
|
</IfModule>
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header set Cache-Control "no-cache, no-store, must-revalidate"
|
||||||
|
Header set Pragma "no-cache"
|
||||||
|
Header set Expires 0
|
||||||
|
</IfModule>
|
||||||
|
</FilesMatch>
|
||||||
193
SERVER_SETUP.md
Normal file
193
SERVER_SETUP.md
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
# TeeTracker Server Setup
|
||||||
|
|
||||||
|
## Problem: Dateispeicherung funktioniert nicht
|
||||||
|
|
||||||
|
Die TeeTracker-App versucht, Daten im `data/`-Verzeichnis zu speichern, aber auf den meisten Webservern funktioniert das nicht standardmäßig. Hier sind die Lösungen:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lösung 1: LocalStorage (Empfohlen für Shared Hosting)
|
||||||
|
|
||||||
|
**Funktioniert immer, ohne Server-Konfiguration!**
|
||||||
|
|
||||||
|
Die App fällt automatisch auf localStorage zurück, wenn die Dateispeicherung nicht verfügbar ist.
|
||||||
|
- Alle Daten werden im Browser gespeichert
|
||||||
|
- Funktioniert auf jedem Webserver
|
||||||
|
- Daten sind nur auf dem aktuellen Gerät/Browser verfügbar
|
||||||
|
|
||||||
|
**Status prüfen:**
|
||||||
|
- Gehe zu "Einstellungen" → "Datenverwaltung"
|
||||||
|
- Wenn "Lokaler Modus (localStorage)" angezeigt wird, funktioniert die Dateispeicherung nicht
|
||||||
|
- Wenn "Dateispeicherung aktiv" angezeigt wird, funktioniert die Dateispeicherung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lösung 2: Apache Server (.htaccess)
|
||||||
|
|
||||||
|
Falls dein Server Apache verwendet (häufig bei Shared Hosting):
|
||||||
|
|
||||||
|
1. **Stelle sicher, dass die `.htaccess`-Datei aktiv ist:**
|
||||||
|
- Die Datei liegt bereits im Hauptverzeichnis
|
||||||
|
- Prüfe, ob `mod_rewrite` aktiviert ist
|
||||||
|
- Kontaktiere deinen Hosting-Anbieter, falls `.htaccess` ignoriert wird
|
||||||
|
|
||||||
|
2. **Erlaube PUT und DELETE Methoden:**
|
||||||
|
Die `.htaccess`-Datei enthält bereits die notwendigen Regeln:
|
||||||
|
```apache
|
||||||
|
<Limit GET POST PUT DELETE OPTIONS>
|
||||||
|
Require all granted
|
||||||
|
</Limit>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Setze die richtigen Berechtigungen:**
|
||||||
|
```bash
|
||||||
|
chmod 755 data/
|
||||||
|
chmod 644 data/.gitkeep
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Aktiviere die notwendigen Apache-Module:**
|
||||||
|
```bash
|
||||||
|
a2enmod rewrite
|
||||||
|
a2enmod headers
|
||||||
|
a2enmod expires
|
||||||
|
service apache2 restart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lösung 3: PHP-Backend (Alternative)
|
||||||
|
|
||||||
|
Falls dein Server PHP unterstützt, kannst du ein einfaches Backend erstellen:
|
||||||
|
|
||||||
|
### 1. Erstelle `save_data.php`:
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$filename = 'data/' . ($_GET['file'] ?? '');
|
||||||
|
$allowed_files = ['teas.json', 'entries.json'];
|
||||||
|
|
||||||
|
if (!in_array(basename($filename), $allowed_files)) {
|
||||||
|
http_response_code(403);
|
||||||
|
die(json_encode(['error' => 'Not allowed']));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||||
|
if (file_exists($filename)) {
|
||||||
|
readfile($filename);
|
||||||
|
} else {
|
||||||
|
echo '[]';
|
||||||
|
}
|
||||||
|
} elseif ($_SERVER['REQUEST_METHOD'] === 'PUT' || $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$data = file_get_contents('php://input');
|
||||||
|
file_put_contents($filename, $data);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} elseif ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
|
||||||
|
if (file_exists($filename)) {
|
||||||
|
unlink($filename);
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Ändere die `LocalFileStorage`-Klasse:
|
||||||
|
Ersetze die `fetch()`-Aufrufe durch Aufrufe an dein PHP-Skript.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lösung 4: Node.js/Express Server
|
||||||
|
|
||||||
|
Falls du Node.js verwenden kannst:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const express = require('express');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.static('public'));
|
||||||
|
|
||||||
|
// Enable PUT and DELETE for data directory
|
||||||
|
app.use('/data', express.raw({ type: '*/*' }));
|
||||||
|
|
||||||
|
app.put('/data/:file', (req, res) => {
|
||||||
|
const filePath = path.join(__dirname, 'data', req.params.file);
|
||||||
|
fs.writeFile(filePath, req.body, (err) => {
|
||||||
|
if (err) return res.status(500).send(err.message);
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/data/:file', (req, res) => {
|
||||||
|
const filePath = path.join(__dirname, 'data', req.params.file);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
res.sendFile(filePath);
|
||||||
|
} else {
|
||||||
|
res.status(404).send('[]');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/data/:file', (req, res) => {
|
||||||
|
const filePath = path.join(__dirname, 'data', req.params.file);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.unlink(filePath, (err) => {
|
||||||
|
if (err) return res.status(500).send(err.message);
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.sendStatus(200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3000, () => console.log('Server running on port 3000'));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Häufige Probleme und Lösungen
|
||||||
|
|
||||||
|
### Problem: "405 Method Not Allowed"
|
||||||
|
**Lösung:** Der Server unterstützt PUT/DELETE nicht. Verwende Lösung 2, 3 oder 4.
|
||||||
|
|
||||||
|
### Problem: "403 Forbidden"
|
||||||
|
**Lösung:** Die Berechtigungen sind falsch. Führe aus:
|
||||||
|
```bash
|
||||||
|
chmod 755 data/
|
||||||
|
chmod 644 data/*
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: "404 Not Found" für data/-Dateien
|
||||||
|
**Lösung:** Das `data/`-Verzeichnis existiert nicht oder die Dateien sind nicht hochgeladen.
|
||||||
|
|
||||||
|
### Problem: CORS-Fehler
|
||||||
|
**Lösung:** Falls du die App von einer anderen Domain lädst, musst du CORS-Header setzen:
|
||||||
|
```apache
|
||||||
|
Header set Access-Control-Allow-Origin "*"
|
||||||
|
Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||||
|
Header set Access-Control-Allow-Headers "Content-Type"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testen der Dateispeicherung
|
||||||
|
|
||||||
|
1. Öffne die Browser-Konsole (F12 → Console)
|
||||||
|
2. Gib ein:
|
||||||
|
```javascript
|
||||||
|
fetch('data/test.txt', { method: 'PUT', body: 'test' })
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(console.log)
|
||||||
|
.catch(console.error);
|
||||||
|
```
|
||||||
|
3. Wenn du eine erfolgreiche Antwort erhältst, funktioniert die Dateispeicherung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Empfehlung
|
||||||
|
|
||||||
|
**Für Shared Hosting:** Verwende einfach localStorage (Lösung 1). Die Daten werden im Browser gespeichert und funktionieren ohne Server-Konfiguration.
|
||||||
|
|
||||||
|
**Für eigene Server:** Konfiguriere Apache mit der `.htaccess`-Datei (Lösung 2) oder verwende das PHP-Backend (Lösung 3).
|
||||||
|
|
||||||
|
**Für Entwickler:** Verwende den Node.js-Server (Lösung 4) oder einen lokalen Entwicklungsserver wie `python -m http.server`.
|
||||||
296
app.js
296
app.js
@ -1,4 +1,239 @@
|
|||||||
// TeeTracker - Main Application
|
// Local File Storage
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
class LocalFileStorage {
|
||||||
|
constructor() {
|
||||||
|
this.basePath = DATA_DIRECTORY;
|
||||||
|
this.connected = false;
|
||||||
|
this.lastError = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async testConnection() {
|
||||||
|
try {
|
||||||
|
// Try to create a test file
|
||||||
|
const testFile = this.basePath + 'test_connection.txt';
|
||||||
|
const response = await fetch(testFile, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: 'test'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Clean up test file
|
||||||
|
await fetch(testFile, { method: 'DELETE' });
|
||||||
|
this.connected = true;
|
||||||
|
this.lastError = null;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this.connected = false;
|
||||||
|
this.lastError = `HTTP Error: ${response.status}`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.connected = false;
|
||||||
|
this.lastError = error.message;
|
||||||
|
console.log('Local file storage not available:', error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFile(filename) {
|
||||||
|
try {
|
||||||
|
const url = this.basePath + filename;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
return text.trim() === '' ? [] : (text ? JSON.parse(text) : null);
|
||||||
|
} else if (response.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error loading ${filename}:`, error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveFile(filename, data) {
|
||||||
|
try {
|
||||||
|
const url = this.basePath + filename;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data, null, 2)
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error saving ${filename}:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fileExists(filename) {
|
||||||
|
try {
|
||||||
|
const url = this.basePath + filename;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
=======
|
||||||
|
// ============================================
|
||||||
|
// Local File Storage
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
class LocalFileStorage {
|
||||||
|
constructor() {
|
||||||
|
this.basePath = DATA_DIRECTORY;
|
||||||
|
this.connected = false;
|
||||||
|
this.lastError = null;
|
||||||
|
this.tested = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async testConnection() {
|
||||||
|
if (this.tested) return this.connected;
|
||||||
|
this.tested = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First, check if we can read from the data directory
|
||||||
|
const testFile = this.basePath + 'teas.json';
|
||||||
|
const readResponse = await fetch(testFile, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (readResponse.ok) {
|
||||||
|
// If we can read, try to write
|
||||||
|
const testWriteFile = this.basePath + '.write_test_' + Date.now() + '.tmp';
|
||||||
|
const writeResponse = await fetch(testWriteFile, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ test: true })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (writeResponse.ok) {
|
||||||
|
// Clean up test file
|
||||||
|
await fetch(testWriteFile, { method: 'DELETE' }).catch(() => {});
|
||||||
|
this.connected = true;
|
||||||
|
this.lastError = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connected = false;
|
||||||
|
this.lastError = 'Server unterstützt Dateispeicherung nicht (PUT/DELETE nicht verfügbar)';
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
this.connected = false;
|
||||||
|
this.lastError = error.message;
|
||||||
|
console.log('Local file storage not available:', error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFile(filename) {
|
||||||
|
try {
|
||||||
|
const url = this.basePath + filename;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
cache: 'no-store'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text || text.trim() === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return JSON.parse(text);
|
||||||
|
} else if (response.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error loading ${filename}:`, error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveFile(filename, data) {
|
||||||
|
try {
|
||||||
|
// First ensure the data directory exists by trying to create it
|
||||||
|
// Note: We can't actually create directories via fetch, but we can try to save the file
|
||||||
|
const url = this.basePath + filename;
|
||||||
|
|
||||||
|
// Try with PUT first (standard for creating/updating files)
|
||||||
|
let response = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data, null, 2)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If PUT fails, try POST (some servers use POST for file creation)
|
||||||
|
response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data, null, 2)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Failed to save ${filename}: PUT and POST both failed`);
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error saving ${filename}:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fileExists(filename) {
|
||||||
|
try {
|
||||||
|
const url = this.basePath + filename;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'HEAD'
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureDirectory() {
|
||||||
|
// Try to create a placeholder file to ensure directory exists
|
||||||
|
// This works on servers that support PUT for file creation
|
||||||
|
try {
|
||||||
|
const placeholder = this.basePath + '.gitkeep';
|
||||||
|
const response = await fetch(placeholder, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
},
|
||||||
|
body: 'This file ensures the data directory exists'
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}TeeTracker - Main Application
|
||||||
// Version 4.0: Local file storage in data/ directory
|
// Version 4.0: Local file storage in data/ directory
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@ -150,6 +385,9 @@ class TeeTracker {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.useFileStorage) {
|
if (this.useFileStorage) {
|
||||||
|
// Try to ensure data directory exists
|
||||||
|
await this.fileStorage.ensureDirectory();
|
||||||
|
|
||||||
// Try to load from file storage first
|
// Try to load from file storage first
|
||||||
const teas = await this.fileStorage.loadFile('teas.json');
|
const teas = await this.fileStorage.loadFile('teas.json');
|
||||||
const entries = await this.fileStorage.loadFile('entries.json');
|
const entries = await this.fileStorage.loadFile('entries.json');
|
||||||
@ -159,7 +397,7 @@ class TeeTracker {
|
|||||||
const localEntries = localStorage.getItem(STORAGE_KEY_ENTRIES);
|
const localEntries = localStorage.getItem(STORAGE_KEY_ENTRIES);
|
||||||
|
|
||||||
// Load from file storage if available, otherwise use local data
|
// Load from file storage if available, otherwise use local data
|
||||||
if (teas) {
|
if (teas && Array.isArray(teas)) {
|
||||||
this.teas = teas.map(tea => ({
|
this.teas = teas.map(tea => ({
|
||||||
...tea,
|
...tea,
|
||||||
organic: tea.organic !== undefined ? tea.organic : false,
|
organic: tea.organic !== undefined ? tea.organic : false,
|
||||||
@ -180,7 +418,7 @@ class TeeTracker {
|
|||||||
this.teas = [];
|
this.teas = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entries) {
|
if (entries && Array.isArray(entries)) {
|
||||||
this.entries = entries.map(entry => ({
|
this.entries = entries.map(entry => ({
|
||||||
...entry,
|
...entry,
|
||||||
teaspoons: entry.teaspoons !== undefined ? entry.teaspoons : 1
|
teaspoons: entry.teaspoons !== undefined ? entry.teaspoons : 1
|
||||||
@ -198,6 +436,11 @@ class TeeTracker {
|
|||||||
} else {
|
} else {
|
||||||
this.entries = [];
|
this.entries = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we loaded from file storage, save to localStorage as backup
|
||||||
|
if ((teas || entries) && (localTeas === null || localEntries === null)) {
|
||||||
|
this.saveToLocalStorage();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Load from localStorage only
|
// Load from localStorage only
|
||||||
this.loadFromLocalStorage();
|
this.loadFromLocalStorage();
|
||||||
@ -516,6 +759,28 @@ class TeeTracker {
|
|||||||
this.switchTab(button.dataset.tab);
|
this.switchTab(button.dataset.tab);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Event delegation for tea card buttons (edit and delete)
|
||||||
|
const teasList = document.getElementById('teas-list');
|
||||||
|
if (teasList) {
|
||||||
|
teasList.addEventListener('click', (e) => {
|
||||||
|
// Edit button
|
||||||
|
const editBtn = e.target.closest('.tea-edit-btn');
|
||||||
|
if (editBtn) {
|
||||||
|
const teaId = editBtn.dataset.teaId;
|
||||||
|
if (teaId) this.editTea(teaId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
const deleteBtn = e.target.closest('.tea-delete-btn');
|
||||||
|
if (deleteBtn) {
|
||||||
|
const teaId = deleteBtn.dataset.teaId;
|
||||||
|
if (teaId) this.showDeleteTeaConfirm(teaId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add tea button
|
// Add tea button
|
||||||
const addTeaBtn = document.getElementById('add-tea-btn');
|
const addTeaBtn = document.getElementById('add-tea-btn');
|
||||||
@ -548,6 +813,12 @@ class TeeTracker {
|
|||||||
if (trackForm) {
|
if (trackForm) {
|
||||||
trackForm.addEventListener('submit', (e) => this.handleTrackFormSubmit(e));
|
trackForm.addEventListener('submit', (e) => this.handleTrackFormSubmit(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stats period filter
|
||||||
|
const statsPeriod = document.getElementById('stats-period');
|
||||||
|
if (statsPeriod) {
|
||||||
|
statsPeriod.addEventListener('change', () => this.updateStats());
|
||||||
|
}
|
||||||
|
|
||||||
// Export/Import/Clear data buttons
|
// Export/Import/Clear data buttons
|
||||||
const exportBtn = document.getElementById('export-data');
|
const exportBtn = document.getElementById('export-data');
|
||||||
@ -1072,9 +1343,14 @@ class TeeTracker {
|
|||||||
<div class="empty-state-icon">🍃</div>
|
<div class="empty-state-icon">🍃</div>
|
||||||
<div class="empty-state-text">Keine Tee-Sorten vorhanden</div>
|
<div class="empty-state-text">Keine Tee-Sorten vorhanden</div>
|
||||||
<div class="empty-state-subtext">Füge deine erste Tee-Sorte hinzu!</div>
|
<div class="empty-state-subtext">Füge deine erste Tee-Sorte hinzu!</div>
|
||||||
<button class="btn btn-primary" onclick="app.openTeaModal()">➕ Tee hinzufügen</button>
|
<button class="btn btn-primary add-first-tea-btn">➕ Tee hinzufügen</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
// Add event listener for the add button
|
||||||
|
const addBtn = container.querySelector('.add-first-tea-btn');
|
||||||
|
if (addBtn) {
|
||||||
|
addBtn.addEventListener('click', () => this.openTeaModal());
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1120,7 +1396,7 @@ class TeeTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = teas.map(tea => `
|
container.innerHTML = teas.map(tea => `
|
||||||
<div class="tea-card" style="--tea-color: ${tea.color}">
|
<div class="tea-card" style="--tea-color: ${tea.color}" data-tea-id="${tea.id}">
|
||||||
<div class="tea-card-header">
|
<div class="tea-card-header">
|
||||||
<div class="tea-card-name">${tea.name} ${tea.organic ? '<span class="organic-badge" title="Bio-Tee">🌱</span>' : ''}</div>
|
<div class="tea-card-name">${tea.name} ${tea.organic ? '<span class="organic-badge" title="Bio-Tee">🌱</span>' : ''}</div>
|
||||||
<div class="tea-card-type">${tea.type}</div>
|
<div class="tea-card-type">${tea.type}</div>
|
||||||
@ -1131,10 +1407,10 @@ class TeeTracker {
|
|||||||
${tea.rating ? this.renderStarRatingStatic(tea.rating) : ''}
|
${tea.rating ? this.renderStarRatingStatic(tea.rating) : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="tea-card-footer">
|
<div class="tea-card-footer">
|
||||||
<button class="btn-icon" onclick="app.editTea('${tea.id}')" title="Bearbeiten">
|
<button class="btn-icon tea-edit-btn" title="Bearbeiten" data-tea-id="${tea.id}">
|
||||||
<span class="icon">✏️</span>
|
<span class="icon">✏️</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon btn-danger" onclick="app.showDeleteTeaConfirm('${tea.id}')" title="Löschen">
|
<button class="btn-icon btn-danger tea-delete-btn" title="Löschen" data-tea-id="${tea.id}">
|
||||||
<span class="icon">🗑️</span>
|
<span class="icon">🗑️</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -1579,7 +1855,8 @@ class TeeTracker {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
updateStats() {
|
updateStats() {
|
||||||
this.updateDetailedStats('all');
|
const period = document.getElementById('stats-period')?.value || 'all';
|
||||||
|
this.renderDetailedStats(period);
|
||||||
|
|
||||||
// Update charts
|
// Update charts
|
||||||
this.createTeaTypeChart();
|
this.createTeaTypeChart();
|
||||||
@ -1666,7 +1943,6 @@ class TeeTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize application
|
// Initialize application
|
||||||
let app;
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
app = new TeeTracker();
|
window.app = new TeeTracker();
|
||||||
});
|
});
|
||||||
|
|||||||
2
data/.gitkeep
Normal file
2
data/.gitkeep
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# This file ensures the data directory exists and is tracked by git
|
||||||
|
# TeeTracker stores teas.json and entries.json in this directory
|
||||||
Reference in New Issue
Block a user