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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user