Project-Image-Uploader/backend/src/routes/groups.js
matthias.lotz 07b436cc4d feat: Complete image description feature implementation
Features:
- Add image description field (max 200 chars) for individual images
- Replace 'Sort' button with 'Edit' button in image gallery cards
- Enable edit mode with text fields for each image in moderation
- Display descriptions in slideshow and public views
- Integrate description saving with main save button

Frontend changes:
- ImageGalleryCard: Add edit mode UI with textarea and character counter
- ModerationGroupImagesPage: Integrate description editing into main save flow
- Fix keyboard event propagation in textarea (spacebar issue)
- Remove separate 'Save Descriptions' button
- Add ESLint fixes for useCallback dependencies

Backend changes:
- Fix route order: batch-description route must come before :imageId route
- Ensure batch description update API works correctly

Build optimizations:
- Add .dockerignore to exclude development data (182MB reduction)
- Fix Dockerfile: Remove non-existent frontend/conf directory
- Reduce backend image size from 437MB to 247MB

Fixes:
- Fix route matching issue with batch-description endpoint
- Prevent keyboard events from triggering drag-and-drop
- Clean up unused functions and ESLint warnings
2025-11-07 23:20:50 +01:00

340 lines
11 KiB
JavaScript

const { Router } = require('express');
const { endpoints } = require('../constants');
const GroupRepository = require('../repositories/GroupRepository');
const MigrationService = require('../services/MigrationService');
const router = Router();
// Alle Gruppen abrufen (für Slideshow mit vollständigen Bilddaten)
router.get(endpoints.GET_ALL_GROUPS, async (req, res) => {
try {
// Auto-Migration beim ersten Zugriff
const migrationStatus = await MigrationService.getMigrationStatus();
if (migrationStatus.needsMigration) {
console.log('🔄 Starte automatische Migration...');
await MigrationService.migrateJsonToSqlite();
}
const groups = await GroupRepository.getAllGroupsWithImages();
res.json({
groups,
totalCount: groups.length
});
} catch (error) {
console.error('Error fetching all groups:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Fehler beim Laden der Gruppen',
details: error.message
});
}
});
// Alle Gruppen für Moderation abrufen (mit Freigabestatus) - MUSS VOR den :groupId routen stehen!
router.get('/moderation/groups', async (req, res) => {
try {
const groups = await GroupRepository.getAllGroupsWithModerationInfo();
res.json({
groups,
totalCount: groups.length,
pendingCount: groups.filter(g => !g.approved).length,
approvedCount: groups.filter(g => g.approved).length
});
} catch (error) {
console.error('Error fetching moderation groups:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Fehler beim Laden der Moderations-Gruppen',
details: error.message
});
}
});
// Einzelne Gruppe für Moderation abrufen (inkl. nicht-freigegebene)
router.get('/moderation/groups/:groupId', async (req, res) => {
try {
const { groupId } = req.params;
const group = await GroupRepository.getGroupForModeration(groupId);
if (!group) {
return res.status(404).json({
error: 'Group not found',
message: `Gruppe mit ID ${groupId} wurde nicht gefunden`
});
}
res.json(group);
} catch (error) {
console.error('Error fetching group for moderation:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Fehler beim Laden der Gruppe für Moderation',
details: error.message
});
}
});
// Einzelne Gruppe abrufen
router.get(endpoints.GET_GROUP, async (req, res) => {
try {
const { groupId } = req.params;
const group = await GroupRepository.getGroupById(groupId);
if (!group) {
return res.status(404).json({
error: 'Group not found',
message: `Gruppe mit ID ${groupId} wurde nicht gefunden`
});
}
res.json(group);
} catch (error) {
console.error('Error fetching group:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Fehler beim Laden der Gruppe',
details: error.message
});
}
});
// Gruppe freigeben/genehmigen
router.patch('/groups/:groupId/approve', async (req, res) => {
try {
const { groupId } = req.params;
const { approved } = req.body;
// Validierung
if (typeof approved !== 'boolean') {
return res.status(400).json({
error: 'Invalid request',
message: 'approved muss ein boolean Wert sein'
});
}
const updated = await GroupRepository.updateGroupApproval(groupId, approved);
if (!updated) {
return res.status(404).json({
error: 'Group not found',
message: `Gruppe mit ID ${groupId} wurde nicht gefunden`
});
}
res.json({
success: true,
message: approved ? 'Gruppe freigegeben' : 'Gruppe gesperrt',
groupId: groupId,
approved: approved
});
} catch (error) {
console.error('Error updating group approval:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Fehler beim Aktualisieren der Freigabe'
});
}
});
// Gruppe bearbeiten (Metadaten aktualisieren)
router.patch('/groups/:groupId', async (req, res) => {
try {
const { groupId } = req.params;
// Erlaubte Felder zum Aktualisieren
const allowed = ['year', 'title', 'description', 'name'];
const updates = {};
for (const field of allowed) {
if (req.body[field] !== undefined) {
updates[field] = req.body[field];
}
}
if (Object.keys(updates).length === 0) {
return res.status(400).json({
error: 'Invalid request',
message: 'Keine gültigen Felder zum Aktualisieren angegeben'
});
}
const updated = await GroupRepository.updateGroup(groupId, updates);
if (!updated) {
return res.status(404).json({
error: 'Group not found',
message: `Gruppe mit ID ${groupId} wurde nicht gefunden`
});
}
res.json({
success: true,
message: 'Gruppe erfolgreich aktualisiert',
groupId: groupId,
updates: updates
});
} catch (error) {
console.error('Error updating group:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Fehler beim Aktualisieren der Gruppe',
details: error.message
});
}
});
// Einzelnes Bild löschen
router.delete('/groups/:groupId/images/:imageId', async (req, res) => {
try {
const { groupId, imageId } = req.params;
const deleted = await GroupRepository.deleteImage(groupId, parseInt(imageId));
if (!deleted) {
return res.status(404).json({
error: 'Image not found',
message: `Bild mit ID ${imageId} in Gruppe ${groupId} wurde nicht gefunden`
});
}
res.json({
success: true,
message: 'Bild erfolgreich gelöscht',
groupId: groupId,
imageId: parseInt(imageId)
});
} catch (error) {
console.error('Error deleting image:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Fehler beim Löschen des Bildes'
});
}
});
// Batch-Update für mehrere Bildbeschreibungen (MUSS VOR der einzelnen Route stehen!)
router.patch('/groups/:groupId/images/batch-description', async (req, res) => {
try {
const { groupId } = req.params;
const { descriptions } = req.body;
// Validierung
if (!Array.isArray(descriptions) || descriptions.length === 0) {
return res.status(400).json({
error: 'Invalid request',
message: 'descriptions muss ein nicht-leeres Array sein'
});
}
// Validiere jede Beschreibung
for (const desc of descriptions) {
if (!desc.imageId || typeof desc.imageId !== 'number') {
return res.status(400).json({
error: 'Invalid request',
message: 'Jede Beschreibung muss eine gültige imageId enthalten'
});
}
if (desc.description && desc.description.length > 200) {
return res.status(400).json({
error: 'Invalid request',
message: `Bildbeschreibung für Bild ${desc.imageId} darf maximal 200 Zeichen lang sein`
});
}
}
const result = await GroupRepository.updateBatchImageDescriptions(groupId, descriptions);
res.json({
success: true,
message: `${result.updatedImages} Bildbeschreibungen erfolgreich aktualisiert`,
groupId: groupId,
updatedImages: result.updatedImages
});
} catch (error) {
console.error('Error batch updating image descriptions:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Fehler beim Aktualisieren der Bildbeschreibungen',
details: error.message
});
}
});
// Einzelne Bildbeschreibung aktualisieren
router.patch('/groups/:groupId/images/:imageId', async (req, res) => {
try {
const { groupId, imageId } = req.params;
const { image_description } = req.body;
// Validierung: Max 200 Zeichen
if (image_description && image_description.length > 200) {
return res.status(400).json({
error: 'Invalid request',
message: 'Bildbeschreibung darf maximal 200 Zeichen lang sein'
});
}
const updated = await GroupRepository.updateImageDescription(
parseInt(imageId),
groupId,
image_description
);
if (!updated) {
return res.status(404).json({
error: 'Image not found',
message: `Bild mit ID ${imageId} in Gruppe ${groupId} wurde nicht gefunden`
});
}
res.json({
success: true,
message: 'Bildbeschreibung erfolgreich aktualisiert',
groupId: groupId,
imageId: parseInt(imageId),
imageDescription: image_description
});
} catch (error) {
console.error('Error updating image description:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Fehler beim Aktualisieren der Bildbeschreibung',
details: error.message
});
}
});
// Gruppe löschen
router.delete(endpoints.DELETE_GROUP, async (req, res) => {
try {
const { groupId } = req.params;
const deleted = await GroupRepository.deleteGroup(groupId);
if (!deleted) {
return res.status(404).json({
error: 'Group not found',
message: `Gruppe mit ID ${groupId} wurde nicht gefunden`
});
}
res.json({
success: true,
message: 'Gruppe erfolgreich gelöscht',
groupId: groupId
});
} catch (error) {
console.error('Error deleting group:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Fehler beim Löschen der Gruppe'
});
}
});
module.exports = router;