From 0a9320991bb8a2eff75c0fa019562abbe1ac4a84 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Tue, 18 Aug 2026 10:06:48 +0200 Subject: [PATCH] =?UTF-8?q?Hull-valget=20overlever=20n=C3=A5=20en=20refres?= =?UTF-8?q?h=20(=3Fhole=3D-URL-parameter)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit currentHole levde kun i React-state, seedet fra round.start_hole ved hver mount -- en refresh mistet valget og hoppet alltid tilbake til starthullet. Fikset med samme URL-parameter-mønster tab-valget allerede bruker: ny setHoleAndUrl() holder state og ?hole=N i sync via router.replace (ikke push, for å unngå å fylle historikken med ett tilbake-steg per hull). Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 28 +++++++++++++++ frontend/components/round-detail.tsx | 54 ++++++++++++++++++++++------ 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15659b5..7f363be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12305,3 +12305,31 @@ Neste steg: frittstående runder, én for org-turneringer) -- verdt å huske ved fremtidige datakorreksjoner, for denne eller andre baner med begge tilkoblingstyper. + +108. **Hull-valget overlevde ikke en refresh — 2026-08-18.** Bruker + rapporterte at en refresh mens man sto på f.eks. hull 7 alltid + hoppet tilbake til starthullet. Årsak: `currentHole` i + `round-detail.tsx` levde KUN i React-state (seedet fra + `round.start_hole` ved hver mount, se den eksisterende `null`- vs + `1`-forklaringen fra 2026-07-24) -- aldri i URL-en, så en full + remount (refresh) mistet valget fullstendig. + + Fikset ved å gjenbruke det samme `?param`-i-URL-mønsteret siden + allerede har for fane-valget (`?tab=manage`): ny `?hole=N`- + parameter, lest ved mount (`initialHole`), holdt i sync ved + senere navigasjon via en ny felles `setHoleAndUrl()`-funksjon + (erstatter ALLE direkte `setCurrentHole`-kall utenom selve + URL-seedingen i `loadRound`) som oppdaterer BÅDE state og URL med + `router.replace` (ikke `push` -- hull-bytte skal ikke fylle + historikken med ett tilbake-steg per hull). En egen `useEffect` + speiler `?hole=`-parameteren tilbake inn i state ved eksterne + URL-endringer (nettleserens frem/tilbake). + + **Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Scratch- + runde (database+API+next dev): naviger til hull 7 (URL bekreftet + `?hole=7`), hard refresh (`ignoreCache`) -- siden viste fortsatt + hull 7, ikke hull 1. "Neste hull" bekreftet også oppdaterer URL-en + (`?hole=8`). Scratch-stacken revet ned. + + **Ingen migrasjon** -- ren frontend-endring + (`frontend/components/round-detail.tsx`). diff --git a/frontend/components/round-detail.tsx b/frontend/components/round-detail.tsx index 28b6414..89c06ba 100644 --- a/frontend/components/round-detail.tsx +++ b/frontend/components/round-detail.tsx @@ -12,7 +12,7 @@ import type React from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import Link from "next/link" import dynamic from "next/dynamic" -import { useRouter, useSearchParams } from "next/navigation" +import { usePathname, useRouter, useSearchParams } from "next/navigation" import { RoundPageShell } from "@/components/round-page-shell" import { RoundMessages } from "@/components/round-messages" import type { FlagMapEntry } from "@/components/flag-map-overview" @@ -453,8 +453,18 @@ export function RoundDetail({ roundId }: { roundId: string }) { // med en effekt som reagerer på selve søkeparameter-ENDRINGEN, ikke // bare mount -- ensrettet (kun til "manage"), rører aldri brukerens // egen lokale "Score"-valg via pillen etterpå. + const pathname = usePathname() const searchParams = useSearchParams() const initialTab = searchParams.get("tab") === "manage" ? "manage" : "score" + // Bruker rapporterte 2026-08-18: en refresh mens man sto på f.eks. hull 7 + // hoppet alltid tilbake til starthullet (se `currentHole` sin seeding fra + // `data.start_hole` i loadRound under) -- hull-valget levde KUN i React- + // state, aldri i URL-en, så en full remount (refresh) mistet det + // fullstendig. Samme `?param`-i-URL-mønster som `tab` over: les ved mount, + // hold i sync ved senere endringer via `setHoleAndUrl` (kalt fra ALLE + // steder som i dag kaller `setCurrentHole` direkte). + const initialHoleParam = searchParams.get("hole") + const initialHole = initialHoleParam && /^\d+$/.test(initialHoleParam) ? Number(initialHoleParam) : null const [round, setRound] = useState(null) const [error, setError] = useState(null) // ADR-057 (2026-08-10): `error` over er en FATAL, hele-siden-erstattende @@ -483,8 +493,23 @@ export function RoundDetail({ roundId }: { roundId: string }) { // `null` betyr "ikke satt ennå" -- MÅ være null, ikke f.eks. 1, siden 1 // er en gyldig, truthy hullverdi og ville gjort `prev || start_hole` // lenger ned til en no-op (funnet 2026-07-24: runden åpnet alltid på - // hull 1 uansett faktisk starthull). - const [currentHole, setCurrentHole] = useState(null) + // hull 1 uansett faktisk starthull). Seedes fra `?hole=`-URL-parameteren + // hvis den finnes (se over) -- ellers forblir den `null` til `loadRound` + // fyller inn `data.start_hole`, akkurat som før. + const [currentHole, setCurrentHole] = useState(initialHole) + // Eneste stedet hull-navigasjon skal skje fra -- oppdaterer BÅDE React- + // state og URL-en (`?hole=`) sammen, slik at en refresh beholder valget. + // `router.replace` (ikke `push`) -- hull-bytte skal ikke fylle historikken + // med et tilbake-steg per hull. + const setHoleAndUrl = useCallback( + (n: number) => { + setCurrentHole(n) + const params = new URLSearchParams(searchParams.toString()) + params.set("hole", String(n)) + router.replace(`${pathname}?${params.toString()}`, { scroll: false }) + }, + [pathname, router, searchParams], + ) // ADR-039 -- bumpes hver gang noe som kan ha endret matchstatus/skins- // tavlen skjer (hull-registrering, websocket-signal), slik at // formatResult-hentingen under vet den skal hente på nytt. @@ -516,6 +541,15 @@ export function RoundDetail({ roundId }: { roundId: string }) { useEffect(() => { if (searchParams.get("tab") === "manage") setPageTab("manage") }, [searchParams]) + // Fanger opp nettleserens frem/tilbake-navigasjon (URL-en endrer seg uten + // at `setHoleAndUrl` kalte den) -- speiler `?hole=`-parameteren inn i + // state igjen. Sammenligner mot gjeldende verdi først for å unngå en + // uendelig løkke med `setHoleAndUrl` sin egen `router.replace`. + useEffect(() => { + const p = searchParams.get("hole") + const n = p && /^\d+$/.test(p) ? Number(p) : null + if (n !== null) setCurrentHole((prev) => (prev === n ? prev : n)) + }, [searchParams]) const [completing, setCompleting] = useState(false) const [deleting, setDeleting] = useState(false) const [showEditRound, setShowEditRound] = useState(false) @@ -1085,12 +1119,12 @@ export function RoundDetail({ roundId }: { roundId: string }) { function goPrev() { const i = holeOrder.indexOf(activeHole) - setCurrentHole(holeOrder[(i - 1 + holeOrder.length) % holeOrder.length]) + setHoleAndUrl(holeOrder[(i - 1 + holeOrder.length) % holeOrder.length]) scrollToHolePanel() } function goNext() { const i = holeOrder.indexOf(activeHole) - setCurrentHole(holeOrder[(i + 1) % holeOrder.length]) + setHoleAndUrl(holeOrder[(i + 1) % holeOrder.length]) scrollToHolePanel() } @@ -1121,7 +1155,7 @@ export function RoundDetail({ roundId }: { roundId: string }) { // ScoringWizard. Alle andre formater (ingen team-side) oppfører seg // som før. function openPlayerOrTeamSideEntry(playerId: string, holeNumber: number) { - setCurrentHole(holeNumber) + setHoleAndUrl(holeNumber) const player = players.find((p) => p.id === playerId) if (scrambleSoloTeamSideId && player?.roundSideId === scrambleSoloTeamSideId) { setWizardSideId(scrambleSoloTeamSideId) @@ -1729,11 +1763,11 @@ export function RoundDetail({ roundId }: { roundId: string }) { isTwoSided={TWO_SIDED_FORMATS.has(round.play_format)} formatResult={formatResult} onSelectHole={(n) => { - setCurrentHole(n) + setHoleAndUrl(n) scrollToHolePanel() }} onOpenEntry={(sideId, holeNumber) => { - setCurrentHole(holeNumber) + setHoleAndUrl(holeNumber) setWizardSideId(sideId) }} /> @@ -1793,7 +1827,7 @@ export function RoundDetail({ roundId }: { roundId: string }) { formatResult={formatResult} ownBagClubs={ownBagClubs} onSelectHole={(n) => { - setCurrentHole(n) + setHoleAndUrl(n) scrollToHolePanel() }} onPrevHole={goPrev} @@ -1832,7 +1866,7 @@ export function RoundDetail({ roundId }: { roundId: string }) { isTwoSided={TWO_SIDED_FORMATS.has(round.play_format)} formatResult={formatResult} onSelectHole={(n) => { - setCurrentHole(n) + setHoleAndUrl(n) scrollToHolePanel() }} onOpenEntry={openPlayerOrTeamSideEntry}