Feature: Einstiegsseite, Einkaufsliste, Wochenplan-Live-Suche & Zufallsgenerator-Optimierung

This commit is contained in:
2026-10-15 13:05:08 +02:00
parent 2b8c4a35cf
commit 0d6d0deb69
6 changed files with 2240 additions and 67 deletions
+639
View File
@@ -0,0 +1,639 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einkaufsliste – What To Cook?</title>
<link rel="stylesheet" href="/static/cookbook.css?v=4.5.4">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛒</text></svg>">
<style>
body.shopping-body {
background-color: var(--grau-bg, #e6e2dc);
background-image: radial-gradient(rgba(30, 77, 59, 0.06) 1px, transparent 1px);
background-size: 20px 20px;
min-height: 100vh;
margin: 0;
padding: 0;
font-family: var(--font-sans);
color: var(--schwarz);
display: flex;
flex-direction: column;
}
/* Top Nav */
.shop-nav {
background-color: var(--creme);
border-bottom: 2px solid var(--schwarz);
padding: 10px 18px;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 4px 10px rgba(0,0,0,0.08);
}
.shop-nav-brand {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: inherit;
}
.shop-nav-title {
font-family: var(--font-hand);
font-size: 1.6rem;
font-weight: 700;
color: var(--tannengruen);
margin: 0;
line-height: 1;
}
.shop-nav-links {
display: flex;
align-items: center;
gap: 8px;
}
.shop-nav-btn {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--creme);
border: 1.5px solid var(--schwarz);
border-radius: 8px;
padding: 6px 12px;
font-size: 0.88rem;
font-weight: 600;
color: var(--schwarz);
text-decoration: none;
box-shadow: 2px 2px 0px rgba(0,0,0,0.12);
transition: all 0.15s;
}
.shop-nav-btn:hover {
background: #fff;
transform: translateY(-1px);
}
/* Hauptbereich Notizblock */
.shop-main {
max-width: 720px;
width: 100%;
margin: 28px auto 60px auto;
padding: 0 16px;
box-sizing: border-box;
flex: 1;
}
.notepad-card {
background: #FFFDF7;
border: 2.5px solid var(--schwarz);
border-radius: 14px;
box-shadow: 6px 8px 0px rgba(0,0,0,0.15);
position: relative;
overflow: hidden;
padding: 24px 22px;
}
/* Rote Randlinie wie beim klassischen Notizblock */
.notepad-card::before {
content: '';
position: absolute;
top: 0; bottom: 0; left: 44px;
width: 2px;
background-color: rgba(210, 155, 155, 0.4);
pointer-events: none;
}
.notepad-header {
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 2px dashed rgba(30, 77, 59, 0.3);
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 10px;
}
.notepad-title-group h2 {
font-family: var(--font-hand);
font-size: 2.3rem;
color: var(--tannengruen);
margin: 0 0 2px 0;
line-height: 1.1;
}
.notepad-subtitle {
font-size: 0.88rem;
color: #666;
margin: 0;
}
.notepad-counter {
font-family: var(--font-hand);
font-size: 1.15rem;
font-weight: 700;
color: var(--tannengruen);
background: #FFF9D2;
border: 1.5px dashed #D4C165;
padding: 4px 10px;
border-radius: 6px;
}
/* Schnelleingabe */
.shop-add-form {
display: flex;
gap: 8px;
margin-bottom: 22px;
background: #FFF9D2;
border: 2px solid var(--schwarz);
border-radius: 10px;
padding: 8px;
box-shadow: 3px 3px 0px rgba(0,0,0,0.1);
}
.shop-input {
flex: 1;
border: none;
background: transparent;
font-family: var(--font-sans);
font-size: 1rem;
padding: 8px 10px;
outline: none;
color: var(--schwarz);
}
.shop-input::placeholder {
color: #8c8260;
}
.shop-add-btn {
background: var(--tannengruen);
color: var(--creme);
border: 1.5px solid var(--schwarz);
border-radius: 6px;
padding: 8px 16px;
font-weight: 700;
font-size: 0.95rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.15s;
}
.shop-add-btn:hover {
background: #27624c;
}
/* Aktionen Toolbar */
.shop-actions-bar {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px dotted rgba(0,0,0,0.15);
}
.shop-action-btn {
background: transparent;
border: 1px solid rgba(0,0,0,0.25);
border-radius: 6px;
padding: 4px 8px;
font-size: 0.8rem;
color: #555;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
transition: all 0.15s;
}
.shop-action-btn:hover {
background: rgba(0,0,0,0.05);
color: #222;
}
.shop-action-btn.btn-wochenplan {
background: rgba(30, 77, 59, 0.1);
border-color: rgba(30, 77, 59, 0.3);
color: var(--tannengruen);
font-weight: 600;
}
.shop-action-btn.btn-wochenplan:hover {
background: rgba(30, 77, 59, 0.18);
}
/* Die Einkaufsliste */
.shopping-items-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.shopping-item-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 12px 8px 48px;
border-bottom: 1px solid rgba(0,0,0,0.06);
transition: background 0.15s;
border-radius: 6px;
}
.shopping-item-row:hover {
background: rgba(30, 77, 59, 0.03);
}
.shopping-item-row.is-checked {
opacity: 0.55;
background: rgba(0, 0, 0, 0.02);
}
.shopping-item-row.is-checked .item-text {
text-decoration: line-through;
color: #777;
}
.item-left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
cursor: pointer;
}
.item-checkbox {
width: 22px;
height: 22px;
border: 2px solid var(--schwarz);
border-radius: 6px;
cursor: pointer;
accent-color: var(--tannengruen);
flex-shrink: 0;
}
.item-text {
font-size: 1.05rem;
font-weight: 500;
color: #222;
line-height: 1.3;
word-break: break-word;
}
.item-source-badge {
font-size: 0.72rem;
background: rgba(30, 77, 59, 0.08);
color: var(--tannengruen);
padding: 2px 6px;
border-radius: 4px;
margin-left: 6px;
display: inline-block;
white-space: nowrap;
}
.item-delete-btn {
background: transparent;
border: none;
color: #aaa;
font-size: 1.1rem;
cursor: pointer;
padding: 4px 6px;
border-radius: 4px;
transition: color 0.15s;
}
.item-delete-btn:hover {
color: #b00;
background: rgba(200, 0, 0, 0.08);
}
/* Leerer Zustand */
.shopping-empty-state {
text-align: center;
padding: 40px 20px;
}
.empty-icon {
font-size: 3rem;
margin-bottom: 8px;
}
.empty-title {
font-family: var(--font-hand);
font-size: 1.7rem;
color: var(--tannengruen);
margin: 0 0 6px 0;
}
.empty-desc {
color: #666;
font-size: 0.95rem;
max-width: 400px;
margin: 0 auto 18px auto;
}
/* Toast Notification */
.shop-toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--tannengruen);
color: var(--creme);
padding: 10px 20px;
border-radius: 30px;
font-weight: 600;
font-size: 0.95rem;
box-shadow: 0 6px 16px rgba(0,0,0,0.25);
border: 1.5px solid var(--schwarz);
z-index: 9999;
opacity: 0;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
pointer-events: none;
}
.shop-toast.active {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
</style>
</head>
<body class="shopping-body">
<!-- Nav -->
<header class="shop-nav">
<a href="/" class="shop-nav-brand">
<span style="font-size: 1.6rem;">🛒</span>
<div>
<h1 class="shop-nav-title">Einkaufsliste</h1>
</div>
</a>
<div class="shop-nav-links">
<a href="/" class="shop-nav-btn">
<span>🏠</span> <span>Menü</span>
</a>
<a href="/cookbook" class="shop-nav-btn">
<span>📖</span> <span>Kochbuch</span>
</a>
<a href="/cookbook?open=mealplan" class="shop-nav-btn">
<span>📅</span> <span>Wochenplan</span>
</a>
</div>
</header>
<!-- Main Container -->
<main class="shop-main">
<div class="notepad-card">
<div class="notepad-header">
<div class="notepad-title-group">
<h2>Einkaufszettel</h2>
<p class="notepad-subtitle">Woche {{ current_week }} • Zutaten & Besorgungen</p>
</div>
<div class="notepad-counter" id="shop-counter">
<span id="checked-count">0</span> / <span id="total-count">{{ items|length }}</span> erledigt
</div>
</div>
<!-- Schnelleingabe -->
<form class="shop-add-form" id="shop-add-form" onsubmit="onAddItemSubmit(event)">
<input
type="text"
id="shop-add-input"
class="shop-input"
placeholder="✏️ Zutat oder Artikel notieren (z. B. 500g Mehl)..."
autocomplete="off"
required
>
<button type="submit" class="shop-add-btn">
<span>➕</span> <span>Hinzufügen</span>
</button>
</form>
<!-- Toolbar Aktionen -->
<div class="shop-actions-bar">
<div>
<a href="/cookbook?open=mealplan&prompt_transfer=1" class="shop-action-btn btn-wochenplan" title="Zutaten aus dem aktuellen Wochenplan abrufen">
<span>📅</span> <span>Aus Wochenplan übernehmen</span>
</a>
</div>
<div style="display: flex; gap: 6px;">
<button type="button" class="shop-action-btn" onclick="deleteCheckedItems()" title="Abgehakte Einträge löschen">
<span>🧹</span> <span>Erledigte löschen</span>
</button>
<button type="button" class="shop-action-btn" onclick="clearAllItems()" title="Ganze Liste leeren">
<span>🗑️</span> <span>Alles leeren</span>
</button>
</div>
</div>
<!-- Items Liste -->
<ul class="shopping-items-list" id="shopping-items-list">
{% for item in items %}
<li class="shopping-item-row {% if item.checked %}is-checked{% endif %}" id="shop-item-{{ item.id }}" data-id="{{ item.id }}">
<label class="item-left">
<input
type="checkbox"
class="item-checkbox"
{% if item.checked %}checked{% endif %}
onchange="toggleItem({{ item.id }})"
>
<span class="item-text">
{% if item.amount or item.unit %}
<strong>{{ item.amount }} {{ item.unit }}</strong>
{% endif %}
{{ item.item_name }}
{% if item.source %}
<span class="item-source-badge">{{ item.source }}</span>
{% endif %}
</span>
</label>
<button type="button" class="item-delete-btn" onclick="deleteSingleItem({{ item.id }})" title="Löschen">&times;</button>
</li>
{% endfor %}
</ul>
<!-- Leerer Zustand (wenn 0 Items) -->
<div class="shopping-empty-state" id="shopping-empty-state" style="{% if items|length > 0 %}display: none;{% endif %}">
<div class="empty-icon">🎉</div>
<h3 class="empty-title">Alles eingekauft!</h3>
<p class="empty-desc">Deine Einkaufsliste ist momentan leer. Füge oben neue Dinge hinzu oder übernimm Zutaten direkt aus deinem Wochenplan.</p>
<a href="/cookbook?open=mealplan&prompt_transfer=1" class="shop-nav-btn" style="background: var(--tannengruen); color: var(--creme); font-weight: bold; padding: 10px 18px;">
<span>📅</span> <span>Zutaten aus Wochenplan holen</span>
</a>
</div>
</div>
</main>
<div id="shop-toast" class="shop-toast">✓ Gespeichert</div>
<script>
function showToast(msg) {
const toast = document.getElementById('shop-toast');
toast.textContent = msg;
toast.classList.add('active');
setTimeout(() => toast.classList.remove('active'), 2500);
}
function updateCounter() {
const rows = document.querySelectorAll('.shopping-item-row');
const total = rows.length;
const checked = document.querySelectorAll('.shopping-item-row.is-checked').length;
document.getElementById('checked-count').textContent = checked;
document.getElementById('total-count').textContent = total;
const emptyState = document.getElementById('shopping-empty-state');
if (total === 0) {
emptyState.style.display = 'block';
} else {
emptyState.style.display = 'none';
}
}
async function onAddItemSubmit(e) {
e.preventDefault();
const input = document.getElementById('shop-add-input');
const text = input.value.trim();
if (!text) return;
try {
const res = await fetch('/api/shopping-list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ item_name: text, source: 'Manuell' })
});
const data = await res.json();
if (data.status === 'ok' && data.item) {
renderNewItem(data.item);
input.value = '';
showToast(`✓ "${text}" hinzugefügt`);
updateCounter();
}
} catch (err) {
alert('Fehler beim Hinzufügen: ' + err.message);
}
}
function renderNewItem(item) {
const ul = document.getElementById('shopping-items-list');
const li = document.createElement('li');
li.className = `shopping-item-row ${item.checked ? 'is-checked' : ''}`;
li.id = `shop-item-${item.id}`;
li.setAttribute('data-id', item.id);
const amountStr = (item.amount || item.unit) ? `<strong>${escapeHtml(item.amount || '')} ${escapeHtml(item.unit || '')}</strong> ` : '';
const sourceStr = item.source ? `<span class="item-source-badge">${escapeHtml(item.source)}</span>` : '';
li.innerHTML = `
<label class="item-left">
<input
type="checkbox"
class="item-checkbox"
${item.checked ? 'checked' : ''}
onchange="toggleItem(${item.id})"
>
<span class="item-text">
${amountStr}${escapeHtml(item.item_name)}${sourceStr}
</span>
</label>
<button type="button" class="item-delete-btn" onclick="deleteSingleItem(${item.id})" title="Löschen">&times;</button>
`;
// Vor den abgehakten Items einfügen
const firstChecked = ul.querySelector('.shopping-item-row.is-checked');
if (firstChecked) {
ul.insertBefore(li, firstChecked);
} else {
ul.appendChild(li);
}
}
async function toggleItem(id) {
const row = document.getElementById(`shop-item-${id}`);
if (!row) return;
try {
const res = await fetch(`/api/shopping-list/${id}/toggle`, { method: 'POST' });
const data = await res.json();
if (data.status === 'ok' && data.item) {
if (data.item.checked) {
row.classList.add('is-checked');
// Nach unten schieben
const ul = document.getElementById('shopping-items-list');
ul.appendChild(row);
} else {
row.classList.remove('is-checked');
}
updateCounter();
}
} catch (err) {
console.error(err);
}
}
async function deleteSingleItem(id) {
const row = document.getElementById(`shop-item-${id}`);
if (row) row.remove();
updateCounter();
try {
await fetch(`/api/shopping-list/${id}`, { method: 'DELETE' });
} catch (err) {
console.error(err);
}
}
async function deleteCheckedItems() {
const checkedRows = document.querySelectorAll('.shopping-item-row.is-checked');
if (checkedRows.length === 0) {
showToast('Keine erledigten Einträge vorhanden.');
return;
}
checkedRows.forEach(r => r.remove());
updateCounter();
showToast('✓ Erledigte Einträge gelöscht');
try {
await fetch('/api/shopping-list/checked', { method: 'DELETE' });
} catch (err) {
console.error(err);
}
}
async function clearAllItems() {
const total = document.querySelectorAll('.shopping-item-row').length;
if (total === 0) return;
if (!confirm('Möchtest du wirklich die gesamte Einkaufsliste leeren?')) return;
document.getElementById('shopping-items-list').innerHTML = '';
updateCounter();
showToast('✓ Einkaufsliste geleert');
try {
await fetch('/api/shopping-list/all', { method: 'DELETE' });
} catch (err) {
console.error(err);
}
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
document.addEventListener('DOMContentLoaded', updateCounter);
</script>
</body>
</html>