diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..e9b69c0 --- /dev/null +++ b/TODO.md @@ -0,0 +1,20 @@ +# TODO / Offene Punkte + +Diese Datei listet die noch offenen Arbeiten, die ich im Projekt verfolge. Ich pflege sie, sobald ich Aufgaben erledige. + +## Aktuelle Aufgaben + +- [ ] CSS-Sweep: Duplikate finden und Regeln in `frontend/src/Components/ComponentUtils/Css/main.css` zentralisieren +- [ ] Page-CSS bereinigen: Entfernen von Regeln, die jetzt in `main.css` sind +- [ ] README: Kurzbeschreibung des Style-Guides und wo zentrale Klassen liegen +- [ ] Persistentes Reordering: Drag-and-drop in `ImagePreviewGallery` + Backend-Endpunkt +- [ ] Kleine Smoke-Tests: Frontend-Build lokal laufen lassen und UI quick-check + +## Erledigte Aufgaben +- [x] Alte Dateien entfernt (`ModerationPage.js`, alte `GroupImagesPage.js`) +- [x] Moderation-Detailseite angepasst (zeigt jetzt nur Bilder, Metadaten-Editor und Save/Back) +- [x] Routing: `/moderation` und `/moderation/groups/:groupId` sowie `/groups/:groupId` (public) gesetzt + +--- + +Wenn du möchtest, dass ich jetzt einzelne Aufgaben aus der "Aktuelle Aufgaben"-Liste abarbeite, sag mir welche Priorität hat (z.B. zuerst `main.css` aufsetzen und die größten Duplikate entfernen). \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js index 87c225c..f6b5113 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -5,7 +5,6 @@ import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; 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 ModerationGroupsPage from './Components/Pages/ModerationGroupsPage'; import ModerationGroupImagesPage from './Components/Pages/ModerationGroupImagesPage'; import PublicGroupImagesPage from './Components/Pages/PublicGroupImagesPage'; @@ -19,7 +18,7 @@ function App() { - + {/* Groups overview removed; public group listing is handled elsewhere. */} diff --git a/frontend/src/Components/Pages/Css/GroupImagesPage.css b/frontend/src/Components/Pages/Css/GroupImagesPage.css index 949e1d1..cb30de1 100644 --- a/frontend/src/Components/Pages/Css/GroupImagesPage.css +++ b/frontend/src/Components/Pages/Css/GroupImagesPage.css @@ -1,3 +1,6 @@ -.header-text { font-family: roboto; font-weight: 400; font-size: 28px; text-align: center; margin-bottom: 10px; color: #333333; } -.subheader-text { font-family: roboto; font-weight: 300; font-size: 16px; color: #666666; text-align: center; margin-bottom: 30px; } -.action-buttons { display: flex; gap: 15px; justify-content: center; margin-top: 20px; flex-wrap: wrap; } +@import '../../ComponentUtils/Css/main.css'; + +/* Page-specific styles for group images edit page */ +.header-text { font-weight: 400; font-size: 28px; text-align: center; margin-bottom: 10px; color:#333333; } +.subheader-text { font-weight: 300; font-size: 16px; color:#666666; text-align:center; margin-bottom:30px; } +.action-buttons { display:flex; gap:15px; justify-content:center; margin-top:20px; flex-wrap:wrap; } diff --git a/frontend/src/Components/Pages/Css/GroupsOverviewPage.css b/frontend/src/Components/Pages/Css/GroupsOverviewPage.css index 06fa9d9..3e3276e 100644 --- a/frontend/src/Components/Pages/Css/GroupsOverviewPage.css +++ b/frontend/src/Components/Pages/Css/GroupsOverviewPage.css @@ -1,6 +1,8 @@ -.header-card { border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); margin-bottom: 30px; text-align: center; padding: 20px; } -.header-title { font-family: roboto; font-weight: 500; font-size: 28px; color: #333333; margin-bottom: 10px; } -.header-subtitle { font-family: roboto; font-size: 16px; color: #666666; margin-bottom: 20px; } -.empty-state { text-align:center; padding:60px 20px; } -.loading-container { text-align:center; padding:60px 20px; } +@import '../../ComponentUtils/Css/main.css'; + +/* Page-specific rules for GroupsOverviewPage */ +.header-card { margin-bottom: 30px; text-align: center; padding: 20px; } +.header-title { font-weight: 500; font-size: 28px; color: #333333; margin-bottom: 10px; } +.header-subtitle { font-size: 16px; color: #666666; margin-bottom: 20px; } + @media (max-width:800px) { .nav__links, .cta { display:none; } } diff --git a/frontend/src/Components/Pages/Css/ModerationPage.css b/frontend/src/Components/Pages/Css/ModerationPage.css index c20f3a3..e48f959 100644 --- a/frontend/src/Components/Pages/Css/ModerationPage.css +++ b/frontend/src/Components/Pages/Css/ModerationPage.css @@ -1,76 +1,16 @@ -.moderation-page { - max-width: 1200px; - margin: 0 auto; - padding: 20px; -} +@import '../../ComponentUtils/Css/main.css'; -.moderation-page h1 { - text-align: center; - color: #333; - margin-bottom: 30px; -} +/* Page-specific moderation styles */ +.moderation-page { max-width: 1200px; margin: 0 auto; padding: 20px; } +.moderation-page h1 { text-align:center; color:#333; margin-bottom:30px; } +.moderation-loading, .moderation-error { text-align:center; padding:50px; font-size:18px; } +.moderation-error { color:#dc3545; } -.moderation-loading, .moderation-error { - text-align: center; - padding: 50px; - font-size: 18px; -} +.moderation-stats { display:flex; justify-content:center; gap:40px; margin-bottom:40px; padding:20px; background:#f8f9fa; border-radius:12px; } +.stat-item { text-align:center; } +.stat-number { display:block; font-size:2.5rem; font-weight:bold; color:#007bff; } +.stat-label { display:block; font-size:0.9rem; color:#6c757d; margin-top:5px; } -.moderation-error { - color: #dc3545; -} - -/* Statistiken */ -.moderation-stats { - display: flex; - justify-content: center; - gap: 40px; - margin-bottom: 40px; - padding: 20px; - background: #f8f9fa; - border-radius: 12px; -} - -.stat-item { - text-align: center; -} - -.stat-number { - display: block; - font-size: 2.5rem; - font-weight: bold; - color: #007bff; -} - -.stat-label { - display: block; - font-size: 0.9rem; - color: #6c757d; - margin-top: 5px; -} - -/* Sections */ -.moderation-section { - margin-bottom: 50px; -} - -.moderation-section h2 { - color: #333; - border-bottom: 2px solid #e9ecef; - padding-bottom: 10px; - margin-bottom: 25px; -} - -.no-groups { - text-align: center; - color: #6c757d; - font-style: italic; - padding: 30px; -} - -/* Groups Grid */ -.groups-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); - gap: 20px; -} \ No newline at end of file +.moderation-section { margin-bottom:50px; } +.moderation-section h2 { color:#333; border-bottom:2px solid #e9ecef; padding-bottom:10px; margin-bottom:25px; } +.no-groups { text-align:center; color:#6c757d; font-style:italic; padding:30px; } \ No newline at end of file diff --git a/frontend/src/Components/Pages/GroupImagesPage.js b/frontend/src/Components/Pages/GroupImagesPage.js deleted file mode 100644 index cf56c47..0000000 --- a/frontend/src/Components/Pages/GroupImagesPage.js +++ /dev/null @@ -1,200 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useParams, useHistory } from 'react-router-dom'; -import { Button, Card, CardContent, Typography, 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 GroupImagesPage = () => { - // use CSS classes from GroupImagesPage.css - 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 handleChange = (field, value) => { - setGroup({ ...group, [field]: value }); - setMetadata(prev => ({ ...prev, [field]: value })); - }; - - 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 GroupImagesPage; diff --git a/frontend/src/Components/Pages/GroupsOverviewPage.js b/frontend/src/Components/Pages/GroupsOverviewPage.js deleted file mode 100644 index fa5c219..0000000 --- a/frontend/src/Components/Pages/GroupsOverviewPage.js +++ /dev/null @@ -1,201 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; -import { Helmet } from 'react-helmet'; -import '../Pages/Css/GroupsOverviewPage.css'; -import { - Container, - Card, - CardContent, - Typography, - Button, - Grid, - CardMedia, - Box, - CircularProgress, - Chip -} from '@material-ui/core'; -import { - Slideshow as SlideshowIcon, - Add as AddIcon, - Home as HomeIcon -} from '@material-ui/icons'; -import Swal from 'sweetalert2/dist/sweetalert2.js'; - -// Components -import Navbar from '../ComponentUtils/Headers/Navbar'; -import Footer from '../ComponentUtils/Footer'; -import GroupCard from '../ComponentUtils/GroupCard'; - -// Utils -import { fetchAllGroups, deleteGroup } from '../../Utils/batchUpload'; - -// Styles -import '../../App.css'; - -function GroupsOverviewPage() { - // use CSS classes from GroupsOverviewPage.css - const history = useHistory(); - - const [groups, setGroups] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - loadGroups(); - }, []); - - const loadGroups = async () => { - try { - setLoading(true); - const response = await fetchAllGroups(); - setGroups(response.groups || []); - setError(null); - } catch (err) { - setError(err.message); - console.error('Error loading groups:', err); - } finally { - setLoading(false); - } - }; - - const handleViewSlideshow = (groupId) => { - history.push(`/slideshow/${groupId}`); - }; - - const handleCreateNew = () => { - history.push('/multi-upload'); - }; - - const handleGoHome = () => { - history.push('/'); - }; - - const formatDate = (dateString) => { - return new Date(dateString).toLocaleDateString('de-DE', { - year: 'numeric', - month: 'short', - day: 'numeric' - }); - }; - - if (loading) { - return ( -
- - -
- - - Slideshows werden geladen... - -
-
-
-
- ); - } - - return ( -
- - Gruppenübersicht - Interne Verwaltung - - - - - - - - {/* Header */} - - - 🎬 Alle Slideshows - - - Verwalten Sie Ihre hochgeladenen Bildersammlungen - - -
- - - -
-
- - {/* Groups Grid */} - {error ? ( -
- - 😕 Fehler beim Laden - - - {error} - - -
- ) : groups.length === 0 ? ( -
- - 📸 Keine Slideshows vorhanden - - - Erstellen Sie Ihre erste Slideshow, indem Sie mehrere Bilder hochladen. - - -
- ) : ( - <> - - - 📊 {groups.length} Slideshow{groups.length !== 1 ? 's' : ''} gefunden - - - -
- {groups.map((group) => ( -
- { /* no-op on public page */ }} - onViewImages={() => handleViewSlideshow(group.groupId)} - onDelete={() => { /* no-op on public page */ }} - isPending={false} - showActions={false} - /> -
- ))} -
- - )} -
- -
-
-
-
- ); -} - -export default GroupsOverviewPage; \ No newline at end of file diff --git a/frontend/src/Components/Pages/ModerationGroupImagesPage.js b/frontend/src/Components/Pages/ModerationGroupImagesPage.js index 32a4ad0..aae1807 100644 --- a/frontend/src/Components/Pages/ModerationGroupImagesPage.js +++ b/frontend/src/Components/Pages/ModerationGroupImagesPage.js @@ -9,7 +9,6 @@ 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'; @@ -122,35 +121,7 @@ const ModerationGroupImagesPage = () => { 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' }); - } - }; + // Note: approve/delete group actions are intentionally removed from this page if (loading) return
Lade Gruppe...
; if (error) return
{error}
; @@ -161,16 +132,7 @@ const ModerationGroupImagesPage = () => { - {/* Use shared GroupCard for top summary/actions */} - approveGroup(approved)} - onViewImages={() => { /* already on this page */ }} - onDelete={() => deleteGroup()} - isPending={!group.approved} - /> - - + {selectedImages.length > 0 && ( <> diff --git a/frontend/src/Components/Pages/ModerationPage.js b/frontend/src/Components/Pages/ModerationPage.js deleted file mode 100644 index d69d71c..0000000 --- a/frontend/src/Components/Pages/ModerationPage.js +++ /dev/null @@ -1,301 +0,0 @@ -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 ModerationPage = () => { - 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(`/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} -
- {image.originalName} - -
-
- ))} -
-
-
-
- ); -}; - -export default ModerationPage; \ No newline at end of file