Før fiks av toppslider og knapper
This commit is contained in:
parent
a7c17b7572
commit
95706260c6
3 changed files with 140 additions and 26 deletions
Binary file not shown.
79
backend/sync_greenfee.py
Normal file
79
backend/sync_greenfee.py
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import asyncio, asyncpg, urllib.request, json
|
||||||
|
|
||||||
|
DB_URL = "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff"
|
||||||
|
# Vi fjerner acf_format=standard da rå-feltnavnene er tryggere her
|
||||||
|
WP_API_URL = "https://teeoff.no/wp-json/wp/v2/golfbaner?per_page=100"
|
||||||
|
|
||||||
|
def decode_html(text):
|
||||||
|
if not text: return ""
|
||||||
|
return str(text).replace('&', '&').replace('&', '&').replace(' ', ' ').strip()
|
||||||
|
|
||||||
|
async def run_greenfee_sync():
|
||||||
|
print("🎯 Starter GREENFEE-SYNC v1.2 (Basert på rå-API mapping)...")
|
||||||
|
conn = await asyncpg.connect(DB_URL)
|
||||||
|
page = 1
|
||||||
|
total_updated = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(f"{WP_API_URL}&page={page}", headers={'User-Agent': 'TeeOff-Sync'})
|
||||||
|
with urllib.request.urlopen(req) as response:
|
||||||
|
data = json.loads(response.read().decode())
|
||||||
|
except: break
|
||||||
|
if not data: break
|
||||||
|
|
||||||
|
for post in data:
|
||||||
|
slug = post['slug']
|
||||||
|
acf = post.get('acf', {})
|
||||||
|
|
||||||
|
# Henter banenavn for å gruppere riktig
|
||||||
|
bane_1_navn = acf.get('navn_pa_hovedbane') or "Hovedbanen"
|
||||||
|
bane_2_navn = acf.get('navn_pa_sekundar_bane') or "Bane 2"
|
||||||
|
|
||||||
|
final_greenfee = []
|
||||||
|
|
||||||
|
# --- MAPPER BANE 1 (Voksne + Junior) ---
|
||||||
|
voksne_1 = acf.get('greenfee_-_voksne') or []
|
||||||
|
junior_1 = acf.get('greenfee_-_junior') or []
|
||||||
|
|
||||||
|
for i, item in enumerate(voksne_1):
|
||||||
|
row = {
|
||||||
|
"banenavn": bane_1_navn,
|
||||||
|
"priskategori": item.get('priskategori'),
|
||||||
|
"pris_voksne": item.get('pris_voksne')
|
||||||
|
}
|
||||||
|
# Legger til juniorpris hvis den finnes på samme index
|
||||||
|
if i < len(junior_1):
|
||||||
|
row["pris_junior"] = junior_1[i].get('pris_junior')
|
||||||
|
final_greenfee.append(row)
|
||||||
|
|
||||||
|
# --- MAPPER BANE 2 (Voksne + Junior) ---
|
||||||
|
voksne_2 = acf.get('greenfee_-_voksne_bane_to') or []
|
||||||
|
junior_2 = acf.get('greenfee_-_junior_bane_to') or []
|
||||||
|
|
||||||
|
for i, item in enumerate(voksne_2):
|
||||||
|
row = {
|
||||||
|
"banenavn": bane_2_navn,
|
||||||
|
"priskategori": item.get('priskategori_bane_to'),
|
||||||
|
"pris_voksne": item.get('pris_voksne_bane_to')
|
||||||
|
}
|
||||||
|
if i < len(junior_2):
|
||||||
|
row["pris_junior"] = junior_2[i].get('pris_junior_bane_to')
|
||||||
|
final_greenfee.append(row)
|
||||||
|
|
||||||
|
# Henter krav (Gjeste_krav)
|
||||||
|
reqs = decode_html(acf.get('krav_til_gjestespillere'))
|
||||||
|
|
||||||
|
if final_greenfee:
|
||||||
|
await conn.execute('''
|
||||||
|
UPDATE facilities SET greenfee = $1::jsonb, guest_requirements = $2 WHERE slug = $3
|
||||||
|
''', json.dumps(final_greenfee), reqs, slug)
|
||||||
|
print(f"✅ {slug}: Importerte {len(final_greenfee)} prisrader for {bane_1_navn}/{bane_2_navn}")
|
||||||
|
total_updated += 1
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
await conn.close()
|
||||||
|
print(f"\n✨ Ferdig! Oppdaterte priser for {total_updated} anlegg.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(run_greenfee_sync())
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
/**
|
/**
|
||||||
* TEE OFF DETAIL VIEW - COMPLETE v3.15 (FINAL POLISH)
|
* TEE OFF DETAIL VIEW - COMPLETE v3.18
|
||||||
* ---------------------------------------------------------------------------
|
* ---------------------------------------------------------------------------
|
||||||
* FIX: Flyttet Turneringer til over Baneguide i ressurslisten.
|
* FIX: Fjernet emojier/ikoner ved siden av "GJESTESPILL" og "MEDLEMSKAP".
|
||||||
* FIX: Endret "Statistik" til "Statistikk".
|
* FIX: Forbedret Greenfee-logikk for å sikre at ALLE priser og baner (Østmork/Vestmork) vises.
|
||||||
* FIX: Ensrettet alle knapper i "Andre ressurser" (samme bakgrunn og hover).
|
* FIX: Skjuler under-header hvis den bare gjentar "Gjestespill".
|
||||||
* FIX: Sørget for at alle felt med lenker i "Andre tilbud" er oransje (#ff5722).
|
* REGEL: Beholder monokrome ikoner, 22/78 layout og 0047-prefix på telefon.
|
||||||
* REGEL: Beholder monokrome ikoner, 22/78 layout og robust JSON-parsing.
|
|
||||||
* ---------------------------------------------------------------------------
|
* ---------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -57,9 +56,17 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
||||||
const activeCourses = Array.isArray(rawCourses) ? rawCourses.filter((c: any) => c.holes && (typeof c.holes === 'string' || c.holes.length > 0)) : [];
|
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 amenities = parseJson(facility.amenities, {});
|
||||||
const gallery = parseJson(facility.gallery, [facility.image_url || FALLBACK_IMAGE]);
|
const gallery = parseJson(facility.gallery, [facility.image_url || FALLBACK_IMAGE]);
|
||||||
const greenfee = parseJson(facility.greenfee, []);
|
const greenfeeRaw = parseJson(facility.greenfee, []);
|
||||||
const shotzoom = parseJson(facility.shotzoom, []);
|
const shotzoom = parseJson(facility.shotzoom, []);
|
||||||
|
|
||||||
|
// Grupper priser etter bane (f.eks Østmork, Vestmork)
|
||||||
|
const groupedGreenfee: Record<string, any[]> = greenfeeRaw.reduce((acc: any, curr: any) => {
|
||||||
|
const bane = curr.banenavn || "Gjestespill";
|
||||||
|
if (!acc[bane]) acc[bane] = [];
|
||||||
|
acc[bane].push(curr);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
const linkClass = "text-orange-600 hover:underline transition-colors font-bold";
|
const linkClass = "text-orange-600 hover:underline transition-colors font-bold";
|
||||||
const sidebarLinkClass = "flex items-center gap-4 hover:text-orange-600 transition-colors group";
|
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 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";
|
||||||
|
|
@ -148,8 +155,6 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
||||||
|
|
||||||
{/* 4. 3-KOLONNE INFO */}
|
{/* 4. 3-KOLONNE INFO */}
|
||||||
<section id="details" className="grid grid-cols-1 lg:grid-cols-3 gap-4 md:gap-8">
|
<section id="details" className="grid grid-cols-1 lg:grid-cols-3 gap-4 md:gap-8">
|
||||||
|
|
||||||
{/* KOLONNE 1: ANDRE RESSURSER (Ensrettet uttrykk & Sortering) */}
|
|
||||||
<div className="bg-white p-10 md:rounded-[3rem] shadow-sm">
|
<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>
|
<h3 className="text-lg font-black mb-8 uppercase tracking-tighter">Andre Ressurser</h3>
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
|
|
@ -175,13 +180,11 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
||||||
)}
|
)}
|
||||||
{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(/ /g, ' ').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>
|
||||||
|
|
||||||
{/* KOLONNE 2: BANEN */}
|
|
||||||
<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>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
|
|
@ -193,8 +196,6 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
||||||
<div className="flex justify-between"><span className="text-gray-400">Arkitekt:</span><span className="text-right truncate ml-4">{facility.architect || '--'}</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>
|
</div>
|
||||||
|
|
||||||
{/* KOLONNE 3: ANDRE TILBUD (Oransje lenker) */}
|
|
||||||
<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-5">
|
||||||
|
|
@ -235,26 +236,58 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 8. PRISER SEKSJON */}
|
{/* 8. PRISER & MEDLEMSKAP (IKONER FJERNET) */}
|
||||||
<section id="prices" className="grid grid-cols-1 lg:grid-cols-2 gap-0 md:gap-8">
|
<section id="prices" className="grid grid-cols-1 lg:grid-cols-2 gap-0 md:gap-8">
|
||||||
<div className="bg-white p-10 md:p-14 md:rounded-[3rem] shadow-sm border-b md:border-none">
|
<div className="bg-white p-10 md:p-14 md:rounded-[3rem] shadow-sm">
|
||||||
<h3 className="text-2xl font-black mb-10 uppercase tracking-tighter">⛳ Gjestespill</h3>
|
<h3 className="text-2xl font-black mb-10 uppercase tracking-tighter">Gjestespill</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-10">
|
||||||
{greenfee.length > 0 ? (
|
{Object.keys(groupedGreenfee).length > 0 ? (
|
||||||
greenfee.map((g: any, i: number) => (
|
Object.entries(groupedGreenfee).map(([bane, priser], idx) => (
|
||||||
<div key={i} className="flex justify-between py-4 border-b border-gray-50 font-bold text-lg"><span className="text-gray-400">{g.priskategori}</span><span>kr {g.pris_voksne || '--'},-</span></div>
|
<div key={idx} className="space-y-4">
|
||||||
|
{/* Skjul baneheader hvis den bare gjentar "Gjestespill" og det bare er én gruppe */}
|
||||||
|
{!(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>
|
||||||
|
)}
|
||||||
|
<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>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>
|
||||||
|
{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>kr {g.pris_junior || '--'},-</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))
|
))
|
||||||
) : <p className="text-gray-400 italic py-6">Ingen priser funnet.</p>}
|
) : <p className="text-gray-400 italic py-6">Ingen priser funnet.</p>}
|
||||||
</div>
|
</div>
|
||||||
|
<p className="mt-10 text-[10px] text-gray-300 font-black uppercase tracking-widest italic">Krav: {facility.guest_requirements || 'Klubbhandicap'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white p-10 md:p-14 md:rounded-[3rem] shadow-sm border-b md:border-none flex flex-col justify-between">
|
|
||||||
|
<div className="bg-white p-10 md:p-14 md:rounded-[3rem] shadow-sm flex flex-col justify-between">
|
||||||
<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-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>}
|
||||||
</div>
|
</div>
|
||||||
|
{facility.navn_rimeligste_alternativ && (
|
||||||
|
<div className="px-8 py-5 bg-gray-50 rounded-2xl border border-gray-100 flex justify-between items-center text-sm font-bold"><span className="text-gray-400 uppercase text-[10px] tracking-widest">{facility.navn_rimeligste_alternativ}</span><span className="text-[#11280f]">kr {facility.rimeligste_alternativ},-</span></div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<a href={facility.medlemskap_url} target="_blank" className="mt-10 block w-full text-center bg-[#11280f] text-white p-7 rounded-2xl font-black uppercase text-xs tracking-widest shadow-xl">Se alle alternativer</a>
|
<a href={facility.medlemskap_url} target="_blank" className="mt-10 block w-full text-center bg-[#11280f] text-white p-7 rounded-2xl font-black uppercase text-xs tracking-widest shadow-xl hover:bg-black transition-all">Se alle alternativer</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -271,7 +304,9 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showBackToTop && ( <button onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} className="fixed bottom-8 right-8 w-14 h-14 bg-[#11280f] text-white rounded-full shadow-2xl flex items-center justify-center text-2xl z-[100] border-4 border-white/20 hover:scale-110 transition-all">↑</button> )}
|
{showBackToTop && (
|
||||||
|
<button onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} className="fixed bottom-8 right-8 w-14 h-14 bg-[#11280f] text-white rounded-full shadow-2xl flex items-center justify-center text-2xl z-[100] border-4 border-white/20 hover:scale-110 transition-all">↑</button>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue