feat: add participation and network support pages, update about fields and home winners section

- Added support for '/teilnahme' and '/netzwerk' in findSupportPages function.
- Updated the description in aboutFields to clarify the relationship with the homepage Preisträger grid.
- Enhanced the About page by integrating HomeWinnersSection for better modularity.
- Created HomeWinnersSection component to encapsulate winners display logic and improve code organization.
- Removed unused WinnerCardData type and related logic from Home component.
This commit is contained in:
syntaxbullet
2026-07-13 19:18:12 +02:00
parent 234da4cdac
commit 308d75c44a
6 changed files with 217 additions and 275 deletions

View File

@@ -1,40 +1,32 @@
import React, { useMemo, useState } from 'react'
import { ChevronRight, Star } from 'lucide-react'
import React, { useState } from 'react'
import { ChevronRight } from 'lucide-react'
import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection'
import HomeWinnersSection, { type HomeWinnersContent } from '@/spa/components/HomeWinnersSection'
import TestimonialsSection from '@/spa/components/TestimonialsSection'
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg'
import Image from '@/spa/components/ui/UnoptimizedImage'
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField'
import { resolveCmsRelationship, useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute'
import { WINNERS, type Winner } from '@/spa/data/winners'
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute'
import { useIsMobile } from '@/spa/hooks/useIsMobile'
import { Link } from '@/spa/router'
import { aboutContent } from '@/spa/aboutContent'
import { usePreistraegerPlaceholderImage } from '@/spa/preistraegerPlaceholder'
const FF = '"IBM Plex Sans", sans-serif'
const FB = '"Inter", sans-serif'
const NAVY = '#111D55'
const GOLD = '#EFBF04'
const CREAM = '#E4E2E3'
const WHITE = '#fff'
const HIDE_MEMBERSHIP_ABOUT_CTA = true
type AboutCms = Partial<typeof aboutContent> & {
hero?: Partial<typeof aboutContent.hero> & { backgroundImage?: unknown }
prize?: Partial<typeof aboutContent.prize> & { image?: unknown }
mittelstand?: Partial<typeof aboutContent.mittelstand> & { image?: unknown }
highlights?: Partial<typeof aboutContent.highlights> & {
fallbackImage?: unknown
fallbackWinner?: Partial<typeof aboutContent.highlights.fallbackWinner>
selectedWinners?: unknown[] | null
}
}
type TextRow = { text?: string | null }
const FALLBACK_HIGHLIGHTS = WINNERS.filter((w) => w.year === aboutContent.highlights.year).slice(0, 8)
const fallbackText = (value: unknown, fallback: string) => (typeof value === 'string' && value.length > 0 ? value : fallback)
const fallbackArray = <T,>(value: unknown, fallback: T[]) => (Array.isArray(value) && value.length ? (value as T[]) : fallback)
@@ -69,6 +61,12 @@ const isParticipationPage = (page: CmsRouteDoc) => {
return page.spaPath === '/teilnahme' || page.slug === 'teilnahme' || page.slug === 'participation' || title === 'teilnahme' || title === 'participation'
}
const isHomePage = (page: CmsRouteDoc) => {
const title = String(page.title || '').toLowerCase()
return page.spaPath === '/' || page.slug === 'startseite' || page.slug === 'home' || title === 'startseite' || title === 'home'
}
function HighlightedText({ text, highlight }: { text: string; highlight: string }) {
const index = text.indexOf(highlight)
if (!highlight || index < 0) return <>{text}</>
@@ -82,72 +80,23 @@ function HighlightedText({ text, highlight }: { text: string; highlight: string
)
}
const normalizeWinner = (
doc: CmsRouteDoc | undefined,
placeholderImage: string,
fallbackWinner: typeof aboutContent.highlights.fallbackWinner,
): Winner | undefined => {
if (!doc) return undefined
return {
id: String(doc.id),
slug: String(doc.slug || ''),
name: String(doc.title || fallbackWinner.name),
category: String(doc.category || fallbackWinner.category),
year: Number(doc.year || aboutContent.highlights.year),
type: String(doc.awardType || fallbackWinner.awardType),
img: mediaUrl(doc.image, 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 || doc.title || ''),
quoteRole: String(doc.quoteRole || ''),
location: String(doc.location || ''),
industry: String(doc.industry || ''),
website: String(doc.website || ''),
hasMedia: Boolean(doc.hasMedia),
}
}
const About: React.FC = () => {
const isMobile = useIsMobile()
const placeholderImage = usePreistraegerPlaceholderImage()
const cms = (useCmsRoute()?.doc?.about || {}) as AboutCms
const cmsWinners = useCmsCollection('preistraeger')
const cmsPages = useCmsCollection('pages')
const hero = { ...aboutContent.hero, ...(cms.hero || {}) }
const prize = { ...aboutContent.prize, ...(cms.prize || {}) }
const mittelstand = { ...aboutContent.mittelstand, ...(cms.mittelstand || {}) }
const highlights = useMemo(() => ({ ...aboutContent.highlights, ...(cms.highlights || {}) }), [cms.highlights])
const testimonials = { ...aboutContent.testimonials, ...(cms.testimonials || {}) }
const history = { ...aboutContent.history, ...(cms.history || {}) }
const goals = { ...aboutContent.goals, ...(cms.goals || {}) }
const values = { ...aboutContent.values, ...(cms.values || {}) }
const cta = { ...aboutContent.cta, ...(cms.cta || {}) }
const participationPage = cmsPages.find(isParticipationPage)
const homePage = cmsPages.find(isHomePage)
const participationAwardsGrid = (participationPage?.participation as { awardsGrid?: AwardsGridContent } | undefined)?.awardsGrid
const homeWinners = (homePage?.home as { winners?: HomeWinnersContent } | undefined)?.winners
const showSecondaryCta = !HIDE_MEMBERSHIP_ABOUT_CTA || !isMembershipCta(cta.secondaryCta)
const highlightFallbackWinner = useMemo(
() => ({ ...aboutContent.highlights.fallbackWinner, ...(highlights.fallbackWinner || {}) }),
[highlights],
)
const highlightWinners = useMemo(() => {
const selected = fallbackArray<unknown>(highlights.selectedWinners, [])
.map((entry) => normalizeWinner(resolveCmsRelationship(entry, cmsWinners), placeholderImage, highlightFallbackWinner))
.filter((winner): winner is Winner => Boolean(winner))
if (selected.length) return selected.slice(0, 8)
const isWinnerFromYear = (winner: Winner | undefined): winner is Winner =>
Boolean(winner) && winner?.year === Number(highlights.year || aboutContent.highlights.year)
const cmsByYear = cmsWinners
.map((winner) => normalizeWinner(winner, placeholderImage, highlightFallbackWinner))
.filter(isWinnerFromYear)
.slice(0, 8)
return cmsByYear.length ? cmsByYear : FALLBACK_HIGHLIGHTS
}, [cmsWinners, highlightFallbackWinner, highlights, placeholderImage])
return (
<div className="animate-fade-in">
@@ -213,7 +162,7 @@ const About: React.FC = () => {
<section id="was-ist" style={{ position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<MunichSkylineBg />
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '55% 45%', minHeight: isMobile ? 'auto' : 580 }}>
<div style={{ background: CREAM, padding: isMobile ? '32px 24px 44px' : '88px 80px', display: 'flex', flexDirection: 'column', justifyContent: 'center', order: isMobile ? 2 : 0 }}>
<div style={{ background: WHITE, padding: isMobile ? '32px 24px 44px' : '88px 80px', display: 'flex', flexDirection: 'column', justifyContent: 'center', order: isMobile ? 2 : 0 }}>
{!isMobile && (
<>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>{fallbackText(prize.eyebrow, aboutContent.prize.eyebrow)}</span>
@@ -247,7 +196,7 @@ const About: React.FC = () => {
/>
<div style={{ position: 'absolute', inset: 0, background: isMobile
? 'linear-gradient(to top, rgba(3,9,58,0.94) 0%, rgba(3,9,58,0.55) 38%, rgba(3,9,58,0.12) 72%, transparent 100%), linear-gradient(to right, rgba(3,9,58,0.5) 0%, transparent 55%)'
: `linear-gradient(to right, ${CREAM} 0%, transparent 25%), linear-gradient(to top, rgba(3,9,58,0.75) 0%, transparent 60%)` }} />
: `linear-gradient(to right, ${WHITE} 0%, transparent 25%), linear-gradient(to top, rgba(3,9,58,0.75) 0%, transparent 60%)` }} />
{isMobile && (
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '0 24px 24px', zIndex: 2 }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#8FBEEC', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 12 }}>{fallbackText(prize.eyebrow, aboutContent.prize.eyebrow)}</span>
@@ -312,26 +261,7 @@ const About: React.FC = () => {
</div>
</section>
<section style={{ background: '#111D55' }}>
<div style={{ padding: isMobile ? '48px 24px 32px' : '72px 80px 48px', display: 'flex', flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'flex-start' : 'flex-end', gap: isMobile ? 16 : 0, justifyContent: 'space-between', maxWidth: 1600, margin: '0 auto' }}>
<div>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: '#EFBF04', display: 'block', marginBottom: 12 }}>{fallbackText(highlights.eyebrow, aboutContent.highlights.eyebrow)}</span>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 4vw, 3rem)', fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', margin: 0, lineHeight: 1 }}>
<Lines text={fallbackText(highlights.heading, aboutContent.highlights.heading)} />
</h2>
</div>
<Link
to={fallbackText(highlights.cta?.url, aboutContent.highlights.cta.url)}
style={{ fontFamily: FF, fontSize: 15, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: GOLD, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6, paddingBottom: 4, borderBottom: `1px solid ${GOLD}`, whiteSpace: 'nowrap' }}
>
{fallbackText(highlights.cta?.label, aboutContent.highlights.cta.label)} <ChevronRight size={14} />
</Link>
</div>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? undefined : 'repeat(4, 1fr)', gridAutoFlow: isMobile ? 'column' : undefined, gridTemplateRows: isMobile ? 'repeat(2, 1fr)' : undefined, gridAutoColumns: isMobile ? '66vw' : undefined, overflowX: isMobile ? 'auto' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined, gap: isMobile ? 8 : 0, padding: isMobile ? '0 16px 8px' : 0, WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none' }}>
{highlightWinners.map((w) => <HighlightCard key={w.id} winner={w} hoverLabel={fallbackText(highlights.hoverLabel, aboutContent.highlights.hoverLabel)} />)}
</div>
</section>
<HomeWinnersSection content={homeWinners} />
<TestimonialsSection data={testimonials} />
@@ -385,7 +315,7 @@ const About: React.FC = () => {
</div>
</section>
<section id="vorteile" style={{ background: CREAM, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<section id="vorteile" style={{ background: WHITE, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<MunichSkylineBg />
<div style={{ padding: isMobile ? '48px 24px 32px' : '88px 80px 56px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 16 : 40, alignItems: 'flex-end', borderBottom: '1px solid #D0D5DD' }}>
<div>
@@ -404,7 +334,7 @@ const About: React.FC = () => {
))}
</section>
<AwardsGridSection content={participationAwardsGrid} />
<AwardsGridSection content={participationAwardsGrid} featuredGoldeneBavaria />
<section style={{ background: NAVY, padding: isMobile ? '48px 24px' : '100px 80px', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 0, left: '50%', transform: 'translateX(-50%)', width: '60%', height: 1, background: `linear-gradient(to right, transparent, ${GOLD}, transparent)`, opacity: 0.3 }} />
@@ -449,37 +379,6 @@ const About: React.FC = () => {
)
}
function HighlightCard({ winner, hoverLabel }: { winner: Winner; hoverLabel: string }) {
const [hovered, setHovered] = useState(false)
const isMobile = useIsMobile()
return (
<Link
to={`/preistraeger/${winner.slug}`}
style={{ textDecoration: 'none', display: 'block', position: 'relative', height: isMobile ? 220 : 280, overflow: 'hidden', scrollSnapAlign: isMobile ? 'start' : undefined }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<Image unoptimized src={winner.img} alt={winner.name} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform 0.6s ease', transform: hovered ? 'scale(1.07)' : 'scale(1)', filter: 'grayscale(15%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.78) 0%, rgba(0,0,0,0.1) 55%, transparent 100%)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.2)', opacity: hovered ? 1 : 0, transition: 'opacity 0.25s' }} />
<div style={{ position: 'absolute', inset: 0, boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.06)' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, padding: '18px 20px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 5, marginBottom: 5 }}>
<Star size={10} style={{ color: GOLD, flexShrink: 0 }} fill={GOLD} />
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: GOLD, letterSpacing: '0.12em', textTransform: 'uppercase' }}>
{winner.year} · {winner.type}
</span>
</div>
<h3 style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: '#fff', margin: '0 0 3px', lineHeight: 1.3, textShadow: '0 1px 4px rgba(0,0,0,0.5)' }}>{winner.name}</h3>
<p style={{ fontFamily: FF, fontSize: 14, color: 'rgba(255,255,255,0.75)', margin: 0, letterSpacing: '0.03em' }}>{winner.category}</p>
</div>
<div style={{ position: 'absolute', bottom: 16, right: 16, background: GOLD, color: '#101828', padding: '6px 14px', borderRadius: 999, fontSize: 10, fontFamily: FF, fontWeight: 800, letterSpacing: '0.1em', textTransform: 'uppercase', opacity: hovered ? 1 : 0, transform: hovered ? 'translateY(0)' : 'translateY(6px)', transition: 'all 0.25s' }}>
{hoverLabel}
</div>
</Link>
)
}
function ValueRow({ item, last }: { item: { num?: string | null; title?: string | null }; last: boolean }) {
const [hovered, setHovered] = useState(false)
const isMobileRow = useIsMobile()