"use client"
import { useEffect, useRef, useState } from "react"
import dynamic from "next/dynamic"
import { Crosshair, LocateFixed, MapPin, X, ChevronLeft, Check } from "lucide-react"
import { ClubPicker } from "@/components/teecup/club-picker"
import { Textarea } from "@/components/ui/textarea"
import { cn } from "@/lib/utils"
/**
* Lazy, client-only reference to the map step. Because dynamic() defers the
* underlying import() until is actually rendered, neither
* this module nor a GPS-only run ever pulls in mapbox-gl. A Mapbox map load
* (which is billed) only happens once the user chooses "Velg punkt på kart".
*/
const MapPointPicker = dynamic(
() => import("@/components/shot/map-point-picker").then((m) => m.MapPointPicker),
{
ssr: false,
loading: () => (
),
},
)
type LngLat = { lng: number; lat: number }
type Step = "start" | "map" | "end" | "club" | "result"
type GeoStatus = "idle" | "loading" | "error"
export type ShotMeasurementSheetProps = {
holeNumber: number
existingShotCount: number
ownBagClubs: string[]
/**
* Computed by the caller (Haversine) once both points are known. Optional
* here so the sheet stays previewable; when absent a rough preview-only
* stub is used purely for display.
*/
distanceMeters?: number
onSubmit: (result: {
club: string
distanceMeters: number
startMethod: "gps" | "map_tap"
startLat: number
startLng: number
endLat: number
endLng: number
share: boolean
shareText: string
}) => void
onCancel: () => void
}
// Preview-only rough distance so the result step shows a real number in v0.
// The real app passes distanceMeters computed server-/caller-side.
function previewDistance(a: LngLat | null, b: LngLat | null): number {
if (!a || !b) return 0
const R = 6371000
const toRad = (d: number) => (d * Math.PI) / 180
const dLat = toRad(b.lat - a.lat)
const dLng = toRad(b.lng - a.lng)
const lat1 = toRad(a.lat)
const lat2 = toRad(b.lat)
const h =
Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2
return Math.round(2 * R * Math.asin(Math.sqrt(h)))
}
export function ShotMeasurementSheet({
holeNumber,
existingShotCount,
ownBagClubs,
distanceMeters,
onSubmit,
onCancel,
}: ShotMeasurementSheetProps) {
const [step, setStep] = useState("start")
const [startMethod, setStartMethod] = useState<"gps" | "map_tap" | null>(null)
const [startPoint, setStartPoint] = useState(null)
const [endPoint, setEndPoint] = useState(null)
const [startStatus, setStartStatus] = useState("idle")
const [endStatus, setEndStatus] = useState("idle")
const [club, setClub] = useState("")
const [share, setShare] = useState(false)
const [shareText, setShareText] = useState("")
const shareEdited = useRef(false)
function acquirePosition(onOk: (p: LngLat) => void, onFail: () => void) {
if (typeof navigator === "undefined" || !navigator.geolocation) {
onFail()
return
}
navigator.geolocation.getCurrentPosition(
(pos) => onOk({ lng: pos.coords.longitude, lat: pos.coords.latitude }),
() => onFail(),
{ enableHighAccuracy: true, timeout: 10000 },
)
}
// Start via device GPS.
function useMyPosition() {
setStartStatus("loading")
acquirePosition(
(p) => {
setStartPoint(p)
setStartMethod("gps")
setStartStatus("idle")
setStep("end")
},
() => setStartStatus("error"),
)
}
// Ball position is ALWAYS GPS, regardless of how the start was chosen.
const endRequested = useRef(false)
useEffect(() => {
if (step !== "end") {
endRequested.current = false
return
}
if (endRequested.current) return
endRequested.current = true
setEndStatus("loading")
acquirePosition(
(p) => {
setEndPoint(p)
setEndStatus("idle")
setStep("club")
},
() => setEndStatus("error"),
)
}, [step])
const distance =
typeof distanceMeters === "number" ? distanceMeters : previewDistance(startPoint, endPoint)
// Prefill the (editable) share text once the result step is reached.
useEffect(() => {
if (step === "result" && !shareEdited.current) {
setShareText(`⛳ ${club || "slag"}, ${distance} m – hull ${holeNumber}`)
}
}, [step, club, distance, holeNumber])
function retryEnd() {
setEndStatus("loading")
acquirePosition(
(p) => {
setEndPoint(p)
setEndStatus("idle")
setStep("club")
},
() => setEndStatus("error"),
)
}
function goBack() {
if (step === "map" || step === "end") {
setStartStatus("idle")
setEndStatus("idle")
setStep("start")
} else if (step === "club") {
// Re-measure the ball position rather than reusing a stale fix.
setEndPoint(null)
setStep("end")
} else if (step === "result") {
setStep("club")
}
}
function confirmSubmit() {
if (!startPoint || !endPoint || !club) return
onSubmit({
club,
distanceMeters: distance,
startMethod: startMethod ?? "gps",
startLat: startPoint.lat,
startLng: startPoint.lng,
endLat: endPoint.lat,
endLng: endPoint.lng,
share,
shareText: share ? shareText : "",
})
}
const onMapStep = step === "map"
return (
{/* Header */}
{step !== "start" ? (
) : null}
{`Mål et slag — hull ${holeNumber}`}
{existingShotCount > 0 ? (
{`${existingShotCount} slag målt tidligere`}
) : null}
{/* Main */}
{step === "start" ? (
Velg hvor slaget startet. Ballens sluttposisjon måles alltid med GPS.
{startStatus === "loading" ? (
Finner posisjonen din …
) : null}
{startStatus === "error" ? (
Fant ikke posisjonen din. Sjekk at posisjonstjenester er på, og prøv igjen.