"use client" // Opprett en frittstående runde (ADR-033). To bane-kilder: offisiell // teeoff-bane (live oppslag, Beslutning C -- ingen lokal kopi lagres) eller // en egendefinert bane (organisasjonsuavhengig katalog, søkbar på tvers av // alle brukere -- samme "søk før du oppretter"-idé som org-banene). import type React from "react" import { useEffect, useState } from "react" import Link from "next/link" import { useRouter } from "next/navigation" import { ArrowLeft, Check, ChevronRight, Plus, Search, X } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Wordmark } from "@/components/wordmark" import { cn } from "@/lib/utils" type Gender = "m" | "f" type TeeOption = { name: string; genders: Gender[] } type SelectedCourse = { source: "teeoff" | "custom" teeoffFacilitySlug?: string teeoffCourseId?: number personalCourseId?: string name: string tees: TeeOption[] } type Step = "source" | "teeoff" | "custom-search" | "custom-create" | "confirm" export function NewRound() { const router = useRouter() const [step, setStep] = useState("source") const [selected, setSelected] = useState(null) const [ownGender, setOwnGender] = useState(null) const [loadingMe, setLoadingMe] = useState(true) 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 pickCourse(course: SelectedCourse) { setSelected(course) setStep("confirm") } return (

Ny runde

{loadingMe ? (
) : !ownGender ? (
Profilen din mangler registrert kjønn, som trengs for å beregne banehandicap riktig.{" "} Gå til kontoinnstillinger .
) : step === "source" ? ( setStep("teeoff")} onPickCustom={() => setStep("custom-search")} /> ) : step === "teeoff" ? ( setStep("source")} onPick={pickCourse} /> ) : step === "custom-search" ? ( setStep("source")} onPick={pickCourse} onCreateNew={() => setStep("custom-create")} /> ) : step === "custom-create" ? ( setStep("custom-search")} onCreated={pickCourse} /> ) : selected ? ( setStep("source")} /> ) : null}
) } // --- Steg 1: velg kilde ------------------------------------------------------ function SourceChoice({ onPickTeeoff, onPickCustom }: { onPickTeeoff: () => void; onPickCustom: () => void }) { return (

Hvor spilte du runden?

) } // --- Steg 2a: teeoff-søk ----------------------------------------------------- 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: TeeOption[] } function TeeoffCourseSearch({ onBack, onPick, }: { onBack: () => void onPick: (course: SelectedCourse) => void }) { const [query, setQuery] = useState("") const [facilities, setFacilities] = useState(null) const [selectedFacility, setSelectedFacility] = useState(null) const [courses, setCourses] = useState(null) const [searching, setSearching] = useState(false) const [error, setError] = useState(null) async function runSearch() { 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}`) setFacilities(await res.json()) } catch { setError("Klarte ikke å søke i teeoff sine baner akkurat nå.") setFacilities([]) } finally { setSearching(false) } } async function pickFacility(facility: ApiFacility) { setSelectedFacility(facility) setError(null) setCourses(null) try { const res = await fetch(`/rounds/official-search/${facility.slug}`, { credentials: "include" }) if (!res.ok) throw new Error(`facility detail: ${res.status}`) const detail: { courses: ApiOfficialCourseOption[] } = await res.json() setCourses(detail.courses) } catch { setError("Klarte ikke å hente baner for dette anlegget.") setCourses([]) } } return (
{!selectedFacility ? ( <> {error &&

{error}

}
setQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault() runSearch() } }} className="h-12 flex-1 rounded-xl text-base" />
{facilities && (
    {facilities.map((f) => (
  • ))} {facilities.length === 0 && (
  • Ingen treff.
  • )}
)} ) : ( <> setSelectedFacility(null)} label={selectedFacility.name} /> {error &&

{error}

} {courses === null ? (

Laster baner…

) : (
    {courses.map((c) => (
  • ))} {courses.length === 0 && (
  • Ingen 18-hulls baner registrert hos dette anlegget ennå.
  • )}
)} )}
) } // --- Steg 2b: egen bane -- søk eksisterende --------------------------------- type ApiPersonalCourse = { id: string; name: string } function CustomCourseSearch({ onBack, onPick, onCreateNew, }: { onBack: () => void onPick: (course: SelectedCourse) => void onCreateNew: () => void }) { const [query, setQuery] = useState("") const [results, setResults] = useState([]) const [error, setError] = useState(null) const [resolving, setResolving] = useState(false) 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]) async function pick(course: ApiPersonalCourse) { setResolving(true) setError(null) try { const res = await fetch(`/personal-courses/${course.id}`, { credentials: "include" }) if (!res.ok) throw new Error(`detail: ${res.status}`) const detail: { tees: TeeOption[] } = await res.json() onPick({ source: "custom", personalCourseId: course.id, name: course.name, tees: detail.tees }) } catch { setError("Klarte ikke å hente banedetaljer. Prøv igjen.") } finally { setResolving(false) } } return (
{error &&

{error}

} setQuery(e.target.value)} className="h-12 rounded-xl text-base" />
    {results.map((c) => (
  • ))} {results.length === 0 && (
  • {query.trim() ? "Ingen treff." : "Skriv for å søke, eller opprett en ny bane under."}
  • )}
) } // --- Steg 2c: egen bane -- opprett ny --------------------------------------- const DEFAULT_PARS = [4, 4, 3, 5, 4, 4, 3, 5, 4, 4, 4, 3, 5, 4, 4, 3, 5, 4] type HoleDraft = { par: number; strokeIndex: number } type TeeRatingDraft = { courseRating: string; slopeRating: string; par: string } type TeeDraft = { name: string; m: TeeRatingDraft | null; f: TeeRatingDraft | null } function CustomCourseCreate({ onBack, onCreated, }: { onBack: () => void onCreated: (course: SelectedCourse) => void }) { const [name, setName] = useState("") const [holes, setHoles] = useState( DEFAULT_PARS.map((par, i) => ({ par, strokeIndex: i + 1 })), ) const [tees, setTees] = useState([ { name: "Gul", m: { courseRating: "", slopeRating: "", par: "72" }, f: null }, ]) const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) const parSum = holes.reduce((s, h) => s + h.par, 0) const indexPermutationValid = new Set(holes.map((h) => h.strokeIndex)).size === 18 function updateHole(i: number, patch: Partial) { setHoles((prev) => prev.map((h, idx) => (idx === i ? { ...h, ...patch } : h))) } function updateTee(i: number, patch: Partial) { setTees((prev) => prev.map((t, idx) => (idx === i ? { ...t, ...patch } : t))) } const teesValid = tees.every( (t) => t.name.trim().length > 0 && (t.m || t.f) && [t.m, t.f].every((r) => !r || (r.courseRating.trim() && r.slopeRating.trim() && r.par.trim())), ) const canSubmit = name.trim().length >= 2 && indexPermutationValid && 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, i) => ({ hole_number: i + 1, par: h.par, stroke_index: h.strokeIndex })), tees: tees.map((t) => ({ name: t.name.trim(), ratings: [ ...(t.m ? [{ gender: "m", course_rating: Number(t.m.courseRating), slope_rating: Number(t.m.slopeRating), par: Number(t.m.par) }] : []), ...(t.f ? [{ gender: "f", course_rating: Number(t.f.courseRating), slope_rating: Number(t.f.slopeRating), par: Number(t.f.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({ source: "custom", personalCourseId: created.id, name: created.name, tees: tees.map((t) => ({ name: t.name.trim(), genders: [...(t.m ? (["m"] as const) : []), ...(t.f ? (["f"] as const) : [])], })), }) } catch { setError("Klarte ikke å opprette banen. Sjekk at hull og utslag er fylt ut riktig.") } finally { setSubmitting(false) } } return (
{error &&

{error}

}
setName(e.target.value)} className="h-12 rounded-xl text-base" />

18 hull

Sum par: {parSum}
{!indexPermutationValid && (

Hver stroke-indeks (1–18) må brukes nøyaktig én gang.

)}
{holes.map((h, i) => (
Hull {i + 1}
))}

Utslag / rating

{tees.map((t, i) => (
updateTee(i, { name: e.target.value })} placeholder="Navn på utslag (f.eks. Gul)" className="h-11 flex-1 rounded-xl text-base" /> {tees.length > 1 && ( )}
updateTee(i, { m: v })} /> updateTee(i, { f: v })} />
))}
) } function GenderRatingFields({ label, value, onChange, }: { label: string value: TeeRatingDraft | null onChange: (v: TeeRatingDraft | null) => void }) { const id = label.toLowerCase().replace(/\s+/g, "-") return (
{value && (
onChange({ ...value, courseRating: e.target.value })} className="h-10 rounded-lg text-sm" />
onChange({ ...value, slopeRating: e.target.value })} className="h-10 rounded-lg text-sm" />
onChange({ ...value, par: e.target.value })} className="h-10 rounded-lg text-sm" />
)}
) } // --- Steg 3: bekreft + opprett runde ----------------------------------------- function ConfirmRound({ course, ownGender, onBack, }: { course: SelectedCourse ownGender: Gender onBack: () => void }) { const router = useRouter() const compatibleTees = course.tees.filter((t) => t.genders.includes(ownGender)) const [teeName, setTeeName] = useState(compatibleTees[0]?.name ?? "") const [playedAt, setPlayedAt] = useState(() => new Date().toISOString().slice(0, 10)) const [startHole, setStartHole] = useState(1) const [holesPlanned, setHolesPlanned] = useState<9 | 18>(18) const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) async function handleSubmit() { if (!teeName) return setSubmitting(true) setError(null) try { const res = await fetch("/rounds", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ course_source: course.source, teeoff_facility_slug: course.teeoffFacilitySlug, teeoff_course_id: course.teeoffCourseId, personal_course_id: course.personalCourseId, tee_name: teeName, played_at: playedAt, start_hole: startHole, holes_planned: holesPlanned, }), }) if (!res.ok) throw new Error(`create round: ${res.status}`) const created: { id: string } = await res.json() router.replace(`/rounds/${created.id}`) } catch { setError("Klarte ikke å opprette runden. Prøv igjen.") setSubmitting(false) } } return (
{course.name}
{error &&

{error}

} {compatibleTees.length === 0 ? (

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.

) : (
{compatibleTees.map((t) => ( ))}
)}
setPlayedAt(e.target.value)} className="h-12 rounded-xl text-base" />
{([9, 18] as const).map((n) => ( ))}
) } // --- Delt -------------------------------------------------------------------- function BackLink({ onClick, label }: { onClick: () => void; label: string }) { return ( ) }