fix(home): honor phase headline line breaks
This commit is contained in:
@@ -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'),
|
||||
|
||||
@@ -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)};`,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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}
|
||||
|
||||
73
tests/int/home-phase-headline.int.spec.tsx
Normal file
73
tests/int/home-phase-headline.int.spec.tsx
Normal 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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user