Erster commit wie von Claude erstellt (unverändert)
This commit is contained in:
43
server/db.js
Normal file
43
server/db.js
Normal 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
58
server/index.js
Normal 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();
|
||||
43
server/middleware/upload.js
Normal file
43
server/middleware/upload.js
Normal 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
15
server/package.json
Normal 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
72
server/routes/images.js
Normal 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
177
server/routes/recipes.js
Normal 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;
|
||||
Reference in New Issue
Block a user