"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] patch({ course, courseMeta: meta, courseSource: meta.source === "teeoff" ? "official" : "own", teeId: tee?.id, s1Sub: "fields", // Eierens EGET spillerkort (steg 3) har sitt eget teeId-felt, atskilt // fra state.teeId over -- uten denne synkroniseringen viser kortet // "Ikke valgt" selv om et utslag faktisk er valgt (kun kosmetisk, selve // innsendingen leser state.teeId direkte for eieren -- men samme // synkronisering som den opprinnelige new-round.tsx sin chooseCourse() // gjorde, så oppførselen matcher eksakt). players: state.players.map((p) => (p.kind === "owner" ? { ...p, teeId: tee?.id } : p)), }) } switch (sub) { case "source": return ( patch({ s1Sub: "official-search" })} onOwn={() => patch({ s1Sub: "own-search" })} onBack={exit} /> ) case "template-source": return ( patch({ templateMode: true, s1Sub: "official-search" })} onOwn={() => patch({ templateMode: true, s1Sub: "own-search" })} onBack={() => patch({ s1Sub: "own-create" })} /> ) case "official-search": return ( patch({ s1Sub: state.templateMode ? "template-source" : "source" })} onPickFacility={(f) => patch({ selectedFacilitySlug: f.slug, s1Sub: "official-courses" })} /> ) case "official-courses": return ( patch({ s1Sub: "official-search" })} onPick={(teeoffCourseId, course) => pickCourse(course, { source: "teeoff", facilitySlug: state.selectedFacilitySlug!, teeoffCourseId }) } /> ) case "own-search": return ( 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 ( patch({ s1Sub: "own-search" })} onPick={(course) => pickCourse(course, { source: "custom", personalCourseId: course.id })} /> ) case "own-create": return ( 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 default: return null } } /* ------------------------------------------------------------------ */ function SubHeader({ title, onBack, subtitle }: { title: string; onBack: () => void; subtitle?: string }) { return (

{title}

{subtitle ?

{subtitle}

: null}
) } /* ------------------------------------------------------------------ */ /* source / template-source */ function SourceScreen({ templateMode, onOfficial, onOwn, onBack, }: { templateMode: boolean onOfficial: () => void onOwn: () => void onBack: () => void }) { return (
} title="Offisiell bane" description="Søk opp en registrert golfbane fra databasen." /> } title="Egen bane" description="Bruk eller lag din egen manuelt registrerte bane." />
) } /* ------------------------------------------------------------------ */ /* 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(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 (
{results === null ? ( nearest.length > 0 ? ( <>

    {nearest.map((f) => ( onPickFacility(f)} distance={f.distanceKm * 1000 < 1000 ? `${Math.round(f.distanceKm * 1000)} m` : `${Math.round(f.distanceKm)} km`} /> ))}
) : (

Begynn å skrive for å søke etter en bane.

) ) : results.length > 0 ? (
    {results.map((f) => ( onPickFacility(f)} /> ))}
) : (

Ingen anlegg matcher «{debounced}».

)}
) } function FacilityRow({ facility, onClick, distance }: { facility: Facility; onClick: () => void; distance?: string }) { return (
  • ) } /* ------------------------------------------------------------------ */ /* 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 (
    {loadError ? (

    Klarte ikke å hente baner fra teeoff akkurat nå. Prøv igjen om litt.

    ) : courses === null ? (

    Laster baner…

    ) : (
      {courses.map(({ teeoffCourseId, course }) => (
    • onPick(teeoffCourseId, course)} />
    • ))}
    )}
    ) } function CourseRow({ course, templateMode, onPick }: { course: Course; templateMode: boolean; onPick: () => void }) { return ( ) } /* ------------------------------------------------------------------ */ /* 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(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 (
    {!templateMode ? ( ) : null}
    {results === null ? (

    Søk etter en egen bane{templateMode ? "" : ", eller opprett en ny"}.

    ) : results.length > 0 ? (
      {results.map((c) => (
    • ))}
    ) : (

    Fant ingen egne baner som matcher «{debounced}».

    )}
    ) } /* ------------------------------------------------------------------ */ /* 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(null) const [selectedClub, setSelectedClub] = useState(null) const [searching, setSearching] = useState(false) const [importingId, setImportingId] = useState(null) const [error, setError] = useState(null) const inputRef = useRef(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 (
    setSelectedClub(null) : onBack} /> {error ? (

    {error}

    ) : null} {!selectedClub ? ( <>
    {clubs === null ? (

    Begynn å skrive for å søke etter en klubb.

    ) : clubs.length > 0 ? (
      {clubs.map((c) => (
    • ))}
    ) : (

    Ingen klubber matcher «{query}».

    )}
    ) : (
      {selectedClub.courses.map((c) => (
    • ))} {selectedClub.courses.length === 0 ? (
    • Ingen baner registrert hos denne klubben hos GolfAPI ennå.
    • ) : null}
    )}
    ) } /* ------------------------------------------------------------------ */ /* own-create */ function OwnCreate({ seed, onBack, onTemplate, onCreate, }: { seed?: Course onBack: () => void onTemplate: () => void onCreate: (c: Course) => void }) { return (
    {!seed ? ( ) : (
    Forhåndsutfylt fra «{seed.name}»
    )}
    ) } /* ------------------------------------------------------------------ */ /* 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 (

    Bane & tid

    Bekreft detaljene for runden.

    {course.name}

    {course.tees.length} utslag

    {state.courseSource === "own" ? "Egen bane" : "Offisiell"}
    patch({ roundName: e.target.value.slice(0, 200) })} placeholder={course.name} maxLength={200} /> {noCompatibleTee ? (
    Ingen kompatible utslag.
    ) : ( patch({ teeId: e.target.value })}> {compatibleTees.map((t) => ( ))} )}
    patch({ date: e.target.value })} /> patch({ teeTime: e.target.value })} />
    patch({ startHole: Number.parseInt(e.target.value, 10) })} > {Array.from({ length: 18 }, (_, i) => i + 1).map((h) => ( ))}
    ) }