New Memberverwaltung
Some checks are pending
Deploy Volleyball Dev / deploy (push) Waiting to run

This commit is contained in:
Marc Wieland 2025-04-29 00:46:57 +02:00
parent 8960806f9b
commit 6f5d0a0fd0
6 changed files with 695 additions and 1 deletions

165
src/admin/PlayerEdit.tsx Normal file
View File

@ -0,0 +1,165 @@
import { useEffect, useState } from "react";
import { useParams, useNavigate, Link } from "react-router-dom";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui//use-toast";
const apiBase = import.meta.env.VITE_API_URL;
const PlayerEdit = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const {toast} = useToast();
const [name, setName] = useState("");
const [nickname, setNickname] = useState("");
const [position, setPosition] = useState("");
const [jerseyNumber, setJerseyNumber] = useState<number | undefined>();
const [birthdate, setBirthdate] = useState("");
const [favoriteFood, setFavoriteFood] = useState("");
const [status, setStatus] = useState("aktiv");
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
useEffect(() => {
fetchPlayer();
}, [id]);
const fetchPlayer = async () => {
try {
const res = await fetch(`${apiBase}/api/players/${id}`);
const data = await res.json();
setName(data.name || "");
setNickname(data.nickname || "");
setPosition(data.position || "");
setJerseyNumber(data.jersey_number || undefined);
setBirthdate(data.birthdate ? data.birthdate.substring(0, 10) : "");
setFavoriteFood(data.favorite_food || "");
setStatus(data.status || "aktiv");
setImageUrl(data.image_url ? `${apiBase}${data.image_url}` : null);
} catch (err) {
console.error("Fehler beim Laden des Spielers:", err);
}
};
const handleImageUpload = async (file: File) => {
const formData = new FormData();
formData.append("image", file);
setUploading(true);
try {
const res = await fetch(`${apiBase}/api/upload-player-image`, {
method: "POST",
body: formData,
});
if (!res.ok) throw new Error("Fehler beim Hochladen des Bildes");
const data = await res.json();
setImageUrl(`${apiBase}${data.imageUrl}`);
} catch (err) {
console.error(err);
} finally {
setUploading(false);
}
};
const handleSave = async () => {
try {
const res = await fetch(`${apiBase}/api/players/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name,
nickname,
position,
jersey_number: jerseyNumber,
birthdate,
favorite_food: favoriteFood,
status,
image_url: imageUrl?.replace(apiBase, ""), // Nur Pfad speichern
}),
});
if (res.ok) {
toast({
title: "Spieler gespeichert",
description: "Die Änderungen wurden erfolgreich übernommen.",
});
setTimeout(() => {
navigate(-1);
}, 500); // Warte kurz, damit der Toast sichtbar bleibt
} else {
console.error("Fehler beim Speichern");
}
} catch (err) {
console.error("Fehler beim Speichern:", err);
}
};
return (
<div className="max-w-3xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold text-frog-600 mb-6">Spieler bearbeiten</h1>
<div className="space-y-4">
<Input placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} />
<Input placeholder="Spitzname" value={nickname} onChange={(e) => setNickname(e.target.value)} />
<Input placeholder="Position" value={position} onChange={(e) => setPosition(e.target.value)} />
<Input
type="number"
placeholder="Trikotnummer"
value={jerseyNumber || ""}
onChange={(e) => setJerseyNumber(Number(e.target.value))}
/>
<Input
type="date"
placeholder="Geburtstag"
value={birthdate}
onChange={(e) => setBirthdate(e.target.value)}
/>
<Input
placeholder="Lieblingsessen"
value={favoriteFood}
onChange={(e) => setFavoriteFood(e.target.value)}
/>
<Input
placeholder="Status (aktiv, verletzt, pausiert)"
value={status}
onChange={(e) => setStatus(e.target.value)}
/>
{/* Bild-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>}
{imageUrl && (
<img
src={imageUrl}
alt="Spielerbild"
className="mt-2 max-h-40 rounded-md object-cover shadow-md"
/>
)}
{/* Buttons */}
<div className="flex justify-end gap-4 mt-6">
<Button variant="outline" asChild>
<Link to="/admin/teams">Abbrechen</Link>
</Button>
<Button onClick={handleSave} className="bg-frog-500 hover:bg-frog-600 text-white">
Speichern
</Button>
</div>
</div>
</div>
);
};
export default PlayerEdit;

217
src/admin/TeamAddPlayer.tsx Normal file
View File

@ -0,0 +1,217 @@
import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
const apiBase = import.meta.env.VITE_API_URL;
type Player = {
id: number;
name: string;
nickname?: string;
position: string;
jersey_number?: number;
age?: number;
favorite_food?: string;
image_url?: string;
};
const TeamAddPlayer = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [players, setPlayers] = useState<Player[]>([]);
const [newPlayer, setNewPlayer] = useState({
name: "",
nickname: "",
position: "",
jersey_number: "",
age: "",
favorite_food: "",
image_url: "",
});
const [uploading, setUploading] = useState(false);
const fetchPlayers = async () => {
try {
const res = await fetch(`${apiBase}/api/players`);
const data = await res.json();
setPlayers(data);
} catch (err) {
console.error("Fehler beim Laden der Spieler:", err);
}
};
const handleAddExistingPlayer = async (playerId: number) => {
try {
const res = await fetch(`${apiBase}/api/teams/${id}/players`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ playerId }),
});
if (res.ok) {
navigate(`/admin/teams/${id}`);
} else {
console.error("Fehler beim Hinzufügen");
}
} catch (err) {
console.error(err);
}
};
const handleCreateNewPlayer = async () => {
if (!newPlayer.name.trim() || !newPlayer.position.trim()) return;
try {
const res = await fetch(`${apiBase}/api/players`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...newPlayer,
jersey_number: newPlayer.jersey_number ? Number(newPlayer.jersey_number) : null,
age: newPlayer.age ? Number(newPlayer.age) : null,
team_ids: [Number(id)],
}),
});
if (res.ok) {
navigate(`/admin/teams/${id}`);
} else {
console.error("Fehler beim Erstellen des Spielers");
}
} catch (err) {
console.error(err);
}
};
const handleImageUpload = async (file: File) => {
const formData = new FormData();
formData.append("image", file);
setUploading(true);
try {
const res = await fetch(`${apiBase}/api/upload-player-image`, {
method: "POST",
body: formData,
});
if (!res.ok) throw new Error("Fehler beim Bild-Upload");
const data = await res.json();
setNewPlayer((prev) => ({ ...prev, image_url: data.imageUrl }));
} catch (err) {
console.error("Bild-Upload fehlgeschlagen:", err);
} finally {
setUploading(false);
}
};
useEffect(() => {
fetchPlayers();
}, []);
return (
<div className="max-w-4xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold text-frog-600 mb-6">Spieler zum Team hinzufügen</h1>
{/* Bestehende Spieler */}
<div className="mb-12">
<h2 className="text-2xl font-bold text-frog-500 mb-4">Bestehende Spieler auswählen</h2>
{players.length === 0 ? (
<p className="text-gray-600">Keine Spieler gefunden.</p>
) : (
<div className="grid md:grid-cols-2 gap-6">
{players.map((player) => (
<Card key={player.id} className="hover:shadow-md transition-shadow">
<CardHeader>
<CardTitle>
{player.name} {player.nickname && `(${player.nickname})`}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600">Position: {player.position}</p>
{player.jersey_number && <p className="text-gray-600">Nr: {player.jersey_number}</p>}
<Button
className="mt-2 bg-frog-500 hover:bg-frog-600 text-white w-full"
onClick={() => handleAddExistingPlayer(player.id)}
>
Hinzufügen
</Button>
</CardContent>
</Card>
))}
</div>
)}
</div>
{/* Neuen Spieler anlegen */}
<div>
<h2 className="text-2xl font-bold text-frog-500 mb-4">Neuen Spieler erstellen</h2>
<div className="space-y-4">
<Input
placeholder="Name *"
value={newPlayer.name}
onChange={(e) => setNewPlayer((prev) => ({ ...prev, name: e.target.value }))}
/>
<Input
placeholder="Spitzname"
value={newPlayer.nickname}
onChange={(e) => setNewPlayer((prev) => ({ ...prev, nickname: e.target.value }))}
/>
<Input
placeholder="Position *"
value={newPlayer.position}
onChange={(e) => setNewPlayer((prev) => ({ ...prev, position: e.target.value }))}
/>
<Input
placeholder="Trikotnummer"
type="number"
value={newPlayer.jersey_number}
onChange={(e) => setNewPlayer((prev) => ({ ...prev, jersey_number: e.target.value }))}
/>
<Input
placeholder="Alter"
type="number"
value={newPlayer.age}
onChange={(e) => setNewPlayer((prev) => ({ ...prev, age: e.target.value }))}
/>
<Input
placeholder="Lieblingsessen"
value={newPlayer.favorite_food}
onChange={(e) => setNewPlayer((prev) => ({ ...prev, favorite_food: e.target.value }))}
/>
{/* Bild 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>}
{newPlayer.image_url && (
<img
src={`${apiBase}${newPlayer.image_url}`}
alt="Spielerbild"
className="mt-2 max-h-40 rounded-md shadow-md"
/>
)}
<Button
onClick={handleCreateNewPlayer}
className="bg-frog-500 hover:bg-frog-600 text-white w-full"
>
Spieler erstellen & hinzufügen
</Button>
</div>
</div>
</div>
);
};
export default TeamAddPlayer;

69
src/admin/TeamCreate.tsx Normal file
View File

@ -0,0 +1,69 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
const apiBase = import.meta.env.VITE_API_URL;
const TeamCreate = () => {
const [teamName, setTeamName] = useState("");
const [saving, setSaving] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
const navigate = useNavigate();
const handleSave = async () => {
if (!teamName.trim()) return;
setSaving(true);
setErrorMsg("");
try {
const res = await fetch(`${apiBase}/api/teams`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: teamName }),
});
if (!res.ok) {
const msg = await res.text();
setErrorMsg(msg);
throw new Error(msg);
}
navigate("/admin/teams");
} catch (err) {
console.error(err);
} finally {
setSaving(false);
}
};
return (
<div className="max-w-2xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold text-frog-600 mb-6">Neues Team anlegen</h1>
<div className="space-y-4">
<Input
placeholder="Teamname"
value={teamName}
onChange={(e) => setTeamName(e.target.value)}
/>
{errorMsg && (
<p className="text-sm text-red-600 bg-red-50 border border-red-200 px-3 py-2 rounded-md">
{errorMsg}
</p>
)}
<Button
onClick={handleSave}
disabled={saving}
className="bg-frog-500 hover:bg-frog-600 text-white"
>
{saving ? "Speichern..." : "Speichern"}
</Button>
</div>
</div>
);
};
export default TeamCreate;

163
src/admin/TeamDetail.tsx Normal file
View File

@ -0,0 +1,163 @@
import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Link } from "react-router-dom";
const apiBase = import.meta.env.VITE_API_URL;
type Player = {
id: number;
name: string;
nickname?: string;
position: string;
jersey_number?: number;
image_url?: string;
};
const TeamDetail = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [teamName, setTeamName] = useState("");
const [players, setPlayers] = useState<Player[]>([]);
const [loading, setLoading] = useState(true);
const fetchTeam = async () => {
try {
const res = await fetch(`${apiBase}/api/teams/${id}`);
const data = await res.json();
setTeamName(data.name ?? "");
setPlayers(data.players ?? []);
} catch (err) {
console.error("Fehler beim Laden des Teams:", err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTeam();
}, [id]);
const handleUpdateName = async () => {
try {
const res = await fetch(`${apiBase}/api/teams/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: teamName }),
});
if (!res.ok) throw new Error("Fehler beim Aktualisieren des Teamnamens");
alert("Teamname aktualisiert!");
} catch (err) {
console.error(err);
}
};
const handleRemovePlayer = async (playerId: number) => {
try {
const res = await fetch(`${apiBase}/api/teams/${id}/players/${playerId}`, {
method: "DELETE",
});
if (res.ok) {
setPlayers((prev) => prev.filter((p) => p.id !== playerId));
} else {
console.error("Fehler beim Entfernen des Spielers");
}
} catch (err) {
console.error(err);
}
};
if (loading) return <p className="text-center py-12">Lade Team...</p>;
return (
<div className="max-w-4xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold text-frog-600 mb-6">Team bearbeiten</h1>
<div className="space-y-4 mb-8">
<Input
value={teamName}
onChange={(e) => setTeamName(e.target.value)}
placeholder="Teamname"
/>
<Button
onClick={handleUpdateName}
className="bg-frog-500 hover:bg-frog-600 text-white"
>
Teamname speichern
</Button>
</div>
<div className="mb-8">
<h2 className="text-2xl font-bold text-frog-500 mb-4">Spieler im Team</h2>
{players.length === 0 ? (
<p className="text-gray-600">Noch keine Spieler zugeordnet.</p>
) : (
<div className="grid md:grid-cols-2 gap-6">
{players.map((player) => (
<Card key={player.id} className="hover:shadow-md transition-shadow">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>
{player.name} {player.nickname && `(${player.nickname})`}
</CardTitle>
</div>
<div>
<img
src={player.image_url ? `${apiBase}${player.image_url}` : "/images/default-player.png"}
alt={player.name}
className="w-16 h-16 object-cover rounded-full border-2 border-frog-500 shadow-sm hover:scale-105 transition-transform"
/>
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-gray-600">Position: {player.position}</p>
{player.jersey_number && (
<p className="text-gray-600">Nummer: {player.jersey_number}</p>
)}
<div className="flex gap-2 mt-4">
<Button
asChild
variant="outline"
className="text-frog-600 border-frog-500 hover:bg-frog-50"
>
<Link to={`/admin/players/${player.id}/edit`}>
Bearbeiten
</Link>
</Button>
<Button
onClick={() => handleRemovePlayer(player.id)}
variant="destructive"
>
Entfernen
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
<div className="text-center">
<Button asChild>
<Link to={`/admin/teams/${id}/add-player`}>
+ Spieler hinzufügen
</Link>
</Button>
</div>
</div>
);
};
export default TeamDetail;

View File

@ -13,6 +13,10 @@ type Team = {
const TeamList = () => { const TeamList = () => {
const [teams, setTeams] = useState<Team[]>([]); const [teams, setTeams] = useState<Team[]>([]);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [teamToDelete, setTeamToDelete] = useState<Team | null>(null);
const [confirmInput, setConfirmInput] = useState("");
const fetchTeams = async () => { const fetchTeams = async () => {
try { try {
@ -24,6 +28,25 @@ const TeamList = () => {
} }
}; };
const deleteTeam = async () => {
if (!teamToDelete) return;
try {
const res = await fetch(`${apiBase}/api/teams/${teamToDelete.id}`, {
method: "DELETE",
});
if (res.ok) {
setTeams((prev) => prev.filter((t) => t.id !== teamToDelete.id));
setShowDeleteModal(false);
} else {
console.error("Fehler beim Löschen");
}
} catch (err) {
console.error("Fehler:", err);
}
};
useEffect(() => { useEffect(() => {
fetchTeams(); fetchTeams();
}, []); }, []);
@ -36,7 +59,7 @@ const TeamList = () => {
<Link to="/admin/teams/new">+ Neues Team</Link> <Link to="/admin/teams/new">+ Neues Team</Link>
</Button> </Button>
</div> </div>
<div className="grid md:grid-cols-2 gap-6"> <div className="grid md:grid-cols-2 gap-6">
{teams.map((team) => ( {teams.map((team) => (
<Card key={team.id} className="hover:shadow-md transition-shadow"> <Card key={team.id} className="hover:shadow-md transition-shadow">
@ -51,12 +74,61 @@ const TeamList = () => {
> >
<Link to={`/admin/teams/${team.id}`}>Bearbeiten</Link> <Link to={`/admin/teams/${team.id}`}>Bearbeiten</Link>
</Button> </Button>
<Button
variant="destructive"
className="mt-2"
onClick={() => {
setTeamToDelete(team);
setShowDeleteModal(true);
setConfirmInput("");
}}
>
Löschen
</Button>
</CardContent> </CardContent>
</Card> </Card>
))} ))}
</div> </div>
{/* 🔥 MODAL INS RETURN */}
{showDeleteModal && teamToDelete && (
<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-md w-full">
<h2 className="text-xl font-bold text-red-600 mb-4 text-center">
Team wirklich löschen?
</h2>
<p className="mb-2 text-sm text-gray-700 text-center">
Gib den Teamnamen <strong>"{teamToDelete.name}"</strong> ein, um zu bestätigen:
</p>
<input
type="text"
value={confirmInput}
onChange={(e) => setConfirmInput(e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-2 mb-4"
placeholder="Teamname zur Bestätigung"
/>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setShowDeleteModal(false)}>
Abbrechen
</Button>
<Button
variant="destructive"
onClick={deleteTeam}
disabled={confirmInput !== teamToDelete.name}
>
Löschen
</Button>
</div>
</div>
</div>
)}
</div> </div>
); );
}; };
export default TeamList; export default TeamList;

View File

@ -20,6 +20,10 @@ import UserManagementPage from "./admin/UserManagementPage";
import UserCreatePage from "./admin/UserCreatePage"; import UserCreatePage from "./admin/UserCreatePage";
import UserEditPage from "./admin/UserEditPage"; import UserEditPage from "./admin/UserEditPage";
import TeamList from "./admin/TeamList"; import TeamList from "./admin/TeamList";
import TeamCreate from "./admin/TeamCreate";
import TeamDetail from "./admin/TeamDetail";
import TeamAddPlayer from "./admin/TeamAddPlayer";
import PlayerEdit from "./admin/PlayerEdit";
@ -61,6 +65,10 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<Route path="/admin/users/new" element={<PrivateRoute><Layout><UserCreatePage /></Layout></PrivateRoute>} /> <Route path="/admin/users/new" element={<PrivateRoute><Layout><UserCreatePage /></Layout></PrivateRoute>} />
<Route path="/admin/users/edit/:id" element={<PrivateRoute><Layout><UserEditPage /></Layout></PrivateRoute>} /> <Route path="/admin/users/edit/:id" element={<PrivateRoute><Layout><UserEditPage /></Layout></PrivateRoute>} />
<Route path="/admin/teams" element={<PrivateRoute><Layout><TeamList /></Layout></PrivateRoute>} /> <Route path="/admin/teams" element={<PrivateRoute><Layout><TeamList /></Layout></PrivateRoute>} />
<Route path="/admin/teams/new" element={<PrivateRoute><Layout><TeamCreate /></Layout></PrivateRoute>} />
<Route path="/admin/teams/:id" element={<PrivateRoute><Layout><TeamDetail /></Layout></PrivateRoute>} />
<Route path="/admin/teams/:id/add-player" element={<PrivateRoute><Layout><TeamAddPlayer /></Layout></PrivateRoute>} />
<Route path="/admin/players/:id/edit" element={<PrivateRoute><Layout><PlayerEdit /></Layout></PrivateRoute>} />
</Routes> </Routes>
</AuthProvider> </AuthProvider>