- {image.name || image.originalName || 'Bild'}{image.size ? ' • ' + formatFileSize(image.size) : ''}
+
+
{image.originalName || image.name || 'Bild'}
+
+ {image.remoteUrl && image.remoteUrl.includes('/download/') ? (
+
Server-Datei: {image.remoteUrl.split('/').pop()}
+ ) : image.filePath ? (
+
Server-Datei: {image.filePath.split('/').pop()}
+ ) : null}
+ {image.captureDate ?
Aufnahmedatum: {new Date(image.captureDate).toLocaleDateString('de-DE')}
: null}
+
+
+
+
+
+
diff --git a/frontend/src/Components/Pages/GroupImagesPage.js b/frontend/src/Components/Pages/GroupImagesPage.js
index 2465f46..cf56c47 100644
--- a/frontend/src/Components/Pages/GroupImagesPage.js
+++ b/frontend/src/Components/Pages/GroupImagesPage.js
@@ -9,6 +9,7 @@ import Navbar from '../ComponentUtils/Headers/Navbar';
import Footer from '../ComponentUtils/Footer';
import ImagePreviewGallery from '../ComponentUtils/MultiUpload/ImagePreviewGallery';
import DescriptionInput from '../ComponentUtils/MultiUpload/DescriptionInput';
+import GroupCard from '../ComponentUtils/GroupCard';
import '../Pages/Css/GroupImagesPage.css';
@@ -127,6 +128,36 @@ const GroupImagesPage = () => {
setSelectedImages(prev => prev.filter((_, index) => index !== indexToRemove));
};
+ const approveGroup = async (approved) => {
+ try {
+ const response = await fetch(`/groups/${groupId}/approve`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ approved })
+ });
+ if (!response.ok) throw new Error('Freigabe fehlgeschlagen');
+ const updated = { ...group, approved };
+ setGroup(updated);
+ Swal.fire({ icon: 'success', title: approved ? 'Gruppe freigegeben' : 'Gruppe gesperrt', timer: 1200, showConfirmButton: false });
+ } catch (e) {
+ console.error(e);
+ Swal.fire({ icon: 'error', title: 'Fehler beim Aktualisieren des Freigabestatus' });
+ }
+ };
+
+ const deleteGroup = async () => {
+ if (!window.confirm('Gruppe wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) return;
+ try {
+ const res = await fetch(`/groups/${groupId}`, { method: 'DELETE' });
+ if (!res.ok) throw new Error('Löschen fehlgeschlagen');
+ Swal.fire({ icon: 'success', title: 'Gruppe gelöscht', timer: 1200, showConfirmButton: false });
+ history.push('/moderation');
+ } catch (e) {
+ console.error(e);
+ Swal.fire({ icon: 'error', title: 'Fehler beim Löschen der Gruppe' });
+ }
+ };
+
if (loading) return
Lade Gruppe...
;
if (error) return
{error}
;
if (!group) return
Gruppe nicht gefunden
;
@@ -135,32 +166,35 @@ const GroupImagesPage = () => {
-
-
-
- Gruppe bearbeiten
- {group.title || ''}
+
+ {/* Use shared GroupCard for top summary/actions */}
+ approveGroup(approved)}
+ onViewImages={() => { /* already on this page */ }}
+ onDelete={() => deleteGroup()}
+ isPending={!group.approved}
+ />
-
+
- {selectedImages.length > 0 && (
- <>
-
+ {selectedImages.length > 0 && (
+ <>
+
-
-
-
-
- >
- )}
+
+
+
+
+ >
+ )}
-
-
);
+
};
export default GroupImagesPage;
diff --git a/frontend/src/Components/Pages/ModerationGroupImagesPage.js b/frontend/src/Components/Pages/ModerationGroupImagesPage.js
new file mode 100644
index 0000000..32a4ad0
--- /dev/null
+++ b/frontend/src/Components/Pages/ModerationGroupImagesPage.js
@@ -0,0 +1,194 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useHistory } from 'react-router-dom';
+import { Button, Container } from '@material-ui/core';
+import Swal from 'sweetalert2/dist/sweetalert2.js';
+import 'sweetalert2/src/sweetalert2.scss';
+
+// Components
+import Navbar from '../ComponentUtils/Headers/Navbar';
+import Footer from '../ComponentUtils/Footer';
+import ImagePreviewGallery from '../ComponentUtils/MultiUpload/ImagePreviewGallery';
+import DescriptionInput from '../ComponentUtils/MultiUpload/DescriptionInput';
+import GroupCard from '../ComponentUtils/GroupCard';
+
+
+import '../Pages/Css/GroupImagesPage.css';
+
+const ModerationGroupImagesPage = () => {
+ 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);
+
+ // selectedImages will hold objects compatible with ImagePreviewGallery
+ const [selectedImages, setSelectedImages] = useState([]);
+ const [metadata, setMetadata] = useState({ year: new Date().getFullYear(), title: '', description: '', name: '' });
+
+ 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);
+
+ // Map group's images to preview-friendly objects
+ if (data.images && data.images.length > 0) {
+ const mapped = data.images.map(img => ({
+ remoteUrl: `/download/${img.fileName}`,
+ originalName: img.originalName || img.fileName,
+ id: img.id
+ }));
+ setSelectedImages(mapped);
+ }
+
+ // populate metadata from group
+ setMetadata({
+ year: data.year || new Date().getFullYear(),
+ title: data.title || '',
+ description: data.description || '',
+ name: data.name || ''
+ });
+ } catch (e) {
+ setError('Fehler beim Laden der Gruppe');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSave = async () => {
+ if (!group) return;
+ setSaving(true);
+ try {
+ // Use metadata state (controlled by DescriptionInput) as source of truth
+ const payload = {
+ title: metadata.title,
+ description: metadata.description,
+ year: metadata.year,
+ name: metadata.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');
+ }
+
+ Swal.fire({ icon: 'success', title: 'Gruppe erfolgreich aktualisiert', timer: 1500, showConfirmButton: false });
+ history.push('/moderation');
+ } catch (e) {
+ console.error(e);
+ Swal.fire({ icon: 'error', title: 'Fehler beim Speichern', text: 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
+ const newImages = group.images.filter(img => img.id !== imageId);
+ setGroup({ ...group, images: newImages, imageCount: (group.imageCount || 0) - 1 });
+ setSelectedImages(prev => prev.filter(img => img.id !== imageId));
+ Swal.fire({ icon: 'success', title: 'Bild gelöscht', timer: 1200, showConfirmButton: false });
+ } catch (e) {
+ console.error(e);
+ Swal.fire({ icon: 'error', title: 'Fehler beim Löschen des Bildes' });
+ }
+ };
+
+ const handleRemoveImage = (indexToRemove) => {
+ // If it's a remote image mapped with id, call delete
+ const img = selectedImages[indexToRemove];
+ if (img && img.id) {
+ handleDeleteImage(img.id);
+ return;
+ }
+ setSelectedImages(prev => prev.filter((_, index) => index !== indexToRemove));
+ };
+
+ const approveGroup = async (approved) => {
+ try {
+ const response = await fetch(`/groups/${groupId}/approve`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ approved })
+ });
+ if (!response.ok) throw new Error('Freigabe fehlgeschlagen');
+ const updated = { ...group, approved };
+ setGroup(updated);
+ Swal.fire({ icon: 'success', title: approved ? 'Gruppe freigegeben' : 'Gruppe gesperrt', timer: 1200, showConfirmButton: false });
+ } catch (e) {
+ console.error(e);
+ Swal.fire({ icon: 'error', title: 'Fehler beim Aktualisieren des Freigabestatus' });
+ }
+ };
+
+ const deleteGroup = async () => {
+ if (!window.confirm('Gruppe wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) return;
+ try {
+ const res = await fetch(`/groups/${groupId}`, { method: 'DELETE' });
+ if (!res.ok) throw new Error('Löschen fehlgeschlagen');
+ Swal.fire({ icon: 'success', title: 'Gruppe gelöscht', timer: 1200, showConfirmButton: false });
+ history.push('/moderation');
+ } catch (e) {
+ console.error(e);
+ Swal.fire({ icon: 'error', title: 'Fehler beim Löschen der Gruppe' });
+ }
+ };
+
+ if (loading) return
Lade Gruppe...
;
+ if (error) return
{error}
;
+ if (!group) return
Gruppe nicht gefunden
;
+
+ return (
+
+
+
+
+ {/* Use shared GroupCard for top summary/actions */}
+ approveGroup(approved)}
+ onViewImages={() => { /* already on this page */ }}
+ onDelete={() => deleteGroup()}
+ isPending={!group.approved}
+ />
+
+
+
+ {selectedImages.length > 0 && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+ );
+
+};
+
+export default ModerationGroupImagesPage;
diff --git a/frontend/src/Components/Pages/ModerationGroupsPage.js b/frontend/src/Components/Pages/ModerationGroupsPage.js
new file mode 100644
index 0000000..9b623c6
--- /dev/null
+++ b/frontend/src/Components/Pages/ModerationGroupsPage.js
@@ -0,0 +1,301 @@
+import React, { useState, useEffect } from 'react';
+import { Helmet } from 'react-helmet';
+import { useHistory } from 'react-router-dom';
+import { Container } from '@material-ui/core';
+import './Css/ModerationPage.css';
+import Navbar from '../ComponentUtils/Headers/Navbar';
+import Footer from '../ComponentUtils/Footer';
+import GroupCard from '../ComponentUtils/GroupCard';
+
+const ModerationGroupsPage = () => {
+ const [groups, setGroups] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [selectedGroup, setSelectedGroup] = useState(null);
+ const [showImages, setShowImages] = useState(false);
+ const history = useHistory();
+
+ useEffect(() => {
+ loadModerationGroups();
+ }, []);
+
+ const loadModerationGroups = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch('/moderation/groups');
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ setGroups(data.groups);
+ } catch (error) {
+ console.error('Fehler beim Laden der Moderations-Gruppen:', error);
+ setError('Fehler beim Laden der Gruppen');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const approveGroup = async (groupId, approved) => {
+ try {
+ const response = await fetch(`/groups/${groupId}/approve`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ approved: approved })
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ // Update local state
+ setGroups(groups.map(group =>
+ group.groupId === groupId
+ ? { ...group, approved: approved }
+ : group
+ ));
+ } catch (error) {
+ console.error('Fehler beim Freigeben der Gruppe:', error);
+ alert('Fehler beim Freigeben der Gruppe');
+ }
+ };
+
+ const deleteImage = async (groupId, imageId) => {
+ console.log('deleteImage called with:', { groupId, imageId });
+ console.log('API_URL:', window._env_.API_URL);
+
+ try {
+ // Use relative URL to go through Nginx proxy
+ const url = `/groups/${groupId}/images/${imageId}`;
+ console.log('DELETE request to:', url);
+
+ const response = await fetch(url, {
+ method: 'DELETE'
+ });
+
+ console.log('Response status:', response.status);
+ console.log('Response ok:', response.ok);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ // Remove image from selectedGroup
+ if (selectedGroup && selectedGroup.groupId === groupId) {
+ const updatedImages = selectedGroup.images.filter(img => img.id !== imageId);
+ setSelectedGroup({
+ ...selectedGroup,
+ images: updatedImages,
+ imageCount: updatedImages.length
+ });
+ }
+
+ // Update group image count
+ setGroups(groups.map(group =>
+ group.groupId === groupId
+ ? { ...group, imageCount: group.imageCount - 1 }
+ : group
+ ));
+
+ } catch (error) {
+ console.error('Fehler beim Löschen des Bildes:', error);
+ console.error('Error details:', error.message, error.stack);
+ alert('Fehler beim Löschen des Bildes: ' + error.message);
+ }
+ };
+
+ const deleteGroup = async (groupId) => {
+ if (!window.confirm('Gruppe wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`/groups/${groupId}`, {
+ method: 'DELETE'
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ setGroups(groups.filter(group => group.groupId !== groupId));
+ if (selectedGroup && selectedGroup.groupId === groupId) {
+ setSelectedGroup(null);
+ setShowImages(false);
+ }
+ } catch (error) {
+ console.error('Fehler beim Löschen der Gruppe:', error);
+ alert('Fehler beim Löschen der Gruppe');
+ }
+ };
+
+ // Navigate to the dedicated group images page
+ const viewGroupImages = (group) => {
+ history.push(`/moderation/groups/${group.groupId}`);
+ };
+
+ if (loading) {
+ return
Lade Gruppen...
;
+ }
+
+ if (error) {
+ return
{error}
;
+ }
+
+ const pendingGroups = groups.filter(g => !g.approved);
+ const approvedGroups = groups.filter(g => g.approved);
+
+ return (
+
+
+
+ Moderation - Interne Verwaltung
+
+
+
+
+
+ Moderation
+
+
+
+ {pendingGroups.length}
+ Wartend
+
+
+ {approvedGroups.length}
+ Freigegeben
+
+
+ {groups.length}
+ Gesamt
+
+
+
+
+
+ {pendingGroups.length}
+ Wartend
+
+
+ {approvedGroups.length}
+ Freigegeben
+
+
+ {groups.length}
+ Gesamt
+
+
+
+ {/* Wartende Gruppen */}
+
+ 🔍 Wartende Freigabe ({pendingGroups.length})
+ {pendingGroups.length === 0 ? (
+ Keine wartenden Gruppen
+ ) : (
+
+ {pendingGroups.map(group => (
+
+ ))}
+
+ )}
+
+
+ {/* Freigegebene Gruppen */}
+
+ ✅ Freigegebene Gruppen ({approvedGroups.length})
+ {approvedGroups.length === 0 ? (
+ Keine freigegebenen Gruppen
+ ) : (
+
+ {approvedGroups.map(group => (
+
+ ))}
+
+ )}
+
+
+ {/* Bilder-Modal */}
+ {showImages && selectedGroup && (
+ {
+ setShowImages(false);
+ setSelectedGroup(null);
+ }}
+ onDeleteImage={deleteImage}
+ />
+ )}
+
+
+
+ );
+};
+
+// `GroupCard` has been extracted to `../ComponentUtils/GroupCard`
+
+const ImageModal = ({ group, onClose, onDeleteImage }) => {
+ return (
+
+
e.stopPropagation()}>
+
+
{group.title}
+
+
+
+
+
+
Jahr: {group.year}
+
Ersteller: {group.name}
+ {group.description && (
+
Beschreibung: {group.description}
+ )}
+
Bilder: {group.images.length}
+
+
+
+ {group.images.map(image => (
+
+

+
+ {image.originalName}
+
+
+
+ ))}
+
+
+
+
+ );
+};
+
+export default ModerationGroupsPage;
diff --git a/frontend/src/Components/Pages/PublicGroupImagesPage.js b/frontend/src/Components/Pages/PublicGroupImagesPage.js
new file mode 100644
index 0000000..41d2e0f
--- /dev/null
+++ b/frontend/src/Components/Pages/PublicGroupImagesPage.js
@@ -0,0 +1,62 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useHistory } from 'react-router-dom';
+import { Button, Container } from '@material-ui/core';
+import Navbar from '../ComponentUtils/Headers/Navbar';
+import Footer from '../ComponentUtils/Footer';
+import GroupCard from '../ComponentUtils/GroupCard';
+import ImagePreviewGallery from '../ComponentUtils/MultiUpload/ImagePreviewGallery';
+import '../Pages/Css/GroupImagesPage.css';
+
+const PublicGroupImagesPage = () => {
+ const { groupId } = useParams();
+ const history = useHistory();
+ const [group, setGroup] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ loadGroup();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [groupId]);
+
+ const loadGroup = async () => {
+ try {
+ setLoading(true);
+ // Public endpoint (no moderation controls)
+ const res = await fetch(`/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);
+ }
+ };
+
+ if (loading) return
Lade Gruppe...
;
+ if (error) return
{error}
;
+ if (!group) return
Gruppe nicht gefunden
;
+
+ return (
+
+
+
+
+
+
+
+ {group.images && group.images.length > 0 ? (
+
({ remoteUrl: `/download/${img.fileName}`, originalName: img.originalName || img.fileName, id: img.id }))} showRemove={false} />
+ ) : (
+ Keine Bilder in dieser Gruppe.
+ )}
+
+
+
+
+
+ );
+};
+
+export default PublicGroupImagesPage;