"use client" import type React from "react" import { useEffect, useMemo, useState } from "react" import Link from "next/link" import { ArrowLeft, Check, MoreVertical, Plus, Star, Trash2, UserPlus, Users, X, } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { cn } from "@/lib/utils" // --- Types (matcher API-kontrakten i app/routers/tournaments.py/players.py) - type ApiPlayer = { id: string display_name: string handicap_index: number | null } type ApiRosterEntry = { id: string player_id: string display_name: string handicap_index_snapshot: number | null is_captain: boolean } type Team = { id: string name: string color: string roster: ApiRosterEntry[] } // Team colors er DATA brukt på scorekort senere -- bevisst atskilt fra // app-ens merkevare-grønn/oransje. const TEAM_COLORS: { value: string; label: string }[] = [ { value: "#2563eb", label: "Blå" }, { value: "#dc2626", label: "Rød" }, { value: "#7c3aed", label: "Lilla" }, { value: "#0891b2", label: "Turkis" }, { value: "#db2777", label: "Rosa" }, { value: "#475569", label: "Skifer" }, ] // --- Component ------------------------------------------------------------- export function TournamentDetail({ organizationId, tournamentId, tournamentName, }: { organizationId: string tournamentId: string tournamentName: string }) { const [teams, setTeams] = useState<[Team | null, Team | null]>([null, null]) const [pool, setPool] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { let cancelled = false async function load() { try { const [teamsRes, poolRes] = await Promise.all([ fetch(`/orgs/${organizationId}/tournaments/${tournamentId}/teams`, { credentials: "include", }), fetch(`/orgs/${organizationId}/players`, { credentials: "include" }), ]) if (!teamsRes.ok || !poolRes.ok) throw new Error("load failed") const teamsData: { id: string; name: string; color: string | null }[] = await teamsRes.json() const poolData: ApiPlayer[] = await poolRes.json() const withRosters = await Promise.all( teamsData.map(async (t) => { const rosterRes = await fetch(`/orgs/${organizationId}/teams/${t.id}/roster`, { credentials: "include", }) const roster: ApiRosterEntry[] = rosterRes.ok ? await rosterRes.json() : [] return { id: t.id, name: t.name, color: t.color ?? TEAM_COLORS[0].value, roster } }), ) if (cancelled) return setTeams([withRosters[0] ?? null, withRosters[1] ?? null]) setPool(poolData) } catch { if (!cancelled) setError("Klarte ikke å laste lag og spillere. Prøv å laste siden på nytt.") } finally { if (!cancelled) setLoading(false) } } void load() return () => { cancelled = true } }, [organizationId, tournamentId]) async function createTeam(slot: 0 | 1, name: string, color: string) { setError(null) try { const res = await fetch(`/orgs/${organizationId}/tournaments/${tournamentId}/teams`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ name, color }), }) if (!res.ok) throw new Error(`create team: ${res.status}`) const created: { id: string; name: string; color: string | null } = await res.json() setTeams((prev) => { const next: [Team | null, Team | null] = [prev[0], prev[1]] next[slot] = { id: created.id, name: created.name, color: created.color ?? color, roster: [] } return next }) } catch { setError("Klarte ikke å opprette laget. Prøv igjen.") } } async function addExistingPlayer(teamId: string, playerId: string) { setError(null) try { const res = await fetch(`/orgs/${organizationId}/teams/${teamId}/roster`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ player_id: playerId, is_captain: false }), }) if (!res.ok) throw new Error(`add roster: ${res.status}`) const entry: ApiRosterEntry = await res.json() setTeams((prev) => prev.map((t) => (t && t.id === teamId ? { ...t, roster: [...t.roster, entry] } : t)) as [ Team | null, Team | null, ], ) } catch { setError("Klarte ikke å legge til spilleren. Prøv igjen.") } } async function addNewPlayer(teamId: string, name: string, handicap?: number) { setError(null) try { const playerRes = await fetch(`/orgs/${organizationId}/players`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ display_name: name, handicap_index: handicap ?? null }), }) if (!playerRes.ok) throw new Error(`create player: ${playerRes.status}`) const player: ApiPlayer = await playerRes.json() setPool((prev) => [...prev, player]) await addExistingPlayer(teamId, player.id) } catch { setError("Klarte ikke å opprette spilleren. Prøv igjen.") } } async function toggleCaptain(teamId: string, rosterId: string, current: boolean) { setError(null) try { const res = await fetch(`/orgs/${organizationId}/teams/${teamId}/roster/${rosterId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ is_captain: !current }), }) if (!res.ok) throw new Error(`toggle captain: ${res.status}`) const updated: ApiRosterEntry = await res.json() setTeams((prev) => prev.map((t) => t && t.id === teamId ? { ...t, roster: t.roster.map((r) => (r.id === updated.id ? updated : r)) } : t, ) as [Team | null, Team | null], ) } catch { setError("Klarte ikke å endre kaptein. Prøv igjen.") } } async function removePlayer(teamId: string, rosterId: string) { setError(null) try { const res = await fetch(`/orgs/${organizationId}/teams/${teamId}/roster/${rosterId}`, { method: "DELETE", credentials: "include", }) if (!res.ok && res.status !== 204) throw new Error(`remove roster: ${res.status}`) setTeams((prev) => prev.map((t) => t && t.id === teamId ? { ...t, roster: t.roster.filter((r) => r.id !== rosterId) } : t, ) as [Team | null, Team | null], ) } catch { setError("Klarte ikke å fjerne spilleren. Prøv igjen.") } } // Hvilket lag er en gitt spiller (fra pool) allerede rostret på, om noen? function findPlayerTeam(playerId: string): Team | null { for (const t of teams) { if (t && t.roster.some((r) => r.player_id === playerId)) return t } return null } return (

Lag og spillere

Ryder Cup-format: nøyaktig to lag. Sett opp begge lagene og fyll troppene før turneringen kan starte.

{error && (

{error}

)} {loading ? (
) : (
{([0, 1] as const).map((slot) => { const team = teams[slot] if (team) { const otherTeam = teams[slot === 0 ? 1 : 0] return ( ) } return })}
)}
) } // --- Create team (State A) ------------------------------------------------- function CreateTeamCard({ slot, onCreate, }: { slot: 0 | 1 onCreate: (slot: 0 | 1, name: string, color: string) => void }) { const [name, setName] = useState("") const [color, setColor] = useState(TEAM_COLORS[slot === 0 ? 0 : 1].value) const valid = name.trim().length >= 2 function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!valid) return onCreate(slot, name.trim(), color) } return (
Lag {slot + 1} Opprett lag
setName(e.target.value)} className="h-12 rounded-2xl text-base" />
{TEAM_COLORS.map((c) => { const selected = c.value === color return ( ) })}
) } // --- Team panel with roster (State B) -------------------------------------- function TeamPanel({ team, otherTeam, pool, findPlayerTeam, onToggleCaptain, onRemovePlayer, onAddExisting, onAddNew, }: { team: Team otherTeam: Team | null pool: ApiPlayer[] findPlayerTeam: (playerId: string) => Team | null onToggleCaptain: (teamId: string, rosterId: string, current: boolean) => void onRemovePlayer: (teamId: string, rosterId: string) => void onAddExisting: (teamId: string, playerId: string) => void onAddNew: (teamId: string, name: string, handicap?: number) => void }) { const [confirmingId, setConfirmingId] = useState(null) return (
    {team.roster.length === 0 && (
  • Ingen spillere ennå. Legg til nedenfor.
  • )} {team.roster.map((entry) => { const confirming = confirmingId === entry.id if (confirming) { return (
  • Fjern {entry.display_name} fra laget?
  • ) } return (
  • {entry.display_name} {entry.is_captain && ( )}
    {formatHandicap(entry.handicap_index_snapshot)} onToggleCaptain(team.id, entry.id, entry.is_captain)} className="flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold" > setConfirmingId(entry.id)} className="flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold text-destructive" >
  • ) })}
) } // --- Add player type-ahead ------------------------------------------------- function AddPlayerControl({ team, pool, findPlayerTeam, onAddExisting, onAddNew, }: { team: Team otherTeam: Team | null pool: ApiPlayer[] findPlayerTeam: (playerId: string) => Team | null onAddExisting: (teamId: string, playerId: string) => void onAddNew: (teamId: string, name: string, handicap?: number) => void }) { const [open, setOpen] = useState(false) const [query, setQuery] = useState("") const [creating, setCreating] = useState(false) const [newHandicap, setNewHandicap] = useState("") const trimmed = query.trim() const matches = useMemo(() => { if (!trimmed) return [] const q = trimmed.toLowerCase() return pool .filter((p) => p.display_name.toLowerCase().includes(q)) .map((p) => { const onTeam = findPlayerTeam(p.id) return { player: p, onThisTeam: onTeam?.id === team.id, onOtherTeam: onTeam && onTeam.id !== team.id ? onTeam : null, } }) .filter((m) => !m.onThisTeam) }, [pool, trimmed, findPlayerTeam, team.id]) const exactMatch = useMemo( () => pool.some((p) => p.display_name.toLowerCase() === trimmed.toLowerCase()), [pool, trimmed], ) const showCreate = trimmed.length >= 2 && !exactMatch function reset() { setQuery("") setCreating(false) setNewHandicap("") } function handleAddExisting(playerId: string) { onAddExisting(team.id, playerId) reset() } function handleCreate() { if (trimmed.length < 2) return const hcpValue = newHandicap.trim() === "" ? undefined : Number(newHandicap.replace(",", ".")) const hcp = hcpValue !== undefined && !Number.isNaN(hcpValue) ? hcpValue : undefined onAddNew(team.id, trimmed, hcp) reset() } if (!open) { return ( ) } return (
{ setQuery(e.target.value) setCreating(false) }} onKeyDown={(e) => { if (e.key === "Escape") { setOpen(false) reset() } }} className="h-11 flex-1 rounded-2xl text-base" />
{trimmed.length > 0 && (
    {matches.map(({ player, onOtherTeam }) => { if (onOtherTeam) { return (
  • {player.display_name} allerede på {onOtherTeam.name}
    {formatHandicap(player.handicap_index)}
  • ) } return (
  • ) })} {matches.length === 0 && !showCreate && (
  • Ingen treff.
  • )} {showCreate && !creating && (
  • )} {showCreate && creating && (
  • Ny spiller: {trimmed}
    setNewHandicap(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault() handleCreate() } }} className="h-11 rounded-xl text-base" />
  • )}
)}
) } // --- Helpers --------------------------------------------------------------- function formatHandicap(handicap: number | null | undefined) { if (handicap === null || handicap === undefined) return "—" return handicap.toFixed(1) }