Compare commits

..

2 Commits

8 changed files with 280 additions and 194 deletions

View File

@@ -243,7 +243,7 @@ export const participationFields: Field[] = [
name: 'awardsGrid', name: 'awardsGrid',
label: '06 · Awards grid', label: '06 · Awards grid',
type: 'group', type: 'group',
admin: sectionAdmin('Awards section rendered after the dates banner. On the participation page, Goldene Bavaria is featured above the remaining award cards.'), admin: sectionAdmin('Awards section rendered after the dates banner. On the participation page, the CMS heading and intro belong to the standalone Goldene Bavaria section; the remaining award cards render in a separate section below.'),
fields: [ fields: [
text('eyebrow', 'Eyebrow', participationContent.awardsGrid.eyebrow), text('eyebrow', 'Eyebrow', participationContent.awardsGrid.eyebrow),
textarea('heading', 'Heading', participationContent.awardsGrid.heading), textarea('heading', 'Heading', participationContent.awardsGrid.heading),

View File

@@ -68,6 +68,11 @@ export const Preistraeger: CollectionConfig = {
{ {
name: 'year', name: 'year',
type: 'number', type: 'number',
label: 'Jahr (optional)',
admin: {
description:
'Einträge von 2007 bis 2023 werden gemeinsam im Archiv-Reiter angezeigt. Wenn das genaue Gewinnjahr unbekannt ist, das Feld leer lassen; der Eintrag wird ebenfalls diesem Archiv zugeordnet.',
},
}, },
{ {
name: 'awardGroup', name: 'awardGroup',
@@ -77,7 +82,7 @@ export const Preistraeger: CollectionConfig = {
options: awardGroupOptions, options: awardGroupOptions,
admin: { admin: {
description: description:
'Steuert die Ausgabe auf der Preisträger-Übersicht. Pro Jahrgang werden 15 Gewinner angezeigt, davon maximal 3 in der Goldene-Bavaria-Section.', 'Steuert die Ausgabe für einzelne Jahrgänge ab 2024. Im Archiv 2007–2023 werden alle Einträge als einfache Liste angezeigt.',
}, },
}, },
{ {
@@ -87,7 +92,7 @@ export const Preistraeger: CollectionConfig = {
min: 1, min: 1,
admin: { admin: {
description: description:
'1 bis 3 sind die besten Gewinner und sollten als Goldene Bavaria markiert werden. Niedrigere Zahlen erscheinen zuerst.', 'Für einzelne Jahrgänge ab 2024: 1 bis 3 sind die besten Gewinner und sollten als Goldene Bavaria markiert werden. Niedrigere Zahlen erscheinen zuerst.',
}, },
}, },
{ {
@@ -96,7 +101,7 @@ export const Preistraeger: CollectionConfig = {
label: 'Sortierung', label: 'Sortierung',
defaultValue: 100, defaultValue: 100,
admin: { admin: {
description: 'Fallback-Sortierung innerhalb eines Jahrgangs, wenn kein Rang gesetzt ist.', description: 'Sortierung innerhalb eines Jahrgangs beziehungsweise innerhalb des Archivs 2007–2023.',
}, },
}, },
{ {

View File

@@ -895,7 +895,7 @@ export interface Page {
| null; | null;
}; };
/** /**
* Awards section rendered after the dates banner. On the participation page, Goldene Bavaria is featured above the remaining award cards. * Awards section rendered after the dates banner. On the participation page, the CMS heading and intro belong to the standalone Goldene Bavaria section; the remaining award cards render in a separate section below.
*/ */
awardsGrid?: { awardsGrid?: {
eyebrow?: string | null; eyebrow?: string | null;
@@ -2495,17 +2495,20 @@ export interface Preistraeger {
title: string; title: string;
image?: (number | null) | Media; image?: (number | null) | Media;
category?: string | null; category?: string | null;
/**
* Einträge von 2007 bis 2023 werden gemeinsam im Archiv-Reiter angezeigt. Wenn das genaue Gewinnjahr unbekannt ist, das Feld leer lassen; der Eintrag wird ebenfalls diesem Archiv zugeordnet.
*/
year?: number | null; year?: number | null;
/** /**
* Steuert die Ausgabe auf der Preisträger-Übersicht. Pro Jahrgang werden 15 Gewinner angezeigt, davon maximal 3 in der Goldene-Bavaria-Section. * Steuert die Ausgabe für einzelne Jahrgänge ab 2024. Im Archiv 2007–2023 werden alle Einträge als einfache Liste angezeigt.
*/ */
awardGroup?: ('preistraeger' | 'goldene-bavaria') | null; awardGroup?: ('preistraeger' | 'goldene-bavaria') | null;
/** /**
* 1 bis 3 sind die besten Gewinner und sollten als Goldene Bavaria markiert werden. Niedrigere Zahlen erscheinen zuerst. * Für einzelne Jahrgänge ab 2024: 1 bis 3 sind die besten Gewinner und sollten als Goldene Bavaria markiert werden. Niedrigere Zahlen erscheinen zuerst.
*/ */
winnerRank?: number | null; winnerRank?: number | null;
/** /**
* Fallback-Sortierung innerhalb eines Jahrgangs, wenn kein Rang gesetzt ist. * Sortierung innerhalb eines Jahrgangs beziehungsweise innerhalb des Archivs 2007–2023.
*/ */
sortOrder?: number | null; sortOrder?: number | null;
showOnIndex?: boolean | null; showOnIndex?: boolean | null;

View File

@@ -12,6 +12,7 @@ const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif'; const FB = '"Inter", sans-serif';
const NAVY = '#111D55'; const NAVY = '#111D55';
const GOLD = '#EFBF04'; const GOLD = '#EFBF04';
const AWARD_ARTWORK_EDGE_NAVY = '#132B48';
type AwardCard = (typeof participationContent.awardsGrid.cards)[number]; type AwardCard = (typeof participationContent.awardsGrid.cards)[number];
type AwardCardCms = Partial<AwardCard> & { image?: unknown }; type AwardCardCms = Partial<AwardCard> & { image?: unknown };
@@ -80,8 +81,8 @@ function FeaturedAwardCard({ card }: { card: ResolvedAwardCard }) {
const isContainedImage = isContainedAwardImage(imageFilename); const isContainedImage = isContainedAwardImage(imageFilename);
return ( return (
<section aria-label={fallbackText(card.title, 'Goldene Bavaria')} style={{ position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) minmax(320px, 1fr)', borderBottom: '1px solid rgba(3,9,58,0.1)' }}> <article aria-label={fallbackText(card.title, 'Goldene Bavaria')} style={{ position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) minmax(320px, 1fr)', borderBottom: '1px solid rgba(3,9,58,0.1)' }}>
<div style={{ position: 'relative', height: isMobile ? 180 : 600, background: 'linear-gradient(145deg, #111D55 0%, #1E2B69 44%, #C9A227 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}> <div style={{ position: 'relative', height: isMobile ? 180 : 600, background: AWARD_ARTWORK_EDGE_NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
<Image unoptimized <Image unoptimized
src={mediaUrl(card.image, `/images/${imageFilename}`)} src={mediaUrl(card.image, `/images/${imageFilename}`)}
alt={mediaAlt(card.image, fallbackText(card.imageAlt, fallbackText(card.title, 'Auszeichnung')))} alt={mediaAlt(card.image, fallbackText(card.imageAlt, fallbackText(card.title, 'Auszeichnung')))}
@@ -90,10 +91,9 @@ function FeaturedAwardCard({ card }: { card: ResolvedAwardCard }) {
height: '100%', height: '100%',
objectFit: isContainedImage ? 'contain' : 'cover', objectFit: isContainedImage ? 'contain' : 'cover',
objectPosition: 'center', objectPosition: 'center',
padding: imageFilename.endsWith('.png') ? (isMobile ? 22 : 30) : 0, padding: 0,
boxSizing: 'border-box', boxSizing: 'border-box',
display: 'block', display: 'block',
filter: imageFilename.endsWith('.png') ? 'drop-shadow(0 32px 46px rgba(3,9,58,0.34))' : undefined,
}} }}
/> />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,9,58,0.42), transparent 42%)', pointerEvents: 'none' }} /> <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,9,58,0.42), transparent 42%)', pointerEvents: 'none' }} />
@@ -160,7 +160,126 @@ function FeaturedAwardCard({ card }: { card: ResolvedAwardCard }) {
</a> </a>
</div> </div>
</div> </div>
</section> </article>
);
}
function SupportingAwardCards({
cards,
allCards,
followsFeaturedCard,
}: {
cards: ResolvedAwardCard[];
allCards: ResolvedAwardCard[];
followsFeaturedCard: boolean;
}) {
const isMobile = useIsMobile();
if (cards.length === 0) return null;
return (
<div style={{ position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : `repeat(${Math.min(Math.max(cards.length, 1), 3)}, minmax(0, 1fr))`, borderBottom: '1px solid rgba(3,9,58,0.08)' }}>
{cards.map((card, index) => {
const sourceIndex = allCards.indexOf(card);
const imageFilename = getAwardImageFilename(card, sourceIndex >= 0 ? sourceIndex : index);
const isContainedImage = isContainedAwardImage(imageFilename);
const usesGoldeneBavariaArtwork = imageFilename.includes('goldene-bavaria');
const isLast = index === cards.length - 1;
const isDarkCard = !followsFeaturedCard && index === 1;
return (
<article
key={`${fallbackText(card.title, 'award')}-${index}`}
style={{
display: 'flex',
flexDirection: 'column',
minHeight: isMobile ? 'auto' : followsFeaturedCard ? 540 : 620,
borderRight: !isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
borderBottom: isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
background: isDarkCard ? NAVY : followsFeaturedCard ? '#fff' : 'rgba(255,255,255,0.72)',
}}
>
<div style={{ height: isMobile ? 220 : 260, background: usesGoldeneBavariaArtwork ? AWARD_ARTWORK_EDGE_NAVY : imageFilename.includes('roland-berger') ? '#DDE7E2' : NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
<Image unoptimized
src={mediaUrl(card.image, `/images/${imageFilename}`)}
alt={mediaAlt(card.image, fallbackText(card.imageAlt, fallbackText(card.title, 'Auszeichnung')))}
style={{
width: '100%',
height: '100%',
objectFit: isContainedImage ? 'contain' : 'cover',
objectPosition: isDarkCard ? 'center top' : 'center',
padding: usesGoldeneBavariaArtwork ? 0 : imageFilename.endsWith('.png') ? 22 : 0,
boxSizing: 'border-box',
display: 'block',
}}
/>
</div>
<div style={{ padding: isMobile ? '28px 24px 32px' : followsFeaturedCard ? '38px 44px 42px' : '36px 36px 40px', display: 'flex', flexDirection: 'column', flex: 1 }}>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.26em', textTransform: 'uppercase', color: isDarkCard ? 'rgba(239,191,4,0.8)' : '#4A8FC9', marginBottom: 12 }}>
{fallbackText(card.label, '')}
</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.25rem, 2vw, 1.7rem)', fontWeight: 900, color: isDarkCard ? '#fff' : '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.08, margin: '0 0 16px', overflowWrap: 'anywhere' }}>
<Lines text={fallbackText(card.title, '')} />
</h3>
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 22 }} />
<p style={{ fontFamily: FB, fontSize: 18, color: isDarkCard ? 'rgba(255,255,255,0.62)' : 'rgba(16,24,40,0.55)', lineHeight: 1.7, margin: 0, flex: 1 }}>
{fallbackText(card.description, '')}
</p>
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 10, marginTop: 32 }}>
<Link
to={fallbackText(card.articleCta?.url, '#bewerben')}
style={{
minHeight: 46,
padding: '0 18px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
background: GOLD,
color: '#101828',
fontFamily: FF,
fontSize: 13,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.08em',
textDecoration: 'none',
whiteSpace: 'normal',
textAlign: 'center',
}}
>
{fallbackText(card.articleCta?.label, 'Artikel lesen')} <ArrowRight size={13} />
</Link>
<a
href={fallbackText(card.mailtoCta?.url, `mailto:${participationContent.form.eligibility.ineligibleContactEmail}`)}
style={{
minHeight: 46,
padding: '0 18px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
color: isDarkCard ? '#fff' : NAVY,
border: `1px solid ${isDarkCard ? 'rgba(255,255,255,0.22)' : 'rgba(3,9,58,0.18)'}`,
fontFamily: FF,
fontSize: 13,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.08em',
textDecoration: 'none',
whiteSpace: 'normal',
textAlign: 'center',
}}
>
<Mail size={13} /> {fallbackText(card.mailtoCta?.label, 'Kontakt aufnehmen')}
</a>
</div>
</div>
</article>
);
})}
</div>
); );
} }
@@ -171,127 +290,37 @@ export default function AwardsGridSection({ content, featuredGoldeneBavaria = fa
const { featuredCard, supportingCards } = splitFeaturedAwardCard(cards, featuredGoldeneBavaria); const { featuredCard, supportingCards } = splitFeaturedAwardCard(cards, featuredGoldeneBavaria);
return ( return (
<section id="auszeichnungen" style={{ background: '#F7F7F5', position: 'relative', overflow: 'hidden', isolation: 'isolate' }}> <>
<MunichSkylineBg /> <section id="auszeichnungen" style={{ background: featuredCard ? '#fff' : '#F7F7F5', position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', borderBottom: '1px solid rgba(3,9,58,0.1)' }}> {!featuredCard ? <MunichSkylineBg /> : null}
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}> <div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', borderBottom: '1px solid rgba(3,9,58,0.1)' }}>
{fallbackText(section.eyebrow, participationContent.awardsGrid.eyebrow)} <span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>
</span> {fallbackText(section.eyebrow, participationContent.awardsGrid.eyebrow)}
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 0.9fr) minmax(0, 1fr)', gap: isMobile ? 16 : 56, alignItems: 'end' }}> </span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3.5vw, 3rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.03, margin: 0, overflowWrap: 'anywhere' }}> <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 0.9fr) minmax(0, 1fr)', gap: isMobile ? 16 : 56, alignItems: 'end' }}>
<Lines text={fallbackText(section.heading, participationContent.awardsGrid.heading)} /> <h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3.5vw, 3rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.03, margin: 0, overflowWrap: 'anywhere' }}>
</h2> <Lines text={fallbackText(section.heading, participationContent.awardsGrid.heading)} />
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.55)', lineHeight: 1.8, margin: 0 }}> </h2>
{fallbackText(section.description, participationContent.awardsGrid.description)} <p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.55)', lineHeight: 1.8, margin: 0 }}>
</p> {fallbackText(section.description, participationContent.awardsGrid.description)}
</p>
</div>
</div> </div>
</div>
{featuredCard ? <FeaturedAwardCard card={featuredCard} /> : null} {featuredCard ? (
<div style={{ position: 'relative', zIndex: 1, background: '#fff', paddingBottom: isMobile ? 32 : 56 }}>
<FeaturedAwardCard card={featuredCard} />
</div>
) : (
<SupportingAwardCards cards={supportingCards} allCards={cards} followsFeaturedCard={false} />
)}
</section>
{supportingCards.length > 0 ? ( {featuredCard && supportingCards.length > 0 ? (
<div style={{ position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : `repeat(${Math.min(Math.max(supportingCards.length, 1), 3)}, minmax(0, 1fr))`, borderBottom: '1px solid rgba(3,9,58,0.08)' }}> <section aria-label="Weitere Auszeichnungen" style={{ background: '#fff', position: 'relative', overflow: 'hidden', padding: isMobile ? '40px 0 56px' : '64px 0 80px', borderTop: '1px solid rgba(17,29,85,0.08)' }}>
{supportingCards.map((card, index) => { <SupportingAwardCards cards={supportingCards} allCards={cards} followsFeaturedCard />
const sourceIndex = cards.indexOf(card); </section>
const imageFilename = getAwardImageFilename(card, sourceIndex >= 0 ? sourceIndex : index);
const isContainedImage = isContainedAwardImage(imageFilename);
const isLast = index === supportingCards.length - 1;
const isDarkCard = !featuredCard && index === 1;
return (
<article
key={`${fallbackText(card.title, 'award')}-${index}`}
style={{
display: 'flex',
flexDirection: 'column',
minHeight: isMobile ? 'auto' : featuredCard ? 540 : 620,
borderRight: !isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
borderBottom: isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
background: isDarkCard ? NAVY : 'rgba(255,255,255,0.72)',
}}
>
<div style={{ height: isMobile ? 220 : 260, background: imageFilename.includes('roland-berger') ? '#DDE7E2' : NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
<Image unoptimized
src={mediaUrl(card.image, `/images/${imageFilename}`)}
alt={mediaAlt(card.image, fallbackText(card.imageAlt, fallbackText(card.title, 'Auszeichnung')))}
style={{
width: '100%',
height: '100%',
objectFit: isContainedImage ? 'contain' : 'cover',
objectPosition: isDarkCard ? 'center top' : 'center',
padding: imageFilename.endsWith('.png') ? 22 : 0,
boxSizing: 'border-box',
display: 'block',
}}
/>
</div>
<div style={{ padding: isMobile ? '28px 24px 32px' : featuredCard ? '38px 44px 42px' : '36px 36px 40px', display: 'flex', flexDirection: 'column', flex: 1 }}>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.26em', textTransform: 'uppercase', color: isDarkCard ? 'rgba(239,191,4,0.8)' : '#4A8FC9', marginBottom: 12 }}>
{fallbackText(card.label, '')}
</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.25rem, 2vw, 1.7rem)', fontWeight: 900, color: isDarkCard ? '#fff' : '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.08, margin: '0 0 16px', overflowWrap: 'anywhere' }}>
<Lines text={fallbackText(card.title, '')} />
</h3>
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 22 }} />
<p style={{ fontFamily: FB, fontSize: 18, color: isDarkCard ? 'rgba(255,255,255,0.62)' : 'rgba(16,24,40,0.55)', lineHeight: 1.7, margin: 0, flex: 1 }}>
{fallbackText(card.description, '')}
</p>
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 10, marginTop: 32 }}>
<Link
to={fallbackText(card.articleCta?.url, '#bewerben')}
style={{
minHeight: 46,
padding: '0 18px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
background: GOLD,
color: '#101828',
fontFamily: FF,
fontSize: 13,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.08em',
textDecoration: 'none',
whiteSpace: 'normal',
textAlign: 'center',
}}
>
{fallbackText(card.articleCta?.label, 'Artikel lesen')} <ArrowRight size={13} />
</Link>
<a
href={fallbackText(card.mailtoCta?.url, `mailto:${participationContent.form.eligibility.ineligibleContactEmail}`)}
style={{
minHeight: 46,
padding: '0 18px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
color: isDarkCard ? '#fff' : NAVY,
border: `1px solid ${isDarkCard ? 'rgba(255,255,255,0.22)' : 'rgba(3,9,58,0.18)'}`,
fontFamily: FF,
fontSize: 13,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.08em',
textDecoration: 'none',
whiteSpace: 'normal',
textAlign: 'center',
}}
>
<Mail size={13} /> {fallbackText(card.mailtoCta?.label, 'Kontakt aufnehmen')}
</a>
</div>
</div>
</article>
);
})}
</div>
) : null} ) : null}
</section> </>
); );
} }

View File

@@ -3,7 +3,7 @@ export interface Winner {
slug: string; slug: string;
name: string; name: string;
category: string; category: string;
year: number; year?: number;
type: string; type: string;
awardGroup?: 'preistraeger' | 'goldene-bavaria'; awardGroup?: 'preistraeger' | 'goldene-bavaria';
winnerRank?: number; winnerRank?: number;
@@ -21,7 +21,9 @@ export interface Winner {
hasMedia: boolean; hasMedia: boolean;
} }
const winnerEntries: Omit<Winner, 'awardGroup' | 'winnerRank' | 'sortOrder' | 'showOnIndex'>[] = [ type StaticWinner = Omit<Winner, 'awardGroup' | 'winnerRank' | 'sortOrder' | 'showOnIndex'> & { year: number };
const winnerEntries: StaticWinner[] = [
// ── 2025 ────────────────────────────────────────────────────────────────── // ── 2025 ──────────────────────────────────────────────────────────────────
{ {
id: '1', slug: 'techvision-muenchen', id: '1', slug: 'techvision-muenchen',
@@ -326,4 +328,6 @@ export const WINNERS: Winner[] = winnerEntries.map((winner) => {
}); });
export const CATEGORIES = [...new Set(WINNERS.map(w => w.category))].sort(); export const CATEGORIES = [...new Set(WINNERS.map(w => w.category))].sort();
export const YEARS = [...new Set(WINNERS.map(w => w.year))].sort((a, b) => b - a); export const YEARS = [...new Set(WINNERS.map(w => w.year))]
.filter((year): year is number => typeof year === 'number')
.sort((a, b) => b - a);

View File

@@ -5,7 +5,6 @@ import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
import { NewsletterInterestForm } from '@/spa/components/forms/NewsletterInterestForm'; import { NewsletterInterestForm } from '@/spa/components/forms/NewsletterInterestForm';
import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection'; import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection';
import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection'; import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField'; import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import { useApplicationPhase, useCmsRoute, useNewsletterForm } from '@/spa/cmsRoute'; import { useApplicationPhase, useCmsRoute, useNewsletterForm } from '@/spa/cmsRoute';
@@ -17,7 +16,6 @@ const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif'; const FB = '"Inter", sans-serif';
const NAVY = '#111D55'; const NAVY = '#111D55';
const GOLD = '#EFBF04'; const GOLD = '#EFBF04';
const CREAM = '#E4E2E3';
type ParticipationCms = Partial<typeof participationContent> & { type ParticipationCms = Partial<typeof participationContent> & {
hero?: Partial<typeof participationContent.hero> & { backgroundImage?: unknown }; hero?: Partial<typeof participationContent.hero> & { backgroundImage?: unknown };
@@ -115,8 +113,7 @@ const Participation: React.FC = () => {
<WegZurAuszeichnungSection content={process} /> <WegZurAuszeichnungSection content={process} />
{/* ── QUALIFICATIONS ───────────────────────────────────────────────────── */} {/* ── QUALIFICATIONS ───────────────────────────────────────────────────── */}
<section id="voraussetzungen" ref={qualSectionRef} style={{ background: CREAM, overflow: 'hidden', position: 'relative', isolation: 'isolate' }}> <section id="voraussetzungen" ref={qualSectionRef} style={{ background: '#fff', overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
<MunichSkylineBg />
{/* Header */} {/* Header */}
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 24 : 48, alignItems: 'flex-end', borderBottom: `1px solid #D0D5DD` }}> <div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 24 : 48, alignItems: 'flex-end', borderBottom: `1px solid #D0D5DD` }}>
@@ -144,7 +141,7 @@ const Participation: React.FC = () => {
</div> </div>
{/* 4 criteria as editorial rows */} {/* 4 criteria as editorial rows */}
<div style={{ position: 'relative', zIndex: 1, borderBottom: `1px solid #D0D5DD` }}> <div style={{ position: 'relative', zIndex: 1, background: '#fff', borderBottom: `1px solid #D0D5DD` }}>
{eligibilityCriteria.map((item, i, arr) => { {eligibilityCriteria.map((item, i, arr) => {
const Icon = ICONS[item.icon as keyof typeof ICONS] || MapPin; const Icon = ICONS[item.icon as keyof typeof ICONS] || MapPin;
return ( return (
@@ -360,8 +357,7 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
(way) => !isFutureAwardApplicationWay(way), (way) => !isFutureAwardApplicationWay(way),
); );
return ( return (
<section style={{ background: CREAM, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}> <section style={{ background: '#fff', position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<MunichSkylineBg />
{/* Header */} {/* Header */}
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', borderBottom: '1px solid #D0D5DD' }}> <div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', borderBottom: '1px solid #D0D5DD' }}>

View File

@@ -15,10 +15,15 @@ const GOLD = '#EFBF04';
const BORDER = '#D0D5DD'; const BORDER = '#D0D5DD';
const GRAY = '#666666'; const GRAY = '#666666';
const BG_ALT = '#E4E2E3'; const BG_ALT = '#E4E2E3';
const SIMPLE_LIST_YEAR = 2023; const ARCHIVE_START_YEAR = 2007;
const ARCHIVE_END_YEAR = 2023;
const ARCHIVE_PERIOD = 'archive-2007-2023' as const;
const ARCHIVE_LABEL = `${ARCHIVE_START_YEAR}–${ARCHIVE_END_YEAR}`;
const GOLDENE_BAVARIA_GROUP = 'goldene-bavaria'; const GOLDENE_BAVARIA_GROUP = 'goldene-bavaria';
const PREISTRAEGER_GROUP = 'preistraeger'; const PREISTRAEGER_GROUP = 'preistraeger';
type WinnerPeriod = number | typeof ARCHIVE_PERIOD;
type PreistraegerIndexCms = Partial<typeof preistraegerIndexContent> & { type PreistraegerIndexCms = Partial<typeof preistraegerIndexContent> & {
hero?: Partial<typeof preistraegerIndexContent.hero> & { image?: unknown } hero?: Partial<typeof preistraegerIndexContent.hero> & { image?: unknown }
card?: Partial<typeof preistraegerIndexContent.card> & { fallbackImage?: unknown } card?: Partial<typeof preistraegerIndexContent.card> & { fallbackImage?: unknown }
@@ -26,6 +31,7 @@ type PreistraegerIndexCms = Partial<typeof preistraegerIndexContent> & {
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback; const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
const optionalNumber = (value: unknown) => { const optionalNumber = (value: unknown) => {
if (value === null || value === undefined || value === '') return undefined;
const parsed = Number(value); const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined; return Number.isFinite(parsed) ? parsed : undefined;
}; };
@@ -53,6 +59,18 @@ const sortWinnersForDisplay = (winners: Winner[]) =>
}) })
.map(({ winner }) => winner); .map(({ winner }) => winner);
const isArchiveWinner = (winner: Winner) =>
winner.year === undefined || (winner.year >= ARCHIVE_START_YEAR && winner.year <= ARCHIVE_END_YEAR);
const sortArchiveWinners = (winners: Winner[]) =>
winners
.map((winner, index) => ({ winner, index }))
.sort((a, b) => {
const sortDiff = (a.winner.sortOrder ?? Number.POSITIVE_INFINITY) - (b.winner.sortOrder ?? Number.POSITIVE_INFINITY);
return sortDiff !== 0 ? sortDiff : a.index - b.index;
})
.map(({ winner }) => winner);
const splitWinnersForYear = (winners: Winner[], totalWinners: number, goldeneBavariaCount: number) => { const splitWinnersForYear = (winners: Winner[], totalWinners: number, goldeneBavariaCount: number) => {
const visibleWinners = sortWinnersForDisplay(winners.filter((winner) => winner.showOnIndex !== false)); const visibleWinners = sortWinnersForDisplay(winners.filter((winner) => winner.showOnIndex !== false));
const maxGoldeneBavaria = Math.min(goldeneBavariaCount, totalWinners); const maxGoldeneBavaria = Math.min(goldeneBavariaCount, totalWinners);
@@ -89,7 +107,7 @@ const normalizeWinner = (
slug: String(doc.slug || ''), slug: String(doc.slug || ''),
name: String(doc.title || fallbackWinner.name || preistraegerIndexContent.card.fallbackWinner.name), name: String(doc.title || fallbackWinner.name || preistraegerIndexContent.card.fallbackWinner.name),
category: String(doc.category || fallbackWinner.category || preistraegerIndexContent.card.fallbackWinner.category), category: String(doc.category || fallbackWinner.category || preistraegerIndexContent.card.fallbackWinner.category),
year: Number(doc.year || new Date().getFullYear()), year: optionalNumber(doc.year),
type: String( type: String(
doc.awardType || doc.awardType ||
(normalizeAwardGroup(doc.awardGroup) === GOLDENE_BAVARIA_GROUP ? 'Goldene Bavaria' : fallbackWinner.awardType) || (normalizeAwardGroup(doc.awardGroup) === GOLDENE_BAVARIA_GROUP ? 'Goldene Bavaria' : fallbackWinner.awardType) ||
@@ -135,16 +153,39 @@ export default function Preistraeger() {
if (cmsPreistraeger.length) return cmsPreistraeger.map((doc) => normalizeWinner(doc, placeholderImage, fallbackWinner)).filter((winner) => winner.slug); if (cmsPreistraeger.length) return cmsPreistraeger.map((doc) => normalizeWinner(doc, placeholderImage, fallbackWinner)).filter((winner) => winner.slug);
return WINNERS; return WINNERS;
}, [card, cmsPreistraeger, placeholderImage]); }, [card, cmsPreistraeger, placeholderImage]);
const years = useMemo(() => [...new Set(winners.map((winner) => winner.year))].sort((a, b) => b - a), [winners]); const periods = useMemo<WinnerPeriod[]>(() => {
const [activeYear, setActiveYear] = useState<number | undefined>(undefined); const annualYears = [...new Set(
const selectedYear = activeYear ?? years[0] ?? new Date().getFullYear(); winners
.map((winner) => winner.year)
.filter((year): year is number => typeof year === 'number' && year > ARCHIVE_END_YEAR),
)].sort((a, b) => b - a);
const filtered = useMemo(() => winners.filter(w => w.year === selectedYear), [selectedYear, winners]); return winners.some(isArchiveWinner) ? [...annualYears, ARCHIVE_PERIOD] : annualYears;
const winnerGroups = useMemo( }, [winners]);
() => splitWinnersForYear(filtered, totalWinners, goldeneBavariaCount), const [activePeriod, setActivePeriod] = useState<WinnerPeriod | undefined>(undefined);
[filtered, goldeneBavariaCount, totalWinners], const selectedPeriod = activePeriod ?? periods[0] ?? ARCHIVE_PERIOD;
const isArchivePeriod = selectedPeriod === ARCHIVE_PERIOD;
const selectedPeriodLabel = isArchivePeriod ? ARCHIVE_LABEL : String(selectedPeriod);
const filtered = useMemo(
() => winners.filter((winner) => isArchivePeriod ? isArchiveWinner(winner) : winner.year === selectedPeriod),
[isArchivePeriod, selectedPeriod, winners],
);
const winnerGroups = useMemo(
() => {
if (isArchivePeriod) {
const archiveWinners = sortArchiveWinners(filtered.filter((winner) => winner.showOnIndex !== false));
return {
goldeneBavariaWinners: [],
remainingWinners: archiveWinners,
displayedWinners: archiveWinners,
};
}
return splitWinnersForYear(filtered, totalWinners, goldeneBavariaCount);
},
[filtered, goldeneBavariaCount, isArchivePeriod, totalWinners],
); );
const usesSimpleWinnerGrid = selectedYear === SIMPLE_LIST_YEAR;
return ( return (
<div style={{ background: '#fff', minHeight: '100vh' }}> <div style={{ background: '#fff', minHeight: '100vh' }}>
@@ -170,23 +211,23 @@ export default function Preistraeger() {
{/* Year Tabs */} {/* Year Tabs */}
<div style={{ display: 'flex', flexWrap: isMobile ? 'nowrap' : 'wrap', gap: 0, borderBottom: `2px solid ${BORDER}`, overflowX: isMobile ? 'auto' : 'visible', WebkitOverflowScrolling: 'touch', maxWidth: '100%' }}> <div style={{ display: 'flex', flexWrap: isMobile ? 'nowrap' : 'wrap', gap: 0, borderBottom: `2px solid ${BORDER}`, overflowX: isMobile ? 'auto' : 'visible', WebkitOverflowScrolling: 'touch', maxWidth: '100%' }}>
{years.map(year => ( {periods.map(period => (
<button <button
key={year} key={period}
onClick={() => setActiveYear(year)} onClick={() => setActivePeriod(period)}
style={{ style={{
fontFamily: FF, fontSize: 16, fontWeight: 700, fontFamily: FF, fontSize: 16, fontWeight: 700,
padding: isMobile ? '12px 20px' : '12px 32px', padding: isMobile ? '12px 20px' : '12px 32px',
flexShrink: 0, flexShrink: 0,
background: 'none', border: 'none', cursor: 'pointer', background: 'none', border: 'none', cursor: 'pointer',
color: selectedYear === year ? NAVY : GRAY, color: selectedPeriod === period ? NAVY : GRAY,
borderBottom: selectedYear === year ? `2px solid ${GOLD}` : '2px solid transparent', borderBottom: selectedPeriod === period ? `2px solid ${GOLD}` : '2px solid transparent',
marginBottom: -2, marginBottom: -2,
letterSpacing: '0.04em', letterSpacing: '0.04em',
transition: 'color 0.15s, border-color 0.15s', transition: 'color 0.15s, border-color 0.15s',
}} }}
> >
{year} {period === ARCHIVE_PERIOD ? ARCHIVE_LABEL : period}
</button> </button>
))} ))}
</div> </div>
@@ -196,7 +237,7 @@ export default function Preistraeger() {
<div style={{ padding: isMobile ? '12px 24px' : '12px 80px', background: BG_ALT, borderTop: `1px solid ${BORDER}`, borderBottom: `1px solid ${BORDER}`, display: 'flex', alignItems: 'center', gap: 12 }}> <div style={{ padding: isMobile ? '12px 24px' : '12px 80px', background: BG_ALT, borderTop: `1px solid ${BORDER}`, borderBottom: `1px solid ${BORDER}`, display: 'flex', alignItems: 'center', gap: 12 }}>
<Trophy size={13} style={{ color: GOLD }} fill={GOLD} /> <Trophy size={13} style={{ color: GOLD }} fill={GOLD} />
<span style={{ fontFamily: FF, fontSize: 15, color: GRAY }}> <span style={{ fontFamily: FF, fontSize: 15, color: GRAY }}>
<strong style={{ color: '#101828' }}>{selectedYear}</strong> – {winnerGroups.displayedWinners.length} {fallbackText(count.label, preistraegerIndexContent.count.label)} <strong style={{ color: '#101828' }}>{selectedPeriodLabel}</strong> – {winnerGroups.displayedWinners.length} {fallbackText(count.label, preistraegerIndexContent.count.label)}
</span> </span>
</div> </div>
@@ -204,6 +245,8 @@ export default function Preistraeger() {
<div style={{ padding: isMobile ? '56px 24px' : '80px', textAlign: 'center', color: GRAY, fontFamily: FF }}> <div style={{ padding: isMobile ? '56px 24px' : '80px', textAlign: 'center', color: GRAY, fontFamily: FF }}>
{fallbackText(empty.message, preistraegerIndexContent.empty.message)} {fallbackText(empty.message, preistraegerIndexContent.empty.message)}
</div> </div>
) : isArchivePeriod ? (
<SimpleWinnerGrid winners={winnerGroups.displayedWinners} />
) : ( ) : (
<> <>
{winnerGroups.goldeneBavariaWinners.length > 0 ? ( {winnerGroups.goldeneBavariaWinners.length > 0 ? (
@@ -217,27 +260,23 @@ export default function Preistraeger() {
{winnerGroups.remainingWinners.length > 0 ? ( {winnerGroups.remainingWinners.length > 0 ? (
<section style={{ background: '#fff' }}> <section style={{ background: '#fff' }}>
<SectionIntro copy={winnersSection} isMobile={isMobile} /> <SectionIntro copy={winnersSection} isMobile={isMobile} />
{usesSimpleWinnerGrid ? ( <div style={{
<SimpleWinnerGrid winners={winnerGroups.remainingWinners} /> display: 'grid',
) : ( gridTemplateColumns: isMobile ? undefined : 'repeat(4, 1fr)',
<div style={{ gridAutoFlow: isMobile ? 'column' : undefined,
display: 'grid', gridTemplateRows: isMobile ? 'repeat(2, 1fr)' : undefined,
gridTemplateColumns: isMobile ? undefined : 'repeat(4, 1fr)', gridAutoColumns: isMobile ? '74vw' : undefined,
gridAutoFlow: isMobile ? 'column' : undefined, overflowX: isMobile ? 'auto' : undefined,
gridTemplateRows: isMobile ? 'repeat(2, 1fr)' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined,
gridAutoColumns: isMobile ? '74vw' : undefined, gap: isMobile ? 8 : 0,
overflowX: isMobile ? 'auto' : undefined, padding: isMobile ? '0 16px 8px' : 0,
scrollSnapType: isMobile ? 'x mandatory' : undefined, WebkitOverflowScrolling: 'touch',
gap: isMobile ? 8 : 0, scrollbarWidth: 'none',
padding: isMobile ? '0 16px 8px' : 0, }}>
WebkitOverflowScrolling: 'touch', {winnerGroups.remainingWinners.map(w => (
scrollbarWidth: 'none', <WinnerCard key={w.id} winner={w} hoverLabel={fallbackText(card.hoverLabel, preistraegerIndexContent.card.hoverLabel)} />
}}> ))}
{winnerGroups.remainingWinners.map(w => ( </div>
<WinnerCard key={w.id} winner={w} hoverLabel={fallbackText(card.hoverLabel, preistraegerIndexContent.card.hoverLabel)} />
))}
</div>
)}
</section> </section>
) : null} ) : null}
</> </>
@@ -436,6 +475,7 @@ function SimpleWinnerGrid({ winners }: { winners: Winner[] }) {
function SimpleWinnerCard({ winner }: { winner: Winner }) { function SimpleWinnerCard({ winner }: { winner: Winner }) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const hasDescription = winner.shortDesc.trim().length > 0; const hasDescription = winner.shortDesc.trim().length > 0;
const winnerMeta = [winner.year, winner.type].filter(Boolean).join(' · ');
return ( return (
<article <article
@@ -450,16 +490,18 @@ function SimpleWinnerCard({ winner }: { winner: Winner }) {
}} }}
> >
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 18 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 18 }}>
<span style={{ {winnerMeta ? (
color: GOLD, <span style={{
fontFamily: FF, color: GOLD,
fontSize: 12, fontFamily: FF,
fontWeight: 700, fontSize: 12,
letterSpacing: '0.12em', fontWeight: 700,
textTransform: 'uppercase', letterSpacing: '0.12em',
}}> textTransform: 'uppercase',
{winner.year} · {winner.type} }}>
</span> {winnerMeta}
</span>
) : null}
{winner.category ? ( {winner.category ? (
<span style={{ <span style={{
border: `1px solid ${BORDER}`, border: `1px solid ${BORDER}`,

View File

@@ -37,6 +37,11 @@ type PreistraegerGalleryCms = {
} }
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback; const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
const optionalNumber = (value: unknown) => {
if (value === null || value === undefined || value === '') return undefined;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
};
const mediaFilename = (media: unknown) => { const mediaFilename = (media: unknown) => {
if (!media || typeof media !== 'object') return undefined; if (!media || typeof media !== 'object') return undefined;
@@ -60,7 +65,7 @@ export default function PreistraegerDetail() {
slug: String(cmsWinner.slug || slug), slug: String(cmsWinner.slug || slug),
name: String(cmsWinner.title || fallbackWinner.name || preistraegerDetailContent.fallbackWinner.name), name: String(cmsWinner.title || fallbackWinner.name || preistraegerDetailContent.fallbackWinner.name),
category: String(cmsWinner.category || fallbackWinner.category || preistraegerDetailContent.fallbackWinner.category), category: String(cmsWinner.category || fallbackWinner.category || preistraegerDetailContent.fallbackWinner.category),
year: Number(cmsWinner.year || new Date().getFullYear()), year: optionalNumber(cmsWinner.year),
type: String(cmsWinner.awardType || (cmsWinnerIsGoldeneBavaria ? 'Goldene Bavaria' : fallbackWinner.awardType) || preistraegerDetailContent.fallbackWinner.awardType), type: String(cmsWinner.awardType || (cmsWinnerIsGoldeneBavaria ? 'Goldene Bavaria' : fallbackWinner.awardType) || preistraegerDetailContent.fallbackWinner.awardType),
img: docImageUrl(cmsWinner, placeholderImage), img: docImageUrl(cmsWinner, placeholderImage),
shortDesc: String(cmsWinner.shortDesc || cmsWinner.description || cmsWinner.meta?.description || ''), shortDesc: String(cmsWinner.shortDesc || cmsWinner.description || cmsWinner.meta?.description || ''),
@@ -134,9 +139,11 @@ export default function PreistraegerDetail() {
}}> }}>
{winner.category} {winner.category}
</span> </span>
<span style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: GOLD }}> {typeof winner.year === 'number' ? (
{winner.year} <span style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, color: GOLD }}>
</span> {winner.year}
</span>
) : null}
<span style={{ fontFamily: FF, fontSize: 15, color: GRAY, textTransform: 'uppercase', letterSpacing: '0.08em' }}> <span style={{ fontFamily: FF, fontSize: 15, color: GRAY, textTransform: 'uppercase', letterSpacing: '0.08em' }}>
{winner.type} {winner.type}
</span> </span>
@@ -196,7 +203,7 @@ export default function PreistraegerDetail() {
{[ {[
{ label: fallbackText(facts.categoryLabel, preistraegerDetailContent.facts.categoryLabel), value: winner.category, icon: <Trophy size={14} style={{ color: GOLD }} /> }, { label: fallbackText(facts.categoryLabel, preistraegerDetailContent.facts.categoryLabel), value: winner.category, icon: <Trophy size={14} style={{ color: GOLD }} /> },
{ label: fallbackText(facts.awardLabel, preistraegerDetailContent.facts.awardLabel), value: winner.type, icon: <Trophy size={14} style={{ color: GOLD }} /> }, { label: fallbackText(facts.awardLabel, preistraegerDetailContent.facts.awardLabel), value: winner.type, icon: <Trophy size={14} style={{ color: GOLD }} /> },
{ label: fallbackText(facts.yearLabel, preistraegerDetailContent.facts.yearLabel), value: String(winner.year), icon: null }, ...(typeof winner.year === 'number' ? [{ label: fallbackText(facts.yearLabel, preistraegerDetailContent.facts.yearLabel), value: String(winner.year), icon: null }] : []),
{ label: fallbackText(facts.locationLabel, preistraegerDetailContent.facts.locationLabel), value: winner.location, icon: <MapPin size={14} style={{ color: GOLD }} /> }, { label: fallbackText(facts.locationLabel, preistraegerDetailContent.facts.locationLabel), value: winner.location, icon: <MapPin size={14} style={{ color: GOLD }} /> },
{ label: fallbackText(facts.industryLabel, preistraegerDetailContent.facts.industryLabel), value: winner.industry, icon: <Building2 size={14} style={{ color: GOLD }} /> }, { label: fallbackText(facts.industryLabel, preistraegerDetailContent.facts.industryLabel), value: winner.industry, icon: <Building2 size={14} style={{ color: GOLD }} /> },
].map(row => ( ].map(row => (