removed old files
This commit is contained in:
parent
c7f75a4bd8
commit
d29aaa05cd
20
TODO.md
Normal file
20
TODO.md
Normal 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).
|
||||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; } }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,16 +132,7 @@ 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} />
|
||||
<ImagePreviewGallery images={selectedImages} onRemoveImage={handleRemoveImage} />
|
||||
|
||||
{selectedImages.length > 0 && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue
Block a user