import React, { useState, useMemo } from 'react'; import { Link } from '@/spa/router'; import { Trophy, ArrowRight, Medal } from 'lucide-react'; import { WINNERS, type Winner } from '@/spa/data/winners'; import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute'; import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField'; import { useIsMobile } from '@/spa/hooks/useIsMobile'; import Image from '@/spa/components/ui/UnoptimizedImage' import { preistraegerIndexContent } from '@/spa/preistraegerIndexContent'; import { usePreistraegerPlaceholderImage } from '@/spa/preistraegerPlaceholder'; const FF = '"IBM Plex Sans", sans-serif'; const NAVY = '#111D55'; const GOLD = '#EFBF04'; const BORDER = '#D0D5DD'; const GRAY = '#666666'; const BG_ALT = '#E4E2E3'; const SIMPLE_LIST_YEAR = 2023; const GOLDENE_BAVARIA_GROUP = 'goldene-bavaria'; const PREISTRAEGER_GROUP = 'preistraeger'; type PreistraegerIndexCms = Partial & { hero?: Partial & { image?: unknown } card?: Partial & { fallbackImage?: unknown } } const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback; const optionalNumber = (value: unknown) => { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : undefined; }; const positiveNumber = (value: unknown, fallback: number) => { const parsed = optionalNumber(value); return parsed && parsed > 0 ? Math.floor(parsed) : fallback; }; const normalizeAwardGroup = (value: unknown): Winner['awardGroup'] => value === GOLDENE_BAVARIA_GROUP || value === PREISTRAEGER_GROUP ? value : undefined; const isGoldeneBavariaWinner = (winner: Winner) => winner.awardGroup === GOLDENE_BAVARIA_GROUP || winner.type.toLowerCase().includes('goldene bavaria'); const sortWinnersForDisplay = (winners: Winner[]) => winners .map((winner, index) => ({ winner, index })) .sort((a, b) => { const rankDiff = (a.winner.winnerRank ?? Number.POSITIVE_INFINITY) - (b.winner.winnerRank ?? Number.POSITIVE_INFINITY); if (rankDiff !== 0) return rankDiff; const sortDiff = (a.winner.sortOrder ?? Number.POSITIVE_INFINITY) - (b.winner.sortOrder ?? Number.POSITIVE_INFINITY); if (sortDiff !== 0) return sortDiff; return a.index - b.index; }) .map(({ winner }) => winner); const splitWinnersForYear = (winners: Winner[], totalWinners: number, goldeneBavariaCount: number) => { const visibleWinners = sortWinnersForDisplay(winners.filter((winner) => winner.showOnIndex !== false)); const maxGoldeneBavaria = Math.min(goldeneBavariaCount, totalWinners); const goldeneBavariaWinners = visibleWinners .filter((winner) => isGoldeneBavariaWinner(winner) || (winner.winnerRank || Number.POSITIVE_INFINITY) <= maxGoldeneBavaria) .slice(0, maxGoldeneBavaria); const goldIds = new Set(goldeneBavariaWinners.map((winner) => winner.id)); for (const winner of visibleWinners) { if (goldeneBavariaWinners.length >= maxGoldeneBavaria) break; if (goldIds.has(winner.id) || winner.awardGroup === PREISTRAEGER_GROUP) continue; goldeneBavariaWinners.push(winner); goldIds.add(winner.id); } const remainingWinners = visibleWinners .filter((winner) => !goldIds.has(winner.id)) .slice(0, Math.max(totalWinners - goldeneBavariaWinners.length, 0)); return { goldeneBavariaWinners, remainingWinners, displayedWinners: [...goldeneBavariaWinners, ...remainingWinners], }; }; const normalizeWinner = ( doc: CmsRouteDoc, placeholderImage: string, fallbackWinner: typeof preistraegerIndexContent.card.fallbackWinner, ): Winner => ({ id: String(doc.id), slug: String(doc.slug || ''), name: String(doc.title || fallbackWinner.name || preistraegerIndexContent.card.fallbackWinner.name), category: String(doc.category || fallbackWinner.category || preistraegerIndexContent.card.fallbackWinner.category), year: Number(doc.year || new Date().getFullYear()), type: String( doc.awardType || (normalizeAwardGroup(doc.awardGroup) === GOLDENE_BAVARIA_GROUP ? 'Goldene Bavaria' : fallbackWinner.awardType) || preistraegerIndexContent.card.fallbackWinner.awardType, ), awardGroup: normalizeAwardGroup(doc.awardGroup), winnerRank: optionalNumber(doc.winnerRank), sortOrder: optionalNumber(doc.sortOrder), showOnIndex: doc.showOnIndex !== false, img: docImageUrl(doc, placeholderImage), shortDesc: String(doc.shortDesc || doc.description || doc.meta?.description || ''), longDesc: String(doc.longDesc || doc.description || doc.meta?.description || ''), quote: String(doc.quote || ''), quotePerson: String(doc.quotePerson || ''), quoteRole: String(doc.quoteRole || ''), location: String(doc.location || fallbackWinner.location || preistraegerIndexContent.card.fallbackWinner.location), industry: String(doc.industry || fallbackWinner.industry || preistraegerIndexContent.card.fallbackWinner.industry), website: String(doc.website || '#'), hasMedia: Boolean(doc.hasMedia), }); export default function Preistraeger() { const isMobile = useIsMobile(); const placeholderImage = usePreistraegerPlaceholderImage(); const cms = (useCmsRoute()?.doc?.preistraegerIndex || {}) as PreistraegerIndexCms; const cmsPreistraeger = useCmsCollection('preistraeger'); const hero = { ...preistraegerIndexContent.hero, ...(cms.hero || {}) }; const breadcrumb = { ...preistraegerIndexContent.breadcrumb, ...(cms.breadcrumb || {}) }; const count = { ...preistraegerIndexContent.count, ...(cms.count || {}) }; const rules = { ...preistraegerIndexContent.rules, ...(cms.rules || {}) }; const empty = { ...preistraegerIndexContent.empty, ...(cms.empty || {}) }; const goldeneBavaria = { ...preistraegerIndexContent.goldeneBavaria, ...(cms.goldeneBavaria || {}) }; const winnersSection = { ...preistraegerIndexContent.winners, ...(cms.winners || {}) }; const card = useMemo(() => ({ ...preistraegerIndexContent.card, ...(cms.card || {}) }), [cms.card]); const cta = { ...preistraegerIndexContent.cta, ...(cms.cta || {}) }; const totalWinners = positiveNumber(rules.totalWinners, preistraegerIndexContent.rules.totalWinners); const goldeneBavariaCount = Math.min( positiveNumber(rules.goldeneBavariaCount, preistraegerIndexContent.rules.goldeneBavariaCount), totalWinners, ); const winners = useMemo(() => { const fallbackWinner = card.fallbackWinner || preistraegerIndexContent.card.fallbackWinner; if (cmsPreistraeger.length) return cmsPreistraeger.map((doc) => normalizeWinner(doc, placeholderImage, fallbackWinner)).filter((winner) => winner.slug); return WINNERS; }, [card, cmsPreistraeger, placeholderImage]); const years = useMemo(() => [...new Set(winners.map((winner) => winner.year))].sort((a, b) => b - a), [winners]); const [activeYear, setActiveYear] = useState(undefined); const selectedYear = activeYear ?? years[0] ?? new Date().getFullYear(); const filtered = useMemo(() => winners.filter(w => w.year === selectedYear), [selectedYear, winners]); const winnerGroups = useMemo( () => splitWinnersForYear(filtered, totalWinners, goldeneBavariaCount), [filtered, goldeneBavariaCount, totalWinners], ); const usesSimpleWinnerGrid = selectedYear === SIMPLE_LIST_YEAR; return (
{/* Hero */}
{mediaAlt(hero.image,
{/* Breadcrumb */}
{fallbackText(breadcrumb.label, preistraegerIndexContent.breadcrumb.label)}
{/* Year Filter Section */}
{/* #93: Headline entfernt – Seitenname steht bereits oben im Breadcrumb. */} {/* Year Tabs */}
{years.map(year => ( ))}
{/* Count bar */}
{selectedYear} – {winnerGroups.displayedWinners.length} {fallbackText(count.label, preistraegerIndexContent.count.label)}
{winnerGroups.displayedWinners.length === 0 ? (
{fallbackText(empty.message, preistraegerIndexContent.empty.message)}
) : ( <> {winnerGroups.goldeneBavariaWinners.length > 0 ? ( ) : null} {winnerGroups.remainingWinners.length > 0 ? (
{usesSimpleWinnerGrid ? ( ) : (
{winnerGroups.remainingWinners.map(w => ( ))}
)}
) : null} )} {/* Bottom CTA */}

{fallbackText(cta.text, preistraegerIndexContent.cta.text)}

{ (e.currentTarget as HTMLElement).style.background = '#FFD130'; (e.currentTarget as HTMLElement).style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }} onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = GOLD; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }} > {fallbackText(cta.primaryCta?.label, preistraegerIndexContent.cta.primaryCta.label)}
); } function SectionIntro({ copy, isMobile }: { copy: typeof preistraegerIndexContent.winners; isMobile: boolean }) { return (
{fallbackText(copy.eyebrow, preistraegerIndexContent.winners.eyebrow)}

{fallbackText(copy.heading, preistraegerIndexContent.winners.heading)}

{fallbackText(copy.description, preistraegerIndexContent.winners.description)}

); } function GoldeneBavariaSection({ copy, hoverLabel, winners, }: { copy: typeof preistraegerIndexContent.goldeneBavaria; hoverLabel: string; winners: Winner[]; }) { const isMobile = useIsMobile(); return (
{fallbackText(copy.eyebrow, preistraegerIndexContent.goldeneBavaria.eyebrow)}

{fallbackText(copy.heading, preistraegerIndexContent.goldeneBavaria.heading)}

{fallbackText(copy.description, preistraegerIndexContent.goldeneBavaria.description)}

{winners.map((winner, index) => ( ))}
); } function GoldeneBavariaCard({ winner, rank, hoverLabel }: { winner: Winner; rank: number; hoverLabel: string }) { const [hovered, setHovered] = useState(false); const isMobile = useIsMobile(); return ( setHovered(true)} onMouseLeave={() => setHovered(false)} >
{winner.name}
#{rank} Goldene Bavaria
{winner.year} · {winner.category}

{winner.name}

{winner.shortDesc}

{hoverLabel}
); } // ── WinnerCard ─────────────────────────────────────────────────────────────── function WinnerCard({ winner, hoverLabel }: { winner: Winner; hoverLabel: string }) { const [hovered, setHovered] = useState(false); const isMobile = useIsMobile(); return ( setHovered(true)} onMouseLeave={() => setHovered(false)} > {winner.name} {/* base gradient */}
{/* hover overlay */}
{/* Text */}
{winner.year} · {winner.type}

{winner.name}

{winner.category}

{/* Pill button */}
{hoverLabel}
); } function SimpleWinnerGrid({ winners }: { winners: Winner[] }) { const isMobile = useIsMobile(); return (
{winners.map(winner => ( ))}
); } function SimpleWinnerCard({ winner }: { winner: Winner }) { const isMobile = useIsMobile(); const hasDescription = winner.shortDesc.trim().length > 0; return (
{winner.year} · {winner.type} {winner.category ? ( {winner.category} ) : null}

{winner.name}

{hasDescription ? (

{winner.shortDesc}

) : null}
); }