removed old files

This commit is contained in:
Matthias Lotz 2025-10-20 20:07:45 +02:00
parent c7f75a4bd8
commit d29aaa05cd
9 changed files with 49 additions and 825 deletions

20
TODO.md Normal file
View File

@ -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).

View File

@ -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() {
<Route path="/upload/:image_url" component={UploadedImage} />
<Route path="/slideshow" component={SlideshowPage} />
<Route path="/groups/:groupId" component={PublicGroupImagesPage} />
<Route path="/groups" component={GroupsOverviewPage} />
{/* Groups overview removed; public group listing is handled elsewhere. */}
<Route path="/moderation" exact component={ModerationGroupsPage} />
<Route path="/moderation/groups/:groupId" component={ModerationGroupImagesPage} />
<Route component={FZF} />

View File

@ -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; }
@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; }

View File

@ -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; } }

View File

@ -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;
}
.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; }

View File

@ -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 <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="allContainer">
<Navbar />
<Container maxWidth="lg" className="page-container">
{/* Use shared GroupCard for top summary/actions */}
<GroupCard
group={group}
onApprove={(id, approved) => approveGroup(approved)}
onViewImages={() => { /* already on this page */ }}
onDelete={() => deleteGroup()}
isPending={!group.approved}
/>
<ImagePreviewGallery images={selectedImages} onRemoveImage={handleRemoveImage} />
{selectedImages.length > 0 && (
<>
<DescriptionInput metadata={metadata} onMetadataChange={setMetadata} />
<div className="action-buttons">
<Button className="btn btn-secondary" onClick={() => history.push('/moderation')}> Zurück</Button>
<Button className="primary-button" onClick={handleSave} disabled={saving}>{saving ? 'Speichern...' : 'Speichern'}</Button>
</div>
</>
)}
</Container>
<div className="footerContainer"><Footer /></div>
</div>
);
};
export default GroupImagesPage;

View File

@ -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 (
<div className="allContainer">
<Navbar />
<Container maxWidth="lg" className="page-container">
<div className="loading-container">
<CircularProgress size={60} color="primary" />
<Typography variant="h6" style={{ marginTop: '20px', color: '#666666' }}>
Slideshows werden geladen...
</Typography>
</div>
</Container>
<Footer />
</div>
);
}
return (
<div className="allContainer">
<Helmet>
<title>Gruppenübersicht - Interne Verwaltung</title>
<meta name="robots" content="noindex, nofollow, nosnippet, noarchive" />
<meta name="googlebot" content="noindex, nofollow" />
<meta name="description" content="Interne Gruppenübersicht - Nicht öffentlich zugänglich" />
</Helmet>
<Navbar />
<Container maxWidth="lg" className="page-container">
{/* Header */}
<Card className="header-card">
<Typography className="header-title">
🎬 Alle Slideshows
</Typography>
<Typography className="header-subtitle">
Verwalten Sie Ihre hochgeladenen Bildersammlungen
</Typography>
<div className="action-buttons">
<Button
className="primary-button"
onClick={handleCreateNew}
startIcon={<AddIcon />}
size="large"
>
Neue Slideshow erstellen
</Button>
<Button
className="home-button"
onClick={handleGoHome}
startIcon={<HomeIcon />}
size="large"
>
🏠 Zur Startseite
</Button>
</div>
</Card>
{/* Groups Grid */}
{error ? (
<div className="empty-state">
<Typography variant="h6" style={{ color: '#f44336', marginBottom: '20px' }}>
😕 Fehler beim Laden
</Typography>
<Typography variant="body1" style={{ color: '#666666', marginBottom: '30px' }}>
{error}
</Typography>
<Button onClick={loadGroups} className="primary-button">
🔄 Erneut versuchen
</Button>
</div>
) : groups.length === 0 ? (
<div className="empty-state">
<Typography variant="h4" style={{ color: '#666666', marginBottom: '20px' }}>
📸 Keine Slideshows vorhanden
</Typography>
<Typography variant="body1" style={{ color: '#999999', marginBottom: '30px' }}>
Erstellen Sie Ihre erste Slideshow, indem Sie mehrere Bilder hochladen.
</Typography>
<Button
className="primary-button"
onClick={handleCreateNew}
size="large"
>
Erste Slideshow erstellen
</Button>
</div>
) : (
<>
<Box marginBottom={2}>
<Typography variant="h6" style={{ color: '#666666' }}>
📊 {groups.length} Slideshow{groups.length !== 1 ? 's' : ''} gefunden
</Typography>
</Box>
<div className="groups-grid">
{groups.map((group) => (
<div key={group.groupId} className="grid-item-stretch">
<GroupCard
group={group}
onApprove={() => { /* no-op on public page */ }}
onViewImages={() => handleViewSlideshow(group.groupId)}
onDelete={() => { /* no-op on public page */ }}
isPending={false}
showActions={false}
/>
</div>
))}
</div>
</>
)}
</Container>
<div className="footerContainer">
<Footer />
</div>
</div>
);
}
export default GroupsOverviewPage;

View File

@ -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 <div className="moderation-loading">Lade Gruppe...</div>;
if (error) return <div className="moderation-error">{error}</div>;
@ -161,15 +132,6 @@ const ModerationGroupImagesPage = () => {
<Navbar />
<Container maxWidth="lg" className="page-container">
{/* Use shared GroupCard for top summary/actions */}
<GroupCard
group={group}
onApprove={(id, approved) => approveGroup(approved)}
onViewImages={() => { /* already on this page */ }}
onDelete={() => deleteGroup()}
isPending={!group.approved}
/>
<ImagePreviewGallery images={selectedImages} onRemoveImage={handleRemoveImage} />
{selectedImages.length > 0 && (

View File

@ -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 <div className="moderation-loading">Lade Gruppen...</div>;
}
if (error) {
return <div className="moderation-error">{error}</div>;
}
const pendingGroups = groups.filter(g => !g.approved);
const approvedGroups = groups.filter(g => g.approved);
return (
<div className="allContainer">
<Navbar />
<Helmet>
<title>Moderation - Interne Verwaltung</title>
<meta name="robots" content="noindex, nofollow, nosnippet, noarchive" />
<meta name="googlebot" content="noindex, nofollow" />
<meta name="description" content="Interne Moderationsseite - Nicht öffentlich zugänglich" />
</Helmet>
<Container className="moderation-content">
<h1>Moderation</h1>
<div className="moderation-stats">
<div className="stat-item">
<span className="stat-number">{pendingGroups.length}</span>
<span className="stat-label">Wartend</span>
</div>
<div className="stat-item">
<span className="stat-number">{approvedGroups.length}</span>
<span className="stat-label">Freigegeben</span>
</div>
<div className="stat-item">
<span className="stat-number">{groups.length}</span>
<span className="stat-label">Gesamt</span>
</div>
</div>
<div className="moderation-stats">
<div className="stat-item">
<span className="stat-number">{pendingGroups.length}</span>
<span className="stat-label">Wartend</span>
</div>
<div className="stat-item">
<span className="stat-number">{approvedGroups.length}</span>
<span className="stat-label">Freigegeben</span>
</div>
<div className="stat-item">
<span className="stat-number">{groups.length}</span>
<span className="stat-label">Gesamt</span>
</div>
</div>
{/* Wartende Gruppen */}
<section className="moderation-section">
<h2>🔍 Wartende Freigabe ({pendingGroups.length})</h2>
{pendingGroups.length === 0 ? (
<p className="no-groups">Keine wartenden Gruppen</p>
) : (
<div className="groups-grid">
{pendingGroups.map(group => (
<GroupCard
key={group.groupId}
group={group}
onApprove={approveGroup}
onViewImages={viewGroupImages}
onDelete={deleteGroup}
isPending={true}
/>
))}
</div>
)}
</section>
{/* Freigegebene Gruppen */}
<section className="moderation-section">
<h2> Freigegebene Gruppen ({approvedGroups.length})</h2>
{approvedGroups.length === 0 ? (
<p className="no-groups">Keine freigegebenen Gruppen</p>
) : (
<div className="groups-grid">
{approvedGroups.map(group => (
<GroupCard
key={group.groupId}
group={group}
onApprove={approveGroup}
onViewImages={viewGroupImages}
onDelete={deleteGroup}
isPending={false}
/>
))}
</div>
)}
</section>
{/* Bilder-Modal */}
{showImages && selectedGroup && (
<ImageModal
group={selectedGroup}
onClose={() => {
setShowImages(false);
setSelectedGroup(null);
}}
onDeleteImage={deleteImage}
/>
)}
</Container>
<div className="footerContainer"><Footer /></div>
</div>
);
};
// `GroupCard` has been extracted to `../ComponentUtils/GroupCard`
const ImageModal = ({ group, onClose, onDeleteImage }) => {
return (
<div className="image-modal-overlay" onClick={onClose}>
<div className="image-modal" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2>{group.title}</h2>
<button className="close-btn" onClick={onClose}>×</button>
</div>
<div className="modal-body">
<div className="group-details">
<p><strong>Jahr:</strong> {group.year}</p>
<p><strong>Ersteller:</strong> {group.name}</p>
{group.description && (
<p><strong>Beschreibung:</strong> {group.description}</p>
)}
<p><strong>Bilder:</strong> {group.images.length}</p>
</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={() => onDeleteImage(group.groupId, image.id)}
title="Bild löschen"
>
🗑
</button>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
};
export default ModerationPage;