Initial commit: What To Cook (WTC) v4.4 - Digitales Kochbuch, Wochenmenueplan & Buchdruck

This commit is contained in:
2026-10-14 20:24:37 +02:00
commit c01d2d7639
171 changed files with 14073 additions and 0 deletions
+1877
View File
File diff suppressed because it is too large Load Diff
+227
View File
@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" content="#1e222d">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>What To Cook? – Essens-Zufallsgenerator</title>
<link rel="manifest" href="/static/manifest.json">
<link rel="apple-touch-icon" href="/static/icon-192.png">
<link rel="stylesheet" href="/static/style.css">
<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>">
</head>
<body>
<div class="app-container">
<!-- Top Header -->
<header class="app-header">
<div class="brand">
<span class="brand-icon">🍽️</span>
<div>
<h1 class="brand-title">What To Cook?</h1>
<p class="brand-subtitle">Zufallsgenerator für Essen</p>
</div>
</div>
<!-- Tab Navigation -->
<nav class="nav-tabs" role="tablist">
<button id="tab-btn-generator" class="nav-tab active" data-tab="generator" role="tab" aria-selected="true">
<span class="icon">🎲</span> Generator
</button>
<a href="/cookbook" class="nav-tab" style="text-decoration: none;">
<span class="icon">📖</span> Kochbuch
</a>
<a href="/cookbook/new" class="nav-tab" style="text-decoration: none;">
<span class="icon">➕</span> Rezept
</a>
<button id="tab-btn-admin" class="nav-tab" data-tab="admin" role="tab" aria-selected="false">
<span class="icon">⚙️</span> Gerichte
</button>
</nav>
</header>
<!-- Main Content Area -->
<main class="app-main">
<!-- ========================================== -->
<!-- TAB 1: GENERATOR -->
<!-- ========================================== -->
<section id="tab-generator" class="tab-panel active" role="tabpanel">
<!-- Banner / Hint -->
<div class="info-card">
<div class="info-content">
<span class="info-icon">💡</span>
<div>
<strong>Bis zu 5 Gerichte konfigurieren:</strong>
Wähle Kategorie & Aufwand pro Gericht. Nicht benötigte Gerichte einfach frei lassen.
</div>
</div>
<div class="active-counter-badge" id="active-slots-counter">
<span id="active-slots-count">2</span> von 5 aktiv
</div>
</div>
<!-- 5 Dish Configuration Slots -->
<div class="slots-container" id="slots-container">
<!-- Rendered dynamically by app.js (1 to 5) -->
</div>
<!-- Floating / Sticky Action Bar -->
<div class="action-bar-wrapper">
<button id="btn-roll-all" class="btn btn-primary btn-hero">
<span class="btn-icon">🎲</span>
<span class="btn-text">Zufallsgenerator starten</span>
</button>
</div>
<!-- Results Section (Anchor / Modal or Inline display) -->
<section id="results-section" class="results-section hidden">
<div class="results-header">
<div class="results-title-group">
<h2 class="results-title">Ausgeloste Gerichte</h2>
<span class="results-badge" id="results-count-badge">0 Gerichte</span>
</div>
<button id="btn-reroll-all" class="btn btn-outline btn-sm">
<span class="icon">🔄</span> Alle neu würfeln
</button>
</div>
<div id="results-list" class="results-grid">
<!-- Dynamic Result Cards -->
</div>
<div class="results-footer">
<button id="btn-back-to-config" class="btn btn-secondary">
✏️ Filter anpassen
</button>
</div>
</section>
</section>
<!-- ========================================== -->
<!-- TAB 2: ADMIN / DATABASE -->
<!-- ========================================== -->
<section id="tab-admin" class="tab-panel" role="tabpanel" hidden>
<div class="admin-topbar">
<div>
<h2 class="section-title">Gerichte verwalten</h2>
<p class="section-desc" id="admin-stats-summary">Lade Datenbank...</p>
</div>
<button id="btn-open-add-dish" class="btn btn-primary">
<span>➕</span> Neues Gericht
</button>
</div>
<!-- Admin Filters -->
<div class="admin-filters-card">
<div class="search-input-wrap">
<span class="search-icon">🔍</span>
<input type="text" id="admin-search-input" placeholder="Gericht nach Namen suchen..." autocomplete="off">
<button id="admin-search-clear" class="btn-clear hidden" title="Suche leeren">✕</button>
</div>
<div class="filter-row">
<div class="form-group flex-1">
<label for="admin-filter-category" class="filter-label">Kategorie:</label>
<select id="admin-filter-category" class="custom-select">
<option value="alle">Alle Kategorien</option>
<!-- Filled via JS -->
</select>
</div>
<div class="form-group flex-1">
<label for="admin-filter-effort" class="filter-label">Aufwand:</label>
<select id="admin-filter-effort" class="custom-select">
<option value="alle">Jeder Aufwand</option>
<option value="gering">Gering 🟢</option>
<option value="mittel">Mittel 🟡</option>
<option value="hoch">Hoch 🔴</option>
</select>
</div>
</div>
</div>
<!-- Dishes Table / Cards for Mobile -->
<div class="dishes-list-wrap">
<div class="dishes-list-header">
<span id="dishes-count-label">Gerichte</span>
</div>
<div id="admin-dishes-list" class="admin-dishes-list">
<div class="loading-spinner">Lade Gerichte...</div>
</div>
</div>
</section>
</main>
<!-- Dialog: Add / Edit Dish -->
<dialog id="dish-modal" class="modal">
<div class="modal-box">
<div class="modal-header">
<h3 id="modal-title" class="modal-title">Neues Gericht anlegen</h3>
<button id="btn-close-modal" class="btn-icon-close" aria-label="Schließen">✕</button>
</div>
<form id="dish-form" method="dialog">
<input type="hidden" id="dish-form-id" value="">
<div class="form-group">
<label for="dish-name-input" class="form-label">Name des Gerichts *</label>
<input type="text" id="dish-name-input" class="text-input" placeholder="z. B. Spaghetti Carbonara" required autocomplete="off">
</div>
<div class="form-group">
<label class="form-label">Kategorien * <span class="form-label-hint">(Mehrfachauswahl möglich)</span></label>
<div class="category-checkbox-group" id="modal-category-group">
<!-- Filled via JS as checkbox pills -->
</div>
</div>
<div class="form-group">
<label class="form-label">Arbeitsaufwand *</label>
<div class="effort-radio-group">
<label class="effort-radio-label">
<input type="radio" name="modal-effort" value="gering" required>
<span class="effort-pill effort-gering">Gering 🟢 (≤ 20 Min)</span>
</label>
<label class="effort-radio-label">
<input type="radio" name="modal-effort" value="mittel" checked required>
<span class="effort-pill effort-mittel">Mittel 🟡 (~ 45 Min)</span>
</label>
<label class="effort-radio-label">
<input type="radio" name="modal-effort" value="hoch" required>
<span class="effort-pill effort-hoch">Hoch 🔴 (> 60 Min)</span>
</label>
</div>
</div>
<div class="modal-actions">
<button type="button" id="btn-cancel-modal" class="btn btn-secondary">Abbrechen</button>
<button type="submit" id="btn-save-modal" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</dialog>
<!-- Dialog: Delete Confirm -->
<dialog id="delete-modal" class="modal">
<div class="modal-box modal-box-sm">
<h3 class="modal-title">Gericht löschen?</h3>
<p id="delete-modal-text" class="modal-text">Möchtest du dieses Gericht wirklich aus der Datenbank entfernen?</p>
<div class="modal-actions">
<button id="btn-cancel-delete" class="btn btn-secondary">Abbrechen</button>
<button id="btn-confirm-delete" class="btn btn-danger">Unwiderruflich löschen</button>
</div>
</div>
</dialog>
<!-- Toast Notifications -->
<div id="toast-container" class="toast-container" aria-live="polite"></div>
</div>
<script src="/static/app.js"></script>
</body>
</html>
+187
View File
@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Anmeldung — WTC Kochbuch</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@600;700&family=Lora:ital,wght@0,400;0,600;1,400&family=Playfair+Display:ital,wght@0,600;0,700;0,800;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/cookbook.css?v=4.0">
<style>
body.login-body {
background-color: var(--creme, #faf6ee);
background-image:
radial-gradient(circle at 100% 0%, rgba(212, 175, 55, 0.08) 0%, transparent 40%),
radial-gradient(circle at 0% 100%, rgba(30, 61, 47, 0.08) 0%, transparent 40%),
repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0, 0, 0, 0.015) 2px, rgba(0, 0, 0, 0.015) 4px);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
margin: 0;
font-family: var(--font-body, 'Lora', Georgia, serif);
color: var(--schwarz, #1a1a1a);
}
.login-card {
background: #ffffff;
border: 3px solid var(--tannengruen, #1e3d2f);
border-radius: 16px;
box-shadow: 6px 8px 0px rgba(30, 61, 47, 0.25);
width: 100%;
max-width: 420px;
padding: 36px 30px;
text-align: center;
position: relative;
}
.login-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
background: var(--creme, #faf6ee);
border: 2px solid var(--gold, #d4af37);
border-radius: 50%;
font-size: 2rem;
margin-bottom: 12px;
box-shadow: 0 4px 10px rgba(0,0,0,0.06);
}
.login-title {
font-family: var(--font-hand, 'Caveat', cursive);
font-size: 2.2rem;
color: var(--tannengruen, #1e3d2f);
margin: 0 0 4px 0;
line-height: 1.1;
}
.login-subtitle {
font-size: 0.95rem;
color: #666;
margin: 0 0 24px 0;
}
.login-form {
display: flex;
flex-direction: column;
gap: 16px;
text-align: left;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-weight: 600;
font-size: 0.9rem;
color: var(--tannengruen, #1e3d2f);
}
.form-input {
padding: 10px 14px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
background: #fafafa;
transition: border-color 0.2s, background-color 0.2s;
}
.form-input:focus {
outline: none;
border-color: var(--tannengruen, #1e3d2f);
background: #ffffff;
box-shadow: 0 0 0 3px rgba(30, 61, 47, 0.12);
}
.form-checkbox-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.88rem;
color: #555;
margin-top: 2px;
}
.form-checkbox-row input {
accent-color: var(--tannengruen, #1e3d2f);
width: 16px;
height: 16px;
cursor: pointer;
}
.login-btn {
background: var(--tannengruen, #1e3d2f);
color: var(--creme, #faf6ee);
border: 2px solid var(--tannengruen, #1e3d2f);
border-radius: 8px;
font-family: var(--font-hand, 'Caveat', cursive);
font-size: 1.25rem;
font-weight: 700;
padding: 10px 16px;
cursor: pointer;
box-shadow: 3px 4px 0px rgba(0, 0, 0, 0.15);
transition: all 0.18s ease;
margin-top: 8px;
}
.login-btn:hover {
background: #274f3d;
border-color: #274f3d;
transform: translateY(-1px);
box-shadow: 3px 5px 0px rgba(0, 0, 0, 0.22);
}
.login-btn:active {
transform: translateY(0);
box-shadow: 1px 2px 0px rgba(0, 0, 0, 0.15);
}
.login-error {
background: #fdf2f2;
border: 1px solid #f8b4b4;
color: #9b1c1c;
padding: 10px 14px;
border-radius: 8px;
font-size: 0.88rem;
margin-bottom: 16px;
text-align: left;
}
.login-footer {
margin-top: 24px;
font-size: 0.82rem;
color: #888;
}
</style>
</head>
<body class="login-body">
<div class="login-card">
<div class="login-badge">📖</div>
<h1 class="login-title">WTC Kochbuch</h1>
<p class="login-subtitle">Privater Bereich • Bitte anmelden</p>
{% if error %}
<div class="login-error">
⚠️ {{ error }}
</div>
{% endif %}
<form method="POST" action="/login" class="login-form">
<input type="hidden" name="next" value="{{ next_url or '/cookbook' }}">
<div class="form-group">
<label class="form-label" for="username">Benutzername</label>
<input class="form-input" type="text" id="username" name="username" value="{{ username or 'admin' }}" required autofocus autocomplete="username">
</div>
<div class="form-group">
<label class="form-label" for="password">Passwort</label>
<input class="form-input" type="password" id="password" name="password" required autocomplete="current-password" placeholder="Passwort eingeben">
</div>
<div class="form-checkbox-row">
<input type="checkbox" id="remember" name="remember" value="true" checked>
<label for="remember">Angemeldet bleiben (30 Tage)</label>
</div>
<button type="submit" class="login-btn">Anmelden ✨</button>
</form>
<div class="login-footer">
What To Cook? • Privates Rezeptarchiv & Kochbuch
</div>
</div>
</body>
</html>
+230
View File
@@ -0,0 +1,230 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ recipe.title }} – Mein Kochbuch</title>
<link rel="stylesheet" href="/static/cookbook.css?v=4.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>">
</head>
<body class="cookbook-body">
<!-- Navigation Bar -->
<header class="cb-nav">
<a href="/cookbook" class="cb-nav-brand">
<span class="cb-brand-icon">📖</span>
<h1 class="cb-nav-title">Kochbuch</h1>
</a>
<!-- Desktop Navigation -->
<div class="cb-nav-actions desktop-nav-actions">
<a href="/cookbook" class="cb-btn cb-btn-outline">
<span>📖</span> <span>Alle Rezepte</span>
</a>
<a href="/cookbook/edit/{{ recipe.id }}" class="cb-btn cb-btn-green">
<span>✏️</span> <span>Bearbeiten</span>
</a>
<button onclick="window.print()" class="cb-btn cb-btn-outline" title="Rezeptseite drucken">
<span>🖨️</span> <span>Drucken</span>
</button>
<a href="/logout" class="cb-nav-btn cb-nav-btn-logout" title="Abmelden">
<span>🚪</span> <span>Abmelden</span>
</a>
</div>
<!-- Mobile Kebab (3-Punkte) Menü -->
<div class="mobile-nav-wrapper">
<button type="button" class="cb-kebab-btn" onclick="toggleMobileMenu(event)" aria-label="Menü öffnen" title="Menü">
<span>⋮</span>
</button>
<div id="cb-mobile-menu" class="cb-mobile-dropdown">
<a href="/cookbook" class="cb-mobile-item">
<span>📖</span> <span>Zum Kochbuch</span>
</a>
<a href="/cookbook/edit/{{ recipe.id }}" class="cb-mobile-item">
<span>✏️</span> <span>Rezept bearbeiten</span>
</a>
<button type="button" onclick="window.print()" class="cb-mobile-item">
<span>🖨️</span> <span>Drucken & PDF</span>
</button>
<hr class="cb-mobile-divider">
<a href="/logout" class="cb-mobile-item cb-mobile-logout">
<span>🚪</span> <span>Abmelden</span>
</a>
</div>
</div>
</header>
<main class="book-wrapper">
<!-- EINHEITLICHE REZEPTSEITE -->
<article class="page recipe-page">
<!-- Header (Ohne Kapitelangabe & ohne Quelle im Titel) -->
<header class="recipe-header">
<h2 class="recipe-title">{{ recipe.title }}</h2>
<div class="recipe-meta-row">
{% if recipe.effort %}
<div class="recipe-meta-item">
<span>⏱️</span>
<span>Aufwand:
{% if recipe.effort == 'gering' %}Gering 🟢{% elif recipe.effort == 'hoch' %}Hoch 🔴{% else %}Mittel 🟡{% endif %}
</span>
</div>
{% endif %}
{% if recipe.prep_time %}
<div class="recipe-meta-item">
<span>🔪</span>
<span>Zubereitung: {{ recipe.prep_time }}</span>
</div>
{% endif %}
{% if recipe.cook_time %}
<div class="recipe-meta-item">
<span>🍳</span>
<span>Kochen/Backen: {{ recipe.cook_time }}</span>
</div>
{% endif %}
{% if recipe.portions %}
<div class="recipe-meta-item">
<span>👥</span>
<span>{{ recipe.portions }} Portionen</span>
</div>
{% endif %}
{% if recipe.source and recipe.source.strip() %}
<div class="recipe-meta-item">
<span>📖</span>
<span>Quelle: {{ recipe.source.strip() }}</span>
</div>
{% endif %}
</div>
</header>
<!-- 2-Spalten Layout -->
<div class="recipe-grid">
<!-- Linke Spalte: Foto + Zutaten -->
<aside class="recipe-side">
<div class="recipe-photo-box">
{% if recipe.image_url %}
<img src="{{ recipe.image_url }}" alt="{{ recipe.title }}">
{% else %}
<div class="recipe-photo-empty">
<span>📸</span>
<span>Kein Foto hinterlegt</span>
</div>
{% endif %}
</div>
<!-- Zutatenleiste ohne Portionsangabe (da schon in Kopfzeile) -->
<div class="ingredients-box {% if recipe.has_portion_table %}has-portion-matrix{% endif %}">
<h4 class="section-hand-title">Zutaten</h4>
{% if recipe.has_portion_table and recipe.portion_matrix %}
<!-- Tabelle für 2/4/6/8 Portionen (z. B. Waffeln / Pancakes) -->
<table class="portion-matrix-table">
<thead>
<tr>
<th class="matrix-th-ing">Zutat</th>
<th class="matrix-th-p">2 P.</th>
<th class="matrix-th-p">4 P.</th>
<th class="matrix-th-p">6 P.</th>
<th class="matrix-th-p">8 P.</th>
</tr>
</thead>
<tbody>
{% for row in recipe.portion_matrix %}
{% if row.is_header %}
<tr class="matrix-header-row">
<th colspan="5">{{ row.text }}</th>
</tr>
{% else %}
<tr class="matrix-row">
<td class="matrix-td-name">{{ row.name }}</td>
<td class="matrix-td-amt">{{ row.amounts[2] }}</td>
<td class="matrix-td-amt highlight">{{ row.amounts[4] }}</td>
<td class="matrix-td-amt">{{ row.amounts[6] }}</td>
<td class="matrix-td-amt">{{ row.amounts[8] }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% else %}
<!-- Normale kompakte Zutatenliste (kein Abschneiden im Druck) -->
<ul class="ingredients-list">
{% for line in recipe.ingredients.split('\n') %}
{% if line.strip() %}
{% set raw_line = line.strip() %}
{% if raw_line.endswith(':') or raw_line.lower().startswith('für ') %}
<li class="ing-subheading">{{ raw_line }}</li>
{% else %}
<li>{{ raw_line }}</li>
{% endif %}
{% endif %}
{% endfor %}
</ul>
{% endif %}
</div>
</aside>
<!-- Rechte Spalte: Zubereitung + Notizen + Quelle -->
<section class="recipe-main">
<div>
<h4 class="section-hand-title">Zubereitung</h4>
<div class="instructions-steps">
{% for step in recipe.instructions.split('\n') %}
{% if step.strip() %}
<div class="step-card">
<span class="step-badge">{{ loop.index }}</span>
<div class="step-text">{{ step.strip() }}</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% if recipe.notes and recipe.notes.strip() %}
<div class="notes-box">
<div class="notes-box-title">💡 Tipp & Notiz</div>
<div>{{ recipe.notes.strip() }}</div>
</div>
{% else %}
<div class="notes-box notes-box-empty print-only">
<div class="notes-box-title">💡 Eigene Notizen & Tipps</div>
<div class="notes-blank-lines">
<div class="notes-blank-line"></div>
<div class="notes-blank-line"></div>
</div>
</div>
</div>
{% endif %}
</section>
</div>
</article>
</main>
<script>
function toggleMobileMenu(e) {
if (e) e.stopPropagation();
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.toggle('is-open');
}
document.addEventListener('click', (e) => {
if (!e.target.closest('.mobile-nav-wrapper')) {
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.remove('is-open');
}
});
</script>
</body>
</html>
+357
View File
@@ -0,0 +1,357 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if recipe %}Rezept bearbeiten{% else %}Neues Rezept eintragen{% endif %} – Mein Kochbuch</title>
<link rel="stylesheet" href="/static/cookbook.css?v=4.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>">
</head>
<body class="cookbook-body">
<!-- Header -->
<header class="cb-nav">
<a href="/cookbook" class="cb-nav-brand">
<span class="cb-brand-icon">📖</span>
<h1 class="cb-nav-title">Kochbuch</h1>
</a>
<!-- Desktop Actions -->
<div class="cb-nav-actions desktop-nav-actions">
<a href="/cookbook" class="cb-btn cb-btn-creme" style="background: var(--tannengruen); color: var(--creme);">
<span>📖</span> <span>Zum Kochbuch</span>
</a>
<a href="/logout" class="cb-nav-btn cb-nav-btn-logout" title="Abmelden">
<span>🚪</span> <span>Abmelden</span>
</a>
</div>
<!-- Mobile 3-Dots Kebab Menu -->
<div class="mobile-nav-wrapper">
<button type="button" class="cb-kebab-btn" id="mobileMenuBtn" aria-label="Menü öffnen" onclick="toggleMobileMenu()">
⋮
</button>
<div class="cb-mobile-dropdown" id="mobileDropdown">
<a href="/cookbook" class="cb-mobile-item">
<span>📖</span> Zum Kochbuch
</a>
<div class="cb-mobile-divider"></div>
<a href="/logout" class="cb-mobile-item" style="color: #c53030;">
<span>🚪</span> Abmelden
</a>
</div>
</div>
</header>
<main class="book-wrapper" style="max-width: 780px;">
<div class="form-card">
<h2 class="form-title">
{% if recipe %}✏️ Rezept bearbeiten{% else %}➕ Neues Rezept eintragen{% endif %}
</h2>
<p class="form-subtitle">
Trage dein Lieblingsrezept ein – mit Foto, Zutaten und Zubereitungsschritten.
</p>
<form id="recipe-form" enctype="multipart/form-data">
{% if recipe %}
<input type="hidden" id="recipe-id" value="{{ recipe.id }}">
{% endif %}
<!-- Titel -->
<div class="cb-form-group">
<label for="title" class="cb-label">Name des Rezepts *</label>
<input type="text" id="title" name="title" class="cb-input"
value="{{ recipe.title if recipe else '' }}"
placeholder="z. B. Klassische Spaghetti Carbonara" required autocomplete="off">
</div>
<!-- Kategorie / Kapitel -->
<div class="cb-form-group">
<label for="chapter_number" class="cb-label">Kategorie / Kapitel *</label>
<select id="chapter_number" name="chapter_number" class="cb-select" required>
<option value="" disabled {% if not recipe and not selected_chapter %}selected{% endif %}>Kapitel wählen...</option>
{% for chap in chapters %}
<option value="{{ chap.number }}"
{% if (recipe and recipe.chapter_number == chap.number) or (not recipe and selected_chapter == chap.number) %}selected{% endif %}>
Kapitel {{ chap.number }}: {{ chap.title }}
</option>
{% endfor %}
</select>
</div>
<!-- Metadaten: Aufwand, Zeiten, Portionen -->
<div class="cb-row">
<div class="cb-col">
<div class="cb-form-group">
<label for="effort" class="cb-label">Arbeitsaufwand *</label>
<select id="effort" name="effort" class="cb-select" required>
<option value="gering" {% if recipe and recipe.effort == 'gering' %}selected{% endif %}>Gering 🟢 (≤ 20 Min)</option>
<option value="mittel" {% if not recipe or recipe.effort == 'mittel' %}selected{% endif %}>Mittel 🟡 (~ 45 Min)</option>
<option value="hoch" {% if recipe and recipe.effort == 'hoch' %}selected{% endif %}>Hoch 🔴 (> 60 Min)</option>
</select>
</div>
</div>
<div class="cb-col">
<div class="cb-form-group">
<label for="portions" class="cb-label">Portionen</label>
<input type="number" id="portions" name="portions" class="cb-input"
value="{{ recipe.portions if recipe else 4 }}" min="1" max="50">
</div>
</div>
</div>
<div class="cb-row">
<div class="cb-col">
<div class="cb-form-group">
<label for="prep_time" class="cb-label">Zubereitungszeit</label>
<input type="text" id="prep_time" name="prep_time" class="cb-input"
value="{{ recipe.prep_time if recipe else '' }}" placeholder="z. B. 15 Min">
</div>
</div>
<div class="cb-col">
<div class="cb-form-group">
<label for="cook_time" class="cb-label">Koch- / Backzeit</label>
<input type="text" id="cook_time" name="cook_time" class="cb-input"
value="{{ recipe.cook_time if recipe else '' }}" placeholder="z. B. 30 Min">
</div>
</div>
</div>
<!-- Checkbox: Multi-Portionen-Tabelle -->
<div class="cb-form-group" style="margin-top: 4px; margin-bottom: 20px; background: rgba(44, 74, 52, 0.05); padding: 10px 14px; border-radius: 8px; border: 1px dashed var(--tannengruen);">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 0.95rem; font-weight: 600; color: var(--dunkelgrau); margin-bottom: 0;">
<input type="checkbox" id="has_portion_table" name="has_portion_table" value="1"
{% if recipe and recipe.has_portion_table %}checked{% endif %}
style="width: 18px; height: 18px; accent-color: var(--tannengruen);">
<span>🔢 Zutatentabelle für 2 / 4 / 6 / 8 Portionen berechnen (z. B. für Waffeln, Pfannkuchen, Teige)</span>
</label>
</div>
<!-- Foto-Upload -->
<div class="cb-form-group">
<label class="cb-label">Foto des Gerichts 📸</label>
<div class="upload-dropzone" id="dropzone" onclick="document.getElementById('image-upload').click();">
<span style="font-size: 2.2rem;">📷</span>
<p style="font-family: var(--font-hand); font-size: 1.15rem; margin: 8px 0 4px 0; color: var(--tannengruen);">
Hier klicken, um ein Foto auszuwählen oder mit dem Smartphone aufzunehmen
</p>
<span style="font-size: 0.85rem; color: #777;">(JPG, PNG, WebP)</span>
<input type="file" id="image-upload" name="image_file" accept="image/*" style="display: none;">
</div>
<div class="preview-container" id="preview-box" {% if recipe and recipe.image_url %}style="display: block;"{% endif %}>
<img id="image-preview" src="{{ recipe.image_url if recipe and recipe.image_url else '' }}" alt="Vorschau">
</div>
</div>
<!-- Zutaten -->
<div class="cb-form-group">
<label for="ingredients" class="cb-label">Zutaten * (eine Zutat pro Zeile)</label>
<textarea id="ingredients" name="ingredients" class="cb-textarea" rows="8"
placeholder="500g Pasta&#10;150g Schinken oder Speck&#10;4 Eigelb&#10;50g Parmesan gerieben&#10;Salz und frisch gemahlener Pfeffer" required>{{ recipe.ingredients if recipe else '' }}</textarea>
</div>
<!-- Zubereitungsschritte -->
<div class="cb-form-group">
<label for="instructions" class="cb-label">Zubereitung * (Absätze werden nummeriert)</label>
<textarea id="instructions" name="instructions" class="cb-textarea" rows="10"
placeholder="Reichlich Salzwasser in einem großen Topf zum Kochen bringen.&#10;Den Schinken in Streifen schneiden und knusprig anbraten.&#10;Die Eigelbe mit dem geriebenen Käse und Pfeffer cremig rühren.&#10;Pasta al dente kochen und mit etwas Nudelwasser vermengen..." required>{{ recipe.instructions if recipe else '' }}</textarea>
</div>
<!-- Notizen & Tipps -->
<div class="cb-form-group">
<label for="notes" class="cb-label">💡 Tipps, Notizen & Kniffe (optional)</label>
<textarea id="notes" name="notes" class="cb-textarea" rows="3"
placeholder="Wichtig: Den Herd vor dem Zugeben der Eier-Käse-Mischung ausschalten, damit das Ei nicht stockt!">{{ recipe.notes if recipe else '' }}</textarea>
</div>
<!-- Credits / Copyright / Quelle -->
<div class="cb-form-group">
<label for="source" class="cb-label">© Quelle / Credits / Copyright (optional)</label>
<input type="text" id="source" name="source" class="cb-input"
value="{{ recipe.source if recipe and recipe.source else '' }}"
placeholder="z. B. Von Oma Anna, Chefkoch.de, Jamie Oliver, Familienrezept">
</div>
<!-- Checkbox: Auch als Zufallsgenerator-Gericht anlegen -->
{% if not recipe %}
<div class="cb-form-group" style="margin-top: 10px;">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; font-family: var(--font-hand); font-size: 1.1rem;">
<input type="checkbox" id="add_to_dishes" name="add_to_dishes" value="true" checked style="width: 20px; height: 20px; accent-color: var(--tannengruen);">
<span>Auch als Gericht für den Zufallsgenerator hinzufügen</span>
</label>
</div>
{% endif %}
<!-- Error Feedback Banner -->
<div id="form-error-banner" style="display: none; background: #fff0f0; border: 2px solid #e53e3e; color: #9b1c1c; padding: 12px 16px; border-radius: 8px; margin-top: 15px; font-weight: 600;"></div>
<!-- Submit Button -->
<div style="display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px;">
<a href="/cookbook" class="cb-btn cb-btn-outline" style="color: var(--schwarz); border-color: rgba(34,34,34,0.3);">
Abbrechen
</a>
<button type="submit" id="btn-submit" class="cb-btn cb-btn-creme" style="background: var(--tannengruen); color: var(--creme); font-size: 1.15rem; padding: 12px 24px;">
<span>💾</span> <span>Rezept speichern</span>
</button>
</div>
</form>
</div>
</main>
<script>
// Live Image Preview & Compression Helper
const fileInput = document.getElementById('image-upload');
const previewBox = document.getElementById('preview-box');
const previewImg = document.getElementById('image-preview');
const errorBanner = document.getElementById('form-error-banner');
let preparedImageFile = null;
fileInput.addEventListener('change', async function() {
const file = this.files[0];
if (!file) return;
// Vorschau anzeigen
const reader = new FileReader();
reader.onload = function(e) {
previewImg.src = e.target.result;
previewBox.style.display = 'block';
};
reader.readAsDataURL(file);
// Foto für schnellen Upload komprimieren (max 1600px Breite/Höhe, JPEG 85%)
try {
preparedImageFile = await compressImageFile(file, 1600, 1600, 0.85);
} catch (err) {
console.warn('Komprimierung fehlgeschlagen, Originaldatei wird verwendet:', err);
preparedImageFile = file;
}
});
function compressImageFile(file, maxWidth, maxHeight, quality) {
return new Promise((resolve) => {
// Bei SVG oder WebP/GIF direkt original belassen
if (file.type === 'image/svg+xml' || file.type === 'image/gif') {
return resolve(file);
}
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
let w = img.width;
let h = img.height;
if (w > maxWidth || h > maxHeight) {
if (w > h) {
h = Math.round((h * maxWidth) / w);
w = maxWidth;
} else {
w = Math.round((w * maxHeight) / h);
h = maxHeight;
}
}
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
canvas.toBlob((blob) => {
if (!blob) return resolve(file);
const safeName = file.name.replace(/\.[^/.]+$/, "") + ".jpg";
resolve(new File([blob], safeName, { type: "image/jpeg" }));
}, 'image/jpeg', quality);
};
img.onerror = () => {
URL.revokeObjectURL(url);
resolve(file);
};
img.src = url;
});
}
// Form Submit (AJAX multipart)
const form = document.getElementById('recipe-form');
const btnSubmit = document.getElementById('btn-submit');
const recipeIdEl = document.getElementById('recipe-id');
form.addEventListener('submit', async function(e) {
e.preventDefault();
errorBanner.style.display = 'none';
errorBanner.textContent = '';
btnSubmit.disabled = true;
btnSubmit.innerHTML = '<span>⏳</span> Speichern...';
const formData = new FormData(form);
// Wenn ein Bild komprimiert vorliegt, dieses im FormData setzen
if (preparedImageFile) {
formData.set('image_file', preparedImageFile);
} else if (fileInput.files.length === 0) {
formData.delete('image_file');
}
// Checkbox Wert explizit absichern
const addDishCb = document.getElementById('add_to_dishes');
if (addDishCb) {
formData.set('add_to_dishes', addDishCb.checked ? 'true' : 'false');
}
const hasPortionTableCb = document.getElementById('has_portion_table');
if (hasPortionTableCb) {
formData.set('has_portion_table', hasPortionTableCb.checked ? '1' : '0');
}
const isEdit = !!recipeIdEl;
const url = isEdit ? `/api/recipes/${recipeIdEl.value}` : '/api/recipes';
const method = isEdit ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method: method,
body: formData
});
let data;
const textResponse = await res.text();
try {
data = JSON.parse(textResponse);
} catch (e) {
throw new Error(`Serverfehler (${res.status}): ${textResponse.slice(0, 150)}`);
}
if (!res.ok) {
const detailMsg = (typeof data.detail === 'string')
? data.detail
: (Array.isArray(data.detail) ? data.detail.map(d => d.msg || d).join(', ') : 'Fehler beim Speichern');
throw new Error(detailMsg);
}
const recipeId = data.recipe ? data.recipe.id : (recipeIdEl ? recipeIdEl.value : '');
window.location.href = `/cookbook#rezept-${recipeId}`;
} catch (err) {
console.error('Fehler beim Speichern:', err);
errorBanner.textContent = `❌ ${err.message}`;
errorBanner.style.display = 'block';
btnSubmit.disabled = false;
btnSubmit.innerHTML = '<span>💾</span> Rezept speichern';
}
});
// Mobile Kebab Menu Functions
function toggleMobileMenu() {
const dd = document.getElementById('mobileDropdown');
if (dd) dd.classList.toggle('show');
}
document.addEventListener('click', function(e) {
const wrap = document.querySelector('.mobile-nav-wrapper');
if (wrap && !wrap.contains(e.target)) {
const dd = document.getElementById('mobileDropdown');
if (dd) dd.classList.remove('show');
}
});
</script>
</body>
</html>