130 lines
No EOL
5.6 KiB
Text
130 lines
No EOL
5.6 KiB
Text
"use client";
|
|
/**
|
|
* TEE OFF SYSTEM INSTRUCTIONS - HERO SLIDER v2.4
|
|
* ---------------------------------------------------------------------------
|
|
* REGEL 1: Kun baner med status 'aapen', 'aapner_snart', 'stenger_snart'
|
|
* eller 'aapen_med_vintergreener' skal prioriteres.
|
|
* REGEL 2: Baner med status 'nedlagt' eller 'under_utvikling' skal ALDRI vises.
|
|
* REGEL 3: Baner med generiske bilder (inneholder 'standard') skal ALDRI vises.
|
|
* REGEL 4: MANUELL EKSKLUDERING: Slugs i MANUAL_EXCLUSION_LIST skal aldri vises.
|
|
* REGEL 5: Slideren skal vise nøyaktig 5 baner.
|
|
* REGEL 6: Maks høyde er låst til 624px. Ingen badges.
|
|
* REGEL 7: Typografi: Nedjustert fontstørrelse (4xl mobil / 7xl desktop) for eleganse.
|
|
* REGEL 8: Utvalget skal være stabilt i én time (Hourly Seed) før det refreshes.
|
|
* ---------------------------------------------------------------------------
|
|
*/
|
|
|
|
import { useState, useEffect, useMemo } from 'react';
|
|
import Link from 'next/link';
|
|
|
|
const MANUAL_EXCLUSION_LIST = [
|
|
'alsten-golfklubb', 'askim-golfklubb', 'bergen-golfklubb', 'eidskog-golfklubb',
|
|
'eiker-golfklubb', 'floro-golfklubb', 'garder-golfklubb', 'hafjell-golfklubb',
|
|
'halden-golfklubb', 'haugesund-golfklubb', 'hinnoy-golfklubb', 'hitra-golfklubb',
|
|
'hurum-golfklubb', 'imjelt-pitch-putt', 'karmoy-golfklubb', 'kristiansund-og-omegn-golfklubb',
|
|
'lommedalen-golfklubb', 'laerdal-golfklubb', 'moa-golfsenter', 'modum-golfklubb',
|
|
'nes-golfklubb-09', 'nittedal-golfklubb', 'selbu-golfklubb', 'stryn-golfklubb',
|
|
'sunnfjord-golfklubb', 'tysnes-golfklubb', 'vanylven-golfklubb', 'vesteralen-golfklubb',
|
|
'vestlia-golf'
|
|
];
|
|
|
|
export default function HeroSlider({ facilities }: { facilities: any[] }) {
|
|
const [currentIndex, setCurrentSlide] = useState(0);
|
|
|
|
const sliderItems = useMemo(() => {
|
|
if (!Array.isArray(facilities) || facilities.length === 0) return [];
|
|
|
|
const preferredStatuses = ['aapen', 'aapner_snart', 'stenger_snart', 'aapen_med_vintergreener'];
|
|
const forbiddenStatuses = ['nedlagt', 'under_utvikling'];
|
|
|
|
const validCandidates = facilities.filter(f => {
|
|
if (MANUAL_EXCLUSION_LIST.includes(f.slug)) return false;
|
|
const img = f.image_url || "";
|
|
if (!img || img.toLowerCase().includes('standard') || img.length < 5) return false;
|
|
|
|
const statuses = Array.isArray(f.course_statuses) ? f.course_statuses : [];
|
|
const isForbidden = statuses.some((s: any) =>
|
|
forbiddenStatuses.includes((s.status || "").toLowerCase())
|
|
);
|
|
return !isForbidden;
|
|
});
|
|
|
|
const highPriority = validCandidates.filter(f => {
|
|
const statuses = Array.isArray(f.course_statuses) ? f.course_statuses : [];
|
|
return statuses.some((s: any) => preferredStatuses.includes((s.status || "").toLowerCase()));
|
|
});
|
|
|
|
const fallbackPool = validCandidates.filter(f => !highPriority.includes(f));
|
|
const now = new Date();
|
|
const hourlySeed = parseInt(`${now.getFullYear()}${now.getMonth()}${now.getDate()}${now.getHours()}`);
|
|
|
|
const seededShuffle = (arr: any[]) => {
|
|
return [...arr].sort((a, b) => ((a.id * hourlySeed) % 100) - ((b.id * hourlySeed) % 100));
|
|
};
|
|
|
|
let selection = seededShuffle(highPriority);
|
|
if (selection.length < 5) {
|
|
selection = [...selection, ...seededShuffle(fallbackPool)].slice(0, 5);
|
|
} else {
|
|
selection = selection.slice(0, 5);
|
|
}
|
|
return selection;
|
|
}, [facilities]);
|
|
|
|
useEffect(() => {
|
|
if (sliderItems.length <= 1) return;
|
|
const interval = setInterval(() => setCurrentSlide((p) => (p + 1) % sliderItems.length), 8000);
|
|
return () => clearInterval(interval);
|
|
}, [sliderItems.length]);
|
|
|
|
if (sliderItems.length === 0) return null;
|
|
|
|
return (
|
|
<section className="relative h-[65vh] max-h-[624px] w-full overflow-hidden bg-[#11280f]">
|
|
{sliderItems.map((f, i) => (
|
|
<div
|
|
key={f.id}
|
|
className={`absolute inset-0 transition-opacity duration-1000 ease-in-out ${
|
|
i === currentIndex ? 'opacity-100 z-10' : 'opacity-0 z-0'
|
|
}`}
|
|
>
|
|
<Link href={`/golfbaner/${f.slug}`} className="block h-full relative group">
|
|
<div className="absolute inset-0 bg-gradient-to-t from-[#11280f] via-[#11280f]/40 to-black/10 z-10" />
|
|
|
|
<img
|
|
src={f.image_url}
|
|
alt={f.name}
|
|
className="w-full h-full object-cover transition-transform duration-[10s] scale-100 group-hover:scale-105"
|
|
/>
|
|
|
|
<div className="absolute inset-0 z-20 flex items-center">
|
|
<div className="max-w-[1400px] mx-auto px-6 w-full">
|
|
<div className="max-w-4xl animate-in fade-in slide-in-from-bottom-8 duration-1000">
|
|
{/* FONT NEDJUSTERT FRA text-6xl md:text-9xl TIL text-4xl md:text-7xl */}
|
|
<h2 className="text-4xl md:text-7xl font-black text-white tracking-tighter drop-shadow-2xl leading-[0.9] mb-4">
|
|
{f.name}
|
|
</h2>
|
|
<p className="text-white/90 text-sm md:text-xl font-bold uppercase tracking-[0.4em] drop-shadow-md">
|
|
{f.county} <span className="text-[#8bc34a] mx-2">•</span> {f.city}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
))}
|
|
|
|
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 z-30 flex gap-4">
|
|
{sliderItems.map((_, i) => (
|
|
<button
|
|
key={i}
|
|
onClick={() => setCurrentSlide(i)}
|
|
className={`h-1 transition-all duration-500 rounded-full ${
|
|
i === currentIndex ? 'w-16 bg-[#8bc34a]' : 'w-4 bg-white/20'
|
|
}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
} |