95 lines
5.8 KiB
Text
95 lines
5.8 KiB
Text
|
|
"use client";
|
||
|
|
import { STATUS_MAP } from "@/config/constants";
|
||
|
|
import { useState, useEffect, useMemo } from 'react';
|
||
|
|
import Link from 'next/link';
|
||
|
|
|
||
|
|
function getDistance(lat1: number, lon1: number, lat2: number, lon2: number) {
|
||
|
|
try {
|
||
|
|
const R = 6371;
|
||
|
|
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||
|
|
const dLon = (lon2 - lon1) * Math.PI / 180;
|
||
|
|
const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2);
|
||
|
|
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||
|
|
} catch (e) { return Infinity; }
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function FacilitySearch({ initialFacilities }: { initialFacilities: any[] }) {
|
||
|
|
const [searchQuery, setSearchQuery] = useState("");
|
||
|
|
const [userLocation, setUserLocation] = useState<{ lat: number, lng: number } | null>(null);
|
||
|
|
const [mounted, setMounted] = useState(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
setMounted(true);
|
||
|
|
if ("geolocation" in navigator) {
|
||
|
|
navigator.geolocation.getCurrentPosition(p => setUserLocation({ lat: p.coords.latitude, lng: p.coords.longitude }));
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const processed = useMemo(() => {
|
||
|
|
if (!mounted || !Array.isArray(initialFacilities)) return [];
|
||
|
|
const words = searchQuery.toLowerCase().trim().split(/\s+/).filter(w => w.length > 0);
|
||
|
|
|
||
|
|
return initialFacilities.map(f => {
|
||
|
|
// Skuddsikker status-vask
|
||
|
|
const raw = Array.isArray(f.course_statuses) ? f.course_statuses : [];
|
||
|
|
const statuses = raw.filter((s: any) => s && s.status && s.status !== 'finnes_ingen_bane_to' && s.name !== 'Bane 2');
|
||
|
|
|
||
|
|
const dist = userLocation && f.lat && f.lng ? getDistance(userLocation.lat, userLocation.lng, f.lat, f.lng) : Infinity;
|
||
|
|
const searchBlob = `${f.name} ${f.city} ${f.county} ${statuses.map((s:any) => s.name + s.status).join(" ")}`.toLowerCase();
|
||
|
|
const matches = words.every(w => searchBlob.includes(w));
|
||
|
|
|
||
|
|
return { ...f, dist, statuses, matches };
|
||
|
|
})
|
||
|
|
.filter(f => f.matches)
|
||
|
|
.sort((a, b) => (userLocation && a.dist !== b.dist ? a.dist - b.dist : (a.name || "").localeCompare(b.name || "", 'nb')));
|
||
|
|
}, [searchQuery, initialFacilities, userLocation, mounted]);
|
||
|
|
|
||
|
|
if (!mounted) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="max-w-7xl mx-auto p-6 -mt-8 relative z-40">
|
||
|
|
<div className="text-center mb-4">
|
||
|
|
<span className="text-[10px] uppercase font-black text-[#7ca982] bg-white px-4 py-1.5 rounded-full shadow-md border border-[#f1f7ed]">
|
||
|
|
{userLocation ? "GPS AKTIV" : "SORTERER ALFABETISK"} • {processed.length} BANER
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<input className="w-full p-6 rounded-3xl shadow-2xl mb-12 text-gray-900 border-none ring-1 ring-black/5 text-xl outline-none focus:ring-2 focus:ring-[#7ca982] transition-all bg-white" placeholder='Søk baner...' value={searchQuery} onChange={e => setSearchQuery(e.target.value)} />
|
||
|
|
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||
|
|
{processed.map((f: any) => (
|
||
|
|
<Link href={`/golfbaner/${f.slug}`} key={f.id} className="bg-white rounded-[2.5rem] overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-300 border border-gray-100 flex flex-col group">
|
||
|
|
<div className="h-48 relative bg-gray-200 overflow-hidden">
|
||
|
|
<img src={f.image_url || "/Toppbilde-standard.jpg"} className="w-full h-full object-cover transition duration-700 group-hover:scale-105" alt={f.name} />
|
||
|
|
{f.dist !== Infinity && <div className="absolute top-4 right-4 bg-white/90 backdrop-blur-sm px-3 py-1.5 rounded-xl text-xs font-black shadow-lg text-gray-800">{Math.round(f.dist)} km unna</div>}
|
||
|
|
<div className="absolute top-4 left-4 flex flex-col items-start gap-2 z-20">
|
||
|
|
{f.statuses.map((s: any, idx: number) => {
|
||
|
|
const raw = (s.status || "ukjent").toLowerCase();
|
||
|
|
let color = "bg-gray-500 text-white";
|
||
|
|
if (raw.includes('aapen') && !raw.includes('vinter')) color = "bg-green-600 text-white";
|
||
|
|
else if (raw.includes('vinter')) color = "bg-emerald-400 text-gray-900";
|
||
|
|
else if (raw.includes('snart')) color = "bg-yellow-500 text-gray-900";
|
||
|
|
else if (raw.includes('stengt')) color = "bg-red-600 text-white";
|
||
|
|
return <div key={idx} className={`px-3 py-1.5 rounded-xl text-[10px] font-black uppercase shadow-lg border border-white/10 backdrop-blur-sm ${color}`}>{f.statuses.length > 1 ? `${s.name}: ${STATUS_MAP[raw] || raw}` : (STATUS_MAP[raw] || raw)}</div>;
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="p-8 flex flex-col flex-grow">
|
||
|
|
<h3 className="font-black text-2xl text-gray-900 mb-1 group-hover:text-[#7ca982] transition-colors">{f.name}</h3>
|
||
|
|
<p className="text-gray-400 text-sm font-bold uppercase tracking-widest">{f.city}{f.county ? ` • ${f.county}` : ''}</p>
|
||
|
|
<div className="pt-6 border-t border-gray-50 flex justify-between items-center mt-auto">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<span className="bg-[#f1f7ed] text-[#7ca982] px-3 py-1 rounded-lg text-xs font-black uppercase">{f.amenities?.antall_hull || f.holes || '--'} Hull</span>
|
||
|
|
{f.golfamore && <div className="bg-orange-500 text-white w-5 h-5 flex items-center justify-center rounded-md text-[8px] font-black" title="GolfAmore">G</div>}
|
||
|
|
{f.nsg_data?.url && <div className="bg-blue-600 text-white w-5 h-5 flex items-center justify-center rounded-md text-[8px] font-black" title="SeniorGolf">S</div>}
|
||
|
|
</div>
|
||
|
|
<span className="text-[#7ca982] font-black text-sm uppercase group-hover:translate-x-1 transition-transform">Se bane →</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Link>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|