feat: stabilization + recipe edit/create UI

This commit is contained in:
clawd
2026-02-18 09:55:39 +00:00
commit ee452efa6a
75 changed files with 15160 additions and 0 deletions

366
features/DATA-MODEL.md Normal file
View 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
View 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 | 2228px |
| **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, 1530, 3060, >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 (2228px)
│ 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)

View 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
View 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.