teecup/frontend/components/watch-players.tsx
2026-08-07 22:02:56 +02:00

143 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
// "Spillere og runde", read-only tilskuer-variant (ADR-044, 2026-08-06).
// Bevisst IKKE et forsøk på å gjenbruke round-detail.tsx sin PlayerList
// via en canManage=false-prop -- den er en lokal, ikke-eksportert funksjon
// tett koblet til eier-autentiserte handlere (onPatchParticipant m.fl.),
// og selv canManage dekker i dag ikke alt konsekvent der (Fullfør runde-
// knappen er kun gated på !completed). Denne komponenten inneholder rett
// og slett INGEN handlings-kode (ingen +Medspiller, ingen Rediger/Fullfør/
// Slett) -- ingen risiko for at en eier-handling glipper gjennom.
import Link from "next/link"
import { Users } from "lucide-react"
export type ApiPublicParticipant = {
id: string
display_name: string
is_owner: boolean
tee_name_snapshot: string
handicap_index_snapshot: number | null
course_handicap_snapshot: number | null
round_side_id: string | null
playing_handicap: number | null
}
export type ApiLeaderboardEntry = {
participant_id: string
display_name: string
holes_played: number
total_score: number | null
score_to_par: number | null
}
export type ApiFlightSummary = {
round_id: string
name: string | null
course_name_snapshot: string
played_at: string
started_at: string | null
completed_at: string | null
participant_count: number
is_anchor: boolean
}
function formatToPar(value: number): string {
if (value === 0) return "E"
return value > 0 ? `+${value}` : `${Math.abs(value)}`
}
function ParticipantCard({ participant, progress }: { participant: ApiPublicParticipant; progress: ApiLeaderboardEntry | null }) {
// Tildelte slag: allowance-justert spillehandicap der satt, ellers
// course handicap -- samme prioritet som eiersidens PlayerList bruker
// for "Tildelte slag".
const strokesGiven = participant.playing_handicap ?? participant.course_handicap_snapshot
return (
<div className="flex flex-col gap-2 rounded-2xl border border-border bg-card p-4 shadow-sm shadow-black/5">
<div className="flex flex-wrap items-center gap-2">
<span className="text-base font-bold text-foreground">{participant.display_name}</span>
{participant.is_owner && (
<span className="rounded-full border border-border px-2 py-0.5 text-xs font-semibold text-muted-foreground">
Eier
</span>
)}
</div>
<p className="text-sm text-muted-foreground">
Utslag: <span className="font-semibold text-foreground">{participant.tee_name_snapshot}</span>
{participant.handicap_index_snapshot !== null && (
<>
{" · "}HCP: <span className="font-semibold text-foreground">{participant.handicap_index_snapshot.toFixed(1)}</span>
</>
)}
{strokesGiven !== null && (
<>
{" · "}Tildelte slag: <span className="font-semibold text-foreground">{strokesGiven}</span>
</>
)}
</p>
{progress && (
<p className="text-sm text-muted-foreground">
langt: <span className="font-semibold text-foreground">{progress.holes_played} hull</span>
{progress.total_score !== null && (
<>
{" · "}
<span className="font-semibold text-foreground">{progress.total_score} slag</span>
</>
)}
{progress.score_to_par !== null && (
<>
{" · "}
<span className="font-semibold text-foreground">{formatToPar(progress.score_to_par)} til par</span>
</>
)}
</p>
)}
</div>
)
}
export function WatchPlayers({
participants,
leaderboardEntries,
flightGroup,
}: {
participants: ApiPublicParticipant[]
leaderboardEntries: ApiLeaderboardEntry[]
flightGroup: { flight_group_id: string | null; flights: ApiFlightSummary[] } | null
}) {
const progressByParticipant = new Map(leaderboardEntries.map((e) => [e.participant_id, e]))
const otherFlights = (flightGroup?.flights ?? []).filter((f) => !f.is_anchor)
return (
<section className="flex flex-col gap-4" aria-label="Spillere og runde">
{otherFlights.length > 0 && (
<div className="flex flex-col gap-2 rounded-2xl border border-border bg-card p-4">
<span className="flex items-center gap-1.5 text-sm font-bold text-foreground">
<Users aria-hidden="true" className="size-4 text-primary" />
Flere flighter deler dette leaderboardet
</span>
<ul className="flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border">
{otherFlights.map((f) => (
<li key={f.round_id}>
<Link
href={`/watch/${f.round_id}`}
className="flex min-h-11 items-center justify-between gap-2 px-3 py-2 text-sm font-semibold text-foreground transition-colors hover:bg-accent/50"
>
<span>{f.name || "Flight"}</span>
<span className="text-xs font-medium text-muted-foreground">{f.participant_count} spillere</span>
</Link>
</li>
))}
</ul>
</div>
)}
<div className="flex flex-col gap-3">
{participants.map((p) => (
<ParticipantCard key={p.id} participant={p} progress={progressByParticipant.get(p.id) ?? null} />
))}
</div>
</section>
)
}