feat: stabilization + recipe edit/create UI
This commit is contained in:
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