import { STATUS_MAP } from "@/config/constants"; export type CourseStatus = { status?: string; name?: string; }; export type FacilityRecord = { id: number; slug: string; name: string; description?: string | null; city?: string | null; county?: string | null; banetype?: string | null; image_url?: string | null; phone?: string | null; email?: string | null; website_url?: string | null; ngf_number?: string | number | null; golfbox_club_id?: string | number | null; golfbox_booking_url?: string | null; golfbox_tournament_url?: string | null; weather_url?: string | null; lat?: number | null; lng?: number | null; golfamore?: boolean | null; nsg_url?: string | null; vtg_pris?: number | null; vtg_lenke?: string | null; vtg_beskrivelse?: string | null; amenities?: unknown; golfamore_data?: unknown; nsg_data?: unknown; vtg_datoer?: unknown; vtg_updated_at?: string | null; social_links?: unknown; course_statuses?: unknown; footnote?: string | null; footnote_updated_at?: string | null; status_updated_at?: string | null; }; export type EnrichedFacility = FacilityRecord & { holeValue: string; primaryStatus: string; hasGolfamore: boolean; hasNSG: boolean; hasSimulator: boolean; hasDrivingRange: boolean; hasVtg: boolean; countySlug: string; regions: string[]; statuses: CourseStatus[]; distance: number; lastUpdatedTs: number; }; export type PlaceConfig = { slug: string; areaFilter: string; label: string; title: string; intro: string; }; export const AREA_GROUPS: Record = { "nord-norge": ["finnmark", "troms", "nordland"], "midt-norge": ["trondelag", "nord-trondelag", "sor-trondelag"], vestlandet: ["more-og-romsdal", "sogn-og-fjordane", "hordaland", "rogaland", "vestland"], sorlandet: ["vest-agder", "aust-agder", "agder"], ostlandet: ["telemark", "vestfold", "ostfold", "buskerud", "hedmark", "oppland", "innlandet", "viken", "akershus", "oslo"], "oslo-og-akershus": ["akershus", "oslo", "viken"], }; const COUNTY_FILTER_ALIASES: Record = { trondelag: ["trondelag", "nord-trondelag", "sor-trondelag"], }; export const HIERARCHICAL_AREA_OPTIONS = [ { value: "", label: "Hele Norge", slug: "norge" }, { value: "region:nord-norge", label: "Nord-Norge", slug: "nord-norge" }, { value: "county:finnmark", label: "Finnmark", slug: "finnmark" }, { value: "county:troms", label: "Troms", slug: "troms" }, { value: "county:nordland", label: "Nordland", slug: "nordland" }, { value: "county:nord-trondelag", label: "Nord-Trøndelag", slug: "nord-trondelag" }, { value: "county:sor-trondelag", label: "Sør-Trøndelag", slug: "sor-trondelag" }, { value: "county:trondelag", label: "Trøndelag", slug: "trondelag" }, { value: "region:vestlandet", label: "Vestlandet", slug: "vestlandet" }, { value: "county:more-og-romsdal", label: "Møre og Romsdal", slug: "more-og-romsdal" }, { value: "county:sogn-og-fjordane", label: "Sogn og Fjordane", slug: "sogn-og-fjordane" }, { value: "county:hordaland", label: "Hordaland", slug: "hordaland" }, { value: "county:rogaland", label: "Rogaland", slug: "rogaland" }, { value: "region:sorlandet", label: "Sørlandet", slug: "sorlandet" }, { value: "county:vest-agder", label: "Vest-Agder", slug: "vest-agder" }, { value: "county:aust-agder", label: "Aust-Agder", slug: "aust-agder" }, { value: "county:agder", label: "Agder", slug: "agder" }, { value: "region:ostlandet", label: "Østlandet", slug: "ostlandet" }, { value: "county:telemark", label: "Telemark", slug: "telemark" }, { value: "county:vestfold", label: "Vestfold", slug: "vestfold" }, { value: "county:ostfold", label: "Østfold", slug: "ostfold" }, { value: "county:buskerud", label: "Buskerud", slug: "buskerud" }, { value: "county:hedmark", label: "Hedmark", slug: "hedmark" }, { value: "county:oppland", label: "Oppland", slug: "oppland" }, { value: "region:oslo-og-akershus", label: "Oslo og Akershus", slug: "oslo-og-akershus" }, { value: "county:akershus", label: "Akershus", slug: "akershus" }, { value: "county:oslo", label: "Oslo", slug: "oslo" }, { value: "county:innlandet", label: "Innlandet", slug: "innlandet" }, { value: "county:viken", label: "Viken", slug: "viken" }, ]; export const STATUS_ICON_PATHS: Record = { aapen: "/icons/open.png", aapen_med_vintergreener: "/icons/open-winter.png", stengt: "/icons/closed.png", aapner_snart: "/icons/open-soon.png", stenger_snart: "/icons/close-soon.png", under_utvikling: "/icons/under-development.png", nedlagt: "/icons/discontinued.png", ukjent: "/icons/unknown.png", }; export const normalizeText = (value: unknown) => String(value ?? "") .replace(/[æøå]/gi, (char) => { const normalized = char.toLowerCase(); if (normalized === "æ") return "ae"; if (normalized === "ø") return "o"; if (normalized === "å") return "a"; return normalized; }) .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^a-z0-9]+/g, " ") .trim(); export const normalizeStatus = (value: unknown) => String(value ?? "") .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/\s+/g, "_") .replace(/[^a-z0-9_]+/g, "") .trim(); export const slugify = (value: unknown) => normalizeText(value) .replace(/\s+/g, "-") .replace(/^-+|-+$/g, ""); export const parseJson = (value: unknown, fallback: T): T => { if (!value) return fallback; if (typeof value === "object") return value as T; try { return JSON.parse(String(value)) as T; } catch { return fallback; } }; const hasTruthyAmenity = (value: unknown) => { const normalized = normalizeText(value); return Boolean(normalized) && !["nei", "no", "false", "0", "ingen"].includes(normalized); }; export const getFacilityRegions = (county: string) => { const countySlug = slugify(county); return Object.entries(AREA_GROUPS) .filter(([, counties]) => counties.includes(countySlug)) .map(([region]) => region); }; const STATUS_ORDER = [ "aapen", "aapen_med_vintergreener", "stenger_snart", "aapner_snart", "ukjent", "stengt", "under_utvikling", "nedlagt", ]; export const getPrimaryStatus = (statuses: Array<{ status?: string }>) => { for (const candidate of STATUS_ORDER) { if (statuses.some((status) => normalizeStatus(status.status) === candidate)) { return candidate; } } return "ukjent"; }; export const getStatusLabel = (status: string) => STATUS_MAP[status] || "Ukjent status"; export const formatUpdatedDate = (value: string | null | undefined) => { if (!value) return "Ukjent"; const date = new Date(value); if (Number.isNaN(date.getTime())) return "Ukjent"; return date.toLocaleDateString("nb-NO", { day: "2-digit", month: "short", year: "numeric", }); }; export const buildMapUrl = (lat?: number | null, lng?: number | null) => { if (typeof lat !== "number" || typeof lng !== "number") return null; return `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`; }; export const getDistance = (lat1: number, lon1: number, lat2: number, lon2: number) => { const r = 6371; const dLat = ((lat2 - lat1) * Math.PI) / 180; const dLon = ((lon2 - lon1) * Math.PI) / 180; const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2); return r * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); }; export const enrichFacilities = ( facilities: FacilityRecord[], userLocation?: { lat: number; lng: number } | null ): EnrichedFacility[] => (Array.isArray(facilities) ? facilities : []).map((facility) => { const amenities = parseJson>(facility.amenities, {}); const golfamoreData = parseJson>(facility.golfamore_data, {}); const nsgData = parseJson>(facility.nsg_data, {}); const vtgDates = parseJson(facility.vtg_datoer, []); const rawStatuses = parseJson(facility.course_statuses, []); const statuses = Array.isArray(rawStatuses) && rawStatuses.length > 0 ? rawStatuses : [{ status: "ukjent", name: "Hovedbane" }]; const holeValue = String(amenities.antall_hull || "").trim(); const countySlug = slugify(facility.county || ""); const regions = getFacilityRegions(facility.county || ""); const updatedTsRaw = facility.status_updated_at ? new Date(facility.status_updated_at).getTime() : 0; return { ...facility, holeValue, countySlug, regions, statuses, primaryStatus: getPrimaryStatus(statuses), hasGolfamore: facility.golfamore === true || Object.keys(golfamoreData).length > 0, hasNSG: Boolean(facility.nsg_url) || Object.keys(nsgData).length > 0, hasSimulator: hasTruthyAmenity(amenities.simulator), hasDrivingRange: hasTruthyAmenity(amenities.drivingrange), hasVtg: Boolean(facility.vtg_pris) || Boolean(facility.vtg_lenke) || Boolean(facility.vtg_beskrivelse) || (Array.isArray(vtgDates) && vtgDates.length > 0), distance: userLocation && typeof facility.lat === "number" && typeof facility.lng === "number" ? getDistance(userLocation.lat, userLocation.lng, facility.lat, facility.lng) : Number.POSITIVE_INFINITY, lastUpdatedTs: Number.isFinite(updatedTsRaw) ? updatedTsRaw : 0, }; }); export const matchesAreaFilter = (facility: EnrichedFacility, areaFilter: string) => { if (!areaFilter) return true; const selectedArea = areaFilter.replace(/^(region:|county:)/, ""); if (areaFilter.startsWith("region:")) { return ( facility.regions.includes(selectedArea) || (AREA_GROUPS[selectedArea] ? AREA_GROUPS[selectedArea].includes(facility.countySlug) : false) ); } if (areaFilter.startsWith("county:")) { const aliases = COUNTY_FILTER_ALIASES[selectedArea]; return aliases ? aliases.includes(facility.countySlug) : facility.countySlug === selectedArea; } return true; }; export const filterFacilitiesByArea = (facilities: EnrichedFacility[], areaFilter: string) => facilities.filter((facility) => matchesAreaFilter(facility, areaFilter)); export const getPlaceConfigFromSlug = (slug: string): PlaceConfig | null => { const option = HIERARCHICAL_AREA_OPTIONS.find((entry) => entry.slug === slug); if (!option) return null; if (!option.value) { return { slug, areaFilter: "", label: option.label, title: "Golfbaner i Norge", intro: "Se alle norske golfbaner samlet med banestatus, kart og lenker videre til hver baneprofil.", }; } const isRegion = option.value.startsWith("region:"); return { slug, areaFilter: option.value, label: option.label, title: `Golfbaner i ${option.label}`, intro: isRegion ? `Utforsk golfbaner i ${option.label} med oppdatert banestatus, kart og direkte lenker til baneprofilene.` : `Utforsk golfbaner i ${option.label} og sammenlign banestatus, plassering og banedetaljer i listen under.`, }; }; export const getAvailablePlaceConfigs = () => HIERARCHICAL_AREA_OPTIONS.map(({ slug }) => slug);