Nye-TeeOff/kode_eksport_3/frontend_src_app_HeroSlider_tsx.txt
2026-04-10 09:52:34 +02:00

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>
);
}