- Update package.json: react-router-dom ^5.2.0→^6.28.0 - Migrate App.js: Switch→Routes, component→element props, path="*" for 404 - Migrate 5 pages: useHistory→useNavigate, history.push()→navigate() - GroupsOverviewPage.js (4x navigate) - ModerationGroupsPage.js (1x navigate) - ModerationGroupImagesPage.js (2x navigate) - PublicGroupImagesPage.js (import updated) - SlideshowPage.js (4x navigate + keyboard handler) - Regenerate package-lock.json with react-router v6 ✅ Tested: Production build 254.46 KB gzip (+1.17 KB) ✅ Manual test: Navigation, moderation routing, slideshow ESC - all working Phase 3 complete: Modern react-router v6 with improved routing API.
273 lines
10 KiB
JavaScript
273 lines
10 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import { Helmet } from 'react-helmet';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { Container } from '@material-ui/core';
|
||
import Navbar from '../ComponentUtils/Headers/Navbar';
|
||
import Footer from '../ComponentUtils/Footer';
|
||
import ImageGallery from '../ComponentUtils/ImageGallery';
|
||
|
||
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 navigate = useNavigate();
|
||
|
||
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) => {
|
||
navigate(`/moderation/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" maxWidth="lg" style={{ paddingTop: '20px' }}>
|
||
<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>
|
||
|
||
{/* Wartende Gruppen */}
|
||
<section className="moderation-section">
|
||
<ImageGallery
|
||
items={pendingGroups}
|
||
title={`🔍 Wartende Freigabe (${pendingGroups.length})`}
|
||
onApprove={approveGroup}
|
||
onViewImages={viewGroupImages}
|
||
onDelete={deleteGroup}
|
||
isPending={true}
|
||
mode="moderation"
|
||
emptyMessage="Keine wartenden Gruppen"
|
||
/>
|
||
</section>
|
||
|
||
{/* Freigegebene Gruppen */}
|
||
<section className="moderation-section">
|
||
<ImageGallery
|
||
items={approvedGroups}
|
||
title={`✅ Freigegebene Gruppen (${approvedGroups.length})`}
|
||
onApprove={approveGroup}
|
||
onViewImages={viewGroupImages}
|
||
onDelete={deleteGroup}
|
||
isPending={false}
|
||
mode="moderation"
|
||
emptyMessage="Keine freigegebenen Gruppen"
|
||
/>
|
||
</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 ModerationGroupsPage;
|