Ferdig med å oppdatere detaljvisningen

This commit is contained in:
Erol 2026-04-15 09:19:16 +02:00
parent d166a7ce5d
commit 9e8d622ca4
4 changed files with 75 additions and 38 deletions

View file

@ -71,13 +71,16 @@ const AREA_GROUPS: Record<string, string[]> = {
"oslo-og-akershus": ["akershus", "oslo", "viken"],
};
const COUNTY_FILTER_ALIASES: Record<string, string[]> = {
trondelag: ["trondelag", "nord-trondelag", "sor-trondelag"],
};
const HIERARCHICAL_AREA_OPTIONS = [
{ value: "", label: "Hele Norge" },
{ value: "region:nord-norge", label: "Nord-Norge" },
{ value: "county:finnmark", label: "\u00A0\u00A0\u00A0Finnmark" },
{ value: "county:troms", label: "\u00A0\u00A0\u00A0Troms" },
{ value: "county:nordland", label: "\u00A0\u00A0\u00A0Nordland" },
{ value: "region:midt-norge", label: "Midt-Norge" },
{ value: "county:nord-trondelag", label: "\u00A0\u00A0\u00A0Nord-Trøndelag" },
{ value: "county:sor-trondelag", label: "\u00A0\u00A0\u00A0Sør-Trøndelag" },
{ value: "county:trondelag", label: "\u00A0\u00A0\u00A0Trøndelag" },
@ -86,7 +89,6 @@ const HIERARCHICAL_AREA_OPTIONS = [
{ value: "county:sogn-og-fjordane", label: "\u00A0\u00A0\u00A0Sogn og Fjordane" },
{ value: "county:hordaland", label: "\u00A0\u00A0\u00A0Hordaland" },
{ value: "county:rogaland", label: "\u00A0\u00A0\u00A0Rogaland" },
{ value: "county:vestland", label: "\u00A0\u00A0\u00A0Vestland" },
{ value: "region:sorlandet", label: "Sørlandet" },
{ value: "county:vest-agder", label: "\u00A0\u00A0\u00A0Vest-Agder" },
{ value: "county:aust-agder", label: "\u00A0\u00A0\u00A0Aust-Agder" },
@ -129,6 +131,13 @@ const STATUS_CLASSES: Record<string, string> = {
const normalizeText = (value: unknown) =>
String(value ?? "")
.replace(/[æøå]/gi, (char) => {
const normalized = char.toLowerCase();
if (normalized === "æ") return "ae";
if (normalized === "ø") return "o";
if (normalized === "å") return "a";
return normalized;
})
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
@ -215,13 +224,6 @@ const buildMapUrl = (lat?: number | null, lng?: number | null) => {
return `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`;
};
const toPlainText = (value: string | null | undefined) =>
String(value || "")
.replace(/<[^>]+>/g, " ")
.replace(/&nbsp;/gi, " ")
.replace(/\s+/g, " ")
.trim();
const escapeHtml = (value: string) =>
value
.replace(/&/g, "&amp;")
@ -473,13 +475,15 @@ export default function FacilitySearch({
.filter((word) => word && !stopWords.has(word));
const selectedArea = areaFilter.replace(/^(region:|county:)/, "");
const countyAliases = COUNTY_FILTER_ALIASES[selectedArea];
const matchesSearch = words.every((word) => searchBlob.includes(word));
const matchesArea =
!areaFilter ||
(areaFilter.startsWith("region:") &&
(regions.includes(selectedArea) ||
(AREA_GROUPS[selectedArea] ? AREA_GROUPS[selectedArea].includes(countySlug) : false))) ||
(areaFilter.startsWith("county:") && countySlug === selectedArea);
(areaFilter.startsWith("county:") &&
(countyAliases ? countyAliases.includes(countySlug) : countySlug === selectedArea));
const matchesStatus = !statusFilter || normalizedStatuses.includes(statusFilter);
const matchesHoles = matchesHoleFilter(holeValue, holeFilter);
const matchesSpecial = matchesSpecialFilter(specialFilter, {

View file

@ -71,13 +71,16 @@ export const AREA_GROUPS: Record<string, string[]> = {
"oslo-og-akershus": ["akershus", "oslo", "viken"],
};
const COUNTY_FILTER_ALIASES: Record<string, string[]> = {
trondelag: ["trondelag", "nord-trondelag", "sor-trondelag"],
};
export const HIERARCHICAL_AREA_OPTIONS = [
{ value: "", label: "Hele Norge", slug: "norge" },
{ value: "region:nord-norge", label: "Nord-Norge", slug: "nord-norge" },
{ value: "county:finnmark", label: "Finnmark", slug: "finnmark" },
{ value: "county:troms", label: "Troms", slug: "troms" },
{ value: "county:nordland", label: "Nordland", slug: "nordland" },
{ value: "region:midt-norge", label: "Midt-Norge", slug: "midt-norge" },
{ value: "county:nord-trondelag", label: "Nord-Trøndelag", slug: "nord-trondelag" },
{ value: "county:sor-trondelag", label: "Sør-Trøndelag", slug: "sor-trondelag" },
{ value: "county:trondelag", label: "Trøndelag", slug: "trondelag" },
@ -86,7 +89,6 @@ export const HIERARCHICAL_AREA_OPTIONS = [
{ value: "county:sogn-og-fjordane", label: "Sogn og Fjordane", slug: "sogn-og-fjordane" },
{ value: "county:hordaland", label: "Hordaland", slug: "hordaland" },
{ value: "county:rogaland", label: "Rogaland", slug: "rogaland" },
{ value: "county:vestland", label: "Vestland", slug: "vestland" },
{ value: "region:sorlandet", label: "Sørlandet", slug: "sorlandet" },
{ value: "county:vest-agder", label: "Vest-Agder", slug: "vest-agder" },
{ value: "county:aust-agder", label: "Aust-Agder", slug: "aust-agder" },
@ -118,6 +120,13 @@ export const STATUS_ICON_PATHS: Record<string, string> = {
export const normalizeText = (value: unknown) =>
String(value ?? "")
.replace(/[æøå]/gi, (char) => {
const normalized = char.toLowerCase();
if (normalized === "æ") return "ae";
if (normalized === "ø") return "o";
if (normalized === "å") return "a";
return normalized;
})
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
@ -264,7 +273,8 @@ export const matchesAreaFilter = (facility: EnrichedFacility, areaFilter: string
);
}
if (areaFilter.startsWith("county:")) {
return facility.countySlug === selectedArea;
const aliases = COUNTY_FILTER_ALIASES[selectedArea];
return aliases ? aliases.includes(facility.countySlug) : facility.countySlug === selectedArea;
}
return true;
};

View file

@ -17,7 +17,7 @@ import { useState, useEffect } from 'react';
import { Icon as LeafletIcon } from "leaflet";
import { MapContainer, Marker, Popup, TileLayer } from "react-leaflet";
import { STATUS_MAP, FALLBACK_IMAGE } from "@/config/constants";
import { STATUS_ICON_PATHS, buildMapUrl, getPrimaryStatus, parseJson as parseSharedJson } from "@/app/facilityData";
import { STATUS_ICON_PATHS, buildMapUrl, getPrimaryStatus, parseJson as parseSharedJson, slugify } from "@/app/facilityData";
import Link from 'next/link';
import CourseDisplay from './CourseDisplay';
@ -198,8 +198,28 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
);
const sidebarLinkClass = "group flex items-center gap-4 text-[#11280f] transition-colors hover:text-[#ff5722]";
const resourceBtnClass = "btn-panel p-5";
const sectionNavButtonClass = "btn btn-sm btn-secondary whitespace-nowrap";
const sidebarLinkTextClass = "transition-colors group-hover:text-[#ff5722]";
const detailMetaLinkClass =
"text-[10px] font-black uppercase tracking-widest text-[#7ca982] transition-colors hover:text-[#ff5722]";
const resourceBtnClass = "btn-panel group w-full p-5";
const sectionNavButtonClass =
"btn btn-secondary h-10 shrink-0 whitespace-nowrap px-3 text-[9px] tracking-[0.12em] md:h-auto md:px-4 md:text-[11px] md:tracking-[0.16em]";
const mobileSectionNavButtonClass =
"whitespace-nowrap text-[8px] font-black uppercase tracking-[0.04em] text-gray-500 transition-colors hover:text-[#11280f]";
const placeSlug = slugify(facility.county || "") || "norge";
const sectionNavItems = [
{ id: 'intro', label: 'Info', showOnMobile: false },
{ id: 'weather', label: 'Vær', showOnMobile: false },
{ id: 'details', label: 'Detaljer', showOnMobile: true },
mapUrl ? { id: 'map', label: 'Kart', showOnMobile: true } : null,
facility.video_url ? { id: 'video', label: 'Video', showOnMobile: true } : null,
{ id: 'prices', label: 'Priser', showOnMobile: true },
hasVtg ? { id: 'vtg', label: 'VTG', showOnMobile: true } : null,
{ id: 'scorecards', label: 'Scorekort', showOnMobile: true },
].filter(
(item): item is { id: string; label: string; showOnMobile: boolean } => Boolean(item)
);
const mobileSectionNavItems = sectionNavItems.filter((item) => item.showOnMobile);
useEffect(() => {
if (gallery.length <= 1) return;
@ -267,16 +287,21 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
</div>
{/* 2. STICKY NAV */}
<nav className="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-gray-100 shadow-sm overflow-x-auto">
<div className="max-w-[1200px] mx-auto px-4 md:px-6 flex min-w-max items-center gap-4 md:gap-6 lg:gap-8 xl:gap-10 h-16 lg:h-20 text-[10px] md:text-xs lg:text-sm xl:text-[15px] font-black uppercase tracking-[0.16em] text-gray-500">
<button className={sectionNavButtonClass} onClick={() => scrollTo('intro')}>Info</button>
<button className={sectionNavButtonClass} onClick={() => scrollTo('weather')}>Vær</button>
<button className={sectionNavButtonClass} onClick={() => scrollTo('details')}>Detaljer</button>
{mapUrl && <button className={sectionNavButtonClass} onClick={() => scrollTo('map')}>Kart</button>}
{facility.video_url && <button className={sectionNavButtonClass} onClick={() => scrollTo('video')}>Video</button>}
<button className={sectionNavButtonClass} onClick={() => scrollTo('prices')}>Priser</button>
{hasVtg && <button className={sectionNavButtonClass} onClick={() => scrollTo('vtg')}>VTG</button>}
<button className={sectionNavButtonClass} onClick={() => scrollTo('scorecards')}>Scorekort</button>
<nav className="sticky top-0 z-50 border-b border-gray-100 bg-white/95 shadow-sm backdrop-blur-md">
<div className="mx-auto max-w-[1200px]">
<div className="flex flex-wrap items-center justify-center gap-x-1.5 gap-y-0.5 px-2 py-2 md:hidden">
{mobileSectionNavItems.map((item, index) => (
<div key={item.id} className="flex items-center gap-1.5">
{index > 0 && <span className="text-[8px] text-gray-300">/</span>}
<button className={mobileSectionNavButtonClass} onClick={() => scrollTo(item.id)}>{item.label}</button>
</div>
))}
</div>
<div className="hidden items-center gap-2 overflow-x-auto px-3 py-3 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden md:flex md:h-16 md:gap-4 md:px-6 md:py-0 lg:h-20 lg:gap-6 xl:gap-8">
{sectionNavItems.map((item) => (
<button key={item.id} className={sectionNavButtonClass} onClick={() => scrollTo(item.id)}>{item.label}</button>
))}
</div>
</div>
</nav>
@ -306,12 +331,12 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
<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">
<a href={facility.website_url} target="_blank" className={sidebarLinkClass}><Icon children={ICONS.web} /> Besøk nettsiden</a>
<a href={facility.website_url} target="_blank" className={sidebarLinkClass}><Icon children={ICONS.web} /> <span className={sidebarLinkTextClass}>Besøk nettsiden</span></a>
<a href={`tel:${formatPhoneForUrl(facility.phone)}`} className={sidebarLinkClass}>
<Icon children={ICONS.phone} /> {facility.phone || 'Ikke oppgitt'}
<Icon children={ICONS.phone} /> <span className={sidebarLinkTextClass}>{facility.phone || 'Ikke oppgitt'}</span>
</a>
<a href={`mailto:${facility.email}`} className={sidebarLinkClass}>
<Icon children={ICONS.mail} /> <span className="truncate">{facility.email || 'Ikke oppgitt'}</span>
<Icon children={ICONS.mail} /> <span className={`truncate ${sidebarLinkTextClass}`}>{facility.email || 'Ikke oppgitt'}</span>
</a>
<div className="pt-2 border-t border-gray-50 mt-4">
<a href={mapUrl || "#"} target="_blank" rel="noreferrer" className={sidebarLinkClass + " pt-4 leading-tight items-start"}>
@ -337,7 +362,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
)}
<div className="mt-10 pt-6 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">
<Link href={`/sted/${placeSlug}`} className={`${detailMetaLinkClass} flex items-center gap-1`}>
Se alle baner i {facility.county}
</Link>
</div>
@ -414,7 +439,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
<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>
? <a href={facility.nsg_url} target="_blank" className="font-black text-blue-600 transition-colors hover:text-[#ff5722] hover:underline">Ja (Vis Avtale)</a>
: (hasNSG ? <span className="text-blue-600 font-black">Ja</span> : "Nei")
}
</span>
@ -450,7 +475,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
{pakke.pris ? `${pakke.pris},-` : 'Pris på forespørsel'}
</span>
{pakke.lenke && (
<a href={pakke.lenke} target="_blank" rel="noopener noreferrer" className="text-xs font-black uppercase tracking-widest text-blue-600 hover:underline">
<a href={pakke.lenke} target="_blank" rel="noopener noreferrer" className="text-xs font-black uppercase tracking-widest text-blue-600 transition-colors hover:text-[#ff5722] hover:underline">
Les mer
</a>
)}
@ -550,7 +575,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
<span></span> Medlemskap
</h3>
{facility.medlemskap_url && (
<a href={facility.medlemskap_url.split(',')[0].trim()} target="_blank" className="text-[10px] font-black uppercase tracking-widest text-[#7ca982] hover:text-[#11280f] transition-colors">
<a href={facility.medlemskap_url.split(',')[0].trim()} target="_blank" className={detailMetaLinkClass}>
Se alle
</a>
)}
@ -592,7 +617,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
<span>🎫</span> Greenfee
</h3>
{facility.greenfee_url && (
<a href={facility.greenfee_url.split(',')[0].trim()} target="_blank" className="text-[10px] font-black uppercase tracking-widest text-[#7ca982] hover:text-[#11280f] transition-colors">
<a href={facility.greenfee_url.split(',')[0].trim()} target="_blank" className={detailMetaLinkClass}>
Alle priser
</a>
)}
@ -612,7 +637,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
<tr key={idx} className="border-b border-gray-50 hover:bg-gray-50 transition-colors">
<td className="py-3 pr-2 leading-tight">
<span className="block truncate max-w-[150px] sm:max-w-[200px]" title={gf.banenavn}>{gf.banenavn}</span>
<span className="text-[9px] text-gray-400 uppercase tracking-widest block truncate max-w-[150px] sm:max-w-[200px]" title={gf.priskategori}>{gf.priskategori}</span>
<span className="block max-w-[150px] whitespace-normal break-words text-[9px] uppercase tracking-widest text-gray-400 sm:max-w-[200px]" title={gf.priskategori}>{gf.priskategori}</span>
</td>
<td className="py-3 text-right pr-2 text-[#8bc34a] font-black text-sm whitespace-nowrap">
{gf.pris_voksne ? `${gf.pris_voksne},-` : '-'}
@ -698,7 +723,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
{facility.vtg_lenke && (
<a href={facility.vtg_lenke.split(',')[0].trim()} target="_blank" rel="noopener noreferrer" className="btn btn-lg btn-primary mt-4 w-full flex-shrink-0 text-center lg:mt-0 lg:w-auto">
Påmelding
til klubben
</a>
)}
</div>

View file

@ -27,7 +27,6 @@ const placeGroups = [
{
label: "Midt-Norge",
items: [
{ href: "/sted/midt-norge", label: "Midt-Norge" },
{ href: "/sted/nord-trondelag", label: "Nord-Trøndelag" },
{ href: "/sted/sor-trondelag", label: "Sør-Trøndelag" },
{ href: "/sted/trondelag", label: "Trøndelag" },
@ -41,7 +40,6 @@ const placeGroups = [
{ href: "/sted/sogn-og-fjordane", label: "Sogn og Fjordane" },
{ href: "/sted/hordaland", label: "Hordaland" },
{ href: "/sted/rogaland", label: "Rogaland" },
{ href: "/sted/vestland", label: "Vestland" },
],
},
{