From 3c28f6ff9c53486b87c2d45ffb57e35bc5a025d7 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Fri, 31 Jul 2026 18:25:33 +0200 Subject: [PATCH] fix(about): rebalance relevance section --- src/spa/cmsMediaField.ts | 15 +++ src/spa/pages/About.tsx | 54 +++++++---- tests/int/about-relevance.int.spec.tsx | 129 +++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 20 deletions(-) create mode 100644 tests/int/about-relevance.int.spec.tsx diff --git a/src/spa/cmsMediaField.ts b/src/spa/cmsMediaField.ts index cc7f44e..3d76a29 100644 --- a/src/spa/cmsMediaField.ts +++ b/src/spa/cmsMediaField.ts @@ -4,6 +4,8 @@ type MediaLike = { url?: string | null filename?: string | null alt?: string | null + focalX?: number | null + focalY?: number | null } export function normalizeMediaUrl(url: string | null | undefined) { @@ -22,6 +24,19 @@ export function mediaAlt(media: unknown, fallback: string) { return doc.alt || fallback } +const focalCoordinate = (value: unknown) => { + if (typeof value !== 'number' || !Number.isFinite(value)) return 50 + return Math.min(100, Math.max(0, value)) +} + +export function mediaObjectPosition(media: unknown, fallback = 'center') { + if (!media || typeof media !== 'object') return fallback + const doc = media as MediaLike + if (typeof doc.focalX !== 'number' && typeof doc.focalY !== 'number') return fallback + + return `${focalCoordinate(doc.focalX)}% ${focalCoordinate(doc.focalY)}%` +} + export function docImageUrl(doc: CmsRouteDoc | undefined, fallback: string, field = 'image') { return mediaUrl(doc?.[field], fallback) } diff --git a/src/spa/pages/About.tsx b/src/spa/pages/About.tsx index 10a0e84..4dd6160 100644 --- a/src/spa/pages/About.tsx +++ b/src/spa/pages/About.tsx @@ -6,7 +6,7 @@ import HomeWinnersSection, { type HomeWinnersContent } from '@/spa/components/Ho import TestimonialsSection from '@/spa/components/TestimonialsSection' import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg' import Image from '@/spa/components/ui/UnoptimizedImage' -import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField' +import { mediaAlt, mediaObjectPosition, mediaUrl } from '@/spa/cmsMediaField' import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute' import { useIsMobile } from '@/spa/hooks/useIsMobile' import { Link } from '@/spa/router' @@ -82,6 +82,7 @@ function HighlightedText({ text, highlight }: { text: string; highlight: string const About: React.FC = () => { const isMobile = useIsMobile() + const isRelevanceStacked = useIsMobile(1024) const cms = (useCmsRoute()?.doc?.about || {}) as AboutCms const cmsPages = useCmsCollection('pages') const hero = { ...aboutContent.hero, ...(cms.hero || {}) } @@ -215,14 +216,22 @@ const About: React.FC = () => { -
-
-
+
+
+
{mediaAlt(mittelstand.image,
{ )}
-
- {!isMobile && ( - <> - {fallbackText(mittelstand.eyebrow, aboutContent.mittelstand.eyebrow)} -

- -

- - )} -
- {paragraphs(mittelstand.body, aboutContent.mittelstand.body).map((text, index, arr) => ( -

- {text} -

- ))} +
+
+ {!isMobile && ( + <> + {fallbackText(mittelstand.eyebrow, aboutContent.mittelstand.eyebrow)} +

+ +

+ + )} +
+ {paragraphs(mittelstand.body, aboutContent.mittelstand.body).map((text, index, arr) => ( +

+ {text} +

+ ))} +
diff --git a/tests/int/about-relevance.int.spec.tsx b/tests/int/about-relevance.int.spec.tsx new file mode 100644 index 0000000..480ed54 --- /dev/null +++ b/tests/int/about-relevance.int.spec.tsx @@ -0,0 +1,129 @@ +import { cleanup, render, screen, within } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { CmsRouteProvider, type CmsRouteData } from '@/spa/cmsRoute' +import About from '@/spa/pages/About' + +const viewport = vi.hoisted(() => ({ mobile: false, relevanceStacked: false })) + +vi.mock('@/spa/hooks/useIsMobile', () => ({ + useIsMobile: (breakpoint = 768) => + breakpoint === 1024 ? viewport.relevanceStacked : viewport.mobile, +})) + +vi.mock('next/navigation', () => ({ + usePathname: () => '/der-bmp', + useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), +})) + +vi.mock('@/spa/components/AwardsGridSection', () => ({ + default: () =>
, +})) + +vi.mock('@/spa/components/HomeWinnersSection', () => ({ + default: () =>
, +})) + +vi.mock('@/spa/components/TestimonialsSection', () => ({ + default: () =>
, +})) + +vi.mock('@/spa/components/ui/munich-skyline-bg', () => ({ + default: () => null, +})) + +const aboutRoute: CmsRouteData = { + collection: 'pages', + doc: { + id: 'about-page', + slug: 'der-bmp', + spaPath: '/der-bmp', + about: { + mittelstand: { + image: { + url: '/api/media/file/cms-relevance.jpg', + alt: 'CMS relevance event', + focalX: 38, + focalY: 42, + }, + imageAlt: 'Page-owned fallback alt', + eyebrow: 'CMS relevance eyebrow', + heading: 'CMS RELEVANCE\nHEADING', + body: [ + { text: 'First CMS relevance paragraph.' }, + { text: 'Second CMS relevance paragraph.' }, + ], + }, + }, + }, +} + +const renderAbout = () => + render( + + + , + ) + +describe('/der-bmp relevance section', () => { + beforeEach(() => { + viewport.mobile = false + viewport.relevanceStacked = false + }) + + afterEach(() => cleanup()) + + it('gives the image the larger desktop share and centers a readable copy measure', () => { + renderAbout() + + const layout = screen.getByTestId('relevance-layout') + const copyPanel = screen.getByTestId('relevance-copy-panel') + const copy = screen.getByTestId('relevance-copy') + + expect(layout.getAttribute('data-layout')).toBe('split') + expect(layout.style.gridTemplateColumns).toBe('55% 45%') + expect(copyPanel.style.padding).toBe('88px 64px') + expect(copy.style.width).toBe('100%') + expect(copy.style.maxWidth).toBe('520px') + expect(copy.style.margin).toBe('0px auto') + }) + + it('preserves CMS-owned copy and media while honoring the media focal point', () => { + renderAbout() + + const section = screen.getByTestId('relevance-section') + const image = within(section).getByRole('img', { name: 'CMS relevance event' }) + + expect(image.getAttribute('src')).toBe('/api/media/file/cms-relevance.jpg') + expect(image.style.objectPosition).toBe('38% 42%') + expect(within(section).getByText('CMS relevance eyebrow')).toBeTruthy() + expect(within(section).getByRole('heading', { level: 2 }).textContent).toBe( + 'CMS RELEVANCEHEADING', + ) + expect(within(section).getByText('First CMS relevance paragraph.')).toBeTruthy() + expect(within(section).getByText('Second CMS relevance paragraph.')).toBeTruthy() + expect( + within(section).queryByText( + 'Der Bayerische Mittelstandspreis ist weit mehr als eine Trophäe.', + ), + ).toBeNull() + }) + + it('keeps the image first with readable padding in the stacked mobile layout', () => { + viewport.mobile = true + viewport.relevanceStacked = true + renderAbout() + + const layout = screen.getByTestId('relevance-layout') + const imagePanel = screen.getByTestId('relevance-image-panel') + const copyPanel = screen.getByTestId('relevance-copy-panel') + + expect(layout.getAttribute('data-layout')).toBe('stacked') + expect(layout.style.gridTemplateColumns).toBe('1fr') + expect(imagePanel.style.minHeight).toBe('280px') + expect(copyPanel.style.padding).toBe('32px 24px 40px') + expect( + imagePanel.compareDocumentPosition(copyPanel) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy() + }) +})