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
340 lines
11 KiB
JavaScript
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; |