teecup/frontend/components/ny-runde/step1-course-time.tsx

779 lines
29 KiB
TypeScript
Raw Normal View History

"use client"
import { ArrowLeft, Building2, ChevronRight, Flag, MapPin, PencilLine, Plus, Search } from "lucide-react"
import { useEffect, useMemo, useRef, useState } from "react"
import {
fetchFacilityCourses,
fetchOwnCourse,
importInternationalCourse,
nearbyOfficialFacilities,
searchInternationalClubs,
searchOfficialFacilities,
searchOwnCourses,
} from "@/lib/ny-runde/api"
import type { ApiInternationalClub, ApiInternationalCourseOption } from "@/lib/ny-runde/api"
import type { Course, Facility } from "@/lib/ny-runde/types"
import { CreateCourseForm } from "./create-course-form"
import { ChoiceCard, Field, NativeSelect, Panel, Pill, TextInput, useDebouncedValue } from "./primitives"
import { useWizard } from "./wizard-context"
export function Step1CourseTime() {
const { state, patch, exit } = useWizard()
const sub = state.s1Sub
function pickCourse(course: Course, meta: { source: "teeoff"; facilitySlug: string; teeoffCourseId: number } | { source: "custom"; personalCourseId: string }) {
if (state.templateMode) {
patch({ templateSeed: course, templateMode: false, s1Sub: "own-create" })
return
}
const compatible = state.ownGender ? course.tees.filter((t) => t.genders.includes(state.ownGender!)) : course.tees
const tee = compatible[0]
// Eierens EGET spillerkort (steg 3) sitt teeId-felt synkroniseres nå
// løpende fra state.teeId av en egen effekt i wizard-context.tsx
// (fanger også senere endringer i "Bane & tid"-bekreftelsen, ikke bare
// dette førstevalget) -- ingen manuell synk nødvendig her lenger.
patch({
course,
courseMeta: meta,
courseSource: meta.source === "teeoff" ? "official" : "own",
teeId: tee?.id,
s1Sub: "fields",
})
}
switch (sub) {
case "source":
return (
<SourceScreen
templateMode={false}
onOfficial={() => patch({ s1Sub: "official-search" })}
onOwn={() => patch({ s1Sub: "own-search" })}
onBack={exit}
/>
)
case "template-source":
return (
<SourceScreen
templateMode
onOfficial={() => patch({ templateMode: true, s1Sub: "official-search" })}
onOwn={() => patch({ templateMode: true, s1Sub: "own-search" })}
onBack={() => patch({ s1Sub: "own-create" })}
/>
)
case "official-search":
return (
<OfficialSearch
templateMode={state.templateMode}
onBack={() => patch({ s1Sub: state.templateMode ? "template-source" : "source" })}
onPickFacility={(f) => patch({ selectedFacilitySlug: f.slug, s1Sub: "official-courses" })}
/>
)
case "official-courses":
return (
<OfficialCourses
slug={state.selectedFacilitySlug}
templateMode={state.templateMode}
onBack={() => patch({ s1Sub: "official-search" })}
onPick={(teeoffCourseId, course) =>
pickCourse(course, { source: "teeoff", facilitySlug: state.selectedFacilitySlug!, teeoffCourseId })
}
/>
)
case "own-search":
return (
<OwnSearch
templateMode={state.templateMode}
onBack={() => patch({ s1Sub: state.templateMode ? "template-source" : "source" })}
onCreate={() => patch({ s1Sub: "own-create", templateSeed: undefined, ownCreateOrigin: "own-search" })}
onPick={(course) => pickCourse(course, { source: "custom", personalCourseId: course.id })}
onInternational={() => patch({ s1Sub: "international-search" })}
/>
)
case "international-search":
return (
<InternationalSearch
onBack={() => patch({ s1Sub: "own-search" })}
onPick={(course) => pickCourse(course, { source: "custom", personalCourseId: course.id })}
/>
)
case "own-create":
return (
<OwnCreate
onBack={() => patch({ s1Sub: "own-search", templateSeed: undefined })}
onTemplate={() => patch({ s1Sub: "template-source", ownCreateOrigin: "own-create" })}
onCreate={(course) => pickCourse(course, { source: "custom", personalCourseId: course.id })}
seed={state.templateSeed}
/>
)
case "fields":
return <Fields />
default:
return null
}
}
/* ------------------------------------------------------------------ */
function SubHeader({ title, onBack, subtitle }: { title: string; onBack: () => void; subtitle?: string }) {
return (
<div className="mb-4 flex items-start gap-3">
<button
type="button"
onClick={onBack}
aria-label="Tilbake"
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-[var(--nr-border)] bg-[var(--nr-surface)] text-[var(--nr-ink)] transition-colors hover:bg-[var(--nr-surface-2)] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[var(--nr-accent-ring)]/40"
>
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
</button>
<div>
<h2 className="text-lg font-semibold tracking-tight text-[var(--nr-ink)]">{title}</h2>
{subtitle ? <p className="text-sm text-[var(--nr-muted)]">{subtitle}</p> : null}
</div>
</div>
)
}
/* ------------------------------------------------------------------ */
/* source / template-source */
function SourceScreen({
templateMode,
onOfficial,
onOwn,
onBack,
}: {
templateMode: boolean
onOfficial: () => void
onOwn: () => void
onBack: () => void
}) {
return (
<div>
<SubHeader
title={templateMode ? "Velg en mal" : "Hvor spiller du?"}
subtitle={templateMode ? "Velg en bane å basere din egen bane på." : "Velg en offisiell bane eller din egen."}
onBack={onBack}
/>
<div className="grid gap-3">
<ChoiceCard
selected={false}
onSelect={onOfficial}
icon={<Building2 className="h-5 w-5" />}
title="Offisiell bane"
description="Søk opp en registrert golfbane fra databasen."
/>
<ChoiceCard
selected={false}
onSelect={onOwn}
icon={<PencilLine className="h-5 w-5" />}
title="Egen bane"
description="Bruk eller lag din egen manuelt registrerte bane."
/>
</div>
</div>
)
}
/* ------------------------------------------------------------------ */
/* official-search (med geolokasjon "nærmest deg") */
function OfficialSearch({
templateMode,
onBack,
onPickFacility,
}: {
templateMode: boolean
onBack: () => void
onPickFacility: (f: Facility) => void
}) {
const [query, setQuery] = useState("")
const debounced = useDebouncedValue(query, 250)
const [results, setResults] = useState<Facility[] | null>(null)
const [nearest, setNearest] = useState<(Facility & { distanceKm: number })[]>([])
const requested = useRef(false)
useEffect(() => {
if (requested.current) return
requested.current = true
if (typeof navigator === "undefined" || !navigator.geolocation) return
navigator.geolocation.getCurrentPosition(
(pos) => {
void nearbyOfficialFacilities(pos.coords.latitude, pos.coords.longitude).then(setNearest)
},
() => {
/* stille ved avslag/feil */
},
{ timeout: 8000 },
)
}, [])
useEffect(() => {
const trimmed = debounced.trim()
if (trimmed === "") {
setResults(null)
return
}
let cancelled = false
void searchOfficialFacilities(trimmed).then((r) => {
if (!cancelled) setResults(r)
})
return () => {
cancelled = true
}
}, [debounced])
return (
<div>
<SubHeader
title={templateMode ? "Offisiell bane som mal" : "Offisiell bane"}
subtitle="Søk etter anlegg på navn."
onBack={onBack}
/>
<Field label="Banenavn" htmlFor="official-q">
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--nr-faint)]" aria-hidden="true" />
<TextInput
id="official-q"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Søk etter anlegg…"
className="pl-9"
autoComplete="off"
/>
</div>
</Field>
<div className="mt-4">
{results === null ? (
nearest.length > 0 ? (
<>
<p className="mb-2 flex items-center gap-1.5 text-[13px] font-medium text-[var(--nr-muted)]">
<MapPin className="h-4 w-4" aria-hidden="true" /> Nærmest deg
</p>
<ul className="flex flex-col gap-2">
{nearest.map((f) => (
<FacilityRow
key={f.slug}
facility={f}
onClick={() => onPickFacility(f)}
distance={f.distanceKm * 1000 < 1000 ? `${Math.round(f.distanceKm * 1000)} m` : `${Math.round(f.distanceKm)} km`}
/>
))}
</ul>
</>
) : (
<p className="rounded-lg border border-dashed border-[var(--nr-border)] px-4 py-6 text-center text-sm text-[var(--nr-muted)]">
Begynn å skrive for å søke etter en bane.
</p>
)
) : results.length > 0 ? (
<ul className="flex flex-col gap-2">
{results.map((f) => (
<FacilityRow key={f.slug} facility={f} onClick={() => onPickFacility(f)} />
))}
</ul>
) : (
<p className="rounded-lg border border-dashed border-[var(--nr-border)] px-4 py-6 text-center text-sm text-[var(--nr-muted)]">
Ingen anlegg matcher «{debounced}».
</p>
)}
</div>
</div>
)
}
function FacilityRow({ facility, onClick, distance }: { facility: Facility; onClick: () => void; distance?: string }) {
return (
<li>
<button
type="button"
onClick={onClick}
className="flex min-h-14 w-full items-center gap-3 rounded-xl border border-[var(--nr-border)] bg-[var(--nr-surface)] px-4 py-3 text-left transition-colors hover:border-[var(--nr-border-strong)] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[var(--nr-accent-ring)]/40"
>
<Building2 className="h-5 w-5 shrink-0 text-[var(--nr-muted)]" aria-hidden="true" />
<span className="flex-1">
<span className="block text-sm font-medium text-[var(--nr-ink)]">{facility.name}</span>
<span className="block text-xs text-[var(--nr-muted)]">{[facility.city, facility.county].filter(Boolean).join(", ") || " "}</span>
</span>
{distance ? <Pill tone="neutral">{distance}</Pill> : null}
<ChevronRight className="h-4 w-4 shrink-0 text-[var(--nr-faint)]" aria-hidden="true" />
</button>
</li>
)
}
/* ------------------------------------------------------------------ */
/* official-courses */
function OfficialCourses({
slug,
templateMode,
onBack,
onPick,
}: {
slug?: string
templateMode: boolean
onBack: () => void
onPick: (teeoffCourseId: number, course: Course) => void
}) {
const [courses, setCourses] = useState<{ teeoffCourseId: number; course: Course }[] | null>(null)
const [loadError, setLoadError] = useState(false)
useEffect(() => {
if (!slug) return
let cancelled = false
setCourses(null)
setLoadError(false)
fetchFacilityCourses(slug)
.then((r) => {
if (!cancelled) setCourses(r)
})
.catch(() => {
if (!cancelled) setLoadError(true)
})
return () => {
cancelled = true
}
}, [slug])
return (
<div>
<SubHeader title="Baner" subtitle="Velg en bane / sløyfe." onBack={onBack} />
{loadError ? (
<p role="alert" className="rounded-lg border border-[var(--nr-danger)]/30 bg-[var(--nr-danger-soft)] px-4 py-3 text-sm text-[var(--nr-danger)]">
Klarte ikke å hente baner fra teeoff akkurat . Prøv igjen om litt.
</p>
) : courses === null ? (
<p className="text-sm text-[var(--nr-muted)]">Laster baner</p>
) : (
<ul className="flex flex-col gap-2">
{courses.map(({ teeoffCourseId, course }) => (
<li key={teeoffCourseId}>
<CourseRow course={course} templateMode={templateMode} onPick={() => onPick(teeoffCourseId, course)} />
</li>
))}
</ul>
)}
</div>
)
}
function CourseRow({ course, templateMode, onPick }: { course: Course; templateMode: boolean; onPick: () => void }) {
return (
<button
type="button"
onClick={onPick}
className="flex min-h-14 w-full items-center gap-3 rounded-xl border border-[var(--nr-border)] bg-[var(--nr-surface)] px-4 py-3 text-left transition-colors hover:border-[var(--nr-border-strong)] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[var(--nr-accent-ring)]/40"
>
<Flag className="h-5 w-5 shrink-0 text-[var(--nr-muted)]" aria-hidden="true" />
<span className="flex-1">
<span className="block text-sm font-medium text-[var(--nr-ink)]">{course.name}</span>
<span className="block text-xs text-[var(--nr-muted)]">{course.tees.length} utslag</span>
</span>
{templateMode ? <Pill tone="accent">Bruk som mal for egen bane</Pill> : <ChevronRight className="h-4 w-4 shrink-0 text-[var(--nr-faint)]" aria-hidden="true" />}
</button>
)
}
/* ------------------------------------------------------------------ */
/* own-search */
function OwnSearch({
templateMode,
onBack,
onCreate,
onPick,
onInternational,
}: {
templateMode: boolean
onBack: () => void
onCreate: () => void
onPick: (c: Course) => void
onInternational: () => void
}) {
const [query, setQuery] = useState("")
const debounced = useDebouncedValue(query, 250)
const [results, setResults] = useState<{ id: string; name: string }[] | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
useEffect(() => {
const trimmed = debounced.trim()
if (trimmed === "") {
setResults(null)
return
}
let cancelled = false
void searchOwnCourses(trimmed).then((r) => {
if (!cancelled) setResults(r)
})
return () => {
cancelled = true
}
}, [debounced])
async function pick(id: string) {
const course = await fetchOwnCourse(id)
if (course) onPick(course)
}
return (
<div>
<SubHeader
title={templateMode ? "Egen bane som mal" : "Egen bane"}
subtitle="Søk blant banene du har laget selv."
onBack={onBack}
/>
<Field label="Banenavn" htmlFor="own-q">
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--nr-faint)]" aria-hidden="true" />
<TextInput
id="own-q"
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Søk blant egne baner…"
className="pl-9"
autoComplete="off"
/>
</div>
</Field>
{!templateMode ? (
<button
type="button"
onClick={onCreate}
className="mt-3 inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-xl border border-dashed border-[var(--nr-border-strong)] bg-[var(--nr-surface)] px-4 text-sm font-medium text-[var(--nr-accent)] transition-colors hover:bg-[var(--nr-surface-2)] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[var(--nr-accent-ring)]/40"
>
<Plus className="h-4 w-4" aria-hidden="true" />
Opprett ny bane
</button>
) : null}
<div className="mt-4">
{results === null ? (
<p className="rounded-lg border border-dashed border-[var(--nr-border)] px-4 py-6 text-center text-sm text-[var(--nr-muted)]">
Søk etter en egen bane{templateMode ? "" : ", eller opprett en ny"}.
</p>
) : results.length > 0 ? (
<ul className="flex flex-col gap-2">
{results.map((c) => (
<li key={c.id}>
<button
type="button"
onClick={() => void pick(c.id)}
className="flex min-h-14 w-full items-center gap-3 rounded-xl border border-[var(--nr-border)] bg-[var(--nr-surface)] px-4 py-3 text-left transition-colors hover:border-[var(--nr-border-strong)] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[var(--nr-accent-ring)]/40"
>
<Flag className="h-5 w-5 shrink-0 text-[var(--nr-muted)]" aria-hidden="true" />
<span className="flex-1 text-sm font-medium text-[var(--nr-ink)]">{c.name}</span>
{templateMode ? <Pill tone="accent">Bruk som mal for egen bane</Pill> : <ChevronRight className="h-4 w-4 shrink-0 text-[var(--nr-faint)]" aria-hidden="true" />}
</button>
</li>
))}
</ul>
) : (
<p className="rounded-lg border border-dashed border-[var(--nr-border)] px-4 py-6 text-center text-sm text-[var(--nr-muted)]">
Fant ingen egne baner som matcher «{debounced}».
</p>
)}
</div>
<button
type="button"
onClick={onInternational}
className="mt-4 flex min-h-11 w-full items-center justify-center gap-2 text-[13px] font-medium text-[var(--nr-accent)] underline-offset-2 hover:underline"
>
Fant ikke banen? Søk internasjonalt (GolfAPI)
</button>
</div>
)
}
/* ------------------------------------------------------------------ */
/* international-search (GolfAPI.io, ADR-064) -- for baner utenfor */
/* teeoffs dekning (Tjøme Golfklubb var den konkrete anledningen). */
/* Importerte baner havner i samme personal_course-katalog som "egen */
/* bane" -- onPick gir tilbake en helt vanlig Course, ingen egen */
/* CourseMeta-variant trengs (speiler OwnSearch/tournament-program.tsx). */
function InternationalSearch({
onBack,
onPick,
}: {
onBack: () => void
onPick: (c: Course) => void
}) {
const [query, setQuery] = useState("")
const [clubs, setClubs] = useState<ApiInternationalClub[] | null>(null)
const [selectedClub, setSelectedClub] = useState<ApiInternationalClub | null>(null)
const [searching, setSearching] = useState(false)
const [importingId, setImportingId] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
async function runSearch() {
if (!query.trim()) return
setSearching(true)
setError(null)
try {
setClubs(await searchInternationalClubs(query.trim()))
} finally {
setSearching(false)
}
}
async function pick(course: ApiInternationalCourseOption) {
setImportingId(course.course_id)
setError(null)
try {
onPick(await importInternationalCourse(course.course_id))
} catch {
setError("Klarte ikke å importere banen fra GolfAPI. Prøv igjen.")
} finally {
setImportingId(null)
}
}
return (
<div>
<SubHeader
title={selectedClub ? selectedClub.club_name : "Søk internasjonalt"}
subtitle={selectedClub ? "Velg en bane hos klubben." : "For baner utenfor TeeOffs dekning, via GolfAPI."}
onBack={selectedClub ? () => setSelectedClub(null) : onBack}
/>
{error ? (
<p role="alert" className="mb-3 rounded-lg border border-[var(--nr-danger)]/30 bg-[var(--nr-danger-soft)] px-4 py-3 text-sm text-[var(--nr-danger)]">
{error}
</p>
) : null}
{!selectedClub ? (
<>
<Field label="Klubbnavn" htmlFor="intl-q">
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--nr-faint)]" aria-hidden="true" />
<TextInput
id="intl-q"
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
void runSearch()
}
}}
placeholder="Søk klubbnavn…"
className="pl-9"
autoComplete="off"
/>
</div>
</Field>
<button
type="button"
onClick={() => void runSearch()}
disabled={searching}
className="mt-3 inline-flex min-h-11 w-full items-center justify-center rounded-xl bg-[var(--nr-accent)] px-4 text-sm font-semibold text-[var(--nr-accent-ink)] transition-colors hover:brightness-95 disabled:opacity-50"
>
{searching ? "Søker…" : "Søk"}
</button>
<div className="mt-4">
{clubs === null ? (
<p className="rounded-lg border border-dashed border-[var(--nr-border)] px-4 py-6 text-center text-sm text-[var(--nr-muted)]">
Begynn å skrive for å søke etter en klubb.
</p>
) : clubs.length > 0 ? (
<ul className="flex flex-col gap-2">
{clubs.map((c) => (
<li key={c.club_id}>
<button
type="button"
onClick={() => setSelectedClub(c)}
className="flex min-h-14 w-full items-center gap-3 rounded-xl border border-[var(--nr-border)] bg-[var(--nr-surface)] px-4 py-3 text-left transition-colors hover:border-[var(--nr-border-strong)] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[var(--nr-accent-ring)]/40"
>
<Building2 className="h-5 w-5 shrink-0 text-[var(--nr-muted)]" aria-hidden="true" />
<span className="flex-1">
<span className="block text-sm font-medium text-[var(--nr-ink)]">{c.club_name}</span>
<span className="block text-xs text-[var(--nr-muted)]">{[c.city, c.country].filter(Boolean).join(", ") || " "}</span>
</span>
<ChevronRight className="h-4 w-4 shrink-0 text-[var(--nr-faint)]" aria-hidden="true" />
</button>
</li>
))}
</ul>
) : (
<p className="rounded-lg border border-dashed border-[var(--nr-border)] px-4 py-6 text-center text-sm text-[var(--nr-muted)]">
Ingen klubber matcher «{query}».
</p>
)}
</div>
</>
) : (
<ul className="flex flex-col gap-2">
{selectedClub.courses.map((c) => (
<li key={c.course_id}>
<button
type="button"
disabled={importingId !== null}
onClick={() => void pick(c)}
className="flex min-h-14 w-full items-center gap-3 rounded-xl border border-[var(--nr-border)] bg-[var(--nr-surface)] px-4 py-3 text-left transition-colors hover:border-[var(--nr-border-strong)] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[var(--nr-accent-ring)]/40 disabled:opacity-50"
>
<Flag className="h-5 w-5 shrink-0 text-[var(--nr-muted)]" aria-hidden="true" />
<span className="flex-1">
<span className="block text-sm font-medium text-[var(--nr-ink)]">{c.course_name}</span>
<span className="block text-xs text-[var(--nr-muted)]">
{c.num_holes} hull{c.has_gps ? " · avstandsdata tilgjengelig" : ""}
</span>
</span>
{importingId === c.course_id ? (
<span className="text-xs font-medium text-[var(--nr-muted)]">Importerer</span>
) : (
<ChevronRight className="h-4 w-4 shrink-0 text-[var(--nr-faint)]" aria-hidden="true" />
)}
</button>
</li>
))}
{selectedClub.courses.length === 0 ? (
<li className="rounded-lg border border-dashed border-[var(--nr-border)] px-4 py-6 text-center text-sm text-[var(--nr-muted)]">
Ingen baner registrert hos denne klubben hos GolfAPI ennå.
</li>
) : null}
</ul>
)}
</div>
)
}
/* ------------------------------------------------------------------ */
/* own-create */
function OwnCreate({
seed,
onBack,
onTemplate,
onCreate,
}: {
seed?: Course
onBack: () => void
onTemplate: () => void
onCreate: (c: Course) => void
}) {
return (
<div>
<SubHeader
title={seed ? "Ny bane (fra mal)" : "Ny egen bane"}
subtitle="Registrer hull, par, stroke-indeks og utslag."
onBack={onBack}
/>
{!seed ? (
<button
type="button"
onClick={onTemplate}
className="mb-4 text-[13px] font-medium text-[var(--nr-accent)] underline-offset-2 hover:underline"
>
Basér en eksisterende bane i stedet
</button>
) : (
<div className="mb-4">
<Pill tone="accent">Forhåndsutfylt fra «{seed.name}»</Pill>
</div>
)}
<CreateCourseForm seed={seed} onCancel={onBack} onCreate={onCreate} />
</div>
)
}
/* ------------------------------------------------------------------ */
/* fields */
function Fields() {
const { state, patch } = useWizard()
const course = state.course
if (!course) return null
const compatibleTees = state.ownGender ? course.tees.filter((t) => t.genders.includes(state.ownGender!)) : course.tees
const noCompatibleTee = compatibleTees.length === 0
return (
<div>
<div className="mb-4">
<h2 className="text-lg font-semibold tracking-tight text-[var(--nr-ink)]">Bane & tid</h2>
<p className="text-sm text-[var(--nr-muted)]">Bekreft detaljene for runden.</p>
</div>
<Panel className="mb-4 bg-[var(--nr-surface-2)]">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-semibold text-[var(--nr-ink)]">{course.name}</p>
<p className="text-xs text-[var(--nr-muted)]">{course.tees.length} utslag</p>
</div>
<Pill tone={state.courseSource === "own" ? "neutral" : "accent"}>{state.courseSource === "own" ? "Egen bane" : "Offisiell"}</Pill>
</div>
<button
type="button"
onClick={() => patch({ s1Sub: "source", course: undefined, courseMeta: undefined, teeId: undefined })}
className="mt-3 text-[13px] font-medium text-[var(--nr-accent)] underline-offset-2 hover:underline"
>
Bytt bane
</button>
</Panel>
<div className="flex flex-col gap-4">
<Field label="Navn på runden" optional htmlFor="round-name" hint="Maks 200 tegn.">
<TextInput
id="round-name"
value={state.roundName}
onChange={(e) => patch({ roundName: e.target.value.slice(0, 200) })}
placeholder={course.name}
maxLength={200}
/>
</Field>
<Field
label="Utslag"
htmlFor="tee"
error={
noCompatibleTee
? "Ingen utslag på denne banen har rating for ditt kjønn. Handicap kan ikke spores for denne runden, men du kan fortsette."
: null
}
>
{noCompatibleTee ? (
<div className="rounded-lg border border-[var(--nr-border)] bg-[var(--nr-surface-2)] px-3 py-2.5 text-sm text-[var(--nr-muted)]">
Ingen kompatible utslag.
</div>
) : (
<NativeSelect id="tee" value={state.teeId ?? ""} onChange={(e) => patch({ teeId: e.target.value })}>
{compatibleTees.map((t) => (
<option key={t.id} value={t.id}>
{t.name}
</option>
))}
</NativeSelect>
)}
</Field>
<div className="grid grid-cols-2 gap-3">
<Field label="Dato" htmlFor="date">
<TextInput id="date" type="date" value={state.date} onChange={(e) => patch({ date: e.target.value })} />
</Field>
<Field label="Utslagstid" optional htmlFor="teetime">
<TextInput id="teetime" type="time" value={state.teeTime} onChange={(e) => patch({ teeTime: e.target.value })} />
</Field>
</div>
<Field label="Starthull" htmlFor="starthole">
<NativeSelect
id="starthole"
value={state.startHole}
onChange={(e) => patch({ startHole: Number.parseInt(e.target.value, 10) })}
>
{Array.from({ length: 18 }, (_, i) => i + 1).map((h) => (
<option key={h} value={h}>
Hull {h}
</option>
))}
</NativeSelect>
</Field>
</div>
</div>
)
}