attracion fertig
This commit is contained in:
18
src/App.jsx
18
src/App.jsx
@@ -10,6 +10,8 @@ import PostsPage from './pages/PostsPage';
|
|||||||
import CreateNewsPage from './pages/CreateNewsPage';
|
import CreateNewsPage from './pages/CreateNewsPage';
|
||||||
import OrganizationPage from './pages/OrganizationPage';
|
import OrganizationPage from './pages/OrganizationPage';
|
||||||
import PushPage from './pages/PushPage';
|
import PushPage from './pages/PushPage';
|
||||||
|
import EditAttractionPage from './pages/EditAttractionPage';
|
||||||
|
import AttractionPage from './pages/AttractionPage';
|
||||||
import HomePage from './pages/HomePage';
|
import HomePage from './pages/HomePage';
|
||||||
import EditOrganizationPage from "./pages/EditOrganizationPage.jsx";
|
import EditOrganizationPage from "./pages/EditOrganizationPage.jsx";
|
||||||
|
|
||||||
@@ -63,6 +65,14 @@ export default function App() {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="attractions"
|
||||||
|
element={
|
||||||
|
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}>
|
||||||
|
<AttractionPage />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="posts"
|
path="posts"
|
||||||
element={
|
element={
|
||||||
@@ -71,6 +81,14 @@ export default function App() {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="attractions/:id/edit"
|
||||||
|
element={
|
||||||
|
<PrivateRoute allowedRoles={['ADMIN', 'REPORTER']}>
|
||||||
|
<EditAttractionPage/>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="organizations/:id/edit"
|
path="organizations/:id/edit"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ const axiosInstance = axios.create({
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
baseURL: 'https://api.testsite.deinedorfapp.de/api',
|
//baseURL: 'https://api.testsite.deinedorfapp.de/api',
|
||||||
//baseURL: 'http://localhost:5173/api',
|
baseURL: 'http://localhost:5173/api',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
|
|||||||
import CorporateFareIcon from '@mui/icons-material/CorporateFare';
|
import CorporateFareIcon from '@mui/icons-material/CorporateFare';
|
||||||
import LogoutIcon from '@mui/icons-material/Logout';
|
import LogoutIcon from '@mui/icons-material/Logout';
|
||||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||||
|
import LocationOnIcon from '@mui/icons-material/LocationOn';
|
||||||
import HomeIcon from '@mui/icons-material/Home';
|
import HomeIcon from '@mui/icons-material/Home';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
@@ -17,9 +18,10 @@ const DRAWER_WIDTH = 240;
|
|||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: 'Home', path: '/', icon: <HomeIcon />, roles: null },
|
{ label: 'Home', path: '/', icon: <HomeIcon />, roles: null },
|
||||||
{ label: 'Users', path: '/users', icon: <PeopleIcon />, roles: ['ADMIN'] },
|
{ label: 'Users', path: '/users', icon: <PeopleIcon />, roles: ['ADMIN'] },
|
||||||
{ label: 'Posts', path: '/posts', icon: <ArticleIcon />, roles: ['ADMIN', 'REPORTER'] },
|
{ label: 'Neuigkeiten', path: '/posts', icon: <ArticleIcon />, roles: ['ADMIN', 'REPORTER'] },
|
||||||
{ label: 'Push', path: '/push', icon: <NotificationsActiveIcon />, roles: ['ADMIN'] },
|
{ label: 'Push', path: '/push', icon: <NotificationsActiveIcon />, roles: ['ADMIN'] },
|
||||||
{ label: 'Organization', path: '/organizations', icon: <CorporateFareIcon />, roles: ['ADMIN', 'SITE_OWNER'] },
|
{ label: 'Organization', path: '/organizations', icon: <CorporateFareIcon />, roles: ['ADMIN', 'SITE_OWNER'] },
|
||||||
|
{ label: 'Sehenswürdigkeiten', path: '/attractions', icon: <LocationOnIcon />, roles: ['ADMIN', 'REPORTER'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
function getActiveLabel(pathname) {
|
function getActiveLabel(pathname) {
|
||||||
|
|||||||
190
src/pages/AttractionPage.jsx
Normal file
190
src/pages/AttractionPage.jsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box, Typography, Alert, IconButton, Tooltip,
|
||||||
|
Dialog, DialogTitle, DialogContent, DialogActions,
|
||||||
|
Button, Stack, TextField, CircularProgress
|
||||||
|
} from '@mui/material';
|
||||||
|
import { DataGrid } from '@mui/x-data-grid';
|
||||||
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import { Chip } from '@mui/material';
|
||||||
|
import axiosInstance from '../api/axiosInstance';
|
||||||
|
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
|
export default function AttractionPage() {
|
||||||
|
const [rows, setRows] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [creatingError, setCreatingError] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [createForm, setCreateForm] = useState({
|
||||||
|
name: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadAttractions = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await axiosInstance.get('/attraction');
|
||||||
|
setRows(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message ?? 'Fehler beim laden der Attractions');
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAttractions();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFormChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setCreateForm((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
setCreating(true);
|
||||||
|
setCreatingError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jsonPayload = {
|
||||||
|
name: createForm.name};
|
||||||
|
|
||||||
|
await axiosInstance.post('/attraction', jsonPayload);
|
||||||
|
|
||||||
|
await loadAttractions(); // Liste neu laden
|
||||||
|
handleCreateClose();
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setCreatingError(err.response?.data?.message ?? 'Erstellen fehlgeschlagen');
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
setCreateForm({ name: '', email: '' });
|
||||||
|
setCreatingError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
if (!window.confirm('Sehenswürdigkeit wirklich löschen?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axiosInstance.delete(`/attraction/${id}`);
|
||||||
|
setRows((prev) => prev.filter((r) => r.id !== id));
|
||||||
|
} catch(err){
|
||||||
|
alert(err.response?.data?.message ?? 'Löschen fehlgeschlagen');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (id) => {
|
||||||
|
navigate(`/attractions/${id}/edit`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ field: 'id', headerName: 'ID', width: 70 },
|
||||||
|
{ field: 'title', headerName: 'Name', flex: 1.5 },
|
||||||
|
{
|
||||||
|
field: 'active',
|
||||||
|
headerName: 'Status',
|
||||||
|
width: 150,
|
||||||
|
renderCell: ({ value }) => (
|
||||||
|
<Chip
|
||||||
|
label={value ? 'Aktiv' : 'Inaktiv'}
|
||||||
|
color={value ? 'success' : 'default'}
|
||||||
|
size="small"
|
||||||
|
variant={value ? 'filled' : 'outlined'}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
headerName: '',
|
||||||
|
width: 120,
|
||||||
|
sortable: false,
|
||||||
|
renderCell: ({ row }) => (
|
||||||
|
<Tooltip title="">
|
||||||
|
<IconButton title="löschen" size="small" color="error" onClick={() => handleDelete(row.id)}>
|
||||||
|
<DeleteOutlineIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton title="bearbeiten" size="small" color="grey" onClick={() => handleEdit(row.id)}>
|
||||||
|
<EditOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* Header */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
|
||||||
|
<Typography variant="h5" fontWeight={600}>Sehenswürdigkeiten</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
Sehenswürdigkeit erstellen
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||||
|
|
||||||
|
{/* Tabelle */}
|
||||||
|
<Box sx={{ bgcolor: 'white', borderRadius: 2, border: '1px solid', borderColor: 'divider' }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
|
loading={loading}
|
||||||
|
autoHeight
|
||||||
|
pageSizeOptions={[25, 50, 100]}
|
||||||
|
initialState={{ pagination: { paginationModel: { pageSize: 25 } } }}
|
||||||
|
disableRowSelectionOnClick
|
||||||
|
sx={{ border: 'none' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Create Dialog */}
|
||||||
|
<Dialog open={open} onClose={handleCreateClose} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle sx={{ fontWeight: 600}}>Sehenswürdigkeit erstellen</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<Stack spacing={2.5} sx={{ mt: 1 }}>
|
||||||
|
{creatingError && <Alert severity="error">{creatingError}</Alert>}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Name"
|
||||||
|
name="name"
|
||||||
|
value={createForm.name}
|
||||||
|
onChange={handleFormChange}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions sx={{ px: 3, pb: 2.5 }}>
|
||||||
|
<Button onClick={handleCreateClose} disabled={creating}>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleCreate}
|
||||||
|
disabled={creating}
|
||||||
|
startIcon={creating ? <CircularProgress size={16} color="inherit" /> : null}
|
||||||
|
>
|
||||||
|
Erstellen
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
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