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

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;