attracion fertig
This commit is contained in:
354
src/pages/EditAttractionPage.jsx
Normal file
354
src/pages/EditAttractionPage.jsx
Normal file
@@ -0,0 +1,354 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Box, Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Checkbox, Chip, CircularProgress, Divider,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import StarIcon from "@mui/icons-material/Star";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternateOutlined";
|
||||
import axiosInstance from "../api/axiosInstance.js";
|
||||
|
||||
const EMPTY_FORM = {
|
||||
description: '',
|
||||
address: '',
|
||||
active: false,
|
||||
};
|
||||
|
||||
export default function EditAttractionPage() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
|
||||
const [form, setForm] = useState(EMPTY_FORM);
|
||||
const [title, setTitle] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Titelbild
|
||||
const [titleImageUrl, setTitleImageUrl] = useState(null);
|
||||
const [titleImageFile, setTitleImageFile] = useState(null);
|
||||
const [titleImagePreview, setTitleImagePreview] = useState(null);
|
||||
|
||||
// Weitere Bilder
|
||||
const [otherImageUrls, setOtherImageUrls] = useState([]);
|
||||
const [otherFiles, setOtherFiles] = useState([]);
|
||||
const [otherPreviews, setOtherPreviews] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await axiosInstance.get('/attraction/' + id);
|
||||
const data = response.data;
|
||||
|
||||
setTitle(data.title || '');
|
||||
setForm({
|
||||
description: data.description || '',
|
||||
address: data.address || '',
|
||||
active: data.active || false,
|
||||
});
|
||||
|
||||
const imgs = data.pictures || [];
|
||||
setTitleImageUrl(imgs[0] || null);
|
||||
setOtherImageUrls(imgs.slice(1));
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message ?? 'Fehler beim Laden der Sehenswürdigkeit');
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [id]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: type === 'checkbox' ? checked : value,
|
||||
}));
|
||||
};
|
||||
|
||||
// Titelbild-Handler
|
||||
const handleTitleImageChange = (e) => {
|
||||
const f = e.target.files[0];
|
||||
if (!f) return;
|
||||
if (titleImagePreview) URL.revokeObjectURL(titleImagePreview);
|
||||
setTitleImageFile(f);
|
||||
setTitleImagePreview(URL.createObjectURL(f));
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const handleRemoveTitleImage = () => {
|
||||
if (titleImagePreview) URL.revokeObjectURL(titleImagePreview);
|
||||
setTitleImageUrl(null);
|
||||
setTitleImageFile(null);
|
||||
setTitleImagePreview(null);
|
||||
};
|
||||
|
||||
// Weitere Bilder-Handler
|
||||
const handleOtherFilesChange = (e) => {
|
||||
const selected = Array.from(e.target.files);
|
||||
setOtherFiles((prev) => [...prev, ...selected]);
|
||||
const newPreviews = selected.map((f) => URL.createObjectURL(f));
|
||||
setOtherPreviews((prev) => [...prev, ...newPreviews]);
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const handleRemoveOtherFile = (index) => {
|
||||
URL.revokeObjectURL(otherPreviews[index]);
|
||||
setOtherFiles((prev) => prev.filter((_, i) => i !== index));
|
||||
setOtherPreviews((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleRemoveOtherUrl = (index) => {
|
||||
setOtherImageUrls((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setSaving(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append(
|
||||
'data',
|
||||
new Blob([JSON.stringify({
|
||||
...form,
|
||||
existingTitleImage: titleImageUrl || null,
|
||||
existingOtherImages: otherImageUrls,
|
||||
})], { type: 'application/json' })
|
||||
);
|
||||
|
||||
if (titleImageFile) formData.append('images', titleImageFile);
|
||||
otherFiles.forEach((f) => formData.append('images', f));
|
||||
|
||||
await axiosInstance.put(`/attraction/${id}`, formData, {
|
||||
headers: { 'Content-Type': undefined },
|
||||
});
|
||||
navigate('/attractions');
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message ?? 'Fehler beim Speichern');
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hasTitleImage = titleImageUrl || titleImageFile;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<IconButton onClick={() => navigate('/attractions')} size="small">
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h5" fontWeight={600}>Sehenswürdigkeit bearbeiten</Typography>
|
||||
</Box>
|
||||
|
||||
<Card sx={{ maxWidth: 680 }}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box component="form" onSubmit={handleSubmit}>
|
||||
<Stack spacing={3}>
|
||||
{error && <Alert severity="error">{error}</Alert>}
|
||||
|
||||
<TextField
|
||||
label="Titel"
|
||||
value={title}
|
||||
fullWidth
|
||||
InputProps={{ readOnly: true }}
|
||||
helperText="Der Titel kann nicht geändert werden."
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Beschreibung"
|
||||
name="description"
|
||||
value={form.description}
|
||||
onChange={handleChange}
|
||||
required
|
||||
fullWidth
|
||||
autoFocus
|
||||
multiline
|
||||
rows={6}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Adresse"
|
||||
name="address"
|
||||
value={form.address}
|
||||
onChange={handleChange}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="active"
|
||||
checked={form.active}
|
||||
onChange={(e) =>
|
||||
setForm((prev) => ({ ...prev, active: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Aktiv"
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* ── Titelbild ── */}
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
||||
<Chip
|
||||
icon={<StarIcon sx={{ fontSize: '13px !important' }} />}
|
||||
label="Titelbild"
|
||||
size="small"
|
||||
color="primary"
|
||||
sx={{ height: 22, fontSize: 11 }}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Wird als Vorschaubild der Sehenswürdigkeit verwendet
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{hasTitleImage ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex', alignItems: 'center', gap: 1.5,
|
||||
p: 1, borderRadius: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.main',
|
||||
bgcolor: 'primary.50',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={titleImagePreview || titleImageUrl}
|
||||
alt="Titelbild"
|
||||
sx={{ width: 72, height: 72, objectFit: 'cover', borderRadius: 1, flexShrink: 0 }}
|
||||
/>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{titleImageFile ? titleImageFile.name : titleImageUrl}
|
||||
</Typography>
|
||||
<Button
|
||||
component="label"
|
||||
size="small"
|
||||
sx={{ mt: 0.5, px: 0, minWidth: 0, fontSize: 12 }}
|
||||
>
|
||||
Bild ersetzen
|
||||
<input type="file" hidden accept="image/*" onChange={handleTitleImageChange} />
|
||||
</Button>
|
||||
</Box>
|
||||
<IconButton size="small" color="error" onClick={handleRemoveTitleImage}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
) : (
|
||||
<Button
|
||||
component="label"
|
||||
startIcon={<AddPhotoAlternateOutlinedIcon />}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
Titelbild hochladen
|
||||
<input type="file" hidden accept="image/*" onChange={handleTitleImageChange} />
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* ── Weitere Bilder ── */}
|
||||
<Box>
|
||||
<Typography variant="subtitle2" fontWeight={500} mb={1.5}>
|
||||
Weitere Bilder
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={1.5}>
|
||||
{otherImageUrls.map((src, i) => (
|
||||
<Box
|
||||
key={`existing-${i}`}
|
||||
sx={{
|
||||
display: 'flex', alignItems: 'center', gap: 1.5,
|
||||
p: 1, borderRadius: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={src}
|
||||
alt={`Bild ${i + 1}`}
|
||||
sx={{ width: 64, height: 64, objectFit: 'cover', borderRadius: 1, flexShrink: 0 }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{src}
|
||||
</Typography>
|
||||
<IconButton size="small" color="error" onClick={() => handleRemoveOtherUrl(i)}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{otherPreviews.map((src, i) => (
|
||||
<Box
|
||||
key={`new-${i}`}
|
||||
sx={{
|
||||
display: 'flex', alignItems: 'center', gap: 1.5,
|
||||
p: 1, borderRadius: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={src}
|
||||
alt={`Neues Bild ${i + 1}`}
|
||||
sx={{ width: 64, height: 64, objectFit: 'cover', borderRadius: 1, flexShrink: 0 }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{otherFiles[i]?.name}
|
||||
</Typography>
|
||||
<IconButton size="small" color="error" onClick={() => handleRemoveOtherFile(i)}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Button
|
||||
component="label"
|
||||
startIcon={<AddPhotoAlternateOutlinedIcon />}
|
||||
size="small"
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
Bild hinzufügen
|
||||
<input type="file" hidden accept="image/*" multiple onChange={handleOtherFilesChange} />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end', pt: 1 }}>
|
||||
<Button onClick={() => navigate('/attractions')} disabled={saving}>Abbrechen</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={saving}
|
||||
startIcon={saving ? <CircularProgress size={16} color="inherit" /> : null}
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user