'use client' import { useEffect, useState, useCallback } from 'react' import { useSession } from 'next-auth/react' import { useRouter } from 'next/navigation' import Link from 'next/link' import Image from 'next/image' import { format } from 'date-fns' import { de } from 'date-fns/locale' import Switch from '@/app/components/Switch' import Button from './Button' import Modal from './Modal' import { Match } from '../types/match' import { useSSEStore } from '@/app/lib/useSSEStore' type Props = { matchType?: string } const getTeamLogo = (logo?: string | null) => logo ? `/assets/img/logos/${logo}` : '/assets/img/logos/cs2.webp' const toDateKey = (d: Date) => d.toISOString().slice(0, 10) const weekdayDE = new Intl.DateTimeFormat('de-DE', { weekday: 'long' }) type TeamOption = { id: string; name: string; logo?: string | null } /** lokale Date+Time -> ISO (bewahrt lokale Uhrzeit) */ function combineLocalDateTime(dateStr: string, timeStr: string) { const [y, m, d] = dateStr.split('-').map(Number) const [hh, mm] = timeStr.split(':').map(Number) const dt = new Date(y, (m - 1), d, hh, mm, 0, 0) // lokale Zeit return dt.toISOString() } /** nächste volle Stunde als Default */ function getNextHourDefaults() { const now = new Date() now.setMinutes(0, 0, 0) now.setHours(now.getHours() + 1) const dateStr = now.toISOString().slice(0, 10) const timeStr = now.toTimeString().slice(0, 5) // HH:MM return { dateStr, timeStr } } export default function CommunityMatchList({ matchType }: Props) { const { data: session } = useSession() const router = useRouter() const { lastEvent } = useSSEStore() const [matches, setMatches] = useState([]) const [onlyOwn, setOnlyOwn] = useState(false) // Modal-States const [showCreate, setShowCreate] = useState(false) const [teams, setTeams] = useState([]) const [loadingTeams, setLoadingTeams] = useState(false) const [saving, setSaving] = useState(false) const [teamAId, setTeamAId] = useState('') const [teamBId, setTeamBId] = useState('') const [title, setTitle] = useState('') // auto editierbar const [autoTitle, setAutoTitle] = useState(true) const [bestOf, setBestOf] = useState<3 | 5>(3) // Datum & Uhrzeit const defaults = getNextHourDefaults() const [matchDateStr, setMatchDateStr] = useState(defaults.dateStr) // YYYY-MM-DD const [matchTimeStr, setMatchTimeStr] = useState(defaults.timeStr) // HH:MM const teamById = useCallback( (id?: string) => teams.find(t => t.id === id), [teams] ) useEffect(() => { const id = setInterval(() => { // force re-render, damit isOpen (Vergleich mit Date.now) neu bewertet wird setMatches(m => [...m]) }, 30_000) return () => clearInterval(id) }, []) // Auto-Titel useEffect(() => { if (!autoTitle) return const a = teamById(teamAId)?.name ?? 'Team A' const b = teamById(teamBId)?.name ?? 'Team B' setTitle(`${a} vs ${b}`) }, [teamAId, teamBId, autoTitle, teamById]) // Matches laden const loadMatches = useCallback(async () => { const url = `/api/matches${matchType ? `?type=${encodeURIComponent(matchType)}` : ''}` try { const r = await fetch(url, { cache: 'no-store' }) const data = r.ok ? await r.json() : [] setMatches(data) } catch (err) { console.error('[MatchList] Laden fehlgeschlagen:', err) } }, [matchType]) useEffect(() => { loadMatches() }, [loadMatches]) useEffect(() => { if (!lastEvent) return // auf diese Typen reagieren const TRIGGER_TYPES = new Set([ 'match-created', 'matches-updated', 'match-deleted', 'map-vote-updated', // damit das Map-Vote Badge live aktualisiert ]) if (!TRIGGER_TYPES.has(lastEvent.type)) return // kurzer Cooldown, falls mehrere Events gleichzeitig eintreffen let cancelled = false const t = setTimeout(async () => { if (!cancelled) await loadMatches() }, 150) return () => { cancelled = true; clearTimeout(t) } }, [lastEvent, loadMatches]) // Teams laden, wenn Modal aufgeht useEffect(() => { if (!showCreate || teams.length) return ;(async () => { setLoadingTeams(true) try { const res = await fetch('/api/teams', { cache: 'no-store' }) const json = await res.json() const opts: TeamOption[] = (json.teams ?? []).map((t: any) => ({ id: t.id, name: t.name, logo: t.logo, })) setTeams(opts) } catch (e) { console.error('[MatchList] /api/teams fehlgeschlagen:', e) setTeams([]) } finally { setLoadingTeams(false) } })() }, [showCreate, teams.length]) const resetCreateState = () => { setTeamAId('') setTeamBId('') setTitle('') setAutoTitle(true) setBestOf(3) const d = getNextHourDefaults() setMatchDateStr(d.dateStr) setMatchTimeStr(d.timeStr) } const canSave = !saving && teamAId && teamBId && teamAId !== teamBId && title.trim().length > 0 && (bestOf === 3 || bestOf === 5) && !!matchDateStr && !!matchTimeStr const handleCreate = async () => { if (!canSave) return setSaving(true) try { const matchDateISO = combineLocalDateTime(matchDateStr, matchTimeStr) const res = await fetch('/api/matches/create', { method : 'POST', headers: { 'Content-Type': 'application/json' }, body : JSON.stringify({ teamAId, teamBId, title: title.trim(), bestOf, // 3 | 5 matchDate: matchDateISO, // <- Datum+Zeit type : matchType, // optional }), }) if (!res.ok) { const j = await res.json().catch(() => ({})) alert(j.message ?? 'Erstellen fehlgeschlagen') return } setShowCreate(false) resetCreateState() await loadMatches() } catch (e) { console.error('[MatchList] Match erstellen fehlgeschlagen:', e) alert('Match konnte nicht erstellt werden.') } finally { setSaving(false) } } // Gruppieren const grouped = (() => { const sorted = [...matches].sort( (a, b) => new Date(a.demoDate).getTime() - new Date(b.demoDate).getTime(), ) const map = new Map() for (const m of sorted) { const key = toDateKey(new Date(m.demoDate)) map.set(key, [...(map.get(key) ?? []), m]) } return Array.from(map.entries()) })() return (
{/* Kopfzeile */}

Geplante Matches

{session?.user?.isAdmin && ( )}
{/* Inhalt */} {grouped.length === 0 ? (

Keine Matches geplant.

) : (
{grouped.map(([dateKey, dayMatches], dayIdx) => { const dateObj = new Date(dateKey + 'T00:00:00') const dayLabel = `Tag #${dayIdx + 1} – ${weekdayDE.format(dateObj)}` return (
{dayLabel}
{dateKey}
{dayMatches.map((m: Match) => { const started = new Date(m.demoDate).getTime() <= Date.now() const unfinished = !m.winnerTeam && m.scoreA == null && m.scoreB == null const isLive = started && unfinished const isOwnTeam = !!session?.user?.team && (m.teamA.id === session.user.team || m.teamB.id === session.user.team) const dimmed = onlyOwn && !isOwnTeam return ( {isLive && ( LIVE )} {/* Map-Vote Badge */} {m.mapVote && ( {m.mapVote.isOpen ? (m.mapVote.status === 'completed' ? 'Map-Vote abgeschlossen' : 'Map-Vote offen') : m.mapVote.opensAt ? `Map-Vote ab ${format(new Date(m.mapVote.opensAt), 'HH:mm', { locale: de })} Uhr` : 'Map-Vote bald'} )}
{m.teamA.name} {m.teamA.name}
vs
{m.teamB.name} {m.teamB.name}
{format(new Date(m.demoDate), 'dd.MM.yyyy', { locale: de })} {format(new Date(m.demoDate), 'HH:mm', { locale: de })} Uhr
) })}
) })}
)} {/* Modal: Match erstellen */} { setShowCreate(false); resetCreateState() }} onSave={handleCreate} closeButtonTitle={saving ? 'Speichern …' : 'Erstellen'} closeButtonColor="blue" disableCloseButton={!canSave} >
{/* Team A */} {/* Team B */} {/* Titel */}
{ setTitle(e.target.value); setAutoTitle(false) }} onFocus={() => setAutoTitle(false)} className="w-full rounded-lg border border-gray-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 px-3 py-2 text-sm" placeholder="Team A vs Team B" /> {!autoTitle && ( )}
{/* Datum & Uhrzeit */}
setMatchDateStr(e.target.value)} className="w-full rounded-lg border border-gray-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 px-3 py-2 text-sm" />
setMatchTimeStr(e.target.value)} step={300} // 5-Minuten-Schritte className="w-full rounded-lg border border-gray-300 dark:border-neutral-700 bg-white dark:bg-neutral-800 px-3 py-2 text-sm" />

Die Uhrzeit wird als lokale Zeit gespeichert.

{/* Best-of */}
{loadingTeams && (

Teams werden geladen …

)} {teamAId && teamBId && teamAId === teamBId && (

Bitte zwei unterschiedliche Teams wählen.

)}
) }