IMP: Editierfunktion für Gruppen ergänzt
This commit is contained in:
parent
81c17c1b30
commit
566eb3aed6
|
|
@ -466,87 +466,3 @@ function SlideshowPage() {
|
|||
|
||||
---
|
||||
|
||||
## 🚀 Deployment-Überlegungen
|
||||
|
||||
### Datei-Größe Limits
|
||||
```javascript
|
||||
// Backend-Konfiguration erweitern
|
||||
app.use(fileUpload({
|
||||
limits: {
|
||||
fileSize: 50 * 1024 * 1024, // 50MB pro Datei
|
||||
files: 20 // Max 20 Dateien pro Upload
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
### Speicher-Management
|
||||
- **Cleanup-Job**: Alte Upload-Gruppen nach X Tagen löschen
|
||||
- **Komprimierung**: Automatische Bildkomprimierung für große Dateien
|
||||
- **CDN-Integration**: Für bessere Performance bei vielen Bildern
|
||||
|
||||
### Sicherheit
|
||||
- **File-Type Validation**: Nur erlaubte Bildformate
|
||||
- **Virus-Scanning**: Optional für Produktionsumgebung
|
||||
- **Rate Limiting**: Upload-Beschränkungen pro IP/User
|
||||
|
||||
---
|
||||
|
||||
## 📈 Erweiterungs-Möglichkeiten (Zukunft)
|
||||
|
||||
### Erweiterte Features
|
||||
- **Benutzer-Accounts**: Upload-Gruppen Benutzern zuordnen
|
||||
- **Tagging-System**: Bilder mit Tags versehen
|
||||
- **Sharing**: Upload-Gruppen per Link teilen
|
||||
- **Export**: Slideshow als Video oder PDF exportieren
|
||||
|
||||
### Slideshow-Features
|
||||
- **Autoplay**: Automatischer Bildwechsel
|
||||
- **Übergangs-Effekte**: Fade, Slide, etc.
|
||||
- **Hintergrundmusik**: Audio-Upload für Slideshows
|
||||
- **Vollbild-Modus**: Immersive Slideshow-Erfahrung
|
||||
|
||||
### Admin-Features
|
||||
- **Upload-Statistiken**: Dashboard mit Nutzungsmetriken
|
||||
- **Content-Moderation**: Gemeldete Inhalte prüfen
|
||||
- **Bulk-Operations**: Mehrere Gruppen gleichzeitig verwalten
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Quick-Start Implementierung
|
||||
|
||||
Für einen schnellen Proof-of-Concept (2-3 Stunden):
|
||||
|
||||
1. **Backend**: Erweitere `/upload` Route für Array-Handling
|
||||
2. **Frontend**: Ändere bestehende Dropzone auf `multiple: true`
|
||||
3. **Einfache Galerie**: Zeige alle Bilder einer "Session" an
|
||||
4. **Basis-Slideshow**: Einfache Vor/Zurück-Navigation
|
||||
|
||||
Dies würde eine funktionale Basis schaffen, die später ausgebaut werden kann.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Erfolgskriterien
|
||||
|
||||
### Must-Have
|
||||
- ✅ Mehrere Bilder gleichzeitig auswählen
|
||||
- ✅ Beschreibungstext hinzufügen
|
||||
- ✅ Upload als zusammengehörige Gruppe
|
||||
- ✅ Einfache Slideshow-Anzeige
|
||||
- ✅ Mobile-Kompatibilität
|
||||
|
||||
### Nice-to-Have
|
||||
- 🎨 Drag & Drop Reihenfolge ändern
|
||||
- 📊 Upload-Progress mit Details
|
||||
- 🖼️ Thumbnail-Navigation in Slideshow
|
||||
- 💾 Auto-Save der Beschreibung
|
||||
- 🔄 Batch-Operations (alle entfernen, etc.)
|
||||
|
||||
### Future Features
|
||||
- 👤 User-Management
|
||||
- 🏷️ Tagging-System
|
||||
- 📤 Export-Funktionen
|
||||
- 🎵 Audio-Integration
|
||||
|
||||
---
|
||||
|
||||
**Fazit**: Die Erweiterung ist gut machbar und baut logisch auf der bestehenden Architektur auf. Der modulare Ansatz ermöglicht schrittweise Implementierung und spätere Erweiterungen.
|
||||
57
ERWEITERUNG2.md
Normal file
57
ERWEITERUNG2.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Erweiterung 2: Zusätzliche Funktionen
|
||||
## Backend
|
||||
[ ] Erweiterung der API um die Funktion bestehende Daten zu editieren/aktualisieren
|
||||
[ ] Integration eines Benachrichtigungssystems (E-Mail, Push-Benachrichtigungen) wenn eine neue Slideshow hochgeladen wurde
|
||||
[ ] Implementierung eines Logging-Systems zur Nachverfolgung von Änderungen und Aktivitäten
|
||||
|
||||
# Frontend
|
||||
[ ] Erweiterung der Benutzeroberfläche um eine Editierfunktion für bestehende Einträge in ModerationPage.js
|
||||
[ ] In der angezeigten Gruppen soll statt Bilder ansehen Gruppe editieren stehen
|
||||
[ ] Diese bestehende Ansicht (Bilder ansehen) soll als eigene Seite implementiert werden
|
||||
|
||||
|
||||
## 🚀 Deployment-Überlegungen
|
||||
|
||||
|
||||
### Speicher-Management
|
||||
- **Komprimierung**: Automatische Bildkomprimierung für große Dateien
|
||||
|
||||
### Sicherheit
|
||||
- **File-Type Validation**: Nur erlaubte Bildformate
|
||||
- **Virus-Scanning**: Optional für Produktionsumgebung
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 📈 Erweiterungs-Möglichkeiten (Zukunft)
|
||||
|
||||
### Erweiterte Features
|
||||
- **Benutzer-Accounts**: Upload-Gruppen Benutzern zuordnen
|
||||
- **Export**: Slideshow als Video oder PDF exportieren
|
||||
|
||||
|
||||
### Admin-Features
|
||||
- **Bulk-Operations**: Mehrere Gruppen gleichzeitig verwalten (zum Beispiel löschen)
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 🎯 Erfolgskriterien
|
||||
|
||||
### Must-Have
|
||||
- ✅ Mobile-Kompatibilität
|
||||
|
||||
### Nice-to-Have
|
||||
- 🎨 Drag & Drop Reihenfolge ändern
|
||||
- 📊 Upload-Progress mit Details
|
||||
- 🖼️ Thumbnail-Navigation in Slideshow
|
||||
- 🔄 Batch-Operations (alle entfernen, etc.)
|
||||
|
||||
### Future Features
|
||||
- 👤 User-Management
|
||||
- 🏷️ Tagging-System
|
||||
- 📤 Export-Funktionen
|
||||
- 🎵 Audio-Integration
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -137,6 +137,53 @@ router.patch('/groups/:groupId/approve', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import UploadedImage from './Components/Pages/UploadedImagePage';
|
|||
import MultiUploadPage from './Components/Pages/MultiUploadPage';
|
||||
import SlideshowPage from './Components/Pages/SlideshowPage';
|
||||
import GroupsOverviewPage from './Components/Pages/GroupsOverviewPage';
|
||||
import GroupImagesPage from './Components/Pages/GroupImagesPage';
|
||||
import ModerationPage from './Components/Pages/ModerationPage';
|
||||
import FZF from './Components/Pages/404Page.js'
|
||||
|
||||
|
|
@ -16,7 +17,8 @@ function App() {
|
|||
<Route path="/" exact component={MultiUploadPage} />
|
||||
<Route path="/upload/:image_url" component={UploadedImage} />
|
||||
<Route path="/slideshow" component={SlideshowPage} />
|
||||
<Route path="/groups" component={GroupsOverviewPage} />
|
||||
<Route path="/groups/:groupId" component={GroupImagesPage} />
|
||||
<Route path="/groups" component={GroupsOverviewPage} />
|
||||
<Route path="/moderation" component={ModerationPage} />
|
||||
<Route component={FZF} />
|
||||
</Switch>
|
||||
|
|
|
|||
124
frontend/src/Components/Pages/GroupImagesPage.js
Normal file
124
frontend/src/Components/Pages/GroupImagesPage.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import './Css/ModerationPage.css';
|
||||
|
||||
const GroupImagesPage = () => {
|
||||
const { groupId } = useParams();
|
||||
const history = useHistory();
|
||||
const [group, setGroup] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadGroup();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [groupId]);
|
||||
|
||||
const loadGroup = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await fetch(`/moderation/groups/${groupId}`);
|
||||
if (!res.ok) throw new Error('Nicht gefunden');
|
||||
const data = await res.json();
|
||||
setGroup(data);
|
||||
} catch (e) {
|
||||
setError('Fehler beim Laden der Gruppe');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
setGroup({ ...group, [field]: value });
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!group) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
title: group.title,
|
||||
description: group.description,
|
||||
year: group.year,
|
||||
name: group.name
|
||||
};
|
||||
|
||||
const res = await fetch(`/groups/${groupId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.message || 'Speichern fehlgeschlagen');
|
||||
}
|
||||
|
||||
alert('Gruppe erfolgreich aktualisiert');
|
||||
history.push('/moderation');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Fehler beim Speichern: ' + e.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteImage = async (imageId) => {
|
||||
if (!window.confirm('Bild wirklich löschen?')) return;
|
||||
try {
|
||||
const res = await fetch(`/groups/${groupId}/images/${imageId}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error('Löschen fehlgeschlagen');
|
||||
// Aktualisiere lokale Ansicht
|
||||
setGroup({ ...group, images: group.images.filter(img => img.id !== imageId), imageCount: group.imageCount - 1 });
|
||||
alert('Bild gelöscht');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Fehler beim Löschen des Bildes');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div className="moderation-loading">Lade Gruppe...</div>;
|
||||
if (error) return <div className="moderation-error">{error}</div>;
|
||||
if (!group) return <div className="moderation-error">Gruppe nicht gefunden</div>;
|
||||
|
||||
return (
|
||||
<div className="group-images-page">
|
||||
<h1>Gruppe bearbeiten</h1>
|
||||
|
||||
<div className="group-edit-form">
|
||||
<label>Titel</label>
|
||||
<input value={group.title || ''} onChange={e => handleChange('title', e.target.value)} />
|
||||
|
||||
<label>Beschreibung</label>
|
||||
<textarea value={group.description || ''} onChange={e => handleChange('description', e.target.value)} />
|
||||
|
||||
<label>Jahr</label>
|
||||
<input value={group.year || ''} onChange={e => handleChange('year', e.target.value)} />
|
||||
|
||||
<label>Ersteller</label>
|
||||
<input value={group.name || ''} onChange={e => handleChange('name', e.target.value)} />
|
||||
|
||||
<div className="edit-actions">
|
||||
<button className="btn btn-secondary" onClick={() => history.push('/moderation')}>↩ Zurück</button>
|
||||
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>{saving ? 'Speichern...' : 'Speichern'}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="images-grid">
|
||||
{group.images.map(image => (
|
||||
<div key={image.id} className="image-item">
|
||||
<img src={`/download/${image.fileName}`} alt={image.originalName} className="modal-image" />
|
||||
<div className="image-actions">
|
||||
<span className="image-name">{image.originalName}</span>
|
||||
<button className="btn btn-danger btn-sm" onClick={() => handleDeleteImage(image.id)}>🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupImagesPage;
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import './Css/ModerationPage.css';
|
||||
|
||||
const ModerationPage = () => {
|
||||
|
|
@ -8,6 +9,7 @@ const ModerationPage = () => {
|
|||
const [error, setError] = useState(null);
|
||||
const [selectedGroup, setSelectedGroup] = useState(null);
|
||||
const [showImages, setShowImages] = useState(false);
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
loadModerationGroups();
|
||||
|
|
@ -127,21 +129,9 @@ const ModerationPage = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const viewGroupImages = async (group) => {
|
||||
try {
|
||||
const response = await fetch(`/moderation/groups/${group.group_id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setSelectedGroup(data);
|
||||
setShowImages(true);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Bilder:', error);
|
||||
alert('Fehler beim Laden der Bilder');
|
||||
}
|
||||
// Navigate to the dedicated group images page
|
||||
const viewGroupImages = (group) => {
|
||||
history.push(`/groups/${group.group_id}`);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
|
|
@ -267,7 +257,7 @@ const GroupCard = ({ group, onApprove, onViewImages, onDelete, isPending }) => {
|
|||
className="btn btn-secondary"
|
||||
onClick={() => onViewImages(group)}
|
||||
>
|
||||
👁️ Bilder ansehen
|
||||
✏️ Gruppe editieren
|
||||
</button>
|
||||
|
||||
{isPending ? (
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user