volleyball-dev-frontend/src/admin/NewsManager.tsx
Marc Wieland 041ffcad96
Some checks are pending
Deploy Volleyball Dev / deploy (push) Waiting to run
News gefixt
2025-04-25 16:31:03 +02:00

300 lines
8.8 KiB
TypeScript

import { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import DOMPurify from "dompurify";
const apiBase = import.meta.env.VITE_API_URL;
type NewsItem = {
id: number;
title: string;
description: string;
image_url: string;
team: string;
created_at: string;
};
const NewsManager = () => {
const [news, setNews] = useState<NewsItem[]>([]);
const [showForm, setShowForm] = useState(false);
const [editMode, setEditMode] = useState(false);
const [currentId, setCurrentId] = useState<number | null>(null);
const [newTitle, setNewTitle] = useState("");
const [newDescription, setNewDescription] = useState("");
const [newImageUrl, setNewImageUrl] = useState("");
const [newTeam, setNewTeam] = useState("");
const [uploading, setUploading] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deleteId, setDeleteId] = useState<number | null>(null);
useEffect(() => {
loadNews();
}, []);
const loadNews = async () => {
try {
const res = await fetch(`${apiBase}/api/news`);
const data = await res.json();
setNews(data);
} catch (err) {
console.error("Fehler beim Laden der News:", err);
}
};
const handleCreateOrUpdateNews = async () => {
const method = editMode ? "PUT" : "POST";
const url = editMode
? `${apiBase}/api/news/${currentId}`
: `${apiBase}/api/news`;
try {
const res = await fetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: newTitle,
description: DOMPurify.sanitize(newDescription),
image_url: newImageUrl,
team: newTeam,
}),
});
if (res.ok) {
await loadNews();
setShowForm(false);
setEditMode(false);
resetForm();
} else {
console.error("Fehler beim Speichern der News");
}
} catch (err) {
console.error("Fehler beim Speichern der News:", err);
}
};
const handleEdit = (item: NewsItem) => {
setNewTitle(item.title);
setNewDescription(item.description);
setNewImageUrl(item.image_url);
setNewTeam(item.team);
setCurrentId(item.id);
setEditMode(true);
setShowForm(true);
};
const confirmDelete = (id: number) => {
setDeleteId(id);
setShowDeleteModal(true);
};
const handleDelete = async () => {
if (!deleteId) return;
try {
const res = await fetch(`${apiBase}/api/news/${deleteId}`, {
method: "DELETE",
});
if (res.ok) {
await loadNews();
} else {
console.error("Fehler beim Löschen der News");
}
} catch (err) {
console.error("Fehler beim Löschen der News:", err);
} finally {
setShowDeleteModal(false);
setDeleteId(null);
}
};
const resetForm = () => {
setNewTitle("");
setNewDescription("");
setNewImageUrl("");
setNewTeam("");
setCurrentId(null);
};
const handleImageUpload = async (file: File) => {
const formData = new FormData();
formData.append("image", file);
setUploading(true);
try {
const res = await fetch(`${apiBase}/api/upload-news-image`, {
method: "POST",
body: formData,
});
if (!res.ok) throw new Error("Fehler beim Bild-Upload");
const data = await res.json();
setNewImageUrl(data.imageUrl);
} catch (err) {
console.error("Bild-Upload fehlgeschlagen:", err);
} finally {
setUploading(false);
}
};
return (
<div className="max-w-5xl mx-auto py-12 px-4">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold text-frog-600">News verwalten</h1>
<Button
className="bg-frog-500 hover:bg-frog-600 text-white"
onClick={() => {
resetForm();
setEditMode(false);
setShowForm(!showForm);
}}
>
{showForm ? "Abbrechen" : "+ Neue News"}
</Button>
</div>
{showForm && (
<div className="mb-8 border p-4 rounded-md bg-gray-50">
<h2 className="text-xl font-bold text-frog-600 mb-4">
{editMode ? "News bearbeiten" : "Neue News anlegen"}
</h2>
<div className="space-y-4">
<Input
placeholder="Titel"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
/>
<ReactQuill
theme="snow"
value={newDescription}
onChange={setNewDescription}
className="bg-white rounded-md"
modules={{
toolbar: [
[{ header: [1, 2, false] }],
["bold", "italic", "underline", "strike"],
[{ list: "ordered" }, { list: "bullet" }],
["link"],
["clean"],
],
}}
formats={[
"header",
"bold",
"italic",
"underline",
"strike",
"list",
"bullet",
"link",
]}
/>
{/* Upload */}
<Input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleImageUpload(file);
}}
/>
{uploading && <p className="text-sm text-gray-400">Bild wird hochgeladen...</p>}
{newImageUrl && (
<img
src={`${apiBase}${newImageUrl}`}
alt="Vorschau"
className="mt-2 max-h-40 rounded-md shadow-md"
/>
)}
<Input
placeholder="Team"
value={newTeam}
onChange={(e) => setNewTeam(e.target.value)}
/>
<Button
onClick={handleCreateOrUpdateNews}
className="bg-frog-500 hover:bg-frog-600 text-white w-full"
>
{editMode ? "Änderungen speichern" : "Speichern"}
</Button>
</div>
</div>
)}
{showDeleteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg shadow-lg max-w-sm w-full">
<h2 className="text-xl font-bold text-frog-600 mb-4 text-center">
News wirklich löschen?
</h2>
<div className="flex justify-center gap-4">
<Button
variant="outline"
className="border-gray-300 text-gray-600 hover:bg-gray-100"
onClick={() => setShowDeleteModal(false)}
>
Abbrechen
</Button>
<Button variant="destructive" onClick={handleDelete}>
Ja, löschen
</Button>
</div>
</div>
</div>
)}
<div className="grid md:grid-cols-2 gap-6">
{news.map((item) => (
<Card key={item.id} className="overflow-hidden">
<CardHeader>
<CardTitle>{item.title}</CardTitle>
</CardHeader>
<CardContent>
{item.image_url && (
<img
src={`${apiBase}${item.image_url}`}
alt={item.title}
className="w-full rounded-md mb-3 max-h-40 object-cover"
/>
)}
<div
className="text-gray-600 mb-2 line-clamp-3 prose max-w-none"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(item.description),
}}
/>
<p className="text-xs text-gray-400">
Erstellt am{" "}
{new Date(item.created_at).toLocaleDateString("de-DE")}
</p>
<div className="flex gap-2 mt-4">
<Button
size="sm"
variant="outline"
className="border-frog-500 text-frog-600 hover:bg-frog-50"
onClick={() => handleEdit(item)}
>
Bearbeiten
</Button>
<Button size="sm" variant="destructive" onClick={() => confirmDelete(item.id)}>
Löschen
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
};
export default NewsManager;