Fiks skjult score-registrering og doble Score-faner på rundens Score-fane

Avstandsmåleren fylte hele mobilskjermen uten å antyde at score-
registrering fantes lenger ned (funnet via brukers skjermbilde). Viser nå
kompakt som standard med en "Vis banekart"-utvidelse og en alltid synlig
"Registrer score"-snarvei. Den separate, duplikate "Score"-pillefanen inni
round-detail.tsx (visuelt identisk med RoundHeader sin egen Score/
Scorekort/Leaderboard-rad) er erstattet med én tydelig annerledes
veksle-knapp -- fikser samtidig et reelt navigasjonshull der det ikke
fantes noen vei tilbake til Score fra en ekstern "?tab=manage"-lenke.
This commit is contained in:
Erol Haagenrud 2026-08-20 05:54:20 +02:00
parent 6765d17109
commit 05599aafe6
3 changed files with 144 additions and 27 deletions

View file

@ -13230,3 +13230,65 @@ Neste steg:
**Rullet ut 2026-08-19** -- bruker bekreftet. Ren frontend. **Rullet ut 2026-08-19** -- bruker bekreftet. Ren frontend.
`docker compose build teecup_frontend && up -d`, rene logger, 200 OK. `docker compose build teecup_frontend && up -d`, rene logger, 200 OK.
133. **Score-fanen (egen runde): skjult score-registrering + doble
"Score"-faner -- funnet via brukers skjermbilde, 2026-08-20.** Bruker
viste et ekte skjermbilde: på et hull med koordinatdata fylte
avstandsmåleren/hull-diagrammet HELE den synlige mobilskjermen, uten
noe som antydet at spillerkortene (selve score-registreringen) fantes
lenger ned -- en førstegangsbruker skjønte ikke at de måtte scrolle.
Samtidig lå det to visuelt identiske pille-fanerader rett oppå
hverandre: RoundHeader sin Score/Scorekort/Leaderboard, og en HELT
SEPARAT, egen "Score"/"Spillere og runde"-fanerad bygget inni
`round-detail.tsx` selv -- begge kalte én fane "Score", leste som
duplikate kontroller.
Løst via en Claude-skrevet V0-prompt (samme etablerte
"Claude skriver prompt, bruker kjører V0, Claude kobler inn"-mønster
som resten av appen) -- V0-eksporten (`round-score-top.tsx`) traff
begge problemene presist og ble brukt som fasit for restrukturering
av de EKTE komponentene (ikke V0s egen mock-`HoleDiagram`, men den
ekte `HoleTargetDistance`/`HoleDiagram`, som allerede -- upåaktet --
støttet en "compact"-modus, aldri faktisk brukt noe sted i kodebasen
før nå):
- `hole-target-distance.tsx`: ny valgfri `onAvailabilityChange`-prop
-- kalleren får vite om banen faktisk har koordinatdata for hullet,
uten å duplisere fetch-en selv.
- `round-detail.tsx` (`PlayerHoleCards`): avstandsmåleren vises nå
`size="compact"` som standard (kun de tre store avstandstallene),
med en "Vis banekart og hindringer"-knapp som utvider til det fulle
diagrammet -- knappen vises KUN når `onAvailabilityChange` faktisk
har meldt at det finnes data (ikke en død knapp på baner uten
koordinater). En alltid synlig "Registrer score ↓"-knapp scroller
rett til spillerkortene.
- `round-detail.tsx` (hovedkomponenten): den separate
"Score"/"Spillere og runde"-pille-fanen er fjernet, erstattet med
ÉN sekundær, tydelig ANNERLEDES knapp (ramme + ikon, ikke en pille)
som veksler `pageTab` mellom de to visningene, med tekst som endrer
seg etter tilstand ("Spillere og runde" / "Tilbake til score").
**Reelt funn underveis, ikke bare kosmetikk:** før denne fiksen
fantes det INGEN pålitelig vei tilbake fra "Spillere og runde" til
"Score" dersom brukeren kom dit via en ekstern lenke (f.eks.
RoundHeader sin Administrer-dialog, som lenker til `?tab=manage`) --
`pageTab`-tilstanden ble aldri tilbakestilt automatisk, og den gamle
fanen for å bytte tilbake fantes ikke i den konteksten. Den nye
veksle-knappen løser dette som et biprodukt, siden den er ren
klient-tilstand, ikke avhengig av URL.
**Verifisert i scratch:** en midlertidig forhåndsvisningsrute som
rendret den EKTE `RoundDetail`-komponenten (ikke en isolert mock) med
et fullstendig mocket `fetch` (runde, deltakere, hull, avstandspunkter
for hull 1 -- IKKE for hull 2, bevisst, for å teste begge
tilstander) og emulert GPS-posisjon (Chrome DevTools). Bekreftet:
spillerkort synlig uten scrolling på mobilviewport (402px), "Vis
banekart"-knappen vises for hull 1 og er HELT fraværende for hull 2,
fanebytte-knappen veksler korrekt begge veier med riktig
tekst/ikon, hele administrasjonspanelet (Flighter/Rediger runde/
Fullfør/Slett/full spillerliste) fortsatt fullt funksjonelt etter
fjerningen av den gamle faneraden. Ingen konsollfeil. `tsc --noEmit`
rent, `vitest run` 55/55.
**Rullet ut 2026-08-20** -- bruker bekreftet. Ren frontend.
`docker compose build teecup_frontend && up -d`, rene logger, 200 OK.

View file

@ -187,11 +187,16 @@ export function HoleTargetDistance({
holeNumber, holeNumber,
size = "compact", size = "compact",
className, className,
onAvailabilityChange,
}: { }: {
baseUrl: string baseUrl: string
holeNumber: number holeNumber: number
size?: "compact" | "full" size?: "compact" | "full"
className?: string className?: string
/** Kalles hver gang det blir klart om banen faktisk har koordinatdata for
* hullet eller ikke -- lar en kaller (f.eks. en utvid/skjul-knapp rundt
* komponenten) vite om det er noe å vise, uten selv å duplisere fetch-en. */
onAvailabilityChange?: (available: boolean) => void
}) { }) {
const [points, setPoints] = useState<ApiTargetPoint[] | null>(null) const [points, setPoints] = useState<ApiTargetPoint[] | null>(null)
const [livePosition, setLivePosition] = useState<LatLng | null>(null) const [livePosition, setLivePosition] = useState<LatLng | null>(null)
@ -213,6 +218,12 @@ export function HoleTargetDistance({
} }
}, [baseUrl, holeNumber]) }, [baseUrl, holeNumber])
useEffect(() => {
if (points === null) return
onAvailabilityChange?.(points.length > 0)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [points])
useEffect(() => { useEffect(() => {
// Ingen vits å spore GPS når banen uansett ikke har koordinatdata for // Ingen vits å spore GPS når banen uansett ikke har koordinatdata for
// dette hullet -- unngår unødvendig batteribruk/tillatelses-prompt. // dette hullet -- unngår unødvendig batteribruk/tillatelses-prompt.

View file

@ -17,6 +17,7 @@ import { RoundPageShell } from "@/components/round-page-shell"
import { RoundMessages } from "@/components/round-messages" import { RoundMessages } from "@/components/round-messages"
import type { FlagMapEntry } from "@/components/flag-map-overview" import type { FlagMapEntry } from "@/components/flag-map-overview"
import { import {
ArrowDown,
ArrowRight, ArrowRight,
Check, Check,
ChevronDown, ChevronDown,
@ -1502,32 +1503,32 @@ export function RoundDetail({ roundId }: { roundId: string }) {
</div> </div>
)} )}
{/* To faner: "Score" (default, KUN hull-navigasjon + registrering -- {/* Ett enkelt, sekundært veksle-valg -- IKKE en fanerad (2026-08-20,
det eneste de fleste trenger midt i en runde) og "Spillere og reelt brukerfunn: en andre pille-fanerad her, rett under
runde" (alt administrativt, samlet ett sted i stedet for å ligge RoundHeader sin egen Score/Scorekort/Leaderboard-rad, leste som
i veien øverst scoringssiden). */} to duplikate "Score"-kontroller rett oppå hverandre). "Score"
<div className="mb-5 inline-flex gap-1 rounded-full bg-muted p-1"> (hull-navigasjon + registrering, default) og "Spillere og runde"
(alt administrativt) er fortsatt to distinkte visninger denne
siden -- kun selve KONTROLLEN er en enkelt, tydelig ANNERLEDES
knapp (ramme + ikon, ikke en pille) i stedet for en segmentert
fanerad som konkurrerer visuelt med hovedfanene over. */}
<div className="mb-5 flex justify-end">
<button <button
type="button" type="button"
onClick={() => setPageTab("score")} onClick={() => setPageTab(pageTab === "score" ? "manage" : "score")}
aria-pressed={pageTab === "score"} className="flex h-11 items-center gap-2 rounded-xl border border-border bg-card px-3 text-sm font-bold text-foreground transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className={cn(
"min-h-11 rounded-full px-4 text-sm font-bold transition-colors",
pageTab === "score" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground",
)}
>
Score
</button>
<button
type="button"
onClick={() => setPageTab("manage")}
aria-pressed={pageTab === "manage"}
className={cn(
"min-h-11 rounded-full px-4 text-sm font-bold transition-colors",
pageTab === "manage" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground",
)}
> >
{pageTab === "score" ? (
<>
<Settings2 aria-hidden="true" className="size-4 shrink-0 text-primary" />
Spillere og runde Spillere og runde
</>
) : (
<>
<ChevronLeft aria-hidden="true" className="size-4 shrink-0" />
Tilbake til score
</>
)}
</button> </button>
</div> </div>
@ -6616,6 +6617,14 @@ function PlayerHoleCards({
// sitter her i kortet (skjermen FØR slagvinduet), åpner denne fullskjerm- // sitter her i kortet (skjermen FØR slagvinduet), åpner denne fullskjerm-
// visningen ved klikk i stedet for å ekspandere inline. // visningen ved klikk i stedet for å ekspandere inline.
const [historyPlayerId, setHistoryPlayerId] = useState<string | null>(null) const [historyPlayerId, setHistoryPlayerId] = useState<string | null>(null)
// Avstandsmåler-utvidelse (2026-08-20): kompakt som standard, slik at
// spillerkortene under peker inn i synlig område med det samme -- en
// førstegangsbruker skal ikke måtte oppdage ved et tilfeldig sveip at
// score-registrering finnes lenger ned. "Vis banekart"-knappen vises kun
// når banen faktisk HAR koordinatdata (ellers ingenting å utvide til).
const [mapExpanded, setMapExpanded] = useState(false)
const [distanceAvailable, setDistanceAvailable] = useState(false)
const playerListRef = useRef<HTMLUListElement>(null)
const holeMeta: Record<number, { par: number; index: number }> = {} const holeMeta: Record<number, { par: number; index: number }> = {}
for (const list of Object.values(holesByParticipant)) { for (const list of Object.values(holesByParticipant)) {
for (const h of list) { for (const h of list) {
@ -6719,10 +6728,45 @@ function PlayerHoleCards({
{/* Avstand til grønn (ADR-064, utvidet til ekte teeoff-baner i {/* Avstand til grønn (ADR-064, utvidet til ekte teeoff-baner i
migrasjon 079) -- kun for baner med koordinatdata; ingen synlig migrasjon 079) -- kun for baner med koordinatdata; ingen synlig
rad i det hele tatt for andre baner (HoleTargetDistance returnerer rad i det hele tatt for andre baner (HoleTargetDistance returnerer
null da). Full bredde, samme plassering brukeren pekte ut (rett null da, "Vis banekart"-knappen skjules da også via
under hull-navigatoren, over spillerkortene) -- ikke den smale distanceAvailable). Kompakt som standard (2026-08-20, reelt
kompaktvarianten. */} brukerfunn: det store diagrammet fylte hele skjermen mobil, og
<HoleTargetDistance baseUrl={`/rounds/${roundId}`} holeNumber={currentHole} size="full" /> ingenting antydet at spillerkortene under fantes) -- kan utvides
til det fulle diagrammet med hindringer forespørsel. */}
<HoleTargetDistance
baseUrl={`/rounds/${roundId}`}
holeNumber={currentHole}
size={mapExpanded ? "full" : "compact"}
onAvailabilityChange={setDistanceAvailable}
/>
{distanceAvailable && (
<button
type="button"
onClick={() => setMapExpanded((v) => !v)}
aria-expanded={mapExpanded}
className="flex h-11 items-center justify-center gap-2 self-center rounded-xl border border-border bg-card px-4 text-sm font-bold text-foreground transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<MapPin aria-hidden="true" className="size-4 shrink-0 text-primary" />
{mapExpanded ? "Skjul banekart" : "Vis banekart og hindringer"}
<ChevronDown
aria-hidden="true"
className={cn("size-4 shrink-0 transition-transform", mapExpanded && "rotate-180")}
/>
</button>
)}
{/* Alltid synlig, tydelig snarvei rett til score-registreringen under
-- samme grunn som knappen over: en førstegangsbruker skal ikke
måtte oppdage ved et tilfeldig sveip at dette finnes. */}
<button
type="button"
onClick={() => playerListRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })}
className="flex h-14 w-full items-center justify-center gap-2 rounded-2xl bg-primary text-base font-extrabold text-primary-foreground shadow-sm transition-colors hover:brightness-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
Registrer score
<ArrowDown aria-hidden="true" className="size-5" />
</button>
{isTwoSided && sides.length === 2 && ( {isTwoSided && sides.length === 2 && (
<div className="flex items-center justify-center gap-2 rounded-2xl border border-border bg-card px-4 py-2.5 text-sm"> <div className="flex items-center justify-center gap-2 rounded-2xl border border-border bg-card px-4 py-2.5 text-sm">
@ -6734,7 +6778,7 @@ function PlayerHoleCards({
</div> </div>
)} )}
<ul className="flex flex-col gap-3"> <ul ref={playerListRef} className="flex scroll-mt-4 flex-col gap-3">
{sortedPlayers.map((player) => { {sortedPlayers.map((player) => {
const list = holesByParticipant[player.id] const list = holesByParticipant[player.id]
const apiHole = list?.find((h) => h.hole_number === currentHole) ?? null const apiHole = list?.find((h) => h.hole_number === currentHole) ?? null