ADD: added tournament creation without backend support

This commit is contained in:
hwinkel
2025-06-09 21:08:30 +02:00
parent 1a2eec44a9
commit 1e6babbf67
14 changed files with 332 additions and 34 deletions

View File

@@ -7,6 +7,9 @@ import TournamentDetails from './pages/TournamentDetails';
import Players from './pages/Players';
import Navigation from './pages/Navigation';
import TeamManagement from './pages/Teams';
import ViewEditPlayer from './pages/ViewEditPlayer';
import Tournaments from './pages/Tournaments';
import NewTournament from './pages/NewTournament';
function App() {
@@ -17,12 +20,16 @@ function App() {
<Routes>
<Route path="/" element={<Dashboard />} /> {/* Öffentlich */}
<Route path="/login" element={<LoginPage />} />
<Route path="tournaments" element={<Tournaments />} />
{/* Geschützte Routen */}
<Route path="/players" element={<ProtectedRoute><Players /></ProtectedRoute>} />
<Route path="/players/:id" element={<ViewEditPlayer />} />
<Route path="/teams" element={<ProtectedRoute><TeamManagement /></ProtectedRoute>} />
<Route path="/tournaments/:id" element={<ProtectedRoute><TournamentDetails /></ProtectedRoute>} />
<Route path="/tournament/:id" element={<ProtectedRoute><TournamentDetails /></ProtectedRoute>} />
<Route path="/tournament/new" element={<ProtectedRoute><NewTournament /></ProtectedRoute>} />
</Routes>
</Router>
</AuthProvider>

View File

@@ -22,12 +22,13 @@ export default function Dashboard() {
return (
<div className="p-6 max-w-5xl mx-auto">
<h1 className="text-3xl font-bold mb-6">Turniere</h1>
{error && <p className="text-red-600">{error}</p>}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{tournaments.map(t => (
<div key={t.id} className="border rounded p-4 shadow hover:shadow-lg transition cursor-pointer">
<Link to={`/tournaments/${t.id}`}>
<Link to={`/tournament/${t.id}`}>
<h2 className="text-xl font-semibold">{t.name}</h2>
<p>{t.teams.length} / {t.maxParticipants} Teilnehmer</p>
<p>Ort: {t.location}</p>

View File

@@ -8,6 +8,8 @@ export default function Navigation() {
<nav className="bg-blue-600 text-white p-4 flex justify-between">
<div className="space-x-4">
<Link to="/">Dashboard</Link>
<Link to="/tournaments">Turniere</Link>
{token && <Link to="/players">Spieler</Link>}
{token && <Link to="/teams">Teams</Link>}
</div>

View File

@@ -0,0 +1,93 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { createTournament } from './api';
const NewTournament = () => {
const navigate = useNavigate();
const token = localStorage.getItem('token')|| '';
const [name, setName] = useState('');
const [location, setLocation] = useState('');
const [date, setDate] = useState('');
const [maxTeams, setMaxTeams] = useState(8);
const [message, setMessage] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const data = JSON.stringify({ name, location, date, maxTeams });
const res = await createTournament(data, token);
// const data = await res.json();
if (res.ok) {
setMessage('Turnier erfolgreich erstellt.');
setTimeout(() => navigate('/tournaments'), 1500);
} else {
setMessage('Fehler beim Erstellen.');
}
};
return (
<div className="max-w-xl mx-auto mt-8 p-6 bg-white shadow-md rounded-md">
<h2 className="text-2xl font-semibold mb-6">Neues Turnier erstellen</h2>
<form className="space-y-4" onSubmit={handleSubmit}>
<div>
<label className="block font-medium mb-1">Turniername</label>
<input
type="text"
className="w-full border border-gray-300 p-2 rounded"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div>
<label className="block font-medium mb-1">Ort</label>
<input
type="text"
className="w-full border border-gray-300 p-2 rounded"
value={location}
onChange={(e) => setLocation(e.target.value)}
required
/>
</div>
<div>
<label className="block font-medium mb-1">Datum</label>
<input
type="date"
className="w-full border border-gray-300 p-2 rounded"
value={date}
onChange={(e) => setDate(e.target.value)}
required
/>
</div>
<div>
<label className="block font-medium mb-1">Maximale Teams</label>
<input
type="number"
className="w-full border border-gray-300 p-2 rounded"
value={maxTeams}
onChange={(e) => setMaxTeams(Number(e.target.value))}
min={2}
max={64}
/>
</div>
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Erstellen
</button>
{message && <p className="text-green-600 font-medium mt-2">{message}</p>}
</form>
</div>
);
};
export default NewTournament;

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { fetchPlayers,createPlayer,deletePlayer,updatePlayer } from './api';
import { Navigate, useNavigate } from 'react-router-dom';
interface Player {
@@ -15,6 +16,7 @@ export default function PlayerManagement() {
const [password, setPassword] = useState("");
const [editingId, setEditingId] = useState<string | null>(null);
const token = localStorage.getItem('token');
const navigate = useNavigate();
useEffect(() => {
if (token) loadPlayers(token);
@@ -60,10 +62,9 @@ export default function PlayerManagement() {
setEmail("");
};
const handleEdit = (player: Player) => {
setName(player.name);
setEmail(player.email);
setEditingId(player.id);
const handleViewEdit = (playerID: string) => () => {
navigate(`/players/${playerID}`);
};
const handleDelete = (id: string) => {
@@ -128,9 +129,10 @@ export default function PlayerManagement() {
<td className="border px-4 py-2">{player.email}</td>
<td className="border px-4 py-2 space-x-2">
<button
onClick={() => handleEdit(player)}
className="bg-yellow-400 text-white px-2 py-1 rounded"
>
key={player.id}
onClick={handleViewEdit(player.id)}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Bearbeiten
</button>
<button

View File

@@ -3,19 +3,7 @@ import { useEffect, useState } from 'react';
import { fetchTournament, updateTournament, registerTeam } from './api';
import { useAuth } from './AuthContext';
interface Team {
id: string;
name: string;
}
interface Tournament {
id: string;
name: string;
location: string;
maxParticipants: number;
organizerId: string;
teams: Team[];
}
import { Tournament } from './types';
export default function TournamentDetails() {
const { id } = useParams<{ id: string }>();

View File

@@ -1,20 +1,61 @@
import { useLocation } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
import { use, useEffect, useState } from 'react';
import { fetchTournaments, fetchTournament } from './api'; // Importiere die API-Funktion
import type { Tournament } from './types';
function useQuery() {
return new URLSearchParams(useLocation().search);
}
export default function Tournaments() {
export default function Tournament() {
const query = useQuery();
const type = query.get("type");
const location = query.get("location");
const [tournaments, setTournaments] = useState<Tournament[]>([]);
const userId = localStorage.getItem('userId') || '';
useEffect(() => {
async function loadTournaments() {
try {
const tournaments = await fetchTournaments();
setTournaments(tournaments);
console.log("Geladene Turniere:", tournaments);
} catch (error) {
console.error("Fehler beim Laden der Turniere:", error);
}
}
loadTournaments();
}, []);
// Replace this with however you get the current user's id
return (
<div className="p-6">
<h1 className="text-3xl font-bold mb-6">Meine Tourniere</h1>
<div className="max-h-64 overflow-y-auto mb-6 border rounded p-4 bg-white shadow">
<div className="flex justify-between items-center mb-4">
<span className="font-semibold text-lg">Meine Turniere</span>
<Link to="/tournament/new" className="px-3 py-1 border rounded bg-green-500 text-white">Neues Tournier erstellen</Link>
</div>
{tournaments.filter(t => t.organizerId === userId).length === 0 ? (
<p className="text-gray-500">Du bist aktuell kein Organisator eines Turniers.</p>
) : (
tournaments
.filter(t => t.organizerId === userId)
.map(t => (
<div key={t.id} className="mb-4 last:mb-0 border-b pb-2">
<div className="font-semibold">{t.name}</div>
<div className="text-sm text-gray-600">{t.date instanceof Date ? t.date.toLocaleDateString() : t.date} {t.location}</div>
</div>
))
)}
</div>
<h2 className="text-2xl font-bold mb-4">Turniere</h2>
{type && <p>Gefiltert nach: <strong>{type}</strong></p>}
{location && <p>Standort: <strong>{location}</strong></p>}
{/* TODO: Backend-Daten hier anzeigen */}
</div>
);
}

View File

@@ -0,0 +1,131 @@
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { getUserFromToken } from '../components/utils/jwt'; // Importiere die Funktion zum Decodieren des Tokens
import {fetchPlayer} from './api'; // Importiere die Funktion zum Abrufen des Spielers
interface Player {
id: string;
name: string;
email: string;
role: string;
}
const ViewEditPlayer = () => {
const { id } = useParams<{ id: string }>();
const token = localStorage.getItem('token');
const currentUser = token ? getUserFromToken(token) : null;
const isAdmin = currentUser?.role === 'admin';
const [player, setPlayer] = useState<Player | null>(null);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [role, setRole] = useState('');
const [message, setMessage] = useState('');
useEffect(() => {
const fetchData = async () => {
if (!token || !id) {
setMessage('Token oder Spieler-ID fehlt.');
return;
}
try {
const data = await fetchPlayer(token, id);
console.log("Geladener Spieler:", data);
setPlayer(data);
setName(data.name);
setEmail(data.email);
setRole(data.role);
} catch (error) {
setMessage('Spieler konnte nicht geladen werden.');
}
};
fetchData();
}, [id, token]);
const handleSave = async () => {
const res = await fetch(`/api/players/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ name, email, role }),
});
if (res.ok) {
setMessage('Spieler wurde aktualisiert.');
} else {
setMessage('Fehler beim Speichern.');
}
};
if (!player) return <p className="text-center mt-10">Lade Spieler...</p>;
return (
<div className="max-w-xl mx-auto mt-8 p-6 bg-white shadow-md rounded-md">
<h2 className="text-2xl font-semibold mb-4">Spielerprofil</h2>
<div className="space-y-4">
<div>
<label className="block font-medium mb-1">Name</label>
{isAdmin ? (
<input
type="text"
className="w-full border border-gray-300 p-2 rounded-md"
value={name}
onChange={(e) => setName(e.target.value)}
/>
) : (
<p>{player.name}</p>
)}
</div>
<div>
<label className="block font-medium mb-1">E-Mail</label>
{isAdmin ? (
<input
type="email"
className="w-full border border-gray-300 p-2 rounded-md"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
) : (
<p>{player.email}</p>
)}
</div>
<div>
<label className="block font-medium mb-1">Rolle</label>
{isAdmin ? (
<select
className="w-full border border-gray-300 p-2 rounded-md"
value={role}
onChange={(e) => setRole(e.target.value)}
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
) : (
<p>{player.role}</p>
)}
</div>
{isAdmin && (
<button
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
onClick={handleSave}
>
Speichern
</button>
)}
{message && (
<p className="text-green-600 font-medium mt-2">{message}</p>
)}
</div>
</div>
);
};
export default ViewEditPlayer;

View File

@@ -53,6 +53,15 @@ export async function deleteTournament(id: string, token: string) {
return res.json();
}
export async function fetchPlayer(token: string, id: string) {
const res = await fetch(`${API_URL}/players/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error('Fehler beim Laden des Spielers');
return res.json();
}
export async function fetchPlayers(token: string) {
const res = await fetch(`${API_URL}/players`, {
headers: { Authorization: `Bearer ${token}` },

View File

@@ -0,0 +1,14 @@
export interface Team {
id: string;
name: string;
}
export interface Tournament {
id: string;
name: string;
location: string;
maxParticipants: number;
organizerId: string;
date: Date;
teams: Team[];
}