Gammel components/new-round.tsx (3364 linjer) slettet, erstattet av
components/ny-runde/ + lib/ny-runde/ -- V0-eksporten (full teknisk spec,
null designføring) ported mot de faktiske backend-kontraktene i
app/routers/rounds.py: Gender-oversettelse, forenklet Tee-form (aldri
CR/Slope til klienten), CourseMeta-discriminated-union for teeoff- vs.
egen-bane, ekte POST /rounds -> sides -> participants-sekvens, ekte
kontosøk/gjeste-oppslag, ekte banesøk inkl. geolokasjon.
Fire bugs funnet og rettet under scratch-verifisering: manglende
forwardRef på TextInput (i selve V0-eksporten), manglende
tee-synk til eierens spillerkort etter banevalg (egen portingsfeil),
manglende Ingen/Alle-hurtigknapp i steg 5 (samme mangel rettet i appen
2026-08-06), og en produksjonsbug oppdaget EKTE brukere etter utrulling:
fetchFacilityCourses() leste responsen fra offisielt banesøk som en rå
array i stedet for {slug,name,courses}-objektet backend faktisk
returnerer -- ga en uendelig "Laster baner..."-hang for enhver runde
startet på en offisiell (teeoff-)bane. Rettet + lagt til reell
feilhåndtering i OfficialCourses (manglet helt, feilen ble tidligere
en stille, ufanget promise-rejection).
Se CHANGELOG.md punkt 41 og 43 for full verifiseringsdetalj.
142 lines
5.1 KiB
TypeScript
142 lines
5.1 KiB
TypeScript
"use client"
|
|
|
|
import Link from "next/link"
|
|
import { useRouter } from "next/navigation"
|
|
import { Flag } from "lucide-react"
|
|
import { WizardProvider, useWizard } from "./wizard-context"
|
|
import { ProgressSteps } from "./progress"
|
|
import { FooterBar } from "./footer-bar"
|
|
import { Step1CourseTime } from "./step1-course-time"
|
|
import { Step2Format } from "./step2-format"
|
|
import { Step3Players } from "./step3-players"
|
|
import { Step4Teams } from "./step4-teams"
|
|
import { Step5Sharing } from "./step5-sharing"
|
|
|
|
function StepBody() {
|
|
const { state } = useWizard()
|
|
switch (state.step) {
|
|
case 1:
|
|
return <Step1CourseTime />
|
|
case 2:
|
|
return <Step2Format />
|
|
case 3:
|
|
return <Step3Players />
|
|
case 4:
|
|
return <Step4Teams />
|
|
case 5:
|
|
return <Step5Sharing />
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
function WizardInner() {
|
|
const { state, goNext, goBack, canProceedStep1, submit, isAdditionalFlight } = useWizard()
|
|
|
|
const isStep1Sub = state.step === 1 && state.s1Sub !== "fields"
|
|
const isStep1Fields = state.step === 1 && state.s1Sub === "fields"
|
|
const isLastStep = state.step === 5
|
|
|
|
function handleNext() {
|
|
if (state.submitting) return
|
|
if (isLastStep) {
|
|
void submit()
|
|
return
|
|
}
|
|
goNext()
|
|
}
|
|
|
|
const nextDisabled = isStep1Fields ? !canProceedStep1 : false
|
|
|
|
if (state.loadingMe) {
|
|
return (
|
|
<div className="flex min-h-dvh items-center justify-center bg-[var(--nr-bg)]">
|
|
<div aria-hidden="true" className="h-10 w-10 animate-spin rounded-full border-4 border-[var(--nr-accent-ring)] border-t-[var(--nr-accent)]" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!state.ownGender) {
|
|
return (
|
|
<div className="flex min-h-dvh flex-col items-center justify-center gap-4 bg-[var(--nr-bg)] px-5 text-center">
|
|
<p className="max-w-prose text-[15px] text-[var(--nr-ink)]">
|
|
Profilen din mangler registrert kjønn, som trengs for å beregne banehandicap riktig.{" "}
|
|
<Link href="/account" className="font-semibold text-[var(--nr-accent)] underline underline-offset-2">
|
|
Gå til kontoinnstillinger
|
|
</Link>
|
|
.
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="nr flex min-h-dvh flex-col bg-[var(--nr-bg)]">
|
|
<header className="sticky top-0 z-10 border-b border-[var(--nr-border)] bg-[var(--nr-surface)]/90 backdrop-blur">
|
|
<div className="mx-auto flex max-w-2xl flex-col gap-3 px-4 py-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className="flex h-7 w-7 items-center justify-center rounded-md bg-[var(--nr-accent)] text-[var(--nr-accent-ink)]">
|
|
<Flag className="h-4 w-4" aria-hidden="true" />
|
|
</span>
|
|
<span className="text-sm font-semibold tracking-tight text-[var(--nr-ink)]">Ny runde</span>
|
|
</div>
|
|
<ProgressSteps />
|
|
</div>
|
|
</header>
|
|
|
|
{isAdditionalFlight ? (
|
|
<div className="mx-auto mt-3 w-full max-w-2xl px-4">
|
|
<p className="rounded-lg border border-[var(--nr-accent-ring)] bg-[var(--nr-accent-soft)] px-3 py-2 text-[13px] text-[var(--nr-ink)]">
|
|
Legger til en ny flight i samme rundegruppe -- dato, starthull, antall hull og spilleform er
|
|
forhåndsutfylt fra den første flighten, men kan endres.
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
|
|
{state.submitError ? (
|
|
<div className="mx-auto mt-3 w-full max-w-2xl px-4">
|
|
<p role="alert" className="rounded-lg border border-[var(--nr-danger)]/30 bg-[var(--nr-danger-soft)] px-3 py-2 text-[13px] text-[var(--nr-danger)]">
|
|
{state.submitError}
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
|
|
{/* pb-28 er reservert klaring for FooterBar (sticky bottom-0) --
|
|
ALDRI en py-/sm:py-klasse her (Tailwind emitterer sm-varianter i
|
|
en egen media-blokk etter grunnklassene, så en slik klasse ville
|
|
slått padding-bottom tilbake ned uansett rekkefølge i selve
|
|
className-strengen). Reell, browser-verifisert bug denne runden:
|
|
uten denne klaringen overlapper FooterBar innhold nederst på
|
|
lengre steg (f.eks. "Legg til gjest"-knappen i steg 3), og sluker
|
|
klikk stille -- samme feilklasse som er dokumentert i
|
|
ARCHITECTURE_DECISIONS.md/CHANGELOG.md fra 2026-08-02. En
|
|
pt-klasse er trygt, den påvirker aldri padding-bottom. */}
|
|
<main className="mx-auto w-full max-w-2xl flex-1 px-4 pb-28 pt-5">
|
|
<StepBody />
|
|
</main>
|
|
|
|
{!isStep1Sub ? (
|
|
<FooterBar
|
|
onBack={goBack}
|
|
onNext={handleNext}
|
|
nextDisabled={nextDisabled}
|
|
backDisabled={state.submitting}
|
|
submitting={state.submitting}
|
|
isSubmitStep={isLastStep}
|
|
nextLabel={isLastStep ? (state.submitting ? "Oppretter…" : "Start runden") : "Neste"}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function NewRoundWizard() {
|
|
const router = useRouter()
|
|
const exit = () => router.push("/my-rounds")
|
|
const created = (roundId: string) => router.replace(`/my-rounds/${roundId}`)
|
|
return (
|
|
<WizardProvider onExit={exit} onCreated={created}>
|
|
<WizardInner />
|
|
</WizardProvider>
|
|
)
|
|
}
|