Refactor code structure for improved readability and maintainability

This commit is contained in:
syntaxbullet
2026-07-09 11:50:35 +02:00
parent d2fdf8e3c5
commit 14691ea118
8 changed files with 275 additions and 183 deletions

View File

@@ -272,7 +272,7 @@ export const participationFields: Field[] = [
name: 'awardsGrid', name: 'awardsGrid',
label: '06 · Awards grid', label: '06 · Awards grid',
type: 'group', type: 'group',
admin: sectionAdmin('Three-column awards grid rendered directly after the dates banner.'), admin: sectionAdmin('Awards section rendered after the dates banner. On the participation page, Goldene Bavaria is featured above the remaining award cards.'),
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

@@ -949,7 +949,7 @@ export interface Page {
| null; | null;
}; };
/** /**
* Three-column awards grid rendered directly after the dates banner. * Awards section rendered after the dates banner. On the participation page, Goldene Bavaria is featured above the remaining award cards.
*/ */
awardsGrid?: { awardsGrid?: {
eyebrow?: string | null; eyebrow?: string | null;

View File

@@ -15,6 +15,7 @@ const GOLD = '#EFBF04';
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 };
type ResolvedAwardCard = AwardCard & { image?: unknown };
export type AwardsGridContent = Partial<typeof participationContent.awardsGrid> & { export type AwardsGridContent = Partial<typeof participationContent.awardsGrid> & {
cards?: AwardCardCms[]; cards?: AwardCardCms[];
@@ -23,19 +24,151 @@ export type AwardsGridContent = Partial<typeof participationContent.awardsGrid>
const fallbackText = (value: unknown, fallback: string) => const fallbackText = (value: unknown, fallback: string) =>
typeof value === 'string' && value.length > 0 ? value : fallback; typeof value === 'string' && value.length > 0 ? value : fallback;
const mergeAwardCards = (value: unknown, fallback: AwardCard[]): (AwardCard & { image?: unknown })[] => { const normalizeLookupText = (value: unknown) =>
fallbackText(value, '').replace(/\s+/g, ' ').trim().toLowerCase();
const findAwardFallback = (card: AwardCardCms, index: number, fallback: AwardCard[]) => {
const ctaUrl = normalizeLookupText(card.articleCta?.url);
const title = normalizeLookupText(card.title);
return (
fallback.find((item) => ctaUrl.length > 0 && normalizeLookupText(item.articleCta?.url) === ctaUrl) ||
fallback.find((item) => title.length > 0 && normalizeLookupText(item.title) === title) ||
fallback[index] ||
fallback[0]
);
};
const mergeAwardCards = (value: unknown, fallback: AwardCard[]): ResolvedAwardCard[] => {
const cards = Array.isArray(value) && value.length > 0 ? (value as AwardCardCms[]) : fallback; const cards = Array.isArray(value) && value.length > 0 ? (value as AwardCardCms[]) : fallback;
return cards.map((card, index) => ({ ...(fallback[index] || fallback[0]), ...card })); return cards.map((card, index) => ({ ...findAwardFallback(card, index, fallback), ...card }));
}; };
function Lines({ text }: { text: string }) { function Lines({ text }: { text: string }) {
return <>{text.split('\n').map((line, i) => <React.Fragment key={`${line}-${i}`}>{i > 0 && <br />}{line}</React.Fragment>)}</>; return <>{text.split('\n').map((line, i) => <React.Fragment key={`${line}-${i}`}>{i > 0 && <br />}{line}</React.Fragment>)}</>;
} }
export default function AwardsGridSection({ content }: { content?: AwardsGridContent }) { const isGoldeneBavariaCard = (card: ResolvedAwardCard) =>
normalizeLookupText(card.articleCta?.url).includes('goldene-bavaria') ||
normalizeLookupText(card.title).includes('goldene') ||
normalizeLookupText(card.label).includes('goldene');
const splitFeaturedAwardCard = (cards: ResolvedAwardCard[], enabled: boolean) => {
if (!enabled) return { featuredCard: null, supportingCards: cards };
const featuredIndex = cards.findIndex(isGoldeneBavariaCard);
if (featuredIndex < 0) return { featuredCard: null, supportingCards: cards };
return {
featuredCard: cards[featuredIndex],
supportingCards: cards.filter((_, index) => index !== featuredIndex),
};
};
const getAwardImageFilename = (card: ResolvedAwardCard, fallbackIndex: number) =>
fallbackText(
card.imageFilename,
participationContent.awardsGrid.cards[fallbackIndex]?.imageFilename || participationContent.awardsGrid.cards[0].imageFilename,
);
const isContainedAwardImage = (imageFilename: string) =>
imageFilename.endsWith('.png') || imageFilename.includes('roland-berger');
function FeaturedAwardCard({ card }: { card: ResolvedAwardCard }) {
const isMobile = useIsMobile();
const imageFilename = getAwardImageFilename(card, 0);
const isContainedImage = isContainedAwardImage(imageFilename);
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)' }}>
<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' }}>
<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: 'center',
padding: imageFilename.endsWith('.png') ? (isMobile ? 22 : 30) : 0,
boxSizing: 'border-box',
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>
<div style={{ background: NAVY, color: '#fff', padding: isMobile ? '24px 24px 30px' : '34px 48px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.32em', textTransform: 'uppercase', color: 'rgba(239,191,4,0.86)', marginBottom: 16 }}>
{fallbackText(card.label, 'Auszeichnung 01')}
</span>
<h3 style={{ fontFamily: FF, fontSize: isMobile ? 'clamp(1.7rem, 8vw, 2.8rem)' : 'clamp(2.1rem, 3.2vw, 3.3rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 0.98, margin: '0 0 14px', overflowWrap: 'anywhere' }}>
<Lines text={fallbackText(card.title, '')} />
</h3>
<div style={{ width: 44, height: 2, background: GOLD, marginBottom: 18 }} />
<p style={{ fontFamily: FB, fontSize: isMobile ? 16 : 17, color: 'rgba(255,255,255,0.68)', lineHeight: 1.6, margin: 0, maxWidth: 560 }}>
{fallbackText(card.description, '')}
</p>
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', alignItems: 'stretch', gap: 10, marginTop: 24 }}>
<Link
to={fallbackText(card.articleCta?.url, '#bewerben')}
style={{
minHeight: 44,
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: 44,
padding: '0 18px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
color: '#fff',
border: '1px solid rgba(255,255,255,0.24)',
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>
</section>
);
}
export default function AwardsGridSection({ content, featuredGoldeneBavaria = false }: { content?: AwardsGridContent; featuredGoldeneBavaria?: boolean }) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const section = { ...participationContent.awardsGrid, ...(content || {}) }; const section = { ...participationContent.awardsGrid, ...(content || {}) };
const cards = mergeAwardCards(section.cards, participationContent.awardsGrid.cards); const cards = mergeAwardCards(section.cards, participationContent.awardsGrid.cards);
const { featuredCard, supportingCards } = splitFeaturedAwardCard(cards, featuredGoldeneBavaria);
return ( return (
<section id="auszeichnungen" style={{ background: '#F7F7F5', position: 'relative', overflow: 'hidden', isolation: 'isolate' }}> <section id="auszeichnungen" style={{ background: '#F7F7F5', position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
@@ -54,11 +187,16 @@ export default function AwardsGridSection({ content }: { content?: AwardsGridCon
</div> </div>
</div> </div>
<div style={{ position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, minmax(0, 1fr))', borderBottom: '1px solid rgba(3,9,58,0.08)' }}> {featuredCard ? <FeaturedAwardCard card={featuredCard} /> : null}
{cards.map((card, index) => {
const imageFilename = fallbackText(card.imageFilename, participationContent.awardsGrid.cards[index]?.imageFilename || participationContent.awardsGrid.cards[0].imageFilename); {supportingCards.length > 0 ? (
const isContainedImage = imageFilename.endsWith('.png') || imageFilename.includes('roland-berger'); <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)' }}>
const isLast = index === cards.length - 1; {supportingCards.map((card, index) => {
const sourceIndex = cards.indexOf(card);
const imageFilename = getAwardImageFilename(card, sourceIndex >= 0 ? sourceIndex : index);
const isContainedImage = isContainedAwardImage(imageFilename);
const isLast = index === supportingCards.length - 1;
const isDarkCard = !featuredCard && index === 1;
return ( return (
<article <article
@@ -66,10 +204,10 @@ export default function AwardsGridSection({ content }: { content?: AwardsGridCon
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
minHeight: isMobile ? 'auto' : 620, minHeight: isMobile ? 'auto' : featuredCard ? 540 : 620,
borderRight: !isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none', borderRight: !isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
borderBottom: isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none', borderBottom: isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
background: index === 1 ? NAVY : 'rgba(255,255,255,0.72)', 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' }}> <div style={{ height: isMobile ? 220 : 260, background: imageFilename.includes('roland-berger') ? '#DDE7E2' : NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
@@ -80,7 +218,7 @@ export default function AwardsGridSection({ content }: { content?: AwardsGridCon
width: '100%', width: '100%',
height: '100%', height: '100%',
objectFit: isContainedImage ? 'contain' : 'cover', objectFit: isContainedImage ? 'contain' : 'cover',
objectPosition: index === 1 ? 'center top' : 'center', objectPosition: isDarkCard ? 'center top' : 'center',
padding: imageFilename.endsWith('.png') ? 22 : 0, padding: imageFilename.endsWith('.png') ? 22 : 0,
boxSizing: 'border-box', boxSizing: 'border-box',
display: 'block', display: 'block',
@@ -88,15 +226,15 @@ export default function AwardsGridSection({ content }: { content?: AwardsGridCon
/> />
</div> </div>
<div style={{ padding: isMobile ? '28px 24px 32px' : '36px 36px 40px', display: 'flex', flexDirection: 'column', flex: 1 }}> <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: index === 1 ? 'rgba(239,191,4,0.8)' : '#4A8FC9', marginBottom: 12 }}> <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, '')} {fallbackText(card.label, '')}
</span> </span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.25rem, 2vw, 1.7rem)', fontWeight: 900, color: index === 1 ? '#fff' : '#101828', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.08, margin: '0 0 16px', overflowWrap: 'anywhere' }}> <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, '')} /> <Lines text={fallbackText(card.title, '')} />
</h3> </h3>
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 22 }} /> <div style={{ width: 36, height: 2, background: GOLD, marginBottom: 22 }} />
<p style={{ fontFamily: FB, fontSize: 18, color: index === 1 ? 'rgba(255,255,255,0.62)' : 'rgba(16,24,40,0.55)', lineHeight: 1.7, margin: 0, flex: 1 }}> <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, '')} {fallbackText(card.description, '')}
</p> </p>
@@ -133,8 +271,8 @@ export default function AwardsGridSection({ content }: { content?: AwardsGridCon
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 8, gap: 8,
color: index === 1 ? '#fff' : NAVY, color: isDarkCard ? '#fff' : NAVY,
border: `1px solid ${index === 1 ? 'rgba(255,255,255,0.22)' : 'rgba(3,9,58,0.18)'}`, border: `1px solid ${isDarkCard ? 'rgba(255,255,255,0.22)' : 'rgba(3,9,58,0.18)'}`,
fontFamily: FF, fontFamily: FF,
fontSize: 13, fontSize: 13,
fontWeight: 700, fontWeight: 700,
@@ -153,6 +291,7 @@ export default function AwardsGridSection({ content }: { content?: AwardsGridCon
); );
})} })}
</div> </div>
) : null}
</section> </section>
); );
} }

View File

@@ -76,7 +76,7 @@ export default function WegZurAuszeichnungSection({ content }: { content?: Proce
</h3> </h3>
</div> </div>
</div> </div>
<p style={{ fontFamily: FB, fontSize: 15, color: 'rgba(255,255,255,0.68)', lineHeight: 1.7, margin: 0 }}> <p style={{ fontFamily: FB, fontSize: 17, color: 'rgba(255,255,255,0.68)', lineHeight: 1.7, margin: 0 }}>
{fallbackText(item.desc, '')} {fallbackText(item.desc, '')}
</p> </p>
{/* Step progress indicator */} {/* Step progress indicator */}
@@ -171,7 +171,7 @@ export default function WegZurAuszeichnungSection({ content }: { content?: Proce
{fallbackText(item.title, '')} {fallbackText(item.title, '')}
</h3> </h3>
</div> </div>
<p style={{ fontFamily: FB, fontSize: 15, color: 'rgba(255,255,255,0.68)', lineHeight: 1.75, margin: 0 }}> <p style={{ fontFamily: FB, fontSize: 17, color: 'rgba(255,255,255,0.68)', lineHeight: 1.7, margin: 0 }}>
{fallbackText(item.desc, '')} {fallbackText(item.desc, '')}
</p> </p>
{/* Step progress indicator */} {/* Step progress indicator */}

View File

@@ -26,22 +26,29 @@ const fallbackText = (value: unknown, fallback: string) =>
const logoShellStyle: React.CSSProperties = { const logoShellStyle: React.CSSProperties = {
alignItems: 'center', alignItems: 'center',
boxSizing: 'border-box',
display: 'flex', display: 'flex',
height: '100%', height: '100%',
justifyContent: 'center', justifyContent: 'center',
maxHeight: '100%', maxHeight: '100%',
maxWidth: '100%',
minWidth: 0, minWidth: 0,
overflow: 'hidden',
width: '100%', width: '100%',
} }
const containedTextStyle: React.CSSProperties = { const containedTextStyle: React.CSSProperties = {
display: 'block',
maxWidth: '100%',
minWidth: 0, minWidth: 0,
textAlign: 'center',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
} }
const fittedTextSize = (text: string, preferred: number, compact = 12) => { const fittedTextSize = (text: string, preferred: number, compact = 12) => {
if (text.length > 18) return compact - 1 if (text.length > 24) return Math.max(9, compact - 3)
if (text.length > 13) return compact if (text.length > 18) return Math.max(10, compact - 2)
if (text.length > 13) return compact - 1
if (text.length > 9) return Math.max(compact + 1, preferred - 3) if (text.length > 9) return Math.max(compact + 1, preferred - 3)
return preferred return preferred
} }
@@ -80,7 +87,7 @@ export function PartnerLogoMark({
if (partner.logoKind === 'badge') { if (partner.logoKind === 'badge') {
return ( return (
<span style={logoShellStyle}> <span style={logoShellStyle}>
<span style={{ background: color, color: '#fff', fontWeight: 900, fontSize: size(14), padding: `${size(4)}px ${size(10)}px`, lineHeight: 1.25, display: 'inline-block', textAlign: 'center', fontFamily: FF }}> <span style={{ background: color, color: '#fff', fontWeight: 900, fontSize: size(fittedTextSize(text, 14, 11)), padding: `${size(4)}px ${size(10)}px`, lineHeight: 1.25, display: 'inline-block', maxWidth: '100%', textAlign: 'center', fontFamily: FF }}>
{text.split('\n').map((line, index) => ( {text.split('\n').map((line, index) => (
<React.Fragment key={`${line}-${index}`}>{index > 0 && <br />}{line}</React.Fragment> <React.Fragment key={`${line}-${index}`}>{index > 0 && <br />}{line}</React.Fragment>
))} ))}
@@ -92,7 +99,7 @@ export function PartnerLogoMark({
if (partner.logoKind === 'monogram') { if (partner.logoKind === 'monogram') {
return ( return (
<span style={logoShellStyle}> <span style={logoShellStyle}>
<span style={{ display: 'flex', alignItems: 'center', gap: size(6), minWidth: 0 }}> <span style={{ display: 'flex', alignItems: 'center', gap: size(6), maxWidth: '100%', minWidth: 0 }}>
<span style={{ width: size(22), height: size(22), background: '#3a3a3a', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 900, fontSize: size(11), fontFamily: FF, flexShrink: 0 }}>{subtext || text.slice(0, 2)}</span> <span style={{ width: size(22), height: size(22), background: '#3a3a3a', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 900, fontSize: size(11), fontFamily: FF, flexShrink: 0 }}>{subtext || text.slice(0, 2)}</span>
<span style={{ ...containedTextStyle, fontWeight: 700, fontSize: size(fittedTextSize(text, 15, 11)), color, fontFamily: FF }}>{text}</span> <span style={{ ...containedTextStyle, fontWeight: 700, fontSize: size(fittedTextSize(text, 15, 11)), color, fontFamily: FF }}>{text}</span>
</span> </span>
@@ -103,7 +110,7 @@ export function PartnerLogoMark({
if (partner.logoKind === 'dot') { if (partner.logoKind === 'dot') {
return ( return (
<span style={logoShellStyle}> <span style={logoShellStyle}>
<span style={{ display: 'flex', alignItems: 'center', gap: size(6), minWidth: 0 }}> <span style={{ display: 'flex', alignItems: 'center', gap: size(6), maxWidth: '100%', minWidth: 0 }}>
<span style={{ width: size(20), height: size(20), borderRadius: '50%', background: color, display: 'block', flexShrink: 0 }} /> <span style={{ width: size(20), height: size(20), borderRadius: '50%', background: color, display: 'block', flexShrink: 0 }} />
<span style={{ ...containedTextStyle, fontWeight: 700, fontSize: size(fittedTextSize(text, 16, 11)), color, fontFamily: FF }}>{text}</span> <span style={{ ...containedTextStyle, fontWeight: 700, fontSize: size(fittedTextSize(text, 16, 11)), color, fontFamily: FF }}>{text}</span>
</span> </span>
@@ -114,7 +121,7 @@ export function PartnerLogoMark({
if (partner.logoKind === 'leaf') { if (partner.logoKind === 'leaf') {
return ( return (
<span style={logoShellStyle}> <span style={logoShellStyle}>
<span style={{ display: 'flex', alignItems: 'center', gap: size(6), minWidth: 0 }}> <span style={{ display: 'flex', alignItems: 'center', gap: size(6), maxWidth: '100%', minWidth: 0 }}>
<svg width={size(20)} height={size(20)} viewBox="0 0 20 20" aria-hidden="true" style={{ flexShrink: 0 }}><circle cx="10" cy="10" r="9" fill={color} /><path d="M10 3 Q14 7 14 10 Q14 14 10 17 Q6 14 6 10 Q6 7 10 3Z" fill="white" opacity="0.8" /></svg> <svg width={size(20)} height={size(20)} viewBox="0 0 20 20" aria-hidden="true" style={{ flexShrink: 0 }}><circle cx="10" cy="10" r="9" fill={color} /><path d="M10 3 Q14 7 14 10 Q14 14 10 17 Q6 14 6 10 Q6 7 10 3Z" fill="white" opacity="0.8" /></svg>
<span style={{ ...containedTextStyle, fontWeight: 700, fontSize: size(fittedTextSize(text, 15, 11)), color, fontFamily: FB }}>{text}</span> <span style={{ ...containedTextStyle, fontWeight: 700, fontSize: size(fittedTextSize(text, 15, 11)), color, fontFamily: FB }}>{text}</span>
</span> </span>
@@ -125,7 +132,7 @@ export function PartnerLogoMark({
if (partner.logoKind === 'rsm') { if (partner.logoKind === 'rsm') {
return ( return (
<span style={logoShellStyle}> <span style={logoShellStyle}>
<span style={{ display: 'flex', alignItems: 'center', gap: size(6), minWidth: 0 }}> <span style={{ display: 'flex', alignItems: 'center', gap: size(6), maxWidth: '100%', minWidth: 0 }}>
<span style={{ display: 'flex', gap: Math.max(2, size(2)), flexShrink: 0 }}> <span style={{ display: 'flex', gap: Math.max(2, size(2)), flexShrink: 0 }}>
<span style={{ width: size(10), height: size(10), background: '#6b7280', display: 'block' }} /> <span style={{ width: size(10), height: size(10), background: '#6b7280', display: 'block' }} />
<span style={{ width: size(10), height: size(10), background: '#22c55e', display: 'block' }} /> <span style={{ width: size(10), height: size(10), background: '#22c55e', display: 'block' }} />
@@ -140,7 +147,7 @@ export function PartnerLogoMark({
if (partner.logoKind === 'deutschebank') { if (partner.logoKind === 'deutschebank') {
return ( return (
<span style={logoShellStyle}> <span style={logoShellStyle}>
<span style={{ display: 'flex', alignItems: 'center', gap: size(8), minWidth: 0 }}> <span style={{ display: 'flex', alignItems: 'center', gap: size(8), maxWidth: '100%', minWidth: 0 }}>
<svg width={size(20)} height={size(20)} viewBox="0 0 20 20" aria-hidden="true" style={{ flexShrink: 0 }}><rect x="1" y="1" width="18" height="18" fill="none" stroke={color} strokeWidth="1.5" /><line x1="5" y1="15" x2="15" y2="5" stroke={color} strokeWidth="2" /></svg> <svg width={size(20)} height={size(20)} viewBox="0 0 20 20" aria-hidden="true" style={{ flexShrink: 0 }}><rect x="1" y="1" width="18" height="18" fill="none" stroke={color} strokeWidth="1.5" /><line x1="5" y1="15" x2="15" y2="5" stroke={color} strokeWidth="2" /></svg>
<span style={{ ...containedTextStyle, fontWeight: 600, fontSize: size(fittedTextSize(text, 15, 11)), color, fontFamily: FB }}>{text}</span> <span style={{ ...containedTextStyle, fontWeight: 600, fontSize: size(fittedTextSize(text, 15, 11)), color, fontFamily: FB }}>{text}</span>
</span> </span>

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Link } from '@/spa/router'; import { Link } from '@/spa/router';
import { ChevronRight, X, ArrowRight, Globe, Linkedin, Building2, MapPin, ExternalLink, Play } from 'lucide-react'; import { X, ArrowRight, Globe, Linkedin, Building2, MapPin, ExternalLink, Play } from 'lucide-react';
import SponsoringForm from '@/spa/components/forms/SponsoringForm'; import SponsoringForm from '@/spa/components/forms/SponsoringForm';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'; import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
@@ -16,7 +16,6 @@ const FB = '"Inter", sans-serif';
const NAVY = '#111D55'; const NAVY = '#111D55';
const GOLD = '#EFBF04'; const GOLD = '#EFBF04';
const CREAM = '#E4E2E3'; const CREAM = '#E4E2E3';
const HIDE_MEMBERSHIP_NETWORK_CTA = true;
type NetzwerkCms = Partial<typeof netzwerkContent> & { type NetzwerkCms = Partial<typeof netzwerkContent> & {
hero?: Partial<typeof netzwerkContent.hero> & { image?: unknown } hero?: Partial<typeof netzwerkContent.hero> & { image?: unknown }
@@ -98,12 +97,6 @@ const sortedJuryMembers = (members: JuryMember[]) =>
.map((member, index) => ({ member, index })) .map((member, index) => ({ member, index }))
.sort((a, b) => Number(a.member.sortOrder ?? a.index) - Number(b.member.sortOrder ?? b.index)) .sort((a, b) => Number(a.member.sortOrder ?? a.index) - Number(b.member.sortOrder ?? b.index))
.map(({ member }) => member) .map(({ member }) => member)
const isMembershipCta = (cta?: { label?: string | null; url?: string | null } | null) => {
const normalizedLabel = cta?.label?.trim().toLowerCase();
return cta?.url === '/mitglied-werden' || normalizedLabel === 'mitglied werden' || normalizedLabel === 'vereinsmitglied werden';
}
function Lines({ text, highlight }: { text: string; highlight?: string }) { function Lines({ text, highlight }: { text: string; highlight?: string }) {
return ( return (
<> <>
@@ -174,10 +167,10 @@ const JuryCard: React.FC<{ member: JuryMember }> = ({ member }) => {
<div <div
onMouseEnter={() => setHovered(true)} onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)} onMouseLeave={() => setHovered(false)}
style={{ display: 'flex', flexDirection: 'column', height: '100%', cursor: 'default', background: hovered ? 'rgba(255,255,255,0.04)' : NAVY, transition: 'background 0.2s', ...(isMobileCard ? { minWidth: '58vw', maxWidth: '58vw', flexShrink: 0, scrollSnapAlign: 'start', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 16, overflow: 'hidden' } : { minWidth: 0, border: '1px solid rgba(255,255,255,0.07)' }) }} style={{ display: 'flex', flexDirection: 'column', height: '100%', cursor: 'default', background: hovered ? 'rgba(255,255,255,0.04)' : NAVY, transition: 'background 0.2s', ...(isMobileCard ? { minWidth: '72vw', maxWidth: '72vw', flexShrink: 0, scrollSnapAlign: 'start', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 16, overflow: 'hidden' } : { minWidth: 0, border: '1px solid rgba(255,255,255,0.07)' }) }}
> >
<div style={{ position: 'relative', overflow: 'hidden', height: isMobileCard ? 142 : 170, flexShrink: 0 }}> <div style={{ position: 'relative', overflow: 'hidden', height: isMobileCard ? 142 : 170, flexShrink: 0, background: 'rgba(255,255,255,0.03)' }}>
<Image unoptimized src={managedImage(member.imageUpload, member.image)} alt={fallbackText(member.imageAlt, member.name)} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} /> <Image unoptimized src={managedImage(member.imageUpload, member.image)} alt={fallbackText(member.imageAlt, member.name)} style={{ width: '100%', height: '100%', objectFit: 'contain', objectPosition: 'center top', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '40%', background: 'linear-gradient(to top, rgba(3,9,58,0.65), transparent)' }} /> <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '40%', background: 'linear-gradient(to top, rgba(3,9,58,0.65), transparent)' }} />
</div> </div>
<div style={{ padding: isMobileCard ? '18px 18px 22px' : '20px 20px 24px', flex: 1, minWidth: 0, overflowWrap: 'anywhere', wordBreak: 'break-word' }}> <div style={{ padding: isMobileCard ? '18px 18px 22px' : '20px 20px 24px', flex: 1, minWidth: 0, overflowWrap: 'anywhere', wordBreak: 'break-word' }}>
@@ -215,8 +208,8 @@ const JuryChairCard: React.FC<{ member: JuryMember; badge: string }> = ({ member
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
<div style={{ position: 'relative', overflow: 'hidden', aspectRatio: isMobileCard ? '16/10' : '3/4', minHeight: isMobileCard ? 240 : 'auto' }}> <div style={{ position: 'relative', overflow: 'hidden', aspectRatio: isMobileCard ? '16/10' : '3/4', minHeight: isMobileCard ? 240 : 'auto', background: 'rgba(255,255,255,0.03)' }}>
<Image unoptimized src={managedImage(member.imageUpload, member.image)} alt={fallbackText(member.imageAlt, member.name)} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} /> <Image unoptimized src={managedImage(member.imageUpload, member.image)} alt={fallbackText(member.imageAlt, member.name)} style={{ width: '100%', height: '100%', objectFit: 'contain', objectPosition: 'center top', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '45%', background: 'linear-gradient(to top, rgba(3,9,58,0.7), transparent)' }} /> <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '45%', background: 'linear-gradient(to top, rgba(3,9,58,0.7), transparent)' }} />
</div> </div>
<div style={{ padding: isMobileCard ? '32px 28px 36px' : '56px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}> <div style={{ padding: isMobileCard ? '32px 28px 36px' : '56px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
@@ -354,20 +347,25 @@ function PartnerModal({ partner, copy, onClose }: { partner: Partner; copy: Part
</button> </button>
</div> </div>
{/* Logo box – bottom left (overflows hero) */} {/* Logo box – bottom left */}
<div style={{ <div style={{
position: 'absolute', bottom: -28, left: 28, position: 'absolute', bottom: -38, left: 28,
width: 80, height: 80, background: '#fff', width: 'min(260px, calc(100% - 56px))', minWidth: 112, height: 112, background: '#fff',
borderRadius: 12, boxShadow: '0 4px 24px rgba(0,0,0,0.14)', borderRadius: 12, boxShadow: '0 4px 24px rgba(0,0,0,0.14)',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: 10, zIndex: 2, boxSizing: 'border-box',
flexShrink: 0,
overflow: 'hidden',
padding: 14, zIndex: 2,
}}> }}>
<div style={{ width: '100%', height: '100%', minWidth: 0, overflow: 'hidden' }}>
<PartnerLogoMark partner={partner} /> <PartnerLogoMark partner={partner} />
</div> </div>
</div> </div>
</div>
{/* ── Content ── */} {/* ── Content ── */}
<div style={{ padding: '52px 32px 32px' }}> <div style={{ padding: '72px 32px 32px' }}>
<h2 style={{ <h2 style={{
fontFamily: FF, fontSize: 'clamp(1.5rem, 3vw, 2rem)', fontFamily: FF, fontSize: 'clamp(1.5rem, 3vw, 2rem)',
fontWeight: 900, color: '#101828', fontWeight: 900, color: '#101828',
@@ -507,9 +505,6 @@ const Netzwerk: React.FC = () => {
const greetingVideoTitle = fallbackText(homeVideoModal.title, homeContent.videoModal.title); const greetingVideoTitle = fallbackText(homeVideoModal.title, homeContent.videoModal.title);
const greetingVideoLabel = fallbackText(home.hero?.videoLabel, homeContent.hero.videoLabel); const greetingVideoLabel = fallbackText(home.hero?.videoLabel, homeContent.hero.videoLabel);
const greetingVideoCloseLabel = fallbackText(homeVideoModal.closeLabel, homeContent.videoModal.closeLabel); const greetingVideoCloseLabel = fallbackText(homeVideoModal.closeLabel, homeContent.videoModal.closeLabel);
const showMembershipFootnote =
!HIDE_MEMBERSHIP_NETWORK_CTA ||
!isMembershipCta({ label: sponsoring.footnoteLabel, url: sponsoring.footnoteUrl });
useEffect(() => { useEffect(() => {
if (!showGreetingVideo || !greetingVideoSrc) return; if (!showGreetingVideo || !greetingVideoSrc) return;
@@ -712,18 +707,11 @@ const Netzwerk: React.FC = () => {
</div> </div>
{/* ── Übrige Jury-Mitglieder ──────────────────────────────────────── */} {/* ── Übrige Jury-Mitglieder ──────────────────────────────────────── */}
<div style={{ display: isMobile ? 'flex' : 'grid', gridTemplateColumns: isMobile ? undefined : 'repeat(auto-fill, minmax(210px, 1fr))', overflowX: isMobile ? 'auto' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined, gap: isMobile ? 12 : 0, padding: isMobile ? '24px 24px 12px' : '0', WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none', borderTop: isMobile ? 'none' : '1px solid rgba(255,255,255,0.07)' }}> <div style={{ display: isMobile ? 'flex' : 'grid', gridTemplateColumns: isMobile ? undefined : 'repeat(auto-fill, minmax(260px, 1fr))', overflowX: isMobile ? 'auto' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined, gap: isMobile ? 12 : 0, padding: isMobile ? '24px 24px 36px' : '0 0 56px', WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none', borderTop: isMobile ? 'none' : '1px solid rgba(255,255,255,0.07)' }}>
{juryMembers.slice(1).map((member, idx) => ( {juryMembers.slice(1).map((member, idx) => (
<JuryCard key={idx} member={member} /> <JuryCard key={idx} member={member} />
))} ))}
</div> </div>
{/* ── Hinweis: Doppelrolle Jury / Partner (#179) ──────────────────── */}
<div style={{ padding: isMobile ? '4px 24px 36px' : '8px 80px 56px' }}>
<p style={{ fontFamily: FB, fontSize: isMobile ? 13 : 14, color: 'rgba(255,255,255,0.35)', lineHeight: 1.6, margin: 0, maxWidth: 720 }}>
{fallbackText(jury.note, netzwerkContent.jury.note)}
</p>
</div>
</section> </section>
@@ -850,40 +838,6 @@ const Netzwerk: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Footnote */}
{showMembershipFootnote && (
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '16px 20px 20px' : '14px 56px 18px', display: 'flex', flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center', borderTop: '1px solid rgba(255,255,255,0.06)', flexShrink: 0 }}>
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 15, color: 'rgba(255,255,255,0.45)', marginRight: 10 }}>{fallbackText(sponsoring.footnotePrefix, netzwerkContent.sponsoring.footnotePrefix)}</span>
<Link
to={fallbackText(sponsoring.footnoteUrl, netzwerkContent.sponsoring.footnoteUrl)}
style={{
fontFamily: '"IBM Plex Sans", sans-serif',
fontSize: 15,
fontWeight: 700,
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: 'rgba(239,191,4,0.7)',
textDecoration: 'none',
display: 'inline-flex',
alignItems: 'center',
gap: 6,
borderBottom: '1px solid rgba(239,191,4,0.3)',
paddingBottom: 1,
transition: 'color 0.15s, borderColor 0.15s',
}}
onMouseEnter={e => {
(e.currentTarget as HTMLElement).style.color = '#EFBF04';
(e.currentTarget as HTMLElement).style.borderBottomColor = '#EFBF04';
}}
onMouseLeave={e => {
(e.currentTarget as HTMLElement).style.color = 'rgba(239,191,4,0.7)';
(e.currentTarget as HTMLElement).style.borderBottomColor = 'rgba(239,191,4,0.3)';
}}
>
{fallbackText(sponsoring.footnoteLabel, netzwerkContent.sponsoring.footnoteLabel)} <ChevronRight size={11} />
</Link>
</div>
)}
<div style={{ position: 'relative', zIndex: 1, height: 2, background: `linear-gradient(to right, ${GOLD}, rgba(239,191,4,0.3), transparent)`, flexShrink: 0 }} /> <div style={{ position: 'relative', zIndex: 1, height: 2, background: `linear-gradient(to right, ${GOLD}, rgba(239,191,4,0.3), transparent)`, flexShrink: 0 }} />
</section> </section>

View File

@@ -169,14 +169,6 @@ const Participation: React.FC = () => {
> >
{fallbackText(eligibility.primaryCta?.label, participationContent.eligibility.primaryCta.label)} <ArrowRight size={13} /> {fallbackText(eligibility.primaryCta?.label, participationContent.eligibility.primaryCta.label)} <ArrowRight size={13} />
</Link> </Link>
<Link
to={fallbackText(eligibility.secondaryCta?.url, participationContent.eligibility.secondaryCta.url)}
style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: NAVY, border: `1.5px solid rgba(3,9,58,0.3)`, padding: '10px 22px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 8, transition: 'border-color 0.2s, color 0.2s', whiteSpace: 'nowrap' }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = NAVY; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(3,9,58,0.3)'; }}
>
{fallbackText(eligibility.secondaryCta?.label, participationContent.eligibility.secondaryCta.label)}
</Link>
</div> </div>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.5)', lineHeight: 1.8, margin: 0, textAlign: isMobile ? 'left' : 'right' }}> <p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.5)', lineHeight: 1.8, margin: 0, textAlign: isMobile ? 'left' : 'right' }}>
{fallbackText(eligibility.description, participationContent.eligibility.description)} {fallbackText(eligibility.description, participationContent.eligibility.description)}
@@ -269,7 +261,7 @@ const Participation: React.FC = () => {
</section> </section>
{/* ── AWARDS GRID ─────────────────────────────────────────────────────── */} {/* ── AWARDS GRID ─────────────────────────────────────────────────────── */}
<AwardsGridSection content={awardsGrid} /> <AwardsGridSection content={awardsGrid} featuredGoldeneBavaria />
{/* ── FORM SECTION ─────────────────────────────────────────────────────── */} {/* ── FORM SECTION ─────────────────────────────────────────────────────── */}
<section style={{ background: NAVY, position: 'relative', overflow: 'hidden', height: isMobile ? 'auto' : showApplicationForm ? 'calc(100vh - 60px)' : 'auto', minHeight: isMobile ? undefined : showApplicationForm ? undefined : 560, display: 'flex', flexDirection: 'column' }} id="bewerben"> <section style={{ background: NAVY, position: 'relative', overflow: 'hidden', height: isMobile ? 'auto' : showApplicationForm ? 'calc(100vh - 60px)' : 'auto', minHeight: isMobile ? undefined : showApplicationForm ? undefined : 560, display: 'flex', flexDirection: 'column' }} id="bewerben">

File diff suppressed because one or more lines are too long