This commit is contained in:
2025-12-31 01:17:12 +01:00
commit b7a802a52c
34 changed files with 4567 additions and 0 deletions

78
src/App.jsx Normal file
View File

@@ -0,0 +1,78 @@
import { useState, useEffect } from 'react'
import Header from './components/Header'
import FieldsSettings from './components/FieldsSettings'
import GroupSection from './components/GroupSection'
import Toast from './components/Toast'
import { useTeams } from './hooks/useTeams'
export default function App() {
const { teams, addTeam, deleteTeam, updateFields, resetAll } = useTeams()
const [toast, setToast] = useState(null)
const showToast = (message, type = 'success') => {
setToast({ message, type })
setTimeout(() => setToast(null), 3000)
}
const handleAddTeam = (group, clubName, teamName, chant) => {
if (!clubName.trim() || !teamName.trim() || !chant.trim()) {
showToast('Bitte alle Felder ausfüllen!', 'error')
return
}
addTeam(group, clubName, teamName, chant)
showToast(`${teamName} hinzugefügt! 🎉`, 'success')
}
const handleDeleteTeam = (group, teamId) => {
deleteTeam(group, teamId)
showToast('Team gelöscht', 'info')
}
const handleResetAll = () => {
if (window.confirm('Alle Teams wirklich löschen?')) {
resetAll()
showToast('Alle Teams gelöscht', 'info')
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-primary to-secondary p-4 md:p-8">
<div className="max-w-6xl mx-auto">
<Header />
<FieldsSettings
fields={teams.fields}
onUpdateFields={updateFields}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 my-8">
<GroupSection
title="<22> Volleyball Bundesliga"
group="bundesliga"
teams={teams.bundesliga}
onAddTeam={handleAddTeam}
onDeleteTeam={handleDeleteTeam}
/>
<GroupSection
title="🏆 Volleyball Champions League"
group="championsleague"
teams={teams.championsleague}
onAddTeam={handleAddTeam}
onDeleteTeam={handleDeleteTeam}
/>
</div>
<div className="text-center">
<button
onClick={handleResetAll}
className="btn-danger"
>
🗑 Alle Teams löschen
</button>
</div>
</div>
{toast && <Toast message={toast.message} type={toast.type} />}
</div>
)
}

View File

@@ -0,0 +1,54 @@
import MatchCard from './MatchCard'
export default function FieldGroup({ groupName, fields, matches, waitingList }) {
return (
<div className="h-full flex flex-col">
<div className="sticky top-0 bg-gradient-to-r from-primary to-secondary text-white p-4 rounded-t-lg">
<h2 className="text-2xl font-bold">{groupName}</h2>
<p className="text-sm opacity-90">Felder: {fields} | Teams: {matches.length * 2 + waitingList.length}</p>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{/* Matches */}
{matches.length > 0 ? (
<div>
<h3 className="text-lg font-bold text-gray-700 mb-3"> Aktuelle Paarungen</h3>
<div className="space-y-2">
{matches.map((match) => (
<div key={match.id}>
<div className="text-xs font-semibold text-gray-500 mb-1">
Feld {match.fieldNumber}
</div>
<MatchCard match={match} />
</div>
))}
</div>
</div>
) : (
<div className="text-center py-12 text-gray-400">
<p className="text-lg">Keine Teams in dieser Gruppe</p>
</div>
)}
{/* Waiting List */}
{waitingList.length > 0 && (
<div className="mt-6 pt-6 border-t-2 border-gray-300">
<h3 className="text-lg font-bold text-gray-700 mb-3"> Warteliste ({waitingList.length})</h3>
<div className="space-y-2">
{waitingList.map((team) => (
<div key={team.id} className="bg-yellow-50 border-l-4 border-yellow-400 p-3 rounded">
<p className="text-xs font-semibold text-gray-500 uppercase">
{team.clubName}
</p>
<p className="text-sm font-bold text-gray-800">
{team.teamName}
</p>
</div>
))}
</div>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,65 @@
import { useState } from 'react'
export default function FieldsSettings({ fields, onUpdateFields }) {
const [isEditing, setIsEditing] = useState(false)
const [inputValue, setInputValue] = useState(fields)
const handleSubmit = () => {
const value = parseInt(inputValue) || 3
if (value < 1) {
alert('Mindestens 1 Feld erforderlich!')
setInputValue(fields)
return
}
if (value > 100) {
alert('Maximal 100 Felder!')
setInputValue(fields)
return
}
onUpdateFields(value)
setIsEditing(false)
}
return (
<div className="bg-white rounded-lg shadow-lg p-6 mb-8">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-gray-800 mb-2"> Feld-Konfiguration</h2>
<p className="text-gray-600">Verfügbare Volleyball-Felder: <span className="font-bold text-2xl text-primary">{fields}</span></p>
</div>
{!isEditing ? (
<button
onClick={() => {
setInputValue(fields)
setIsEditing(true)
}}
className="btn-primary"
>
Bearbeiten
</button>
) : (
<div className="flex gap-3">
<input
type="number"
min="1"
max="100"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className="input-field w-20 text-center"
autoFocus
/>
<button onClick={handleSubmit} className="btn-primary btn-sm">
</button>
<button
onClick={() => setIsEditing(false)}
className="btn-sm px-3 py-2 bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold rounded-lg"
>
</button>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,51 @@
import { useState } from 'react'
import TeamForm from './TeamForm'
import TeamCard from './TeamCard'
export default function GroupSection({ title, group, teams, onAddTeam, onDeleteTeam }) {
const [isFormOpen, setIsFormOpen] = useState(false)
return (
<section className="card p-6 md:p-8 animate-fade-in">
<h2 className="text-2xl md:text-3xl font-bold text-gray-800 mb-6 pb-3 border-b-4 border-primary">
{title}
</h2>
{!isFormOpen ? (
<button
onClick={() => setIsFormOpen(true)}
className="btn-primary w-full mb-6"
>
Team hinzufügen
</button>
) : (
<div className="mb-6">
<TeamForm
group={group}
onSubmit={(clubName, teamName, chant) => {
onAddTeam(group, clubName, teamName, chant)
setIsFormOpen(false)
}}
onCancel={() => setIsFormOpen(false)}
/>
</div>
)}
<div className="space-y-3">
{teams && teams.length > 0 ? (
teams.map(team => (
<TeamCard
key={team.id}
team={team}
onDelete={() => onDeleteTeam(group, team.id)}
/>
))
) : (
<div className="text-center py-8 text-gray-400 italic">
Noch keine Teams 🏜
</div>
)}
</div>
</section>
)
}

11
src/components/Header.jsx Normal file
View File

@@ -0,0 +1,11 @@
export default function Header() {
return (
<header className="text-center text-white py-8 md:py-12">
<h1 className="text-4xl md:text-5xl font-bold mb-3 drop-shadow-lg">
🏐 NVJ-Spielfest Planer
</h1>
<p className="text-lg md:text-xl opacity-90">
</p>
</header>
)
}

View File

@@ -0,0 +1,32 @@
export default function MatchCard({ match }) {
return (
<div className="card p-4 rounded-lg bg-white border-l-4 border-primary hover:shadow-md transition-shadow">
<div className="flex items-center justify-between gap-4">
{/* Team 1 */}
<div className="flex-1 text-right min-w-0">
<p className="text-xs font-semibold text-gray-500 uppercase truncate">
{match.team1.clubName}
</p>
<p className="text-sm font-bold text-gray-800 truncate">
{match.team1.teamName}
</p>
</div>
{/* VS */}
<div className="flex-shrink-0 px-4">
<span className="text-xl font-bold text-gray-400">VS</span>
</div>
{/* Team 2 */}
<div className="flex-1 min-w-0">
<p className="text-xs font-semibold text-gray-500 uppercase truncate">
{match.team2.clubName}
</p>
<p className="text-sm font-bold text-gray-800 truncate">
{match.team2.teamName}
</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,58 @@
export default function ScoreboardModal({ isOpen, onClose, matches, groupName }) {
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-gray-900 rounded-lg shadow-2xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto border border-gray-700">
{/* Header */}
<div className="sticky top-0 bg-gradient-to-r from-primary to-secondary p-6 border-b border-gray-700">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-white">📊 Scoreboard</h2>
<button
onClick={onClose}
className="text-white text-2xl hover:text-gray-300 transition-colors"
>
</button>
</div>
<p className="text-gray-100 mt-2">{groupName}</p>
</div>
{/* Matches */}
<div className="p-6 space-y-3">
{matches.length === 0 ? (
<div className="text-center text-gray-400 py-8">
Keine Spiele
</div>
) : (
matches.map(match => (
<div key={match.id} className="bg-gray-800 rounded-lg p-4 border border-gray-700 hover:border-primary transition-colors">
<div className="flex items-center justify-between">
{/* Team 1 */}
<div className="flex-1">
<p className="text-sm text-gray-400 uppercase tracking-wide">{match.team1.clubName}</p>
<p className="font-bold text-lg text-white">{match.team1.teamName}</p>
</div>
{/* Score */}
<div className="text-center px-6">
<div className="text-4xl font-mono font-bold text-primary">
{match.score1 !== undefined ? match.score1 : '-'} : {match.score2 !== undefined ? match.score2 : '-'}
</div>
<p className="text-xs text-gray-500 mt-1">Feld {match.fieldNumber}</p>
</div>
{/* Team 2 */}
<div className="flex-1 text-right">
<p className="text-sm text-gray-400 uppercase tracking-wide">{match.team2.clubName}</p>
<p className="font-bold text-lg text-white">{match.team2.teamName}</p>
</div>
</div>
</div>
))
)}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,49 @@
import { useStopwatch } from '../hooks/useStopwatch'
export default function Stopwatch() {
const { time, isRunning, inputMinutes, togglePlay, reset, setMinutes, formatTime } = useStopwatch()
return (
<div className="flex items-center gap-2 bg-gray-900 px-4 py-2 rounded-lg border border-gray-700">
{/* Zeit Eingabe */}
<div className="flex items-center gap-1">
<input
type="number"
min="0"
max="99"
value={inputMinutes}
onChange={(e) => setMinutes(e.target.value)}
disabled={isRunning}
className="w-12 bg-gray-800 text-white text-center rounded px-2 py-1 text-sm border border-gray-600 focus:border-primary disabled:opacity-50"
/>
<span className="text-gray-400 text-xs">min</span>
</div>
{/* Zeitanzeige */}
<div className="text-2xl font-mono font-bold text-primary min-w-[80px] text-center">
{formatTime(time)}
</div>
{/* Play/Pause Button */}
<button
onClick={togglePlay}
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all ${
isRunning
? 'bg-red-600 hover:bg-red-700'
: 'bg-green-600 hover:bg-green-700'
} text-white font-bold shadow-lg hover:shadow-xl`}
>
{isRunning ? '⏸' : '▶'}
</button>
{/* Reset Button */}
<button
onClick={reset}
className="w-10 h-10 rounded-full flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white text-lg transition-all shadow-lg hover:shadow-xl"
title="Reset"
>
</button>
</div>
)
}

View File

@@ -0,0 +1,23 @@
export default function TeamCard({ team, onDelete }) {
return (
<div className="bg-gradient-to-r from-gray-50 to-gray-100 border-l-4 border-primary rounded-lg p-4 hover:shadow-md transition-shadow animate-slide-in">
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-1">
{team.clubName}
</p>
<h3 className="text-lg font-bold text-gray-800 mb-2">🏐 {team.teamName}</h3>
<p className="text-gray-600 italic mb-2">"{team.chant}"</p>
<p className="text-xs text-gray-400">{team.createdAt}</p>
</div>
<button
onClick={onDelete}
className="btn-danger btn-sm ml-3 flex-shrink-0"
title="Team löschen"
>
🗑
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,97 @@
import { useState } from 'react'
export default function TeamForm({ group, onSubmit, onCancel }) {
const [clubName, setClubName] = useState('')
const [teamName, setTeamName] = useState('')
const [chant, setChant] = useState('')
const [errors, setErrors] = useState({})
const handleSubmit = (e) => {
e.preventDefault()
const newErrors = {}
if (!clubName.trim()) newErrors.clubName = 'Vereinsname erforderlich'
if (!teamName.trim()) newErrors.teamName = 'Team-Name erforderlich'
if (!chant.trim()) newErrors.chant = 'Schlachtruf erforderlich'
if (clubName.length > 50) newErrors.clubName = 'Max. 50 Zeichen'
if (teamName.length > 50) newErrors.teamName = 'Max. 50 Zeichen'
if (chant.length > 100) newErrors.chant = 'Max. 100 Zeichen'
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
onSubmit(clubName, teamName, chant)
setClubName('')
setTeamName('')
setChant('')
setErrors({})
}
return (
<form onSubmit={handleSubmit} className="space-y-4 animate-slide-down">
<div>
<input
type="text"
placeholder="Vereinsname"
value={clubName}
onChange={(e) => {
setClubName(e.target.value)
if (errors.clubName) setErrors({ ...errors, clubName: '' })
}}
maxLength={50}
className="input-field"
autoFocus
/>
{errors.clubName && <p className="text-red-500 text-sm mt-1">{errors.clubName}</p>}
<p className="text-gray-400 text-xs mt-1">{clubName.length}/50</p>
</div>
<div>
<input
type="text"
placeholder="Team Name"
value={teamName}
onChange={(e) => {
setTeamName(e.target.value)
if (errors.teamName) setErrors({ ...errors, teamName: '' })
}}
maxLength={50}
className="input-field"
/>
{errors.teamName && <p className="text-red-500 text-sm mt-1">{errors.teamName}</p>}
<p className="text-gray-400 text-xs mt-1">{teamName.length}/50</p>
</div>
<div>
<input
type="text"
placeholder="Schlachtruf"
value={chant}
onChange={(e) => {
setChant(e.target.value)
if (errors.chant) setErrors({ ...errors, chant: '' })
}}
maxLength={100}
className="input-field"
/>
{errors.chant && <p className="text-red-500 text-sm mt-1">{errors.chant}</p>}
<p className="text-gray-400 text-xs mt-1">{chant.length}/100</p>
</div>
<div className="flex gap-3">
<button type="submit" className="btn-primary flex-1">
Hinzufügen
</button>
<button
type="button"
onClick={onCancel}
className="flex-1 px-4 py-2 bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold rounded-lg transition-colors"
>
Abbrechen
</button>
</div>
</form>
)
}

13
src/components/Toast.jsx Normal file
View File

@@ -0,0 +1,13 @@
export default function Toast({ message, type = 'success' }) {
const bgColor = {
success: 'bg-green-500',
error: 'bg-red-500',
info: 'bg-blue-500'
}[type] || 'bg-green-500'
return (
<div className={`fixed bottom-4 right-4 ${bgColor} text-white px-6 py-3 rounded-lg shadow-lg animate-slide-up`}>
{message}
</div>
)
}

75
src/hooks/useMatches.js Normal file
View File

@@ -0,0 +1,75 @@
import { useState, useEffect } from 'react'
export function useMatches(teams, fields, group) {
const [matches, setMatches] = useState([])
const [waitingList, setWaitingList] = useState([])
// Matches generieren wenn Teams oder Felder sich ändern
useEffect(() => {
generateMatches()
}, [teams, fields, group])
const generateMatches = () => {
const groupTeams = teams[group] || []
if (groupTeams.length < 2) {
setMatches([])
setWaitingList([])
return
}
// Matches berechnen: jedes Team spielt gegen jedes andere Team
// Pro Runde: Teams / 2 = Matches gleichzeitig
const matchesNeeded = Math.ceil(groupTeams.length / 2)
const canPlayPerRound = Math.min(matchesNeeded, fields)
const teamsPerRound = canPlayPerRound * 2
const waitingCount = groupTeams.length - teamsPerRound
// Erste Runde: Teams pairen
const currentMatches = []
for (let i = 0; i < canPlayPerRound; i++) {
if (groupTeams[i * 2] && groupTeams[i * 2 + 1]) {
currentMatches.push({
id: `match-${group}-${i}`,
fieldNumber: i + 1,
team1: groupTeams[i * 2],
team2: groupTeams[i * 2 + 1],
status: 'upcoming', // upcoming, playing, finished
score1: 0,
score2: 0
})
}
}
const waiting = groupTeams.slice(teamsPerRound)
setMatches(currentMatches)
setWaitingList(waiting)
}
const updateMatchScore = (matchId, score1, score2) => {
setMatches(matches.map(m =>
m.id === matchId ? { ...m, score1, score2 } : m
))
}
const startMatch = (matchId) => {
setMatches(matches.map(m =>
m.id === matchId ? { ...m, status: 'playing' } : m
))
}
const finishMatch = (matchId) => {
setMatches(matches.map(m =>
m.id === matchId ? { ...m, status: 'finished' } : m
))
}
return {
matches,
waitingList,
updateMatchScore,
startMatch,
finishMatch
}
}

53
src/hooks/useStopwatch.js Normal file
View File

@@ -0,0 +1,53 @@
import { useState, useEffect } from 'react'
export function useStopwatch() {
const [time, setTime] = useState(0)
const [isRunning, setIsRunning] = useState(false)
const [inputMinutes, setInputMinutes] = useState('5')
useEffect(() => {
let interval
if (isRunning && time > 0) {
interval = setInterval(() => {
setTime(t => t - 1)
}, 1000)
} else if (time === 0 && isRunning) {
setIsRunning(false)
}
return () => clearInterval(interval)
}, [isRunning, time])
const togglePlay = () => {
setIsRunning(!isRunning)
}
const reset = () => {
setIsRunning(false)
const minutes = parseInt(inputMinutes) || 5
setTime(minutes * 60)
}
const setMinutes = (minutes) => {
const m = parseInt(minutes) || 5
setInputMinutes(m.toString())
if (!isRunning) {
setTime(m * 60)
}
}
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
return {
time,
isRunning,
inputMinutes,
togglePlay,
reset,
setMinutes,
formatTime
}
}

94
src/hooks/useTeams.js Normal file
View File

@@ -0,0 +1,94 @@
import { useState, useEffect } from 'react'
const STORAGE_KEY = 'teamManagerData'
const initialState = {
bundesliga: [],
championsleague: [],
fields: 3 // Standard: 3 Felder
}
export function useTeams() {
const [teams, setTeams] = useState(initialState)
// Daten aus localStorage laden beim Mount
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
try {
const data = JSON.parse(stored)
// Fallback für alte Version ohne fields
setTeams({
bundesliga: data.bundesliga || [],
championsleague: data.championsleague || [],
fields: data.fields !== undefined ? data.fields : 3
})
} catch (error) {
console.error('Fehler beim Laden der Teams:', error)
setTeams(initialState)
}
}
}, [])
// Daten in localStorage speichern bei Änderungen
const saveToStorage = (newTeams) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(newTeams))
}
const addTeam = (group, clubName, teamName, chant) => {
const newTeam = {
id: Date.now(),
clubName: clubName.trim(),
teamName: teamName.trim(),
chant: chant.trim(),
createdAt: new Date().toLocaleString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
const newTeams = {
...teams,
[group]: [...teams[group], newTeam]
}
setTeams(newTeams)
saveToStorage(newTeams)
}
const deleteTeam = (group, teamId) => {
const newTeams = {
...teams,
[group]: teams[group].filter(team => team.id !== teamId)
}
setTeams(newTeams)
saveToStorage(newTeams)
}
const updateFields = (fieldCount) => {
const newTeams = {
...teams,
fields: Math.max(1, parseInt(fieldCount) || 3)
}
setTeams(newTeams)
saveToStorage(newTeams)
}
const resetAll = () => {
setTeams(initialState)
localStorage.removeItem(STORAGE_KEY)
}
return {
teams,
addTeam,
deleteTeam,
updateFields,
resetAll
}
}

25
src/index.css Normal file
View File

@@ -0,0 +1,25 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn-primary {
@apply px-4 py-2 bg-primary hover:bg-secondary text-white font-bold rounded-lg transition-colors duration-200 cursor-pointer;
}
.btn-danger {
@apply px-4 py-2 bg-red-500 hover:bg-red-600 text-white font-bold rounded-lg transition-colors duration-200 cursor-pointer;
}
.btn-sm {
@apply px-3 py-1 text-sm;
}
.input-field {
@apply w-full px-4 py-2 border-2 border-gray-200 rounded-lg focus:outline-none focus:border-primary transition-colors duration-200;
}
.card {
@apply bg-white rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-200;
}
}

19
src/main.jsx Normal file
View File

@@ -0,0 +1,19 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import SetupPage from './pages/SetupPage'
import TournamentPage from './pages/TournamentPage'
import ScoringPage from './pages/ScoringPage'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<SetupPage />} />
<Route path="/tournament" element={<TournamentPage />} />
<Route path="/scoring" element={<ScoringPage />} />
</Routes>
</BrowserRouter>
</React.StrictMode>,
)

131
src/pages/ScoringPage.jsx Normal file
View File

@@ -0,0 +1,131 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTeams } from '../hooks/useTeams'
import { useMatches } from '../hooks/useMatches'
import Header from '../components/Header'
export default function ScoringPage() {
const navigate = useNavigate()
const { teams } = useTeams()
const { matches: championsMatches } = useMatches(teams.championsleague, teams.fields)
const { matches: bundesligaMatches } = useMatches(teams.bundesliga, teams.fields)
const [scores, setScores] = useState({})
const [selectedGroup, setSelectedGroup] = useState('championsleague')
const allMatches = selectedGroup === 'championsleague' ? championsMatches : bundesligaMatches
const handleScoreChange = (matchId, team, value) => {
setScores(prev => ({
...prev,
[matchId]: {
...prev[matchId],
[team]: parseInt(value) || 0
}
}))
}
const handleSaveScore = (matchId) => {
// TODO: Hier könnte man die Scores speichern
console.log('Scores gespeichert:', scores[matchId])
}
return (
<div className="min-h-screen bg-gradient-to-b from-gray-900 to-gray-800">
<Header />
<div className="max-w-4xl mx-auto p-4">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-white">📊 Punkte Eintragen</h1>
<button
onClick={() => navigate('/tournament')}
className="btn-primary"
>
Zurück
</button>
</div>
{/* Gruppen Toggle */}
<div className="flex gap-2 mb-6">
<button
onClick={() => setSelectedGroup('championsleague')}
className={`px-4 py-2 rounded-lg font-semibold transition-all ${
selectedGroup === 'championsleague'
? 'bg-primary text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
🏆 Champions League
</button>
<button
onClick={() => setSelectedGroup('bundesliga')}
className={`px-4 py-2 rounded-lg font-semibold transition-all ${
selectedGroup === 'bundesliga'
? 'bg-primary text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
Bundesliga
</button>
</div>
{/* Matches zum Scores eingeben */}
<div className="space-y-4">
{allMatches.length === 0 ? (
<div className="bg-gray-800 rounded-lg p-6 text-center text-gray-400">
Keine Spiele in dieser Gruppe
</div>
) : (
allMatches.map(match => (
<div key={match.id} className="bg-gray-800 rounded-lg p-4 border border-gray-700">
<div className="flex items-center justify-between">
{/* Team 1 */}
<div className="flex-1">
<p className="text-sm text-gray-400">{match.team1.clubName}</p>
<p className="font-bold text-white">{match.team1.teamName}</p>
</div>
{/* Score Input */}
<div className="flex items-center gap-3">
<input
type="number"
min="0"
max="999"
value={scores[match.id]?.team1 || ''}
onChange={(e) => handleScoreChange(match.id, 'team1', e.target.value)}
placeholder="0"
className="w-16 bg-gray-700 text-white text-center text-xl font-bold rounded px-2 py-1 border border-gray-600 focus:border-primary"
/>
<span className="text-xl font-bold text-gray-400">:</span>
<input
type="number"
min="0"
max="999"
value={scores[match.id]?.team2 || ''}
onChange={(e) => handleScoreChange(match.id, 'team2', e.target.value)}
placeholder="0"
className="w-16 bg-gray-700 text-white text-center text-xl font-bold rounded px-2 py-1 border border-gray-600 focus:border-primary"
/>
</div>
{/* Team 2 */}
<div className="flex-1 text-right">
<p className="text-sm text-gray-400">{match.team2.clubName}</p>
<p className="font-bold text-white">{match.team2.teamName}</p>
</div>
{/* Speichern Button */}
<button
onClick={() => handleSaveScore(match.id)}
className="ml-4 btn-primary btn-sm"
>
</button>
</div>
</div>
))
)}
</div>
</div>
</div>
)
}

95
src/pages/SetupPage.jsx Normal file
View File

@@ -0,0 +1,95 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import Header from '../components/Header'
import FieldsSettings from '../components/FieldsSettings'
import GroupSection from '../components/GroupSection'
import Toast from '../components/Toast'
import { useTeams } from '../hooks/useTeams'
export default function SetupPage() {
const { teams, addTeam, deleteTeam, updateFields, resetAll } = useTeams()
const [toast, setToast] = useState(null)
const navigate = useNavigate()
const showToast = (message, type = 'success') => {
setToast({ message, type })
setTimeout(() => setToast(null), 3000)
}
const handleAddTeam = (group, clubName, teamName, chant) => {
if (!clubName.trim() || !teamName.trim() || !chant.trim()) {
showToast('Bitte alle Felder ausfüllen!', 'error')
return
}
addTeam(group, clubName, teamName, chant)
showToast(`${teamName} hinzugefügt! 🎉`, 'success')
}
const handleDeleteTeam = (group, teamId) => {
deleteTeam(group, teamId)
showToast('Team gelöscht', 'info')
}
const handleStartTournament = () => {
const totalTeams = teams.bundesliga.length + teams.championsleague.length
if (totalTeams < 2) {
showToast('Du brauchst mindestens 2 Teams!', 'error')
return
}
navigate('/tournament')
}
const handleResetAll = () => {
if (window.confirm('Alle Teams wirklich löschen?')) {
resetAll()
showToast('Alle Teams gelöscht', 'info')
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-primary to-secondary p-4 md:p-8">
<div className="max-w-6xl mx-auto">
<Header />
<FieldsSettings
fields={teams.fields}
onUpdateFields={updateFields}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 my-8">
<GroupSection
title="🏐 Volleyball Bundesliga"
group="bundesliga"
teams={teams.bundesliga}
onAddTeam={handleAddTeam}
onDeleteTeam={handleDeleteTeam}
/>
<GroupSection
title="🏆 Volleyball Champions League"
group="championsleague"
teams={teams.championsleague}
onAddTeam={handleAddTeam}
onDeleteTeam={handleDeleteTeam}
/>
</div>
<div className="flex gap-4 justify-center flex-wrap">
<button
onClick={handleStartTournament}
className="btn-primary text-lg px-8 py-3"
>
🎮 Turnier Starten
</button>
<button
onClick={handleResetAll}
className="btn-danger text-lg px-8 py-3"
>
🗑 Alle Teams löschen
</button>
</div>
</div>
{toast && <Toast message={toast.message} type={toast.type} />}
</div>
)
}

View File

@@ -0,0 +1,110 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTeams } from '../hooks/useTeams'
import { useMatches } from '../hooks/useMatches'
import FieldGroup from '../components/FieldGroup'
import Stopwatch from '../components/Stopwatch'
import ScoreboardModal from '../components/ScoreboardModal'
export default function TournamentPage() {
const navigate = useNavigate()
const { teams } = useTeams()
const [scoreboardOpen, setScoreboardOpen] = useState(false)
const [scoreboardGroup, setScoreboardGroup] = useState('championsleague')
const bundesliga = useMatches(teams, teams.fields, 'bundesliga')
const championsleague = useMatches(teams, teams.fields, 'championsleague')
const handleShowScoreboard = (group) => {
setScoreboardGroup(group)
setScoreboardOpen(true)
}
return (
<div className="flex flex-col h-screen bg-gray-900 text-white">
{/* Header */}
<div className="bg-gradient-to-r from-primary to-secondary p-4 shadow-lg">
<div className="flex justify-between items-center">
<h1 className="text-2xl md:text-3xl font-bold">🏐 Volleyball Turnier Manager</h1>
{/* Center Buttons */}
<div className="flex gap-2">
<button
onClick={() => navigate('/scoring')}
className="btn-primary"
title="Punkte eintragen"
>
📝 Punkte
</button>
<button
onClick={() => handleShowScoreboard('championsleague')}
className="btn-primary"
title="Scoreboard anzeigen"
>
📊 Ergebnisse
</button>
</div>
{/* Right Side */}
<div className="flex gap-2 items-center">
<Stopwatch />
<button
onClick={() => navigate('/')}
className="btn-primary"
title="Zurück zur Team-Verwaltung"
>
Zurück
</button>
</div>
</div>
</div>
{/* Main Content - Two Column Layout */}
<div className="flex-1 overflow-hidden grid grid-cols-1 md:grid-cols-2 gap-4 p-4">
{/* Champions League */}
<div className="bg-white text-gray-800 rounded-lg shadow-xl overflow-hidden flex flex-col">
<FieldGroup
groupName="🏆 Champions League"
fields={teams.fields}
matches={championsleague.matches}
waitingList={championsleague.waitingList}
/>
</div>
{/* Bundesliga */}
<div className="bg-white text-gray-800 rounded-lg shadow-xl overflow-hidden flex flex-col">
<FieldGroup
groupName="🏐 Bundesliga"
fields={teams.fields}
matches={bundesliga.matches}
waitingList={bundesliga.waitingList}
/>
</div>
</div>
{/* Footer Stats */}
<div className="bg-gray-800 border-t border-gray-700 p-4 flex justify-around text-sm">
<div>
<p className="text-gray-400">Champions League</p>
<p className="text-lg font-bold">{championsleague.matches.length} aktiv | {championsleague.waitingList.length} warten</p>
</div>
<div className="border-l border-gray-700 pl-4">
<p className="text-gray-400">Bundesliga</p>
<p className="text-lg font-bold">{bundesliga.matches.length} aktiv | {bundesliga.waitingList.length} warten</p>
</div>
<div className="border-l border-gray-700 pl-4">
<p className="text-gray-400">Verfügbare Felder</p>
<p className="text-lg font-bold">{teams.fields}</p>
</div>
</div>
{/* Scoreboard Modal */}
<ScoreboardModal
isOpen={scoreboardOpen}
onClose={() => setScoreboardOpen(false)}
matches={scoreboardGroup === 'championsleague' ? championsleague.matches : bundesliga.matches}
groupName={scoreboardGroup === 'championsleague' ? '🏆 Champions League' : '🏐 Bundesliga'}
/>
</div>
)
}