feat: stabilization + recipe edit/create UI
This commit is contained in:
366
features/DATA-MODEL.md
Normal file
366
features/DATA-MODEL.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# 🗄️ Luna Recipes — Datenmodell
|
||||
|
||||
## 1. Entity-Relationship Übersicht
|
||||
|
||||
```
|
||||
┌──────────┐ ┌──────────────┐ ┌──────────┐
|
||||
│ Category │────<│ Recipe │>────│ Tag │
|
||||
└──────────┘ └──────┬───────┘ └──────────┘
|
||||
│
|
||||
┌─────────┼──────────┐
|
||||
│ │ │
|
||||
┌─────┴──┐ ┌────┴───┐ ┌───┴────┐
|
||||
│Ingredi-│ │ Step │ │ Note │
|
||||
│ ent │ │ │ │ │
|
||||
└────┬───┘ └────────┘ └────────┘
|
||||
│
|
||||
┌────┴────────┐
|
||||
│ShoppingItem │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## 2. Entitäten im Detail
|
||||
|
||||
### Recipe (Rezept)
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| id | TEXT (ULID) | Primärschlüssel |
|
||||
| title | TEXT | Rezeptname |
|
||||
| slug | TEXT | URL-freundlicher Name (unique) |
|
||||
| description | TEXT | Kurzbeschreibung |
|
||||
| image_path | TEXT | Pfad zum Hauptbild |
|
||||
| category_id | TEXT | FK → Category |
|
||||
| servings | INTEGER | Anzahl Portionen |
|
||||
| prep_time_min | INTEGER | Vorbereitungszeit in Minuten |
|
||||
| cook_time_min | INTEGER | Koch-/Backzeit in Minuten |
|
||||
| total_time_min | INTEGER | Gesamtzeit (computed oder manuell) |
|
||||
| difficulty | TEXT | 'easy', 'medium', 'hard' |
|
||||
| source_url | TEXT | Originalquelle (Pinterest-Link etc.) |
|
||||
| is_favorite | INTEGER | 0/1 Boolean |
|
||||
| is_draft | INTEGER | 0/1 Entwurf |
|
||||
| created_at | TEXT | ISO 8601 Timestamp |
|
||||
| updated_at | TEXT | ISO 8601 Timestamp |
|
||||
| created_by | TEXT | 'user' oder 'bot' |
|
||||
|
||||
### Category (Kategorie)
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| id | TEXT (ULID) | Primärschlüssel |
|
||||
| name | TEXT | Anzeigename |
|
||||
| slug | TEXT | URL-freundlich (unique) |
|
||||
| icon | TEXT | Emoji oder Icon-Name |
|
||||
| sort_order | INTEGER | Sortierung |
|
||||
| is_default | INTEGER | Vom System vorgegeben |
|
||||
|
||||
**Standard-Kategorien:** Backen, Torten, Frühstück, Mittag, Abend, Snacks, Desserts
|
||||
|
||||
### Tag
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| id | TEXT (ULID) | Primärschlüssel |
|
||||
| name | TEXT | Tag-Name (unique, lowercase) |
|
||||
|
||||
### RecipeTag (n:m Verknüpfung)
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| recipe_id | TEXT | FK → Recipe |
|
||||
| tag_id | TEXT | FK → Tag |
|
||||
|
||||
### Ingredient (Zutat)
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| id | TEXT (ULID) | Primärschlüssel |
|
||||
| recipe_id | TEXT | FK → Recipe |
|
||||
| amount | REAL | Menge (nullable für "eine Prise") |
|
||||
| unit | TEXT | Einheit (g, ml, EL, TL, Stück, ...) |
|
||||
| name | TEXT | Zutatname |
|
||||
| group_name | TEXT | Optionale Gruppe ("Für den Teig", "Für die Creme") |
|
||||
| sort_order | INTEGER | Reihenfolge |
|
||||
|
||||
### Step (Zubereitungsschritt)
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| id | TEXT (ULID) | Primärschlüssel |
|
||||
| recipe_id | TEXT | FK → Recipe |
|
||||
| step_number | INTEGER | Schrittnummer |
|
||||
| instruction | TEXT | Anweisung |
|
||||
| image_path | TEXT | Optionales Bild zum Schritt |
|
||||
| timer_minutes | INTEGER | Timer in Minuten (nullable) |
|
||||
| timer_label | TEXT | Timer-Beschreibung ("Im Ofen backen") |
|
||||
|
||||
### Note (Notiz)
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| id | TEXT (ULID) | Primärschlüssel |
|
||||
| recipe_id | TEXT | FK → Recipe |
|
||||
| content | TEXT | Markdown-Text |
|
||||
| created_at | TEXT | ISO 8601 |
|
||||
| updated_at | TEXT | ISO 8601 |
|
||||
|
||||
### ShoppingItem (Einkaufslisteneintrag)
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| id | TEXT (ULID) | Primärschlüssel |
|
||||
| ingredient_id | TEXT | FK → Ingredient (nullable) |
|
||||
| recipe_id | TEXT | FK → Recipe (nullable, für Gruppierung) |
|
||||
| name | TEXT | Anzeigename |
|
||||
| amount | REAL | Menge |
|
||||
| unit | TEXT | Einheit |
|
||||
| is_checked | INTEGER | 0/1 abgehakt |
|
||||
| is_custom | INTEGER | 0/1 manuell hinzugefügt |
|
||||
| sort_order | INTEGER | Sortierung |
|
||||
| created_at | TEXT | ISO 8601 |
|
||||
|
||||
## 3. SQLite Schema
|
||||
|
||||
```sql
|
||||
-- Pragmas
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- ============================================================
|
||||
-- Kategorien
|
||||
-- ============================================================
|
||||
CREATE TABLE categories (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
icon TEXT DEFAULT '🍽️',
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
is_default INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
INSERT INTO categories (id, name, slug, icon, sort_order, is_default) VALUES
|
||||
('cat_backen', 'Backen', 'backen', '🧁', 1, 1),
|
||||
('cat_torten', 'Torten', 'torten', '🎂', 2, 1),
|
||||
('cat_fruehstueck','Frühstück', 'fruehstueck','🥐', 3, 1),
|
||||
('cat_mittag', 'Mittag', 'mittag', '🍝', 4, 1),
|
||||
('cat_abend', 'Abend', 'abend', '🥘', 5, 1),
|
||||
('cat_snacks', 'Snacks', 'snacks', '🥨', 6, 1),
|
||||
('cat_desserts', 'Desserts', 'desserts', '🍮', 7, 1);
|
||||
|
||||
-- ============================================================
|
||||
-- Tags
|
||||
-- ============================================================
|
||||
CREATE TABLE tags (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tags_name ON tags(name);
|
||||
|
||||
-- ============================================================
|
||||
-- Rezepte
|
||||
-- ============================================================
|
||||
CREATE TABLE recipes (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
image_path TEXT,
|
||||
category_id TEXT NOT NULL REFERENCES categories(id),
|
||||
servings INTEGER DEFAULT 4,
|
||||
prep_time_min INTEGER,
|
||||
cook_time_min INTEGER,
|
||||
total_time_min INTEGER,
|
||||
difficulty TEXT DEFAULT 'medium' CHECK(difficulty IN ('easy','medium','hard')),
|
||||
source_url TEXT,
|
||||
is_favorite INTEGER DEFAULT 0,
|
||||
is_draft INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
created_by TEXT DEFAULT 'user'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_recipes_category ON recipes(category_id);
|
||||
CREATE INDEX idx_recipes_favorite ON recipes(is_favorite) WHERE is_favorite = 1;
|
||||
CREATE INDEX idx_recipes_slug ON recipes(slug);
|
||||
|
||||
-- Volltextsuche
|
||||
CREATE VIRTUAL TABLE recipes_fts USING fts5(
|
||||
title, description, content='recipes', content_rowid='rowid'
|
||||
);
|
||||
|
||||
-- Trigger für FTS-Sync
|
||||
CREATE TRIGGER recipes_ai AFTER INSERT ON recipes BEGIN
|
||||
INSERT INTO recipes_fts(rowid, title, description)
|
||||
VALUES (new.rowid, new.title, new.description);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER recipes_ad AFTER DELETE ON recipes BEGIN
|
||||
INSERT INTO recipes_fts(recipes_fts, rowid, title, description)
|
||||
VALUES ('delete', old.rowid, old.title, old.description);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER recipes_au AFTER UPDATE ON recipes BEGIN
|
||||
INSERT INTO recipes_fts(recipes_fts, rowid, title, description)
|
||||
VALUES ('delete', old.rowid, old.title, old.description);
|
||||
INSERT INTO recipes_fts(rowid, title, description)
|
||||
VALUES (new.rowid, new.title, new.description);
|
||||
END;
|
||||
|
||||
-- Auto-Update updated_at
|
||||
CREATE TRIGGER recipes_updated AFTER UPDATE ON recipes BEGIN
|
||||
UPDATE recipes SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ','now')
|
||||
WHERE id = new.id;
|
||||
END;
|
||||
|
||||
-- ============================================================
|
||||
-- Recipe-Tags (n:m)
|
||||
-- ============================================================
|
||||
CREATE TABLE recipe_tags (
|
||||
recipe_id TEXT NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
|
||||
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (recipe_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_recipe_tags_tag ON recipe_tags(tag_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Zutaten
|
||||
-- ============================================================
|
||||
CREATE TABLE ingredients (
|
||||
id TEXT PRIMARY KEY,
|
||||
recipe_id TEXT NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
|
||||
amount REAL,
|
||||
unit TEXT,
|
||||
name TEXT NOT NULL,
|
||||
group_name TEXT,
|
||||
sort_order INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ingredients_recipe ON ingredients(recipe_id);
|
||||
|
||||
-- Zutaten-Volltextsuche (für "Suche nach Zutat")
|
||||
CREATE VIRTUAL TABLE ingredients_fts USING fts5(
|
||||
name, content='ingredients', content_rowid='rowid'
|
||||
);
|
||||
|
||||
CREATE TRIGGER ingredients_ai AFTER INSERT ON ingredients BEGIN
|
||||
INSERT INTO ingredients_fts(rowid, name) VALUES (new.rowid, new.name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER ingredients_ad AFTER DELETE ON ingredients BEGIN
|
||||
INSERT INTO ingredients_fts(ingredients_fts, rowid, name)
|
||||
VALUES ('delete', old.rowid, old.name);
|
||||
END;
|
||||
|
||||
-- ============================================================
|
||||
-- Zubereitungsschritte
|
||||
-- ============================================================
|
||||
CREATE TABLE steps (
|
||||
id TEXT PRIMARY KEY,
|
||||
recipe_id TEXT NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
|
||||
step_number INTEGER NOT NULL,
|
||||
instruction TEXT NOT NULL,
|
||||
image_path TEXT,
|
||||
timer_minutes INTEGER,
|
||||
timer_label TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_steps_recipe ON steps(recipe_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Notizen
|
||||
-- ============================================================
|
||||
CREATE TABLE notes (
|
||||
id TEXT PRIMARY KEY,
|
||||
recipe_id TEXT NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_notes_recipe ON notes(recipe_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Einkaufsliste
|
||||
-- ============================================================
|
||||
CREATE TABLE shopping_items (
|
||||
id TEXT PRIMARY KEY,
|
||||
ingredient_id TEXT REFERENCES ingredients(id) ON DELETE SET NULL,
|
||||
recipe_id TEXT REFERENCES recipes(id) ON DELETE SET NULL,
|
||||
name TEXT NOT NULL,
|
||||
amount REAL,
|
||||
unit TEXT,
|
||||
is_checked INTEGER DEFAULT 0,
|
||||
is_custom INTEGER DEFAULT 0,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_shopping_recipe ON shopping_items(recipe_id);
|
||||
CREATE INDEX idx_shopping_checked ON shopping_items(is_checked);
|
||||
```
|
||||
|
||||
## 4. Beispiel-Queries
|
||||
|
||||
```sql
|
||||
-- Alle Rezepte einer Kategorie, neuste zuerst
|
||||
SELECT r.*, c.name as category_name, c.icon as category_icon
|
||||
FROM recipes r
|
||||
JOIN categories c ON r.category_id = c.id
|
||||
WHERE c.slug = 'torten' AND r.is_draft = 0
|
||||
ORDER BY r.created_at DESC;
|
||||
|
||||
-- Volltextsuche
|
||||
SELECT r.*
|
||||
FROM recipes r
|
||||
JOIN recipes_fts ON recipes_fts.rowid = r.rowid
|
||||
WHERE recipes_fts MATCH 'schokolade'
|
||||
ORDER BY rank;
|
||||
|
||||
-- Rezepte die eine bestimmte Zutat enthalten
|
||||
SELECT DISTINCT r.*
|
||||
FROM recipes r
|
||||
JOIN ingredients i ON i.recipe_id = r.id
|
||||
JOIN ingredients_fts ON ingredients_fts.rowid = i.rowid
|
||||
WHERE ingredients_fts MATCH 'mascarpone';
|
||||
|
||||
-- Komplettansicht eines Rezepts
|
||||
SELECT r.*, c.name as category_name
|
||||
FROM recipes r
|
||||
JOIN categories c ON r.category_id = c.id
|
||||
WHERE r.slug = 'schwarzwaelder-kirschtorte';
|
||||
|
||||
-- Zutaten für Einkaufsliste generieren
|
||||
INSERT INTO shopping_items (id, ingredient_id, recipe_id, name, amount, unit)
|
||||
SELECT
|
||||
'shop_' || hex(randomblob(8)),
|
||||
i.id,
|
||||
i.recipe_id,
|
||||
i.name,
|
||||
i.amount,
|
||||
i.unit
|
||||
FROM ingredients i
|
||||
WHERE i.recipe_id = ?;
|
||||
|
||||
-- Einkaufsliste gruppiert nach Rezept
|
||||
SELECT si.*, r.title as recipe_title
|
||||
FROM shopping_items si
|
||||
LEFT JOIN recipes r ON si.recipe_id = r.id
|
||||
ORDER BY si.is_checked ASC, r.title, si.sort_order;
|
||||
```
|
||||
|
||||
## 5. ID-Strategie
|
||||
|
||||
**ULID** (Universally Unique Lexicographically Sortable Identifier) statt Auto-Increment:
|
||||
- Sortierbar nach Erstellzeitpunkt
|
||||
- Generierbar auf Client und Server (offline-fähig)
|
||||
- Keine Kollisionen bei Bot-API + UI gleichzeitig
|
||||
- Format: `01ARZ3NDEKTSV4RRFFQ69G5FAV` (26 Zeichen)
|
||||
|
||||
## 6. Bild-Speicherung
|
||||
|
||||
```
|
||||
/data/images/
|
||||
recipes/
|
||||
{recipe_id}/
|
||||
hero.webp ← Hauptbild (max 1200px breit)
|
||||
hero_thumb.webp ← Thumbnail (400px)
|
||||
step_{n}.webp ← Schrittbilder
|
||||
```
|
||||
|
||||
- Format: WebP (beste Kompression für Web)
|
||||
- Thumbnails automatisch beim Upload generiert
|
||||
- Bilder werden über statische Route ausgeliefert: `/images/recipes/{id}/hero.webp`
|
||||
344
features/DESIGN-CONCEPT.md
Normal file
344
features/DESIGN-CONCEPT.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# 🎨 Luna Recipes — Design-Konzept
|
||||
|
||||
## 1. Farbschema
|
||||
|
||||
Warm, einladend, modern-feminin ohne kitschig zu wirken. Inspiriert von Backzutaten: Vanille, Karamell, Beeren.
|
||||
|
||||
| Rolle | Farbe | Hex | Verwendung |
|
||||
|---|---|---|---|
|
||||
| **Primary** | Dusty Rose | `#C4737E` | Buttons, aktive States, Akzente |
|
||||
| **Primary Light** | Soft Blush | `#F2D7DB` | Hover, Badges, Tags |
|
||||
| **Secondary** | Warm Caramel | `#D4A574` | Sekundäre Buttons, Icons |
|
||||
| **Background** | Cream White | `#FBF8F5` | Seitenhintergrund |
|
||||
| **Surface** | Pure White | `#FFFFFF` | Cards, Modals |
|
||||
| **Text Primary** | Espresso | `#2D2016` | Überschriften, Body |
|
||||
| **Text Secondary** | Warm Grey | `#7A6E65` | Subtexte, Metadaten |
|
||||
| **Success** | Sage Green | `#7BAE7F` | Timer fertig, Erfolg |
|
||||
| **Error** | Berry Red | `#C94C4C` | Fehler, Löschen |
|
||||
| **Border** | Sand | `#E8E0D8` | Trennlinien, Card-Borders |
|
||||
|
||||
### Dark Mode (optional, Phase 2)
|
||||
- Background: `#1A1614`
|
||||
- Surface: `#2D2620`
|
||||
- Text: `#F5EDE8`
|
||||
|
||||
## 2. Typografie
|
||||
|
||||
| Rolle | Font | Gewicht | Größe |
|
||||
|---|---|---|---|
|
||||
| **Logo/Brand** | *Playfair Display* | 700 | 28px |
|
||||
| **H1** | *Playfair Display* | 600 | 24px |
|
||||
| **H2** | *Playfair Display* | 600 | 20px |
|
||||
| **H3** | *Inter* | 600 | 17px |
|
||||
| **Body** | *Inter* | 400 | 15px |
|
||||
| **Small/Meta** | *Inter* | 400 | 13px |
|
||||
| **Kochmodus** | *Inter* | 500 | 22–28px |
|
||||
| **Timer** | *JetBrains Mono* | 500 | 48px |
|
||||
|
||||
> Playfair Display für Eleganz in Überschriften, Inter als saubere UI-Schrift. Monospace nur für Timer-Anzeige.
|
||||
|
||||
## 3. Screen-Übersicht
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ App-Struktur │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 📱 Bottom Navigation: │
|
||||
│ [Startseite] [Suche] [+Neu] [Liste] [Me] │
|
||||
│ │
|
||||
│ Screens: │
|
||||
│ ├── Startseite (Feed) │
|
||||
│ ├── Kategorie-Ansicht │
|
||||
│ ├── Suche + Filter │
|
||||
│ ├── Rezept-Detail │
|
||||
│ │ ├── Kochmodus (Fullscreen) │
|
||||
│ │ └── Notizen-Sheet │
|
||||
│ ├── Rezept erstellen/bearbeiten │
|
||||
│ ├── Einkaufsliste │
|
||||
│ ├── Profil / Einstellungen │
|
||||
│ └── Onboarding (Erststart) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 4. Component-Liste
|
||||
|
||||
### Layout
|
||||
- `AppShell` — Wrapper mit Bottom-Nav
|
||||
- `BottomNav` — 5 Icons, aktiver State mit Farbe + Label
|
||||
- `TopBar` — Kontextuell: Titel, Back-Button, Actions
|
||||
- `FullscreenOverlay` — Für Kochmodus
|
||||
|
||||
### Cards
|
||||
- `RecipeCard` — Foto (3:4), Titel, Kategorie-Badge, Dauer, Favorit-Herz
|
||||
- `RecipeCardSmall` — Horizontal, Thumbnail links, Infos rechts
|
||||
- `CategoryCard` — Rundes Bild + Label darunter
|
||||
|
||||
### Rezept-Detail
|
||||
- `HeroImage` — Parallax-Scroll Foto oben
|
||||
- `RecipeMeta` — Dauer, Portionen, Schwierigkeit als Icons
|
||||
- `IngredientList` — Checkbare Zutatenliste mit Mengenrechner
|
||||
- `StepList` — Nummerierte Schritte mit optionalen Bildern
|
||||
- `NoteSection` — Eigene Notizen mit Bearbeiten-Button
|
||||
- `ActionBar` — Kochmodus starten, zur Liste hinzufügen, Teilen
|
||||
|
||||
### Kochmodus
|
||||
- `CookingStep` — Großer Text, Schrittnummer, Swipe-Geste
|
||||
- `CookingTimer` — Kreisförmiger Countdown, Vibration bei Ende
|
||||
- `CookingProgress` — Fortschrittsbalken oben
|
||||
- `CookingNav` — Vor/Zurück-Buttons unten
|
||||
|
||||
### Suche & Filter
|
||||
- `SearchBar` — Prominent, mit Mikrofon-Icon (Phase 2)
|
||||
- `FilterChips` — Horizontale Scroll-Chips (Kategorie, Zeit, Zutat)
|
||||
- `FilterSheet` — Bottom-Sheet mit erweiterten Filtern
|
||||
|
||||
### Einkaufsliste
|
||||
- `ShoppingItem` — Checkbox + Zutat + Menge + Rezeptherkunft
|
||||
- `ShoppingSection` — Gruppiert nach Rezept oder Abteilung
|
||||
- `AddItemInput` — Schnelles Hinzufügen eigener Einträge
|
||||
|
||||
### Formulare
|
||||
- `RecipeForm` — Multi-Step Formular für Rezepterstellung
|
||||
- `ImageUpload` — Drag/Drop + Kamera-Auslöser
|
||||
- `IngredientInput` — Menge + Einheit + Zutat als Inline-Row
|
||||
- `StepInput` — Textfeld + optionales Bild + Timer-Feld
|
||||
|
||||
### Allgemein
|
||||
- `Button` — Primary, Secondary, Ghost, Danger Varianten
|
||||
- `Badge` — Kategorie-Tags farblich kodiert
|
||||
- `Toast` — Erfolgs-/Fehlermeldungen
|
||||
- `EmptyState` — Illustration + Text wenn Liste leer
|
||||
- `Skeleton` — Loading-Platzhalter für Cards
|
||||
|
||||
## 5. Wireframe-Beschreibungen
|
||||
|
||||
### 5.1 Startseite (Feed)
|
||||
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ 🍰 Luna Recipes [👤] │ ← TopBar mit Logo + Profil-Avatar
|
||||
├──────────────────────────┤
|
||||
│ ○Backen ○Torten ○Mittag │ ← Horizontale Kategorie-Chips (scrollbar)
|
||||
│ ○Frühstück ○Snacks ... │
|
||||
├──────────────────────────┤
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ 📷 │ │ 📷 │ │ ← Pinterest-artiges Masonry Grid
|
||||
│ │ │ │ groß │ │ Unterschiedliche Höhen
|
||||
│ │ Titel │ │ │ │ Tap → Rezept-Detail
|
||||
│ │ 30min ❤│ │ Titel │ │
|
||||
│ └────────┘ │ 45min ❤│ │
|
||||
│ ┌────────┐ └────────┘ │
|
||||
│ │ 📷 │ ┌────────┐ │
|
||||
│ │ Titel │ │ 📷 │ │
|
||||
│ │ 20min ❤│ │ Titel │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
├──────────────────────────┤
|
||||
│ [🏠] [🔍] [➕] [🛒] [👤] │ ← Bottom Navigation
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
- Standard: Alle Rezepte, neuste zuerst
|
||||
- Kategorie-Chips filtern den Feed inline
|
||||
- Pull-to-Refresh
|
||||
- Endlos-Scroll mit Lazy Loading
|
||||
- Favorit-Herz direkt auf der Card antippbar
|
||||
|
||||
### 5.2 Suche + Filter
|
||||
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ [← ] 🔍 Suche... [✕] │ ← Autofokus auf Suchfeld
|
||||
├──────────────────────────┤
|
||||
│ Letzte Suchen: │
|
||||
│ Schokoladentorte Pasta │ ← Antippbare Tags
|
||||
├──────────────────────────┤
|
||||
│ [Kategorie ▼] [Zeit ▼] │ ← Filter-Chips
|
||||
│ [Schwierigkeit ▼] │
|
||||
├──────────────────────────┤
|
||||
│ 3 Ergebnisse │
|
||||
│ ┌────────────────────┐ │
|
||||
│ │ 🖼 │ Schwarzwälder │ │ ← RecipeCardSmall Layout
|
||||
│ │ │ Kirschtorte │ │ Horizontal für Suchergebnisse
|
||||
│ │ │ ⏱90min ⭐Mittel │ │
|
||||
│ └────────────────────┘ │
|
||||
│ ┌────────────────────┐ │
|
||||
│ │ 🖼 │ Schoko-Mousse │ │
|
||||
│ └────────────────────┘ │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
- Suche durchsucht: Titel, Zutaten, Tags, Notizen
|
||||
- Debounced (300ms) Live-Ergebnisse
|
||||
- Filter kombinierbar (AND-Verknüpfung)
|
||||
- Zeitfilter: <15min, 15–30, 30–60, >60min
|
||||
|
||||
### 5.3 Rezept-Detail
|
||||
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ [←] [❤] [⋯]│ ← Floating über Hero-Bild
|
||||
│ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ │ │ ← Hero-Image, 16:10
|
||||
│ │ 📷 Foto │ │ Parallax beim Scrollen
|
||||
│ │ │ │
|
||||
│ └──────────────┘ │
|
||||
├──────────────────────────┤
|
||||
│ Schwarzwälder Kirschtorte│ ← H1 Playfair Display
|
||||
│ 🏷 Torten ⏱ 90min │
|
||||
│ 👤 8 Portionen ⭐ Mittel │
|
||||
├──────────────────────────┤
|
||||
│ [🍳 Kochen] [🛒 Liste] │ ← Primary + Secondary Button
|
||||
├──────────────────────────┤
|
||||
│ ── Zutaten ──────────────│
|
||||
│ Portionen: [−] 8 [+] │ ← Mengenrechner
|
||||
│ ☐ 200g Mehl │
|
||||
│ ☐ 150g Zucker │ ← Antippbar → Einkaufsliste
|
||||
│ ☐ 5 Eier │
|
||||
│ ☐ 500ml Sahne │
|
||||
│ ... │
|
||||
├──────────────────────────┤
|
||||
│ ── Zubereitung ──────────│
|
||||
│ 1. Eier trennen und... │
|
||||
│ 2. Mehl mit Kakao... │ ← Kompakte Schritt-Liste
|
||||
│ 3. Backofen auf 180°... │
|
||||
│ ... │
|
||||
├──────────────────────────┤
|
||||
│ ── Meine Notizen ────────│
|
||||
│ 📝 "Nächstes Mal weniger │ ← Editierbar
|
||||
│ Zucker, dafür mehr │
|
||||
│ Kirschen!" │
|
||||
│ [✏️ Bearbeiten] │
|
||||
├──────────────────────────┤
|
||||
│ [🏠] [🔍] [➕] [🛒] [👤] │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
- Drei-Punkte-Menü (⋯): Bearbeiten, Teilen, Löschen
|
||||
- Zutaten-Checkbox → einzeln zur Einkaufsliste
|
||||
- Portionen-Rechner skaliert alle Mengen live
|
||||
- Notizen: Inline-Edit, Markdown-Unterstützung
|
||||
|
||||
### 5.4 Kochmodus (Fullscreen)
|
||||
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ Schritt 3 von 12 [✕] │ ← Progress-Info + Schließen
|
||||
│ ████████░░░░░░░░░░░░░░░░ │ ← Fortschrittsbalken
|
||||
├──────────────────────────┤
|
||||
│ │
|
||||
│ │
|
||||
│ Backofen auf 180°C │ ← GROSSE SCHRIFT (22–28px)
|
||||
│ Ober-/Unterhitze │ Zentriert
|
||||
│ vorheizen. │ Max 3-4 Zeilen
|
||||
│ │
|
||||
│ │
|
||||
│ ┌──────────┐ │
|
||||
│ │ 05:00 │ │ ← Timer (falls Schritt Timer hat)
|
||||
│ │ ▶ Start │ │ Kreisförmig
|
||||
│ └──────────┘ │
|
||||
│ │
|
||||
├──────────────────────────┤
|
||||
│ [← Zurück] [Weiter →] │ ← Navigation + Swipe-Geste
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
- Bildschirm bleibt an (Wake Lock API)
|
||||
- Swipe links/rechts für Navigation
|
||||
- Timer: Antippen startet, Vibration + Sound bei 0:00
|
||||
- Mehrere Timer gleichzeitig möglich (Badge-Anzeige)
|
||||
- Schließen fragt "Wirklich beenden?"
|
||||
|
||||
### 5.5 Einkaufsliste
|
||||
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ Einkaufsliste [🗑 ✓] │ ← Erledigte löschen
|
||||
├──────────────────────────┤
|
||||
│ [+ Eigenen Artikel ...] │ ← Quick-Add Input
|
||||
├──────────────────────────┤
|
||||
│ 🍰 Schwarzwälder Kirsch. │ ← Gruppiert nach Rezept
|
||||
│ ☐ 200g Mehl │
|
||||
│ ☑ 150g Zucker │ ← Durchgestrichen
|
||||
│ ☐ 5 Eier │
|
||||
│ ☐ 500ml Sahne │
|
||||
├──────────────────────────┤
|
||||
│ 🥗 Caesar Salad │
|
||||
│ ☐ 1 Römersalat │
|
||||
│ ☐ Parmesan │
|
||||
├──────────────────────────┤
|
||||
│ 📝 Eigene │
|
||||
│ ☐ Küchenrolle │
|
||||
├──────────────────────────┤
|
||||
│ [🏠] [🔍] [➕] [🛒] [👤] │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
- Gleiche Zutaten aus verschiedenen Rezepten werden zusammengefasst (Phase 2)
|
||||
- Wisch nach links → Artikel entfernen
|
||||
- Sortierbar: nach Rezept oder nach Abteilung
|
||||
- Offline verfügbar (PWA!)
|
||||
|
||||
### 5.6 Rezept erstellen/bearbeiten
|
||||
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ [✕] Neues Rezept [Save] │
|
||||
├──────────────────────────┤
|
||||
│ Schritt 1 von 4 │
|
||||
│ ●───○───○───○ │ ← Stepper
|
||||
├──────────────────────────┤
|
||||
│ │
|
||||
│ [📷 Foto hinzufügen] │ ← Kamera oder Galerie
|
||||
│ │
|
||||
│ Titel * │
|
||||
│ [________________________]│
|
||||
│ │
|
||||
│ Kategorie * │
|
||||
│ [Backen ▼] │
|
||||
│ │
|
||||
│ Tags │
|
||||
│ [Schoko] [Torte] [+ ...]│
|
||||
│ │
|
||||
│ Portionen Dauer │
|
||||
│ [_8_] [_90_] min │
|
||||
│ │
|
||||
│ Schwierigkeit │
|
||||
│ ○ Einfach ● Mittel ○ Pro│
|
||||
│ │
|
||||
│ [Weiter →] │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
- 4 Schritte: Basis → Zutaten → Zubereitung → Vorschau
|
||||
- Auto-Save als Entwurf
|
||||
- Zutaten: dynamische Rows (Menge + Einheit + Name)
|
||||
- Zubereitung: Schritte mit optionalem Timer pro Schritt
|
||||
|
||||
### 5.7 Profil / Einstellungen
|
||||
|
||||
- Anzahl Rezepte, Favoriten
|
||||
- Kategorien verwalten (eigene hinzufügen)
|
||||
- Einheiten-Präferenz (metrisch/imperial)
|
||||
- Dark Mode Toggle
|
||||
- Daten exportieren/importieren
|
||||
- App-Version & Info
|
||||
|
||||
## 6. Design-Prinzipien
|
||||
|
||||
1. **Fotos im Fokus** — Große, appetitliche Bilder treiben die Navigation
|
||||
2. **One-Thumb-Use** — Alle wichtigen Aktionen im Daumenbereich
|
||||
3. **Kochmodus = Stressfrei** — Große Schrift, einfache Navigation, keine Ablenkung
|
||||
4. **Schnelles Wiederfinden** — Suche, Kategorien, Favoriten auf max 2 Taps
|
||||
5. **Offline First** — Einkaufsliste und gespeicherte Rezepte immer verfügbar
|
||||
|
||||
## 7. Animationen & Micro-Interactions
|
||||
|
||||
- Card-Tap: Sanftes Scale-Up (0.98 → 1.0)
|
||||
- Favorit-Herz: Kurze Bounce-Animation + Farbwechsel
|
||||
- Kochmodus-Swipe: Slide-Transition zwischen Schritten
|
||||
- Timer-Ende: Pulsieren + Vibration
|
||||
- Pull-to-Refresh: Custom Cupcake-Animation (Phase 2)
|
||||
- Page-Transitions: Shared Element Transition für Hero-Image (Card → Detail)
|
||||
27
features/LESSONS-FROM-WERK.md
Normal file
27
features/LESSONS-FROM-WERK.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Lessons from WERK — Was wir bei der Rezept-App NICHT wiederholen
|
||||
|
||||
## Architektur
|
||||
- **Kein Monorepo** — Eine App, ein Repo, fertig
|
||||
- **Kein shared-stubs Chaos** — Kein Packages-Ordner, alles in einem Projekt
|
||||
- **SQLite statt PostgreSQL** — Kein Docker für DB nötig, einfacher
|
||||
- **Kein SSR** — Reines SPA/PWA, kein hydrateRoot-Desaster
|
||||
|
||||
## Frontend
|
||||
- **Relative API-URLs** — Kein hardcoded localhost in .env
|
||||
- **Keine Radix Dialog Forms** — Forms als eigene Seiten/Routes
|
||||
- **Mobile-first testen** — Von Anfang an, nicht nachträglich
|
||||
|
||||
## Backend
|
||||
- **Fastify JWT richtig** — `register(jwt); await app.after();`
|
||||
- **NODE_ENV=production** in Deployment, immer
|
||||
- **Kein pino-pretty** in Production
|
||||
|
||||
## Deployment
|
||||
- **Docker simpel** — Ein Container, ein Service
|
||||
- **Keine Caddy URI-Rewrites** — Einfaches Routing
|
||||
- **CORS von Anfang an richtig** — Mit Port testen
|
||||
|
||||
## Vorgehen
|
||||
- **Inkrementell** — Erst lauffähiges Minimum, dann Features
|
||||
- **Jeden Schritt testen** — Nicht 5 Features auf einmal, einzeln bauen + verifizieren
|
||||
- **Agent-Tasks klein halten** — Ein Task = eine Sache
|
||||
418
features/TECH-STACK.md
Normal file
418
features/TECH-STACK.md
Normal file
@@ -0,0 +1,418 @@
|
||||
# ⚙️ Luna Recipes — Tech-Stack
|
||||
|
||||
## 1. Übersicht
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Frontend (PWA) │
|
||||
│ React 19 + Vite + TailwindCSS v4 │
|
||||
│ React Router v7 · Tanstack Query v5 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Backend (API) │
|
||||
│ Fastify v5 + better-sqlite3 │
|
||||
│ Bild-Upload: @fastify/multipart │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Datenbank │
|
||||
│ SQLite (WAL-Mode) + FTS5 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 2. Frontend
|
||||
|
||||
### Core
|
||||
| Paket | Version | Zweck |
|
||||
|---|---|---|
|
||||
| `react` | ^19.0 | UI-Framework |
|
||||
| `react-dom` | ^19.0 | DOM-Rendering |
|
||||
| `vite` | ^6.x | Build-Tool, Dev-Server, HMR |
|
||||
| `tailwindcss` | ^4.x | Utility-first CSS |
|
||||
| `react-router` | ^7.x | Client-Side Routing |
|
||||
| `@tanstack/react-query` | ^5.x | Server-State, Caching, Mutations |
|
||||
|
||||
### UI & Interaktion
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `react-masonry-css` | Pinterest-artiges Grid-Layout |
|
||||
| `framer-motion` | Animationen, Page-Transitions, Swipe-Gesten |
|
||||
| `react-hot-toast` | Toast-Benachrichtigungen |
|
||||
| `lucide-react` | Icon-Set (sauber, konsistent) |
|
||||
| `react-hook-form` + `zod` | Formulare + Validierung |
|
||||
|
||||
### PWA
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `vite-plugin-pwa` | Service Worker Generation, Manifest |
|
||||
| `workbox` (via Plugin) | Caching-Strategien, Offline-Support |
|
||||
|
||||
### Projektstruktur
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── public/
|
||||
│ ├── icons/ ← PWA Icons (192, 512px)
|
||||
│ └── manifest.webmanifest
|
||||
├── src/
|
||||
│ ├── main.tsx
|
||||
│ ├── App.tsx
|
||||
│ ├── api/ ← API-Client (fetch-Wrapper)
|
||||
│ │ ├── client.ts
|
||||
│ │ ├── recipes.ts
|
||||
│ │ ├── shopping.ts
|
||||
│ │ └── types.ts
|
||||
│ ├── components/
|
||||
│ │ ├── layout/ ← AppShell, BottomNav, TopBar
|
||||
│ │ ├── recipe/ ← RecipeCard, IngredientList, StepList
|
||||
│ │ ├── cooking/ ← CookingStep, CookingTimer
|
||||
│ │ ├── shopping/ ← ShoppingItem, ShoppingSection
|
||||
│ │ ├── forms/ ← RecipeForm, ImageUpload
|
||||
│ │ └── ui/ ← Button, Badge, Input, Skeleton
|
||||
│ ├── pages/
|
||||
│ │ ├── HomePage.tsx
|
||||
│ │ ├── SearchPage.tsx
|
||||
│ │ ├── RecipePage.tsx
|
||||
│ │ ├── CookingModePage.tsx
|
||||
│ │ ├── ShoppingPage.tsx
|
||||
│ │ ├── CreateRecipePage.tsx
|
||||
│ │ └── SettingsPage.tsx
|
||||
│ ├── hooks/ ← useRecipes, useTimer, useWakeLock
|
||||
│ ├── lib/ ← Hilfsfunktionen, Konstanten
|
||||
│ └── styles/
|
||||
│ └── globals.css ← Tailwind-Imports, Custom Properties
|
||||
├── index.html
|
||||
├── tailwind.config.ts
|
||||
├── vite.config.ts
|
||||
├── tsconfig.json
|
||||
└── package.json
|
||||
```
|
||||
|
||||
### PWA-Konfiguration (vite.config.ts)
|
||||
|
||||
```typescript
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: ['icons/*.png'],
|
||||
manifest: {
|
||||
name: 'Luna Recipes',
|
||||
short_name: 'Luna',
|
||||
description: 'Deine persönliche Rezeptsammlung',
|
||||
theme_color: '#C4737E',
|
||||
background_color: '#FBF8F5',
|
||||
display: 'standalone',
|
||||
orientation: 'portrait',
|
||||
start_url: '/',
|
||||
icons: [
|
||||
{ src: 'icons/icon-192.png', sizes: '192x192', type: 'image/png' },
|
||||
{ src: 'icons/icon-512.png', sizes: '512x512', type: 'image/png' },
|
||||
{ src: 'icons/icon-512-maskable.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /\/api\/recipes/,
|
||||
handler: 'StaleWhileRevalidate',
|
||||
options: { cacheName: 'recipes-cache', expiration: { maxEntries: 200 } },
|
||||
},
|
||||
{
|
||||
urlPattern: /\/images\//,
|
||||
handler: 'CacheFirst',
|
||||
options: { cacheName: 'image-cache', expiration: { maxEntries: 500, maxAgeSeconds: 30 * 24 * 60 * 60 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Tailwind Theme (tailwind.config.ts)
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: { DEFAULT: '#C4737E', light: '#F2D7DB' },
|
||||
secondary: '#D4A574',
|
||||
cream: '#FBF8F5',
|
||||
espresso: '#2D2016',
|
||||
'warm-grey': '#7A6E65',
|
||||
sage: '#7BAE7F',
|
||||
berry: '#C94C4C',
|
||||
sand: '#E8E0D8',
|
||||
},
|
||||
fontFamily: {
|
||||
display: ['Playfair Display', 'serif'],
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'monospace'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 3. Backend
|
||||
|
||||
### Core
|
||||
| Paket | Zweck |
|
||||
|---|---|
|
||||
| `fastify` v5 | HTTP-Framework (schnell, Schema-basiert) |
|
||||
| `better-sqlite3` | SQLite-Treiber (synchron, schnell) |
|
||||
| `@fastify/multipart` | Bild-Upload |
|
||||
| `@fastify/static` | Statische Dateien (Bilder) |
|
||||
| `@fastify/cors` | CORS für Frontend |
|
||||
| `@fastify/rate-limit` | Rate-Limiting (Bot-API Schutz) |
|
||||
| `sharp` | Bild-Verarbeitung (Resize, WebP) |
|
||||
| `ulid` | ID-Generierung |
|
||||
| `zod` | Request-Validierung |
|
||||
|
||||
### Projektstruktur
|
||||
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── index.ts ← Server-Start
|
||||
│ ├── app.ts ← Fastify-App Setup
|
||||
│ ├── db/
|
||||
│ │ ├── connection.ts ← SQLite-Verbindung
|
||||
│ │ ├── migrate.ts ← Schema-Migrationen
|
||||
│ │ └── migrations/ ← SQL-Dateien
|
||||
│ ├── routes/
|
||||
│ │ ├── recipes.ts ← CRUD + Suche
|
||||
│ │ ├── categories.ts ← Kategorien
|
||||
│ │ ├── shopping.ts ← Einkaufsliste
|
||||
│ │ ├── notes.ts ← Notizen
|
||||
│ │ ├── images.ts ← Bild-Upload
|
||||
│ │ └── bot.ts ← Bot-API (Token-Auth)
|
||||
│ ├── services/
|
||||
│ │ ├── recipe.service.ts
|
||||
│ │ ├── shopping.service.ts
|
||||
│ │ └── image.service.ts
|
||||
│ ├── schemas/ ← Zod-Schemas
|
||||
│ └── lib/ ← Hilfsfunktionen
|
||||
├── data/
|
||||
│ ├── luna.db ← SQLite-Datenbank
|
||||
│ └── images/ ← Hochgeladene Bilder
|
||||
├── tsconfig.json
|
||||
└── package.json
|
||||
```
|
||||
|
||||
### Authentifizierung
|
||||
|
||||
Einfaches Token-System (Single-User App):
|
||||
|
||||
```
|
||||
# .env
|
||||
BOT_API_TOKEN=geheimer-bot-token-hier
|
||||
ADMIN_TOKEN=optionaler-admin-token
|
||||
```
|
||||
|
||||
- **Frontend:** Kein Auth nötig (lokale Nutzung / privat)
|
||||
- **Bot-API:** Bearer-Token im Header: `Authorization: Bearer {BOT_API_TOKEN}`
|
||||
- **Optional Phase 2:** Session-basierte Auth falls öffentlich gehostet
|
||||
|
||||
## 4. API-Endpunkte
|
||||
|
||||
### Rezepte
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/recipes` | Alle Rezepte (paginiert, filterbar) |
|
||||
| `GET` | `/api/recipes/:slug` | Einzelnes Rezept (komplett mit Zutaten, Steps, Notes) |
|
||||
| `POST` | `/api/recipes` | Neues Rezept erstellen |
|
||||
| `PUT` | `/api/recipes/:id` | Rezept aktualisieren |
|
||||
| `DELETE` | `/api/recipes/:id` | Rezept löschen |
|
||||
| `PATCH` | `/api/recipes/:id/favorite` | Favorit togglen |
|
||||
| `GET` | `/api/recipes/search` | Volltextsuche |
|
||||
|
||||
#### Query-Parameter für `GET /api/recipes`
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| `category` | string | Filter nach Kategorie-Slug |
|
||||
| `favorite` | boolean | Nur Favoriten |
|
||||
| `difficulty` | string | easy/medium/hard |
|
||||
| `maxTime` | number | Maximale Gesamtzeit in Minuten |
|
||||
| `ingredient` | string | Enthält Zutat (Textsuche) |
|
||||
| `page` | number | Seite (default: 1) |
|
||||
| `limit` | number | Einträge pro Seite (default: 20, max: 50) |
|
||||
| `sort` | string | `newest`, `oldest`, `title`, `time` |
|
||||
|
||||
#### Request-Body `POST /api/recipes`
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Schwarzwälder Kirschtorte",
|
||||
"description": "Klassiker der deutschen Backkunst",
|
||||
"category_id": "cat_torten",
|
||||
"servings": 12,
|
||||
"prep_time_min": 45,
|
||||
"cook_time_min": 35,
|
||||
"difficulty": "medium",
|
||||
"source_url": "https://pinterest.com/...",
|
||||
"tags": ["schokolade", "kirschen", "sahne"],
|
||||
"ingredients": [
|
||||
{ "amount": 200, "unit": "g", "name": "Mehl", "group_name": "Teig" },
|
||||
{ "amount": 150, "unit": "g", "name": "Zucker", "group_name": "Teig" },
|
||||
{ "amount": 500, "unit": "ml", "name": "Sahne", "group_name": "Füllung" }
|
||||
],
|
||||
"steps": [
|
||||
{ "instruction": "Eier trennen und Eiweiß steif schlagen.", "timer_minutes": null },
|
||||
{ "instruction": "Teig in Springform füllen und backen.", "timer_minutes": 35, "timer_label": "Backen" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Kategorien
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/categories` | Alle Kategorien |
|
||||
| `POST` | `/api/categories` | Neue Kategorie (custom) |
|
||||
|
||||
### Notizen
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/recipes/:id/notes` | Notizen eines Rezepts |
|
||||
| `POST` | `/api/recipes/:id/notes` | Notiz hinzufügen |
|
||||
| `PUT` | `/api/notes/:id` | Notiz bearbeiten |
|
||||
| `DELETE` | `/api/notes/:id` | Notiz löschen |
|
||||
|
||||
### Einkaufsliste
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/shopping` | Aktuelle Einkaufsliste |
|
||||
| `POST` | `/api/shopping/from-recipe/:id` | Zutaten eines Rezepts hinzufügen |
|
||||
| `POST` | `/api/shopping` | Eigenen Eintrag hinzufügen |
|
||||
| `PATCH` | `/api/shopping/:id/check` | Abhaken/Enthaken |
|
||||
| `DELETE` | `/api/shopping/:id` | Eintrag entfernen |
|
||||
| `DELETE` | `/api/shopping/checked` | Alle abgehakten entfernen |
|
||||
|
||||
### Bilder
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---|---|---|
|
||||
| `POST` | `/api/recipes/:id/image` | Hauptbild hochladen (multipart) |
|
||||
| `POST` | `/api/recipes/:id/steps/:n/image` | Schrittbild hochladen |
|
||||
|
||||
### Bot-API
|
||||
|
||||
Alle Bot-Endpunkte erfordern `Authorization: Bearer {token}`.
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---|---|---|
|
||||
| `POST` | `/api/bot/recipes` | Rezept per Bot erstellen (wie POST /api/recipes) |
|
||||
| `POST` | `/api/bot/recipes/:id/image` | Bild per URL importieren |
|
||||
| `GET` | `/api/bot/recipes` | Rezepte auflisten (für Bot-Abfragen) |
|
||||
|
||||
#### Bot-spezifisches Feld
|
||||
|
||||
```json
|
||||
{
|
||||
"image_url": "https://example.com/foto.jpg",
|
||||
"...normaler recipe body..."
|
||||
}
|
||||
```
|
||||
|
||||
Das Backend lädt das Bild herunter, konvertiert zu WebP, erstellt Thumbnail.
|
||||
|
||||
### Tags
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/tags` | Alle Tags (mit Anzahl Rezepte) |
|
||||
| `GET` | `/api/tags/:name/recipes` | Rezepte eines Tags |
|
||||
|
||||
## 5. Deployment
|
||||
|
||||
### Entwicklung
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
cd backend && npm install && npm run dev # Port 3000
|
||||
|
||||
# Frontend
|
||||
cd frontend && npm install && npm run dev # Port 5173 (Proxy → 3000)
|
||||
```
|
||||
|
||||
### Produktion
|
||||
|
||||
```bash
|
||||
# Frontend bauen
|
||||
cd frontend && npm run build # → dist/
|
||||
|
||||
# Backend serviert Frontend-Build als Static Files
|
||||
# Alles als ein Prozess:
|
||||
cd backend && NODE_ENV=production npm start
|
||||
```
|
||||
|
||||
### Docker (empfohlen)
|
||||
|
||||
```dockerfile
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Backend Dependencies
|
||||
COPY backend/package*.json backend/
|
||||
RUN cd backend && npm ci --production
|
||||
|
||||
# Frontend Build
|
||||
COPY frontend/ frontend/
|
||||
RUN cd frontend && npm ci && npm run build
|
||||
|
||||
# Backend Source
|
||||
COPY backend/ backend/
|
||||
|
||||
# Daten-Verzeichnis
|
||||
VOLUME /app/data
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV DATABASE_PATH=/app/data/luna.db
|
||||
ENV IMAGES_PATH=/app/data/images
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["node", "backend/dist/index.js"]
|
||||
```
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
luna:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
environment:
|
||||
- BOT_API_TOKEN=${BOT_API_TOKEN}
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
## 6. Performance-Überlegungen
|
||||
|
||||
- **SQLite WAL-Mode** → Paralleles Lesen während Schreiben
|
||||
- **FTS5** → Schnelle Volltextsuche ohne externen Service
|
||||
- **Bild-Thumbnails** → Kleine Bilder für Card-Grid, große nur bei Detail
|
||||
- **Stale-While-Revalidate** → Instant UI, Background-Refresh
|
||||
- **CacheFirst für Bilder** → Bilder ändern sich selten
|
||||
- **Lazy Loading** → Bilder + Seiten on-demand laden
|
||||
- **Bundle-Splitting** → Kochmodus als separater Chunk
|
||||
|
||||
## 7. Offline-Strategie (PWA)
|
||||
|
||||
| Ressource | Strategie | Cache |
|
||||
|---|---|---|
|
||||
| App-Shell (HTML/JS/CSS) | Precache | Beim Build |
|
||||
| API-Responses | StaleWhileRevalidate | Runtime |
|
||||
| Bilder | CacheFirst | Runtime, max 500 |
|
||||
| Einkaufsliste | NetworkFirst | Runtime |
|
||||
|
||||
Die Einkaufsliste bekommt NetworkFirst, damit im Laden immer der aktuellste Stand gezeigt wird, aber offline trotzdem funktioniert.
|
||||
Reference in New Issue
Block a user