# ⚙️ 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.