Nye-TeeOff/frontend/src/app/sted/[slug]/page.tsx

218 lines
9.1 KiB
TypeScript
Raw Normal View History

2026-04-12 22:07:51 +02:00
import type { Metadata } from "next";
import { notFound } from "next/navigation";
2026-04-19 12:33:05 +02:00
import PlaceExplorer from "@/app/sted/[slug]/PlaceExplorer";
import {
buildPlaceAverageComparison,
buildPlaceStats,
buildPlaceStatsIntro,
formatPlaceCount,
formatPlaceCurrency,
type FacilityRecord,
enrichFacilities,
filterFacilitiesByArea,
getPlaceConfigFromSlug,
getPlacePreposition,
} from "@/app/facilityData";
import { API_URL } from "@/config/constants";
2026-04-12 22:07:51 +02:00
import {
createBreadcrumbJsonLd,
createCollectionPageJsonLd,
2026-04-18 09:00:16 +02:00
createItemListJsonLd,
2026-04-12 22:07:51 +02:00
createPageMetadata,
} from "@/app/seo";
export const dynamicParams = true;
2026-04-22 06:48:15 +02:00
export const revalidate = 3600;
2026-04-12 22:07:51 +02:00
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const place = getPlaceConfigFromSlug(slug);
if (!place) {
return createPageMetadata({
title: "Sted ikke funnet",
description: "Denne stedssiden finnes ikke på TeeOff.",
path: `/sted/${slug}`,
});
}
return createPageMetadata({
title: place.title,
2026-04-18 09:00:16 +02:00
description: `${place.intro} TeeOff samler golfbaner i ${place.label} med oppdatert banestatus og baneprofiler.`,
2026-04-12 22:07:51 +02:00
path: `/sted/${slug}`,
});
}
export default async function PlacePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const place = getPlaceConfigFromSlug(slug);
if (!place) {
notFound();
}
let facilities: FacilityRecord[] = [];
try {
2026-04-22 06:48:15 +02:00
const res = await fetch(`${API_URL}/facilities?summary=1`, {
next: { revalidate },
});
if (!res.ok) {
throw new Error(`API returnerte status ${res.status}`);
}
facilities = await res.json();
} catch (error) {
console.error("Kritisk feil ved henting av sted-data:", error);
facilities = [];
}
const safeData = Array.isArray(facilities) ? facilities : [];
const enrichedFacilities = enrichFacilities(safeData);
const facilitiesInPlace = filterFacilitiesByArea(enrichedFacilities, place.areaFilter);
const placeStats = buildPlaceStats(facilitiesInPlace);
const nationalStats = buildPlaceStats(enrichedFacilities);
const placeStatsIntro = buildPlaceStatsIntro(place.label, placeStats);
const isNationalPlace = place.slug === "norge";
const placePreposition = isNationalPlace ? "i" : getPlacePreposition(place.label);
const greenfeeComparison = isNationalPlace
? null
: buildPlaceAverageComparison(placeStats.avgPrimetimeGreenfee, nationalStats.avgPrimetimeGreenfee);
const membershipComparison = isNationalPlace
? null
: buildPlaceAverageComparison(placeStats.avgStandardMembership, nationalStats.avgStandardMembership);
const holeDistributionParts = [
`${formatPlaceCount(placeStats.par3HoleCount)} par 3-hull`,
`${formatPlaceCount(placeStats.par4HoleCount)} par 4-hull`,
`${formatPlaceCount(placeStats.par5HoleCount)} par 5-hull`,
placeStats.par6HoleCount > 0 ? `${formatPlaceCount(placeStats.par6HoleCount)} par 6-hull` : null,
].filter((part): part is string => Boolean(part));
const holeDistributionText =
holeDistributionParts.length > 1
? `${holeDistributionParts.slice(0, -1).join(", ")} og ${holeDistributionParts.at(-1)}`
: holeDistributionParts[0] ?? null;
const shortestLongestText =
placeStats.shortestHoleMeters !== null && placeStats.longestHoleMeters !== null
? `Det korteste golfhullet ${placePreposition} ${place.label} er ${placeStats.shortestHoleMeters} meter, mens det lengste er ${placeStats.longestHoleMeters} meter.`
: null;
2026-04-12 22:07:51 +02:00
const collectionJsonLd = createCollectionPageJsonLd({
name: place.title,
description: place.intro,
path: `/sted/${slug}`,
});
2026-04-18 09:00:16 +02:00
const itemListJsonLd = createItemListJsonLd({
name: place.title,
path: `/sted/${slug}`,
items: facilitiesInPlace
.filter((facility) => facility?.slug && facility?.name)
.map((facility) => ({
name: facility.name,
path: `/golfbaner/${facility.slug}`,
description: facility.description,
})),
});
2026-04-12 22:07:51 +02:00
const breadcrumbJsonLd = createBreadcrumbJsonLd([
{ name: "Hjem", path: "/" },
{ name: "Steder", path: "/sted/norge" },
{ name: place.label, path: `/sted/${slug}` },
]);
return (
2026-04-12 22:07:51 +02:00
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(collectionJsonLd) }}
/>
2026-04-18 09:00:16 +02:00
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(itemListJsonLd) }}
/>
2026-04-12 22:07:51 +02:00
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
/>
<main className="site-shell min-h-screen">
<section className="mx-auto max-w-[1400px] px-4 py-8 sm:px-6 sm:py-10 lg:px-8 lg:py-12">
<div className="max-w-4xl">
<p className="mb-3 text-[11px] font-extrabold uppercase tracking-[0.3em] text-[#6FA786]">Steder</p>
<h1 className="section-title text-4xl text-[#112015] sm:text-5xl lg:text-6xl">{place.title}</h1>
<p className="mt-4 max-w-3xl text-base leading-8 text-[#617063]">{place.intro}</p>
<div className="mt-5 flex flex-wrap gap-3">
<span className="rounded-full bg-white px-4 py-2 text-[11px] font-extrabold uppercase tracking-[0.18em] text-[#112015] shadow-sm">
{placeStats.facilityCount} golfanlegg
2026-04-12 22:07:51 +02:00
</span>
</div>
</div>
<section className="mt-8 overflow-hidden rounded-[2rem] border border-[#D7DED0] bg-white shadow-sm">
<div className="px-5 py-6 sm:px-6 lg:px-8 lg:py-8">
<div className="max-w-5xl">
<p className="text-[11px] font-extrabold uppercase tracking-[0.28em] text-[#6FA786]">Nøkkeltall</p>
<h2 className="mt-2 text-3xl text-[#112015] sm:text-4xl">
{isNationalPlace ? "Fakta om golfanleggene i Norge" : `Fakta om golfanleggene ${placePreposition} ${place.label}`}
</h2>
<p className="mt-4 text-base leading-8 text-[#213127]">{placeStatsIntro}</p>
</div>
</div>
<div className="grid gap-px border-t border-[#D7DED0] bg-[#D7DED0] lg:grid-cols-3">
<div className="bg-white/88 px-5 py-5 sm:px-6 lg:px-8">
<p className="text-[11px] font-extrabold uppercase tracking-[0.2em] text-[#6FA786]">Greenfee</p>
<p className="mt-2 text-2xl font-black text-[#112015]">
{placeStats.avgPrimetimeGreenfee !== null ? formatPlaceCurrency(placeStats.avgPrimetimeGreenfee) : "Mangler grunnlag"}
</p>
<p className="mt-2 text-sm leading-6 text-[#4F6052]">
{placeStats.avgPrimetimeGreenfee !== null
? "Gjennomsnittlig primetime-pris i høysesong."
: "Ingen tilstrekkelige greenfee-data tilgjengelig."}
</p>
{greenfeeComparison ? <p className="mt-2 text-sm font-bold text-[#112015]">{greenfeeComparison}</p> : null}
</div>
<div className="bg-white/88 px-5 py-5 sm:px-6 lg:px-8">
<p className="text-[11px] font-extrabold uppercase tracking-[0.2em] text-[#6FA786]">Medlemskap</p>
<p className="mt-2 text-2xl font-black text-[#112015]">
{placeStats.avgStandardMembership !== null ? formatPlaceCurrency(placeStats.avgStandardMembership) : "Mangler grunnlag"}
</p>
<p className="mt-2 text-sm leading-6 text-[#4F6052]">
{placeStats.avgStandardMembership !== null
? "Gjennomsnittlig standard medlemskap med spillerett."
: "Ingen tilstrekkelige medlemskapsdata tilgjengelig."}
</p>
{membershipComparison ? <p className="mt-2 text-sm font-bold text-[#112015]">{membershipComparison}</p> : null}
</div>
<div className="bg-white/88 px-5 py-5 sm:px-6 lg:px-8">
<p className="text-[11px] font-extrabold uppercase tracking-[0.2em] text-[#6FA786]">Hullstatistikk</p>
<p className="mt-2 text-sm leading-7 text-[#112015]">
{formatPlaceCount(placeStats.hole18Count, "neuter")} 18-hullsanlegg
<br />
{formatPlaceCount(placeStats.hole9Count, "neuter")} 9-hullsanlegg
<br />
{formatPlaceCount(placeStats.hole6Count, "neuter")} 6-hullsanlegg
<br />
{formatPlaceCount(placeStats.hole27PlusCount, "neuter")} anlegg med 27 hull eller mer
</p>
{holeDistributionText ? <p className="mt-3 text-sm leading-6 text-[#4F6052]">{holeDistributionText}.</p> : null}
{shortestLongestText ? <p className="mt-2 text-sm leading-6 text-[#4F6052]">{shortestLongestText}</p> : null}
</div>
</div>
</section>
2026-04-19 12:33:05 +02:00
<PlaceExplorer
2026-04-22 06:48:15 +02:00
facilities={facilitiesInPlace}
2026-04-19 12:33:05 +02:00
placeLabel={place.label}
placeAreaFilter={place.areaFilter}
placeTitle={place.title}
/>
2026-04-12 22:07:51 +02:00
</section>
</main>
</>
);
}