mirror of
https://github.com/trevor1969/teatracker.git
synced 2026-08-09 10:41:59 +00:00
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:
866
app.js
Normal file
866
app.js
Normal 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
223
index.html
Normal 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">×</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">×</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
765
styles.css
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user