Score-fanen: flytende knapp ved mange hindringer, hindringsrekkefølge, kartretning
- Flytende "Registrer score" trigges nå av faktisk synlighet til side-flyt-knappen, ikke av om banekartet er utvidet -- en lang hindringsliste alene (uten utvidet kart) kunne dytte knappen ut av syne uten at den flytende versjonen noensinne dukket opp. - Hindringslisten sortert fra fjernest (øverst) til nærmest (nederst), matcher loddrett telefon-holdning med green øverst/utslag nederst. Bakkant brukt som sorteringsnøkkel der en finnes. - Kartretning ved slagmåling følger nå ekte utslag->green-geometri (green opp/utslag ned) når begge er kjent, vinner over den eldre forrige-slag-gjetningen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
a0163eaa16
commit
e940e932c1
2 changed files with 130 additions and 50 deletions
|
|
@ -191,13 +191,15 @@ function groupDiagramHazards(
|
|||
location: p.location,
|
||||
})
|
||||
}
|
||||
// ADR-083-konvensjonen ("sortert nærmest spilleren først") manglet her --
|
||||
// uten denne falt rekkefølgen tilbake til rå API-/innsettingsrekkefølge,
|
||||
// som ikke sier noe om avstand og heller ikke oppdaterer seg når
|
||||
// spilleren beveger seg. `below` er alltid den nærmeste av forkant/
|
||||
// enkeltpunkt-avstanden, så stigende sortering på den gir riktig
|
||||
// "nærmest spilleren først" for både parrede og enkeltstående hindringer.
|
||||
return entities.sort((a, b) => a.below - b.below)
|
||||
// Rettet 2026-08-21 (brukerfunn, skjermdump): listen skal lese som
|
||||
// kartet den står under -- holdt loddrett med greenen øverst (lengst
|
||||
// unna) og utslaget nederst (nærmest), skal hindringene imellom følge
|
||||
// SAMME rekkefølge, ikke motsatt. Sorterer derfor synkende på
|
||||
// BAKKANTEN når en finnes (`above` -- den ferneste kanten av en parret
|
||||
// hindring, eksplisitt bedt om av bruker: "avstanden til bakkant [som]
|
||||
// vi hensyntar når vi skal sortere"), ellers enkeltpunktets egen
|
||||
// avstand (`below`, kun eksisterende verdi for et upar ret punkt).
|
||||
return entities.sort((a, b) => (b.above ?? b.below) - (a.above ?? a.below))
|
||||
}
|
||||
|
||||
export function HoleTargetDistance({
|
||||
|
|
@ -314,9 +316,13 @@ export function HoleTargetDistance({
|
|||
}
|
||||
|
||||
// ADR-083: ALLE hindringer, ikke bare nærmeste (tidligere reduserte denne
|
||||
// løkken til ett enkelt nearestHazard-objekt) -- sortert nærmest
|
||||
// spilleren først. Avstandene er ALLTID live fra spilleren, uavhengig av
|
||||
// om et geometrisk diagram kan tegnes under.
|
||||
// løkken til ett enkelt nearestHazard-objekt). Avstandene er ALLTID live
|
||||
// fra spilleren, uavhengig av om et geometrisk diagram kan tegnes under.
|
||||
// Sortert lengst unna FØRST (2026-08-21, samme retting/begrunnelse som
|
||||
// groupDiagramHazards under -- listen leses loddrett med greenen
|
||||
// øverst) -- denne grenen (ingen tee-koordinater) har ingen forkant/
|
||||
// bakkant-parring i det hele tatt, kun ett enkelt avstandstall per
|
||||
// punkt, så ingen "bakkant"-nyanse å ta hensyn til her.
|
||||
const hazardPoints = points.filter((p) => p.poi_type in _HAZARD_LABELS)
|
||||
// ADR-085 (brukerønske 2026-08-20): passerte hindringer skal ikke vises.
|
||||
// Denne (tee-referanse mangler) grenen har ingen akse å projisere langs,
|
||||
|
|
@ -336,7 +342,7 @@ export function HoleTargetDistance({
|
|||
: base
|
||||
return { label, distanceMeters: Math.round(haversineMeters(livePosition, { lat: h.latitude, lng: h.longitude })), lat: h.latitude, lng: h.longitude }
|
||||
})
|
||||
.sort((a, b) => a.distanceMeters - b.distanceMeters)
|
||||
.sort((a, b) => b.distanceMeters - a.distanceMeters)
|
||||
|
||||
// Fast tee-referanse (IKKE spillerens bevegelige livePosition) for selve
|
||||
// hull-diagrammet -- se ADR-083. Midtpunkt hvis begge tee_front/tee_back
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import { ShotMeasurementSheet } from "@/components/shot/shot-measurement-sheet"
|
|||
import { HoleTargetDistance } from "@/components/hole-target-distance"
|
||||
import { HoleHistoryDetail } from "@/components/hole-history-detail"
|
||||
import { bearingDegrees } from "@/lib/geo"
|
||||
import { midpoint } from "@/lib/hole-geometry"
|
||||
import { enqueueWrite, flushQueue, queueCount } from "@/lib/offline-queue"
|
||||
|
||||
// --- Types -----------------------------------------------------------------
|
||||
|
|
@ -2431,25 +2432,69 @@ function ShotMeasurementEntry({
|
|||
}, [urlBase])
|
||||
const count = shots.length
|
||||
|
||||
// Kartretning ved slagmåling (brukerønske 2026-08-10): "opp på skjermen"
|
||||
// følger spillerens EGEN gangretning på hullet, uten lagrede green-
|
||||
// koordinater. Regnet ut fra det SISTE tidligere målte slaget på dette
|
||||
// hullet (start→slutt-retningen) -- `shots` er allerede sortert
|
||||
// stigende på shot_number av GET-endepunktet (se _list_shots_for_hole i
|
||||
// rounds.py), så siste element ER forrige slag. `undefined` (ingen
|
||||
// rotasjon regnet ut ennå) for det aller første slaget på hullet --
|
||||
// MapPointPicker faller da selv tilbake til enhetens GPS-heading/nord,
|
||||
// se den komponentens dokumentasjon. Bevisst en STATISK retning, satt
|
||||
// idet arket åpnes -- ikke løpende oppdatert (ville vært urolig ved lav
|
||||
// Ekte banegeometri (2026-08-21, brukerønske) -- når hullet har BÅDE
|
||||
// utslags- OG green-koordinater kjent (samme datakilde som
|
||||
// HoleTargetDistance/HoleDiagram bruker), skal kartretningen ALLTID
|
||||
// følge utslag->green, ikke gjettes fra spillerens forrige slag (som
|
||||
// kan avvike, f.eks. en slice/hook). Egen, liten fetch her (i stedet
|
||||
// for å prop-drille et tall gjennom RoundDetail/PlayerHoleCards/
|
||||
// ScoringWizard -- ShotMeasurementEntry brukes fra tre atskilte steder
|
||||
// i denne filen, kun roundId+holeNumber er felles for alle) -- ett
|
||||
// ekstra, lett GET-kall per spillerkort er en akseptert kostnad for at
|
||||
// ALLE tre inngangene til kartet får riktig retning, ikke bare én.
|
||||
const [courseBearing, setCourseBearing] = useState<number | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
setCourseBearing(undefined)
|
||||
fetch(`/rounds/${roundId}/holes/${holeNumber}/target-points`, { credentials: "include" })
|
||||
.then((res) => (res.ok ? res.json() : []))
|
||||
.then((points: { poi_type: string; location: string | null; latitude: number; longitude: number }[]) => {
|
||||
if (cancelled) return
|
||||
const middle = points.find((p) => p.poi_type === "green" && p.location === "middle")
|
||||
const teeFront = points.find((p) => p.poi_type === "tee_front")
|
||||
const teeBack = points.find((p) => p.poi_type === "tee_back")
|
||||
const teeRef =
|
||||
teeFront && teeBack
|
||||
? midpoint({ lat: teeFront.latitude, lng: teeFront.longitude }, { lat: teeBack.latitude, lng: teeBack.longitude })
|
||||
: teeFront
|
||||
? { lat: teeFront.latitude, lng: teeFront.longitude }
|
||||
: teeBack
|
||||
? { lat: teeBack.latitude, lng: teeBack.longitude }
|
||||
: null
|
||||
setCourseBearing(
|
||||
teeRef && middle ? bearingDegrees(teeRef, { lat: middle.latitude, lng: middle.longitude }) : undefined,
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setCourseBearing(undefined)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [roundId, holeNumber])
|
||||
|
||||
// Kartretning ved slagmåling (brukerønske 2026-08-10, utvidet
|
||||
// 2026-08-21): "opp på skjermen" følger banens EGEN spillretning
|
||||
// (utslag->green, courseBearing over) når den er kjent -- ellers
|
||||
// faller tilbake til spillerens EGEN gangretning, regnet ut fra det
|
||||
// SISTE tidligere målte slaget på dette hullet (start→slutt-retningen)
|
||||
// -- `shots` er allerede sortert stigende på shot_number av GET-
|
||||
// endepunktet (se _list_shots_for_hole i rounds.py), så siste element
|
||||
// ER forrige slag. `undefined` (ingen rotasjon regnet ut ennå) for det
|
||||
// aller første slaget på et hull uten kjent banegeometri -- MapPoint-
|
||||
// Picker faller da selv tilbake til enhetens GPS-heading/nord, se den
|
||||
// komponentens dokumentasjon. Bevisst en STATISK retning, satt idet
|
||||
// arket åpnes -- ikke løpende oppdatert (ville vært urolig ved lav
|
||||
// gangfart), se map-point-picker.tsx.
|
||||
const initialBearing = useMemo(() => {
|
||||
if (typeof courseBearing === "number") return courseBearing
|
||||
const previous = shots[shots.length - 1]
|
||||
if (!previous) return undefined
|
||||
return bearingDegrees(
|
||||
{ lat: previous.start_lat, lng: previous.start_lng },
|
||||
{ lat: previous.end_lat, lng: previous.end_lng },
|
||||
)
|
||||
}, [shots])
|
||||
}, [shots, courseBearing])
|
||||
|
||||
async function deleteShot(shotId: string) {
|
||||
const res = await fetch(`/rounds/${roundId}/shots/${shotId}`, { method: "DELETE", credentials: "include" })
|
||||
|
|
@ -6812,6 +6857,35 @@ function PlayerHoleCards({
|
|||
window.removeEventListener("resize", check)
|
||||
}
|
||||
}, [mapExpanded])
|
||||
// Den flytende knappen ble opprinnelig styrt av `mapExpanded` alene --
|
||||
// FEIL, funnet av bruker 2026-08-20: den IKKE-utvidede ("compact")
|
||||
// visningen viser OGSÅ hele hindringslisten (ADR-083, alle hindringer,
|
||||
// ikke bare nærmeste), som på hull med mange hindringer alene kan bli
|
||||
// høy nok til å dytte den vanlige, side-flyt-plasserte knappen utenfor
|
||||
// synlig område -- UTEN at banekartet noensinne ble utvidet. Riktig
|
||||
// signal er derfor ikke "er kartet utvidet", men "er selve
|
||||
// side-flyt-knappen faktisk synlig akkurat nå" -- målt direkte, samme
|
||||
// mønster som `playerListReached` over (ikke IntersectionObserver, av
|
||||
// samme grunn som der: unngår enhver antakelse om når komponenten
|
||||
// regnes som "utvidet" eller ikke -- fungerer likt uansett ÅRSAK til at
|
||||
// knappen er utenfor synlig område).
|
||||
const inFlowButtonRef = useRef<HTMLDivElement>(null)
|
||||
const [inFlowButtonVisible, setInFlowButtonVisible] = useState(true)
|
||||
useEffect(() => {
|
||||
function check() {
|
||||
const el = inFlowButtonRef.current
|
||||
if (!el) return
|
||||
const rect = el.getBoundingClientRect()
|
||||
setInFlowButtonVisible(rect.bottom > getHeaderHeight() && rect.top < window.innerHeight)
|
||||
}
|
||||
check()
|
||||
window.addEventListener("scroll", check, { passive: true })
|
||||
window.addEventListener("resize", check)
|
||||
return () => {
|
||||
window.removeEventListener("scroll", check)
|
||||
window.removeEventListener("resize", check)
|
||||
}
|
||||
}, [mapExpanded, distanceAvailable])
|
||||
// Manuell scroll (ikke `scrollIntoView` + CSS `scroll-margin-top`) --
|
||||
// samme begrunnelse som `getHeaderHeight()` over: en CSS-klasse som
|
||||
// `scroll-mt-28` kan ikke være dynamisk, og headerens høyde varierer
|
||||
|
|
@ -6954,18 +7028,18 @@ function PlayerHoleCards({
|
|||
</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. KUN i normal
|
||||
side-flyt når banekartet er kollapset -- utvidet blir den samme
|
||||
knappen flytende i stedet, se render-stedet nederst i denne
|
||||
komponenten (brukerønske 2026-08-20: det utvidede diagrammet
|
||||
kunne dytte knappen helt utenfor synlig skjermbilde). */}
|
||||
{!mapExpanded && (
|
||||
{/* Alltid i normal side-flyt, uansett banekart-tilstand (rettet
|
||||
2026-08-20 -- se `inFlowButtonVisible`-kommentaren over: den
|
||||
MÅTTE bli værende i flyten uansett, siden en lang hindringsliste
|
||||
alene kan dytte den ut av syne selv når kartet ALDRI ble utvidet).
|
||||
`ref` brukt til å måle om DENNE knappen faktisk er synlig -- den
|
||||
flytende versjonen (render-stedet nederst i komponenten) dukker
|
||||
kun opp når den IKKE er det. */}
|
||||
<div ref={inFlowButtonRef}>
|
||||
<RegisterScoreButton
|
||||
onClick={scrollToPlayerList}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{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">
|
||||
|
|
@ -7206,24 +7280,26 @@ function PlayerHoleCards({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Flytende "Registrer score" (V0-eksport, 2026-08-20) -- mens
|
||||
banekartet er utvidet OG spillerlisten fortsatt er utenfor synlig
|
||||
område (se `playerListReached` over -- uten denne betingelsen hang
|
||||
knappen igjen synlig, og dekket delvis over spillerkort 1, selv
|
||||
etter at brukeren allerede hadde scrollet dit). `fixed`
|
||||
posisjonerer mot selve viewporten (ingen ancestor her setter
|
||||
transform/filter, så det er trygt), gradient-scrim + skygge
|
||||
løfter den visuelt over innholdet under, `env(safe-area-inset-
|
||||
bottom)` unngår at den klemmes helt inntil kanten på telefoner
|
||||
med gest-navigasjon. `pointer-events-none` på selve scrim-sonen
|
||||
når skjult/i overgang, slik at den aldri blokkerer klikk på
|
||||
innhold bak. */}
|
||||
{/* Flytende "Registrer score" (V0-eksport, 2026-08-20, utløser-
|
||||
logikk rettet samme dag) -- vises når side-flyt-knappen over
|
||||
IKKE er synlig (uansett ÅRSAK: utvidet banekart ELLER en lang
|
||||
hindringsliste alene, se `inFlowButtonVisible`-kommentaren over)
|
||||
OG spillerlisten fortsatt er utenfor synlig område (se
|
||||
`playerListReached` -- uten denne dekket den delvis over
|
||||
spillerkort 1 selv etter at brukeren allerede hadde scrollet
|
||||
dit). `fixed` posisjonerer mot selve viewporten (ingen ancestor
|
||||
her setter transform/filter, så det er trygt), gradient-scrim +
|
||||
skygge løfter den visuelt over innholdet under,
|
||||
`env(safe-area-inset-bottom)` unngår at den klemmes helt inntil
|
||||
kanten på telefoner med gest-navigasjon. `pointer-events-none`
|
||||
på selve scrim-sonen når skjult/i overgang, slik at den aldri
|
||||
blokkerer klikk på innhold bak. */}
|
||||
<div
|
||||
aria-hidden={!mapExpanded || playerListReached}
|
||||
aria-hidden={inFlowButtonVisible || playerListReached}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-30 px-5 pb-[calc(0.875rem+env(safe-area-inset-bottom))] pt-8 transition-all duration-300 ease-out",
|
||||
"bg-gradient-to-t from-background via-background/90 to-transparent",
|
||||
mapExpanded && !playerListReached
|
||||
!inFlowButtonVisible && !playerListReached
|
||||
? "translate-y-0 opacity-100"
|
||||
: "pointer-events-none translate-y-full opacity-0",
|
||||
)}
|
||||
|
|
@ -7231,7 +7307,7 @@ function PlayerHoleCards({
|
|||
<div
|
||||
className={cn(
|
||||
"mx-auto w-full max-w-3xl",
|
||||
mapExpanded && !playerListReached ? "pointer-events-auto" : "pointer-events-none",
|
||||
!inFlowButtonVisible && !playerListReached ? "pointer-events-auto" : "pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<div className="rounded-2xl shadow-xl shadow-black/25">
|
||||
|
|
@ -7243,10 +7319,8 @@ function PlayerHoleCards({
|
|||
</div>
|
||||
{/* Gir siste innhold på siden rom til å scrolle klar av den flytende
|
||||
knappen i stedet for å bli permanent skjult bak den -- samme
|
||||
betingelse som selve knappen (2026-08-20-rettelsen), ellers ble
|
||||
det stående et tomt, uforklart mellomrom selv etter at knappen
|
||||
alt var skjult. */}
|
||||
{mapExpanded && !playerListReached && <div aria-hidden="true" className="h-20" />}
|
||||
betingelse som selve knappen. */}
|
||||
{!inFlowButtonVisible && !playerListReached && <div aria-hidden="true" className="h-20" />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue