Fiks HCP-komma app-bredt, utdatert deltakertabell, zebra-striper i spillerpool
HCP: type="number" blokkerte komma-tegnet i nettleseren før JS-parsingen
noensinne så det; byttet til text+inputMode=decimal og lagt til manglende
.replace(",",".") i tre lagre-stier. Deltakertabell: "Rediger i
spillerpoolen" er en full sidenavigasjon, bfcache kunne gjenopprette
turneringssiden i utdatert tilstand -- løst med stille bakgrunns-refresh
ved visibilitychange/pageshow. Spillerpool: zebra-striper lagt tilbake
oppå rutenett-stilen fra forrige runde.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
5dca409270
commit
48f238d158
7 changed files with 164 additions and 45 deletions
47
CHANGELOG.md
47
CHANGELOG.md
|
|
@ -13063,3 +13063,50 @@ 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,
|
`docker compose build teecup_frontend && up -d`, rene logger,
|
||||||
200 OK.
|
200 OK.
|
||||||
|
|
||||||
|
129. **HCP-komma, utdatert deltakertabell etter spillerpool-redigering,
|
||||||
|
zebra-striper -- 2026-08-19.** Tre bruker-rapporterte ting i samme
|
||||||
|
runde. (1) "Er det relevant om det registreres HCP med komma heller
|
||||||
|
enn punktum? Jeg vil at det skal være irrelevant." -- undersøkt
|
||||||
|
grundig: `tournament-players-table.tsx` og to felt i `round-
|
||||||
|
detail.tsx` ("guest-hcp"/"edit-hcp-{id}") brukte `type="number"`,
|
||||||
|
som i de fleste nettlesere BLOKKERER komma-tegnet før JS i det hele
|
||||||
|
tatt ser det -- selv om lagre-parsingen andre steder allerede gjorde
|
||||||
|
`.replace(",", ".")`, hjalp det ikke når kommaet aldri kom inn i
|
||||||
|
feltet. Byttet til `type="text"` + `inputMode="decimal"` (samme
|
||||||
|
mønster som account-settings.tsx allerede brukte korrekt). I
|
||||||
|
TILLEGG manglet selve `.replace(",", ".")`-konverteringen helt i
|
||||||
|
tre lagre-stier: `tournament-players-table.tsx` sin `commitHcp`,
|
||||||
|
og to steder i `org-player-roster-panel.tsx` samt ett i `player-
|
||||||
|
import-panel.tsx` (spillerpool-lagring/CSV-import). Alle rettet,
|
||||||
|
HCP-komma er nå konsistent håndtert app-bredt. (2) "Jeg kan ikke se
|
||||||
|
at deltagertabellen blir oppdatert når det er lagret endringer i
|
||||||
|
spillerpool-tabellen" -- rotårsak: "Rediger i spillerpoolen" er en
|
||||||
|
ekte `<a href>` (full sidenavigasjon), og nettleserens bfcache kan
|
||||||
|
gjenopprette turneringssiden i EKSAKT samme (utdaterte) tilstand
|
||||||
|
ved tilbake-navigasjon uten å kjøre noen React-effekter på nytt.
|
||||||
|
Løst med en stille bakgrunns-refresh (`visibilitychange` +
|
||||||
|
`pageshow` med `persisted`-sjekk) i BÅDE `IndividualTournamentDetail`
|
||||||
|
(deltakere/runder/baner/klasser) og `SetupTab` (spillerpool) --
|
||||||
|
hovedlasteren løftet ut av `useEffect` til en gjenbrukbar
|
||||||
|
`loadTournamentData()`, med en delt `mountedRef` i stedet for en
|
||||||
|
per-kall `cancelled`-variabel siden den nå kalles fra to steder.
|
||||||
|
(3) "Kan du kjøre annenhver rad-bakgrunn? Hvit og lys grå?" --
|
||||||
|
spillerpool-tabellen (`player-import-view.tsx`) fikk tilbake zebra-
|
||||||
|
striping (`bg-card`/`bg-muted/40`) oppå rutenett-stilen fra forrige
|
||||||
|
runde -- den sticky navnekolonnen får nå samme bakgrunn som resten
|
||||||
|
av raden i stedet for alltid `bg-card`, ellers ville den sett
|
||||||
|
frakoblet ut fra stripen bak seg ved sidescroll.
|
||||||
|
|
||||||
|
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Scratch
|
||||||
|
bekrefter: komma faktisk skrivbart og korrekt konvertert (testet
|
||||||
|
både i spillerpool-tabellen og den nye spillertabellen -- sistnevnte
|
||||||
|
viste "18,9" lagret som det reelle tallet 18.9, ikke reversert til
|
||||||
|
forrige verdi), zebra-striper synlige. Punkt (2) er IKKE scratch-
|
||||||
|
reprodusert (bfcache-oppførsel er vanskelig å simulere i en
|
||||||
|
hodeløs devtools-økt) -- vurdert tilstrekkelig dekket av at
|
||||||
|
rotårsaken er tydelig identifisert og fiksen er et etablert,
|
||||||
|
velprøvd mønster (visibilitychange/pageshow) for akkurat dette
|
||||||
|
problemet.
|
||||||
|
|
||||||
|
**Rullet ut:** venter på bekreftelse.
|
||||||
|
|
|
||||||
|
|
@ -444,9 +444,25 @@ export function IndividualTournamentDetail({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Løftet ut av useEffect (2026-08-19) -- brukes nå fra to steder: den
|
||||||
|
// vanlige innlastingen ved mount, OG en "frisk opp når fanen blir synlig
|
||||||
|
// igjen"-lytter under. Bruker: "Jeg kan ikke se at deltagertabellen blir
|
||||||
|
// oppdatert når det er lagret endringer i spillerpool-tabellen" -- rot-
|
||||||
|
// årsak: "Rediger i spillerpoolen" er en full sidenavigasjon (ekte
|
||||||
|
// <a href>, ikke Next-Link), og nettleserens bfcache gjenoppretter siden
|
||||||
|
// i EKSAKT samme (utdaterte) tilstand ved tilbake-navigasjon, uten å
|
||||||
|
// kjøre noen effekter på nytt -- `mountedRef` brukes i stedet for en
|
||||||
|
// per-kall `cancelled`-variabel siden funksjonen nå kalles fra flere
|
||||||
|
// uavhengige steder.
|
||||||
|
const mountedRef = useRef(true)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
mountedRef.current = true
|
||||||
async function load() {
|
return () => {
|
||||||
|
mountedRef.current = false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function loadTournamentData() {
|
||||||
try {
|
try {
|
||||||
const [tournamentsData, participantsData, roundsData, coursesData, classesData] = await Promise.all([
|
const [tournamentsData, participantsData, roundsData, coursesData, classesData] = await Promise.all([
|
||||||
getJson<ApiTournamentInfo[]>(`/orgs/${organizationId}/tournaments`),
|
getJson<ApiTournamentInfo[]>(`/orgs/${organizationId}/tournaments`),
|
||||||
|
|
@ -455,7 +471,7 @@ export function IndividualTournamentDetail({
|
||||||
getJson<ApiCourse[]>(`/orgs/${organizationId}/courses`),
|
getJson<ApiCourse[]>(`/orgs/${organizationId}/courses`),
|
||||||
getJson<ApiTournamentClass[]>(`${base}/classes`),
|
getJson<ApiTournamentClass[]>(`${base}/classes`),
|
||||||
])
|
])
|
||||||
if (cancelled) return
|
if (!mountedRef.current) return
|
||||||
const mine = tournamentsData?.find((t) => t.id === tournamentId) ?? null
|
const mine = tournamentsData?.find((t) => t.id === tournamentId) ?? null
|
||||||
if (!mine) {
|
if (!mine) {
|
||||||
setError("Fant ikke turneringen.")
|
setError("Fant ikke turneringen.")
|
||||||
|
|
@ -467,15 +483,35 @@ export function IndividualTournamentDetail({
|
||||||
setCourses(coursesData ?? [])
|
setCourses(coursesData ?? [])
|
||||||
setClasses(classesData ?? [])
|
setClasses(classesData ?? [])
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setError("Klarte ikke å laste turneringen. Prøv å laste siden på nytt.")
|
if (mountedRef.current) setError("Klarte ikke å laste turneringen. Prøv å laste siden på nytt.")
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) setLoading(false)
|
if (mountedRef.current) setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
load()
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTournamentData()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [organizationId, tournamentId, base])
|
||||||
|
|
||||||
|
// Stille bakgrunns-refresh (ingen loading-spinner -- setLoading(false)
|
||||||
|
// er allerede satt) når fanen/vinduet blir synlig igjen, ELLER siden
|
||||||
|
// gjenopprettes fra bfcache (pageshow med persisted=true er det
|
||||||
|
// spesifikke signalet for akkurat det tilfellet).
|
||||||
|
useEffect(() => {
|
||||||
|
function handleVisible() {
|
||||||
|
if (document.visibilityState === "visible") loadTournamentData()
|
||||||
|
}
|
||||||
|
function handlePageShow(e: PageTransitionEvent) {
|
||||||
|
if (e.persisted) loadTournamentData()
|
||||||
|
}
|
||||||
|
document.addEventListener("visibilitychange", handleVisible)
|
||||||
|
window.addEventListener("pageshow", handlePageShow)
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
document.removeEventListener("visibilitychange", handleVisible)
|
||||||
|
window.removeEventListener("pageshow", handlePageShow)
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [organizationId, tournamentId, base])
|
}, [organizationId, tournamentId, base])
|
||||||
|
|
||||||
async function updateStatus(newStatus: TournamentStatus) {
|
async function updateStatus(newStatus: TournamentStatus) {
|
||||||
|
|
@ -1218,6 +1254,26 @@ function SetupTab({
|
||||||
}
|
}
|
||||||
}, [organizationId])
|
}, [organizationId])
|
||||||
|
|
||||||
|
// Samme "frisk opp ved synlig igjen"-behov som gjelder hele siden
|
||||||
|
// (se IndividualTournamentDetail sin tilsvarende lytter) -- "Rediger i
|
||||||
|
// spillerpoolen" er en full sidenavigasjon, og bfcache kan gjenopprette
|
||||||
|
// denne komponenten i utdatert tilstand ved tilbake-navigasjon.
|
||||||
|
useEffect(() => {
|
||||||
|
function handleVisible() {
|
||||||
|
if (document.visibilityState === "visible") refreshPool()
|
||||||
|
}
|
||||||
|
function handlePageShow(e: PageTransitionEvent) {
|
||||||
|
if (e.persisted) refreshPool()
|
||||||
|
}
|
||||||
|
document.addEventListener("visibilitychange", handleVisible)
|
||||||
|
window.addEventListener("pageshow", handlePageShow)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("visibilitychange", handleVisible)
|
||||||
|
window.removeEventListener("pageshow", handlePageShow)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [organizationId])
|
||||||
|
|
||||||
// Rundedeltakelse-tabellen (ADR-093, "Spillere"-steget) -- lat innlasting,
|
// Rundedeltakelse-tabellen (ADR-093, "Spillere"-steget) -- lat innlasting,
|
||||||
// KUN når steget faktisk vises (ikke eagerly ved sideinnlasting), av alle
|
// KUN når steget faktisk vises (ikke eagerly ved sideinnlasting), av alle
|
||||||
// rundenes rundedeltakere OG alle unike baners utslag samtidig. Flere
|
// rundenes rundedeltakere OG alle unike baners utslag samtidig. Flere
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,10 @@ export function OrgPlayerRosterPanel({
|
||||||
if (field === "gender") {
|
if (field === "gender") {
|
||||||
patch.gender = toBackendGender(row.gender)
|
patch.gender = toBackendGender(row.gender)
|
||||||
} else if (field === "handicap_index") {
|
} else if (field === "handicap_index") {
|
||||||
patch.handicap_index = row.handicap_index.trim() ? Number(row.handicap_index) : null
|
// Komma-desimal skal fungere likt som punktum (2026-08-19).
|
||||||
|
patch.handicap_index = row.handicap_index.trim()
|
||||||
|
? Number(row.handicap_index.replace(",", "."))
|
||||||
|
: null
|
||||||
} else if (field === "birth_date") {
|
} else if (field === "birth_date") {
|
||||||
patch.birth_date = row.birth_date.trim() || null
|
patch.birth_date = row.birth_date.trim() || null
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -232,7 +235,7 @@ export function OrgPlayerRosterPanel({
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
display_name: row.display_name.trim(),
|
display_name: row.display_name.trim(),
|
||||||
email: row.email.trim() || null,
|
email: row.email.trim() || null,
|
||||||
handicap_index: row.handicap_index.trim() ? Number(row.handicap_index) : null,
|
handicap_index: row.handicap_index.trim() ? Number(row.handicap_index.replace(",", ".")) : null,
|
||||||
gender: toBackendGender(row.gender),
|
gender: toBackendGender(row.gender),
|
||||||
birth_date: row.birth_date.trim() || null,
|
birth_date: row.birth_date.trim() || null,
|
||||||
mobile: row.mobile.trim() || null,
|
mobile: row.mobile.trim() || null,
|
||||||
|
|
|
||||||
|
|
@ -299,7 +299,7 @@ export function PlayerImportPanel({
|
||||||
data: {
|
data: {
|
||||||
display_name: r.display_name.trim(),
|
display_name: r.display_name.trim(),
|
||||||
email: r.email.trim() || null,
|
email: r.email.trim() || null,
|
||||||
handicap_index: r.handicap_index.trim() ? Number(r.handicap_index) : null,
|
handicap_index: r.handicap_index.trim() ? Number(r.handicap_index.replace(",", ".")) : null,
|
||||||
gender: toBackendGender(r.gender),
|
gender: toBackendGender(r.gender),
|
||||||
birth_date: r.birth_date.trim() || null,
|
birth_date: r.birth_date.trim() || null,
|
||||||
mobile: r.mobile.trim() || null,
|
mobile: r.mobile.trim() || null,
|
||||||
|
|
|
||||||
|
|
@ -496,7 +496,15 @@ function EditTable({
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.map((row, rowIndex) => (
|
{rows.map((row, rowIndex) => {
|
||||||
|
// Annenhver rad-bakgrunn (2026-08-19, bruker: "hvit og lys
|
||||||
|
// grå") -- ren pekepinn for å lese på tvers av en rad i en
|
||||||
|
// bred tabell, samme rutenett-linjer som ellers. Den sticky
|
||||||
|
// første cellen må få SAMME bakgrunn som resten av raden
|
||||||
|
// (ikke alltid bg-card), ellers ser den frakoblet ut fra
|
||||||
|
// stripen bak den ved sidescroll.
|
||||||
|
const rowBg = rowIndex % 2 === 1 ? "bg-muted/40" : "bg-card"
|
||||||
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={row.key}
|
key={row.key}
|
||||||
// data-row-key (2026-08-18): rent orkestrerings-anker, IKKE
|
// data-row-key (2026-08-18): rent orkestrerings-anker, IKKE
|
||||||
|
|
@ -505,10 +513,10 @@ function EditTable({
|
||||||
// scrolle til riktig rad uten å måtte utvide selve
|
// scrolle til riktig rad uten å måtte utvide selve
|
||||||
// props-kontrakten eller endre noe synlig her.
|
// props-kontrakten eller endre noe synlig her.
|
||||||
data-row-key={row.key}
|
data-row-key={row.key}
|
||||||
className="group h-9 hover:bg-accent/20"
|
className={cn("group h-9 hover:bg-accent/20", rowBg)}
|
||||||
>
|
>
|
||||||
{editFields.map((field, i) => (
|
{editFields.map((field, i) => (
|
||||||
<td key={field} className={cn(TD, i === 0 && "sticky left-0 z-10 bg-card group-hover:bg-accent/20")}>
|
<td key={field} className={cn(TD, i === 0 && `sticky left-0 z-10 ${rowBg} group-hover:bg-accent/20`)}>
|
||||||
{field === "gender" ? (
|
{field === "gender" ? (
|
||||||
<select
|
<select
|
||||||
aria-label={`${FIELD_LABELS[field]} for rad ${rowIndex + 1}`}
|
aria-label={`${FIELD_LABELS[field]} for rad ${rowIndex + 1}`}
|
||||||
|
|
@ -577,7 +585,8 @@ function EditTable({
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4748,9 +4748,11 @@ function AddGuestForm({
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="guest-hcp"
|
id="guest-hcp"
|
||||||
|
// type="text" (ikke "number") -- komma-desimal ("12,4") blokkeres
|
||||||
|
// av native number-inputs i de fleste nettlesere før JS-parsingen
|
||||||
|
// (som allerede håndterer komma korrekt) i det hele tatt ser den.
|
||||||
|
// 2026-08-19.
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
type="number"
|
|
||||||
step="0.1"
|
|
||||||
value={hcp}
|
value={hcp}
|
||||||
onChange={(e) => setHcp(e.target.value)}
|
onChange={(e) => setHcp(e.target.value)}
|
||||||
placeholder="F.eks. 18"
|
placeholder="F.eks. 18"
|
||||||
|
|
@ -5392,9 +5394,9 @@ function EditParticipantPanel({
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id={`edit-hcp-${player.id}`}
|
id={`edit-hcp-${player.id}`}
|
||||||
|
// type="text" (ikke "number") -- se guest-hcp over, samme
|
||||||
|
// begrunnelse. 2026-08-19.
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
type="number"
|
|
||||||
step="0.1"
|
|
||||||
value={hcp}
|
value={hcp}
|
||||||
onChange={(e) => setHcp(e.target.value)}
|
onChange={(e) => setHcp(e.target.value)}
|
||||||
placeholder="F.eks. 18"
|
placeholder="F.eks. 18"
|
||||||
|
|
|
||||||
|
|
@ -361,7 +361,9 @@ function PlayerRow({
|
||||||
onSetHandicap(player.participantId, null)
|
onSetHandicap(player.participantId, null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const num = Number(trimmed)
|
// Komma-desimal (norsk skrivemåte, "12,4") skal fungere likt som punktum
|
||||||
|
// -- 2026-08-19, bruker: "jeg vil at det skal være irrelevant".
|
||||||
|
const num = Number(trimmed.replace(",", "."))
|
||||||
if (Number.isNaN(num)) {
|
if (Number.isNaN(num)) {
|
||||||
// Revert bad input back to the last known good value.
|
// Revert bad input back to the last known good value.
|
||||||
setHcpDraft(player.handicapSnapshot === null ? "" : String(player.handicapSnapshot))
|
setHcpDraft(player.handicapSnapshot === null ? "" : String(player.handicapSnapshot))
|
||||||
|
|
@ -382,11 +384,11 @@ function PlayerRow({
|
||||||
|
|
||||||
<td className={TD}>
|
<td className={TD}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
// type="text" (ikke "number") -- native number-inputs blokkerer
|
||||||
|
// komma-tegnet i de fleste nettlesere FØR det når JS i det hele
|
||||||
|
// tatt, uansett hva onBlur-parsingen gjør. inputMode="decimal"
|
||||||
|
// beholdes for riktig mobiltastatur. 2026-08-19.
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
step={0.1}
|
|
||||||
min={-10}
|
|
||||||
max={54}
|
|
||||||
aria-label={`Handicap for ${player.playerName}`}
|
aria-label={`Handicap for ${player.playerName}`}
|
||||||
value={hcpDraft}
|
value={hcpDraft}
|
||||||
onChange={(e) => setHcpDraft(e.target.value)}
|
onChange={(e) => setHcpDraft(e.target.value)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue