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:
parent
6765d17109
commit
05599aafe6
3 changed files with 144 additions and 27 deletions
62
CHANGELOG.md
62
CHANGELOG.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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 på 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 på denne
|
||||||
|
siden -- kun selve KONTROLLEN er nå 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
|
{pageTab === "score" ? (
|
||||||
</button>
|
<>
|
||||||
<button
|
<Settings2 aria-hidden="true" className="size-4 shrink-0 text-primary" />
|
||||||
type="button"
|
Spillere og runde
|
||||||
onClick={() => setPageTab("manage")}
|
</>
|
||||||
aria-pressed={pageTab === "manage"}
|
) : (
|
||||||
className={cn(
|
<>
|
||||||
"min-h-11 rounded-full px-4 text-sm font-bold transition-colors",
|
<ChevronLeft aria-hidden="true" className="size-4 shrink-0" />
|
||||||
pageTab === "manage" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground",
|
Tilbake til score
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
Spillere og runde
|
|
||||||
</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 på mobil, og
|
||||||
<HoleTargetDistance baseUrl={`/rounds/${roundId}`} holeNumber={currentHole} size="full" />
|
ingenting antydet at spillerkortene under fantes) -- kan utvides
|
||||||
|
til det fulle diagrammet med hindringer på 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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue