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.
|
||||
`docker compose build teecup_frontend && up -d`, rene logger,
|
||||
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,38 +444,74 @@ 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(() => {
|
||||
let cancelled = false
|
||||
async function load() {
|
||||
try {
|
||||
const [tournamentsData, participantsData, roundsData, coursesData, classesData] = await Promise.all([
|
||||
getJson<ApiTournamentInfo[]>(`/orgs/${organizationId}/tournaments`),
|
||||
getJson<ApiParticipant[]>(`${base}/participants`),
|
||||
getJson<ApiRound[]>(`${base}/rounds`),
|
||||
getJson<ApiCourse[]>(`/orgs/${organizationId}/courses`),
|
||||
getJson<ApiTournamentClass[]>(`${base}/classes`),
|
||||
])
|
||||
if (cancelled) return
|
||||
const mine = tournamentsData?.find((t) => t.id === tournamentId) ?? null
|
||||
if (!mine) {
|
||||
setError("Fant ikke turneringen.")
|
||||
return
|
||||
}
|
||||
setTournament(mine)
|
||||
setParticipants(participantsData ?? [])
|
||||
setRounds(roundsData ?? [])
|
||||
setCourses(coursesData ?? [])
|
||||
setClasses(classesData ?? [])
|
||||
} catch {
|
||||
if (!cancelled) setError("Klarte ikke å laste turneringen. Prøv å laste siden på nytt.")
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
}
|
||||
}
|
||||
load()
|
||||
mountedRef.current = true
|
||||
return () => {
|
||||
cancelled = true
|
||||
mountedRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
async function loadTournamentData() {
|
||||
try {
|
||||
const [tournamentsData, participantsData, roundsData, coursesData, classesData] = await Promise.all([
|
||||
getJson<ApiTournamentInfo[]>(`/orgs/${organizationId}/tournaments`),
|
||||
getJson<ApiParticipant[]>(`${base}/participants`),
|
||||
getJson<ApiRound[]>(`${base}/rounds`),
|
||||
getJson<ApiCourse[]>(`/orgs/${organizationId}/courses`),
|
||||
getJson<ApiTournamentClass[]>(`${base}/classes`),
|
||||
])
|
||||
if (!mountedRef.current) return
|
||||
const mine = tournamentsData?.find((t) => t.id === tournamentId) ?? null
|
||||
if (!mine) {
|
||||
setError("Fant ikke turneringen.")
|
||||
return
|
||||
}
|
||||
setTournament(mine)
|
||||
setParticipants(participantsData ?? [])
|
||||
setRounds(roundsData ?? [])
|
||||
setCourses(coursesData ?? [])
|
||||
setClasses(classesData ?? [])
|
||||
} catch {
|
||||
if (mountedRef.current) setError("Klarte ikke å laste turneringen. Prøv å laste siden på nytt.")
|
||||
} finally {
|
||||
if (mountedRef.current) setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
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 () => {
|
||||
document.removeEventListener("visibilitychange", handleVisible)
|
||||
window.removeEventListener("pageshow", handlePageShow)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [organizationId, tournamentId, base])
|
||||
|
||||
async function updateStatus(newStatus: TournamentStatus) {
|
||||
|
|
@ -1218,6 +1254,26 @@ function SetupTab({
|
|||
}
|
||||
}, [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,
|
||||
// KUN når steget faktisk vises (ikke eagerly ved sideinnlasting), av alle
|
||||
// rundenes rundedeltakere OG alle unike baners utslag samtidig. Flere
|
||||
|
|
|
|||
|
|
@ -200,7 +200,10 @@ export function OrgPlayerRosterPanel({
|
|||
if (field === "gender") {
|
||||
patch.gender = toBackendGender(row.gender)
|
||||
} 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") {
|
||||
patch.birth_date = row.birth_date.trim() || null
|
||||
} else {
|
||||
|
|
@ -232,7 +235,7 @@ export function OrgPlayerRosterPanel({
|
|||
body: JSON.stringify({
|
||||
display_name: row.display_name.trim(),
|
||||
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),
|
||||
birth_date: row.birth_date.trim() || null,
|
||||
mobile: row.mobile.trim() || null,
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@ export function PlayerImportPanel({
|
|||
data: {
|
||||
display_name: r.display_name.trim(),
|
||||
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),
|
||||
birth_date: r.birth_date.trim() || null,
|
||||
mobile: r.mobile.trim() || null,
|
||||
|
|
|
|||
|
|
@ -496,7 +496,15 @@ function EditTable({
|
|||
</tr>
|
||||
</thead>
|
||||
<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
|
||||
key={row.key}
|
||||
// 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
|
||||
// props-kontrakten eller endre noe synlig her.
|
||||
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) => (
|
||||
<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" ? (
|
||||
<select
|
||||
aria-label={`${FIELD_LABELS[field]} for rad ${rowIndex + 1}`}
|
||||
|
|
@ -577,7 +585,8 @@ function EditTable({
|
|||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4748,9 +4748,11 @@ function AddGuestForm({
|
|||
</Label>
|
||||
<Input
|
||||
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"
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={hcp}
|
||||
onChange={(e) => setHcp(e.target.value)}
|
||||
placeholder="F.eks. 18"
|
||||
|
|
@ -5392,9 +5394,9 @@ function EditParticipantPanel({
|
|||
</Label>
|
||||
<Input
|
||||
id={`edit-hcp-${player.id}`}
|
||||
// type="text" (ikke "number") -- se guest-hcp over, samme
|
||||
// begrunnelse. 2026-08-19.
|
||||
inputMode="decimal"
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={hcp}
|
||||
onChange={(e) => setHcp(e.target.value)}
|
||||
placeholder="F.eks. 18"
|
||||
|
|
|
|||
|
|
@ -361,7 +361,9 @@ function PlayerRow({
|
|||
onSetHandicap(player.participantId, null)
|
||||
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)) {
|
||||
// Revert bad input back to the last known good value.
|
||||
setHcpDraft(player.handicapSnapshot === null ? "" : String(player.handicapSnapshot))
|
||||
|
|
@ -382,11 +384,11 @@ function PlayerRow({
|
|||
|
||||
<td className={TD}>
|
||||
<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"
|
||||
step={0.1}
|
||||
min={-10}
|
||||
max={54}
|
||||
aria-label={`Handicap for ${player.playerName}`}
|
||||
value={hcpDraft}
|
||||
onChange={(e) => setHcpDraft(e.target.value)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue