This commit is contained in:
parent
f64264b930
commit
28f2f74769
@ -57,7 +57,15 @@ const AdminDashboard = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* 🎉 Neue Galerie-Kachel */}
|
<Link to="/admin/events">
|
||||||
|
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||||
|
<CardContent className="p-6 text-center">
|
||||||
|
<CardTitle className="text-frog-600">Events verwalten</CardTitle>
|
||||||
|
<p className="text-gray-600 mt-2 text-sm">Alle Events sehen und bearbeiten</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<Link to="/admin/gallery">
|
<Link to="/admin/gallery">
|
||||||
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
|
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||||
<CardContent className="p-6 text-center">
|
<CardContent className="p-6 text-center">
|
||||||
|
|||||||
152
src/admin/EventsAdmin.tsx
Normal file
152
src/admin/EventsAdmin.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
type Event = {
|
||||||
|
id?: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
max_participants?: number;
|
||||||
|
fee?: number;
|
||||||
|
address?: string;
|
||||||
|
is_private: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultEvent: Event = {
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
max_participants: undefined,
|
||||||
|
fee: undefined,
|
||||||
|
address: "",
|
||||||
|
is_private: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventsAdmin = () => {
|
||||||
|
const [events, setEvents] = useState<Event[]>([]);
|
||||||
|
const [form, setForm] = useState<Event>(defaultEvent);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
const fetchEvents = async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get("/api/events?showPrivate=true");
|
||||||
|
if (Array.isArray(res.data)) {
|
||||||
|
setEvents(res.data);
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ Events-API hat kein Array geliefert:", res.data);
|
||||||
|
setEvents([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Fehler beim Laden der Events:", error);
|
||||||
|
setEvents([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchEvents();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (isEditing && form.id) {
|
||||||
|
await axios.put(`/api/events/${form.id}`, form);
|
||||||
|
} else {
|
||||||
|
await axios.post("/api/events", form);
|
||||||
|
}
|
||||||
|
setForm(defaultEvent);
|
||||||
|
setIsEditing(false);
|
||||||
|
fetchEvents();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
await axios.delete(`/api/events/${id}`);
|
||||||
|
fetchEvents();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (event: Event) => {
|
||||||
|
setForm(event);
|
||||||
|
setIsEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-2xl mx-auto p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">{isEditing ? "Event bearbeiten" : "Neues Event erstellen"}</h1>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Titel"
|
||||||
|
value={form.title}
|
||||||
|
onChange={(e) => setForm({ ...form, title: e.target.value })}
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Beschreibung"
|
||||||
|
value={form.description}
|
||||||
|
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Max. Teilnehmer"
|
||||||
|
value={form.max_participants ?? ""}
|
||||||
|
onChange={(e) => setForm({ ...form, max_participants: parseInt(e.target.value) || undefined })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="Gebühr (€)"
|
||||||
|
value={form.fee ?? ""}
|
||||||
|
onChange={(e) => setForm({ ...form, fee: parseFloat(e.target.value) || undefined })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Adresse"
|
||||||
|
value={form.address}
|
||||||
|
onChange={(e) => setForm({ ...form, address: e.target.value })}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
checked={form.is_private}
|
||||||
|
onCheckedChange={(val) => setForm({ ...form, is_private: val })}
|
||||||
|
/>
|
||||||
|
<span>Privat</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button onClick={handleSubmit}>
|
||||||
|
{isEditing ? "Speichern" : "Erstellen"}
|
||||||
|
</Button>
|
||||||
|
{isEditing && (
|
||||||
|
<Button variant="ghost" onClick={() => { setForm(defaultEvent); setIsEditing(false); }}>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="my-6" />
|
||||||
|
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Bestehende Events</h2>
|
||||||
|
{Array.isArray(events) && events.length > 0 ? (
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{events.map((ev) => (
|
||||||
|
<li key={ev.id} className="p-3 border rounded-md shadow-sm flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{ev.title}</div>
|
||||||
|
<div className="text-sm text-gray-500">{ev.description?.slice(0, 60)}...</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button variant="outline" onClick={() => handleEdit(ev)}>Bearbeiten</Button>
|
||||||
|
<Button variant="destructive" onClick={() => handleDelete(ev.id!)}>Löschen</Button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500">Noch keine Events vorhanden.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventsAdmin;
|
||||||
@ -27,6 +27,7 @@ import PlayerEdit from "./admin/PlayerEdit";
|
|||||||
import PlayerManagementPage from "./admin/PlayerManagementPage";
|
import PlayerManagementPage from "./admin/PlayerManagementPage";
|
||||||
import GalleryManager from "./admin/GalleryManager";
|
import GalleryManager from "./admin/GalleryManager";
|
||||||
import GalleryPage from "./pages/GalleryPage";
|
import GalleryPage from "./pages/GalleryPage";
|
||||||
|
import EventsAdmin from "./admin/EventsAdmin";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -75,6 +76,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<Route path="/admin/players/:id/edit" element={<PrivateRoute><Layout><PlayerEdit /></Layout></PrivateRoute>} />
|
<Route path="/admin/players/:id/edit" element={<PrivateRoute><Layout><PlayerEdit /></Layout></PrivateRoute>} />
|
||||||
<Route path="/admin/players" element={<PrivateRoute><Layout><PlayerManagementPage /></Layout></PrivateRoute>} />
|
<Route path="/admin/players" element={<PrivateRoute><Layout><PlayerManagementPage /></Layout></PrivateRoute>} />
|
||||||
<Route path="/admin/gallery" element={<PrivateRoute><Layout><GalleryManager /></Layout></PrivateRoute>} />
|
<Route path="/admin/gallery" element={<PrivateRoute><Layout><GalleryManager /></Layout></PrivateRoute>} />
|
||||||
|
<Route path="/admin/events" element={<PrivateRoute><Layout><EventsAdmin /></Layout></PrivateRoute>} />
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
@ -24,17 +24,24 @@ const LoginPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error("Login fehlgeschlagen");
|
if (res.status === 429) {
|
||||||
|
throw new Error("rate_limit");
|
||||||
|
}
|
||||||
|
throw new Error("login_failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
localStorage.setItem("token", data.token);
|
localStorage.setItem("token", data.token);
|
||||||
|
|
||||||
navigate("/admin");
|
navigate("/admin");
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setError("Login fehlgeschlagen. Bitte prüfe Benutzername und Passwort.");
|
if (err.message === "rate_limit") {
|
||||||
|
setError("Zu viele fehlgeschlagene Login-Versuche. Bitte versuch es später erneut.");
|
||||||
|
} else {
|
||||||
|
setError("Login fehlgeschlagen. Bitte prüfe Benutzername und Passwort.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user