Files
wtc.waiswuaschd.de/app/templates/recipe_form.html
T

358 lines
16 KiB
HTML
Executable File
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>{% 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>