Nye-TeeOff/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx

753 lines
46 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<string, LeafletIcon> = {};
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('<a');
return (
<span
className={hasLink ? "text-[#ff5722] font-bold hover:underline" : "text-[#11280f]"}
dangerouslySetInnerHTML={{ __html: val }}
/>
);
};
const escapeHtml = (value: string) =>
value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
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<string, string>();
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("<br />"))
.replace(/<\s*(strong|b)\s*>/gi, () => keep("<strong>"))
.replace(/<\s*\/\s*(strong|b)\s*>/gi, () => keep("</strong>"))
.replace(/<\s*(em|i)\s*>/gi, () => keep("<em>"))
.replace(/<\s*\/\s*(em|i)\s*>/gi, () => keep("</em>"))
.replace(/<\s*p\s*>/gi, () => keep("<p>"))
.replace(/<\s*\/\s*p\s*>/gi, () => keep("</p>"))
.replace(/<\s*(ul|ol)\s*>/gi, (_, tag: string) => keep(`<${tag.toLowerCase()}>`))
.replace(/<\s*\/\s*(ul|ol)\s*>/gi, (_, tag: string) => keep(`</${tag.toLowerCase()}>`))
.replace(/<\s*li\s*>/gi, () => keep("<li>"))
.replace(/<\s*\/\s*li\s*>/gi, () => keep("</li>"))
.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(`<a href="${escapeHtml(href)}">`);
}
return keep(`<a href="${escapeHtml(href)}" target="_blank" rel="noreferrer noopener">`);
})
.replace(/<\s*\/\s*a\s*>/gi, () => keep("</a>"));
safe = escapeHtml(safe).replace(/\n/g, "<br />");
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 }) => (
<svg
className={`${className} flex-shrink-0 text-current`}
viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
style={{ width: '20px', height: '20px' }}
>
{children}
</svg>
);
const ICONS = {
web: <><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></>,
phone: <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />,
mail: <><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" /><polyline points="22,6 12,13 2,6" /></>,
pin: <><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" /><circle cx="12" cy="10" r="3" /></>,
booking: <><path d="M3 10h18M7 15h.01M11 15h.01M15 15h.01M7 19h.01M11 19h.01M15 19h.01M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2zM16 3v4M8 3v4"/></>,
trophy: <><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22M18 2H6v7a6 6 0 0 0 12 0V2z"/></>,
guide: <><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20M4 4.5A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1-2.5-2.5V4.5z"/></>,
camera: <><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></>,
webcam: <><path d="M23 7l-7 5 7 5V7z"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></>,
chart: <><path d="M18 20V10M12 20V4M6 20v-6"/></>,
weather: <><path d="M12 2v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="M20 12h2"/><path d="m19.07 4.93-1.41 1.41"/><path d="M15.947 12.65a4 4 0 0 0-5.925-4.128"/><path d="M13 22H7a5 5 0 1 1 4.9-6H13a3 3 0 0 1 0 6Z"/></>
};
const SOCIAL_ICONS: Record<string, React.ReactNode> = {
facebook: <path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z" />,
instagram: <><rect x="2" y="2" width="20" height="20" rx="5" ry="5" /><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" /><line x1="17.5" y1="6.5" x2="17.51" y2="6.5" /></>,
twitter: <path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z" />,
x: <path d="M4 4l16 16M4 20L20 4" />,
linkedin: <><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" /><rect x="2" y="9" width="4" height="12" /><circle cx="4" cy="4" r="2" /></>,
youtube: <><path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33 2.78 2.78 0 0 0 1.94 2c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.33 29 29 0 0 0-.46-5.33z" /><polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02" /></>,
tiktok: <path d="M9 12a4 4 0 1 0 4 4V2a5 5 0 0 0 5 5h-2a3 3 0 0 1-3-3V16a2 2 0 1 1-2-2v-2z" />,
snapchat: <path d="M12 2C8.5 2 6 5 6 8.5c0 1.5.5 3 1.5 4-1 .5-2.5 1-3.5 1-.5 0-1 .5-1 1s.5 1 1.5 1h15c1 0 1.5-.5 1.5-1s-.5-1-1-1c-1 0-2.5-.5-3.5-1 1-1 1.5-2.5 1.5-4C18 5 15.5 2 12 2zm0 15c-3 0-5-1-5-1s.5 1.5 1.5 2h7C16.5 17.5 17 16 17 16s-2 1-5 1z" />
};
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 (
<main className="min-h-screen bg-[#f1f7ed] pb-20 relative font-sans text-[#11280f]">
{/* 1. HERO SLIDER */}
<div className="h-[55vh] min-h-[450px] relative overflow-hidden bg-[#11280f]">
{gallery.map((img: string, i: number) => (
<img key={i} src={img} className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-1000 ${i === currentSlide ? 'opacity-100 z-10' : 'opacity-0 z-0'}`} alt="" />
))}
<div className="absolute inset-0 bg-gradient-to-t from-[#11280f]/90 via-transparent to-black/10 z-20" />
{/* BANESTATUS BADGES */}
<div className="absolute top-8 right-8 z-40 flex flex-col items-end gap-2">
<div className="flex flex-wrap justify-end gap-2">
{activeCourses.map((c: any) => (
<span key={c.id} className="px-3 py-1.5 rounded-lg text-[10px] font-black uppercase bg-[#7ca982] text-white shadow-xl">
{c.name.toUpperCase()}: {STATUS_MAP[c.status] || c.status}
</span>
))}
</div>
{facility.status_updated_at && (
<span className="text-white/60 text-[10px] uppercase font-black tracking-widest bg-black/20 px-2 py-1 rounded">
Sist oppdatert: {formatDate(facility.status_updated_at)}
</span>
)}
</div>
{/* FLYTENDE HURTIGKNAPPER */}
<div className="absolute bottom-8 right-8 z-40 flex gap-2.5 bg-black/30 backdrop-blur-md p-2 rounded-2xl border border-white/10 shadow-2xl text-[#11280f]">
{facility.website_url && <a href={facility.website_url} target="_blank" className="btn-icon"><Icon children={ICONS.web} /></a>}
{facility.golfbox_booking_url && <a href={facility.golfbox_booking_url} target="_blank" className="btn-icon"><Icon children={ICONS.booking} /></a>}
{facility.golfbox_tournament_url && <a href={facility.golfbox_tournament_url} target="_blank" className="btn-icon"><Icon children={ICONS.trophy} /></a>}
{mapUrl && <a href={mapUrl} target="_blank" rel="noreferrer" className="btn-icon"><Icon children={ICONS.pin} /></a>}
{facility.weather_url && <a href={facility.weather_url} target="_blank" className="btn-icon"><Icon children={ICONS.weather} /></a>}
</div>
{/* HERO TEXT */}
<div className="relative z-30 max-w-[1200px] mx-auto px-6 w-full h-full flex flex-col justify-end pb-32 md:pb-12">
{facility.logo_url && (
<div className="hidden md:block mb-8 w-24 h-24 bg-white p-2 rounded-2xl shadow-2xl border-4 border-white/20 overflow-hidden"><img src={facility.logo_url} className="w-full h-full object-contain" alt="Logo" /></div>
)}
<h1 className="text-5xl md:text-8xl font-black text-white mb-3 tracking-tighter drop-shadow-2xl">{facility.name}</h1>
<p className="text-[#7ca982] uppercase tracking-[0.4em] font-black text-xs md:text-sm pl-1">{facility.county} {facility.city}</p>
</div>
</div>
{/* 2. STICKY NAV */}
<nav className="sticky top-0 z-50 border-b border-gray-100 bg-white/95 shadow-sm backdrop-blur-md">
<div className="mx-auto max-w-[1200px]">
<div className="flex flex-wrap items-center justify-center gap-x-1.5 gap-y-0.5 px-2 py-2 md:hidden">
{mobileSectionNavItems.map((item, index) => (
<div key={item.id} className="flex items-center gap-1.5">
{index > 0 && <span className="text-[8px] text-gray-300">/</span>}
<button className={mobileSectionNavButtonClass} onClick={() => scrollTo(item.id)}>{item.label}</button>
</div>
))}
</div>
<div className="hidden items-center gap-2 overflow-x-auto px-3 py-3 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden md:flex md:h-16 md:gap-4 md:px-6 md:py-0 lg:h-20 lg:gap-6 xl:gap-8">
{sectionNavItems.map((item) => (
<button key={item.id} className={sectionNavButtonClass} onClick={() => scrollTo(item.id)}>{item.label}</button>
))}
</div>
</div>
</nav>
<div className="max-w-[1200px] mx-auto px-0 md:px-6 space-y-4 md:space-y-12 mt-0 md:mt-12">
{/* 3. INTRO & SIDEBAR */}
<section id="intro" className="flex flex-col lg:flex-row gap-0 md:gap-8 items-stretch">
{/* HOVEDINNHOLD (78%) */}
<div className="lg:w-[78%] bg-white p-10 md:p-16 md:rounded-[3rem] shadow-sm border-b md:border-none">
{facility.footnote && (
<div className="mb-8 pb-8 border-b border-gray-50">
<p className="mb-3 text-[11px] font-black uppercase tracking-[0.24em] text-[#C98B76]">
{formatDate(facility.footnote_updated_at || facility.status_updated_at)}
</p>
<div className="italic text-[#ff5722] text-lg font-serif">
{facility.footnote}
</div>
</div>
)}
<div
className="leading-relaxed text-lg md:text-xl text-gray-600 [&_a]:font-bold [&_a]:text-[#ff5722] [&_a]:underline-offset-2 hover:[&_a]:text-[#d53300] [&_em]:italic [&_ol]:my-5 [&_ol]:list-decimal [&_ol]:pl-6 [&_p]:mb-5 [&_p:last-child]:mb-0 [&_strong]:font-bold [&_ul]:my-5 [&_ul]:list-disc [&_ul]:pl-6"
dangerouslySetInnerHTML={{ __html: sanitizeRichText(facility.description || 'Ingen beskrivelse tilgjengelig.') }}
/>
</div>
{/* SIDEBAR (22%) */}
<div className="lg:w-[22%] bg-white p-10 md:rounded-[3rem] shadow-sm flex flex-col order-last lg:order-none">
<h3 className="text-[10px] font-black text-gray-300 uppercase tracking-widest mb-10">Kontakt & Adresse</h3>
<div className="flex-grow space-y-7 text-sm font-bold">
<a href={facility.website_url} target="_blank" className={sidebarLinkClass}><Icon children={ICONS.web} /> <span className={sidebarLinkTextClass}>Besøk nettsiden</span></a>
<a href={`tel:${formatPhoneForUrl(facility.phone)}`} className={sidebarLinkClass}>
<Icon children={ICONS.phone} /> <span className={sidebarLinkTextClass}>{facility.phone || 'Ikke oppgitt'}</span>
</a>
<a href={`mailto:${facility.email}`} className={sidebarLinkClass}>
<Icon children={ICONS.mail} /> <span className={`truncate ${sidebarLinkTextClass}`}>{facility.email || 'Ikke oppgitt'}</span>
</a>
<div className="pt-2 border-t border-gray-50 mt-4">
<a href={mapUrl || "#"} target="_blank" rel="noreferrer" className={sidebarLinkClass + " pt-4 leading-tight items-start"}>
<Icon children={ICONS.pin} /> <span className="text-gray-400 group-hover:text-[#ff5722] transition-colors">{facility.address}<br/>{facility.city}</span>
</a>
</div>
</div>
{/* SOSIALE MEDIER IKONER */}
{socialLinks.length > 0 && (
<div className="pt-6 border-t border-gray-50 mt-6 flex flex-wrap gap-3">
{socialLinks.map((social: any, idx: number) => {
const platform = (social.platform || '').toLowerCase().trim();
const iconData = SOCIAL_ICONS[platform] || <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3" />;
return (
<a key={idx} href={social.url} target="_blank" rel="noreferrer" title={social.platform} className="btn-icon h-10 w-10 rounded-full shadow-sm">
<Icon children={iconData} className="w-4 h-4 text-current" />
</a>
);
})}
</div>
)}
<div className="mt-10 pt-6 border-t border-gray-50">
<Link href={`/sted/${placeSlug}`} className={`${detailMetaLinkClass} flex items-center gap-1`}>
Se alle baner i {facility.county}
</Link>
</div>
</div>
</section>
{/* 4. 3-KOLONNE INFO */}
<section id="details" className="grid grid-cols-1 lg:grid-cols-3 gap-4 md:gap-8">
<div className="bg-white p-10 md:rounded-[3rem] shadow-sm">
<h3 className="text-lg font-black mb-8 uppercase tracking-tighter">Andre Ressurser</h3>
<div className="space-y-2.5">
{facility.golfbox_booking_url && (
<a href={facility.golfbox_booking_url} target="_blank" className={resourceBtnClass}>
<span className="flex items-center gap-3"><Icon children={ICONS.booking} className="group-hover:text-white" /> Book Starttid</span><span></span>
</a>
)}
{facility.golfbox_tournament_url && (
<a href={facility.golfbox_tournament_url} target="_blank" className={resourceBtnClass}>
<span className="flex items-center gap-3"><Icon children={ICONS.trophy} className="group-hover:text-white" /> Turneringer</span><span></span>
</a>
)}
{facility.baneguide_url && (
<a href={facility.baneguide_url} target="_blank" className={resourceBtnClass}>
<span className="flex items-center gap-3"><Icon children={ICONS.guide} className="group-hover:text-white" /> Baneguide</span><span></span>
</a>
)}
{facility.flyfoto_url && (
<a href={facility.flyfoto_url} target="_blank" className={resourceBtnClass}>
<span className="flex items-center gap-3"><Icon children={ICONS.camera} className="group-hover:text-white" /> Flyfoto</span><span></span>
</a>
)}
{facility.webcam_url && (
<a href={facility.webcam_url} target="_blank" className={resourceBtnClass}>
<span className="flex items-center gap-3"><Icon children={ICONS.webcam} className="group-hover:text-white" /> Webkamera</span><span></span>
</a>
)}
{shotzoom.map((sz: any, i: number) => (
<a key={i} href={sz.shotzoom_url} target="_blank" className={resourceBtnClass}>
<span className="flex items-center gap-3"><Icon children={ICONS.chart} className="group-hover:text-white" /> Statistikk: {sz.shotzoom_beskrivelse?.replace(/ ?/g, ' ').trim().toUpperCase()}</span><span></span>
</a>
))}
</div>
</div>
<div className="bg-white p-10 md:rounded-[3rem] shadow-sm text-sm font-bold text-gray-700 flex flex-col">
<h3 className="text-lg font-black mb-8 uppercase tracking-tighter text-[#11280f]">Banen</h3>
<div className="space-y-5 flex-grow">
<div className="flex justify-between border-b border-gray-50 pb-3"><span className="text-gray-400">Hull:</span><span>{amenities.antall_hull || '--'}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-3"><span className="text-gray-400">Lengde:</span><span>{facility.length_meters ? `${facility.length_meters}m` : '--'}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-3"><span className="text-gray-400">Sesong:</span><span>{facility.season || '--'}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-3"><span className="text-gray-400">Byggeår:</span><span>{facility.established_year || '--'}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-3"><span className="text-gray-400">Banetype:</span><span>{facility.banetype || 'Park/Skog'}</span></div>
<div className="flex justify-between"><span className="text-gray-400">Arkitekt:</span><span className="text-right truncate ml-4">{facility.architect || '--'}</span></div>
</div>
</div>
<div className="bg-white p-10 md:rounded-[3rem] shadow-sm text-sm font-bold text-gray-700">
<h3 className="text-lg font-black mb-8 uppercase tracking-tighter text-[#11280f]">Andre Tilbud</h3>
<div className="space-y-4">
<div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Drivingrange:</span><span>{amenities.drivingrange || 'Nei'}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Nærspill:</span><span>{amenities.treningsgreen || 'Ja'}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Proshop:</span><span className="text-right ml-4">{renderValue(amenities.proshop)}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Kølleutleie:</span><span>{amenities.kolleutleie || 'Ja'}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Bilutleie:</span><span>{amenities.bilutleie || 'Nei'}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Simulator:</span><span className="text-right ml-4">{renderValue(amenities.simulator)}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Kafé:</span><span className="text-right ml-4">{renderValue(amenities.kafe)}</span></div>
{/* Golfamore og NSG */}
<div className="flex justify-between border-b border-gray-50 pb-2">
<span className="text-gray-400">Golfamore:</span>
<span className="text-right ml-4">
{hasGolfamore ? <span className="text-[#ff5722] font-black">{golfamoreData.gyldighet || "Ja"}</span> : "Nei"}
</span>
</div>
<div className="flex justify-between border-b border-gray-50 pb-2">
<span className="text-gray-400">Seniorgolf (NSG):</span>
<span className="text-right ml-4">
{hasNSG && facility.nsg_url
? <a href={facility.nsg_url} target="_blank" className="font-black text-blue-600 transition-colors hover:text-[#ff5722] hover:underline">Ja (Vis Avtale)</a>
: (hasNSG ? <span className="text-blue-600 font-black">Ja</span> : "Nei")
}
</span>
</div>
{/* SAMARBEIDENDE KLUBBER */}
{cooperatingClubs.length > 0 && (
<div className="pt-2">
<span className="text-gray-400 block mb-2">Samarbeider med:</span>
<div className="flex flex-wrap gap-2">
{cooperatingClubs.map((slug: string) => (
<Link key={slug} href={`/golfbaner/${slug}`} className="btn btn-sm btn-secondary">
{slug.replace('-golfklubb', '').replace(/-/g, ' ')}
</Link>
))}
</div>
</div>
)}
{golfpakkerRaw.length > 0 && (
<div className="pt-4">
<span className="text-gray-400 block mb-2">Golfpakker:</span>
<div className="space-y-3">
{golfpakkerRaw.map((pakke: any, index: number) => (
<div key={index} className="rounded-2xl border border-gray-100 bg-white p-4">
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div>
<p className="font-black text-[#11280f]">{pakke.navn || 'Golfpakke'}</p>
{pakke.beskrivelse && <p className="mt-1 text-sm text-gray-500">{pakke.beskrivelse}</p>}
</div>
<div className="flex flex-col items-start gap-2 sm:items-end">
<span className="text-xs font-black uppercase tracking-widest text-[#7ca982]">
{pakke.pris ? `${pakke.pris},-` : 'Pris på forespørsel'}
</span>
{pakke.lenke && (
<a href={pakke.lenke} target="_blank" rel="noopener noreferrer" className="text-xs font-black uppercase tracking-widest text-blue-600 transition-colors hover:text-[#ff5722] hover:underline">
Les mer
</a>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</section>
{/* 5. VÆR SEKSJON */}
<section id="weather" className="bg-white p-0 md:p-12 md:rounded-[3rem] shadow-sm border-b md:border-none overflow-hidden text-center">
<h3 className="text-[10px] font-black text-gray-300 uppercase tracking-[0.2em] py-8 md:py-0 md:mb-10 flex items-center justify-center gap-3"><Icon children={ICONS.weather} /> Vær for {facility.name}</h3>
<div className="w-full flex justify-center px-4 md:px-0">
{facility.weather_url ? ( <img src={weatherImg} className="w-full h-auto block max-w-5xl" alt="Vær" /> ) : <p className="text-center py-24 text-gray-300 italic text-sm">Værvarsel ikke tilgjengelig</p>}
</div>
</section>
{/* 6. KART SEKSJON */}
{mapUrl && (
<section id="map" className="space-y-6">
<h2 className="text-3xl md:text-4xl font-black uppercase tracking-tighter flex items-center gap-5 ml-6 md:ml-0">Kart <span className="h-1 flex-grow bg-gray-100 rounded-full" /></h2>
<div className="teeoff-map overflow-hidden md:rounded-[3rem] border-y-4 md:border-[12px] border-white bg-white shadow-xl">
<div className="h-[450px] md:h-[650px] w-full">
<MapContainer
center={[facility.lat as number, facility.lng as number]}
zoom={13}
scrollWheelZoom={false}
zoomControl
className="h-full w-full"
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker
position={[facility.lat as number, facility.lng as number]}
icon={getDetailMarkerIcon(primaryStatus)}
>
<Popup>
<div className="space-y-3">
<div>
<p className="text-lg font-extrabold text-[#112015]">{facility.name}</p>
<p className="mt-1 text-[11px] font-extrabold uppercase tracking-[0.18em] text-[#617063]">
{facility.city} {facility.county}
</p>
</div>
<div className="inline-flex rounded-full bg-[#F3F6EE] px-3 py-1 text-[10px] font-extrabold uppercase tracking-[0.18em] text-[#112015]">
{STATUS_MAP[primaryStatus] || "Ukjent status"}
</div>
{mapUrl && (
<a
href={mapUrl}
target="_blank"
rel="noreferrer"
className="btn btn-sm btn-secondary"
>
Åpne kart
</a>
)}
</div>
</Popup>
</Marker>
</MapContainer>
</div>
</div>
</section>
)}
{/* 7. VIDEO SEKSJON */}
{facility.video_url && (
<section id="video" className="space-y-6">
<h2 className="text-3xl md:text-4xl font-black uppercase tracking-tighter flex items-center gap-5 ml-6 md:ml-0">Video <span className="h-1 flex-grow bg-gray-100 rounded-full" /></h2>
<div className="w-full md:rounded-[3rem] overflow-hidden shadow-2xl aspect-video bg-black border-y-4 md:border-[12px] border-white">
<iframe src={facility.video_url} className="w-full h-full" allowFullScreen />
</div>
</section>
)}
{/* 8. PRISER (MEDLEMSKAP & GREENFEE) */}
<section id="prices" className="pt-10">
<h2 className="text-3xl md:text-4xl font-black uppercase tracking-tighter flex items-center gap-5 ml-6 md:ml-0 mb-8">
Priser <span className="h-1 flex-grow bg-gray-100 rounded-full" />
</h2>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6 lg:gap-8 items-start">
{/* VENSTRE KOLONNE: MEDLEMSKAP */}
{(facility.standard_medlemskap || facility.rimeligste_alternativ) && (
<div className="bg-white border-2 border-[#11280f]/5 rounded-3xl p-6 lg:p-8 shadow-sm h-full flex flex-col">
<div className="flex justify-between items-center mb-6">
<h3 className="text-xl font-black text-[#11280f] uppercase tracking-tighter flex items-center gap-2">
<span></span> Medlemskap
</h3>
{facility.medlemskap_url && (
<a href={facility.medlemskap_url.split(',')[0].trim()} target="_blank" className={detailMetaLinkClass}>
Se alle
</a>
)}
</div>
<div className="space-y-6 flex-grow">
{facility.standard_medlemskap && (
<div className="bg-gray-50 rounded-2xl p-5 border border-gray-100 relative overflow-hidden group hover:border-[#8bc34a]/30 transition-colors">
<div className="absolute top-0 right-0 bg-[#8bc34a] text-white text-[9px] font-black uppercase tracking-widest px-3 py-1 rounded-bl-xl">Mest valgte</div>
<span className="block text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Standard</span>
<span className="text-3xl font-black text-[#11280f]">{facility.standard_medlemskap},-</span>
{facility.standard_medlemskap_navn && <p className="text-xs font-bold mt-1 text-[#8bc34a]">{facility.standard_medlemskap_navn}</p>}
{facility.standard_medlemskap_kommentarer && (
<p className="text-[10px] text-gray-500 mt-2 uppercase font-bold italic leading-tight border-t border-gray-200 pt-2">
{facility.standard_medlemskap_kommentarer.split('\n').map((line: string, i: number) => (
<span key={i}>{line}<br /></span>
))}
</p>
)}
</div>
)}
{facility.rimeligste_alternativ && (
<div className="bg-white rounded-2xl p-5 border border-gray-100 hover:border-[#8bc34a]/30 transition-colors">
<span className="block text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Rimeligste golfkort</span>
<span className="text-xl font-black text-gray-600">{facility.rimeligste_alternativ},-</span>
{facility.rimeligste_navn && <p className="text-[10px] font-bold mt-1 text-gray-500">{facility.rimeligste_navn}</p>}
</div>
)}
</div>
</div>
)}
{/* HØYRE KOLONNE: GREENFEE */}
{greenfeeRaw && greenfeeRaw.length > 0 && (
<div className="bg-white border-2 border-[#11280f]/5 rounded-3xl p-6 lg:p-8 shadow-sm h-full flex flex-col">
<div className="flex justify-between items-center mb-6">
<h3 className="text-xl font-black text-[#11280f] uppercase tracking-tighter flex items-center gap-2">
<span>🎫</span> Greenfee
</h3>
{facility.greenfee_url && (
<a href={facility.greenfee_url.split(',')[0].trim()} target="_blank" className={detailMetaLinkClass}>
Alle priser
</a>
)}
</div>
<div className="overflow-x-auto hide-scrollbar flex-grow">
<table className="w-full text-left min-w-[300px]">
<thead>
<tr className="border-b-2 border-gray-100 text-[10px] font-black uppercase tracking-widest text-gray-400">
<th className="pb-3 pr-2">Bane/Kat.</th>
<th className="pb-3 text-right pr-2">Voksen</th>
<th className="pb-3 text-right">Junior</th>
</tr>
</thead>
<tbody className="text-xs font-bold text-[#11280f]">
{greenfeeRaw.map((gf: any, idx: number) => (
<tr key={idx} className="border-b border-gray-50 hover:bg-gray-50 transition-colors">
<td className="py-3 pr-2 leading-tight">
<span className="block truncate max-w-[150px] sm:max-w-[200px]" title={gf.banenavn}>{gf.banenavn}</span>
<span className="block max-w-[150px] whitespace-normal break-words text-[9px] uppercase tracking-widest text-gray-400 sm:max-w-[200px]" title={gf.priskategori}>{gf.priskategori}</span>
</td>
<td className="py-3 text-right pr-2 text-[#8bc34a] font-black text-sm whitespace-nowrap">
{gf.pris_voksne ? `${gf.pris_voksne},-` : '-'}
</td>
<td className="py-3 text-right text-blue-400 whitespace-nowrap">
{gf.pris_junior ? `${gf.pris_junior},-` : '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
{facility.guest_requirements && (
<p className="mt-4 text-[10px] text-gray-400 font-black uppercase tracking-widest italic pt-4 border-t border-gray-50">
Krav: {facility.guest_requirements}
</p>
)}
</div>
)}
</div>
</section>
{hasVtg && (
<section id="vtg" className="pt-10 space-y-6">
<h2 className="text-3xl md:text-4xl font-black uppercase tracking-tighter flex items-center gap-5 ml-6 md:ml-0">
Veien til Golf <span className="h-1 flex-grow bg-gray-100 rounded-full" />
</h2>
<div className="bg-[#8bc34a] text-white rounded-3xl p-6 lg:p-10 shadow-lg relative overflow-hidden group">
<div className="absolute -right-20 -top-20 opacity-10 text-[200px] pointer-events-none transform group-hover:scale-110 transition-transform duration-700">🏌</div>
<div className="relative z-10">
<div className="mb-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<h3 className="text-2xl font-black uppercase tracking-tighter flex items-center gap-3">
Nybegynnerkurs hos {facility.name}
</h3>
<Link
href="/vtg"
className="inline-flex items-center text-[10px] font-black uppercase tracking-[0.18em] text-white/80 transition hover:text-white"
>
Se alle VTG-kurs
</Link>
</div>
{facility.vtg_beskrivelse && (
<p className="text-sm md:text-base text-white/90 mb-8 leading-relaxed font-medium max-w-4xl">
{facility.vtg_beskrivelse}
</p>
)}
<div className="flex flex-col lg:flex-row gap-6 items-start lg:items-center justify-between bg-white/10 rounded-2xl p-6 backdrop-blur-sm border border-white/20">
{facility.vtg_pris && (
<div className="flex-shrink-0">
<span className="block text-[10px] font-black uppercase tracking-widest text-white/70 mb-1">Standard voksenpris</span>
<span className="text-4xl font-black">{facility.vtg_pris},-</span>
</div>
)}
{vtgDatoer && vtgDatoer.length > 0 && (
<div className="flex-grow w-full lg:w-auto lg:px-6">
<h4 className="text-[10px] font-black uppercase tracking-widest text-white/70 mb-3">Kommende kurs:</h4>
<div className="flex flex-wrap gap-2">
{vtgDatoer.map((kurs: any, i: number) => {
const status = (kurs.status || '').toLowerCase();
const isFull = status.includes('full');
const isWaitlist = status.includes('vente') || status.includes('få');
let badgeColor = "bg-white/20 text-white";
if (isFull) badgeColor = "bg-red-500/80 text-white line-through opacity-75";
if (isWaitlist) badgeColor = "bg-yellow-400 text-[#11280f]";
return (
<div key={i} className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-bold border border-white/10 ${badgeColor}`}>
<span>{kurs.dato}</span>
<span className="text-[8px] uppercase tracking-widest opacity-80 border-l border-white/20 pl-2 ml-1">{kurs.status}</span>
</div>
);
})}
</div>
</div>
)}
{facility.vtg_lenke && (
<a href={facility.vtg_lenke.split(',')[0].trim()} target="_blank" rel="noopener noreferrer" className="btn btn-lg btn-primary mt-4 w-full flex-shrink-0 text-center lg:mt-0 lg:w-auto">
til klubben
</a>
)}
</div>
</div>
</div>
</section>
)}
{/* 9. SCOREKORT SEKSJON */}
<section id="scorecards" className="pt-10 space-y-20 overflow-hidden">
<h3 className="text-center text-3xl md:text-5xl font-black uppercase tracking-tighter">Scorekort</h3>
<div className="w-full flex flex-col items-center gap-20">
{activeCourses.map((c: any) => (
<div key={c.id} className="w-full overflow-x-auto md:overflow-visible no-scrollbar">
<div className="min-w-[800px] md:min-w-0"><CourseDisplay course={c} /></div>
</div>
))}
</div>
</section>
</div>
{showBackToTop && (
<button onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} className="btn btn-ink fixed bottom-8 right-8 z-[100] flex h-14 w-14 items-center justify-center rounded-full border-4 border-white/20 p-0 text-2xl shadow-2xl"></button>
)}
</main>
);
}