Files
cookbook/app/templates/shopping_list.html
T

640 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>