2026-07-18 18:22:10 +02:00
|
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
|
|
import { useEffect, useState } from "react"
|
|
|
|
|
|
import Link from "next/link"
|
|
|
|
|
|
import { ArrowLeft, Check, Circle, Trophy } from "lucide-react"
|
|
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
|
|
|
|
|
|
|
|
// --- Types (matcher API-kontrakten i app/routers/tournaments.py sin leaderboard-endepunkt)
|
|
|
|
|
|
|
|
|
|
|
|
type ApiTeamStanding = {
|
|
|
|
|
|
team_id: string
|
|
|
|
|
|
team_name: string
|
|
|
|
|
|
color: string | null
|
|
|
|
|
|
points: number
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
|
// ADR-020: hva stillingen ville blitt om pågående, ikke-avgjorte matcher
|
|
|
|
|
|
// holder seg som de står nå.
|
|
|
|
|
|
projected_points: number
|
2026-07-18 18:22:10 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ApiSessionStanding = {
|
|
|
|
|
|
session_id: string
|
|
|
|
|
|
sequence: number
|
|
|
|
|
|
name: string | null
|
|
|
|
|
|
points_by_team: Record<string, number>
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
|
projected_points_by_team: Record<string, number>
|
2026-07-18 18:22:10 +02:00
|
|
|
|
matches_total: number
|
|
|
|
|
|
matches_decided: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ApiLeaderboard = {
|
|
|
|
|
|
teams: [ApiTeamStanding, ApiTeamStanding]
|
|
|
|
|
|
matches_total: number
|
|
|
|
|
|
matches_decided: number
|
|
|
|
|
|
sessions: ApiSessionStanding[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Formatting helpers ----------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function formatPoints(value: number): string {
|
|
|
|
|
|
return value.toLocaleString("nb-NO", { maximumFractionDigits: 1 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sessionTitle(s: ApiSessionStanding): string {
|
|
|
|
|
|
return s.name?.trim() || `Økt ${s.sequence}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Component -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
export function TournamentLeaderboard({
|
|
|
|
|
|
organizationId,
|
|
|
|
|
|
tournamentId,
|
|
|
|
|
|
tournamentName,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
organizationId: string
|
|
|
|
|
|
tournamentId: string
|
|
|
|
|
|
tournamentName: string
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [board, setBoard] = useState<ApiLeaderboard | null>(null)
|
|
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
async function load() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`/orgs/${organizationId}/tournaments/${tournamentId}/leaderboard`, {
|
|
|
|
|
|
credentials: "include",
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!res.ok) throw new Error(`load failed: ${res.status}`)
|
|
|
|
|
|
const data: ApiLeaderboard = await res.json()
|
|
|
|
|
|
if (cancelled) return
|
|
|
|
|
|
setBoard({ ...data, sessions: [...data.sessions].sort((a, b) => a.sequence - b.sequence) })
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
if (!cancelled) setError("Klarte ikke å laste leaderboardet. Prøv å laste siden på nytt.")
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (!cancelled) setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
void load()
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [organizationId, tournamentId])
|
|
|
|
|
|
|
|
|
|
|
|
const detailHref = `/tournaments/${tournamentId}?org=${organizationId}&name=${encodeURIComponent(tournamentName)}`
|
|
|
|
|
|
const programHref = `/tournaments/${tournamentId}/program?org=${organizationId}&name=${encodeURIComponent(tournamentName)}`
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex min-h-[100dvh] flex-col bg-background">
|
|
|
|
|
|
<header className="sticky top-0 z-10 border-b border-border bg-background/80 backdrop-blur">
|
|
|
|
|
|
<div className="mx-auto flex w-full max-w-4xl items-center gap-3 px-5 py-4">
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href={detailHref}
|
|
|
|
|
|
aria-label="Tilbake til turnering"
|
|
|
|
|
|
className="flex size-10 shrink-0 items-center justify-center rounded-xl border border-border bg-card text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ArrowLeft aria-hidden="true" className="size-5" />
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
<div className="flex min-w-0 flex-col">
|
|
|
|
|
|
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
|
Turnering
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<h1 className="truncate text-xl font-extrabold tracking-tight text-foreground">
|
|
|
|
|
|
{tournamentName}
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
|
<nav
|
|
|
|
|
|
className="mx-auto flex w-full max-w-4xl items-center gap-2 overflow-x-auto px-5 pb-3"
|
|
|
|
|
|
aria-label="Turneringsseksjoner"
|
|
|
|
|
|
>
|
2026-07-18 18:22:10 +02:00
|
|
|
|
<Link
|
|
|
|
|
|
href={detailHref}
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
|
className="shrink-0 rounded-full px-4 py-2 text-sm font-semibold text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
2026-07-18 18:22:10 +02:00
|
|
|
|
>
|
|
|
|
|
|
Lag og spillere
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href={programHref}
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
|
className="shrink-0 rounded-full px-4 py-2 text-sm font-semibold text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
2026-07-18 18:22:10 +02:00
|
|
|
|
>
|
|
|
|
|
|
Program
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
<span
|
|
|
|
|
|
aria-current="page"
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
|
className="shrink-0 rounded-full bg-primary px-4 py-2 text-sm font-bold text-primary-foreground"
|
2026-07-18 18:22:10 +02:00
|
|
|
|
>
|
|
|
|
|
|
Leaderboard
|
|
|
|
|
|
</span>
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
|
<Link
|
|
|
|
|
|
href={`/tournaments/${tournamentId}/presentation?org=${organizationId}&name=${encodeURIComponent(tournamentName)}`}
|
|
|
|
|
|
className="shrink-0 rounded-full px-4 py-2 text-sm font-semibold text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
Presentasjon
|
|
|
|
|
|
</Link>
|
2026-08-21 13:54:01 +02:00
|
|
|
|
<Link
|
|
|
|
|
|
href={`/tournaments/${tournamentId}/print/resultlist?org=${organizationId}`}
|
|
|
|
|
|
className="shrink-0 rounded-full px-4 py-2 text-sm font-semibold text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
Skriv ut
|
|
|
|
|
|
</Link>
|
2026-07-18 18:22:10 +02:00
|
|
|
|
</nav>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<main className="mx-auto w-full max-w-4xl flex-1 px-5 py-6 sm:py-8">
|
|
|
|
|
|
<div className="mb-5 flex flex-col gap-1">
|
|
|
|
|
|
<h2 className="text-base font-bold text-foreground">Stilling</h2>
|
|
|
|
|
|
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
|
|
|
|
|
|
Totalt antall poeng summert på tvers av alle økter, oppdatert etter hvert som matcher
|
|
|
|
|
|
avgjøres.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
|
<p role="alert" className="mb-4 text-sm font-medium text-destructive">
|
|
|
|
|
|
{error}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{loading || !board ? (
|
|
|
|
|
|
<div className="flex justify-center py-16">
|
|
|
|
|
|
<div
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
className="size-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{(() => {
|
|
|
|
|
|
const [teamA, teamB] = board.teams
|
|
|
|
|
|
const leaderId =
|
|
|
|
|
|
teamA.points === teamB.points ? null : teamA.points > teamB.points ? teamA.team_id : teamB.team_id
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
|
<div className="flex flex-col gap-3">
|
|
|
|
|
|
<SegmentedBar
|
|
|
|
|
|
label="Stilling nå"
|
|
|
|
|
|
teamA={teamA}
|
|
|
|
|
|
teamB={teamB}
|
|
|
|
|
|
pointsA={teamA.points}
|
|
|
|
|
|
pointsB={teamB.points}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<SegmentedBar
|
|
|
|
|
|
label="Projisert (hvis pågående matcher holder seg)"
|
|
|
|
|
|
teamA={teamA}
|
|
|
|
|
|
teamB={teamB}
|
|
|
|
|
|
pointsA={teamA.projected_points}
|
|
|
|
|
|
pointsB={teamB.projected_points}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-4">
|
|
|
|
|
|
<Scoreboard teamA={teamA} teamB={teamB} leaderId={leaderId} />
|
|
|
|
|
|
</div>
|
2026-07-18 18:22:10 +02:00
|
|
|
|
<p className="mt-3 text-center text-sm font-medium text-muted-foreground">
|
|
|
|
|
|
{board.matches_decided} av {board.matches_total} matcher avgjort
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="mt-8 flex flex-col gap-3">
|
|
|
|
|
|
<h3 className="text-sm font-bold text-foreground">Poeng per økt</h3>
|
|
|
|
|
|
{board.sessions.length === 0 ? (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">Ingen økter opprettet ennå.</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
board.sessions.map((s) => (
|
|
|
|
|
|
<SessionRow key={s.session_id} session={s} teamA={teamA} teamB={teamB} />
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)
|
|
|
|
|
|
})()}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
|
// --- Segmentert bar (to per skjerm: faktisk + projisert, ADR-020) ----------
|
|
|
|
|
|
// Ett rektangel delt i to fargede segmenter proporsjonalt med hvert lags
|
|
|
|
|
|
// poeng -- viser umiddelbart hvem som leder, ikke bare tall side om side.
|
|
|
|
|
|
// Ved 0-0 (ingenting avgjort/projisert ennå) splittes 50/50 nøytralt.
|
|
|
|
|
|
|
|
|
|
|
|
function SegmentedBar({
|
|
|
|
|
|
label,
|
|
|
|
|
|
teamA,
|
|
|
|
|
|
teamB,
|
|
|
|
|
|
pointsA,
|
|
|
|
|
|
pointsB,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
label: string
|
|
|
|
|
|
teamA: ApiTeamStanding
|
|
|
|
|
|
teamB: ApiTeamStanding
|
|
|
|
|
|
pointsA: number
|
|
|
|
|
|
pointsB: number
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const total = pointsA + pointsB
|
|
|
|
|
|
const widthA = total > 0 ? (pointsA / total) * 100 : 50
|
|
|
|
|
|
const widthB = 100 - widthA
|
|
|
|
|
|
const colorA = teamA.color ?? "#64748b"
|
|
|
|
|
|
const colorB = teamB.color ?? "#64748b"
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
|
|
|
|
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{label}</span>
|
2026-07-29 12:44:51 +02:00
|
|
|
|
<div className="flex h-11 w-full overflow-hidden rounded-2xl shadow-md shadow-black/8">
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
|
<div
|
|
|
|
|
|
className="flex items-center justify-center text-sm font-extrabold text-white transition-[width] duration-300"
|
|
|
|
|
|
style={{ width: `${widthA}%`, backgroundColor: colorA }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{widthA > 12 && formatPoints(pointsA)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="flex items-center justify-center text-sm font-extrabold text-white transition-[width] duration-300"
|
|
|
|
|
|
style={{ width: `${widthB}%`, backgroundColor: colorB }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{widthB > 12 && formatPoints(pointsB)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center justify-between text-xs font-medium text-muted-foreground">
|
|
|
|
|
|
<span className="truncate">{teamA.team_name}</span>
|
|
|
|
|
|
<span className="truncate">{teamB.team_name}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-18 18:22:10 +02:00
|
|
|
|
// --- Scoreboard ------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function Scoreboard({
|
|
|
|
|
|
teamA,
|
|
|
|
|
|
teamB,
|
|
|
|
|
|
leaderId,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
teamA: ApiTeamStanding
|
|
|
|
|
|
teamB: ApiTeamStanding
|
|
|
|
|
|
leaderId: string | null
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return (
|
2026-07-29 12:44:51 +02:00
|
|
|
|
<div className="grid grid-cols-[1fr_auto_1fr] items-stretch overflow-hidden rounded-3xl border border-border bg-card shadow-md shadow-black/8">
|
2026-07-18 18:22:10 +02:00
|
|
|
|
<TeamScore team={teamA} isLeader={leaderId === teamA.team_id} align="left" />
|
|
|
|
|
|
<div className="flex items-center justify-center bg-border/40 px-2">
|
|
|
|
|
|
<span className="text-sm font-bold uppercase tracking-wide text-muted-foreground">vs</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<TeamScore team={teamB} isLeader={leaderId === teamB.team_id} align="right" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function TeamScore({
|
|
|
|
|
|
team,
|
|
|
|
|
|
isLeader,
|
|
|
|
|
|
align,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
team: ApiTeamStanding
|
|
|
|
|
|
isLeader: boolean
|
|
|
|
|
|
align: "left" | "right"
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const color = team.color ?? "#64748b"
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"relative flex flex-col gap-2 p-5 sm:p-6",
|
|
|
|
|
|
align === "right" ? "items-end text-right" : "items-start text-left",
|
|
|
|
|
|
)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderTop: `4px solid ${color}`,
|
|
|
|
|
|
backgroundColor: isLeader ? `${color}14` : undefined,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className={cn("flex items-center gap-2", align === "right" && "flex-row-reverse")}>
|
|
|
|
|
|
<span aria-hidden="true" className="size-3 shrink-0 rounded-full" style={{ backgroundColor: color }} />
|
|
|
|
|
|
<span className="truncate text-sm font-bold text-foreground sm:text-base">{team.team_name}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{isLeader && (
|
|
|
|
|
|
<span className="inline-flex items-center gap-1 rounded-full bg-primary/15 px-2 py-0.5 text-xs font-bold text-foreground">
|
|
|
|
|
|
<Trophy aria-hidden="true" className="size-3 fill-primary text-primary" />
|
|
|
|
|
|
Leder
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"font-extrabold tabular-nums leading-none tracking-tight",
|
|
|
|
|
|
isLeader ? "text-6xl sm:text-7xl" : "text-5xl sm:text-6xl text-foreground/80",
|
|
|
|
|
|
)}
|
|
|
|
|
|
style={isLeader ? { color } : undefined}
|
|
|
|
|
|
>
|
|
|
|
|
|
{formatPoints(team.points)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">poeng</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Session row -----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function SessionRow({
|
|
|
|
|
|
session,
|
|
|
|
|
|
teamA,
|
|
|
|
|
|
teamB,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
session: ApiSessionStanding
|
|
|
|
|
|
teamA: ApiTeamStanding
|
|
|
|
|
|
teamB: ApiTeamStanding
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const notStarted = session.matches_decided === 0
|
|
|
|
|
|
const complete = session.matches_decided === session.matches_total
|
|
|
|
|
|
const aPts = session.points_by_team[teamA.team_id] ?? 0
|
|
|
|
|
|
const bPts = session.points_by_team[teamB.team_id] ?? 0
|
|
|
|
|
|
const colorA = teamA.color ?? "#64748b"
|
|
|
|
|
|
const colorB = teamB.color ?? "#64748b"
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-07-29 12:44:51 +02:00
|
|
|
|
<div className="rounded-3xl border border-border bg-card p-4 shadow-md shadow-black/8 sm:p-5">
|
2026-07-18 18:22:10 +02:00
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
|
|
|
|
<h4 className="text-base font-bold text-foreground">{sessionTitle(session)}</h4>
|
|
|
|
|
|
{complete ? (
|
|
|
|
|
|
<span className="inline-flex items-center gap-1 rounded-full bg-primary/15 px-2.5 py-1 text-xs font-semibold text-foreground">
|
|
|
|
|
|
<Check aria-hidden="true" className="size-3.5 text-primary" />
|
|
|
|
|
|
Fullført
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : notStarted ? (
|
|
|
|
|
|
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
|
|
|
|
|
|
<Circle aria-hidden="true" className="size-3" />
|
|
|
|
|
|
Ikke startet ennå
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
|
|
|
|
{session.matches_decided} av {session.matches_total} matcher avgjort
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="mt-3 flex items-center gap-3">
|
|
|
|
|
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
|
|
|
|
<span aria-hidden="true" className="size-2.5 shrink-0 rounded-full" style={{ backgroundColor: colorA }} />
|
|
|
|
|
|
<span className="truncate text-sm font-medium text-foreground">{teamA.team_name}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex shrink-0 items-center gap-1.5 tabular-nums">
|
|
|
|
|
|
<span className={cn("text-lg font-extrabold", notStarted ? "text-muted-foreground" : "text-foreground")}>
|
|
|
|
|
|
{formatPoints(aPts)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-sm font-semibold text-muted-foreground">–</span>
|
|
|
|
|
|
<span className={cn("text-lg font-extrabold", notStarted ? "text-muted-foreground" : "text-foreground")}>
|
|
|
|
|
|
{formatPoints(bPts)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex min-w-0 flex-1 items-center justify-end gap-2">
|
|
|
|
|
|
<span className="truncate text-right text-sm font-medium text-foreground">{teamB.team_name}</span>
|
|
|
|
|
|
<span aria-hidden="true" className="size-2.5 shrink-0 rounded-full" style={{ backgroundColor: colorB }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|