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
|
frittstående runder, én for org-turneringer) -- verdt å huske ved
|
||||||
fremtidige datakorreksjoner, for denne eller andre baner med
|
fremtidige datakorreksjoner, for denne eller andre baner med
|
||||||
begge tilkoblingstyper.
|
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 { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import dynamic from "next/dynamic"
|
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 { 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"
|
||||||
|
|
@ -453,8 +453,18 @@ export function RoundDetail({ roundId }: { roundId: string }) {
|
||||||
// med en effekt som reagerer på selve søkeparameter-ENDRINGEN, ikke
|
// med en effekt som reagerer på selve søkeparameter-ENDRINGEN, ikke
|
||||||
// bare mount -- ensrettet (kun til "manage"), rører aldri brukerens
|
// bare mount -- ensrettet (kun til "manage"), rører aldri brukerens
|
||||||
// egen lokale "Score"-valg via pillen etterpå.
|
// egen lokale "Score"-valg via pillen etterpå.
|
||||||
|
const pathname = usePathname()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const initialTab = searchParams.get("tab") === "manage" ? "manage" : "score"
|
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 [round, setRound] = useState<ApiRound | null>(null)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
// ADR-057 (2026-08-10): `error` over er en FATAL, hele-siden-erstattende
|
// 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
|
// `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`
|
// 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å
|
// lenger ned til en no-op (funnet 2026-07-24: runden åpnet alltid på
|
||||||
// hull 1 uansett faktisk starthull).
|
// hull 1 uansett faktisk starthull). Seedes fra `?hole=`-URL-parameteren
|
||||||
const [currentHole, setCurrentHole] = useState<number | null>(null)
|
// 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-
|
// ADR-039 -- bumpes hver gang noe som kan ha endret matchstatus/skins-
|
||||||
// tavlen skjer (hull-registrering, websocket-signal), slik at
|
// tavlen skjer (hull-registrering, websocket-signal), slik at
|
||||||
// formatResult-hentingen under vet den skal hente på nytt.
|
// formatResult-hentingen under vet den skal hente på nytt.
|
||||||
|
|
@ -516,6 +541,15 @@ export function RoundDetail({ roundId }: { roundId: string }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchParams.get("tab") === "manage") setPageTab("manage")
|
if (searchParams.get("tab") === "manage") setPageTab("manage")
|
||||||
}, [searchParams])
|
}, [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 [completing, setCompleting] = useState(false)
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
const [showEditRound, setShowEditRound] = useState(false)
|
const [showEditRound, setShowEditRound] = useState(false)
|
||||||
|
|
@ -1085,12 +1119,12 @@ export function RoundDetail({ roundId }: { roundId: string }) {
|
||||||
|
|
||||||
function goPrev() {
|
function goPrev() {
|
||||||
const i = holeOrder.indexOf(activeHole)
|
const i = holeOrder.indexOf(activeHole)
|
||||||
setCurrentHole(holeOrder[(i - 1 + holeOrder.length) % holeOrder.length])
|
setHoleAndUrl(holeOrder[(i - 1 + holeOrder.length) % holeOrder.length])
|
||||||
scrollToHolePanel()
|
scrollToHolePanel()
|
||||||
}
|
}
|
||||||
function goNext() {
|
function goNext() {
|
||||||
const i = holeOrder.indexOf(activeHole)
|
const i = holeOrder.indexOf(activeHole)
|
||||||
setCurrentHole(holeOrder[(i + 1) % holeOrder.length])
|
setHoleAndUrl(holeOrder[(i + 1) % holeOrder.length])
|
||||||
scrollToHolePanel()
|
scrollToHolePanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1121,7 +1155,7 @@ export function RoundDetail({ roundId }: { roundId: string }) {
|
||||||
// ScoringWizard. Alle andre formater (ingen team-side) oppfører seg
|
// ScoringWizard. Alle andre formater (ingen team-side) oppfører seg
|
||||||
// som før.
|
// som før.
|
||||||
function openPlayerOrTeamSideEntry(playerId: string, holeNumber: number) {
|
function openPlayerOrTeamSideEntry(playerId: string, holeNumber: number) {
|
||||||
setCurrentHole(holeNumber)
|
setHoleAndUrl(holeNumber)
|
||||||
const player = players.find((p) => p.id === playerId)
|
const player = players.find((p) => p.id === playerId)
|
||||||
if (scrambleSoloTeamSideId && player?.roundSideId === scrambleSoloTeamSideId) {
|
if (scrambleSoloTeamSideId && player?.roundSideId === scrambleSoloTeamSideId) {
|
||||||
setWizardSideId(scrambleSoloTeamSideId)
|
setWizardSideId(scrambleSoloTeamSideId)
|
||||||
|
|
@ -1729,11 +1763,11 @@ export function RoundDetail({ roundId }: { roundId: string }) {
|
||||||
isTwoSided={TWO_SIDED_FORMATS.has(round.play_format)}
|
isTwoSided={TWO_SIDED_FORMATS.has(round.play_format)}
|
||||||
formatResult={formatResult}
|
formatResult={formatResult}
|
||||||
onSelectHole={(n) => {
|
onSelectHole={(n) => {
|
||||||
setCurrentHole(n)
|
setHoleAndUrl(n)
|
||||||
scrollToHolePanel()
|
scrollToHolePanel()
|
||||||
}}
|
}}
|
||||||
onOpenEntry={(sideId, holeNumber) => {
|
onOpenEntry={(sideId, holeNumber) => {
|
||||||
setCurrentHole(holeNumber)
|
setHoleAndUrl(holeNumber)
|
||||||
setWizardSideId(sideId)
|
setWizardSideId(sideId)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1793,7 +1827,7 @@ export function RoundDetail({ roundId }: { roundId: string }) {
|
||||||
formatResult={formatResult}
|
formatResult={formatResult}
|
||||||
ownBagClubs={ownBagClubs}
|
ownBagClubs={ownBagClubs}
|
||||||
onSelectHole={(n) => {
|
onSelectHole={(n) => {
|
||||||
setCurrentHole(n)
|
setHoleAndUrl(n)
|
||||||
scrollToHolePanel()
|
scrollToHolePanel()
|
||||||
}}
|
}}
|
||||||
onPrevHole={goPrev}
|
onPrevHole={goPrev}
|
||||||
|
|
@ -1832,7 +1866,7 @@ export function RoundDetail({ roundId }: { roundId: string }) {
|
||||||
isTwoSided={TWO_SIDED_FORMATS.has(round.play_format)}
|
isTwoSided={TWO_SIDED_FORMATS.has(round.play_format)}
|
||||||
formatResult={formatResult}
|
formatResult={formatResult}
|
||||||
onSelectHole={(n) => {
|
onSelectHole={(n) => {
|
||||||
setCurrentHole(n)
|
setHoleAndUrl(n)
|
||||||
scrollToHolePanel()
|
scrollToHolePanel()
|
||||||
}}
|
}}
|
||||||
onOpenEntry={openPlayerOrTeamSideEntry}
|
onOpenEntry={openPlayerOrTeamSideEntry}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue