feat: add scrolling functionality for winners section with navigation buttons

This commit is contained in:
syntaxbullet
2026-07-13 13:52:02 +02:00
parent bd2e99ca91
commit 7668e1464b

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Button } from '@/spa/components/ui/button'; import { Button } from '@/spa/components/ui/button';
import { Play, ChevronRight, X, Star } from 'lucide-react'; import { Play, ChevronLeft, ChevronRight, X, Star } from 'lucide-react';
import { Link } from '@/spa/router'; import { Link } from '@/spa/router';
import { PartnerTicker } from '@/spa/components/ui/partner-ticker'; import { PartnerTicker } from '@/spa/components/ui/partner-ticker';
import { WINNERS } from '@/spa/data/winners'; import { WINNERS } from '@/spa/data/winners';
@@ -225,6 +225,7 @@ function HomeWinnerCard({
const Home: React.FC = () => { const Home: React.FC = () => {
const [showVideo, setShowVideo] = useState(false); const [showVideo, setShowVideo] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const winnersScrollRef = useRef<HTMLDivElement>(null);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const placeholderImage = usePreistraegerPlaceholderImage(); const placeholderImage = usePreistraegerPlaceholderImage();
const home = (useCmsRoute()?.doc?.home || {}) as HomeCms; const home = (useCmsRoute()?.doc?.home || {}) as HomeCms;
@@ -259,9 +260,17 @@ const Home: React.FC = () => {
const selectedWinnerDocs = selectedWinners const selectedWinnerDocs = selectedWinners
.map((selected) => resolveCmsRelationship(selected, cmsPreistraeger)) .map((selected) => resolveCmsRelationship(selected, cmsPreistraeger))
.filter(Boolean) .filter(Boolean)
const eligibleWinners = (cmsPreistraeger.length ? cmsPreistraeger : WINNERS)
.filter((winner) => Number((winner as { year?: string | number }).year) === 2025)
const selectedWinnerIDs = new Set(
selectedWinnerDocs.map((winner) => String((winner as WinnerCardData).id || (winner as WinnerCardData).slug)),
)
const remainingWinners = selectedWinnerDocs.length >= 8
? eligibleWinners.filter((winner) => !selectedWinnerIDs.has(String((winner as WinnerCardData).id || (winner as WinnerCardData).slug)))
: []
const homeWinners = selectedWinnerDocs.length const homeWinners = selectedWinnerDocs.length
? selectedWinnerDocs ? [...selectedWinnerDocs, ...remainingWinners]
: (cmsPreistraeger.length ? cmsPreistraeger : WINNERS).filter((w) => Number((w as { year?: string | number }).year) === 2025).slice(0, 8); : eligibleWinners;
const scrollDesktopWinners = !isMobile && homeWinners.length > 8; const scrollDesktopWinners = !isMobile && homeWinners.length > 8;
const scrollWinnerGrid = isMobile || scrollDesktopWinners; const scrollWinnerGrid = isMobile || scrollDesktopWinners;
const quickCriteria = fallbackArray(quickCheck.criteria, homeContent.quickCheck.criteria); const quickCriteria = fallbackArray(quickCheck.criteria, homeContent.quickCheck.criteria);
@@ -281,6 +290,16 @@ const Home: React.FC = () => {
}); });
}, [showVideo, videoSrc]); }, [showVideo, videoSrc]);
const scrollWinners = (direction: -1 | 1) => {
const container = winnersScrollRef.current;
if (!container) return;
container.scrollBy({
left: direction * Math.max(container.clientWidth * 0.8, 320),
behavior: 'smooth',
});
};
return ( return (
<div className="animate-fade-in"> <div className="animate-fade-in">
{/* Hero Section */} {/* Hero Section */}
@@ -380,6 +399,27 @@ const Home: React.FC = () => {
<Lines text={fallbackText(winnersSection.heading, homeContent.winners.heading)} /> <Lines text={fallbackText(winnersSection.heading, homeContent.winners.heading)} />
</h2> </h2>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 20 }}>
{scrollDesktopWinners && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<button
type="button"
aria-label="Vorherige Preisträger anzeigen"
onClick={() => scrollWinners(-1)}
style={{ width: 42, height: 42, border: '1px solid rgba(255,255,255,0.28)', background: 'transparent', color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
>
<ChevronLeft size={18} />
</button>
<button
type="button"
aria-label="Weitere Preisträger anzeigen"
onClick={() => scrollWinners(1)}
style={{ width: 42, height: 42, border: '1px solid rgba(255,255,255,0.28)', background: 'transparent', color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
>
<ChevronRight size={18} />
</button>
</div>
)}
<Link <Link
to={fallbackText(winnersSection.cta?.url, homeContent.winners.cta.url)} to={fallbackText(winnersSection.cta?.url, homeContent.winners.cta.url)}
style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 15, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#EFBF04', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6, paddingBottom: 4, borderBottom: '1px solid #EFBF04', whiteSpace: 'nowrap' }} style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 15, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#EFBF04', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6, paddingBottom: 4, borderBottom: '1px solid #EFBF04', whiteSpace: 'nowrap' }}
@@ -387,8 +427,9 @@ const Home: React.FC = () => {
{fallbackText(winnersSection.cta?.label, homeContent.winners.cta.label)} <ChevronRight size={14} /> {fallbackText(winnersSection.cta?.label, homeContent.winners.cta.label)} <ChevronRight size={14} />
</Link> </Link>
</div> </div>
</div>
<div style={{ <div ref={winnersScrollRef} style={{
display: 'grid', display: 'grid',
gridTemplateColumns: scrollWinnerGrid ? undefined : 'repeat(4, 1fr)', gridTemplateColumns: scrollWinnerGrid ? undefined : 'repeat(4, 1fr)',
gridAutoFlow: scrollWinnerGrid ? 'column' : undefined, gridAutoFlow: scrollWinnerGrid ? 'column' : undefined,
@@ -398,9 +439,10 @@ const Home: React.FC = () => {
scrollSnapType: scrollWinnerGrid ? 'x mandatory' : undefined, scrollSnapType: scrollWinnerGrid ? 'x mandatory' : undefined,
overscrollBehaviorX: scrollWinnerGrid ? 'contain' : undefined, overscrollBehaviorX: scrollWinnerGrid ? 'contain' : undefined,
gap: isMobile ? 8 : 0, gap: isMobile ? 8 : 0,
padding: isMobile ? '0 16px 8px' : 0, padding: isMobile ? '0 16px 8px' : scrollDesktopWinners ? '0 0 12px' : 0,
WebkitOverflowScrolling: 'touch', WebkitOverflowScrolling: 'touch',
scrollbarWidth: 'none', scrollbarWidth: scrollDesktopWinners ? 'thin' : 'none',
scrollbarColor: scrollDesktopWinners ? '#EFBF04 rgba(255,255,255,0.14)' : undefined,
}}> }}>
{homeWinners.map((w) => ( {homeWinners.map((w) => (
<HomeWinnerCard <HomeWinnerCard
@@ -849,7 +891,7 @@ function StatusSlideCard({
<h2 style={{ fontFamily: FF, fontSize: isMobile ? 'clamp(1.8rem, 8vw, 2.6rem)' : 'clamp(2.4rem, 3.8vw, 3.8rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.03em', lineHeight: 0.94, margin: '0 0 24px' }}> <h2 style={{ fontFamily: FF, fontSize: isMobile ? 'clamp(1.8rem, 8vw, 2.6rem)' : 'clamp(2.4rem, 3.8vw, 3.8rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.03em', lineHeight: 0.94, margin: '0 0 24px' }}>
{phase.headline} {phase.headline}
</h2> </h2>
<p style={{ fontFamily: '"Inter", sans-serif', fontSize: 17, color: 'rgba(255,255,255,0.68)', lineHeight: 1.8, marginBottom: 32, maxWidth: 520 }}> <p style={{ fontFamily: '"Inter", sans-serif', fontSize: 17, color: '#C7CAD1', lineHeight: 1.8, marginBottom: 32, maxWidth: isMobile ? 520 : 760 }}>
{phase.body} {phase.body}
</p> </p>
{phase.cta ? ( {phase.cta ? (