teecup/frontend/components/watch-players.tsx

144 lines
5.3 KiB
TypeScript
Raw Normal View History

2026-08-07 22:02:56 +02:00
"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>
)
}