Hull-valget overlever nå en refresh (?hole=-URL-parameter)
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 <noreply@anthropic.com>
This commit is contained in:
parent
f5c79e8767
commit
0a9320991b
2 changed files with 72 additions and 10 deletions
28
CHANGELOG.md
28
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`).
|
||||
|
|
|
|||
|
|
@ -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<ApiRound | null>(null)
|
||||
const [error, setError] = useState<string | null>(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<number | null>(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<number | null>(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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue