feat: implement newsletter subscription feature with new forms and database structure
- Added new newsletter subscription functionality to the participation page. - Created a new `NewsletterForm` component to handle user subscriptions. - Introduced `NewsletterSubscriptions` collection to manage subscriptions in the database. - Developed migration script to set up necessary database tables and fields for the newsletter feature. - Refactored participation content to remove deprecated newsletter phase data and integrate new structure. - Updated the `Participation` page to utilize the new newsletter form and handle different application phases.
This commit is contained in:
@@ -5,18 +5,19 @@ import { Link } from '@/spa/router';
|
||||
import { PartnerTicker } from '@/spa/components/ui/partner-ticker';
|
||||
import { WINNERS } from '@/spa/data/winners';
|
||||
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
|
||||
import { NewsletterInterestForm, type NewsletterBenefit, type NewsletterInterestCms, type NewsletterInterestCopy } from '@/spa/components/forms/NewsletterInterestForm';
|
||||
import { NewsletterInterestForm } from '@/spa/components/forms/NewsletterInterestForm';
|
||||
import TestimonialsSection from '@/spa/components/TestimonialsSection';
|
||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||
import type { SpaApplicationPhaseData, SpaApplicationPhaseItem } from '@/spa/applicationPhase'
|
||||
import { resolveCmsRelationship, useApplicationPhase, useCmsCollection, useCmsRoute } from '@/spa/cmsRoute'
|
||||
import { resolveCmsRelationship, useApplicationPhase, useCmsCollection, useCmsRoute, useNewsletterForm } from '@/spa/cmsRoute'
|
||||
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField'
|
||||
import { homeContent } from '@/spa/homeContent'
|
||||
import { netzwerkContent } from '@/spa/netzwerkContent'
|
||||
import { usePreistraegerPlaceholderImage } from '@/spa/preistraegerPlaceholder'
|
||||
import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo'
|
||||
import { newsletterFormCopyForPhase, newsletterPhaseForApplicationPhase } from '@/spa/newsletterForm'
|
||||
|
||||
const FF = '"IBM Plex Sans", sans-serif';
|
||||
|
||||
@@ -57,8 +58,6 @@ type HomeSectionCms = CmsRecord & {
|
||||
imageAlt?: string | null;
|
||||
imageLabel?: string | null;
|
||||
items?: unknown;
|
||||
newsletterPhaseCompleted?: NewsletterInterestCms | null;
|
||||
newsletterPhaseEvaluation?: NewsletterInterestCms | null;
|
||||
note?: string | null;
|
||||
quote?: string | null;
|
||||
stats?: unknown;
|
||||
@@ -118,39 +117,17 @@ const fallbackArray = <T,>(value: unknown, fallback: readonly T[]) =>
|
||||
|
||||
const HIDE_MEMBERSHIP_HOME_HERO_CTA = true;
|
||||
const APPLICATION_PHASE_OPEN = '0';
|
||||
const STATUS_SECTION_BACKGROUND = '#111D55';
|
||||
const LEGACY_STATUS_SECTION_BACKGROUNDS = new Set(['#020A1E', '#060D1C', '#06090F']);
|
||||
|
||||
const newsletterInterestCopy: Record<string, NewsletterInterestCopy> = {
|
||||
'1': homeContent.application.newsletterPhaseEvaluation,
|
||||
'2': homeContent.application.newsletterPhaseCompleted,
|
||||
};
|
||||
const statusSectionBackground = (value?: string | null) => {
|
||||
const background = value?.trim();
|
||||
|
||||
const mergeNewsletterInterestCopy = (
|
||||
value: NewsletterInterestCms | null | undefined,
|
||||
fallback: NewsletterInterestCopy,
|
||||
): NewsletterInterestCopy => ({
|
||||
eyebrow: fallbackText(value?.eyebrow, fallback.eyebrow),
|
||||
heading: fallbackText(value?.heading, fallback.heading),
|
||||
body: fallbackText(value?.body, fallback.body),
|
||||
benefits: fallbackArray<NewsletterBenefit>(value?.benefits, fallback.benefits),
|
||||
formEyebrow: fallbackText(value?.formEyebrow, fallback.formEyebrow),
|
||||
formTitle: fallbackText(value?.formTitle, fallback.formTitle),
|
||||
formBody: fallbackText(value?.formBody, fallback.formBody),
|
||||
emailLabel: fallbackText(value?.emailLabel, fallback.emailLabel),
|
||||
emailPlaceholder: fallbackText(value?.emailPlaceholder, fallback.emailPlaceholder),
|
||||
submitLabel: fallbackText(value?.submitLabel, fallback.submitLabel),
|
||||
privacy: fallbackText(value?.privacy, fallback.privacy),
|
||||
validationRequired: fallbackText(value?.validationRequired, fallback.validationRequired),
|
||||
validationInvalid: fallbackText(value?.validationInvalid, fallback.validationInvalid),
|
||||
successHeading: fallbackText(value?.successHeading, fallback.successHeading),
|
||||
successBody: fallbackText(value?.successBody, fallback.successBody),
|
||||
footnote: fallbackText(value?.footnote, fallback.footnote),
|
||||
footnoteStrong: fallbackText(value?.footnoteStrong, fallback.footnoteStrong),
|
||||
});
|
||||
if (!background || LEGACY_STATUS_SECTION_BACKGROUNDS.has(background.toUpperCase())) {
|
||||
return STATUS_SECTION_BACKGROUND;
|
||||
}
|
||||
|
||||
const getNewsletterInterestCopy = (phase: string | undefined, application: HomeSectionCms) => {
|
||||
const fallback = newsletterInterestCopy[phase || ''] || newsletterInterestCopy['2'];
|
||||
const cmsCopy = phase === '1' ? application.newsletterPhaseEvaluation : application.newsletterPhaseCompleted;
|
||||
return mergeNewsletterInterestCopy(cmsCopy, fallback);
|
||||
return background;
|
||||
};
|
||||
|
||||
const isMembershipCta = (cta?: { label?: string; url?: string; to?: string } | null) =>
|
||||
@@ -199,11 +176,13 @@ function HomeWinnerCard({
|
||||
fallbackTitle,
|
||||
fallbackImage,
|
||||
hoverLabel,
|
||||
horizontalScroll = false,
|
||||
}: {
|
||||
winner: WinnerCardData;
|
||||
fallbackTitle: string;
|
||||
fallbackImage: string;
|
||||
hoverLabel: string;
|
||||
horizontalScroll?: boolean;
|
||||
}) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
@@ -220,7 +199,7 @@ function HomeWinnerCard({
|
||||
return (
|
||||
<Link
|
||||
to={href}
|
||||
style={{ textDecoration: 'none', display: 'block', position: 'relative', height: isMobile ? 220 : 280, overflow: 'hidden', scrollSnapAlign: isMobile ? 'start' : undefined }}
|
||||
style={{ textDecoration: 'none', display: 'block', position: 'relative', height: isMobile ? 220 : 280, overflow: 'hidden', scrollSnapAlign: isMobile || horizontalScroll ? 'start' : undefined }}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
@@ -265,9 +244,11 @@ const Home: React.FC = () => {
|
||||
const form = home.form || {};
|
||||
const videoModal = home.videoModal || {};
|
||||
const applicationPhase = useApplicationPhase();
|
||||
const newsletterForm = useNewsletterForm();
|
||||
const activeApplicationPhase = applicationPhase?.activePhase || APPLICATION_PHASE_OPEN;
|
||||
const showApplicationForm = activeApplicationPhase === APPLICATION_PHASE_OPEN;
|
||||
const newsletterCopy = getNewsletterInterestCopy(activeApplicationPhase, application);
|
||||
const newsletterCopy = newsletterFormCopyForPhase(newsletterForm, activeApplicationPhase);
|
||||
const newsletterPhase = newsletterPhaseForApplicationPhase(activeApplicationPhase);
|
||||
const cmsPreistraeger = useCmsCollection('preistraeger');
|
||||
const selectedWinners = fallbackArray(winnersSection.featured, []);
|
||||
const heroSecondaryCta = {
|
||||
@@ -281,6 +262,8 @@ const Home: React.FC = () => {
|
||||
const homeWinners = selectedWinnerDocs.length
|
||||
? selectedWinnerDocs
|
||||
: (cmsPreistraeger.length ? cmsPreistraeger : WINNERS).filter((w) => Number((w as { year?: string | number }).year) === 2025).slice(0, 8);
|
||||
const scrollDesktopWinners = !isMobile && homeWinners.length > 8;
|
||||
const scrollWinnerGrid = isMobile || scrollDesktopWinners;
|
||||
const quickCriteria = fallbackArray(quickCheck.criteria, homeContent.quickCheck.criteria);
|
||||
const introStats = fallbackArray(intro.stats, homeContent.intro.stats);
|
||||
const benefitItems = fallbackArray(benefits.items, homeContent.benefits.items);
|
||||
@@ -313,53 +296,58 @@ const Home: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto relative z-10" style={{ padding: isMobile ? '0 24px' : '0 24px' }}>
|
||||
<div style={{ maxWidth: isMobile ? '100%' : '48rem' }}>
|
||||
<div style={{ maxWidth: isMobile ? '100%' : '62rem' }}>
|
||||
<div className="inline-flex items-center gap-3 bg-accent/20 border border-accent/30 rounded-full px-4 py-1.5 mb-8 backdrop-blur-sm animate-fade-in" style={{ marginBottom: isMobile ? 20 : undefined }}>
|
||||
<span className="flex h-2 w-2 rounded-full bg-accent"></span>
|
||||
<span className="text-accent text-[10px] uppercase tracking-[0.2em] font-bold">{fallbackText(hero.badge, homeContent.hero.badge)}</span>
|
||||
</div>
|
||||
|
||||
<h1 className="font-display font-bold text-white mb-6 leading-[0.9] tracking-tight" style={{ fontSize: isMobile ? 'clamp(1.8rem, 10vw, 3rem)' : 'clamp(3.5rem, 8vw, 6rem)' }}>
|
||||
{fallbackText(hero.headingLine1, homeContent.hero.headingLine1)} <br />
|
||||
<span className="text-accent">{fallbackText(hero.headingAccent, homeContent.hero.headingAccent)}</span>
|
||||
<h1
|
||||
className="font-display font-bold text-white mb-6 leading-[0.9] tracking-tight"
|
||||
style={{ fontSize: isMobile ? 'clamp(1.75rem, 8vw, 3rem)' : 'clamp(3.25rem, 6.5vw, 6rem)' }}
|
||||
>
|
||||
<span style={{ display: 'block', whiteSpace: 'nowrap' }}>
|
||||
{fallbackText(hero.headingLine1, homeContent.hero.headingLine1)}
|
||||
</span>
|
||||
<span className="text-accent" style={{ display: 'block', whiteSpace: 'nowrap' }}>
|
||||
{fallbackText(hero.headingAccent, homeContent.hero.headingAccent)}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-white/70 mb-12 font-light leading-relaxed" style={{ fontSize: isMobile ? '1.0625rem' : '1.375rem', maxWidth: isMobile ? '100%' : '36rem', marginBottom: isMobile ? 28 : undefined }}>
|
||||
{fallbackText(hero.description, homeContent.hero.description)}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', flexWrap: isMobile ? 'nowrap' : 'wrap', gap: 16, alignItems: isMobile ? 'stretch' : 'center' }}>
|
||||
<Link
|
||||
to={fallbackText(hero.primaryCta?.url, homeContent.hero.primaryCta.url)}
|
||||
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'#101828', background:'#EFBF04', padding:'16px 36px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent: isMobile ? 'center' : 'flex-start', gap:10, transition:'background 0.15s, box-shadow 0.2s', width: isMobile ? '100%' : 'auto' }}
|
||||
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'#101828', background:'#EFBF04', padding:'16px 24px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent:'center', gap:10, transition:'background 0.15s, box-shadow 0.2s', width: isMobile ? '100%' : 'auto' }}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#FFD130'; (e.currentTarget as HTMLElement).style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = '#EFBF04'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
|
||||
>
|
||||
{fallbackText(hero.primaryCta?.label, homeContent.hero.primaryCta.label)} <ChevronRight size={16} />
|
||||
</Link>
|
||||
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 16, alignItems: isMobile ? 'stretch' : 'center' }}>
|
||||
{showHeroSecondaryCta && (
|
||||
<Link
|
||||
to={heroSecondaryCta.url}
|
||||
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'rgba(255,255,255,0.85)', border:'1.5px solid rgba(255,255,255,0.35)', padding:'14px 32px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent: isMobile ? 'center' : 'flex-start', gap:10, transition:'border-color 0.15s, color 0.15s', background:'transparent', width: isMobile ? '100%' : 'auto' }}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = '#EFBF04'; (e.currentTarget as HTMLElement).style.color = '#EFBF04'; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.35)'; (e.currentTarget as HTMLElement).style.color = 'rgba(255,255,255,0.85)'; }}
|
||||
>
|
||||
{heroSecondaryCta.label}
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowVideo(true)}
|
||||
className="flex items-center gap-4 text-white hover:text-accent transition-colors group"
|
||||
style={{ justifyContent: isMobile ? 'center' : 'flex-start' }}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowVideo(true)}
|
||||
className="flex items-center gap-4 text-white hover:text-accent transition-colors group"
|
||||
style={{ justifyContent: isMobile ? 'center' : 'flex-start' }}
|
||||
>
|
||||
<div className="w-12 h-12 rounded-full border border-white/30 flex items-center justify-center group-hover:border-accent group-hover:bg-accent/10 transition-all">
|
||||
<Play size={20} fill="currentColor" />
|
||||
</div>
|
||||
<span className="uppercase text-xs tracking-widest font-bold">{fallbackText(hero.videoLabel, homeContent.hero.videoLabel)}</span>
|
||||
</button>
|
||||
{showHeroSecondaryCta && (
|
||||
<Link
|
||||
to={heroSecondaryCta.url}
|
||||
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'rgba(255,255,255,0.85)', border:'1.5px solid rgba(255,255,255,0.35)', padding:'14px 32px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent:'center', gap:10, transition:'border-color 0.15s, color 0.15s', background:'transparent', width: isMobile ? '100%' : 'auto' }}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = '#EFBF04'; (e.currentTarget as HTMLElement).style.color = '#EFBF04'; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.35)'; (e.currentTarget as HTMLElement).style.color = 'rgba(255,255,255,0.85)'; }}
|
||||
>
|
||||
<div className="w-12 h-12 rounded-full border border-white/30 flex items-center justify-center group-hover:border-accent group-hover:bg-accent/10 transition-all">
|
||||
<Play size={20} fill="currentColor" />
|
||||
</div>
|
||||
<span className="uppercase text-xs tracking-widest font-bold">{fallbackText(hero.videoLabel, homeContent.hero.videoLabel)}</span>
|
||||
</button>
|
||||
</div>
|
||||
{heroSecondaryCta.label}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -402,12 +390,13 @@ const Home: React.FC = () => {
|
||||
|
||||
<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,
|
||||
gridTemplateColumns: scrollWinnerGrid ? undefined : 'repeat(4, 1fr)',
|
||||
gridAutoFlow: scrollWinnerGrid ? 'column' : undefined,
|
||||
gridTemplateRows: scrollWinnerGrid ? `repeat(2, ${isMobile ? '1fr' : '280px'})` : undefined,
|
||||
gridAutoColumns: isMobile ? '66vw' : scrollDesktopWinners ? '25vw' : undefined,
|
||||
overflowX: scrollWinnerGrid ? 'auto' : undefined,
|
||||
scrollSnapType: scrollWinnerGrid ? 'x mandatory' : undefined,
|
||||
overscrollBehaviorX: scrollWinnerGrid ? 'contain' : undefined,
|
||||
gap: isMobile ? 8 : 0,
|
||||
padding: isMobile ? '0 16px 8px' : 0,
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
@@ -420,6 +409,7 @@ const Home: React.FC = () => {
|
||||
fallbackTitle={fallbackText(winnersSection.cardFallbackTitle, homeContent.winners.cardFallbackTitle)}
|
||||
fallbackImage={winnerFallbackImage}
|
||||
hoverLabel={fallbackText(winnersSection.hoverLabel, homeContent.winners.hoverLabel)}
|
||||
horizontalScroll={scrollDesktopWinners}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -716,7 +706,7 @@ const Home: React.FC = () => {
|
||||
<div style={{ padding: isMobile ? '32px 24px' : '24px 48px 32px 40px', flex: isMobile ? 'none' : 1, height: isMobile && showApplicationForm ? '82svh' : undefined, display: 'flex', flexDirection: 'column', minHeight: 0, position: 'relative', zIndex: 1 }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{showApplicationForm ? fallbackText(application.formEyebrow, homeContent.application.formEyebrow) : newsletterCopy.formEyebrow}</span>
|
||||
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{showApplicationForm ? fallbackText(application.formTitle, homeContent.application.formTitle) : newsletterCopy.formTitle}</h3>
|
||||
{showApplicationForm ? <BewerbungsForm theme="gold" content={form} /> : <NewsletterInterestForm copy={newsletterCopy} />}
|
||||
{showApplicationForm ? <BewerbungsForm theme="gold" content={form} /> : <NewsletterInterestForm copy={newsletterCopy} phase={newsletterPhase} source="homepage" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -784,7 +774,7 @@ function StatusSlider({ data }: { data?: SpaApplicationPhaseData }) {
|
||||
return {
|
||||
...phase,
|
||||
accent: phase.accent || '#EFBF04',
|
||||
bg: phase.bg || '#020A1E',
|
||||
bg: statusSectionBackground(phase.bg),
|
||||
cta,
|
||||
membershipCta,
|
||||
}
|
||||
|
||||
@@ -2,15 +2,16 @@ import React, { useRef } from 'react';
|
||||
import { MapPin, Users, TrendingUp, Building2, ArrowRight, CheckCircle2, UserCheck, Send, Star } from 'lucide-react';
|
||||
import { Link } from '@/spa/router';
|
||||
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
|
||||
import { NewsletterInterestForm, type NewsletterBenefit, type NewsletterInterestCms, type NewsletterInterestCopy } from '@/spa/components/forms/NewsletterInterestForm';
|
||||
import { NewsletterInterestForm } from '@/spa/components/forms/NewsletterInterestForm';
|
||||
import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection';
|
||||
import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection';
|
||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||
import { useApplicationPhase, useCmsRoute } from '@/spa/cmsRoute';
|
||||
import { useApplicationPhase, useCmsRoute, useNewsletterForm } from '@/spa/cmsRoute';
|
||||
import { participationContent } from '@/spa/participationContent';
|
||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||
import { newsletterFormCopyForPhase, newsletterPhaseForApplicationPhase } from '@/spa/newsletterForm';
|
||||
|
||||
const FF = '"IBM Plex Sans", sans-serif';
|
||||
const FB = '"Inter", sans-serif';
|
||||
@@ -26,8 +27,6 @@ type ParticipationCms = Partial<typeof participationContent> & {
|
||||
|
||||
type ParticipationApplicationFormCms = Partial<typeof participationContent.applicationForm> & {
|
||||
image?: unknown;
|
||||
newsletterPhaseCompleted?: NewsletterInterestCms | null;
|
||||
newsletterPhaseEvaluation?: NewsletterInterestCms | null;
|
||||
};
|
||||
|
||||
const ICONS = {
|
||||
@@ -48,40 +47,6 @@ const fallbackArray = <T,>(value: unknown, fallback: readonly T[]) =>
|
||||
|
||||
const APPLICATION_PHASE_OPEN = '0';
|
||||
|
||||
const newsletterInterestCopy: Record<string, NewsletterInterestCopy> = {
|
||||
'1': participationContent.applicationForm.newsletterPhaseEvaluation,
|
||||
'2': participationContent.applicationForm.newsletterPhaseCompleted,
|
||||
};
|
||||
|
||||
const mergeNewsletterInterestCopy = (
|
||||
value: NewsletterInterestCms | null | undefined,
|
||||
fallback: NewsletterInterestCopy,
|
||||
): NewsletterInterestCopy => ({
|
||||
eyebrow: fallbackText(value?.eyebrow, fallback.eyebrow),
|
||||
heading: fallbackText(value?.heading, fallback.heading),
|
||||
body: fallbackText(value?.body, fallback.body),
|
||||
benefits: fallbackArray<NewsletterBenefit>(value?.benefits, fallback.benefits),
|
||||
formEyebrow: fallbackText(value?.formEyebrow, fallback.formEyebrow),
|
||||
formTitle: fallbackText(value?.formTitle, fallback.formTitle),
|
||||
formBody: fallbackText(value?.formBody, fallback.formBody),
|
||||
emailLabel: fallbackText(value?.emailLabel, fallback.emailLabel),
|
||||
emailPlaceholder: fallbackText(value?.emailPlaceholder, fallback.emailPlaceholder),
|
||||
submitLabel: fallbackText(value?.submitLabel, fallback.submitLabel),
|
||||
privacy: fallbackText(value?.privacy, fallback.privacy),
|
||||
validationRequired: fallbackText(value?.validationRequired, fallback.validationRequired),
|
||||
validationInvalid: fallbackText(value?.validationInvalid, fallback.validationInvalid),
|
||||
successHeading: fallbackText(value?.successHeading, fallback.successHeading),
|
||||
successBody: fallbackText(value?.successBody, fallback.successBody),
|
||||
footnote: fallbackText(value?.footnote, fallback.footnote),
|
||||
footnoteStrong: fallbackText(value?.footnoteStrong, fallback.footnoteStrong),
|
||||
});
|
||||
|
||||
const getNewsletterInterestCopy = (phase: string | undefined, applicationForm: ParticipationApplicationFormCms) => {
|
||||
const fallback = newsletterInterestCopy[phase || ''] || newsletterInterestCopy['2'];
|
||||
const cmsCopy = phase === '1' ? applicationForm.newsletterPhaseEvaluation : applicationForm.newsletterPhaseCompleted;
|
||||
return mergeNewsletterInterestCopy(cmsCopy, fallback);
|
||||
};
|
||||
|
||||
const isFutureAwardApplicationWay = (value: { label?: unknown; title?: unknown }) => {
|
||||
const label = typeof value.label === 'string' ? value.label.toLowerCase() : '';
|
||||
const title = typeof value.title === 'string' ? value.title.toLowerCase() : '';
|
||||
@@ -98,6 +63,7 @@ const Participation: React.FC = () => {
|
||||
const qualSectionRef = useRef<HTMLElement>(null);
|
||||
const cms = (useCmsRoute()?.doc?.participation || {}) as ParticipationCms;
|
||||
const applicationPhase = useApplicationPhase();
|
||||
const newsletterForm = useNewsletterForm();
|
||||
const hero = { ...participationContent.hero, ...(cms.hero || {}) };
|
||||
const process = { ...participationContent.process, ...(cms.process || {}) };
|
||||
const eligibility = { ...participationContent.eligibility, ...(cms.eligibility || {}) };
|
||||
@@ -108,7 +74,8 @@ const Participation: React.FC = () => {
|
||||
const form = { ...participationContent.form, ...(cms.form || {}) };
|
||||
const activeApplicationPhase = applicationPhase?.activePhase || APPLICATION_PHASE_OPEN;
|
||||
const showApplicationForm = activeApplicationPhase === APPLICATION_PHASE_OPEN;
|
||||
const newsletterCopy = getNewsletterInterestCopy(activeApplicationPhase, applicationForm);
|
||||
const newsletterCopy = newsletterFormCopyForPhase(newsletterForm, activeApplicationPhase);
|
||||
const newsletterPhase = newsletterPhaseForApplicationPhase(activeApplicationPhase);
|
||||
const eligibilityCriteria = fallbackArray(eligibility.criteria, participationContent.eligibility.criteria);
|
||||
const eligibilityNotes = fallbackArray(eligibility.notes, participationContent.eligibility.notes);
|
||||
const mobileDates = fallbackArray(datesBanner.mobileItems, participationContent.datesBanner.mobileItems);
|
||||
@@ -324,7 +291,7 @@ const Participation: React.FC = () => {
|
||||
<>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{newsletterCopy.formEyebrow}</span>
|
||||
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{newsletterCopy.formTitle}</h3>
|
||||
<NewsletterInterestForm copy={newsletterCopy} />
|
||||
<NewsletterInterestForm copy={newsletterCopy} phase={newsletterPhase} source="participation" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user