From c8e4f3b5ececd61ca25097694fb511f4d8aebc5d Mon Sep 17 00:00:00 2001 From: eddy Date: Sat, 28 Mar 2026 12:24:18 +0100 Subject: [PATCH] =?UTF-8?q?PostsPage=20=C3=BCberarbeitet=20und=20extra=20s?= =?UTF-8?q?eite=20f=C3=BCr=20create=20news=20erstellt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 16 ++- src/pages/CreateNewsPage.jsx | 222 +++++++++++++++++++++++++++++++++++ src/pages/PostsPage.jsx | 125 ++++++++++++++++---- 3 files changed, 338 insertions(+), 25 deletions(-) create mode 100644 src/pages/CreateNewsPage.jsx 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)}> + + + + ))} + + + + + + + + {/* Push Notification */} + + Push Notification + + { + if (val === null) return; + setForm((prev) => ({ ...prev, pushNotification: val })); + }} + size="small" + > + + + Aus + + + + An + + + + + + + + + + + + + + + + + + + ); +} \ 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 + + + {error && {error}} + + + {/* Detail Dialog */} + setDetailNews(null)} maxWidth="sm" fullWidth> + {detailNews?.title} + + + + Beschreibung + {detailNews?.description} + + + + Autor + {detailNews?.author} + + + Kategorie + {detailNews?.category} + + + Veröffentlicht + + {detailNews?.releaseDate ? new Date(detailNews.releaseDate).toLocaleDateString('de-DE') : '—'} + + + + {Array.isArray(detailNews?.picture) && detailNews.picture.length > 0 && ( + + Bilder + + {detailNews.picture.map((url, i) => ( + + ))} + + + )} + + + + + + ); -} +} \ No newline at end of file