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"
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
import { useRouter, useSearchParams } from "next/navigation"
|
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
|
|
|
|
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"
|
2026-07-24 06:27:35 +02:00
|
|
|
|
type StatLevel = "strokes_only" | "strokes_and_putts" | "full"
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-038/ADR-039 -- selvdeklarert spilleform. Ingen egen match-/skins-motor
|
|
|
|
|
|
// for frittstående runder, så dette taes brukerens ord for. Sider (for de
|
|
|
|
|
|
// to-sidede formatene) opprettes IKKE her -- det skjer i selve runden
|
|
|
|
|
|
// (POST .../sides), siden det krever at deltakerne allerede finnes.
|
|
|
|
|
|
type PlayFormat = "stroke" | "match" | "skins" | "fourball" | "foursome" | "greensome" | "scramble_2" | "scramble_4"
|
|
|
|
|
|
type SkinsScoring = "net" | "gross"
|
|
|
|
|
|
type SkinsTieHandling = "carry" | "split"
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
// ADR-036 Beslutning B (fase 2, 2026-07-28) -- styrer KUN tredjeparts
|
|
|
|
|
|
// innsyn (venner/offentligheten), aldri en faktisk lagt-til medspiller
|
|
|
|
|
|
// (som alltid ser runden uansett). Trygg standard "private".
|
|
|
|
|
|
type VisibilityMode = "private" | "public" | "friends"
|
|
|
|
|
|
|
|
|
|
|
|
// Samme faste sett/rekkefølge som friends.py sin Category og
|
|
|
|
|
|
// friends.tsx sin CATEGORY_OPTIONS -- egen, lokal kopi (samme
|
|
|
|
|
|
// "liten duplisering fremfor cross-file-import"-mønster som FORMAT_LABELS
|
|
|
|
|
|
// ellers i appen).
|
|
|
|
|
|
const VISIBILITY_CATEGORY_OPTIONS = [
|
|
|
|
|
|
{ code: "spouse", label: "Make" },
|
|
|
|
|
|
{ code: "close_family", label: "Nær familie" },
|
|
|
|
|
|
{ code: "extended_family", label: "Storfamilie" },
|
|
|
|
|
|
{ code: "close_friends", label: "Nære venner" },
|
|
|
|
|
|
{ code: "golf_friends", label: "Golfvenner" },
|
|
|
|
|
|
{ code: "colleagues", label: "Kollegaer" },
|
|
|
|
|
|
{ code: "business", label: "Forretningsforbindelser" },
|
|
|
|
|
|
{ code: "classmates", label: "Studiekamerater" },
|
|
|
|
|
|
{ code: "acquaintances", label: "Perifere bekjente" },
|
|
|
|
|
|
{ code: "other", label: "Ymse" },
|
|
|
|
|
|
] as const
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
const PLAY_FORMAT_OPTIONS: { value: PlayFormat; label: string; desc: string }[] = [
|
|
|
|
|
|
{ value: "stroke", label: "Slagspill", desc: "Vanlig individuell runde." },
|
|
|
|
|
|
{ value: "match", label: "Match", desc: "To sider, hull mot hull." },
|
|
|
|
|
|
{ value: "skins", label: "Skins", desc: "Hvert hull er en egen premie -- minst 3 spillere." },
|
|
|
|
|
|
{ value: "fourball", label: "Fourball", desc: "To mot to, beste ball per side." },
|
|
|
|
|
|
{ value: "foursome", label: "Foursome", desc: "To mot to, én delt ball, annenhver slår." },
|
|
|
|
|
|
{ value: "greensome", label: "Greensome", desc: "To mot to, begge slår ut, velger så én ball." },
|
|
|
|
|
|
{ value: "scramble_2", label: "Scramble (2)", desc: "To mot to, velger beste slag hver gang." },
|
|
|
|
|
|
{ value: "scramble_4", label: "Scramble (4)", desc: "Fire mot fire, velger beste slag hver gang." },
|
|
|
|
|
|
]
|
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()
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
const searchParams = useSearchParams()
|
|
|
|
|
|
// Flere flighter i én "vanlig" runde (migrasjon 035) -- kommer fra
|
|
|
|
|
|
// round-detail.tsx sin "Legg til en flight til"-handling. Rene primitiver
|
|
|
|
|
|
// i URL-en (ikke et bane-oppslag) -- brukeren velger selv bane på nytt
|
|
|
|
|
|
// for hver flight, men slipper å taste dato/starthull/spilleform igjen.
|
|
|
|
|
|
const flightGroupId = searchParams.get("flightGroupId")
|
|
|
|
|
|
const prefillPlayedAt = searchParams.get("playedAt") ?? undefined
|
|
|
|
|
|
const prefillStartHole = searchParams.get("startHole")
|
|
|
|
|
|
const prefillHoles = searchParams.get("holes")
|
|
|
|
|
|
const prefillPlayFormat = searchParams.get("playFormat") as PlayFormat | 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 [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: {
|
2026-07-25 06:29:09 +02:00
|
|
|
|
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
|
2026-07-24 11:21:39 +02:00
|
|
|
|
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
|
2026-07-24 06:27:35 +02:00
|
|
|
|
statLevel: StatLevel
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
playFormat: PlayFormat
|
|
|
|
|
|
excludeFromHandicap: boolean
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
skinsScoring: SkinsScoring | null
|
|
|
|
|
|
skinsTieHandling: SkinsTieHandling | null
|
2026-07-28 19:25:37 +02:00
|
|
|
|
visibilityMode: VisibilityMode
|
|
|
|
|
|
visibleCategories: 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
|
|
|
|
}): Promise<{ id: string }> {
|
|
|
|
|
|
if (!courseMeta) throw new Error("Mangler valgt bane")
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
const shared = {
|
|
|
|
|
|
tee_name: payload.teeName,
|
|
|
|
|
|
name: payload.name || null,
|
|
|
|
|
|
played_at: payload.date,
|
|
|
|
|
|
started_at: payload.startedAt,
|
|
|
|
|
|
start_hole: payload.startHole,
|
|
|
|
|
|
holes_planned: payload.holes,
|
|
|
|
|
|
stat_level: payload.statLevel,
|
|
|
|
|
|
play_format: payload.playFormat,
|
|
|
|
|
|
exclude_owner_from_handicap: payload.excludeFromHandicap,
|
|
|
|
|
|
skins_scoring: payload.skinsScoring,
|
|
|
|
|
|
skins_tie_handling: payload.skinsTieHandling,
|
2026-07-28 19:25:37 +02:00
|
|
|
|
visibility_mode: payload.visibilityMode,
|
|
|
|
|
|
visible_categories: payload.visibilityMode === "friends" ? payload.visibleCategories : [],
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
flight_group_id: flightGroupId || undefined,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}
|
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 body =
|
|
|
|
|
|
courseMeta.source === "teeoff"
|
|
|
|
|
|
? {
|
|
|
|
|
|
course_source: "teeoff",
|
|
|
|
|
|
teeoff_facility_slug: courseMeta.facilitySlug,
|
|
|
|
|
|
teeoff_course_id: courseMeta.teeoffCourseId,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
...shared,
|
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,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
...shared,
|
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">
|
|
|
|
|
|
Gå 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}`)}
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
isAdditionalFlight={Boolean(flightGroupId)}
|
|
|
|
|
|
defaultDate={prefillPlayedAt}
|
|
|
|
|
|
defaultStartHole={prefillStartHole ?? undefined}
|
|
|
|
|
|
defaultHoles={prefillHoles === "9" ? 9 : prefillHoles === "18" ? 18 : undefined}
|
|
|
|
|
|
defaultPlayFormat={prefillPlayFormat ?? undefined}
|
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}
|
2026-07-29 12:44:51 +02:00
|
|
|
|
className="flex min-h-[44px] flex-col items-start gap-4 rounded-3xl border border-border bg-card p-6 text-left shadow-md shadow-black/8 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"
|
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 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 ----------------------------------------------
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
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." />
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
<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: [],
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-07-29 12:44:51 +02:00
|
|
|
|
className="flex w-full items-center gap-4 rounded-2xl border border-border bg-card p-5 text-left shadow-md shadow-black/8 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"
|
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 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)}
|
2026-07-29 12:44:51 +02:00
|
|
|
|
className="flex w-full items-center gap-4 rounded-2xl border border-border bg-card p-5 text-left shadow-md shadow-black/8 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"
|
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 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)}
|
2026-07-29 12:44:51 +02:00
|
|
|
|
className="flex w-full items-center gap-4 rounded-2xl border border-border bg-card p-5 text-left shadow-md shadow-black/8 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"
|
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 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 på 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 (1–18) må 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 (
|
2026-07-29 12:44:51 +02:00
|
|
|
|
<div className="flex flex-col gap-4 rounded-2xl border border-border bg-card p-5 shadow-md shadow-black/8">
|
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 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 på 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,
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
isAdditionalFlight = false,
|
|
|
|
|
|
defaultDate,
|
|
|
|
|
|
defaultStartHole,
|
|
|
|
|
|
defaultHoles,
|
|
|
|
|
|
defaultPlayFormat,
|
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: Course
|
|
|
|
|
|
ownGender: Gender
|
2026-07-24 06:27:35 +02:00
|
|
|
|
onSubmit: (payload: {
|
2026-07-25 06:29:09 +02:00
|
|
|
|
name: string
|
2026-07-24 06:27:35 +02:00
|
|
|
|
teeName: string
|
|
|
|
|
|
date: string
|
2026-07-24 11:21:39 +02:00
|
|
|
|
startedAt: string | null
|
2026-07-24 06:27:35 +02:00
|
|
|
|
startHole: number
|
|
|
|
|
|
holes: 9 | 18
|
|
|
|
|
|
statLevel: StatLevel
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
playFormat: PlayFormat
|
|
|
|
|
|
excludeFromHandicap: boolean
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
skinsScoring: SkinsScoring | null
|
|
|
|
|
|
skinsTieHandling: SkinsTieHandling | null
|
2026-07-28 19:25:37 +02:00
|
|
|
|
visibilityMode: VisibilityMode
|
|
|
|
|
|
visibleCategories: string[]
|
2026-07-24 06:27:35 +02:00
|
|
|
|
}) => 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
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
// Flere flighter (migrasjon 035) -- satt av round-detail.tsx sin
|
|
|
|
|
|
// "Legg til en flight til" via URL-parametre, se NewRound() over.
|
|
|
|
|
|
isAdditionalFlight?: boolean
|
|
|
|
|
|
defaultDate?: string
|
|
|
|
|
|
defaultStartHole?: string
|
|
|
|
|
|
defaultHoles?: 9 | 18
|
|
|
|
|
|
defaultPlayFormat?: PlayFormat
|
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 compatibleTees = course.tees.filter((t) => (ownGender === "m" ? t.men : t.women))
|
|
|
|
|
|
const [teeId, setTeeId] = useState(compatibleTees[0]?.id ?? "")
|
2026-07-25 06:29:09 +02:00
|
|
|
|
const [name, setName] = useState("")
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
const [date, setDate] = useState(defaultDate ?? todayIso)
|
2026-07-24 11:21:39 +02:00
|
|
|
|
const [teeTime, setTeeTime] = useState("")
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
const [startHole, setStartHole] = useState(defaultStartHole ?? "1")
|
|
|
|
|
|
const [holes, setHoles] = useState<9 | 18>(defaultHoles ?? 18)
|
2026-07-24 06:27:35 +02:00
|
|
|
|
const [statLevel, setStatLevel] = useState<StatLevel>("strokes_only")
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
const [playFormat, setPlayFormat] = useState<PlayFormat>(defaultPlayFormat ?? "stroke")
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
const [excludeFromHandicap, setExcludeFromHandicap] = useState(false)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-039 Beslutning D -- kun meningsfullt (og påkrevd av backend) for
|
|
|
|
|
|
// play_format="skins". Fornuftige forhåndsvalg, begge overstyrbare.
|
|
|
|
|
|
const [skinsScoring, setSkinsScoring] = useState<SkinsScoring>("net")
|
|
|
|
|
|
const [skinsTieHandling, setSkinsTieHandling] = useState<SkinsTieHandling>("carry")
|
2026-07-28 19:25:37 +02:00
|
|
|
|
// ADR-036 Beslutning B (fase 2, 2026-07-28) -- trygg standard "private".
|
|
|
|
|
|
const [visibilityMode, setVisibilityMode] = useState<VisibilityMode>("private")
|
2026-07-29 12:44:51 +02:00
|
|
|
|
// Alle forhåndsvalgt (2026-07-29, presisert av bruker: "man må heller
|
|
|
|
|
|
// velge bort") -- kun brukt/sendt når visibilityMode faktisk er
|
|
|
|
|
|
// "friends", så det er trygt at dette alltid starter fullt uansett.
|
|
|
|
|
|
const [visibleCategories, setVisibleCategories] = useState<string[]>(VISIBILITY_CATEGORY_OPTIONS.map((c) => c.code))
|
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)
|
|
|
|
|
|
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
function choosePlayFormat(next: PlayFormat) {
|
|
|
|
|
|
setPlayFormat(next)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-038 Beslutning D -- foreslå (ikke tving) eksklusjon når match
|
|
|
|
|
|
// velges. De andre formatene har egne HCP-regler (ADR-039 Beslutning E)
|
|
|
|
|
|
// som ikke bruker dette feltet -- nullstill det da for å unngå et
|
|
|
|
|
|
// misvisende forhåndsvalg som likevel ikke sendes videre meningsfullt.
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
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)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-039 Beslutning A -- match/fourball/foursome/greensome/scramble
|
|
|
|
|
|
// krever sider, opprettet ETTERPÅ inne i selve runden (deltakerne må
|
|
|
|
|
|
// finnes først). Vises som en forklarende note her, ikke et skjema.
|
|
|
|
|
|
const needsSidesSetup = playFormat !== "stroke" && playFormat !== "skins"
|
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
|
|
|
|
|
|
|
|
|
|
async function handleStart() {
|
|
|
|
|
|
if (!selectedTee) return
|
|
|
|
|
|
setSubmitting(true)
|
|
|
|
|
|
setError(null)
|
|
|
|
|
|
try {
|
2026-07-24 11:21:39 +02:00
|
|
|
|
// 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({
|
2026-07-25 06:29:09 +02:00
|
|
|
|
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,
|
2026-07-24 11:21:39 +02:00
|
|
|
|
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,
|
2026-07-24 06:27:35 +02:00
|
|
|
|
statLevel,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
playFormat,
|
|
|
|
|
|
excludeFromHandicap,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
skinsScoring: playFormat === "skins" ? skinsScoring : null,
|
|
|
|
|
|
skinsTieHandling: playFormat === "skins" ? skinsTieHandling : null,
|
2026-07-28 19:25:37 +02:00
|
|
|
|
visibilityMode,
|
|
|
|
|
|
visibleCategories,
|
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." />
|
|
|
|
|
|
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
{isAdditionalFlight && (
|
|
|
|
|
|
<p className="rounded-2xl border border-dashed border-primary/40 bg-primary/5 px-4 py-3 text-sm font-semibold text-foreground">
|
|
|
|
|
|
Du setter opp enda en flight i samme oppsett -- dato, starthull, hullantall og spilleform er
|
|
|
|
|
|
forhåndsutfylt fra den første flighten, men kan endres.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
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 */}
|
2026-07-29 12:44:51 +02:00
|
|
|
|
<div className="flex items-center gap-4 rounded-2xl border border-border bg-card p-5 shadow-md shadow-black/8">
|
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 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>}
|
|
|
|
|
|
|
2026-07-25 06:29:09 +02:00
|
|
|
|
{/* Round name */}
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<Label htmlFor="round-name" className="text-base font-semibold">
|
|
|
|
|
|
Navn på 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 på noe utslag -- HCP-sporing er
|
|
|
|
|
|
ikke mulig for denne runden. Velg en annen bane, eller fullfør profilen din på 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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
{/* 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>
|
2026-07-24 11:21:39 +02:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-07-24 06:27:35 +02:00
|
|
|
|
{/* 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>
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
{/* Spilleform (ADR-038/ADR-039) -- selvdeklarert, ingen egen match-/
|
|
|
|
|
|
skins-motor. match/fourball/foursome/greensome/scramble krever
|
|
|
|
|
|
sider -- opprettes i selve runden etter at deltakerne er lagt til. */}
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
<fieldset className="flex flex-col gap-3">
|
|
|
|
|
|
<legend className="mb-1 text-base font-semibold text-foreground">Spilleform</legend>
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
|
|
|
|
{PLAY_FORMAT_OPTIONS.map((opt) => (
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
<button
|
|
|
|
|
|
key={opt.value}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => choosePlayFormat(opt.value)}
|
|
|
|
|
|
aria-pressed={playFormat === opt.value}
|
|
|
|
|
|
className={cn(
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
"flex min-h-[44px] flex-col items-center justify-center gap-0.5 rounded-2xl border px-3 py-3 text-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
playFormat === opt.value
|
|
|
|
|
|
? "border-primary bg-primary/10 text-foreground"
|
|
|
|
|
|
: "border-border bg-card text-muted-foreground hover:bg-accent/50 hover:text-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
<span className="text-base font-bold">{opt.label}</span>
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
{PLAY_FORMAT_OPTIONS.find((o) => o.value === playFormat)?.desc}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
{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">
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
Anbefalt for match -- konsederte hull gir ofte et upålitelig grunnlag for
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
HCP-beregning. Du kan endre dette senere.
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
)}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
|
|
|
|
|
{playFormat === "skins" && (
|
|
|
|
|
|
<div className="flex flex-col gap-4 rounded-2xl border border-border bg-card px-5 py-4">
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">Poengberegning</span>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
{(
|
|
|
|
|
|
[
|
|
|
|
|
|
{ value: "net" as const, label: "Netto (m/HCP)" },
|
|
|
|
|
|
{ value: "gross" as const, label: "Brutto" },
|
|
|
|
|
|
]
|
|
|
|
|
|
).map((opt) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={opt.value}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setSkinsScoring(opt.value)}
|
|
|
|
|
|
aria-pressed={skinsScoring === opt.value}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-h-11 items-center justify-center rounded-xl border text-sm font-bold transition-colors",
|
|
|
|
|
|
skinsScoring === opt.value
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-background text-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{opt.label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">Ved uavgjort hull</span>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
{(
|
|
|
|
|
|
[
|
|
|
|
|
|
{ value: "carry" as const, label: "Rullerer over" },
|
|
|
|
|
|
{ value: "split" as const, label: "Deles likt" },
|
|
|
|
|
|
]
|
|
|
|
|
|
).map((opt) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={opt.value}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setSkinsTieHandling(opt.value)}
|
|
|
|
|
|
aria-pressed={skinsTieHandling === opt.value}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-h-11 items-center justify-center rounded-xl border text-sm font-bold transition-colors",
|
|
|
|
|
|
skinsTieHandling === opt.value
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-background text-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{opt.label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Skins krever minst 3 spillere i runden -- legg til medspillere etter at runden er opprettet.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{needsSidesSetup && (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Du setter opp sidene (og fordeler spillerne) inne i runden, etter at medspillerne er
|
|
|
|
|
|
lagt til.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
</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>
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
{/* Synlighet (ADR-036 Beslutning B, fase 2) -- styrer KUN tredjeparts
|
|
|
|
|
|
innsyn (venner/offentligheten), aldri en faktisk lagt-til
|
|
|
|
|
|
medspiller (som alltid ser runden uansett). Trygg standard
|
|
|
|
|
|
"Privat", samme filosofi som RLS-policyenes "se ingenting" ved
|
|
|
|
|
|
manglende kontekst ellers i appen. */}
|
|
|
|
|
|
<fieldset className="flex flex-col gap-3">
|
|
|
|
|
|
<legend className="mb-1 text-base font-semibold text-foreground">Synlighet</legend>
|
|
|
|
|
|
<div className="grid grid-cols-3 gap-2">
|
|
|
|
|
|
{(
|
|
|
|
|
|
[
|
|
|
|
|
|
{ value: "private", label: "Privat", desc: "Kun deg og faktiske medspillere." },
|
|
|
|
|
|
{ value: "friends", label: "Venner", desc: "Valgte venne-kategorier kan følge live." },
|
|
|
|
|
|
{ value: "public", label: "Offentlig", desc: "Alle kan følge live, ingen innlogging." },
|
|
|
|
|
|
] as { value: VisibilityMode; label: string; desc: string }[]
|
|
|
|
|
|
).map((opt) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={opt.value}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setVisibilityMode(opt.value)}
|
|
|
|
|
|
aria-pressed={visibilityMode === opt.value}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-h-[44px] flex-col items-center gap-0.5 rounded-2xl border px-2 py-3 text-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
|
|
|
|
visibilityMode === opt.value
|
|
|
|
|
|
? "border-primary bg-primary/10 text-foreground"
|
|
|
|
|
|
: "border-border bg-card text-muted-foreground hover:bg-accent/50 hover:text-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="text-sm font-bold">{opt.label}</span>
|
|
|
|
|
|
<span className="text-xs">{opt.desc}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{visibilityMode === "friends" && (
|
|
|
|
|
|
<div className="flex flex-col gap-2 rounded-2xl border border-border bg-card p-4">
|
|
|
|
|
|
<p className="text-sm font-semibold text-foreground">
|
2026-07-29 12:44:51 +02:00
|
|
|
|
Hvilke av dine venne-kategorier skal kunne se runden? Alle er forhåndsvalgt — velg heller bort det
|
|
|
|
|
|
du ikke vil skal se den.
|
2026-07-28 19:25:37 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{VISIBILITY_CATEGORY_OPTIONS.map(({ code, label }) => {
|
|
|
|
|
|
const active = visibleCategories.includes(code)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={code}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
setVisibleCategories((prev) =>
|
|
|
|
|
|
active ? prev.filter((c) => c !== code) : [...prev, code],
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
aria-pressed={active}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"min-h-[44px] rounded-full border px-3 text-sm font-semibold transition-colors",
|
|
|
|
|
|
active
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-background text-muted-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{visibleCategories.length === 0 && (
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
|
Ingen kategori valgt ennå -- ingen venner kan se runden før du velger minst én.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</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
|
|
|
|
<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>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|