13 KiB
13 KiB
⚙️ 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)
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)
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
{
"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
{
"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
# Backend
cd backend && npm install && npm run dev # Port 3000
# Frontend
cd frontend && npm install && npm run dev # Port 5173 (Proxy → 3000)
Produktion
# 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)
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"]
# 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.