diff --git a/2026-04-20 09.00.46 teeoff.no d8d72ce0bea9.jpg b/2026-04-20 09.00.46 teeoff.no d8d72ce0bea9.jpg deleted file mode 100644 index 2d40e7a..0000000 Binary files a/2026-04-20 09.00.46 teeoff.no d8d72ce0bea9.jpg and /dev/null differ diff --git a/2026-04-20 09.33.55 teeoff.no cc7384f6fb3e.jpg b/2026-04-20 09.33.55 teeoff.no cc7384f6fb3e.jpg new file mode 100644 index 0000000..4c5dcf9 Binary files /dev/null and b/2026-04-20 09.33.55 teeoff.no cc7384f6fb3e.jpg differ diff --git a/frontend/src/app/FacilitySearch.tsx b/frontend/src/app/FacilitySearch.tsx index b8fca0d..9b179d1 100755 --- a/frontend/src/app/FacilitySearch.tsx +++ b/frontend/src/app/FacilitySearch.tsx @@ -4,7 +4,7 @@ import { STATUS_MAP } from "@/config/constants"; import Image from "next/image"; import Link from "next/link"; import { useEffect, useMemo, useState, type CSSProperties } from "react"; -import { type EnrichedFacility } from "@/app/facilityData"; +import { getPublicCourseDisplayName, type EnrichedFacility } from "@/app/facilityData"; type SortMethod = "updated" | "dist" | "alpha"; type Variant = "home" | "catalog"; @@ -247,6 +247,38 @@ const formatUpdatedDate = (value: string | null | undefined) => { const getStatusLabel = (status: string) => STATUS_MAP[status] || "Ukjent"; +type StatusBadge = { + label: string; + status: string; +}; + +const buildFacilityStatusBadges = (statuses: CourseStatus[]): StatusBadge[] => { + const normalizedStatuses = (Array.isArray(statuses) ? statuses : []) + .map((status) => ({ + name: String(status?.name || "").trim(), + status: normalizeStatus(status?.status) || "ukjent", + })) + .filter((status) => status.status); + + if (normalizedStatuses.length === 0) { + return [{ label: getStatusLabel("ukjent"), status: "ukjent" }]; + } + + const uniqueStatuses = [...new Set(normalizedStatuses.map((status) => status.status))]; + if (normalizedStatuses.length === 1 || uniqueStatuses.length === 1) { + const status = uniqueStatuses[0] || "ukjent"; + return [{ label: getStatusLabel(status), status }]; + } + + return normalizedStatuses.map((course, index) => { + const courseName = getPublicCourseDisplayName(course.name, index, normalizedStatuses.length); + return { + label: courseName ? `${courseName}: ${getStatusLabel(course.status)}` : getStatusLabel(course.status), + status: course.status, + }; + }); +}; + 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}`; @@ -505,7 +537,7 @@ export default function FacilitySearch({ const statuses = Array.isArray(rawStatuses) && rawStatuses.length > 0 ? rawStatuses - : [{ status: "ukjent", name: "Hovedbane" }]; + : [{ status: "ukjent", name: "" }]; const countySlug = slugify(facility.county || ""); const regions = getFacilityRegions(facility.county || ""); @@ -870,11 +902,14 @@ export default function FacilitySearch({ ) : (
- {processedFacilities.map((facility) => ( -
+ {processedFacilities.map((facility) => { + const statusBadges = buildFacilityStatusBadges(facility.statuses); + + return ( +
- - {getStatusLabel(facility.primaryStatus)} - + {statusBadges.map((badge) => ( + + {badge.label} + + ))} {facility.hasGolfamore && ( Golfamore @@ -1009,8 +1047,9 @@ export default function FacilitySearch({
-
- ))} +
+ ); + })}
)} diff --git a/frontend/src/app/facilityData.ts b/frontend/src/app/facilityData.ts index 249f7bf..992910e 100755 --- a/frontend/src/app/facilityData.ts +++ b/frontend/src/app/facilityData.ts @@ -146,6 +146,35 @@ export const normalizeStatus = (value: unknown) => .replace(/[^a-z0-9_]+/g, "") .trim(); +const GENERIC_SINGLE_COURSE_NAMES = new Set(["hovedbane", "hovedbanen"]); + +export const getPublicCourseDisplayName = (name: unknown, index: number, total: number) => { + const trimmedName = String(name ?? "").trim(); + const normalizedName = normalizeText(trimmedName).replace(/\s+/g, ""); + + if (total <= 1 && GENERIC_SINGLE_COURSE_NAMES.has(normalizedName)) { + return ""; + } + + if (trimmedName) { + return trimmedName; + } + + if (total <= 1) { + return ""; + } + + if (index === 0) { + return "Hovedbane"; + } + + if (total === 2) { + return "Sekundærbane"; + } + + return `Sekundærbane ${index}`; +}; + export const slugify = (value: unknown) => normalizeText(value) .replace(/\s+/g, "-") @@ -237,7 +266,7 @@ export const enrichFacilities = ( const statuses = Array.isArray(rawStatuses) && rawStatuses.length > 0 ? rawStatuses - : [{ status: "ukjent", name: "Hovedbane" }]; + : [{ status: "ukjent", name: "" }]; const holeValue = String(amenities.antall_hull || "").trim(); const countySlug = slugify(facility.county || ""); const regions = getFacilityRegions(facility.county || ""); diff --git a/frontend/src/app/golfbaner/[slug]/CourseDisplay.tsx b/frontend/src/app/golfbaner/[slug]/CourseDisplay.tsx index 67ca477..7ae5c42 100644 --- a/frontend/src/app/golfbaner/[slug]/CourseDisplay.tsx +++ b/frontend/src/app/golfbaner/[slug]/CourseDisplay.tsx @@ -28,7 +28,7 @@ const getTeeTheme = (label: string) => { return { header: "bg-gray-200 text-gray-700", col: "bg-gray-100/60", text: "text-gray-600" }; }; -export default function CourseDisplay({ course }: { course: any }) { +export default function CourseDisplay({ course, courseDisplayName = "" }: { course: any; courseDisplayName?: string }) { const [hcp, setHcp] = useState("15.0"); const [gender, setGender] = useState<'herrer' | 'damer'>('herrer'); const [selectedTeeIndex, setSelectedTeeIndex] = useState(0); @@ -134,7 +134,9 @@ export default function CourseDisplay({ course }: { course: any }) { {/* HEADER / KALKULATOR */}
-

{course.name}

+ {courseDisplayName ? ( +

{courseDisplayName}

+ ) : null}

Par {course.par} • {course.length_meters || '--'} meter

diff --git a/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx b/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx index cfcd86a..0245412 100644 --- a/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx +++ b/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx @@ -16,7 +16,7 @@ import { useState, useEffect } from 'react'; import dynamic from "next/dynamic"; import { STATUS_MAP, FALLBACK_IMAGE } from "@/config/constants"; -import { STATUS_ICON_PATHS, buildMapUrl, getPrimaryStatus, parseJson as parseSharedJson, slugify } from "@/app/facilityData"; +import { STATUS_ICON_PATHS, buildMapUrl, getPrimaryStatus, getPublicCourseDisplayName, parseJson as parseSharedJson, slugify } from "@/app/facilityData"; import Link from 'next/link'; import CourseDisplay from './CourseDisplay'; import FacilityFeedbackForm from './FacilityFeedbackForm'; @@ -277,11 +277,16 @@ export default function FacilityDetailView({ facility }: { facility: any }) { {/* BANESTATUS BADGES */}
- {activeCourses.map((c: any) => ( - - {c.name.toUpperCase()}: {STATUS_MAP[c.status] || c.status} - - ))} + {activeCourses.map((c: any, index: number) => { + const courseName = getPublicCourseDisplayName(c.name, index, activeCourses.length); + const label = STATUS_MAP[c.status] || c.status; + + return ( + + {courseName ? `${courseName.toUpperCase()}: ${label}` : label} + + ); + })}
{facility.status_updated_at && ( @@ -769,9 +774,9 @@ export default function FacilityDetailView({ facility }: { facility: any }) {

Scorekort

- {activeCourses.map((c: any) => ( + {activeCourses.map((c: any, index: number) => (
-
+
))}
diff --git a/frontend/src/components/PlaceMapLeaflet.tsx b/frontend/src/components/PlaceMapLeaflet.tsx index e8bf133..c1f1f3e 100644 --- a/frontend/src/components/PlaceMapLeaflet.tsx +++ b/frontend/src/components/PlaceMapLeaflet.tsx @@ -7,6 +7,7 @@ import { MapContainer, Marker, Popup, TileLayer, useMap } from "react-leaflet"; import { buildMapUrl, formatUpdatedDate, + getPublicCourseDisplayName, getStatusLabel, parseJson, STATUS_ICON_PATHS, @@ -17,6 +18,11 @@ type PlaceMapLeafletProps = { facilities: EnrichedFacility[]; }; +type PopupStatusBadge = { + label: string; + status: string; +}; + const markerIconCache: Record = {}; const getMarkerIcon = (status: string) => { @@ -32,6 +38,53 @@ const getMarkerIcon = (status: string) => { return markerIconCache[key]; }; +const STATUS_BADGE_CLASSES: Record = { + aapen: "bg-[#8BC34A] text-white", + aapen_med_vintergreener: "bg-[#D2A63A] text-[#112015]", + stenger_snart: "bg-[#FF5722] text-white", + aapner_snart: "bg-sky-600 text-white", + stengt: "bg-[#B6473D] text-white", + under_utvikling: "bg-slate-600 text-white", + nedlagt: "bg-[#112015] text-white", + ukjent: "bg-[#D9DED5] text-[#112015]", +}; + +const normalizeStatus = (value: unknown) => + String(value ?? "") + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/\s+/g, "_") + .replace(/[^a-z0-9_]+/g, "") + .trim() || "ukjent"; + +const buildPopupStatusBadges = (statuses: Array<{ name?: string; status?: string }>): PopupStatusBadge[] => { + const normalizedStatuses = (Array.isArray(statuses) ? statuses : []) + .map((status) => ({ + name: String(status?.name || "").trim(), + status: normalizeStatus(status?.status), + })) + .filter((status) => status.status); + + if (normalizedStatuses.length === 0) { + return [{ label: getStatusLabel("ukjent"), status: "ukjent" }]; + } + + const uniqueStatuses = [...new Set(normalizedStatuses.map((status) => status.status))]; + if (normalizedStatuses.length === 1 || uniqueStatuses.length === 1) { + const status = uniqueStatuses[0] || "ukjent"; + return [{ label: getStatusLabel(status), status }]; + } + + return normalizedStatuses.map((course, index) => { + const courseName = getPublicCourseDisplayName(course.name, index, normalizedStatuses.length); + return { + label: courseName ? `${courseName}: ${getStatusLabel(course.status)}` : getStatusLabel(course.status), + status: course.status, + }; + }); +}; + function ShiftScrollZoomGuard() { const map = useMap(); @@ -180,6 +233,7 @@ export default function PlaceMapLeaflet({ facilities }: PlaceMapLeafletProps) { const socialLinks = parseJson>(facility.social_links, []); const facebook = socialLinks.find((entry) => entry.platform?.toLowerCase() === "facebook")?.url; const instagram = socialLinks.find((entry) => entry.platform?.toLowerCase() === "instagram")?.url; + const statusBadges = buildPopupStatusBadges(facility.statuses); return (
-
- {getStatusLabel(facility.primaryStatus)} +
+ {statusBadges.map((badge) => ( +
+ {badge.label} +
+ ))}
{facility.status_updated_at && (