"use client" // Tredjeparts/offentlig live-visning av en frittstående runde (ADR-036 // fase 2, 2026-07-28). Ferskbygget, IKKE en gjenbruk av round-detail.tsx // (som antar eier-/deltaker-tilgang og full skrive-UI) -- denne siden er // rendyrket lesevisning mot de nye /public/rounds/*-endepunktene, som // fungerer like fint anonymt som innlogget (get_current_user_optional). // // Score-fanen (denne filen): kompakt status + "Spillere og runde" // (deltakerliste, HCP/utslag/tildelte slag, "så langt", flight- // gruppering) -- se watch-players.tsx. Selve matchstatus/skins-tavle/ // leaderboard flyttet til /watch/[id]/leaderboard (ADR-044, 2026-08-06), // som gjenbruker den ALLEREDE eksisterende round-leaderboard.tsx uendret // (kun `publicMode`-bryteren), for full formatparitet med eiersiden. import { useEffect, useState } from "react" import Link from "next/link" import { Radio, Trophy } from "lucide-react" import { WatchTabs } from "@/components/watch-tabs" import { WatchPlayers, type ApiPublicParticipant, type ApiLeaderboardEntry, type ApiFlightSummary } from "@/components/watch-players" import { RoundMessages } from "@/components/round-messages" type ApiSide = { id: string; label: string | null } type ApiRound = { id: string name: string | null course_name_snapshot: string tee_name_snapshot: string played_at: string play_format: string completed_at: string | null owner_display_name: string owner_user_id: string participants: ApiPublicParticipant[] sides: ApiSide[] } type ApiLeaderboard = { holes_planned: number; completed: boolean; entries: ApiLeaderboardEntry[] } const FORMAT_LABELS: Record = { stroke: "Slagspill", stableford: "Stableford", match: "Match", skins: "Skins", fourball: "Fourball", foursome: "Foursome", greensome: "Greensome", scramble_2: "Scramble (2)", scramble_4: "Scramble (4)", chapman: "Chapman", copenhagen: "Københavner", bbb: "Bingo Bango Bongo", flag: "Flaggturnering", shamble: "Shamble", money_ball: "Money Ball", high_low_high: "High-low-high", scramble_solo: "Scramble mot enkeltspiller", scramble_solo_match: "Scramble mot enkeltspiller (matchspill)", } const dateFormatter = new Intl.DateTimeFormat("no-NO", { day: "numeric", month: "long", year: "numeric" }) export function WatchRound({ roundId }: { roundId: string }) { const [round, setRound] = useState(null) const [leaderboard, setLeaderboard] = useState(null) const [flightGroup, setFlightGroup] = useState<{ flight_group_id: string | null; flights: ApiFlightSummary[] } | null>(null) const [error, setError] = useState(null) const [refreshKey, setRefreshKey] = useState(0) // Innlogget bruker (kan være anonym -- se moduldoc). Kun brukt til // RoundMessages sin slett-knapp-synlighet; "" er trygt som fallback // siden ingen ekte author_user_id/eier-id noensinne er en tom streng. const [viewerId, setViewerId] = useState(null) useEffect(() => { let cancelled = false fetch("/auth/me", { credentials: "include" }) .then((res) => (res.ok ? res.json() : null)) .then((data: { id: string } | null) => { if (!cancelled && data) setViewerId(data.id) }) .catch(() => {}) return () => { cancelled = true } }, []) useEffect(() => { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:" const socket = new WebSocket(`${protocol}//${window.location.host}/ws/public/rounds/${roundId}/live`) socket.onmessage = () => setRefreshKey((k) => k + 1) return () => socket.close() }, [roundId]) useEffect(() => { let cancelled = false async function load() { const roundRes = await fetch(`/public/rounds/${roundId}`, { credentials: "include" }) if (roundRes.status === 404) { if (!cancelled) setError("Denne runden finnes ikke.") return } if (roundRes.status === 403) { if (!cancelled) setError("Du har ikke tilgang til å se denne runden.") return } if (!roundRes.ok) { if (!cancelled) setError("Klarte ikke å hente runden. Prøv igjen om litt.") return } const roundData: ApiRound = await roundRes.json() if (cancelled) return setRound(roundData) const [lbRes, flightRes] = await Promise.all([ fetch(`/public/rounds/${roundId}/leaderboard`, { credentials: "include" }), fetch(`/public/rounds/${roundId}/flight-group`, { credentials: "include" }), ]) if (lbRes.ok && !cancelled) setLeaderboard(await lbRes.json()) if (flightRes.ok && !cancelled) setFlightGroup(await flightRes.json()) } void load() return () => { cancelled = true } }, [roundId, refreshKey]) // Anker-scroll til kommentarseksjonen (ADR-047) -- se samme mønster/ // begrunnelse i round-detail.tsx. useEffect(() => { if (!round) return if (window.location.hash !== "#kommentarer") return document.getElementById("kommentarer")?.scrollIntoView({ block: "start" }) }, [round]) if (error) { return (

{error}

Tilbake til dashbordet
) } if (!round) { return (
) } const ongoing = round.completed_at === null return (
{round.name?.trim() || round.course_name_snapshot} {ongoing && ( )} {round.name?.trim() ? `${round.course_name_snapshot} · ` : ""} {FORMAT_LABELS[round.play_format] ?? round.play_format} · {dateFormatter.format(new Date(round.played_at))} Spilt av {round.owner_display_name}
{/* Kommentarer/bilder (ADR-044) -- BUG funnet 2026-08-06: fantes aldri her, kun på round-detail.tsx sin egen "Score"-fane. En person runden er delt med hadde dermed ingen vei til å se eller delta i samtalen. `GET/POST /rounds/{id}/messages` er allerede synlighets-gatet (_can_view_round, ikke eier/deltaker-only), så samme endepunkt gjenbrukes uendret -- ingen ny backend-kode. */} {/* Anker-mål for "Kommentarer"-lenken fra leaderboardets utvidede rader (round-leaderboard.tsx) -- samme seksjon, ikke en duplikat. */}
) }