This commit is contained in:
parent
8960806f9b
commit
6f5d0a0fd0
165
src/admin/PlayerEdit.tsx
Normal file
165
src/admin/PlayerEdit.tsx
Normal 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
217
src/admin/TeamAddPlayer.tsx
Normal 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
69
src/admin/TeamCreate.tsx
Normal 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
163
src/admin/TeamDetail.tsx
Normal 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;
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user