Mellomlagring
This commit is contained in:
parent
1d8ce4c52d
commit
4713eced56
6 changed files with 3566 additions and 55 deletions
150
backend/import_nye_felter.py
Normal file
150
backend/import_nye_felter.py
Normal 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())
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
"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.
|
||||
* Bruk STATUS_MAP for tekst.
|
||||
* REGEL 2: DATA-PARSING: Bruk parseJson() for 'course_statuses', 'amenities' og 'nsg_data'
|
||||
* fordi API-et ofte returnerer disse som strenger.
|
||||
* REGEL 2: DATA-PARSING: Bruk parseJson() for 'course_statuses', 'amenities' og 'nsg_data'.
|
||||
* 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 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.
|
||||
* Datoen har en svak bakgrunnsfarge for å sikre lesbarhet på 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 Link from 'next/link';
|
||||
|
||||
|
|
@ -45,15 +45,22 @@ export default function FacilitySearch({ initialFacilities }: { initialFacilitie
|
|||
const processed = useMemo(() => {
|
||||
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 => {
|
||||
// --- ROBUST DATA-PARSING (Håndterer tekst vs objekt fra API) ---
|
||||
// --- ROBUST DATA-PARSING ---
|
||||
const parseJson = (val: any, fallback: any) => {
|
||||
if (!val) return fallback;
|
||||
if (typeof val === 'object') return val;
|
||||
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 nsgData = parseJson(f.nsg_data, {});
|
||||
|
||||
|
|
@ -61,11 +68,44 @@ export default function FacilitySearch({ initialFacilities }: { initialFacilitie
|
|||
const hasNSG = nsgData && Object.keys(nsgData).length > 0;
|
||||
const hasGolfamore = f.golfamore === true;
|
||||
|
||||
const words = searchQuery.toLowerCase().trim().split(/\s+/).filter(w => w.length > 0);
|
||||
const blob = `${f.name} ${f.city} ${f.county}`.toLowerCase();
|
||||
const matches = words.every(w => blob.includes(w));
|
||||
// --- THE SEARCH BLOB ---
|
||||
// Vi starter med å legge navn, by og fylke i en stor, usynlig tekststreng
|
||||
let searchableText = `${f.name} ${f.city} ${f.county}`.toLowerCase();
|
||||
|
||||
// 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}`;
|
||||
}
|
||||
});
|
||||
|
||||
return { ...f, statuses, amenities, dist, hasNSG, hasGolfamore, matches };
|
||||
// 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)
|
||||
.sort((a, b) => {
|
||||
|
|
@ -82,12 +122,11 @@ export default function FacilitySearch({ initialFacilities }: { initialFacilitie
|
|||
</button>
|
||||
</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">
|
||||
{processed.map((f: any) => {
|
||||
// Sikrer at vi har en gyldig array for banestatuser
|
||||
const sArr = Array.isArray(f.statuses) && f.statuses.length > 0 ? f.statuses : [{ status: 'ukjent', name: 'Hovedbane' }];
|
||||
const sArr = f.statuses; // Sikret via pre-prosesseringen over
|
||||
|
||||
// Formater datoen pent: "05. mars 2026"
|
||||
const lastUpdated = f.status_updated_at
|
||||
|
|
@ -134,7 +173,7 @@ export default function FacilitySearch({ initialFacilities }: { initialFacilitie
|
|||
})}
|
||||
</div>
|
||||
|
||||
{/* Avstandspille (Mørk oliven #2d3319) */}
|
||||
{/* Avstandspille */}
|
||||
{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">
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
"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: 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: Webkamera under Andre ressurser, NSG, Golfamore og Bilutleie under Andre Tilbud.
|
||||
* REGEL: Beholder monokrome ikoner, 22/78 layout og robust JSON-parsing.
|
||||
* ---------------------------------------------------------------------------
|
||||
*/
|
||||
|
|
@ -25,7 +26,7 @@ const renderValue = (val: string) => {
|
|||
const hasLink = val.includes('<a');
|
||||
return (
|
||||
<span
|
||||
className={hasLink ? "text-orange-600 font-bold" : "text-[#11280f]"}
|
||||
className={hasLink ? "text-[#ff5722] font-bold hover:underline" : "text-[#11280f]"}
|
||||
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"/></>,
|
||||
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"/></>
|
||||
};
|
||||
|
|
@ -71,6 +73,14 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
|||
const gallery = galleryRaw.length > 0 ? galleryRaw : [facility.image_url || FALLBACK_IMAGE];
|
||||
const greenfeeRaw = parseJson(facility.greenfee, []);
|
||||
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 bane = curr.banenavn || "Gjestespill";
|
||||
|
|
@ -79,8 +89,8 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
|||
return acc;
|
||||
}, {});
|
||||
|
||||
const sidebarLinkClass = "flex items-center gap-4 hover:text-orange-600 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 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-[#ff5722] hover:text-white transition-all group";
|
||||
|
||||
useEffect(() => {
|
||||
if (gallery.length <= 1) return;
|
||||
|
|
@ -123,21 +133,21 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
|||
</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)}
|
||||
Sist oppdatert: {formatDate(facility.status_updated_at)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
{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.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_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>}
|
||||
<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>
|
||||
{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.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-[#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-[#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-[#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-[#ff5722] hover:text-white transition-all"><Icon children={ICONS.weather} /></a>}
|
||||
</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">
|
||||
{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>
|
||||
|
|
@ -149,7 +159,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
|||
|
||||
{/* 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">
|
||||
<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('weather')}>Vær</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">
|
||||
|
||||
{/* 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">
|
||||
{/* 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">
|
||||
{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>
|
||||
|
||||
{/* 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">
|
||||
<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">
|
||||
|
|
@ -183,13 +193,13 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
|||
</a>
|
||||
<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"}>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
Se alle baner i {facility.county} →
|
||||
Se alle baner i {facility.county} →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -202,12 +212,12 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
|||
<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>
|
||||
<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>
|
||||
<span className="flex items-center gap-3"><Icon children={ICONS.trophy} className="group-hover:text-white" /> Turneringer</span><span>→</span>
|
||||
</a>
|
||||
)}
|
||||
{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>
|
||||
</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) => (
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
|
@ -240,14 +256,32 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
|||
</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-5">
|
||||
<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-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-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-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-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-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"><span className="text-gray-400">Kafé:</span><span className="text-right ml-4">{renderValue(amenities.kafe)}</span></div>
|
||||
<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">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>
|
||||
</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">
|
||||
<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>}
|
||||
{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 */}
|
||||
<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="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 />
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -271,7 +305,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
|||
{/* 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>
|
||||
<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>
|
||||
|
|
@ -287,23 +321,23 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
|||
Object.entries(groupedGreenfee).map(([bane, priser], idx) => (
|
||||
<div key={idx} className="space-y-4">
|
||||
{!(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">
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Voksne</p>
|
||||
{priser.map((g, i) => (
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
{priser.some(g => g.pris_junior) && (
|
||||
<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) => (
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -320,7 +354,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
|||
<div>
|
||||
<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">
|
||||
<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>
|
||||
{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>
|
||||
|
|
@ -338,7 +372,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
|||
<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 className="min-w-[800px] md:min-w-0"><CourseDisplay course={c} /></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
72
losby_dump.txt
Normal file
72
losby_dump.txt
Normal file
File diff suppressed because one or more lines are too long
3160
nsg.txt
Normal file
3160
nsg.txt
Normal file
File diff suppressed because one or more lines are too long
56
rene_urler.txt
Normal file
56
rene_urler.txt
Normal 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/
|
||||
Loading…
Reference in a new issue