stabile version
This commit is contained in:
@@ -8,8 +8,8 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass https://restapi.testsite.deinedorfapp.de;
|
proxy_pass http://backend:5173;
|
||||||
proxy_set_header Host restapi.testsite.deinedorfapp.de;
|
proxy_set_header Host backend;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ 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 HomePage from './pages/HomePage';
|
||||||
import EditOrganizationPage from "./pages/EditOrganizationPage.jsx";
|
import EditOrganizationPage from "./pages/EditOrganizationPage.jsx";
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
@@ -53,7 +54,7 @@ export default function App() {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route index element={<Navigate to="/users" replace />} />
|
<Route index element={<HomePage />} />
|
||||||
<Route
|
<Route
|
||||||
path="users"
|
path="users"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
/*
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: 'https://api.testsite.deinedorfapp.de/api',
|
||||||
|
//baseURL: 'http://localhost:5173/api',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
|
||||||
axiosInstance.interceptors.request.use((config) => {
|
axiosInstance.interceptors.request.use((config) => {
|
||||||
const token = localStorage.getItem('jwt_token');
|
const token = localStorage.getItem('jwt_token');
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
|
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Box, Drawer, List, ListItem, ListItemButton, ListItemIcon, ListItemText,
|
Box, Drawer, List, ListItem, ListItemButton, ListItemIcon, ListItemText,
|
||||||
@@ -10,91 +9,109 @@ 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 HomeIcon from '@mui/icons-material/Home';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
const DRAWER_WIDTH = 240;
|
const DRAWER_WIDTH = 240;
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: 'Users', path: '/users', icon: <PeopleIcon />, roles: ['ADMIN'] },
|
{ label: 'Home', path: '/', icon: <HomeIcon />, roles: null },
|
||||||
{ label: 'Posts', path: '/posts', icon: <ArticleIcon />, roles: ['ADMIN', 'REPORTER'] },
|
{ label: 'Users', path: '/users', icon: <PeopleIcon />, roles: ['ADMIN'] },
|
||||||
{ label: 'Push', path: '/push', icon: <NotificationsActiveIcon />, roles: ['ADMIN'] },
|
{ label: 'Posts', path: '/posts', icon: <ArticleIcon />, roles: ['ADMIN', 'REPORTER'] },
|
||||||
{ label: 'Organization', path: '/organizations', icon: <CorporateFareIcon />, roles: ['ADMIN', 'REPORTER'] },
|
{ label: 'Push', path: '/push', icon: <NotificationsActiveIcon />, roles: ['ADMIN'] },
|
||||||
|
{ label: 'Organization', path: '/organizations', icon: <CorporateFareIcon />, roles: ['ADMIN', 'SITE_OWNER'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function getActiveLabel(pathname) {
|
||||||
|
// Exakter Match für "/" zuerst prüfen, dann startsWith für den Rest
|
||||||
|
const exact = navItems.find((i) => i.path === pathname);
|
||||||
|
if (exact) return exact.label;
|
||||||
|
const partial = navItems.find((i) => i.path !== '/' && pathname.startsWith(i.path));
|
||||||
|
return partial?.label ?? 'Admin';
|
||||||
|
}
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { logout } = useAuth();
|
const { logout, role } = useAuth();
|
||||||
|
|
||||||
|
const visibleNavItems = navItems.filter(
|
||||||
|
(item) => !item.roles || item.roles.includes(role)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<Drawer
|
<Drawer
|
||||||
variant="permanent"
|
variant="permanent"
|
||||||
sx={{
|
sx={{
|
||||||
width: DRAWER_WIDTH,
|
width: DRAWER_WIDTH,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
'& .MuiDrawer-paper': {
|
'& .MuiDrawer-paper': {
|
||||||
width: DRAWER_WIDTH,
|
width: DRAWER_WIDTH,
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
borderRight: '1px solid',
|
borderRight: '1px solid',
|
||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Toolbar sx={{ px: 2 }}>
|
<Toolbar sx={{ px: 2 }}>
|
||||||
<DashboardIcon sx={{ mr: 1, color: 'primary.main' }} />
|
<DashboardIcon sx={{ mr: 1, color: 'primary.main' }} />
|
||||||
<Typography variant="h6" fontWeight={600} color="primary.main">
|
<Typography variant="h6" fontWeight={600} color="primary.main">
|
||||||
Admin Panel
|
Admin Panel
|
||||||
</Typography>
|
</Typography>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<Divider />
|
<Divider />
|
||||||
<List sx={{ px: 1, pt: 1 }}>
|
<List sx={{ px: 1, pt: 1 }}>
|
||||||
{navItems.map((item) => (
|
{visibleNavItems.map((item) => (
|
||||||
<ListItem key={item.path} disablePadding sx={{ mb: 0.5 }}>
|
<ListItem key={item.path} disablePadding sx={{ mb: 0.5 }}>
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
selected={location.pathname.startsWith(item.path)}
|
selected={
|
||||||
onClick={() => navigate(item.path)}
|
item.path === '/'
|
||||||
sx={{ borderRadius: 2 }}
|
? location.pathname === '/'
|
||||||
>
|
: location.pathname.startsWith(item.path)
|
||||||
<ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>
|
}
|
||||||
<ListItemText primary={item.label} />
|
onClick={() => navigate(item.path)}
|
||||||
|
sx={{ borderRadius: 2 }}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>
|
||||||
|
<ListItemText primary={item.label} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
<Divider />
|
||||||
|
<List sx={{ px: 1, py: 1 }}>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemButton onClick={logout} sx={{ borderRadius: 2 }}>
|
||||||
|
<ListItemIcon sx={{ minWidth: 36 }}><LogoutIcon /></ListItemIcon>
|
||||||
|
<ListItemText primary="Logout" />
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
</List>
|
||||||
</List>
|
</Drawer>
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
|
||||||
<Divider />
|
|
||||||
<List sx={{ px: 1, py: 1 }}>
|
|
||||||
<ListItem disablePadding>
|
|
||||||
<ListItemButton onClick={logout} sx={{ borderRadius: 2 }}>
|
|
||||||
<ListItemIcon sx={{ minWidth: 36 }}><LogoutIcon /></ListItemIcon>
|
|
||||||
<ListItemText primary="Logout" />
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
</Drawer>
|
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<Box component="main" sx={{ flexGrow: 1, minHeight: '100vh', bgcolor: 'grey.50' }}>
|
<Box component="main" sx={{ flexGrow: 1, minHeight: '100vh', bgcolor: 'grey.50' }}>
|
||||||
<AppBar
|
<AppBar
|
||||||
position="static"
|
position="static"
|
||||||
elevation={0}
|
elevation={0}
|
||||||
sx={{ bgcolor: 'white', borderBottom: '1px solid', borderColor: 'divider' }}
|
sx={{ bgcolor: 'white', borderBottom: '1px solid', borderColor: 'divider' }}
|
||||||
>
|
>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<Typography variant="h6" color="text.primary" sx={{ flexGrow: 1 }}>
|
<Typography variant="h6" color="text.primary" sx={{ flexGrow: 1 }}>
|
||||||
{navItems.find((i) => location.pathname.startsWith(i.path))?.label ?? 'Admin'}
|
{getActiveLabel(location.pathname)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Tooltip title="Account">
|
<Tooltip title="Account">
|
||||||
<Avatar sx={{ width: 32, height: 32, bgcolor: 'primary.main', fontSize: 14 }}>A</Avatar>
|
<Avatar sx={{ width: 32, height: 32, bgcolor: 'primary.main', fontSize: 14 }}>A</Avatar>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -30,28 +30,35 @@ const EMPTY_FORM = {
|
|||||||
active: false,
|
active: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function EditOrganizationPage() {
|
export default function EditOrganizationPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [form, setForm] = useState(EMPTY_FORM);
|
const [form, setForm] = useState(EMPTY_FORM);
|
||||||
const [files, setFiles] = useState([]);
|
const [name, setName] = useState('');
|
||||||
const [previews, setPreviews] = useState([]);
|
|
||||||
const [existingImages, setExistingImages] = useState([]);
|
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [data, setData] = 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(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get('/organizations/' + id);
|
const response = await axiosInstance.get('/organization/' + id);
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
setData(data);
|
setName(data.name || '');
|
||||||
|
|
||||||
setForm({
|
setForm({
|
||||||
description: data.description || '',
|
description: data.description || '',
|
||||||
phone: data.phone || '',
|
phone: data.number || '',
|
||||||
email: data.email || '',
|
email: data.email || '',
|
||||||
address: data.address || '',
|
address: data.address || '',
|
||||||
website: data.website || '',
|
website: data.website || '',
|
||||||
@@ -59,7 +66,9 @@ export default function EditOrganizationPage() {
|
|||||||
active: data.active || false,
|
active: data.active || false,
|
||||||
});
|
});
|
||||||
|
|
||||||
setExistingImages(data.images || []);
|
const imgs = data.pictures || [];
|
||||||
|
setTitleImageUrl(imgs[0] || null);
|
||||||
|
setOtherImageUrls(imgs.slice(1));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.response?.data || 'Fehler beim Laden');
|
setError(err.response?.data || 'Fehler beim Laden');
|
||||||
}
|
}
|
||||||
@@ -75,31 +84,74 @@ export default function EditOrganizationPage() {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveExisting = (index) => {
|
// Titelbild-Handler
|
||||||
setExistingImages((prev) => prev.filter((_, i) => i !== index));
|
const handleTitleImageChange = (e) => {
|
||||||
};
|
const f = e.target.files[0];
|
||||||
|
if (!f) return;
|
||||||
const handleSubmit = (e) => {
|
if (titleImagePreview) URL.revokeObjectURL(titleImagePreview);
|
||||||
/*
|
setTitleImageFile(f);
|
||||||
Drauf achten alte und neue Bilder zu verwenden
|
setTitleImagePreview(URL.createObjectURL(f));
|
||||||
Vielleicht extra Feld für Anzeigebild, anstatt erstes zu nehmen
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileChange = (e) => {
|
|
||||||
const selected = Array.from(e.target.files);
|
|
||||||
setFiles((prev) => [...prev, ...selected]);
|
|
||||||
const newPreviews = selected.map((f) => URL.createObjectURL(f));
|
|
||||||
setPreviews((prev) => [...prev, ...newPreviews]);
|
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveFile = (index) => {
|
const handleRemoveTitleImage = () => {
|
||||||
URL.revokeObjectURL(previews[index]);
|
if (titleImagePreview) URL.revokeObjectURL(titleImagePreview);
|
||||||
setFiles((prev) => prev.filter((_, i) => i !== index));
|
setTitleImageUrl(null);
|
||||||
setPreviews((prev) => prev.filter((_, i) => i !== index));
|
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(`/organization/${id}`, formData, {
|
||||||
|
headers: { 'Content-Type': undefined }
|
||||||
|
});
|
||||||
|
navigate('/organizations');
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data || 'Fehler beim Speichern');
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasTitleImage = titleImageUrl || titleImageFile;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||||
@@ -110,17 +162,18 @@ export default function EditOrganizationPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Card sx={{ maxWidth: 680 }}>
|
<Card sx={{ maxWidth: 680 }}>
|
||||||
<CardContent sx={{ p: 3}}>
|
<CardContent sx={{ p: 3 }}>
|
||||||
<Box component="form" onSubmit={handleSubmit}>
|
<Box component="form" onSubmit={handleSubmit}>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
{error && <Alert severity="error">{error}</Alert>}
|
{error && <Alert severity="error">{error}</Alert>}
|
||||||
|
|
||||||
<TextField label="Beschreibung" name="description" value={form.description} onChange={handleChange} required fullWidth autoFocus multiline rows={6}/>
|
<TextField label="Name" value={name} fullWidth InputProps={{ readOnly: true }} helperText="Der Name kann nicht geändert werden." />
|
||||||
<TextField label="Telefonnummber" name="phone" value={form.phone} onChange={handleChange} required fullWidth/>
|
<TextField label="Beschreibung" name="description" value={form.description} onChange={handleChange} required fullWidth autoFocus multiline rows={6} />
|
||||||
<TextField label="Email Adresse" name="email" value={form.email} onChange={handleChange} required fullWidth/>
|
<TextField label="Telefonnummer" name="phone" value={form.phone} onChange={handleChange} required fullWidth />
|
||||||
<TextField label="Adresse" name="address" value={form.address} onChange={handleChange} required fullWidth/>
|
<TextField label="Email Adresse" name="email" value={form.email} onChange={handleChange} required fullWidth />
|
||||||
<TextField label="Webseite" name="website" value={form.website} onChange={handleChange} required fullWidth/>
|
<TextField label="Adresse" name="address" value={form.address} onChange={handleChange} required fullWidth />
|
||||||
<TextField label="Kontaktperson" name="contactPerson" value={form.contactPerson} onChange={handleChange} required fullWidth/>
|
<TextField label="Webseite" name="website" value={form.website} onChange={handleChange} required fullWidth />
|
||||||
|
<TextField label="Kontaktperson" name="contactPerson" value={form.contactPerson} onChange={handleChange} required fullWidth />
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -136,60 +189,125 @@ export default function EditOrganizationPage() {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{/* Bild-Upload */}
|
{/* ── Titelbild ── */}
|
||||||
<Box>
|
<Box>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
||||||
<Typography variant="subtitle2" fontWeight={500}>Bilder</Typography>
|
<Chip
|
||||||
{files.length > 0 && (
|
icon={<StarIcon sx={{ fontSize: '13px !important' }} />}
|
||||||
<Typography variant="caption" color="text.secondary">
|
label="Titelbild"
|
||||||
Das erste Bild wird als Vorschaubild verwendet
|
size="small"
|
||||||
</Typography>
|
color="primary"
|
||||||
)}
|
sx={{ height: 22, fontSize: 11 }}
|
||||||
{existingImages.map((src, i) => (
|
/>
|
||||||
<Box key={`existing-${i}`}>
|
<Typography variant="caption" color="text.secondary">
|
||||||
<img src={src} width={64} height={64} />
|
Wird als Vorschaubild der Organisation verwendet
|
||||||
<IconButton onClick={() => handleRemoveExisting(i)}>
|
</Typography>
|
||||||
<DeleteOutlineIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack spacing={1.5}>
|
{hasTitleImage ? (
|
||||||
{previews.map((src, i) => (
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 1.5,
|
||||||
|
p: 1, borderRadius: 1,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
bgcolor: 'primary.50',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
key={i}
|
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>
|
||||||
|
{/* Titelbild tauschen ohne zu löschen */}
|
||||||
|
<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}>
|
||||||
|
{/* Bestehende weitere Bilder */}
|
||||||
|
{otherImageUrls.map((src, i) => (
|
||||||
|
<Box
|
||||||
|
key={`existing-${i}`}
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex', alignItems: 'center', gap: 1.5,
|
display: 'flex', alignItems: 'center', gap: 1.5,
|
||||||
p: 1, borderRadius: 1,
|
p: 1, borderRadius: 1,
|
||||||
border: '1px solid',
|
border: '1px solid',
|
||||||
borderColor: i === 0 ? 'primary.main' : 'divider',
|
borderColor: 'divider',
|
||||||
bgcolor: i === 0 ? 'primary.50' : 'transparent',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
component="img"
|
component="img"
|
||||||
src={src}
|
src={src}
|
||||||
alt={`Vorschau ${i + 1}`}
|
alt={`Bild ${i + 1}`}
|
||||||
sx={{ width: 64, height: 64, objectFit: 'cover', borderRadius: 1, flexShrink: 0 }}
|
sx={{ width: 64, height: 64, objectFit: 'cover', borderRadius: 1, flexShrink: 0 }}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
|
{src}
|
||||||
{i === 0 && (
|
</Typography>
|
||||||
<Chip
|
<IconButton size="small" color="error" onClick={() => handleRemoveOtherUrl(i)}>
|
||||||
icon={<StarIcon sx={{ fontSize: '13px !important' }} />}
|
<DeleteOutlineIcon fontSize="small" />
|
||||||
label="Vorschaubild"
|
</IconButton>
|
||||||
size="small"
|
</Box>
|
||||||
color="primary"
|
))}
|
||||||
sx={{ height: 20, fontSize: 11 }}
|
|
||||||
/>
|
{/* Neu hinzugefügte weitere Bilder */}
|
||||||
)}
|
{otherPreviews.map((src, i) => (
|
||||||
</Box>
|
<Box
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
key={`new-${i}`}
|
||||||
{files[i]?.name}
|
sx={{
|
||||||
</Typography>
|
display: 'flex', alignItems: 'center', gap: 1.5,
|
||||||
</Box>
|
p: 1, borderRadius: 1,
|
||||||
<IconButton size="small" color="error" onClick={() => handleRemoveFile(i)}>
|
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" />
|
<DeleteOutlineIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -201,8 +319,8 @@ export default function EditOrganizationPage() {
|
|||||||
size="small"
|
size="small"
|
||||||
sx={{ alignSelf: 'flex-start' }}
|
sx={{ alignSelf: 'flex-start' }}
|
||||||
>
|
>
|
||||||
{files.length === 0 ? 'Vorschaubild hinzufügen' : 'Weiteres Bild hinzufügen'}
|
Bild hinzufügen
|
||||||
<input type="file" hidden accept="image/*" multiple onChange={handleFileChange} />
|
<input type="file" hidden accept="image/*" multiple onChange={handleOtherFilesChange} />
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -212,16 +330,16 @@ export default function EditOrganizationPage() {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
disabled={saving}
|
||||||
startIcon={saving ? <CircularProgress size={16} color="inherit" /> : null}
|
startIcon={saving ? <CircularProgress size={16} color="inherit" /> : null}
|
||||||
>
|
>
|
||||||
Speichern
|
Speichern
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
14
src/pages/HomePage.jsx
Normal file
14
src/pages/HomePage.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Box, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" fontWeight={600} gutterBottom>
|
||||||
|
Willkommen im Admin Panel
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
Wähle links im Menü einen Bereich aus.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ export default function LoginPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await login(email, password);
|
await login(email, password);
|
||||||
navigate('/users');
|
navigate('/');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.response?.data?.message ?? 'Login fehlgeschlagen');
|
setError(err.response?.data?.message ?? 'Login fehlgeschlagen');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -122,11 +122,11 @@ export default function OrganizationPage() {
|
|||||||
width: 120,
|
width: 120,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
renderCell: ({ row }) => (
|
renderCell: ({ row }) => (
|
||||||
<Tooltip title="Löschen">
|
<Tooltip title="">
|
||||||
<IconButton size="small" color="error" onClick={() => handleDelete(row.id)}>
|
<IconButton title="löschen" size="small" color="error" onClick={() => handleDelete(row.id)}>
|
||||||
<DeleteOutlineIcon fontSize="small" />
|
<DeleteOutlineIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton size="small" color="grey" onClick={() => handleEdit(row.id)}>
|
<IconButton title="bearbeiten" size="small" color="grey" onClick={() => handleEdit(row.id)}>
|
||||||
<EditOutlinedIcon fontSize="small" />
|
<EditOutlinedIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export default function PostsPage() {
|
|||||||
{ field: 'author', headerName: 'Autor', width: 130 },
|
{ field: 'author', headerName: 'Autor', width: 130 },
|
||||||
{ field: 'category', headerName: 'Kategorie', width: 130 },
|
{ field: 'category', headerName: 'Kategorie', width: 130 },
|
||||||
{
|
{
|
||||||
field: 'picture',
|
field: 'pictures',
|
||||||
headerName: 'Bilder',
|
headerName: 'Bilder',
|
||||||
width: 80,
|
width: 80,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
@@ -140,11 +140,11 @@ export default function PostsPage() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{Array.isArray(detailNews?.picture) && detailNews.picture.length > 0 && (
|
{Array.isArray(detailNews?.pictures) && detailNews.pictures.length > 0 && (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="caption" color="text.secondary">Bilder</Typography>
|
<Typography variant="caption" color="text.secondary">Bilder</Typography>
|
||||||
<Stack spacing={1} sx={{ mt: 0.5 }}>
|
<Stack spacing={1} sx={{ mt: 0.5 }}>
|
||||||
{detailNews.picture.map((url, i) => (
|
{detailNews.pictures.map((url, i) => (
|
||||||
<Box
|
<Box
|
||||||
key={i}
|
key={i}
|
||||||
component="img"
|
component="img"
|
||||||
|
|||||||
Reference in New Issue
Block a user