Mellomlagring

This commit is contained in:
Erol 2026-03-06 13:39:11 +01:00
parent 1d8ce4c52d
commit 4713eced56
6 changed files with 3566 additions and 55 deletions

View file

@ -0,0 +1,150 @@
import asyncio
import asyncpg
import requests
import json
import os
import re
from datetime import datetime
from dotenv import load_dotenv
# Laster miljøvariabler
load_dotenv()
DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff")
# Grunn-URL uten page-parameter
WP_API_BASE_URL = "https://teeoff.no/wp-json/wp/v2/golfbaner?per_page=100"
def extract_price(text):
"""Finner første hele tall i en tekst og returnerer det som integer."""
if not text:
return None
clean_text = str(text).replace(" ", "").replace(".", "")
match = re.search(r'\d+', clean_text)
if match:
return int(match.group())
return None
def parse_date(date_string):
"""Forsøker å konvertere ulike tekstformater for dato til et ekte Date-objekt."""
if not date_string:
return None
ds = str(date_string).strip().lower()
if ds in ["ukjent", "ikke oppgitt", "har ikke", ""]:
return None
formats = ['%Y-%m-%d', '%d.%m.%Y', '%d/%m/%Y', '%Y%m%d', '%d.%m.%y']
for fmt in formats:
try:
return datetime.strptime(ds, fmt).date()
except ValueError:
continue
return None
def clean_jsonb(value):
"""Sørger for at vi ikke fyller databasen med 'Ikke oppgitt', men bruker tomme lister."""
if not value or str(value).lower() in ["ikke oppgitt", "har ikke / ikke oppgitt"]:
return []
if isinstance(value, str):
return [{"beskrivelse": value}]
if isinstance(value, list):
cleaned = [v for v in value if v and "ikke oppgitt" not in str(v).lower()]
return cleaned
return value
async def run_import():
print("📡 Henter anleggsdata fra WordPress (inkluderer paginering)...")
all_data = []
page = 1
# --- LØKKE SOM HENTER ALLE SIDER FRA WORDPRESS ---
while True:
url = f"{WP_API_BASE_URL}&page={page}"
print(f" -> Henter side {page}...")
response = requests.get(url)
# Hvis vi får 400 Bad Request, betyr det at vi har nådd forbi siste side
if response.status_code != 200:
break
data = response.json()
if not data:
break
all_data.extend(data)
page += 1
print(f"✅ Fant totalt {len(all_data)} anlegg. Starter oppdatering av database...")
conn = await asyncpg.connect(DB_URL)
success_count = 0
for item in all_data:
slug = item.get('slug')
acf = item.get('acf', {})
# Ekstraher og vask verdiene
golfpakker = clean_jsonb(acf.get('golfpakke'))
rabattert_greenfee = clean_jsonb(acf.get('rabattert_greenfee'))
vtg_presentasjon = acf.get('vtg_presentasjon') or None
vtg_lenke = acf.get('lenke_til_kurssider') or None
vtg_pris = extract_price(acf.get('vtg_pris'))
vtg_kursdatoer = clean_jsonb(acf.get('kursdatoer'))
slope_hovedbane = parse_date(acf.get('gyldig_til_og_med'))
slope_bane_to = parse_date(acf.get('gyldig_til_og_med_bane_to'))
try:
# 1. Oppdater fasilitets-tabellen
await conn.execute("""
UPDATE facilities
SET
golfpakker = $1::jsonb,
rabattert_greenfee = $2::jsonb,
vtg_presentasjon = $3,
vtg_lenke = $4,
vtg_pris = $5,
vtg_kursdatoer = $6::jsonb
WHERE slug = $7
""",
json.dumps(golfpakker),
json.dumps(rabattert_greenfee),
vtg_presentasjon,
vtg_lenke,
vtg_pris,
json.dumps(vtg_kursdatoer),
slug)
# 2. Oppdater utløpsdato på hovedbanen
if slope_hovedbane:
await conn.execute("""
UPDATE courses
SET slope_valid_until = $1
WHERE facility_id = (SELECT id FROM facilities WHERE slug = $2)
AND is_main_course = true
""", slope_hovedbane, slug)
# 3. Oppdater utløpsdato på bane 2
if slope_bane_to:
await conn.execute("""
UPDATE courses
SET slope_valid_until = $1
WHERE facility_id = (SELECT id FROM facilities WHERE slug = $2)
AND is_main_course = false
""", slope_bane_to, slug)
success_count += 1
except Exception as e:
print(f" ❌ Feil ved oppdatering av {slug}: {e}")
await conn.close()
print(f"\n🎉 Kjøring fullført! Målrettet import for {success_count} anlegg er lagret.")
if __name__ == "__main__":
asyncio.run(run_import())

View file

@ -1,20 +1,20 @@
"use client"; "use client";
/** /**
* TEE OFF SYSTEM INSTRUCTIONS - FACILITY CARDS v3.6 (STABLE) * TEE OFF SYSTEM INSTRUCTIONS - FACILITY CARDS v3.8 (BLOB SEARCH)
* --------------------------------------------------------------------------- * ---------------------------------------------------------------------------
* REGEL 1: Status-badge SKAL vises øverst til venstre FOR ALLE BANER. * REGEL 1: Status-badge SKAL vises øverst til venstre FOR ALLE BANER.
* Bruk STATUS_MAP for tekst. * Bruk STATUS_MAP for tekst.
* REGEL 2: DATA-PARSING: Bruk parseJson() for 'course_statuses', 'amenities' og 'nsg_data' * REGEL 2: DATA-PARSING: Bruk parseJson() for 'course_statuses', 'amenities' og 'nsg_data'.
* fordi API-et ofte returnerer disse som strenger.
* REGEL 3: Avstand-pillen skal ha fargen #2d3319 (Mørk oliven) med hvit tekst. * REGEL 3: Avstand-pillen skal ha fargen #2d3319 (Mørk oliven) med hvit tekst.
* REGEL 4: NSG (Blå 'N') og Golfamore (Oransje 'G') sirkler skal ha hvit kant (border-2). * REGEL 4: NSG (Blå 'N') og Golfamore (Oransje 'G') sirkler skal ha hvit kant (border-2).
* REGEL 5: Bunnen: Antall Hull (grønn pill), Banetype (grå pill), og Ikon-sirkler. * REGEL 5: Bunnen: Antall Hull (grønn pill), Banetype (grå pill), og Ikon-sirkler.
* REGEL 6: Viser dato (f.eks "05. mars 2026") rett til høyre for øverste status-pille. * REGEL 6: Viser dato (f.eks "05. mars 2026") rett til høyre for øverste status-pille.
* Datoen har en svak bakgrunnsfarge for å sikre lesbarhet lys bakgrunn. * REGEL 7: Natural Language Search bruker en "Search Blob" for å støtte delvise
* ord og skrivefeil slik at listen ikke tømmes mens brukeren skriver.
* --------------------------------------------------------------------------- * ---------------------------------------------------------------------------
*/ */
import { STATUS_MAP } from "@/config/constants"; import { STATUS_MAP, REGIONS } from "@/config/constants";
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo } from 'react';
import Link from 'next/link'; import Link from 'next/link';
@ -45,15 +45,22 @@ export default function FacilitySearch({ initialFacilities }: { initialFacilitie
const processed = useMemo(() => { const processed = useMemo(() => {
if (!Array.isArray(initialFacilities)) return []; if (!Array.isArray(initialFacilities)) return [];
// Fyllord som fjernes slik at "Åpne baner i Oslo" blir til søkeordene ["åpne", "oslo"]
const stopWords = new Set(["i", "på", "for", "med", "av", "og"]);
return initialFacilities.map(f => { return initialFacilities.map(f => {
// --- ROBUST DATA-PARSING (Håndterer tekst vs objekt fra API) --- // --- ROBUST DATA-PARSING ---
const parseJson = (val: any, fallback: any) => { const parseJson = (val: any, fallback: any) => {
if (!val) return fallback; if (!val) return fallback;
if (typeof val === 'object') return val; if (typeof val === 'object') return val;
try { return JSON.parse(val); } catch (e) { return fallback; } try { return JSON.parse(val); } catch (e) { return fallback; }
}; };
const statuses = parseJson(f.course_statuses, []); const rawStatuses = parseJson(f.course_statuses, []);
const sArr = Array.isArray(rawStatuses) && rawStatuses.length > 0
? rawStatuses
: [{ status: 'ukjent', name: 'Hovedbane' }];
const amenities = parseJson(f.amenities, {}); const amenities = parseJson(f.amenities, {});
const nsgData = parseJson(f.nsg_data, {}); const nsgData = parseJson(f.nsg_data, {});
@ -61,11 +68,44 @@ export default function FacilitySearch({ initialFacilities }: { initialFacilitie
const hasNSG = nsgData && Object.keys(nsgData).length > 0; const hasNSG = nsgData && Object.keys(nsgData).length > 0;
const hasGolfamore = f.golfamore === true; const hasGolfamore = f.golfamore === true;
const words = searchQuery.toLowerCase().trim().split(/\s+/).filter(w => w.length > 0); // --- THE SEARCH BLOB ---
const blob = `${f.name} ${f.city} ${f.county}`.toLowerCase(); // Vi starter med å legge navn, by og fylke i en stor, usynlig tekststreng
const matches = words.every(w => blob.includes(w)); let searchableText = `${f.name} ${f.city} ${f.county}`.toLowerCase();
return { ...f, statuses, amenities, dist, hasNSG, hasGolfamore, matches }; // 1. Injiser statuser i tekststrengen
const hasOpen = sArr.some((c: any) => (c.status || "") === 'aapen');
const hasClosed = sArr.some((c: any) => (c.status || "") === 'stengt');
const hasWinter = sArr.some((c: any) => (c.status || "") === 'aapen_med_vintergreener');
const hasNedlagt = sArr.some((c: any) => (c.status || "") === 'nedlagt');
if (hasOpen) searchableText += " åpen åpne aapen";
if (hasClosed) searchableText += " stengt stengte";
if (hasWinter) searchableText += " vinter vintergreener vinterbane";
if (hasNedlagt) searchableText += " nedlagt nedlagte";
// 2. Injiser spesial-tags
if (hasNSG) searchableText += " nsg norsk seniorgolf";
if (hasGolfamore) searchableText += " golfamore amore";
// 3. Injiser landsdel (f.eks. hvis fylket er Akershus, legger vi til "østlandet")
const fylke = (f.county || "").toLowerCase();
Object.entries(REGIONS).forEach(([regionName, counties]) => {
if (counties.includes(fylke)) {
searchableText += ` ${regionName}`;
}
});
// Splitter brukerens søk inn i enkeltord og fjerner stopWords + ordene "bane"/"baner"
const words = searchQuery
.toLowerCase()
.trim()
.split(/\s+/)
.filter(w => w.length > 0 && !stopWords.has(w) && w !== "bane" && w !== "baner");
// Sjekker at ALLE ordene brukeren har skrevet, finnes et sted i "Search Blob"-en
const matches = words.every(w => searchableText.includes(w));
return { ...f, statuses: sArr, amenities, dist, hasNSG, hasGolfamore, matches };
}) })
.filter(f => f.matches) .filter(f => f.matches)
.sort((a, b) => { .sort((a, b) => {
@ -82,12 +122,11 @@ export default function FacilitySearch({ initialFacilities }: { initialFacilitie
</button> </button>
</div> </div>
<input className="w-full p-8 rounded-[2.5rem] shadow-2xl mb-16 text-gray-900 border-none ring-1 ring-black/5 text-2xl outline-none focus:ring-4 focus:ring-[#8bc34a]/20 transition-all bg-white" placeholder='Søk baner...' value={searchQuery} onChange={e => setSearchQuery(e.target.value)} /> <input className="w-full p-8 rounded-[2.5rem] shadow-2xl mb-16 text-gray-900 border-none ring-1 ring-black/5 text-2xl outline-none focus:ring-4 focus:ring-[#8bc34a]/20 transition-all bg-white" placeholder='Søk baner, fylke, status eller spesial (f.eks "Åpne baner i Akershus" eller "NSG")...' value={searchQuery} onChange={e => setSearchQuery(e.target.value)} />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
{processed.map((f: any) => { {processed.map((f: any) => {
// Sikrer at vi har en gyldig array for banestatuser const sArr = f.statuses; // Sikret via pre-prosesseringen over
const sArr = Array.isArray(f.statuses) && f.statuses.length > 0 ? f.statuses : [{ status: 'ukjent', name: 'Hovedbane' }];
// Formater datoen pent: "05. mars 2026" // Formater datoen pent: "05. mars 2026"
const lastUpdated = f.status_updated_at const lastUpdated = f.status_updated_at
@ -134,7 +173,7 @@ export default function FacilitySearch({ initialFacilities }: { initialFacilitie
})} })}
</div> </div>
{/* Avstandspille (Mørk oliven #2d3319) */} {/* Avstandspille */}
{f.dist !== Infinity && ( {f.dist !== Infinity && (
<div className="absolute bottom-5 right-5 bg-[#2d3319] text-white px-4 py-2 rounded-2xl text-[10px] font-black shadow-lg z-20"> <div className="absolute bottom-5 right-5 bg-[#2d3319] text-white px-4 py-2 rounded-2xl text-[10px] font-black shadow-lg z-20">
{Math.round(f.dist)} km unna {Math.round(f.dist)} km unna
@ -167,7 +206,7 @@ export default function FacilitySearch({ initialFacilities }: { initialFacilitie
<div className="w-9 h-9 bg-[#ff5722] text-white rounded-full flex items-center justify-center font-black text-sm shadow-lg border-2 border-white translate-y-1">G</div> <div className="w-9 h-9 bg-[#ff5722] text-white rounded-full flex items-center justify-center font-black text-sm shadow-lg border-2 border-white translate-y-1">G</div>
)} )}
</div> </div>
</div> </div>
</div> </div>
</Link> </Link>
); );

View file

@ -1,11 +1,12 @@
"use client"; "use client";
/** /**
* TEE OFF DETAIL VIEW - COMPLETE v3.20 * TEE OFF DETAIL VIEW - COMPLETE v3.21
* --------------------------------------------------------------------------- * ---------------------------------------------------------------------------
* FIX: Gjenopprettet "Turneringer" i den flytende knapperaden over bildet. * FIX: Gjenopprettet "Turneringer" i den flytende knapperaden over bildet.
* FIX: Byttet plass tekst og sidebar (Tekst øverst mobil). * FIX: Byttet plass tekst og sidebar (Tekst øverst mobil).
* FIX: Økt padding (pb-32) i Hero-teksten mobil for å unngå krasj med knapper. * FIX: Økt padding (pb-32) i Hero-teksten mobil for å unngå krasj med knapper.
* FIX: Alle 4 kontaktpunkter i sidebar er klikkbare (tel:0047 fix inkludert). * FIX: Alle 4 kontaktpunkter i sidebar er klikkbare (tel:0047 fix inkludert).
* NEW: Webkamera under Andre ressurser, NSG, Golfamore og Bilutleie under Andre Tilbud.
* REGEL: Beholder monokrome ikoner, 22/78 layout og robust JSON-parsing. * REGEL: Beholder monokrome ikoner, 22/78 layout og robust JSON-parsing.
* --------------------------------------------------------------------------- * ---------------------------------------------------------------------------
*/ */
@ -25,7 +26,7 @@ const renderValue = (val: string) => {
const hasLink = val.includes('<a'); const hasLink = val.includes('<a');
return ( return (
<span <span
className={hasLink ? "text-orange-600 font-bold" : "text-[#11280f]"} className={hasLink ? "text-[#ff5722] font-bold hover:underline" : "text-[#11280f]"}
dangerouslySetInnerHTML={{ __html: val }} dangerouslySetInnerHTML={{ __html: val }}
/> />
); );
@ -50,6 +51,7 @@ const ICONS = {
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"/></>, 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"/></>, 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"/></>, 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"/></>, 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"/></> 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"/></>
}; };
@ -72,6 +74,14 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
const greenfeeRaw = parseJson(facility.greenfee, []); const greenfeeRaw = parseJson(facility.greenfee, []);
const shotzoom = parseJson(facility.shotzoom, []); const shotzoom = parseJson(facility.shotzoom, []);
// Parse Golfamore og NSG
const golfamoreData = parseJson(facility.golfamore_data, {});
const nsgData = parseJson(facility.nsg_data, {});
// Sjekker om de er aktive
const hasGolfamore = facility.golfamore === true;
const hasNSG = facility.nsg_url || (nsgData && Object.keys(nsgData).length > 0);
const groupedGreenfee: Record<string, any[]> = greenfeeRaw.reduce((acc: any, curr: any) => { const groupedGreenfee: Record<string, any[]> = greenfeeRaw.reduce((acc: any, curr: any) => {
const bane = curr.banenavn || "Gjestespill"; const bane = curr.banenavn || "Gjestespill";
if (!acc[bane]) acc[bane] = []; if (!acc[bane]) acc[bane] = [];
@ -79,8 +89,8 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
return acc; return acc;
}, {}); }, {});
const sidebarLinkClass = "flex items-center gap-4 hover:text-orange-600 transition-colors group"; const sidebarLinkClass = "flex items-center gap-4 hover:text-[#ff5722] transition-colors group";
const resourceBtnClass = "flex justify-between items-center p-5 bg-gray-50 rounded-2xl text-[11px] font-black uppercase hover:bg-orange-600 hover:text-white transition-all group"; const resourceBtnClass = "flex justify-between items-center p-5 bg-gray-50 rounded-2xl text-[11px] font-black uppercase hover:bg-[#ff5722] hover:text-white transition-all group";
useEffect(() => { useEffect(() => {
if (gallery.length <= 1) return; if (gallery.length <= 1) return;
@ -123,21 +133,21 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
</div> </div>
{facility.status_updated_at && ( {facility.status_updated_at && (
<span className="text-white/60 text-[10px] uppercase font-black tracking-widest bg-black/20 px-2 py-1 rounded"> <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)} Sist oppdatert: {formatDate(facility.status_updated_at)}
</span> </span>
)} )}
</div> </div>
{/* FLYTENDE HURTIGKNAPPER (Inkludert Turneringer) */} {/* FLYTENDE HURTIGKNAPPER (Inkludert Turneringer) */}
<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"> <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">
{facility.website_url && <a href={facility.website_url} target="_blank" className="w-9 h-9 bg-white rounded-xl flex items-center justify-center hover:bg-orange-600 hover:text-white transition-all"><Icon children={ICONS.web} /></a>} {facility.website_url && <a href={facility.website_url} target="_blank" className="w-9 h-9 bg-white rounded-xl flex items-center justify-center hover:bg-[#ff5722] hover:text-white transition-all"><Icon children={ICONS.web} /></a>}
{facility.golfbox_booking_url && <a href={facility.golfbox_booking_url} target="_blank" className="w-9 h-9 bg-white rounded-xl flex items-center justify-center hover:bg-orange-600 hover:text-white transition-all"><Icon children={ICONS.booking} /></a>} {facility.golfbox_booking_url && <a href={facility.golfbox_booking_url} target="_blank" className="w-9 h-9 bg-white rounded-xl flex items-center justify-center hover:bg-[#ff5722] hover:text-white transition-all"><Icon children={ICONS.booking} /></a>}
{facility.golfbox_tournament_url && <a href={facility.golfbox_tournament_url} target="_blank" className="w-9 h-9 bg-white rounded-xl flex items-center justify-center hover:bg-orange-600 hover:text-white transition-all"><Icon children={ICONS.trophy} /></a>} {facility.golfbox_tournament_url && <a href={facility.golfbox_tournament_url} target="_blank" className="w-9 h-9 bg-white rounded-xl flex items-center justify-center hover:bg-[#ff5722] hover:text-white transition-all"><Icon children={ICONS.trophy} /></a>}
<a href={`https://www.google.com/maps/search/?api=1&query=${facility.lat},${facility.lng}`} target="_blank" className="w-9 h-9 bg-white rounded-xl flex items-center justify-center hover:bg-orange-600 hover:text-white transition-all"><Icon children={ICONS.pin} /></a> <a href={`https://www.google.com/maps/search/?api=1&query=${facility.lat},${facility.lng}`} target="_blank" className="w-9 h-9 bg-white rounded-xl flex items-center justify-center hover:bg-[#ff5722] hover:text-white transition-all"><Icon children={ICONS.pin} /></a>
{facility.weather_url && <a href={facility.weather_url} target="_blank" className="w-9 h-9 bg-white rounded-xl flex items-center justify-center hover:bg-orange-600 hover:text-white transition-all"><Icon children={ICONS.weather} /></a>} {facility.weather_url && <a href={facility.weather_url} target="_blank" className="w-9 h-9 bg-white rounded-xl flex items-center justify-center hover:bg-[#ff5722] hover:text-white transition-all"><Icon children={ICONS.weather} /></a>}
</div> </div>
{/* HERO TEXT - pb-32 på mobil hindrer overlap med knapper */} {/* 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"> <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 && ( {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> <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>
@ -149,7 +159,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
{/* 2. STICKY NAV */} {/* 2. STICKY NAV */}
<nav className="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-gray-100 shadow-sm overflow-hidden"> <nav className="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-gray-100 shadow-sm overflow-hidden">
<div className="max-w-[1200px] mx-auto px-6 flex justify-between md:justify-start gap-4 md:gap-10 h-16 items-center text-[10px] font-black uppercase tracking-widest text-gray-400"> <div className="max-w-[1200px] mx-auto px-6 flex justify-between md:justify-start gap-4 md:gap-10 h-16 items-center text-[10px] font-black uppercase tracking-widest text-gray-400">
<button onClick={() => scrollTo('intro')}>Info</button> <button onClick={() => scrollTo('intro')}>Info</button>
<button onClick={() => scrollTo('weather')}>Vær</button> <button onClick={() => scrollTo('weather')}>Vær</button>
<button onClick={() => scrollTo('details')}>Detaljer</button> <button onClick={() => scrollTo('details')}>Detaljer</button>
@ -162,15 +172,15 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
<div className="max-w-[1200px] mx-auto px-0 md:px-6 space-y-4 md:space-y-12 mt-0 md:mt-12"> <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 (PRESENTASJON FØRST FOR MOBIL) */} {/* 3. INTRO & SIDEBAR */}
<section id="intro" className="flex flex-col lg:flex-row gap-0 md:gap-8 items-stretch"> <section id="intro" className="flex flex-col lg:flex-row gap-0 md:gap-8 items-stretch">
{/* HOVEDINNHOLD (78%) - Kommer først på mobil pga koderrekkefølge */} {/* HOVEDINNHOLD (78%) */}
<div className="lg:w-[78%] bg-white p-10 md:p-16 md:rounded-[3rem] shadow-sm border-b md:border-none"> <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 italic text-gray-400 text-lg font-serif">{facility.footnote}</div>} {facility.footnote && <div className="mb-8 pb-8 border-b border-gray-50 italic text-gray-400 text-lg font-serif">{facility.footnote}</div>}
<div className="leading-relaxed text-lg md:text-xl text-gray-600" dangerouslySetInnerHTML={{ __html: facility.description || 'Ingen beskrivelse tilgjengelig.' }} /> <div className="leading-relaxed text-lg md:text-xl text-gray-600" dangerouslySetInnerHTML={{ __html: facility.description || 'Ingen beskrivelse tilgjengelig.' }} />
</div> </div>
{/* SIDEBAR (22%) - Kommer etter tekst på mobil */} {/* SIDEBAR (22%) */}
<div className="lg:w-[22%] bg-white p-10 md:rounded-[3rem] shadow-sm flex flex-col order-last lg:order-none"> <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> <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"> <div className="flex-grow space-y-7 text-sm font-bold">
@ -183,13 +193,13 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
</a> </a>
<div className="pt-2 border-t border-gray-50 mt-4"> <div className="pt-2 border-t border-gray-50 mt-4">
<a href={`https://www.google.com/maps/search/?api=1&query=${facility.lat},${facility.lng}`} target="_blank" className={sidebarLinkClass + " pt-4 leading-tight items-start"}> <a href={`https://www.google.com/maps/search/?api=1&query=${facility.lat},${facility.lng}`} target="_blank" className={sidebarLinkClass + " pt-4 leading-tight items-start"}>
<Icon children={ICONS.pin} /> <span className="text-gray-400 group-hover:text-orange-600 transition-colors">{facility.address}<br/>{facility.city}</span> <Icon children={ICONS.pin} /> <span className="text-gray-400 group-hover:text-[#ff5722] transition-colors">{facility.address}<br/>{facility.city}</span>
</a> </a>
</div> </div>
</div> </div>
<div className="mt-auto pt-10 border-t border-gray-50"> <div className="mt-auto pt-10 border-t border-gray-50">
<Link href={`/`} className="text-[10px] font-black uppercase tracking-widest text-[#7ca982] hover:text-[#11280f] transition-all flex items-center gap-1"> <Link href={`/`} className="text-[10px] font-black uppercase tracking-widest text-[#7ca982] hover:text-[#11280f] transition-all flex items-center gap-1">
Se alle baner i {facility.county} Se alle baner i {facility.county}
</Link> </Link>
</div> </div>
</div> </div>
@ -202,12 +212,12 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
<div className="space-y-2.5"> <div className="space-y-2.5">
{facility.golfbox_booking_url && ( {facility.golfbox_booking_url && (
<a href={facility.golfbox_booking_url} target="_blank" className={resourceBtnClass}> <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> <span className="flex items-center gap-3"><Icon children={ICONS.booking} className="group-hover:text-white" /> Book Starttid</span><span></span>
</a> </a>
)} )}
{facility.golfbox_tournament_url && ( {facility.golfbox_tournament_url && (
<a href={facility.golfbox_tournament_url} target="_blank" className={resourceBtnClass}> <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> <span className="flex items-center gap-3"><Icon children={ICONS.trophy} className="group-hover:text-white" /> Turneringer</span><span></span>
</a> </a>
)} )}
{facility.baneguide_url && ( {facility.baneguide_url && (
@ -220,12 +230,18 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
<span className="flex items-center gap-3"><Icon children={ICONS.camera} className="group-hover:text-white" /> Flyfoto</span><span></span> <span className="flex items-center gap-3"><Icon children={ICONS.camera} className="group-hover:text-white" /> Flyfoto</span><span></span>
</a> </a>
)} )}
{/* Ny rad for Webkamera */}
{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) => ( {shotzoom.map((sz: any, i: number) => (
<a key={i} href={sz.shotzoom_url} target="_blank" className={resourceBtnClass}> <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(/&nbsp;?/g, ' ').trim().toUpperCase()}</span><span></span> <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> </a>
))} ))}
</div> </div>
</div> </div>
<div className="bg-white p-10 md:rounded-[3rem] shadow-sm text-sm font-bold text-gray-700"> <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]">Banen</h3> <h3 className="text-lg font-black mb-8 uppercase tracking-tighter text-[#11280f]">Banen</h3>
@ -240,14 +256,32 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
</div> </div>
<div className="bg-white p-10 md:rounded-[3rem] shadow-sm text-sm font-bold text-gray-700"> <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> <h3 className="text-lg font-black mb-8 uppercase tracking-tighter text-[#11280f]">Andre Tilbud</h3>
<div className="space-y-5"> <div className="space-y-4">
<div className="flex justify-between border-b border-gray-50 pb-3"><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">Drivingrange:</span><span>{amenities.drivingrange || 'Nei'}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-3"><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">Nærspill:</span><span>{amenities.treningsgreen || 'Ja'}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-3"><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">Proshop:</span><span className="text-right ml-4">{renderValue(amenities.proshop)}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-3"><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">Kølleutleie:</span><span>{amenities.kolleutleie || 'Ja'}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-3"><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">Bilutleie:</span><span>{amenities.bilutleie || 'Nei'}</span></div>
<div className="flex justify-between border-b border-gray-50 pb-3"><span className="text-gray-400">Head Pro:</span><span className="text-right ml-4">{renderValue(amenities.pro)}</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"><span className="text-gray-400">Kafé:</span><span className="text-right ml-4">{renderValue(amenities.kafe)}</span></div> <div className="flex justify-between border-b border-gray-50 pb-2"><span className="text-gray-400">Head Pro:</span><span className="text-right ml-4">{renderValue(amenities.pro)}</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>
{/* NYE RADER: 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">
<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="text-blue-600 font-black hover:underline">Ja (Vis Avtale)</a>
: (hasNSG ? <span className="text-blue-600 font-black">Ja</span> : "Nei")
}
</span>
</div>
</div> </div>
</div> </div>
</section> </section>
@ -256,14 +290,14 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
<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"> <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> <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"> <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>} {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> </div>
</section> </section>
{/* 6. KART SEKSJON */} {/* 6. KART SEKSJON */}
<section id="map" className="space-y-6"> <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> <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="w-full md:rounded-[3rem] overflow-hidden shadow-xl h-[450px] md:h-[650px] border-y-4 md:border-[12px] border-white bg-gray-100"> <div className="w-full md:rounded-[3rem] overflow-hidden shadow-xl h-[450px] md:h-[650px] border-y-4 md:border-[12px] border-white bg-gray-100">
<iframe width="100%" height="100%" style={{ border: 0 }} src={`https://maps.google.com/maps?q=${facility.lat},${facility.lng}&t=k&z=15&ie=UTF8&iwloc=&output=embed`} allowFullScreen /> <iframe width="100%" height="100%" style={{ border: 0 }} src={`https://maps.google.com/maps?q=${facility.lat},${facility.lng}&t=k&z=15&ie=UTF8&iwloc=&output=embed`} allowFullScreen />
</div> </div>
</section> </section>
@ -271,7 +305,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
{/* 7. VIDEO SEKSJON */} {/* 7. VIDEO SEKSJON */}
{facility.video_url && ( {facility.video_url && (
<section id="video" className="space-y-6"> <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> <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"> <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 /> <iframe src={facility.video_url} className="w-full h-full" allowFullScreen />
</div> </div>
@ -287,23 +321,23 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
Object.entries(groupedGreenfee).map(([bane, priser], idx) => ( Object.entries(groupedGreenfee).map(([bane, priser], idx) => (
<div key={idx} className="space-y-4"> <div key={idx} className="space-y-4">
{!(bane === "Gjestespill" && Object.keys(groupedGreenfee).length === 1) && ( {!(bane === "Gjestespill" && Object.keys(groupedGreenfee).length === 1) && (
<h4 className="text-lg font-black uppercase tracking-tighter text-[#11280f] border-b-2 border-gray-50 pb-2">{bane}</h4> <h4 className="text-lg font-black uppercase tracking-tighter text-[#11280f] border-b-2 border-gray-50 pb-2">{bane}</h4>
)} )}
<div className="space-y-2"> <div className="space-y-2">
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Voksne</p> <p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Voksne</p>
{priser.map((g, i) => ( {priser.map((g, i) => (
<div key={i} className="flex justify-between py-2 border-b border-gray-50/50 text-sm font-bold"> <div key={i} className="flex justify-between py-2 border-b border-gray-50/50 text-sm font-bold">
<span className="text-gray-500">{g.priskategori}</span> <span className="text-gray-500">{g.priskategori}</span>
<span>kr {g.pris_voksne || '--'},-</span> <span>kr {g.pris_voksne || '--'},-</span>
</div> </div>
))} ))}
</div> </div>
{priser.some(g => g.pris_junior) && ( {priser.some(g => g.pris_junior) && (
<div className="space-y-2 pt-4"> <div className="space-y-2 pt-4">
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Junior</p> <p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Junior</p>
{priser.map((g, i) => ( {priser.map((g, i) => (
<div key={i} className="flex justify-between py-2 border-b border-gray-50/50 text-sm font-bold"> <div key={i} className="flex justify-between py-2 border-b border-gray-50/50 text-sm font-bold">
<span className="text-gray-500">{g.priskategori}</span> <span className="text-gray-500">{g.priskategori}</span>
<span>kr {g.pris_junior || '--'},-</span> <span>kr {g.pris_junior || '--'},-</span>
</div> </div>
))} ))}
@ -320,7 +354,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
<div> <div>
<h3 className="text-2xl font-black mb-10 uppercase tracking-tighter">Medlemskap</h3> <h3 className="text-2xl font-black mb-10 uppercase tracking-tighter">Medlemskap</h3>
<div className="bg-[#f1f7ed] p-12 md:rounded-[2.5rem] mb-8 text-center border border-[#7ca982]/10"> <div className="bg-[#f1f7ed] p-12 md:rounded-[2.5rem] mb-8 text-center border border-[#7ca982]/10">
<p className="text-[#7ca982] text-[11px] font-black uppercase mb-3 tracking-widest">{facility.navn_standard_medlemskap || "Standard"}</p> <p className="text-[#7ca982] text-[11px] font-black uppercase mb-3 tracking-widest">{facility.navn_standard_medlemskap || "Standard"}</p>
<p className="text-6xl font-black text-[#11280f]">kr {facility.standard_medlemskap || '--'},-</p> <p className="text-6xl font-black text-[#11280f]">kr {facility.standard_medlemskap || '--'},-</p>
{facility.standard_medlemskap_kommentarer && <p className="text-[10px] text-gray-400 mt-4 uppercase font-bold italic leading-tight">{facility.standard_medlemskap_kommentarer}</p>} {facility.standard_medlemskap_kommentarer && <p className="text-[10px] text-gray-400 mt-4 uppercase font-bold italic leading-tight">{facility.standard_medlemskap_kommentarer}</p>}
</div> </div>
@ -338,7 +372,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
<div className="w-full flex flex-col items-center gap-20"> <div className="w-full flex flex-col items-center gap-20">
{activeCourses.map((c: any) => ( {activeCourses.map((c: any) => (
<div key={c.id} className="w-full overflow-x-auto md:overflow-visible no-scrollbar"> <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 className="min-w-[800px] md:min-w-0"><CourseDisplay course={c} /></div>
</div> </div>
))} ))}
</div> </div>

72
losby_dump.txt Normal file

File diff suppressed because one or more lines are too long

3160
nsg.txt Normal file

File diff suppressed because one or more lines are too long

56
rene_urler.txt Normal file
View file

@ -0,0 +1,56 @@
https://seniorgolf.no/lojalitetskort/10-hos-flight-park/
https://seniorgolf.no/lojalitetskort/10-hos-flight-park/
https://seniorgolf.no/lojalitetskort/alesund-gk/
https://seniorgolf.no/lojalitetskort/alesund-gk/
https://seniorgolf.no/lojalitetskort/arendal-omegn-gk/
https://seniorgolf.no/lojalitetskort/arendal-omegn-gk/
https://seniorgolf.no/lojalitetskort/asker-gk/
https://seniorgolf.no/lojalitetskort/asker-gk/
https://seniorgolf.no/lojalitetskort/askim-gk/
https://seniorgolf.no/lojalitetskort/askim-gk/
https://seniorgolf.no/lojalitetskort/atlungstad-gk/
https://seniorgolf.no/lojalitetskort/atlungstad-gk/
https://seniorgolf.no/lojalitetskort/baerum-gk/
https://seniorgolf.no/lojalitetskort/baerum-gk/
https://seniorgolf.no/lojalitetskort/bamble-gk/
https://seniorgolf.no/lojalitetskort/bamble-gk/
https://seniorgolf.no/lojalitetskort/bjaavann-gk/
https://seniorgolf.no/lojalitetskort/bjaavann-gk/
https://seniorgolf.no/lojalitetskort/bjornefjorden-gk/
https://seniorgolf.no/lojalitetskort/bjornefjorden-gk/
https://seniorgolf.no/lojalitetskort/borre-gk/
https://seniorgolf.no/lojalitetskort/borre-gk/
https://seniorgolf.no/lojalitetskort/borregaard-gk/
https://seniorgolf.no/lojalitetskort/borregaard-gk/
https://seniorgolf.no/lojalitetskort/drammen-gk/
https://seniorgolf.no/lojalitetskort/drammen-gk/
https://seniorgolf.no/lojalitetskort/eiker-gk/
https://seniorgolf.no/lojalitetskort/eiker-gk/
https://seniorgolf.no/lojalitetskort/elverum-gk/
https://seniorgolf.no/lojalitetskort/elverum-gk/
https://seniorgolf.no/lojalitetskort/fana-gk/
https://seniorgolf.no/lojalitetskort/fana-gk/
https://seniorgolf.no/lojalitetskort/gamle-fredrikstad-gk/
https://seniorgolf.no/lojalitetskort/gamle-fredrikstad-gk/
https://seniorgolf.no/lojalitetskort/gjerdrum-gk/
https://seniorgolf.no/lojalitetskort/gjerdrum-gk/
https://seniorgolf.no/lojalitetskort/gjersjoen-gk/
https://seniorgolf.no/lojalitetskort/gjersjoen-gk/
https://seniorgolf.no/lojalitetskort/gjovik-toten-gk/
https://seniorgolf.no/lojalitetskort/gjovik-toten-gk/
https://seniorgolf.no/lojalitetskort/grenland-gk/
https://seniorgolf.no/lojalitetskort/grenland-gk/
https://seniorgolf.no/lojalitetskort/groruddalen-gk/
https://seniorgolf.no/lojalitetskort/groruddalen-gk/
https://seniorgolf.no/lojalitetskort/hafjell-gk/
https://seniorgolf.no/lojalitetskort/hafjell-gk/
https://seniorgolf.no/lojalitetskort/hakadal-gk/
https://seniorgolf.no/lojalitetskort/hakadal-gk/
https://seniorgolf.no/lojalitetskort/halden-gk/
https://seniorgolf.no/lojalitetskort/halden-gk/
https://seniorgolf.no/lojalitetskort/hallingdal-gk/
https://seniorgolf.no/lojalitetskort/hallingdal-gk/
https://seniorgolf.no/lojalitetskort/hauger-gk/
https://seniorgolf.no/lojalitetskort/hauger-gk/
https://seniorgolf.no/lojalitetskort/haugesund-gk/
https://seniorgolf.no/lojalitetskort/haugesund-gk/