Renaming GroupImagePage -> ModerationGroupImagesPage.js

This commit is contained in:
Matthias Lotz 2025-10-20 19:47:06 +02:00
parent 0c0547b4f5
commit c7f75a4bd8
6 changed files with 642 additions and 78 deletions

View File

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

View File

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

View File

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

View 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;

View 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;

View 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;