Renaming GroupImagePage -> ModerationGroupImagesPage.js
This commit is contained in:
parent
0c0547b4f5
commit
c7f75a4bd8
|
|
@ -6,8 +6,9 @@ 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 ModerationGroupsPage from './Components/Pages/ModerationGroupsPage';
|
||||
import ModerationGroupImagesPage from './Components/Pages/ModerationGroupImagesPage';
|
||||
import PublicGroupImagesPage from './Components/Pages/PublicGroupImagesPage';
|
||||
import FZF from './Components/Pages/404Page.js'
|
||||
|
||||
function App() {
|
||||
|
|
@ -17,9 +18,10 @@ function App() {
|
|||
<Route path="/" exact component={MultiUploadPage} />
|
||||
<Route path="/upload/:image_url" component={UploadedImage} />
|
||||
<Route path="/slideshow" component={SlideshowPage} />
|
||||
<Route path="/groups/:groupId" component={GroupImagesPage} />
|
||||
<Route path="/groups/:groupId" component={PublicGroupImagesPage} />
|
||||
<Route path="/groups" component={GroupsOverviewPage} />
|
||||
<Route path="/moderation" component={ModerationPage} />
|
||||
<Route path="/moderation" exact component={ModerationGroupsPage} />
|
||||
<Route path="/moderation/groups/:groupId" component={ModerationGroupImagesPage} />
|
||||
<Route component={FZF} />
|
||||
</Switch>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Grid, Card, CardMedia, IconButton, Typography, Box } from '@material-ui/core';
|
||||
import { Close as CloseIcon, DragIndicator as DragIcon } from '@material-ui/icons';
|
||||
import { Grid, Card, Typography } from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
galleryContainer: {
|
||||
|
|
@ -21,48 +20,24 @@ const useStyles = makeStyles({
|
|||
height: 150,
|
||||
objectFit: 'cover'
|
||||
},
|
||||
removeButton: {
|
||||
position: 'absolute',
|
||||
top: '5px',
|
||||
right: '5px',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#f44336',
|
||||
'&:hover': {
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#d32f2f'
|
||||
}
|
||||
},
|
||||
dragHandle: {
|
||||
position: 'absolute',
|
||||
top: '5px',
|
||||
left: '5px',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
color: '#666666',
|
||||
cursor: 'grab'
|
||||
},
|
||||
imageOrder: {
|
||||
position: 'absolute',
|
||||
bottom: '5px',
|
||||
left: '5px',
|
||||
top: '10px',
|
||||
left: '10px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
color: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '2px 8px',
|
||||
padding: '4px 8px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold'
|
||||
fontWeight: 'bold',
|
||||
zIndex: 2
|
||||
},
|
||||
fileName: {
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
left: '0',
|
||||
right: '0',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
color: 'white',
|
||||
padding: '5px',
|
||||
fontSize: '11px',
|
||||
textOverflow: 'ellipsis',
|
||||
fileMeta: {
|
||||
fontSize: '12px',
|
||||
color: '#6c757d',
|
||||
marginTop: '6px',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap'
|
||||
textOverflow: 'ellipsis'
|
||||
},
|
||||
galleryHeader: {
|
||||
marginBottom: '15px',
|
||||
|
|
@ -103,34 +78,30 @@ function ImagePreviewGallery({ images, onRemoveImage, onReorderImages }) {
|
|||
<div className="group-preview">
|
||||
<img
|
||||
className="preview-image"
|
||||
src={image && image.remoteUrl ? image.remoteUrl : image && image.url ? image.url : URL.createObjectURL(image)}
|
||||
src={image && image.remoteUrl ? image.remoteUrl : image && image.url ? image.url : (image && image.filePath ? image.filePath : '')}
|
||||
alt={`Vorschau ${index + 1}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
className={classes.removeButton}
|
||||
size="small"
|
||||
onClick={() => handleRemoveImage(index)}
|
||||
title="Bild entfernen"
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
className={classes.dragHandle}
|
||||
size="small"
|
||||
title="Zum Sortieren ziehen"
|
||||
>
|
||||
<DragIcon fontSize="small" />
|
||||
</IconButton>
|
||||
|
||||
<div className={classes.imageOrder}>
|
||||
<div style={{ position: 'absolute', top: 10, left: 10 }} className={classes.imageOrder}>
|
||||
{index + 1}
|
||||
</div>
|
||||
|
||||
<div className={classes.fileName} title={`${image.name || image.originalName || ''} ${image.size ? ' • ' + formatFileSize(image.size) : ''}`}>
|
||||
{image.name || image.originalName || 'Bild'}{image.size ? ' • ' + formatFileSize(image.size) : ''}
|
||||
<div className="group-info">
|
||||
<h3 style={{ fontSize: '0.95rem', margin: 0 }}>{image.originalName || image.name || 'Bild'}</h3>
|
||||
<div className={classes.fileMeta}>
|
||||
{image.remoteUrl && image.remoteUrl.includes('/download/') ? (
|
||||
<div>Server-Datei: {image.remoteUrl.split('/').pop()}</div>
|
||||
) : image.filePath ? (
|
||||
<div>Server-Datei: {image.filePath.split('/').pop()}</div>
|
||||
) : null}
|
||||
{image.captureDate ? <div>Aufnahmedatum: {new Date(image.captureDate).toLocaleDateString('de-DE')}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group-actions">
|
||||
<button className="btn btn-danger" onClick={() => handleRemoveImage(index)}>🗑️ Löschen</button>
|
||||
<button className="btn btn-secondary btn-sm" disabled>Sort</button>
|
||||
</div>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -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 <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>;
|
||||
|
|
@ -135,32 +166,35 @@ const GroupImagesPage = () => {
|
|||
<div className="allContainer">
|
||||
<Navbar />
|
||||
|
||||
<Container maxWidth="lg" className="page-container">
|
||||
<Card className="group-card">
|
||||
<CardContent>
|
||||
<Typography className="header-text">Gruppe bearbeiten</Typography>
|
||||
<Typography className="subheader-text">{group.title || ''}</Typography>
|
||||
<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} />
|
||||
<ImagePreviewGallery images={selectedImages} onRemoveImage={handleRemoveImage} />
|
||||
|
||||
{selectedImages.length > 0 && (
|
||||
<>
|
||||
<DescriptionInput metadata={metadata} onMetadataChange={setMetadata} />
|
||||
{selectedImages.length > 0 && (
|
||||
<>
|
||||
<DescriptionInput metadata={metadata} onMetadataChange={setMetadata} />
|
||||
|
||||
<div className="action-buttons">
|
||||
<Button variant="outlined" onClick={() => history.push('/moderation')}>↩ Zurück</Button>
|
||||
<Button color="primary" variant="contained" onClick={handleSave} disabled={saving}>{saving ? 'Speichern...' : 'Speichern'}</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Container>
|
||||
|
||||
<div className="footerContainer"><Footer /></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default GroupImagesPage;
|
||||
|
|
|
|||
194
frontend/src/Components/Pages/ModerationGroupImagesPage.js
Normal file
194
frontend/src/Components/Pages/ModerationGroupImagesPage.js
Normal file
|
|
@ -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 <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 ModerationGroupImagesPage;
|
||||
301
frontend/src/Components/Pages/ModerationGroupsPage.js
Normal file
301
frontend/src/Components/Pages/ModerationGroupsPage.js
Normal file
|
|
@ -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 <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 ModerationGroupsPage;
|
||||
62
frontend/src/Components/Pages/PublicGroupImagesPage.js
Normal file
62
frontend/src/Components/Pages/PublicGroupImagesPage.js
Normal file
|
|
@ -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 <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">
|
||||
<GroupCard group={group} showActions={false} isPending={!group.approved} />
|
||||
|
||||
<div className="public-gallery">
|
||||
{group.images && group.images.length > 0 ? (
|
||||
<ImagePreviewGallery images={group.images.map(img => ({ remoteUrl: `/download/${img.fileName}`, originalName: img.originalName || img.fileName, id: img.id }))} showRemove={false} />
|
||||
) : (
|
||||
<p>Keine Bilder in dieser Gruppe.</p>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
<div className="footerContainer"><Footer /></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicGroupImagesPage;
|
||||
Loading…
Reference in New Issue
Block a user