speichern ohne funktion

This commit is contained in:
Matthias Lotz 2025-10-19 19:56:13 +02:00
parent 566eb3aed6
commit 8d2d27c71d
3 changed files with 120 additions and 38 deletions

View File

@ -27,7 +27,7 @@ services:
networks:
- image-uploader-internal
volumes:
- app-data:/usr/src/app/data
- app-data:/usr/src/app/src/data
volumes:
app-data:

View File

@ -103,7 +103,11 @@ function ImagePreviewGallery({ images, onRemoveImage, onReorderImages }) {
<CardMedia
component="img"
className={classes.imageMedia}
image={URL.createObjectURL(image)}
image={
// If image is a File-like object, create an object URL.
// If it's a remote image descriptor, use its remoteUrl or url field.
image && image.remoteUrl ? image.remoteUrl : image && image.url ? image.url : URL.createObjectURL(image)
}
alt={`Vorschau ${index + 1}`}
/>
@ -128,8 +132,8 @@ function ImagePreviewGallery({ images, onRemoveImage, onReorderImages }) {
{index + 1}
</div>
<div className={classes.fileName} title={`${image.name} (${formatFileSize(image.size)})`}>
{image.name} {formatFileSize(image.size)}
<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>
</Card>
</Grid>

View File

@ -1,8 +1,57 @@
import React, { useState, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import './Css/ModerationPage.css';
import { makeStyles } from '@material-ui/core/styles';
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 '../ComponentUtils/Css/Background.css';
const useStyles = makeStyles({
container: {
paddingTop: '20px',
paddingBottom: '40px',
minHeight: '80vh'
},
card: {
borderRadius: '12px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
padding: '20px',
marginBottom: '20px'
},
headerText: {
fontFamily: 'roboto',
fontWeight: '400',
fontSize: '28px',
textAlign: 'center',
marginBottom: '10px',
color: '#333333'
},
subheaderText: {
fontFamily: 'roboto',
fontWeight: '300',
fontSize: '16px',
color: '#666666',
textAlign: 'center',
marginBottom: '30px'
},
actionButtons: {
display: 'flex',
gap: '15px',
justifyContent: 'center',
marginTop: '20px',
flexWrap: 'wrap'
}
});
const GroupImagesPage = () => {
const classes = useStyles();
const { groupId } = useParams();
const history = useHistory();
const [group, setGroup] = useState(null);
@ -10,6 +59,10 @@ const GroupImagesPage = () => {
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
@ -22,6 +75,24 @@ const GroupImagesPage = () => {
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 {
@ -31,6 +102,7 @@ const GroupImagesPage = () => {
const handleChange = (field, value) => {
setGroup({ ...group, [field]: value });
setMetadata(prev => ({ ...prev, [field]: value }));
};
const handleSave = async () => {
@ -55,11 +127,11 @@ const GroupImagesPage = () => {
throw new Error(body.message || 'Speichern fehlgeschlagen');
}
alert('Gruppe erfolgreich aktualisiert');
Swal.fire({ icon: 'success', title: 'Gruppe erfolgreich aktualisiert', timer: 1500, showConfirmButton: false });
history.push('/moderation');
} catch (e) {
console.error(e);
alert('Fehler beim Speichern: ' + e.message);
Swal.fire({ icon: 'error', title: 'Fehler beim Speichern', text: e.message });
} finally {
setSaving(false);
}
@ -71,53 +143,59 @@ const GroupImagesPage = () => {
const res = await fetch(`/groups/${groupId}/images/${imageId}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Löschen fehlgeschlagen');
// Aktualisiere lokale Ansicht
setGroup({ ...group, images: group.images.filter(img => img.id !== imageId), imageCount: group.imageCount - 1 });
alert('Bild gelöscht');
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);
alert('Fehler beim Löschen des Bildes');
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));
};
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="group-images-page">
<h1>Gruppe bearbeiten</h1>
<div className="allContainer">
<Navbar />
<div className="group-edit-form">
<label>Titel</label>
<input value={group.title || ''} onChange={e => handleChange('title', e.target.value)} />
<Container maxWidth="lg" className={classes.container}>
<Card className={classes.card}>
<CardContent>
<Typography className={classes.headerText}>Gruppe bearbeiten</Typography>
<Typography className={classes.subheaderText}>{group.title || ''}</Typography>
<label>Beschreibung</label>
<textarea value={group.description || ''} onChange={e => handleChange('description', e.target.value)} />
<ImagePreviewGallery images={selectedImages} onRemoveImage={handleRemoveImage} />
<label>Jahr</label>
<input value={group.year || ''} onChange={e => handleChange('year', e.target.value)} />
{selectedImages.length > 0 && (
<>
<DescriptionInput metadata={metadata} onMetadataChange={setMetadata} />
<label>Ersteller</label>
<input value={group.name || ''} onChange={e => handleChange('name', e.target.value)} />
<div className={classes.actionButtons}>
<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="edit-actions">
<button className="btn btn-secondary" onClick={() => history.push('/moderation')}> Zurück</button>
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>{saving ? 'Speichern...' : 'Speichern'}</button>
</div>
</div>
</CardContent>
</Card>
</Container>
<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={() => handleDeleteImage(image.id)}>🗑</button>
</div>
</div>
))}
</div>
</div>
<div className="footerContainer"><Footer /></div>
</div>
);
};