teecup/frontend/components/new-round.tsx

1396 lines
50 KiB
TypeScript
Raw Normal View History

V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
"use client"
// Opprett en frittstående runde (ADR-033). Presentasjon fra V0, datalag
// skrevet om fra mock til ekte søk/opprettelse/bekreftelse mot API-et.
// To bane-kilder: offisiell teeoff-bane (live oppslag, ADR-033 Beslutning C
// -- ingen lokal kopi lagres) eller en egendefinert bane (organisasjons-
// uavhengig katalog, søkbar på tvers av alle brukere).
import type React from "react"
import { useEffect, useMemo, useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import {
ArrowLeft,
Building2,
Check,
Flag,
MapPin,
Pencil,
Plus,
Search,
Trophy,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Wordmark } from "@/components/wordmark"
import { cn } from "@/lib/utils"
// --- Types -----------------------------------------------------------------
type Rating = { courseRating: string; slope: string; par: string }
type Tee = {
id: string
name: string
men: boolean
women: boolean
menRating?: Rating
womenRating?: Rating
}
type Course = {
id: string
name: string
tees: Tee[]
}
type OfficialClub = {
id: string
name: string
location: string
courses: Course[]
}
type Step =
| "source"
| "official-search"
| "official-courses"
| "own-search"
| "own-create"
| "confirm"
// Hvilken bane-kilde+id som faktisk skal sendes til /rounds -- Course-typen
// over (fra V0) bærer kun det UI-et trenger, ikke dette. Holdt separat i
// stedet for å presse det inn i Course, som er felles for begge kilder.
type CourseMeta =
| { source: "teeoff"; facilitySlug: string; teeoffCourseId: number }
| { source: "custom"; personalCourseId: string }
type Gender = "m" | "f"
type StatLevel = "strokes_only" | "strokes_and_putts" | "full"
// ADR-038 -- selvdeklarert spilleform. Ingen egen match-motor for
// frittstående runder, så dette taes brukerens ord for.
type PlayFormat = "stroke" | "match"
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
// --- API-typer ---------------------------------------------------------------
type ApiTeeOption = { name: string; genders: Gender[] }
type ApiFacility = { slug: string; name: string; city: string | null; county: string | null }
type ApiOfficialCourseOption = {
teeoff_course_id: number
name: string
is_main_course: boolean
tees: ApiTeeOption[]
}
type ApiPersonalCourse = { id: string; name: string }
type ApiPersonalCourseDetail = { id: string; name: string; tees: ApiTeeOption[] }
function apiTeesToTees(apiTees: ApiTeeOption[]): Tee[] {
return apiTees.map((t, i) => ({
id: `${t.name}-${i}`,
name: t.name,
men: t.genders.includes("m"),
women: t.genders.includes("f"),
}))
}
// --- Root component --------------------------------------------------------
export function NewRound() {
const router = useRouter()
const [step, setStep] = useState<Step>("source")
const [ownGender, setOwnGender] = useState<Gender | null>(null)
const [loadingMe, setLoadingMe] = useState(true)
// Official flow
const [selectedClub, setSelectedClub] = useState<OfficialClub | null>(null)
const [resolvingClub, setResolvingClub] = useState(false)
// Chosen course carried into the confirm step (from any source), plus
// which source/id it actually came from -- needed to build the /rounds
// payload correctly.
const [course, setCourse] = useState<Course | null>(null)
const [courseMeta, setCourseMeta] = useState<CourseMeta | null>(null)
useEffect(() => {
let cancelled = false
async function loadMe() {
try {
const res = await fetch("/auth/me", { credentials: "include" })
if (res.status === 401) {
router.replace("/")
return
}
const data: { gender: Gender | null } = await res.json()
if (!cancelled) setOwnGender(data.gender)
} finally {
if (!cancelled) setLoadingMe(false)
}
}
void loadMe()
return () => {
cancelled = true
}
}, [router])
function chooseCourse(next: Course, meta: CourseMeta) {
setCourse(next)
setCourseMeta(meta)
setStep("confirm")
}
async function selectClub(club: OfficialClub) {
setSelectedClub(club)
setResolvingClub(true)
try {
const res = await fetch(`/rounds/official-search/${club.id}`, { credentials: "include" })
if (!res.ok) throw new Error(`facility detail: ${res.status}`)
const detail: { courses: ApiOfficialCourseOption[] } = await res.json()
const withCourses: OfficialClub = {
...club,
courses: detail.courses.map((c) => ({
id: String(c.teeoff_course_id),
name: c.name,
tees: apiTeesToTees(c.tees),
})),
}
setSelectedClub(withCourses)
setStep("official-courses")
} catch {
setSelectedClub(null)
} finally {
setResolvingClub(false)
}
}
function chooseOfficialCourse(officialCourse: Course) {
if (!selectedClub) return
chooseCourse(officialCourse, {
source: "teeoff",
facilitySlug: selectedClub.id,
teeoffCourseId: Number(officialCourse.id),
})
}
async function selectOwnCourse(picked: ApiPersonalCourse) {
const res = await fetch(`/personal-courses/${picked.id}`, { credentials: "include" })
if (!res.ok) return
const detail: ApiPersonalCourseDetail = await res.json()
chooseCourse(
{ id: detail.id, name: detail.name, tees: apiTeesToTees(detail.tees) },
{ source: "custom", personalCourseId: detail.id },
)
}
function ownCourseCreated(created: { id: string; name: string; tees: Tee[] }) {
chooseCourse(
{ id: created.id, name: created.name, tees: created.tees },
{ source: "custom", personalCourseId: created.id },
)
}
async function submitRound(payload: {
name: string
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
teeName: string
date: string
startedAt: string | null
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
startHole: number
holes: 9 | 18
statLevel: StatLevel
playFormat: PlayFormat
excludeFromHandicap: boolean
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
}): Promise<{ id: string }> {
if (!courseMeta) throw new Error("Mangler valgt bane")
const body =
courseMeta.source === "teeoff"
? {
course_source: "teeoff",
teeoff_facility_slug: courseMeta.facilitySlug,
teeoff_course_id: courseMeta.teeoffCourseId,
tee_name: payload.teeName,
name: payload.name || null,
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
played_at: payload.date,
started_at: payload.startedAt,
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
start_hole: payload.startHole,
holes_planned: payload.holes,
stat_level: payload.statLevel,
play_format: payload.playFormat,
exclude_owner_from_handicap: payload.excludeFromHandicap,
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
}
: {
course_source: "custom",
personal_course_id: courseMeta.personalCourseId,
tee_name: payload.teeName,
name: payload.name || null,
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
played_at: payload.date,
started_at: payload.startedAt,
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
start_hole: payload.startHole,
holes_planned: payload.holes,
stat_level: payload.statLevel,
play_format: payload.playFormat,
exclude_owner_from_handicap: payload.excludeFromHandicap,
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
}
const res = await fetch("/rounds", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(body),
})
if (!res.ok) throw new Error(`create round: ${res.status}`)
return res.json()
}
return (
<div className="flex min-h-[100dvh] flex-col bg-background">
<header className="sticky top-0 z-10 border-b border-border bg-background/80 backdrop-blur">
<div className="mx-auto flex w-full max-w-3xl items-center justify-between gap-4 px-5 py-4">
Update Todos Diagnostisere de to feilmeldingene fra skjermbildene Flytte frontend-sidene fra /rounds/* til /my-rounds/* (kollisjon med API-proxyen) Oppdatere alle interne lenker/router-kall til nye stier Oppdatere next.config.mjs-kommentaren med denne lærdommen Typesjekket produksjonsbuild Rulle ut live etter bekreftelse Oppdatere statusdokumentasjon Fant og fikset rot-årsaken til begge feilmeldingene — en reell, alvorlig bug, ikke bare kosmetikk: /rounds var samtidig navnet på både frontend-listesiden og backend-APIets ressursprefiks. Next.js sin rewrite-presedens ("afterFiles") gjorde at: GET/POST /rounds fra klienten aldri nådde backend — den statiske siden vant, så fetch() fikk Next sin egen HTML tilbake i stedet for JSON. Det ga akkurat de to feilmeldingene i skjermbildene. /rounds/[id] (rundedetaljsiden) hadde det motsatte problemet — der vant rewriten, så selve siden var fullstendig uoppnåelig (ville vist rå backend-JSON i stedet for UI-et om du hadde kommet så langt). Dette er nøyaktig samme klasse feil som medlemsside-hendelsen under ADR-016, bare i begge retninger samtidig siden det denne gangen var en eksakt navnekollisjon, ikke bare nøsting. Fikset: flyttet alle tre frontend-sidene til /my-rounds/* (list, new, [id]) — API-et beholder /rounds uendret. Oppdatert alle interne lenker (round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx, dashboard.tsx), lagt inn en tydelig advarsel i next.config.mjs for å hindre at dette gjentar seg. Bekreftet: «55/50/44/32» i det andre skjermbildet er ikke en bug — det er Tjøme Golfklubb sine faktiske utslagsnavn i teeoff (lengde i hundremeter). Typesjekket produksjonsbuild kompilerer rent med de nye rutene.
2026-07-23 12:07:40 +02:00
<BackLink step={step} setStep={setStep} onExit={() => router.push("/my-rounds")} />
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
<Wordmark compact />
</div>
</header>
<main className="mx-auto w-full max-w-3xl flex-1 px-5 py-8 sm:py-10">
{loadingMe ? (
<p className="text-lg text-muted-foreground">Laster </p>
) : !ownGender ? (
<div className="rounded-2xl border border-border bg-card p-5 text-base text-muted-foreground">
Profilen din mangler registrert kjønn, som trengs for å beregne banehandicap riktig.{" "}
<Link href="/account" className="font-semibold text-primary underline underline-offset-2">
til kontoinnstillinger
</Link>
.
</div>
) : (
<>
{step === "source" && (
<SourceStep
onOfficial={() => setStep("official-search")}
onOwn={() => setStep("own-search")}
/>
)}
{step === "official-search" && (
<OfficialSearchStep onSelectClub={selectClub} resolving={resolvingClub} />
)}
{step === "official-courses" && selectedClub && (
<OfficialCoursesStep club={selectedClub} onSelectCourse={chooseOfficialCourse} />
)}
{step === "own-search" && (
<OwnSearchStep onSelectCourse={selectOwnCourse} onCreate={() => setStep("own-create")} />
)}
{step === "own-create" && (
<OwnCreateStep onCreated={ownCourseCreated} onCancel={() => setStep("own-search")} />
)}
{step === "confirm" && course && (
<ConfirmStep
course={course}
ownGender={ownGender}
onSubmit={submitRound}
Update Todos Diagnostisere de to feilmeldingene fra skjermbildene Flytte frontend-sidene fra /rounds/* til /my-rounds/* (kollisjon med API-proxyen) Oppdatere alle interne lenker/router-kall til nye stier Oppdatere next.config.mjs-kommentaren med denne lærdommen Typesjekket produksjonsbuild Rulle ut live etter bekreftelse Oppdatere statusdokumentasjon Fant og fikset rot-årsaken til begge feilmeldingene — en reell, alvorlig bug, ikke bare kosmetikk: /rounds var samtidig navnet på både frontend-listesiden og backend-APIets ressursprefiks. Next.js sin rewrite-presedens ("afterFiles") gjorde at: GET/POST /rounds fra klienten aldri nådde backend — den statiske siden vant, så fetch() fikk Next sin egen HTML tilbake i stedet for JSON. Det ga akkurat de to feilmeldingene i skjermbildene. /rounds/[id] (rundedetaljsiden) hadde det motsatte problemet — der vant rewriten, så selve siden var fullstendig uoppnåelig (ville vist rå backend-JSON i stedet for UI-et om du hadde kommet så langt). Dette er nøyaktig samme klasse feil som medlemsside-hendelsen under ADR-016, bare i begge retninger samtidig siden det denne gangen var en eksakt navnekollisjon, ikke bare nøsting. Fikset: flyttet alle tre frontend-sidene til /my-rounds/* (list, new, [id]) — API-et beholder /rounds uendret. Oppdatert alle interne lenker (round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx, dashboard.tsx), lagt inn en tydelig advarsel i next.config.mjs for å hindre at dette gjentar seg. Bekreftet: «55/50/44/32» i det andre skjermbildet er ikke en bug — det er Tjøme Golfklubb sine faktiske utslagsnavn i teeoff (lengde i hundremeter). Typesjekket produksjonsbuild kompilerer rent med de nye rutene.
2026-07-23 12:07:40 +02:00
onCreated={(id) => router.replace(`/my-rounds/${id}`)}
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
/>
)}
</>
)}
</main>
</div>
)
}
// --- Back link -------------------------------------------------------------
function BackLink({
step,
setStep,
onExit,
}: {
step: Step
setStep: (step: Step) => void
onExit: () => void
}) {
const config: Record<Step, { label: string; onClick: () => void }> = {
source: { label: "Til egne runder", onClick: onExit },
"official-search": { label: "Tilbake", onClick: () => setStep("source") },
"official-courses": { label: "Tilbake til søk", onClick: () => setStep("official-search") },
"own-search": { label: "Tilbake", onClick: () => setStep("source") },
"own-create": { label: "Tilbake til søk", onClick: () => setStep("own-search") },
confirm: { label: "Tilbake", onClick: () => setStep("source") },
}
const { label, onClick } = config[step]
return (
<button
type="button"
onClick={onClick}
className="inline-flex min-h-[44px] items-center gap-2 rounded-xl px-2 py-2 text-base font-semibold text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<ArrowLeft aria-hidden="true" className="size-5" />
{label}
</button>
)
}
// --- Step header -----------------------------------------------------------
function StepHeader({ title, description }: { title: string; description: string }) {
return (
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-extrabold tracking-tight text-foreground text-balance">
{title}
</h1>
<p className="max-w-prose text-lg leading-relaxed text-muted-foreground text-pretty">
{description}
</p>
</div>
)
}
// --- Step 1: source --------------------------------------------------------
function SourceStep({ onOfficial, onOwn }: { onOfficial: () => void; onOwn: () => void }) {
return (
<div className="flex flex-col gap-8">
<StepHeader
title="Ny runde"
description="Velg hvor banen kommer fra for å komme i gang."
/>
<div className="grid gap-4 sm:grid-cols-2">
<SourceCard
icon={<Trophy aria-hidden="true" className="size-7 text-primary" />}
title="Offisiell bane"
description="Søk opp klubben i det offisielle baneregisteret."
onClick={onOfficial}
/>
<SourceCard
icon={<Pencil aria-hidden="true" className="size-7 text-primary" />}
title="Egen bane"
description="Banen finnes ikke i registeret bruk en tidligere egendefinert bane eller opprett en ny."
onClick={onOwn}
/>
</div>
</div>
)
}
function SourceCard({
icon,
title,
description,
onClick,
}: {
icon: React.ReactNode
title: string
description: string
onClick: () => void
}) {
return (
<button
type="button"
onClick={onClick}
className="flex min-h-[44px] flex-col items-start gap-4 rounded-3xl border border-border bg-card p-6 text-left shadow-sm shadow-black/5 transition-colors hover:border-primary/50 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<div className="flex size-14 items-center justify-center rounded-2xl bg-primary/15">{icon}</div>
<div className="flex flex-col gap-1.5">
<h2 className="text-xl font-bold text-foreground">{title}</h2>
<p className="text-base leading-relaxed text-muted-foreground text-pretty">{description}</p>
</div>
</button>
)
}
// --- Step 2a: official search ----------------------------------------------
type ApiNearbyFacility = ApiFacility & { distance_km: number }
function NearbyClubs({
onSelectClub,
resolving,
}: {
onSelectClub: (club: OfficialClub) => void
resolving: boolean
}) {
const [nearby, setNearby] = useState<ApiNearbyFacility[] | null>(null)
useEffect(() => {
if (!("geolocation" in navigator)) return
let cancelled = false
navigator.geolocation.getCurrentPosition(
(position) => {
if (cancelled) return
const { latitude, longitude } = position.coords
fetch(`/rounds/official-search/nearby?lat=${latitude}&lng=${longitude}&limit=5`, { credentials: "include" })
.then((res) => (res.ok ? res.json() : null))
.then((data: ApiNearbyFacility[] | null) => {
if (!cancelled && data) setNearby(data)
})
.catch(() => {})
},
() => {
// Avvist eller utilgjengelig -- stille no-op, dette er kun en
// hjelp, ikke en forutsetning for å kunne søke.
},
{ timeout: 8000 },
)
return () => {
cancelled = true
}
}, [])
if (!nearby || nearby.length === 0) return null
return (
<div className="flex flex-col gap-2">
<span className="text-sm font-semibold text-muted-foreground">Nærmest deg</span>
<ul className="flex flex-col overflow-hidden rounded-2xl border border-border">
{nearby.map((facility, i) => (
<li key={facility.slug} className="border-b border-border last:border-b-0">
<button
type="button"
disabled={resolving}
onClick={() =>
onSelectClub({
id: facility.slug,
name: facility.name,
location: [facility.city, facility.county].filter(Boolean).join(", "),
courses: [],
})
}
className={cn(
"flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors hover:bg-accent/60",
i === 0 && "bg-primary/5",
)}
>
<div className="flex min-w-0 flex-col">
<span className="flex items-center gap-2 text-base font-semibold text-foreground">
{facility.name}
{i === 0 && (
<span className="rounded-full bg-primary/15 px-2 py-0.5 text-xs font-bold text-primary">Nærmest</span>
)}
</span>
{(facility.city || facility.county) && (
<span className="truncate text-sm text-muted-foreground">
{[facility.city, facility.county].filter(Boolean).join(", ")}
</span>
)}
</div>
<span className="shrink-0 text-sm font-semibold tabular-nums text-muted-foreground">
{facility.distance_km < 1
? `${Math.round(facility.distance_km * 1000)} m`
: `${facility.distance_km.toFixed(0)} km`}
</span>
</button>
</li>
))}
</ul>
</div>
)
}
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
function OfficialSearchStep({
onSelectClub,
resolving,
}: {
onSelectClub: (club: OfficialClub) => void
resolving: boolean
}) {
const [query, setQuery] = useState("")
const [results, setResults] = useState<ApiFacility[] | null>(null)
const [searching, setSearching] = useState(false)
const [error, setError] = useState<string | null>(null)
async function handleSearch(e: React.FormEvent) {
e.preventDefault()
setSearching(true)
setError(null)
try {
const res = await fetch(`/rounds/official-search?q=${encodeURIComponent(query.trim())}`, {
credentials: "include",
})
if (!res.ok) throw new Error(`search: ${res.status}`)
setResults(await res.json())
} catch {
setError("Klarte ikke å søke i baneregisteret akkurat nå.")
setResults([])
} finally {
setSearching(false)
}
}
return (
<div className="flex flex-col gap-8">
<StepHeader title="Offisiell bane" description="Søk opp klubben i baneregisteret." />
<NearbyClubs onSelectClub={onSelectClub} resolving={resolving} />
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
<form onSubmit={handleSearch} className="flex flex-col gap-3 sm:flex-row">
<div className="relative flex-1">
<Search
aria-hidden="true"
className="pointer-events-none absolute left-4 top-1/2 size-5 -translate-y-1/2 text-muted-foreground"
/>
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Søk etter klubbnavn…"
aria-label="Klubbnavn"
className="h-14 rounded-2xl pl-12 text-lg"
/>
</div>
<Button type="submit" size="lg" disabled={searching} className="h-14 gap-2 rounded-2xl px-8 text-base font-bold">
<Search aria-hidden="true" className="size-5" />
{searching ? "Søker…" : "Søk"}
</Button>
</form>
{error && <p className="text-base font-medium text-destructive">{error}</p>}
{results !== null && (
<div className="flex flex-col gap-3">
{results.length === 0 ? (
<p className="rounded-2xl border border-border bg-muted/50 px-5 py-6 text-center text-base text-muted-foreground">
Ingen klubber matcher «{query}». Prøv et annet søk.
</p>
) : (
<ul className="flex flex-col gap-3">
{results.map((facility) => (
<li key={facility.slug}>
<button
type="button"
disabled={resolving}
onClick={() =>
onSelectClub({
id: facility.slug,
name: facility.name,
location: [facility.city, facility.county].filter(Boolean).join(", "),
courses: [],
})
}
className="flex w-full items-center gap-4 rounded-2xl border border-border bg-card p-5 text-left shadow-sm shadow-black/5 transition-colors hover:border-primary/50 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:opacity-60"
>
<div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/15">
<Building2 aria-hidden="true" className="size-6 text-primary" />
</div>
<div className="flex min-w-0 flex-col">
<span className="text-lg font-bold text-foreground">{facility.name}</span>
{facility.city || facility.county ? (
<span className="flex items-center gap-1.5 text-base text-muted-foreground">
<MapPin aria-hidden="true" className="size-4" />
{[facility.city, facility.county].filter(Boolean).join(", ")}
</span>
) : null}
</div>
</button>
</li>
))}
</ul>
)}
{resolving && <p className="text-base text-muted-foreground">Henter baner </p>}
</div>
)}
</div>
)
}
// --- Step 2a (cont): courses at club ---------------------------------------
function OfficialCoursesStep({
club,
onSelectCourse,
}: {
club: OfficialClub
onSelectCourse: (course: Course) => void
}) {
return (
<div className="flex flex-col gap-8">
<StepHeader title={club.name} description="Velg hvilken bane du skal spille." />
<ul className="flex flex-col gap-3">
{club.courses.map((course) => (
<li key={course.id}>
<button
type="button"
onClick={() => onSelectCourse(course)}
className="flex w-full items-center gap-4 rounded-2xl border border-border bg-card p-5 text-left shadow-sm shadow-black/5 transition-colors hover:border-primary/50 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/15">
<Flag aria-hidden="true" className="size-6 text-primary" />
</div>
<div className="flex min-w-0 flex-col">
<span className="text-lg font-bold text-foreground">{course.name}</span>
<span className="text-base text-muted-foreground">{course.tees.length} utslag</span>
</div>
</button>
</li>
))}
{club.courses.length === 0 && (
<li className="rounded-2xl border border-border bg-muted/50 px-5 py-6 text-center text-base text-muted-foreground">
Ingen 18-hulls baner registrert hos dette anlegget ennå.
</li>
)}
</ul>
</div>
)
}
// --- Step 2b: own course search --------------------------------------------
function OwnSearchStep({
onSelectCourse,
onCreate,
}: {
onSelectCourse: (course: ApiPersonalCourse) => void
onCreate: () => void
}) {
const [query, setQuery] = useState("")
const [results, setResults] = useState<ApiPersonalCourse[]>([])
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
const timer = setTimeout(async () => {
try {
const res = await fetch(`/personal-courses?q=${encodeURIComponent(query.trim())}`, {
credentials: "include",
})
if (!res.ok) throw new Error(`search: ${res.status}`)
const data: ApiPersonalCourse[] = await res.json()
if (!cancelled) setResults(data)
} catch {
if (!cancelled) setError("Klarte ikke å søke i egendefinerte baner akkurat nå.")
}
}, 250)
return () => {
cancelled = true
clearTimeout(timer)
}
}, [query])
return (
<div className="flex flex-col gap-8">
<StepHeader
title="Egen bane"
description="Søk opp en tidligere egendefinert bane, eller opprett en ny."
/>
{error && <p className="text-base font-medium text-destructive">{error}</p>}
<div className="relative">
<Search
aria-hidden="true"
className="pointer-events-none absolute left-4 top-1/2 size-5 -translate-y-1/2 text-muted-foreground"
/>
<Input
autoFocus
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Søk etter banenavn…"
aria-label="Banenavn"
className="h-14 rounded-2xl pl-12 text-lg"
/>
</div>
{results.length > 0 && (
<ul className="flex flex-col gap-3">
{results.map((course) => (
<li key={course.id}>
<button
type="button"
onClick={() => onSelectCourse(course)}
className="flex w-full items-center gap-4 rounded-2xl border border-border bg-card p-5 text-left shadow-sm shadow-black/5 transition-colors hover:border-primary/50 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/15">
<Flag aria-hidden="true" className="size-6 text-primary" />
</div>
<div className="flex min-w-0 flex-col">
<span className="text-lg font-bold text-foreground">{course.name}</span>
</div>
</button>
</li>
))}
</ul>
)}
{results.length === 0 && (
<p className="rounded-2xl border border-border bg-muted/50 px-5 py-6 text-center text-base text-muted-foreground">
{query.trim() ? `Ingen egne baner matcher «${query}».` : "Skriv for å søke, eller opprett en ny bane under."}
</p>
)}
<button
type="button"
onClick={onCreate}
className="flex min-h-[44px] items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-border bg-card/40 px-6 py-5 text-lg font-bold text-foreground transition-colors hover:border-primary/60 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<Plus aria-hidden="true" className="size-5" />
Opprett ny bane
</button>
</div>
)
}
// --- Step 2b (cont): create course -----------------------------------------
function emptyHoles() {
return Array.from({ length: 18 }, (_, i) => ({
hole: i + 1,
par: "4",
strokeIndex: String(i + 1),
}))
}
function parseDecimal(value: string): number {
return Number(value.replace(",", "."))
}
function OwnCreateStep({
onCreated,
onCancel,
}: {
onCreated: (course: { id: string; name: string; tees: Tee[] }) => void
onCancel: () => void
}) {
const [name, setName] = useState("")
const [holes, setHoles] = useState(emptyHoles)
const [tees, setTees] = useState<Tee[]>([
{ id: `nt-${Date.now()}`, name: "", men: true, women: false },
])
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
function updateHole(index: number, field: "par" | "strokeIndex", value: string) {
setHoles((prev) => prev.map((h, i) => (i === index ? { ...h, [field]: value } : h)))
}
function updateTee(id: string, patch: Partial<Tee>) {
setTees((prev) => prev.map((t) => (t.id === id ? { ...t, ...patch } : t)))
}
function addTee() {
setTees((prev) => [...prev, { id: `nt-${Date.now()}`, name: "", men: false, women: false }])
}
function removeTee(id: string) {
setTees((prev) => (prev.length > 1 ? prev.filter((t) => t.id !== id) : prev))
}
const indexesValid = useMemo(
() => new Set(holes.map((h) => h.strokeIndex)).size === 18,
[holes],
)
const teesValid = tees.every(
(t) =>
t.name.trim() !== "" &&
(t.men || t.women) &&
(!t.men || (t.menRating && t.menRating.courseRating.trim() && t.menRating.slope.trim() && t.menRating.par.trim())) &&
(!t.women || (t.womenRating && t.womenRating.courseRating.trim() && t.womenRating.slope.trim() && t.womenRating.par.trim())),
)
const canSubmit = name.trim() !== "" && indexesValid && teesValid && !submitting
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!canSubmit) return
setSubmitting(true)
setError(null)
try {
const body = {
name: name.trim(),
holes: holes.map((h) => ({
hole_number: h.hole,
par: Number(h.par),
stroke_index: Number(h.strokeIndex),
})),
tees: tees.map((t) => ({
name: t.name.trim(),
ratings: [
...(t.men && t.menRating
? [{ gender: "m", course_rating: parseDecimal(t.menRating.courseRating), slope_rating: Number(t.menRating.slope), par: Number(t.menRating.par) }]
: []),
...(t.women && t.womenRating
? [{ gender: "f", course_rating: parseDecimal(t.womenRating.courseRating), slope_rating: Number(t.womenRating.slope), par: Number(t.womenRating.par) }]
: []),
],
})),
}
const res = await fetch("/personal-courses", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(body),
})
if (!res.ok) throw new Error(`create: ${res.status}`)
const created: { id: string; name: string } = await res.json()
onCreated({ id: created.id, name: created.name, tees: tees.map((t) => ({ ...t, name: t.name.trim() })) })
} catch {
setError("Klarte ikke å opprette banen. Sjekk at hull og utslag er fylt ut riktig.")
} finally {
setSubmitting(false)
}
}
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-8">
<StepHeader title="Opprett ny bane" description="Fyll inn banedata. Du kan endre detaljene senere." />
{error && <p className="text-base font-medium text-destructive">{error}</p>}
{/* Name */}
<div className="flex flex-col gap-2">
<Label htmlFor="course-name" className="text-base font-semibold">
Navn banen
</Label>
<Input
id="course-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="F.eks. Hyttebanen"
className="h-14 rounded-2xl text-lg"
/>
</div>
{/* Holes */}
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<h2 className="text-xl font-bold text-foreground">Hull</h2>
<p className="text-base text-muted-foreground">Sett par og stroke-indeks for hvert hull.</p>
</div>
{!indexesValid && (
<p role="alert" className="text-base font-medium text-destructive">
Hver stroke-indeks (118) brukes nøyaktig én gang.
</p>
)}
<div className="overflow-hidden rounded-2xl border border-border">
<div className="grid grid-cols-[auto_1fr_1fr] items-center gap-3 border-b border-border bg-muted/60 px-4 py-3 text-sm font-bold uppercase tracking-wide text-muted-foreground">
<span className="w-16">Hull</span>
<span>Par</span>
<span>Stroke-indeks</span>
</div>
<ul>
{holes.map((h, index) => (
<li
key={h.hole}
className="grid grid-cols-[auto_1fr_1fr] items-center gap-3 border-b border-border px-4 py-2.5 last:border-b-0"
>
<span className="w-16 text-base font-semibold text-foreground">Hull {h.hole}</span>
<select
aria-label={`Par for hull ${h.hole}`}
value={h.par}
onChange={(e) => updateHole(index, "par", e.target.value)}
className="h-12 rounded-xl border border-input bg-background px-3 text-base font-semibold text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{[3, 4, 5, 6].map((p) => (
<option key={p} value={String(p)}>
Par {p}
</option>
))}
</select>
<select
aria-label={`Stroke-indeks for hull ${h.hole}`}
value={h.strokeIndex}
onChange={(e) => updateHole(index, "strokeIndex", e.target.value)}
className="h-12 rounded-xl border border-input bg-background px-3 text-base font-semibold text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{Array.from({ length: 18 }, (_, i) => i + 1).map((si) => (
<option key={si} value={String(si)}>
{si}
</option>
))}
</select>
</li>
))}
</ul>
</div>
</div>
{/* Tees */}
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<h2 className="text-xl font-bold text-foreground">Utslag</h2>
<p className="text-base text-muted-foreground">Legg til ett eller flere utslag med rating.</p>
</div>
{tees.map((t, index) => (
<TeeEditor
key={t.id}
tee={t}
index={index}
canRemove={tees.length > 1}
onChange={(patch) => updateTee(t.id, patch)}
onRemove={() => removeTee(t.id)}
/>
))}
<button
type="button"
onClick={addTee}
className="flex min-h-[44px] items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-border bg-card/40 px-6 py-4 text-base font-bold text-foreground transition-colors hover:border-primary/60 hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<Plus aria-hidden="true" className="size-5" />
Legg til utslag
</button>
</div>
<div className="flex flex-col gap-3 sm:flex-row-reverse">
<Button
type="submit"
size="lg"
disabled={!canSubmit}
className="h-14 flex-1 rounded-2xl text-lg font-bold shadow-sm"
>
{submitting ? "Oppretter…" : "Opprett bane og fortsett"}
</Button>
<Button
type="button"
variant="ghost"
size="lg"
onClick={onCancel}
className="h-14 rounded-2xl px-8 text-base font-semibold"
>
Avbryt
</Button>
</div>
</form>
)
}
function TeeEditor({
tee,
index,
canRemove,
onChange,
onRemove,
}: {
tee: Tee
index: number
canRemove: boolean
onChange: (patch: Partial<Tee>) => void
onRemove: () => void
}) {
return (
<div className="flex flex-col gap-4 rounded-2xl border border-border bg-card p-5 shadow-sm shadow-black/5">
<div className="flex items-end gap-3">
<div className="flex flex-1 flex-col gap-2">
<Label htmlFor={`tee-name-${tee.id}`} className="text-base font-semibold">
Navn utslag {index + 1}
</Label>
<Input
id={`tee-name-${tee.id}`}
value={tee.name}
onChange={(e) => onChange({ name: e.target.value })}
placeholder="F.eks. Gul"
className="h-12 rounded-xl text-base"
/>
</div>
{canRemove && (
<Button type="button" variant="outline" onClick={onRemove} className="h-12 rounded-xl px-4 text-base font-semibold">
Fjern
</Button>
)}
</div>
<RatingToggle
label="Herre-rating"
enabled={tee.men}
rating={tee.menRating}
onToggle={(men) =>
onChange({ men, menRating: men ? tee.menRating ?? { courseRating: "", slope: "", par: "" } : tee.menRating })
}
onRatingChange={(menRating) => onChange({ menRating })}
idPrefix={`men-${tee.id}`}
/>
<RatingToggle
label="Dame-rating"
enabled={tee.women}
rating={tee.womenRating}
onToggle={(women) =>
onChange({ women, womenRating: women ? tee.womenRating ?? { courseRating: "", slope: "", par: "" } : tee.womenRating })
}
onRatingChange={(womenRating) => onChange({ womenRating })}
idPrefix={`women-${tee.id}`}
/>
</div>
)
}
function RatingToggle({
label,
enabled,
rating,
onToggle,
onRatingChange,
idPrefix,
}: {
label: string
enabled: boolean
rating?: Rating
onToggle: (enabled: boolean) => void
onRatingChange: (rating: Rating) => void
idPrefix: string
}) {
const current = rating ?? { courseRating: "", slope: "", par: "" }
function update(field: keyof Rating, value: string) {
onRatingChange({ ...current, [field]: value })
}
return (
<div className="flex flex-col gap-3 rounded-xl border border-border bg-background/60 p-4">
<div className="flex items-center justify-between gap-4">
<Label htmlFor={idPrefix} className="text-base font-semibold text-foreground">
{label}
</Label>
<Switch id={idPrefix} checked={enabled} onCheckedChange={onToggle} />
</div>
{enabled && (
<div className="grid grid-cols-3 gap-3">
<div className="flex flex-col gap-1.5">
<Label htmlFor={`${idPrefix}-cr`} className="text-sm font-medium text-muted-foreground">
Course Rating
</Label>
<Input
id={`${idPrefix}-cr`}
inputMode="decimal"
value={current.courseRating}
onChange={(e) => update("courseRating", e.target.value)}
placeholder="71,2"
className="h-12 rounded-xl text-base"
/>
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor={`${idPrefix}-slope`} className="text-sm font-medium text-muted-foreground">
Slope
</Label>
<Input
id={`${idPrefix}-slope`}
inputMode="numeric"
value={current.slope}
onChange={(e) => update("slope", e.target.value)}
placeholder="132"
className="h-12 rounded-xl text-base"
/>
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor={`${idPrefix}-par`} className="text-sm font-medium text-muted-foreground">
Par
</Label>
<Input
id={`${idPrefix}-par`}
inputMode="numeric"
value={current.par}
onChange={(e) => update("par", e.target.value)}
placeholder="72"
className="h-12 rounded-xl text-base"
/>
</div>
</div>
)}
</div>
)
}
// --- Step 3: confirm -------------------------------------------------------
function todayIso() {
const now = new Date()
const offset = now.getTimezoneOffset()
const local = new Date(now.getTime() - offset * 60 * 1000)
return local.toISOString().slice(0, 10)
}
function ConfirmStep({
course,
ownGender,
onSubmit,
onCreated,
}: {
course: Course
ownGender: Gender
onSubmit: (payload: {
name: string
teeName: string
date: string
startedAt: string | null
startHole: number
holes: 9 | 18
statLevel: StatLevel
playFormat: PlayFormat
excludeFromHandicap: boolean
}) => Promise<{ id: string }>
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
onCreated: (roundId: string) => void
}) {
const compatibleTees = course.tees.filter((t) => (ownGender === "m" ? t.men : t.women))
const [teeId, setTeeId] = useState(compatibleTees[0]?.id ?? "")
const [name, setName] = useState("")
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
const [date, setDate] = useState(todayIso)
const [teeTime, setTeeTime] = useState("")
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
const [startHole, setStartHole] = useState("1")
const [holes, setHoles] = useState<9 | 18>(18)
const [statLevel, setStatLevel] = useState<StatLevel>("strokes_only")
const [playFormat, setPlayFormat] = useState<PlayFormat>("stroke")
const [excludeFromHandicap, setExcludeFromHandicap] = useState(false)
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
function choosePlayFormat(next: PlayFormat) {
setPlayFormat(next)
// ADR-038 Beslutning D -- foreslå (ikke tving) eksklusjon når
// matchspill velges. Brukeren kan uansett overstyre under.
setExcludeFromHandicap(next === "match")
}
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
const selectedTee = compatibleTees.find((t) => t.id === teeId)
async function handleStart() {
if (!selectedTee) return
setSubmitting(true)
setError(null)
try {
// Utslagstid er valgfritt -- kun dato er påkrevd. Kombinerer dato+
// klokkeslett til et ekte tidspunkt (nettleserens lokale tidssone,
// konvertert til UTC av toISOString()) kun når klokkeslett er satt.
const startedAt = teeTime ? new Date(`${date}T${teeTime}`).toISOString() : null
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
const created = await onSubmit({
name: name.trim(),
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
teeName: selectedTee.name,
date,
startedAt,
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
startHole: Number(startHole),
holes,
statLevel,
playFormat,
excludeFromHandicap,
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
})
onCreated(created.id)
} catch {
setError("Klarte ikke å opprette runden. Prøv igjen.")
setSubmitting(false)
}
}
return (
<div className="flex flex-col gap-8">
<StepHeader title="Bekreft og fullfør" description="Sjekk detaljene før du starter runden." />
{/* Course */}
<div className="flex items-center gap-4 rounded-2xl border border-border bg-card p-5 shadow-sm shadow-black/5">
<div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/15">
<Flag aria-hidden="true" className="size-6 text-primary" />
</div>
<div className="flex flex-col">
<span className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">Valgt bane</span>
<span className="text-lg font-bold text-foreground">{course.name}</span>
</div>
</div>
{error && <p className="text-base font-medium text-destructive">{error}</p>}
{/* Round name */}
<div className="flex flex-col gap-2">
<Label htmlFor="round-name" className="text-base font-semibold">
Navn runden <span className="font-normal text-muted-foreground">(valgfritt)</span>
</Label>
<Input
id="round-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={course.name}
maxLength={200}
className="h-14 rounded-2xl text-lg"
/>
</div>
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
{/* Tee */}
{compatibleTees.length === 0 ? (
<p role="alert" className="text-base font-medium text-destructive">
Denne banen har ingen registrert rating for ditt kjønn noe utslag -- HCP-sporing er
ikke mulig for denne runden. Velg en annen bane, eller fullfør profilen din nytt.
</p>
) : (
<fieldset className="flex flex-col gap-3">
<legend className="mb-1 text-base font-semibold text-foreground">Utslag</legend>
<div className="flex flex-wrap gap-2">
{compatibleTees.map((t) => (
<button
key={t.id}
type="button"
onClick={() => setTeeId(t.id)}
aria-pressed={teeId === t.id}
className={cn(
"inline-flex min-h-[44px] items-center gap-2 rounded-2xl border px-5 py-2.5 text-base font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
teeId === t.id
? "border-primary bg-primary/10 text-foreground"
: "border-border bg-card text-muted-foreground hover:bg-accent/50 hover:text-foreground",
)}
>
{teeId === t.id && <Check aria-hidden="true" className="size-4 text-primary" />}
{t.name || "Uten navn"}
</button>
))}
</div>
</fieldset>
)}
{/* Date + tee time + start hole */}
<div className="grid gap-5 sm:grid-cols-3">
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
<div className="flex flex-col gap-2">
<Label htmlFor="round-date" className="text-base font-semibold">
Dato
</Label>
<Input
id="round-date"
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
className="h-14 rounded-2xl text-lg"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="tee-time" className="text-base font-semibold">
Utslagstid <span className="font-normal text-muted-foreground">(valgfritt)</span>
</Label>
<Input
id="tee-time"
type="time"
value={teeTime}
onChange={(e) => setTeeTime(e.target.value)}
className="h-14 rounded-2xl text-lg"
/>
</div>
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
<div className="flex flex-col gap-2">
<Label htmlFor="start-hole" className="text-base font-semibold">
Starthull
</Label>
<select
id="start-hole"
value={startHole}
onChange={(e) => setStartHole(e.target.value)}
className="h-14 rounded-2xl border border-input bg-background px-4 text-lg font-semibold text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{Array.from({ length: 18 }, (_, i) => i + 1).map((h) => (
<option key={h} value={String(h)}>
Hull {h}
</option>
))}
</select>
</div>
</div>
{/* Statistikknivå -- kun slag er strengt tatt nødvendig for resultat/HCP */}
<fieldset className="flex flex-col gap-3">
<legend className="mb-1 text-base font-semibold text-foreground">Statistikk for deg selv</legend>
<div className="flex flex-col gap-2">
{(
[
{ value: "strokes_only", label: "Kun slag", desc: "Raskest -- bare det som trengs for resultat og HCP." },
{ value: "strokes_and_putts", label: "Slag og putter", desc: "Legger til putt-telling per hull." },
{ value: "full", label: "All statistikk", desc: "Kølle, retning, chip, bunker, straffeslag med mer." },
] as const
).map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setStatLevel(opt.value)}
aria-pressed={statLevel === opt.value}
className={cn(
"flex min-h-[44px] flex-col items-start gap-0.5 rounded-2xl border px-5 py-3 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
statLevel === opt.value
? "border-primary bg-primary/10"
: "border-border bg-card hover:bg-accent/50",
)}
>
<span className="text-base font-bold text-foreground">{opt.label}</span>
<span className="text-sm text-muted-foreground">{opt.desc}</span>
</button>
))}
</div>
</fieldset>
{/* Spilleform (ADR-038) -- selvdeklarert, ingen egen match-motor */}
<fieldset className="flex flex-col gap-3">
<legend className="mb-1 text-base font-semibold text-foreground">Spilleform</legend>
<div className="grid grid-cols-2 gap-3">
{(
[
{ value: "stroke" as const, label: "Slagspill" },
{ value: "match" as const, label: "Matchspill" },
]
).map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => choosePlayFormat(opt.value)}
aria-pressed={playFormat === opt.value}
className={cn(
"flex min-h-[44px] items-center justify-center rounded-2xl border px-4 py-3 text-lg font-bold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
playFormat === opt.value
? "border-primary bg-primary/10 text-foreground"
: "border-border bg-card text-muted-foreground hover:bg-accent/50 hover:text-foreground",
)}
>
{opt.label}
</button>
))}
</div>
{playFormat === "match" && (
<label className="flex min-h-[44px] items-start gap-3 rounded-2xl border border-border bg-card px-5 py-3">
<input
type="checkbox"
checked={excludeFromHandicap}
onChange={(e) => setExcludeFromHandicap(e.target.checked)}
className="mt-1 size-5 shrink-0 accent-primary"
/>
<span className="flex flex-col gap-0.5">
<span className="text-base font-bold text-foreground">
Ekskluder denne runden fra mitt faktiske HCP
</span>
<span className="text-sm text-muted-foreground">
Anbefalt for matchspill -- konsederte hull gir ofte et upålitelig grunnlag for
HCP-beregning. Du kan endre dette senere.
</span>
</span>
</label>
)}
</fieldset>
V0-integrasjonen er ferdig. Kort om hva som skjedde: Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig. Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet: Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet. La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon. Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang). Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder. Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk. Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
{/* Hole count */}
<fieldset className="flex flex-col gap-3">
<legend className="mb-1 text-base font-semibold text-foreground">Antall hull</legend>
<div className="grid grid-cols-2 gap-3">
{([9, 18] as const).map((count) => (
<button
key={count}
type="button"
onClick={() => setHoles(count)}
aria-pressed={holes === count}
className={cn(
"flex min-h-[44px] items-center justify-center rounded-2xl border px-4 py-5 text-xl font-extrabold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
holes === count
? "border-primary bg-primary/10 text-foreground"
: "border-border bg-card text-muted-foreground hover:bg-accent/50 hover:text-foreground",
)}
>
{count} hull
</button>
))}
</div>
</fieldset>
<Button
type="button"
size="lg"
disabled={submitting || compatibleTees.length === 0 || !selectedTee}
onClick={handleStart}
className="h-16 rounded-2xl text-xl font-extrabold shadow-sm"
>
{submitting ? "Oppretter…" : "Start runden"}
</Button>
</div>
)
}