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

146 lines
4.6 KiB
TypeScript
Executable file

import type { Metadata } from "next";
import { notFound } from "next/navigation";
import FacilitySearch from "@/app/FacilitySearch";
import PlaceMap from "@/components/PlaceMap";
import {
type FacilityRecord,
enrichFacilities,
filterFacilitiesByArea,
getAvailablePlaceConfigs,
getPlaceConfigFromSlug,
} from "@/app/facilityData";
import { API_URL } from "@/config/constants";
import {
createBreadcrumbJsonLd,
createCollectionPageJsonLd,
createItemListJsonLd,
createPageMetadata,
} from "@/app/seo";
export const dynamicParams = true;
export const dynamic = "force-dynamic";
export async function generateStaticParams() {
return getAvailablePlaceConfigs().map((slug) => ({ slug }));
}
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,
description: `${place.intro} TeeOff samler golfbaner i ${place.label} med oppdatert banestatus og baneprofiler.`,
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 {
const res = await fetch(`${API_URL}/facilities`, {
next: { revalidate: 0 },
cache: "no-store",
});
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 facilitiesInPlace = filterFacilitiesByArea(enrichFacilities(safeData), place.areaFilter);
const collectionJsonLd = createCollectionPageJsonLd({
name: place.title,
description: place.intro,
path: `/sted/${slug}`,
});
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,
})),
});
const breadcrumbJsonLd = createBreadcrumbJsonLd([
{ name: "Hjem", path: "/" },
{ name: "Steder", path: "/sted/norge" },
{ name: place.label, path: `/sted/${slug}` },
]);
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(collectionJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(itemListJsonLd) }}
/>
<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">
{facilitiesInPlace.length} golfbaner
</span>
<span className="rounded-full bg-[#25312A] px-4 py-2 text-[11px] font-extrabold uppercase tracking-[0.18em] text-white">
Kart og liste i samme visning
</span>
</div>
</div>
<div className="mt-8">
<PlaceMap facilities={facilitiesInPlace} placeLabel={place.label} />
</div>
</section>
<FacilitySearch
initialFacilities={safeData}
variant="catalog"
title={place.title}
intro={`Filtrer golfbanene i ${place.label} videre etter banestatus, antall hull og andre egenskaper.`}
fixedAreaFilter={place.areaFilter}
hideTitleBlock
/>
</main>
</>
);
}