"use client"; /** * TEE OFF DETAIL VIEW - COMPLETE v3.4 (FINAL LAYOUT FIX) * --------------------------------------------------------------------------- * FIX: Gjenopprettet "Turneringer" i den flytende knapperaden over bildet. * FIX: Byttet plass på tekst og sidebar (Tekst øverst på mobil). * FIX: Økt padding (pb-32) i Hero-teksten på mobil for å unngå krasj med knapper. * FIX: Alle 4 kontaktpunkter i sidebar er klikkbare (tel:0047 fix inkludert). * NEW: Sosiale Medier, Footnote og Samarbeidende klubber integrert. * NEW: Priser (Medlemskap + Greenfee) i 2-kolonne Grid (xl:grid-cols-2). * NEW: Veien til Golf (VTG) i full bredde under prisene, med robust array-parsing. * REGEL: Beholder monokrome ikoner, 22/78 layout og robust JSON-parsing. * --------------------------------------------------------------------------- */ import { useState, useEffect } from 'react'; import { Icon as LeafletIcon } from "leaflet"; import { MapContainer, Marker, Popup, TileLayer } from "react-leaflet"; import { STATUS_MAP, FALLBACK_IMAGE } from "@/config/constants"; import { STATUS_ICON_PATHS, buildMapUrl, getPrimaryStatus, parseJson as parseSharedJson, slugify } from "@/app/facilityData"; import Link from 'next/link'; import CourseDisplay from './CourseDisplay'; const detailMarkerIconCache: Record = {}; const getDetailMarkerIcon = (status: string) => { const key = STATUS_ICON_PATHS[status] ? status : "ukjent"; if (!detailMarkerIconCache[key]) { detailMarkerIconCache[key] = new LeafletIcon({ iconUrl: STATUS_ICON_PATHS[key], iconSize: [34, 48], iconAnchor: [17, 48], popupAnchor: [0, -42], }); } return detailMarkerIconCache[key]; }; const formatPhoneForUrl = (phone: string) => { if (!phone) return ""; return phone.replace('+', '00').replace(/\s/g, ''); }; const renderValue = (val: string) => { if (!val) return "Nei"; const hasLink = val.includes(' ); }; const escapeHtml = (value: string) => value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); const sanitizeHref = (value: string) => { const href = value.trim(); return /^(https?:|mailto:|tel:|\/|#)/i.test(href) ? href : "#"; }; const isInternalTeeoffHref = (href: string) => { if (!href || href.startsWith("/") || href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:")) { return true; } try { const url = new URL(href, "https://nye.teeoff.no"); return url.hostname === "teeoff.no" || url.hostname.endsWith(".teeoff.no"); } catch { return false; } }; const sanitizeRichText = (value: string | null | undefined) => { const source = String(value || "").replace(/\r\n?/g, "\n"); if (!source.trim()) return ""; const placeholders = new Map(); let index = 0; const keep = (html: string) => { const key = `__HTML_TOKEN_${index++}__`; placeholders.set(key, html); return key; }; let safe = source .replace(/<\s*br\s*\/?\s*>/gi, () => keep("
")) .replace(/<\s*(strong|b)\s*>/gi, () => keep("")) .replace(/<\s*\/\s*(strong|b)\s*>/gi, () => keep("")) .replace(/<\s*(em|i)\s*>/gi, () => keep("")) .replace(/<\s*\/\s*(em|i)\s*>/gi, () => keep("")) .replace(/<\s*p\s*>/gi, () => keep("

")) .replace(/<\s*\/\s*p\s*>/gi, () => keep("

")) .replace(/<\s*(ul|ol)\s*>/gi, (_, tag: string) => keep(`<${tag.toLowerCase()}>`)) .replace(/<\s*\/\s*(ul|ol)\s*>/gi, (_, tag: string) => keep(``)) .replace(/<\s*li\s*>/gi, () => keep("
  • ")) .replace(/<\s*\/\s*li\s*>/gi, () => keep("
  • ")) .replace(/<\s*a\b([^>]*)>/gi, (_, attrs: string) => { const hrefMatch = attrs.match(/href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i); const href = sanitizeHref(hrefMatch?.[1] || hrefMatch?.[2] || hrefMatch?.[3] || "#"); if (isInternalTeeoffHref(href)) { return keep(``); } return keep(``); }) .replace(/<\s*\/\s*a\s*>/gi, () => keep("")); safe = escapeHtml(safe).replace(/\n/g, "
    "); for (const [token, html] of placeholders) { safe = safe.replaceAll(token, html); } return safe; }; const Icon = ({ children, className = "w-5 h-5" }: { children: React.ReactNode, className?: string }) => ( {children} ); const ICONS = { web: <>, phone: , mail: <>, pin: <>, booking: <>, trophy: <>, guide: <>, camera: <>, webcam: <>, chart: <>, weather: <> }; const SOCIAL_ICONS: Record = { facebook: , instagram: <>, twitter: , x: , linkedin: <>, youtube: <>, tiktok: , snapchat: }; export default function FacilityDetailView({ facility }: { facility: any }) { const [showBackToTop, setShowBackToTop] = useState(false); const [currentSlide, setCurrentSlide] = useState(0); const parseJson = (val: any, fallback: any) => { return parseSharedJson(val, fallback); }; const rawCourses = parseJson(facility.courses, []); const activeCourses = Array.isArray(rawCourses) ? rawCourses.filter((c: any) => c.holes && (typeof c.holes === 'string' || c.holes.length > 0)) : []; const amenities = parseJson(facility.amenities, {}); const galleryRaw = parseJson(facility.gallery, []); const gallery = galleryRaw.length > 0 ? galleryRaw : [facility.image_url || FALLBACK_IMAGE]; const shotzoom = parseJson(facility.shotzoom, []); // Pris og kurs-arrays const greenfeeRaw = parseJson(facility.greenfee, []); const golfpakkerRaw = parseJson(facility.golfpakker, []); const vtgDatoer = parseJson(facility.vtg_datoer, []); const golfamoreData = parseJson(facility.golfamore_data, {}); const nsgData = parseJson(facility.nsg_data, {}); const socialLinksRaw = parseJson(facility.social_links, []); const socialLinks = Array.isArray(socialLinksRaw) ? socialLinksRaw : []; const coopClubsRaw = parseJson(facility.cooperating_clubs, []); const cooperatingClubs = Array.isArray(coopClubsRaw) ? coopClubsRaw : []; const hasGolfamore = facility.golfamore === true; const hasNSG = facility.nsg_url || (nsgData && Object.keys(nsgData).length > 0); const hasVtg = Boolean( facility.vtg_pris || facility.vtg_beskrivelse || facility.vtg_lenke || (Array.isArray(vtgDatoer) && vtgDatoer.length > 0) ); const mapUrl = buildMapUrl(facility.lat, facility.lng); const primaryStatus = getPrimaryStatus( activeCourses.map((course: any) => ({ status: course.status })) ); const sidebarLinkClass = "group flex items-center gap-4 text-[#11280f] transition-colors hover:text-[#ff5722]"; const sidebarLinkTextClass = "transition-colors group-hover:text-[#ff5722]"; const detailMetaLinkClass = "text-[10px] font-black uppercase tracking-widest text-[#7ca982] transition-colors hover:text-[#ff5722]"; const resourceBtnClass = "btn-panel group w-full p-5"; const sectionNavButtonClass = "btn btn-secondary h-10 shrink-0 whitespace-nowrap px-3 text-[9px] tracking-[0.12em] md:h-auto md:px-4 md:text-[11px] md:tracking-[0.16em]"; const mobileSectionNavButtonClass = "whitespace-nowrap text-[8px] font-black uppercase tracking-[0.04em] text-gray-500 transition-colors hover:text-[#11280f]"; const placeSlug = slugify(facility.county || "") || "norge"; const sectionNavItems = [ { id: 'intro', label: 'Info', showOnMobile: false }, { id: 'weather', label: 'Vær', showOnMobile: false }, { id: 'details', label: 'Detaljer', showOnMobile: true }, mapUrl ? { id: 'map', label: 'Kart', showOnMobile: true } : null, facility.video_url ? { id: 'video', label: 'Video', showOnMobile: true } : null, { id: 'prices', label: 'Priser', showOnMobile: true }, hasVtg ? { id: 'vtg', label: 'VTG', showOnMobile: true } : null, { id: 'scorecards', label: 'Scorekort', showOnMobile: true }, ].filter( (item): item is { id: string; label: string; showOnMobile: boolean } => Boolean(item) ); const mobileSectionNavItems = sectionNavItems.filter((item) => item.showOnMobile); useEffect(() => { if (gallery.length <= 1) return; const timer = setInterval(() => setCurrentSlide((p) => (p + 1) % gallery.length), 5000); return () => clearInterval(timer); }, [gallery.length]); useEffect(() => { const handleScroll = () => setShowBackToTop(window.scrollY > 500); window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, []); const scrollTo = (id: string) => { const el = document.getElementById(id); if (el) window.scrollTo({ top: el.getBoundingClientRect().top + window.pageYOffset - 80, behavior: 'smooth' }); }; const formatDate = (d: string) => d ? new Date(d).toLocaleDateString('nb-NO', { day: 'numeric', month: 'long', year: 'numeric' }) : null; const weatherImg = facility.weather_url?.replace("/graf/dag/", "/innhold/").replace(/\/$/, "") + "/meteogram.svg"; return (
    {/* 1. HERO SLIDER */}
    {gallery.map((img: string, i: number) => ( ))}
    {/* BANESTATUS BADGES */}
    {activeCourses.map((c: any) => ( {c.name.toUpperCase()}: {STATUS_MAP[c.status] || c.status} ))}
    {facility.status_updated_at && ( Sist oppdatert: {formatDate(facility.status_updated_at)} )}
    {/* FLYTENDE HURTIGKNAPPER */}
    {facility.website_url && } {facility.golfbox_booking_url && } {facility.golfbox_tournament_url && } {mapUrl && } {facility.weather_url && }
    {/* HERO TEXT */}
    {facility.logo_url && (
    Logo
    )}

    {facility.name}

    {facility.county} • {facility.city}

    {/* 2. STICKY NAV */}
    {/* 3. INTRO & SIDEBAR */}
    {/* HOVEDINNHOLD (78%) */}
    {facility.footnote && (

    {formatDate(facility.footnote_updated_at || facility.status_updated_at)}

    {facility.footnote}
    )}
    {/* SIDEBAR (22%) */}

    Kontakt & Adresse

    {/* SOSIALE MEDIER IKONER */} {socialLinks.length > 0 && (
    {socialLinks.map((social: any, idx: number) => { const platform = (social.platform || '').toLowerCase().trim(); const iconData = SOCIAL_ICONS[platform] || ; return ( ); })}
    )}
    Se alle baner i {facility.county} →
    {/* 4. 3-KOLONNE INFO */}

    Andre Ressurser

    {facility.golfbox_booking_url && ( Book Starttid )} {facility.golfbox_tournament_url && ( Turneringer )} {facility.baneguide_url && ( Baneguide )} {facility.flyfoto_url && ( Flyfoto )} {facility.webcam_url && ( Webkamera )} {shotzoom.map((sz: any, i: number) => ( Statistikk: {sz.shotzoom_beskrivelse?.replace(/ ?/g, ' ').trim().toUpperCase()} ))}

    Banen

    Hull:{amenities.antall_hull || '--'}
    Lengde:{facility.length_meters ? `${facility.length_meters}m` : '--'}
    Sesong:{facility.season || '--'}
    Byggeår:{facility.established_year || '--'}
    Banetype:{facility.banetype || 'Park/Skog'}
    Arkitekt:{facility.architect || '--'}

    Andre Tilbud

    Drivingrange:{amenities.drivingrange || 'Nei'}
    Nærspill:{amenities.treningsgreen || 'Ja'}
    Proshop:{renderValue(amenities.proshop)}
    Kølleutleie:{amenities.kolleutleie || 'Ja'}
    Bilutleie:{amenities.bilutleie || 'Nei'}
    Simulator:{renderValue(amenities.simulator)}
    Kafé:{renderValue(amenities.kafe)}
    {/* Golfamore og NSG */}
    Golfamore: {hasGolfamore ? {golfamoreData.gyldighet || "Ja"} : "Nei"}
    Seniorgolf (NSG): {hasNSG && facility.nsg_url ? Ja (Vis Avtale) : (hasNSG ? Ja : "Nei") }
    {/* SAMARBEIDENDE KLUBBER */} {cooperatingClubs.length > 0 && (
    Samarbeider med:
    {cooperatingClubs.map((slug: string) => ( {slug.replace('-golfklubb', '').replace(/-/g, ' ')} ))}
    )} {golfpakkerRaw.length > 0 && (
    Golfpakker:
    {golfpakkerRaw.map((pakke: any, index: number) => (

    {pakke.navn || 'Golfpakke'}

    {pakke.beskrivelse &&

    {pakke.beskrivelse}

    }
    {pakke.pris ? `${pakke.pris},-` : 'Pris på forespørsel'} {pakke.lenke && ( Les mer ↗ )}
    ))}
    )}
    {/* 5. VÆR SEKSJON */}

    Vær for {facility.name}

    {facility.weather_url ? ( Vær ) :

    Værvarsel ikke tilgjengelig

    }
    {/* 6. KART SEKSJON */} {mapUrl && (

    Kart

    {facility.name}

    {facility.city} • {facility.county}

    {STATUS_MAP[primaryStatus] || "Ukjent status"}
    {mapUrl && ( Åpne kart )}
    )} {/* 7. VIDEO SEKSJON */} {facility.video_url && (

    Video