teecup/frontend/lib/ny-runde/api.ts

176 lines
5.9 KiB
TypeScript
Raw Normal View History

import type { ApiCourseHole, ApiCourseTee } from "@/components/course-template-editor"
import type { Account, ApiGender, Course, Facility, Gender, KnownGuest, Tee } from "./types"
// --- Kjønn-konvertering (samme mønster som new-round.tsx) ------------------
export function apiGenderOf(g: Gender): ApiGender | "x" {
return g === "mann" ? "m" : g === "kvinne" ? "f" : "x"
}
export function uiGenderOf(g: ApiGender): Gender {
return g === "m" ? "mann" : "kvinne"
}
// --- Rå API-typer (kun det denne veiviseren faktisk leser) -----------------
type ApiTeeOption = { name: string; genders: ApiGender[] }
type ApiFacility = { slug: string; name: string; city: string | null; county: string | null }
type ApiNearbyFacility = ApiFacility & { distance_km: number }
type ApiOfficialCourseOption = {
teeoff_course_id: number
name: string
is_main_course: boolean
tees: ApiTeeOption[]
holes: ApiCourseHole[]
full_tees: ApiCourseTee[]
}
type ApiPersonalCourse = { id: string; name: string; is_mine: boolean; created_by_display_name: string }
type ApiPersonalCourseDetail = {
id: string
name: string
tees: ApiTeeOption[]
holes: ApiCourseHole[]
full_tees: ApiCourseTee[]
is_mine: boolean
created_by_display_name: string
forked_from_id: string | null
}
export type PersonMatch = {
id: string
first_name: string
last_name: string
avatar_url: string | null
home_club: string | null
handicap_index: number | null
}
type ApiKnownGuest = {
guest_first_name: string
guest_last_name: string | null
gender: ApiGender | null
handicap_index: number | null
}
let idSeq = 0
function newId(prefix: string) {
idSeq += 1
return `${prefix}-${idSeq}`
}
function apiTeesToTees(apiTees: ApiTeeOption[]): Tee[] {
return apiTees.map((t, i) => ({ id: `${t.name}-${i}`, name: t.name, genders: t.genders }))
}
function courseFromOfficial(teeoffCourseId: number, c: {
name: string
tees: ApiTeeOption[]
holes: ApiCourseHole[]
full_tees: ApiCourseTee[]
}): Course {
return {
id: `teeoff-${teeoffCourseId}`,
name: c.name,
tees: apiTeesToTees(c.tees),
templateHoles: c.holes,
templateTees: c.full_tees,
}
}
// --- Offisielt banesøk -------------------------------------------------------
export async function searchOfficialFacilities(query: string): Promise<Facility[]> {
const res = await fetch(`/rounds/official-search?q=${encodeURIComponent(query)}`, { credentials: "include" })
if (!res.ok) return []
const data: ApiFacility[] = await res.json()
return data
}
export async function nearbyOfficialFacilities(lat: number, lng: number): Promise<(Facility & { distanceKm: number })[]> {
const res = await fetch(`/rounds/official-search/nearby?lat=${lat}&lng=${lng}&limit=5`, { credentials: "include" })
if (!res.ok) return []
const data: ApiNearbyFacility[] = await res.json()
return data.map((f) => ({ slug: f.slug, name: f.name, city: f.city, county: f.county, distanceKm: f.distance_km }))
}
export async function fetchFacilityCourses(slug: string): Promise<{ teeoffCourseId: number; course: Course }[]> {
const res = await fetch(`/rounds/official-search/${slug}`, { credentials: "include" })
if (!res.ok) return []
const data: { slug: string; name: string; courses: ApiOfficialCourseOption[] } = await res.json()
return data.courses.map((c) => ({
teeoffCourseId: c.teeoff_course_id,
course: courseFromOfficial(c.teeoff_course_id, c),
}))
}
// --- Egne baner ---------------------------------------------------------------
export async function searchOwnCourses(query: string): Promise<{ id: string; name: string }[]> {
const res = await fetch(`/personal-courses?q=${encodeURIComponent(query)}`, { credentials: "include" })
if (!res.ok) return []
const data: ApiPersonalCourse[] = await res.json()
return data.map((c) => ({ id: c.id, name: c.name }))
}
export async function fetchOwnCourse(id: string): Promise<Course | null> {
const res = await fetch(`/personal-courses/${id}`, { credentials: "include" })
if (!res.ok) return null
const c: ApiPersonalCourseDetail = await res.json()
return {
id: c.id,
name: c.name,
tees: apiTeesToTees(c.tees),
templateHoles: c.holes,
templateTees: c.full_tees,
}
}
export async function createOwnCourse(body: {
name: string
holes: { hole_number: number; par: number; stroke_index: number }[]
tees: { name: string; ratings: { gender: ApiGender; course_rating: number; slope_rating: number; par: number }[] }[]
forkedFromId?: string | null
}): Promise<Course> {
const res = await fetch("/personal-courses", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
name: body.name,
holes: body.holes,
tees: body.tees,
forked_from_id: body.forkedFromId ?? null,
}),
})
if (!res.ok) throw new Error(`create: ${res.status}`)
const created: { id: string; name: string } = await res.json()
return {
id: created.id,
name: created.name,
tees: body.tees.map((t) => ({
id: newId(t.name),
name: t.name,
genders: t.ratings.map((r) => r.gender),
})),
}
}
// --- Spillersøk / kjent gjest -------------------------------------------------
export async function searchAccounts(query: string): Promise<Account[]> {
const res = await fetch(`/people/search?q=${encodeURIComponent(query)}`, { credentials: "include" })
if (!res.ok) return []
const data: PersonMatch[] = await res.json()
return data.map((p) => ({ id: p.id, firstName: p.first_name, lastName: p.last_name, handicap: p.handicap_index }))
}
export async function lookupKnownGuest(email: string): Promise<KnownGuest | null> {
const res = await fetch(`/rounds/guests/known?email=${encodeURIComponent(email)}`, { credentials: "include" })
if (!res.ok) return null
const g: ApiKnownGuest | null = await res.json()
if (!g) return null
return {
firstName: g.guest_first_name,
lastName: g.guest_last_name ?? "",
gender: g.gender,
hcp: g.handicap_index,
}
}