mirror of
https://github.com/trevor1969/teatracker.git
synced 2026-08-09 10:41:59 +00:00
feat: Add bio checkbox, today period, and recent entries on track page
- Add organic/bio checkbox to tea form and display 🌱 badge - Add 'Heute' (today) option to stats period filter - Add recent 10 entries display on track page below form - Update tea cards and dropdowns to show bio indicator - Add migration for existing teas without organic field - Style organic badge with green color and hover effect - Update all entry displays to show bio status Generated by Vibe Code Co-authored-by: trevor1969 <trevor1969@users.noreply.github.com>
This commit is contained in:
116
app.js
116
app.js
@ -20,7 +20,14 @@ class TeaTracker {
|
|||||||
// Load data from localStorage
|
// Load data from localStorage
|
||||||
loadTeas() {
|
loadTeas() {
|
||||||
const data = localStorage.getItem(STORAGE_KEY_TEAS);
|
const data = localStorage.getItem(STORAGE_KEY_TEAS);
|
||||||
return data ? JSON.parse(data) : [];
|
if (!data) return [];
|
||||||
|
|
||||||
|
const teas = JSON.parse(data);
|
||||||
|
// Migrate existing teas to add organic field if missing
|
||||||
|
return teas.map(tea => ({
|
||||||
|
...tea,
|
||||||
|
organic: tea.organic !== undefined ? tea.organic : false
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
loadEntries() {
|
loadEntries() {
|
||||||
@ -64,6 +71,7 @@ class TeaTracker {
|
|||||||
brand: tea.brand || '',
|
brand: tea.brand || '',
|
||||||
description: tea.description || '',
|
description: tea.description || '',
|
||||||
color: tea.color || '#8B4513',
|
color: tea.color || '#8B4513',
|
||||||
|
organic: tea.organic || false,
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
this.teas.push(newTea);
|
this.teas.push(newTea);
|
||||||
@ -139,20 +147,14 @@ class TeaTracker {
|
|||||||
.reduce((sum, entry) => sum + entry.amount, 0);
|
.reduce((sum, entry) => sum + entry.amount, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
getWeekCups() {
|
|
||||||
const now = new Date();
|
|
||||||
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
return this.entries
|
|
||||||
.filter(entry => new Date(entry.date) >= weekAgo)
|
|
||||||
.reduce((sum, entry) => sum + entry.amount, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCupsByPeriod(period) {
|
getCupsByPeriod(period) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let startDate;
|
let startDate;
|
||||||
|
|
||||||
switch (period) {
|
switch (period) {
|
||||||
|
case 'today':
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
|
break;
|
||||||
case 'week':
|
case 'week':
|
||||||
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||||
break;
|
break;
|
||||||
@ -174,6 +176,47 @@ class TeaTracker {
|
|||||||
.reduce((sum, entry) => sum + entry.amount, 0);
|
.reduce((sum, entry) => sum + entry.amount, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getWeekCups() {
|
||||||
|
const now = new Date();
|
||||||
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
return this.entries
|
||||||
|
.filter(entry => new Date(entry.date) >= weekAgo)
|
||||||
|
.reduce((sum, entry) => sum + entry.amount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntriesByPeriod(period) {
|
||||||
|
const now = new Date();
|
||||||
|
let startDate;
|
||||||
|
|
||||||
|
switch (period) {
|
||||||
|
case 'today':
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
|
||||||
|
break;
|
||||||
|
case 'year':
|
||||||
|
startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
|
||||||
|
break;
|
||||||
|
case 'all':
|
||||||
|
startDate = new Date(0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.entries.filter(entry => new Date(entry.date) >= startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCupsByPeriod(period) {
|
||||||
|
const entries = this.getEntriesByPeriod(period);
|
||||||
|
return entries.reduce((sum, entry) => sum + entry.amount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
getCupsByTeaType(period = 'all') {
|
getCupsByTeaType(period = 'all') {
|
||||||
const entries = this.getEntriesByPeriod(period);
|
const entries = this.getEntriesByPeriod(period);
|
||||||
const typeCounts = {};
|
const typeCounts = {};
|
||||||
@ -389,7 +432,7 @@ class TeaTracker {
|
|||||||
return `
|
return `
|
||||||
<div class="activity-item">
|
<div class="activity-item">
|
||||||
<div class="activity-info">
|
<div class="activity-info">
|
||||||
<div class="activity-tea">${tea ? tea.name : 'Unbekannter Tee'}</div>
|
<div class="activity-tea">${tea ? tea.name : 'Unbekannter Tee'} ${tea && tea.organic ? '<span class="organic-badge" title="Bio-Tee">🌱</span>' : ''}</div>
|
||||||
<div class="activity-details">${formattedDate} ${formattedTime ? `um ${formattedTime}` : ''}</div>
|
<div class="activity-details">${formattedDate} ${formattedTime ? `um ${formattedTime}` : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-amount">${entry.amount} Tasse${entry.amount > 1 ? 'n' : ''}</div>
|
<div class="activity-amount">${entry.amount} Tasse${entry.amount > 1 ? 'n' : ''}</div>
|
||||||
@ -420,7 +463,7 @@ class TeaTracker {
|
|||||||
<div class="tea-card" data-id="${tea.id}">
|
<div class="tea-card" data-id="${tea.id}">
|
||||||
<div class="tea-card-header">
|
<div class="tea-card-header">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="tea-name">${tea.name}</h3>
|
<h3 class="tea-name">${tea.name} ${tea.organic ? '<span class="organic-badge" title="Bio-Tee">🌱</span>' : ''}</h3>
|
||||||
<span class="tea-type">${tea.type}</span>
|
<span class="tea-type">${tea.type}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -463,7 +506,7 @@ class TeaTracker {
|
|||||||
<div class="tea-card" data-id="${tea.id}">
|
<div class="tea-card" data-id="${tea.id}">
|
||||||
<div class="tea-card-header">
|
<div class="tea-card-header">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="tea-name">${tea.name}</h3>
|
<h3 class="tea-name">${tea.name} ${tea.organic ? '<span class="organic-badge" title="Bio-Tee">🌱</span>' : ''}</h3>
|
||||||
<span class="tea-type">${tea.type}</span>
|
<span class="tea-type">${tea.type}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -477,6 +520,42 @@ class TeaTracker {
|
|||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render recent entries on track page
|
||||||
|
renderTrackRecentEntries() {
|
||||||
|
const container = document.getElementById('track-recent-entries');
|
||||||
|
const recentEntries = [...this.entries]
|
||||||
|
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
|
if (recentEntries.length === 0) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-state-icon">📝</div>
|
||||||
|
<div class="empty-state-text">Keine Einträge vorhanden</div>
|
||||||
|
<div class="empty-state-subtext">Fange an, deinen Tee-Konsum zu tracken!</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = recentEntries.map(entry => {
|
||||||
|
const tea = this.getTeaById(entry.teaId);
|
||||||
|
const date = new Date(entry.date);
|
||||||
|
const formattedDate = date.toLocaleDateString('de-DE');
|
||||||
|
const formattedTime = entry.time ? entry.time.substring(0, 5) : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="activity-item">
|
||||||
|
<div class="activity-info">
|
||||||
|
<div class="activity-tea">${tea ? tea.name : 'Unbekannter Tee'} ${tea && tea.organic ? '<span class="organic-badge" title="Bio-Tee">🌱</span>' : ''}</div>
|
||||||
|
<div class="activity-details">${formattedDate} ${formattedTime ? `um ${formattedTime}` : ''}</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-amount">${entry.amount} Tasse${entry.amount > 1 ? 'n' : ''}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
openTeaModal(teaId = null) {
|
openTeaModal(teaId = null) {
|
||||||
const modal = document.getElementById('tea-modal');
|
const modal = document.getElementById('tea-modal');
|
||||||
const form = document.getElementById('tea-form');
|
const form = document.getElementById('tea-form');
|
||||||
@ -492,6 +571,7 @@ class TeaTracker {
|
|||||||
document.getElementById('tea-brand').value = tea.brand || '';
|
document.getElementById('tea-brand').value = tea.brand || '';
|
||||||
document.getElementById('tea-description').value = tea.description || '';
|
document.getElementById('tea-description').value = tea.description || '';
|
||||||
document.getElementById('tea-color').value = tea.color || '#8B4513';
|
document.getElementById('tea-color').value = tea.color || '#8B4513';
|
||||||
|
document.getElementById('tea-organic').checked = tea.organic || false;
|
||||||
deleteBtn.style.display = 'inline-flex';
|
deleteBtn.style.display = 'inline-flex';
|
||||||
this.currentTeaId = tea.id;
|
this.currentTeaId = tea.id;
|
||||||
}
|
}
|
||||||
@ -508,6 +588,7 @@ class TeaTracker {
|
|||||||
|
|
||||||
closeTeaModal() {
|
closeTeaModal() {
|
||||||
document.getElementById('tea-modal').classList.remove('active');
|
document.getElementById('tea-modal').classList.remove('active');
|
||||||
|
document.getElementById('tea-form').reset();
|
||||||
this.currentTeaId = null;
|
this.currentTeaId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,7 +600,8 @@ class TeaTracker {
|
|||||||
type: document.getElementById('tea-type').value,
|
type: document.getElementById('tea-type').value,
|
||||||
brand: document.getElementById('tea-brand').value.trim(),
|
brand: document.getElementById('tea-brand').value.trim(),
|
||||||
description: document.getElementById('tea-description').value.trim(),
|
description: document.getElementById('tea-description').value.trim(),
|
||||||
color: document.getElementById('tea-color').value
|
color: document.getElementById('tea-color').value,
|
||||||
|
organic: document.getElementById('tea-organic').checked
|
||||||
};
|
};
|
||||||
|
|
||||||
const teaId = document.getElementById('tea-id').value;
|
const teaId = document.getElementById('tea-id').value;
|
||||||
@ -602,9 +684,12 @@ class TeaTracker {
|
|||||||
} else {
|
} else {
|
||||||
teaSelect.innerHTML = '<option value="">-- Wähle eine Tee-Sorte --</option>' +
|
teaSelect.innerHTML = '<option value="">-- Wähle eine Tee-Sorte --</option>' +
|
||||||
this.teas.map(tea =>
|
this.teas.map(tea =>
|
||||||
`<option value="${tea.id}">${tea.name} (${tea.type})</option>`
|
`<option value="${tea.id}">${tea.name} (${tea.type})${tea.organic ? ' 🌱' : ''}</option>`
|
||||||
).join('');
|
).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render recent entries
|
||||||
|
this.renderTrackRecentEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTrackFormSubmit(e) {
|
handleTrackFormSubmit(e) {
|
||||||
@ -638,6 +723,7 @@ class TeaTracker {
|
|||||||
// Update UI
|
// Update UI
|
||||||
this.updateDashboard();
|
this.updateDashboard();
|
||||||
this.updateStats();
|
this.updateStats();
|
||||||
|
this.renderTrackRecentEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
resetTrackForm() {
|
resetTrackForm() {
|
||||||
|
|||||||
14
index.html
14
index.html
@ -110,6 +110,11 @@
|
|||||||
<button type="button" id="cancel-track" class="btn btn-secondary">Abbrechen</button>
|
<button type="button" id="cancel-track" class="btn btn-secondary">Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="recent-tracked">
|
||||||
|
<h3>Letzte Einträge</h3>
|
||||||
|
<div id="track-recent-entries" class="activity-list"></div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Statistiken Tab -->
|
<!-- Statistiken Tab -->
|
||||||
@ -119,6 +124,7 @@
|
|||||||
<div class="stats-filters">
|
<div class="stats-filters">
|
||||||
<label for="stats-period">Zeitraum:</label>
|
<label for="stats-period">Zeitraum:</label>
|
||||||
<select id="stats-period">
|
<select id="stats-period">
|
||||||
|
<option value="today">Heute</option>
|
||||||
<option value="week">Letzte 7 Tage</option>
|
<option value="week">Letzte 7 Tage</option>
|
||||||
<option value="month">Letzter Monat</option>
|
<option value="month">Letzter Monat</option>
|
||||||
<option value="year">Letztes Jahr</option>
|
<option value="year">Letztes Jahr</option>
|
||||||
@ -186,6 +192,14 @@
|
|||||||
<textarea id="tea-description" rows="3" placeholder="Beschreibung des Tees..."></textarea>
|
<textarea id="tea-description" rows="3" placeholder="Beschreibung des Tees..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="tea-organic">
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
Bio-Tee
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="tea-color">Farbe</label>
|
<label for="tea-color">Farbe</label>
|
||||||
<input type="color" id="tea-color" value="#8B4513">
|
<input type="color" id="tea-color" value="#8B4513">
|
||||||
|
|||||||
52
styles.css
52
styles.css
@ -378,6 +378,45 @@ header p {
|
|||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Checkbox Styling */
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Recent tracked section */
|
||||||
|
.recent-tracked {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-tracked h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: var(--text-color);
|
||||||
|
border-bottom: 2px solid var(--primary-light);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-actions {
|
.form-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
@ -763,3 +802,16 @@ header p {
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
border: 2px solid #ddd;
|
border: 2px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Organic Badge */
|
||||||
|
.organic-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-left: 5px;
|
||||||
|
color: var(--success-color);
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organic-badge:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user