feat: update Preistraeger page to include a simple winner grid for 2023 and remove unused components

fix: change featured status for participation content and remove deprecated award card

feat: add migration for new participation phase form fields and backfill existing data

feat: create AwardsGridSection component for displaying awards with improved layout

feat: implement NewsletterInterestForm component for newsletter sign-up with validation
This commit is contained in:
syntaxbullet
2026-07-01 18:02:12 +02:00
parent 2fda7e8122
commit 2842caa936
15 changed files with 1274 additions and 255 deletions

View File

@@ -1,6 +1,6 @@
import React, { useState, useMemo } from 'react';
import { Link } from '@/spa/router';
import { Trophy, ChevronRight, ArrowRight } from 'lucide-react';
import { Trophy, ArrowRight } from 'lucide-react';
import { WINNERS, type Winner } from '@/spa/data/winners';
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
@@ -14,6 +14,7 @@ const GOLD = '#EFBF04';
const BORDER = '#D0D5DD';
const GRAY = '#666666';
const BG_ALT = '#E4E2E3';
const SIMPLE_LIST_YEAR = 2023;
type PreistraegerIndexCms = Partial<typeof preistraegerIndexContent> & {
hero?: Partial<typeof preistraegerIndexContent.hero> & { image?: unknown }
@@ -69,6 +70,7 @@ export default function Preistraeger() {
const selectedYear = activeYear ?? years[0] ?? new Date().getFullYear();
const filtered = useMemo(() => winners.filter(w => w.year === selectedYear), [selectedYear, winners]);
const usesSimpleWinnerGrid = selectedYear === SIMPLE_LIST_YEAR;
return (
<div style={{ background: '#fff', minHeight: '100vh' }}>
@@ -129,6 +131,8 @@ export default function Preistraeger() {
<div style={{ padding: isMobile ? '56px 24px' : '80px', textAlign: 'center', color: GRAY, fontFamily: FF }}>
{fallbackText(empty.message, preistraegerIndexContent.empty.message)}
</div>
) : usesSimpleWinnerGrid ? (
<SimpleWinnerGrid winners={filtered} />
) : (
<div style={{
display: 'grid',
@@ -165,38 +169,6 @@ export default function Preistraeger() {
>
{fallbackText(cta.primaryCta?.label, preistraegerIndexContent.cta.primaryCta.label)} <ArrowRight size={14} />
</Link>
<div style={{ marginTop: 20, display: 'flex', alignItems: 'center', justifyContent: isMobile ? 'center' : undefined, flexWrap: isMobile ? 'wrap' : 'nowrap', gap: 8 }}>
<span style={{ width: 20, height: 1, background: 'rgba(239,191,4,0.3)', display: 'inline-block' }} />
<span style={{ fontFamily: '"IBM Plex Sans", sans-serif', fontSize: 11, color: 'rgba(16,24,40,0.4)' }}>{fallbackText(cta.secondaryPrefix, preistraegerIndexContent.cta.secondaryPrefix)}</span>
<Link
to={fallbackText(cta.secondaryCta?.url, preistraegerIndexContent.cta.secondaryCta.url)}
style={{
fontFamily: '"IBM Plex Sans", sans-serif',
fontSize: 11,
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(cta.secondaryCta?.label, preistraegerIndexContent.cta.secondaryCta.label)} <ChevronRight size={10} />
</Link>
</div>
</div>
</div>
);
@@ -257,3 +229,91 @@ function WinnerCard({ winner, hoverLabel }: { winner: Winner; hoverLabel: string
</Link>
);
}
function SimpleWinnerGrid({ winners }: { winners: Winner[] }) {
const isMobile = useIsMobile();
return (
<div style={{
background: '#F7F7F8',
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, minmax(0, 1fr))',
gap: isMobile ? 12 : 16,
padding: isMobile ? '24px' : '40px 80px 56px',
}}>
{winners.map(winner => (
<SimpleWinnerCard key={winner.id} winner={winner} />
))}
</div>
);
}
function SimpleWinnerCard({ winner }: { winner: Winner }) {
const isMobile = useIsMobile();
const hasDescription = winner.shortDesc.trim().length > 0;
return (
<article
style={{
background: '#fff',
border: `1px solid ${BORDER}`,
color: '#101828',
display: 'flex',
flexDirection: 'column',
minHeight: isMobile ? 0 : 220,
padding: isMobile ? '20px' : '24px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 18 }}>
<span style={{
color: GOLD,
fontFamily: FF,
fontSize: 12,
fontWeight: 700,
letterSpacing: '0.12em',
textTransform: 'uppercase',
}}>
{winner.year} · {winner.type}
</span>
{winner.category ? (
<span style={{
border: `1px solid ${BORDER}`,
color: GRAY,
fontFamily: FF,
fontSize: 12,
fontWeight: 600,
lineHeight: 1.2,
padding: '5px 8px',
}}>
{winner.category}
</span>
) : null}
</div>
<h3 style={{
color: NAVY,
fontFamily: FF,
fontSize: isMobile ? 20 : 22,
fontWeight: 700,
lineHeight: 1.18,
margin: 0,
overflowWrap: 'anywhere',
}}>
{winner.name}
</h3>
{hasDescription ? (
<p style={{
color: GRAY,
fontFamily: FF,
fontSize: 15,
lineHeight: 1.55,
margin: '16px 0 0',
overflowWrap: 'anywhere',
}}>
{winner.shortDesc}
</p>
) : null}
</article>
);
}