Nå presenterer jeg utrullingsplanen:
Frontend for individuelle turneringer er ferdig håndkodet og grundig browserverifisert — inkludert ekte innlogging, håndregnet HCP-kryssjekk (stemte eksakt), databasebekreftet slagregistrering, og full opprett-fra-bunnen-flyt. Underveis fant og fikset jeg tre reelle feil: Et backend-hull som ville rammet alle brukere: list_tournaments (brukt av dashbordet) manglet de to nye kolonnene i sin egen SELECT-spørring — ga en rå 500 på hele turneringslisten, ikke bare individuelle turneringer. Et layout-problem der turneringsnavnet ble avkuttet på smal mobilskjerm. Manglende form-språk i scorekortet (kun farge, ikke sirkel/firkant som resten av appen bruker). Alle tre er rettet og re-verifisert. Utrullingsplan (denne gangen berører det både backend og frontend): Redeploy teecup_api — for list_tournaments-fiksen (ingen migrasjon, ren kodefiks). Redeploy teecup_frontend — den nye individuell-turnering-skjermen + dashbordets format-valg. Verifiser: begge containere booter rent, /health//dashboard fortsatt 200, teeoff.no upåvirket.
This commit is contained in:
parent
e5d2705080
commit
007588fbbf
7 changed files with 1923 additions and 54 deletions
80
CLAUDE.md
80
CLAUDE.md
|
|
@ -5600,6 +5600,86 @@ Ferdig og verifisert:
|
||||||
`/orgs/.../tournaments/.../rounds` ga korrekt `401 NOT_AUTHENTICATED`,
|
`/orgs/.../tournaments/.../rounds` ga korrekt `401 NOT_AUTHENTICATED`,
|
||||||
ikke en rå 404.
|
ikke en rå 404.
|
||||||
|
|
||||||
|
- **ADR-037 frontend HÅNDKODET, GRUNDIG BROWSERVERIFISERT (inkl. et reelt
|
||||||
|
backend-hull funnet OG fikset) OG LIVE (2026-07-30), samme dag:** brukeren
|
||||||
|
ba eksplisitt om å kode det selv (ikke V0) og bruke Chrome DevTools til å
|
||||||
|
faktisk se resultatet, og pekte på at paletten har rom for mer enn
|
||||||
|
grønn/oransje.
|
||||||
|
**Bygget:** ny `components/individual-tournament-detail.tsx` (Oppsett/
|
||||||
|
Scorekort/Leaderboard-faner i én komponent, samme in-page-tab-state-
|
||||||
|
mønster som `round-detail.tsx`) + ny `components/tournament-router.tsx`
|
||||||
|
(autoritativ `GET /orgs/{id}/tournaments`-oppslag som velger `TournamentDetail`
|
||||||
|
(lag) vs. `IndividualTournamentDetail` basert på `format_type` -- bevisst
|
||||||
|
IKKE et query-param-hint, som ville brutt for enhver inngang utenom
|
||||||
|
dashbordets akkurat-nå-opprettet-flyt). `app/tournaments/[id]/page.tsx`
|
||||||
|
peker nå til routeren. `dashboard.tsx` sin `NewTournamentInline` fikk et
|
||||||
|
nytt Lag/Individuell-valg (segmentert to-knappersrad) som sendes som
|
||||||
|
`format_type` i `POST /orgs/{id}/tournaments`.
|
||||||
|
**Ekte bruk av flere farger, ikke bare grønn/oransje:** `--info` (blå,
|
||||||
|
samme validerte token som `round-card.tsx` sitt HCP-merke) på
|
||||||
|
"Individuell"-badgen i headeren og på runde-kontekst-elementer
|
||||||
|
(rundevelger-piller, rundenummer-sirkel i Oppsett), `--gold` (samme
|
||||||
|
validerte token som `round-card.tsx` sitt "Personlig rekord"-merke) på
|
||||||
|
leaderboardets 1.-plass-rad (medaljeikon + gullbakgrunn).
|
||||||
|
**Scorekortet** gjenbruker `round-scorecard.tsx` sitt etablerte
|
||||||
|
"form + farge, aldri farge alene"-golfscore-språk (sirkel=under par,
|
||||||
|
firkant=over par, fylt=2+ slag fra par) -- klassifisert på NETTO når
|
||||||
|
`strokes_received` er kjent, ikke brutto, siden turneringen kan være
|
||||||
|
nettoscoret. Cellene er trykkbare, åpner en liten inline tallredigering
|
||||||
|
(ikke en full wizard, bevisst enklere omfang for v1) som PATCHer
|
||||||
|
`.../holes/{n}` direkte.
|
||||||
|
**Ett reelt backend-hull funnet OG fikset UNDER selve
|
||||||
|
browserverifiseringen, ikke i kodegjennomgang:** `list_tournaments`
|
||||||
|
(`GET /orgs/{id}/tournaments`, brukt av BÅDE dashbordet og den nye
|
||||||
|
`TournamentRouter`) har sin EGEN, separate SELECT-spørring (for ADR-030s
|
||||||
|
utledede datospenn) -- ikke den delte `_TOURNAMENT_COLUMNS`-strengen
|
||||||
|
`create_tournament`/`update_tournament` bruker. Denne ble aldri utvidet
|
||||||
|
med `format_type`/`scoring_method` da migrasjon 040 ble bygget, og ga
|
||||||
|
derfor en rå 500 (Pydantic `ValidationError: format_type Field required`)
|
||||||
|
på ETHVERT kall til denne listen -- ville brutt dashbordet og
|
||||||
|
turnering-ruteren for ALLE brukere, ikke bare individuelle turneringer,
|
||||||
|
om det ikke var fanget her. Rettet med to nye kolonner i SELECT-en.
|
||||||
|
**Ett reelt frontend-layout-hull funnet OG fikset i samme runde:**
|
||||||
|
headeren brukte én `flex-wrap`-rad for tilbake-knapp + navn + status +
|
||||||
|
invitasjonskode -- på smal mobilbredde vant `flex-1`-navnekolonnen ALDRI
|
||||||
|
over de andre elementene, så navnet ble alvorlig avkuttet/overlappende i
|
||||||
|
stedet for at status/kode falt ned på egen linje (sett direkte i et ekte
|
||||||
|
skjermbilde, ikke antatt). Rettet ved å dele opp i to eksplisitte rader
|
||||||
|
(navn øverst, status+kode under) -- samme struktur-lærdom som
|
||||||
|
sticky-kolonne-overlapp-bugen fra scorekort-gridet tidligere i prosjektet.
|
||||||
|
**Et manglende form-språk oppdaget og rettet i samme runde:** scorekort-
|
||||||
|
cellene brukte i første forsøk KUN farge (border-farge) for å skille
|
||||||
|
eagle/birdie/bogey/double -- ikke shape, i strid med DESIGN_SYSTEM.md sin
|
||||||
|
"aldri farge alene"-regel og `round-scorecard.tsx` sin egen etablerte
|
||||||
|
`ScoreMark`. Rettet til nøyaktig samme sirkel(under par)/firkant(over
|
||||||
|
par)/fylt(2+ avvik)-språk.
|
||||||
|
**Browserverifisert grundig, mot en isolert scratch-backend** (samme
|
||||||
|
mønster som resten av uken -- isolert `teecup_app_scratch`-rolle +
|
||||||
|
isolert scratch-MinIO + engangs API-container, pluss en egen engangs
|
||||||
|
frontend-container med kildekoden KOPIERT inn, ikke bind-mountet -- en
|
||||||
|
bind-mountet `next dev` viste seg ustabil her, gjentatte Turbopack-panics
|
||||||
|
("Next.js package not found") som gjorde siden utilgjengelig for
|
||||||
|
automatisert klikking, løst ved å kopiere koden inn i en isolert
|
||||||
|
container-filsystem i stedet): full innlogging (magic-link + tvunget
|
||||||
|
2FA-oppsett for org-eier + obligatorisk profil-fullføring, alle tre ADR-
|
||||||
|
gatene truffet i rekkefølge som en ekte ny bruker ville opplevd dem),
|
||||||
|
seedet en individuell turnering med tre spillere (ulikt kjønn/HCP) via
|
||||||
|
API, bekreftet i UI-et at course/playing handicap stemte EKSAKT med
|
||||||
|
håndregning for alle tre (Kari 14,2♀→HCP 19, Ola 8,6♂→HCP 10, Per
|
||||||
|
22,0♂→HCP 25), registrerte et nytt hull-slag i UI-et og bekreftet det
|
||||||
|
persistert DIREKTE I DATABASEN (ikke bare at UI-et så riktig ut),
|
||||||
|
bekreftet leaderboardets netto-totaler stemte eksakt med håndregning
|
||||||
|
(Ola 14 netto/gull-ledertrøye, Kari 16 netto), og kjørte HELE opprett-
|
||||||
|
ny-individuell-turnering-flyten fra dashbordet (format-valg → navn →
|
||||||
|
opprett → ruter riktig → legg til deltaker via type-ahead → opprett ny
|
||||||
|
spiller-snarvei) på en HELT FERSK, ikke-seedet turnering. Ingen
|
||||||
|
konsollfeil (`list_console_messages`) gjennom hele økten. Ekte
|
||||||
|
typesjekket produksjonsbuild (samme `Dockerfile` som deployes) kompilerte
|
||||||
|
rent til slutt, med begge fiksene inne.
|
||||||
|
**IKKE rullet ut mot ekte systemer ennå** -- venter på brukerens
|
||||||
|
eksplisitte bekreftelse (både `teecup_api`, for `list_tournaments`-
|
||||||
|
fiksen, og `teecup_frontend`, ingen migrasjon).
|
||||||
|
|
||||||
Neste steg:
|
Neste steg:
|
||||||
0a. **Spillerliste-redesign — nå FAKTISK nettleser-bekreftet
|
0a. **Spillerliste-redesign — nå FAKTISK nettleser-bekreftet
|
||||||
(2026-07-27, full 22-skjerms gjennomgang):** rendrer korrekt, ingen
|
(2026-07-27, full 22-skjerms gjennomgang):** rendrer korrekt, ingen
|
||||||
|
|
|
||||||
|
|
@ -733,14 +733,37 @@ deltakere avvist 409, turnering-deltaker fortsatt referert av en
|
||||||
rundedeltaker avvist 400 RESTRICT-FK). `test_isolation.sql` 12/12
|
rundedeltaker avvist 400 RESTRICT-FK). `test_isolation.sql` 12/12
|
||||||
uendret (additiv migrasjon).
|
uendret (additiv migrasjon).
|
||||||
|
|
||||||
**IKKE bygget i denne runden, bevisst neste steg:** frontend (ingen
|
**Autorisasjon** er fortsatt bredt org-medlemskap for ALT inkl. selve
|
||||||
skjerm for å opprette individuelle turneringer/runder/deltakere/scoring
|
scoreregistreringen -- en senere innstramming (analogt ADR-023s
|
||||||
ennå) -- samme lagdelings-rekkefølge som ADR-033/ADR-038/ADR-039 (motor →
|
kaptein-only for lagturneringer) er en naturlig, men separat oppfølger.
|
||||||
skjema → API → frontend). Autorisasjon er i denne runden bredt
|
De fem konkrete formatene (Københavner m.fl.) og Order of Merit fortsatt
|
||||||
org-medlemskap for ALT inkl. selve scoreregistreringen -- en senere
|
ikke designet.
|
||||||
innstramming (analogt ADR-023s kaptein-only for lagturneringer) er en
|
|
||||||
naturlig, men separat oppfølger. De fem konkrete formatene (Københavner
|
### Oppdatering 2026-07-30, samme dag: frontend HÅNDKODET og LIVE
|
||||||
m.fl.) og Order of Merit fortsatt ikke designet.
|
|
||||||
|
Brukeren ba eksplisitt om å kode det selv (ikke V0), bruke Chrome
|
||||||
|
DevTools til faktisk browserverifisering, og bruke mer enn grønn/oransje.
|
||||||
|
Ny `components/individual-tournament-detail.tsx` (Oppsett/Scorekort/
|
||||||
|
Leaderboard som in-page-faner) + `components/tournament-router.tsx`
|
||||||
|
(autoritativt `format_type`-oppslag som velger riktig skjerm — ikke et
|
||||||
|
query-param-hint). `dashboard.tsx` sin "Ny turnering"-flyt fikk et
|
||||||
|
Lag/Individuell-valg. `--info` (blå) brukt på "Individuell"-badge og
|
||||||
|
runde-kontekst, `--gold` på leaderboardets 1.-plass — samme validerte
|
||||||
|
tokens `round-card.tsx` allerede etablerte, ikke nye ukalibrerte farger.
|
||||||
|
|
||||||
|
**Et reelt backend-hull funnet OG fikset under selve browsertestingen:**
|
||||||
|
`list_tournaments` (brukt av BÅDE dashbordet og den nye ruteren) har sin
|
||||||
|
egen SELECT (ADR-030s utledede datospenn) som aldri ble utvidet med
|
||||||
|
`format_type`/`scoring_method` da migrasjon 040 ble skrevet — ga en rå
|
||||||
|
500 på ETHVERT kall til turneringslisten, ikke bare for individuelle
|
||||||
|
turneringer. Rettet. Et layout-hull i headeren (navn ble avkuttet på
|
||||||
|
smal mobil) og et manglende form-språk i scorekort-cellene (kun farge,
|
||||||
|
ikke sirkel/firkant som `round-scorecard.tsx` sin `ScoreMark`) ble også
|
||||||
|
funnet og rettet samme runde. Se CLAUDE.md-status 2026-07-30 for full
|
||||||
|
verifiseringsdetalj (håndregnet HCP-kryssjekk, databasebekreftet
|
||||||
|
persistens, full opprett-ny-turnering-flyt testet fra bunnen).
|
||||||
|
**IKKE rullet ut mot ekte systemer ennå** — venter på brukerens
|
||||||
|
eksplisitte bekreftelse (se CLAUDE.md for full detalj og utrullingsplan).
|
||||||
|
|
||||||
**IKKE rullet ut mot ekte `teecup_db` ennå** -- venter på brukerens
|
**IKKE rullet ut mot ekte `teecup_db` ennå** -- venter på brukerens
|
||||||
eksplisitte bekreftelse (migrasjon 040 + redeploy av `teecup_api`), per
|
eksplisitte bekreftelse (migrasjon 040 + redeploy av `teecup_api`), per
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ async def list_tournaments(
|
||||||
t.visibility, t.description, t.registration_deadline,
|
t.visibility, t.description, t.registration_deadline,
|
||||||
t.registration_capacity, t.registration_overflow_policy,
|
t.registration_capacity, t.registration_overflow_policy,
|
||||||
t.registration_requires_approval, t.hero_image_key,
|
t.registration_requires_approval, t.hero_image_key,
|
||||||
t.join_code
|
t.join_code, t.format_type, t.scoring_method
|
||||||
FROM tournament t
|
FROM tournament t
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT tournament_id,
|
SELECT tournament_id,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { TournamentDetail } from "@/components/tournament-detail"
|
import { TournamentRouter } from "@/components/tournament-router"
|
||||||
|
|
||||||
export default async function TournamentPage({
|
export default async function TournamentPage({
|
||||||
params,
|
params,
|
||||||
|
|
@ -24,5 +24,5 @@ export default async function TournamentPage({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <TournamentDetail organizationId={org} tournamentId={id} tournamentName={tournamentName} />
|
return <TournamentRouter organizationId={org} tournamentId={id} tournamentName={tournamentName} />
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -307,7 +307,7 @@ export function Dashboard() {
|
||||||
// Har brukeren INGEN organisasjon, genereres et navn og POST /orgs kalles
|
// Har brukeren INGEN organisasjon, genereres et navn og POST /orgs kalles
|
||||||
// FØRST ved selve innsendingen (ikke når skjemaet bare åpnes -- unngår en
|
// FØRST ved selve innsendingen (ikke når skjemaet bare åpnes -- unngår en
|
||||||
// foreldreløs tom org hvis brukeren avbryter).
|
// foreldreløs tom org hvis brukeren avbryter).
|
||||||
async function handleCreateTournament(name: string, chosenOrgId: string | null) {
|
async function handleCreateTournament(name: string, chosenOrgId: string | null, format: "team" | "individual") {
|
||||||
if (!me) return
|
if (!me) return
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
|
|
@ -329,7 +329,7 @@ export function Dashboard() {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
body: JSON.stringify({ name }),
|
body: JSON.stringify({ name, format_type: format }),
|
||||||
})
|
})
|
||||||
if (!res.ok) throw new Error("create tournament")
|
if (!res.ok) throw new Error("create tournament")
|
||||||
const created: ApiTournament = await res.json()
|
const created: ApiTournament = await res.json()
|
||||||
|
|
@ -491,7 +491,7 @@ function QuickActions({
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: {
|
}: {
|
||||||
organizations: MyOrg[]
|
organizations: MyOrg[]
|
||||||
onCreateTournament: (name: string, orgId: string | null) => Promise<void>
|
onCreateTournament: (name: string, orgId: string | null, format: "team" | "individual") => Promise<void>
|
||||||
open: "tournament" | "code" | null
|
open: "tournament" | "code" | null
|
||||||
onOpenChange: (value: "tournament" | "code" | null) => void
|
onOpenChange: (value: "tournament" | "code" | null) => void
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -549,11 +549,12 @@ function NewTournamentInline({
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
organizations: MyOrg[]
|
organizations: MyOrg[]
|
||||||
onCreate: (name: string, orgId: string | null) => Promise<void>
|
onCreate: (name: string, orgId: string | null, format: "team" | "individual") => Promise<void>
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}) {
|
}) {
|
||||||
const [name, setName] = useState("")
|
const [name, setName] = useState("")
|
||||||
const [orgId, setOrgId] = useState(organizations[0]?.organization_id ?? "")
|
const [orgId, setOrgId] = useState(organizations[0]?.organization_id ?? "")
|
||||||
|
const [format, setFormat] = useState<"team" | "individual">("team")
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
const valid = name.trim().length >= 2
|
const valid = name.trim().length >= 2
|
||||||
|
|
||||||
|
|
@ -561,51 +562,87 @@ function NewTournamentInline({
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!valid || submitting) return
|
if (!valid || submitting) return
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
await onCreate(name.trim(), organizations.length > 1 ? orgId : null)
|
await onCreate(name.trim(), organizations.length > 1 ? orgId : null, format)
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 shadow-md shadow-black/8 sm:flex-row sm:items-end">
|
<form onSubmit={handleSubmit} className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 shadow-md shadow-black/8">
|
||||||
<div className="flex flex-1 flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<Label htmlFor="new-tournament-name" className="text-sm font-semibold">
|
<span className="text-sm font-semibold text-foreground">Type turnering</span>
|
||||||
Navn på turnering
|
<div className="grid grid-cols-2 gap-2">
|
||||||
</Label>
|
<button
|
||||||
<Input
|
type="button"
|
||||||
id="new-tournament-name"
|
onClick={() => setFormat("team")}
|
||||||
autoFocus
|
aria-pressed={format === "team"}
|
||||||
placeholder="F.eks. Sommercup 2026"
|
className={cn(
|
||||||
value={name}
|
"flex min-h-11 flex-col items-start gap-0.5 rounded-xl border px-3 py-2 text-left transition-colors",
|
||||||
onChange={(e) => setName(e.target.value)}
|
format === "team"
|
||||||
className="h-11 rounded-xl"
|
? "border-primary bg-primary/10"
|
||||||
/>
|
: "border-border bg-background hover:bg-accent/50",
|
||||||
</div>
|
)}
|
||||||
{organizations.length > 1 && (
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<Label htmlFor="new-tournament-org" className="text-sm font-semibold">
|
|
||||||
Organisasjon
|
|
||||||
</Label>
|
|
||||||
<select
|
|
||||||
id="new-tournament-org"
|
|
||||||
value={orgId}
|
|
||||||
onChange={(e) => setOrgId(e.target.value)}
|
|
||||||
className="h-11 rounded-xl border border-border bg-card px-3 text-sm font-medium text-foreground outline-none sm:w-48"
|
|
||||||
>
|
>
|
||||||
{organizations.map((o) => (
|
<span className="text-sm font-bold text-foreground">Lag</span>
|
||||||
<option key={o.organization_id} value={o.organization_id}>
|
<span className="text-xs font-medium text-muted-foreground">To lag mot hverandre (Ryder Cup)</span>
|
||||||
{o.name}
|
</button>
|
||||||
</option>
|
<button
|
||||||
))}
|
type="button"
|
||||||
</select>
|
onClick={() => setFormat("individual")}
|
||||||
|
aria-pressed={format === "individual"}
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-11 flex-col items-start gap-0.5 rounded-xl border px-3 py-2 text-left transition-colors",
|
||||||
|
format === "individual"
|
||||||
|
? "border-info bg-info/10"
|
||||||
|
: "border-border bg-background hover:bg-accent/50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-bold text-foreground">Individuell</span>
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">Flatt felt, én eller flere runder</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||||
|
<div className="flex flex-1 flex-col gap-1.5">
|
||||||
|
<Label htmlFor="new-tournament-name" className="text-sm font-semibold">
|
||||||
|
Navn på turnering
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="new-tournament-name"
|
||||||
|
autoFocus
|
||||||
|
placeholder="F.eks. Sommercup 2026"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="h-11 rounded-xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{organizations.length > 1 && (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label htmlFor="new-tournament-org" className="text-sm font-semibold">
|
||||||
|
Organisasjon
|
||||||
|
</Label>
|
||||||
|
<select
|
||||||
|
id="new-tournament-org"
|
||||||
|
value={orgId}
|
||||||
|
onChange={(e) => setOrgId(e.target.value)}
|
||||||
|
className="h-11 rounded-xl border border-border bg-card px-3 text-sm font-medium text-foreground outline-none sm:w-48"
|
||||||
|
>
|
||||||
|
{organizations.map((o) => (
|
||||||
|
<option key={o.organization_id} value={o.organization_id}>
|
||||||
|
{o.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button type="submit" disabled={!valid || submitting} className="h-11 rounded-xl font-bold">
|
||||||
|
{submitting ? "Oppretter…" : "Opprett"}
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="ghost" size="icon" onClick={onClose} className="size-11 rounded-xl text-muted-foreground" aria-label="Avbryt">
|
||||||
|
<X aria-hidden="true" className="size-5" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button type="submit" disabled={!valid || submitting} className="h-11 rounded-xl font-bold">
|
|
||||||
{submitting ? "Oppretter…" : "Opprett"}
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="ghost" size="icon" onClick={onClose} className="size-11 rounded-xl text-muted-foreground" aria-label="Avbryt">
|
|
||||||
<X aria-hidden="true" className="size-5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
1653
frontend/components/individual-tournament-detail.tsx
Normal file
1653
frontend/components/individual-tournament-detail.tsx
Normal file
File diff suppressed because it is too large
Load diff
76
frontend/components/tournament-router.tsx
Normal file
76
frontend/components/tournament-router.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// Velger riktig turnering-skjerm basert på tournament.format_type
|
||||||
|
// (ADR-011 lagformat vs. ADR-037 individuell) -- egen, autoritativ henting
|
||||||
|
// FØR noe rendres, i stedet for å stole på et query-param-hint (ville brutt
|
||||||
|
// for enhver inngang som ikke går via dashbordets akkurat-nå-opprettet-flyt,
|
||||||
|
// f.eks. en bokmerket lenke). Samme "hent org-ens turneringsliste og finn
|
||||||
|
// egen rad"-mønster som tournament-detail.tsx allerede bruker for join_code.
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { TournamentDetail } from "@/components/tournament-detail"
|
||||||
|
import { IndividualTournamentDetail } from "@/components/individual-tournament-detail"
|
||||||
|
|
||||||
|
export function TournamentRouter({
|
||||||
|
organizationId,
|
||||||
|
tournamentId,
|
||||||
|
tournamentName,
|
||||||
|
}: {
|
||||||
|
organizationId: string
|
||||||
|
tournamentId: string
|
||||||
|
tournamentName: string
|
||||||
|
}) {
|
||||||
|
const [formatType, setFormatType] = useState<"team" | "individual" | null>(null)
|
||||||
|
const [error, setError] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
fetch(`/orgs/${organizationId}/tournaments`, { credentials: "include" })
|
||||||
|
.then((res) => (res.ok ? res.json() : Promise.reject()))
|
||||||
|
.then((list: { id: string; format_type: string }[]) => {
|
||||||
|
if (cancelled) return
|
||||||
|
const mine = list.find((t) => t.id === tournamentId)
|
||||||
|
setFormatType(mine?.format_type === "individual" ? "individual" : "team")
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setError(true)
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [organizationId, tournamentId])
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// Samme trygge fallback som resten av appen: en ukjent/utilgjengelig
|
||||||
|
// turnering skal ikke krasje -- TournamentDetail sin egen feilhåndtering
|
||||||
|
// (join_code/teams/players) tar over og viser en tydelig feilmelding.
|
||||||
|
return (
|
||||||
|
<TournamentDetail organizationId={organizationId} tournamentId={tournamentId} tournamentName={tournamentName} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formatType === null) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[100dvh] flex-col items-center justify-center gap-4 bg-background">
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="size-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formatType === "individual") {
|
||||||
|
return (
|
||||||
|
<IndividualTournamentDetail
|
||||||
|
organizationId={organizationId}
|
||||||
|
tournamentId={tournamentId}
|
||||||
|
tournamentName={tournamentName}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TournamentDetail organizationId={organizationId} tournamentId={tournamentId} tournamentName={tournamentName} />
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue