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

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.