Før endringer i artikkelpubliseringssystemet

This commit is contained in:
Erol Haagenrud 2026-04-20 09:39:30 +02:00
parent cf0f049ea6
commit d000c87324
7 changed files with 167 additions and 29 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View file

@ -4,7 +4,7 @@ import { STATUS_MAP } from "@/config/constants";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useMemo, useState, type CSSProperties } from "react";
import { type EnrichedFacility } from "@/app/facilityData";
import { getPublicCourseDisplayName, type EnrichedFacility } from "@/app/facilityData";
type SortMethod = "updated" | "dist" | "alpha";
type Variant = "home" | "catalog";
@ -247,6 +247,38 @@ const formatUpdatedDate = (value: string | null | undefined) => {
const getStatusLabel = (status: string) => STATUS_MAP[status] || "Ukjent";
type StatusBadge = {
label: string;
status: string;
};
const buildFacilityStatusBadges = (statuses: CourseStatus[]): StatusBadge[] => {
const normalizedStatuses = (Array.isArray(statuses) ? statuses : [])
.map((status) => ({
name: String(status?.name || "").trim(),
status: normalizeStatus(status?.status) || "ukjent",
}))
.filter((status) => status.status);
if (normalizedStatuses.length === 0) {
return [{ label: getStatusLabel("ukjent"), status: "ukjent" }];
}
const uniqueStatuses = [...new Set(normalizedStatuses.map((status) => status.status))];
if (normalizedStatuses.length === 1 || uniqueStatuses.length === 1) {
const status = uniqueStatuses[0] || "ukjent";
return [{ label: getStatusLabel(status), status }];
}
return normalizedStatuses.map((course, index) => {
const courseName = getPublicCourseDisplayName(course.name, index, normalizedStatuses.length);
return {
label: courseName ? `${courseName}: ${getStatusLabel(course.status)}` : getStatusLabel(course.status),
status: course.status,
};
});
};
const buildMapUrl = (lat?: number | null, lng?: number | null) => {
if (typeof lat !== "number" || typeof lng !== "number") return null;
return `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`;
@ -505,7 +537,7 @@ export default function FacilitySearch({
const statuses =
Array.isArray(rawStatuses) && rawStatuses.length > 0
? rawStatuses
: [{ status: "ukjent", name: "Hovedbane" }];
: [{ status: "ukjent", name: "" }];
const countySlug = slugify(facility.county || "");
const regions = getFacilityRegions(facility.county || "");
@ -870,7 +902,10 @@ export default function FacilitySearch({
</div>
) : (
<div className="mt-6 grid grid-cols-1 gap-5 md:grid-cols-2 2xl:grid-cols-3">
{processedFacilities.map((facility) => (
{processedFacilities.map((facility) => {
const statusBadges = buildFacilityStatusBadges(facility.statuses);
return (
<article
key={facility.id}
className="surface-card group flex h-full flex-col overflow-hidden rounded-[2rem] transition hover:-translate-y-1 hover:shadow-xl"
@ -888,13 +923,16 @@ export default function FacilitySearch({
<div className="absolute left-4 top-4 flex max-w-[calc(100%-7rem)] flex-col items-start gap-2">
<div className="flex flex-wrap gap-2">
{statusBadges.map((badge) => (
<span
key={badge.label}
className={`rounded-full px-3 py-1.5 text-[10px] font-extrabold uppercase tracking-[0.15em] ${
STATUS_CLASSES[facility.primaryStatus] || STATUS_CLASSES.ukjent
STATUS_CLASSES[badge.status] || STATUS_CLASSES.ukjent
}`}
>
{getStatusLabel(facility.primaryStatus)}
{badge.label}
</span>
))}
{facility.hasGolfamore && (
<span className="rounded-full bg-[#FF5722] px-3 py-1.5 text-[10px] font-extrabold uppercase tracking-[0.15em] text-white">
Golfamore
@ -1010,7 +1048,8 @@ export default function FacilitySearch({
</div>
</div>
</article>
))}
);
})}
</div>
)}
</section>

View file

@ -146,6 +146,35 @@ export const normalizeStatus = (value: unknown) =>
.replace(/[^a-z0-9_]+/g, "")
.trim();
const GENERIC_SINGLE_COURSE_NAMES = new Set(["hovedbane", "hovedbanen"]);
export const getPublicCourseDisplayName = (name: unknown, index: number, total: number) => {
const trimmedName = String(name ?? "").trim();
const normalizedName = normalizeText(trimmedName).replace(/\s+/g, "");
if (total <= 1 && GENERIC_SINGLE_COURSE_NAMES.has(normalizedName)) {
return "";
}
if (trimmedName) {
return trimmedName;
}
if (total <= 1) {
return "";
}
if (index === 0) {
return "Hovedbane";
}
if (total === 2) {
return "Sekundærbane";
}
return `Sekundærbane ${index}`;
};
export const slugify = (value: unknown) =>
normalizeText(value)
.replace(/\s+/g, "-")
@ -237,7 +266,7 @@ export const enrichFacilities = (
const statuses =
Array.isArray(rawStatuses) && rawStatuses.length > 0
? rawStatuses
: [{ status: "ukjent", name: "Hovedbane" }];
: [{ status: "ukjent", name: "" }];
const holeValue = String(amenities.antall_hull || "").trim();
const countySlug = slugify(facility.county || "");
const regions = getFacilityRegions(facility.county || "");

View file

@ -28,7 +28,7 @@ const getTeeTheme = (label: string) => {
return { header: "bg-gray-200 text-gray-700", col: "bg-gray-100/60", text: "text-gray-600" };
};
export default function CourseDisplay({ course }: { course: any }) {
export default function CourseDisplay({ course, courseDisplayName = "" }: { course: any; courseDisplayName?: string }) {
const [hcp, setHcp] = useState("15.0");
const [gender, setGender] = useState<'herrer' | 'damer'>('herrer');
const [selectedTeeIndex, setSelectedTeeIndex] = useState(0);
@ -134,7 +134,9 @@ export default function CourseDisplay({ course }: { course: any }) {
{/* HEADER / KALKULATOR */}
<div className="flex min-w-0 flex-col items-stretch justify-between gap-5 border-b border-gray-100 bg-white p-4 sm:p-5 md:flex-row md:items-center md:gap-8 md:p-12">
<div className="min-w-0 text-left">
<h2 className="break-words text-4xl font-black tracking-tighter text-[#11280f] sm:text-5xl">{course.name}</h2>
{courseDisplayName ? (
<h2 className="break-words text-4xl font-black tracking-tighter text-[#11280f] sm:text-5xl">{courseDisplayName}</h2>
) : null}
<p className="text-[#7ca982] font-black uppercase text-xs tracking-[0.2em] mt-2 mb-1">
Par {course.par} {course.length_meters || '--'} meter
</p>

View file

@ -16,7 +16,7 @@
import { useState, useEffect } from 'react';
import dynamic from "next/dynamic";
import { STATUS_MAP, FALLBACK_IMAGE } from "@/config/constants";
import { STATUS_ICON_PATHS, buildMapUrl, getPrimaryStatus, parseJson as parseSharedJson, slugify } from "@/app/facilityData";
import { STATUS_ICON_PATHS, buildMapUrl, getPrimaryStatus, getPublicCourseDisplayName, parseJson as parseSharedJson, slugify } from "@/app/facilityData";
import Link from 'next/link';
import CourseDisplay from './CourseDisplay';
import FacilityFeedbackForm from './FacilityFeedbackForm';
@ -277,11 +277,16 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
{/* BANESTATUS BADGES */}
<div className="absolute top-8 right-8 z-40 flex flex-col items-end gap-2">
<div className="flex flex-wrap justify-end gap-2">
{activeCourses.map((c: any) => (
{activeCourses.map((c: any, index: number) => {
const courseName = getPublicCourseDisplayName(c.name, index, activeCourses.length);
const label = STATUS_MAP[c.status] || c.status;
return (
<span key={c.id} className="px-3 py-1.5 rounded-lg text-[10px] font-black uppercase bg-[#7ca982] text-white shadow-xl">
{c.name.toUpperCase()}: {STATUS_MAP[c.status] || c.status}
{courseName ? `${courseName.toUpperCase()}: ${label}` : label}
</span>
))}
);
})}
</div>
{facility.status_updated_at && (
<span className="text-white/60 text-[10px] uppercase font-black tracking-widest bg-black/20 px-2 py-1 rounded">
@ -769,9 +774,9 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
<section id="scorecards" className="pt-10 space-y-20 overflow-hidden">
<h3 className="text-center text-3xl md:text-5xl font-black uppercase tracking-tighter">Scorekort</h3>
<div className="w-full flex flex-col items-center gap-20">
{activeCourses.map((c: any) => (
{activeCourses.map((c: any, index: number) => (
<div key={c.id} className="w-full overflow-x-auto md:overflow-visible no-scrollbar">
<div className="min-w-[800px] md:min-w-0"><CourseDisplay course={c} /></div>
<div className="min-w-[800px] md:min-w-0"><CourseDisplay course={c} courseDisplayName={getPublicCourseDisplayName(c.name, index, activeCourses.length)} /></div>
</div>
))}
</div>

View file

@ -7,6 +7,7 @@ import { MapContainer, Marker, Popup, TileLayer, useMap } from "react-leaflet";
import {
buildMapUrl,
formatUpdatedDate,
getPublicCourseDisplayName,
getStatusLabel,
parseJson,
STATUS_ICON_PATHS,
@ -17,6 +18,11 @@ type PlaceMapLeafletProps = {
facilities: EnrichedFacility[];
};
type PopupStatusBadge = {
label: string;
status: string;
};
const markerIconCache: Record<string, Icon> = {};
const getMarkerIcon = (status: string) => {
@ -32,6 +38,53 @@ const getMarkerIcon = (status: string) => {
return markerIconCache[key];
};
const STATUS_BADGE_CLASSES: Record<string, string> = {
aapen: "bg-[#8BC34A] text-white",
aapen_med_vintergreener: "bg-[#D2A63A] text-[#112015]",
stenger_snart: "bg-[#FF5722] text-white",
aapner_snart: "bg-sky-600 text-white",
stengt: "bg-[#B6473D] text-white",
under_utvikling: "bg-slate-600 text-white",
nedlagt: "bg-[#112015] text-white",
ukjent: "bg-[#D9DED5] text-[#112015]",
};
const normalizeStatus = (value: unknown) =>
String(value ?? "")
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/\s+/g, "_")
.replace(/[^a-z0-9_]+/g, "")
.trim() || "ukjent";
const buildPopupStatusBadges = (statuses: Array<{ name?: string; status?: string }>): PopupStatusBadge[] => {
const normalizedStatuses = (Array.isArray(statuses) ? statuses : [])
.map((status) => ({
name: String(status?.name || "").trim(),
status: normalizeStatus(status?.status),
}))
.filter((status) => status.status);
if (normalizedStatuses.length === 0) {
return [{ label: getStatusLabel("ukjent"), status: "ukjent" }];
}
const uniqueStatuses = [...new Set(normalizedStatuses.map((status) => status.status))];
if (normalizedStatuses.length === 1 || uniqueStatuses.length === 1) {
const status = uniqueStatuses[0] || "ukjent";
return [{ label: getStatusLabel(status), status }];
}
return normalizedStatuses.map((course, index) => {
const courseName = getPublicCourseDisplayName(course.name, index, normalizedStatuses.length);
return {
label: courseName ? `${courseName}: ${getStatusLabel(course.status)}` : getStatusLabel(course.status),
status: course.status,
};
});
};
function ShiftScrollZoomGuard() {
const map = useMap();
@ -180,6 +233,7 @@ export default function PlaceMapLeaflet({ facilities }: PlaceMapLeafletProps) {
const socialLinks = parseJson<Array<{ platform?: string; url?: string }>>(facility.social_links, []);
const facebook = socialLinks.find((entry) => entry.platform?.toLowerCase() === "facebook")?.url;
const instagram = socialLinks.find((entry) => entry.platform?.toLowerCase() === "instagram")?.url;
const statusBadges = buildPopupStatusBadges(facility.statuses);
return (
<Marker
@ -198,8 +252,17 @@ export default function PlaceMapLeaflet({ facilities }: PlaceMapLeafletProps) {
</p>
</div>
<div className="inline-flex rounded-full bg-[#F3F6EE] px-3 py-1 text-[10px] font-extrabold uppercase tracking-[0.18em] text-[#112015]">
{getStatusLabel(facility.primaryStatus)}
<div className="flex flex-wrap gap-2">
{statusBadges.map((badge) => (
<div
key={badge.label}
className={`rounded-full px-3 py-1 text-[10px] font-extrabold uppercase tracking-[0.18em] ${
STATUS_BADGE_CLASSES[badge.status] || STATUS_BADGE_CLASSES.ukjent
}`}
>
{badge.label}
</div>
))}
</div>
{facility.status_updated_at && (