Files
DeineDorfApp-Admin-Panel/src/pages/News/CreateNewsPage.jsx
2026-04-15 15:17:11 +02:00

235 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.js';
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('/news', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
navigate('/posts');
} catch (err) {
setError(err.response?.data?.error ?? 'Erstellen fehlgeschlagen');
} finally {
setSaving(false);
}
};
const isValid = form.title && form.description && form.category;
return (
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<IconButton onClick={() => navigate('/posts')} size="small">
<ArrowBackIcon />
</IconButton>
<Typography variant="h5" fontWeight={600}>News erstellen</Typography>
</Box>
<Card sx={{ maxWidth: 680 }}>
<CardContent sx={{ p: 3 }}>
<Box component="form" onSubmit={handleSubmit}>
<Stack spacing={3}>
{error && (
<Alert severity="error">
{typeof error === 'string' ? error : JSON.stringify(error)}
</Alert>
)}
<TextField label="Titel" name="title" value={form.title} onChange={handleChange} required fullWidth autoFocus />
<TextField label="Beschreibung" name="description" value={form.description} onChange={handleChange} required fullWidth multiline rows={6} />
<TextField label="Kategorie" name="category" value={form.category} onChange={handleChange} required fullWidth />
<Divider />
{/* Bild-Upload */}
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
<Typography variant="subtitle2" fontWeight={500}>Bilder</Typography>
{files.length > 0 && (
<Typography variant="caption" color="text.secondary">
Das erste Bild wird als Vorschaubild verwendet
</Typography>
)}
</Box>
<Stack spacing={1.5}>
{previews.map((src, i) => (
<Box
key={i}
sx={{
display: 'flex', alignItems: 'center', gap: 1.5,
p: 1, borderRadius: 1,
border: '1px solid',
borderColor: i === 0 ? 'primary.main' : 'divider',
bgcolor: i === 0 ? 'primary.50' : 'transparent',
}}
>
<Box
component="img"
src={src}
alt={`Vorschau ${i + 1}`}
sx={{ width: 64, height: 64, objectFit: 'cover', borderRadius: 1, flexShrink: 0 }}
/>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
{i === 0 && (
<Chip
icon={<StarIcon sx={{ fontSize: '13px !important' }} />}
label="Vorschaubild"
size="small"
color="primary"
sx={{ height: 20, fontSize: 11 }}
/>
)}
</Box>
<Typography variant="body2" color="text.secondary" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{files[i]?.name}
</Typography>
</Box>
<IconButton size="small" color="error" onClick={() => handleRemoveFile(i)}>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
</Box>
))}
<Button
component="label"
startIcon={<AddPhotoAlternateOutlinedIcon />}
size="small"
sx={{ alignSelf: 'flex-start' }}
>
{files.length === 0 ? 'Vorschaubild hinzufügen' : 'Weiteres Bild hinzufügen'}
<input type="file" hidden accept="image/jpeg,image/png,image/webp" multiple onChange={handleFileChange} />
</Button>
</Stack>
</Box>
<Divider />
{/* Push Notification */}
<Box>
<Typography variant="subtitle2" fontWeight={500} mb={1.5}>Push Notification</Typography>
<Alert
severity="info"
variant="outlined"
sx={{ py: 0.5, mb: 2, fontSize: 13 }}
>
Wird gebündelt nach ~30 min gesendet. Kein Versand bei Ruhezeiten (228 Uhr),
wenn in den letzten 90 min bereits gesendet wurde oder das Tageslimit (3) erreicht ist.
</Alert>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<ToggleButtonGroup
exclusive
value={form.pushNotification}
onChange={(_, val) => {
if (val === null) return;
setForm((prev) => ({ ...prev, pushNotification: val }));
}}
size="small"
>
<ToggleButton value={false} sx={{ gap: 0.75, px: 2 }}>
<NotificationsOffOutlinedIcon fontSize="small" />
Aus
</ToggleButton>
<ToggleButton value={true} sx={{ gap: 0.75, px: 2 }}>
<NotificationsActiveIcon fontSize="small" />
An
</ToggleButton>
</ToggleButtonGroup>
<Collapse in={form.pushNotification} orientation="horizontal" sx={{ flex: 1 }}>
<TextField
label="Nachricht"
name="pushMessage"
value={form.pushMessage}
onChange={handleChange}
fullWidth
size="small"
placeholder="Kurze Info für die Notification..."
inputProps={{ maxLength: 100 }}
helperText={`${form.pushMessage.length}/100`}
/>
</Collapse>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end', pt: 1 }}>
<Button onClick={() => navigate('/posts')} disabled={saving}>Abbrechen</Button>
<Button
type="submit"
variant="contained"
disabled={saving || !isValid}
startIcon={saving ? <CircularProgress size={16} color="inherit" /> : null}
>
Veröffentlichen
</Button>
</Box>
</Stack>
</Box>
</CardContent>
</Card>
</Box>
);
}