Erster commit wie von Claude erstellt (unverändert)

This commit is contained in:
rxf
2025-10-03 18:16:58 +02:00
commit b3160de204
23 changed files with 1955 additions and 0 deletions

39
.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
k# Dependencies
node_modules/
package-lock.json
# Environment Variables
.env
.env.local
.env.production
# Uploads (Bilder nicht in Git)
uploads/
# Logs
logs/
*.log
npm-debug.log*
# OS Files
.DS_Store
Thumbs.db
desktop.ini
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Docker
docker-compose.override.yml
# Build
dist/
build/
# Temporary files
tmp/
temp/

BIN
Aneitung_1.pdf Normal file

Binary file not shown.

BIN
Anleitung.pdf Normal file

Binary file not shown.

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM node:22-alpine
WORKDIR /app
# Package files kopieren
COPY server/package*.json ./
# Dependencies installieren
RUN npm ci --only=production
# Source Code kopieren
COPY server/ ./
# Public und uploads Ordner
COPY public/ ../public/
RUN mkdir -p ../uploads
# Non-root User
USER node
EXPOSE 3000
CMD ["node", "index.js"]

188
GIT_SETUP.md Normal file
View File

@@ -0,0 +1,188 @@
# Git Setup & Workflow
## Initiales Repository erstellen
```bash
# 1. Git Repository initialisieren
git init
# 2. Alle Dateien hinzufügen
git add .
# 3. Ersten Commit erstellen
git commit -m "Initial commit: PHP zu Node.js Migration"
# 4. Main Branch umbenennen (optional, wenn du 'main' statt 'master' willst)
git branch -M main
```
## Remote Repository verbinden
### GitHub
```bash
# Repository auf GitHub erstellen, dann:
git remote add origin https://github.com/DEIN_USERNAME/recipe-app.git
git push -u origin main
```
### GitLab
```bash
git remote add origin https://gitlab.com/DEIN_USERNAME/recipe-app.git
git push -u origin main
```
### Eigener Git-Server
```bash
git remote add origin git@dein-server.de:/pfad/zu/recipe-app.git
git push -u origin main
```
## Empfohlene Branch-Strategie
```bash
# Feature-Branch erstellen
git checkout -b feature/neue-funktion
# Änderungen committen
git add .
git commit -m "feat: Neue Funktion hinzugefügt"
# In main mergen
git checkout main
git merge feature/neue-funktion
# Feature-Branch löschen
git branch -d feature/neue-funktion
```
## Commit-Konventionen
```bash
# Feature
git commit -m "feat: Neue Suchfunktion für Kategorien"
# Bugfix
git commit -m "fix: Bild-Upload bei langen Dateinamen"
# Dokumentation
git commit -m "docs: README aktualisiert"
# Refactoring
git commit -m "refactor: DB-Verbindung optimiert"
# Style
git commit -m "style: CSS für mobile Ansicht verbessert"
# Tests
git commit -m "test: Unit-Tests für Rezept-API"
```
## Wichtige Dateien NICHT in Git
Die `.gitignore` verhindert, dass folgende Dateien committed werden:
- `node_modules/` - Dependencies (werden via `npm install` installiert)
- `.env` - Enthält sensible Zugangsdaten
- `uploads/` - Benutzergenerierte Bilder (zu groß für Git)
## Uploads separat sichern
Da `uploads/` nicht in Git ist, erstelle ein separates Backup:
```bash
# Backup erstellen
tar -czf uploads-backup-$(date +%Y%m%d).tar.gz uploads/
# Oder mit rsync zu Backup-Server
rsync -avz uploads/ user@backup-server:/backups/recipe-app/uploads/
```
## .env Template
Erstelle eine `.env.example` für andere Entwickler:
```bash
# .env.example (WIRD in Git committed)
PORT=3000
NODE_ENV=development
MONGODB_URI=mongodb://localhost:27017/recipes
```
Dann:
```bash
git add .env.example
git commit -m "docs: .env.example hinzugefügt"
```
Andere Entwickler kopieren dann:
```bash
cp .env.example .env
# Dann .env mit echten Werten anpassen
```
## Git-Befehle Cheatsheet
```bash
# Status anzeigen
git status
# Änderungen anzeigen
git diff
# Log anzeigen
git log --oneline
# Bestimmte Datei rückgängig machen
git checkout -- datei.js
# Letzten Commit rückgängig (behält Änderungen)
git reset --soft HEAD~1
# Alle lokalen Änderungen verwerfen
git reset --hard HEAD
# Remote-Änderungen holen
git pull
# Branch wechseln
git checkout branch-name
# Alle Branches anzeigen
git branch -a
```
## GitHub Actions (CI/CD) - Optional
Erstelle `.github/workflows/deploy.yml` für automatisches Deployment:
```yaml
name: Deploy
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to Server
run: |
# SSH in deinen Server und Pull+Restart
```
## Zusammenarbeit mit anderen
```bash
# Anderen Entwickler hinzufügen
# Auf GitHub: Settings → Collaborators → Add people
# Änderungen holen und mergen
git pull origin main
# Bei Merge-Konflikten
git status # Zeigt konfliktreiche Dateien
# Dateien manuell bearbeiten und Konflikte lösen
git add .
git commit -m "fix: Merge-Konflikte gelöst"
```

151
README.md Normal file
View File

@@ -0,0 +1,151 @@
# Rezepte WebApp - Node.js/MongoDB Migration
Vollständige Migration der PHP/MySQL Rezepte-App zu Node.js/Express mit MongoDB.
## Stack
- **Backend**: Node.js 22 + Express.js
- **Datenbank**: MongoDB 8
- **Frontend**: Vanilla JavaScript, HTML, CSS
- **Container**: Docker + Docker Compose
## Projektstruktur
```
recipe-app/
├── server/ # Backend
│ ├── index.js # Express Server
│ ├── db.js # MongoDB Connection
│ ├── routes/ # API Routes
│ └── middleware/ # Upload Middleware
├── public/ # Frontend
│ ├── *.html
│ ├── css/
│ └── js/
├── uploads/ # Bilder (Volume)
└── docker-compose.yml
```
## Installation & Start
### Mit Docker (empfohlen)
```bash
# Container bauen und starten
docker-compose up -d
# Logs anzeigen
docker-compose logs -f app
# Stoppen
docker-compose down
```
App läuft auf: **http://localhost:3000**
### Lokal (ohne Docker)
```bash
# MongoDB lokal installieren und starten
# Dependencies installieren
cd server
npm install
# Server starten
npm start
# Entwicklungsmodus (mit Auto-Reload)
npm run dev
```
## Daten migrieren
Wenn du deine bestehenden MySQL-Daten migrieren möchtest, erstelle ein Migrations-Script oder importiere die Daten manuell in MongoDB.
## MongoDB Schema
### Collection: `recipes`
```javascript
{
_id: ObjectId,
rezeptnummer: Number (unique),
bezeichnung: String,
beschreibung: String,
kategorie: String,
vorbereitung: String,
anzahl: Number,
zutaten: String,
zubereitung: [
{ schritt: Number, text: String }
],
kommentar: String,
erstelltAm: Date,
aktualisiertAm: Date
}
```
### Collection: `images`
```javascript
{
_id: ObjectId,
rezeptId: ObjectId (ref recipes),
dateiPfad: String,
dateiName: String,
hochgeladenAm: Date
}
```
## API Endpoints
### Rezepte
- `GET /api/recipes` - Alle Rezepte (mit Suche & Sortierung)
- `GET /api/recipes/:id` - Einzelnes Rezept
- `POST /api/recipes` - Neues Rezept erstellen
- `PUT /api/recipes/:id` - Rezept aktualisieren
- `DELETE /api/recipes/:id` - Rezept löschen
### Bilder
- `POST /api/images/:rezeptId` - Bild hochladen
- `GET /api/images/recipe/:rezeptId` - Bilder eines Rezepts
- `DELETE /api/images/:bildId` - Bild löschen
## Features
✅ Rezepte anzeigen, erstellen, bearbeiten, löschen
✅ Volltextsuche
✅ Sortierung nach Nummer, Name, Kategorie
✅ Bilder hochladen (JPG, PNG)
✅ Lightbox für Bildvergrößerung
✅ Responsive Design
✅ Docker-Ready
## Umgebungsvariablen
Erstelle eine `.env` Datei:
```
PORT=3000
NODE_ENV=production
MONGODB_URI=mongodb://mongo:27017/recipes
```
## Entwicklung
VSCode Extensions empfohlen:
- ESLint
- Prettier
- MongoDB for VS Code
## Unterschiede zur PHP-Version
- **Datenbank**: MySQL → MongoDB (denormalisiert)
- **Backend**: PHP → Node.js/Express
- **Frontend**: Gleiche UI, aber mit Fetch API statt Forms
- **Upload**: Multer statt move_uploaded_file
- **Sessions**: Nicht mehr benötigt (Stateless API)
## Lizenz
Privat

37
docker-compose.yml Normal file
View File

@@ -0,0 +1,37 @@
services:
mongo:
image: mongo:8
container_name: recipe_mongo
restart: unless-stopped
environment:
MONGO_INITDB_DATABASE: recipes
volumes:
- mongo_data:/data/db
ports:
- "27017:27017"
networks:
- recipe_network
app:
build: .
container_name: recipe_app
restart: unless-stopped
environment:
- NODE_ENV=production
- PORT=3000
- MONGODB_URI=mongodb://mongo:27017/recipes
ports:
- "3000:3000"
volumes:
- ./uploads:/app/uploads
depends_on:
- mongo
networks:
- recipe_network
volumes:
mongo_data:
networks:
recipe_network:
driver: bridge

61
init-git.sh Normal file
View File

@@ -0,0 +1,61 @@
#!/bin/bash
echo "🚀 Recipe App - Git Repository Setup"
echo "======================================"
# Prüfe ob Git installiert ist
if ! command -v git &> /dev/null; then
echo "❌ Git ist nicht installiert"
exit 1
fi
# Prüfe ob bereits ein Git-Repo existiert
if [ -d .git ]; then
echo "⚠️ Git Repository existiert bereits"
read -p "Möchtest du es neu initialisieren? (j/n): " answer
if [ "$answer" != "j" ]; then
echo "Abgebrochen"
exit 0
fi
rm -rf .git
fi
# Git Repository initialisieren
echo ""
echo "📦 Initialisiere Git Repository..."
git init
git branch -M main
# .env.example aus .env erstellen falls nicht vorhanden
if [ ! -f .env.example ] && [ -f .env ]; then
echo "📄 Erstelle .env.example..."
cp .env .env.example
fi
# Erste Dateien hinzufügen
echo " Füge Dateien hinzu..."
git add .
# Ersten Commit erstellen
echo "💾 Erstelle ersten Commit..."
git commit -m "Initial commit: Recipe App - PHP zu Node.js Migration
- Node.js 22 + Express.js Backend
- MongoDB 8 Datenbank
- Vanilla JavaScript Frontend
- Docker + Docker Compose Setup
- Komplette CRUD-Funktionalität
- Bild-Upload mit Multer
- Volltextsuche"
echo ""
echo "✅ Git Repository erfolgreich initialisiert!"
echo ""
echo "Nächste Schritte:"
echo "1. Remote Repository erstellen (GitHub/GitLab/etc.)"
echo "2. Remote hinzufügen:"
echo " git remote add origin <DEINE_REPOSITORY_URL>"
echo "3. Push zum Remote:"
echo " git push -u origin main"
echo ""
echo "📚 Mehr Infos: siehe GIT_SETUP.md"

118
migrate.js Normal file
View File

@@ -0,0 +1,118 @@
import mysql from 'mysql2/promise';
import { MongoClient } from 'mongodb';
// Konfiguration - ANPASSEN!
const mysqlConfig = {
host: 'DEIN_MYSQL_HOST',
user: 'DEIN_MYSQL_USER',
password: 'DEIN_MYSQL_PASSWORD',
database: 'DEINE_MYSQL_DB'
};
const mongoUri = 'mongodb://localhost:27017/recipes';
async function migrate() {
let mysqlConn, mongoClient, db;
try {
console.log('📦 Verbinde mit MySQL...');
mysqlConn = await mysql.createConnection(mysqlConfig);
console.log('📦 Verbinde mit MongoDB...');
mongoClient = new MongoClient(mongoUri);
await mongoClient.connect();
db = mongoClient.db();
console.log('🗑️ Lösche bestehende MongoDB-Daten...');
await db.collection('recipes').deleteMany({});
await db.collection('images').deleteMany({});
console.log('📋 Migriere Rezepte...');
const [rezepte] = await mysqlConn.query('SELECT * FROM Rezepte ORDER BY Rezeptnummer');
let recipeCount = 0;
let imageCount = 0;
for (const rezept of rezepte) {
const rezeptnr = 'R' + String(rezept.Rezeptnummer).padStart(3, '0');
// Hole Zutaten aus ingredients
const [ingredients] = await mysqlConn.query(
'SELECT ingr FROM ingredients WHERE rezeptnr = ?',
[rezeptnr]
);
const zutaten = ingredients.length > 0 ? ingredients[0].ingr : rezept.Zutaten || '';
// Hole Zubereitungsschritte
const [zubereitungRows] = await mysqlConn.query(
'SELECT schritt, text FROM Zubereitung WHERE rezeptnummer = ? ORDER BY schritt',
[rezeptnr]
);
const zubereitung = zubereitungRows.map(row => ({
schritt: row.schritt,
text: row.text
}));
// Erstelle MongoDB-Dokument
const recipeDoc = {
rezeptnummer: parseInt(rezept.Rezeptnummer),
bezeichnung: rezept.Bezeichnung || '',
beschreibung: rezept.Beschreibung || '',
kategorie: rezept.Kategorie || '',
vorbereitung: rezept.Vorbereitung || '',
anzahl: rezept.Anzahl || 2,
zutaten,
zubereitung,
kommentar: rezept.Kommentar || '',
erstelltAm: new Date(),
aktualisiertAm: new Date()
};
const result = await db.collection('recipes').insertOne(recipeDoc);
recipeCount++;
console.log(` ✓ Rezept ${rezeptnr}: ${rezept.Bezeichnung}`);
// Bilder migrieren
const [bilder] = await mysqlConn.query(
'SELECT * FROM rezepte_bilder WHERE rezepte_id = ?',
[rezept.id]
);
for (const bild of bilder) {
const imageDoc = {
rezeptId: result.insertedId,
dateiPfad: bild.datei_pfad,
dateiName: bild.datei_pfad.split('/').pop(),
hochgeladenAm: new Date()
};
await db.collection('images').insertOne(imageDoc);
imageCount++;
}
}
// Erstelle Indizes
console.log('📇 Erstelle Indizes...');
await db.collection('recipes').createIndex({ rezeptnummer: 1 }, { unique: true });
await db.collection('recipes').createIndex({
bezeichnung: 'text',
beschreibung: 'text',
kategorie: 'text'
});
console.log('\n✅ Migration abgeschlossen!');
console.log(` ${recipeCount} Rezepte migriert`);
console.log(` ${imageCount} Bilder migriert`);
} catch (error) {
console.error('❌ Fehler bei der Migration:', error);
process.exit(1);
} finally {
if (mysqlConn) await mysqlConn.end();
if (mongoClient) await mongoClient.close();
}
}
// Script ausführen
migrate();

12
migration/package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "recipe-migration",
"version": "1.0.0",
"type": "module",
"scripts": {
"migrate": "node migrate.js"
},
"dependencies": {
"mongodb": "^6.8.0",
"mysql2": "^3.11.0"
}
}

331
public/css/style.css Normal file
View File

@@ -0,0 +1,331 @@
body {
font-family: Arial, Helvetica, sans-serif;
background: #fcf6e3;
color: #5b4636;
font-size: 22px;
margin: 0;
padding: 0;
}
.container {
width: 90%;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Buttons */
.custom-button {
display: inline-block;
padding: 10px 20px;
background-color: #bb7e19;
color: white;
text-decoration: none;
border-radius: 5px;
font-size: 1em;
margin-right: 10px;
margin-bottom: 10px;
border: none;
cursor: pointer;
text-align: center;
}
.custom-button:hover {
background-color: #a06f15;
}
.delete-btn {
background-color: #d65c5c;
}
.delete-btn:hover {
background-color: #b83d3d;
}
/* Search Bar */
.search-container {
text-align: center;
margin-bottom: 20px;
}
.search-container form {
display: inline-flex;
align-items: center;
}
.search-container input[type="text"] {
padding: 8px;
font-size: 18px;
width: 15ch;
border: 1px solid #ccc;
border-radius: 4px;
margin-right: 10px;
}
.search-container button {
padding: 8px 16px;
background: #bb7e19;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 18px;
}
.search-container button:hover {
background: #a06d15;
}
/* Table */
.table-aufgaben {
width: 900px;
margin: 0 auto;
border-collapse: collapse;
table-layout: fixed;
background: #fff;
}
.table-aufgaben th,
.table-aufgaben td {
padding: 8px;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
text-align: center;
}
.table-aufgaben th:first-child,
.table-aufgaben td:first-child {
width: 10%;
text-align: center;
}
.table-aufgaben th a {
color: #8b5a2b;
text-decoration: none;
cursor: pointer;
}
.table-aufgaben th a:hover {
text-decoration: underline;
}
.table-aufgaben td.bezeichnung {
text-align: left;
}
.table-aufgaben td.bezeichnung a {
color: #c17e1b;
text-decoration: none;
}
.table-aufgaben td.bezeichnung a:hover {
text-decoration: underline;
}
.table-aufgaben th.kategorie,
.table-aufgaben td.kategorie {
width: 30%;
text-align: left;
}
.table-aufgaben tr {
background: #fbeee6;
}
/* Form */
.form-container {
width: 80%;
margin: 0 auto;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.form-container table {
width: 100%;
border-collapse: collapse;
}
.form-container th,
.form-container td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
color: #8b5a2b;
}
.form-container input[type="text"],
.form-container input[type="number"],
.form-container textarea,
.form-container select {
width: 100%;
padding: 8px;
font-size: 18px;
border: 1px solid #ccc;
border-radius: 4px;
color: #5b4636;
}
.form-container textarea {
height: 100px;
resize: vertical;
}
/* Images */
.bilder-container {
margin-top: 15px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.bild-vorschau {
position: relative;
width: 80px;
height: 80px;
}
.bild-vorschau img {
width: 100%;
height: 100%;
object-fit: cover;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.bild-loeschen {
position: absolute;
top: 5px;
right: 5px;
background: rgba(255, 255, 255, 0.8);
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
cursor: pointer;
font-size: 14px;
color: #d65c5c;
line-height: 20px;
text-align: center;
}
/* Lightbox */
#lightbox {
display: none;
position: fixed;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
text-align: center;
padding-top: 50px;
}
#lightbox img {
max-width: 80%;
max-height: 80%;
margin: auto;
display: block;
}
#lightbox .close {
position: absolute;
top: 20px;
right: 30px;
color: #fff;
font-size: 35px;
font-weight: bold;
cursor: pointer;
}
/* Recipe View */
.recipe-container {
width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
}
.recipe-container img {
max-width: 100%;
}
.recipe-container img.oben {
width: 500px;
}
.recipe-container img.vergroesserbar {
width: 200px;
cursor: pointer;
}
.recipe-container img.vergroessert {
width: 400px !important;
transition: width 0.3s ease;
}
.recipe-container pre.zutaten {
font-size: 1.2em;
font-family: Arial, sans-serif;
white-space: pre-wrap;
}
.recipe-container .unter {
font-size: 1.4em;
}
.recipe-container .title {
font-size: 1.6em;
margin-bottom: 10px;
}
.neu-container {
width: 100%;
display: flex;
justify-content: center;
margin-top: 22px;
}
.neu {
background: #bb7e19;
color: #fff;
padding: 10px 22px;
border: none;
border-radius: 4px;
text-decoration: none;
font-size: 1.5em;
box-shadow: 1px 1px 4px #efd7b1;
}
.file-upload-container {
position: relative;
overflow: hidden;
display: inline-block;
margin-top: 10px;
}
.file-upload-container input[type="file"] {
position: absolute;
left: 0;
top: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.file-upload-btn {
background: #bb7e19;
color: white;
padding: 8px 12px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 16px;
}

87
public/edit.html Normal file
View File

@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rezept bearbeiten</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="form-container">
<a href="/index.html" class="custom-button">Zurück zur Übersicht</a>
<button id="viewRecipeBtn" class="custom-button" style="display:none;">Rezept anzeigen</button>
<h1 id="pageTitle">Neues Rezept</h1>
<form id="recipeForm">
<input type="hidden" id="recipeId" name="id">
<table>
<tr>
<th>Rezeptnummer:</th>
<td><input type="number" id="rezeptnummer" name="rezeptnummer" required></td>
</tr>
<tr>
<th>Bezeichnung:</th>
<td><input type="text" id="bezeichnung" name="bezeichnung"></td>
</tr>
<tr>
<th>Beschreibung:</th>
<td><textarea id="beschreibung" name="beschreibung"></textarea></td>
</tr>
<tr>
<th>Kategorie:</th>
<td><input type="text" id="kategorie" name="kategorie"></td>
</tr>
<tr>
<th>Vorbereitung:</th>
<td><textarea id="vorbereitung" name="vorbereitung"></textarea></td>
</tr>
<tr>
<th>Anzahl der Personen:</th>
<td><input type="number" id="anzahl" name="anzahl" value="2" min="0" required></td>
</tr>
<tr>
<th>Zutaten:</th>
<td><textarea id="zutaten" name="zutaten"></textarea></td>
</tr>
<tr>
<th>Zubereitung:</th>
<td><textarea id="zubereitung" name="zubereitung"></textarea></td>
</tr>
<tr>
<th>Kommentar:</th>
<td><textarea id="kommentar" name="kommentar"></textarea></td>
</tr>
<tr>
<th>Bilder hochladen:</th>
<td>
<div class="file-upload-row">
<div class="file-upload-container">
<button type="button" class="file-upload-btn" onclick="document.getElementById('fileInput').click()">
Bild auswählen
</button>
<input type="file" id="fileInput" accept=".jpg,.jpeg,.png" style="display:none;">
</div>
<button type="button" id="uploadImageBtn" class="save-single-image-btn">Bild speichern</button>
</div>
<div id="bildvorschau" class="bilder-container"></div>
</td>
</tr>
</table>
<div class="button-container">
<button type="submit" class="submit-btn">Speichern</button>
<button type="button" id="deleteBtn" class="delete-btn" style="display:none;">Rezept löschen</button>
</div>
</form>
</div>
<div id="lightbox" onclick="closeLightbox()">
<span class="close">&times;</span>
<img id="lightbox-img" src="" alt="Vergrößerte Ansicht">
</div>
<script src="/js/edit.js"></script>
</body>
</html>

46
public/index.html Normal file
View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rezepte Übersicht</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="top-bar">
<div class="search-container">
<form id="searchForm">
<input type="text" id="searchInput" placeholder="Suche..." />
<button type="submit" class="search-btn">Suche</button>
</form>
</div>
</div>
<div class="tabelle-container">
<table class="table-aufgaben">
<thead>
<tr>
<th class="rezeptnr">
<a href="#" data-sort="rezeptnummer">Rezeptnr.</a>
</th>
<th class="bezeichnung">
<a href="#" data-sort="bezeichnung">Bezeichnung</a>
</th>
<th class="kategorie">
<a href="#" data-sort="kategorie">Kategorie</a>
</th>
</tr>
</thead>
<tbody id="recipeTable">
<tr><td colspan="3">Lade Rezepte...</td></tr>
</tbody>
</table>
<div class="neu-container">
<a href="/edit.html" class="neu">Neues Rezept</a>
</div>
</div>
<script src="/js/index.js"></script>
</body>
</html>

191
public/js/edit.js Normal file
View File

@@ -0,0 +1,191 @@
const urlParams = new URLSearchParams(window.location.search);
const recipeId = urlParams.get('id');
let currentRecipe = null;
if (recipeId) {
document.getElementById('pageTitle').textContent = 'Rezept bearbeiten';
document.getElementById('deleteBtn').style.display = 'inline-block';
document.getElementById('viewRecipeBtn').style.display = 'inline-block';
loadRecipe();
}
async function loadRecipe() {
try {
const response = await fetch(`/api/recipes/${recipeId}`);
if (!response.ok) throw new Error('Rezept nicht gefunden');
currentRecipe = await response.json();
populateForm(currentRecipe);
loadImages();
} catch (error) {
console.error('Fehler:', error);
alert('Fehler beim Laden des Rezepts');
}
}
function populateForm(recipe) {
document.getElementById('recipeId').value = recipe._id;
document.getElementById('rezeptnummer').value = recipe.rezeptnummer;
document.getElementById('bezeichnung').value = recipe.bezeichnung || '';
document.getElementById('beschreibung').value = recipe.beschreibung || '';
document.getElementById('kategorie').value = recipe.kategorie || '';
document.getElementById('vorbereitung').value = recipe.vorbereitung || '';
document.getElementById('anzahl').value = recipe.anzahl || 2;
document.getElementById('zutaten').value = recipe.zutaten || '';
const zubereitungText = recipe.zubereitung
? recipe.zubereitung.map(z => z.text).join('\n')
: '';
document.getElementById('zubereitung').value = zubereitungText;
document.getElementById('kommentar').value = recipe.kommentar || '';
}
async function loadImages() {
if (!recipeId) return;
try {
const response = await fetch(`/api/images/recipe/${recipeId}`);
if (!response.ok) return;
const images = await response.json();
displayImages(images);
} catch (error) {
console.error('Fehler beim Laden der Bilder:', error);
}
}
function displayImages(images) {
const container = document.getElementById('bildvorschau');
container.innerHTML = images.map(img => `
<div class="bild-vorschau">
<img src="/${img.dateiPfad}" alt="Vorschau" onclick="openLightbox('/${img.dateiPfad}')">
<button class="bild-loeschen" onclick="deleteImage('${img._id}')">×</button>
</div>
`).join('');
}
document.getElementById('recipeForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = {
rezeptnummer: document.getElementById('rezeptnummer').value,
bezeichnung: document.getElementById('bezeichnung').value,
beschreibung: document.getElementById('beschreibung').value,
kategorie: document.getElementById('kategorie').value,
vorbereitung: document.getElementById('vorbereitung').value,
anzahl: document.getElementById('anzahl').value,
zutaten: document.getElementById('zutaten').value,
zubereitung: document.getElementById('zubereitung').value,
kommentar: document.getElementById('kommentar').value
};
try {
let response;
if (recipeId) {
response = await fetch(`/api/recipes/${recipeId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
} else {
response = await fetch('/api/recipes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
}
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Fehler beim Speichern');
}
alert('Rezept erfolgreich gespeichert');
window.location.href = '/index.html';
} catch (error) {
console.error('Fehler:', error);
alert('Fehler beim Speichern: ' + error.message);
}
});
document.getElementById('uploadImageBtn').addEventListener('click', async () => {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('Bitte wähle zuerst ein Bild aus');
return;
}
if (!recipeId) {
alert('Bitte speichere zuerst das Rezept, bevor du Bilder hochlädst');
return;
}
const formData = new FormData();
formData.append('bild', file);
formData.append('rezeptnummer', document.getElementById('rezeptnummer').value);
try {
const response = await fetch(`/api/images/${recipeId}`, {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('Upload fehlgeschlagen');
fileInput.value = '';
loadImages();
} catch (error) {
console.error('Fehler:', error);
alert('Fehler beim Hochladen des Bildes');
}
});
async function deleteImage(imageId) {
if (!confirm('Möchtest du dieses Bild wirklich löschen?')) return;
try {
const response = await fetch(`/api/images/${imageId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Löschen fehlgeschlagen');
loadImages();
} catch (error) {
console.error('Fehler:', error);
alert('Fehler beim Löschen des Bildes');
}
}
document.getElementById('deleteBtn').addEventListener('click', async () => {
if (!confirm('Möchtest du dieses Rezept wirklich löschen?')) return;
try {
const response = await fetch(`/api/recipes/${recipeId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Löschen fehlgeschlagen');
alert('Rezept gelöscht');
window.location.href = '/index.html';
} catch (error) {
console.error('Fehler:', error);
alert('Fehler beim Löschen des Rezepts');
}
});
document.getElementById('viewRecipeBtn').addEventListener('click', () => {
window.location.href = `/recipe.html?id=${recipeId}`;
});
function openLightbox(src) {
document.getElementById('lightbox').style.display = 'block';
document.getElementById('lightbox-img').src = src;
}
function closeLightbox() {
document.getElementById('lightbox').style.display = 'none';
}

108
public/js/index.js Normal file
View File

@@ -0,0 +1,108 @@
const urlParams = new URLSearchParams(window.location.search);
const recipeId = urlParams.get('id');
if (!recipeId) {
window.location.href = '/index.html';
}
async function loadRecipe() {
try {
const response = await fetch(`/api/recipes/${recipeId}`);
if (!response.ok) throw new Error('Rezept nicht gefunden');
const recipe = await response.json();
displayRecipe(recipe);
} catch (error) {
console.error('Fehler:', error);
alert('Fehler beim Laden des Rezepts');
window.location.href = '/index.html';
}
}
function displayRecipe(recipe) {
const rNummer = 'R' + String(recipe.rezeptnummer).padStart(3, '0');
document.getElementById('rezeptnummer').textContent = rNummer;
document.getElementById('bezeichnung').textContent = recipe.bezeichnung;
document.getElementById('beschreibung').innerHTML = makeClickableLinks(recipe.beschreibung || '');
if (recipe.bilder && recipe.bilder.length > 0) {
document.getElementById('hauptbild').innerHTML =
`<img class="oben" src="/${recipe.bilder[0].dateiPfad}" alt="Rezeptbild">`;
}
if (recipe.vorbereitung && recipe.vorbereitung.trim()) {
document.getElementById('vorbereitung').innerHTML =
`<br>Vorbereitung: <span style="font-size:0.9em;">${makeClickableLinks(recipe.vorbereitung)}</span>`;
}
if (recipe.zutaten && recipe.zutaten.trim()) {
let header = '<br>Zutaten';
if (recipe.anzahl) {
header += ` <span style="font-size: 1.2em;">für ${recipe.anzahl} Personen:</span>`;
}
document.getElementById('zutatenHeader').innerHTML = header;
document.getElementById('zutaten').textContent = recipe.zutaten;
} else {
document.getElementById('zutatenContainer').style.display = 'none';
}
if (recipe.zubereitung && recipe.zubereitung.length > 0) {
const maxSteps = Math.max(recipe.zubereitung.length, recipe.bilder ? recipe.bilder.length - 1 : 0);
let html = '';
for (let i = 0; i < maxSteps; i++) {
const step = recipe.zubereitung[i];
const bild = recipe.bilder && recipe.bilder[i + 1] ? recipe.bilder[i + 1] : null;
html += '<tr>';
if (bild) {
html += `<td><img class="vergroesserbar" src="/${bild.dateiPfad}" alt="Schritt ${i+1}" onclick="openLightbox('/${bild.dateiPfad}')"></td>`;
} else {
html += '<td></td>';
}
html += `<td style="text-align:justify;padding:20px;font-size:1.2em;">${step ? escapeHtml(step.text) : '&nbsp;'}</td>`;
html += '</tr>';
}
document.getElementById('zubereitungSteps').innerHTML = html;
} else {
document.getElementById('zubereitungContainer').style.display = 'none';
}
if (recipe.kommentar && recipe.kommentar.trim()) {
document.getElementById('kommentarContainer').innerHTML = `
<table>
<tr><td><br><br><span style="font-size:1.4em">Kommentar</span><br><br>
<span style="font-size: 1.2em; display: block;">${makeClickableLinks(recipe.kommentar)}</span></td></tr>
</table>
`;
}
document.getElementById('editBtn').onclick = () => {
window.location.href = `/edit.html?id=${recipeId}`;
};
}
function makeClickableLinks(text) {
const urlPattern = /(?:(?:https?:\/\/)|(?:www\.))[^\s<>"'()]+/gi;
return text.replace(urlPattern, url => `<a href="${url}" target="_blank">${url}</a>`);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function openLightbox(src) {
document.getElementById('lightbox').style.display = 'block';
document.getElementById('lightbox-img').src = src;
}
function closeLightbox() {
document.getElementById('lightbox').style.display = 'none';
}
loadRecipe();

108
public/js/recipe.js Normal file
View File

@@ -0,0 +1,108 @@
const urlParams = new URLSearchParams(window.location.search);
const recipeId = urlParams.get('id');
if (!recipeId) {
window.location.href = '/index.html';
}
async function loadRecipe() {
try {
const response = await fetch(`/api/recipes/${recipeId}`);
if (!response.ok) throw new Error('Rezept nicht gefunden');
const recipe = await response.json();
displayRecipe(recipe);
} catch (error) {
console.error('Fehler:', error);
alert('Fehler beim Laden des Rezepts');
window.location.href = '/index.html';
}
}
function displayRecipe(recipe) {
const rNummer = 'R' + String(recipe.rezeptnummer).padStart(3, '0');
document.getElementById('rezeptnummer').textContent = rNummer;
document.getElementById('bezeichnung').textContent = recipe.bezeichnung;
document.getElementById('beschreibung').innerHTML = makeClickableLinks(recipe.beschreibung || '');
if (recipe.bilder && recipe.bilder.length > 0) {
document.getElementById('hauptbild').innerHTML =
`<img class="oben" src="/${recipe.bilder[0].dateiPfad}" alt="Rezeptbild">`;
}
if (recipe.vorbereitung && recipe.vorbereitung.trim()) {
document.getElementById('vorbereitung').innerHTML =
`<br>Vorbereitung: <span style="font-size:0.9em;">${makeClickableLinks(recipe.vorbereitung)}</span>`;
}
if (recipe.zutaten && recipe.zutaten.trim()) {
let header = '<br>Zutaten';
if (recipe.anzahl) {
header += ` <span style="font-size: 1.2em;">für ${recipe.anzahl} Personen:</span>`;
}
document.getElementById('zutatenHeader').innerHTML = header;
document.getElementById('zutaten').textContent = recipe.zutaten;
} else {
document.getElementById('zutatenContainer').style.display = 'none';
}
if (recipe.zubereitung && recipe.zubereitung.length > 0) {
const maxSteps = Math.max(recipe.zubereitung.length, recipe.bilder ? recipe.bilder.length - 1 : 0);
let html = '';
for (let i = 0; i < maxSteps; i++) {
const step = recipe.zubereitung[i];
const bild = recipe.bilder && recipe.bilder[i + 1] ? recipe.bilder[i + 1] : null;
html += '<tr>';
if (bild) {
html += `<td><img class="vergroesserbar" src="/${bild.dateiPfad}" alt="Schritt ${i+1}" onclick="openLightbox('/${bild.dateiPfad}')"></td>`;
} else {
html += '<td></td>';
}
html += `<td style="text-align:justify;padding:20px;font-size:1.2em;">${step ? escapeHtml(step.text) : '&nbsp;'}</td>`;
html += '</tr>';
}
document.getElementById('zubereitungSteps').innerHTML = html;
} else {
document.getElementById('zubereitungContainer').style.display = 'none';
}
if (recipe.kommentar && recipe.kommentar.trim()) {
document.getElementById('kommentarContainer').innerHTML = `
<table>
<tr><td><br><br><span style="font-size:1.4em">Kommentar</span><br><br>
<span style="font-size: 1.2em; display: block;">${makeClickableLinks(recipe.kommentar)}</span></td></tr>
</table>
`;
}
document.getElementById('editBtn').onclick = () => {
window.location.href = `/edit.html?id=${recipeId}`;
};
}
function makeClickableLinks(text) {
const urlPattern = /(?:(?:https?:\/\/)|(?:www\.))[^\s<>"'()]+/gi;
return text.replace(urlPattern, url => `<a href="${url}" target="_blank">${url}</a>`);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function openLightbox(src) {
document.getElementById('lightbox').style.display = 'block';
document.getElementById('lightbox-img').src = src;
}
function closeLightbox() {
document.getElementById('lightbox').style.display = 'none';
}
loadRecipe();

46
public/recipe.html Normal file
View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rezept</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body class="sans">
<table width="800px" align="center" border="0">
<tr><td>
<a href="/index.html" class="custom-button">Zurück zur Übersicht</a>
<button id="editBtn" class="custom-button">Rezept editieren</button>
</td></tr>
<tr><td>
<table align="left" border="0">
<tr><td id="rezeptnummer" style="font-size:1.6em"></td></tr>
<tr><td id="bezeichnung" style="font-size:1.6em"></td></tr>
<tr><td id="beschreibung" class="unter"></td></tr>
<tr><td id="hauptbild" align="left"></td></tr>
<tr><td id="vorbereitung" class="unter"></td></tr>
<tr><td id="zutatenHeader" class="unter"></td></tr>
</table>
</td></tr>
<tr><td id="zutatenContainer">
<table align="center" border="0">
<tr><td><pre id="zutaten" class="zutaten"></pre></td></tr>
</table>
</td></tr>
<tr><td id="zubereitungContainer">
<table align="left" border="0">
<tr><td class="unter" align="left" colspan="2"><br>Zubereitung<br><br></td></tr>
<tbody id="zubereitungSteps"></tbody>
</table>
</td></tr>
<tr><td id="kommentarContainer"></td></tr>
</table>
<div id="lightbox" onclick="closeLightbox()">
<span class="close" onclick="closeLightbox()">&times;</span>
<img id="lightbox-img" src="" alt="Vergrößerte Ansicht">
</div>
<script src="/js/recipe.js"></script>
</body>
</html>

43
server/db.js Normal file
View File

@@ -0,0 +1,43 @@
import multer from 'multer';
import path from 'path';
import fs from 'fs';
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const rezeptnummer = req.body.rezeptnummer || req.params.rezeptnummer;
const rNummer = 'R' + String(rezeptnummer).padStart(3, '0');
const uploadPath = path.join('uploads', rNummer);
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath, { recursive: true });
}
cb(null, uploadPath);
},
filename: (req, file, cb) => {
const rezeptnummer = req.body.rezeptnummer || req.params.rezeptnummer;
const rNummer = 'R' + String(rezeptnummer).padStart(3, '0');
const uploadPath = path.join('uploads', rNummer);
const existingFiles = fs.readdirSync(uploadPath).filter(f => f.startsWith(rNummer));
const nextIndex = existingFiles.length;
const filename = `${rNummer}_${nextIndex}.jpg`;
cb(null, filename);
}
});
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Nur JPG, JPEG und PNG Dateien erlaubt'), false);
}
};
export const upload = multer({
storage,
fileFilter,
limits: { fileSize: 10 * 1024 * 1024 } // 10MB
});

58
server/index.js Normal file
View File

@@ -0,0 +1,58 @@
import express from 'express';
import cors from 'cors';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
import { connectDB, closeDB } from './db.js';
import recipesRoutes from './routes/recipes.js';
import imagesRoutes from './routes/images.js';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Statische Dateien
app.use(express.static(path.join(__dirname, '../public')));
app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
// API Routes
app.use('/api/recipes', recipesRoutes);
app.use('/api/images', imagesRoutes);
// Catch-all Route für SPA
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../public/index.html'));
});
// Server starten
async function startServer() {
try {
await connectDB();
app.listen(PORT, () => {
console.log(`Server läuft auf Port ${PORT}`);
console.log(`http://localhost:${PORT}`);
});
} catch (error) {
console.error('Fehler beim Starten des Servers:', error);
process.exit(1);
}
}
// Graceful Shutdown
process.on('SIGINT', async () => {
console.log('\nServer wird heruntergefahren...');
await closeDB();
process.exit(0);
});
startServer();

View File

@@ -0,0 +1,43 @@
import multer from 'multer';
import path from 'path';
import fs from 'fs';
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const rezeptnummer = req.body.rezeptnummer || req.params.rezeptnummer;
const rNummer = 'R' + String(rezeptnummer).padStart(3, '0');
const uploadPath = path.join('uploads', rNummer);
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath, { recursive: true });
}
cb(null, uploadPath);
},
filename: (req, file, cb) => {
const rezeptnummer = req.body.rezeptnummer || req.params.rezeptnummer;
const rNummer = 'R' + String(rezeptnummer).padStart(3, '0');
const uploadPath = path.join('uploads', rNummer);
const existingFiles = fs.readdirSync(uploadPath).filter(f => f.startsWith(rNummer));
const nextIndex = existingFiles.length;
const filename = `${rNummer}_${nextIndex}.jpg`;
cb(null, filename);
}
});
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Nur JPG, JPEG und PNG Dateien erlaubt'), false);
}
};
export const upload = multer({
storage,
fileFilter,
limits: { fileSize: 10 * 1024 * 1024 } // 10MB
});

15
server/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "recipe-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js"
},
"dependencies": {
"express": "^4.19.2",
"mongodb": "^6.8.0",
"multer": "^1.4.5-lts.1",
"dotenv": "^16.4.5"
}
}

72
server/routes/images.js Normal file
View File

@@ -0,0 +1,72 @@
import express from 'express';
import { ObjectId } from 'mongodb';
import { getDB } from '../db.js';
import { upload } from '../middleware/upload.js';
import fs from 'fs';
import path from 'path';
const router = express.Router();
// Bild hochladen
router.post('/:rezeptId', upload.single('bild'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'Keine Datei hochgeladen' });
}
const db = getDB();
const rezeptId = new ObjectId(req.params.rezeptId);
const imageDoc = {
rezeptId,
dateiPfad: req.file.path.replace(/\\/g, '/'),
dateiName: req.file.filename,
hochgeladenAm: new Date()
};
const result = await db.collection('images').insertOne(imageDoc);
res.status(201).json({ _id: result.insertedId, ...imageDoc });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Bild löschen
router.delete('/:bildId', async (req, res) => {
try {
const db = getDB();
const bildId = new ObjectId(req.params.bildId);
const image = await db.collection('images').findOne({ _id: bildId });
if (!image) {
return res.status(404).json({ error: 'Bild nicht gefunden' });
}
// Datei löschen
if (fs.existsSync(image.dateiPfad)) {
fs.unlinkSync(image.dateiPfad);
}
await db.collection('images').deleteOne({ _id: bildId });
res.json({ message: 'Bild gelöscht' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Bilder eines Rezepts abrufen
router.get('/recipe/:rezeptId', async (req, res) => {
try {
const db = getDB();
const images = await db.collection('images')
.find({ rezeptId: new ObjectId(req.params.rezeptId) })
.sort({ _id: 1 })
.toArray();
res.json(images);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
export default router;

177
server/routes/recipes.js Normal file
View File

@@ -0,0 +1,177 @@
kimport express from 'express';
import { ObjectId } from 'mongodb';
import { getDB } from '../db.js';
const router = express.Router();
// Alle Rezepte abrufen (mit Suche und Sortierung)
router.get('/', async (req, res) => {
try {
const db = getDB();
const { search, sort = 'rezeptnummer' } = req.query;
let query = {};
if (search) {
query.$text = { $search: search };
}
const sortField = ['rezeptnummer', 'bezeichnung', 'kategorie'].includes(sort)
? sort
: 'rezeptnummer';
const recipes = await db.collection('recipes')
.find(query)
.sort({ [sortField]: 1 })
.project({ _id: 1, rezeptnummer: 1, bezeichnung: 1, kategorie: 1 })
.toArray();
res.json(recipes);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Einzelnes Rezept abrufen
router.get('/:id', async (req, res) => {
try {
const db = getDB();
const recipe = await db.collection('recipes').findOne({
_id: new ObjectId(req.params.id)
});
if (!recipe) {
return res.status(404).json({ error: 'Rezept nicht gefunden' });
}
// Bilder abrufen
const images = await db.collection('images')
.find({ rezeptId: new ObjectId(req.params.id) })
.sort({ _id: 1 })
.toArray();
res.json({ ...recipe, bilder: images });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Neues Rezept erstellen
router.post('/', async (req, res) => {
try {
const db = getDB();
const {
rezeptnummer,
bezeichnung,
beschreibung,
kategorie,
vorbereitung,
anzahl,
zutaten,
zubereitung,
kommentar
} = req.body;
// Zubereitungsschritte aufteilen
const zubereitungArray = zubereitung
.split('\n')
.map(step => step.trim())
.filter(step => step.length > 0)
.map((text, index) => ({ schritt: index + 1, text }));
const recipe = {
rezeptnummer: parseInt(rezeptnummer),
bezeichnung,
beschreibung,
kategorie,
vorbereitung,
anzahl: parseInt(anzahl) || 2,
zutaten,
zubereitung: zubereitungArray,
kommentar,
erstelltAm: new Date(),
aktualisiertAm: new Date()
};
const result = await db.collection('recipes').insertOne(recipe);
res.status(201).json({ _id: result.insertedId, ...recipe });
} catch (error) {
if (error.code === 11000) {
res.status(400).json({ error: 'Rezeptnummer existiert bereits' });
} else {
res.status(500).json({ error: error.message });
}
}
});
// Rezept aktualisieren
router.put('/:id', async (req, res) => {
try {
const db = getDB();
const {
rezeptnummer,
bezeichnung,
beschreibung,
kategorie,
vorbereitung,
anzahl,
zutaten,
zubereitung,
kommentar
} = req.body;
const zubereitungArray = zubereitung
.split('\n')
.map(step => step.trim())
.filter(step => step.length > 0)
.map((text, index) => ({ schritt: index + 1, text }));
const updateData = {
rezeptnummer: parseInt(rezeptnummer),
bezeichnung,
beschreibung,
kategorie,
vorbereitung,
anzahl: parseInt(anzahl) || 2,
zutaten,
zubereitung: zubereitungArray,
kommentar,
aktualisiertAm: new Date()
};
const result = await db.collection('recipes').updateOne(
{ _id: new ObjectId(req.params.id) },
{ $set: updateData }
);
if (result.matchedCount === 0) {
return res.status(404).json({ error: 'Rezept nicht gefunden' });
}
res.json({ message: 'Rezept aktualisiert' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Rezept löschen
router.delete('/:id', async (req, res) => {
try {
const db = getDB();
const recipeId = new ObjectId(req.params.id);
// Lösche zugehörige Bilder
await db.collection('images').deleteMany({ rezeptId: recipeId });
const result = await db.collection('recipes').deleteOne({ _id: recipeId });
if (result.deletedCount === 0) {
return res.status(404).json({ error: 'Rezept nicht gefunden' });
}
res.json({ message: 'Rezept gelöscht' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
export default router;