feat: Add TeeTracker web application

- Add index.html with responsive layout and tab navigation
- Add styles.css with modern design and tea-themed colors
- Add app.js with full functionality:
  - Tea management (add, edit, delete, search, filter)
  - Consumption tracking with date, time, amount, notes
  - Dashboard with statistics cards and recent activity
  - Statistics with Chart.js visualizations
  - Data persistence using localStorage
  - Toast notifications for user feedback
  - Confirmation modals for deletions

Features:
- Dashboard: Total cups, today's cups, weekly cups, tea count
- Tea Management: Add, edit, delete teas with type, brand, description
- Tracking: Record tea consumption with date, time, amount, notes
- Statistics: Charts and detailed stats with period filtering
- Responsive design for mobile and desktop
- German language interface

Generated by Vibe Code

Co-authored-by: trevor1969 <trevor1969@users.noreply.github.com>
This commit is contained in:
Vibe Nuage Agent
2026-06-04 10:35:40 +00:00
commit fc5d63604e
3 changed files with 1854 additions and 0 deletions

866
app.js Normal file
View File

@ -0,0 +1,866 @@
// TeeTracker - Main Application
// ============================================
// Data Storage & Management
// ============================================
const STORAGE_KEY_TEAS = 'teatracker_teas';
const STORAGE_KEY_ENTRIES = 'teatracker_entries';
class TeaTracker {
constructor() {
this.teas = this.loadTeas();
this.entries = this.loadEntries();
this.currentTeaId = null;
this.charts = {};
this.init();
}
// Load data from localStorage
loadTeas() {
const data = localStorage.getItem(STORAGE_KEY_TEAS);
return data ? JSON.parse(data) : [];
}
loadEntries() {
const data = localStorage.getItem(STORAGE_KEY_ENTRIES);
return data ? JSON.parse(data) : [];
}
// Save data to localStorage
saveTeas() {
localStorage.setItem(STORAGE_KEY_TEAS, JSON.stringify(this.teas));
}
saveEntries() {
localStorage.setItem(STORAGE_KEY_ENTRIES, JSON.stringify(this.entries));
}
// Generate unique ID
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// Get tea by ID
getTeaById(id) {
return this.teas.find(tea => tea.id === id);
}
// Get tea index by ID
getTeaIndexById(id) {
return this.teas.findIndex(tea => tea.id === id);
}
// ============================================
// Tea Management
// ============================================
addTea(tea) {
const newTea = {
id: this.generateId(),
name: tea.name,
type: tea.type,
brand: tea.brand || '',
description: tea.description || '',
color: tea.color || '#8B4513',
createdAt: new Date().toISOString()
};
this.teas.push(newTea);
this.saveTeas();
return newTea;
}
updateTea(id, updates) {
const index = this.getTeaIndexById(id);
if (index !== -1) {
this.teas[index] = { ...this.teas[index], ...updates };
this.saveTeas();
return this.teas[index];
}
return null;
}
deleteTea(id) {
const index = this.getTeaIndexById(id);
if (index !== -1) {
// Remove entries that reference this tea
this.entries = this.entries.filter(entry => entry.teaId !== id);
this.saveEntries();
this.teas.splice(index, 1);
this.saveTeas();
return true;
}
return false;
}
// ============================================
// Entry Management
// ============================================
addEntry(entry) {
const newEntry = {
id: this.generateId(),
teaId: entry.teaId,
date: entry.date,
time: entry.time || '',
amount: parseInt(entry.amount) || 1,
notes: entry.notes || '',
createdAt: new Date().toISOString()
};
this.entries.push(newEntry);
this.saveEntries();
return newEntry;
}
deleteEntry(id) {
const index = this.entries.findIndex(entry => entry.id === id);
if (index !== -1) {
this.entries.splice(index, 1);
this.saveEntries();
return true;
}
return false;
}
// ============================================
// Statistics
// ============================================
getTotalCups() {
return this.entries.reduce((sum, entry) => sum + entry.amount, 0);
}
getTodayCups() {
const today = new Date().toISOString().split('T')[0];
return this.entries
.filter(entry => entry.date === today)
.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) {
const now = new Date();
let startDate;
switch (period) {
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)
.reduce((sum, entry) => sum + entry.amount, 0);
}
getCupsByTeaType(period = 'all') {
const entries = this.getEntriesByPeriod(period);
const typeCounts = {};
entries.forEach(entry => {
const tea = this.getTeaById(entry.teaId);
if (tea) {
typeCounts[tea.type] = (typeCounts[tea.type] || 0) + entry.amount;
}
});
return typeCounts;
}
getCupsByTea(period = 'all') {
const entries = this.getEntriesByPeriod(period);
const teaCounts = {};
entries.forEach(entry => {
const tea = this.getTeaById(entry.teaId);
if (tea) {
teaCounts[tea.name] = (teaCounts[tea.name] || 0) + entry.amount;
}
});
return teaCounts;
}
getDailyConsumption(period = 'week') {
const entries = this.getEntriesByPeriod(period);
const dailyCounts = {};
entries.forEach(entry => {
const date = entry.date;
dailyCounts[date] = (dailyCounts[date] || 0) + entry.amount;
});
return dailyCounts;
}
getEntriesByPeriod(period) {
const now = new Date();
let startDate;
switch (period) {
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);
}
getMostConsumedTea(period = 'all') {
const teaCounts = this.getCupsByTea(period);
let maxCount = 0;
let mostConsumed = null;
for (const [teaName, count] of Object.entries(teaCounts)) {
if (count > maxCount) {
maxCount = count;
mostConsumed = teaName;
}
}
return { name: mostConsumed, count: maxCount };
}
getAverageDailyConsumption(period = 'month') {
const entries = this.getEntriesByPeriod(period);
if (entries.length === 0) return 0;
const totalDays = this.countUniqueDays(entries);
const totalCups = entries.reduce((sum, entry) => sum + entry.amount, 0);
return Math.round(totalCups / totalDays);
}
countUniqueDays(entries) {
const days = new Set();
entries.forEach(entry => days.add(entry.date));
return days.size;
}
// ============================================
// UI Initialization
// ============================================
init() {
this.setupEventListeners();
this.renderAll();
this.initCharts();
}
setupEventListeners() {
// Tab navigation
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab));
});
// Add tea button
document.getElementById('add-tea-btn').addEventListener('click', () => this.openTeaModal());
// Tea modal
document.getElementById('close-modal').addEventListener('click', () => this.closeTeaModal());
document.getElementById('tea-form').addEventListener('submit', (e) => this.handleTeaFormSubmit(e));
document.getElementById('delete-tea-btn').addEventListener('click', () => this.handleDeleteTea());
// Confirm modal
document.getElementById('close-confirm').addEventListener('click', () => this.closeConfirmModal());
document.getElementById('confirm-no').addEventListener('click', () => this.closeConfirmModal());
document.getElementById('confirm-yes').addEventListener('click', () => this.confirmDelete());
// Track form
document.getElementById('track-form').addEventListener('submit', (e) => this.handleTrackFormSubmit(e));
document.getElementById('cancel-track').addEventListener('click', () => this.resetTrackForm());
// Tea search and filter
document.getElementById('tea-search').addEventListener('input', (e) => this.filterTeas());
document.getElementById('tea-type-filter').addEventListener('change', (e) => this.filterTeas());
// Stats period filter
document.getElementById('stats-period').addEventListener('change', (e) => this.updateStats());
// Close modals on outside click
document.getElementById('tea-modal').addEventListener('click', (e) => {
if (e.target.id === 'tea-modal') this.closeTeaModal();
});
document.getElementById('confirm-modal').addEventListener('click', (e) => {
if (e.target.id === 'confirm-modal') this.closeConfirmModal();
});
}
switchTab(tabId) {
// Save current form data if needed
// Switch tabs
document.querySelectorAll('.tab-button').forEach(button => {
button.classList.toggle('active', button.dataset.tab === tabId);
});
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.classList.toggle('active', pane.id === tabId);
});
// Refresh content for specific tabs
if (tabId === 'teas') {
this.renderTeasList();
} else if (tabId === 'track') {
this.renderTrackForm();
} else if (tabId === 'stats') {
this.updateStats();
} else if (tabId === 'dashboard') {
this.updateDashboard();
}
}
renderAll() {
this.updateDashboard();
this.renderTeasList();
this.renderTrackForm();
this.updateStats();
}
// ============================================
// Dashboard
// ============================================
updateDashboard() {
document.getElementById('total-cups').textContent = `${this.getTotalCups()} Tassen`;
document.getElementById('today-cups').textContent = `${this.getTodayCups()} Tassen`;
document.getElementById('week-cups').textContent = `${this.getWeekCups()} Tassen`;
document.getElementById('total-teas').textContent = `${this.teas.length} Sorten`;
this.renderRecentEntries();
}
renderRecentEntries() {
const container = document.getElementById('recent-entries');
const recentEntries = [...this.entries]
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
.slice(0, 5);
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'}</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('');
}
// ============================================
// Tea Management UI
// ============================================
renderTeasList() {
const container = document.getElementById('teas-list');
if (this.teas.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">🍵</div>
<div class="empty-state-text">Keine Tee-Sorten vorhanden</div>
<div class="empty-state-subtext">Klicke auf "Tee hinzufügen", um deine erste Sorte anzulegen</div>
</div>
`;
return;
}
container.innerHTML = this.teas.map(tea => `
<div class="tea-card" data-id="${tea.id}">
<div class="tea-card-header">
<div>
<h3 class="tea-name">${tea.name}</h3>
<span class="tea-type">${tea.type}</span>
</div>
</div>
${tea.brand ? `<div class="tea-brand">Marke: ${tea.brand}</div>` : ''}
${tea.description ? `<div class="tea-description">${tea.description}</div>` : ''}
<div class="tea-actions">
<button class="tea-action-btn tea-edit-btn" onclick="app.editTea('${tea.id}')">Bearbeiten</button>
<button class="tea-action-btn tea-delete-btn" onclick="app.showDeleteTeaConfirm('${tea.id}')">Löschen</button>
</div>
</div>
`).join('');
}
filterTeas() {
const searchTerm = document.getElementById('tea-search').value.toLowerCase();
const typeFilter = document.getElementById('tea-type-filter').value;
const filteredTeas = this.teas.filter(tea => {
const matchesSearch = tea.name.toLowerCase().includes(searchTerm) ||
tea.brand.toLowerCase().includes(searchTerm) ||
tea.description.toLowerCase().includes(searchTerm);
const matchesType = typeFilter === 'all' || tea.type === typeFilter;
return matchesSearch && matchesType;
});
const container = document.getElementById('teas-list');
if (filteredTeas.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">🔍</div>
<div class="empty-state-text">Keine Tee-Sorten gefunden</div>
<div class="empty-state-subtext">Versuche andere Suchbegriffe oder Filter</div>
</div>
`;
return;
}
container.innerHTML = filteredTeas.map(tea => `
<div class="tea-card" data-id="${tea.id}">
<div class="tea-card-header">
<div>
<h3 class="tea-name">${tea.name}</h3>
<span class="tea-type">${tea.type}</span>
</div>
</div>
${tea.brand ? `<div class="tea-brand">Marke: ${tea.brand}</div>` : ''}
${tea.description ? `<div class="tea-description">${tea.description}</div>` : ''}
<div class="tea-actions">
<button class="tea-action-btn tea-edit-btn" onclick="app.editTea('${tea.id}')">Bearbeiten</button>
<button class="tea-action-btn tea-delete-btn" onclick="app.showDeleteTeaConfirm('${tea.id}')">Löschen</button>
</div>
</div>
`).join('');
}
openTeaModal(teaId = null) {
const modal = document.getElementById('tea-modal');
const form = document.getElementById('tea-form');
const deleteBtn = document.getElementById('delete-tea-btn');
if (teaId) {
const tea = this.getTeaById(teaId);
if (tea) {
document.getElementById('modal-title').textContent = 'Tee bearbeiten';
document.getElementById('tea-id').value = tea.id;
document.getElementById('tea-name').value = tea.name;
document.getElementById('tea-type').value = tea.type;
document.getElementById('tea-brand').value = tea.brand || '';
document.getElementById('tea-description').value = tea.description || '';
document.getElementById('tea-color').value = tea.color || '#8B4513';
deleteBtn.style.display = 'inline-flex';
this.currentTeaId = tea.id;
}
} else {
document.getElementById('modal-title').textContent = 'Tee hinzufügen';
form.reset();
document.getElementById('tea-id').value = '';
deleteBtn.style.display = 'none';
this.currentTeaId = null;
}
modal.classList.add('active');
}
closeTeaModal() {
document.getElementById('tea-modal').classList.remove('active');
this.currentTeaId = null;
}
handleTeaFormSubmit(e) {
e.preventDefault();
const formData = {
name: document.getElementById('tea-name').value.trim(),
type: document.getElementById('tea-type').value,
brand: document.getElementById('tea-brand').value.trim(),
description: document.getElementById('tea-description').value.trim(),
color: document.getElementById('tea-color').value
};
const teaId = document.getElementById('tea-id').value;
if (teaId) {
// Update existing tea
this.updateTea(teaId, formData);
this.showToast('Tee erfolgreich aktualisiert!', 'success');
} else {
// Add new tea
this.addTea(formData);
this.showToast('Tee erfolgreich hinzugefügt!', 'success');
}
this.closeTeaModal();
this.renderTeasList();
this.renderTrackForm();
this.updateDashboard();
this.updateStats();
}
editTea(teaId) {
this.openTeaModal(teaId);
}
showDeleteTeaConfirm(teaId) {
this.currentTeaId = teaId;
const tea = this.getTeaById(teaId);
document.getElementById('confirm-message').textContent =
`Möchtest du den Tee "${tea.name}" wirklich löschen? Alle zugehörigen Einträge werden ebenfalls gelöscht.`;
document.getElementById('confirm-modal').classList.add('active');
}
handleDeleteTea() {
if (this.currentTeaId) {
const tea = this.getTeaById(this.currentTeaId);
if (tea) {
document.getElementById('confirm-message').textContent =
`Möchtest du den Tee "${tea.name}" wirklich löschen? Alle zugehörigen Einträge werden ebenfalls gelöscht.`;
document.getElementById('confirm-modal').classList.add('active');
}
}
}
confirmDelete() {
if (this.currentTeaId) {
const tea = this.getTeaById(this.currentTeaId);
if (tea) {
this.deleteTea(this.currentTeaId);
this.showToast(`Tee "${tea.name}" wurde gelöscht.`, 'success');
this.closeConfirmModal();
this.closeTeaModal();
this.renderTeasList();
this.renderTrackForm();
this.updateDashboard();
this.updateStats();
}
}
}
closeConfirmModal() {
document.getElementById('confirm-modal').classList.remove('active');
this.currentTeaId = null;
}
// ============================================
// Track Form
// ============================================
renderTrackForm() {
const teaSelect = document.getElementById('track-tea');
const today = new Date().toISOString().split('T')[0];
// Set default date to today
document.getElementById('track-date').value = today;
// Populate tea dropdown
if (this.teas.length === 0) {
teaSelect.innerHTML = '<option value="">-- Keine Tee-Sorten verfügbar --</option>';
} else {
teaSelect.innerHTML = '<option value="">-- Wähle eine Tee-Sorte --</option>' +
this.teas.map(tea =>
`<option value="${tea.id}">${tea.name} (${tea.type})</option>`
).join('');
}
}
handleTrackFormSubmit(e) {
e.preventDefault();
const teaId = document.getElementById('track-tea').value;
const date = document.getElementById('track-date').value;
const time = document.getElementById('track-time').value;
const amount = document.getElementById('track-amount').value;
const notes = document.getElementById('track-notes').value.trim();
if (!teaId || !date || !amount) {
this.showToast('Bitte fülle alle Pflichtfelder aus!', 'error');
return;
}
const entry = {
teaId,
date,
time,
amount,
notes
};
this.addEntry(entry);
this.showToast('Tee-Konsum erfolgreich getrackt!', 'success');
// Reset form
this.resetTrackForm();
// Update UI
this.updateDashboard();
this.updateStats();
}
resetTrackForm() {
const form = document.getElementById('track-form');
form.reset();
const today = new Date().toISOString().split('T')[0];
document.getElementById('track-date').value = today;
document.getElementById('track-amount').value = 1;
}
// ============================================
// Statistics
// ============================================
initCharts() {
// Initialize Chart.js
this.createTeaTypeChart();
this.createDailyChart();
}
createTeaTypeChart() {
const ctx = document.getElementById('tea-type-chart');
if (!ctx) return;
const period = document.getElementById('stats-period').value || 'week';
const typeCounts = this.getCupsByTeaType(period);
const labels = Object.keys(typeCounts);
const data = Object.values(typeCounts);
const backgroundColors = labels.map((type, index) => {
const colors = [
'#8B4513', '#228B22', '#90EE90', '#FFD700',
'#FF6347', '#FF69B4', '#4169E1', '#8A2BE2'
];
return colors[index % colors.length];
});
if (this.charts.teaType) {
this.charts.teaType.destroy();
}
this.charts.teaType = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: backgroundColors,
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
padding: 15,
font: {
size: 12
}
}
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw || 0;
return `${label}: ${value} Tasse${value > 1 ? 'n' : ''}`;
}
}
}
},
layout: {
padding: 20
}
}
});
}
createDailyChart() {
const ctx = document.getElementById('daily-chart');
if (!ctx) return;
const period = document.getElementById('stats-period').value || 'week';
const dailyCounts = this.getDailyConsumption(period);
// Sort dates chronologically
const sortedDates = Object.keys(dailyCounts).sort();
const labels = sortedDates.map(date => {
const d = new Date(date);
return period === 'year' ? d.toLocaleDateString('de-DE', { month: 'short', day: 'numeric' }) : d.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric' });
});
const data = sortedDates.map(date => dailyCounts[date]);
if (this.charts.daily) {
this.charts.daily.destroy();
}
this.charts.daily = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Tassen pro Tag',
data: data,
backgroundColor: 'rgba(139, 69, 19, 0.7)',
borderColor: 'rgba(139, 69, 19, 1)',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
return `${context.raw} Tasse${context.raw > 1 ? 'n' : ''}`;
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1,
callback: function(value) {
return value + (value === 1 ? ' Tasse' : ' Tassen');
}
}
}
},
layout: {
padding: 20
}
}
});
}
updateStats() {
const period = document.getElementById('stats-period').value || 'week';
// Update charts
this.createTeaTypeChart();
this.createDailyChart();
// Update detailed stats
this.renderDetailedStats(period);
}
renderDetailedStats(period) {
const container = document.getElementById('stats-details');
const entries = this.getEntriesByPeriod(period);
const totalCups = entries.reduce((sum, entry) => sum + entry.amount, 0);
const uniqueDays = this.countUniqueDays(entries);
const avgDaily = uniqueDays > 0 ? Math.round(totalCups / uniqueDays) : 0;
const mostConsumed = this.getMostConsumedTea(period);
const teaCount = new Set(entries.map(entry => entry.teaId)).size;
container.innerHTML = `
<div class="detail-item">
<div class="detail-item-label">Gesamt getrunken</div>
<div class="detail-item-value">${totalCups} Tassen</div>
</div>
<div class="detail-item">
<div class="detail-item-label">Durchschnitt pro Tag</div>
<div class="detail-item-value">${avgDaily} Tassen</div>
</div>
<div class="detail-item">
<div class="detail-item-label">Aktive Tage</div>
<div class="detail-item-value">${uniqueDays} Tage</div>
</div>
<div class="detail-item">
<div class="detail-item-label">Verschiedene Tees</div>
<div class="detail-item-value">${teaCount} Sorten</div>
</div>
${mostConsumed.name ? `
<div class="detail-item">
<div class="detail-item-label">Meist getrunken</div>
<div class="detail-item-value">${mostConsumed.name}</div>
</div>
<div class="detail-item">
<div class="detail-item-label">Favorit (Anzahl)</div>
<div class="detail-item-value">${mostConsumed.count} Tassen</div>
</div>
` : ''}
`;
}
// ============================================
// Utility Functions
// ============================================
showToast(message, type = 'info') {
// Remove existing toasts
const existingToast = document.querySelector('.toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'toastSlideIn 0.3s ease reverse';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
}
// Initialize the application
let app;
document.addEventListener('DOMContentLoaded', () => {
app = new TeaTracker();
window.app = app;
});

223
index.html Normal file
View File

@ -0,0 +1,223 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TeeTracker - Dein Tee-Konsum Tracker</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="app-container">
<header>
<h1>🍵 TeeTracker</h1>
<p>Verwalte deine Tee-Sorten und tracke deinen Konsum</p>
</header>
<nav class="tabs">
<button class="tab-button active" data-tab="dashboard">Dashboard</button>
<button class="tab-button" data-tab="teas">Tee-Sorten</button>
<button class="tab-button" data-tab="track">Tracken</button>
<button class="tab-button" data-tab="stats">Statistiken</button>
</nav>
<main class="tab-content">
<!-- Dashboard Tab -->
<section id="dashboard" class="tab-pane active">
<div class="dashboard-cards">
<div class="card">
<h3>Gesamt getrunken</h3>
<div class="card-value" id="total-cups">0 Tassen</div>
</div>
<div class="card">
<h3>Heute</h3>
<div class="card-value" id="today-cups">0 Tassen</div>
</div>
<div class="card">
<h3>Diese Woche</h3>
<div class="card-value" id="week-cups">0 Tassen</div>
</div>
<div class="card">
<h3>Tee-Sorten</h3>
<div class="card-value" id="total-teas">0 Sorten</div>
</div>
</div>
<div class="recent-activity">
<h3>Letzte Aktivitäten</h3>
<div id="recent-entries" class="activity-list"></div>
</div>
</section>
<!-- Tee-Sorten Tab -->
<section id="teas" class="tab-pane">
<div class="teas-header">
<h2>Tee-Sorten</h2>
<button id="add-tea-btn" class="btn btn-primary"> Tee hinzufügen</button>
</div>
<div class="tea-filters">
<input type="text" id="tea-search" placeholder="Suche nach Tee-Sorten...">
<select id="tea-type-filter">
<option value="all">Alle Typen</option>
<option value="Schwarztee">Schwarztee</option>
<option value="Grüntee">Grüntee</option>
<option value="Weißtee">Weißtee</option>
<option value="Oolong">Oolong</option>
<option value="Pu-Erh">Pu-Erh</option>
<option value="Früchtetee">Früchtetee</option>
<option value="Krätertee">Krätertee</option>
<option value="Sonstiges">Sonstiges</option>
</select>
</div>
<div id="teas-list" class="teas-grid"></div>
</section>
<!-- Tracken Tab -->
<section id="track" class="tab-pane">
<h2>Tee-Konsum tracken</h2>
<form id="track-form" class="track-form">
<div class="form-group">
<label for="track-tea">Tee-Sorte *</label>
<select id="track-tea" required>
<option value="">-- Wähle eine Tee-Sorte --</option>
</select>
</div>
<div class="form-group">
<label for="track-date">Datum *</label>
<input type="date" id="track-date" required>
</div>
<div class="form-group">
<label for="track-time">Uhrzeit</label>
<input type="time" id="track-time">
</div>
<div class="form-group">
<label for="track-amount">Menge (Tassen) *</label>
<input type="number" id="track-amount" min="1" max="10" value="1" required>
</div>
<div class="form-group">
<label for="track-notes">Notizen</label>
<textarea id="track-notes" rows="3" placeholder="z.B. Mit Honig, besonders stark..."></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">📝 Tracken</button>
<button type="button" id="cancel-track" class="btn btn-secondary">Abbrechen</button>
</div>
</form>
</section>
<!-- Statistiken Tab -->
<section id="stats" class="tab-pane">
<h2>Statistiken</h2>
<div class="stats-filters">
<label for="stats-period">Zeitraum:</label>
<select id="stats-period">
<option value="week">Letzte 7 Tage</option>
<option value="month">Letzter Monat</option>
<option value="year">Letztes Jahr</option>
<option value="all">Gesamt</option>
</select>
</div>
<div class="stats-container">
<div class="chart-container">
<h3>Tee-Konsum nach Sorte</h3>
<canvas id="tea-type-chart"></canvas>
</div>
<div class="chart-container">
<h3>Tagesverlauf</h3>
<canvas id="daily-chart"></canvas>
</div>
</div>
<div class="stats-details">
<h3>Detaillierte Statistiken</h3>
<div id="stats-details" class="details-grid"></div>
</div>
</section>
</main>
<!-- Modal für Tee hinzufügen/bearbeiten -->
<div id="tea-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">Tee hinzufügen</h2>
<button id="close-modal" class="close-btn">&times;</button>
</div>
<form id="tea-form" class="modal-form">
<input type="hidden" id="tea-id">
<div class="form-group">
<label for="tea-name">Name *</label>
<input type="text" id="tea-name" required placeholder="z.B. Earl Grey">
</div>
<div class="form-group">
<label for="tea-type">Typ *</label>
<select id="tea-type" required>
<option value="">-- Wähle einen Typ --</option>
<option value="Schwarztee">Schwarztee</option>
<option value="Grüntee">Grüntee</option>
<option value="Weißtee">Weißtee</option>
<option value="Oolong">Oolong</option>
<option value="Pu-Erh">Pu-Erh</option>
<option value="Früchtetee">Früchtetee</option>
<option value="Krätertee">Krätertee</option>
<option value="Sonstiges">Sonstiges</option>
</select>
</div>
<div class="form-group">
<label for="tea-brand">Marke</label>
<input type="text" id="tea-brand" placeholder="z.B. Twinings, Pukka">
</div>
<div class="form-group">
<label for="tea-description">Beschreibung</label>
<textarea id="tea-description" rows="3" placeholder="Beschreibung des Tees..."></textarea>
</div>
<div class="form-group">
<label for="tea-color">Farbe</label>
<input type="color" id="tea-color" value="#8B4513">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">💾 Speichern</button>
<button type="button" id="delete-tea-btn" class="btn btn-danger" style="display: none;">🗑️ Löschen</button>
</div>
</form>
</div>
</div>
<!-- Bestätigungsmodal -->
<div id="confirm-modal" class="modal">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h2>Bestätigung</h2>
<button id="close-confirm" class="close-btn">&times;</button>
</div>
<div class="modal-body">
<p id="confirm-message"></p>
</div>
<div class="modal-footer">
<button id="confirm-yes" class="btn btn-danger">Ja, löschen</button>
<button id="confirm-no" class="btn btn-secondary">Abbrechen</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="app.js"></script>
</body>
</html>

765
styles.css Normal file
View File

@ -0,0 +1,765 @@
/* TeeTracker - Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-color: #8B4513;
--primary-light: #A0522D;
--primary-dark: #654321;
--secondary-color: #4ECDC4;
--background-color: #F5F5F5;
--card-bg: #FFFFFF;
--text-color: #333333;
--text-light: #666666;
--success-color: #28a745;
--danger-color: #dc3545;
--warning-color: #ffc107;
--border-radius: 8px;
--box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
min-height: 100vh;
}
.app-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Header */
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
border-radius: var(--border-radius);
color: white;
box-shadow: var(--box-shadow);
}
header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
}
header p {
font-size: 1.1rem;
opacity: 0.9;
}
/* Tabs */
.tabs {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tab-button {
padding: 12px 24px;
background-color: var(--card-bg);
border: 2px solid var(--primary-color);
border-radius: var(--border-radius);
cursor: pointer;
font-size: 1rem;
font-weight: 600;
color: var(--primary-color);
transition: var(--transition);
box-shadow: var(--box-shadow);
}
.tab-button:hover {
background-color: var(--primary-light);
color: white;
}
.tab-button.active {
background-color: var(--primary-color);
color: white;
}
/* Tab Content */
.tab-content {
min-height: 60vh;
}
.tab-pane {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-pane.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Dashboard Cards */
.dashboard-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background-color: var(--card-bg);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--box-shadow);
text-align: center;
transition: var(--transition);
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.card h3 {
font-size: 1rem;
color: var(--text-light);
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 1px;
}
.card-value {
font-size: 2rem;
font-weight: bold;
color: var(--primary-color);
}
/* Recent Activity */
.recent-activity {
background-color: var(--card-bg);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--box-shadow);
}
.recent-activity h3 {
margin-bottom: 15px;
color: var(--text-color);
border-bottom: 2px solid var(--primary-light);
padding-bottom: 10px;
}
.activity-list {
max-height: 300px;
overflow-y: auto;
}
.activity-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-bottom: 1px solid #eee;
transition: var(--transition);
}
.activity-item:last-child {
border-bottom: none;
}
.activity-item:hover {
background-color: #f9f9f9;
}
.activity-info {
flex: 1;
}
.activity-tea {
font-weight: 600;
color: var(--primary-color);
margin-bottom: 5px;
}
.activity-details {
font-size: 0.9rem;
color: var(--text-light);
}
.activity-amount {
font-weight: bold;
color: var(--secondary-color);
font-size: 1.1rem;
}
/* Teas Section */
.teas-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.teas-header h2 {
color: var(--text-color);
}
.tea-filters {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.tea-filters input,
.tea-filters select {
padding: 10px 15px;
border: 2px solid #ddd;
border-radius: var(--border-radius);
font-size: 1rem;
flex: 1;
min-width: 200px;
transition: var(--transition);
}
.tea-filters input:focus,
.tea-filters select:focus {
outline: none;
border-color: var(--primary-color);
}
.teas-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.tea-card {
background-color: var(--card-bg);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--box-shadow);
cursor: pointer;
transition: var(--transition);
border-left: 5px solid var(--primary-color);
}
.tea-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.tea-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10px;
}
.tea-name {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-color);
}
.tea-type {
background-color: var(--primary-light);
color: white;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.8rem;
text-transform: uppercase;
}
.tea-brand {
font-size: 0.9rem;
color: var(--text-light);
margin-bottom: 10px;
}
.tea-description {
font-size: 0.9rem;
color: var(--text-light);
margin-bottom: 15px;
line-height: 1.5;
}
.tea-actions {
display: flex;
gap: 10px;
}
.tea-action-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
transition: var(--transition);
}
.tea-edit-btn {
background-color: var(--secondary-color);
color: white;
}
.tea-edit-btn:hover {
background-color: #3aa89e;
}
.tea-delete-btn {
background-color: var(--danger-color);
color: white;
}
.tea-delete-btn:hover {
background-color: #c82333;
}
/* Track Form */
.track-form {
background-color: var(--card-bg);
border-radius: var(--border-radius);
padding: 30px;
box-shadow: var(--box-shadow);
max-width: 600px;
margin: 0 auto;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--text-color);
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group input[type="date"],
.form-group input[type="time"],
.form-group input[type="color"],
.form-group select,
.form-group textarea {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: var(--border-radius);
font-size: 1rem;
transition: var(--transition);
font-family: inherit;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary-color);
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.form-actions {
display: flex;
gap: 15px;
justify-content: flex-end;
margin-top: 10px;
}
/* Buttons */
.btn {
padding: 12px 24px;
border: none;
border-radius: var(--border-radius);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-dark);
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.btn-danger {
background-color: var(--danger-color);
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.btn-success {
background-color: var(--success-color);
color: white;
}
.btn-success:hover {
background-color: #218838;
}
/* Stats Section */
.stats-filters {
margin-bottom: 20px;
}
.stats-filters label {
margin-right: 10px;
font-weight: 600;
}
.stats-filters select {
padding: 10px 15px;
border: 2px solid #ddd;
border-radius: var(--border-radius);
font-size: 1rem;
}
.stats-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.chart-container {
background-color: var(--card-bg);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--box-shadow);
}
.chart-container h3 {
margin-bottom: 15px;
color: var(--text-color);
text-align: center;
}
.chart-container canvas {
max-height: 300px;
}
.stats-details {
background-color: var(--card-bg);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--box-shadow);
}
.stats-details h3 {
margin-bottom: 15px;
color: var(--text-color);
}
.details-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
.detail-item {
background-color: #f9f9f9;
padding: 15px;
border-radius: var(--border-radius);
text-align: center;
}
.detail-item-label {
font-size: 0.9rem;
color: var(--text-light);
margin-bottom: 5px;
}
.detail-item-value {
font-size: 1.3rem;
font-weight: bold;
color: var(--primary-color);
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal.active {
display: flex;
}
.modal-content {
background-color: var(--card-bg);
border-radius: var(--border-radius);
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
animation: modalSlideIn 0.3s ease;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 2px solid #eee;
}
.modal-header h2 {
color: var(--text-color);
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-light);
transition: var(--transition);
}
.close-btn:hover {
color: var(--danger-color);
}
.modal-form {
padding: 20px;
}
.modal-body {
padding: 20px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 15px;
padding: 20px;
border-top: 2px solid #eee;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--primary-dark);
}
/* Responsive */
@media (max-width: 768px) {
.app-container {
padding: 10px;
}
header h1 {
font-size: 2rem;
}
.tabs {
flex-direction: column;
align-items: stretch;
}
.tab-button {
width: 100%;
}
.dashboard-cards {
grid-template-columns: repeat(2, 1fr);
}
.stats-container {
grid-template-columns: 1fr;
}
.tea-filters {
flex-direction: column;
}
.tea-filters input,
.tea-filters select {
width: 100%;
}
.form-actions {
flex-direction: column;
align-items: stretch;
}
.btn {
width: 100%;
justify-content: center;
}
}
@media (max-width: 480px) {
.dashboard-cards {
grid-template-columns: 1fr;
}
.teas-grid {
grid-template-columns: 1fr;
}
.details-grid {
grid-template-columns: 1fr;
}
}
/* Empty State */
.empty-state {
text-align: center;
padding: 40px;
color: var(--text-light);
}
.empty-state-icon {
font-size: 3rem;
margin-bottom: 15px;
}
.empty-state-text {
font-size: 1.1rem;
margin-bottom: 10px;
}
.empty-state-subtext {
font-size: 0.9rem;
}
/* Loading State */
.loading {
text-align: center;
padding: 20px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid var(--primary-color);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Toast Notifications */
.toast {
position: fixed;
bottom: 20px;
right: 20px;
background-color: var(--text-color);
color: white;
padding: 15px 25px;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
z-index: 1001;
animation: toastSlideIn 0.3s ease;
}
.toast.success {
background-color: var(--success-color);
}
.toast.error {
background-color: var(--danger-color);
}
.toast.info {
background-color: var(--secondary-color);
}
@keyframes toastSlideIn {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Tea Color Preview */
.tea-color-preview {
width: 20px;
height: 20px;
border-radius: 50%;
display: inline-block;
margin-right: 10px;
border: 2px solid #ddd;
}