fix(home): honor phase headline line breaks
This commit is contained in:
@@ -26,7 +26,7 @@ const phaseFields: Field[] = [
|
|||||||
text('tag', 'Badge'),
|
text('tag', 'Badge'),
|
||||||
{ name: 'pulse', label: 'Show pulse dot', type: 'checkbox', defaultValue: true },
|
{ name: 'pulse', label: 'Show pulse dot', type: 'checkbox', defaultValue: true },
|
||||||
text('phase', 'Phase label'),
|
text('phase', 'Phase label'),
|
||||||
text('headline', 'Headline'),
|
textarea('headline', 'Headline'),
|
||||||
textarea('body', 'Body copy'),
|
textarea('body', 'Body copy'),
|
||||||
{ name: 'cta', label: 'Primary CTA', type: 'group', fields: linkGroup() },
|
{ name: 'cta', label: 'Primary CTA', type: 'group', fields: linkGroup() },
|
||||||
text('accent', 'Accent color'),
|
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_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_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_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 = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
@@ -132,4 +133,9 @@ export const migrations = [
|
|||||||
down: migration_20260731_190000_responsive_sponsor_hierarchy.down,
|
down: migration_20260731_190000_responsive_sponsor_hierarchy.down,
|
||||||
name: '20260731_190000_responsive_sponsor_hierarchy',
|
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',
|
tag: 'ABGESCHLOSSEN',
|
||||||
pulse: false,
|
pulse: false,
|
||||||
phase: 'Preisverleihung · Oktober 2026',
|
phase: 'Preisverleihung · Oktober 2026',
|
||||||
headline: 'DER BMP 2026 WAR EIN ERFOLG.',
|
headline: 'Ein weiteres\nerfolgreiches Jahr',
|
||||||
body:
|
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.',
|
'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' },
|
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';
|
cta?.label?.trim().toLowerCase() === 'mitglied werden';
|
||||||
|
|
||||||
function Lines({ text }: { text: string }) {
|
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[]) {
|
function partnerLogos(partners: PartnerLogoData[]) {
|
||||||
@@ -542,7 +542,7 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
// ─── StatusSlider ─────────────────────────────────────────────────────────────
|
// ─── StatusSlider ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function StatusSlider({ data }: { data?: SpaApplicationPhaseData }) {
|
export function StatusSlider({ data }: { data?: SpaApplicationPhaseData }) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
if (!data?.phases?.length) return null;
|
if (!data?.phases?.length) return null;
|
||||||
|
|
||||||
@@ -568,11 +568,12 @@ function StatusSlider({ data }: { data?: SpaApplicationPhaseData }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
|
data-testid="phase-section"
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
height: isMobile ? 'auto' : 480,
|
height: 'auto',
|
||||||
minHeight: isMobile ? 360 : 'auto',
|
minHeight: isMobile ? 360 : 480,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StatusSlideCard
|
<StatusSlideCard
|
||||||
@@ -605,7 +606,7 @@ function StatusSlideCard({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '100vw',
|
width: '100vw',
|
||||||
height: '100%',
|
minHeight: isMobile ? 360 : 480,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
background: phase.bg,
|
background: phase.bg,
|
||||||
@@ -629,8 +630,8 @@ function StatusSlideCard({
|
|||||||
|
|
||||||
{/* Headline + body + CTA */}
|
{/* Headline + body + CTA */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="type-page-title text-role-inverse" style={{ textTransform: 'uppercase', margin: '0 0 24px' }}>
|
<h2 className="type-page-title text-role-inverse" data-testid="phase-headline" style={{ textTransform: 'uppercase', margin: '0 0 24px' }}>
|
||||||
{phase.headline}
|
<Lines text={fallbackText(phase.headline, '')} />
|
||||||
</h2>
|
</h2>
|
||||||
<p className="type-body text-role-inverse-secondary" style={{ marginBottom: 32, maxWidth: isMobile ? 520 : 760 }}>
|
<p className="type-body text-role-inverse-secondary" style={{ marginBottom: 32, maxWidth: isMobile ? 520 : 760 }}>
|
||||||
{phase.body}
|
{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