fix(home): honor phase headline line breaks

This commit is contained in:
syntaxbullet
2026-07-31 17:24:36 +02:00
parent adda9ddbd0
commit f465839af0
6 changed files with 112 additions and 9 deletions

View File

@@ -26,7 +26,7 @@ const phaseFields: Field[] = [
text('tag', 'Badge'),
{ name: 'pulse', label: 'Show pulse dot', type: 'checkbox', defaultValue: true },
text('phase', 'Phase label'),
text('headline', 'Headline'),
textarea('headline', 'Headline'),
textarea('body', 'Body copy'),
{ name: 'cta', label: 'Primary CTA', type: 'group', fields: linkGroup() },
text('accent', 'Accent color'),

View File

@@ -0,0 +1,23 @@
import { type MigrateDownArgs, type MigrateUpArgs, sql } from '@payloadcms/db-sqlite'
const literal = (value: string) => `'${value.replace(/'/g, "''")}'`
const previousHeadline = 'DER BMP 2026 WAR EIN ERFOLG.'
const unbrokenHeadline = 'Ein weiteres erfolgreiches Jahr'
const splitHeadline = 'Ein weiteres\nerfolgreiches Jahr'
export async function up({ db, payload: _payload, req: _req }: MigrateUpArgs): Promise<void> {
await db.run(
sql.raw(
`UPDATE application_phase_phases SET headline = ${literal(splitHeadline)} WHERE num = '03' AND headline IN (${literal(previousHeadline)}, ${literal(unbrokenHeadline)});`,
),
)
}
export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise<void> {
await db.run(
sql.raw(
`UPDATE application_phase_phases SET headline = ${literal(previousHeadline)} WHERE num = '03' AND headline = ${literal(splitHeadline)};`,
),
)
}

View File

@@ -20,6 +20,7 @@ import * as migration_20260731_160000_focused_payload_cleanup from './20260731_1
import * as migration_20260731_170000_netzwerk_greeting_video from './20260731_170000_netzwerk_greeting_video';
import * as migration_20260731_180000_press_contact_image from './20260731_180000_press_contact_image';
import * as migration_20260731_190000_responsive_sponsor_hierarchy from './20260731_190000_responsive_sponsor_hierarchy';
import * as migration_20260731_200000_home_phase_headline_line_break from './20260731_200000_home_phase_headline_line_break';
export const migrations = [
{
@@ -132,4 +133,9 @@ export const migrations = [
down: migration_20260731_190000_responsive_sponsor_hierarchy.down,
name: '20260731_190000_responsive_sponsor_hierarchy',
},
{
up: migration_20260731_200000_home_phase_headline_line_break.up,
down: migration_20260731_200000_home_phase_headline_line_break.down,
name: '20260731_200000_home_phase_headline_line_break',
},
];

View File

@@ -70,7 +70,7 @@ export const defaultApplicationPhaseData: SpaApplicationPhaseData = {
tag: 'ABGESCHLOSSEN',
pulse: false,
phase: 'Preisverleihung · Oktober 2026',
headline: 'DER BMP 2026 WAR EIN ERFOLG.',
headline: 'Ein weiteres\nerfolgreiches Jahr',
body:
'Wir danken allen 247 Bewerberinnen und Bewerbern für Mut und Exzellenz. Drei außergewöhnliche Unternehmen wurden in München ausgezeichnet ein unvergesslicher Abend.',
cta: { label: 'Alle Preisträger ansehen', url: '/preistraeger' },

View File

@@ -116,7 +116,7 @@ const isMembershipCta = (cta?: { label?: string; url?: string; to?: string } | n
cta?.label?.trim().toLowerCase() === 'mitglied werden';
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(/\r?\n/).map((line, i) => <React.Fragment key={`${line}-${i}`}>{i > 0 && <br />}{line}</React.Fragment>)}</>;
}
function partnerLogos(partners: PartnerLogoData[]) {
@@ -542,7 +542,7 @@ const Home: React.FC = () => {
// ─── StatusSlider ─────────────────────────────────────────────────────────────
function StatusSlider({ data }: { data?: SpaApplicationPhaseData }) {
export function StatusSlider({ data }: { data?: SpaApplicationPhaseData }) {
const isMobile = useIsMobile();
if (!data?.phases?.length) return null;
@@ -568,11 +568,12 @@ function StatusSlider({ data }: { data?: SpaApplicationPhaseData }) {
return (
<section
data-testid="phase-section"
style={{
position: 'relative',
overflow: 'hidden',
height: isMobile ? 'auto' : 480,
minHeight: isMobile ? 360 : 'auto',
height: 'auto',
minHeight: isMobile ? 360 : 480,
}}
>
<StatusSlideCard
@@ -605,7 +606,7 @@ function StatusSlideCard({
<div
style={{
width: '100vw',
height: '100%',
minHeight: isMobile ? 360 : 480,
flexShrink: 0,
position: 'relative',
background: phase.bg,
@@ -629,8 +630,8 @@ function StatusSlideCard({
{/* Headline + body + CTA */}
<div>
<h2 className="type-page-title text-role-inverse" style={{ textTransform: 'uppercase', margin: '0 0 24px' }}>
{phase.headline}
<h2 className="type-page-title text-role-inverse" data-testid="phase-headline" style={{ textTransform: 'uppercase', margin: '0 0 24px' }}>
<Lines text={fallbackText(phase.headline, '')} />
</h2>
<p className="type-body text-role-inverse-secondary" style={{ marginBottom: 32, maxWidth: isMobile ? 520 : 760 }}>
{phase.body}

View File

@@ -0,0 +1,73 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { SpaApplicationPhaseData } from '@/spa/applicationPhase'
import { StatusSlider } from '@/spa/pages/Home'
vi.mock('@/spa/hooks/useIsMobile', () => ({
useIsMobile: () => false,
}))
vi.mock('next/navigation', () => ({
usePathname: () => '/',
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
}))
const applicationPhase = (headline: string): SpaApplicationPhaseData => ({
activePhase: '0',
noActionLabel: 'No action',
successApplicationsLabel: 'Applications',
successWinnersLabel: 'Winners',
phases: [
{
num: '03',
tag: 'Complete',
phase: 'Award ceremony',
headline,
body: 'CMS body copy',
accent: '#EFBF04',
bg: '#111D55',
meta: 'CMS meta copy',
successApplications: '247',
successWinners: '3',
},
],
})
describe('homepage application-phase headline', () => {
afterEach(() => cleanup())
it('renders each explicit CMS newline as an intentional line break', () => {
render(<StatusSlider data={applicationPhase('Ein weiteres\nerfolgreiches Jahr')} />)
const headline = screen.getByTestId('phase-headline')
expect(headline.textContent).toBe('Ein weitereserfolgreiches Jahr')
expect(headline.querySelectorAll('br')).toHaveLength(1)
expect(headline.childNodes[0]?.textContent).toBe('Ein weiteres')
expect(headline.childNodes[2]?.textContent).toBe('erfolgreiches Jahr')
})
it('keeps arbitrary CMS-edited copy authoritative and naturally wrappable', () => {
const editedHeadline =
'Eine redaktionell geänderte Überschrift mit zusätzlichem Inhalt für schmale Ansichten'
render(<StatusSlider data={applicationPhase(editedHeadline)} />)
const headline = screen.getByTestId('phase-headline')
expect(headline.textContent).toBe(editedHeadline)
expect(headline.querySelector('br')).toBeNull()
expect(headline.classList.contains('type-page-title')).toBe(true)
expect(screen.getByTestId('phase-section').style.height).toBe('auto')
expect(screen.getByTestId('phase-section').style.minHeight).toBe('480px')
})
it('keeps the requested copy out of the homepage component', () => {
const source = readFileSync(join(process.cwd(), 'src/spa/pages/Home.tsx'), 'utf8')
expect(source).not.toContain('Ein weiteres')
expect(source).not.toContain('erfolgreiches Jahr')
})
})