commit c01d2d7639bf1b2ec1f120a8b5fdd73c9e017e63 Author: kobold-ai Date: Wed Oct 14 20:24:37 2026 +0200 Initial commit: What To Cook (WTC) v4.4 - Digitales Kochbuch, Wochenmenueplan & Buchdruck diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..62b9c3c --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Database Configuration (mariadb oder sqlite) +DB_ENGINE=mariadb +MARIADB_HOST=localhost +MARIADB_PORT=3306 +MARIADB_USER=wtc_user +MARIADB_PASS=dein_sicheres_passwort +MARIADB_NAME=wtc_cookbook + +# Authentication & Session Security +APP_USERNAME=admin +APP_PASSWORD=aendern_beim_ersten_start! +SECRET_KEY=generiere_einen_langen_zufaelligen_hex_key_hier + +# Optional: Host & Port (falls abweichend von Standard 127.0.0.1:8003) +# PORT=8003 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8df7922 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +*.so + +# C extensions +*.obj +*.o + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +venv/ +.venv/ +env/ +ENV/ + +# Environment and secrets +.env +.env.local +*.pem +*.key + +# SQLite and local databases +*.sqlite +*.sqlite3 +*.db +!data/*.placeholder + +# Temporary files and test dumps +dump_out.txt +*.log +.pytest_cache/ +.coverage +htmlcov/ + +# Editor and OS metadata +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store +Thumbs.db +desktop.ini +recipes_details.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ee94a1 --- /dev/null +++ b/README.md @@ -0,0 +1,198 @@ +# 📖 What To Cook? (WTC) – Digitales Kochbuch, Wochenmenüplan & Buchdruck-Engine + +Eine moderne, elegante Web-Applikation zur Verwaltung von Lieblingsrezepten, zur spielerischen Menüauslosung (Shuffle-Generator), zur flexiblen Wochenmenü-Planung und zum professionellen Duplex-Buchdruck mit Inhaltsverzeichnis und abwechselnden Seitenzahlen. + +Live-Instanz: [wtc.dev.waiswuaschd.de](https://wtc.dev.waiswuaschd.de) + +--- + +## ✨ Features im Überblick + +### 🍽️ Digitales Kochbuch +* **11 liebevoll kuratierte Kapitel:** + 1. 🍲 Suppen & Eintöpfe + 2. 🥨 Traditionell & Basics + 3. 🍝 Pasta & Nudelgerichte + 4. 🥔 Kartoffeln + 5. 🍚 Reis & Getreide + 6. 🥐 Teig & Gebackenes + 7. 🥢 Asiatische Küche + 8. 🥞 Süßspeisen + 9. 🍮 Desserts + 10. 🥧 Kuchen & Gebäck + 11. 🍪 Plätzchen +* **Handgezeichnete Kapitel-Illustrationen:** Hochauflösende Doodles auf jeder Kapitel-Trennseite (sowohl auf dem Bildschirm als auch im Druck). +* **2-Spalten-Layout:** Links Bild und Zutaten, rechts nummerierte Zubereitungsschritte und persönliche Notizen. +* **Intelligente Portionstabelle:** Automatische 2 / 4 / 6 / 8 Portionen-Berechnung (z. B. für Waffeln, Pfannkuchen und Teige). +* **Rezept-Management:** Schnelles Hinzufügen, Bearbeiten, Löschen und Foto-Upload mit automatischer Bildoptimierung und Komprimierung (WebP/JPEG). + +### 🎲 Shuffle-Generator (Essensauslosung) +* Würfelt 1 bis 5 Gerichte gleichzeitig ohne doppelte Vorschläge in einer Runde. +* Filterbar nach **Kapitel** und **Aufwand** (Mehrfachauswahl: Gering, Mittel, Hoch). +* Direkte Schaltfläche zum Öffnen oder Tauschen eines Gerichts. + +### 📅 Interaktiver Wochenmenü-Plan +* **4 Mahlzeiten pro Tag:** 🌅 Früh, ☀️ Mittag, ☕ Kaffee & Kuchen, 🌙 Abend. +* **Nur belegte Mahlzeiten sichtbar:** Schlanke, aufgeräumte Wochenansicht ohne überflüssige leere Blöcke. +* **Flexible Zuordnung:** Rezepte direkt aus dem Kochbuch wählen, Freitext eingeben (z. B. *Kantine*, *Pizzaabend*, *Brotzeit*) oder Zufallsgericht würfeln. +* **Wochennavigation:** Beliebiges Blättern durch vergangene und zukünftige Kalenderwochen mit Anzeige der aktuellen KW. +* **Persistente Speicherung:** Dauerhafte Speicherung aller Einträge in der Datenbank. + +### 🖨️ A4-Buchdruck & PDF-Export (Duplex-Buchbindung) +* **Reine Druck-Elemente:** Deckblatt (*Meine Lieblingsrezepte*), 2-seitiges Inhaltsverzeichnis mit Punktlinien und Seitenzahlen erscheinen ausschließlich im Ausdruck. +* **Abwechselnde Seitenzahlen:** + - Gerade Seiten (links im Buch): Seitenzahl links unten + - Ungerade Seiten (rechts im Buch): Seitenzahl rechts unten +* **Millimetergenaue 1-Seiten-Garantie:** Jedes Rezept und jede Kapitelseite ist so optimiert (282mm Druckhöhe), dass auch unter Apple AirPrint (iOS Safari) und Android Chrome keine ungewollten 2. Seiten entstehen. + +--- + +## 🛠️ Systemvoraussetzungen + +* **Betriebssystem:** Linux (Debian 12 / Ubuntu 22.04+ empfohlen) oder Windows +* **Python:** 3.10 oder neuer +* **Datenbank:** MariaDB 10.6+ / MySQL 8.0+ (oder SQLite als Fallback) +* **Webserver:** Nginx (als Reverse Proxy mit SSL via Let's Encrypt) + +--- + +## 🚀 Installation & Einrichtung + +### 1. Repository klonen +```bash +git clone https://gitea.sp13l.de/kobold-ai/wtc.waiswuaschd.de.git /home/maxi/Projekt/wtc.waiswuaschd.de +cd /home/maxi/Projekt/wtc.waiswuaschd.de +``` + +### 2. Python Virtual Environment anlegen +```bash +python3 -m venv venv +source venv/bin/activate +pip install --upgrade pip +pip install -r requirements.txt +``` + +### 3. Konfiguration (`.env`) +Erstelle eine `.env`-Datei auf Basis der Vorlage: +```bash +cp .env.example .env +nano .env +``` + +Beispiel-Konfiguration: +```ini +DB_ENGINE=mariadb +MARIADB_HOST=localhost +MARIADB_PORT=3306 +MARIADB_USER=wtc_user +MARIADB_PASS=dein_sicheres_passwort +MARIADB_NAME=wtc_cookbook + +# Login Zugangsdaten für das Kochbuch +APP_USERNAME=admin +APP_PASSWORD=dein_super_geheimes_passwort! +SECRET_KEY=wtc_hmac_secret_key_zufaelliger_string_hier +``` + +### 4. MariaDB Datenbank & Benutzer anlegen +Verbinde dich mit deiner MySQL/MariaDB-Konsole: +```sql +CREATE DATABASE IF NOT EXISTS `wtc_cookbook` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE USER IF NOT EXISTS 'wtc_user'@'localhost' IDENTIFIED BY 'dein_sicheres_passwort'; +GRANT ALL PRIVILEGES ON `wtc_cookbook`.* TO 'wtc_user'@'localhost'; +FLUSH PRIVILEGES; +``` +*(Tabellen für Rezepte und den Wochenplan werden beim ersten Start der Applikation automatisch migriert und angelegt).* + +--- + +## ⚙️ Produktion: Systemd Service einrichten + +Erstelle die Service-Datei `/etc/systemd/system/wtc.service`: +```ini +[Unit] +Description=What To Cook (WTC) Kochbuch & Menüplaner (FastAPI) +After=network.target mariadb.service + +[Service] +Type=simple +User=maxi +Group=maxi +WorkingDirectory=/home/maxi/Projekt/wtc.waiswuaschd.de +Environment="PATH=/home/maxi/Projekt/wtc.waiswuaschd.de/venv/bin:/usr/local/bin:/usr/bin:/bin" +ExecStart=/home/maxi/Projekt/wtc.waiswuaschd.de/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8003 --workers 2 +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +``` + +Dienst aktivieren und starten: +```bash +sudo systemctl daemon-reload +sudo systemctl enable wtc.service +sudo systemctl start wtc.service +sudo systemctl status wtc.service +``` + +--- + +## 🌐 Nginx Reverse Proxy Konfiguration + +Erstelle `/etc/nginx/sites-available/wtc.dev.waiswuaschd.de`: +```nginx +server { + server_name wtc.dev.waiswuaschd.de; + + client_max_body_size 25M; + + location / { + proxy_pass http://127.0.0.1:8003; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +Aktivieren und SSL-Zertifikat via Certbot einbinden: +```bash +sudo ln -s /etc/nginx/sites-available/wtc.dev.waiswuaschd.de /etc/nginx/sites-enabled/ +sudo nginx -t +sudo systemctl reload nginx +sudo certbot --nginx -d wtc.dev.waiswuaschd.de +``` + +--- + +## 📂 Projektstruktur + +```text +wtc.waiswuaschd.de/ +├── app/ +│ ├── static/ +│ │ ├── cookbook.css # Haupt-Stylesheet & A4-Druckregeln +│ │ └── uploads/ # Hochgeladene Rezeptfotos & Illustrationen +│ ├── templates/ +│ │ ├── cookbook.html # Hauptansicht, Buch-Wrapper, Modals & Wochenplan +│ │ ├── recipe_detail.html # Einzelrezeptansicht +│ │ ├── recipe_form.html # Standalone Formular +│ │ └── login.html # Login-Maske +│ ├── database.py # Datenbank-Abstraktion (MariaDB / SQLite CRUD) +│ ├── ingredients.py # Portions-Skalierung & Zutaten-Parser +│ └── main.py # FastAPI Routen, Session-Auth & Endpunkte +├── requirements.txt # Python Dependencies +├── wtc.service # Systemd Service Vorlage +├── .env.example # Konfigurationsvorlage +└── README.md # Projektdokumentation +``` + +--- + +## 📄 Lizenz & Credits +Entwickelt für den privaten Gebrauch und die Erstellung gebundener Familien-Kochbücher. Alle Rechte vorbehalten. diff --git a/app/database.py b/app/database.py new file mode 100755 index 0000000..79847fa --- /dev/null +++ b/app/database.py @@ -0,0 +1,754 @@ +import os +import sqlite3 +from datetime import datetime +from typing import List, Dict, Any, Optional + +try: + from dotenv import load_dotenv + load_dotenv() +except ImportError: + pass + +try: + import pymysql + import pymysql.cursors +except ImportError: + pymysql = None + +DB_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "wtc.db") + +DB_ENGINE = os.getenv("DB_ENGINE", "mariadb").lower() +MARIADB_HOST = os.getenv("MARIADB_HOST", "db.waiswuaschd.de") +MARIADB_PORT = int(os.getenv("MARIADB_PORT", "3306")) +MARIADB_USER = os.getenv("MARIADB_USER", "v2_usr_wtc_cookbook") +MARIADB_PASS = os.getenv("MARIADB_PASS", "XKikHf&j6Wj!rs5%A8#ZzyvE#*A1hT") +MARIADB_NAME = os.getenv("MARIADB_NAME", "v2_db_wtc_cookbook") + +LEGACY_CATEGORIES = [ + "Reis", + "Nudeln", + "Kartoffeln", + "Teigig", + "asiatisch", + "Hausmannskost", + "Suppen", + "Eintöpfe", + "Süßspeisen", + "Fisch" +] + +# 11 Kochbuch-Kapitel aus dem Benutzerentwurf mit handgezeichneten Doodles & Icons +CHAPTERS = [ + {"number": 1, "title": "Suppen & Eintöpfe", "icon": "🍲", "doodle_hint": "Dampfende Suppenschale", "illustration_url": "/static/img/chapters/chapter_1.png"}, + {"number": 2, "title": "Traditionell & Basics", "icon": "🥟", "doodle_hint": "Klassiker & Brotzeit", "illustration_url": "/static/img/chapters/chapter_2.png"}, + {"number": 3, "title": "Pasta & Nudelgerichte", "icon": "🍝", "doodle_hint": "Spaghetti & Pasta", "illustration_url": "/static/img/chapters/chapter_3.png"}, + {"number": 4, "title": "Kartoffeln", "icon": "🥔", "doodle_hint": "Kartoffelauflauf & Klöße", "illustration_url": "/static/img/chapters/chapter_4.png"}, + {"number": 5, "title": "Reis & Getreide", "icon": "🍚", "doodle_hint": "Reisschale & Körner", "illustration_url": "/static/img/chapters/chapter_5.png"}, + {"number": 6, "title": "Teig & Gebackenes", "icon": "🍕", "doodle_hint": "Pizza, Flammkuchen & Brot", "illustration_url": "/static/img/chapters/chapter_6.png"}, + {"number": 7, "title": "Asiatische Küche", "icon": "🥢", "doodle_hint": "Wok, Ramen & Thai-Curry", "illustration_url": "/static/img/chapters/chapter_7.png"}, + {"number": 8, "title": "Süßspeisen", "icon": "🥞", "doodle_hint": "Kaiserschmarrn & Knödel", "illustration_url": "/static/img/chapters/chapter_8.png"}, + {"number": 9, "title": "Desserts", "icon": "🍰", "doodle_hint": "Tiramisu & Mousse", "illustration_url": "/static/img/chapters/chapter_9.png"}, + {"number": 10, "title": "Kuchen & Gebäck", "icon": "🎂", "doodle_hint": "Torten & Feingebäck", "illustration_url": "/static/img/chapters/chapter_10.png"}, + {"number": 11, "title": "Plätzchen", "icon": "🍪", "doodle_hint": "Weihnachtsplätzchen & Kekse", "illustration_url": "/static/img/chapters/chapter_11.png"} +] + +CATEGORIES = [c["title"] for c in CHAPTERS] + [c for c in LEGACY_CATEGORIES if c not in [ch["title"] for ch in CHAPTERS]] + +EFFORTS = ["gering", "mittel", "hoch"] + +CHAPTER_MAP = {c["title"]: c["number"] for c in CHAPTERS} +for c in CHAPTERS: + CHAPTER_MAP[str(c["number"])] = c["number"] + + +def get_connection(): + if DB_ENGINE == "mariadb": + if pymysql is None: + raise RuntimeError("PyMySQL ist nicht installiert, wird aber für MariaDB benötigt.") + return pymysql.connect( + host=MARIADB_HOST, + port=MARIADB_PORT, + user=MARIADB_USER, + password=MARIADB_PASS, + database=MARIADB_NAME, + charset="utf8mb4", + cursorclass=pymysql.cursors.DictCursor, + autocommit=True + ) + else: + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + + +def execute_query(query: str, params: tuple = (), fetch: str = "all", commit: bool = False): + conn = get_connection() + try: + if DB_ENGINE == "mariadb": + # MariaDB uses %s instead of ? + maria_query = query.replace("?", "%s") + with conn.cursor() as cursor: + cursor.execute(maria_query, params) + last_id = cursor.lastrowid + rowcount = cursor.rowcount + if fetch == "all": + rows = cursor.fetchall() + return rows + elif fetch == "one": + return cursor.fetchone() + else: + return {"lastrowid": last_id, "rowcount": rowcount} + else: + cursor = conn.cursor() + cursor.execute(query, params) + if commit: + conn.commit() + last_id = cursor.lastrowid + rowcount = cursor.rowcount + if fetch == "all": + rows = [dict(r) for r in cursor.fetchall()] + return rows + elif fetch == "one": + row = cursor.fetchone() + return dict(row) if row else None + else: + return {"lastrowid": last_id, "rowcount": rowcount} + finally: + conn.close() + + +def init_db(): + conn = get_connection() + try: + if DB_ENGINE == "mariadb": + with conn.cursor() as cursor: + cursor.execute(""" + CREATE TABLE IF NOT EXISTS dishes ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + category VARCHAR(100) NOT NULL, + effort VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_dish_cat (category), + INDEX idx_dish_eff (effort) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + """) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS recipes ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + category VARCHAR(100) NOT NULL, + chapter_number INT NOT NULL, + effort VARCHAR(20) NOT NULL DEFAULT 'mittel', + prep_time VARCHAR(50) DEFAULT '', + cook_time VARCHAR(50) DEFAULT '', + portions INT DEFAULT 4, + ingredients LONGTEXT NOT NULL, + instructions LONGTEXT NOT NULL, + notes TEXT, + source VARCHAR(255) DEFAULT '', + image_url VARCHAR(500) DEFAULT '', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_recipe_chapter (chapter_number), + INDEX idx_recipe_cat (category) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + """) + # Migration check for MariaDB + cursor.execute(""" + SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = %s AND TABLE_NAME = 'recipes' AND COLUMN_NAME = 'source' + """, (MARIADB_NAME,)) + if not cursor.fetchone(): + cursor.execute("ALTER TABLE recipes ADD COLUMN source VARCHAR(255) DEFAULT ''") + + cursor.execute(""" + SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = %s AND TABLE_NAME = 'recipes' AND COLUMN_NAME = 'printed_at' + """, (MARIADB_NAME,)) + if not cursor.fetchone(): + cursor.execute("ALTER TABLE recipes ADD COLUMN printed_at DATETIME NULL DEFAULT NULL AFTER image_url") + + cursor.execute(""" + SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = %s AND TABLE_NAME = 'recipes' AND COLUMN_NAME = 'has_portion_table' + """, (MARIADB_NAME,)) + if not cursor.fetchone(): + cursor.execute("ALTER TABLE recipes ADD COLUMN has_portion_table TINYINT(1) NOT NULL DEFAULT 0 AFTER portions") + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS meal_plans ( + id INT AUTO_INCREMENT PRIMARY KEY, + year INT NOT NULL, + week INT NOT NULL, + day_of_week INT NOT NULL, + meal_type VARCHAR(20) NOT NULL, + recipe_id INT NULL, + custom_text VARCHAR(255) NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uq_meal_slot (year, week, day_of_week, meal_type), + INDEX idx_plan_week (year, week) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + """) + + cursor.execute("DELETE FROM dishes") + else: + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS dishes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE COLLATE NOCASE, + category TEXT NOT NULL, + effort TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS recipes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + category TEXT NOT NULL, + chapter_number INTEGER NOT NULL, + effort TEXT NOT NULL DEFAULT 'mittel', + prep_time TEXT DEFAULT '', + cook_time TEXT DEFAULT '', + portions INTEGER DEFAULT 4, + has_portion_table INTEGER DEFAULT 0, + ingredients TEXT NOT NULL, + instructions TEXT NOT NULL, + notes TEXT DEFAULT '', + source TEXT DEFAULT '', + image_url TEXT DEFAULT '', + printed_at TIMESTAMP NULL DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + # Migration check for SQLite + cursor.execute("PRAGMA table_info(recipes)") + existing_cols = [c[1] for c in cursor.fetchall()] + if "source" not in existing_cols: + cursor.execute("ALTER TABLE recipes ADD COLUMN source TEXT DEFAULT ''") + if "printed_at" not in existing_cols: + cursor.execute("ALTER TABLE recipes ADD COLUMN printed_at TIMESTAMP NULL DEFAULT NULL") + if "has_portion_table" not in existing_cols: + cursor.execute("ALTER TABLE recipes ADD COLUMN has_portion_table INTEGER DEFAULT 0") + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS meal_plans ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + year INTEGER NOT NULL, + week INTEGER NOT NULL, + day_of_week INTEGER NOT NULL, + meal_type TEXT NOT NULL, + recipe_id INTEGER NULL, + custom_text TEXT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (year, week, day_of_week, meal_type) + ) + """) + + cursor.execute("DELETE FROM dishes") + conn.commit() + finally: + conn.close() + + +# --- Dishes (Zufallsgenerator) --- + +def get_all_dishes(category: Optional[str] = None, effort: Optional[str] = None, search: Optional[str] = None) -> List[Dict[str, Any]]: + query = "SELECT id, name, category, effort, created_at FROM dishes WHERE 1=1" + params = [] + + if category and category.strip() and category != "alle": + query += " AND category = ?" + params.append(category.strip()) + + if effort and effort.strip() and effort != "alle": + query += " AND effort = ?" + params.append(effort.strip()) + + if search and search.strip(): + query += " AND name LIKE ?" + params.append(f"%{search.strip()}%") + + query += " ORDER BY category ASC, name ASC" + return execute_query(query, tuple(params), fetch="all") + + +def add_dish(name: str, category: str, effort: str) -> Dict[str, Any]: + name = name.strip() + category = category.strip() + effort = effort.strip() + + if not name: + raise ValueError("Name darf nicht leer sein.") + if category not in CATEGORIES: + raise ValueError(f"Ungültige Kategorie '{category}'. Gültig sind: {', '.join(CATEGORIES)}") + if effort not in EFFORTS: + raise ValueError(f"Ungültiger Aufwand '{effort}'. Gültig sind: {', '.join(EFFORTS)}") + + try: + res = execute_query( + "INSERT INTO dishes (name, category, effort) VALUES (?, ?, ?)", + (name, category, effort), + fetch="none", + commit=True + ) + dish_id = res["lastrowid"] + return execute_query("SELECT id, name, category, effort, created_at FROM dishes WHERE id = ?", (dish_id,), fetch="one") + except Exception as e: + if "UNIQUE" in str(e).upper() or "DUPLICATE" in str(e).upper(): + raise ValueError(f"Ein Gericht mit dem Namen '{name}' existiert bereits.") + raise + + +def update_dish(dish_id: int, name: str, category: str, effort: str) -> Dict[str, Any]: + name = name.strip() + category = category.strip() + effort = effort.strip() + + if not name: + raise ValueError("Name darf nicht leer sein.") + if category not in CATEGORIES: + raise ValueError(f"Ungültige Kategorie '{category}'.") + if effort not in EFFORTS: + raise ValueError(f"Ungültiger Aufwand '{effort}'.") + + try: + res = execute_query( + "UPDATE dishes SET name = ?, category = ?, effort = ? WHERE id = ?", + (name, category, effort, dish_id), + fetch="none", + commit=True + ) + if res["rowcount"] == 0: + raise KeyError(f"Gericht #{dish_id} nicht gefunden.") + return execute_query("SELECT id, name, category, effort, created_at FROM dishes WHERE id = ?", (dish_id,), fetch="one") + except Exception as e: + if "UNIQUE" in str(e).upper() or "DUPLICATE" in str(e).upper(): + raise ValueError(f"Ein Gericht mit dem Namen '{name}' existiert bereits.") + raise + + +def delete_dish(dish_id: int) -> bool: + res = execute_query("DELETE FROM dishes WHERE id = ?", (dish_id,), fetch="none", commit=True) + return res["rowcount"] > 0 + + +def clear_all_dishes(): + execute_query("DELETE FROM dishes", fetch="none", commit=True) + if DB_ENGINE == "sqlite": + try: + execute_query("DELETE FROM sqlite_sequence WHERE name = 'dishes'", fetch="none", commit=True) + except Exception: + pass + + +def get_stats() -> Dict[str, Any]: + total_res = execute_query("SELECT COUNT(*) as total FROM dishes", fetch="one") + total = total_res["total"] if total_res else 0 + + cat_rows = execute_query("SELECT category, COUNT(*) as cnt FROM dishes GROUP BY category", fetch="all") + by_category = {row["category"]: row["cnt"] for row in cat_rows} + + eff_rows = execute_query("SELECT effort, COUNT(*) as cnt FROM dishes GROUP BY effort", fetch="all") + by_effort = {row["effort"]: row["cnt"] for row in eff_rows} + + recipe_count_res = execute_query("SELECT COUNT(*) as total FROM recipes", fetch="one") + total_recipes = recipe_count_res["total"] if recipe_count_res else 0 + + return { + "total": total, + "by_category": by_category, + "by_effort": by_effort, + "total_recipes": total_recipes + } + + +def resolve_category_terms(cat_input: Optional[str]) -> List[str]: + if not cat_input or str(cat_input).strip().lower() in ["", "alle", "beliebig", "none"]: + return [] + s = str(cat_input).strip() + chap_num = None + if s.isdigit(): + chap_num = int(s) + else: + for c in CHAPTERS: + if s.lower() == c["title"].lower() or s.lower() in c["title"].lower(): + chap_num = c["number"] + break + + chapter_terms = { + 1: ["Suppen", "Eintöpfe", "Suppe", "Eintopf", "Suppen & Eintöpfe", "Chili", "Gulasch"], + 2: ["Traditionell & Basics", "Hausmannskost", "Traditionell", "Basics", "Fisch", "Brotzeit", "Klassiker", "Schnitzel", "Rouladen", "Klopse", "Forelle", "Lachs"], + 3: ["Pasta & Nudelgerichte", "Nudeln", "Pasta", "Spaghetti", "Penne", "Tagliatelle", "Carbonara", "Lasagne", "Spätzle"], + 4: ["Kartoffeln", "Kartoffel", "Gratin", "Pommes", "Püree"], + 5: ["Reis & Getreide", "Reis", "Getreide", "Risotto", "Paella"], + 6: ["Teig & Gebackenes", "Teigig", "Teig", "Pizza", "Flammkuchen", "Quiche", "Calzone"], + 7: ["Asiatische Küche", "asiatisch", "Asiatisch", "Curry", "Pad Thai", "Bami", "Wok", "Ramen", "Sushi"], + 8: ["Süßspeisen", "Süßspeise", "Kaiserschmarrn", "Germknödel", "Dampfnudel", "Apfelküchle", "Milchreis"], + 9: ["Desserts", "Dessert", "Tiramisu", "Mousse", "Pudding"], + 10: ["Kuchen & Gebäck", "Kuchen", "Gebäck", "Torte"], + 11: ["Plätzchen", "Kekse", "Keks"] + } + + if chap_num and chap_num in chapter_terms: + return chapter_terms[chap_num] + + # Legacy category names mapping + legacy_map = { + "nudeln": chapter_terms[3], + "pasta": chapter_terms[3], + "reis": chapter_terms[5], + "kartoffeln": chapter_terms[4], + "teigig": chapter_terms[6], + "suppen": chapter_terms[1], + "eintöpfe": chapter_terms[1], + "eintoepfe": chapter_terms[1], + "hausmannskost": chapter_terms[2], + "asiatisch": chapter_terms[7], + "süßspeisen": chapter_terms[8], + "suessspeisen": chapter_terms[8], + "fisch": ["Fisch", "Forelle", "Lachs", "Kabeljau", "Hausmannskost"] + } + if s.lower() in legacy_map: + return legacy_map[s.lower()] + + return [s] + + +def get_chapter_info_for_dish(dish_category: str = "", dish_name: str = "", recipe_chapter: Optional[int] = None) -> Dict[str, Any]: + if recipe_chapter and 1 <= recipe_chapter <= len(CHAPTERS): + return CHAPTERS[recipe_chapter - 1] + + combo = f"{dish_category or ''} {dish_name or ''}".lower() + + if any(k in combo for k in ["suppe", "eintopf", "chili", "brühe"]): + return CHAPTERS[0] # 1: Suppen & Eintöpfe + if any(k in combo for k in ["nudel", "pasta", "spaghetti", "penne", "tagliatelle", "carbonara", "bolognese", "spätzle", "lasagne"]): + return CHAPTERS[2] # 3: Pasta & Nudelgerichte + if any(k in combo for k in ["kartoffel", "pommes", "gratin", "püree"]): + return CHAPTERS[3] # 4: Kartoffeln + if any(k in combo for k in ["curry", "pad thai", "bami", "wok", "ramen", "sushi", "asiatisch"]): + return CHAPTERS[6] # 7: Asiatische Küche + if any(k in combo for k in ["reis", "risotto", "paella", "djuvec"]): + return CHAPTERS[4] # 5: Reis & Getreide + if any(k in combo for k in ["pizza", "flammkuchen", "quiche", "calzone", "teig", "brot"]): + return CHAPTERS[5] # 6: Teig & Gebackenes + if any(k in combo for k in ["kuchen", "torte", "muffin", "gebäck"]): + return CHAPTERS[9] # 10: Kuchen & Gebäck + if any(k in combo for k in ["keks", "plätzchen"]): + return CHAPTERS[10] # 11: Plätzchen + if any(k in combo for k in ["tiramisu", "mousse", "pudding", "dessert"]): + return CHAPTERS[8] # 9: Desserts + if any(k in combo for k in ["kaiserschmarrn", "knödel", "apfelküchle", "dampfnudel", "milchreis", "süßspeis"]): + return CHAPTERS[7] # 8: Süßspeisen + + return CHAPTERS[1] # 2: Traditionell & Basics + + +def query_recipe_candidates(chapter_number: Optional[int] = None, efforts: Optional[List[str]] = None) -> List[Dict[str, Any]]: + """Gibt alle Rezepte aus der recipes-Tabelle zurück, die zu den Shuffle-Kriterien passen.""" + all_recipes = get_all_recipes() + if not all_recipes: + return [] + + # 1. Nach Kapitel filtern (wenn spezifiziert) + if chapter_number is not None and chapter_number > 0: + pool = [r for r in all_recipes if r.get("chapter_number") == chapter_number] + else: + pool = list(all_recipes) + + if not pool: + return [] + + # 2. Nach Aufwand filtern (nur, wenn eine Teilmenge gewählt wurde) + if efforts and len(efforts) > 0 and len(efforts) < len(EFFORTS): + clean_efforts = [e.strip().lower() for e in efforts if e.strip() in EFFORTS] + effort_pool = [r for r in pool if (r.get("effort") or "").lower() in clean_efforts] + if effort_pool: + return effort_pool + + return pool + + +def query_candidates(category: Optional[str], efforts: Optional[List[str]]) -> List[Dict[str, Any]]: + """Fallback-Funktion für alte Dish-Abfragen""" + return query_recipe_candidates() + + +# --- Recipes (Kochbuch) --- + +def format_printed_date(p_at: Any) -> str: + if not p_at: + return "" + if hasattr(p_at, "strftime"): + return p_at.strftime("%d.%m.%Y") + try: + parts = str(p_at).split(" ")[0].split("-") + if len(parts) == 3: + return f"{parts[2]}.{parts[1]}.{parts[0]}" + except Exception: + pass + return str(p_at) + + +def add_recipe( + title: str, + category: str, + chapter_number: int, + effort: str = "mittel", + prep_time: str = "", + cook_time: str = "", + portions: int = 4, + ingredients: str = "", + instructions: str = "", + notes: str = "", + source: str = "", + image_url: str = "", + printed_at: Optional[str] = None, + has_portion_table: int = 0 +) -> Dict[str, Any]: + title = title.strip() + category = category.strip() + effort = effort.strip().lower() + source = source.strip() + + if not title: + raise ValueError("Rezepttitel darf nicht leer sein.") + if not category: + raise ValueError("Kategorie darf nicht leer sein.") + if chapter_number not in [c["number"] for c in CHAPTERS]: + # Fallback to mapped chapter number if possible + chapter_number = CHAPTER_MAP.get(category, 1) + + res = execute_query( + """ + INSERT INTO recipes (title, category, chapter_number, effort, prep_time, cook_time, portions, has_portion_table, ingredients, instructions, notes, source, image_url, printed_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + (title, category, chapter_number, effort, prep_time, cook_time, portions, int(bool(has_portion_table)), ingredients, instructions, notes, source, image_url, printed_at), + fetch="none", + commit=True + ) + recipe_id = res["lastrowid"] + return get_recipe(recipe_id) + + +def update_recipe( + recipe_id: int, + title: str, + category: str, + chapter_number: int, + effort: str = "mittel", + prep_time: str = "", + cook_time: str = "", + portions: int = 4, + ingredients: str = "", + instructions: str = "", + notes: str = "", + source: str = "", + image_url: Optional[str] = None, + printed_at: Optional[str] = None, + has_portion_table: Optional[int] = None +) -> Dict[str, Any]: + title = title.strip() + category = category.strip() + effort = effort.strip().lower() + source = source.strip() + + if not title: + raise ValueError("Rezepttitel darf nicht leer sein.") + + existing = get_recipe(recipe_id) + if not existing: + raise KeyError(f"Rezept #{recipe_id} nicht gefunden.") + + use_portion_table = int(bool(has_portion_table)) if has_portion_table is not None else existing.get("has_portion_table", 0) + + if image_url is not None: + query = """ + UPDATE recipes + SET title = ?, category = ?, chapter_number = ?, effort = ?, prep_time = ?, cook_time = ?, portions = ?, has_portion_table = ?, ingredients = ?, instructions = ?, notes = ?, source = ?, image_url = ?, printed_at = ? + WHERE id = ? + """ + params = (title, category, chapter_number, effort, prep_time, cook_time, portions, use_portion_table, ingredients, instructions, notes, source, image_url, printed_at, recipe_id) + else: + query = """ + UPDATE recipes + SET title = ?, category = ?, chapter_number = ?, effort = ?, prep_time = ?, cook_time = ?, portions = ?, has_portion_table = ?, ingredients = ?, instructions = ?, notes = ?, source = ?, printed_at = ? + WHERE id = ? + """ + params = (title, category, chapter_number, effort, prep_time, cook_time, portions, use_portion_table, ingredients, instructions, notes, source, printed_at, recipe_id) + + res = execute_query(query, params, fetch="none", commit=True) + return get_recipe(recipe_id) + + +def delete_recipe(recipe_id: int) -> bool: + res = execute_query("DELETE FROM recipes WHERE id = ?", (recipe_id,), fetch="none", commit=True) + return res["rowcount"] > 0 + + +def get_recipe(recipe_id: int) -> Optional[Dict[str, Any]]: + row = execute_query("SELECT * FROM recipes WHERE id = ?", (recipe_id,), fetch="one") + if row: + row["printed_at_formatted"] = format_printed_date(row.get("printed_at")) + row["has_portion_table"] = bool(row.get("has_portion_table", 0)) + return row + + +def get_all_recipes( + chapter_number: Optional[int] = None, + category: Optional[str] = None, + search: Optional[str] = None, + printed_status: Optional[str] = None +) -> List[Dict[str, Any]]: + query = "SELECT * FROM recipes WHERE 1=1" + params = [] + + if chapter_number is not None and chapter_number > 0: + query += " AND chapter_number = ?" + params.append(chapter_number) + + if category and category.strip() and category != "alle": + query += " AND category = ?" + params.append(category.strip()) + + if search and search.strip(): + query += " AND (title LIKE ? OR ingredients LIKE ? OR instructions LIKE ?)" + s = f"%{search.strip()}%" + params.extend([s, s, s]) + + if printed_status == "unprinted": + query += " AND printed_at IS NULL" + elif printed_status == "printed": + query += " AND printed_at IS NOT NULL" + + query += " ORDER BY chapter_number ASC, title ASC" + rows = execute_query(query, tuple(params), fetch="all") + for r in rows: + r["printed_at_formatted"] = format_printed_date(r.get("printed_at")) + r["has_portion_table"] = bool(r.get("has_portion_table", 0)) + return rows + + +def mark_recipes_printed(recipe_ids: List[int], print_date: Optional[str] = None) -> int: + """Markiert die angegebenen Rezepte als gedruckt.""" + if not recipe_ids: + return 0 + date_val = print_date or datetime.now().strftime("%Y-%m-%d %H:%M:%S") + placeholders = ", ".join(["?"] * len(recipe_ids)) + query = f"UPDATE recipes SET printed_at = ? WHERE id IN ({placeholders})" + params = [date_val] + list(recipe_ids) + res = execute_query(query, tuple(params), fetch="none", commit=True) + return res.get("rowcount", 0) + + +def unmark_recipes_printed(recipe_ids: List[int]) -> int: + """Entfernt den Gedruckt-Status von den angegebenen Rezepten.""" + if not recipe_ids: + return 0 + placeholders = ", ".join(["?"] * len(recipe_ids)) + query = f"UPDATE recipes SET printed_at = NULL WHERE id IN ({placeholders})" + res = execute_query(query, tuple(recipe_ids), fetch="none", commit=True) + return res.get("rowcount", 0) + + +def toggle_recipe_printed(recipe_id: int) -> Dict[str, Any]: + """Schaltet den Druckstatus eines Rezepts um.""" + rec = get_recipe(recipe_id) + if not rec: + raise KeyError(f"Rezept #{recipe_id} nicht gefunden.") + if rec.get("printed_at"): + unmark_recipes_printed([recipe_id]) + else: + mark_recipes_printed([recipe_id]) + return get_recipe(recipe_id) + + +# ========================================== +# WOCHENPLAN (MEAL PLANNER) +# ========================================== + +def get_meal_plan(year: int, week: int) -> List[Dict[str, Any]]: + """Gibt alle Einträge des Wochenplans für die angegebene KW zurück.""" + query = """ + SELECT + m.id, m.year, m.week, m.day_of_week, m.meal_type, m.recipe_id, m.custom_text, + r.title AS recipe_title, r.image_url AS recipe_image_url, r.chapter_number AS recipe_chapter_number, + r.category AS recipe_category + FROM meal_plans m + LEFT JOIN recipes r ON m.recipe_id = r.id + WHERE m.year = ? AND m.week = ? + ORDER BY m.day_of_week ASC, + CASE m.meal_type + WHEN 'frueh' THEN 1 + WHEN 'mittag' THEN 2 + WHEN 'kaffee' THEN 3 + WHEN 'abend' THEN 4 + ELSE 5 + END ASC + """ + return execute_query(query, (year, week), fetch="all") + + +def set_meal_plan_slot( + year: int, + week: int, + day_of_week: int, + meal_type: str, + recipe_id: Optional[int] = None, + custom_text: Optional[str] = None +) -> Dict[str, Any]: + """Setzt oder aktualisiert einen Slot im Wochenplan.""" + if DB_ENGINE == "mariadb": + query = """ + INSERT INTO meal_plans (year, week, day_of_week, meal_type, recipe_id, custom_text) + VALUES (?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + recipe_id = VALUES(recipe_id), + custom_text = VALUES(custom_text), + updated_at = CURRENT_TIMESTAMP + """ + execute_query(query, (year, week, day_of_week, meal_type, recipe_id, custom_text), fetch="none", commit=True) + else: + query = """ + INSERT INTO meal_plans (year, week, day_of_week, meal_type, recipe_id, custom_text, updated_at) + VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(year, week, day_of_week, meal_type) DO UPDATE SET + recipe_id = excluded.recipe_id, + custom_text = excluded.custom_text, + updated_at = CURRENT_TIMESTAMP + """ + execute_query(query, (year, week, day_of_week, meal_type, recipe_id, custom_text), fetch="none", commit=True) + + slot_query = """ + SELECT + m.id, m.year, m.week, m.day_of_week, m.meal_type, m.recipe_id, m.custom_text, + r.title AS recipe_title, r.image_url AS recipe_image_url + FROM meal_plans m + LEFT JOIN recipes r ON m.recipe_id = r.id + WHERE m.year = ? AND m.week = ? AND m.day_of_week = ? AND m.meal_type = ? + """ + return execute_query(slot_query, (year, week, day_of_week, meal_type), fetch="one") + + +def delete_meal_plan_slot(year: int, week: int, day_of_week: int, meal_type: str) -> bool: + """Löscht einen einzelnen Slot aus dem Wochenplan.""" + query = "DELETE FROM meal_plans WHERE year = ? AND week = ? AND day_of_week = ? AND meal_type = ?" + res = execute_query(query, (year, week, day_of_week, meal_type), fetch="none", commit=True) + return res.get("rowcount", 0) > 0 + + +def clear_meal_plan_week(year: int, week: int) -> int: + """Leert den gesamten Wochenplan für eine Kalenderwoche.""" + query = "DELETE FROM meal_plans WHERE year = ? AND week = ?" + res = execute_query(query, (year, week), fetch="none", commit=True) + return res.get("rowcount", 0) + + diff --git a/app/ingredients.py b/app/ingredients.py new file mode 100644 index 0000000..4f4f7ff --- /dev/null +++ b/app/ingredients.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +""" +Helper module for ingredient parsing, table structuring, and quantity scaling. +""" +import re +from typing import List, Dict, Any, Optional + +COMMON_UNITS = [ + "kg", "g", "mg", "l", "liter", "ml", "cl", "el", "tl", "esslöffel", "teelöffel", + "pck.", "pck", "päckchen", "pkt.", "pkt", "dose", "dosen", "glas", "gläser", + "prise", "prisen", "msp.", "msp", "messerspitze", "bund", "stk.", "stk", + "stück", "stücke", "zehe", "zehen", "zweig", "zweige", "blatt", "blätter", + "scheibe", "scheiben", "stange", "stangen", "kopf", "nest", "nester", "tropfen", "tr." +] + +def parse_fraction(val_str: str) -> Optional[float]: + val_str = val_str.strip() + if "/" in val_str: + parts = val_str.split("/") + if len(parts) == 2: + try: + return float(parts[0]) / float(parts[1]) + except ValueError: + pass + try: + return float(val_str.replace(",", ".")) + except ValueError: + return None + +def parse_ingredient_line(line: str) -> Dict[str, Any]: + raw = line.strip() + if not raw: + return {"type": "empty", "raw": ""} + + # Check if section header (ends with colon or keywords) + lower = raw.lower() + if raw.endswith(":") or lower.startswith("für ") or lower in [ + "teig", "sauce", "soße", "füllung", "belag", "creme", "glasur", "marinade", "deko", + "schichten & deko", "keksschicht", "heidelbeersoße", "zitronen-zimt-sirup", "zutaten", + "einlage & toppings", "gemüse & toppings", "lemon curd", "mascarponecreme" + ]: + hdr_text = raw.rstrip(":") + return { + "type": "header", + "is_header": True, + "text": hdr_text, + "name": hdr_text, + "raw": raw + } + + # Regex to match leading amount + match = re.match(r"^(\d+\s+\d+/\d+|\d+/\d+|\d+(?:[.,]\d+)?)(?:\s*-\s*(\d+(?:[.,]\d+)?))?\s*(.*)$", raw) + if match: + amt_str = match.group(1).strip() + range_end = match.group(2) + rest = match.group(3).strip() + + if " " in amt_str and "/" in amt_str: + whole, frac = amt_str.split() + val = float(whole) + (parse_fraction(frac) or 0.0) + elif "/" in amt_str: + val = parse_fraction(amt_str) or 0.0 + else: + val = parse_fraction(amt_str) or 0.0 + + display_amt = f"{amt_str}-{range_end}" if range_end else amt_str + + rest_parts = rest.split(None, 1) + if rest_parts: + first_word_clean = rest_parts[0].lower().rstrip(".,") + if first_word_clean in COMMON_UNITS or rest_parts[0].lower() in COMMON_UNITS: + unit = rest_parts[0] + ing_name = rest_parts[1] if len(rest_parts) > 1 else "" + return { + "type": "ingredient", + "is_header": False, + "amount_val": val, + "amount": val, + "amount_str": display_amt, + "amount_formatted": display_amt, + "unit": unit, + "name": ing_name, + "raw": raw + } + + return { + "type": "ingredient", + "is_header": False, + "amount_val": val, + "amount": val, + "amount_str": display_amt, + "amount_formatted": display_amt, + "unit": "", + "name": rest, + "raw": raw + } + + return { + "type": "ingredient", + "is_header": False, + "amount_val": None, + "amount": None, + "amount_str": "", + "amount_formatted": "", + "unit": "", + "name": raw, + "raw": raw + } + +def parse_ingredients_block(text: str) -> List[Dict[str, Any]]: + lines = [l.strip() for l in (text or "").split("\n") if l.strip()] + return [parse_ingredient_line(l) for l in lines] + + +def format_quantity_py(num: Optional[float]) -> str: + """Formatiert eine Zahl lesbar für Rezepte (z. B. 0.5 -> ½, 1.5 -> 1 ½, 2.0 -> 2).""" + if num is None or num <= 0: + return "" + whole = int(num) + frac = round((num - whole) * 100) / 100.0 + + frac_str = "" + if 0.20 <= frac <= 0.30: + frac_str = "¼" + elif 0.31 <= frac <= 0.40: + frac_str = "⅓" + elif 0.45 <= frac <= 0.55: + frac_str = "½" + elif 0.63 <= frac <= 0.70: + frac_str = "⅔" + elif 0.72 <= frac <= 0.80: + frac_str = "¾" + + if frac_str: + return f"{whole} {frac_str}".strip() if whole > 0 else frac_str + + if abs(num - round(num)) < 0.05: + return str(int(round(num))) + + # Deutsche Kommaschreibweise + val_str = f"{num:.1f}".rstrip("0").rstrip(".").replace(".", ",") + return val_str + + +def calculate_portion_matrix( + parsed_ingredients: List[Dict[str, Any]], + base_portions: int = 4, + targets: tuple = (2, 4, 6, 8) +) -> List[Dict[str, Any]]: + """Berechnet für Waffeln / Pancakes etc. eine vorberechnete Tabelle für 2, 4, 6, 8 Portionen.""" + if not base_portions or base_portions <= 0: + base_portions = 4 + + matrix = [] + for item in parsed_ingredients: + if item.get("is_header"): + matrix.append({ + "is_header": True, + "text": item.get("text") or item.get("name") or "", + "amounts": {p: "" for p in targets} + }) + continue + + row_amounts = {} + for p in targets: + amt_val = item.get("amount_val") + unit = item.get("unit") or "" + if amt_val is not None: + scaled_val = amt_val * (p / float(base_portions)) + formatted_amt = format_quantity_py(scaled_val) + row_amounts[p] = f"{formatted_amt} {unit}".strip() + else: + row_amounts[p] = "—" + + matrix.append({ + "is_header": False, + "name": item.get("name") or item.get("raw") or "", + "raw": item.get("raw", ""), + "amounts": row_amounts + }) + + return matrix + diff --git a/app/main.py b/app/main.py new file mode 100755 index 0000000..d6de366 --- /dev/null +++ b/app/main.py @@ -0,0 +1,856 @@ +import os +import uuid +import random +import shutil +import time +import hmac +import hashlib +from typing import List, Optional, Dict, Any +from contextlib import asynccontextmanager + +from fastapi import FastAPI, HTTPException, Request, Query, Form, File, UploadFile +from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from datetime import datetime, date, timedelta +from pydantic import BaseModel, Field + +from .ingredients import parse_ingredients_block, calculate_portion_matrix +from .database import ( + init_db, + get_all_dishes, + add_dish, + update_dish, + delete_dish, + get_stats, + query_candidates, + query_recipe_candidates, + CATEGORIES, + EFFORTS, + CHAPTERS, + CHAPTER_MAP, + get_all_recipes, + get_recipe, + add_recipe, + update_recipe, + delete_recipe, + get_chapter_info_for_dish, + mark_recipes_printed, + unmark_recipes_printed, + toggle_recipe_printed, + get_meal_plan, + set_meal_plan_slot, + delete_meal_plan_slot, + clear_meal_plan_week +) + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +STATIC_DIR = os.path.join(BASE_DIR, "static") +TEMPLATES_DIR = os.path.join(BASE_DIR, "templates") + +# --- Authentifizierung & Privater Modus --- +APP_USERNAME = os.getenv("APP_USERNAME", "admin") +APP_PASSWORD = os.getenv("APP_PASSWORD", "wtc2026!") +SECRET_KEY = os.getenv("SECRET_KEY", "wtc_cookbook_secret_salt_2026_supersecure") +COOKIE_NAME = "wtc_auth" +PUBLIC_PATHS = {"/login", "/api/login", "/favicon.ico"} + +def create_auth_token(username: str, days: int = 30) -> str: + expiry = int(time.time()) + (days * 86400) + msg = f"{username}:{expiry}" + sig = hmac.new(SECRET_KEY.encode("utf-8"), msg.encode("utf-8"), hashlib.sha256).hexdigest() + return f"{username}:{expiry}:{sig}" + +def verify_auth_token(token: Optional[str]) -> bool: + if not token: + return False + parts = token.split(":") + if len(parts) != 3: + return False + user, expiry_str, sig = parts + try: + expiry = int(expiry_str) + except ValueError: + return False + if time.time() > expiry: + return False + msg = f"{user}:{expiry}" + expected_sig = hmac.new(SECRET_KEY.encode("utf-8"), msg.encode("utf-8"), hashlib.sha256).hexdigest() + return hmac.compare_digest(sig, expected_sig) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + init_db() + os.makedirs(os.path.join(STATIC_DIR, "uploads", "recipes"), exist_ok=True) + yield + +app = FastAPI(title="What To Cook? - Zufallsgenerator & Kochbuch", lifespan=lifespan) + +# Middleware für Website-weiten Schutz +@app.middleware("http") +async def auth_middleware(request: Request, call_next): + path = request.url.path + if path.startswith("/static/") or path in PUBLIC_PATHS: + return await call_next(request) + + token = request.cookies.get(COOKIE_NAME) + if not verify_auth_token(token): + if path.startswith("/api/"): + return JSONResponse( + status_code=401, + content={"status": "unauthorized", "message": "Bitte anmelden."} + ) + next_target = request.url.path + if request.url.query: + next_target += f"?{request.url.query}" + return RedirectResponse(url=f"/login?next={next_target}", status_code=303) + + return await call_next(request) + + +app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") +templates = Jinja2Templates(directory=TEMPLATES_DIR) + +# Mapping Kochbuch-Kapitel -> Zufallsgenerator-Kategorien +CHAPTER_TO_DISH_CAT = { + "Suppen & Eintöpfe": "Suppen", + "Traditionell & Basics": "Hausmannskost", + "Pasta & Nudelgerichte": "Nudeln", + "Kartoffeln": "Kartoffeln", + "Reis & Getreide": "Reis", + "Teig & Gebackenes": "Teigig", + "Asiatische Küche": "asiatisch", + "Süßspeisen": "Süßspeisen", + "Desserts": "Süßspeisen", + "Kuchen & Gebäck": "Süßspeisen", + "Plätzchen": "Süßspeisen" +} + +# --- Pydantic Models --- + +class SlotRollRequest(BaseModel): + slot_number: int = Field(..., ge=1, le=5) + chapter_number: Optional[int] = None + category: Optional[str] = None + efforts: Optional[List[str]] = None + current_dish_id: Optional[int] = None + +class MultiRollRequest(BaseModel): + slots: List[SlotRollRequest] + +class SingleRollRequest(BaseModel): + slot_number: int = Field(..., ge=1, le=5) + chapter_number: Optional[int] = None + category: Optional[str] = None + efforts: Optional[List[str]] = None + current_dish_id: Optional[int] = None + other_dish_ids: Optional[List[int]] = Field(default_factory=list) + +class DishCreate(BaseModel): + name: str + category: Optional[str] = None + effort: str + +class DishUpdate(BaseModel): + name: str + category: Optional[str] = None + effort: str + +class MarkPrintedRequest(BaseModel): + recipe_ids: List[int] + printed_at: Optional[str] = None + +class UnmarkPrintedRequest(BaseModel): + recipe_ids: List[int] + +class MealPlanSlotRequest(BaseModel): + year: int + week: int + day_of_week: int + meal_type: str + recipe_id: Optional[int] = None + custom_text: Optional[str] = None + + +# ========================================== +# AUTH VIEWS +# ========================================== + +@app.get("/login", response_class=HTMLResponse) +async def login_get(request: Request, next: str = "/cookbook"): + token = request.cookies.get(COOKIE_NAME) + if verify_auth_token(token): + return RedirectResponse(url=next or "/cookbook", status_code=303) + return templates.TemplateResponse( + request=request, + name="login.html", + context={"next_url": next, "error": None, "username": APP_USERNAME} + ) + +@app.post("/login", response_class=HTMLResponse) +async def login_post( + request: Request, + username: str = Form("admin"), + password: str = Form(...), + remember: Optional[str] = Form(None), + next: str = Form("/cookbook") +): + valid_user = (username.strip().lower() == APP_USERNAME.lower()) or (username.strip() == "admin") + valid_pass = (password == APP_PASSWORD) + + if valid_user and valid_pass: + days = 30 if remember else 1 + token = create_auth_token(username.strip(), days=days) + target = next if next and not next.startswith("/login") else "/cookbook" + resp = RedirectResponse(url=target, status_code=303) + max_age = days * 86400 + resp.set_cookie( + key=COOKIE_NAME, + value=token, + max_age=max_age, + expires=max_age, + path="/", + httponly=True, + samesite="lax" + ) + return resp + else: + return templates.TemplateResponse( + request=request, + name="login.html", + context={ + "next_url": next, + "error": "Ungültiges Passwort oder Benutzername.", + "username": username + }, + status_code=400 + ) + +@app.get("/logout") +async def logout(): + resp = RedirectResponse(url="/login", status_code=303) + resp.delete_cookie(key=COOKIE_NAME, path="/") + return resp + + +# ========================================== +# VIEWS +# ========================================== + +@app.get("/") +async def index_view(): + """Hauptroute leitet direkt auf das Kochbuch um""" + return RedirectResponse(url="/cookbook", status_code=302) + +@app.get("/cookbook", response_class=HTMLResponse) +async def cookbook_view( + request: Request, + chapter: Optional[int] = Query(None) +): + """Kochbuch Übersicht mit Kapiteln, Inhaltsverzeichnis und Rezepten""" + all_recipes = get_all_recipes() + + # Berechne Portions-Matrix nur für Waffeln / ausgewählte Rezepte + for r in all_recipes: + is_matrix = bool(r.get("has_portion_table")) or any(k in (r.get("title") or "").lower() for k in ["waffel", "pancake", "pfannkuchen"]) + r["has_portion_table"] = is_matrix + if is_matrix: + parsed = parse_ingredients_block(r.get("ingredients", "")) + r["portion_matrix"] = calculate_portion_matrix(parsed, base_portions=r.get("portions") or 4, targets=(2, 4, 6, 8)) + + # Gruppieren nach Kapitel + recipes_by_chapter: Dict[int, List[Dict[str, Any]]] = {} + for r in all_recipes: + chap_num = r.get("chapter_number", 1) + if chap_num not in recipes_by_chapter: + recipes_by_chapter[chap_num] = [] + recipes_by_chapter[chap_num].append(r) + + recipe_counts = {c["number"]: len(recipes_by_chapter.get(c["number"], [])) for c in CHAPTERS} + + if chapter is not None and chapter > 0: + chapters_to_display = [c for c in CHAPTERS if c["number"] == chapter] + else: + chapters_to_display = CHAPTERS + + # --- Seitenzahlen & Inhaltsverzeichnis (TOC) berechnen --- + # Seite 1: Cover + # TOC: 1 oder 2 Seiten (wenn > 20 Rezepte vorhanden sind) + total_display_recipes = len(all_recipes) + toc_page_count = 2 if total_display_recipes > 20 else (1 if total_display_recipes > 0 else 0) + current_page = 1 + toc_page_count + 1 + + display_chapters_data = [] + for chap in chapters_to_display: + chap_copy = dict(chap) + chap_recipes = recipes_by_chapter.get(chap["number"], []) + if not chap_recipes: + display_chapters_data.append(chap_copy) + continue + + chap_copy["page_number"] = current_page + current_page += 1 + + for r in chap_recipes: + r["page_number"] = current_page + current_page += 1 + + chap_copy["recipes"] = chap_recipes + display_chapters_data.append(chap_copy) + + # Inhaltsverzeichnis auf 2 Seiten aufteilen + toc_p1 = [] + toc_p2 = [] + if toc_page_count == 2: + accum = 0 + for c in display_chapters_data: + c_recs = recipes_by_chapter.get(c["number"], []) + if not c_recs: + continue + item_weight = len(c_recs) + 2 + if accum < 28 or not toc_p1: + toc_p1.append(c) + accum += item_weight + else: + toc_p2.append(c) + else: + toc_p1 = [c for c in display_chapters_data if recipes_by_chapter.get(c["number"], [])] + + # User Agent Check für Apple iOS + user_agent = request.headers.get("user-agent", "").lower() + is_ios = any(d in user_agent for d in ["iphone", "ipad", "ipod"]) + + return templates.TemplateResponse( + request=request, + name="cookbook.html", + context={ + "chapters": CHAPTERS, + "current_chapter": chapter, + "chapters_to_display": display_chapters_data, + "recipes_by_chapter": recipes_by_chapter, + "recipe_counts": recipe_counts, + "total_recipes": len(all_recipes), + "toc_page_count": toc_page_count, + "toc_p1": toc_p1, + "toc_p2": toc_p2, + "is_ios": is_ios + } + ) + +@app.get("/cookbook/recipe/{recipe_id}", response_class=HTMLResponse) +async def recipe_detail_view(request: Request, recipe_id: int): + """Einheitliche Rezeptseite im handgezeichneten Stil""" + recipe = get_recipe(recipe_id) + if not recipe: + raise HTTPException(status_code=404, detail="Rezept nicht gefunden.") + + is_matrix = bool(recipe.get("has_portion_table")) or any(k in (recipe.get("title") or "").lower() for k in ["waffel", "pancake", "pfannkuchen"]) + recipe["has_portion_table"] = is_matrix + if is_matrix: + parsed = parse_ingredients_block(recipe.get("ingredients", "")) + recipe["portion_matrix"] = calculate_portion_matrix(parsed, base_portions=recipe.get("portions") or 4, targets=(2, 4, 6, 8)) + + return templates.TemplateResponse( + request=request, + name="recipe_detail.html", + context={ + "recipe": recipe, + "chapters": CHAPTERS + } + ) + +@app.get("/cookbook/new", response_class=HTMLResponse) +async def recipe_new_view(request: Request, chapter: Optional[int] = Query(None)): + """Eingabemaske für neues Rezept""" + return templates.TemplateResponse( + request=request, + name="recipe_form.html", + context={ + "recipe": None, + "chapters": CHAPTERS, + "selected_chapter": chapter + } + ) + +@app.get("/cookbook/edit/{recipe_id}", response_class=HTMLResponse) +async def recipe_edit_view(request: Request, recipe_id: int): + """Eingabemaske zum Bearbeiten eines Rezepts""" + recipe = get_recipe(recipe_id) + if not recipe: + raise HTTPException(status_code=404, detail="Rezept nicht gefunden.") + + return templates.TemplateResponse( + request=request, + name="recipe_form.html", + context={ + "recipe": recipe, + "chapters": CHAPTERS, + "selected_chapter": recipe.get("chapter_number") + } + ) + + +# ========================================== +# API: KOCHBUCH (RECIPES) +# ========================================== + +def save_uploaded_image(image_file: UploadFile) -> Optional[str]: + if not image_file or not image_file.filename: + return None + + ext = os.path.splitext(image_file.filename)[1].lower() + if ext not in [".jpg", ".jpeg", ".png", ".webp", ".gif"]: + return None + + upload_dir = os.path.join(STATIC_DIR, "uploads", "recipes") + os.makedirs(upload_dir, exist_ok=True) + + safe_name = f"{uuid.uuid4().hex[:10]}_{os.path.basename(image_file.filename)}" + file_path = os.path.join(upload_dir, safe_name) + + with open(file_path, "wb") as buffer: + shutil.copyfileobj(image_file.file, buffer) + + return f"/static/uploads/recipes/{safe_name}" + +@app.get("/api/recipes") +def api_get_recipes( + chapter: Optional[int] = Query(None), + category: Optional[str] = Query(None), + search: Optional[str] = Query(None), + printed: Optional[str] = Query(None) +): + return get_all_recipes(chapter_number=chapter, category=category, search=search, printed_status=printed) + +@app.get("/api/recipes/{recipe_id}") +def api_get_recipe(recipe_id: int): + recipe = get_recipe(recipe_id) + if not recipe: + raise HTTPException(status_code=404, detail="Rezept nicht gefunden.") + recipe["parsed_ingredients"] = parse_ingredients_block(recipe.get("ingredients", "")) + return recipe + +@app.post("/api/recipes") +async def api_create_recipe( + title: str = Form(...), + chapter_number: int = Form(...), + effort: str = Form("mittel"), + prep_time: str = Form(""), + cook_time: str = Form(""), + portions: int = Form(4), + ingredients: str = Form(...), + instructions: str = Form(...), + notes: str = Form(""), + source: str = Form(""), + printed_at: Optional[str] = Form(None), + has_portion_table: Optional[int] = Form(0), + add_to_dishes: Optional[str] = Form(None), + image_file: Optional[UploadFile] = File(None) +): + chapter_info = next((c for c in CHAPTERS if c["number"] == chapter_number), None) + category_title = chapter_info["title"] if chapter_info else "Traditionell & Basics" + + image_url = "" + if image_file and image_file.filename: + image_url = save_uploaded_image(image_file) or "" + + try: + recipe = add_recipe( + title=title, + category=category_title, + chapter_number=chapter_number, + effort=effort, + prep_time=prep_time, + cook_time=cook_time, + portions=portions, + ingredients=ingredients, + instructions=instructions, + notes=notes, + source=source, + image_url=image_url, + printed_at=printed_at if printed_at and printed_at.strip() else None, + has_portion_table=1 if (has_portion_table and str(has_portion_table).strip() in ("1", "true", "on", "yes")) else 0 + ) + + should_add = bool(add_to_dishes and str(add_to_dishes).strip().lower() in ("true", "on", "1", "yes")) + if should_add: + dish_cat = CHAPTER_TO_DISH_CAT.get(category_title, "Hausmannskost") + dish_effort = effort if effort in EFFORTS else "mittel" + try: + add_dish(name=title, category=dish_cat, effort=dish_effort) + except Exception: + pass + + return {"status": "ok", "recipe": recipe} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + +@app.put("/api/recipes/{recipe_id}") +async def api_update_recipe( + recipe_id: int, + title: str = Form(...), + chapter_number: int = Form(...), + effort: str = Form("mittel"), + prep_time: str = Form(""), + cook_time: str = Form(""), + portions: int = Form(4), + ingredients: str = Form(...), + instructions: str = Form(...), + notes: str = Form(""), + source: str = Form(""), + printed_at: Optional[str] = Form(None), + has_portion_table: Optional[int] = Form(None), + image_file: Optional[UploadFile] = File(None) +): + chapter_info = next((c for c in CHAPTERS if c["number"] == chapter_number), None) + category_title = chapter_info["title"] if chapter_info else "Traditionell & Basics" + + new_image_url = None + if image_file and image_file.filename: + new_image_url = save_uploaded_image(image_file) + + try: + table_val = 1 if (has_portion_table and str(has_portion_table).strip() in ("1", "true", "on", "yes")) else (0 if has_portion_table is not None else None) + recipe = update_recipe( + recipe_id=recipe_id, + title=title, + category=category_title, + chapter_number=chapter_number, + effort=effort, + prep_time=prep_time, + cook_time=cook_time, + portions=portions, + ingredients=ingredients, + instructions=instructions, + notes=notes, + source=source, + image_url=new_image_url, + printed_at=printed_at if printed_at and printed_at.strip() else None, + has_portion_table=table_val + ) + return {"status": "ok", "recipe": recipe} + except KeyError as e: + raise HTTPException(status_code=404, detail=str(e)) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + +@app.delete("/api/recipes/{recipe_id}") +def api_delete_recipe(recipe_id: int): + success = delete_recipe(recipe_id) + if not success: + raise HTTPException(status_code=404, detail="Rezept nicht gefunden.") + return {"status": "ok", "deleted": True} + + +# --- API: Druckstatus & Druckdatum --- + +@app.post("/api/recipes/mark-printed") +def api_mark_recipes_printed(payload: MarkPrintedRequest): + count = mark_recipes_printed(payload.recipe_ids, payload.printed_at) + return {"status": "ok", "marked_count": count} + +@app.post("/api/recipes/unmark-printed") +def api_unmark_recipes_printed(payload: UnmarkPrintedRequest): + count = unmark_recipes_printed(payload.recipe_ids) + return {"status": "ok", "unmarked_count": count} + +@app.post("/api/recipes/{recipe_id}/toggle-printed") +def api_toggle_recipe_printed(recipe_id: int): + try: + updated = toggle_recipe_printed(recipe_id) + return {"status": "ok", "recipe": updated} + except KeyError as e: + raise HTTPException(status_code=404, detail=str(e)) + + +# ========================================== +# API: ZUFALLSGENERATOR & GERICHTE +# ========================================== + +@app.get("/api/categories") +def api_categories(): + return { + "categories": CATEGORIES, + "efforts": EFFORTS, + "chapters": CHAPTERS + } + +@app.get("/api/stats") +def api_stats(): + return get_stats() + +@app.get("/api/dishes") +def api_get_dishes( + category: Optional[str] = Query(None), + effort: Optional[str] = Query(None), + search: Optional[str] = Query(None) +): + return get_all_dishes(category=category, effort=effort, search=search) + +@app.post("/api/dishes") +def api_create_dish(payload: DishCreate): + try: + cat = payload.category or "Hausmannskost" + dish = add_dish(payload.name, cat, payload.effort) + return {"status": "ok", "dish": dish} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + +@app.put("/api/dishes/{dish_id}") +def api_update_dish(dish_id: int, payload: DishUpdate): + try: + cat = payload.category or "Hausmannskost" + dish = update_dish(dish_id, payload.name, cat, payload.effort) + return {"status": "ok", "dish": dish} + except KeyError as e: + raise HTTPException(status_code=404, detail=str(e)) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + +@app.delete("/api/dishes/{dish_id}") +def api_delete_dish(dish_id: int): + success = delete_dish(dish_id) + if not success: + raise HTTPException(status_code=404, detail="Gericht nicht gefunden.") + return {"status": "ok", "deleted": True} + +def resolve_slot_chapter(slot) -> Optional[int]: + if getattr(slot, "chapter_number", None) and slot.chapter_number > 0: + return slot.chapter_number + if slot.category: + s = str(slot.category).strip() + if s.isdigit(): + return int(s) + for c in CHAPTERS: + if s.lower() in c["title"].lower(): + return c["number"] + return None + +@app.post("/api/roll") +def api_roll_dishes(payload: MultiRollRequest): + """Rolls recipes directly from the cookbook without duplicates in round.""" + slots = payload.slots + if not slots: + raise HTTPException(status_code=400, detail="Keine Mahlzeiten zur Auslosung übergeben.") + if len(slots) > 5: + raise HTTPException(status_code=400, detail="Maximal 5 Mahlzeiten können gleichzeitig ausgelost werden.") + + results = [] + chosen_ids = set() + + slot_candidates = [] + for slot in slots: + chap_num = resolve_slot_chapter(slot) + candidates = query_recipe_candidates(chap_num, slot.efforts) + slot_candidates.append({ + "slot": slot, + "chapter_number": chap_num, + "candidates": candidates + }) + + indexed_slots = list(enumerate(slot_candidates)) + indexed_slots.sort(key=lambda item: len(item[1]["candidates"])) + + temp_assignment = {} + + for original_idx, item in indexed_slots: + slot = item["slot"] + chap_num = item["chapter_number"] + candidates = item["candidates"] + + available = [c for c in candidates if c["id"] not in chosen_ids] + if not available: + available = candidates + + if not available: + chap_title = CHAPTERS[chap_num - 1]["title"] if chap_num and 1 <= chap_num <= len(CHAPTERS) else "" + temp_assignment[original_idx] = { + "slot_number": slot.slot_number, + "recipe": None, + "dish": None, + "status": "not_found", + "chapter_number": chap_num, + "chapter_title": chap_title, + "message": f"Noch keine Rezepte in Kapitel {chap_num} ({chap_title}) vorhanden." if chap_num else "Noch keine Rezepte im Kochbuch vorhanden." + } + continue + + alternatives = [c for c in available if c["id"] != slot.current_dish_id] + selection_pool = alternatives if alternatives else available + + selected = random.choice(selection_pool) + chosen_ids.add(selected["id"]) + + selected_recipe = dict(selected) + c_num = selected_recipe.get("chapter_number", 1) + chap = CHAPTERS[c_num - 1] if 1 <= c_num <= len(CHAPTERS) else CHAPTERS[0] + selected_recipe["chapter_title"] = chap["title"] + selected_recipe["chapter_icon"] = chap["icon"] + selected_recipe["chapter_illustration"] = chap["illustration_url"] + selected_recipe["name"] = selected_recipe["title"] + selected_recipe["recipe_id"] = selected_recipe["id"] + + temp_assignment[original_idx] = { + "slot_number": slot.slot_number, + "dish": selected_recipe, + "recipe": selected_recipe, + "status": "ok" + } + + for idx in range(len(slots)): + results.append(temp_assignment[idx]) + + return {"status": "ok", "results": results} + +@app.post("/api/roll-single") +def api_roll_single(payload: SingleRollRequest): + """Rerolls a single recipe slot directly from cookbook.""" + chap_num = resolve_slot_chapter(payload) + candidates = query_recipe_candidates(chap_num, payload.efforts) + if not candidates: + chap_title = CHAPTERS[chap_num - 1]["title"] if chap_num and 1 <= chap_num <= len(CHAPTERS) else "" + return { + "status": "not_found", + "dish": None, + "recipe": None, + "chapter_number": chap_num, + "chapter_title": chap_title, + "message": f"Noch keine Rezepte in Kapitel {chap_num} ({chap_title}) vorhanden." if chap_num else "Noch keine Rezepte im Kochbuch vorhanden." + } + + other_ids = set(payload.other_dish_ids or []) + pool = [c for c in candidates if c["id"] not in other_ids] + if not pool: + pool = candidates + + alternatives = [c for c in pool if c["id"] != payload.current_dish_id] + selection_pool = alternatives if alternatives else pool + + selected = random.choice(selection_pool) + selected_recipe = dict(selected) + c_num = selected_recipe.get("chapter_number", 1) + chap = CHAPTERS[c_num - 1] if 1 <= c_num <= len(CHAPTERS) else CHAPTERS[0] + selected_recipe["chapter_title"] = chap["title"] + selected_recipe["chapter_icon"] = chap["icon"] + selected_recipe["chapter_illustration"] = chap["illustration_url"] + selected_recipe["name"] = selected_recipe["title"] + selected_recipe["recipe_id"] = selected_recipe["id"] + + return { + "status": "ok", + "slot_number": payload.slot_number, + "dish": selected_recipe, + "recipe": selected_recipe, + "is_repetition": (selected["id"] == payload.current_dish_id and len(candidates) > 1) + } + + +# ========================================== +# API: WOCHENMENÜ-PLAN (MEAL PLANNER) +# ========================================== + +def get_week_dates(year: int, week: int) -> Dict[str, Any]: + """Berechnet die Kalenderdaten (Montag bis Sonntag) für eine gegebene KW.""" + try: + monday = date.fromisocalendar(year, week, 1) + except AttributeError: + first_day = date(year, 1, 4) + monday = first_day + timedelta(days=((week - 1) * 7) - first_day.weekday()) + + day_names = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] + days = [] + for i in range(7): + cur_date = monday + timedelta(days=i) + days.append({ + "day_index": i, + "name": day_names[i], + "date_formatted": cur_date.strftime("%d.%m."), + "iso_date": cur_date.strftime("%Y-%m-%d"), + "is_today": (cur_date == date.today()) + }) + sunday = monday + timedelta(days=6) + return { + "year": year, + "week": week, + "range_formatted": f"{monday.strftime('%d.%m.')} – {sunday.strftime('%d.%m.%Y')}", + "days": days + } + + +@app.get("/api/mealplan") +def api_get_mealplan(year: Optional[int] = Query(None), week: Optional[int] = Query(None)): + today = date.today() + iso_year, iso_week, _ = today.isocalendar() + y = year or iso_year + w = week or iso_week + week_info = get_week_dates(y, w) + slots = get_meal_plan(y, w) + + # Slot-Map: "0_frueh", "0_mittag", "0_abend" etc. + slot_map = {} + for s in slots: + k = f"{s['day_of_week']}_{s['meal_type']}" + slot_map[k] = s + + # Kompakte Rezeptliste für schnelles Dropdown / Zuweisung + all_recs = get_all_recipes() + recipes_summary = [ + { + "id": r["id"], + "title": r["title"], + "category": r.get("category", ""), + "chapter_number": r.get("chapter_number", 1), + "effort": r.get("effort", "mittel") + } + for r in all_recs + ] + + return { + "status": "ok", + "year": y, + "week": w, + "current_year": iso_year, + "current_week": iso_week, + "is_current_week": (y == iso_year and w == iso_week), + "range_formatted": week_info["range_formatted"], + "days": week_info["days"], + "slots": slot_map, + "recipes": recipes_summary + } + + +@app.post("/api/mealplan/slot") +def api_save_mealplan_slot(payload: MealPlanSlotRequest): + if payload.day_of_week < 0 or payload.day_of_week > 6: + raise HTTPException(status_code=400, detail="Ungültiger Wochentag (0-6).") + if payload.meal_type not in ["frueh", "mittag", "kaffee", "abend"]: + raise HTTPException(status_code=400, detail="Ungültiger Mahlzeittyp (frueh, mittag, kaffee, abend).") + + slot = set_meal_plan_slot( + year=payload.year, + week=payload.week, + day_of_week=payload.day_of_week, + meal_type=payload.meal_type, + recipe_id=payload.recipe_id, + custom_text=payload.custom_text + ) + return {"status": "ok", "slot": slot} + + +@app.delete("/api/mealplan/slot") +def api_delete_mealplan_slot( + year: int = Query(...), + week: int = Query(...), + day_of_week: int = Query(...), + meal_type: str = Query(...) +): + success = delete_meal_plan_slot(year, week, day_of_week, meal_type) + return {"status": "ok", "deleted": success} + + +@app.delete("/api/mealplan/week") +def api_clear_mealplan_week(year: int = Query(...), week: int = Query(...)): + count = clear_meal_plan_week(year, week) + return {"status": "ok", "cleared_count": count} + diff --git a/app/static/app.js b/app/static/app.js new file mode 100755 index 0000000..df09068 --- /dev/null +++ b/app/static/app.js @@ -0,0 +1,695 @@ +/** + * What To Cook? - Frontend Application + */ + +// Category icons mapping +const CATEGORY_ICONS = { + "Reis": "🍚", + "Nudeln": "🍝", + "Kartoffeln": "🥔", + "Teigig": "🍕", + "asiatisch": "🥢", + "Hausmannskost": "🥩", + "Suppen": "🍲", + "Eintöpfe": "🥘", + "Süßspeisen": "🥞", + "Fisch": "🐟" +}; + +const CATEGORIES = [ + "Reis", + "Nudeln", + "Kartoffeln", + "Teigig", + "asiatisch", + "Hausmannskost", + "Suppen", + "Eintöpfe", + "Süßspeisen", + "Fisch" +]; + +const EFFORTS = ["gering", "mittel", "hoch"]; + +function getDishCategories(dish) { + if (!dish) return []; + if (Array.isArray(dish.categories) && dish.categories.length > 0) { + return dish.categories; + } + if (dish.category) { + return dish.category.split(",").map(c => c.trim()).filter(Boolean); + } + return []; +} + +function renderCategoryBadges(dish) { + const cats = getDishCategories(dish); + if (cats.length === 0) return ""; + return cats.map(cat => { + const icon = CATEGORY_ICONS[cat] || "🍲"; + return `${icon} ${cat}`; + }).join(""); +} + +// Application State +const state = { + activeTab: "generator", + slots: [ + { slot_number: 1, active: true, category: "", efforts: [], current_dish: null }, + { slot_number: 2, active: true, category: "", efforts: [], current_dish: null }, + { slot_number: 3, active: false, category: "", efforts: [], current_dish: null }, + { slot_number: 4, active: false, category: "", efforts: [], current_dish: null }, + { slot_number: 5, active: false, category: "", efforts: [], current_dish: null }, + ], + admin: { + dishes: [], + search: "", + category: "alle", + effort: "alle", + deleteTargetId: null + } +}; + +// --- Initialization --- +document.addEventListener("DOMContentLoaded", () => { + initTabs(); + renderSlots(); + populateDropdowns(); + initGeneratorEvents(); + initAdminEvents(); + loadAdminDishes(); +}); + +// --- Tab Navigation --- +function initTabs() { + const btnGenerator = document.getElementById("tab-btn-generator"); + const btnAdmin = document.getElementById("tab-btn-admin"); + const panelGenerator = document.getElementById("tab-generator"); + const panelAdmin = document.getElementById("tab-admin"); + + btnGenerator.addEventListener("click", () => switchTab("generator")); + btnAdmin.addEventListener("click", () => switchTab("admin")); + + function switchTab(tabName) { + state.activeTab = tabName; + if (tabName === "generator") { + btnGenerator.classList.add("active"); + btnAdmin.classList.remove("active"); + panelGenerator.classList.add("active"); + panelAdmin.classList.remove("active"); + panelAdmin.hidden = true; + panelGenerator.hidden = false; + } else { + btnAdmin.classList.add("active"); + btnGenerator.classList.remove("active"); + panelAdmin.classList.add("active"); + panelGenerator.classList.remove("active"); + panelGenerator.hidden = true; + panelAdmin.hidden = false; + loadAdminDishes(); + } + } +} + +// --- Populate Category Dropdowns --- +function populateDropdowns() { + const adminCatSelect = document.getElementById("admin-filter-category"); + const modalCatGroup = document.getElementById("modal-category-group"); + + if (modalCatGroup) { + modalCatGroup.innerHTML = ""; + } + + CATEGORIES.forEach(cat => { + const icon = CATEGORY_ICONS[cat] || "🍲"; + + // Admin filter + if (adminCatSelect) { + const optFilter = document.createElement("option"); + optFilter.value = cat; + optFilter.textContent = `${icon} ${cat}`; + adminCatSelect.appendChild(optFilter); + } + + // Modal checkbox pill + if (modalCatGroup) { + const label = document.createElement("label"); + label.className = "category-checkbox-label"; + label.innerHTML = ` + + + ${icon} ${cat} + + `; + modalCatGroup.appendChild(label); + } + }); +} + +// --- Generator Slots Rendering --- +function renderSlots() { + const container = document.getElementById("slots-container"); + container.innerHTML = ""; + + state.slots.forEach(slot => { + const card = document.createElement("div"); + card.className = `slot-card ${slot.active ? "active" : "inactive"}`; + card.id = `slot-card-${slot.slot_number}`; + + card.innerHTML = ` +
+
+ ${slot.slot_number} + Gericht ${slot.slot_number} +
+ +
+ +
+
+ + +
+ +
+ +
+ + + +
+
+
+ `; + + container.appendChild(card); + + // Event: Toggle Active + const toggleInput = card.querySelector(`#slot-toggle-${slot.slot_number}`); + toggleInput.addEventListener("change", (e) => { + slot.active = e.target.checked; + renderSlots(); + updateActiveCounter(); + }); + + // Event: Category Change + const catSelect = card.querySelector(`#slot-cat-${slot.slot_number}`); + catSelect.addEventListener("change", (e) => { + slot.category = e.target.value; + }); + + // Event: Effort Multi-Select Chips + const chips = card.querySelectorAll(`#effort-chips-${slot.slot_number} .effort-chip`); + chips.forEach(chip => { + chip.addEventListener("click", () => { + const effortVal = chip.getAttribute("data-effort"); + const idx = slot.efforts.indexOf(effortVal); + if (idx > -1) { + slot.efforts.splice(idx, 1); + chip.classList.remove("selected"); + } else { + slot.efforts.push(effortVal); + chip.classList.add("selected"); + } + }); + }); + }); + + updateActiveCounter(); +} + +function updateActiveCounter() { + const activeCount = state.slots.filter(s => s.active).length; + const countEl = document.getElementById("active-slots-count"); + if (countEl) countEl.textContent = activeCount; + + const btnHero = document.getElementById("btn-roll-all"); + if (btnHero) { + const btnText = btnHero.querySelector(".btn-text"); + if (activeCount === 0) { + btnText.textContent = "Bitte mindestens 1 Gericht aktivieren"; + btnHero.disabled = true; + } else if (activeCount === 1) { + btnText.textContent = "1 Gericht auslosen"; + btnHero.disabled = false; + } else { + btnText.textContent = `${activeCount} Gerichte auslosen`; + btnHero.disabled = false; + } + } +} + +// --- Generator Events (Roll Logic) --- +function initGeneratorEvents() { + const btnRollAll = document.getElementById("btn-roll-all"); + const btnRerollAll = document.getElementById("btn-reroll-all"); + const btnBack = document.getElementById("btn-back-to-config"); + + btnRollAll.addEventListener("click", () => executeRoll(false)); + btnRerollAll.addEventListener("click", () => executeRoll(true)); + + btnBack.addEventListener("click", () => { + window.scrollTo({ top: 0, behavior: "smooth" }); + }); +} + +async function executeRoll(isReroll = false) { + const activeSlots = state.slots.filter(s => s.active); + if (activeSlots.length === 0) { + showToast("Bitte aktiviere mindestens 1 Gericht.", "error"); + return; + } + + const btnRollAll = document.getElementById("btn-roll-all"); + const btnRerollAll = document.getElementById("btn-reroll-all"); + btnRollAll.disabled = true; + btnRerollAll.disabled = true; + + try { + const payload = { + slots: activeSlots.map(s => ({ + slot_number: s.slot_number, + category: s.category || null, + efforts: s.efforts.length > 0 ? s.efforts : null, + current_dish_id: isReroll && s.current_dish ? s.current_dish.id : null + })) + }; + + const res = await fetch("/api/roll", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + + const data = await res.json(); + if (!res.ok) { + throw new Error(data.detail || "Fehler beim Auslosen."); + } + + // Update slots state with rolled dishes + data.results.forEach(resItem => { + const slot = state.slots.find(s => s.slot_number === resItem.slot_number); + if (slot) { + slot.current_dish = resItem.dish; + } + }); + + renderResults(data.results); + showToast(isReroll ? "Alle Gerichte neu ausgelost!" : "Zufallsauswahl erfolgreich!", "success"); + + // Scroll smoothly to results + const resultsSection = document.getElementById("results-section"); + resultsSection.classList.remove("hidden"); + resultsSection.scrollIntoView({ behavior: "smooth", block: "start" }); + + } catch (err) { + showToast(err.message, "error"); + } finally { + btnRollAll.disabled = false; + btnRerollAll.disabled = false; + } +} + +// Render Results Grid +function renderResults(results) { + const container = document.getElementById("results-list"); + const countBadge = document.getElementById("results-count-badge"); + container.innerHTML = ""; + + const validDishes = results.filter(r => r.dish !== null); + countBadge.textContent = `${validDishes.length} ${validDishes.length === 1 ? "Gericht" : "Gerichte"}`; + + results.forEach(item => { + const slot = state.slots.find(s => s.slot_number === item.slot_number); + const dish = item.dish; + const card = document.createElement("div"); + card.className = "result-card"; + card.id = `result-card-${item.slot_number}`; + + if (!dish) { + card.innerHTML = ` +
+ Gericht ${item.slot_number} +
+
+ ⚠️ Keine passenden Gerichte gefunden +
+

+ Für die gewählte Kombination (Kategorie / Aufwand) gibt es noch keine Einträge. +

+ `; + container.appendChild(card); + return; + } + + const effortLabel = dish.effort.charAt(0).toUpperCase() + dish.effort.slice(1); + + card.innerHTML = ` +
+ Gericht ${item.slot_number} +
+
${dish.name}
+
+ ${renderCategoryBadges(dish)} + + ${effortLabel} + +
+
+ +
+ `; + + container.appendChild(card); + + // Event: Single Reroll Button + const btnRerollSingle = card.querySelector(".btn-reroll-single"); + btnRerollSingle.addEventListener("click", () => rerollSingleSlot(item.slot_number, btnRerollSingle, card)); + }); +} + +// --- Single Slot Reroll Logic --- +async function rerollSingleSlot(slotNumber, btnElement, cardElement) { + const slot = state.slots.find(s => s.slot_number === slotNumber); + if (!slot || !slot.current_dish) return; + + btnElement.classList.add("spinning"); + cardElement.classList.add("rerolling"); + + // Collect other dishes currently on screen so we don't duplicate + const otherDishIds = state.slots + .filter(s => s.active && s.slot_number !== slotNumber && s.current_dish) + .map(s => s.current_dish.id); + + try { + const payload = { + slot_number: slotNumber, + category: slot.category || null, + efforts: slot.efforts.length > 0 ? slot.efforts : null, + current_dish_id: slot.current_dish.id, + other_dish_ids: otherDishIds + }; + + const res = await fetch("/api/roll-single", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || "Fehler beim Neu-Würfeln."); + + if (data.status === "ok" && data.dish) { + slot.current_dish = data.dish; + const effortLabel = data.dish.effort.charAt(0).toUpperCase() + data.dish.effort.slice(1); + + // Smoothly update the card content + const nameEl = cardElement.querySelector(`#dish-name-${slotNumber}`); + if (nameEl) nameEl.textContent = data.dish.name; + + const tagsEl = cardElement.querySelector(".result-tags"); + if (tagsEl) { + tagsEl.innerHTML = ` + ${renderCategoryBadges(data.dish)} + + ${effortLabel} + + `; + } + + showToast(`Gericht ${slotNumber} neu gewürfelt: ${data.dish.name}`, "success"); + } else { + showToast("Kein anderes passendes Gericht in dieser Kategorie gefunden.", "error"); + } + } catch (err) { + showToast(err.message, "error"); + } finally { + setTimeout(() => { + btnElement.classList.remove("spinning"); + cardElement.classList.remove("rerolling"); + }, 450); + } +} + +// --- Admin Section & CRUD --- +function initAdminEvents() { + const searchInput = document.getElementById("admin-search-input"); + const clearSearchBtn = document.getElementById("admin-search-clear"); + const catFilter = document.getElementById("admin-filter-category"); + const effortFilter = document.getElementById("admin-filter-effort"); + + searchInput.addEventListener("input", (e) => { + state.admin.search = e.target.value.trim(); + clearSearchBtn.classList.toggle("hidden", state.admin.search.length === 0); + loadAdminDishes(); + }); + + clearSearchBtn.addEventListener("click", () => { + searchInput.value = ""; + state.admin.search = ""; + clearSearchBtn.classList.add("hidden"); + loadAdminDishes(); + }); + + catFilter.addEventListener("change", (e) => { + state.admin.category = e.target.value; + loadAdminDishes(); + }); + + effortFilter.addEventListener("change", (e) => { + state.admin.effort = e.target.value; + loadAdminDishes(); + }); + + // Modal Open/Close + const dishModal = document.getElementById("dish-modal"); + const btnOpenAdd = document.getElementById("btn-open-add-dish"); + const btnCloseModal = document.getElementById("btn-close-modal"); + const btnCancelModal = document.getElementById("btn-cancel-modal"); + const dishForm = document.getElementById("dish-form"); + + btnOpenAdd.addEventListener("click", () => { + openDishModal(); + }); + + btnCloseModal.addEventListener("click", () => dishModal.close()); + btnCancelModal.addEventListener("click", () => dishModal.close()); + + dishForm.addEventListener("submit", async (e) => { + e.preventDefault(); + await saveDishFromModal(); + }); + + // Delete Modal + const deleteModal = document.getElementById("delete-modal"); + const btnCancelDelete = document.getElementById("btn-cancel-delete"); + const btnConfirmDelete = document.getElementById("btn-confirm-delete"); + + btnCancelDelete.addEventListener("click", () => deleteModal.close()); + btnConfirmDelete.addEventListener("click", async () => { + if (state.admin.deleteTargetId) { + await executeDeleteDish(state.admin.deleteTargetId); + deleteModal.close(); + } + }); +} + +async function loadAdminDishes() { + const listContainer = document.getElementById("admin-dishes-list"); + const statsSummary = document.getElementById("admin-stats-summary"); + const countLabel = document.getElementById("dishes-count-label"); + + try { + const params = new URLSearchParams(); + if (state.admin.category && state.admin.category !== "alle") { + params.append("category", state.admin.category); + } + if (state.admin.effort && state.admin.effort !== "alle") { + params.append("effort", state.admin.effort); + } + if (state.admin.search) { + params.append("search", state.admin.search); + } + + const res = await fetch(`/api/dishes?${params.toString()}`); + const dishes = await res.json(); + state.admin.dishes = dishes; + + // Load overall stats + const statsRes = await fetch("/api/stats"); + const stats = await statsRes.json(); + statsSummary.textContent = `${stats.total} Gerichte in der Datenbank`; + countLabel.textContent = `${dishes.length} ${dishes.length === 1 ? "Gericht" : "Gerichte"} angezeigt`; + + if (dishes.length === 0) { + listContainer.innerHTML = ` +
Keine Gerichte mit diesen Suchkriterien gefunden.
+ `; + return; + } + + listContainer.innerHTML = ""; + dishes.forEach(dish => { + const row = document.createElement("div"); + row.className = "admin-dish-row"; + const effortLabel = dish.effort.charAt(0).toUpperCase() + dish.effort.slice(1); + + row.innerHTML = ` +
+
${dish.name}
+
+ ${renderCategoryBadges(dish)} + ${effortLabel} +
+
+
+ + +
+ `; + + listContainer.appendChild(row); + + row.querySelector(".btn-edit").addEventListener("click", () => openDishModal(dish)); + row.querySelector(".btn-delete").addEventListener("click", () => confirmDeleteDish(dish)); + }); + + } catch (err) { + listContainer.innerHTML = `
Fehler beim Laden: ${err.message}
`; + } +} + +function openDishModal(dish = null) { + const modal = document.getElementById("dish-modal"); + const modalTitle = document.getElementById("modal-title"); + const formId = document.getElementById("dish-form-id"); + const nameInput = document.getElementById("dish-name-input"); + const catCheckboxes = document.querySelectorAll('input[name="modal-category"]'); + + if (dish) { + modalTitle.textContent = "Gericht bearbeiten"; + formId.value = dish.id; + nameInput.value = dish.name; + const cats = getDishCategories(dish); + catCheckboxes.forEach(cb => { + cb.checked = cats.includes(cb.value); + }); + const radio = document.querySelector(`input[name="modal-effort"][value="${dish.effort}"]`); + if (radio) radio.checked = true; + } else { + modalTitle.textContent = "Neues Gericht anlegen"; + formId.value = ""; + nameInput.value = ""; + catCheckboxes.forEach(cb => { + cb.checked = false; + }); + const radio = document.querySelector(`input[name="modal-effort"][value="mittel"]`); + if (radio) radio.checked = true; + } + + modal.showModal(); +} + +async function saveDishFromModal() { + const modal = document.getElementById("dish-modal"); + const formId = document.getElementById("dish-form-id").value; + const name = document.getElementById("dish-name-input").value.trim(); + const selectedCats = Array.from(document.querySelectorAll('input[name="modal-category"]:checked')).map(cb => cb.value); + const effortRadio = document.querySelector('input[name="modal-effort"]:checked'); + const effort = effortRadio ? effortRadio.value : "mittel"; + + if (!name) { + showToast("Bitte gib einen Namen für das Gericht ein.", "error"); + return; + } + if (selectedCats.length === 0) { + showToast("Bitte wähle mindestens eine Kategorie aus.", "error"); + return; + } + if (!effort) { + showToast("Bitte wähle den Aufwand aus.", "error"); + return; + } + + const isEdit = !!formId; + const url = isEdit ? `/api/dishes/${formId}` : "/api/dishes"; + const method = isEdit ? "PUT" : "POST"; + + try { + const res = await fetch(url, { + method: method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, categories: selectedCats, effort }) + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || "Fehler beim Speichern."); + + showToast(isEdit ? "Gericht aktualisiert!" : "Gericht erfolgreich hinzugefügt!", "success"); + modal.close(); + loadAdminDishes(); + } catch (err) { + showToast(err.message, "error"); + } +} + +function confirmDeleteDish(dish) { + state.admin.deleteTargetId = dish.id; + const deleteModal = document.getElementById("delete-modal"); + const textEl = document.getElementById("delete-modal-text"); + textEl.innerHTML = `Möchtest du "${dish.name}" wirklich löschen?`; + deleteModal.showModal(); +} + +async function executeDeleteDish(dishId) { + try { + const res = await fetch(`/api/dishes/${dishId}`, { method: "DELETE" }); + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || "Fehler beim Löschen."); + + showToast("Gericht wurde gelöscht.", "success"); + loadAdminDishes(); + } catch (err) { + showToast(err.message, "error"); + } +} + +// --- Toast System --- +function showToast(message, type = "info") { + const container = document.getElementById("toast-container"); + if (!container) return; + + const toast = document.createElement("div"); + toast.className = `toast toast-${type}`; + const icon = type === "success" ? "✅" : (type === "error" ? "⚠️" : "ℹ️"); + + toast.innerHTML = `${icon}${message}`; + container.appendChild(toast); + + setTimeout(() => { + toast.style.transition = "opacity 0.3s, transform 0.3s"; + toast.style.opacity = "0"; + toast.style.transform = "translateY(-10px)"; + setTimeout(() => toast.remove(), 300); + }, 3500); +} diff --git a/app/static/cookbook.css b/app/static/cookbook.css new file mode 100755 index 0000000..efc7e85 --- /dev/null +++ b/app/static/cookbook.css @@ -0,0 +1,2805 @@ +/* ========================================================================== + Mein Kochbuch - Digital & A4 Print Layout Theme + Farben & Typografie inspiriert vom Design-Prototyp + ========================================================================== */ + +@import url('https://fonts.googleapis.com/css2?family=Architects+Daughter&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap'); + +:root { + --creme: #FAF6F0; + --tannengruen: #1E4D3B; + --tannengruen-light: rgba(30, 77, 59, 0.12); + --altrosa: #D29B9B; + --altrosa-light: rgba(210, 155, 155, 0.2); + --schwarz: #222222; + --grau-bg: #e6e2dc; + --border-hand: 2px solid var(--schwarz); + --font-hand: 'Architects Daughter', cursive, sans-serif; + --font-sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --radius-recipe: 14px; +} + +/* Base Body for Cookbook view */ +.cookbook-body { + margin: 0; + padding: 0; + background-color: var(--grau-bg); + font-family: var(--font-sans); + color: var(--schwarz); + min-height: 100vh; + display: flex; + flex-direction: column; +} + +@media screen { + html { + overflow-x: clip; + } + .cookbook-body { + overflow-x: clip; + width: 100%; + } +} + +/* Cookbook Navigation Bar (Fest / Sticky beim Scrollen) */ +.cb-nav { + position: sticky; + top: 0; + z-index: 1000; + background: var(--tannengruen); + color: var(--creme); + padding: 6px 14px; + min-height: 48px; + box-shadow: 0 4px 15px rgba(0,0,0,0.25); + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + box-sizing: border-box; +} + +.cb-nav-brand { + display: flex; + align-items: center; + gap: 8px; + text-decoration: none; + color: var(--creme); + flex-shrink: 0; +} + +.cb-brand-icon { + font-size: 1.5rem; + line-height: 1; +} + +.cb-nav-title { + font-family: var(--font-hand); + font-size: 1.35rem; + letter-spacing: 0.02em; + margin: 0; + white-space: nowrap; +} + +.cb-nav-actions { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.cb-btn { + font-family: var(--font-hand); + font-size: 1rem; + font-weight: 700; + padding: 8px 14px; + border-radius: 8px; + border: 2px solid transparent; + cursor: pointer; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 6px; + transition: all 0.2s ease; +} + +/* 100% einheitliche Navigationsleisten-Buttons */ +.cb-nav-btn { + background: var(--creme); + color: var(--tannengruen); + border: 2px solid var(--creme); + border-radius: 8px; + font-family: var(--font-hand); + font-weight: 700; + font-size: 0.98rem; + padding: 6px 14px; + cursor: pointer; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + transition: all 0.18s ease; + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); + white-space: nowrap; + line-height: 1.2; +} + +.cb-nav-btn:hover { + background: #ffffff; + border-color: #ffffff; + color: var(--tannengruen); + transform: translateY(-1px); + box-shadow: 2px 3px 0px rgba(0, 0, 0, 0.25); +} + +.cb-nav-btn:active { + transform: translateY(0); + box-shadow: 1px 1px 0px rgba(0, 0, 0, 0.15); +} + +.cb-btn-creme { + background: var(--creme); + color: var(--tannengruen); + border-color: var(--creme); +} + +.cb-btn-creme:hover { + background: #fff; + transform: translateY(-1px); +} + +.cb-btn-outline { + background: transparent; + color: var(--creme); + border-color: var(--creme); +} + +.cb-btn-outline:hover { + background: rgba(250, 246, 240, 0.18); + border-color: #ffffff; + color: #ffffff; +} + +.cb-btn-green { + background: #143729; + color: var(--creme) !important; + border-color: var(--creme); + box-shadow: none; +} + +.cb-btn-green:hover { + background: #0f2c1f; + color: #ffffff !important; + border-color: #ffffff; + transform: translateY(-1px); +} + +/* Filter Bar: Schlank & horizontal scrollbar (links-rechts) */ +.cb-filter-bar { + position: sticky; + top: 48px; + z-index: 990; + background: var(--creme); + border-bottom: 2px solid rgba(34, 34, 34, 0.15); + padding: 6px 12px; + display: flex; + align-items: center; + overflow-x: auto; + white-space: nowrap; + scrollbar-width: thin; + -webkit-overflow-scrolling: touch; + width: 100%; + box-sizing: border-box; + box-shadow: 0 2px 6px rgba(0,0,0,0.06); +} + +.cb-filter-scroll-container { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: nowrap; + width: max-content; + padding: 2px 4px; +} + +.cb-chip { + flex-shrink: 0; + white-space: nowrap; + font-family: var(--font-hand); + font-size: 0.9rem; + padding: 5px 13px; + border-radius: 20px; + background: var(--grau-bg); + border: 1.5px solid rgba(34, 34, 34, 0.2); + color: var(--schwarz); + cursor: pointer; + text-decoration: none; + transition: all 0.2s; + display: inline-flex; + align-items: center; +} + +.cb-chip.active, .cb-chip:hover { + background: var(--tannengruen); + color: var(--creme); + border-color: var(--tannengruen); +} + +/* Page Container (Virtual Book / Scroll view) */ +.book-wrapper { + max-width: 960px; + width: 100%; + margin: 24px auto; + padding: 0 16px; + display: flex; + flex-direction: column; + gap: 36px; + box-sizing: border-box; +} + +/* --- A4 Page Container --- */ +.page { + background-color: var(--creme); + box-shadow: 0 6px 20px rgba(0,0,0,0.18); + position: relative; + overflow: hidden; + box-sizing: border-box; + border-radius: 6px; + min-height: 580px; +} + +/* ========================================================================== + A4 Print Engine (Android Chrome, iOS Safari, Desktop) + ========================================================================== */ +@page { + size: A4 portrait; + margin: 0; +} + +@media print { + * { + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + color-adjust: exact !important; + box-sizing: border-box !important; + } + + html, + body.cookbook-body { + width: 210mm !important; + min-width: 210mm !important; + max-width: 210mm !important; + height: auto !important; + min-height: 297mm !important; + margin: 0 !important; + padding: 0 !important; + background-color: var(--creme) !important; + color: var(--schwarz) !important; + display: block !important; + overflow: visible !important; + float: none !important; + } + + .cb-nav, + .cb-filter-bar, + .no-print, + .no-print *, + .empty-chapter-section, + .empty-chapter-section *, + .empty-chapter-box, + .empty-chapter-box * { + display: none !important; + visibility: hidden !important; + height: 0 !important; + min-height: 0 !important; + max-height: 0 !important; + margin: 0 !important; + padding: 0 !important; + border: none !important; + page-break-before: avoid !important; + page-break-after: avoid !important; + break-before: avoid !important; + break-after: avoid !important; + overflow: hidden !important; + } + + .book-wrapper { + display: block !important; + width: 210mm !important; + max-width: 210mm !important; + min-width: 210mm !important; + margin: 0 !important; + padding: 0 !important; + gap: 0 !important; + float: none !important; + } + + /* Basis für JEDE A4-Seite im Druck: Exakt 1 Seite ohne Überlauf */ + .page { + width: 210mm !important; + max-width: 210mm !important; + min-width: 210mm !important; + height: 297mm !important; + min-height: 297mm !important; + max-height: 297mm !important; + margin: 0 !important; + padding: 0 !important; + background-color: var(--creme) !important; + position: relative !important; + box-sizing: border-box !important; + box-shadow: none !important; + border-radius: 0 !important; + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + overflow: hidden !important; + } + + .page:last-child { + page-break-after: auto !important; + break-after: auto !important; + } + + /* 1. COVER IM DRUCK */ + .cover { + width: 210mm !important; + max-width: 210mm !important; + min-width: 210mm !important; + height: 297mm !important; + min-height: 297mm !important; + max-height: 297mm !important; + box-sizing: border-box !important; + padding: 30mm 15mm !important; + display: flex !important; + justify-content: center !important; + align-items: center !important; + background-color: var(--creme) !important; + background-image: url("data:image/svg+xml,%3Csvg preserveAspectRatio='none' viewBox='0 0 200 800' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M 5 0 Q 15 200 0 400 T 10 800 L 20 800 Q 10 600 25 400 T 15 0 Z' fill='%231E4D3B' opacity='0.9'/%3E%3Cpath d='M 35 0 Q 25 250 40 500 T 30 800 L 55 800 Q 65 500 45 250 T 50 0 Z' fill='%231E4D3B' opacity='0.85'/%3E%3Cpath d='M 70 0 Q 80 300 65 600 T 75 800 L 85 800 Q 75 500 90 300 T 80 0 Z' fill='%231E4D3B' opacity='0.9'/%3E%3Cpath d='M 105 0 Q 95 200 110 400 T 100 800 L 125 800 Q 135 600 120 400 T 125 0 Z' fill='%231E4D3B' opacity='0.8'/%3E%3Cpath d='M 140 0 Q 150 250 135 500 T 145 800 L 155 800 Q 145 500 160 250 T 150 0 Z' fill='%231E4D3B' opacity='0.9'/%3E%3Cpath d='M 175 0 Q 165 300 180 600 T 170 800 L 195 800 Q 205 500 185 300 T 190 0 Z' fill='%231E4D3B' opacity='0.85'/%3E%3C/svg%3E") !important; + background-size: 100% 100% !important; + background-position: 0 0 !important; + background-repeat: no-repeat !important; + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + overflow: hidden !important; + } + + .cover-box { + border: 3.5px solid var(--schwarz) !important; + background-color: var(--creme) !important; + box-shadow: none !important; + transform: rotate(-1deg) !important; + padding: 25mm 20mm !important; + max-width: 82% !important; + box-sizing: border-box !important; + margin: auto !important; + } + + .cover-box h1 { + font-family: var(--font-hand) !important; + font-size: 3.5rem !important; + line-height: 1.15 !important; + margin: 0 !important; + color: var(--schwarz) !important; + } + + .cover-box p { + font-family: var(--font-hand) !important; + font-size: 1.6rem !important; + color: var(--tannengruen) !important; + margin-top: 14px !important; + margin-bottom: 0 !important; + } + + /* 2. TRENNSEITEN IM DRUCK */ + .divider { + width: 210mm !important; + max-width: 210mm !important; + min-width: 210mm !important; + height: 297mm !important; + min-height: 297mm !important; + max-height: 297mm !important; + box-sizing: border-box !important; + padding: 35mm 15mm !important; + display: flex !important; + flex-direction: column !important; + justify-content: center !important; + align-items: center !important; + text-align: center !important; + background-color: var(--creme) !important; + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + overflow: hidden !important; + } + + .chapter-number { + font-family: var(--font-hand) !important; + font-size: 3rem !important; + color: var(--altrosa) !important; + margin-bottom: -8px !important; + } + + .divider h2 { + font-family: var(--font-hand) !important; + font-size: 3.8rem !important; + color: var(--tannengruen) !important; + margin: 0 0 25px 0 !important; + line-height: 1.15 !important; + } + + .doodle-placeholder { + width: 240px !important; + height: 240px !important; + border: 2.5px dashed var(--altrosa) !important; + border-radius: 12px !important; + background: rgba(210, 155, 155, 0.05) !important; + display: flex !important; + justify-content: center !important; + align-items: center !important; + position: relative !important; + margin: 0 auto !important; + } + + .doodle-placeholder.has-illustration, + .doodle-placeholder:has(img) { + border: none !important; + background: transparent !important; + } + + /* 3. EINZELNE REZEPTSEITE IM DRUCK */ + .recipe-page { + width: 210mm !important; + max-width: 210mm !important; + min-width: 210mm !important; + height: 297mm !important; + min-height: 297mm !important; + max-height: 297mm !important; + box-sizing: border-box !important; + padding: 12mm 16mm 10mm 16mm !important; + display: flex !important; + flex-direction: column !important; + justify-content: flex-start !important; + gap: 8px !important; + background-color: var(--creme) !important; + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + overflow: hidden !important; + } + + .recipe-header { + border-bottom: 2px dashed rgba(30, 77, 59, 0.35) !important; + padding-bottom: 6px !important; + margin-bottom: 0 !important; + } + + .recipe-header-top { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + margin-bottom: 2px !important; + } + + .recipe-chapter-badge { + font-size: 0.82rem !important; + padding: 2px 8px !important; + margin-bottom: 0 !important; + background: var(--altrosa-light) !important; + border: 1.5px solid var(--altrosa) !important; + } + + .recipe-credits-badge { + display: inline-block !important; + font-family: var(--font-hand) !important; + font-size: 0.78rem !important; + color: #444 !important; + padding: 1px 7px !important; + background: rgba(30, 77, 59, 0.05) !important; + border: 1px dashed rgba(30, 77, 59, 0.35) !important; + border-radius: 8px !important; + } + + .recipe-title { + font-size: 2rem !important; + margin: 0 0 4px 0 !important; + line-height: 1.15 !important; + color: var(--tannengruen) !important; + } + + .recipe-meta-row { + font-size: 0.88rem !important; + gap: 12px !important; + color: #4a453f !important; + } + + /* Im Druck IMMER 2 Spalten: Links Bild + Zutaten, Rechts Zubereitung */ + .recipe-grid { + display: grid !important; + grid-template-columns: 62mm 1fr !important; + gap: 12mm !important; + align-items: start !important; + flex: 1 !important; + } + + .recipe-side { + display: flex !important; + flex-direction: column !important; + gap: 10px !important; + } + + /* Bildgröße im Druck gezielt begrenzen (44mm hoch), damit Rezept auf 1 Seite passt */ + .recipe-photo-box { + width: 100% !important; + height: 44mm !important; + max-height: 44mm !important; + aspect-ratio: auto !important; + border: 1.5px solid var(--schwarz) !important; + box-shadow: none !important; + border-radius: 6px !important; + background: rgba(30, 77, 59, 0.04) !important; + overflow: hidden !important; + } + + .recipe-photo-box img { + width: 100% !important; + height: 100% !important; + object-fit: cover !important; + display: block !important; + } + + .recipe-photo-empty { + display: none !important; + } + + .ingredients-box { + padding: 8px 12px !important; + border: 1.5px solid rgba(30, 77, 59, 0.4) !important; + background: rgba(255, 255, 255, 0.7) !important; + border-radius: 6px !important; + } + + .section-hand-title { + font-size: 1.35rem !important; + margin: 0 0 6px 0 !important; + padding-bottom: 2px !important; + border-bottom: 1.5px solid var(--altrosa) !important; + color: var(--tannengruen) !important; + } + + .ingredients-list { + gap: 3px !important; + } + + .ingredients-list li { + font-size: 0.85rem !important; + line-height: 1.25 !important; + } + + .recipe-main { + display: flex !important; + flex-direction: column !important; + gap: 10px !important; + } + + .instructions-steps { + gap: 8px !important; + } + + .step-card { + gap: 10px !important; + } + + .step-badge { + width: 22px !important; + height: 22px !important; + font-size: 0.9rem !important; + background: var(--tannengruen) !important; + color: var(--creme) !important; + } + + .step-text { + font-size: 0.9rem !important; + line-height: 1.34 !important; + padding-top: 1px !important; + } + + .notes-box { + padding: 6px 12px !important; + font-size: 0.88rem !important; + margin-top: 2px !important; + background: #FFF9D2 !important; + border: 1.5px solid #E2D172 !important; + box-shadow: none !important; + } + + .notes-box-title { + font-size: 0.92rem !important; + margin-bottom: 2px !important; + } + + .recipe-source { + display: none !important; + } + + .notes-box-empty { + display: block !important; + padding: 6px 12px !important; + margin-top: 4px !important; + background: #FFFDF0 !important; + border: 1.5px dashed #D4C165 !important; + box-shadow: none !important; + border-radius: 6px !important; + } + + .notes-blank-lines { + display: flex !important; + flex-direction: column !important; + gap: 8px !important; + margin-top: 4px !important; + padding-bottom: 2px !important; + } + + .notes-blank-line { + border-bottom: 1.5px dotted rgba(80, 70, 50, 0.45) !important; + height: 14px !important; + width: 100% !important; + display: block !important; + } + + .print-only { + display: block !important; + } + + .no-print { + display: none !important; + } +} + +/* iOS Safari AirPrint Anpassung: Verhindert Seitenüberlauf und Stauchung */ +@supports (-webkit-touch-callout: none) { + @media print { + html, body.cookbook-body { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + height: auto !important; + min-height: auto !important; + overflow: visible !important; + background-color: var(--creme) !important; + } + .book-wrapper { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + margin: 0 !important; + padding: 0 !important; + } + .page { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + height: auto !important; + min-height: auto !important; + max-height: none !important; + box-sizing: border-box !important; + background-color: var(--creme) !important; + overflow: hidden !important; + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + } + .cover { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + height: 220mm !important; + min-height: auto !important; + max-height: 220mm !important; + background-size: 100% 100% !important; + background-position: 0 0 !important; + background-repeat: no-repeat !important; + padding: 16mm 10mm !important; + box-sizing: border-box !important; + overflow: hidden !important; + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + } + .cover-box { + padding: 16mm 14mm !important; + max-width: 85% !important; + } + .divider { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + height: 220mm !important; + min-height: auto !important; + max-height: 220mm !important; + padding: 18mm 10mm !important; + box-sizing: border-box !important; + overflow: hidden !important; + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + } + .recipe-page { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + height: auto !important; + min-height: auto !important; + max-height: 225mm !important; + padding: 6mm 10mm !important; + box-sizing: border-box !important; + overflow: hidden !important; + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + } + .recipe-grid { + grid-template-columns: 55mm 1fr !important; + gap: 8mm !important; + width: 100% !important; + min-width: 0 !important; + } + .page:last-child { + page-break-after: auto !important; + break-after: auto !important; + } + } +} + +/* Fallback-Klasse falls JS is-ios gesetzt hat */ +@media print { + html.is-ios, + body.is-ios.cookbook-body { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + height: auto !important; + min-height: auto !important; + overflow: visible !important; + background-color: var(--creme) !important; + } + .is-ios .book-wrapper { + display: block !important; + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + margin: 0 !important; + padding: 0 !important; + gap: 0 !important; + } + .is-ios .page { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + height: auto !important; + min-height: auto !important; + max-height: none !important; + margin: 0 !important; + padding: 0 !important; + box-sizing: border-box !important; + background-color: var(--creme) !important; + overflow: hidden !important; + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + } + .is-ios .cover { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + height: 220mm !important; + min-height: auto !important; + max-height: 220mm !important; + background-size: 100% 100% !important; + background-position: 0 0 !important; + background-repeat: no-repeat !important; + padding: 16mm 10mm !important; + box-sizing: border-box !important; + overflow: hidden !important; + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + } + .is-ios .cover-box { + padding: 16mm 14mm !important; + max-width: 85% !important; + } + .is-ios .divider { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + height: 220mm !important; + min-height: auto !important; + max-height: 220mm !important; + padding: 18mm 10mm !important; + box-sizing: border-box !important; + overflow: hidden !important; + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + } + .is-ios .recipe-page { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + height: auto !important; + min-height: auto !important; + max-height: 225mm !important; + padding: 6mm 10mm !important; + box-sizing: border-box !important; + overflow: hidden !important; + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + } + .is-ios .recipe-grid { + grid-template-columns: 55mm 1fr !important; + gap: 8mm !important; + width: 100% !important; + min-width: 0 !important; + } + .is-ios .notes-box-empty { + display: block !important; + padding: 4px 8px !important; + margin-top: 3px !important; + background: #FFFDF0 !important; + border: 1px dashed #D4C165 !important; + } + .is-ios .notes-blank-lines { + display: flex !important; + flex-direction: column !important; + gap: 8px !important; + margin-top: 3px !important; + } + .is-ios .notes-blank-line { + border-bottom: 1.5px dotted rgba(80, 70, 50, 0.45) !important; + height: 12px !important; + width: 100% !important; + display: block !important; + } + .is-ios .print-only { + display: block !important; + } + .is-ios .page:last-child { + page-break-after: auto !important; + break-after: auto !important; + } +} + +/* --- Cover Gestaltung --- */ +.cover { + background-color: var(--creme); + background-image: url("data:image/svg+xml,%3Csvg preserveAspectRatio='none' viewBox='0 0 200 800' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M 5 0 Q 15 200 0 400 T 10 800 L 20 800 Q 10 600 25 400 T 15 0 Z' fill='%231E4D3B' opacity='0.9'/%3E%3Cpath d='M 35 0 Q 25 250 40 500 T 30 800 L 55 800 Q 65 500 45 250 T 50 0 Z' fill='%231E4D3B' opacity='0.85'/%3E%3Cpath d='M 70 0 Q 80 300 65 600 T 75 800 L 85 800 Q 75 500 90 300 T 80 0 Z' fill='%231E4D3B' opacity='0.9'/%3E%3Cpath d='M 105 0 Q 95 200 110 400 T 100 800 L 125 800 Q 135 600 120 400 T 125 0 Z' fill='%231E4D3B' opacity='0.8'/%3E%3Cpath d='M 140 0 Q 150 250 135 500 T 145 800 L 155 800 Q 145 500 160 250 T 150 0 Z' fill='%231E4D3B' opacity='0.9'/%3E%3Cpath d='M 175 0 Q 165 300 180 600 T 170 800 L 195 800 Q 205 500 185 300 T 190 0 Z' fill='%231E4D3B' opacity='0.85'/%3E%3C/svg%3E"); + background-size: 100% 100%; + display: flex; + justify-content: center; + align-items: center; + padding: 60px 20px; +} + +.cover-box { + background-color: var(--creme); + padding: 40px 50px; + border: 3.5px solid var(--schwarz); + border-radius: 4px; + text-align: center; + position: relative; + transform: rotate(-1deg); + box-shadow: 6px 6px 0px rgba(30, 77, 59, 0.4); +} + +.cover-box h1 { + font-family: var(--font-hand); + font-size: 3.8rem; + margin: 0; + line-height: 1.15; + color: var(--schwarz); +} + +.cover-box p { + font-family: var(--font-hand); + font-size: 1.7rem; + color: var(--tannengruen); + margin-top: 18px; + margin-bottom: 0; +} + +/* --- Kapitel-Trennseiten --- */ +.divider { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + padding: 50px 30px; +} + +.chapter-number { + font-family: var(--font-hand); + font-size: 3rem; + color: var(--altrosa); + margin-bottom: -10px; +} + +.divider h2 { + font-family: var(--font-hand); + font-size: 4rem; + color: var(--tannengruen); + margin: 0 0 30px 0; + line-height: 1.2; +} + +.doodle-placeholder { + width: 440px; + height: 440px; + max-width: 85vw; + max-height: 52vh; + margin-top: 15px; + display: flex; + justify-content: center; + align-items: center; + border: 2.5px dashed var(--altrosa); + border-radius: 12px; + position: relative; + background: rgba(210, 155, 155, 0.05); +} + +.doodle-placeholder.has-illustration, +.doodle-placeholder:has(img) { + border: none; + background: transparent; +} + +.doodle-placeholder img { + width: 100%; + height: 100%; + max-width: 100%; + max-height: 100%; + object-fit: contain; + mix-blend-mode: multiply; + transition: transform 0.2s ease; +} + +.doodle-placeholder img:hover { + transform: scale(1.02); +} + +.placeholder-text { + font-family: var(--font-hand); + color: #7a6e65; + font-size: 1.25rem; + position: absolute; + text-align: center; + padding: 20px; +} + +/* --- Platzhalter für leere Kapitel (nur im Web-Browser) --- */ +.empty-chapter-section { + display: flex; + flex-direction: column; + gap: 20px; +} + +.empty-chapter-header { + background: var(--creme); + border: 2px dashed rgba(30, 77, 59, 0.35); + border-radius: 12px; + padding: 40px 20px; + text-align: center; +} + +.empty-chapter-box { + background: var(--creme); + border: 2px dashed rgba(30, 77, 59, 0.35); + border-radius: 12px; + padding: 32px 20px; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + box-shadow: 0 4px 12px rgba(0,0,0,0.06); +} + +.empty-chapter-text { + font-family: var(--font-hand); + font-size: 1.35rem; + color: #7a6e65; + margin: 0; +} + +/* --- Einheitliches Rezept-Template --- */ +.recipe-page { + padding: 36px 40px; + display: flex; + flex-direction: column; + gap: 24px; +} + +.recipe-header { + border-bottom: 2.5px dashed rgba(30, 77, 59, 0.3); + padding-bottom: 18px; + position: relative; +} + +@media screen { + .print-only { + display: none !important; + } +} + +.recipe-header-top { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + flex-wrap: wrap; + gap: 8px; +} + +.recipe-chapter-badge { + font-family: var(--font-hand); + display: inline-block; + background: var(--altrosa-light); + color: var(--schwarz); + border: 1.5px solid var(--altrosa); + padding: 3px 12px; + border-radius: 14px; + font-size: 0.95rem; + font-weight: 700; + margin-bottom: 0; +} + +.recipe-credits-badge { + font-family: var(--font-hand); + display: inline-block; + background: rgba(30, 77, 59, 0.06); + color: #4a453f; + border: 1.5px dashed rgba(30, 77, 59, 0.35); + padding: 3px 12px; + border-radius: 14px; + font-size: 0.92rem; + font-weight: 700; +} + +.recipe-title { + font-family: var(--font-hand); + font-size: 2.7rem; + color: var(--tannengruen); + margin: 0 0 10px 0; + line-height: 1.15; +} + +.recipe-meta-row { + display: flex; + flex-wrap: wrap; + gap: 16px; + font-family: var(--font-hand); + font-size: 1.1rem; + color: #4a453f; +} + +.recipe-meta-item { + display: flex; + align-items: center; + gap: 6px; +} + +/* Recipe Content Grid (2 Columns on Desktop) */ +.recipe-grid { + display: grid; + grid-template-columns: 320px 1fr; + gap: 30px; +} + +@media (max-width: 768px) { + .recipe-grid { + grid-template-columns: 1fr; + gap: 20px; + } +} + +/* Left Column: Image + Ingredients */ +.recipe-side { + display: flex; + flex-direction: column; + gap: 20px; +} + +.recipe-photo-box { + width: 100%; + aspect-ratio: 4 / 3; + border: 2px solid var(--schwarz); + border-radius: 8px; + overflow: hidden; + position: relative; + background: rgba(30, 77, 59, 0.04); + box-shadow: 4px 4px 0px rgba(0,0,0,0.1); +} + +.recipe-photo-box img { + width: 100%; + height: 100%; + object-fit: cover; + mix-blend-mode: multiply; + display: block; +} + +.recipe-photo-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: #8c827a; + font-family: var(--font-hand); + font-size: 1.1rem; + text-align: center; + padding: 10px; +} + +.ingredients-box { + background: rgba(255, 255, 255, 0.6); + border: 2px solid rgba(30, 77, 59, 0.35); + border-radius: 8px; + padding: 20px; +} + +.section-hand-title { + font-family: var(--font-hand); + font-size: 1.6rem; + color: var(--tannengruen); + margin: 0 0 12px 0; + display: flex; + align-items: center; + gap: 8px; + border-bottom: 1.5px solid var(--altrosa); + padding-bottom: 4px; +} + +.ingredients-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.ingredients-list li { + font-family: var(--font-sans); + font-size: 0.95rem; + color: var(--schwarz); + display: flex; + align-items: flex-start; + gap: 8px; + line-height: 1.4; +} + +.ingredients-list li::before { + content: "•"; + color: var(--tannengruen); + font-weight: bold; + font-size: 1.2rem; + line-height: 1; +} + +/* Right Column: Steps + Notes */ +.recipe-main { + display: flex; + flex-direction: column; + gap: 22px; +} + +.instructions-content { + font-family: var(--font-sans); + font-size: 1.05rem; + line-height: 1.7; + color: #2b2b2b; + white-space: pre-line; +} + +.instructions-steps { + display: flex; + flex-direction: column; + gap: 14px; +} + +.step-card { + display: flex; + gap: 14px; + align-items: flex-start; +} + +.step-badge { + font-family: var(--font-hand); + background: var(--tannengruen); + color: var(--creme); + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + font-weight: 700; + flex-shrink: 0; +} + +.step-text { + flex: 1; + padding-top: 3px; +} + +/* Notes & Tips Box (Post-It style) */ +.notes-box { + background: #FFF9D2; + border: 1.5px solid #E2D172; + border-radius: 6px; + padding: 16px 20px; + font-family: var(--font-hand); + font-size: 1.15rem; + color: #4a3e14; + transform: rotate(-0.5deg); + box-shadow: 3px 3px 8px rgba(0,0,0,0.06); +} + +.notes-box-title { + font-weight: bold; + color: #735a09; + margin-bottom: 6px; +} + +/* Credits / Copyright Badge */ +.recipe-source { + font-family: var(--font-hand); + font-size: 1.05rem; + color: #555; + display: flex; + align-items: center; + gap: 6px; + margin-top: 10px; + padding-top: 8px; + border-top: 1.5px dashed rgba(34, 34, 34, 0.2); +} + +/* --- Recipe Form Styles --- */ +.form-card { + background: var(--creme); + border: 3px solid var(--schwarz); + border-radius: 12px; + padding: 30px; + box-shadow: 6px 6px 0px rgba(30, 77, 59, 0.4); +} + +.form-title { + font-family: var(--font-hand); + font-size: 2.4rem; + color: var(--tannengruen); + margin: 0 0 8px 0; +} + +.form-subtitle { + font-family: var(--font-hand); + font-size: 1.2rem; + color: #666; + margin-bottom: 24px; +} + +.cb-form-group { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 18px; +} + +.cb-label { + font-family: var(--font-hand); + font-size: 1.15rem; + font-weight: 700; + color: var(--schwarz); +} + +.cb-input, .cb-textarea, .cb-select { + font-family: var(--font-sans); + font-size: 1rem; + padding: 12px 14px; + background: #fff; + border: 2px solid var(--schwarz); + border-radius: 6px; + color: var(--schwarz); + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; + box-sizing: border-box; +} + +.cb-input:focus, .cb-textarea:focus, .cb-select:focus { + border-color: var(--tannengruen); + box-shadow: 0 0 0 3px rgba(30, 77, 59, 0.2); +} + +.cb-row { + display: flex; + gap: 16px; + flex-wrap: wrap; +} + +.cb-col { + flex: 1; + min-width: 160px; +} + +/* Image Upload Widget */ +.upload-dropzone { + border: 2.5px dashed var(--tannengruen); + border-radius: 10px; + background: rgba(30, 77, 59, 0.04); + padding: 24px; + text-align: center; + cursor: pointer; + transition: background 0.2s; + position: relative; +} + +.upload-dropzone:hover { + background: rgba(30, 77, 59, 0.08); +} + +.preview-container { + margin-top: 14px; + max-width: 260px; + border-radius: 8px; + overflow: hidden; + border: 2px solid var(--schwarz); + display: none; +} + +.preview-container img { + width: 100%; + display: block; + mix-blend-mode: multiply; +} + +/* Mobile responsive adjustments (nur Screen, niemals Print!) */ +@media screen and (max-width: 680px) { + .hide-mobile { + display: none !important; + } + + .cb-nav { + padding: 8px 10px; + gap: 6px; + } + + .cb-filter-bar { + top: 46px; + padding: 6px 10px; + gap: 6px; + } + + .cb-nav-brand { + gap: 4px; + } + + .cb-brand-icon { + font-size: 1.3rem; + } + + .cb-nav-title { + font-size: 1.15rem; + } + + .cb-nav-actions { + gap: 5px; + } + + .cb-btn, + .cb-nav-btn { + padding: 5px 9px; + font-size: 0.88rem; + border-radius: 6px; + gap: 4px; + } + + .book-wrapper { + padding: 0 10px; + margin: 16px auto; + gap: 20px; + max-width: 100vw; + } + + .page { + min-height: auto; + max-width: 100%; + overflow-x: hidden; + } + + .cover { + padding: 36px 12px; + } + + .cover-box { + padding: 24px 16px; + max-width: 92vw; + box-sizing: border-box; + } + + .cover-box h1 { + font-size: 2.3rem; + line-height: 1.15; + } + + .cover-box p { + font-size: 1.25rem; + margin-top: 10px; + } + + .divider { + padding: 36px 14px; + } + + .chapter-number { + font-size: 2rem; + margin-bottom: -5px; + } + + .divider h2 { + font-size: 2.4rem; + margin: 0 0 20px 0; + word-break: break-word; + } + + .doodle-placeholder { + width: min(280px, 85vw); + height: min(280px, 85vw); + margin-top: 10px; + } + + .placeholder-text { + font-size: 1.1rem; + padding: 12px; + } + + .recipe-page { + padding: 18px 14px; + gap: 16px; + } + + .recipe-title { + font-size: 1.85rem; + margin-bottom: 8px; + word-break: break-word; + } + + .recipe-meta-row { + font-size: 0.95rem; + gap: 10px; + } + + .recipe-grid { + grid-template-columns: 1fr; + gap: 16px; + } + + .form-card { + padding: 18px 14px; + } + + .form-title { + font-size: 1.85rem; + } +} + +@media screen and (max-width: 380px) { + .cb-nav { + padding: 6px 8px; + } + .cb-filter-bar { + top: 40px; + padding: 5px 8px; + } + .cb-nav-title { + font-size: 1rem; + } + .cb-btn { + padding: 5px 8px; + font-size: 0.82rem; + } +} + +/* ========================================================================== + Modals: Generator & Neues Rezept (im Buch-Design) + ========================================================================== */ +.cb-modal-backdrop { + position: fixed; + inset: 0; + background: rgba(20, 30, 25, 0.7); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + z-index: 2000; + display: flex; + align-items: flex-start; + justify-content: center; + padding: 16px 12px calc(36px + env(safe-area-inset-bottom, 0px)) 12px; + opacity: 0; + visibility: hidden; + transition: opacity 0.25s ease, visibility 0.25s ease; + box-sizing: border-box; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; + touch-action: pan-y; +} + +.cb-modal-backdrop.active { + opacity: 1; + visibility: visible; +} + +.cb-modal-card { + background: var(--creme); + border: 2.5px solid var(--schwarz); + border-radius: 12px; + width: 100%; + max-width: 680px; + max-height: 88vh; + max-height: calc(100svh - 36px); + max-height: calc(100dvh - 36px); + display: flex; + flex-direction: column; + box-shadow: 6px 6px 0px rgba(30, 77, 59, 0.4); + transform: translateY(20px) scale(0.98); + transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1); + overflow: hidden; + box-sizing: border-box; + margin: 16px auto calc(24px + env(safe-area-inset-bottom, 0px)) auto; + position: relative; + touch-action: pan-y; +} + +.cb-modal-backdrop.active .cb-modal-card { + transform: translateY(0) scale(1); +} + +.cb-modal-header { + padding: 12px 18px; + background: var(--tannengruen); + color: var(--creme); + border-bottom: 2px solid var(--schwarz); + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 20; + flex-shrink: 0; +} + +.cb-modal-title { + font-family: var(--font-hand); + font-size: 1.45rem; + margin: 0; + color: var(--creme); + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 0; + word-break: break-word; + line-height: 1.2; +} + +.cb-modal-close { + background: rgba(250, 246, 240, 0.15); + border: 1.5px solid rgba(250, 246, 240, 0.6); + color: var(--creme); + font-size: 1.5rem; + cursor: pointer; + line-height: 1; + width: 36px; + height: 36px; + min-width: 36px; + min-height: 36px; + padding: 0; + border-radius: 8px; + transition: all 0.2s; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-left: 10px; +} + +.cb-modal-close:hover { + background: rgba(250, 246, 240, 0.3); + border-color: #ffffff; + color: #ffffff; +} + +.cb-modal-body { + padding: 18px 22px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + flex: 1 1 auto; + min-height: 0; + padding: 18px 22px calc(32px + env(safe-area-inset-bottom, 0px)) 22px; +} + +@media screen and (max-width: 600px) { + .cb-modal-backdrop { + padding: 8px 6px calc(28px + env(safe-area-inset-bottom, 0px)) 6px; + } + .cb-modal-card { + max-height: 90vh; + max-height: calc(100svh - 20px); + max-height: calc(100dvh - 20px); + border-radius: 10px; + margin: 8px auto calc(16px + env(safe-area-inset-bottom, 0px)) auto; + } + .cb-modal-header { + padding: 10px 14px; + } + .cb-modal-title { + font-size: 1.2rem; + } + .cb-modal-close { + width: 34px; + height: 34px; + min-width: 34px; + min-height: 34px; + font-size: 1.35rem; + } + .cb-modal-body { + padding: 14px 14px calc(28px + env(safe-area-inset-bottom, 0px)) 14px; + } +} + +/* --- Generator Modal Spezifisch --- */ +.gen-count-pills { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 18px; +} + +.gen-count-btn { + font-family: var(--font-hand); + font-size: 1.1rem; + font-weight: 700; + padding: 6px 14px; + border-radius: 20px; + border: 2px solid var(--schwarz); + background: #fff; + color: var(--schwarz); + cursor: pointer; + transition: all 0.15s ease; +} + +.gen-count-btn.active, .gen-count-btn:hover { + background: var(--tannengruen); + color: var(--creme); + border-color: var(--tannengruen); +} + +.gen-slot-item { + background: #fff; + border: 2px solid var(--schwarz); + border-radius: 10px; + padding: 14px 16px; + margin-bottom: 12px; + box-shadow: 3px 3px 0px rgba(30, 77, 59, 0.2); +} + +.gen-slot-header { + font-family: var(--font-hand); + font-size: 1.25rem; + font-weight: 700; + color: var(--tannengruen); + margin-bottom: 8px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.gen-effort-chips { + display: flex; + gap: 6px; + margin-top: 6px; +} + +.gen-effort-chip { + font-family: var(--font-hand); + font-size: 0.95rem; + padding: 4px 10px; + border-radius: 16px; + border: 1.5px solid rgba(34, 34, 34, 0.3); + background: var(--creme); + cursor: pointer; + transition: all 0.15s; +} + +.gen-effort-chip.selected { + background: var(--tannengruen); + color: var(--creme); + border-color: var(--tannengruen); + font-weight: 700; +} + +.gen-result-card { + background: #fff; + border: 2.5px solid var(--schwarz); + border-radius: 12px; + padding: 16px 20px; + margin-bottom: 12px; + box-shadow: 4px 4px 0px rgba(30, 77, 59, 0.25); + display: flex; + flex-direction: column; + gap: 8px; + animation: cardPopIn 0.3s ease; +} + +@keyframes cardPopIn { + from { opacity: 0; transform: scale(0.96) translateY(10px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +.gen-dish-name { + font-family: var(--font-hand); + font-size: 1.8rem; + color: var(--tannengruen); + margin: 0; + line-height: 1.2; +} + +.gen-badge-row { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; +} + +.gen-tag { + font-family: var(--font-sans); + font-size: 0.85rem; + font-weight: 600; + padding: 3px 8px; + border-radius: 6px; + border: 1px solid rgba(34, 34, 34, 0.2); +} + +.gen-tag-chapter { + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--creme); + border: 1.5px solid var(--tannengruen); + color: var(--tannengruen); + font-weight: 700; + padding: 3px 10px; + border-radius: 12px; +} + +.gen-chapter-thumb { + width: 20px; + height: 20px; + object-fit: contain; + mix-blend-mode: multiply; + vertical-align: middle; +} + +.gen-actions { + display: flex; + gap: 8px; + align-items: center; + margin-top: 6px; + flex-wrap: wrap; +} + +/* Toast Notifications */ +.cb-toast { + position: fixed; + bottom: 24px; + right: 24px; + background: var(--tannengruen); + color: var(--creme); + border: 2px solid var(--schwarz); + border-radius: 8px; + padding: 12px 20px; + font-family: var(--font-hand); + font-size: 1.2rem; + box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3); + z-index: 3000; + display: flex; + align-items: center; + gap: 10px; + animation: toastFade 0.3s ease; +} + +@keyframes toastFade { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +/* --- Modal Action Buttons --- */ +.cb-btn-modal-cancel { + background: transparent; + color: var(--schwarz); + border: 2px solid rgba(34, 34, 34, 0.35); + font-size: 1.05rem; + padding: 8px 18px; +} +.cb-btn-modal-cancel:hover { + background: rgba(0, 0, 0, 0.05); + border-color: var(--schwarz); +} + +.cb-btn-modal-submit { + background: var(--tannengruen); + color: var(--creme); + border: 2px solid var(--tannengruen); + font-size: 1.1rem; + padding: 8px 22px; +} +.cb-btn-modal-submit:hover { + background: #143729; + border-color: #143729; + color: #ffffff; + transform: translateY(-1px); +} + +/* ========================================================================== + Custom Chapter Selector (Mit handgezeichneten Doodles) + ========================================================================== */ +.custom-chapter-select { + position: relative; + width: 100%; +} + +.chapter-select-btn { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + padding: 8px 12px; + background: #ffffff; + border: 2px solid rgba(34, 34, 34, 0.35); + border-radius: 8px; + font-family: var(--font-hand); + font-size: 1.05rem; + color: var(--schwarz); + cursor: pointer; + text-align: left; + transition: all 0.15s ease; + box-sizing: border-box; +} + +.chapter-select-btn:hover, +.chapter-select-btn:focus { + border-color: var(--tannengruen); + box-shadow: 0 0 0 2px rgba(30, 77, 59, 0.15); + outline: none; +} + +.chapter-select-btn-content { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + min-width: 0; +} + +.chapter-select-thumb { + width: 28px; + height: 28px; + object-fit: contain; + flex-shrink: 0; + border-radius: 4px; +} + +.chapter-select-btn-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 700; +} + +.chapter-select-arrow { + font-size: 0.8rem; + color: #777; + transition: transform 0.2s ease; + flex-shrink: 0; +} + +.custom-chapter-select.is-open .chapter-select-arrow { + transform: rotate(180deg); +} + +.chapter-dropdown-menu { + display: none; + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + background: var(--creme); + border: 2.5px solid var(--schwarz); + border-radius: 10px; + box-shadow: 4px 6px 16px rgba(0, 0, 0, 0.25); + max-height: 280px; + overflow-y: auto; + z-index: 1000; + padding: 4px 0; + -webkit-overflow-scrolling: touch; +} + +.custom-chapter-select.is-open .chapter-dropdown-menu { + display: block; + animation: dropdownFadeIn 0.18s ease-out; +} + +@keyframes dropdownFadeIn { + from { opacity: 0; transform: translateY(-6px); } + to { opacity: 1; transform: translateY(0); } +} + +.chapter-option { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px dashed rgba(34, 34, 34, 0.12); + transition: background 0.12s ease; + font-family: var(--font-hand); + font-size: 1.05rem; + color: var(--schwarz); +} + +.chapter-option:last-child { + border-bottom: none; +} + +.chapter-option:hover { + background: rgba(30, 77, 59, 0.1); +} + +.chapter-option.is-selected { + background: rgba(30, 77, 59, 0.16); + font-weight: 700; +} + +.chapter-option-thumb { + width: 32px; + height: 32px; + object-fit: contain; + flex-shrink: 0; + background: #ffffff; + border: 1px solid rgba(34, 34, 34, 0.18); + border-radius: 6px; + padding: 2px; +} + +.chapter-option-text { + flex: 1; + min-width: 0; + line-height: 1.2; +} + +.chapter-option-title { + font-weight: 700; + color: var(--tannengruen); +} + +.chapter-recipe-count { + font-family: var(--font-sans); + font-size: 0.78rem; + font-weight: 700; + padding: 2px 7px; + border-radius: 10px; + background: rgba(30, 77, 59, 0.1); + color: var(--tannengruen); + white-space: nowrap; +} + +.chapter-recipe-count.zero { + background: rgba(0, 0, 0, 0.06); + color: #888888; +} + + +/* ========================================================================== + VERSION 4.1: MOBILE 3-PUNKTE-MENÜ, PORTION-MATRIX-TABELLE, RECHTE SEITENZAHL + ========================================================================== */ + +/* --- Mobile Navigation: 3-Punkte (Kebab) Menü --- */ +.desktop-nav-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.mobile-nav-wrapper { + display: none; + position: relative; +} + +.cb-kebab-btn { + background: var(--creme); + color: var(--tannengruen); + border: 2px solid var(--tannengruen); + border-radius: 8px; + width: 38px; + height: 38px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.6rem; + line-height: 1; + cursor: pointer; + padding: 0; + transition: all 0.15s ease; + font-weight: bold; +} + +.cb-kebab-btn:hover, .cb-kebab-btn:active { + background: var(--tannengruen); + color: var(--creme); +} + +.cb-mobile-dropdown { + display: none; + position: absolute; + top: calc(100% + 8px); + right: 0; + background: var(--creme); + border: 2px solid var(--schwarz); + border-radius: 10px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); + min-width: 220px; + padding: 6px 0; + z-index: 2500; + animation: dropdownFadeIn 0.18s ease-out; +} + +.cb-mobile-dropdown.is-open { + display: block; +} + +.cb-mobile-item { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + padding: 10px 18px; + background: transparent; + border: none; + text-align: left; + font-family: var(--font-hand); + font-size: 1.15rem; + color: var(--schwarz); + cursor: pointer; + text-decoration: none; + box-sizing: border-box; + transition: background 0.12s ease; +} + +.cb-mobile-item:hover, .cb-mobile-item:active { + background: rgba(30, 77, 59, 0.12); + color: var(--tannengruen); +} + +.cb-mobile-divider { + border: none; + border-top: 1px dashed rgba(34, 34, 34, 0.2); + margin: 4px 0; +} + +.cb-mobile-logout { + color: #9b1c1c !important; +} + +.cb-nav-btn-logout { + background: rgba(255, 255, 255, 0.15) !important; + color: #fff !important; + border-color: rgba(255, 255, 255, 0.4) !important; +} + +.cb-nav-btn-logout:hover { + background: #fdf2f2 !important; + color: #9b1c1c !important; + border-color: #f8b4b4 !important; +} + +@media screen and (max-width: 768px) { + .desktop-nav-actions { + display: none !important; + } + .mobile-nav-wrapper { + display: block !important; + } +} + +/* --- Multi-Portionen-Tabelle (2 / 4 / 6 / 8 Portionen) --- */ +.portion-matrix-table { + width: 100%; + border-collapse: collapse; + margin-top: 4px; + font-size: 0.82rem; +} + +.portion-matrix-table th { + font-family: var(--font-sans); + font-size: 0.78rem; + font-weight: 700; + color: var(--tannengruen); + padding: 4px 3px; + border-bottom: 1.5px solid var(--tannengruen); + text-align: center; +} + +.portion-matrix-table th.matrix-th-ing { + text-align: left; +} + +.portion-matrix-table td { + padding: 3px 2px; + border-bottom: 1px dashed rgba(30, 61, 47, 0.15); + vertical-align: middle; +} + +.portion-matrix-table td.matrix-td-name { + font-family: var(--font-sans); + color: var(--schwarz); + font-weight: 500; +} + +.portion-matrix-table td.matrix-td-amt { + text-align: center; + font-weight: 600; + color: var(--tannengruen); + white-space: nowrap; +} + +.portion-matrix-table td.matrix-td-amt.highlight { + background: rgba(30, 61, 47, 0.04); +} + +.portion-matrix-table tr.matrix-header-row th { + padding: 6px 0 2px 0; + text-align: left; + font-family: var(--font-hand); + font-size: 0.95rem; + color: var(--tannengruen); + border-bottom: 1px solid var(--tannengruen); +} + +/* --- Zutaten-Unterüberschriften in normaler Liste --- */ +.ingredients-list li.ing-subheading { + font-family: var(--font-hand); + font-size: 1.15rem; + font-weight: 700; + color: var(--tannengruen); + margin-top: 8px; + margin-bottom: 2px; + list-style: none; + border-bottom: 1px dashed var(--altrosa); + padding-bottom: 2px; +} + +.ingredients-list li.ing-subheading::before { + display: none !important; +} + +/* --- Quelle am Rezept-Ende --- */ +.recipe-source-line { + margin-top: 10px; + font-family: var(--font-sans); + font-size: 0.88rem; + color: #666; + display: flex; + align-items: baseline; + gap: 6px; + border-top: 1px dotted rgba(34, 34, 34, 0.2); + padding-top: 6px; +} + +.recipe-source-label { + font-weight: 700; + color: var(--tannengruen); +} + +.recipe-source-val { + font-style: italic; + color: #444; +} + +/* --- Inhaltsverzeichnis (TOC) --- */ +.toc-page { + padding: 14mm 16mm 14mm 16mm; + position: relative; + display: flex; + flex-direction: column; +} + +.toc-header { + text-align: center; + margin-bottom: 8mm; +} + +.toc-subhead { + font-family: var(--font-sans); + text-transform: uppercase; + letter-spacing: 2px; + font-size: 0.78rem; + color: var(--gold); + font-weight: 700; + margin-bottom: 2px; +} + +.toc-title { + font-family: var(--font-title); + font-size: 2.2rem; + color: var(--tannengruen); + margin: 0; + line-height: 1.1; +} + +.toc-divider-ornament { + color: var(--gold); + font-size: 0.9rem; + letter-spacing: 6px; + margin-top: 4px; +} + +.toc-columns { + columns: 2; + column-gap: 10mm; + flex: 1; +} + +.toc-chapter-group { + break-inside: avoid; + page-break-inside: avoid; + margin-bottom: 6mm; +} + +.toc-chapter-header { + display: flex; + align-items: baseline; + gap: 6px; + font-family: var(--font-title); + font-size: 1rem; + font-weight: 700; + color: var(--tannengruen); + border-bottom: 1.5px solid var(--gold); + padding-bottom: 2px; + margin-bottom: 4px; +} + +.toc-chap-icon { + font-size: 1.1rem; +} + +.toc-chap-title { + white-space: nowrap; +} + +.toc-dots { + flex: 1; + border-bottom: 1px dotted rgba(30, 61, 47, 0.4); + margin: 0 4px; + height: 1px; +} + +.toc-page-num { + font-family: var(--font-sans); + font-weight: 700; + font-size: 0.88rem; + color: var(--tannengruen); +} + +.toc-recipe-list { + list-style: none; + padding: 0; + margin: 0; +} + +.toc-recipe-item { + margin: 3px 0; +} + +.toc-recipe-link { + display: flex; + align-items: baseline; + text-decoration: none; + color: var(--schwarz); + font-size: 0.84rem; + line-height: 1.25; + transition: color 0.15s ease; +} + +.toc-recipe-link:hover { + color: var(--tannengruen); +} + +.toc-rec-title { + max-width: 75%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* --- Einheitliche Seitenzahlen: Buch-Druck abwechselnd links (gerade) / rechts (ungerade) --- */ +.page-footer { + position: absolute; + bottom: 8mm; + border-top: none; + padding: 0; + margin: 0; + background: transparent; + display: flex; + align-items: center; + z-index: 50; +} + +.page-footer.page-even { + left: 14mm; + right: auto; + justify-content: flex-start; + text-align: left; +} + +.page-footer.page-odd { + right: 14mm; + left: auto; + justify-content: flex-end; + text-align: right; +} + +.page-footer .page-number { + font-family: var(--font-sans); + font-size: 1rem; + font-weight: 700; + color: var(--tannengruen); + line-height: 1; +} + +/* Titelseite, Inhaltsverzeichnis und Seitenzahlen sind rein für den Druck gedacht */ +.page.cover, +.page.toc-page, +.page-footer { + display: none !important; +} + +/* Modal Scroll Lock für Hintergrundseite */ +body.modal-open { + overflow: hidden !important; +} + +.cb-modal-backdrop { + overscroll-behavior: contain; + touch-action: pan-y; +} + +.cb-modal-card { + overscroll-behavior: contain; + touch-action: pan-y; +} + +.cb-modal-body { + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + touch-action: pan-y; +} + +/* ========================================================================== + Wochenmenü-Plan (Meal Planner) + ========================================================================== */ +.mealplan-week-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + background: var(--creme); + border: 2px solid var(--tannengruen); + border-radius: 10px; + padding: 8px 14px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.mealplan-week-nav { + display: flex; + align-items: center; + gap: 8px; +} + +.mealplan-week-badge { + font-family: var(--font-hand); + font-size: 1.25rem; + font-weight: 700; + color: var(--tannengruen); + display: flex; + align-items: center; + gap: 6px; +} + +.mealplan-today-tag { + font-size: 0.75rem; + background: var(--altrosa); + color: var(--creme); + padding: 2px 8px; + border-radius: 12px; + font-weight: 700; + text-transform: uppercase; +} + +.mealplan-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); + gap: 12px; + margin-top: 10px; +} + +@media screen and (min-width: 1024px) { + .mealplan-grid { + grid-template-columns: repeat(7, 1fr); + gap: 8px; + } +} + +.mealplan-day-card { + background: #ffffff; + border: 1.5px solid rgba(34, 34, 34, 0.2); + border-radius: 10px; + overflow: hidden; + display: flex; + flex-direction: column; + transition: box-shadow 0.2s; + box-shadow: 2px 2px 0px rgba(0,0,0,0.06); +} + +.mealplan-day-card.is-today { + border: 2px solid var(--tannengruen); + box-shadow: 3px 3px 0px var(--tannengruen); +} + +.mealplan-day-header { + background: rgba(30, 77, 59, 0.08); + padding: 8px 10px; + border-bottom: 1.5px solid rgba(34, 34, 34, 0.15); + display: flex; + justify-content: space-between; + align-items: center; +} + +.mealplan-day-card.is-today .mealplan-day-header { + background: var(--tannengruen); + color: var(--creme); +} + +.mealplan-day-name { + font-family: var(--font-hand); + font-size: 1.15rem; + font-weight: 700; + line-height: 1.1; +} + +.mealplan-day-date { + font-size: 0.82rem; + font-weight: 600; + color: #666; +} + +.mealplan-day-card.is-today .mealplan-day-date { + color: rgba(250, 246, 240, 0.85); +} + +.mealplan-day-slots { + padding: 8px; + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; +} + +/* Gefüllte Mahlzeit im Wochenplan (kompakt & übersichtlich) */ +.mealplan-slot.has-meal { + background: #ffffff; + border: 1.5px solid rgba(30, 77, 59, 0.22); + border-radius: 8px; + padding: 8px 10px; + font-size: 0.88rem; + display: flex; + flex-direction: column; + gap: 4px; + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.05); + transition: all 0.15s; +} + +.mealplan-slot.has-meal:hover { + background: #faf7f2; + border-color: var(--tannengruen); +} + +.mealplan-slot-top { + display: flex; + justify-content: space-between; + align-items: center; +} + +.mealplan-slot-badge { + font-size: 0.74rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--tannengruen); +} + +.mealplan-slot-title { + font-size: 0.9rem; + font-weight: 600; + color: var(--schwarz); + line-height: 1.3; + word-break: break-word; +} + +.mealplan-recipe-link { + color: var(--tannengruen); + text-decoration: underline; + text-underline-offset: 2px; +} + +.mealplan-recipe-link:hover { + color: #143729; +} + +.mealplan-slot-actions { + display: flex; + gap: 4px; + align-items: center; +} + +.mealplan-action-btn { + background: transparent; + border: none; + font-size: 0.82rem; + cursor: pointer; + padding: 2px 4px; + border-radius: 4px; + color: #666; + transition: background 0.15s; +} + +.mealplan-action-btn:hover { + background: rgba(0, 0, 0, 0.08); + color: var(--schwarz); +} + +.mealplan-action-btn.btn-delete:hover { + color: #c53030; + background: #fff0f0; +} + +/* Button zum Hinzufügen einer weiteren Mahlzeit für den Tag */ +.mealplan-add-meal-btn { + background: rgba(30, 77, 59, 0.04); + border: 1.5px dashed rgba(30, 77, 59, 0.35); + border-radius: 8px; + padding: 7px 10px; + color: var(--tannengruen); + font-size: 0.84rem; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + transition: all 0.15s; + width: 100%; +} + +.mealplan-add-meal-btn:hover { + background: rgba(30, 77, 59, 0.1); + border-color: var(--tannengruen); +} + +.mealplan-empty-day-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 10px 4px; +} + +/* Mahlzeittyp-Auswahlchips im Bearbeiten-Modal */ +.slot-type-chips { + display: flex; + gap: 8px; + margin-bottom: 14px; +} + +.slot-type-chip { + flex: 1; + padding: 8px 10px; + background: #ffffff; + border: 1.5px solid rgba(34, 34, 34, 0.25); + border-radius: 8px; + font-family: var(--font-sans); + font-size: 0.92rem; + font-weight: 600; + color: var(--schwarz); + cursor: pointer; + transition: all 0.15s; + text-align: center; +} + +.slot-type-chip:hover { + border-color: var(--tannengruen); + background: rgba(30, 77, 59, 0.05); +} + +.slot-type-chip.active { + background: var(--tannengruen); + border-color: var(--tannengruen); + color: var(--creme); + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); +} + +/* --- Druck-Spezifische Optimierungen (Exakte A4-Höhe 297mm & Seitenzahlen millimetergenau am Fuß) --- */ +@media print { + /* Titelseite, Inhaltsverzeichnis und Seitenzahlen IM DRUCK SICHTBAR */ + .page.cover { + display: flex !important; + } + + .page.toc-page { + display: flex !important; + } + + /* A4 Druck: 282mm Höhe passt exakt in AirPrint & Standard-Drucker ohne 2. Seite für Seitenzahl */ + .page, .cover, .toc-page, .divider, .recipe-page { + width: 210mm !important; + max-width: 210mm !important; + min-width: 210mm !important; + height: 282mm !important; + min-height: 282mm !important; + max-height: 282mm !important; + box-sizing: border-box !important; + overflow: hidden !important; + page-break-after: always !important; + break-after: page !important; + page-break-inside: avoid !important; + break-inside: avoid !important; + display: flex !important; + flex-direction: column !important; + } + + /* Fußzeile mit Seitenzahl: Im Flex-Flow ganz unten (niemals im Zutatenblock oder schwebend) */ + .page-footer { + display: flex !important; + margin-top: auto !important; + position: relative !important; + bottom: auto !important; + border: none !important; + padding: 3mm 0 2mm 0 !important; + width: 100% !important; + box-sizing: border-box !important; + z-index: 100 !important; + } + + .divider { + padding: 14mm 10mm !important; + justify-content: center !important; + } + + .doodle-placeholder { + width: 140mm !important; + height: 140mm !important; + max-width: 140mm !important; + max-height: 140mm !important; + margin: 6mm auto 0 auto !important; + } + + .doodle-placeholder img { + width: 100% !important; + height: 100% !important; + object-fit: contain !important; + } + + .page-footer.page-even { + left: auto !important; + right: auto !important; + justify-content: flex-start !important; + text-align: left !important; + padding-left: 4mm !important; + padding-right: 0 !important; + } + + .page-footer.page-odd { + right: auto !important; + left: auto !important; + justify-content: flex-end !important; + text-align: right !important; + padding-right: 4mm !important; + padding-left: 0 !important; + } + + .page-footer .page-number { + font-size: 0.95rem !important; + font-weight: 700 !important; + color: var(--tannengruen) !important; + } + + .recipe-page { + padding: 6mm 10mm !important; + overflow: hidden !important; + } + + .recipe-source-line { + display: none !important; + } + + .ingredients-box { + padding: 6px 8px !important; + } + + .ingredients-list { + gap: 2px !important; + } + + .ingredients-list li { + font-size: 0.80rem !important; + line-height: 1.25 !important; + } + + .ingredients-list li.ing-subheading { + font-size: 0.84rem !important; + margin-top: 3px !important; + padding-bottom: 1px !important; + } + + .recipe-photo-box { + height: 38mm !important; + max-height: 38mm !important; + } + + .portion-matrix-table { + font-size: 0.72rem !important; + line-height: 1.2 !important; + } + + .portion-matrix-table th { + padding: 1px 2px !important; + font-size: 0.7rem !important; + } + + .portion-matrix-table td { + padding: 1px 2px !important; + } + + .recipe-grid:has(.has-portion-matrix) { + grid-template-columns: 68mm 1fr !important; + gap: 8mm !important; + } +} + diff --git a/app/static/css/main.css b/app/static/css/main.css new file mode 120000 index 0000000..5191bf7 --- /dev/null +++ b/app/static/css/main.css @@ -0,0 +1 @@ +../style.css \ No newline at end of file diff --git a/app/static/css/style.css b/app/static/css/style.css new file mode 120000 index 0000000..5191bf7 --- /dev/null +++ b/app/static/css/style.css @@ -0,0 +1 @@ +../style.css \ No newline at end of file diff --git a/app/static/icon-192.png b/app/static/icon-192.png new file mode 100755 index 0000000..2408beb Binary files /dev/null and b/app/static/icon-192.png differ diff --git a/app/static/icon-512.png b/app/static/icon-512.png new file mode 100755 index 0000000..70d057e Binary files /dev/null and b/app/static/icon-512.png differ diff --git a/app/static/img/chapters/chapter_1.png b/app/static/img/chapters/chapter_1.png new file mode 100755 index 0000000..ccc836b Binary files /dev/null and b/app/static/img/chapters/chapter_1.png differ diff --git a/app/static/img/chapters/chapter_10.png b/app/static/img/chapters/chapter_10.png new file mode 100755 index 0000000..fcca2be Binary files /dev/null and b/app/static/img/chapters/chapter_10.png differ diff --git a/app/static/img/chapters/chapter_11.png b/app/static/img/chapters/chapter_11.png new file mode 100755 index 0000000..1bc6d47 Binary files /dev/null and b/app/static/img/chapters/chapter_11.png differ diff --git a/app/static/img/chapters/chapter_2.png b/app/static/img/chapters/chapter_2.png new file mode 100755 index 0000000..a20d832 Binary files /dev/null and b/app/static/img/chapters/chapter_2.png differ diff --git a/app/static/img/chapters/chapter_3.png b/app/static/img/chapters/chapter_3.png new file mode 100755 index 0000000..c51a24f Binary files /dev/null and b/app/static/img/chapters/chapter_3.png differ diff --git a/app/static/img/chapters/chapter_4.png b/app/static/img/chapters/chapter_4.png new file mode 100755 index 0000000..206bf9e Binary files /dev/null and b/app/static/img/chapters/chapter_4.png differ diff --git a/app/static/img/chapters/chapter_5.png b/app/static/img/chapters/chapter_5.png new file mode 100755 index 0000000..50bceb0 Binary files /dev/null and b/app/static/img/chapters/chapter_5.png differ diff --git a/app/static/img/chapters/chapter_6.png b/app/static/img/chapters/chapter_6.png new file mode 100755 index 0000000..95cc13c Binary files /dev/null and b/app/static/img/chapters/chapter_6.png differ diff --git a/app/static/img/chapters/chapter_7.png b/app/static/img/chapters/chapter_7.png new file mode 100755 index 0000000..82e1a56 Binary files /dev/null and b/app/static/img/chapters/chapter_7.png differ diff --git a/app/static/img/chapters/chapter_8.png b/app/static/img/chapters/chapter_8.png new file mode 100755 index 0000000..1938ef7 Binary files /dev/null and b/app/static/img/chapters/chapter_8.png differ diff --git a/app/static/img/chapters/chapter_9.png b/app/static/img/chapters/chapter_9.png new file mode 100755 index 0000000..ef818b6 Binary files /dev/null and b/app/static/img/chapters/chapter_9.png differ diff --git a/app/static/img/chapters/source_chapter_1.jpg b/app/static/img/chapters/source_chapter_1.jpg new file mode 100755 index 0000000..3295c18 Binary files /dev/null and b/app/static/img/chapters/source_chapter_1.jpg differ diff --git a/app/static/img/chapters/source_chapters.jpg b/app/static/img/chapters/source_chapters.jpg new file mode 100755 index 0000000..df1df6c Binary files /dev/null and b/app/static/img/chapters/source_chapters.jpg differ diff --git a/app/static/js/app.js b/app/static/js/app.js new file mode 120000 index 0000000..3c6e70c --- /dev/null +++ b/app/static/js/app.js @@ -0,0 +1 @@ +../app.js \ No newline at end of file diff --git a/app/static/js/main.js b/app/static/js/main.js new file mode 120000 index 0000000..3c6e70c --- /dev/null +++ b/app/static/js/main.js @@ -0,0 +1 @@ +../app.js \ No newline at end of file diff --git a/app/static/manifest.json b/app/static/manifest.json new file mode 100755 index 0000000..bf9a115 --- /dev/null +++ b/app/static/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "What To Cook? – Essens-Zufallsgenerator", + "short_name": "WhatToCook", + "description": "Zufallsgenerator für Essen mit Kategorien und Aufwandsfilter", + "start_url": "/", + "display": "standalone", + "background_color": "#0f141c", + "theme_color": "#ff6b35", + "icons": [ + { + "src": "/static/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/static/icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/app/static/style.css b/app/static/style.css new file mode 100755 index 0000000..0f00da3 --- /dev/null +++ b/app/static/style.css @@ -0,0 +1,1085 @@ +/* ========================================================================== + What To Cook? - Mobile-First Modern Theme + ========================================================================== */ + +:root { + --bg-main: #0f141c; + --bg-card: #18202c; + --bg-card-hover: #202b3b; + --bg-input: #121822; + --border-color: #2b3648; + --border-focus: #ff7849; + + --primary: #ff6b35; + --primary-hover: #fa5719; + --primary-light: rgba(255, 107, 53, 0.15); + + --accent: #2ec4b6; + --accent-light: rgba(46, 196, 182, 0.15); + + --text-main: #f8fafc; + --text-muted: #94a3b8; + --text-dim: #64748b; + + --effort-gering: #10b981; + --effort-gering-bg: rgba(16, 185, 129, 0.15); + --effort-mittel: #f59e0b; + --effort-mittel-bg: rgba(245, 158, 11, 0.15); + --effort-hoch: #ef4444; + --effort-hoch-bg: rgba(239, 68, 68, 0.15); + + --danger: #ef4444; + --danger-hover: #dc2626; + + --radius-sm: 8px; + --radius-md: 14px; + --radius-lg: 20px; + --radius-pill: 9999px; + + --shadow-sm: 0 2px 4px rgba(0,0,0,0.25); + --shadow-md: 0 8px 24px rgba(0,0,0,0.35); + --shadow-glow: 0 0 20px rgba(255, 107, 53, 0.3); + + --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --safe-bottom: env(safe-area-inset-bottom, 16px); +} + +/* Reset & Base */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-family); + background-color: var(--bg-main); + color: var(--text-main); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + min-height: 100vh; + padding-bottom: calc(85px + var(--safe-bottom)); +} + +.app-container { + max-width: 680px; + margin: 0 auto; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Header */ +.app-header { + position: sticky; + top: 0; + z-index: 100; + background: rgba(15, 20, 28, 0.92); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border-color); + padding: 12px 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.brand { + display: flex; + align-items: center; + gap: 10px; +} + +.brand-icon { + font-size: 2rem; + line-height: 1; + filter: drop-shadow(0 2px 8px rgba(255, 107, 53, 0.4)); +} + +.brand-title { + font-size: 1.35rem; + font-weight: 800; + letter-spacing: -0.02em; + color: #fff; + background: linear-gradient(135deg, #fff 40%, #ff9e7d); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.brand-subtitle { + font-size: 0.8rem; + color: var(--text-muted); +} + +/* Tabs */ +.nav-tabs { + display: flex; + background: var(--bg-input); + padding: 4px; + border-radius: var(--radius-pill); + border: 1px solid var(--border-color); +} + +.nav-tab { + flex: 1; + background: transparent; + border: none; + color: var(--text-muted); + font-size: 0.9rem; + font-weight: 600; + padding: 8px 12px; + border-radius: var(--radius-pill); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + transition: all 0.2s ease; +} + +.nav-tab.active { + background: var(--primary); + color: #fff; + box-shadow: 0 2px 10px rgba(255, 107, 53, 0.35); +} + +/* Main Layout */ +.app-main { + padding: 16px; + flex: 1; +} + +.tab-panel { + display: none; + animation: fadeIn 0.25s ease; +} + +.tab-panel.active { + display: block; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Info Card */ +.info-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 12px 14px; + margin-bottom: 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + font-size: 0.85rem; +} + +.info-content { + display: flex; + align-items: center; + gap: 10px; + color: var(--text-muted); +} + +.info-icon { + font-size: 1.4rem; +} + +.active-counter-badge { + background: var(--accent-light); + color: var(--accent); + font-weight: 700; + font-size: 0.75rem; + padding: 4px 10px; + border-radius: var(--radius-pill); + white-space: nowrap; + border: 1px solid rgba(46, 196, 182, 0.3); +} + +/* Slots List */ +.slots-container { + display: flex; + flex-direction: column; + gap: 14px; + margin-bottom: 24px; +} + +/* Dish Slot Card */ +.slot-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 16px; + transition: border-color 0.2s, box-shadow 0.2s, opacity 0.2s; +} + +.slot-card.active { + border-color: rgba(255, 107, 53, 0.4); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); +} + +.slot-card.inactive { + opacity: 0.65; + background: rgba(24, 32, 44, 0.5); + border-style: dashed; +} + +.slot-card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + padding-bottom: 10px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.slot-title-group { + display: flex; + align-items: center; + gap: 8px; +} + +.slot-number-badge { + background: var(--primary); + color: #fff; + font-size: 0.75rem; + font-weight: 800; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.slot-card.inactive .slot-number-badge { + background: var(--text-dim); +} + +.slot-name { + font-size: 1rem; + font-weight: 700; +} + +/* Toggle Switch */ +.slot-toggle-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + user-select: none; + font-size: 0.8rem; + color: var(--text-muted); +} + +.switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: var(--bg-input); + border: 1px solid var(--border-color); + transition: 0.25s; + border-radius: 24px; +} + +.slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 2px; + bottom: 2px; + background-color: var(--text-muted); + transition: 0.25s; + border-radius: 50%; +} + +input:checked + .slider { + background-color: var(--primary); + border-color: var(--primary); +} + +input:checked + .slider:before { + transform: translateX(20px); + background-color: #fff; +} + +/* Card Body Inputs */ +.slot-body { + display: flex; + flex-direction: column; + gap: 12px; +} + +.slot-card.inactive .slot-body { + pointer-events: none; + opacity: 0.4; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.form-label { + font-size: 0.78rem; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +/* Select Box */ +.custom-select, .text-input { + width: 100%; + background: var(--bg-input); + border: 1px solid var(--border-color); + color: var(--text-main); + padding: 10px 14px; + border-radius: var(--radius-sm); + font-size: 0.95rem; + font-family: inherit; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; + appearance: none; + -webkit-appearance: none; +} + +.custom-select { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 18px; + padding-right: 36px; +} + +.custom-select:focus, .text-input:focus { + border-color: var(--border-focus); + box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.2); +} + +/* Effort Multi-Select Chips */ +.effort-chips-group { + display: flex; + gap: 8px; +} + +.effort-chip { + flex: 1; + background: var(--bg-input); + border: 1px solid var(--border-color); + color: var(--text-muted); + padding: 8px 6px; + border-radius: var(--radius-sm); + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + text-align: center; + user-select: none; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; +} + +.effort-chip.selected[data-effort="gering"] { + background: var(--effort-gering-bg); + border-color: var(--effort-gering); + color: var(--effort-gering); + font-weight: 700; +} + +.effort-chip.selected[data-effort="mittel"] { + background: var(--effort-mittel-bg); + border-color: var(--effort-mittel); + color: var(--effort-mittel); + font-weight: 700; +} + +.effort-chip.selected[data-effort="hoch"] { + background: var(--effort-hoch-bg); + border-color: var(--effort-hoch); + color: var(--effort-hoch); + font-weight: 700; +} + +/* Action Bar Floating/Sticky at bottom */ +.action-bar-wrapper { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(to top, rgba(15, 20, 28, 0.98) 60%, rgba(15, 20, 28, 0)); + padding: 12px 16px calc(14px + var(--safe-bottom)) 16px; + z-index: 90; + display: flex; + justify-content: center; +} + +.action-bar-wrapper .btn-hero { + max-width: 648px; + width: 100%; +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-family: inherit; + font-size: 0.95rem; + font-weight: 700; + padding: 12px 18px; + border-radius: var(--radius-md); + border: none; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + text-decoration: none; + user-select: none; +} + +.btn:active { + transform: scale(0.97); +} + +.btn-primary { + background: var(--primary); + color: #fff; + box-shadow: 0 4px 14px rgba(255, 107, 53, 0.4); +} + +.btn-primary:hover { + background: var(--primary-hover); + box-shadow: 0 6px 20px rgba(255, 107, 53, 0.5); +} + +.btn-hero { + padding: 15px 24px; + font-size: 1.1rem; + border-radius: var(--radius-pill); +} + +.btn-secondary { + background: var(--bg-card-hover); + color: var(--text-main); + border: 1px solid var(--border-color); +} + +.btn-secondary:hover { + background: var(--border-color); +} + +.btn-outline { + background: transparent; + color: var(--text-main); + border: 1px solid var(--border-color); +} + +.btn-outline:hover { + background: rgba(255, 255, 255, 0.06); + border-color: var(--text-muted); +} + +.btn-danger { + background: var(--danger); + color: #fff; +} + +.btn-danger:hover { + background: var(--danger-hover); +} + +.btn-sm { + padding: 6px 12px; + font-size: 0.8rem; + border-radius: var(--radius-sm); +} + +/* Results Section */ +.results-section { + margin-top: 10px; + margin-bottom: 30px; + scroll-margin-top: 120px; +} + +.results-section.hidden { + display: none; +} + +.results-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.results-title-group { + display: flex; + align-items: center; + gap: 10px; +} + +.results-title { + font-size: 1.25rem; + font-weight: 800; +} + +.results-badge { + background: var(--primary-light); + color: var(--primary); + font-size: 0.75rem; + font-weight: 700; + padding: 3px 8px; + border-radius: var(--radius-pill); +} + +.results-grid { + display: flex; + flex-direction: column; + gap: 14px; + margin-bottom: 20px; +} + +/* Result Card */ +.result-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + position: relative; + overflow: hidden; + box-shadow: var(--shadow-sm); + transition: transform 0.25s, box-shadow 0.25s, border-color 0.25s; +} + +.result-card:hover { + border-color: rgba(255, 107, 53, 0.5); + box-shadow: var(--shadow-md); +} + +.result-card.rerolling { + animation: pulseCard 0.4s ease; +} + +@keyframes pulseCard { + 0% { transform: scale(1); } + 50% { transform: scale(0.97); opacity: 0.7; } + 100% { transform: scale(1); opacity: 1; } +} + +.result-card-top { + display: flex; + align-items: center; + justify-content: space-between; +} + +.result-slot-label { + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-dim); +} + +.result-dish-name { + font-size: 1.2rem; + font-weight: 800; + color: #fff; + line-height: 1.3; +} + +.result-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +.badge-tag { + font-size: 0.75rem; + font-weight: 700; + padding: 4px 10px; + border-radius: var(--radius-pill); + display: inline-flex; + align-items: center; + gap: 5px; +} + +.badge-category { + background: rgba(255, 255, 255, 0.08); + color: #e2e8f0; + border: 1px solid rgba(255, 255, 255, 0.12); +} + +.badge-effort-gering { + background: var(--effort-gering-bg); + color: var(--effort-gering); + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.badge-effort-mittel { + background: var(--effort-mittel-bg); + color: var(--effort-mittel); + border: 1px solid rgba(245, 158, 11, 0.3); +} + +.badge-effort-hoch { + background: var(--effort-hoch-bg); + color: var(--effort-hoch); + border: 1px solid rgba(239, 68, 68, 0.3); +} + +.result-card-actions { + display: flex; + justify-content: flex-end; + padding-top: 8px; + border-top: 1px solid rgba(255, 255, 255, 0.05); +} + +.btn-reroll-single { + background: var(--bg-input); + border: 1px solid var(--border-color); + color: var(--text-main); + padding: 8px 14px; + font-size: 0.85rem; + border-radius: var(--radius-pill); + display: inline-flex; + align-items: center; + gap: 6px; + cursor: pointer; + transition: all 0.2s; +} + +.btn-reroll-single:hover { + background: var(--primary-light); + border-color: var(--primary); + color: #fff; +} + +.btn-reroll-single .dice-icon { + display: inline-block; + transition: transform 0.4s ease; +} + +.btn-reroll-single.spinning .dice-icon { + animation: spinDice 0.5s ease; +} + +@keyframes spinDice { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.results-footer { + display: flex; + justify-content: center; + padding: 10px 0; +} + +/* ========================================================================== + Admin Section Styles + ========================================================================== */ +.admin-topbar { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + gap: 12px; +} + +.section-title { + font-size: 1.25rem; + font-weight: 800; +} + +.section-desc { + font-size: 0.8rem; + color: var(--text-muted); +} + +.admin-filters-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 14px; + margin-bottom: 16px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.search-input-wrap { + position: relative; + display: flex; + align-items: center; +} + +.search-icon { + position: absolute; + left: 12px; + font-size: 0.95rem; + pointer-events: none; + color: var(--text-dim); +} + +.search-input-wrap input { + width: 100%; + background: var(--bg-input); + border: 1px solid var(--border-color); + color: var(--text-main); + padding: 10px 36px 10px 36px; + border-radius: var(--radius-sm); + font-size: 0.9rem; + outline: none; +} + +.search-input-wrap input:focus { + border-color: var(--border-focus); +} + +.btn-clear { + position: absolute; + right: 10px; + background: transparent; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 4px; + font-size: 0.9rem; +} + +.btn-clear.hidden { + display: none; +} + +.filter-row { + display: flex; + gap: 10px; +} + +.flex-1 { + flex: 1; +} + +.dishes-list-wrap { + display: flex; + flex-direction: column; + gap: 8px; +} + +.dishes-list-header { + display: flex; + justify-content: space-between; + font-size: 0.8rem; + font-weight: 700; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 0 4px; +} + +.admin-dishes-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.admin-dish-row { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + padding: 12px 14px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + transition: border-color 0.2s; +} + +.admin-dish-row:hover { + border-color: rgba(255, 255, 255, 0.2); +} + +.admin-dish-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.admin-dish-name { + font-weight: 700; + font-size: 0.95rem; +} + +.admin-dish-badges { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.admin-dish-actions { + display: flex; + align-items: center; + gap: 6px; +} + +.btn-action-icon { + background: var(--bg-input); + border: 1px solid var(--border-color); + color: var(--text-muted); + width: 34px; + height: 34px; + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.15s; +} + +.btn-action-icon:hover { + background: var(--bg-card-hover); + color: #fff; + border-color: var(--text-muted); +} + +.btn-action-icon.btn-delete:hover { + background: var(--danger); + border-color: var(--danger); + color: #fff; +} + +/* Modals */ +.modal { + border: none; + background: transparent; + padding: 16px; + margin: auto; + max-width: 480px; + width: 100%; +} + +.modal::backdrop { + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(4px); +} + +.modal-box { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: 22px; + box-shadow: var(--shadow-md); + color: var(--text-main); + display: flex; + flex-direction: column; + gap: 16px; +} + +.modal-box-sm { + max-width: 380px; + margin: auto; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.modal-title { + font-size: 1.15rem; + font-weight: 800; +} + +.btn-icon-close { + background: transparent; + border: none; + color: var(--text-muted); + font-size: 1.2rem; + cursor: pointer; + padding: 4px; +} + +.form-label-hint { + font-size: 0.75rem; + font-weight: normal; + text-transform: none; + color: var(--text-dim); +} + +.category-checkbox-group { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 4px; +} + +.category-checkbox-label { + display: inline-flex; + cursor: pointer; + user-select: none; +} + +.category-checkbox-label input { + position: absolute; + opacity: 0; + width: 0; + height: 0; + pointer-events: none; +} + +.category-checkbox-label .cat-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + background: var(--bg-input); + border: 1px solid var(--border-color); + border-radius: var(--radius-pill); + font-size: 0.85rem; + font-weight: 500; + color: var(--text-muted); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.category-checkbox-label .cat-pill:hover { + border-color: var(--text-muted); + color: var(--text-main); +} + +.category-checkbox-label input:checked + .cat-pill { + background: var(--primary-light); + border-color: var(--primary); + color: var(--primary); + font-weight: 700; + box-shadow: 0 0 0 1px var(--primary); +} + +.effort-radio-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.effort-radio-label { + display: flex; + align-items: center; + cursor: pointer; +} + +.effort-radio-label input { + display: none; +} + +.effort-radio-label .effort-pill { + flex: 1; + padding: 10px 14px; + border-radius: var(--radius-sm); + background: var(--bg-input); + border: 1px solid var(--border-color); + font-size: 0.85rem; + font-weight: 600; + color: var(--text-muted); + transition: all 0.2s; +} + +.effort-radio-label input:checked + .effort-pill.effort-gering { + background: var(--effort-gering-bg); + border-color: var(--effort-gering); + color: var(--effort-gering); + font-weight: 700; +} + +.effort-radio-label input:checked + .effort-pill.effort-mittel { + background: var(--effort-mittel-bg); + border-color: var(--effort-mittel); + color: var(--effort-mittel); + font-weight: 700; +} + +.effort-radio-label input:checked + .effort-pill.effort-hoch { + background: var(--effort-hoch-bg); + border-color: var(--effort-hoch); + color: var(--effort-hoch); + font-weight: 700; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 8px; +} + +/* Toast Notifications */ +.toast-container { + position: fixed; + top: 16px; + left: 50%; + transform: translateX(-50%); + z-index: 1000; + display: flex; + flex-direction: column; + gap: 8px; + width: 90%; + max-width: 400px; + pointer-events: none; +} + +.toast { + background: var(--bg-card); + border: 1px solid var(--border-color); + color: var(--text-main); + padding: 12px 16px; + border-radius: var(--radius-md); + box-shadow: var(--shadow-md); + font-size: 0.9rem; + display: flex; + align-items: center; + gap: 10px; + animation: slideDown 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28); +} + +.toast.toast-success { + border-color: var(--effort-gering); +} + +.toast.toast-error { + border-color: var(--effort-hoch); +} + +@keyframes slideDown { + from { opacity: 0; transform: translateY(-16px); } + to { opacity: 1; transform: translateY(0); } +} + +.loading-spinner { + text-align: center; + padding: 24px; + color: var(--text-muted); + font-size: 0.9rem; +} diff --git a/app/static/uploads/recipes/9e5040e135_1791922273901559465380300848871.jpg b/app/static/uploads/recipes/9e5040e135_1791922273901559465380300848871.jpg new file mode 100755 index 0000000..3c41ffb Binary files /dev/null and b/app/static/uploads/recipes/9e5040e135_1791922273901559465380300848871.jpg differ diff --git a/app/static/uploads/recipes/american_pancakes.png b/app/static/uploads/recipes/american_pancakes.png new file mode 100644 index 0000000..aa3d83e Binary files /dev/null and b/app/static/uploads/recipes/american_pancakes.png differ diff --git a/app/static/uploads/recipes/ananaskuchen.jpg b/app/static/uploads/recipes/ananaskuchen.jpg new file mode 100644 index 0000000..ef1f461 Binary files /dev/null and b/app/static/uploads/recipes/ananaskuchen.jpg differ diff --git a/app/static/uploads/recipes/apfel_erdnuss_tarte.jpeg b/app/static/uploads/recipes/apfel_erdnuss_tarte.jpeg new file mode 100644 index 0000000..069eca1 Binary files /dev/null and b/app/static/uploads/recipes/apfel_erdnuss_tarte.jpeg differ diff --git a/app/static/uploads/recipes/bao_buns_mit_champignons.png b/app/static/uploads/recipes/bao_buns_mit_champignons.png new file mode 100644 index 0000000..e1b025e Binary files /dev/null and b/app/static/uploads/recipes/bao_buns_mit_champignons.png differ diff --git a/app/static/uploads/recipes/blaetterteigpaeckchen_spargel.jpg b/app/static/uploads/recipes/blaetterteigpaeckchen_spargel.jpg new file mode 100644 index 0000000..f49427e Binary files /dev/null and b/app/static/uploads/recipes/blaetterteigpaeckchen_spargel.jpg differ diff --git a/app/static/uploads/recipes/blauchbeer.jpeg b/app/static/uploads/recipes/blauchbeer.jpeg new file mode 100644 index 0000000..ac476a3 Binary files /dev/null and b/app/static/uploads/recipes/blauchbeer.jpeg differ diff --git a/app/static/uploads/recipes/bounty-kuchen_rezept___dr_oetker.jpg b/app/static/uploads/recipes/bounty-kuchen_rezept___dr_oetker.jpg new file mode 100644 index 0000000..a6ad1fc Binary files /dev/null and b/app/static/uploads/recipes/bounty-kuchen_rezept___dr_oetker.jpg differ diff --git a/app/static/uploads/recipes/brezen_cracker.jpeg b/app/static/uploads/recipes/brezen_cracker.jpeg new file mode 100644 index 0000000..164ecf2 Binary files /dev/null and b/app/static/uploads/recipes/brezen_cracker.jpeg differ diff --git a/app/static/uploads/recipes/champignons_weihnachtsmarkt.jpg b/app/static/uploads/recipes/champignons_weihnachtsmarkt.jpg new file mode 100644 index 0000000..99e07b2 Binary files /dev/null and b/app/static/uploads/recipes/champignons_weihnachtsmarkt.jpg differ diff --git a/app/static/uploads/recipes/chili_sin_carne.jpeg b/app/static/uploads/recipes/chili_sin_carne.jpeg new file mode 100644 index 0000000..96a17fb Binary files /dev/null and b/app/static/uploads/recipes/chili_sin_carne.jpeg differ diff --git a/app/static/uploads/recipes/crema_catalana_von_aurora.png b/app/static/uploads/recipes/crema_catalana_von_aurora.png new file mode 100644 index 0000000..1af7daa Binary files /dev/null and b/app/static/uploads/recipes/crema_catalana_von_aurora.png differ diff --git a/app/static/uploads/recipes/cremige_zucchini-bohnenpfanne_mit_erdnussbutter_-_eat_this.jpg b/app/static/uploads/recipes/cremige_zucchini-bohnenpfanne_mit_erdnussbutter_-_eat_this.jpg new file mode 100644 index 0000000..803810a Binary files /dev/null and b/app/static/uploads/recipes/cremige_zucchini-bohnenpfanne_mit_erdnussbutter_-_eat_this.jpg differ diff --git a/app/static/uploads/recipes/donauwelle_-_super_easy_von_alina1st.png b/app/static/uploads/recipes/donauwelle_-_super_easy_von_alina1st.png new file mode 100644 index 0000000..2a07116 Binary files /dev/null and b/app/static/uploads/recipes/donauwelle_-_super_easy_von_alina1st.png differ diff --git a/app/static/uploads/recipes/edca4b2d4c_17919220926958466358991436025020.jpg b/app/static/uploads/recipes/edca4b2d4c_17919220926958466358991436025020.jpg new file mode 100755 index 0000000..4672b58 Binary files /dev/null and b/app/static/uploads/recipes/edca4b2d4c_17919220926958466358991436025020.jpg differ diff --git a/app/static/uploads/recipes/galaktoboureko_-_griechischer_grießauflauf.png b/app/static/uploads/recipes/galaktoboureko_-_griechischer_grießauflauf.png new file mode 100644 index 0000000..fa0e493 Binary files /dev/null and b/app/static/uploads/recipes/galaktoboureko_-_griechischer_grießauflauf.png differ diff --git a/app/static/uploads/recipes/gebratene_kasknödel_von_felidae177.png b/app/static/uploads/recipes/gebratene_kasknödel_von_felidae177.png new file mode 100644 index 0000000..3cb0bd7 Binary files /dev/null and b/app/static/uploads/recipes/gebratene_kasknödel_von_felidae177.png differ diff --git a/app/static/uploads/recipes/gedeckter_apfelkuchen_mit_streuseln_von_mbus.png b/app/static/uploads/recipes/gedeckter_apfelkuchen_mit_streuseln_von_mbus.png new file mode 100644 index 0000000..941a6e3 Binary files /dev/null and b/app/static/uploads/recipes/gedeckter_apfelkuchen_mit_streuseln_von_mbus.png differ diff --git a/app/static/uploads/recipes/gemüse_nach_gonbao-art_von_zuiko.png b/app/static/uploads/recipes/gemüse_nach_gonbao-art_von_zuiko.png new file mode 100644 index 0000000..287a8b5 Binary files /dev/null and b/app/static/uploads/recipes/gemüse_nach_gonbao-art_von_zuiko.png differ diff --git a/app/static/uploads/recipes/griessnockerl_suppeneinlage_von_jawi.png b/app/static/uploads/recipes/griessnockerl_suppeneinlage_von_jawi.png new file mode 100644 index 0000000..2e3e25a Binary files /dev/null and b/app/static/uploads/recipes/griessnockerl_suppeneinlage_von_jawi.png differ diff --git a/app/static/uploads/recipes/grießbrei_von_pilotilka.png b/app/static/uploads/recipes/grießbrei_von_pilotilka.png new file mode 100644 index 0000000..2fd92b3 Binary files /dev/null and b/app/static/uploads/recipes/grießbrei_von_pilotilka.png differ diff --git a/app/static/uploads/recipes/gulasch.jpeg b/app/static/uploads/recipes/gulasch.jpeg new file mode 100644 index 0000000..29b2f20 Binary files /dev/null and b/app/static/uploads/recipes/gulasch.jpeg differ diff --git a/app/static/uploads/recipes/heidelbeer.jpeg b/app/static/uploads/recipes/heidelbeer.jpeg new file mode 100644 index 0000000..855c33b Binary files /dev/null and b/app/static/uploads/recipes/heidelbeer.jpeg differ diff --git a/app/static/uploads/recipes/kidneybohnen.jpeg b/app/static/uploads/recipes/kidneybohnen.jpeg new file mode 100644 index 0000000..f294876 Binary files /dev/null and b/app/static/uploads/recipes/kidneybohnen.jpeg differ diff --git a/app/static/uploads/recipes/lachs_in_thai-curry-soße_mit_zucchini.png b/app/static/uploads/recipes/lachs_in_thai-curry-soße_mit_zucchini.png new file mode 100644 index 0000000..5c84ca3 Binary files /dev/null and b/app/static/uploads/recipes/lachs_in_thai-curry-soße_mit_zucchini.png differ diff --git a/app/static/uploads/recipes/lebkuchen-mousse_mit_glühweinkirschen.jpg b/app/static/uploads/recipes/lebkuchen-mousse_mit_glühweinkirschen.jpg new file mode 100644 index 0000000..29dcc64 Binary files /dev/null and b/app/static/uploads/recipes/lebkuchen-mousse_mit_glühweinkirschen.jpg differ diff --git a/app/static/uploads/recipes/one-pot-chili-reispfanne-aus-dem-ofen-3.jpg b/app/static/uploads/recipes/one-pot-chili-reispfanne-aus-dem-ofen-3.jpg new file mode 100644 index 0000000..8ba0c47 Binary files /dev/null and b/app/static/uploads/recipes/one-pot-chili-reispfanne-aus-dem-ofen-3.jpg differ diff --git a/app/static/uploads/recipes/rezeptkarten.png b/app/static/uploads/recipes/rezeptkarten.png new file mode 100644 index 0000000..cfc16cb Binary files /dev/null and b/app/static/uploads/recipes/rezeptkarten.png differ diff --git a/app/static/uploads/recipes/spaghetti_zitronenbutter_lachs.png b/app/static/uploads/recipes/spaghetti_zitronenbutter_lachs.png new file mode 100644 index 0000000..8956fae Binary files /dev/null and b/app/static/uploads/recipes/spaghetti_zitronenbutter_lachs.png differ diff --git a/app/static/uploads/recipes/weihnachtliche_bratapfel_biskuitrolle_-_madame_dessert.jpg b/app/static/uploads/recipes/weihnachtliche_bratapfel_biskuitrolle_-_madame_dessert.jpg new file mode 100644 index 0000000..2b7f5b9 Binary files /dev/null and b/app/static/uploads/recipes/weihnachtliche_bratapfel_biskuitrolle_-_madame_dessert.jpg differ diff --git a/app/static/uploads/recipes/zimt-biskuitrolle_mit_pflaumen.png b/app/static/uploads/recipes/zimt-biskuitrolle_mit_pflaumen.png new file mode 100644 index 0000000..5e5940f Binary files /dev/null and b/app/static/uploads/recipes/zimt-biskuitrolle_mit_pflaumen.png differ diff --git a/app/static/uploads/recipes/zitronen-tiramisu-ohne-rohes-ei.jpg b/app/static/uploads/recipes/zitronen-tiramisu-ohne-rohes-ei.jpg new file mode 100644 index 0000000..d39dc58 Binary files /dev/null and b/app/static/uploads/recipes/zitronen-tiramisu-ohne-rohes-ei.jpg differ diff --git a/app/templates/cookbook.html b/app/templates/cookbook.html new file mode 100755 index 0000000..8ed91b4 --- /dev/null +++ b/app/templates/cookbook.html @@ -0,0 +1,1877 @@ + + + + + + Mein Kochbuch – What To Cook? + + + + + + + + +
+ + 📖 +

Kochbuch

+
+ + +
+ + + + + + 🚪 Abmelden + +
+ + +
+ +
+ + + + +
+ + 🚪 Abmelden + +
+
+
+ + + + + +
+ + + {% if current_chapter is none or current_chapter == 1 %} +
+
+

Meine
Lieblings-
rezepte

+

Klassiker & Entdeckungen

+
+
+ + + {% if total_recipes > 0 %} + +
+
+
Übersicht aller Gerichte
+

Inhaltsverzeichnis{% if toc_page_count > 1 %} (Teil 1){% endif %}

+
✦ ✦ ✦
+
+ +
+ {% for chap in toc_p1 %} +
+
+ {{ chap.icon }} + Kapitel {{ chap.number }}: {{ chap.title }} + + {{ chap.page_number }} +
+ +
+ {% endfor %} +
+ +
+ 2 +
+
+ + + {% if toc_page_count > 1 %} +
+
+
Übersicht aller Gerichte
+

Inhaltsverzeichnis (Teil 2)

+
✦ ✦ ✦
+
+ +
+ {% for chap in toc_p2 %} +
+
+ {{ chap.icon }} + Kapitel {{ chap.number }}: {{ chap.title }} + + {{ chap.page_number }} +
+ +
+ {% endfor %} +
+ +
+ 3 +
+
+ {% endif %} + {% endif %} + {% endif %} + + + {% for chap in chapters_to_display %} + {% set chap_recipes = recipes_by_chapter.get(chap.number, []) %} + + {% if chap_recipes|length > 0 %} + +
+
Kapitel {{ chap.number }}
+

{{ chap.title }}

+ +
+ {% if chap.illustration_url %} + {{ chap.title }} Illustration + {% else %} + [ {{ chap.doodle_hint }} ] + {% endif %} +
+ +
+ {{ chap.page_number }} +
+
+ + + {% for recipe in chap_recipes %} +
+ + +
+

{{ recipe.title }}

+ +
+ {% if recipe.effort %} +
+ ⏱️ + Aufwand: + {% if recipe.effort == 'gering' %}Gering 🟢{% elif recipe.effort == 'hoch' %}Hoch 🔴{% else %}Mittel 🟡{% endif %} + +
+ {% endif %} + + {% if recipe.prep_time %} +
+ 🔪 + Zubereitung: {{ recipe.prep_time }} +
+ {% endif %} + + {% if recipe.cook_time %} +
+ 🍳 + Kochen/Backen: {{ recipe.cook_time }} +
+ {% endif %} + + {% if recipe.portions %} +
+ 👥 + {{ recipe.portions }} Portionen +
+ {% endif %} + + {% if recipe.source and recipe.source.strip() %} +
+ 📖 + Quelle: {{ recipe.source.strip() }} +
+ {% endif %} +
+
+ + +
+ + + + + +
+
+

Zubereitung

+
+ {% for step in recipe.instructions.split('\n') %} + {% if step.strip() %} +
+ {{ loop.index }} +
{{ step.strip() }}
+
+ {% endif %} + {% endfor %} +
+
+ + {% if recipe.notes and recipe.notes.strip() %} +
+
💡 Tipp & Notiz
+
{{ recipe.notes.strip() }}
+
+ {% else %} + + {% endif %} + + + + +
+ +
+ + +
+ {{ recipe.page_number }} +
+ +
+ {% endfor %} + {% else %} + +
+ {% if current_chapter == chap.number %} +
+
Kapitel {{ chap.number }}
+

{{ chap.title }}

+
+ {% if chap.illustration_url %} + {{ chap.title }} Illustration + {% else %} + [ {{ chap.doodle_hint }} ] + {% endif %} +
+
+ {% endif %} +
+

+ Noch keine Rezepte in Kapitel {{ chap.number }} ({{ chap.title }}). +

+ +
+
+ {% endif %} + + {% endfor %} + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100755 index 0000000..65089c0 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,227 @@ + + + + + + + + + What To Cook? – Essens-Zufallsgenerator + + + + + + +
+ + +
+
+ 🍽️ +
+

What To Cook?

+

Zufallsgenerator für Essen

+
+
+ + +
+ + +
+ + + + +
+ + +
+
+ 💡 +
+ Bis zu 5 Gerichte konfigurieren: + Wähle Kategorie & Aufwand pro Gericht. Nicht benötigte Gerichte einfach frei lassen. +
+
+
+ 2 von 5 aktiv +
+
+ + +
+ +
+ + +
+ +
+ + + + +
+ + + + + + +
+ + + + + + + + + + + + +
+ +
+ + + + diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..d04015b --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,187 @@ + + + + + + Anmeldung — WTC Kochbuch + + + + + + + +
+ +

WTC Kochbuch

+ + + {% if error %} + + {% endif %} + + + + +
+ + diff --git a/app/templates/recipe_detail.html b/app/templates/recipe_detail.html new file mode 100755 index 0000000..9fa1698 --- /dev/null +++ b/app/templates/recipe_detail.html @@ -0,0 +1,230 @@ + + + + + + {{ recipe.title }} – Mein Kochbuch + + + + + + +
+ + 📖 +

Kochbuch

+
+ + + + + +
+ + +
+
+ +
+ + +
+ + +
+

{{ recipe.title }}

+ +
+ {% if recipe.effort %} +
+ ⏱️ + Aufwand: + {% if recipe.effort == 'gering' %}Gering 🟢{% elif recipe.effort == 'hoch' %}Hoch 🔴{% else %}Mittel 🟡{% endif %} + +
+ {% endif %} + + {% if recipe.prep_time %} +
+ 🔪 + Zubereitung: {{ recipe.prep_time }} +
+ {% endif %} + + {% if recipe.cook_time %} +
+ 🍳 + Kochen/Backen: {{ recipe.cook_time }} +
+ {% endif %} + + {% if recipe.portions %} +
+ 👥 + {{ recipe.portions }} Portionen +
+ {% endif %} + + {% if recipe.source and recipe.source.strip() %} +
+ 📖 + Quelle: {{ recipe.source.strip() }} +
+ {% endif %} +
+
+ + +
+ + + + + +
+
+

Zubereitung

+
+ {% for step in recipe.instructions.split('\n') %} + {% if step.strip() %} +
+ {{ loop.index }} +
{{ step.strip() }}
+
+ {% endif %} + {% endfor %} +
+
+ + {% if recipe.notes and recipe.notes.strip() %} +
+
💡 Tipp & Notiz
+
{{ recipe.notes.strip() }}
+
+ {% else %} + +
+ {% endif %} + + + + + +
+ +
+ + + + diff --git a/app/templates/recipe_form.html b/app/templates/recipe_form.html new file mode 100755 index 0000000..cc35160 --- /dev/null +++ b/app/templates/recipe_form.html @@ -0,0 +1,357 @@ + + + + + + {% if recipe %}Rezept bearbeiten{% else %}Neues Rezept eintragen{% endif %} – Mein Kochbuch + + + + + + +
+ + 📖 +

Kochbuch

+
+ + + + + +
+ +
+ +
+

+ {% if recipe %}✏️ Rezept bearbeiten{% else %}➕ Neues Rezept eintragen{% endif %} +

+

+ Trage dein Lieblingsrezept ein – mit Foto, Zutaten und Zubereitungsschritten. +

+ +
+ {% if recipe %} + + {% endif %} + + +
+ + +
+ + +
+ + +
+ + +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ + +
+ +
+ + +
+ +
+ 📷 +

+ Hier klicken, um ein Foto auszuwählen oder mit dem Smartphone aufzunehmen +

+ (JPG, PNG, WebP) + +
+ +
+ Vorschau +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + {% if not recipe %} +
+ +
+ {% endif %} + + + + + +
+ + Abbrechen + + +
+ +
+
+ +
+ + + + + diff --git a/check_db.py b/check_db.py new file mode 100755 index 0000000..ef4cbbf --- /dev/null +++ b/check_db.py @@ -0,0 +1,24 @@ +import sqlite3 +import os + +db_path = "/home/maxi/Projekt/wtc.waiswuaschd.de/data/wtc.db" +conn = sqlite3.connect(db_path) +c = conn.cursor() + +tables = c.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall() +print("Tables:", tables) + +for t in tables: + tname = t[0] + cnt = c.execute(f"SELECT COUNT(*) FROM {tname}").fetchone()[0] + print(f"Table {tname}: {cnt} rows") + +rows = c.execute("SELECT id, name, category, effort FROM dishes").fetchall() +print(f"Dishes ({len(rows)}):") +for r in rows[:5]: + print(" ", r) + +r_rows = c.execute("SELECT * FROM recipes").fetchall() +print(f"Recipes ({len(r_rows)}):") +for r in r_rows: + print(" ", r) diff --git a/crop_chapters.py b/crop_chapters.py new file mode 100644 index 0000000..4eb36ce --- /dev/null +++ b/crop_chapters.py @@ -0,0 +1,97 @@ +import os +from PIL import Image + +src_path = "/home/maxi/Projekt/wtc.waiswuaschd.de/app/static/img/chapters/source_chapters.jpg" +out_dir = "/home/maxi/Projekt/wtc.waiswuaschd.de/app/static/img/chapters" +im = Image.open(src_path).convert("RGBA") +w, h = im.size + +def make_transparent(sub_img): + sub_img = sub_img.convert("RGBA") + data = sub_img.getdata() + new_data = [] + for item in data: + r, g, b, a = item + lum = 0.299 * r + 0.587 * g + 0.114 * b + if lum >= 248: + new_data.append((r, g, b, 0)) + elif lum <= 200: + new_data.append((r, g, b, 255)) + else: + alpha = int(255 * (248 - lum) / (248 - 200)) + new_data.append((r, g, b, alpha)) + + res = Image.new("RGBA", sub_img.size) + res.putdata(new_data) + + bbox = res.getbbox() + if bbox: + px1 = max(0, bbox[0] - 8) + py1 = max(0, bbox[1] - 8) + px2 = min(res.width, bbox[2] + 8) + py2 = min(res.height, bbox[3] + 8) + res = res.crop((px1, py1, px2, py2)) + return res + +chapters_data = [] + +# 2. Traditionell & Basics +sub2 = im.crop((25, 80, 308, 262)) +for y in range(sub2.height): + for x in range(sub2.width): + actual_x = 25 + x + actual_y = 80 + y + if actual_x > 268 and actual_y > 165: + sub2.putpixel((x, y), (255, 255, 255, 255)) +chapters_data.append((2, "traditionell_basics", sub2)) + +# 3. Pasta & Nudelgerichte +sub3 = im.crop((270, 20, 518, 270)) +for y in range(sub3.height): + for x in range(sub3.width): + actual_x = 270 + x + actual_y = 20 + y + if actual_x < 308 and actual_y < 165: + sub3.putpixel((x, y), (255, 255, 255, 255)) +chapters_data.append((3, "pasta_nudelgerichte", sub3)) + +# 4. Kartoffeln +sub4 = im.crop((515, 20, 745, 270)) +chapters_data.append((4, "kartoffeln", sub4)) + +# 5. Reis & Getreide +sub5 = im.crop((40, 315, 270, 505)) +chapters_data.append((5, "reis_getreide", sub5)) + +# 6. Teig & Gebackenes +sub6 = im.crop((270, 330, 520, 536)) +chapters_data.append((6, "teig_gebackenes", sub6)) + +# 7. Asiatische Küche: bowl and chopsticks within [45, 580, 292, 725] +sub7 = im.crop((45, 580, 292, 725)) +chapters_data.append((7, "asiatische_kueche", sub7)) + +# 8. Süßspeisen: starts at y=568 (below label 6) +sub8 = im.crop((460, 568, 750, 725)) +chapters_data.append((8, "suessspeisen", sub8)) + +# 9. Desserts +sub9 = im.crop((30, 765, 258, 938)) +chapters_data.append((9, "desserts", sub9)) + +# 10. Kuchen & Gebäck +sub10 = im.crop((265, 745, 510, 938)) +chapters_data.append((10, "kuchen_gebaeck", sub10)) + +# 11. Plätzchen: starts at y=774 (below label 8 descender!) +sub11 = im.crop((515, 774, 745, 938)) +chapters_data.append((11, "plaetzchen", sub11)) + +for num, name, img in chapters_data: + final_img = make_transparent(img) + filename = f"chapter_{num}.png" + filepath = os.path.join(out_dir, filename) + final_img.save(filepath, "PNG") + print(f"Saved Chapter {num:2d} ({name}) -> {filename} ({final_img.width}x{final_img.height})") + +print("All 10 chapters extracted with 100% purity!") diff --git a/data/extracted_texts/American Pancakes.txt b/data/extracted_texts/American Pancakes.txt new file mode 100644 index 0000000..f87b593 --- /dev/null +++ b/data/extracted_texts/American Pancakes.txt @@ -0,0 +1,33 @@ + +--- Page 1 --- +Rezept online aufrufen +Zutaten für 4 Portionen: +150 gMehl +1 ELZucker +1 TLBackpulver +2Ei(er) +1 Prise(n)Salz +150 mlMilch +3 ELÖl +n. B.Ahornsirup +200 gBeeren, (TK) +Rezept von: hannah_kocht_einfach +American Pancakes +Ein amerikanischer Frühstückstraum +Mehl, Zucker, Backpulver, Eier, Salz und Milch in einer Schüssel +vermengen. +Jetzt ein wenig Öl in einer Pfanne erhitzen. Dann den +Pfannkuchenteig portionsweise (etwa 2 - 3 Esslö�el reichen) +hineingeben und zu einem großen, drei Zentimeter dicken Kreis +formen. +Bei mittlerer Hitzestufe wird der Teig nun so lange gebraten, bis die +untere Seite golden und braun ist. +Anschließend ein Mal wenden, bis er goldbraun gebraten ist. +Ahornsirup darüber gießen und mit Tiefkühlbeeren garnieren. +Warm genießen. +Arbeitszeitca. 10 Minuten +Koch-/Backzeitca. 15 Minuten +Gesamtzeitca. 25 Minuten +Schwierigkeitsgradsimpel +Chefkoch Rezept: American Pancakes von hannah_kocht_einfach about:blank +1 von 1 30.05.2024, 16:00 \ No newline at end of file diff --git a/data/extracted_texts/Ananaskuchen.txt b/data/extracted_texts/Ananaskuchen.txt new file mode 100644 index 0000000..080f031 --- /dev/null +++ b/data/extracted_texts/Ananaskuchen.txt @@ -0,0 +1,269 @@ + +--- Page 1 --- +Einfach Backen / Brigitte Sporrer +ZUTATEN +(FÜR 1 KUCHEN) +1 + +Dose +Ananas + +in Scheiben +(Abtropfgewicht ca. 350 g) +220 + +g +weiche + +Butter +30 + +g +brauner + +Zucker +130 + +g +Zucker +1 + +Prise +Salz +3 +Eier + +(Gr. M) +200 + +g +Weizenmehl + +(Type 405, alternativ +Dinkelmehl Type 630) +2 + +TL +Backpulver +nach Belieben + +Cocktailkirsche + +oder +Maraschinokirschen zum +Dekorieren +Backen +: + +40 Min. +Dauer +: + +25 Min. +Niveau +: + +Einfach +Zubereitung +1 +. +SCHRITT +1 +1 + + +Dose +Dose + + +Ananas +Ananas + + +in Scheiben (Abtropfgewicht ca. 350 g) +in Scheiben (Abtropfgewicht ca. 350 g) +Den Backofen auf + 180 Grad Ober-/Unterhitze (Umluft: 160 + 180 Grad Ober-/Unterhitze (Umluft: 160 +Grad) +Grad) +vorheizen. + + +Die + + +Ananasscheiben auf einem Sieb +abtropfen lassen. +Ein Stück Backpapier in die +Springform (Ø +Springform (Ø +26 cm) +26 cm) + einspannen. +2 +. +SCHRITT +40 +40 + + +g +g + + +weiche +weiche + + +Butter +Butter + + +, +, +30 +30 + + +g +g + + +brauner +brauner + + +Zucker +Zucker +40 g der weichen Butter mit einem Pinsel auf dem Boden der +Form verteilen. Den braunen Zucker gleichmäßig darauf +streuen. +3 +. +SCHRITT +Acht der Ananasscheiben dicht an dicht auf den +Springformboden legen. Die übrigen Ananasscheiben (ca. 3-4 +Stück) sehr fein hacken oder pürieren. +4 +. +SCHRITT +180 +180 + + +g +g + + +weiche +weiche + + +Butter +Butter + + +, +, +130 +130 + + +g +g + + +Zucker +Zucker + + +, +, +1 +1 + + +Prise +Prise + + +Salz +Salz + + +, +, +3 +3 + + +Eier +Eier + + +(Gr. M) +(Gr. M) + + +, +, +200 +200 + + +g +g +Weizenmehl +Weizenmehl + + +(Type 405, alternativ Dinkelmehl Type 630) +(Type 405, alternativ Dinkelmehl Type 630) + + +, +, +2 +2 + + +TL +TL + + +Backpulver +Backpulver +Für den Teig +Für den Teig + die übrige weiche Butter mit Zucker und Salz in +einer Rührschüssel schaumig schlagen. Eier nach und nach +zugeben und auf höchster Stufe weiterrühren. Das Mehl mit +Backpulver mischen und abwechselnd mit dem Ananaspüree +unter den Teig rühren. +5 +. +SCHRITT +super saftig +Ananaskuchen +Noch mehr leckere Rezepte: https://www.einfachbacken.de +--- Page 2 --- +Nach Belieben die Löcher der Ananasscheiben mit je einer +Kirsche ausfüllen. Teig darauf geben und glatt streichen. Im +unteren Drittel des vorgeheizten Backofens +ca. 40 Minuten +ca. 40 Minuten +goldgelb backen. Am besten ein Stück Backpapier auf den +Kuchenrost legen, damit der eventuell heraustropfende Saft +bzw. Karamell nicht am Ofenboden einbrennt. +6 +. +SCHRITT +Den fertigen Kuchen +ca. 5 Minuten +ca. 5 Minuten +auf einem Kuchenrost +auskühlen lassen. Dann direkt auf die gewünschte Tortenplatte +stürzen und das mitgebackene Backpapier entfernen. Der +Kuchen ergibt ca. 12 Stücke. +Noch mehr leckere Rezepte: https://www.einfachbacken.de \ No newline at end of file diff --git a/data/extracted_texts/Apfel Erdnuss Tarte.txt b/data/extracted_texts/Apfel Erdnuss Tarte.txt new file mode 100644 index 0000000..e64c232 --- /dev/null +++ b/data/extracted_texts/Apfel Erdnuss Tarte.txt @@ -0,0 +1,27 @@ +Apfel-Salzkaramell-Tarte mit Erdnüssen (krimi&Keks) +Für eine rechteckige Tarteform +Zutaten +Zuerst den Backofen auf 170 Grad Umluft vorheizen und die Tarteform einfetten. +Für den Mürbeteig alle Zutaten mit den Knethaken des Mixers oder der Küchenmaschine (oder den Händen) zu einem glatten Teig verkneten. Den Teig auf dem Boden der Tarteform ausrollen und am Rand hochziehen. Jetzt ein Blatt Backpapier auf den Teig legen und die Hülsenfrüchte auf dem Papier verteilen. Das simuliert eine Füllung beim Vorbacken und verhindert, dass der Teig am Boden Blasen wirft und am Rand herunterfließen kann. Den Teig für 12 Minuten auf der mittleren Schiene backen. +Inzwischen das Salzkaramell kochen. Dazu Zucker, Butter und Sahne in einem kleinen Topf aufkochen und so lange einköcheln lassen, bis sich eine hellbraune, dickflüssige Soße gebildet hat. Das dauert ungefähr 15 Minuten. Zwischendurch unbedingt umrühren, damit das Karamell nicht anbrennt. Ist die gewünschte Konsistenz erreicht, Meersalz nach Lust und Laune dazugeben und kurz verrühren. +Jetzt sollte auch der Mürbeteig vorgebacken sein und aus dem Ofen kommen. Das Backpapier mit den Hülsenfrüchten wieder entfernen. Achtung: Die Hülsenfrüchte (ich nehme getrocknete weiße Bohnen) werden extrem heiß, also verbrennt Euch nicht die Finger! +Die Äpfel vierteln, entkernen und in dünne Scheiben schneiden. +Die Salzkaramell-Soße auf den Mürbeteig streichen, sodass der Teigboden gut bedeckt ist. Auf das Karamell einige Erdnüsse streuen und dann die Apfelscheiben wie Dachziegel auf dem Teig verteilen. Wer mag, kann noch ein wenig Zimt über die Apfelspalten streuen. +Die Apfeltarte backt für 20 Minuten auf der mittleren Schiene im Ofen. Dann herausziehen und mit einem Backpinsel noch etwas von dem Salzkaramell auf die Apfelspalten pinseln. Noch ein paar Erdnüsse über die Äpfel streuen und die Tarte für weitere 10 Minuten in den Ofen geben. Nach Ende der Backzeit die Apfeltarte aus dem Backofen nehmen und auf einem Gitter abkühlen lassen. Es bleibt ein wenig von der Salzkaramell-Soße übrig – ungefäh so viel, wie in dem kleinen Weckglas auf den Fotos zu sehen ist. Die restliche Soße könnt Ihr entweder zur Apfeltarte servieren oder anderweitig naschen. Herr krimiundkeks hat einen Löffel davon in Naturjoghurt gerührt und war sehr begeistert… +Die Apfeltarte lauwarm oder komplett abgekühlt servieren. + +--- Table --- +Mürbeteig: +200g Mehl +100g kalte Butter in kleinen Würfeln +50g Zucker +1 EL kaltes Wasser +Zimt +Salzkaramell: +125g Zucker +200ml Sahne +2 EL Butter +Meersalz nach Geschmack +Belag: +2 mittelgroße, säuerliche Äpfel +etwa 2 EL geröstete und gesalzene Erdnüsse diff --git a/data/extracted_texts/Bao Buns mit Champignons.txt b/data/extracted_texts/Bao Buns mit Champignons.txt new file mode 100644 index 0000000..3a53db5 --- /dev/null +++ b/data/extracted_texts/Bao Buns mit Champignons.txt @@ -0,0 +1,46 @@ + +--- Slide 1 --- +Bao Buns mit Champignons +Mit Karotten, Gurken und Edamame +Zutaten 2 Personen +Fertige Bao Buns (TK) +Edamame in Schale +Gurke +Röstzwiebeln +Mayonnaise +Tonkatsu Soße +Für die Karotten: +2 Karotten +50ml Wasser +50ml Branntweinessig +½ TL Salz +1 TL Zucker +Für die Champignons: +400g Champignons +4 EL Sojasauce +1 EL Reisessig +1 EL Agavendicksaft +1 Knoblauchzehe +Chilipulver +Karotten schälen und in feine Stifte hobeln. Zutaten der Marinade (50ml Wasser, 50ml Essig, ½ TL Salz, 1 TL Zucker) in einen Topf geben und aufkochen. Karotten dazugeben und für 30 Sekunden köcheln lassen. Herd ausschalten und Karotten weiter marinieren lassen. +Champignons in Scheiben schneiden. +Zutaten der Marinade (4EL Sojasauce, 1 EL Reisessig, 1 EL Agavendicksaft, 1 Knoblauchzehe, Chilipulver) vermischen und über die Champignons geben. +Gurken in feine Stifte hobeln. +2 Töpfe mit Wasser für Edamame und Bao Buns aufsetzen. +Eadmame in das kochende Wasser geben. +Bao Buns mit dem Dampfeinsatz in den Topf geben, mit Deckel dämpfen lassen. +Champignons mit Marinade heiß braten. + Dauer: gute 5 Minuten +Bao Buns herausnehmen und mit +Champignons +Gurke +Karotte +Röstzwiebeln +Mayonaise +Tonkatsu Soße füllen +Edamame abgießen und salzen. +1 +2 +3 +4 +5 diff --git a/data/extracted_texts/Blauchbeer.txt b/data/extracted_texts/Blauchbeer.txt new file mode 100644 index 0000000..84a54ca --- /dev/null +++ b/data/extracted_texts/Blauchbeer.txt @@ -0,0 +1,19 @@ +Blauchbeer-Ricotta-Tarte (zimtkeksundapfeltarte) +Für den Teig Mehl, Puderzucker und kalte Butter mit den Mixer zu eine krümeligen Teig verrühren, dann Ei und 2 EL kaltes Wasser einarbeiten und mit den Händen schnell zu einem glatten Teig verkneten. Für 30 Minuten in Folie gewickelt in den Kühlschrank legen. +Backofen auf 180 °C (160 °C Umluft) vorheizen. Den Mürbteig nach der Ruhezeit ausrollen und in die gefettete Form geben. Mit einer Gabel mehrfach einstechen, dann für 15 Minuten vorbacken. +Inzwischen die Eier, den Zucker und Vanillemark schaumig schlagen. Ricotta zufügen, weiterrühren, zuletzt Zitronensaft und -schale unterschlagen. Die Masse auf den vorgebackenen Mürbteig streichen und für weitere 45 Minuten backen. In der Form auskühlen lassen, dann mit den Beeren belegen. Mit etwas Puderzucker bestäubt servieren. + +--- Table --- +Für den Mürbteig +220 g Mehl +100 g kalte Butter +50 g Puderzucker +1 Ei +2 EL kaltes Wasser +Belag +500 g Ricotta +2 Eier +Mark einer Vanilleschote +Saft- und Schale einer Bio-Zitrone +150 g Zucker +Ca. 300 g Heidelbeeren (oder andere Beeren Eurer Wahl) diff --git a/data/extracted_texts/Blätterteigpäckchen mit Spargel.txt b/data/extracted_texts/Blätterteigpäckchen mit Spargel.txt new file mode 100644 index 0000000..51f1951 --- /dev/null +++ b/data/extracted_texts/Blätterteigpäckchen mit Spargel.txt @@ -0,0 +1 @@ +[.jpg file: Blätterteigpäckchen mit Spargel.jpg] \ No newline at end of file diff --git a/data/extracted_texts/Bounty®-Kuchen Rezept _ Dr. Oetker.txt b/data/extracted_texts/Bounty®-Kuchen Rezept _ Dr. Oetker.txt new file mode 100644 index 0000000..8698f0a --- /dev/null +++ b/data/extracted_texts/Bounty®-Kuchen Rezept _ Dr. Oetker.txt @@ -0,0 +1,16 @@ + +--- Page 1 --- +Bounty®-Kuchen Rezept | Dr. Oetker https://www.oetker.de/rezepte/r/bounty-kuchen +1 von 4 24.08.2024, 12:25 + +--- Page 2 --- +Bounty®-Kuchen Rezept | Dr. Oetker https://www.oetker.de/rezepte/r/bounty-kuchen +2 von 4 24.08.2024, 12:25 + +--- Page 3 --- +Bounty®-Kuchen Rezept | Dr. Oetker https://www.oetker.de/rezepte/r/bounty-kuchen +3 von 4 24.08.2024, 12:25 + +--- Page 4 --- +Bounty®-Kuchen Rezept | Dr. Oetker https://www.oetker.de/rezepte/r/bounty-kuchen +4 von 4 24.08.2024, 12:25 diff --git a/data/extracted_texts/Brezen Cracker.txt b/data/extracted_texts/Brezen Cracker.txt new file mode 100644 index 0000000..b82bdd9 --- /dev/null +++ b/data/extracted_texts/Brezen Cracker.txt @@ -0,0 +1,16 @@ +Honig-Senf-Brezen-Cracker (toertchenmadeinberling) +Die Brezeln in kleine Stücke schneiden. +Senf, Honig, Öl, Essig, Salz, Paprika und Pfeffer in eine Schüssel geben und mit einem Schneebesen verrühren. +Die Soße über die Brezelstücke geben, gut verrühren und die Brezelstücke auf einem mit Backpapier belegten Blech verteilen. +Im vorgeheiztem Ofen bei 160° C Ober-/ Unterhitze im unteren Drittel für ca. 30 Minuten backen. Danach die Brezelstücke durchwenden, den Ofen auf 100° C runterdrehen und weitere 30 Minuten backen. +Die Brezelstücke aus dem Ofen nehmen und abkühlen lassen. Die Stücke werden erst hart, wenn sie abgekühlt sind. + +--- Table --- +2 „alte“ Brezeln +50 g Dijonsenf +25 g Honig +25 ml Sonnenblumenöl +15 ml Apfelessig +1/2 Tl Salz +1/2 Tl Paprikapulver +1/8 Tl Pfeffer diff --git a/data/extracted_texts/Champignons Weihnachtsmarkt.txt b/data/extracted_texts/Champignons Weihnachtsmarkt.txt new file mode 100644 index 0000000..66b4440 --- /dev/null +++ b/data/extracted_texts/Champignons Weihnachtsmarkt.txt @@ -0,0 +1,18 @@ + +--- Page 1 --- +2 10 Minuten 10 Minuten +Ernährung: 26 Kalorien, 0,3 fett +Champignons Weihnachtsmarkt mit Knoblauchsauce + +--- Page 2 --- +ZUTATEN +ANLEITUNG +Zutaten: +Firefox https://ayseskochblog.de/champignons-weihnachtsmarkt-mit-knoblau... +2 von 3 08.11.2024, 14:29 + +--- Page 3 --- +TIPP: +Tipp: du kannst die Knoblauchsauce auch mit ein wenig Tabasco verfeinern. +Firefox https://ayseskochblog.de/champignons-weihnachtsmarkt-mit-knoblau... +3 von 3 08.11.2024, 14:29 \ No newline at end of file diff --git a/data/extracted_texts/Chili sin Carne.txt b/data/extracted_texts/Chili sin Carne.txt new file mode 100644 index 0000000..c5dcd8e --- /dev/null +++ b/data/extracted_texts/Chili sin Carne.txt @@ -0,0 +1,28 @@ +Chili sin Carne (daily vegan) +8 Portionen +Etwas Öl in einer Pfanne erhitzen und das trockene Sojagranulat scharf darin abraten. +Parallel dazu in einem großen Topf etwas Öl erhitzen und die Zwiebeln darin glasig braten. Den Knoblauch und das angebratene Sojagranulat dazu geben. Mit dem Salz, Pfeffer, Koriander und Kreuzkümmel würzen und die Chilis dazu geben. Alles zusammen unter Rühren kurz anbraten. +Pizzatomaten, Tomatenmark und 1 l Brühe einrühren und ca. 15 Minuten köcheln, dabei ab und zu umrühren. +Anschließend auch Mais, Paprika, Kidney-Bohnen, Oregano und Schokolade dazu geben und weitere 15 Minuten köcheln. +Tipps: +Mein veganes Chili con Carne lässt sich sehr gut zu Beginn einer Party oder in den nächsten Tag wieder aufwärmen. Dadurch ist es auch bestens geeignet für Meal Prep. +Besonders gut schmeckt veganes Chili Sin Carne mit Brot oder Reis. +Ihr könnt das Chili übrigens auch (portioniert) einfrieren und bei Bedarf wieder auftauen. + +--- Table --- +200 g Sojagranulat +4 Zwiebeln, gewürfelt +3 Knoblauch-Zehen, gepresst +2-4 scharfe Chilischoten, sehr fein gehackt (Menge je nach Geschmack) +800 ml Pizza-Tomaten +1 l Brühe +2 Gläser oder Dosen Mais, abgetropft +2 Gläser oder Dosen Kidneybohnen +7 EL Tomatenmark +2 grüne Paprika, gewürfelt1 Tafel +vegane Zartbitter-Schokolade, in Stücken +2 EL Kreuzkümmmel (Cumin) +2 EL Oregano +1 TL Koriander, gemahlen +2 TL Salz +2 TL Pfeffer diff --git a/data/extracted_texts/Crema catalana von Aurora.txt b/data/extracted_texts/Crema catalana von Aurora.txt new file mode 100644 index 0000000..984fa45 --- /dev/null +++ b/data/extracted_texts/Crema catalana von Aurora.txt @@ -0,0 +1,50 @@ + +--- Page 1 --- +Rezept online aufrufen +Zutaten für 6 Portionen: +Für die Creme: +300 mlMilch +200 mlSahne, sü +2 Stück(e)Orange(n), Schale, unbehandelt +1 Stange/nZimt +½Vanilleschote(n), aufgeschlitzt +5Ei(er), davon das Eigelb +50 gZucker +Muskat, gerieben +6 ELZucker +Rezept von: Aurora +Crema catalana +Den Backofen auf 150°C vorheizen. Ein tiefes Backblech mit +Küchenkrepp auslegen, 2 cm hoch mit heißem Wasser füllen und in +die mittlere Schiene des Backofens schieben. +Milch mit süßer Sahne, der unbehandelten Orangenschale, +Zimtstange und der aufgeschlitzten Vanillestange in einen Topf +geben und zum Kochen bringen. Einmal aufkochen, den Topf vom +Herd nehmen und Orangenschale, Vanillestange und Zimtstange +herausnehmen. Etwas abkühlen lassen. +Eigelb mit Zucker mit dem Handrührgerät schaumig schlagen. +Anschließend langsam die Milch-Sahne-Mischung darunter rühren +und durch ein feines Sieb gießen. Die Eier-Milch-Masse vorsichtig in +kleine ofenfeste Förmchen geben, dabei darauf achten, dass die +Oberfläche schaumfrei bleibt. Eventuell den Schaum mit einem +Lö�el abnehmen. Die Förmchen so in das Wasserbad stellen, dass +sie zur Häl�e im Wasser stehen. Das ganze etwa 1 Stunde im +Backofen stocken lassen. Die Förmchen aus dem Ofen nehmen, +abkühlen lassen. +Vor dem Servieren: Den Grill des Backofens vorheizen oder den +Backofen auf größte Leistung stellen. Die Creme mit einem Hauch +geriebenem Muskat und pro Förmchen 1 EL Zucker bestreuen. Den +Zucker unter dem Grill goldbraun karamellisieren lassen. Das sollte +so schnell wie möglich geschehen, da die Creme selbst nicht heiß +werden soll. +Tipp: +Einfacher erhält man die Zuckerkruste mit einer Lötlampe. Einfach +mit der Flamme der Lötlampe über den Zucker streichen. Fertig. +Bei der "originalen" Crema catalana wird die Zuckerkruste mit +einem speziellen glühendem spiralförmigen Eisen gebrannt. +Arbeitszeitca. 45 Minuten +Gesamtzeitca. 45 Minuten +Schwierigkeitsgradnormal +Kcal p. P .ca. 285 +Chefkoch Rezept: Crema catalana von Aurora https://www.chefkoch.de/rezepte/drucken/79651030530977/Crema-cata... +1 von 1 30.05.2024, 16:07 \ No newline at end of file diff --git a/data/extracted_texts/Cremige Zucchini-Bohnenpfanne mit Erdnussbutter - Eat this!.txt b/data/extracted_texts/Cremige Zucchini-Bohnenpfanne mit Erdnussbutter - Eat this!.txt new file mode 100644 index 0000000..be1bee0 --- /dev/null +++ b/data/extracted_texts/Cremige Zucchini-Bohnenpfanne mit Erdnussbutter - Eat this!.txt @@ -0,0 +1,11 @@ + +--- Page 1 --- +Cremige Zucchini-Bohnenpfanne mit +Erdnussbutter +Ja, Resteverwertung kann auch Spaß machen! Diese schnelle und leckere, +cremige Zucchini-Bohnenpfanne haben wir aus Kühlschrankresten und +geretteten Lebensmitteln zubereitet. +10 Min. 15 Min. 25 Min. +Hauptgericht Afrikanisch 2 Portionen Nadine +Zutaten +Anleitungen diff --git a/data/extracted_texts/Currytaschen.txt b/data/extracted_texts/Currytaschen.txt new file mode 100644 index 0000000..5864c67 --- /dev/null +++ b/data/extracted_texts/Currytaschen.txt @@ -0,0 +1,23 @@ +Currytaschen (zuckerjagswurst) +30 Minuten | 8 Currytaschen +Zutaten +0,5 Zwiebeln +75g Lauch +75g Möhren +75g veganes Hack +1,5TL Thailändisches rotes Curry +100ml Sahne +0,5TL Gemüsebrühe +1,5EL Sojasauce +1 Blätterteig +Zubereitung +Ofen auf 180°C Umluft vorheizen. +Zwiebeln würfeln & Lauch in feine Ringe schneiden. +Möhren raspeln. +Zwiebeln anbraten. Hack dazugeben und 3-4 Minuten braten. +Lauch, Möhren und Curry dazugeben, weitere 3-4 Minuten braten. +¾ der Sahne dazugeben, mit Gemüsebrühe und Sojasauce würzen. +Pfanne vom Herd nehmen. +Blätterteig in 8 Quadrate schneiden. Füllung auf den Teig geben und Taschen verschließen. +Oberseite mit Sahne bestreichen. +10-12 Minuten backen. diff --git a/data/extracted_texts/Donauwelle - super easy von alina1st.txt b/data/extracted_texts/Donauwelle - super easy von alina1st.txt new file mode 100644 index 0000000..49638b6 --- /dev/null +++ b/data/extracted_texts/Donauwelle - super easy von alina1st.txt @@ -0,0 +1,74 @@ + +--- Page 1 --- +Rezept online aufrufen +Zutaten für 1 Portionen: +Für den Teig: +3Ei(er) +175 gZucker +175 mlÖl +175 mlMilch +350 gMehl +3 TLBackpulver +1 ELKakaopulver +1 ELMilch +1 GlasSauerkirschen, abgetrop� +Fett für das Blech +Für die Creme: +1 Pck.Vanillepuddingpulver +400 mlMilch +2 ELZucker +250 gButter +Für die Glasur: +200 gZartbitterschokolade +2 ELNutella +1 ELÖl +Rezept von: alina1st +Donauwelle - super easy +hier kann eigentlich gar nichts mehr schief +gehen... +Für ein tiefes Backblech von ca. 30 x 40 cm. Geringfügig kleiner ist +auch möglich. +Zuerst die Butter aus dem Kühlschrank nehmen und in Stücken in +eine Schüssel geben, damit sie schön weich werden kann. +Dann das Puddingpulver mit 50 ml Milch und dem Zucker glatt +rühren, den Rest der Milch aufkochen. Das angerührte +Puddingpulver einrühren, nochmals aufkochen lassen und sofort in +eine Schüssel umgießen. Frischhaltefolie direkt auf den warmen +Pudding legen, so dass möglichst keine Lu�blasen zwischen +Pudding und Folie entstehen. Den Pudding nun bei +Raumtemperatur auskühlen lassen, bitte nicht kalt stellen! +Ein tiefes Blech (ich nutze ein 30 x 40 cm Blech, auf das ich einen +Backrahmen stelle) dünn einfetten und mit Backpapier belegen, so +dass das Papier gut anliegt. Den Ofen auf 180 °C Ober-/Unterhitze +vorheizen. +Die Eier mit dem Zucker dick-cremig aufschlagen und langsam das +Öl zugießen. Anschließend Mehl und Backpulver mischen und +abwechselnd mit der Milch unter die Eiercreme rühren. Die Häl�e +des Teiges auf das Blech streichen. Den übrigen Teig mit Kakao und +Milch verrühren und vorsichtig darüber streichen. Die Kirschen +darauf verteilen. +Den Kuchen im vorgeheizten Ofen ca. 25 - 30 min. backen. +Anschließend das Backpapier vom Blech auf einen Rost ziehen und +auskühlen lassen. +Die inzwischen zimmerwarm gewordene Butter cremig rühren. Die +Folie vom ebenfalls zimmerwarm gewordenen Pudding abnehmen +und nun lö�elweise den Pudding unter die Butter rühren. Falls der +Kuchenboden noch warm ist, die Creme evtl. so lange kalt stellen. +Danach die Creme gleichmäßig auf den Kuchen streichen. Wieder +kalt stellen. +Die Zartbitterschokolade im Wasserbad unter Rühren schmelzen +lassen, dann Nutella und Öl darunter rühren, so dass ein recht +flüssiger Schokoladenguss entsteht. Den Guss gleichmäßig auf +dem Kuchen verteilen und mit einer Teigkarte oder einem Messer +Chefkoch Rezept: Donauwelle - super easy von alina1st https://www.chefkoch.de/rezepte/drucken/563541154614076/Donauwel... +1 von 2 30.05.2024, 16:34 +--- Page 2 --- +auf der Creme glattstreichen. +Am besten im Kühlschrank fest werden lassen. +Arbeitszeitca. 1 Stunde +Koch-/Backzeitca. 35 Minuten +Gesamtzeitca. 1 Stunde 35 Minuten +Schwierigkeitsgradsimpel +Kcal p. P .ca. 7914 +Chefkoch Rezept: Donauwelle - super easy von alina1st https://www.chefkoch.de/rezepte/drucken/563541154614076/Donauwel... +2 von 2 30.05.2024, 16:34 \ No newline at end of file diff --git a/data/extracted_texts/Focaccia.txt b/data/extracted_texts/Focaccia.txt new file mode 100644 index 0000000..3eca318 --- /dev/null +++ b/data/extracted_texts/Focaccia.txt @@ -0,0 +1,33 @@ + +--- Page 1 --- +Rezept online aufrufen +Zutaten für 8 Portionen: +500 gMehl +20 gHefe, frische +375 mlWasser +1 TLZucker +1 TLSalz +5 ELOlivenöl +Mehl, für die Arbeitsfläche und +Bleche +Rosmarin oder Thymian +Focaccia +Mehl in eine große Schüssel sieben, in die Mitte eine Mulde +drücken. Hefe und Zucker in 3/8 l lauwarmem Wasser auflösen und +in die Mulde gießen. Zugedeckt 30 Minuten gehen lassen. +3 EL Olivenöl und Salz unterrühren und den Teig gut durchkneten +(dauert ca. 10 Minuten). Zugedeckt nochmals gehen lassen, ca. 1 +Stunde. Backofen auf 220° C vorheizen. Den Teig halbieren und zu +dicken Fladen ausrollen, diese auf die bemehlten Bleche legen. Mit +den Fingern Dellen in die Oberfläche drücken. Ein weiteres Mal +zudecken und eine weitere halbe Stunde gehen lassen. Mit dem +restlichen Öl bestreichen und dann nach Wahl mit Rosmarinnadeln +oder getrocknetem Thymian bestreuen. Auf der mittleren Schiene +ca. 15 - 20 Minuten backen (Umlu� 200! C). +Arbeitszeitca. 30 Minuten +Ruhezeitca. 2 Stunden +Gesamtzeitca. 2 Stunden 30 Minuten +Schwierigkeitsgradnormal +Kcal p. P .ca. 273 +Chefkoch Rezept: Focaccia https://www.chefkoch.de/rezepte/drucken/405741129901058/Focaccia.html +1 von 1 30.05.2024, 16:09 \ No newline at end of file diff --git a/data/extracted_texts/Galaktoboureko - griechischer Grießauflauf.txt b/data/extracted_texts/Galaktoboureko - griechischer Grießauflauf.txt new file mode 100644 index 0000000..7260719 --- /dev/null +++ b/data/extracted_texts/Galaktoboureko - griechischer Grießauflauf.txt @@ -0,0 +1,45 @@ + +--- Page 1 --- +Rezept online aufrufen +Zutaten für 4 Portionen: +Für die Creme: +50 gWeichweizengrieß +1 Pck.Vanillepuddingpulver +700 mlMilch, kalt +4Ei(er) +125 gZucker +2 ELButter, weich +Für den Teig: +500 gBlätterteig +65 gButter +Für den Sirup: : +375 gZucker +250 mlWasser, lauwarm +1 TLZimt +1Zitrone(n), ausgepresst +Galaktoboureko - griechischer +Grießauflauf +Den Sirup sollte man zuerst zubereiten, da er kalt sein muss, bevor +man ihn auf den Auflauf gießt. +Grieß und Vanillepuddingpulver mit etwas kalter Milch anrühren. +Restliche Milch aufkochen lassen, Grieß-/Puddingpulvermischung +unterrühren. +Eier mit Zucker und flüssiger Butter verrühren und unter die +Grießmasse rühren. +Eine rechteckige Auflau�orm mit Butter ausstreichen, die Häl�e +des Blätterteigs hineinlegen, Creme darauf verteilen und mit +restlichem Blätterteig abdecken. Den Blätterteig mit einem Messer +an der Oberfläche rautenförmig einschneiden, mit ein wenig +Wasser bespritzen und bei 180 °C auf der untersten Schiene ca. 30 +Minuten goldgelb backen. +Für den Sirup Feinkristallzucker mit Wasser aufkochen und solange +köcheln lassen, bis sich dieser aufgelöst hat. Dann Zimt und +Zitronensa� dazugeben. Sirup auskühlen lassen. +Heißen Grießauflauf aus dem Backrohr nehmen und sofort mit +abgekühltem Sirup übergießen. +Arbeitszeitca. 30 Minuten +Koch-/Backzeitca. 50 Minuten +Gesamtzeitca. 1 Stunde 20 Minuten +Schwierigkeitsgradnormal +Chefkoch Rezept: Galaktoboureko - griechischer Grießauflauf https://www.chefkoch.de/rezepte/drucken/1048301209719211/Galakto... +1 von 1 30.05.2024, 16:10 \ No newline at end of file diff --git a/data/extracted_texts/Gebratene Kasknödel von felidae177.txt b/data/extracted_texts/Gebratene Kasknödel von felidae177.txt new file mode 100644 index 0000000..849be6b --- /dev/null +++ b/data/extracted_texts/Gebratene Kasknödel von felidae177.txt @@ -0,0 +1,59 @@ + +--- Page 1 --- +Rezept online aufrufen +Zutaten für 4 Portionen: +250 gBrötchen oder Brot, altbacken +300 mlMilch zum Einweichen +2Ei(er) +2 ELMehl +200 gBergkäse oder Gruyere +n. B.Salz und Pfe�er +n. B.Muskat +etwasKümmel, gerieben, optional +1 großeZwiebel(n), in Würfel geschnitten +½ BundPetersilie +3 ELSemmelbrösel +1 ELButter +etwasPflanzenöl +Rezept von: felidae177 +Gebratene Kasknödel +Käseknödel, prima als Beilage für Pilze in +Sahnesoße oder Geschnetzeltes +Das Brot oder Brötchen in kleine Stücke schneiden, in eine große +Schüssel füllen und mit warmer Milch übergießen. Mit den Händen +etwas mischen und ca. 20 - 30 Minuten einweichen. Die +Milchmenge hängt davon ab, wie trocken das Brot ist, 300 bis 400 +ml Milch sollten reichen. Man kann auch nach einiger Zeit noch +etwas angießen, falls es zu wenig ist. +Die Zwiebel schälen und in Stücke schneiden. In Butter andünsten, +nicht bräunen, und auf die geweichte Brotmasse geben. Den Käse +klein würfeln und mit den Eiern ebenfalls zur Masse geben. +Die Petersilie waschen, trocken schütteln und klein hacken. +Zusammen mit Salz, Pfe�er, Muskat und evtl. etwas geriebenem +Kümmel in die Schüssel geben und die Masse nun gut mit den +Händen durcharbeiten, probieren und ggf. nachwürzen. Die +Feuchtigkeit reguliert man mit 2 bis 3 EL Mehl. Der Teig sollte +weich, aber nicht zu flüssig werden. Ggf. kann man ihn +anschließend noch 15 Minuten ruhen lassen. +Die Semmelbrösel in einen tiefen Teller füllen und in einer großen, +breiten Pfanne einen EL Butter und etwas neutrales Pflanzenöl +erhitzen. Mit feuchten Händen - wichtig, sonst klebt es - aus jeweils +ca. 1 - 2 EL Masse kleine Buletten formen, in den Semmelbröseln +wälzen und im Fett von beiden Seiten braten. Dazu das Fett nicht zu +heiß werden lassen, lieber etwas länger backen. +Die heißen Kasknödel rasch mit Rahmsoße, Pilzen oder +Geschnetzeltem servieren. Wenn man sie anschneidet läu� immer +etwas vom würzigen Käse aus, das schmeckt sehr lecker. +Sie schmecken am nächsten Tag auch kalt im Büro, aber heiß und +knusprig sind sie natürlich am Besten. +Arbeitszeitca. 30 Minuten +Ruhezeitca. 35 Minuten +Koch-/Backzeitca. 20 Minuten +Gesamtzeitca. 1 Stunde 25 Minuten +Schwierigkeitsgradnormal +Kcal p. P .ca. 585 +Chefkoch Rezept: Gebratene Kasknödel von felidae177 https://www.chefkoch.de/rezepte/drucken/2656011417082549/Gebrate... +1 von 2 30.05.2024, 16:12 +--- Page 2 --- +Chefkoch Rezept: Gebratene Kasknödel von felidae177 https://www.chefkoch.de/rezepte/drucken/2656011417082549/Gebrate... +2 von 2 30.05.2024, 16:12 \ No newline at end of file diff --git a/data/extracted_texts/Gedeckter Apfelkuchen mit Streuseln von mbus.txt b/data/extracted_texts/Gedeckter Apfelkuchen mit Streuseln von mbus.txt new file mode 100644 index 0000000..d724123 --- /dev/null +++ b/data/extracted_texts/Gedeckter Apfelkuchen mit Streuseln von mbus.txt @@ -0,0 +1,55 @@ + +--- Page 1 --- +Rezept online aufrufen +Zutaten für 1 Portionen: +Für den Mürbeteig: +300 gMehl +100 gButter +100 gZucker +1 TLBackpulver +1 Pkt.Vanillinzucker +2Ei(er) +Für die Füllung: +1.500 gÄpfel, möglichst saure +50 gButter +50 gRosinen +50 gZucker +1 Pkt.Vanillepuddingpulver +Für die Streusel: +150 gMehl +100 gButter +100 gZucker +Rezept von: mbus +Gedeckter Apfelkuchen mit +Streuseln +Mehl, Butter, Zucker, Backpulver, Vanillinzucker und Eier zu einem +Mürbeteig verarbeiten, diesen dann 30 Min. ruhen lassen. +Die Äpfel schälen, achteln und dann in Würfel schneiden. Dabei das +Kerngehäuse entfernen. Zusammen mit der Butter, dem Zucker +und den Rosinen in einen Topf geben und solange erhitzen, bis die +Apfelstückchen anfangen zu verkochen und eine festere Masse +entsteht. Dann zur Seite stellen. +Einen Vanillepudding nach Packungsangabe zubereiten. Evtl. +werden dafür weiter Zutaten benötigt (Milch). Den Pudding +abkühlen lassen, jedoch nur soweit, dass er nicht ganz fest ist +sondern dickflüssig. +Eine Springform einfetten und den Mürbeteig darin ausrollen. +Einen Rand ca. 3 cm hochziehen, damit die Masse nicht ausläu�. +Nun erst den Pudding auf den Teig geben und gleichmäßig +verteilen. Dann die Apfelmasse darauf geben und vorsichtig +verteilen. +Aus dem Mehl, der Butter und dem Zucker einen Krümelteig +erstellen und oben auf die Apfelfüllung geben. +Bei 160 Grad für ca. 60 Minuten in den vorgeheizten Backofen +geben. Damit der Kuchen auch braun wird, drehe ich den Backofen +10 Minuten vor dem Ende noch einmal auf 200 Grad hoch, aber +dabeistehen, sonst wird er vielleicht zu braun. +Wer mag, kann auch den Äpfeln Zimt zusetzen, wir mögen jedoch +keinen Zimt. +Arbeitszeitca. 45 Minuten +Koch-/Backzeitca. 1 Stunde +Gesamtzeitca. 1 Stunde 45 Minuten +Schwierigkeitsgradsimpel +Kcal p. P .ca. 5707 +Chefkoch Rezept: Gedeckter Apfelkuchen mit Streuseln von mbus https://www.chefkoch.de/rezepte/drucken/305151110793264/Gedeckte... +1 von 1 30.05.2024, 16:36 \ No newline at end of file diff --git a/data/extracted_texts/Gemüse nach Gonbao-Art von zuiko.txt b/data/extracted_texts/Gemüse nach Gonbao-Art von zuiko.txt new file mode 100644 index 0000000..060a9c3 --- /dev/null +++ b/data/extracted_texts/Gemüse nach Gonbao-Art von zuiko.txt @@ -0,0 +1,46 @@ + +--- Page 1 --- +Rezept online aufrufen +Zutaten für 3 Portionen: +Für die Sauce: +2 TL, gehäu�Speisestärke +3 ELWasser, kalt +3 ELZucker +3 ELReisessig +3 ELSojasauce, hell +2 ELSojasauce, dunkel +1 Zehe/nKnoblauch, fein gehackt +Für das Gemüse: +2Paprikaschote(n) +n. B.Chilischote(n) +500 gChampignons +1 BundFrühlingszwiebel(n) +1 HandvollErdnüsse, geröstet +Sonnenblumenöl zum Braten +Salz und Pfe�er +Rezept von: zuiko +Gemüse nach Gonbao-Art +vegetarisches Wok-Gericht mit dunkler, +pikanter Sauce +Für die Gonbao-Sauce die Speisestärke in 3 EL kaltem Wasser +auflösen. Die übrigen Zutaten für die Sauce dazugeben und alles +miteinander verrühren. +Die Champignons vierteln, Frühlingszwiebeln in Ringe schneiden, +Paprikaschoten in Würfel schneiden, Chilischoten fein hacken. +Im Wok oder einer hohen Pfanne etwas Sonnenblumenöl erhitzen +und zunächst die Paprika sowie die fein gehackten Chilischoten bei +mittlerer bis hoher Hitze anbraten. Nach ungefähr 3 Minuten die +Champignons dazugeben. Alles mit Pfe�er und Salz würzen und +weitere 5 Minuten unter ständigem Rühren anbraten. +Die Gonbao-Sauce sowie die Frühlingszwiebeln in den Wok geben. +Die Sauce einmal aufkochen lassen. Dann die Hitze reduzieren und +die gerösteten Erdnüsse dazugeben. Das Gemüse solange weiter +braten, bis es gar, aber noch bissfest ist. +Das Gemüse nach Gonbao-Art mit gekochtem Reis oder +chinesischen Nudeln servieren. +Arbeitszeitca. 15 Minuten +Koch-/Backzeitca. 15 Minuten +Gesamtzeitca. 30 Minuten +Schwierigkeitsgradnormal +Chefkoch Rezept: Gemüse nach Gonbao-Art von zuiko https://www.chefkoch.de/rezepte/drucken/3676711553884546/Gemues... +1 von 1 30.05.2024, 16:13 \ No newline at end of file diff --git a/data/extracted_texts/Griessnockerl (Suppeneinlage) von jawi.txt b/data/extracted_texts/Griessnockerl (Suppeneinlage) von jawi.txt new file mode 100644 index 0000000..ce3463a --- /dev/null +++ b/data/extracted_texts/Griessnockerl (Suppeneinlage) von jawi.txt @@ -0,0 +1,21 @@ + +--- Page 1 --- +Rezept online aufrufen +Zutaten für 6 Portionen: +30 gFett oder Butter +1Ei(er) +Salz +70 gGrieß +1 LiterSuppe +Rezept von: jawi +Griessnockerl (Suppeneinlage) +Das Fett schaumig rühren, das Ei dazurühren, ebenso den Grieß +und das Salz. Die Masse 5 Minuten stehen lassen. Mit 2 Esslö�eln +Nockerln formen. Die Nockerl in siedendem Salzwasser oder auch +gleich in der Suppe 5 Minuten kochen lassen und danach ca. 15 +Minuten ziehen lassen. +Arbeitszeitca. 35 Minuten +Gesamtzeitca. 35 Minuten +Schwierigkeitsgradnormal +Chefkoch Rezept: Griessnockerl (Suppeneinlage) von jawi https://www.chefkoch.de/rezepte/drucken/47781016275357/Griessnock... +1 von 1 30.05.2024, 16:15 \ No newline at end of file diff --git a/data/extracted_texts/Grießbrei von pilotilka.txt b/data/extracted_texts/Grießbrei von pilotilka.txt new file mode 100644 index 0000000..1ee058b --- /dev/null +++ b/data/extracted_texts/Grießbrei von pilotilka.txt @@ -0,0 +1,24 @@ + +--- Page 1 --- +Rezept online aufrufen +Zutaten für 2 Portionen: +½ LiterMilch +5 ELGrieß +3 ELZucker +1 ELKakaopulver oder etwas Zimt +Rezept von: pilotilka +Grießbrei +sehr beliebt bei Kindern +Einen kleinen Teil der Milch in eine Rührschüssel geben, den Rest +aufkochen. Grieß und Zucker in die Rührschüssel geben, gut +verrühren. Wenn die Milch aufkocht, die Flüssigkeit aus der +Rührschüssel dazugeben und unterrühren. +Unter ständigem Rühren aufkochen und ein paar Minuten kochen +lassen. Auf dem Teller mit etwas Kakaopulver oder +Zimtzuckergemisch bestreuen. +Arbeitszeitca. 10 Minuten +Gesamtzeitca. 10 Minuten +Schwierigkeitsgradsimpel +Kcal p. P .ca. 398 +Chefkoch Rezept: Grießbrei von pilotilka https://www.chefkoch.de/rezepte/drucken/1086221214500502/Griessbre... +1 von 1 30.05.2024, 16:14 \ No newline at end of file diff --git a/data/extracted_texts/Gulasch.txt b/data/extracted_texts/Gulasch.txt new file mode 100644 index 0000000..6e7e03b --- /dev/null +++ b/data/extracted_texts/Gulasch.txt @@ -0,0 +1,40 @@ +Gulasch (vollveggie) +6 Portionen +Eine große Rührschüssel auf eine Küchenwaage stellen, alle Zutaten bis auf das Sojafleisch darin abwiegen bzw. abmessen und mit einem Schneebesen gut durchrühren. +Nun das trockene Sojafleisch zu der Marinade in die Schüssel geben und mit einem Rührlöffel gut unterheben. Zum Ziehen entweder in der Schüssel belassen, abdecken und immer mal wieder durchrühren. Oder aber die Masse in einen Plastikbeutel geben, verschließen und immer mal wieder gut durchkneten. Damit die Marinade besonders gut in das Sojafleisch einziehen kann, mindestens 12 Stunden, am besten über Nacht, einweichen lassen. +Die Zwiebeln schälen und in feine Würfel schneiden. Die Knoblauchzehen ebenfalls schälen und entweder in feine Würfel schneiden oder mit einer Knoblauchpresse direkt in den Topf pressen – die Knoblauchzehen werden aber erst später untergehoben, damit sie nicht verbrennen oder bitter werden. Paprikaschote waschen, halbieren, entkernen und in feine Würfel oder Streifen schneiden. +Das Olivenöl in einem großen Topf erhitzen und darin die über Nacht eingelegten Sojawürfel (falls erforderlich, diese vorher etwas ausdrücken) samt eventuell vorhandener Restmarinade für 5 Minuten auf hoher Stufe scharf anbraten, zwischendurch mit einem Kochlöffel immer mal wieder kräftig umrühren. Keine Sorge: Wenn sich etwas am Topfboden ansetzen sollte, lässt sich das spätestens nach der Flüssigkeitszugabe gut lösen und einrühren. +Die Zwiebel- und Paprikawürfel hinzugeben und weitere 5 Minuten scharf mitbraten, zwischendurch immer mal wieder kräftig umrühren. Dann den Herd auf mittlere Stufe herunterdrehen. Danach das Tomatenmark unterrühren, kurz anschwitzen lassen und die Knoblauchwürfel unterheben oder mit einer Knoblauchpresse direkt in den Topf pressen. +Die Gewürze Paprika edelsüß und scharf, Majoran und Thymian hinzugeben, kräftig unterrühren und kurz mitbraten; eine sogenannte Feinwaage eignet sich übrigens perfekt zum Abwiegen von Gewürzen und Kräutern. Auch hier keine Sorge: Wenn sich etwas am Topfboden ansetzen sollte, lässt sich das spätestens nach der Flüssigkeitszugabe gut lösen und einrühren. +Dann das Mehl bzw. Speisestärke oder Saucenpulver unterheben. Kurz ziehen lassen. Anschließend mit dem Rotwein, alternativ Kirschsaft oder Gemüsebrühe, vorsichtig ablöschen, kräftig umrühren und kurz simmern lassen. Auch hier wieder keine Sorge: Wenn sich etwas am Topfboden ansetzen sollte, lässt sich das spätestens nach der Flüssigkeitszugabe gut lösen und einrühren. +Das Gulasch nun etwa 30 Minuten zugedeckt bei kleiner Stufe köcheln lassen und das Wasser dabei portionsweise unterrühren, bis die gewünschte Konsistenz erreicht ist. Dabei immer mal wieder kräftig durchrühren. Mit Zucker, Salz und Pfeffer sowie nach Belieben mit den anderen Gewürzen abschmecken. Das Gulasch dickt übrigens noch nach, besonders am nächsten Tag; dann einfach etwas Rotwein, Wasser, Kirschsaft oder Gemüsebrühe bis zur gewünschten Konsistenz nachgießen. + +--- Table --- +Marinade: +70 Sojasauce +40g Worcestershiresauce +50 g Tomatenmark +30g Balsamico +20g Olivenöl +15g Senf, mittelscharf +15 g Agavendicksaft +1 Msp. Sambal Oelek +10g Paprikapulver edelsüß +½ TL Paprikapulver, rosenscharf +500ml Wasser, heiß +250g Sojafleisch (Medaillions) + +Gulasch: +175g Zwiebeln +200g Paprika, rot +2 Knoblauchzehen +3EL Olivenöl +3EL Tomatenmark +1TL Paprikapulver, edelsüß +½ TL Paprikapulver, rosenscharf +1TL Majoran +1TL Thymian +6EL Weizenmehl +500ml Rotwein, trocken +1200ml Wasser +1Prise Rohrzucker diff --git a/data/extracted_texts/Heidelbeer.txt b/data/extracted_texts/Heidelbeer.txt new file mode 100644 index 0000000..9a3a426 --- /dev/null +++ b/data/extracted_texts/Heidelbeer.txt @@ -0,0 +1,28 @@ +Heidelbeer-Tiramisu +8 Portionen +Bio-Zitrone heiß abwaschen, abtrocknen, Schale abreiben und Saft auspressen. Zitronensaft beiseite stellen. +Heidelbeeren waschen, verlesen und 400 g abwiegen. Heidelbeeren zusammen mit der Hälfte der Zitronenschale und Gelierzucker in einen Topf geben. Aufkochen. Mit einem Kartoffelstampfer die Beeren zerquetschen. Köcheln, bis die Gelierprobe gut ist. +Sahne mit Vanillezucker steif schlagen. Mascarpone mit Joghurt, Diamant Feinster Zucker, restlicher Zitronenschale und Zitronensaft glatt rühren. Zuletzt Sahne vorsichtig unterheben. +Orangen auspressen. Löffelbiskuits kurz in den gepressten Orangensaft tauchen und den Boden einer rechteckigen Auflaufform (ca. 15 x 30 cm) damit auslegen. Darauf vorsichtig erst die Hälfte der Heidelbeersoße, dann die Hälfte der Cremeschicht verteilen. So fortfahren, bis alle Zutaten verbraucht sind. Die letzte Lage sollte aus Creme bestehen. Mindestens 5 Stunden im Kühlschrank durchziehen lassen. +Heidelbeeren waschen und verlesen, Schokolade raspeln. Tiramisu mit Heidelbeeren und Schokoladenraspeln verzieren. + +--- Table --- +Heidelbeersoße: +1 Zitrone Bio +400g Heidelbeeren +150g Gelierzucker 2:1 + +Creme: +250g Sahne +1 Päckchen Vanillezucker +500g Mascarpone +300g Joghurt +80g Zucker + +Keksschickt: +1-2 Orangen +250g Löffelbiskuits + +Deko: +50g Heidelbeeren +25g weiße Schokolade diff --git a/data/extracted_texts/Kidneybohnen.txt b/data/extracted_texts/Kidneybohnen.txt new file mode 100644 index 0000000..1af232c --- /dev/null +++ b/data/extracted_texts/Kidneybohnen.txt @@ -0,0 +1,15 @@ +Kidneybohnen-Brownies +Zutaten +Zunächst die Kidneybohnen mit einem Mixer oder einer Küchenmaschine pürieren, anschließend mit den restlichen Zutaten (bis auf die Zartbitterschokolade) zu einer cremigen Masse verrühren. +Die gehackte Schokolade unterheben und die Masse dann in eine mit Backpapier ausgelegte bzw. gefettete Form gegeben und im vorgeheizten Ofen bei 200 Grad Umluft für ca. 15-20 Minuten gebacken. + +--- Table --- +1 Dose Kidneybohnen +30 g gemahlene Mandeln +2 Eier +50 g Kakaopulver +100 g Zucker oder Zuckerersatz (z.B. 100 g Xylit oder 125 g Erythrit) +2 EL neutrales Öl (z.B. Rapsöl) +1 Pck. Vanillezucker +1/2 Pck. Backpulver +100 g gehackte Zartbitterschokolade diff --git a/data/extracted_texts/LEBKUCHEN-MOUSSE MIT GLÜHWEINKIRSCHEN.txt b/data/extracted_texts/LEBKUCHEN-MOUSSE MIT GLÜHWEINKIRSCHEN.txt new file mode 100644 index 0000000..5d29106 --- /dev/null +++ b/data/extracted_texts/LEBKUCHEN-MOUSSE MIT GLÜHWEINKIRSCHEN.txt @@ -0,0 +1,7 @@ + +--- Page 1 --- +LEBKUCHEN-MOUSSE MIT GLÜHWEINKIRSCHEN https://transglobalpanparty.com/lebkuchen-mousse-gluehweinkirschen/ +1 von 2 24.08.2024, 12:41 +--- Page 2 --- +LEBKUCHEN-MOUSSE MIT GLÜHWEINKIRSCHEN https://transglobalpanparty.com/lebkuchen-mousse-gluehweinkirschen/ +2 von 2 24.08.2024, 12:41 \ No newline at end of file diff --git a/data/extracted_texts/Lachs in Thai-Curry-Soße mit Zucchini.txt b/data/extracted_texts/Lachs in Thai-Curry-Soße mit Zucchini.txt new file mode 100644 index 0000000..cee4f12 --- /dev/null +++ b/data/extracted_texts/Lachs in Thai-Curry-Soße mit Zucchini.txt @@ -0,0 +1,49 @@ + +--- Slide 1 --- +Lachs in Thai-Curry-Soße mit Zucchini +Goldenem Reis und eingelegten roten Zwiebelb +Zutaten 2 Personen +200g Lachsfilet +150g Jasminreis +1 Zucchini +1 Knoblauchzehe +1 Zwiebel +180ml Kokosmilch +25g rote Currypaste +2g Currygewürz +4g Brühepulver +25g Frühlingszwiebeln +1 rote Zwiebel +Was Du zu Hause haben solltest +300ml Wasser +1/4TL Weißweinessig +Salz +Pfeffer +Zucker +25 – 35 Minuten +Erhitze 300 ml Wasser im Wasserkocher. +In einen kleinen Topf mit Deckel 300 ml heißes Wasser füllen. +Hälfte „Hello Curry“ und 0,25 TL Salz dazugeben und aufkochen lassen. +Reis zugeben und bei niedriger Hitze 10 Min. abgedeckt köcheln lassen. +Vom Herd nehmen und abgedeckt 10 Min. ziehen lassen. +Gelbe Zwiebel grob würfeln. +Rote Zwiebel in feine Streifen schneiden. +Frühlingszwiebel in feine Ringe schneiden. +Knoblauch fein hacken. +Zucchini längs halbieren und in 0,5 cm Halbmonde schneiden. +In einem kleinen Topf 50 ml Wasser, 50 ml Essig, Salz und Zucker aufkochen. +Zwiebelstreifen hinzugeben und 30 Sek. köcheln lassen. Von der Hitze nehmen und marinieren lassen. +Lachsfilets rundum salzen. +In einer großen Pfanne 1 EL Öl erhitzen. Lachs darin auf der Hautseite 2 Min. anbraten, dann wenden und weitere 2 – 3 Min. braten, bis der Fisch innen nicht mehr glasig ist. +Lachs herausnehmen und nach Belieben die Haut abziehen. +In derselben Pfanne erneut 1 EL Öl erhitzen. Zwiebel, weißen Teil der Frühlingszwiebel und Zucchini darin 3 – 4 Min. anschwitzen. +Knoblauch, rote Currypaste und restliches „Hello Curry“ darin 1 weitere Min. anbraten. +Pfanneninhalt mit Kokosmilch, 100 ml Wasser und Brühepulver ablöschen und 2 – 3 Min. köcheln lassen. +Soße mit 0,25 TL Essig, Salz, Pfeffer und eine Prise Zucker abschmecken. +Lachs zurück in die Pfanne geben und warm halten, bis der Reis gar ist. +Reis und Thai-Curry auf tiefen Tellern verteilen. Lachs darauf anrichten und mit grünem Teil der Frühlingszwiebel und eingelegten roten Zwiebeln garnieren. +1 +2 +3 +4 +5 diff --git a/data/extracted_texts/Mango Limetten Triffle.txt b/data/extracted_texts/Mango Limetten Triffle.txt new file mode 100644 index 0000000..3b09715 --- /dev/null +++ b/data/extracted_texts/Mango Limetten Triffle.txt @@ -0,0 +1,19 @@ +Mango-Limetten-Triffle +Für 2 Personen +Zutaten +1 Limette +2 Esslöffel Wasser heiss +1 Teelöffel Puderzucker (1) +50 g weisse Schokolade +180 g griechischer Joghurt +1 Esslöffel Puderzucker (2) +4 Löffelbiscuits +0.5 Mango klein +1 Teelöffel Puderzucker (3) +Zubereitung +Die Schale von ½ Limette fein abreiben. Den Saft der ganzen Limette auspressen. +1 Esslöffel Limettensaft mit dem heissen Wasser und der ersten Portion Puderzucker (1) gut verrühren. Beiseitestellen. +Die weisse Schokolade fein hacken. In eine Metallschüssel geben und ü ber einem heissen – nicht kochenden – Wasserbad schmelzen lassen. Dann den Joghurt, die zweite Portion Puderzucker (2), 1 Esslöffel Limettensaft sowie die abgeriebene Limettenschale unterrühren. +Die Löffelbiskuits halbieren. Jeweils abwechselnd mit der Limettencreme in 2 Gläser schichten, dabei jede Lage Löffelbiskuits mit der Limettensaftmischung beträufeln. Mindestens 15 Minuten kalt stellen. +Die Mango schälen und das Fruchtfleisch vom Stein schneiden. Dieses mit der dritten Portion Puderzucker (3) in einen hohen Becher geben und fein pürieren. Bis zum Servieren kühl stellen. +Vor dem Servieren das Mangopüree über die Triffles in den Gläsern verteilen. diff --git a/data/extracted_texts/One Pot Mie Nudeln.txt b/data/extracted_texts/One Pot Mie Nudeln.txt new file mode 100644 index 0000000..0b45987 --- /dev/null +++ b/data/extracted_texts/One Pot Mie Nudeln.txt @@ -0,0 +1,20 @@ + +--- Slide 1 --- +One-Pot Mie Nudeln +Zutaten 2 Personen +400ml Kokosmilch +300ml Wasser +3 EL Sojasoße +2 EL Erdnussbutter +1 EL Currypulver +3 Nester Mie Nudeln +½ Brokkoli +3 Karotten +Karotten schälen und schneiden. +Brokkoli schneiden. +Kokosmilch, Wasser, Sojasoße, Erdnussbutter, Currypulver in eine hohe und große Pfanne geben. Unter rühren aufkochen. +Nudeln und Gemüse in die Pfanne geben. Deckel drauf und ca. 15 Minuten auf kleiner Flamme gar ziehen lassen. Ab und zu umrühren. +1 +2 +3 +Mit Kokos, Curry, Karotte und Brokkoli diff --git a/data/extracted_texts/One-Pot-Chili-Reispfanne-aus-dem-Ofen-3.txt b/data/extracted_texts/One-Pot-Chili-Reispfanne-aus-dem-Ofen-3.txt new file mode 100644 index 0000000..e5e37c2 --- /dev/null +++ b/data/extracted_texts/One-Pot-Chili-Reispfanne-aus-dem-Ofen-3.txt @@ -0,0 +1,54 @@ + +--- Page 1 --- +2 Paprika +1 rote Zwiebel +2 Knoblauchzehen +60 g Olivenöl +1 TL Cumin (Kreuzkümmel) +1 TL Cayennepfeffer +1 TL Salz +1 TL Pfeffer +1 EL Majoran +1 TL Rosmarin +1 TL Thymian +1/2 TL Muskatnuss +1/2 TL Curry +2 EL Tomatenmark +30 ml Ahornsirup +220 g Basmatireis +80 g Berglinsen +400 g gehackte Tomaten aus +der Dose +600 ml Gemüsebrühe +30 g Zartbitterschokolade + 200 g Mais (Abtropfgewicht) +200 g Kidneybohnen +100 g ger. Cheddar + 2 EL Pankomehl (Paniermehl) +2 EL Petersilie +4-6 Personen I 28 cm Ofenform Den Backofen auf 180 °C Umluft vorheizen. Paprika, Zwiebel +und Knoblauch inStücke schneiden. In zwei Esslöffel +Olivenöl scharf anbraten. Gewürze und Kräuter mischen +und hinzugeben, kurz mitbraten. Ahornsirup hinzugeben +und leicht karamellsieren lassen. +Reis und Berglinsen hinzugeben und mit Tomaten und +Brühe ablöschen. Schokolade, Tomatenmark, Mais und +Kidneybohnen unterrühren. Die Hälfte des Käses ebenso +grob untergeben. +Wenn die Pfanne sich nicht für den Ofen eignet, Masse in +eine Ofenform geben. Im Ofen 20 Minuten backen. Den +restlichen Cheddar, Olivenöl, Panko- oder Paniermehl (oder +ein Toastbrot zerkleinert), Petersilie miteinander +vermengen. Nach der Backzeit über die Masse geben und +weiter 5-10 Minuten garen. Reis und Linsen sollten weich +sein. +Gerne mit Creme Fraiche oder Schmand servieren. +One Pot Chili Reispfanne +aus dem Ofen +Schnell gemacht, würzig und lecker +So wird's gemachtEinkaufsliste +knusperstuebchen.net +Zeit +Vorbereitung | 5 Min +Kochen | 20-30 Min +Fertig in |30 Min diff --git a/data/extracted_texts/Orangentofu mit Reis.txt b/data/extracted_texts/Orangentofu mit Reis.txt new file mode 100644 index 0000000..5e937d0 --- /dev/null +++ b/data/extracted_texts/Orangentofu mit Reis.txt @@ -0,0 +1,29 @@ +Orangentofu mit Reis +2-3 Personen +Zutaten +Für den Tofu: +400g Tofu +4EL Speisestärke +1TL Paprikapulver edelsüß +Für die Soße: +6-8 Orangen für 250g Orangensaft +1 Orange (Abrieb) +1 Zwiebel +5g Ingwer +1 Knoblauchzehe +0,5 TL Chiliflocken +50g Rohrzucker +1EL Reisessig +2EL Sojasauce +1TL Speisestärke +Zum Servieren: +Reis +Sesam +Frühlingszwiebeln +Zubereitung +Den Tofu in grobe Stücke zerteilen & in eine verschließbare Box geben. Speisestärke, Paprikapulver, Salz & Pfeffer dazugeben. Box verschließen und schütteln. +Öl in einer Pfanne erhitzen. Tofu von allen Seiten anbraten, bis es außen goldbraun ist. +In der Zwischenzeit Orangensaft auspressen und Schale abreiben. +Zwiebel, Ingwer, Knoblauch schäle und fein hacken. +Tofu aus der Pfanne nehmen und beiseitestellen. Zwiebeln, Ingwer, Knoblauch und Chili anbraten. Großteil des Orangensafts, Zucker, Reisessig, Sojasauce und Orangenabrieb dazugeben und ca. 5 Minuten köcheln lassen. +Speisestärke mit dem Rest des Orangensafts verrühren, zur Soße geben und eindicken lassen. Tofu zur Sauce geben. diff --git a/data/extracted_texts/Orzo Halloumi Salat.txt b/data/extracted_texts/Orzo Halloumi Salat.txt new file mode 100644 index 0000000..c7eb9da --- /dev/null +++ b/data/extracted_texts/Orzo Halloumi Salat.txt @@ -0,0 +1,24 @@ +Orzo Halloumi Salat (nisi_cherie) +3 Portionen +Für den Salat: +100g Orzo Nudeln +80g Granatapfelkerne +100g Halloumi Natur +Honig +150g kleine Tomaten +½ Gurke +Basilikum +1 Frühlingszwiebel +Dressing: +30ml Olivenöl +15ml weißer Balsamico +1,5TL Honig +1,5TL Senf +Salz & Pfeffer +Orzo nach Packungsanweisung kochen & abkühlen lassen. +Zutaten für das Dressing verrühren. +Halloumi in Stücke schneiden. +Öl in einer Pfanne erhitzen & Halloumi rundeherum anbraten. Etwas Honig darüber geben und kurz in der Pfanne weiter schwenken. +Tomaten & Gurken schneiden. +Frühlingszwiebeln & Basilikum hacken. +Alles miteinander vermengen. diff --git a/data/extracted_texts/Rezeptkarten.txt b/data/extracted_texts/Rezeptkarten.txt new file mode 100644 index 0000000..5ab002f --- /dev/null +++ b/data/extracted_texts/Rezeptkarten.txt @@ -0,0 +1,201 @@ + +--- Slide 1 --- +Butterplätzchen +Zutaten: +6 Eigelb +500g Mehl +250g Zucker +250g Butter +Alle Zutaten verknete. Über Nacht in den Kühlschrank. +Teig ausrollen und ausstechen. +Für ca. 10 Minuten bei 180°C backen. +Variationen: Spitzbuben, Hausfreunde + +--- Slide 2 --- +Marzipankissen +Teig: +300g Mehl +100g Puderzucker +200g kalte Butter +2 Eigelb +1 Prise Salz +Füllung: +300g Marzipan +2 Eiweiß +2EL Zitronensaft +8 Tr. Bittermandelaroma +Puderzucker zum Bestäuben +Zubereitung: +Alle Zutaten des Teigs verkneten. Teig zu einem Rechteck (ca. 20x15cm) formen und ca. 2 Std. in den Kühlschrank (nicht zu lange!). Marzipan 2 Std. in den Kühlschrank oder 15 Min. in die Gefriere. +Marzipan grob raspeln. Mit anderen Zutaten der Füllung gut quirlen. +Teig viertel, 8 cm breit ausrollen. Teig mit der Füllung bestreichen, einrollen und dann Stücke abschneiden (wie Mini-Blätterteigschnecken). +Bei 180°C 12-14 Min. backen. +Noch warm mit Puderzucker bestäuben. + +--- Slide 3 --- +gefüllte Marzipan-Orangen-Plätzchen +Teig: +150g Mehl +50g Speisestärke +125g Zucker +1 Pck. Vanillezucker +1 TL Orangenabrieb +1 Prise Salz +175g kalte Butter +1 Ei +100g Marzipan +100g gemahlene Mandeln +Füllung: Orangenmarmelade +Glasur: +2-3 EL Orangensaft +1 TL Orangenabrieb +150g Puderzucker +Zubereitung: +Alle Zutaten des Teigs verkneten. Für 30 Min. in den Kühlschrank. +Teig ausrollen, Kreise ausstechen. +Bei 175°C ca. 12 Min. backen. +Auskühlen lassen, dann mit Marmelade bestreichen und zusammensetzen. +Glasur anrühren und Plätzchen bestreichen. + +--- Slide 4 --- +Lebkuchenplätzchen +Teig: +250g Honig +125g Butter +125g Zucker +1 Ei +500g Mehl +1 Pck. Backpulver +2EL Kakao +1 Pck. Lebkuchengewürz +Prise Salz +Zitronenabrieb +Deko: +Schokoglasuren +Puderzucker +Streusel +Zubereitung: +Honig, Butter und Zucker in einem Top unter Rühren erhitzen, bis sich alles gut verbunden hat. Abkühlen lassen. +Ei unterrühren. Mehl mit Backpulver und Kakao vermischen und untersieben. Gewürze hinzufügen und alles zu einem Teig verkneten. +1 Std. kalt stellen. +Teig ca. ½ cm dick ausrollen und ausstechen. +Bei 180°C 15-20 Minuten backen. +Mit Schokoguss, Zuckerguss und Streuseln verzieren. + +--- Slide 5 --- +Zimt-Orangen-Plätzchen +Zubereitung: +Butter und Zucker schaumig schlage. Orangenabrieb und Zimt untermischen. Mehl unterrühren. +Teig sofort zwischen Backpapier. ausrollen und ausstechen. +Bei 175°C ca. 10 Min backen. +Abkühlen lassen, dann zur Hälfte in Schokoladenguss tunken. +Zutaten: +200g weiche Butter +100g Puderzucker +300g Mehl +Abrieb von 2 Bio Orangen +2 TL Zimt +Schokoglasur Zartbitter + +--- Slide 6 --- +Vanillekipferl +Teig: +250g Mehl +210g Butter +100g gemahlene Mandeln +80g Zucker +2 Pck. guter Vanillezucker +Deko: +½ Pck. Puderzucker +1 Pck. guter Vanillezucker +Zubereitung: +Zutaten des Teigs verknete, dann 30 Min. in den Kühlschrank. +In der Zwischenzeit den Puderzucker sieben und mit Vanillezucker mischen. +Kipferlteig zu Rollen formen. Stücke abschneiden und Kipferl formen. Vorsicht: werden im Ofen sehr viel größer. +Bei 175°C Umluft ca. 10-15 Min. backen. +Lauwarm im Puderzucker wenden. Zu warm zerbröseln die Plätzchen, zu kalt haftet der Puderzucker nicht. + +--- Slide 7 --- +Kaffee Kakao Plätzchen +Teig: +160g Mehl +30g Kakao +40g Puderzucker +2TL Kaffeepulver +30g Schokoraspeln +150g Butter +Deko: +Weiße Kuvertüre +1 Pck. Schoko-Kaffeebohnen +Zubereitung: +Alle Zutaten des Teigs verknete. +3 Rollen formen. Rollen 20 Min. kalt stellen. +Scheiben von der Rolle anschneiden. +Bei 160°C ca. 12 Min. backen. +Dekorieren + +--- Slide 8 --- +Zimtsterne +Teig: +500g gemahlene Mandeln +300g Puderzucker +2TL Zimt +2 Eiweiß +2EL Mandellikör +Glasur: +1 Eiweiß +125g Puderzucker +Zubereitung: +Mandeln, Puderzucker und Zimt mischen. Eiweiß und Likör zugeben. Verkneten. +Teig zwischen Backpapier ca. 1cm dick ausrollen. Sterne ausstechen, Ausstecher zwischendurch in Puderzucker tauchen. +Eiweiß steif schlagen. Puderzucker nach und nach zugeben, dabei weiterschlagen. +Sterne bepinseln. +Auf unterster Schiene bei 150°C Ober-/ Unterhitze ca. 10-15 Min. backen. + +--- Slide 9 --- +Orangen Schoko Kipferl +220g Mehl +120g weiche Butter +100g Puderzucker +4EL Kakao +2 Eigelb +Alle Zutaten außer der Glasur verkneten +Teig zu einer Rolle formen. Stücke abschneiden und zu Kipferl formen. +Bei 180°C ca. 10-12 Min. backen. +Kipferl auskühlen lassen, anschließend die Enden in Schokoglasur tunken. +2EL Milch +1 Msp. Zimt +Abrieb von 1 Orange +5 Tr. Orangenaroma +Kuchenglasur +ZUTATEN + +--- Slide 10 --- +Marzipanwölkchen +Zutaten: +400g Marzipan +2 Eiweiß +100g Puderzucker +140g gehobelte Mandeln +Marzipan hobeln. +Eiweiß mit Puderzucker und Marzipan in einer Schüssel mit dem Handrührgerät zu mixen, bis eine gleichmäßige Masse entsteht. +Hände befeuchten. Ca. Walnussgroße Kugeln formen. Kugeln in Mandeln wälzen. +Backen: 150°C Ober-/Unterhitze – 20 Minuten + +--- Slide 11 --- +Erdnuss-Karamell-Makronen +Zubereitung: +Ofen auf 160°C Ober-/ Unterhitze vorheizen +Erdnüsse grob hacken +Eiweiß mit einer Prise Salz steif schlagen +Rohrzucker nach und nach einrieseln lassen +Erdnussbutter & Erdnüsse unterheben +2 TL auf die Oblaten setzen +20-25 Min. backen +Zutaten: +2 Eiweiß +100g Rohrzucker +50g Erdnussbutter crunchy +150g Erdnüsse (geröstet, gesalzen) +25 Oblaten +1 Prise Salz diff --git a/data/extracted_texts/Schokomousse.txt b/data/extracted_texts/Schokomousse.txt new file mode 100644 index 0000000..bd436af --- /dev/null +++ b/data/extracted_texts/Schokomousse.txt @@ -0,0 +1,6 @@ +Schokomousse (Zucker & Jagdwurst) +400g Seidentofu +200g Schokolade +Schokolade am Wasserbad schmelzen. +Seidentofu in einem hohen Gefäß mit geschmolzener Schokolade pürieren. +Kaltstellen diff --git a/data/extracted_texts/Spaghetti in Zitronenbutter und Lachs mit Macadamiakruste.txt b/data/extracted_texts/Spaghetti in Zitronenbutter und Lachs mit Macadamiakruste.txt new file mode 100644 index 0000000..d84a75f --- /dev/null +++ b/data/extracted_texts/Spaghetti in Zitronenbutter und Lachs mit Macadamiakruste.txt @@ -0,0 +1 @@ +[.png file: Spaghetti in Zitronenbutter und Lachs mit Macadamiakruste.PNG] \ No newline at end of file diff --git a/data/extracted_texts/Weihnachtliche Bratapfel Biskuitrolle - Madame Dessert.txt b/data/extracted_texts/Weihnachtliche Bratapfel Biskuitrolle - Madame Dessert.txt new file mode 100644 index 0000000..3eacfe0 --- /dev/null +++ b/data/extracted_texts/Weihnachtliche Bratapfel Biskuitrolle - Madame Dessert.txt @@ -0,0 +1,49 @@ + +--- Page 1 --- +Weihnachtliche Bratapfel Biskuitrolle +Was passt besser zu Weihnachten als Äpfel, Zimt und Nüsse? Meine leckere +Antwort: Alles in einem! Besonders lecker wird die Kombination als weihnachtliche Bratapfel +Biskuitrolle in der sich die fruchtig-nussige Füllung zwischen eine samtige Creme und fluffigen +Biskuit schmiegt. +12 Min. +Dessert Madame Dessert +Zutaten +Biskuitteig +Bratapfel-Füllung +Mascarpone Creme +Arbeitsschritte +Biskuit + +--- Page 2 --- +Bratapfel-Füllung +Mascarpone Creme +Bratapfel Biskuitrolle zusammensetzen +Weihnachtliche Bratapfel Biskuitrolle https://madamedessert.de/weihnachtliche-bratapfel-biskuitrolle/ +vorbereitete Backblech geben, etwas glatt streichen und auf der mittleren Schiene für etwa +12 Minuten hellbraun backen. +4. Den fertigen Biskuit aus dem Ofen nehmen und auf die saubere, mit einem Bogen +Backpapier ausgelegte Arbeitsfläche stürzen. +1. Äpfel schälen, entkernen und anschließend in ca. 5x5mm große Würfelchen schneiden. +Apfelwürfel mit etwas Zitronensaft beträufeln und beiseitestellen. +2. Nüsse nach Wahl in einer kleinen beschichteten leicht anrösten. Anschließend leicht +abkühlen lassen. +3. Zucker zusammen mit dem Apfelsaft in eine beschichtete Pfanne geben und solange +erhitzen, bis sich der Zucker vollständig aufgelöst hat. Butter unterrühren. Mit Zimt und +Vanille würzen. Apfelwürfel und optional die Rosinen dazugeben. +4. Bratapfelmasse nun für ca. 10 bis 20 Minuten einköcheln, bis der Saft leicht eingedickt ist und +die Äpfel die für dich perfekte Konsistenz erreicht haben. Zuletzt die gerösteten Nüsse +dazugeben und abkühlen lassen. +1. Sahne steifschlagen. Puderzucker und Mascarpone miteinander verrühren. Aufgeschlagene +Sahne in die Mascarponecreme rühren, bis eine schöne, glatte Masse entsteht. Optional +kannst du die Creme auch mit etwas Zitronensaft abschmecken. +1. Backpapier vorsichtig vom Biskuit abziehen und die Außenkanten des Bodens mit einem +scharfen Messer begradigen. Anschließend gleichmäßig mit der Mascarpone-Creme +einstreichen. +2. Abgekühlte Bratapfelmasse auf der Creme verteilen, dabei an einer der langen Seiten etwa 2 +bis 3cm zum Rand hin aussparen. Biskuit von der anderen langen Seite her eng aufrollen. +Die fertige Biskuitrolle rundherum mit dem Backpapier ein und für ca. 1 bis 2 Stunden in den +Kühlschrank stellen, damit die Creme anziehen kann. +3. Vor dem Servieren die Biskuitrolle mit Bratapfel-Füllung mit Creme-Tupfen garnieren oder mit +etwas Zimt bestäuben. Für eine schöne Optik eventuell die Enden abschneiden. +Weihnachtliche Bratapfel Biskuitrolle - Madame Dessert https://madamedessert.de/wprm_print/weihnachtliche-bratapfel-biskuit... +2 von 2 24.08.2024, 12:36 \ No newline at end of file diff --git a/data/extracted_texts/Zimt-Biskuitrolle mit Pflaumen.txt b/data/extracted_texts/Zimt-Biskuitrolle mit Pflaumen.txt new file mode 100644 index 0000000..f326b81 --- /dev/null +++ b/data/extracted_texts/Zimt-Biskuitrolle mit Pflaumen.txt @@ -0,0 +1,63 @@ + +--- Page 1 --- +kuechenmomente.de | 1 +Zimt-Biskuitrolle mit +Pflaumen +Biskuitrolle mit karamellisierten Pflaumen +Zubereitungszeit: 40 min +Wartezeit: +Backzeit: 20 min +ZUTATEN FÜR DEN BISKUIT +2 Eier +1 Prise Salz +140 g Zucker +1 TL Zimt +80 g Mehl +40 g Speisestärke +1 TL Backpulver +--- Page 2 --- +kuechenmomente.de | 2 +FÜR DIE PFLAUMEN +800 g Pflaumen +1 EL Speisestärke +100 g Zucker +Abrieb 1 Bio-Zitrone +Mark 1 Vanilleschote +FÜR DIE FÜLLUNG +700 g Schlagsahne +2 P. Vanille-Zucker +7 TL SanApart (oder 1 P. Sofort-Gelatine) +AUSSERDEM +50 g Haselnuss-Krokant +ZUBEREITUNG +Backofen auf Umluft 180 °C vorheizen.1. +1 Backblech (ca. 30 x 40 cm) mit Backpapier belegen.2. +Für den Teig die Eier trennen. Eiweiß mit der Prise Salz steifschlagen, dabei den3. +Zucker und den Zimt einrieseln lassen und noch etwas weiterschlagen, bis der +Zucker gelöst ist. Danach die Eigelbe unterrühren. +Mehl, Speisestärke und Backpulver mischen und auf die Eimasse sieben.4. +Vorsichtig unterheben – nicht rühren! +Den Teig auf das Backblech geben und glattstreichen und für 20 Minuten im5. +vorgeheizten Ofen backen. +Nach der Backzeit das Blech auf ein mit Zucker bestreutes Geschirrtuch stürzen,6. +das Backpapier vorsichtig abziehen und von der langen Seite her mit dem +Handtuchtuch aufrollen und so auskühlen lassen. +--- Page 3 --- +kuechenmomente.de | 3 +In der Zwischenzeit die Pflaumen waschen, entkernen und vierteln.7. +Pflaumenstücke in einer fettfreien Pfanne anschmoren. Stärke und Zucker +mischen und zu den Pflaumen geben, unterrühren und den Zucker karamellisieren +lassen. Vanille und Zitronenabrieb einrühren und einige Minuten bei niedriger +Temperatur köcheln lassen. Dann die Pflaumen in ein Sieb geben, dabei den Sud +auffangen und alles abkühlen lassen. +Für die Füllung Sahne steifschlagen, Vanillezucker und SanApart (oder Sofort-8. +Gelatine) einrieseln lassen. +Die Biskuitrolle ausrollen und mit einem Pinsel den Sud darauf verteilen. 2/3 der9. +Sahne darauf verteilen und glattstreichen. +Bis auf einige Stücke für die Deko alle Pflaumen auf der Sahne verteilen.10. +Haselnuss-Krokant darüber streuen (auch davon etwas für die Deko übrig lassen!) +und die Rolle wieder einrollen. +Mit der restlichen Sahne die Rolle rundherum bestreichen und für ca. 1 Stunde in11. +den Kühlschrank stellen. +Kurz vor dem Servieren die Rolle mit den restlichen Pflaumenstücken belegen und12. +mit Haselnuss-Krokant bestreuen. \ No newline at end of file diff --git a/data/extracted_texts/ramen_rezepte.txt b/data/extracted_texts/ramen_rezepte.txt new file mode 100644 index 0000000..5a4ed62 --- /dev/null +++ b/data/extracted_texts/ramen_rezepte.txt @@ -0,0 +1,143 @@ + +--- Page 1 --- + + + +Kochstudio Bilou . Schrenkstr. 13 . 80339 München +0176 / 810 190 92 . www.bilou-kitchen.de . jana@bilou-kitchen.de + +ZUTATEN FÜR 2 PERSONEN + +BRÜHE + +2 L Wasser +12 getrocknete Shitake Pilze oder 250 g frische Pilze +1 Zwiebel, mit Schale halbiert +10 cm Stück Ingwer, in Scheiben geschnitten +1-2 St. Kombu oder 10-15 g getrockneter Seetang +50 ml Sojasauce +Salz + +Alle Zutaten außer die Sojasauce und Salz in einen Topf geben und bei leichter Hitze 30-40 +Minuten kochen. +Die Brühe gerne noch etwas ziehen lassen und erst kurz vor dem Servieren abseihen und mit +Sojasauce und Salz abschmecken. + +CHIILI ÖL + +1kleine Zwiebel in Würfel geschnitten +15 g milde Chili Flocken oder Pulver (z.B. Gochugaru oder Pul Biber) +1 kleine rote Chili Schote, fein geschnitten +1-2 Knoblauchzehen fein zerdrückt +2 EL geröstetes Sesamöl +50-70 ml Öl + +4 EL Öl in einem Topf erhitzen, Zwiebel, Chili und Knoblauch darin anschwitzen, bis sie +Zwiebeln glasig sind. Das restliche Öl aufgießen und1 mal kurz aufkochen. Das Öl mit einem +Pürierstab pürieren. + +TARE + +1 EL Chili Öl +2 TL Genmai Miso +1-2 EL Wasser + +Alle Zutaten zu einer sämigen Paste verrühren. + +NUDELN + +200-250 g Ramen Nudeln + +Die Nudeln nach Packungsanleitung kochen, abseihen und in die heiße Brühe geben. + + +--- Page 2 --- + + + +Kochstudio Bilou . Schrenkstr. 13 . 80339 München +0176 / 810 190 92 . www.bilou-kitchen.de . jana@bilou-kitchen.de + + + + + + +KNUSPRIGES HACKFLEISCH + +200-300 g BIO Hackfleisch oder fester Tofu, mit den Händen zerkrümelt +1-2 EL Ingwer, gerieben +1 Knoblauchzehe, gerieben +2 Frühlingszwiebeln, in Ringe geschnitten +1 EL Chili-Bohnen Paste (alternativ Sambal Olek) +1 TL geröstetes Sesamöl +1 EL Sojasauce +2 EL neutrales Öl zum Braten + +Alle Zutaten bis auf das Bratöl miteinander vermischen. +Das Öl in einer Pfanne erhitzen und alles so lange darin anbraten bis es braun uns knusprig +ist. + +GEMÜSE + +2 kleine Pak Choi +100-150 g Sojasprossen + +Einen Topf mit Salzwasser zum Kochen bringen. Den Pak Choi ca. 1 Minute darin garen und +anschließend mit kaltem Wasser abschrecken. +Kurz vor dem Servieren den Pak Choi und die Sojasprossen noch mal kurz in heißes Wasser +geben, damit alles schön heiß ist. + +WEICH GEKOCHTE EIER + +2 Eier + +Die Eier an der Flachen Seite einstechen und im kochenden Wasser 7 Minuten kochen, +anschließend mit kaltem Wasser abschrecken und schälen. Die Eier kurz vor dem Anrichten +halbieren und auf die Ramen legen. + + + + + + + +--- Page 3 --- + + + +Kochstudio Bilou . Schrenkstr. 13 . 80339 München +0176 / 810 190 92 . www.bilou-kitchen.de . jana@bilou-kitchen.de + +FURIKAKE + +1 Nori Blatt, in grobe Stücke zerrissen +1 g Bonito Flocken (optional) +15 g schwarzer und weißer Sesam +½ EL Röstzwiebeln +Meersalz + +Den Sesam in einer Pfanne kurz anrösten. +Alle Zutaten mit einem Mixer grob mahlen. + +WEITERE TOPPINGS + +3 Frühlingszwiebeln in feine Streifen geschnitten +¼ Nori Blatt in 4 kleine Vierecke geschnitten + + +RAMEN ANRICHTEN + +1. Die Suppenschalen im Ofen vorwärmen oder etwas heißes Wasser hinein geben, damit +die Schüsseln beim Anrichten schön warm sind. +2. Tare in Schüsseln verteilen +3. Brühe aufgießen +4. Die heiße Ramen Nudeln in die Brühe geben +5. Gemüse kurz erhitzen und dazu geben +6. Knuspriges Hackfleisch dazu geben +7. Eier halbieren und mit der Schnittfläche nach oben darauf geben +8. Jeweils 2 Nori Vierecke an die Seite stecken +9. Mit Frühlingszwiebeln und Furikake bestreuen + + + \ No newline at end of file diff --git a/data/extracted_texts/schnelle Ramen.txt b/data/extracted_texts/schnelle Ramen.txt new file mode 100644 index 0000000..5847fba --- /dev/null +++ b/data/extracted_texts/schnelle Ramen.txt @@ -0,0 +1,28 @@ + +--- Slide 1 --- +Schnelle Ramen +Zutaten 2 Personen +2l Wasser +12g Dashi-Pulver +1EL Zucker +1EL Essig +1 ½ EL Miso +1 Schuss Sojasauce +1 Schuss dunkle Sojasauce +Hoisin +Räuchertofu +¼ - ½ Kopf Chinakohl +250g Champignons +3 Nester Mi Nudeln +4 Frühlingszwiebeln +Räuchertofu würfeln, dann mit einem Schuss dunkler Sojasauce und Hoisinsauce vermischen, bis der Tofu komplett mit Soße bedeckt ist. Tofu bei Seite stellen. +Champignons je nach Größe ganz lassen oder halbieren. Chinakohl und Frühlingszwiebeln schneiden. +Öl in einem Topf erhitzen. Champignons, Frühlingszwiebeln und den dicken Teil des Chinakohls anbraten. Mit Wasser, Dashibrühe, Zucker, Essig, Miso und Sojasauce aufgießen. Etwa 10 Minuten köcheln lassen. +1 +2 +3 +Mit Tofu, Champignons und Chinakohl +Tofu für 7 Minuten in der Heißluftfritteuse garen. +4 +Nudeln und grünen Teil des Chinakohls zur Suppe geben und nach Packungsanweisung köcheln lassen. +5 diff --git a/data/extracted_texts/zitronen-tiramisu-ohne-rohes-ei.txt b/data/extracted_texts/zitronen-tiramisu-ohne-rohes-ei.txt new file mode 100644 index 0000000..1e55760 --- /dev/null +++ b/data/extracted_texts/zitronen-tiramisu-ohne-rohes-ei.txt @@ -0,0 +1,65 @@ + +--- Page 1 --- +ZITRONEN TIRAMISU / OHNE ROHES EI + +Klassisches Tiramisu ist leider oft für Kinder nicht +geeignet und auch auf rohes Ei müssen viele verzichten. +Diese fruchtige Variante mit Lemon Curd ist da die +perfekte Lösung. Das Tiramisu habe ich mit einem +Backrahmen zubereitet, so steht es später ähnlich wie +eine Torte und kann sogar noch kurz eingefroren +werden, so entsteht eine erfrischende geeiste Version. +Gerade an heißen Sommertagen passt das +Zitronentiramisu perfekt. Die Löffelbiskuits könnt ihr +mit einem Fruchtsaft eurer Wahl tränken, ich habe mich +im Video für frisch gepressten Orangensaft entschieden, +aber auch Mango- oder Maracujasaft passen perfekt +dazu. +Zubereitungszeit: 45 min +Kühlzeit: 4 h +Backform: Eckig +Länge: 21 cm +Breite: 21 cm +ZUTATEN +LEMON CURD +  3 Zitrone / Zitronen (Abrieb) +  150 g Zitrone / Zitronen (Saft) +  3 Ei / Eier +  150 g Zucker +  1 TL Speisestärke +  100 g Butter (kalt) +MASCARPONECREME +  375 g Mascarpone +  75 g Puderzucker +  200 g Schmand +  1 TL Vanilleextrakt +  250 g Sahne + +ZUM SCHICHTEN +  200 g Löffelbiskuit / Löffelbiskuits +  175 g Orangensaft (oder Mangosaft) +DEKORATION +  0.5 Zitrone / Zitronen +ZUBEREITUNG +--- Page 2 --- +LEMON CURD +Reibe die Schale der Zitronen ab, achte darauf, dass es unbehandelte Bio-Zitronen sind, deren Schale verzehrt werden kann. Verrühre den +Abrieb mit dem Zitronensaft, den Eiern, dem Zucker und der Stärke klümpchenfrei. Erwärme die Masse bei mittelhoher Hitze, rühre dabei +stetig, sodass keine Klümpchen entstehen und das Ei nicht gerinnt. Rühre so lange, bis die Masse eindickt und lasse sie nach Bedarf einmal +kurz aufkochen. Nimm das Lemon Curd vom Herd und rühre die kalte Butter in Stücken ein, so kühlt die Masse schneller ab und kann direkt +weiterverwendet werden. +Fülle das nicht benötigte Lemon Curd in Schraubgläser um und stelle es, nachdem es vollständig +abgekühlt ist, in den Kühlschrank, dort hält es sich etwa 8 Tage. +MASCARPONECREME +Verrühre den Mascarpone cremig, gib Puderzucker, Schmand und Vanilleextrakt dazu und rühre alles ein. Gieße die Sahne langsam dazu und +schlage die Creme steif. Achte darauf, die Creme nicht zu lange zu schlagen, damit Sahne und Mascarpone nicht gerinnen. Stelle die Creme +bis zur Weiterverarbeitung kalt. +TIRAMISU SCHICHTEN +Tauche die Hälfte der Löffelbiskuits in den Saft und lege sie als Boden auf eine Servierplatte. Stelle einen Backrahmen darum und verteile +etwas Lemon Curd darauf. Gib die Hälfte der Creme darüber und streiche sie glatt. Tränke dann die zweite Hälfte der Löffelbiskuits im Saft +und schichte sie auf die Creme. Verteile wieder etwas Lemon Curd und die übrige Creme darüber und streiche sie glatt. +DEKORATION +Gib ein paar Löffel Lemon Curd auf das Tiramisu und marmoriere die Creme damit etwas. Schneide mit einem Zestenreißer feine Zesten von +der Zitrone ab. Schneide die Zitrone in Scheiben und diese dann in Viertel und verteile sie mit den Zesten über dem Tiramisu. Stelle das +Tiramisu dann für mindestens 4 Stunden in den Kühlschrank und friere es nach Belieben vor dem Servieren für etwa 30 Minuten ein. Entferne +den Backrahmen zum Servieren. Viel Spaß beim Nachmachen, deine Sally! \ No newline at end of file diff --git a/data/import_2026/American Pancakes.pdf b/data/import_2026/American Pancakes.pdf new file mode 100644 index 0000000..a89bdef Binary files /dev/null and b/data/import_2026/American Pancakes.pdf differ diff --git a/data/import_2026/Ananaskuchen.pdf b/data/import_2026/Ananaskuchen.pdf new file mode 100644 index 0000000..23d8207 Binary files /dev/null and b/data/import_2026/Ananaskuchen.pdf differ diff --git a/data/import_2026/Apfel Erdnuss Tarte.docx b/data/import_2026/Apfel Erdnuss Tarte.docx new file mode 100644 index 0000000..7f7466e Binary files /dev/null and b/data/import_2026/Apfel Erdnuss Tarte.docx differ diff --git a/data/import_2026/Bao Buns mit Champignons.pptx b/data/import_2026/Bao Buns mit Champignons.pptx new file mode 100644 index 0000000..080363a Binary files /dev/null and b/data/import_2026/Bao Buns mit Champignons.pptx differ diff --git a/data/import_2026/Blauchbeer.docx b/data/import_2026/Blauchbeer.docx new file mode 100644 index 0000000..fa671ab Binary files /dev/null and b/data/import_2026/Blauchbeer.docx differ diff --git a/data/import_2026/Blätterteigpäckchen mit Spargel.jpg b/data/import_2026/Blätterteigpäckchen mit Spargel.jpg new file mode 100644 index 0000000..f49427e Binary files /dev/null and b/data/import_2026/Blätterteigpäckchen mit Spargel.jpg differ diff --git a/data/import_2026/Bounty®-Kuchen Rezept _ Dr. Oetker.pdf b/data/import_2026/Bounty®-Kuchen Rezept _ Dr. Oetker.pdf new file mode 100644 index 0000000..01919cb Binary files /dev/null and b/data/import_2026/Bounty®-Kuchen Rezept _ Dr. Oetker.pdf differ diff --git a/data/import_2026/Brezen Cracker.docx b/data/import_2026/Brezen Cracker.docx new file mode 100644 index 0000000..9c7cdeb Binary files /dev/null and b/data/import_2026/Brezen Cracker.docx differ diff --git a/data/import_2026/Champignons Weihnachtsmarkt.pdf b/data/import_2026/Champignons Weihnachtsmarkt.pdf new file mode 100644 index 0000000..318028c Binary files /dev/null and b/data/import_2026/Champignons Weihnachtsmarkt.pdf differ diff --git a/data/import_2026/Chili sin Carne.docx b/data/import_2026/Chili sin Carne.docx new file mode 100644 index 0000000..c31f66f Binary files /dev/null and b/data/import_2026/Chili sin Carne.docx differ diff --git a/data/import_2026/Crema catalana von Aurora.pdf b/data/import_2026/Crema catalana von Aurora.pdf new file mode 100644 index 0000000..1044df1 Binary files /dev/null and b/data/import_2026/Crema catalana von Aurora.pdf differ diff --git a/data/import_2026/Cremige Zucchini-Bohnenpfanne mit Erdnussbutter - Eat this!.pdf b/data/import_2026/Cremige Zucchini-Bohnenpfanne mit Erdnussbutter - Eat this!.pdf new file mode 100644 index 0000000..53c53c6 Binary files /dev/null and b/data/import_2026/Cremige Zucchini-Bohnenpfanne mit Erdnussbutter - Eat this!.pdf differ diff --git a/data/import_2026/Currytaschen.docx b/data/import_2026/Currytaschen.docx new file mode 100644 index 0000000..b30ed07 Binary files /dev/null and b/data/import_2026/Currytaschen.docx differ diff --git a/data/import_2026/Donauwelle - super easy von alina1st.pdf b/data/import_2026/Donauwelle - super easy von alina1st.pdf new file mode 100644 index 0000000..73f3290 Binary files /dev/null and b/data/import_2026/Donauwelle - super easy von alina1st.pdf differ diff --git a/data/import_2026/Focaccia.pdf b/data/import_2026/Focaccia.pdf new file mode 100644 index 0000000..85e87ff Binary files /dev/null and b/data/import_2026/Focaccia.pdf differ diff --git a/data/import_2026/Galaktoboureko - griechischer Grießauflauf.pdf b/data/import_2026/Galaktoboureko - griechischer Grießauflauf.pdf new file mode 100644 index 0000000..ffd0625 Binary files /dev/null and b/data/import_2026/Galaktoboureko - griechischer Grießauflauf.pdf differ diff --git a/data/import_2026/Gebratene Kasknödel von felidae177.pdf b/data/import_2026/Gebratene Kasknödel von felidae177.pdf new file mode 100644 index 0000000..3df9074 Binary files /dev/null and b/data/import_2026/Gebratene Kasknödel von felidae177.pdf differ diff --git a/data/import_2026/Gedeckter Apfelkuchen mit Streuseln von mbus.pdf b/data/import_2026/Gedeckter Apfelkuchen mit Streuseln von mbus.pdf new file mode 100644 index 0000000..ab2de62 Binary files /dev/null and b/data/import_2026/Gedeckter Apfelkuchen mit Streuseln von mbus.pdf differ diff --git a/data/import_2026/Gemüse nach Gonbao-Art von zuiko.pdf b/data/import_2026/Gemüse nach Gonbao-Art von zuiko.pdf new file mode 100644 index 0000000..71dc51f Binary files /dev/null and b/data/import_2026/Gemüse nach Gonbao-Art von zuiko.pdf differ diff --git a/data/import_2026/Griessnockerl (Suppeneinlage) von jawi.pdf b/data/import_2026/Griessnockerl (Suppeneinlage) von jawi.pdf new file mode 100644 index 0000000..e492a52 Binary files /dev/null and b/data/import_2026/Griessnockerl (Suppeneinlage) von jawi.pdf differ diff --git a/data/import_2026/Grießbrei von pilotilka.pdf b/data/import_2026/Grießbrei von pilotilka.pdf new file mode 100644 index 0000000..4711bb1 Binary files /dev/null and b/data/import_2026/Grießbrei von pilotilka.pdf differ diff --git a/data/import_2026/Gulasch.docx b/data/import_2026/Gulasch.docx new file mode 100644 index 0000000..54fbe8b Binary files /dev/null and b/data/import_2026/Gulasch.docx differ diff --git a/data/import_2026/Heidelbeer.docx b/data/import_2026/Heidelbeer.docx new file mode 100644 index 0000000..58531bc Binary files /dev/null and b/data/import_2026/Heidelbeer.docx differ diff --git a/data/import_2026/Kidneybohnen.docx b/data/import_2026/Kidneybohnen.docx new file mode 100644 index 0000000..b8f6e85 Binary files /dev/null and b/data/import_2026/Kidneybohnen.docx differ diff --git a/data/import_2026/LEBKUCHEN-MOUSSE MIT GLÜHWEINKIRSCHEN.pdf b/data/import_2026/LEBKUCHEN-MOUSSE MIT GLÜHWEINKIRSCHEN.pdf new file mode 100644 index 0000000..69892c3 Binary files /dev/null and b/data/import_2026/LEBKUCHEN-MOUSSE MIT GLÜHWEINKIRSCHEN.pdf differ diff --git a/data/import_2026/Lachs in Thai-Curry-Soße mit Zucchini.pptx b/data/import_2026/Lachs in Thai-Curry-Soße mit Zucchini.pptx new file mode 100644 index 0000000..4613106 Binary files /dev/null and b/data/import_2026/Lachs in Thai-Curry-Soße mit Zucchini.pptx differ diff --git a/data/import_2026/Mango Limetten Triffle.docx b/data/import_2026/Mango Limetten Triffle.docx new file mode 100644 index 0000000..b1ab5c1 Binary files /dev/null and b/data/import_2026/Mango Limetten Triffle.docx differ diff --git a/data/import_2026/One Pot Mie Nudeln.pptx b/data/import_2026/One Pot Mie Nudeln.pptx new file mode 100644 index 0000000..f4a992d Binary files /dev/null and b/data/import_2026/One Pot Mie Nudeln.pptx differ diff --git a/data/import_2026/One-Pot-Chili-Reispfanne-aus-dem-Ofen-3.pdf b/data/import_2026/One-Pot-Chili-Reispfanne-aus-dem-Ofen-3.pdf new file mode 100644 index 0000000..f93b594 Binary files /dev/null and b/data/import_2026/One-Pot-Chili-Reispfanne-aus-dem-Ofen-3.pdf differ diff --git a/data/import_2026/Orangentofu mit Reis.docx b/data/import_2026/Orangentofu mit Reis.docx new file mode 100644 index 0000000..f4beb3c Binary files /dev/null and b/data/import_2026/Orangentofu mit Reis.docx differ diff --git a/data/import_2026/Orzo Halloumi Salat.docx b/data/import_2026/Orzo Halloumi Salat.docx new file mode 100644 index 0000000..3e521e4 Binary files /dev/null and b/data/import_2026/Orzo Halloumi Salat.docx differ diff --git a/data/import_2026/Rezeptkarten.pptx b/data/import_2026/Rezeptkarten.pptx new file mode 100644 index 0000000..b68d9e4 Binary files /dev/null and b/data/import_2026/Rezeptkarten.pptx differ diff --git a/data/import_2026/Schokomousse.docx b/data/import_2026/Schokomousse.docx new file mode 100644 index 0000000..541901b Binary files /dev/null and b/data/import_2026/Schokomousse.docx differ diff --git a/data/import_2026/Spaghetti in Zitronenbutter und Lachs mit Macadamiakruste.PNG b/data/import_2026/Spaghetti in Zitronenbutter und Lachs mit Macadamiakruste.PNG new file mode 100644 index 0000000..8956fae Binary files /dev/null and b/data/import_2026/Spaghetti in Zitronenbutter und Lachs mit Macadamiakruste.PNG differ diff --git a/data/import_2026/Weihnachtliche Bratapfel Biskuitrolle - Madame Dessert.pdf b/data/import_2026/Weihnachtliche Bratapfel Biskuitrolle - Madame Dessert.pdf new file mode 100644 index 0000000..667b006 Binary files /dev/null and b/data/import_2026/Weihnachtliche Bratapfel Biskuitrolle - Madame Dessert.pdf differ diff --git a/data/import_2026/Zimt-Biskuitrolle mit Pflaumen.pdf b/data/import_2026/Zimt-Biskuitrolle mit Pflaumen.pdf new file mode 100644 index 0000000..8128180 Binary files /dev/null and b/data/import_2026/Zimt-Biskuitrolle mit Pflaumen.pdf differ diff --git a/data/import_2026/ramen_rezepte.pdf b/data/import_2026/ramen_rezepte.pdf new file mode 100644 index 0000000..5ffec34 Binary files /dev/null and b/data/import_2026/ramen_rezepte.pdf differ diff --git a/data/import_2026/schnelle Ramen.pptx b/data/import_2026/schnelle Ramen.pptx new file mode 100644 index 0000000..90765a7 Binary files /dev/null and b/data/import_2026/schnelle Ramen.pptx differ diff --git a/data/import_2026/zitronen-tiramisu-ohne-rohes-ei.pdf b/data/import_2026/zitronen-tiramisu-ohne-rohes-ei.pdf new file mode 100644 index 0000000..18c5642 Binary files /dev/null and b/data/import_2026/zitronen-tiramisu-ohne-rohes-ei.pdf differ diff --git a/data/recipes_summary.json b/data/recipes_summary.json new file mode 100644 index 0000000..34563d4 --- /dev/null +++ b/data/recipes_summary.json @@ -0,0 +1,41 @@ +{ + "American Pancakes.txt": "\n--- Page 1 ---\nRezept online aufrufen\nZutaten für 4 Portionen:\n150 gMehl\n1 ELZucker\n1 TLBackpulver\n2Ei(er)\n1 Prise(n)Salz\n150 mlMilch\n3 ELÖl\nn. B.Ahornsirup\n200 gBeeren, (TK)\nRezept von: hannah_kocht_einfach\nAmerican Pancakes\nEin amerikanischer Frühstückstraum\nMehl, Zucker, Backpulver, Eier, Salz und Milch in einer Schüssel\nvermengen.\nJetzt ein wenig Öl in einer Pfanne erhitzen. Dann den\nPfannkuchenteig portionsweise (etwa 2 - 3 Esslö�el reichen)\nhineingeben und zu einem großen, drei Zentimeter dicken Kreis\nformen.\nBei mittlerer Hitzestufe wird der Teig nun so lange gebraten, bis die\nuntere Seite golden und braun ist.\nAnschließend ein Mal wenden, bis er goldbraun gebraten ist.\nAhornsirup darüber gießen und mit Tiefkühlbeeren garnieren.\nWarm genießen.\nArbeitszeitca. 10 Minuten\nKoch-/Backzeitca. 15 Minuten\nGesamtzeitca. 25 Minuten\nSchwierigkeitsgradsimpel\nChefkoch Rezept: American Pancakes von hannah_kocht_einfach about:blank\n1 von 1 30.05.2024, 16:00", + "Ananaskuchen.txt": "\n--- Page 1 ---\nEinfach Backen / Brigitte Sporrer\nZUTATEN \n(FÜR 1 KUCHEN)\n1\n \nDose\nAnanas\n \nin Scheiben\n(Abtropfgewicht ca. 350 g)\n220\n \ng\nweiche\n \nButter\n30\n \ng\nbrauner\n \nZucker\n130\n \ng\nZucker\n1\n \nPrise\nSalz\n3\nEier\n \n(Gr. M)\n200\n \ng\nWeizenmehl\n \n(Type 405, alternativ\nDinkelmehl Type 630)\n2\n \nTL\nBackpulver\nnach Belieben\n \nCocktailkirsche\n \noder\nMaraschinokirschen zum\nDekorieren\nBacken\n:\n \n40 Min.\nDauer\n:\n \n25 Min.\nNiveau\n:\n \nEinfach\nZubereitung\n1\n. \nSCHRITT\n1\n1\n \n \nDose\nDose\n \n \nAnanas\nAnanas\n \n \nin Scheiben (Abtropfgewicht ca. 350 g)\nin Scheiben (Abtropfgewicht ca. 350 g)\nDen Backofen auf\n 180 Grad Ober-/Unterhitze (Umluft: 160\n 180 Grad Ober-/Unterhitze (Umluft: 160\nGrad) \nGrad) \nvorheizen.\n \n \nDie\n \n \nAnanasscheiben auf einem Sieb\nabtropfen lassen. \nEin Stück Backpapier in die \nSpringform (Ø\nSpringform (Ø\n26 cm)\n26 cm)\n einspannen.\n2\n. \nSCHRITT\n40\n40\n \n \ng\ng\n \n \nweiche\nweiche\n \n \nButter\nButter\n \n \n, \n, \n30\n30\n \n \ng\ng\n \n \nbrauner\nbrauner\n \n \nZucker\nZucker\n40 g der weichen Butter mit einem Pinsel auf dem Boden der\nForm verteilen. Den braunen Zucker gleichmäßig darauf\nstreuen.\n3\n. \nSCHRITT\nAcht der Ananasscheiben dicht an dicht auf den\nSpringformboden legen. Die übrigen Ananasscheiben (ca. 3-4\nStück) sehr fein hacken oder pürieren.\n4\n. \nSCHRITT\n180\n180\n \n \ng\ng\n \n \nweiche\nweiche\n \n \nButter\nButter\n \n \n, \n, \n130\n130\n \n \ng\ng\n \n \nZucker\nZucker\n \n \n, \n, \n1\n1\n \n \nPrise\nPrise\n \n \nSalz\nSalz\n \n \n, \n, \n3\n3\n \n \nEier\nEier\n \n \n(Gr. M)\n(Gr. M)\n \n \n, \n, \n200\n200\n \n \ng\ng\nWeizenmehl\nWeizenmehl\n \n \n(Type 405, alternativ Dinkelmehl Type 630)\n(Type 405, alternativ Dinkelmehl Type 630)\n \n \n, \n, \n2\n2\n \n \nTL\nTL\n \n \nBackpulver\nBackpulver\nFür den Teig\nFür den Teig\n die übrige weiche Butter mit Zucker und Salz in\neiner Rührschüssel schaumig schlagen. Eier nach und nach\nzugeben und auf höchster Stufe weiterrühren. Das Mehl mit\nBackpulver mischen und abwechselnd mit dem Ananaspüree\nunter den Teig rühren.\n5\n. \nSCHRITT\nsuper saftig\nAnanaskuchen\nNoch mehr leckere Rezepte: https://www.einfachbacken.de\n--- Page 2 ---\nNach Belieben die Löcher der Ananasscheiben mit je einer\nKirsche ausfüllen. Teig darauf geben und glatt streichen. Im\nunteren Drittel des vorgeheizten Backofens \nca. 40 Minuten\nca. 40 Minuten\ngoldgelb backen. Am besten ein Stück Backpapier auf den\nKuchenrost legen, damit der eventuell heraustropfende Saft\nbzw. Karamell nicht am Ofenboden einbrennt.\n6\n. \nSCHRITT\nDen fertigen Kuchen \nca. 5 Minuten \nca. 5 Minuten \nauf einem Kuchenrost\nauskühlen lassen. Dann direkt auf die gewünschte Tortenplatte\nstürzen und das mitgebackene Backpapier entfernen. Der\nKuchen ergibt ca. 12 Stücke.\nNoch mehr leckere Rezepte: https://www.einfachbacken.de", + "Apfel Erdnuss Tarte.txt": "Apfel-Salzkaramell-Tarte mit Erdnüssen (krimi&Keks)\nFür eine rechteckige Tarteform\nZutaten\nZuerst den Backofen auf 170 Grad Umluft vorheizen und die Tarteform einfetten.\nFür den Mürbeteig alle Zutaten mit den Knethaken des Mixers oder der Küchenmaschine (oder den Händen) zu einem glatten Teig verkneten. Den Teig auf dem Boden der Tarteform ausrollen und am Rand hochziehen. Jetzt ein Blatt Backpapier auf den Teig legen und die Hülsenfrüchte auf dem Papier verteilen. Das simuliert eine Füllung beim Vorbacken und verhindert, dass der Teig am Boden Blasen wirft und am Rand herunterfließen kann. Den Teig für 12 Minuten auf der mittleren Schiene backen.\nInzwischen das Salzkaramell kochen. Dazu Zucker, Butter und Sahne in einem kleinen Topf aufkochen und so lange einköcheln lassen, bis sich eine hellbraune, dickflüssige Soße gebildet hat. Das dauert ungefähr 15 Minuten. Zwischendurch unbedingt umrühren, damit das Karamell nicht anbrennt. Ist die gewünschte Konsistenz erreicht, Meersalz nach Lust und Laune dazugeben und kurz verrühren.\nJetzt sollte auch der Mürbeteig vorgebacken sein und aus dem Ofen kommen. Das Backpapier mit den Hülsenfrüchten wieder entfernen. Achtung: Die Hülsenfrüchte (ich nehme getrocknete weiße Bohnen) werden extrem heiß, also verbrennt Euch nicht die Finger!\nDie Äpfel vierteln, entkernen und in dünne Scheiben schneiden.\nDie Salzkaramell-Soße auf den Mürbeteig streichen, sodass der Teigboden gut bedeckt ist. Auf das Karamell einige Erdnüsse streuen und dann die Apfelscheiben wie Dachziegel auf dem Teig verteilen. Wer mag, kann noch ein wenig Zimt über die Apfelspalten streuen.\nDie Apfeltarte backt für 20 Minuten auf der mittleren Schiene im Ofen. Dann herausziehen und mit einem Backpinsel noch etwas von dem Salzkaramell auf die Apfelspalten pinseln. Noch ein paar Erdnüsse über die Äpfel streuen und die Tarte für weitere 10 Minuten in den Ofen geben. Nach Ende der Backzeit die Apfeltarte aus dem Backofen nehmen und auf einem Gitter abkühlen lassen. Es bleibt ein wenig von der Salzkaramell-Soße übrig – ungefäh so viel, wie in dem kleinen Weckglas auf den Fotos zu sehen ist. Die restliche Soße könnt Ihr entweder zur Apfeltarte servieren oder anderweitig naschen. Herr krimiundkeks hat einen Löffel davon in Naturjoghurt gerührt und war sehr begeistert…\nDie Apfeltarte lauwarm oder komplett abgekühlt servieren.\n\n--- Table ---\nMürbeteig:\n200g Mehl\n100g kalte Butter in kleinen Würfeln\n50g Zucker\n1 EL kaltes Wasser\nZimt\nSalzkaramell:\n125g Zucker\n200ml Sahne\n2 EL Butter\nMeersalz nach Geschmack\nBelag:\n2 mittelgroße, säuerliche Äpfel\netwa 2 EL geröstete und gesalzene Erdnüsse\n", + "Bao Buns mit Champignons.txt": "\n--- Slide 1 ---\nBao Buns mit Champignons\nMit Karotten, Gurken und Edamame\nZutaten 2 Personen\nFertige Bao Buns (TK)\nEdamame in Schale\nGurke\nRöstzwiebeln\nMayonnaise\nTonkatsu Soße\nFür die Karotten:\n2 Karotten\n50ml Wasser\n50ml Branntweinessig\n½ TL Salz\n1 TL Zucker\nFür die Champignons:\n400g Champignons\n4 EL Sojasauce\n1 EL Reisessig\n1 EL Agavendicksaft\n1 Knoblauchzehe\nChilipulver\nKarotten schälen und in feine Stifte hobeln. Zutaten der Marinade (50ml Wasser, 50ml Essig, ½ TL Salz, 1 TL Zucker) in einen Topf geben und aufkochen. Karotten dazugeben und für 30 Sekunden köcheln lassen. Herd ausschalten und Karotten weiter marinieren lassen.\nChampignons in Scheiben schneiden.\nZutaten der Marinade (4EL Sojasauce, 1 EL Reisessig, 1 EL Agavendicksaft, 1 Knoblauchzehe, Chilipulver) vermischen und über die Champignons geben.\nGurken in feine Stifte hobeln.\n2 Töpfe mit Wasser für Edamame und Bao Buns aufsetzen.\nEadmame in das kochende Wasser geben.\nBao Buns mit dem Dampfeinsatz in den Topf geben, mit Deckel dämpfen lassen.\nChampignons mit Marinade heiß braten.\n Dauer: gute 5 Minuten\nBao Buns herausnehmen und mit\nChampignons\nGurke\nKarotte\nRöstzwiebeln\nMayonaise\nTonkatsu Soße füllen\nEdamame abgießen und salzen.\n1\n2\n3\n4\n5\n", + "Blauchbeer.txt": "Blauchbeer-Ricotta-Tarte (zimtkeksundapfeltarte)\nFür den Teig Mehl, Puderzucker und kalte Butter mit den Mixer zu eine krümeligen Teig verrühren, dann Ei und 2 EL kaltes Wasser einarbeiten und mit den Händen schnell zu einem glatten Teig verkneten. Für 30 Minuten in Folie gewickelt in den Kühlschrank legen.\nBackofen auf 180 °C (160 °C Umluft) vorheizen. Den Mürbteig nach der Ruhezeit ausrollen und in die gefettete Form geben. Mit einer Gabel mehrfach einstechen, dann für 15 Minuten vorbacken.\nInzwischen die Eier, den Zucker und Vanillemark schaumig schlagen. Ricotta zufügen, weiterrühren, zuletzt Zitronensaft und -schale unterschlagen. Die Masse auf den vorgebackenen Mürbteig streichen und für weitere 45 Minuten backen. In der Form auskühlen lassen, dann mit den Beeren belegen. Mit etwas Puderzucker bestäubt servieren.\n\n--- Table ---\nFür den Mürbteig\n220 g Mehl\n100 g kalte Butter\n50 g Puderzucker\n1 Ei\n2 EL kaltes Wasser\nBelag\n500 g Ricotta\n2 Eier\nMark einer Vanilleschote\nSaft- und Schale einer Bio-Zitrone\n150 g Zucker\nCa. 300 g Heidelbeeren (oder andere Beeren Eurer Wahl)\n", + "Blätterteigpäckchen mit Spargel.txt": "[.jpg file: Blätterteigpäckchen mit Spargel.jpg]", + "Bounty®-Kuchen Rezept _ Dr. Oetker.txt": "\n--- Page 1 ---\nBounty®-Kuchen Rezept | Dr. Oetker https://www.oetker.de/rezepte/r/bounty-kuchen \n1 von 4 24.08.2024, 12:25 \n\n--- Page 2 ---\nBounty®-Kuchen Rezept | Dr. Oetker https://www.oetker.de/rezepte/r/bounty-kuchen \n2 von 4 24.08.2024, 12:25 \n\n--- Page 3 ---\nBounty®-Kuchen Rezept | Dr. Oetker https://www.oetker.de/rezepte/r/bounty-kuchen \n3 von 4 24.08.2024, 12:25 \n\n--- Page 4 ---\nBounty®-Kuchen Rezept | Dr. Oetker https://www.oetker.de/rezepte/r/bounty-kuchen \n4 von 4 24.08.2024, 12:25 \n", + "Brezen Cracker.txt": "Honig-Senf-Brezen-Cracker (toertchenmadeinberling)\nDie Brezeln in kleine Stücke schneiden.\nSenf, Honig, Öl, Essig, Salz, Paprika und Pfeffer in eine Schüssel geben und mit einem Schneebesen verrühren.\nDie Soße über die Brezelstücke geben, gut verrühren und die Brezelstücke auf einem mit Backpapier belegten Blech verteilen.\nIm vorgeheiztem Ofen bei 160° C Ober-/ Unterhitze im unteren Drittel für ca. 30 Minuten backen. Danach die Brezelstücke durchwenden, den Ofen auf 100° C runterdrehen und weitere 30 Minuten backen.\nDie Brezelstücke aus dem Ofen nehmen und abkühlen lassen. Die Stücke werden erst hart, wenn sie abgekühlt sind.\n\n--- Table ---\n2 „alte“ Brezeln\n50 g Dijonsenf\n25 g Honig\n25 ml Sonnenblumenöl\n15 ml Apfelessig\n1/2 Tl Salz\n1/2 Tl Paprikapulver\n1/8 Tl Pfeffer\n", + "Champignons Weihnachtsmarkt.txt": "\n--- Page 1 ---\n2 10 Minuten 10 Minuten \nErnährung: 26 Kalorien, 0,3 fett \nChampignons Weihnachtsmarkt mit Knoblauchsauce \n\n--- Page 2 ---\nZUTATEN \nANLEITUNG \nZutaten: \nFirefox https://ayseskochblog.de/champignons-weihnachtsmarkt-mit-knoblau... \n2 von 3 08.11.2024, 14:29 \n\n--- Page 3 ---\nTIPP: \nTipp: du kannst die Knoblauchsauce auch mit ein wenig Tabasco verfeinern. \nFirefox https://ayseskochblog.de/champignons-weihnachtsmarkt-mit-knoblau... \n3 von 3 08.11.2024, 14:29 ", + "Chili sin Carne.txt": "Chili sin Carne (daily vegan)\n8 Portionen\nEtwas Öl in einer Pfanne erhitzen und das trockene Sojagranulat scharf darin abraten.\nParallel dazu in einem großen Topf etwas Öl erhitzen und die Zwiebeln darin glasig braten. Den Knoblauch und das angebratene Sojagranulat dazu geben. Mit dem Salz, Pfeffer, Koriander und Kreuzkümmel würzen und die Chilis dazu geben. Alles zusammen unter Rühren kurz anbraten.\nPizzatomaten, Tomatenmark und 1 l Brühe einrühren und ca. 15 Minuten köcheln, dabei ab und zu umrühren.\nAnschließend auch Mais, Paprika, Kidney-Bohnen, Oregano und Schokolade dazu geben und weitere 15 Minuten köcheln.\nTipps:\nMein veganes Chili con Carne lässt sich sehr gut zu Beginn einer Party oder in den nächsten Tag wieder aufwärmen. Dadurch ist es auch bestens geeignet für Meal Prep.\nBesonders gut schmeckt veganes Chili Sin Carne mit Brot oder Reis.\nIhr könnt das Chili übrigens auch (portioniert) einfrieren und bei Bedarf wieder auftauen.\n\n--- Table ---\n200 g Sojagranulat\n4 Zwiebeln, gewürfelt\n3 Knoblauch-Zehen, gepresst\n2-4 scharfe Chilischoten, sehr fein gehackt (Menge je nach Geschmack)\n800 ml Pizza-Tomaten\n1 l Brühe \n2 Gläser oder Dosen Mais, abgetropft\n2 Gläser oder Dosen Kidneybohnen\n7 EL Tomatenmark \n2 grüne Paprika, gewürfelt1 Tafel\nvegane Zartbitter-Schokolade, in Stücken\n2 EL Kreuzkümmmel (Cumin)\n2 EL Oregano \n1 TL Koriander, gemahlen\n2 TL Salz\n2 TL Pfeffer\n", + "Crema catalana von Aurora.txt": "\n--- Page 1 ---\nRezept online aufrufen\nZutaten für 6 Portionen:\nFür die Creme:\n300 mlMilch\n200 mlSahne, sü\n2 Stück(e)Orange(n), Schale, unbehandelt\n1 Stange/nZimt\n½Vanilleschote(n), aufgeschlitzt\n5Ei(er), davon das Eigelb\n50 gZucker\nMuskat, gerieben\n6 ELZucker\nRezept von: Aurora\nCrema catalana\nDen Backofen auf 150°C vorheizen. Ein tiefes Backblech mit\nKüchenkrepp auslegen, 2 cm hoch mit heißem Wasser füllen und in\ndie mittlere Schiene des Backofens schieben.\nMilch mit süßer Sahne, der unbehandelten Orangenschale,\nZimtstange und der aufgeschlitzten Vanillestange in einen Topf\ngeben und zum Kochen bringen. Einmal aufkochen, den Topf vom\nHerd nehmen und Orangenschale, Vanillestange und Zimtstange\nherausnehmen. Etwas abkühlen lassen.\nEigelb mit Zucker mit dem Handrührgerät schaumig schlagen.\nAnschließend langsam die Milch-Sahne-Mischung darunter rühren\nund durch ein feines Sieb gießen. Die Eier-Milch-Masse vorsichtig in\nkleine ofenfeste Förmchen geben, dabei darauf achten, dass die\nOberfläche schaumfrei bleibt. Eventuell den Schaum mit einem\nLö�el abnehmen. Die Förmchen so in das Wasserbad stellen, dass\nsie zur Häl�e im Wasser stehen. Das ganze etwa 1 Stunde im\nBackofen stocken lassen. Die Förmchen aus dem Ofen nehmen,\nabkühlen lassen.\nVor dem Servieren: Den Grill des Backofens vorheizen oder den\nBackofen auf größte Leistung stellen. Die Creme mit einem Hauch\ngeriebenem Muskat und pro Förmchen 1 EL Zucker bestreuen. Den\nZucker unter dem Grill goldbraun karamellisieren lassen. Das sollte\nso schnell wie möglich geschehen, da die Creme selbst nicht heiß\nwerden soll.\nTipp:\nEinfacher erhält man die Zuckerkruste mit einer Lötlampe. Einfach\nmit der Flamme der Lötlampe über den Zucker streichen. Fertig.\nBei der \"originalen\" Crema catalana wird die Zuckerkruste mit\neinem speziellen glühendem spiralförmigen Eisen gebrannt.\nArbeitszeitca. 45 Minuten\nGesamtzeitca. 45 Minuten\nSchwierigkeitsgradnormal\nKcal p. P .ca. 285\nChefkoch Rezept: Crema catalana von Aurora https://www.chefkoch.de/rezepte/drucken/79651030530977/Crema-cata...\n1 von 1 30.05.2024, 16:07", + "Cremige Zucchini-Bohnenpfanne mit Erdnussbutter - Eat this!.txt": "\n--- Page 1 ---\nCremige Zucchini-Bohnenpfanne mit \nErdnussbutter \nJa, Resteverwertung kann auch Spaß machen! Diese schnelle und leckere, \ncremige Zucchini-Bohnenpfanne haben wir aus Kühlschrankresten und \ngeretteten Lebensmitteln zubereitet. \n10 Min. 15 Min. 25 Min. \nHauptgericht Afrikanisch 2 Portionen Nadine \nZutaten \nAnleitungen \n", + "Currytaschen.txt": "Currytaschen (zuckerjagswurst)\n30 Minuten | 8 Currytaschen\nZutaten\n0,5 Zwiebeln\n75g Lauch\n75g Möhren\n75g veganes Hack\n1,5TL Thailändisches rotes Curry\n100ml Sahne\n0,5TL Gemüsebrühe\n1,5EL Sojasauce\n1 Blätterteig\nZubereitung\nOfen auf 180°C Umluft vorheizen.\nZwiebeln würfeln & Lauch in feine Ringe schneiden.\nMöhren raspeln.\nZwiebeln anbraten. Hack dazugeben und 3-4 Minuten braten.\nLauch, Möhren und Curry dazugeben, weitere 3-4 Minuten braten.\n¾ der Sahne dazugeben, mit Gemüsebrühe und Sojasauce würzen.\nPfanne vom Herd nehmen.\nBlätterteig in 8 Quadrate schneiden. Füllung auf den Teig geben und Taschen verschließen.\nOberseite mit Sahne bestreichen.\n10-12 Minuten backen.\n", + "Donauwelle - super easy von alina1st.txt": "\n--- Page 1 ---\nRezept online aufrufen\nZutaten für 1 Portionen:\nFür den Teig:\n3Ei(er)\n175 gZucker\n175 mlÖl\n175 mlMilch\n350 gMehl\n3 TLBackpulver\n1 ELKakaopulver\n1 ELMilch\n1 GlasSauerkirschen, abgetrop�\nFett für das Blech\nFür die Creme:\n1 Pck.Vanillepuddingpulver\n400 mlMilch\n2 ELZucker\n250 gButter\nFür die Glasur:\n200 gZartbitterschokolade\n2 ELNutella\n1 ELÖl\nRezept von: alina1st\nDonauwelle - super easy\nhier kann eigentlich gar nichts mehr schief\ngehen...\nFür ein tiefes Backblech von ca. 30 x 40 cm. Geringfügig kleiner ist\nauch möglich.\nZuerst die Butter aus dem Kühlschrank nehmen und in Stücken in\neine Schüssel geben, damit sie schön weich werden kann.\nDann das Puddingpulver mit 50 ml Milch und dem Zucker glatt\nrühren, den Rest der Milch aufkochen. Das angerührte\nPuddingpulver einrühren, nochmals aufkochen lassen und sofort in\neine Schüssel umgießen. Frischhaltefolie direkt auf den warmen\nPudding legen, so dass möglichst keine Lu�blasen zwischen\nPudding und Folie entstehen. Den Pudding nun bei\nRaumtemperatur auskühlen lassen, bitte nicht kalt stellen!\nEin tiefes Blech (ich nutze ein 30 x 40 cm Blech, auf das ich einen\nBackrahmen stelle) dünn einfetten und mit Backpapier belegen, so\ndass das Papier gut anliegt. Den Ofen auf 180 °C Ober-/Unterhitze\nvorheizen.\nDie Eier mit dem Zucker dick-cremig aufschlagen und langsam das\nÖl zugießen. Anschließend Mehl und Backpulver mischen und\nabwechselnd mit der Milch unter die Eiercreme rühren. Die Häl�e\ndes Teiges auf das Blech streichen. Den übrigen Teig mit Kakao und\nMilch verrühren und vorsichtig darüber streichen. Die Kirschen\ndarauf verteilen.\nDen Kuchen im vorgeheizten Ofen ca. 25 - 30 min. backen.\nAnschließend das Backpapier vom Blech auf einen Rost ziehen und\nauskühlen lassen.\nDie inzwischen zimmerwarm gewordene Butter cremig rühren. Die\nFolie vom ebenfalls zimmerwarm gewordenen Pudding abnehmen\nund nun lö�elweise den Pudding unter die Butter rühren. Falls der\nKuchenboden noch warm ist, die Creme evtl. so lange kalt stellen.\nDanach die Creme gleichmäßig auf den Kuchen streichen. Wieder\nkalt stellen.\nDie Zartbitterschokolade im Wasserbad unter Rühren schmelzen\nlassen, dann Nutella und Öl darunter rühren, so dass ein recht\nflüssiger Schokoladenguss entsteht. Den Guss gleichmäßig auf\ndem Kuchen verteilen und mit einer Teigkarte oder einem Messer\nChefkoch Rezept: Donauwelle - super easy von alina1st https://www.chefkoch.de/rezepte/drucken/563541154614076/Donauwel...\n1 von 2 30.05.2024, 16:34\n--- Page 2 ---\nauf der Creme glattstreichen.\nAm besten im Kühlschrank fest werden lassen.\nArbeitszeitca. 1 Stunde\nKoch-/Backzeitca. 35 Minuten\nGesamtzeitca. 1 Stunde 35 Minuten\nSchwierigkeitsgradsimpel\nKcal p. P .ca. 7914\nChefkoch Rezept: Donauwelle - super easy von alina1st https://www.chefkoch.de/rezepte/drucken/563541154614076/Donauwel...\n2 von 2 30.05.2024, 16:34", + "Focaccia.txt": "\n--- Page 1 ---\nRezept online aufrufen\nZutaten für 8 Portionen:\n500 gMehl\n20 gHefe, frische\n375 mlWasser\n1 TLZucker\n1 TLSalz\n5 ELOlivenöl\nMehl, für die Arbeitsfläche und\nBleche\nRosmarin oder Thymian\nFocaccia\nMehl in eine große Schüssel sieben, in die Mitte eine Mulde\ndrücken. Hefe und Zucker in 3/8 l lauwarmem Wasser auflösen und\nin die Mulde gießen. Zugedeckt 30 Minuten gehen lassen.\n3 EL Olivenöl und Salz unterrühren und den Teig gut durchkneten\n(dauert ca. 10 Minuten). Zugedeckt nochmals gehen lassen, ca. 1\nStunde. Backofen auf 220° C vorheizen. Den Teig halbieren und zu\ndicken Fladen ausrollen, diese auf die bemehlten Bleche legen. Mit\nden Fingern Dellen in die Oberfläche drücken. Ein weiteres Mal\nzudecken und eine weitere halbe Stunde gehen lassen. Mit dem\nrestlichen Öl bestreichen und dann nach Wahl mit Rosmarinnadeln\noder getrocknetem Thymian bestreuen. Auf der mittleren Schiene\nca. 15 - 20 Minuten backen (Umlu� 200! C).\nArbeitszeitca. 30 Minuten\nRuhezeitca. 2 Stunden\nGesamtzeitca. 2 Stunden 30 Minuten\nSchwierigkeitsgradnormal\nKcal p. P .ca. 273\nChefkoch Rezept: Focaccia https://www.chefkoch.de/rezepte/drucken/405741129901058/Focaccia.html\n1 von 1 30.05.2024, 16:09", + "Galaktoboureko - griechischer Grießauflauf.txt": "\n--- Page 1 ---\nRezept online aufrufen\nZutaten für 4 Portionen:\nFür die Creme:\n50 gWeichweizengrieß\n1 Pck.Vanillepuddingpulver\n700 mlMilch, kalt\n4Ei(er)\n125 gZucker\n2 ELButter, weich\nFür den Teig:\n500 gBlätterteig\n65 gButter\nFür den Sirup: :\n375 gZucker\n250 mlWasser, lauwarm\n1 TLZimt\n1Zitrone(n), ausgepresst\nGalaktoboureko - griechischer\nGrießauflauf\nDen Sirup sollte man zuerst zubereiten, da er kalt sein muss, bevor\nman ihn auf den Auflauf gießt.\nGrieß und Vanillepuddingpulver mit etwas kalter Milch anrühren.\nRestliche Milch aufkochen lassen, Grieß-/Puddingpulvermischung\nunterrühren.\nEier mit Zucker und flüssiger Butter verrühren und unter die\nGrießmasse rühren.\nEine rechteckige Auflau�orm mit Butter ausstreichen, die Häl�e\ndes Blätterteigs hineinlegen, Creme darauf verteilen und mit\nrestlichem Blätterteig abdecken. Den Blätterteig mit einem Messer\nan der Oberfläche rautenförmig einschneiden, mit ein wenig\nWasser bespritzen und bei 180 °C auf der untersten Schiene ca. 30\nMinuten goldgelb backen.\nFür den Sirup Feinkristallzucker mit Wasser aufkochen und solange\nköcheln lassen, bis sich dieser aufgelöst hat. Dann Zimt und\nZitronensa� dazugeben. Sirup auskühlen lassen.\nHeißen Grießauflauf aus dem Backrohr nehmen und sofort mit\nabgekühltem Sirup übergießen.\nArbeitszeitca. 30 Minuten\nKoch-/Backzeitca. 50 Minuten\nGesamtzeitca. 1 Stunde 20 Minuten\nSchwierigkeitsgradnormal\nChefkoch Rezept: Galaktoboureko - griechischer Grießauflauf https://www.chefkoch.de/rezepte/drucken/1048301209719211/Galakto...\n1 von 1 30.05.2024, 16:10", + "Gebratene Kasknödel von felidae177.txt": "\n--- Page 1 ---\nRezept online aufrufen\nZutaten für 4 Portionen:\n250 gBrötchen oder Brot, altbacken\n300 mlMilch zum Einweichen\n2Ei(er)\n2 ELMehl\n200 gBergkäse oder Gruyere\nn. B.Salz und Pfe�er\nn. B.Muskat\netwasKümmel, gerieben, optional\n1 großeZwiebel(n), in Würfel geschnitten\n½ BundPetersilie\n3 ELSemmelbrösel\n1 ELButter\netwasPflanzenöl\nRezept von: felidae177\nGebratene Kasknödel\nKäseknödel, prima als Beilage für Pilze in\nSahnesoße oder Geschnetzeltes\nDas Brot oder Brötchen in kleine Stücke schneiden, in eine große\nSchüssel füllen und mit warmer Milch übergießen. Mit den Händen\netwas mischen und ca. 20 - 30 Minuten einweichen. Die\nMilchmenge hängt davon ab, wie trocken das Brot ist, 300 bis 400\nml Milch sollten reichen. Man kann auch nach einiger Zeit noch\netwas angießen, falls es zu wenig ist.\nDie Zwiebel schälen und in Stücke schneiden. In Butter andünsten,\nnicht bräunen, und auf die geweichte Brotmasse geben. Den Käse\nklein würfeln und mit den Eiern ebenfalls zur Masse geben.\nDie Petersilie waschen, trocken schütteln und klein hacken.\nZusammen mit Salz, Pfe�er, Muskat und evtl. etwas geriebenem\nKümmel in die Schüssel geben und die Masse nun gut mit den\nHänden durcharbeiten, probieren und ggf. nachwürzen. Die\nFeuchtigkeit reguliert man mit 2 bis 3 EL Mehl. Der Teig sollte\nweich, aber nicht zu flüssig werden. Ggf. kann man ihn\nanschließend noch 15 Minuten ruhen lassen.\nDie Semmelbrösel in einen tiefen Teller füllen und in einer großen,\nbreiten Pfanne einen EL Butter und etwas neutrales Pflanzenöl\nerhitzen. Mit feuchten Händen - wichtig, sonst klebt es - aus jeweils\nca. 1 - 2 EL Masse kleine Buletten formen, in den Semmelbröseln\nwälzen und im Fett von beiden Seiten braten. Dazu das Fett nicht zu\nheiß werden lassen, lieber etwas länger backen.\nDie heißen Kasknödel rasch mit Rahmsoße, Pilzen oder\nGeschnetzeltem servieren. Wenn man sie anschneidet läu� immer\netwas vom würzigen Käse aus, das schmeckt sehr lecker.\nSie schmecken am nächsten Tag auch kalt im Büro, aber heiß und\nknusprig sind sie natürlich am Besten.\nArbeitszeitca. 30 Minuten\nRuhezeitca. 35 Minuten\nKoch-/Backzeitca. 20 Minuten\nGesamtzeitca. 1 Stunde 25 Minuten\nSchwierigkeitsgradnormal\nKcal p. P .ca. 585\nChefkoch Rezept: Gebratene Kasknödel von felidae177 https://www.chefkoch.de/rezepte/drucken/2656011417082549/Gebrate...\n1 von 2 30.05.2024, 16:12\n--- Page 2 ---\nChefkoch Rezept: Gebratene Kasknödel von felidae177 https://www.chefkoch.de/rezepte/drucken/2656011417082549/Gebrate...\n2 von 2 30.05.2024, 16:12", + "Gedeckter Apfelkuchen mit Streuseln von mbus.txt": "\n--- Page 1 ---\nRezept online aufrufen\nZutaten für 1 Portionen:\nFür den Mürbeteig:\n300 gMehl\n100 gButter\n100 gZucker\n1 TLBackpulver\n1 Pkt.Vanillinzucker\n2Ei(er)\nFür die Füllung:\n1.500 gÄpfel, möglichst saure\n50 gButter\n50 gRosinen\n50 gZucker\n1 Pkt.Vanillepuddingpulver\nFür die Streusel:\n150 gMehl\n100 gButter\n100 gZucker\nRezept von: mbus\nGedeckter Apfelkuchen mit\nStreuseln\nMehl, Butter, Zucker, Backpulver, Vanillinzucker und Eier zu einem\nMürbeteig verarbeiten, diesen dann 30 Min. ruhen lassen.\nDie Äpfel schälen, achteln und dann in Würfel schneiden. Dabei das\nKerngehäuse entfernen. Zusammen mit der Butter, dem Zucker\nund den Rosinen in einen Topf geben und solange erhitzen, bis die\nApfelstückchen anfangen zu verkochen und eine festere Masse\nentsteht. Dann zur Seite stellen.\nEinen Vanillepudding nach Packungsangabe zubereiten. Evtl.\nwerden dafür weiter Zutaten benötigt (Milch). Den Pudding\nabkühlen lassen, jedoch nur soweit, dass er nicht ganz fest ist\nsondern dickflüssig.\nEine Springform einfetten und den Mürbeteig darin ausrollen.\nEinen Rand ca. 3 cm hochziehen, damit die Masse nicht ausläu�.\nNun erst den Pudding auf den Teig geben und gleichmäßig\nverteilen. Dann die Apfelmasse darauf geben und vorsichtig\nverteilen.\nAus dem Mehl, der Butter und dem Zucker einen Krümelteig\nerstellen und oben auf die Apfelfüllung geben.\nBei 160 Grad für ca. 60 Minuten in den vorgeheizten Backofen\ngeben. Damit der Kuchen auch braun wird, drehe ich den Backofen\n10 Minuten vor dem Ende noch einmal auf 200 Grad hoch, aber\ndabeistehen, sonst wird er vielleicht zu braun.\nWer mag, kann auch den Äpfeln Zimt zusetzen, wir mögen jedoch\nkeinen Zimt.\nArbeitszeitca. 45 Minuten\nKoch-/Backzeitca. 1 Stunde\nGesamtzeitca. 1 Stunde 45 Minuten\nSchwierigkeitsgradsimpel\nKcal p. P .ca. 5707\nChefkoch Rezept: Gedeckter Apfelkuchen mit Streuseln von mbus https://www.chefkoch.de/rezepte/drucken/305151110793264/Gedeckte...\n1 von 1 30.05.2024, 16:36", + "Gemüse nach Gonbao-Art von zuiko.txt": "\n--- Page 1 ---\nRezept online aufrufen\nZutaten für 3 Portionen:\nFür die Sauce:\n2 TL, gehäu�Speisestärke\n3 ELWasser, kalt\n3 ELZucker\n3 ELReisessig\n3 ELSojasauce, hell\n2 ELSojasauce, dunkel\n1 Zehe/nKnoblauch, fein gehackt\nFür das Gemüse:\n2Paprikaschote(n)\nn. B.Chilischote(n)\n500 gChampignons\n1 BundFrühlingszwiebel(n)\n1 HandvollErdnüsse, geröstet\nSonnenblumenöl zum Braten\nSalz und Pfe�er\nRezept von: zuiko\nGemüse nach Gonbao-Art\nvegetarisches Wok-Gericht mit dunkler,\npikanter Sauce\nFür die Gonbao-Sauce die Speisestärke in 3 EL kaltem Wasser\nauflösen. Die übrigen Zutaten für die Sauce dazugeben und alles\nmiteinander verrühren.\nDie Champignons vierteln, Frühlingszwiebeln in Ringe schneiden,\nPaprikaschoten in Würfel schneiden, Chilischoten fein hacken.\nIm Wok oder einer hohen Pfanne etwas Sonnenblumenöl erhitzen\nund zunächst die Paprika sowie die fein gehackten Chilischoten bei\nmittlerer bis hoher Hitze anbraten. Nach ungefähr 3 Minuten die\nChampignons dazugeben. Alles mit Pfe�er und Salz würzen und\nweitere 5 Minuten unter ständigem Rühren anbraten.\nDie Gonbao-Sauce sowie die Frühlingszwiebeln in den Wok geben.\nDie Sauce einmal aufkochen lassen. Dann die Hitze reduzieren und\ndie gerösteten Erdnüsse dazugeben. Das Gemüse solange weiter\nbraten, bis es gar, aber noch bissfest ist.\nDas Gemüse nach Gonbao-Art mit gekochtem Reis oder\nchinesischen Nudeln servieren.\nArbeitszeitca. 15 Minuten\nKoch-/Backzeitca. 15 Minuten\nGesamtzeitca. 30 Minuten\nSchwierigkeitsgradnormal\nChefkoch Rezept: Gemüse nach Gonbao-Art von zuiko https://www.chefkoch.de/rezepte/drucken/3676711553884546/Gemues...\n1 von 1 30.05.2024, 16:13", + "Griessnockerl (Suppeneinlage) von jawi.txt": "\n--- Page 1 ---\nRezept online aufrufen\nZutaten für 6 Portionen:\n30 gFett oder Butter\n1Ei(er)\nSalz\n70 gGrieß\n1 LiterSuppe\nRezept von: jawi\nGriessnockerl (Suppeneinlage)\nDas Fett schaumig rühren, das Ei dazurühren, ebenso den Grieß\nund das Salz. Die Masse 5 Minuten stehen lassen. Mit 2 Esslö�eln\nNockerln formen. Die Nockerl in siedendem Salzwasser oder auch\ngleich in der Suppe 5 Minuten kochen lassen und danach ca. 15\nMinuten ziehen lassen.\nArbeitszeitca. 35 Minuten\nGesamtzeitca. 35 Minuten\nSchwierigkeitsgradnormal\nChefkoch Rezept: Griessnockerl (Suppeneinlage) von jawi https://www.chefkoch.de/rezepte/drucken/47781016275357/Griessnock...\n1 von 1 30.05.2024, 16:15", + "Grießbrei von pilotilka.txt": "\n--- Page 1 ---\nRezept online aufrufen\nZutaten für 2 Portionen:\n½ LiterMilch\n5 ELGrieß\n3 ELZucker\n1 ELKakaopulver oder etwas Zimt\nRezept von: pilotilka\nGrießbrei\nsehr beliebt bei Kindern\nEinen kleinen Teil der Milch in eine Rührschüssel geben, den Rest\naufkochen. Grieß und Zucker in die Rührschüssel geben, gut\nverrühren. Wenn die Milch aufkocht, die Flüssigkeit aus der\nRührschüssel dazugeben und unterrühren.\nUnter ständigem Rühren aufkochen und ein paar Minuten kochen\nlassen. Auf dem Teller mit etwas Kakaopulver oder\nZimtzuckergemisch bestreuen.\nArbeitszeitca. 10 Minuten\nGesamtzeitca. 10 Minuten\nSchwierigkeitsgradsimpel\nKcal p. P .ca. 398\nChefkoch Rezept: Grießbrei von pilotilka https://www.chefkoch.de/rezepte/drucken/1086221214500502/Griessbre...\n1 von 1 30.05.2024, 16:14", + "Gulasch.txt": "Gulasch (vollveggie)\n6 Portionen\nEine große Rührschüssel auf eine Küchenwaage stellen, alle Zutaten bis auf das Sojafleisch darin abwiegen bzw. abmessen und mit einem Schneebesen gut durchrühren.\nNun das trockene Sojafleisch zu der Marinade in die Schüssel geben und mit einem Rührlöffel gut unterheben. Zum Ziehen entweder in der Schüssel belassen, abdecken und immer mal wieder durchrühren. Oder aber die Masse in einen Plastikbeutel geben, verschließen und immer mal wieder gut durchkneten. Damit die Marinade besonders gut in das Sojafleisch einziehen kann, mindestens 12 Stunden, am besten über Nacht, einweichen lassen.\nDie Zwiebeln schälen und in feine Würfel schneiden. Die Knoblauchzehen ebenfalls schälen und entweder in feine Würfel schneiden oder mit einer Knoblauchpresse direkt in den Topf pressen – die Knoblauchzehen werden aber erst später untergehoben, damit sie nicht verbrennen oder bitter werden. Paprikaschote waschen, halbieren, entkernen und in feine Würfel oder Streifen schneiden.\nDas Olivenöl in einem großen Topf erhitzen und darin die über Nacht eingelegten Sojawürfel (falls erforderlich, diese vorher etwas ausdrücken) samt eventuell vorhandener Restmarinade für 5 Minuten auf hoher Stufe scharf anbraten, zwischendurch mit einem Kochlöffel immer mal wieder kräftig umrühren. Keine Sorge: Wenn sich etwas am Topfboden ansetzen sollte, lässt sich das spätestens nach der Flüssigkeitszugabe gut lösen und einrühren.\nDie Zwiebel- und Paprikawürfel hinzugeben und weitere 5 Minuten scharf mitbraten, zwischendurch immer mal wieder kräftig umrühren. Dann den Herd auf mittlere Stufe herunterdrehen. Danach das Tomatenmark unterrühren, kurz anschwitzen lassen und die Knoblauchwürfel unterheben oder mit einer Knoblauchpresse direkt in den Topf pressen.\nDie Gewürze Paprika edelsüß und scharf, Majoran und Thymian hinzugeben, kräftig unterrühren und kurz mitbraten; eine sogenannte Feinwaage eignet sich übrigens perfekt zum Abwiegen von Gewürzen und Kräutern. Auch hier keine Sorge: Wenn sich etwas am Topfboden ansetzen sollte, lässt sich das spätestens nach der Flüssigkeitszugabe gut lösen und einrühren.\nDann das Mehl bzw. Speisestärke oder Saucenpulver unterheben. Kurz ziehen lassen. Anschließend mit dem Rotwein, alternativ Kirschsaft oder Gemüsebrühe, vorsichtig ablöschen, kräftig umrühren und kurz simmern lassen. Auch hier wieder keine Sorge: Wenn sich etwas am Topfboden ansetzen sollte, lässt sich das spätestens nach der Flüssigkeitszugabe gut lösen und einrühren.\nDas Gulasch nun etwa 30 Minuten zugedeckt bei kleiner Stufe köcheln lassen und das Wasser dabei portionsweise unterrühren, bis die gewünschte Konsistenz erreicht ist. Dabei immer mal wieder kräftig durchrühren. Mit Zucker, Salz und Pfeffer sowie nach Belieben mit den anderen Gewürzen abschmecken. Das Gulasch dickt übrigens noch nach, besonders am nächsten Tag; dann einfach etwas Rotwein, Wasser, Kirschsaft oder Gemüsebrühe bis zur gewünschten Konsistenz nachgießen.\n\n--- Table ---\nMarinade:\n70 Sojasauce\n40g Worcestershiresauce\n50 g Tomatenmark\n30g Balsamico\n20g Olivenöl\n15g Senf, mittelscharf\n15 g Agavendicksaft\n1 Msp. Sambal Oelek\n10g Paprikapulver edelsüß\n½ TL Paprikapulver, rosenscharf\n500ml Wasser, heiß\n250g Sojafleisch (Medaillions)\n\nGulasch:\n175g Zwiebeln\n200g Paprika, rot\n2 Knoblauchzehen\n3EL Olivenöl\n3EL Tomatenmark\n1TL Paprikapulver, edelsüß\n½ TL Paprikapulver, rosenscharf\n1TL Majoran\n1TL Thymian\n6EL Weizenmehl\n500ml Rotwein, trocken\n1200ml Wasser\n1Prise Rohrzucker\n", + "Heidelbeer.txt": "Heidelbeer-Tiramisu\n8 Portionen\nBio-Zitrone heiß abwaschen, abtrocknen, Schale abreiben und Saft auspressen. Zitronensaft beiseite stellen.\nHeidelbeeren waschen, verlesen und 400 g abwiegen. Heidelbeeren zusammen mit der Hälfte der Zitronenschale und Gelierzucker in einen Topf geben. Aufkochen. Mit einem Kartoffelstampfer die Beeren zerquetschen. Köcheln, bis die Gelierprobe gut ist.\nSahne mit Vanillezucker steif schlagen. Mascarpone mit Joghurt, Diamant Feinster Zucker, restlicher Zitronenschale und Zitronensaft glatt rühren. Zuletzt Sahne vorsichtig unterheben.\nOrangen auspressen. Löffelbiskuits kurz in den gepressten Orangensaft tauchen und den Boden einer rechteckigen Auflaufform (ca. 15 x 30 cm) damit auslegen. Darauf vorsichtig erst die Hälfte der Heidelbeersoße, dann die Hälfte der Cremeschicht verteilen. So fortfahren, bis alle Zutaten verbraucht sind. Die letzte Lage sollte aus Creme bestehen. Mindestens 5 Stunden im Kühlschrank durchziehen lassen.\nHeidelbeeren waschen und verlesen, Schokolade raspeln. Tiramisu mit Heidelbeeren und Schokoladenraspeln verzieren.\n\n--- Table ---\nHeidelbeersoße:\n1 Zitrone Bio\n400g Heidelbeeren\n150g Gelierzucker 2:1\n\nCreme:\n250g Sahne\n1 Päckchen Vanillezucker\n500g Mascarpone\n300g Joghurt\n80g Zucker\n\nKeksschickt:\n1-2 Orangen\n250g Löffelbiskuits\n\nDeko:\n50g Heidelbeeren\n25g weiße Schokolade\n", + "Kidneybohnen.txt": "Kidneybohnen-Brownies\nZutaten\nZunächst die Kidneybohnen mit einem Mixer oder einer Küchenmaschine pürieren, anschließend mit den restlichen Zutaten (bis auf die Zartbitterschokolade) zu einer cremigen Masse verrühren.\nDie gehackte Schokolade unterheben und die Masse dann in eine mit Backpapier ausgelegte bzw. gefettete Form gegeben und im vorgeheizten Ofen bei 200 Grad Umluft für ca. 15-20 Minuten gebacken.\n\n--- Table ---\n1 Dose Kidneybohnen\n30 g gemahlene Mandeln\n2 Eier\n50 g Kakaopulver\n100 g Zucker oder Zuckerersatz (z.B. 100 g Xylit oder 125 g Erythrit)\n2 EL neutrales Öl (z.B. Rapsöl)\n1 Pck. Vanillezucker\n1/2 Pck. Backpulver\n100 g gehackte Zartbitterschokolade\n", + "LEBKUCHEN-MOUSSE MIT GLÜHWEINKIRSCHEN.txt": "\n--- Page 1 ---\nLEBKUCHEN-MOUSSE MIT GLÜHWEINKIRSCHEN https://transglobalpanparty.com/lebkuchen-mousse-gluehweinkirschen/ \n1 von 2 24.08.2024, 12:41 \n--- Page 2 ---\nLEBKUCHEN-MOUSSE MIT GLÜHWEINKIRSCHEN https://transglobalpanparty.com/lebkuchen-mousse-gluehweinkirschen/ \n2 von 2 24.08.2024, 12:41 ", + "Lachs in Thai-Curry-Soße mit Zucchini.txt": "\n--- Slide 1 ---\nLachs in Thai-Curry-Soße mit Zucchini\nGoldenem Reis und eingelegten roten Zwiebelb\nZutaten 2 Personen\n200g Lachsfilet\n150g Jasminreis\n1 Zucchini\n1 Knoblauchzehe\n1 Zwiebel\n180ml Kokosmilch\n25g rote Currypaste\n2g Currygewürz\n4g Brühepulver\n25g Frühlingszwiebeln\n1 rote Zwiebel\nWas Du zu Hause haben solltest\n300ml Wasser\n1/4TL Weißweinessig\nSalz\nPfeffer\nZucker\n25 – 35 Minuten\nErhitze 300 ml Wasser im Wasserkocher.\nIn einen kleinen Topf mit Deckel 300 ml heißes Wasser füllen.\nHälfte „Hello Curry“ und 0,25 TL Salz dazugeben und aufkochen lassen.\nReis zugeben und bei niedriger Hitze 10 Min. abgedeckt köcheln lassen.\nVom Herd nehmen und abgedeckt 10 Min. ziehen lassen.\nGelbe Zwiebel grob würfeln.\nRote Zwiebel in feine Streifen schneiden.\nFrühlingszwiebel in feine Ringe schneiden.\nKnoblauch fein hacken.\nZucchini längs halbieren und in 0,5 cm Halbmonde schneiden.\nIn einem kleinen Topf 50 ml Wasser, 50 ml Essig, Salz und Zucker aufkochen.\nZwiebelstreifen hinzugeben und 30 Sek. köcheln lassen. Von der Hitze nehmen und marinieren lassen.\nLachsfilets rundum salzen.\nIn einer großen Pfanne 1 EL Öl erhitzen. Lachs darin auf der Hautseite 2 Min. anbraten, dann wenden und weitere 2 – 3 Min. braten, bis der Fisch innen nicht mehr glasig ist.\nLachs herausnehmen und nach Belieben die Haut abziehen.\nIn derselben Pfanne erneut 1 EL Öl erhitzen. Zwiebel, weißen Teil der Frühlingszwiebel und Zucchini darin 3 – 4 Min. anschwitzen.\nKnoblauch, rote Currypaste und restliches „Hello Curry“ darin 1 weitere Min. anbraten.\nPfanneninhalt mit Kokosmilch, 100 ml Wasser und Brühepulver ablöschen und 2 – 3 Min. köcheln lassen.\nSoße mit 0,25 TL Essig, Salz, Pfeffer und eine Prise Zucker abschmecken.\nLachs zurück in die Pfanne geben und warm halten, bis der Reis gar ist.\nReis und Thai-Curry auf tiefen Tellern verteilen. Lachs darauf anrichten und mit grünem Teil der Frühlingszwiebel und eingelegten roten Zwiebeln garnieren.\n1\n2\n3\n4\n5\n", + "Mango Limetten Triffle.txt": "Mango-Limetten-Triffle\nFür 2 Personen\nZutaten\n1 Limette\n2 Esslöffel Wasser heiss\n1 Teelöffel Puderzucker (1)\n50 g weisse Schokolade\n180 g griechischer Joghurt\n1 Esslöffel Puderzucker (2)\n4 Löffelbiscuits\n0.5 Mango klein\n1 Teelöffel Puderzucker (3)\nZubereitung\nDie Schale von ½ Limette fein abreiben. Den Saft der ganzen Limette auspressen.\n1 Esslöffel Limettensaft mit dem heissen Wasser und der ersten Portion Puderzucker (1) gut verrühren. Beiseitestellen.\nDie weisse Schokolade fein hacken. In eine Metallschüssel geben und ü ber einem heissen – nicht kochenden – Wasserbad schmelzen lassen. Dann den Joghurt, die zweite Portion Puderzucker (2), 1 Esslöffel Limettensaft sowie die abgeriebene Limettenschale unterrühren.\nDie Löffelbiskuits halbieren. Jeweils abwechselnd mit der Limettencreme in 2 Gläser schichten, dabei jede Lage Löffelbiskuits mit der Limettensaftmischung beträufeln. Mindestens 15 Minuten kalt stellen.\nDie Mango schälen und das Fruchtfleisch vom Stein schneiden. Dieses mit der dritten Portion Puderzucker (3) in einen hohen Becher geben und fein pürieren. Bis zum Servieren kühl stellen.\nVor dem Servieren das Mangopüree über die Triffles in den Gläsern verteilen.\n", + "One Pot Mie Nudeln.txt": "\n--- Slide 1 ---\nOne-Pot Mie Nudeln\nZutaten 2 Personen\n400ml Kokosmilch\n300ml Wasser\n3 EL Sojasoße\n2 EL Erdnussbutter\n1 EL Currypulver\n3 Nester Mie Nudeln\n½ Brokkoli\n3 Karotten\nKarotten schälen und schneiden.\nBrokkoli schneiden.\nKokosmilch, Wasser, Sojasoße, Erdnussbutter, Currypulver in eine hohe und große Pfanne geben. Unter rühren aufkochen.\nNudeln und Gemüse in die Pfanne geben. Deckel drauf und ca. 15 Minuten auf kleiner Flamme gar ziehen lassen. Ab und zu umrühren.\n1\n2\n3\nMit Kokos, Curry, Karotte und Brokkoli\n", + "One-Pot-Chili-Reispfanne-aus-dem-Ofen-3.txt": "\n--- Page 1 ---\n2 Paprika\n1 rote Zwiebel\n2 Knoblauchzehen\n60 g Olivenöl\n1 TL Cumin (Kreuzkümmel)\n1 TL Cayennepfeffer\n1 TL Salz\n1 TL Pfeffer\n1 EL Majoran\n1 TL Rosmarin\n1 TL Thymian\n1/2 TL Muskatnuss\n1/2 TL Curry\n2 EL Tomatenmark\n30 ml Ahornsirup\n220 g Basmatireis\n80 g Berglinsen \n400 g gehackte Tomaten aus\nder Dose\n600 ml Gemüsebrühe \n30 g Zartbitterschokolade\n 200 g Mais (Abtropfgewicht)\n200 g Kidneybohnen\n100 g ger. Cheddar\n 2 EL Pankomehl (Paniermehl)\n2 EL Petersilie\n4-6 Personen I 28 cm Ofenform Den Backofen auf 180 °C Umluft vorheizen. Paprika, Zwiebel\nund Knoblauch inStücke schneiden. In zwei Esslöffel\nOlivenöl scharf anbraten. Gewürze und Kräuter mischen\nund hinzugeben, kurz mitbraten. Ahornsirup hinzugeben\nund leicht karamellsieren lassen. \nReis und Berglinsen hinzugeben und mit Tomaten und\nBrühe ablöschen. Schokolade, Tomatenmark, Mais und\nKidneybohnen unterrühren. Die Hälfte des Käses ebenso\ngrob untergeben. \nWenn die Pfanne sich nicht für den Ofen eignet, Masse in\neine Ofenform geben. Im Ofen 20 Minuten backen. Den\nrestlichen Cheddar, Olivenöl, Panko- oder Paniermehl (oder\nein Toastbrot zerkleinert), Petersilie miteinander\nvermengen. Nach der Backzeit über die Masse geben und\nweiter 5-10 Minuten garen. Reis und Linsen sollten weich\nsein.\nGerne mit Creme Fraiche oder Schmand servieren.\nOne Pot Chili Reispfanne\naus dem Ofen\nSchnell gemacht, würzig und lecker\nSo wird's gemachtEinkaufsliste\nknusperstuebchen.net\nZeit\nVorbereitung | 5 Min\nKochen | 20-30 Min\nFertig in |30 Min \n", + "Orangentofu mit Reis.txt": "Orangentofu mit Reis\n2-3 Personen\nZutaten\nFür den Tofu:\n400g Tofu\n4EL Speisestärke\n1TL Paprikapulver edelsüß\nFür die Soße:\n6-8 Orangen für 250g Orangensaft\n1 Orange (Abrieb)\n1 Zwiebel\n5g Ingwer\n1 Knoblauchzehe\n0,5 TL Chiliflocken\n50g Rohrzucker\n1EL Reisessig\n2EL Sojasauce\n1TL Speisestärke\nZum Servieren:\nReis\nSesam\nFrühlingszwiebeln\nZubereitung\nDen Tofu in grobe Stücke zerteilen & in eine verschließbare Box geben. Speisestärke, Paprikapulver, Salz & Pfeffer dazugeben. Box verschließen und schütteln.\nÖl in einer Pfanne erhitzen. Tofu von allen Seiten anbraten, bis es außen goldbraun ist.\nIn der Zwischenzeit Orangensaft auspressen und Schale abreiben.\nZwiebel, Ingwer, Knoblauch schäle und fein hacken.\nTofu aus der Pfanne nehmen und beiseitestellen. Zwiebeln, Ingwer, Knoblauch und Chili anbraten. Großteil des Orangensafts, Zucker, Reisessig, Sojasauce und Orangenabrieb dazugeben und ca. 5 Minuten köcheln lassen.\nSpeisestärke mit dem Rest des Orangensafts verrühren, zur Soße geben und eindicken lassen. Tofu zur Sauce geben.\n", + "Orzo Halloumi Salat.txt": "Orzo Halloumi Salat (nisi_cherie)\n3 Portionen\nFür den Salat:\n100g Orzo Nudeln\n80g Granatapfelkerne\n100g Halloumi Natur\nHonig\n150g kleine Tomaten\n½ Gurke\nBasilikum\n1 Frühlingszwiebel\nDressing:\n30ml Olivenöl\n15ml weißer Balsamico\n1,5TL Honig\n1,5TL Senf\nSalz & Pfeffer\nOrzo nach Packungsanweisung kochen & abkühlen lassen.\nZutaten für das Dressing verrühren.\nHalloumi in Stücke schneiden.\nÖl in einer Pfanne erhitzen & Halloumi rundeherum anbraten. Etwas Honig darüber geben und kurz in der Pfanne weiter schwenken.\nTomaten & Gurken schneiden.\nFrühlingszwiebeln & Basilikum hacken.\nAlles miteinander vermengen.\n", + "Rezeptkarten.txt": "\n--- Slide 1 ---\nButterplätzchen\nZutaten:\n6 Eigelb\n500g Mehl\n250g Zucker\n250g Butter\nAlle Zutaten verknete. Über Nacht in den Kühlschrank.\nTeig ausrollen und ausstechen.\nFür ca. 10 Minuten bei 180°C backen.\nVariationen: Spitzbuben, Hausfreunde\n\n--- Slide 2 ---\nMarzipankissen\nTeig:\n300g Mehl\n100g Puderzucker\n200g kalte Butter\n2 Eigelb\n1 Prise Salz\nFüllung:\n300g Marzipan\n2 Eiweiß\n2EL Zitronensaft\n8 Tr. Bittermandelaroma\nPuderzucker zum Bestäuben\nZubereitung:\nAlle Zutaten des Teigs verkneten. Teig zu einem Rechteck (ca. 20x15cm) formen und ca. 2 Std. in den Kühlschrank (nicht zu lange!). Marzipan 2 Std. in den Kühlschrank oder 15 Min. in die Gefriere.\nMarzipan grob raspeln. Mit anderen Zutaten der Füllung gut quirlen.\nTeig viertel, 8 cm breit ausrollen. Teig mit der Füllung bestreichen, einrollen und dann Stücke abschneiden (wie Mini-Blätterteigschnecken).\nBei 180°C 12-14 Min. backen.\nNoch warm mit Puderzucker bestäuben.\n\n--- Slide 3 ---\ngefüllte Marzipan-Orangen-Plätzchen\nTeig:\n150g Mehl\n50g Speisestärke\n125g Zucker\n1 Pck. Vanillezucker\n1 TL Orangenabrieb\n1 Prise Salz\n175g kalte Butter\n1 Ei\n100g Marzipan\n100g gemahlene Mandeln\nFüllung: Orangenmarmelade\nGlasur:\n2-3 EL Orangensaft\n1 TL Orangenabrieb\n150g Puderzucker\nZubereitung:\nAlle Zutaten des Teigs verkneten. Für 30 Min. in den Kühlschrank.\nTeig ausrollen, Kreise ausstechen.\nBei 175°C ca. 12 Min. backen.\nAuskühlen lassen, dann mit Marmelade bestreichen und zusammensetzen.\nGlasur anrühren und Plätzchen bestreichen.\n\n--- Slide 4 ---\nLebkuchenplätzchen\nTeig:\n250g Honig\n125g Butter\n125g Zucker\n1 Ei\n500g Mehl\n1 Pck. Backpulver\n2EL Kakao\n1 Pck. Lebkuchengewürz\nPrise Salz\nZitronenabrieb\nDeko:\nSchokoglasuren\nPuderzucker\nStreusel\nZubereitung:\nHonig, Butter und Zucker in einem Top unter Rühren erhitzen, bis sich alles gut verbunden hat. Abkühlen lassen.\nEi unterrühren. Mehl mit Backpulver und Kakao vermischen und untersieben. Gewürze hinzufügen und alles zu einem Teig verkneten.\n1 Std. kalt stellen.\nTeig ca. ½ cm dick ausrollen und ausstechen.\nBei 180°C 15-20 Minuten backen.\nMit Schokoguss, Zuckerguss und Streuseln verzieren.\n\n--- Slide 5 ---\nZimt-Orangen-Plätzchen\nZubereitung:\nButter und Zucker schaumig schlage. Orangenabrieb und Zimt untermischen. Mehl unterrühren.\nTeig sofort zwischen Backpapier. ausrollen und ausstechen.\nBei 175°C ca. 10 Min backen.\nAbkühlen lassen, dann zur Hälfte in Schokoladenguss tunken.\nZutaten:\n200g weiche Butter\n100g Puderzucker\n300g Mehl\nAbrieb von 2 Bio Orangen\n2 TL Zimt\nSchokoglasur Zartbitter\n\n--- Slide 6 ---\nVanillekipferl\nTeig:\n250g Mehl\n210g Butter\n100g gemahlene Mandeln\n80g Zucker\n2 Pck. guter Vanillezucker\nDeko:\n½ Pck. Puderzucker\n1 Pck. guter Vanillezucker\nZubereitung:\nZutaten des Teigs verknete, dann 30 Min. in den Kühlschrank.\nIn der Zwischenzeit den Puderzucker sieben und mit Vanillezucker mischen.\nKipferlteig zu Rollen formen. Stücke abschneiden und Kipferl formen. Vorsicht: werden im Ofen sehr viel größer.\nBei 175°C Umluft ca. 10-15 Min. backen.\nLauwarm im Puderzucker wenden. \u000bZu warm zerbröseln die Plätzchen, zu kalt haftet der Puderzucker nicht.\n\n--- Slide 7 ---\nKaffee Kakao Plätzchen\nTeig:\n160g Mehl\n30g Kakao\n40g Puderzucker\n2TL Kaffeepulver\n30g Schokoraspeln\n150g Butter\nDeko:\nWeiße Kuvertüre\n1 Pck. Schoko-Kaffeebohnen\nZubereitung:\nAlle Zutaten des Teigs verknete.\n3 Rollen formen. Rollen 20 Min. kalt stellen.\nScheiben von der Rolle anschneiden.\nBei 160°C ca. 12 Min. backen.\nDekorieren\n\n--- Slide 8 ---\nZimtsterne\nTeig:\n500g gemahlene Mandeln\n300g Puderzucker\n2TL Zimt\n2 Eiweiß\n2EL Mandellikör\nGlasur:\n1 Eiweiß\n125g Puderzucker\nZubereitung:\nMandeln, Puderzucker und Zimt mischen. Eiweiß und Likör zugeben. Verkneten.\nTeig zwischen Backpapier ca. 1cm dick ausrollen. Sterne ausstechen, Ausstecher zwischendurch in Puderzucker tauchen.\nEiweiß steif schlagen. Puderzucker nach und nach zugeben, dabei weiterschlagen.\nSterne bepinseln.\nAuf unterster Schiene bei 150°C Ober-/ Unterhitze ca. 10-15 Min. backen.\n\n--- Slide 9 ---\nOrangen Schoko Kipferl\n220g Mehl\n120g weiche Butter\n100g Puderzucker\n4EL Kakao\n2 Eigelb\nAlle Zutaten außer der Glasur verkneten\nTeig zu einer Rolle formen. Stücke abschneiden und zu Kipferl formen.\nBei 180°C ca. 10-12 Min. backen.\nKipferl auskühlen lassen, anschließend die Enden in Schokoglasur tunken.\n2EL Milch\n1 Msp. Zimt\nAbrieb von 1 Orange\n5 Tr. Orangenaroma\nKuchenglasur\nZUTATEN\n\n--- Slide 10 ---\nMarzipanwölkchen\nZutaten:\n400g Marzipan\n2 Eiweiß\n100g Puderzucker\n140g gehobelte Mandeln\nMarzipan hobeln.\nEiweiß mit Puderzucker und Marzipan in einer Schüssel mit dem Handrührgerät zu mixen, bis eine gleichmäßige Masse entsteht.\nHände befeuchten. Ca. Walnussgroße Kugeln formen. Kugeln in Mandeln wälzen.\nBacken: 150°C Ober-/Unterhitze – 20 Minuten\n\n--- Slide 11 ---\nErdnuss-Karamell-Makronen\nZubereitung:\nOfen auf 160°C Ober-/ Unterhitze vorheizen\nErdnüsse grob hacken\nEiweiß mit einer Prise Salz steif schlagen\nRohrzucker nach und nach einrieseln lassen\nErdnussbutter & Erdnüsse unterheben\n2 TL auf die Oblaten setzen\n20-25 Min. backen\nZutaten:\n2 Eiweiß\n100g Rohrzucker\n50g Erdnussbutter crunchy\n150g Erdnüsse (geröstet, gesalzen)\n25 Oblaten\n1 Prise Salz\n", + "Schokomousse.txt": "Schokomousse (Zucker & Jagdwurst)\n400g Seidentofu\n200g Schokolade\nSchokolade am Wasserbad schmelzen.\nSeidentofu in einem hohen Gefäß mit geschmolzener Schokolade pürieren.\nKaltstellen\n", + "Spaghetti in Zitronenbutter und Lachs mit Macadamiakruste.txt": "[.png file: Spaghetti in Zitronenbutter und Lachs mit Macadamiakruste.PNG]", + "Weihnachtliche Bratapfel Biskuitrolle - Madame Dessert.txt": "\n--- Page 1 ---\nWeihnachtliche Bratapfel Biskuitrolle \nWas passt besser zu Weihnachten als Äpfel, Zimt und Nüsse? Meine leckere \nAntwort: Alles in einem! Besonders lecker wird die Kombination als weihnachtliche Bratapfel \nBiskuitrolle in der sich die fruchtig-nussige Füllung zwischen eine samtige Creme und fluffigen \nBiskuit schmiegt. \n12 Min. \nDessert Madame Dessert \nZutaten \nBiskuitteig \nBratapfel-Füllung \nMascarpone Creme \nArbeitsschritte \nBiskuit \n\n--- Page 2 ---\nBratapfel-Füllung \nMascarpone Creme \nBratapfel Biskuitrolle zusammensetzen \nWeihnachtliche Bratapfel Biskuitrolle https://madamedessert.de/weihnachtliche-bratapfel-biskuitrolle/ \nvorbereitete Backblech geben, etwas glatt streichen und auf der mittleren Schiene für etwa \n12 Minuten hellbraun backen. \n4. Den fertigen Biskuit aus dem Ofen nehmen und auf die saubere, mit einem Bogen \nBackpapier ausgelegte Arbeitsfläche stürzen. \n1. Äpfel schälen, entkernen und anschließend in ca. 5x5mm große Würfelchen schneiden. \nApfelwürfel mit etwas Zitronensaft beträufeln und beiseitestellen. \n2. Nüsse nach Wahl in einer kleinen beschichteten leicht anrösten. Anschließend leicht \nabkühlen lassen. \n3. Zucker zusammen mit dem Apfelsaft in eine beschichtete Pfanne geben und solange \nerhitzen, bis sich der Zucker vollständig aufgelöst hat. Butter unterrühren. Mit Zimt und \nVanille würzen. Apfelwürfel und optional die Rosinen dazugeben. \n4. Bratapfelmasse nun für ca. 10 bis 20 Minuten einköcheln, bis der Saft leicht eingedickt ist und \ndie Äpfel die für dich perfekte Konsistenz erreicht haben. Zuletzt die gerösteten Nüsse \ndazugeben und abkühlen lassen. \n1. Sahne steifschlagen. Puderzucker und Mascarpone miteinander verrühren. Aufgeschlagene \nSahne in die Mascarponecreme rühren, bis eine schöne, glatte Masse entsteht. Optional \nkannst du die Creme auch mit etwas Zitronensaft abschmecken.\n1. Backpapier vorsichtig vom Biskuit abziehen und die Außenkanten des Bodens mit einem \nscharfen Messer begradigen. Anschließend gleichmäßig mit der Mascarpone-Creme \neinstreichen. \n2. Abgekühlte Bratapfelmasse auf der Creme verteilen, dabei an einer der langen Seiten etwa 2 \nbis 3cm zum Rand hin aussparen. Biskuit von der anderen langen Seite her eng aufrollen. \nDie fertige Biskuitrolle rundherum mit dem Backpapier ein und für ca. 1 bis 2 Stunden in den \nKühlschrank stellen, damit die Creme anziehen kann. \n3. Vor dem Servieren die Biskuitrolle mit Bratapfel-Füllung mit Creme-Tupfen garnieren oder mit \netwas Zimt bestäuben. Für eine schöne Optik eventuell die Enden abschneiden. \nWeihnachtliche Bratapfel Biskuitrolle - Madame Dessert https://madamedessert.de/wprm_print/weihnachtliche-bratapfel-biskuit... \n2 von 2 24.08.2024, 12:36 ", + "Zimt-Biskuitrolle mit Pflaumen.txt": "\n--- Page 1 ---\nkuechenmomente.de | 1\nZimt-Biskuitrolle mit\nPflaumen\nBiskuitrolle mit karamellisierten Pflaumen\nZubereitungszeit: 40 min\nWartezeit:\nBackzeit: 20 min\nZUTATEN FÜR DEN BISKUIT\n2 Eier\n1 Prise Salz\n140 g Zucker\n1 TL Zimt\n80 g Mehl\n40 g Speisestärke\n1 TL Backpulver\n--- Page 2 ---\nkuechenmomente.de | 2\nFÜR DIE PFLAUMEN\n800 g Pflaumen\n1 EL Speisestärke\n100 g Zucker\nAbrieb 1 Bio-Zitrone\nMark 1 Vanilleschote\nFÜR DIE FÜLLUNG\n700 g Schlagsahne\n2 P. Vanille-Zucker\n7 TL SanApart (oder 1 P. Sofort-Gelatine)\nAUSSERDEM\n50 g Haselnuss-Krokant\nZUBEREITUNG\nBackofen auf Umluft 180 °C vorheizen.1.\n1 Backblech (ca. 30 x 40 cm) mit Backpapier belegen.2.\nFür den Teig die Eier trennen. Eiweiß mit der Prise Salz steifschlagen, dabei den3.\nZucker und den Zimt einrieseln lassen und noch etwas weiterschlagen, bis der\nZucker gelöst ist. Danach die Eigelbe unterrühren.\nMehl, Speisestärke und Backpulver mischen und auf die Eimasse sieben.4.\nVorsichtig unterheben – nicht rühren!\nDen Teig auf das Backblech geben und glattstreichen und für 20 Minuten im5.\nvorgeheizten Ofen backen.\nNach der Backzeit das Blech auf ein mit Zucker bestreutes Geschirrtuch stürzen,6.\ndas Backpapier vorsichtig abziehen und von der langen Seite her mit dem\nHandtuchtuch aufrollen und so auskühlen lassen.\n--- Page 3 ---\nkuechenmomente.de | 3\nIn der Zwischenzeit die Pflaumen waschen, entkernen und vierteln.7.\nPflaumenstücke in einer fettfreien Pfanne anschmoren. Stärke und Zucker\nmischen und zu den Pflaumen geben, unterrühren und den Zucker karamellisieren\nlassen. Vanille und Zitronenabrieb einrühren und einige Minuten bei niedriger\nTemperatur köcheln lassen. Dann die Pflaumen in ein Sieb geben, dabei den Sud\nauffangen und alles abkühlen lassen.\nFür die Füllung Sahne steifschlagen, Vanillezucker und SanApart (oder Sofort-8.\nGelatine) einrieseln lassen.\nDie Biskuitrolle ausrollen und mit einem Pinsel den Sud darauf verteilen. 2/3 der9.\nSahne darauf verteilen und glattstreichen.\nBis auf einige Stücke für die Deko alle Pflaumen auf der Sahne verteilen.10.\nHaselnuss-Krokant darüber streuen (auch davon etwas für die Deko übrig lassen!)\nund die Rolle wieder einrollen.\nMit der restlichen Sahne die Rolle rundherum bestreichen und für ca. 1 Stunde in11.\nden Kühlschrank stellen.\nKurz vor dem Servieren die Rolle mit den restlichen Pflaumenstücken belegen und12.\nmit Haselnuss-Krokant bestreuen.", + "ramen_rezepte.txt": "\n--- Page 1 ---\n\t\n \n \nKochstudio Bilou . Schrenkstr. 13 . 80339 München \n0176 / 810 190 92 . www.bilou-kitchen.de . jana@bilou-kitchen.de \n\t\nZUTATEN FÜR 2 PERSONEN \n \nBRÜHE \n \n2 L Wasser \n12 getrocknete Shitake Pilze oder 250 g frische Pilze \n1 Zwiebel, mit Schale halbiert \n10 cm Stück Ingwer, in Scheiben geschnitten \n1-2 St. Kombu oder 10-15 g getrockneter Seetang \n50 ml Sojasauce \nSalz \n \nAlle Zutaten außer die Sojasauce und Salz in einen Topf geben und bei leichter Hitze 30-40 \nMinuten kochen. \nDie Brühe gerne noch etwas ziehen lassen und erst kurz vor dem Servieren abseihen und mit \nSojasauce und Salz abschmecken. \n \nCHIILI ÖL \n \n1kleine Zwiebel in Würfel geschnitten \n15 g milde Chili Flocken oder Pulver (z.B. Gochugaru oder Pul Biber) \n1 kleine rote Chili Schote, fein geschnitten \n1-2 Knoblauchzehen fein zerdrückt \n2 EL geröstetes Sesamöl \n50-70 ml Öl \n \n4 EL Öl in einem Topf erhitzen, Zwiebel, Chili und Knoblauch darin anschwitzen, bis sie \nZwiebeln glasig sind. Das restliche Öl aufgießen und1 mal kurz aufkochen. Das Öl mit einem \nPürierstab pürieren. \n \nTARE \n \n1 EL Chili Öl \n2 TL Genmai Miso \n1-2 EL Wasser \n \nAlle Zutaten zu einer sämigen Paste verrühren. \n \nNUDELN \n \n200-250 g Ramen Nudeln \n \nDie Nudeln nach Packungsanleitung kochen, abseihen und in die heiße Brühe geben. \n \n \n--- Page 2 ---\n\t\n \n \nKochstudio Bilou . Schrenkstr. 13 . 80339 München \n0176 / 810 190 92 . www.bilou-kitchen.de . jana@bilou-kitchen.de \n\t\n \n \n \n \n \nKNUSPRIGES HACKFLEISCH \n \n200-300 g BIO Hackfleisch oder fester Tofu, mit den Händen zerkrümelt \n1-2 EL Ingwer, gerieben \n1 Knoblauchzehe, gerieben \n2 Frühlingszwiebeln, in Ringe geschnitten \n1 EL Chili-Bohnen Paste (alternativ Sambal Olek) \n1 TL geröstetes Sesamöl \n1 EL Sojasauce \n2 EL neutrales Öl zum Braten \n \nAlle Zutaten bis auf das Bratöl miteinander vermischen. \nDas Öl in einer Pfanne erhitzen und alles so lange darin anbraten bis es braun uns knusprig \nist. \n \nGEMÜSE \n \n2 kleine Pak Choi \n100-150 g Sojasprossen \n \nEinen Topf mit Salzwasser zum Kochen bringen. Den Pak Choi ca. 1 Minute darin garen und \nanschließend mit kaltem Wasser abschrecken. \nKurz vor dem Servieren den Pak Choi und die Sojasprossen noch mal kurz in heißes Wasser \ngeben, damit alles schön heiß ist. \n \nWEICH GEKOCHTE EIER \n \n2 Eier \n \nDie Eier an der Flachen Seite einstechen und im kochenden Wasser 7 Minuten kochen, \nanschließend mit kaltem Wasser abschrecken und schälen. Die Eier kurz vor dem Anrichten \nhalbieren und auf die Ramen legen. \n \n \n \n \n \n \n \n--- Page 3 ---\n\t\n \n \nKochstudio Bilou . Schrenkstr. 13 . 80339 München \n0176 / 810 190 92 . www.bilou-kitchen.de . jana@bilou-kitchen.de \n\t\nFURIKAKE \n \n1 Nori Blatt, in grobe Stücke zerrissen \n1 g Bonito Flocken (optional) \n15 g schwarzer und weißer Sesam \n½ EL Röstzwiebeln \nMeersalz \n \nDen Sesam in einer Pfanne kurz anrösten. \nAlle Zutaten mit einem Mixer grob mahlen. \n \nWEITERE TOPPINGS \n \n3 Frühlingszwiebeln in feine Streifen geschnitten \n¼ Nori Blatt in 4 kleine Vierecke geschnitten \n \n \nRAMEN ANRICHTEN \n \n1. Die Suppenschalen im Ofen vorwärmen oder etwas heißes Wasser hinein geben, damit \ndie Schüsseln beim Anrichten schön warm sind. \n2. Tare in Schüsseln verteilen \n3. Brühe aufgießen \n4. Die heiße Ramen Nudeln in die Brühe geben \n5. Gemüse kurz erhitzen und dazu geben \n6. Knuspriges Hackfleisch dazu geben \n7. Eier halbieren und mit der Schnittfläche nach oben darauf geben \n8. Jeweils 2 Nori Vierecke an die Seite stecken \n9. Mit Frühlingszwiebeln und Furikake bestreuen \n \n \n\t", + "schnelle Ramen.txt": "\n--- Slide 1 ---\nSchnelle Ramen\nZutaten 2 Personen\n2l Wasser\n12g Dashi-Pulver\n1EL Zucker\n1EL Essig\n1 ½ EL Miso\n1 Schuss Sojasauce\n1 Schuss dunkle Sojasauce\nHoisin\nRäuchertofu\n¼ - ½ Kopf Chinakohl\n250g Champignons\n3 Nester Mi Nudeln\n4 Frühlingszwiebeln\nRäuchertofu würfeln, dann mit einem Schuss dunkler Sojasauce und Hoisinsauce vermischen, bis der Tofu komplett mit Soße bedeckt ist. Tofu bei Seite stellen.\nChampignons je nach Größe ganz lassen oder halbieren. Chinakohl und Frühlingszwiebeln schneiden.\nÖl in einem Topf erhitzen. Champignons, Frühlingszwiebeln und den dicken Teil des Chinakohls anbraten. Mit Wasser, Dashibrühe, Zucker, Essig, Miso und Sojasauce aufgießen. Etwa 10 Minuten köcheln lassen.\n1\n2\n3\nMit Tofu, Champignons und Chinakohl\nTofu für 7 Minuten in der Heißluftfritteuse garen.\n4\nNudeln und grünen Teil des Chinakohls zur Suppe geben und nach Packungsanweisung köcheln lassen.\n5\n", + "zitronen-tiramisu-ohne-rohes-ei.txt": "\n--- Page 1 ---\nZITRONEN TIRAMISU / OHNE ROHES EI \n \nKlassisches Tiramisu ist leider oft für Kinder nicht\ngeeignet und auch auf rohes Ei müssen viele verzichten.\nDiese fruchtige Variante mit Lemon Curd ist da die\nperfekte Lösung. Das Tiramisu habe ich mit einem\nBackrahmen zubereitet, so steht es später ähnlich wie\neine Torte und kann sogar noch kurz eingefroren\nwerden, so entsteht eine erfrischende geeiste Version.\nGerade an heißen Sommertagen passt das\nZitronentiramisu perfekt. Die Löffelbiskuits könnt ihr\nmit einem Fruchtsaft eurer Wahl tränken, ich habe mich\nim Video für frisch gepressten Orangensaft entschieden,\naber auch Mango- oder Maracujasaft passen perfekt\ndazu.\nZubereitungszeit: 45 min\nKühlzeit: 4 h\nBackform: Eckig\nLänge: 21 cm\nBreite: 21 cm\nZUTATEN\nLEMON CURD\n  3 Zitrone / Zitronen (Abrieb)\n  150 g Zitrone / Zitronen (Saft)\n  3 Ei / Eier\n  150 g Zucker\n  1 TL Speisestärke\n  100 g Butter (kalt) \nMASCARPONECREME\n  375 g Mascarpone\n  75 g Puderzucker\n  200 g Schmand\n  1 TL Vanilleextrakt\n  250 g Sahne\n \nZUM SCHICHTEN\n  200 g Löffelbiskuit / Löffelbiskuits\n  175 g Orangensaft (oder Mangosaft)\nDEKORATION\n  0.5 Zitrone / Zitronen\nZUBEREITUNG\n--- Page 2 ---\nLEMON CURD\nReibe die Schale der Zitronen ab, achte darauf, dass es unbehandelte Bio-Zitronen sind, deren Schale verzehrt werden kann. Verrühre den\nAbrieb mit dem Zitronensaft, den Eiern, dem Zucker und der Stärke klümpchenfrei. Erwärme die Masse bei mittelhoher Hitze, rühre dabei\nstetig, sodass keine Klümpchen entstehen und das Ei nicht gerinnt. Rühre so lange, bis die Masse eindickt und lasse sie nach Bedarf einmal\nkurz aufkochen. Nimm das Lemon Curd vom Herd und rühre die kalte Butter in Stücken ein, so kühlt die Masse schneller ab und kann direkt\nweiterverwendet werden.\nFülle das nicht benötigte Lemon Curd in Schraubgläser um und stelle es, nachdem es vollständig\nabgekühlt ist, in den Kühlschrank, dort hält es sich etwa 8 Tage.\nMASCARPONECREME\nVerrühre den Mascarpone cremig, gib Puderzucker, Schmand und Vanilleextrakt dazu und rühre alles ein. Gieße die Sahne langsam dazu und\nschlage die Creme steif. Achte darauf, die Creme nicht zu lange zu schlagen, damit Sahne und Mascarpone nicht gerinnen. Stelle die Creme\nbis zur Weiterverarbeitung kalt.\nTIRAMISU SCHICHTEN\nTauche die Hälfte der Löffelbiskuits in den Saft und lege sie als Boden auf eine Servierplatte. Stelle einen Backrahmen darum und verteile\netwas Lemon Curd darauf. Gib die Hälfte der Creme darüber und streiche sie glatt. Tränke dann die zweite Hälfte der Löffelbiskuits im Saft\nund schichte sie auf die Creme. Verteile wieder etwas Lemon Curd und die übrige Creme darüber und streiche sie glatt.\nDEKORATION\nGib ein paar Löffel Lemon Curd auf das Tiramisu und marmoriere die Creme damit etwas. Schneide mit einem Zestenreißer feine Zesten von\nder Zitrone ab. Schneide die Zitrone in Scheiben und diese dann in Viertel und verteile sie mit den Zesten über dem Tiramisu. Stelle das\nTiramisu dann für mindestens 4 Stunden in den Kühlschrank und friere es nach Belieben vor dem Servieren für etwa 30 Minuten ein. Entferne\nden Backrahmen zum Servieren. Viel Spaß beim Nachmachen, deine Sally!" +} \ No newline at end of file diff --git a/db_inspect.py b/db_inspect.py new file mode 100644 index 0000000..82c79c4 --- /dev/null +++ b/db_inspect.py @@ -0,0 +1,23 @@ +import os +import pymysql +from dotenv import load_dotenv + +load_dotenv("/home/maxi/Projekt/wtc.waiswuaschd.de/.env") + +conn = pymysql.connect( + host=os.getenv("MARIADB_HOST", "db.waiswuaschd.de"), + user=os.getenv("MARIADB_USER", "v2_usr_wtc_cookbook"), + password=os.getenv("MARIADB_PASS", "XKikHf&j6Wj!rs5%A8#ZzyvE#*A1hT"), + database=os.getenv("MARIADB_NAME", "v2_db_wtc_cookbook"), + charset="utf8mb4", + cursorclass=pymysql.cursors.DictCursor +) + +with conn.cursor() as cur: + cur.execute("DESCRIBE recipes") + columns = cur.fetchall() + print("Columns in recipes:") + for col in columns: + print(f" {col['Field']} - {col['Type']} (Null: {col['Null']})") + +conn.close() diff --git a/dump_recipe_texts.py b/dump_recipe_texts.py new file mode 100644 index 0000000..66f470b --- /dev/null +++ b/dump_recipe_texts.py @@ -0,0 +1,31 @@ +import os + +DIR = "/home/maxi/Projekt/wtc.waiswuaschd.de/data/extracted_texts" +files = [ + "schnelle Ramen.txt", + "ramen_rezepte.txt", + "Brezen Cracker.txt", + "Currytaschen.txt", + "Kidneybohnen.txt", + "Mango Limetten Triffle.txt", + "Orangentofu mit Reis.txt", + "Orzo Halloumi Salat.txt", + "Schokomousse.txt", + "One Pot Mie Nudeln.txt", + "Bao Buns mit Champignons.txt", + "Lachs in Thai-Curry-Soße mit Zucchini.txt", + "Blätterteigpäckchen mit Spargel.txt", + "Spaghetti in Zitronenbutter und Lachs mit Macadamiakruste.txt" +] + +for fname in files: + fpath = os.path.join(DIR, fname) + if os.path.exists(fpath): + print(f"==================================================") + print(f"FILE: {fname}") + print(f"==================================================") + with open(fpath, "r", encoding="utf-8", errors="replace") as f: + print(f.read()) + print("\n") + else: + print(f"FILE NOT FOUND: {fname}") diff --git a/extract_all.py b/extract_all.py new file mode 100644 index 0000000..0247be1 --- /dev/null +++ b/extract_all.py @@ -0,0 +1,48 @@ +import os +from pypdf import PdfReader +from docx import Document +from pptx import Presentation + +SRC = '/home/maxi/Projekt/wtc.waiswuaschd.de/data/import_2026' +DEST = '/home/maxi/Projekt/wtc.waiswuaschd.de/data/extracted_texts' +os.makedirs(DEST, exist_ok=True) + +for f in sorted(os.listdir(SRC)): + path = os.path.join(SRC, f) + ext = os.path.splitext(f)[1].lower() + text = "" + try: + if ext == '.pdf': + reader = PdfReader(path) + for i, page in enumerate(reader.pages): + text += f"\n--- Page {i+1} ---\n" + (page.extract_text() or "") + elif ext == '.docx': + doc = Document(path) + for p in doc.paragraphs: + if p.text.strip(): + text += p.text.strip() + "\n" + for t in doc.tables: + text += "\n--- Table ---\n" + for row in t.rows: + row_txt = [c.text.strip() for c in row.cells if c.text.strip()] + if row_txt: + text += " | ".join(row_txt) + "\n" + elif ext == '.pptx': + prs = Presentation(path) + for i, slide in enumerate(prs.slides): + text += f"\n--- Slide {i+1} ---\n" + for shape in slide.shapes: + if shape.has_text_frame: + for paragraph in shape.text_frame.paragraphs: + if paragraph.text.strip(): + text += paragraph.text.strip() + "\n" + else: + text = f"[{ext} file: {f}]" + except Exception as e: + text = f"[ERROR: {e}]" + + out_name = os.path.splitext(f)[0] + ".txt" + with open(os.path.join(DEST, out_name), 'w', encoding='utf-8') as out_f: + out_f.write(text) + +print("Extraction complete. Files written to", DEST) diff --git a/extract_images.py b/extract_images.py new file mode 100644 index 0000000..7d9c798 --- /dev/null +++ b/extract_images.py @@ -0,0 +1,41 @@ +import os +import zipfile + +SRC = '/home/maxi/Projekt/wtc.waiswuaschd.de/data/import_2026' +IMG_OUT = '/home/maxi/Projekt/wtc.waiswuaschd.de/app/static/uploads/recipes' +os.makedirs(IMG_OUT, exist_ok=True) + +extracted_images = {} + +for f in sorted(os.listdir(SRC)): + path = os.path.join(SRC, f) + base = os.path.splitext(f)[0] + ext = os.path.splitext(f)[1].lower() + + if ext in ['.docx', '.pptx']: + try: + with zipfile.ZipFile(path, 'r') as z: + # Find images in word/media or ppt/media + media_files = [m for m in z.namelist() if m.startswith(('word/media/', 'ppt/media/')) and m.lower().endswith(('.jpg', '.jpeg', '.png'))] + # Filter out tiny icons (e.g. < 5KB) + large_media = [] + for m in media_files: + info = z.getinfo(m) + if info.file_size > 15000: # > 15KB + large_media.append((m, info.file_size)) + + large_media.sort(key=lambda x: x[1], reverse=True) + if large_media: + best_img = large_media[0][0] + img_ext = os.path.splitext(best_img)[1].lower() + safe_name = "".join(c for c in base if c.isalnum() or c in (' ', '_', '-')).strip().replace(' ', '_').lower() + target_file = f"{safe_name}{img_ext}" + target_path = os.path.join(IMG_OUT, target_file) + with open(target_path, 'wb') as out_f: + out_f.write(z.read(best_img)) + extracted_images[f] = f"/static/uploads/recipes/{target_file}" + print(f"Extracted image for {f}: {target_file} ({large_media[0][1]} bytes)") + except Exception as e: + print(f"Error extracting image from {f}: {e}") + +print(f"\nTotal images extracted: {len(extracted_images)}") diff --git a/extract_pdf_images.py b/extract_pdf_images.py new file mode 100644 index 0000000..d9ea57a --- /dev/null +++ b/extract_pdf_images.py @@ -0,0 +1,35 @@ +import os +from pypdf import PdfReader + +SRC = '/home/maxi/Projekt/wtc.waiswuaschd.de/data/import_2026' +IMG_OUT = '/home/maxi/Projekt/wtc.waiswuaschd.de/app/static/uploads/recipes' + +for f in sorted(os.listdir(SRC)): + if not f.lower().endswith('.pdf'): + continue + path = os.path.join(SRC, f) + base = os.path.splitext(f)[0] + safe_name = "".join(c for c in base if c.isalnum() or c in (' ', '_', '-')).strip().replace(' ', '_').lower() + + try: + reader = PdfReader(path) + img_idx = 0 + best_img = None + best_size = 0 + best_ext = '.jpg' + for page in reader.pages: + for img in page.images: + data = img.data + if len(data) > 20000 and len(data) > best_size: # > 20KB + best_size = len(data) + best_img = data + best_ext = os.path.splitext(img.name)[1].lower() or '.jpg' + + if best_img: + target_file = f"{safe_name}{best_ext}" + target_path = os.path.join(IMG_OUT, target_file) + with open(target_path, 'wb') as out_f: + out_f.write(best_img) + print(f"Extracted PDF image for {f}: {target_file} ({best_size} bytes)") + except Exception as e: + print(f"Error reading PDF {f}: {e}") diff --git a/init_mariadb.sql b/init_mariadb.sql new file mode 100755 index 0000000..b972d64 --- /dev/null +++ b/init_mariadb.sql @@ -0,0 +1,52 @@ +-- ===================================================================== +-- SQL Init & Setup Script für MariaDB (wtc.dev.waiswuaschd.de) +-- Kochbuch & Essens-Zufallsgenerator (What To Cook?) +-- ===================================================================== +-- Ausführung auf dem DB-Server: +-- mariadb --ssl=0 -u root -p < init_mariadb.sql +-- ===================================================================== + +-- 1. DATENBANK ERSTELLEN +CREATE DATABASE IF NOT EXISTS `v2_db_wtc` + DEFAULT CHARACTER SET utf8mb4 + DEFAULT COLLATE utf8mb4_unicode_ci; + +-- 2. BENUTZER ERSTELLEN (Passwort nach Bedarf anpassen) +CREATE USER IF NOT EXISTS 'v2_usr_wtc'@'%' IDENTIFIED BY 'Wtc_Kochbuch_Pass_2026!'; +GRANT ALL PRIVILEGES ON `v2_db_wtc`.* TO 'v2_usr_wtc'@'%'; +FLUSH PRIVILEGES; + +USE `v2_db_wtc`; + +-- 3. TABELLE: DISHES (Zufallsgenerator) +CREATE TABLE IF NOT EXISTS `dishes` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL UNIQUE, + `category` VARCHAR(100) NOT NULL, + `effort` VARCHAR(20) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_dish_category` (`category`), + INDEX `idx_dish_effort` (`effort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 4. TABELLE: RECIPES (Digitales Kochbuch) +CREATE TABLE IF NOT EXISTS `recipes` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `title` VARCHAR(255) NOT NULL, + `category` VARCHAR(100) NOT NULL, + `chapter_number` INT NOT NULL, + `effort` VARCHAR(20) NOT NULL DEFAULT 'mittel', + `prep_time` VARCHAR(50) DEFAULT '', + `cook_time` VARCHAR(50) DEFAULT '', + `portions` INT DEFAULT 4, + `ingredients` LONGTEXT NOT NULL, + `instructions` LONGTEXT NOT NULL, + `notes` TEXT, + `source` VARCHAR(255) DEFAULT '', + `image_url` VARCHAR(500) DEFAULT '', + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_recipe_chapter` (`chapter_number`), + INDEX `idx_recipe_category` (`category`), + INDEX `idx_recipe_title` (`title`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/inspect_details.py b/inspect_details.py new file mode 100644 index 0000000..9893a7e --- /dev/null +++ b/inspect_details.py @@ -0,0 +1,18 @@ +import os +import json + +DIR = '/home/maxi/Projekt/wtc.waiswuaschd.de/data/extracted_texts' + +files = sorted(os.listdir(DIR)) +for f in files: + if f in ['Rezeptkarten.txt', 'ramen_rezepte.txt']: + continue + path = os.path.join(DIR, f) + with open(path, 'r', encoding='utf-8', errors='ignore') as fp: + txt = fp.read().strip() + print(f"=== {f} ===") + lines = [l.strip() for l in txt.split('\n') if l.strip()] + print("\n".join(lines[:20])) + print("\n--- LAST 10 LINES ---") + print("\n".join(lines[-10:])) + print("="*40) diff --git a/inspect_recipes.py b/inspect_recipes.py new file mode 100644 index 0000000..28e5c2a --- /dev/null +++ b/inspect_recipes.py @@ -0,0 +1,47 @@ +import os +import glob +from pypdf import PdfReader +from docx import Document +from pptx import Presentation + +DIR = '/home/maxi/Projekt/wtc.waiswuaschd.de/data/import_2026' +files = sorted(os.listdir(DIR)) + +for f in files: + path = os.path.join(DIR, f) + ext = os.path.splitext(f)[1].lower() + text = "" + try: + if ext == '.pdf': + reader = PdfReader(path) + for page in reader.pages: + text += (page.extract_text() or "") + "\n" + elif ext == '.docx': + doc = Document(path) + for p in doc.paragraphs: + if p.text.strip(): + text += p.text.strip() + "\n" + for t in doc.tables: + for row in t.rows: + row_txt = [c.text.strip() for c in row.cells if c.text.strip()] + if row_txt: + text += " | ".join(row_txt) + "\n" + elif ext == '.pptx': + prs = Presentation(path) + for slide in prs.slides: + for shape in slide.shapes: + if shape.has_text_frame: + for paragraph in shape.text_frame.paragraphs: + if paragraph.text.strip(): + text += paragraph.text.strip() + "\n" + elif ext in ['.jpg', '.png', '.jpeg']: + text = f"[IMAGE FILE: {f}]" + else: + text = f"[UNKNOWN EXT: {ext}]" + except Exception as e: + text = f"[ERROR: {e}]" + + first_lines = [l.strip() for l in text.split('\n') if l.strip()][:5] + print(f"=== {f} ({len(text)} chars) ===") + print("\n".join(first_lines[:4])) + print() diff --git a/make_icons.py b/make_icons.py new file mode 100755 index 0000000..510b418 --- /dev/null +++ b/make_icons.py @@ -0,0 +1,50 @@ +from PIL import Image, ImageDraw + +def generate_icon(size, filename): + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + # Rounded rectangle background + corner_radius = size // 5 + bg_color = (255, 107, 53, 255) # Primary orange + draw.rounded_rectangle([0, 0, size, size], radius=corner_radius, fill=bg_color) + + # Outer plate rim + center = size // 2 + rim_radius = int(size * 0.38) + draw.ellipse( + [center - rim_radius, center - rim_radius, center + rim_radius, center + rim_radius], + fill=(255, 255, 255, 240) + ) + + # Inner plate circle + inner_radius = int(size * 0.30) + draw.ellipse( + [center - inner_radius, center - inner_radius, center + inner_radius, center + inner_radius], + fill=(245, 247, 250, 255) + ) + + # Food accent cloche / fork / dish shapes + cloche_r = int(size * 0.18) + draw.pieslice( + [center - cloche_r, center - cloche_r, center + cloche_r, center + cloche_r], + start=180, end=360, fill=(255, 107, 53, 255) + ) + # Cloche base line + draw.line( + [(center - cloche_r, center), (center + cloche_r, center)], + fill=(255, 107, 53, 255), width=max(2, size // 40) + ) + # Cloche knob + knob_r = max(2, size // 32) + draw.ellipse( + [center - knob_r, center - cloche_r - knob_r, center + knob_r, center - cloche_r + knob_r], + fill=(255, 107, 53, 255) + ) + + img.save(filename, "PNG") + print(f"Generated {filename}") + +if __name__ == "__main__": + generate_icon(192, "/home/maxi/Projekt/wtc.waiswuaschd.de/app/static/icon-192.png") + generate_icon(512, "/home/maxi/Projekt/wtc.waiswuaschd.de/app/static/icon-512.png") diff --git a/migrate_printed_column.py b/migrate_printed_column.py new file mode 100644 index 0000000..2ec1b1d --- /dev/null +++ b/migrate_printed_column.py @@ -0,0 +1,44 @@ +import os +import pymysql +from dotenv import load_dotenv + +load_dotenv("/home/maxi/Projekt/wtc.waiswuaschd.de/.env") + +conn = pymysql.connect( + host=os.getenv("MARIADB_HOST", "db.waiswuaschd.de"), + user=os.getenv("MARIADB_USER", "v2_usr_wtc_cookbook"), + password=os.getenv("MARIADB_PASS", "XKikHf&j6Wj!rs5%A8#ZzyvE#*A1hT"), + database=os.getenv("MARIADB_NAME", "v2_db_wtc_cookbook"), + charset="utf8mb4", + cursorclass=pymysql.cursors.DictCursor, + autocommit=False +) + +try: + with conn.cursor() as cur: + # Check if column printed_at exists + cur.execute(""" + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = %s AND TABLE_NAME = 'recipes' AND COLUMN_NAME = 'printed_at' + """, (os.getenv("MARIADB_NAME", "v2_db_wtc_cookbook"),)) + row = cur.fetchone() + if not row: + print("Füge Spalte 'printed_at' zur Tabelle 'recipes' hinzu...") + cur.execute("ALTER TABLE recipes ADD COLUMN printed_at DATETIME NULL DEFAULT NULL AFTER image_url") + conn.commit() + print("Spalte 'printed_at' erfolgreich hinzugefügt.") + else: + print("Spalte 'printed_at' existiert bereits.") + + cur.execute("DESCRIBE recipes") + for col in cur.fetchall(): + if col["Field"] in ["id", "title", "image_url", "printed_at", "created_at"]: + print(f" {col['Field']}: {col['Type']} (Null: {col['Null']})") + +except Exception as e: + conn.rollback() + print("Fehler:", e) + raise +finally: + conn.close() diff --git a/migrate_to_mariadb.py b/migrate_to_mariadb.py new file mode 100755 index 0000000..bf4d862 --- /dev/null +++ b/migrate_to_mariadb.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +""" +Migration Script: SQLite -> MariaDB +Überträgt alle Daten (Gerichte und Rezepte) von der lokalen SQLite-Datenbank in die MariaDB. +Verwendung: + python3 migrate_to_mariadb.py +""" +import os +import sqlite3 +import sys +from dotenv import load_dotenv + +load_dotenv() + +try: + import pymysql + import pymysql.cursors +except ImportError: + print("Fehler: PyMySQL ist nicht installiert. Bitte 'pip install pymysql' ausführen.") + sys.exit(1) + +SQLITE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "wtc.db") + +MARIADB_HOST = os.getenv("MARIADB_HOST", "127.0.0.1") +MARIADB_PORT = int(os.getenv("MARIADB_PORT", "3306")) +MARIADB_USER = os.getenv("MARIADB_USER", "v2_usr_wtc") +MARIADB_PASS = os.getenv("MARIADB_PASS", "Wtc_Kochbuch_Pass_2026!") +MARIADB_NAME = os.getenv("MARIADB_NAME", "v2_db_wtc") + +def migrate(): + print(f"=== Starte Migration: SQLite ({SQLITE_PATH}) -> MariaDB ({MARIADB_HOST}:{MARIADB_PORT}/{MARIADB_NAME}) ===") + + if not os.path.exists(SQLITE_PATH): + print(f"Hinweis: Keine SQLite-Datenbank unter '{SQLITE_PATH}' gefunden.") + return + + # 1. SQLite öffnen + sqlite_conn = sqlite3.connect(SQLITE_PATH) + sqlite_conn.row_factory = sqlite3.Row + sqlite_cursor = sqlite_conn.cursor() + + # 2. MariaDB öffnen + try: + maria_conn = pymysql.connect( + host=MARIADB_HOST, + port=MARIADB_PORT, + user=MARIADB_USER, + password=MARIADB_PASS, + database=MARIADB_NAME, + charset="utf8mb4", + cursorclass=pymysql.cursors.DictCursor, + autocommit=False + ) + except Exception as e: + print(f"Fehler bei Verbindung zur MariaDB: {e}") + print("Tipp: Wurde init_mariadb.sql bereits auf dem MariaDB-Server ausgeführt?") + sys.exit(1) + + maria_cursor = maria_conn.cursor() + + try: + # A. Gerichte migrieren + sqlite_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='dishes'") + if sqlite_cursor.fetchone(): + sqlite_cursor.execute("SELECT id, name, category, effort, created_at FROM dishes") + dishes = sqlite_cursor.fetchall() + print(f"Gefundene Gerichte in SQLite: {len(dishes)}") + + for d in dishes: + maria_cursor.execute(""" + INSERT INTO dishes (id, name, category, effort, created_at) + VALUES (%s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE category=VALUES(category), effort=VALUES(effort) + """, (d["id"], d["name"], d["category"], d["effort"], d["created_at"])) + print("Gerichte erfolgreich nach MariaDB übertragen.") + + # B. Rezepte migrieren + sqlite_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='recipes'") + if sqlite_cursor.fetchone(): + sqlite_cursor.execute(""" + SELECT id, title, category, chapter_number, effort, prep_time, cook_time, portions, ingredients, instructions, notes, source, image_url, created_at, updated_at + FROM recipes + """) + recipes = sqlite_cursor.fetchall() + print(f"Gefundene Rezepte in SQLite: {len(recipes)}") + + for r in recipes: + source_val = r["source"] if "source" in r.keys() and r["source"] else "" + maria_cursor.execute(""" + INSERT INTO recipes (id, title, category, chapter_number, effort, prep_time, cook_time, portions, ingredients, instructions, notes, source, image_url, created_at, updated_at) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + title=VALUES(title), category=VALUES(category), chapter_number=VALUES(chapter_number), + effort=VALUES(effort), prep_time=VALUES(prep_time), cook_time=VALUES(cook_time), + portions=VALUES(portions), ingredients=VALUES(ingredients), instructions=VALUES(instructions), + notes=VALUES(notes), source=VALUES(source), image_url=VALUES(image_url) + """, ( + r["id"], r["title"], r["category"], r["chapter_number"], r["effort"], + r["prep_time"], r["cook_time"], r["portions"], r["ingredients"], + r["instructions"], r["notes"], source_val, r["image_url"], r["created_at"], r["updated_at"] + )) + print("Rezepte erfolgreich nach MariaDB übertragen.") + + maria_conn.commit() + print("\n✅ Migration erfolgreich abgeschlossen!") + print("Um die Anwendung auf MariaDB umzustellen, setze in der .env-Datei:") + print("DB_ENGINE=mariadb") + + except Exception as e: + maria_conn.rollback() + print(f"Fehler während der Migration: {e}") + raise + finally: + sqlite_conn.close() + maria_conn.close() + +if __name__ == "__main__": + migrate() diff --git a/populate_mariadb.py b/populate_mariadb.py new file mode 100644 index 0000000..b035fc9 --- /dev/null +++ b/populate_mariadb.py @@ -0,0 +1,1584 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Import script to populate MariaDB (v2_db_wtc_cookbook) with all recipes from Kochbuch 2026. +""" +import os +import sys + +try: + import pymysql + import pymysql.cursors +except ImportError: + print("Fehler: pymysql nicht installiert.") + sys.exit(1) + +MARIADB_HOST = os.getenv("MARIADB_HOST", "db.waiswuaschd.de") +MARIADB_PORT = int(os.getenv("MARIADB_PORT", "3306")) +MARIADB_USER = os.getenv("MARIADB_USER", "v2_usr_wtc_cookbook") +MARIADB_PASS = os.getenv("MARIADB_PASS", "XKikHf&j6Wj!rs5%A8#ZzyvE#*A1hT") +MARIADB_NAME = os.getenv("MARIADB_NAME", "v2_db_wtc_cookbook") + +RECIPES = [ + # ==================== KAPITEL 1: Suppen & Eintöpfe ==================== + { + "title": "Grießnockerlsuppe (Klassische Suppeneinlage)", + "category": "Suppen & Eintöpfe", + "chapter_number": 1, + "effort": "gering", + "prep_time": "15 Min", + "cook_time": "20 Min", + "portions": 6, + "ingredients": ( + "30 g Butter (zimmerwarm) oder Fett\n" + "1 frisches Ei\n" + "70 g Weichweizengrieß\n" + "1 Prise Salz\n" + "1 Prise Muskatnuss (frisch gerieben)\n" + "1 Liter kräftige Gemüse- oder Rinderbrühe\n" + "Frisch geschnittener Schnittlauch zum Servieren" + ), + "instructions": ( + "1. Die weiche Butter mit einer Prise Salz in einer Schüssel schaumig rühren.\n" + "2. Das Ei dazugeben und gründlich unterrühren. Anschließend den Grieß und eine Prise Muskatnuss einarbeiten.\n" + "3. Die Nockerlmasse ca. 5 Minuten quellen lassen, damit der Grieß anzieht.\n" + "4. Mit zwei angefeuchteten Esslöffeln gleichmäßige, längliche Nockerln formen.\n" + "5. Die Nockerln in leicht siedendem Salzwasser oder direkt in der heißen Brühe ca. 5 Minuten leise kochen lassen.\n" + "6. Den Topf vom Herd nehmen und die Nockerln bei geschlossenem Deckel noch ca. 15 Minuten ziehen lassen.\n" + "7. In tiefen Tellern mit heißer Brühe und reichlich Schnittlauch anrichten." + ), + "notes": "Die Nockerln gehen beim Ziehen schön auf und werden wunderbar fluffig.", + "source": "jawi (Chefkoch)", + "image_url": "/static/uploads/recipes/griessnockerl_suppeneinlage_von_jawi.png" + }, + { + "title": "Chili sin Carne mit Sojagranulat & Zartbitterschokolade", + "category": "Suppen & Eintöpfe", + "chapter_number": 1, + "effort": "mittel", + "prep_time": "20 Min", + "cook_time": "35 Min", + "portions": 8, + "ingredients": ( + "200 g feines Sojagranulat\n" + "4 Zwiebeln (gewürfelt)\n" + "3 Knoblauchzehen (gepresst)\n" + "2-4 scharfe Chilischoten (sehr fein gehackt)\n" + "800 ml stückige Pizzatomaten (Dose)\n" + "1 Liter kräftige Gemüsebrühe\n" + "2 Dosen / Gläser Mais (abgetropft)\n" + "2 Dosen / Gläser Kidneybohnen (abgespült)\n" + "7 EL Tomatenmark\n" + "2 grüne Paprika (in mundgerechten Würfeln)\n" + "1 Tafel vegane Zartbitterschokolade (in Stücken)\n" + "2 EL Kreuzkümmel (Cumin)\n" + "2 EL getrockneter Oregano\n" + "1 TL gemahlener Koriander\n" + "2 TL Salz & 2 TL schwarzer Pfeffer\n" + "Etwas Pflanzenöl zum Anbraten" + ), + "instructions": ( + "1. Etwas Öl in einer großen Pfanne erhitzen und das trockene Sojagranulat darin scharf anbraten, bis es leicht gebräunt und aromatisch duftet.\n" + "2. Parallel dazu in einem großen Topf etwas Öl erhitzen und die Zwiebelwürfel darin glasig anschwitzen. Knoblauch und das angebratene Sojagranulat dazugeben.\n" + "3. Mit Salz, Pfeffer, Koriander, Kreuzkümmel und den Chilis würzen und unter Rühren 2 Minuten kurz anrösten.\n" + "4. Pizzatomaten, Tomatenmark und 1 Liter Gemüsebrühe einrühren. Alles aufkochen und ca. 15 Minuten leise köcheln lassen, dabei ab und zu umrühren.\n" + "5. Mais, Paprikawürfel, Kidneybohnen, Oregano und die Zartbitterschokolade dazugeben. Weitere 15 Minuten sanft köcheln lassen, bis die Schokolade geschmolzen und das Chili sämig eingedickt ist.\n" + "6. Nochmals mit Salz und Kreuzkümmel abschmecken und servieren." + ), + "notes": "Lässt sich hervorragend am Vortag vorbereiten und schmeckt aufgewärmt fast noch besser. Ideal zum Einfrieren.", + "source": "daily vegan", + "image_url": "/static/uploads/recipes/chili_sin_carne.jpeg" + }, + + # ==================== KAPITEL 2: Traditionell & Basics ==================== + { + "title": "Champignons wie vom Weihnachtsmarkt mit Knoblauchsauce", + "category": "Traditionell & Basics", + "chapter_number": 2, + "effort": "gering", + "prep_time": "10 Min", + "cook_time": "10 Min", + "portions": 2, + "ingredients": ( + "500 g frische weiße oder braune Champignons\n" + "1 Zwiebel (in feine Würfel geschnitten)\n" + "1 Knoblauchzehe (fein gehackt)\n" + "1 TL neutrales Pflanzenöl\n" + "50 g Kräuterbutter\n" + "1 TL geräuchertes Paprikapulver\n" + "1 TL Paprikapulver edelsüß\n" + "1 TL italienische Kräuter\n" + "1/2 TL Chiliflocken (oder 1 frische Chilischote)\n" + "1/2 TL Salz & ordentlich Pfeffer aus der Mühle\n" + "Für die weiße Knoblauchsauce:\n" + "250 g Crème fraîche\n" + "2 Knoblauchzehen (fein gerieben)\n" + "2 EL Mayonnaise\n" + "1 TL natives Olivenöl\n" + "1 TL glatte Petersilie (fein gehackt)\n" + "1 TL Schnittlauch (in feinen Röllchen)\n" + "1/2 TL Salz, Pfeffer & 1 Prise Zucker" + ), + "instructions": ( + "1. Knoblauch und Zwiebel in ganz feine Würfel schneiden. Champignons putzen und je nach Größe ganz lassen oder halbieren.\n" + "2. Die Pilze in einer sehr heißen Pfanne für ca. 1 Minute ohne Fett von allen Seiten unter Wenden scharf anbraten.\n" + "3. Die Hitze auf mittlere Stufe reduzieren. Knoblauch, Zwiebeln und 1 TL Öl dazugeben und ca. 3 Minuten weiter anschwitzen.\n" + "4. Alle Gewürze (geräuchertes & edelsüßes Paprikapulver, italienische Kräuter, Chili, Salz und Pfeffer) einmischen. Danach die Kräuterbutter hineingeben, schmelzen lassen und alles ca. 10 Minuten leise köcheln lassen.\n" + "5. Für die Knoblauchsauce: Crème fraîche mit Mayonnaise, geriebenem Knoblauch, Olivenöl, Kräutern, Salz, Pfeffer und Zucker glatt rühren.\n" + "6. Die heißen, aromatischen Champignons direkt mit der kalten Knoblauchsauce und knusprigem Baguette servieren." + ), + "notes": "Tipp: Die Knoblauchsauce lässt sich auch mit 2-3 Spritzern Tabasco pikant verfeinern.", + "source": "ayseskochblog.de", + "image_url": "/static/uploads/recipes/champignons_weihnachtsmarkt.jpg" + }, + { + "title": "Honig-Senf-Brezen-Cracker", + "category": "Traditionell & Basics", + "chapter_number": 2, + "effort": "gering", + "prep_time": "15 Min", + "cook_time": "60 Min", + "portions": 4, + "ingredients": ( + "2 alte bayerische Laugenbrezeln (vom Vortag)\n" + "50 g Dijonsenf\n" + "25 g flüssiger Bienenhonig\n" + "25 ml Sonnenblumenöl\n" + "15 ml milder Apfelessig\n" + "1/2 TL feines Salz\n" + "1/2 TL Paprikapulver edelsüß\n" + "1/8 TL frisch gemahlener Pfeffer" + ), + "instructions": ( + "1. Die Brezeln mit einem Brotmesser in kleine, mundgerechte Stücke (ca. 1-2 cm) schneiden.\n" + "2. Senf, Honig, Sonnenblumenöl, Apfelessig, Salz, Paprikapulver und Pfeffer in eine Schüssel geben und mit einem Schneebesen gründlich zu einer Emulsion verrühren.\n" + "3. Die Sauce über die Brezelstücke geben und so lange wenden, bis alle Stücke gleichmäßig benetzt sind.\n" + "4. Die Brezelstücke auf einem mit Backpapier ausgelegten Backblech gleichmäßig verteilen.\n" + "5. Im vorgeheizten Ofen bei 160 °C Ober-/Unterhitze im unteren Drittel ca. 30 Minuten backen.\n" + "6. Danach die Brezelstücke auf dem Blech durchwenden, den Ofen auf 100 °C herunterschalten und weitere 30 Minuten trocknen lassen.\n" + "7. Aus dem Ofen nehmen und auf dem Blech vollständig abkühlen lassen. Die Brezelstücke werden erst herrlich knusprig, wenn sie ganz kalt sind." + ), + "notes": "Der ideale bayerische Knabbersnack für gemütliche Abende und Biergarten-Feeling.", + "source": "toertchenmadeinberlin", + "image_url": "/static/uploads/recipes/brezen_cracker.jpeg" + }, + { + "title": "Ungarisches Festtags-Gulasch (Vollveggie)", + "category": "Traditionell & Basics", + "chapter_number": 2, + "effort": "hoch", + "prep_time": "30 Min", + "cook_time": "45 Min", + "portions": 6, + "ingredients": ( + "200 g grobe Soja-Medaillons oder Sojawürfel\n" + "Für die Marinade:\n" + "70 g Sojasauce & 40 g Worcestershiresauce\n" + "50 g Tomatenmark & 30 g Balsamico-Essig\n" + "20 g Olivenöl & 15 g Senf (mittelscharf)\n" + "15 g Agavendicksaft & 1 Messerspitze Sambal Oelek\n" + "Für den Gulaschtopf:\n" + "4 Zwiebeln (in feine Würfel geschnitten)\n" + "4 Knoblauchzehen\n" + "2 Paprikaschoten (rot/gelb, gewürfelt)\n" + "3 EL Olivenöl\n" + "3 EL Tomatenmark\n" + "1 TL Paprikapulver edelsüß & 1/2 TL Paprikapulver rosenscharf\n" + "1 TL getrockneter Majoran & 1 TL Thymian\n" + "6 EL Weizenmehl (zum Binden)\n" + "500 ml trockener Rotwein (oder Kirschsaft / Brühe)\n" + "1200 ml heißes Wasser / Gemüsebrühe\n" + "1 Prise Rohrzucker, Salz & frisch gemahlener Pfeffer" + ), + "instructions": ( + "1. Alle Zutaten für die Marinade mit einem Schneebesen gut durchrühren. Die trockenen Sojawürfel unterheben und mindestens 12 Stunden (am besten über Nacht) marinieren lassen.\n" + "2. Olivenöl in einem großen Bräter erhitzen und die eingelegten Sojawürfel samt Restmarinade ca. 5 Minuten auf hoher Stufe scharf anbraten.\n" + "3. Zwiebeln und Paprika hinzugeben und weitere 5 Minuten mitbraten. Danach Tomatenmark und gepressten Knoblauch kurz mit anschwitzen.\n" + "4. Gewürze (Paprika edelsüß & scharf, Majoran, Thymian) einrühren. Das Mehl darüber stäuben, kurz anrösten und mit dem Rotwein vorsichtig ablöschen.\n" + "5. Das Wasser bzw. die Brühe angießen. Zugedeckt bei kleiner Hitze ca. 30 Minuten sanft köcheln lassen, bis die Sauce herrlich sämig wird.\n" + "6. Mit Salz, Pfeffer und einer Prise Rohrzucker final abschmecken. Schmeckt am nächsten Tag noch aromatischer!" + ), + "notes": "Hervorragend zu Spätzle, Salzkartoffeln oder frischem Sauerteigbrot.", + "source": "vollveggie", + "image_url": "/static/uploads/recipes/gulasch.jpeg" + }, + + # ==================== KAPITEL 3: Pasta & Nudelgerichte ==================== + { + "title": "Original Italienische Spaghetti Carbonara", + "category": "Pasta & Nudelgerichte", + "chapter_number": 3, + "effort": "mittel", + "prep_time": "15 Min", + "cook_time": "15 Min", + "portions": 4, + "ingredients": ( + "400 g Spaghetti\n" + "150 g Guanciale (oder Pancetta)\n" + "4 frische Eigelb + 1 ganzes Ei\n" + "60 g Pecorino Romano (frisch gerieben)\n" + "Viel frisch gemahlener schwarzer Pfeffer\n" + "Salz für das Nudelwasser" + ), + "instructions": ( + "1. Wasser in einem großen Topf zum Kochen bringen und salzen.\n" + "2. Guanciale in feine Streifen schneiden und bei mittlerer Hitze in einer Pfanne ohne zusätzliches Fett knusprig auslassen.\n" + "3. Eier mit dem geriebenen Pecorino und reichlich frisch gemahlenem Pfeffer in einer Schüssel cremig verrühren.\n" + "4. Spaghetti al dente kochen. Mit einer Zange direkt in die Pfanne zum Guanciale heben.\n" + "5. Die Pfanne komplett von der Herdplatte nehmen! 1 Kelle stärkehaltiges Nudelkochwasser und die Ei-Käse-Mischung rasch unterrühren, bis eine seidig glänzende Emulsion entsteht.\n" + "6. Sofort servieren und mit zusätzlichem Pecorino und Pfeffer bestreuen." + ), + "notes": "Niemals Sahne verwenden! Das Ei darf nicht stocken, daher immer abseits der Herdplatte binden.", + "source": "Klassiker", + "image_url": "" + }, + { + "title": "Spaghetti in Zitronenbutter und Lachs mit Macadamiakruste", + "category": "Pasta & Nudelgerichte", + "chapter_number": 3, + "effort": "mittel", + "prep_time": "25 Min", + "cook_time": "15 Min", + "portions": 4, + "ingredients": ( + "80 g Macadamianusskerne (grob gehackt)\n" + "1/2 Bund glatte Petersilie (gehackt)\n" + "1 Knoblauchzehe (fein gehackt)\n" + "1 Bio-Zitrone (feiner Schalenabrieb)\n" + "5 EL Semmelbrösel\n" + "1 EL natives Olivenöl extra\n" + "8 Tranchen Lachsfilet (à ca. 100 g)\n" + "400 g Spaghetti\n" + "120 g gute Butter\n" + "Salz und frisch gemahlener schwarzer Pfeffer" + ), + "instructions": ( + "1. Backofen auf 180 °C Ober-/Unterhitze vorheizen. Macadamianüsse, Petersilie, Knoblauch, 1 TL Zitronenschale, Semmelbrösel und Olivenöl vermengen, salzen und pfeffern.\n" + "2. Lachs waschen, trocken tupfen, salzen und pfeffern. Auf ein mit Backpapier belegtes Blech legen. Die Macadamiamasse auf dem Fisch verteilen und leicht andrücken. Im Ofen ca. 15 Minuten garen.\n" + "3. Inzwischen Spaghetti in sprudelndem Salzwasser al dente kochen. Beim Abgießen eine Tasse Kochwasser auffangen.\n" + "4. Butter in einer großen Pfanne sanft schmelzen. Die restliche Zitronenschale hineingeben und 1-2 Minuten darin ziehen lassen.\n" + "5. Pfanne vom Herd nehmen, Spaghetti und etwas Nudelwasser zur Zitronenbutter geben und durchschwenken. Mit Salz und Pfeffer abschmecken.\n" + "6. Zitronenspaghetti auf vorgewärmten Tellern anrichten und jeweils mit 2 Stücken Lachs servieren." + ), + "notes": "Weinempfehlung: Roseri Riviera del Garda Classico Valtènesi Chiaretto DOP.", + "source": "Pasta & Vino (Frauke Antholz)", + "image_url": "/static/uploads/recipes/spaghetti_zitronenbutter_lachs.png" + }, + { + "title": "Orzo-Halloumi-Salat mit Granatapfel & Honig-Senf-Dressing", + "category": "Pasta & Nudelgerichte", + "chapter_number": 3, + "effort": "gering", + "prep_time": "20 Min", + "cook_time": "10 Min", + "portions": 3, + "ingredients": ( + "100 g Orzo-Nudeln (Kritharaki / Risoni)\n" + "80 g frische Granatapfelkerne\n" + "100 g Halloumi Natur\n" + "1 EL flüssiger Honig zum Glasieren\n" + "150 g Kirschtomaten (halbiert)\n" + "1/2 Salatgurke (in Würfeln)\n" + "1 Frühlingszwiebel (in Ringen)\n" + "1 Handvoll frisches Basilikum (grob gezupft)\n" + "Für das Dressing:\n" + "30 ml natives Olivenöl\n" + "15 ml weißer Balsamicoessig\n" + "1,5 TL flüssiger Honig\n" + "1,5 TL mittelscharfer Senf\n" + "Salz und frisch gemahlener Pfeffer" + ), + "instructions": ( + "1. Orzo-Nudeln in reichlich Salzwasser nach Packungsanweisung bissfest garen. Abgießen, kalt abschrecken und vollständig abkühlen lassen.\n" + "2. Alle Zutaten für das Dressing (Olivenöl, weißer Balsamico, Honig, Senf, Salz, Pfeffer) mit einer Gabel cremig verrühren.\n" + "3. Halloumi in mundgerechte Stücke schneiden. Etwas Öl in einer Pfanne erhitzen und Halloumi von allen Seiten goldbraun anbraten. Zum Schluss Honig darübergießen und kurz karamellisieren lassen.\n" + "4. Kirschtomaten halbieren, Gurke würfeln, Frühlingszwiebel und Basilikum hacken.\n" + "5. Die abgekühlten Orzo mit Gemüse, Granatapfelkernen, Halloumi und Dressing vermengen und servieren." + ), + "notes": "Perfekt als leichter Sommersalat oder feine Grillbeilage.", + "source": "nisi_cherie", + "image_url": "" + }, + + # ==================== KAPITEL 4: Kartoffeln ==================== + { + "title": "Gebratene Kasknödel (Käseknödel aus der Pfanne)", + "category": "Kartoffeln", + "chapter_number": 4, + "effort": "mittel", + "prep_time": "35 Min", + "cook_time": "20 Min", + "portions": 4, + "ingredients": ( + "250 g altbackenes Brot oder Semmeln (in Würfeln)\n" + "300 ml lauwarme Milch\n" + "2 frische Eier\n" + "2 EL Weizenmehl\n" + "200 g würziger Bergkäse oder Gruyère (grob gerieben)\n" + "1 große Zwiebel (in feine Würfel geschnitten)\n" + "1/2 Bund Petersilie (fein gehackt)\n" + "3 EL Semmelbrösel\n" + "1 EL Butter\n" + "Salz, frisch gemahlener Pfeffer & Muskat\n" + "Etwas geriebener Kümmel (optional)\n" + "Pflanzenöl oder Butterschmalz zum Braten" + ), + "instructions": ( + "1. Brotwürfel in eine große Schüssel geben, mit der lauwarmen Milch übergießen und ca. 15 Minuten ziehen lassen.\n" + "2. Zwiebelwürfel in 1 EL Butter glasig dünsten, gehackte Petersilie kurz unterschwenken und abkühlen lassen.\n" + "3. Zwiebel-Petersilien-Mischung, geriebenen Bergkäse, Eier, Mehl, Semmelbrösel sowie Salz, Pfeffer, Muskat und Kümmel zur Brotmasse geben. Alles gründlich zu einem Teig verkneten und 20 Minuten ruhen lassen.\n" + "4. Mit feuchten Händen gleichmäßige, flache Knödel formen.\n" + "5. Pflanzenöl oder Butterschmalz in einer Pfanne erhitzen und die Kasknödel bei mittlerer Hitze von beiden Seiten langsam goldbraun und knusprig ausbacken (ca. 4-5 Minuten pro Seite).\n" + "6. Heiß servieren, z.B. zu frischem Blattsalat oder Pilzrahmsauce." + ), + "notes": "Prima als Hauptgericht mit Salat oder als kräftige Beilage zu Waldpilzen in Rahm.", + "source": "felidae177 (Chefkoch)", + "image_url": "/static/uploads/recipes/gebratene_kasknödel_von_felidae177.png" + }, + + # ==================== KAPITEL 5: Reis & Getreide ==================== + { + "title": "One-Pot-Chili-Reispfanne aus dem Ofen", + "category": "Reis & Getreide", + "chapter_number": 5, + "effort": "gering", + "prep_time": "5 Min", + "cook_time": "25 Min", + "portions": 5, + "ingredients": ( + "220 g Basmatireis\n" + "80 g Berglinsen\n" + "2 Paprikaschoten (in Stücken)\n" + "1 rote Zwiebel (gewürfelt)\n" + "2 Knoblauchzehen (fein gehackt)\n" + "60 g Olivenöl\n" + "400 g gehackte Tomaten aus der Dose\n" + "600 ml heiße Gemüsebrühe\n" + "2 EL Tomatenmark\n" + "30 ml Ahornsirup\n" + "30 g gehackte Zartbitterschokolade\n" + "200 g Mais (Abtropfgewicht)\n" + "200 g Kidneybohnen (abgespült)\n" + "100 g geriebener Cheddar\n" + "2 EL Pankomehl (oder Paniermehl)\n" + "2 EL gehackte Petersilie\n" + "Gewürze: je 1 TL Kreuzkümmel, Cayennepfeffer, Salz, Pfeffer, Majoran, Rosmarin, Thymian, je 1/2 TL Muskat und Curry" + ), + "instructions": ( + "1. Den Backofen auf 180 °C Umluft vorheizen. Paprika, Zwiebel und Knoblauch in 2 EL Olivenöl scharf anbraten. Kräuter und Gewürze untermischen, kurz anrösten und mit dem Ahornsirup leicht karamellisieren lassen.\n" + "2. Reis und Berglinsen hinzugeben, mit Tomaten und Gemüsebrühe ablöschen. Schokolade, Tomatenmark, Mais und Kidneybohnen unterrühren. Die Hälfte des Cheddars untermischen.\n" + "3. Masse in eine Auflaufform (ca. 28 cm) geben und im Ofen 20 Minuten backen.\n" + "4. Den restlichen Cheddar mit Panko, gehackter Petersilie und etwas Olivenöl vermengen. Über die Pfanne streuen und weitere 5-10 Minuten goldbraun überbacken, bis Reis und Linsen butterweich sind.\n" + "5. Heiß mit einem Löffel Crème fraîche oder Schmand servieren." + ), + "notes": "Würzig, lecker und ohne großen Abwasch – ein genialer Ofen-Klassiker.", + "source": "knusperstuebchen.net", + "image_url": "/static/uploads/recipes/one-pot-chili-reispfanne-aus-dem-ofen-3.jpg" + }, + { + "title": "Cremige Zucchini-Bohnenpfanne mit Erdnussbutter", + "category": "Reis & Getreide", + "chapter_number": 5, + "effort": "gering", + "prep_time": "10 Min", + "cook_time": "15 Min", + "portions": 2, + "ingredients": ( + "400 g Zucchini (in ca. 3 cm große Stücke geschnitten)\n" + "2 EL natives Kokosöl\n" + "1 rote Zwiebel (gewürfelt)\n" + "3 Zehen Knoblauch (fein gehackt)\n" + "200 g stückige Tomaten\n" + "250 ml cremige Kokosmilch\n" + "1 EL Erdnussbutter\n" + "1 Dose gekochte, gemischte Bohnen (240 g Abtropfgewicht)\n" + "1 TL Sambal Oelek\n" + "1/2 TL schwarzer Pfeffer\n" + "1 TL Salz\n" + "Saft einer halben Zitrone\n" + "15 g frische Petersilie (gehackt)" + ), + "instructions": ( + "1. Zucchini in Stücke schneiden. Kokosöl in einer heißen Pfanne schmelzen, Zucchini mit 1/2 TL Salz bei mittlerer bis hoher Hitze 10-12 Minuten rundherum anbraten, bis sie schön bräunen. Herausnehmen.\n" + "2. Zwiebel und Knoblauch in das verbliebene Öl der Pfanne geben und 1 Minute anschwitzen. Tomaten und Kokosmilch dazugeben, Erdnussbutter einrühren und kurz sämig aufkochen.\n" + "3. Bohnen und die gebratenen Zucchinistücke unterrühren und mit Sambal Oelek, Pfeffer und restlichem Salz abschmecken.\n" + "4. Vor dem Servieren den Zitronensaft unterrühren und mit gehackter Petersilie bestreuen. Schmeckt fantastisch auf warmem Reis." + ), + "notes": "Afrikanisch inspirierte, herrlich cremige Resteverwertung.", + "source": "Eat this! (Nadine)", + "image_url": "/static/uploads/recipes/cremige_zucchini-bohnenpfanne_mit_erdnussbutter_-_eat_this.jpg" + }, + + # ==================== KAPITEL 6: Teig & Gebackenes ==================== + { + "title": "Blätterteigpäckchen mit grünem Spargel & Pecorino", + "category": "Teig & Gebackenes", + "chapter_number": 6, + "effort": "mittel", + "prep_time": "25 Min", + "cook_time": "25 Min", + "portions": 4, + "ingredients": ( + "750 g frischer grüner Spargel\n" + "275 g Blätterteig (frisch aus dem Kühlregal)\n" + "200 g Frischkäse\n" + "60 g frisch geriebener Pecorino\n" + "1/2 Bio-Zitrone (abgeriebene Schale)\n" + "4 Stiele frischer Thymian\n" + "6 Stiele glatte Petersilie\n" + "4 EL natives Olivenöl extra (zzgl. etwas zum Bestreichen)\n" + "150 g Rucola & 100 g Kirschtomaten\n" + "2 EL Balsamicoessig & 1 TL flüssiger Honig\n" + "Meersalz und frisch gemahlener schwarzer Pfeffer" + ), + "instructions": ( + "1. Den Spargel von den holzigen Enden befreien und im unteren Drittel schälen. Kräuter waschen und fein hacken. Frischkäse mit Zitronenschale, Kräutern, Salz und Pfeffer glattrühren.\n" + "2. Backofen auf 220 °C Ober-/Unterhitze vorheizen. Blätterteig entrollen und in 4 gleich große Quadrate (ca. 10x10 cm) schneiden.\n" + "3. Jedes Quadrat mit Frischkäsemasse bestreichen und diagonal mit 3-4 Spargelstangen belegen. Mit Pecorino bestreuen und zwei gegenüberliegende Teigecken über der Füllung gut zusammendrücken.\n" + "4. Mit Olivenöl bestreichen und im vorgeheizten Ofen auf einem Backblech ca. 20-25 Minuten goldbraun backen.\n" + "5. Rucola und halbierte Kirschtomaten mit Olivenöl, Balsamicoessig, Honig, Salz und Pfeffer marinieren und zusammen mit den warmen Päckchen servieren." + ), + "notes": "Eignet sich auch kalt hervorragend für Buffets oder Picknicks.", + "source": "Fagottini di pasta sfoglia", + "image_url": "/static/uploads/recipes/blaetterteigpaeckchen_spargel.jpg" + }, + { + "title": "Knusprige Blätterteig-Currytaschen", + "category": "Teig & Gebackenes", + "chapter_number": 6, + "effort": "gering", + "prep_time": "15 Min", + "cook_time": "15 Min", + "portions": 4, + "ingredients": ( + "1 Rolle Blätterteig (aus dem Kühlregal)\n" + "75 g veganes oder klassisches Hackfleisch\n" + "0,5 Zwiebel (fein gewürfelt)\n" + "75 g Lauch (in feinen Ringen)\n" + "75 g Möhren (fein geraspelt)\n" + "1,5 TL thailändische rote Currypaste\n" + "100 ml flüssige Sahne\n" + "0,5 TL Gemüsebrühepulver\n" + "1,5 EL Sojasauce\n" + "Etwas Pflanzenöl zum Braten" + ), + "instructions": ( + "1. Backofen auf 180 °C Umluft vorheizen.\n" + "2. Zwiebeln würfeln und in heißem Öl anbraten. Hack dazugeben und 3-4 Minuten anbraten.\n" + "3. Lauch, Möhren und Currypaste dazugeben und weitere 3-4 Minuten mitbraten.\n" + "4. 3/4 der Sahne dazugeben, mit Gemüsebrühe und Sojasauce würzen und kurz einköcheln lassen. Pfanne vom Herd nehmen.\n" + "5. Blätterteig in 8 Quadrate schneiden. Füllung mittig aufgeben und die Taschen gut verschließen.\n" + "6. Oberseite mit der restlichen Sahne bepinseln und im Ofen ca. 10-12 Minuten goldbraun backen." + ), + "notes": "Herrlich knuspriges Fingerfood für Partys und den Feierabend.", + "source": "zuckerjagdwurst", + "image_url": "" + }, + { + "title": "Traditionelle italienische Focaccia mit Rosmarin", + "category": "Teig & Gebackenes", + "chapter_number": 6, + "effort": "mittel", + "prep_time": "30 Min", + "cook_time": "20 Min", + "portions": 8, + "ingredients": ( + "500 g Weizenmehl\n" + "20 g frische Hefe\n" + "375 ml lauwarmes Wasser\n" + "1 TL Zucker & 1 TL Meersalz\n" + "5 EL natives Olivenöl extra\n" + "Frische Rosmarinnadeln oder getrockneter Thymian\n" + "Grobes Meersalz zum Bestreuen" + ), + "instructions": ( + "1. Mehl in eine große Schüssel sieben, Mulde formen. Hefe und Zucker in lauwarmem Wasser auflösen, in die Mulde gießen und zugedeckt 30 Minuten gehen lassen.\n" + "2. 3 EL Olivenöl und Salz unterrühren und den Teig ca. 10 Minuten gründlich durchkneten, bis er geschmeidig ist. Zugedeckt nochmals ca. 1 Stunde gehen lassen.\n" + "3. Backofen auf 220 °C (Umluft 200 °C) vorheizen.\n" + "4. Den Teig halbieren, zu dicken Fladen ausrollen/drücken und auf bemehlte Bleche legen. Mit den Fingern Vertiefungen hineindrücken.\n" + "5. Mit dem restlichen Olivenöl bestreichen und mit Rosmarin sowie grobem Meersalz bestreuen.\n" + "6. Auf mittlerer Schiene ca. 15-20 Minuten backen, bis die Focaccia herrlich duftet und goldbraun ist." + ), + "notes": "Schmeckt am allerbesten frisch aus dem Ofen zu Antipasti oder Pasta.", + "source": "Chefkoch", + "image_url": "" + }, + { + "title": "Fluffige Bao Buns mit marinierten Champignons & Edamame", + "category": "Teig & Gebackenes", + "chapter_number": 6, + "effort": "mittel", + "prep_time": "25 Min", + "cook_time": "15 Min", + "portions": 2, + "ingredients": ( + "Fertige Bao Buns (Asiamarkt / TK)\n" + "400 g Champignons (in Scheiben)\n" + "Champignon-Marinade: 4 EL Sojasauce, 1 EL Reisessig, 1 EL Agavendicksaft, 1 Knoblauchzehe, Chilipulver\n" + "Eingelegte Karotten: 2 Karotten, 50 ml Wasser, 50 ml Essig, 1/2 TL Salz, 1 TL Zucker\n" + "1/2 Salatgurke (in feine Stifte gehobelt)\n" + "200 g Edamame in Schale\n" + "Röstzwiebeln, japanische Mayonnaise & Tonkatsu-Sauce" + ), + "instructions": ( + "1. Karotten in feine Stifte hobeln. Mit Wasser, Essig, Salz und Zucker kurz aufkochen und marinieren lassen.\n" + "2. Champignons mit den Marinadenzutaten vermengen und in einer heißen Pfanne ca. 5 Minuten braten.\n" + "3. Edamame in Salzwasser ca. 5 Minuten garen, abgießen und salzen.\n" + "4. Bao Buns mit dem Dampfeinsatz im Topf ca. 6-8 Minuten dämpfen, bis sie weich und fluffig sind.\n" + "5. Bao Buns öffnen und mit gebratenen Champignons, Gurke, Karotte, Röstzwiebeln, Mayonnaise und Tonkatsu-Sauce füllen. Mit Edamame servieren." + ), + "notes": "Authentisches asiatisches Streetfood für zu Hause.", + "source": "WTC Asiatisch", + "image_url": "/static/uploads/recipes/bao_buns_mit_champignons.png" + }, + + # ==================== KAPITEL 7: Asiatische Küche ==================== + { + "title": "Schnelle Ramen mit Miso-Dashibrühe & Räuchertofu", + "category": "Asiatische Küche", + "chapter_number": 7, + "effort": "gering", + "prep_time": "15 Min", + "cook_time": "15 Min", + "portions": 2, + "ingredients": ( + "2 Liter Wasser\n" + "12 g Dashi-Pulver\n" + "1,5 EL Misopaste\n" + "1 EL Zucker & 1 EL Essig\n" + "1 Schuss helle Sojasauce & 1 Schuss dunkle Sojasauce\n" + "Hoisinsauce nach Belieben\n" + "200 g Räuchertofu\n" + "1/4 - 1/2 Kopf Chinakohl\n" + "250 g Champignons\n" + "4 Frühlingszwiebeln\n" + "3 Nester Mie-Nudeln" + ), + "instructions": ( + "1. Räuchertofu würfeln und mit dunkler Sojasauce und Hoisinsauce vermengen, bis alles glasiert ist. 7 Minuten im Airfryer bei 190 °C garen.\n" + "2. Champignons putzen und halbieren. Chinakohl und Frühlingszwiebeln schneiden.\n" + "3. Öl im Topf erhitzen, Champignons, Frühlingszwiebeln und den festen Strunk des Chinakohls anbraten.\n" + "4. Mit Wasser, Dashi, Zucker, Essig, Miso und Sojasauce aufgießen und 10 Minuten köcheln lassen.\n" + "5. Nudeln und das grüne Blattwerk des Chinakohls zur Suppe geben und nach Packungsanweisung gar ziehen lassen.\n" + "6. In tiefe Schalen füllen und mit dem krossen Räuchertofu toppen." + ), + "notes": "Blitzschnell zubereitet und vollgepackt mit Umami.", + "source": "WTC Asiatisch", + "image_url": "" + }, + { + "title": "Traditionelle Ramen mit Shoyu-Tare & knusprigem Hack", + "category": "Asiatische Küche", + "chapter_number": 7, + "effort": "hoch", + "prep_time": "30 Min", + "cook_time": "45 Min", + "portions": 2, + "ingredients": ( + "Brühe: 2 l Wasser, 12 getrocknete Shiitake-Pilze, 1 Zwiebel mit Schale halbiert, 10 cm Ingwer, 1-2 St. Kombu-Algen, 50 ml Sojasauce, Salz\n" + "Chili-Öl: 1 kleine Zwiebel gewürfelt, 15 g Gochugaru / Pul Biber, 1 frische rote Chili, 2 Zehen Knoblauch, 2 EL Sesamöl, 60 ml Pflanzenöl\n" + "Tare: 1 EL Chili-Öl, 2 TL Genmai Miso, 1-2 EL Wasser\n" + "Knuspriges Hack: 250 g Hackfleisch (oder fester Tofu zerkrümelt), geriebener Ingwer & Knoblauch, Frühlingszwiebeln, 1 EL Chili-Bohnenpaste, Sesamöl, Sojasauce\n" + "Gemüse & Toppings: 250 g Ramen-Nudeln, 2 Pak Choi, Sojasprossen, 2 weichgekochte Eier, Furikake (Nori, gerösteter Sesam, Röstzwiebeln)" + ), + "instructions": ( + "1. Brühe: Alle Zutaten außer Sojasauce und Salz im Wasser ca. 35 Minuten sanft köcheln lassen. Abseihen und mit Sojasauce und Salz abschmecken.\n" + "2. Chili-Öl: Zwiebel, Knoblauch und Chili in Öl anschwitzen, restliches Öl aufgießen, aufkochen und pürieren.\n" + "3. Tare: Chili-Öl und Genmai Miso glattrühren.\n" + "4. Knuspriges Hack: Hackfleisch mit Knoblauch, Ingwer, Bohnenpaste und Sojasauce anbraten, bis es tiefbraun und knusprig ist.\n" + "5. Pak Choi kurz blanchieren. Eier 7 Minuten kochen, abschrecken und halbieren. Nudeln kochen.\n" + "6. Anrichten: Schalen vorwärmen, Tare hineingeben, mit heißer Brühe aufgießen. Nudeln, Gemüse, Knusperhack und Eier darauf anrichten. Mit Frühlingszwiebeln und Furikake bestreuen." + ), + "notes": "Eine Meisterklasse japanischer Nudelsuppen nach dem Rezept von Kochstudio Bilou.", + "source": "Kochstudio Bilou München", + "image_url": "" + }, + { + "title": "One-Pot Mie-Nudeln mit Kokos, Curry & Erdnussbutter", + "category": "Asiatische Küche", + "chapter_number": 7, + "effort": "gering", + "prep_time": "10 Min", + "cook_time": "15 Min", + "portions": 2, + "ingredients": ( + "3 Nester Mie-Nudeln\n" + "400 ml Kokosmilch\n" + "300 ml Wasser oder Gemüsebrühe\n" + "3 EL Sojasauce\n" + "2 EL Erdnussbutter\n" + "1 EL gelbes Currypulver\n" + "1/2 Kopf Brokkoli\n" + "3 Karotten" + ), + "instructions": ( + "1. Karotten schälen und in feine Scheiben schneiden. Brokkoli in mundgerechte Röschen teilen.\n" + "2. Kokosmilch, Wasser, Sojasauce, Erdnussbutter und Currypulver in eine große Pfanne geben und unter Rühren aufkochen.\n" + "3. Mie-Nudeln und Gemüse in die Pfanne geben.\n" + "4. Deckel aufsetzen und ca. 15 Minuten auf kleiner Flamme gar ziehen lassen, ab und zu umrühren." + ), + "notes": "Herrlich sämig und aromatisch – das perfekte Feierabendgericht.", + "source": "WTC Asiatisch", + "image_url": "" + }, + { + "title": "Gemüse nach Gonbao-Art mit gerösteten Erdnüssen", + "category": "Asiatische Küche", + "chapter_number": 7, + "effort": "gering", + "prep_time": "15 Min", + "cook_time": "15 Min", + "portions": 3, + "ingredients": ( + "Für das Gemüse:\n" + "500 g frische Champignons (geviertelt)\n" + "2 bunte Paprikaschoten (in Stücken)\n" + "1 Bund Frühlingszwiebeln (in 2 cm Stücken)\n" + "1 Handvoll geröstete Erdnüsse\n" + "Chilischoten nach Belieben\n" + "Sonnenblumenöl zum Braten, Salz & Pfeffer\n" + "Für die Sauce:\n" + "2 gehäufte TL Speisestärke & 3 EL kaltes Wasser\n" + "3 EL Zucker & 3 EL Reisessig\n" + "3 EL helle Sojasauce & 2 EL dunkle Sojasauce\n" + "1 Knoblauchzehe (fein gehackt)" + ), + "instructions": ( + "1. Speisestärke mit kaltem Wasser glattrühren. Zucker, Essig, beide Sojasaucen und Knoblauch einrühren.\n" + "2. Wok stark erhitzen, etwas Öl hineingeben und Champignons scharf anbraten.\n" + "3. Paprika, Chilis und Frühlingszwiebeln dazugeben und 3-4 Minuten pfannenrühren.\n" + "4. Die Sauce nochmals kurz aufrühren, zum Gemüse gießen und aufkochen, bis sie glänzend bindet.\n" + "5. Erdnüsse unterheben und sofort mit gekochtem Reis oder Nudeln servieren." + ), + "notes": "Szechuan-Klassiker mit der perfekten Balance aus Süße, Säure und Schärfe.", + "source": "zuiko (Chefkoch)", + "image_url": "/static/uploads/recipes/gemüse_nach_gonbao-art_von_zuiko.png" + }, + { + "title": "Knuspriger Orangentofu mit Duftreis", + "category": "Asiatische Küche", + "chapter_number": 7, + "effort": "mittel", + "prep_time": "20 Min", + "cook_time": "15 Min", + "portions": 3, + "ingredients": ( + "Für den Tofu:\n" + "400 g Naturtofu (in Würfeln)\n" + "4 EL Speisestärke\n" + "1 TL Paprikapulver edelsüß, Salz & Pfeffer\n" + "Öl zum Braten\n" + "Für die Orangensauce:\n" + "250 ml frisch gepresster Orangensaft (ca. 6-8 Orangen)\n" + "Abrieb von 1 Bio-Orange\n" + "1 Zwiebel, 1 Knoblauchzehe & 5 g Ingwer (alles fein gehackt)\n" + "0,5 TL Chiliflocken & 50 g Rohrzucker\n" + "1 EL Reisessig & 2 EL Sojasauce\n" + "1 TL Speisestärke\n" + "Zum Servieren: Jasminreis, gerösteter Sesam, Frühlingszwiebeln" + ), + "instructions": ( + "1. Tofu in Stücke schneiden, mit Stärke, Paprikapulver, Salz und Pfeffer in eine Box geben und kräftig schütteln.\n" + "2. Tofu in heißem Öl von allen Seiten rundherum knusprig goldbraun braten und beiseitestellen.\n" + "3. Zwiebel, Ingwer, Knoblauch und Chili im Bratfett anbraten. 200 ml Orangensaft, Zucker, Essig, Sojasauce und Orangenabrieb zugeben und 5 Minuten einköcheln.\n" + "4. Stärke mit den restlichen 50 ml Orangensaft anrühren, in die Sauce rühren und andicken lassen. Den Tofu hineingeben.\n" + "5. Auf duftendem Reis anrichten und mit Frühlingszwiebeln und Sesam garnieren." + ), + "notes": "Außen super kross, innen zart und umhüllt von einer fruchtig-würzigen Glasur.", + "source": "WTC Asiatisch", + "image_url": "" + }, + { + "title": "Lachs in Thai-Curry-Soße mit Zucchini & goldenem Reis", + "category": "Asiatische Küche", + "chapter_number": 7, + "effort": "mittel", + "prep_time": "15 Min", + "cook_time": "20 Min", + "portions": 2, + "ingredients": ( + "200 g frisches Lachsfilet (mit Haut)\n" + "150 g Jasminreis & 300 ml Wasser\n" + "1 Zucchini (in 0,5 cm Halbmonde geschnitten)\n" + "1 rote Zwiebel (in Streifen) & 1 gelbe Zwiebel (gewürfelt)\n" + "1 Knoblauchzehe & 25 g Frühlingszwiebeln\n" + "180 ml cremige Kokosmilch\n" + "25 g rote Currypaste\n" + "2 g Currygewürz & 4 g Gemüsebrühepulver\n" + "Pickle: 50 ml Wasser, 50 ml Essig, Salz & Zucker" + ), + "instructions": ( + "1. Reis mit 300 ml Wasser, 1 g Currygewürz und 1/4 TL Salz aufkochen, 10 Minuten leise köcheln lassen und 10 Minuten ziehen lassen.\n" + "2. Pickle-Sud aufkochen, rote Zwiebelstreifen 30 Sekunden darin ziehen lassen und beiseitestellen.\n" + "3. Lachs salzen. In heißem Öl auf der Hautseite 2 Minuten anbraten, wenden und weitere 2-3 Minuten braten. Herausnehmen.\n" + "4. Zucchini, gelbe Zwiebel und Frühlingszwiebelweiß 3-4 Minuten anschwitzen. Knoblauch und rote Currypaste 1 Minute mitrösten.\n" + "5. Mit Kokosmilch, 100 ml Wasser und Brühepulver ablöschen, 3 Minuten einköcheln lassen. Mit etwas Essig und Zucker abschmecken. Lachs zurück in die Sauce geben.\n" + "6. Goldenen Reis und Thai-Curry auf tiefen Tellern anrichten, Lachs aufsetzen und mit Frühlingszwiebelgrün und eingelegten pinken Zwiebeln garnieren." + ), + "notes": "Harmonische Kombination aus scharfem Curry, zartem Lachs und säuerlich eingelegten Zwiebeln.", + "source": "HelloFresh / WTC Asiatisch", + "image_url": "/static/uploads/recipes/lachs_in_thai-curry-soße_mit_zucchini.png" + }, + + # ==================== KAPITEL 8: Süßspeisen ==================== + { + "title": "American Pancakes mit Beeren & Ahornsirup", + "category": "Süßspeisen", + "chapter_number": 8, + "effort": "gering", + "prep_time": "10 Min", + "cook_time": "15 Min", + "portions": 4, + "ingredients": ( + "150 g Weizenmehl\n" + "1 EL Zucker\n" + "1 TL Backpulver\n" + "2 frische Eier\n" + "1 Prise Salz\n" + "150 ml Milch\n" + "3 EL Pflanzenöl zum Ausbacken\n" + "Echter Ahornsirup nach Belieben\n" + "200 g gemischte Beeren (frisch oder TK)" + ), + "instructions": ( + "1. Mehl, Zucker, Backpulver, Eier, Salz und Milch in einer Schüssel zu einem glatten, dickflüssigen Teig verrühren.\n" + "2. Etwas Öl in einer beschichteten Pfanne auf mittlerer Hitze erwärmen.\n" + "3. Pfannkuchenteig portionsweise (ca. 2-3 EL pro Pancake) hineingeben.\n" + "4. Braten, bis die Unterseite goldbraun ist und sich an der Oberseite kleine Bläschen bilden.\n" + "5. Einmal wenden und auch die andere Seite goldbraun backen.\n" + "6. Die Pancakes stapeln, reichlich mit Ahornsirup übergießen und mit Beeren garniert servieren." + ), + "notes": "Ein amerikanischer Frühstückstraum – herrlich dick und fluffig.", + "source": "hannah_kocht_einfach (Chefkoch)", + "image_url": "/static/uploads/recipes/american_pancakes.png" + }, + { + "title": "Klassischer Grießbrei", + "category": "Süßspeisen", + "chapter_number": 8, + "effort": "gering", + "prep_time": "5 Min", + "cook_time": "5 Min", + "portions": 2, + "ingredients": ( + "1/2 Liter Vollmilch\n" + "5 EL Weichweizengrieß\n" + "3 EL Zucker\n" + "1 Prise Salz\n" + "1 EL Kakaopulver oder aromatische Zimt-Zucker-Mischung" + ), + "instructions": ( + "1. Einen kleinen Schluck der Milch in eine Rührschüssel geben, den Rest der Milch im Topf aufkochen.\n" + "2. Grieß und Zucker mit der kalten Milch in der Rührschüssel glattrühren.\n" + "3. Sobald die Milch aufkocht, die Grießmischung unter ständigem Rühren mit dem Schneebesen einfließen lassen.\n" + "4. Bei kleiner Hitze unter ständigem Rühren ein paar Minuten leise kochen und andicken lassen.\n" + "5. Auf Tellern anrichten und warm mit Kakaopulver oder Zimtzucker bestreuen." + ), + "notes": "Seelenfutter pur und besonders bei Kindern heiß geliebt.", + "source": "pilotilka (Chefkoch)", + "image_url": "/static/uploads/recipes/grießbrei_von_pilotilka.png" + }, + { + "title": "Galaktoboureko (Griechischer Grießauflauf)", + "category": "Süßspeisen", + "chapter_number": 8, + "effort": "mittel", + "prep_time": "30 Min", + "cook_time": "50 Min", + "portions": 4, + "ingredients": ( + "Für die Grießcreme:\n" + "50 g Weichweizengrieß\n" + "1 Pck. Vanillepuddingpulver\n" + "700 ml kalte Milch\n" + "4 Eier\n" + "125 g Zucker\n" + "2 EL weiche Butter\n" + "Für den Teig:\n" + "500 g Blätterteig oder Filoteig\n" + "65 g flüssige Butter zum Bestreichen\n" + "Für den Zitronen-Zimt-Sirup:\n" + "375 g Zucker\n" + "250 ml Wasser\n" + "1 TL Zimt & Saft einer Zitrone" + ), + "instructions": ( + "1. Grieß und Puddingpulver mit etwas kalter Milch verrühren. Restliche Milch mit Zucker aufkochen. Grießmischung einrühren und andicken lassen. Butter und verquirlte Eier einarbeiten.\n" + "2. Die Hälfte des Teiges in eine gebutterte Form legen, jede Lage mit flüssiger Butter bepinseln.\n" + "3. Die Grießcreme einfüllen und mit den restlichen Teigblättern bedecken (jede Lage buttern).\n" + "4. Bei 180 °C Ober-/Unterhitze ca. 45-50 Minuten goldbraun backen.\n" + "5. Wasser mit Zucker 10 Minuten einköcheln, Zimt und Zitronensaft zugeben und abkühlen lassen.\n" + "6. Den ofenheißen Auflauf sofort mit dem kalten Sirup übergießen und ziehen lassen." + ), + "notes": "Der griechische Dessert-Klassiker – knusprig außen, samtig innen.", + "source": "Chefkoch", + "image_url": "/static/uploads/recipes/galaktoboureko_-_griechischer_grießauflauf.png" + }, + + # ==================== KAPITEL 9: Desserts ==================== + { + "title": "Crema Catalana mit feiner Zuckerkruste", + "category": "Desserts", + "chapter_number": 9, + "effort": "mittel", + "prep_time": "45 Min", + "cook_time": "0 Min", + "portions": 6, + "ingredients": ( + "300 ml Vollmilch\n" + "200 ml süße Sahne\n" + "Abrieb von 2 Bio-Orangen\n" + "1 Stange Zimt\n" + "1/2 Vanilleschote (aufgeschlitzt)\n" + "5 Eigelbe\n" + "50 g feiner Zucker\n" + "1 Prise geriebene Muskatnuss\n" + "6 EL brauner Zucker zum Karamellisieren" + ), + "instructions": ( + "1. Backofen auf 150 °C vorheizen. Ein tiefes Blech mit Küchenkrepp auslegen, 2 cm hoch mit heißem Wasser füllen und in den Ofen schieben.\n" + "2. Milch, Sahne, Orangenschale, Zimtstange und Vanilleschote aufkochen und 10 Minuten ziehen lassen.\n" + "3. Eigelbe mit 50 g Zucker und Muskat cremig schlagen. Die warme Milch durch ein Sieb unter ständigem Rühren langsam einfließen lassen.\n" + "4. In 6 ofenfeste flache Förmchen gießen und im Wasserbad ca. 45 Minuten stocken lassen.\n" + "5. Vollständig abkühlen lassen und im Kühlschrank kühlen.\n" + "6. Vor dem Servieren mit braunem Zucker bestreuen und mit dem Flambierbrenner karamellisieren." + ), + "notes": "Herrlich aromatisch mit Orange und Zimt – das spanische Pendant zur Crème Brûlée.", + "source": "Aurora (Chefkoch)", + "image_url": "/static/uploads/recipes/crema_catalana_von_aurora.png" + }, + { + "title": "Cremiges veganes Schokomousse aus Seidentofu", + "category": "Desserts", + "chapter_number": 9, + "effort": "gering", + "prep_time": "10 Min", + "cook_time": "5 Min", + "portions": 4, + "ingredients": ( + "400 g Seidentofu (gut abgetropft)\n" + "200 g hochwertige Zartbitterschokolade (mind. 60-70% Kakao)\n" + "Optional: 1 Prise Meersalz & 1 TL Ahornsirup" + ), + "instructions": ( + "1. Die Zartbitterschokolade in Stücke brechen und in einer Schüssel über dem heißen Wasserbad sanft schmelzen lassen.\n" + "2. Den Seidentofu in ein hohes Gefäß geben.\n" + "3. Die flüssige Schokolade zum Seidentofu gießen.\n" + "4. Mit dem Pürierstab 2-3 Minuten auf höchster Stufe cremig pürieren, bis eine absolut samtige Masse entsteht.\n" + "5. In Gläschen füllen und mindestens 2 Stunden kalt stellen." + ), + "notes": "Niemand errät, dass hier Seidentofu drinsteckt – schmeckt extrem schokoladig und cremig.", + "source": "Zucker & Jagdwurst", + "image_url": "" + }, + { + "title": "Mango-Limetten-Triffle mit weißer Schokolade", + "category": "Desserts", + "chapter_number": 9, + "effort": "gering", + "prep_time": "20 Min", + "cook_time": "5 Min", + "portions": 2, + "ingredients": ( + "1 Bio-Limette (Schale & Saft)\n" + "50 g weiße Schokolade\n" + "180 g griechischer Joghurt\n" + "4 Löffelbiskuits\n" + "0,5 kleine reife Mango\n" + "3 TL Puderzucker\n" + "2 EL heißes Wasser" + ), + "instructions": ( + "1. Schale von 1/2 Limette fein abreiben, ganze Limette auspressen. 1 EL Saft mit heißem Wasser und 1 TL Puderzucker verrühren.\n" + "2. Weiße Schokolade hacken und im Wasserbad schmelzen. Mit Joghurt, 1 EL Puderzucker, 1 EL Limettensaft und Schale verrühren.\n" + "3. Löffelbiskuits halbieren. Abwechselnd mit Creme in 2 Gläser schichten, Biskuits jeweils mit der Limettentränke beträufeln. 15 Minuten kühlen.\n" + "4. Mango mit 1 TL Puderzucker fein pürieren.\n" + "5. Das Mangopüree auf den Gläsern verteilen und kühl servieren." + ), + "notes": "Fruchtig, erfrischend und elegant im Glas geschichtet.", + "source": "WTC Rezepte", + "image_url": "" + }, + { + "title": "Lebkuchen-Mousse mit Glühweinkirschen", + "category": "Desserts", + "chapter_number": 9, + "effort": "mittel", + "prep_time": "30 Min", + "cook_time": "10 Min", + "portions": 4, + "ingredients": ( + "200 g dunkle Schokolade\n" + "200 ml kalte Sahne (oder aufschlagbare Sojasahne)\n" + "2 EL Zucker & 1 TL Lebkuchengewürz\n" + "2 Lebkuchen ohne Glasur (gewürfelt)\n" + "1 Glas Sauerkirschen (300 g)\n" + "100 ml Glühwein\n" + "Abrieb von 1 Bio-Orange\n" + "2 EL Speisestärke" + ), + "instructions": ( + "1. Schokolade mit 2 EL Sahne schmelzen und abkühlen lassen. Sahne mit Zucker und Lebkuchengewürz steif schlagen.\n" + "2. Lebkuchen würfeln. Schokolade unter die Sahne rühren, Lebkuchen unterheben und in Gläser füllen. Kalt stellen.\n" + "3. Kirschen abgießen, Saft mit Glühwein und Orangenschale 5 Minuten aufkochen. Stärke anrühren, hineingießen und andicken lassen. Kirschen zugeben.\n" + "4. Kirschen abkühlen lassen und vor dem Servieren auf der Mousse verteilen." + ), + "notes": "Das perfekte Festtagsdessert für den Winter und das Weihnachtsmenü.", + "source": "transglobalpanparty.com", + "image_url": "/static/uploads/recipes/lebkuchen-mousse_mit_glühweinkirschen.jpg" + }, + { + "title": "Zitronen-Tiramisu mit Lemon Curd (ohne rohes Ei)", + "category": "Desserts", + "chapter_number": 9, + "effort": "mittel", + "prep_time": "45 Min", + "cook_time": "0 Min", + "portions": 8, + "ingredients": ( + "Lemon Curd:\n" + "3 Bio-Zitronen (Abrieb & 150 g Saft)\n" + "3 Eier\n" + "150 g Zucker\n" + "1 TL Speisestärke & 100 g kalte Butter in Stücken\n" + "Mascarponecreme:\n" + "375 g Mascarpone, 75 g Puderzucker, 200 g Schmand, 1 TL Vanilleextrakt, 250 g Schlagsahne\n" + "Schichten & Deko:\n" + "200 g Löffelbiskuits\n" + "175 ml frisch gepresster Orangensaft (oder Mangosaft)\n" + "0,5 Zitrone für feine Zesten" + ), + "instructions": ( + "1. Lemon Curd: Zitronenabrieb, Saft, Eier, Zucker und Stärke verrühren und bei mittlerer Hitze rühren, bis es andickt. Kalte Butterstücke einrühren. Abkühlen lassen.\n" + "2. Creme: Mascarpone mit Puderzucker, Schmand und Vanille verrühren. Sahne langsam zugeben und steif schlagen.\n" + "3. Schichten: Löffelbiskuits in Saft tauchen, Boden auslegen. Etwas Lemon Curd und halbe Creme darauf verstreichen. Zweite Lage Biskuits, Lemon Curd und restliche Creme aufschichten.\n" + "4. Mit Lemon Curd marmorieren und mit Zitronenzesten garnieren. Mindestens 4 Stunden kühlen." + ), + "notes": "Auch perfekt für Kinder geeignet, da komplett ohne rohes Ei und ohne Alkohol.", + "source": "Sallys Welt", + "image_url": "/static/uploads/recipes/zitronen-tiramisu-ohne-rohes-ei.jpg" + }, + { + "title": "Heidelbeer-Tiramisu (fruchtig-frisch ohne Ei)", + "category": "Desserts", + "chapter_number": 9, + "effort": "mittel", + "prep_time": "30 Min", + "cook_time": "10 Min", + "portions": 8, + "ingredients": ( + "Heidelbeersoße:\n" + "400 g frische Heidelbeeren\n" + "1 Bio-Zitrone (Saft & Abrieb)\n" + "150 g Gelierzucker 2:1\n" + "Creme:\n" + "500 g Mascarpone & 300 g Joghurt\n" + "250 g Schlagsahne & 1 Pck. Vanillezucker\n" + "80 g Zucker\n" + "Schichten & Deko:\n" + "1-2 Orangen (ausgepresst)\n" + "250 g Löffelbiskuits\n" + "50 g frische Heidelbeeren & 25 g weiße Schokolade geraspelt" + ), + "instructions": ( + "1. Heidelbeeren mit halber Zitronenschale und Gelierzucker aufkochen, mit dem Kartoffelstampfer zerdrücken und einköcheln lassen. Abkühlen lassen.\n" + "2. Sahne mit Vanillezucker steif schlagen. Mascarpone mit Joghurt, Zucker, restlicher Zitronenschale und Zitronensaft glattrühren. Sahne unterheben.\n" + "3. Löffelbiskuits kurz in Orangensaft tauchen und eine Auflaufform damit auslegen.\n" + "4. Hälfte Heidelbeersoße und Hälfte Creme aufschichten. Wiederholen, sodass Creme oben abschließt.\n" + "5. Mindestens 5 Stunden kühlen. Mit Heidelbeeren und Schokoraspeln verzieren." + ), + "notes": "Ein Traum in Violett – wunderbar beerig und cremig.", + "source": "WTC Rezepte", + "image_url": "/static/uploads/recipes/heidelbeer.jpeg" + }, + { + "title": "Weihnachtliche Bratapfel-Biskuitrolle", + "category": "Desserts", + "chapter_number": 9, + "effort": "mittel", + "prep_time": "40 Min", + "cook_time": "12 Min", + "portions": 10, + "ingredients": ( + "Biskuitteig:\n" + "4 Eier (Größe M), 1 Prise Salz, 115 g Zucker\n" + "80 g Mehl, 40 g Speisestärke, 1 TL Backpulver, 1-2 TL Lebkuchengewürz\n" + "Bratapfel-Füllung:\n" + "400 g Äpfel (geschält, in 5 mm Würfeln)\n" + "30 g geröstete gehackte Mandeln oder Walnüsse\n" + "85 g Zucker, 50 ml Apfelsaft, 1 EL Butter, 1 TL Zimt, etwas Vanille, 40 g Rosinen\n" + "Mascarpone-Creme:\n" + "250 g Sahne, 250 g Mascarpone, 70 g Puderzucker, etwas Zitronensaft" + ), + "instructions": ( + "1. Biskuit: Eier mit Salz und Zucker schaumig schlagen. Mehl, Stärke, Backpulver und Lebkuchengewürz unterheben. Bei 180 °C 12 Minuten backen. Auf Backpapier stürzen.\n" + "2. Füllung: Zucker mit Apfelsaft karamellisieren, Butter, Zimt, Vanille, Äpfel und Rosinen ca. 15 Minuten einköcheln. Nüsse zugeben und abkühlen lassen.\n" + "3. Sahne steif schlagen, mit Mascarpone und Puderzucker glattrühren.\n" + "4. Biskuit mit Mascarponecreme bestreichen, Bratapfelmasse aufgeben und eng aufrollen. 2 Stunden kühlen." + ), + "notes": "Schmiegt samtige Creme und aromatische Äpfel in einen fluffigen Biskuit.", + "source": "Madame Dessert", + "image_url": "/static/uploads/recipes/weihnachtliche_bratapfel_biskuitrolle_-_madame_dessert.jpg" + }, + { + "title": "Zimt-Biskuitrolle mit karamellisierten Pflaumen & Krokant", + "category": "Desserts", + "chapter_number": 9, + "effort": "mittel", + "prep_time": "40 Min", + "cook_time": "20 Min", + "portions": 10, + "ingredients": ( + "Biskuit: 2 Eier, 1 Prise Salz, 140 g Zucker, 1 TL Zimt, 80 g Mehl, 40 g Stärke, 1 TL Backpulver\n" + "Pflaumen: 800 g Pflaumen (entsteint, in Stücken), 50 g brauner Zucker, 1 Zimtstange, 100 ml Apfelsaft\n" + "Creme & Deko: 400 ml Sahne, 2 Pck. Sahnesteif, 2 Pck. Vanillezucker, 50 g Haselnusskrokant" + ), + "instructions": ( + "1. Biskuitteig anrühren, auf Blech streichen und bei 180 °C 10-12 Minuten backen. Auf feuchtes Tuch stürzen und einrollen.\n" + "2. Pflaumen mit Zucker, Zimtstange und Apfelsaft weich dünsten. Sud auffangen.\n" + "3. Sahne mit Sahnesteif und Vanillezucker steif schlagen.\n" + "4. Biskuit entrollen, mit Pflaumensud bepinseln, 2/3 der Sahne aufstreichen, Pflaumen und Krokant auflegen und aufrollen.\n" + "5. Mit restlicher Sahne bestreichen und mit Krokant und Pflaumenstücken garnieren." + ), + "notes": "Ein herbstlich-winterlicher Genuss für die Kaffeetafel.", + "source": "kuechenmomente.de", + "image_url": "/static/uploads/recipes/zimt-biskuitrolle_mit_pflaumen.png" + }, + + # ==================== KAPITEL 10: Kuchen & Gebäck ==================== + { + "title": "Saftiger umgedrehter Ananaskuchen", + "category": "Kuchen & Gebäck", + "chapter_number": 10, + "effort": "gering", + "prep_time": "25 Min", + "cook_time": "40 Min", + "portions": 12, + "ingredients": ( + "1 Dose Ananas in Scheiben (Abtropfgewicht ca. 350 g)\n" + "220 g weiche Butter (40 g für Form, 180 g für Teig)\n" + "30 g brauner Zucker\n" + "130 g Zucker & 1 Prise Salz\n" + "3 Eier (Größe M)\n" + "200 g Weizenmehl & 2 TL Backpulver\n" + "Cocktailkirschen nach Belieben" + ), + "instructions": ( + "1. Backofen auf 180 °C (Umluft 160 °C) vorheizen. Springform (26 cm) am Boden mit 40 g Butter bepinseln und braunen Zucker gleichmäßig aufstreuen.\n" + "2. 8 Ananasscheiben dicht auf den Boden legen, Löcher nach Belieben mit Kirschen füllen. Restliche Ananas pürieren.\n" + "3. 180 g Butter mit Zucker und Salz schaumig schlagen. Eier nach und nach zugeben. Mehl mit Backpulver abwechselnd mit dem Ananaspüree unterrühren.\n" + "4. Teig auf den Ananasscheiben verstreichen und bei 180 °C ca. 40 Minuten backen.\n" + "5. 5 Minuten ruhen lassen, dann direkt auf eine Tortenplatte stürzen und das Backpapier abziehen." + ), + "notes": "Unglaublich saftig durch den karamellisierten Ananasboden.", + "source": "Einfach Backen (Brigitte Sporrer)", + "image_url": "/static/uploads/recipes/ananaskuchen.jpg" + }, + { + "title": "Apfel-Salzkaramell-Tarte mit Erdnüssen", + "category": "Kuchen & Gebäck", + "chapter_number": 10, + "effort": "mittel", + "prep_time": "30 Min", + "cook_time": "30 Min", + "portions": 8, + "ingredients": ( + "Mürbeteig: 200 g Mehl, 100 g kalte Butter, 50 g Zucker, 1 EL kaltes Wasser, 1 Prise Zimt\n" + "Salzkaramell: 125 g Zucker, 200 ml Sahne, 2 EL Butter, grobes Meersalz\n" + "Belag: 2 säuerliche Äpfel (in feinen Scheiben), 2 EL geröstete und gesalzene Erdnüsse" + ), + "instructions": ( + "1. Mürbeteig kneten, in Tarteform drücken und bei 170 °C Umluft 12 Minuten blind vorbacken.\n" + "2. Zucker, Butter und Sahne im Topf ca. 15 Minuten zu einem cremigen Karamell einköcheln, Meersalz unterrühren.\n" + "3. Vorgebackenen Boden mit Karamell bestreichen, Erdnüsse aufstreuen und Apfelscheiben dachziegelartig auflegen.\n" + "4. 20 Minuten backen, nochmals mit warmem Karamell bepinseln, Erdnüsse aufstreuen und weitere 10 Minuten backen." + ), + "notes": "Göttliche Kombination aus süßem Karamell, säuerlichem Apfel und salzigen Nüssen.", + "source": "krimi&keks", + "image_url": "/static/uploads/recipes/apfel_erdnuss_tarte.jpeg" + }, + { + "title": "Blaubeer-Ricotta-Tarte", + "category": "Kuchen & Gebäck", + "chapter_number": 10, + "effort": "mittel", + "prep_time": "30 Min", + "cook_time": "60 Min", + "portions": 8, + "ingredients": ( + "Für den Mürbteig: 220 g Mehl, 100 g kalte Butter, 50 g Puderzucker, 1 Ei, 2 EL kaltes Wasser\n" + "Für den Belag: 500 g Ricotta, 2 Eier, Mark einer Vanilleschote, Saft & Schale einer Bio-Zitrone, 150 g Zucker\n" + "Dekoration: Ca. 300 g frische Heidelbeeren, Puderzucker zum Bestäuben" + ), + "instructions": ( + "1. Teigzutaten verkneten, 30 Minuten kühlen. Ausrollen, in Form geben und 15 Minuten bei 180 °C vorbacken.\n" + "2. Eier, Zucker und Vanillemark schaumig schlagen. Ricotta, Zitronensaft und -schale unterrühren.\n" + "3. Masse auf den Boden geben und weitere 45 Minuten backen. In der Form abkühlen lassen.\n" + "4. Mit frischen Beeren belegen und mit Puderzucker bestäuben." + ), + "notes": "Sommerlich frisch, cremig und wunderbar leicht.", + "source": "zimtkeksundapfeltarte", + "image_url": "/static/uploads/recipes/blauchbeer.jpeg" + }, + { + "title": "Bounty®-Kuchen vom Blech mit Kokoscreme", + "category": "Kuchen & Gebäck", + "chapter_number": 10, + "effort": "mittel", + "prep_time": "40 Min", + "cook_time": "20 Min", + "portions": 20, + "ingredients": ( + "Rührteig: 250 g Butter, 150 g Zucker, 5 Eier, 150 g Mehl, 2 gestr. TL Backpulver, 40 g Kakao, 100 g gemahlene Mandeln, 125 ml Milch\n" + "Kokos-Creme: 2 Pck. Sahnepuddingpulver, 80 g Zucker, 500 ml Milch, 400 ml Kokosmilch, 200 g Kokosraspel, 200 g Sahne, 1 Pck. Sahnesteif\n" + "Glasur & Deko: 150 g Zartbitterkuvertüre, 75 g Butter, ca. 10 Bounty-Riegel, 3 EL Kokoschips" + ), + "instructions": ( + "1. Pudding mit Milch, Kokosmilch und Zucker kochen, Kokosraspel einrühren, abkühlen lassen. Sahne steif schlagen und unterheben.\n" + "2. Rührteig anrühren, auf das Blech streichen und bei 180 °C ca. 20 Minuten backen. Erkalten lassen.\n" + "3. Kokoscreme auf dem Boden verstreichen und 1 Stunde kalt stellen.\n" + "4. Kuvertüre mit Butter schmelzen, auf der Creme verstreichen. Mit halbierten Bountys und Kokoschips verzieren." + ), + "notes": "Schokoladig, kokosnussig und unwiderstehlich lecker.", + "source": "Dr. Oetker", + "image_url": "/static/uploads/recipes/bounty-kuchen_rezept___dr_oetker.jpg" + }, + { + "title": "Donauwelle (Super Easy vom Blech)", + "category": "Kuchen & Gebäck", + "chapter_number": 10, + "effort": "mittel", + "prep_time": "35 Min", + "cook_time": "35 Min", + "portions": 16, + "ingredients": ( + "Teig: 3 Eier, 175 g Zucker, 175 ml Öl, 175 ml Milch, 350 g Mehl, 3 TL Backpulver, 1 EL Kakaopulver, 1 EL Milch, 1 Glas abgetropfte Sauerkirschen\n" + "Buttercreme: 1 Pck. Vanillepuddingpulver, 400 ml Milch, 2 EL Zucker, 250 g weiche Butter (zimmertemperiert)\n" + "Schokoglasur: 200 g Zartbitterschokolade, 2 EL Kokosfett / Butter" + ), + "instructions": ( + "1. Eier, Zucker, Öl und Milch verquirlen. Mehl und Backpulver unterrühren. 2/3 des Teiges auf ein gefettetes Blech streichen.\n" + "2. Restlichen Teig mit Kakao und 1 EL Milch verrühren, auf dem hellen Teig verteilen. Kirschen daraufgeben. Bei 180 °C 30-35 Minuten backen.\n" + "3. Pudding kochen und auf Zimmertemperatur abkühlen lassen. Butter schaumig schlagen, Pudding esslöffelweise unterrühren. Auf dem kalten Kuchen verstreichen.\n" + "4. Schokolade schmelzen, auf der Creme verstreichen und mit einer Kuchengabel das typische Wellenmuster ziehen. Kalt stellen." + ), + "notes": "Der unschlagbare Klassiker auf jeder Kuchentafel.", + "source": "alina1st (Chefkoch)", + "image_url": "/static/uploads/recipes/donauwelle_-_super_easy_von_alina1st.png" + }, + { + "title": "Gedeckter Apfelkuchen mit Streuseln", + "category": "Kuchen & Gebäck", + "chapter_number": 10, + "effort": "mittel", + "prep_time": "45 Min", + "cook_time": "60 Min", + "portions": 12, + "ingredients": ( + "Mürbeteig: 300 g Mehl, 100 g Butter, 100 g Zucker, 1 TL Backpulver, 1 Pck. Vanillinzucker, 2 Eier\n" + "Füllung: 1500 g säuerliche Äpfel (in Stücken), 50 g Butter, 50 g Rosinen, 50 g Zucker, 1 Pck. Vanillepuddingpulver\n" + "Streusel: 150 g Mehl, 100 g Butter, 100 g Zucker, 1 Prise Zimt (optional)" + ), + "instructions": ( + "1. Mürbeteig verkneten, in Springform drücken und am Rand hochziehen.\n" + "2. Äpfel mit Butter, Rosinen und Zucker andünsten. Puddingpulver mit etwas Wasser verrühren und unterrühren, bis es bindet. Auf den Teig geben.\n" + "3. Mehl, Butter und Zucker mit den Händen zu Streuseln verreiben und dicht auf den Äpfeln verteilen.\n" + "4. Bei 180 °C ca. 55-60 Minuten goldbraun backen." + ), + "notes": "Herrlich knusprige Streusel treffen auf saftige Apfelfülle.", + "source": "mbus (Chefkoch)", + "image_url": "/static/uploads/recipes/gedeckter_apfelkuchen_mit_streuseln_von_mbus.png" + }, + { + "title": "Fudgy Kidneybohnen-Brownies (ohne Mehl)", + "category": "Kuchen & Gebäck", + "chapter_number": 10, + "effort": "gering", + "prep_time": "10 Min", + "cook_time": "20 Min", + "portions": 8, + "ingredients": ( + "1 Dose Kidneybohnen (abgespült und gut abgetropft)\n" + "30 g gemahlene Mandeln\n" + "2 frische Eier\n" + "50 g echtes Kakaopulver\n" + "100 g Zucker (oder Xylit / Erythrit)\n" + "2 EL neutrales Rapsöl oder Kokosöl\n" + "1 Pck. Vanillezucker & 1/2 Pck. Backpulver\n" + "100 g gehackte Zartbitterschokolade" + ), + "instructions": ( + "1. Backofen auf 200 °C Umluft vorheizen. Form mit Backpapier auslegen.\n" + "2. Bohnen im Mixer fein pürieren. Alle restlichen Zutaten (außer der gehackten Schokolade) zugeben und cremig rühren.\n" + "3. Gehackte Schokolade unterheben.\n" + "4. In die Form füllen und ca. 15-20 Minuten backen, bis die Oberfläche fest, das Innere aber herrlich weich ist." + ), + "notes": "Extra schokoladig und saftig – ohne Mehl und voller guter Nährstoffe.", + "source": "WTC Rezepte", + "image_url": "/static/uploads/recipes/kidneybohnen.jpeg" + }, + + # ==================== KAPITEL 11: Plätzchen ==================== + { + "title": "Klassische Butterplätzchen & Spitzbuben", + "category": "Plätzchen", + "chapter_number": 11, + "effort": "gering", + "prep_time": "20 Min", + "cook_time": "10 Min", + "portions": 40, + "ingredients": ( + "500 g Mehl\n" + "250 g weiche Butter\n" + "250 g feiner Zucker\n" + "6 Eigelbe\n" + "Variationen: Rote Johannisbeermarmelade für Spitzbuben, Schokoglasur" + ), + "instructions": ( + "1. Alle Zutaten zügig zu einem glatten Mürbeteig verkneten.\n" + "2. Den Teig in Frischhaltefolie wickeln und über Nacht im Kühlschrank ruhen lassen.\n" + "3. Teig auf einer bemehlten Arbeitsfläche gleichmäßig ausrollen und beliebige Motive ausstechen.\n" + "4. Bei 180 °C Ober-/Unterhitze ca. 10 Minuten hell backen.\n" + "5. Nach Wunsch mit Puderzucker bestreuen, mit Marmelade zusammensetzen oder glasieren." + ), + "notes": "Der unverzichtbare Grundteig für die Weihnachtsbäckerei.", + "source": "Rezeptkarten", + "image_url": "/static/uploads/recipes/rezeptkarten.png" + }, + { + "title": "Marzipankissen", + "category": "Plätzchen", + "chapter_number": 11, + "effort": "mittel", + "prep_time": "30 Min", + "cook_time": "14 Min", + "portions": 30, + "ingredients": ( + "Teig: 300 g Mehl, 100 g Puderzucker, 200 g kalte Butter, 2 Eigelb, 1 Prise Salz\n" + "Füllung: 300 g Marzipan (gekühlt), 2 Eiweiß, 2 EL Zitronensaft, 8 Tropfen Bittermandelaroma\n" + "Dekoration: Puderzucker zum Bestäuben" + ), + "instructions": ( + "1. Teigzutaten verkneten, zu Rechteck (ca. 20x15 cm) formen und 2 Std. in den Kühlschrank legen. Marzipan 15 Min. in die Gefriertruhe legen.\n" + "2. Marzipan grob raspeln und mit Eiweiß, Zitronensaft und Aroma gut verquirlen.\n" + "3. Teig vierteln, 8 cm breit ausrollen. Mit Füllung bestreichen, einrollen und Stücke abschneiden.\n" + "4. Bei 180 °C 12-14 Minuten backen. Noch warm mit Puderzucker bestäuben." + ), + "notes": "Zart und marzipanig – wie Mini-Blätterteigschnecken.", + "source": "Rezeptkarten", + "image_url": "/static/uploads/recipes/rezeptkarten.png" + }, + { + "title": "Gefüllte Marzipan-Orangen-Plätzchen", + "category": "Plätzchen", + "chapter_number": 11, + "effort": "mittel", + "prep_time": "30 Min", + "cook_time": "12 Min", + "portions": 30, + "ingredients": ( + "Teig: 150 g Mehl, 50 g Speisestärke, 125 g Zucker, 1 Pck. Vanillezucker, 1 TL Orangenabrieb, 1 Prise Salz, 175 g kalte Butter, 1 Ei, 100 g Marzipan, 100 g gemahlene Mandeln\n" + "Füllung: Orangenmarmelade\n" + "Glasur: 2-3 EL Orangensaft, 1 TL Orangenabrieb, 150 g Puderzucker" + ), + "instructions": ( + "1. Alle Zutaten des Teiges verkneten und 30 Minuten kühlen.\n" + "2. Teig ausrollen und Kreise ausstechen.\n" + "3. Bei 175 °C ca. 12 Minuten backen.\n" + "4. Auskühlen lassen, mit Marmelade bestreichen und zusammensetzen.\n" + "5. Glasur aus Orangensaft, Abrieb und Puderzucker anrühren und Plätzchen bestreichen." + ), + "notes": "Fruchtige Orange harmoniert perfekt mit feinem Mandelmarzipan.", + "source": "Rezeptkarten", + "image_url": "/static/uploads/recipes/rezeptkarten.png" + }, + { + "title": "Lebkuchenplätzchen", + "category": "Plätzchen", + "chapter_number": 11, + "effort": "mittel", + "prep_time": "30 Min", + "cook_time": "18 Min", + "portions": 40, + "ingredients": ( + "250 g Honig, 125 g Butter, 125 g Zucker, 1 Ei\n" + "500 g Mehl, 1 Pck. Backpulver, 2 EL Kakao, 1 Pck. Lebkuchengewürz, Prise Salz, Zitronenabrieb\n" + "Deko: Schokoglasuren, Zuckerguss, bunte Streusel" + ), + "instructions": ( + "1. Honig, Butter und Zucker im Topf unter Rühren erhitzen, bis alles geschmolzen ist. Abkühlen lassen.\n" + "2. Ei unterrühren. Mehl mit Backpulver und Kakao darübersieben. Gewürze zugeben und verkneten. 1 Std. kühlen.\n" + "3. Teig ca. 5 mm dick ausrollen und Plätzchen ausstechen.\n" + "4. Bei 180 °C 15-20 Minuten backen.\n" + "5. Nach Belieben mit Schoko-, Zuckerguss und Streuseln verzieren." + ), + "notes": "Wunderbar weich und würzig duftend.", + "source": "Rezeptkarten", + "image_url": "/static/uploads/recipes/rezeptkarten.png" + }, + { + "title": "Zimt-Orangen-Plätzchen", + "category": "Plätzchen", + "chapter_number": 11, + "effort": "gering", + "prep_time": "20 Min", + "cook_time": "10 Min", + "portions": 35, + "ingredients": ( + "200 g weiche Butter\n" + "100 g Puderzucker\n" + "300 g Mehl\n" + "Abrieb von 2 Bio-Orangen\n" + "2 TL Zimt\n" + "Schokoglasur Zartbitter zum Eintauchen" + ), + "instructions": ( + "1. Butter und Zucker schaumig schlagen. Orangenabrieb und Zimt untermischen, Mehl unterrühren.\n" + "2. Teig sofort zwischen Backpapier ausrollen und Plätzchen ausstechen.\n" + "3. Bei 175 °C ca. 10 Minuten backen.\n" + "4. Abkühlen lassen, dann zur Hälfte in flüssige Zartbitterglasur tauchen." + ), + "notes": "Herrlich mürbe und aromatisch durch frischen Orangenabrieb.", + "source": "Rezeptkarten", + "image_url": "/static/uploads/recipes/rezeptkarten.png" + }, + { + "title": "Klassische Vanillekipferl", + "category": "Plätzchen", + "chapter_number": 11, + "effort": "gering", + "prep_time": "25 Min", + "cook_time": "12 Min", + "portions": 40, + "ingredients": ( + "Teig: 250 g Mehl, 210 g kalte Butter, 100 g gemahlene Mandeln, 80 g Zucker, 2 Pck. echter Bourbon-Vanillezucker\n" + "Wälzen: 1/2 Pck. Puderzucker, 1 Pck. Bourbon-Vanillezucker" + ), + "instructions": ( + "1. Zutaten zügig verkneten und 30 Minuten in den Kühlschrank stellen.\n" + "2. Puderzucker sieben und mit Vanillezucker mischen.\n" + "3. Teig zu daumendicken Rollen formen, Stücke abschneiden und Kipferl biegen.\n" + "4. Bei 175 °C Umluft ca. 10-12 Minuten backen (dürfen nicht zu dunkel werden).\n" + "5. Lauwarm im Vanille-Puderzucker wenden (zu heiß zerbrechen sie, zu kalt haftet der Zucker nicht)." + ), + "notes": "Zergehen buchstäblich auf der Zunge.", + "source": "Rezeptkarten", + "image_url": "/static/uploads/recipes/rezeptkarten.png" + }, + { + "title": "Kaffee-Kakao-Plätzchen von der Rolle", + "category": "Plätzchen", + "chapter_number": 11, + "effort": "gering", + "prep_time": "15 Min", + "cook_time": "12 Min", + "portions": 35, + "ingredients": ( + "Teig: 160 g Mehl, 30 g Kakaopulver, 40 g Puderzucker, 2 TL Kaffeepulver (Espressopulver), 30 g Schokoraspeln, 150 g Butter\n" + "Deko: Weiße Kuvertüre & 1 Pck. Mokka-Schokobohnen" + ), + "instructions": ( + "1. Alle Teigzutaten rasch verkneten.\n" + "2. 3 gleichmäßige Rollen formen und 20 Minuten kalt stellen.\n" + "3. Mit einem scharfen Messer gleichmäßige Scheiben von der Rolle schneiden.\n" + "4. Bei 160 °C ca. 12 Minuten backen.\n" + "5. Mit weißer Kuvertüre und Mokkabohnen verzieren." + ), + "notes": "Ein Traum für alle Kaffeeliebhaber.", + "source": "Rezeptkarten", + "image_url": "/static/uploads/recipes/rezeptkarten.png" + }, + { + "title": "Zimtsterne mit Baiserhaube", + "category": "Plätzchen", + "chapter_number": 11, + "effort": "mittel", + "prep_time": "30 Min", + "cook_time": "15 Min", + "portions": 30, + "ingredients": ( + "Teig: 500 g gemahlene Mandeln, 300 g Puderzucker, 2 TL Zimt, 2 Eiweiß, 2 EL Mandellikör (Amaretto)\n" + "Glasur: 1 Eiweiß, 125 g Puderzucker" + ), + "instructions": ( + "1. Mandeln, Puderzucker und Zimt mischen. Eiweiß und Amaretto zugeben und zu einem kompakten Teig verkneten.\n" + "2. Teig zwischen Backpapier ca. 1 cm dick ausrollen. Sterne ausstechen (Ausstecher in Puderzucker tauchen).\n" + "3. Für die Glasur: Eiweiß steif schlagen, Puderzucker esslöffelweise weiterschlagend einrieseln lassen.\n" + "4. Sterne sorgfältig mit der Baisermasse bepinseln.\n" + "5. Auf unterster Schiene bei 150 °C Ober-/Unterhitze ca. 10-15 Minuten backen, sodass das Baiser weiß bleibt." + ), + "notes": "Bleiben wunderbar saftig und weich im Kern.", + "source": "Rezeptkarten", + "image_url": "/static/uploads/recipes/rezeptkarten.png" + }, + { + "title": "Orangen-Schoko-Kipferl", + "category": "Plätzchen", + "chapter_number": 11, + "effort": "gering", + "prep_time": "20 Min", + "cook_time": "12 Min", + "portions": 35, + "ingredients": ( + "220 g Mehl, 120 g weiche Butter, 100 g Puderzucker, 4 EL Kakao, 2 Eigelb\n" + "2 EL Milch, 1 Messerspitze Zimt, Abrieb von 1 Bio-Orange, 5 Tropfen Orangenaroma\n" + "Schokoglasur zum Eintunken" + ), + "instructions": ( + "1. Alle Zutaten außer der Glasur zu einem geschmeidigen Mürbteig verkneten.\n" + "2. Zu Rollen formen, Stücke abschneiden und Kipferl biegen.\n" + "3. Bei 180 °C ca. 10-12 Minuten backen.\n" + "4. Auskühlen lassen und die beiden Enden in flüssige Schokoladenglasur tunken." + ), + "notes": "Schokolade und Orange in zart mürber Kipferlform.", + "source": "Rezeptkarten", + "image_url": "/static/uploads/recipes/rezeptkarten.png" + }, + { + "title": "Marzipanwölkchen mit Mandelblättchen", + "category": "Plätzchen", + "chapter_number": 11, + "effort": "gering", + "prep_time": "15 Min", + "cook_time": "20 Min", + "portions": 30, + "ingredients": ( + "400 g Marzipanrohmasse\n" + "2 frische Eiweiß\n" + "100 g Puderzucker\n" + "140 g gehobelte Mandeln" + ), + "instructions": ( + "1. Marzipan grob hobeln oder reiben.\n" + "2. Eiweiß mit Puderzucker und geriebenem Marzipan mit dem Handrührgerät zu einer homogenen Masse verarbeiten.\n" + "3. Hände befeuchten und walnussgroße Kugeln formen.\n" + "4. Die Kugeln rundherum in den Mandelblättchen wälzen und auf ein Backblech setzen.\n" + "5. Bei 150 °C Ober-/Unterhitze ca. 20 Minuten sanft backen." + ), + "notes": "Außen knusprig mandelig, innen herrlich saftig wie Marzipanwölkchen.", + "source": "Rezeptkarten", + "image_url": "/static/uploads/recipes/rezeptkarten.png" + }, + { + "title": "Erdnuss-Karamell-Makronen", + "category": "Plätzchen", + "chapter_number": 11, + "effort": "gering", + "prep_time": "15 Min", + "cook_time": "25 Min", + "portions": 25, + "ingredients": ( + "2 frische Eiweiß\n" + "100 g brauner Rohrzucker\n" + "50 g Erdnussbutter crunchy\n" + "150 g geröstete und gesalzene Erdnüsse (grob gehackt)\n" + "25 runde Backoblaten\n" + "1 Prise Salz" + ), + "instructions": ( + "1. Backofen auf 160 °C Ober-/Unterhitze vorheizen. Erdnüsse grob hacken.\n" + "2. Eiweiß mit einer Prise Salz steif schlagen. Den Rohrzucker nach und nach unter ständigem Weiterschlagen einrieseln lassen.\n" + "3. Die cremige Erdnussbutter und die gehackten Erdnüsse vorsichtig unter den Eischnee heben.\n" + "4. Je 2 Teelöffel Masse auf die Oblaten setzen.\n" + "5. Bei 160 °C ca. 20-25 Minuten backen." + ), + "notes": "Karamellig, nussig und herrlich knusprig.", + "source": "Rezeptkarten", + "image_url": "/static/uploads/recipes/rezeptkarten.png" + } +] + + +def run(): + print(f"Verbinde zu MariaDB auf {MARIADB_HOST}...") + conn = pymysql.connect( + host=MARIADB_HOST, + port=MARIADB_PORT, + user=MARIADB_USER, + password=MARIADB_PASS, + database=MARIADB_NAME, + charset="utf8mb4", + cursorclass=pymysql.cursors.DictCursor, + autocommit=False + ) + cur = conn.cursor() + + try: + # 1. Bereinige etwaige Testeinträge ("Test Rezept", "Peter", "test", "jköjkö") + cur.execute("DELETE FROM recipes WHERE title IN ('Test Rezept', 'Peter', 'test', 'jköjkö')") + print(f"Testeinträge bereinigt: {cur.rowcount} Zeilen entfernt.") + + # 2. Füge alle Rezepte ein oder aktualisiere sie + inserted = 0 + updated = 0 + for r in RECIPES: + cur.execute(""" + SELECT id FROM recipes WHERE title = %s + """, (r["title"],)) + existing = cur.fetchone() + + if existing: + cur.execute(""" + UPDATE recipes SET + category = %s, + chapter_number = %s, + effort = %s, + prep_time = %s, + cook_time = %s, + portions = %s, + ingredients = %s, + instructions = %s, + notes = %s, + source = %s, + image_url = %s, + updated_at = NOW() + WHERE id = %s + """, ( + r["category"], r["chapter_number"], r["effort"], + r["prep_time"], r["cook_time"], r["portions"], + r["ingredients"], r["instructions"], r["notes"], + r["source"], r["image_url"], existing["id"] + )) + updated += 1 + else: + cur.execute(""" + INSERT INTO recipes ( + title, category, chapter_number, effort, + prep_time, cook_time, portions, + ingredients, instructions, notes, source, image_url, + created_at, updated_at + ) VALUES ( + %s, %s, %s, %s, + %s, %s, %s, + %s, %s, %s, %s, %s, + NOW(), NOW() + ) + """, ( + r["title"], r["category"], r["chapter_number"], r["effort"], + r["prep_time"], r["cook_time"], r["portions"], + r["ingredients"], r["instructions"], r["notes"], + r["source"], r["image_url"] + )) + inserted += 1 + + conn.commit() + print(f"Erfolgreich ausgeführt: {inserted} neu eingefügt, {updated} aktualisiert.") + + # Gesamtstatus + cur.execute("SELECT COUNT(*) AS total FROM recipes") + print(f"Gesamtanzahl Rezepte in MariaDB: {cur.fetchone()['total']}") + + cur.execute("SELECT chapter_number, COUNT(*) as count FROM recipes GROUP BY chapter_number ORDER BY chapter_number") + print("Rezepte pro Kapitel:") + for row in cur.fetchall(): + print(f" Kapitel {row['chapter_number']}: {row['count']} Rezepte") + + except Exception as e: + conn.rollback() + print(f"Fehler beim Import: {e}") + raise + finally: + conn.close() + +if __name__ == "__main__": + run() diff --git a/process_ch1.py b/process_ch1.py new file mode 100644 index 0000000..5fe35d1 --- /dev/null +++ b/process_ch1.py @@ -0,0 +1,38 @@ +import os +from PIL import Image + +src_path = "/home/maxi/Projekt/wtc.waiswuaschd.de/app/static/img/chapters/source_chapter_1.jpg" +out_dir = "/home/maxi/Projekt/wtc.waiswuaschd.de/app/static/img/chapters" +im = Image.open(src_path).convert("RGBA") + +def make_transparent(sub_img): + sub_img = sub_img.convert("RGBA") + data = sub_img.getdata() + new_data = [] + for item in data: + r, g, b, a = item + lum = 0.299 * r + 0.587 * g + 0.114 * b + if lum >= 248: + new_data.append((r, g, b, 0)) + elif lum <= 200: + new_data.append((r, g, b, 255)) + else: + alpha = int(255 * (248 - lum) / (248 - 200)) + new_data.append((r, g, b, alpha)) + + res = Image.new("RGBA", sub_img.size) + res.putdata(new_data) + + bbox = res.getbbox() + if bbox: + px1 = max(0, bbox[0] - 8) + py1 = max(0, bbox[1] - 8) + px2 = min(res.width, bbox[2] + 8) + py2 = min(res.height, bbox[3] + 8) + res = res.crop((px1, py1, px2, py2)) + return res + +final_1 = make_transparent(im) +out_path = os.path.join(out_dir, "chapter_1.png") +final_1.save(out_path, "PNG") +print(f"Saved Chapter 1 -> chapter_1.png ({final_1.width}x{final_1.height})") diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..80ee062 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +fastapi>=0.110.0 +uvicorn>=0.29.0 +jinja2>=3.1.3 +python-dotenv>=1.0.1 +pydantic>=2.6.0 +python-multipart>=0.0.9 +pymysql>=1.1.0 +cryptography>=42.0.0 +pillow>=10.0.0 diff --git a/resize_ch1.py b/resize_ch1.py new file mode 100644 index 0000000..10042ab --- /dev/null +++ b/resize_ch1.py @@ -0,0 +1,9 @@ +import os +from PIL import Image + +p = "/home/maxi/Projekt/wtc.waiswuaschd.de/app/static/img/chapters/chapter_1.png" +im = Image.open(p) +# Downscale with high quality Lanczos filter to 450x450 +im = im.resize((450, 450), Image.Resampling.LANCZOS) +im.save(p, "PNG", optimize=True) +print("Resized chapter_1.png to 450x450") diff --git a/show_cards.py b/show_cards.py new file mode 100644 index 0000000..7f89343 --- /dev/null +++ b/show_cards.py @@ -0,0 +1,2 @@ +with open('/home/maxi/Projekt/wtc.waiswuaschd.de/data/extracted_texts/Rezeptkarten.txt', 'r', encoding='utf-8') as f: + print(f.read()) diff --git a/summarize_texts.py b/summarize_texts.py new file mode 100644 index 0000000..42d9f34 --- /dev/null +++ b/summarize_texts.py @@ -0,0 +1,17 @@ +import os +import json + +TXT_DIR = '/home/maxi/Projekt/wtc.waiswuaschd.de/data/extracted_texts' +files = sorted(os.listdir(TXT_DIR)) + +summary = {} +for f in files: + path = os.path.join(TXT_DIR, f) + with open(path, 'r', encoding='utf-8', errors='ignore') as fp: + content = fp.read() + summary[f] = content + +with open('/home/maxi/Projekt/wtc.waiswuaschd.de/data/recipes_summary.json', 'w', encoding='utf-8') as fp: + json.dump(summary, fp, ensure_ascii=False, indent=2) + +print("Saved recipes_summary.json with", len(summary), "files.") diff --git a/test_cookbook.py b/test_cookbook.py new file mode 100755 index 0000000..6608e1b --- /dev/null +++ b/test_cookbook.py @@ -0,0 +1,85 @@ +""" +Automated Integration Tests for WTC Kochbuch & Recipes +""" +import unittest +from fastapi.testclient import TestClient + +from app.main import app +from app.database import ( + init_db, + add_recipe, + get_all_recipes, + get_recipe, + update_recipe, + delete_recipe, + CHAPTERS +) + +class TestCookbookApp(unittest.TestCase): + @classmethod + def setUpClass(cls): + init_db() + cls.client = TestClient(app) + + def test_01_cookbook_pages(self): + # 1. Main cookbook view + res = self.client.get("/cookbook") + self.assertEqual(res.status_code, 200) + self.assertIn("Mein Kochbuch", res.text) + self.assertIn("Meine", res.text) + + # 2. Recipe new view + res2 = self.client.get("/cookbook/new") + self.assertEqual(res2.status_code, 200) + self.assertIn("Neues Rezept eintragen", res2.text) + + def test_02_create_recipe_and_view(self): + # Create recipe via API with source and checkbox 'on' + data = { + "title": "Klassische Tiramisu", + "chapter_number": 9, + "effort": "mittel", + "prep_time": "25 Min", + "cook_time": "0 Min", + "portions": 6, + "ingredients": "500g Mascarpone\n4 frische Eier\n100g Zucker\n200g Löffelbiskuits\nKalter Espresso\nKakaopulver zum Bestäuben", + "instructions": "Eigelb mit Zucker schaumig schlagen.\nMascarpone unterrühren.\nLöffelbiskuits in Espresso tränken und schichten.\nMit Kakaopulver bestäuben und kühlen.", + "notes": "Mindestens 4 Stunden im Kühlschrank durchziehen lassen!", + "source": "Oma Marias italienisches Originalrezept", + "add_to_dishes": "on" + } + res = self.client.post("/api/recipes", data=data) + self.assertEqual(res.status_code, 200) + recipe = res.json()["recipe"] + recipe_id = recipe["id"] + self.assertEqual(recipe["title"], "Klassische Tiramisu") + self.assertEqual(recipe["chapter_number"], 9) + self.assertEqual(recipe.get("source"), "Oma Marias italienisches Originalrezept") + + # View recipe detail page + detail_res = self.client.get(f"/cookbook/recipe/{recipe_id}") + self.assertEqual(detail_res.status_code, 200) + self.assertIn("Klassische Tiramisu", detail_res.text) + self.assertIn("500g Mascarpone", detail_res.text) + self.assertIn("Oma Marias italienisches Originalrezept", detail_res.text) + + # Edit recipe view + edit_res = self.client.get(f"/cookbook/edit/{recipe_id}") + self.assertEqual(edit_res.status_code, 200) + self.assertIn("Rezept bearbeiten", edit_res.text) + self.assertIn("Oma Marias italienisches Originalrezept", edit_res.text) + + # Update recipe with modified source + data["source"] = "Familienkochbuch Seite 42" + del data["add_to_dishes"] + put_res = self.client.put(f"/api/recipes/{recipe_id}", data=data) + self.assertEqual(put_res.status_code, 200) + updated_recipe = put_res.json()["recipe"] + self.assertEqual(updated_recipe.get("source"), "Familienkochbuch Seite 42") + + # Clean up + del_res = self.client.delete(f"/api/recipes/{recipe_id}") + self.assertEqual(del_res.status_code, 200) + +if __name__ == "__main__": + unittest.main() diff --git a/test_live.py b/test_live.py new file mode 100755 index 0000000..c2af8d2 --- /dev/null +++ b/test_live.py @@ -0,0 +1,56 @@ +import urllib.request +import json +import ssl + +ctx = ssl.create_default_context() + +def test_live(): + # 1. Roll 3 slots + url = "https://wtc.dev.waiswuaschd.de/api/roll" + payload = { + "slots": [ + {"slot_number": 1, "category": "Nudeln", "efforts": ["gering", "mittel"]}, + {"slot_number": 2, "category": "asiatisch", "efforts": ["mittel"]}, + {"slot_number": 3, "category": "Hausmannskost", "efforts": ["hoch"]} + ] + } + req = urllib.request.Request( + url, + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + with urllib.request.urlopen(req, context=ctx) as resp: + data = json.loads(resp.read().decode("utf-8")) + print("Initial Roll:") + for r in data["results"]: + dish = r["dish"] + print(f" Slot {r['slot_number']}: ID={dish['id']} - {dish['name']}") + + slot1_dish = data["results"][0]["dish"] + other_ids = [r["dish"]["id"] for r in data["results"][1:]] + + # 2. Reroll slot 1 + reroll_url = "https://wtc.dev.waiswuaschd.de/api/roll-single" + reroll_payload = { + "slot_number": 1, + "category": "Nudeln", + "efforts": ["gering", "mittel"], + "current_dish_id": slot1_dish["id"], + "other_dish_ids": other_ids + } + req2 = urllib.request.Request( + reroll_url, + data=json.dumps(reroll_payload).encode("utf-8"), + headers={"Content-Type": "application/json"} + ) + with urllib.request.urlopen(req2, context=ctx) as resp2: + reroll_data = json.loads(resp2.read().decode("utf-8")) + new_dish = reroll_data["dish"] + print("\nReroll Slot 1 Result:") + print(f" New Dish: ID={new_dish['id']} - {new_dish['name']}") + assert new_dish["id"] != slot1_dish["id"], "Reroll gave same dish!" + assert new_dish["id"] not in other_ids, "Reroll duplicated another slot!" + print(" SUCCESS: Different dish picked, no duplication with active slots!") + +if __name__ == "__main__": + test_live() diff --git a/test_parser.py b/test_parser.py new file mode 100644 index 0000000..28bdd08 --- /dev/null +++ b/test_parser.py @@ -0,0 +1,110 @@ +import re +from typing import List, Dict, Any, Optional + +COMMON_UNITS = [ + "kg", "g", "mg", "l", "liter", "ml", "cl", "el", "tl", "esslöffel", "teelöffel", + "pck.", "pck", "päckchen", "pkt.", "pkt", "dose", "dosen", "glas", "gläser", + "prise", "prisen", "msp.", "msp", "messerspitze", "bund", "stk.", "stk", + "stück", "stücke", "zehe", "zehen", "zweig", "zweige", "blatt", "blätter", + "scheibe", "scheiben", "stange", "stangen", "kopf", "nest", "nester", "tropfen", "tr." +] + +def parse_fraction(val_str: str) -> Optional[float]: + val_str = val_str.strip() + if "/" in val_str: + parts = val_str.split("/") + if len(parts) == 2: + try: + return float(parts[0]) / float(parts[1]) + except ValueError: + pass + try: + return float(val_str.replace(",", ".")) + except ValueError: + return None + +def parse_ingredient_line(line: str) -> Dict[str, Any]: + raw = line.strip() + if not raw: + return {"type": "empty", "raw": ""} + + if raw.endswith(":") or raw.lower().startswith("für ") or raw.lower() in [ + "teig", "sauce", "soße", "füllung", "belag", "creme", "glasur", "marinade", "deko", + "schichten & deko", "keksschicht", "heidelbeersoße", "zitronen-zimt-sirup" + ]: + return { + "type": "header", + "text": raw.rstrip(":"), + "raw": raw + } + + match = re.match(r"^(\d+\s+\d+/\d+|\d+/\d+|\d+(?:[.,]\d+)?)(?:\s*-\s*(\d+(?:[.,]\d+)?))?\s*(.*)$", raw) + if match: + amt_str = match.group(1).strip() + range_end = match.group(2) + rest = match.group(3).strip() + + if " " in amt_str and "/" in amt_str: + whole, frac = amt_str.split() + val = float(whole) + (parse_fraction(frac) or 0.0) + elif "/" in amt_str: + val = parse_fraction(amt_str) or 0.0 + else: + val = parse_fraction(amt_str) or 0.0 + + display_amt = f"{amt_str}-{range_end}" if range_end else amt_str + + rest_parts = rest.split(None, 1) + if rest_parts: + first_word_clean = rest_parts[0].lower().rstrip(".,") + if first_word_clean in COMMON_UNITS or rest_parts[0].lower() in COMMON_UNITS: + unit = rest_parts[0] + ing_name = rest_parts[1] if len(rest_parts) > 1 else "" + return { + "type": "ingredient", + "amount_val": val, + "amount_str": display_amt, + "unit": unit, + "name": ing_name, + "raw": raw + } + + return { + "type": "ingredient", + "amount_val": val, + "amount_str": display_amt, + "unit": "", + "name": rest, + "raw": raw + } + + return { + "type": "ingredient", + "amount_val": None, + "amount_str": "", + "unit": "", + "name": raw, + "raw": raw + } + +def parse_ingredients_block(text: str) -> List[Dict[str, Any]]: + lines = [l.strip() for l in (text or "").split("\n") if l.strip()] + return [parse_ingredient_line(l) for l in lines] + +# Test cases +tests = [ + "150 g Mehl", + "1 EL Zucker", + "1/2 TL Muskat", + "1,5 TL Salz", + "2 frische Eier", + "2-3 Karotten", + "Für den Teig:", + "Salz und Pfeffer nach Belieben", + "300 ml Milch", + "1 Pck. Vanillezucker" +] + +for t in tests: + res = parse_ingredient_line(t) + print(f"'{t}' => {res['type']}: amt={res.get('amount_val')}, unit='{res.get('unit')}', name='{res.get('name') or res.get('text')}'") diff --git a/test_roll.py b/test_roll.py new file mode 100644 index 0000000..eb22815 --- /dev/null +++ b/test_roll.py @@ -0,0 +1,26 @@ +import urllib.request +import json + +payload = { + "slots": [ + {"slot_number": 1, "chapter_number": 1}, + {"slot_number": 2, "chapter_number": 3}, + {"slot_number": 3, "chapter_number": 7}, + {"slot_number": 4, "chapter_number": 10}, + {"slot_number": 5, "chapter_number": 11} + ] +} + +data = json.dumps(payload).encode("utf-8") +req = urllib.request.Request("http://127.0.0.1:8003/api/roll", data=data, headers={"Content-Type": "application/json"}) +res = urllib.request.urlopen(req) +result = json.loads(res.read().decode("utf-8")) + +print("Status:", result.get("status")) +print("Slots rolled:") +for s in result.get("results", []): + recipe = s.get("recipe") + if recipe: + print(f" Slot {s.get('slot_number')}: {recipe.get('title')} (Kapitel {recipe.get('chapter_number')}: {recipe.get('category')})") + else: + print(f" Slot {s.get('slot_number')}: None ({s.get('message')})") diff --git a/test_wtc.py b/test_wtc.py new file mode 100755 index 0000000..8b24c06 --- /dev/null +++ b/test_wtc.py @@ -0,0 +1,97 @@ +""" +Automated Integration and Unit Tests for WTC Essen-Zufallsgenerator +""" +import sys +import unittest +from fastapi.testclient import TestClient + +from app.main import app +from app.database import ( + init_db, + get_all_dishes, + add_dish, + update_dish, + delete_dish, + query_candidates, + CATEGORIES, + EFFORTS +) + +class TestWtcApp(unittest.TestCase): + @classmethod + def setUpClass(cls): + init_db() + cls.client = TestClient(app) + # Create a few temporary dishes for test execution + cls.created_dish_ids = [] + for i, cat in enumerate(["Reis", "Nudeln", "Kartoffeln", "asiatisch", "Hausmannskost"]): + d = add_dish(f"_Testgericht {cat} {i}", cat, "mittel") + cls.created_dish_ids.append(d["id"]) + + @classmethod + def tearDownClass(cls): + for did in cls.created_dish_ids: + delete_dish(did) + + def test_01_categories_present(self): + self.assertIn("Nudeln", CATEGORIES) + self.assertIn("Reis", CATEGORIES) + self.assertIn("Kartoffeln", CATEGORIES) + self.assertEqual(len(EFFORTS), 3) + + def test_02_roll_5_dishes_no_duplicates(self): + payload = { + "slots": [ + {"slot_number": 1, "category": "Reis", "efforts": ["mittel"]}, + {"slot_number": 2, "category": "Nudeln", "efforts": ["mittel"]}, + {"slot_number": 3, "category": "Kartoffeln", "efforts": None}, + {"slot_number": 4, "category": "asiatisch", "efforts": ["mittel"]}, + {"slot_number": 5, "category": "Hausmannskost", "efforts": None}, + ] + } + res = self.client.post("/api/roll", json=payload) + self.assertEqual(res.status_code, 200) + data = res.json() + self.assertEqual(data["status"], "ok") + results = data["results"] + self.assertEqual(len(results), 5) + + chosen_ids = set() + for r in results: + self.assertEqual(r["status"], "ok") + dish = r["dish"] + self.assertIsNotNone(dish) + self.assertNotIn(dish["id"], chosen_ids) + chosen_ids.add(dish["id"]) + + def test_03_admin_crud(self): + test_dish_name = "_CRUD_Test_Dish" + # 1. Create + create_res = self.client.post("/api/dishes", json={ + "name": test_dish_name, + "category": "Teigig", + "effort": "gering" + }) + self.assertEqual(create_res.status_code, 200) + dish_id = create_res.json()["dish"]["id"] + + # 2. Read + get_res = self.client.get(f"/api/dishes?search={test_dish_name}") + self.assertEqual(get_res.status_code, 200) + dishes = get_res.json() + self.assertEqual(len(dishes), 1) + + # 3. Update + up_res = self.client.put(f"/api/dishes/{dish_id}", json={ + "name": test_dish_name + "_upd", + "category": "Teigig", + "effort": "mittel" + }) + self.assertEqual(up_res.status_code, 200) + + # 4. Delete + del_res = self.client.delete(f"/api/dishes/{dish_id}") + self.assertEqual(del_res.status_code, 200) + +if __name__ == "__main__": + unittest.main() diff --git a/wtc.dev.waiswuaschd.de.conf b/wtc.dev.waiswuaschd.de.conf new file mode 100755 index 0000000..e87baba --- /dev/null +++ b/wtc.dev.waiswuaschd.de.conf @@ -0,0 +1,41 @@ +server { + listen 80; + listen [::]:80; + server_name wtc.dev.waiswuaschd.de; + + location /.well-known/acme-challenge/ { + root /var/www/html; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name wtc.dev.waiswuaschd.de; + + client_max_body_size 50M; + + ssl_certificate /etc/nginx/ssl/wtc.dev.waiswuaschd.de/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/wtc.dev.waiswuaschd.de/key.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location /static/ { + alias /home/maxi/Projekt/wtc.waiswuaschd.de/app/static/; + add_header Cache-Control "no-cache, must-revalidate"; + } + + location / { + proxy_pass http://127.0.0.1:8003; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/wtc.service b/wtc.service new file mode 100755 index 0000000..6be86c1 --- /dev/null +++ b/wtc.service @@ -0,0 +1,16 @@ +[Unit] +Description=What To Cook (WTC) Zufallsgenerator für Essen (FastAPI) +After=network.target + +[Service] +Type=simple +User=maxi +Group=maxi +WorkingDirectory=/home/maxi/Projekt/wtc.waiswuaschd.de +Environment="PATH=/home/maxi/Projekt/wtc.waiswuaschd.de/venv/bin:/usr/local/bin:/usr/bin:/bin" +ExecStart=/home/maxi/Projekt/wtc.waiswuaschd.de/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8003 --workers 2 +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target