diff --git a/src/App.jsx b/src/App.jsx
index a36e282..f4280b3 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -7,6 +7,7 @@ import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage';
import UsersPage from './pages/UsersPage';
import PostsPage from './pages/PostsPage';
+import CreateNewsPage from './pages/CreateNewsPage';
import PushPage from './pages/PushPage';
const theme = createTheme({
@@ -50,10 +51,11 @@ export default function App() {
}
>
+ } />
+
}
@@ -61,15 +63,23 @@ export default function App() {
+
}
/>
+
+
+
+ }
+ />
+
}
diff --git a/src/pages/CreateNewsPage.jsx b/src/pages/CreateNewsPage.jsx
new file mode 100644
index 0000000..45e12df
--- /dev/null
+++ b/src/pages/CreateNewsPage.jsx
@@ -0,0 +1,222 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import {
+ Box, Typography, Card, CardContent, TextField, Button,
+ Alert, CircularProgress, Stack, IconButton, Divider,
+ ToggleButton, ToggleButtonGroup, Collapse, Chip,
+} from '@mui/material';
+import ArrowBackIcon from '@mui/icons-material/ArrowBack';
+import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
+import AddPhotoAlternateOutlinedIcon from '@mui/icons-material/AddPhotoAlternateOutlined';
+import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
+import NotificationsOffOutlinedIcon from '@mui/icons-material/NotificationsOffOutlined';
+import StarIcon from '@mui/icons-material/Star';
+import axiosInstance from '../api/axiosInstance';
+
+const EMPTY_FORM = {
+ title: '',
+ description: '',
+ category: '',
+ pushNotification: false,
+ pushMessage: '',
+};
+
+export default function CreateNewsPage() {
+ const navigate = useNavigate();
+ const [form, setForm] = useState(EMPTY_FORM);
+ const [files, setFiles] = useState([]);
+ const [previews, setPreviews] = useState([]);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState('');
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setForm((prev) => ({ ...prev, [name]: value }));
+ };
+
+ 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 = '';
+ };
+
+ const handleRemoveFile = (index) => {
+ URL.revokeObjectURL(previews[index]);
+ setFiles((prev) => prev.filter((_, i) => i !== index));
+ setPreviews((prev) => prev.filter((_, i) => i !== index));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setSaving(true);
+ setError('');
+ try {
+ const formData = new FormData();
+ const jsonPayload = {
+ title: form.title,
+ description: form.description,
+ category: form.category,
+ pushNotification: form.pushNotification,
+ ...(form.pushNotification && form.pushMessage ? { pushMessage: form.pushMessage } : {}),
+ };
+ formData.append('data', new Blob([JSON.stringify(jsonPayload)], { type: 'application/json' }));
+ files.forEach((file) => formData.append('images', file));
+
+ await axiosInstance.post('/api/news', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+ navigate('/posts');
+ } catch (err) {
+ setError(err.response?.data?.message ?? 'Erstellen fehlgeschlagen');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const isValid = form.title && form.description && form.category;
+
+ return (
+
+
+ navigate('/posts')} size="small">
+
+
+ News erstellen
+
+
+
+
+
+
+ {error && {error}}
+
+
+
+
+
+
+
+ {/* Bild-Upload */}
+
+
+ Bilder
+ {files.length > 0 && (
+
+ Das erste Bild wird als Vorschaubild verwendet
+
+ )}
+
+
+
+ {previews.map((src, i) => (
+
+
+
+
+ {i === 0 && (
+ }
+ label="Vorschaubild"
+ size="small"
+ color="primary"
+ sx={{ height: 20, fontSize: 11 }}
+ />
+ )}
+
+
+ {files[i]?.name}
+
+
+ handleRemoveFile(i)}>
+
+
+
+ ))}
+
+ }
+ size="small"
+ sx={{ alignSelf: 'flex-start' }}
+ >
+ {files.length === 0 ? 'Vorschaubild hinzufügen' : 'Weiteres Bild hinzufügen'}
+
+
+
+
+
+
+
+ {/* Push Notification */}
+
+ Push Notification
+
+ {
+ if (val === null) return;
+ setForm((prev) => ({ ...prev, pushNotification: val }));
+ }}
+ size="small"
+ >
+
+
+ Aus
+
+
+
+ An
+
+
+
+
+
+
+
+
+
+
+
+ : null}
+ >
+ Veröffentlichen
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/PostsPage.jsx b/src/pages/PostsPage.jsx
index 4630575..6a7c3fd 100644
--- a/src/pages/PostsPage.jsx
+++ b/src/pages/PostsPage.jsx
@@ -1,27 +1,33 @@
import { useEffect, useState } from 'react';
import {
- Box, Typography, Alert, Chip, IconButton, Tooltip,
+ Box, Typography, Alert, IconButton, Tooltip,
+ Dialog, DialogTitle, DialogContent, DialogActions,
+ Button, Stack,
} 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 { useNavigate } from 'react-router-dom';
import axiosInstance from '../api/axiosInstance';
export default function PostsPage() {
const [rows, setRows] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
+ const [detailNews, setDetailNews] = useState(null);
+ const navigate = useNavigate();
useEffect(() => {
- axiosInstance.get('/api/posts')
+ axiosInstance.get('/api/news')
.then(({ data }) => setRows(data))
- .catch(() => setError('Fehler beim Laden der Posts'))
+ .catch(() => setError('Fehler beim Laden der News'))
.finally(() => setLoading(false));
}, []);
const handleDelete = async (id) => {
- if (!window.confirm('Post wirklich löschen?')) return;
+ if (!window.confirm('News wirklich löschen?')) return;
try {
- await axiosInstance.delete(`/api/posts/${id}`);
+ await axiosInstance.delete(`/api/news/${id}`);
setRows((prev) => prev.filter((r) => r.id !== id));
} catch {
alert('Löschen fehlgeschlagen');
@@ -29,25 +35,45 @@ export default function PostsPage() {
};
const columns = [
- { field: 'id', headerName: 'ID', width: 80 },
- { field: 'title', headerName: 'Titel', flex: 2 },
- { field: 'author', headerName: 'Autor', flex: 1 },
+ { field: 'id', headerName: 'ID', width: 70 },
{
- field: 'status',
- headerName: 'Status',
- width: 120,
- renderCell: ({ value }) => (
-
+ field: 'title', headerName: 'Titel', flex: 1.5,
+ renderCell: ({ row, value }) => (
+
+ setDetailNews(row)}
+ sx={{ cursor: 'pointer', color: 'primary.main', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
+ >
+ {value}
+
+
),
},
{
- field: 'createdAt',
- headerName: 'Datum',
- flex: 1,
+ field: 'description', headerName: 'Beschreibung', flex: 2,
+ renderCell: ({ value }) => (
+
+ {value}
+
+ ),
+ },
+ { field: 'author', headerName: 'Autor', width: 130 },
+ { field: 'category', headerName: 'Kategorie', width: 130 },
+ {
+ field: 'picture',
+ headerName: 'Bilder',
+ width: 80,
+ sortable: false,
+ renderCell: ({ value }) => (
+
+ {Array.isArray(value) ? `${value.length} Bild${value.length !== 1 ? 'er' : ''}` : '—'}
+
+ ),
+ },
+ {
+ field: 'releaseDate',
+ headerName: 'Veröffentlicht',
+ width: 140,
valueFormatter: (value) => value ? new Date(value).toLocaleDateString('de-DE') : '—',
},
{
@@ -67,8 +93,15 @@ export default function PostsPage() {
return (
- Posts / Inhalte
+
+ News
+ } onClick={() => navigate('/posts/create')}>
+ News erstellen
+
+
+
{error && {error}}
+
+
+ {/* Detail Dialog */}
+
);
-}
+}
\ No newline at end of file