Før endringer i artikkelpubliseringssystemet
This commit is contained in:
parent
cf0f049ea6
commit
d000c87324
7 changed files with 167 additions and 29 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
BIN
2026-04-20 09.33.55 teeoff.no cc7384f6fb3e.jpg
Normal file
BIN
2026-04-20 09.33.55 teeoff.no cc7384f6fb3e.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
|
|
@ -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,11 +902,14 @@ 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) => (
|
||||
<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"
|
||||
>
|
||||
{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"
|
||||
>
|
||||
<Link href={`/golfbaner/${facility.slug}`} className="block shrink-0">
|
||||
<div className="relative h-56 overflow-hidden bg-[#D9DED5] sm:h-60">
|
||||
<Image
|
||||
|
|
@ -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">
|
||||
<span
|
||||
className={`rounded-full px-3 py-1.5 text-[10px] font-extrabold uppercase tracking-[0.15em] ${
|
||||
STATUS_CLASSES[facility.primaryStatus] || STATUS_CLASSES.ukjent
|
||||
}`}
|
||||
>
|
||||
{getStatusLabel(facility.primaryStatus)}
|
||||
</span>
|
||||
{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[badge.status] || STATUS_CLASSES.ukjent
|
||||
}`}
|
||||
>
|
||||
{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
|
||||
|
|
@ -1009,8 +1047,9 @@ export default function FacilitySearch({
|
|||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -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 || "");
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<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}
|
||||
</span>
|
||||
))}
|
||||
{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">
|
||||
{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>
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
Loading…
Reference in a new issue