Før vi begynner på SEO

This commit is contained in:
Erol 2026-04-12 21:53:54 +02:00
parent f6aa50a0a7
commit b5f9f52ffe
7 changed files with 712 additions and 4 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -31,6 +31,7 @@ export type FacilityRecord = {
golfamore_data?: unknown;
nsg_data?: unknown;
vtg_datoer?: unknown;
vtg_updated_at?: string | null;
social_links?: unknown;
course_statuses?: unknown;
footnote?: string | null;

View file

@ -526,10 +526,18 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
<div className="absolute -right-20 -top-20 opacity-10 text-[200px] pointer-events-none transform group-hover:scale-110 transition-transform duration-700">🏌</div>
<div className="relative z-10">
<h3 className="text-2xl font-black uppercase tracking-tighter mb-4 flex items-center gap-3">
Nybegynnerkurs (Veien til Golf)
</h3>
<div className="mb-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<h3 className="text-2xl font-black uppercase tracking-tighter flex items-center gap-3">
Nybegynnerkurs (Veien til Golf)
</h3>
<Link
href="/vtg"
className="inline-flex items-center text-[10px] font-black uppercase tracking-[0.18em] text-white/80 transition hover:text-white"
>
Se alle VTG-kurs
</Link>
</div>
{facility.vtg_beskrivelse && (
<p className="text-sm md:text-base text-white/90 mb-8 leading-relaxed font-medium max-w-4xl">
{facility.vtg_beskrivelse}

View file

@ -18,6 +18,11 @@ const displayFont = Oswald({
export const metadata: Metadata = {
title: "TeeOff.no - Din guide til norske golfbaner",
description: "Oppdatert banestatus, priser og informasjon om alle norske golfanlegg.",
icons: {
icon: "/icons/cropped-siteicon-1.png",
shortcut: "/icons/cropped-siteicon-1.png",
apple: "/icons/cropped-siteicon-1.png",
},
};
export default function RootLayout({ children }: { children: React.ReactNode }) {

View file

@ -0,0 +1,661 @@
"use client";
import Link from "next/link";
import { useMemo, useState } from "react";
import {
enrichFacilities,
filterFacilitiesByArea,
HIERARCHICAL_AREA_OPTIONS,
parseJson,
type EnrichedFacility,
type FacilityRecord,
} from "@/app/facilityData";
type SortKey = "soonest" | "price" | "alpha";
type TimeFilter = "all" | "upcoming" | "thisMonth" | "next30";
type PriceFilter = "all" | "under1500" | "1500to2500" | "2500plus";
type CourseDateRecord = {
dato?: string;
status?: string;
};
type CourseDateSummary = {
raw: string;
status: string;
comparableDate: Date | null;
};
type VtgExplorerProps = {
facilities: FacilityRecord[];
};
type VtgListing = {
id: number;
slug: string;
name: string;
city?: string | null;
county?: string | null;
vtgPris?: number | null;
vtgLenke?: string | null;
vtgBeskrivelse?: string | null;
vtgUpdatedAt?: string | null;
nextCourse: CourseDateSummary | null;
upcomingCourseCount: number;
hasPublishedDates: boolean;
allDates: CourseDateSummary[];
enriched: EnrichedFacility;
};
const timeOptions: Array<{ value: TimeFilter; label: string }> = [
{ value: "all", label: "Alle" },
{ value: "upcoming", label: "Kommende" },
{ value: "thisMonth", label: "Denne måneden" },
{ value: "next30", label: "Neste 30 dager" },
];
const priceOptions: Array<{ value: PriceFilter; label: string }> = [
{ value: "all", label: "Alle priser" },
{ value: "under1500", label: "Under 1500" },
{ value: "1500to2500", label: "15002500" },
{ value: "2500plus", label: "2500+" },
];
const sortOptions: Array<{ value: SortKey; label: string }> = [
{ value: "soonest", label: "Snartest først" },
{ value: "price", label: "Billigst først" },
{ value: "alpha", label: "Alfabetisk" },
];
const monthMap: Record<string, number> = {
januar: 0,
jan: 0,
februar: 1,
feb: 1,
mars: 2,
mar: 2,
april: 3,
apr: 3,
mai: 4,
juni: 5,
jun: 5,
juli: 6,
jul: 6,
august: 7,
aug: 7,
september: 8,
sep: 8,
sept: 8,
oktober: 9,
okt: 9,
november: 10,
nov: 10,
desember: 11,
des: 11,
};
const currencyFormatter = new Intl.NumberFormat("nb-NO");
function formatCurrency(value?: number | null) {
if (typeof value !== "number") return "Ikke publisert";
return `${currencyFormatter.format(value)},-`;
}
function formatDate(value?: string | null) {
if (!value) return "Ukjent";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "Ukjent";
return date.toLocaleDateString("nb-NO", {
day: "2-digit",
month: "short",
year: "numeric",
});
}
function normalizeWhitespace(value: string) {
return value.replace(/\s+/g, " ").trim();
}
function getStartOfToday() {
const today = new Date();
today.setHours(0, 0, 0, 0);
return today;
}
function parseComparableDate(raw: string) {
const trimmed = normalizeWhitespace(raw);
if (!trimmed) return null;
const isoCandidate = new Date(trimmed);
if (!Number.isNaN(isoCandidate.getTime())) {
isoCandidate.setHours(0, 0, 0, 0);
return isoCandidate;
}
const numericDateMatch = trimmed.match(/(\d{1,2})[./](\d{1,2})[./](\d{2,4})/);
if (numericDateMatch) {
const day = Number(numericDateMatch[1]);
const month = Number(numericDateMatch[2]) - 1;
const yearValue = Number(numericDateMatch[3]);
const year = yearValue < 100 ? 2000 + yearValue : yearValue;
const parsed = new Date(year, month, day);
parsed.setHours(0, 0, 0, 0);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
const normalized = trimmed
.toLowerCase()
.replace(/[.,]/g, " ")
.replace(/\s+/g, " ");
const monthToken = Object.keys(monthMap).find((monthName) =>
normalized.includes(monthName),
);
if (!monthToken) return null;
const monthIndex = monthMap[monthToken];
const dayMatch = normalized.match(/(\d{1,2})/);
if (!dayMatch) return null;
const explicitYearMatch = normalized.match(/\b(20\d{2})\b/);
const today = getStartOfToday();
let year = explicitYearMatch ? Number(explicitYearMatch[1]) : today.getFullYear();
const day = Number(dayMatch[1]);
let parsed = new Date(year, monthIndex, day);
parsed.setHours(0, 0, 0, 0);
if (!explicitYearMatch && parsed.getTime() < today.getTime() - 7 * 24 * 60 * 60 * 1000) {
year += 1;
parsed = new Date(year, monthIndex, day);
parsed.setHours(0, 0, 0, 0);
}
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
function summarizeCourseDates(rawDates: unknown) {
const parsedDates = parseJson<CourseDateRecord[]>(rawDates, []);
const courseDates = (Array.isArray(parsedDates) ? parsedDates : [])
.map((entry) => ({
raw: normalizeWhitespace(String(entry?.dato || "")),
status: normalizeWhitespace(String(entry?.status || "Ukjent")),
comparableDate: parseComparableDate(String(entry?.dato || "")),
}))
.filter((entry) => entry.raw);
const today = getStartOfToday();
const datedEntries = courseDates
.filter((entry) => entry.comparableDate)
.sort((a, b) => a.comparableDate!.getTime() - b.comparableDate!.getTime());
const upcomingEntries = datedEntries.filter(
(entry) => entry.comparableDate!.getTime() >= today.getTime(),
);
return {
allDates: courseDates,
nextCourse: upcomingEntries[0] || datedEntries[0] || null,
upcomingCourseCount: upcomingEntries.length,
hasPublishedDates: courseDates.length > 0,
};
}
function matchesTimeFilter(nextCourse: CourseDateSummary | null, filter: TimeFilter) {
if (filter === "all") return true;
if (!nextCourse?.comparableDate) return false;
const today = getStartOfToday();
const target = nextCourse.comparableDate;
if (filter === "upcoming") {
return target.getTime() >= today.getTime();
}
if (filter === "thisMonth") {
return (
target.getFullYear() === today.getFullYear() &&
target.getMonth() === today.getMonth() &&
target.getTime() >= today.getTime()
);
}
const nextThirty = new Date(today);
nextThirty.setDate(today.getDate() + 30);
return target.getTime() >= today.getTime() && target.getTime() <= nextThirty.getTime();
}
function matchesPriceFilter(price: number | null | undefined, filter: PriceFilter) {
if (filter === "all") return true;
if (typeof price !== "number") return false;
if (filter === "under1500") return price < 1500;
if (filter === "1500to2500") return price >= 1500 && price <= 2500;
return price > 2500;
}
function statusTone(status: string) {
const normalized = status.toLowerCase();
if (normalized.includes("full")) return "bg-rose-100 text-rose-700";
if (normalized.includes("vente") || normalized.includes("få")) return "bg-amber-100 text-amber-800";
if (normalized.includes("ledig")) return "bg-emerald-100 text-emerald-700";
return "bg-[#EFF4E8] text-[#556555]";
}
export default function VtgExplorer({ facilities }: VtgExplorerProps) {
const [areaFilter, setAreaFilter] = useState("");
const [clubQuery, setClubQuery] = useState("");
const [timeFilter, setTimeFilter] = useState<TimeFilter>("upcoming");
const [priceFilter, setPriceFilter] = useState<PriceFilter>("all");
const [sortKey, setSortKey] = useState<SortKey>("soonest");
const [onlyWithDates, setOnlyWithDates] = useState(false);
const enrichedFacilities = useMemo(() => enrichFacilities(facilities), [facilities]);
const listings = useMemo<VtgListing[]>(
() =>
enrichedFacilities
.filter((facility) => facility.hasVtg)
.map((facility) => {
const summary = summarizeCourseDates(facility.vtg_datoer);
return {
id: facility.id,
slug: facility.slug,
name: facility.name,
city: facility.city,
county: facility.county,
vtgPris: facility.vtg_pris,
vtgLenke: facility.vtg_lenke,
vtgBeskrivelse: facility.vtg_beskrivelse,
vtgUpdatedAt: facility.vtg_updated_at || facility.status_updated_at,
nextCourse: summary.nextCourse,
upcomingCourseCount: summary.upcomingCourseCount,
hasPublishedDates: summary.hasPublishedDates,
allDates: summary.allDates,
enriched: facility,
};
}),
[enrichedFacilities],
);
const areaFilteredListings = useMemo(
() =>
areaFilter
? filterFacilitiesByArea(
listings.map((listing) => listing.enriched),
areaFilter,
).map((facility) => listings.find((listing) => listing.id === facility.id)!).filter(Boolean)
: listings,
[areaFilter, listings],
);
const filteredListings = useMemo(() => {
const normalizedClubQuery = clubQuery.trim().toLowerCase();
const filtered = areaFilteredListings.filter((listing) => {
const matchesClub =
!normalizedClubQuery ||
listing.name.toLowerCase().includes(normalizedClubQuery) ||
String(listing.city || "")
.toLowerCase()
.includes(normalizedClubQuery);
if (!matchesClub) return false;
if (!matchesTimeFilter(listing.nextCourse, timeFilter)) return false;
if (!matchesPriceFilter(listing.vtgPris, priceFilter)) return false;
if (onlyWithDates && !listing.hasPublishedDates) return false;
return true;
});
const sorted = [...filtered].sort((a, b) => {
if (sortKey === "alpha") {
return a.name.localeCompare(b.name, "nb-NO");
}
if (sortKey === "price") {
const priceA = typeof a.vtgPris === "number" ? a.vtgPris : Number.POSITIVE_INFINITY;
const priceB = typeof b.vtgPris === "number" ? b.vtgPris : Number.POSITIVE_INFINITY;
if (priceA !== priceB) return priceA - priceB;
return a.name.localeCompare(b.name, "nb-NO");
}
const dateA = a.nextCourse?.comparableDate?.getTime() ?? Number.POSITIVE_INFINITY;
const dateB = b.nextCourse?.comparableDate?.getTime() ?? Number.POSITIVE_INFINITY;
if (dateA !== dateB) return dateA - dateB;
return a.name.localeCompare(b.name, "nb-NO");
});
return sorted;
}, [areaFilteredListings, clubQuery, onlyWithDates, priceFilter, sortKey, timeFilter]);
const stats = useMemo(() => {
const withDates = listings.filter((listing) => listing.hasPublishedDates).length;
const cheapest = listings
.map((listing) => listing.vtgPris)
.filter((value): value is number => typeof value === "number")
.sort((a, b) => a - b)[0];
return {
clubs: listings.length,
withDates,
cheapest,
};
}, [listings]);
return (
<div className="space-y-8">
<section className="border-b border-[#112015]/8 bg-[linear-gradient(135deg,rgba(139,195,74,0.16),rgba(255,255,255,0.92))]">
<div className="mx-auto max-w-[1400px] px-4 py-14 sm:px-6 lg:px-8 lg:py-20">
<div className="max-w-4xl">
<p className="mb-4 text-[11px] font-black uppercase tracking-[0.28em] text-[#8BC34A]">
Veien til Golf
</p>
<h1 className="max-w-3xl text-5xl font-black text-[#112015] sm:text-6xl">
Finn nybegynnerkurs i golf
</h1>
<p className="mt-6 max-w-3xl text-base leading-7 text-[#4F5F50] sm:text-lg">
Sammenlign VTG-tilbud etter område, dato, pris og klubb. Dette er laget for folk
som vil finne et konkret sted å starte, ikke grave seg gjennom hver enkelt klubbside.
</p>
</div>
<div className="mt-10 grid gap-4 md:grid-cols-3">
<div className="surface-card rounded-[1.75rem] p-5">
<p className="text-[11px] font-black uppercase tracking-[0.18em] text-[#8BC34A]">
Klubber
</p>
<p className="mt-3 text-4xl font-black text-[#112015]">{stats.clubs}</p>
<p className="mt-2 text-sm text-[#5B675C]">tilbyr VTG TeeOff akkurat </p>
</div>
<div className="surface-card rounded-[1.75rem] p-5">
<p className="text-[11px] font-black uppercase tracking-[0.18em] text-[#8BC34A]">
Kursdatoer
</p>
<p className="mt-3 text-4xl font-black text-[#112015]">{stats.withDates}</p>
<p className="mt-2 text-sm text-[#5B675C]">har publiserte kursdatoer</p>
</div>
<div className="surface-card rounded-[1.75rem] p-5">
<p className="text-[11px] font-black uppercase tracking-[0.18em] text-[#8BC34A]">
Billigste pris
</p>
<p className="mt-3 text-4xl font-black text-[#112015]">
{stats.cheapest ? formatCurrency(stats.cheapest) : "Ukjent"}
</p>
<p className="mt-2 text-sm text-[#5B675C]">laveste registrerte voksenpris</p>
</div>
</div>
</div>
</section>
<section className="mx-auto max-w-[1400px] px-4 sm:px-6 lg:px-8">
<div className="surface-card rounded-[2rem] p-5 sm:p-6">
<div className="grid gap-4 lg:grid-cols-[1.2fr_1.2fr_0.9fr_0.9fr]">
<div>
<label className="mb-2 block text-[11px] font-black uppercase tracking-[0.18em] text-[#516052]">
Område
</label>
<select
value={areaFilter}
onChange={(event) => setAreaFilter(event.target.value)}
className="filter-field w-full px-4"
>
{HIERARCHICAL_AREA_OPTIONS.map((option) => (
<option key={option.value || option.slug} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div>
<label className="mb-2 block text-[11px] font-black uppercase tracking-[0.18em] text-[#516052]">
Klubb
</label>
<input
type="search"
value={clubQuery}
onChange={(event) => setClubQuery(event.target.value)}
className="filter-field w-full px-4"
placeholder="Søk etter klubb eller sted"
/>
</div>
<div>
<label className="mb-2 block text-[11px] font-black uppercase tracking-[0.18em] text-[#516052]">
Når
</label>
<select
value={timeFilter}
onChange={(event) => setTimeFilter(event.target.value as TimeFilter)}
className="filter-field w-full px-4"
>
{timeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div>
<label className="mb-2 block text-[11px] font-black uppercase tracking-[0.18em] text-[#516052]">
Pris
</label>
<select
value={priceFilter}
onChange={(event) => setPriceFilter(event.target.value as PriceFilter)}
className="filter-field w-full px-4"
>
{priceOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
<div className="mt-4 flex flex-col gap-4 border-t border-[#112015]/8 pt-4 lg:flex-row lg:items-center lg:justify-between">
<label className="inline-flex items-center gap-3 text-sm font-semibold text-[#112015]">
<input
type="checkbox"
checked={onlyWithDates}
onChange={(event) => setOnlyWithDates(event.target.checked)}
className="h-4 w-4 accent-[#8BC34A]"
/>
Vis bare kurs med publiserte datoer
</label>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<div className="flex flex-wrap gap-2">
{sortOptions.map((option) => {
const isActive = option.value === sortKey;
return (
<button
key={option.value}
type="button"
onClick={() => setSortKey(option.value)}
className={`rounded-full px-4 py-2 text-xs font-black uppercase tracking-[0.16em] transition ${
isActive
? "bg-[#112015] text-white"
: "border border-[#112015]/10 bg-white text-[#112015] hover:border-[#FF5722] hover:text-[#FF5722]"
}`}
>
{option.label}
</button>
);
})}
</div>
<button
type="button"
onClick={() => {
setAreaFilter("");
setClubQuery("");
setTimeFilter("upcoming");
setPriceFilter("all");
setSortKey("soonest");
setOnlyWithDates(false);
}}
className="rounded-full border border-[#112015]/10 bg-white px-4 py-2 text-xs font-black uppercase tracking-[0.16em] text-[#112015] transition hover:border-[#FF5722] hover:text-[#FF5722]"
>
Nullstill filtre
</button>
</div>
</div>
</div>
</section>
<section className="mx-auto max-w-[1400px] px-4 pb-12 sm:px-6 lg:px-8 lg:pb-16">
<div className="mb-5 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="text-[11px] font-black uppercase tracking-[0.18em] text-[#8BC34A]">
Resultater
</p>
<h2 className="mt-2 text-3xl font-black text-[#112015]">
{filteredListings.length} VTG-tilbud
</h2>
</div>
<p className="max-w-2xl text-sm text-[#5B675C]">
Vi viser klubbene som har pris, beskrivelse, lenke eller kursdatoer registrert.
Resultater uten dato kan fortsatt være nyttige hvis du bare vil finne riktig klubb.
</p>
</div>
<div className="space-y-4">
{filteredListings.length ? (
filteredListings.map((listing) => (
<article key={listing.id} className="surface-card rounded-[2rem] p-5 sm:p-6">
<div className="flex flex-col gap-6 xl:flex-row xl:items-start xl:justify-between">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-3">
<h3 className="text-2xl font-black text-[#112015]">{listing.name}</h3>
<span className="rounded-full bg-[#EFF4E8] px-3 py-1 text-[11px] font-black uppercase tracking-[0.16em] text-[#556555]">
{listing.city ? `${listing.city} · ${listing.county}` : listing.county || "Norge"}
</span>
{listing.hasPublishedDates ? (
<span className="rounded-full bg-emerald-100 px-3 py-1 text-[11px] font-black uppercase tracking-[0.16em] text-emerald-700">
Har kursdato
</span>
) : (
<span className="rounded-full bg-[#EFF4E8] px-3 py-1 text-[11px] font-black uppercase tracking-[0.16em] text-[#556555]">
Ingen dato publisert
</span>
)}
</div>
<div className="mt-4 grid gap-4 md:grid-cols-3">
<div className="rounded-[1.4rem] bg-[#F7F9F2] p-4">
<p className="text-[11px] font-black uppercase tracking-[0.18em] text-[#8BC34A]">
Pris
</p>
<p className="mt-2 text-2xl font-black text-[#112015]">
{formatCurrency(listing.vtgPris)}
</p>
</div>
<div className="rounded-[1.4rem] bg-[#F7F9F2] p-4">
<p className="text-[11px] font-black uppercase tracking-[0.18em] text-[#8BC34A]">
Neste kurs
</p>
<p className="mt-2 text-lg font-black text-[#112015]">
{listing.nextCourse?.raw || "Ingen dato publisert"}
</p>
{listing.nextCourse?.status ? (
<span
className={`mt-2 inline-flex rounded-full px-2.5 py-1 text-[10px] font-black uppercase tracking-[0.14em] ${statusTone(
listing.nextCourse.status,
)}`}
>
{listing.nextCourse.status}
</span>
) : null}
</div>
<div className="rounded-[1.4rem] bg-[#F7F9F2] p-4">
<p className="text-[11px] font-black uppercase tracking-[0.18em] text-[#8BC34A]">
Kursdatoer
</p>
<p className="mt-2 text-2xl font-black text-[#112015]">
{listing.hasPublishedDates ? listing.allDates.length : 0}
</p>
<p className="mt-2 text-sm text-[#5B675C]">
{listing.upcomingCourseCount > 0
? `${listing.upcomingCourseCount} kommende`
: listing.hasPublishedDates
? "Kun historiske / utydelige datoer"
: "Ingen kursdatoer registrert"}
</p>
</div>
</div>
{listing.vtgBeskrivelse ? (
<p className="mt-4 max-w-4xl text-sm leading-6 text-[#4E5E4F]">
{listing.vtgBeskrivelse}
</p>
) : (
<p className="mt-4 max-w-4xl text-sm leading-6 text-[#697669]">
Ingen kort kursbeskrivelse er publisert ennå, men du kan fortsatt videre
til klubbside eller innmelding dersom lenke finnes.
</p>
)}
{listing.allDates.length > 1 ? (
<div className="mt-4 flex flex-wrap gap-2">
{listing.allDates.slice(0, 6).map((courseDate, index) => (
<span
key={`${listing.id}-${courseDate.raw}-${index}`}
className={`inline-flex rounded-full px-3 py-1.5 text-[11px] font-black tracking-[0.08em] ${statusTone(
courseDate.status,
)}`}
>
{courseDate.raw}
</span>
))}
</div>
) : null}
</div>
<div className="flex w-full flex-col gap-3 xl:w-auto xl:min-w-[13rem]">
<Link
href={`/golfbaner/${listing.slug}`}
className="inline-flex items-center justify-center rounded-full bg-[#112015] px-5 py-3 text-sm font-black text-white transition hover:bg-[#25312A]"
>
Se klubbside
</Link>
{listing.vtgLenke ? (
<a
href={listing.vtgLenke.split(",")[0].trim()}
target="_blank"
rel="noreferrer"
className="inline-flex items-center justify-center rounded-full border border-[#112015]/10 bg-white px-5 py-3 text-sm font-black text-[#112015] transition hover:border-[#FF5722] hover:text-[#FF5722]"
>
Ta VTG her
</a>
) : null}
<p className="text-center text-xs text-[#697669] xl:text-right">
Sist oppdatert: {formatDate(listing.vtgUpdatedAt)}
</p>
</div>
</div>
</article>
))
) : (
<div className="surface-card rounded-[2rem] p-10 text-center">
<p className="text-[11px] font-black uppercase tracking-[0.18em] text-[#8BC34A]">
Ingen treff
</p>
<h3 className="mt-3 text-2xl font-black text-[#112015]">
Ingen VTG-tilbud matcher filtrene dine
</h3>
<p className="mx-auto mt-3 max-w-2xl text-sm leading-6 text-[#5B675C]">
Prøv et større område, fjern kravet om publiserte datoer eller bytt til en bredere
prisgruppe. Noen klubber publiserer bare pris og lenke.
</p>
</div>
)}
</div>
</section>
</div>
);
}

32
frontend/src/app/vtg/page.tsx Executable file
View file

@ -0,0 +1,32 @@
import { API_URL } from "@/config/constants";
import VtgExplorer from "./VtgExplorer";
import type { FacilityRecord } from "@/app/facilityData";
export const dynamic = "force-dynamic";
export default async function VtgPage() {
let facilities: FacilityRecord[] = [];
try {
const res = await fetch(`${API_URL}/facilities`, {
next: { revalidate: 0 },
cache: "no-store",
});
if (!res.ok) {
throw new Error(`API returnerte status ${res.status}`);
}
const data = await res.json();
facilities = Array.isArray(data) ? data : [];
} catch (error) {
console.error("Kunne ikke hente VTG-data:", error);
facilities = [];
}
return (
<main className="site-shell min-h-screen">
<VtgExplorer facilities={facilities} />
</main>
);
}

View file

@ -72,6 +72,7 @@ export default function Header() {
{ href: "/", label: "Hjem" },
{ href: "/golfbaner", label: "Golfbaner" },
{ href: "/medlemskap", label: "Medlemskap" },
{ href: "/vtg", label: "VTG" },
];
return (