From ee7e891a7d7dcfca852edc8acc208432f3f481c4 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Mon, 13 Jul 2026 15:19:37 +0200 Subject: [PATCH] feat: implement home winners selection logic with fallback handling and add integration tests --- src/spa/homeWinnerSelection.ts | 26 +++++++++++++++++ src/spa/pages/Home.tsx | 19 ++---------- tests/int/home-winner-selection.int.spec.ts | 32 +++++++++++++++++++++ 3 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 src/spa/homeWinnerSelection.ts create mode 100644 tests/int/home-winner-selection.int.spec.ts diff --git a/src/spa/homeWinnerSelection.ts b/src/spa/homeWinnerSelection.ts new file mode 100644 index 0000000..e05284c --- /dev/null +++ b/src/spa/homeWinnerSelection.ts @@ -0,0 +1,26 @@ +import { resolveCmsRelationship, type CmsRouteDoc } from '@/spa/cmsRoute' + +const FALLBACK_YEAR = 2025 +const FALLBACK_LIMIT = 8 + +type FallbackWinner = { + id: string | number + year?: string | number +} + +export function resolveHomeWinners( + featured: unknown, + cmsWinners: CmsRouteDoc[], + fallbackWinners: FallbackWinner[], +) { + const selectedWinners = Array.isArray(featured) ? featured : [] + const selectedWinnerDocs = selectedWinners + .map((selected) => resolveCmsRelationship(selected, cmsWinners)) + .filter((winner): winner is CmsRouteDoc => Boolean(winner)) + + if (selectedWinnerDocs.length) return selectedWinnerDocs + + return (cmsWinners.length ? cmsWinners : fallbackWinners) + .filter((winner) => Number(winner.year) === FALLBACK_YEAR) + .slice(0, FALLBACK_LIMIT) +} diff --git a/src/spa/pages/Home.tsx b/src/spa/pages/Home.tsx index bb9ab84..0079620 100644 --- a/src/spa/pages/Home.tsx +++ b/src/spa/pages/Home.tsx @@ -11,9 +11,10 @@ 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, useNewsletterForm } from '@/spa/cmsRoute' +import { useApplicationPhase, useCmsCollection, useCmsRoute, useNewsletterForm } from '@/spa/cmsRoute' import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField' import { homeContent } from '@/spa/homeContent' +import { resolveHomeWinners } from '@/spa/homeWinnerSelection' import { netzwerkContent } from '@/spa/netzwerkContent' import { usePreistraegerPlaceholderImage } from '@/spa/preistraegerPlaceholder' import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo' @@ -251,26 +252,12 @@ const Home: React.FC = () => { const newsletterCopy = newsletterFormCopyForPhase(newsletterForm, activeApplicationPhase); const newsletterPhase = newsletterPhaseForApplicationPhase(activeApplicationPhase); const cmsPreistraeger = useCmsCollection('preistraeger'); - const selectedWinners = fallbackArray(winnersSection.featured, []); const heroSecondaryCta = { label: fallbackText(hero.secondaryCta?.label, homeContent.hero.secondaryCta.label), url: fallbackText(hero.secondaryCta?.url, homeContent.hero.secondaryCta.url), }; const showHeroSecondaryCta = !HIDE_MEMBERSHIP_HOME_HERO_CTA || !isMembershipCta(heroSecondaryCta); - const selectedWinnerDocs = selectedWinners - .map((selected) => resolveCmsRelationship(selected, cmsPreistraeger)) - .filter(Boolean) - const eligibleWinners = (cmsPreistraeger.length ? cmsPreistraeger : WINNERS) - .filter((winner) => Number((winner as { year?: string | number }).year) === 2025) - const selectedWinnerIDs = new Set( - selectedWinnerDocs.map((winner) => String((winner as WinnerCardData).id || (winner as WinnerCardData).slug)), - ) - const remainingWinners = selectedWinnerDocs.length >= 8 - ? eligibleWinners.filter((winner) => !selectedWinnerIDs.has(String((winner as WinnerCardData).id || (winner as WinnerCardData).slug))) - : [] - const homeWinners = selectedWinnerDocs.length - ? [...selectedWinnerDocs, ...remainingWinners] - : eligibleWinners; + const homeWinners = resolveHomeWinners(winnersSection.featured, cmsPreistraeger, WINNERS); const scrollDesktopWinners = !isMobile && homeWinners.length > 8; const scrollWinnerGrid = isMobile || scrollDesktopWinners; const quickCriteria = fallbackArray(quickCheck.criteria, homeContent.quickCheck.criteria); diff --git a/tests/int/home-winner-selection.int.spec.ts b/tests/int/home-winner-selection.int.spec.ts new file mode 100644 index 0000000..4cd53fb --- /dev/null +++ b/tests/int/home-winner-selection.int.spec.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest' + +import type { CmsRouteDoc } from '@/spa/cmsRoute' +import { resolveHomeWinners } from '@/spa/homeWinnerSelection' + +const selected2026Winners: CmsRouteDoc[] = Array.from({ length: 16 }, (_, index) => ({ + id: 65 + index, + title: `Winner ${65 + index}`, + year: 2026, +})) + +const published2025Winners: CmsRouteDoc[] = Array.from({ length: 9 }, (_, index) => ({ + id: 12 + index, + title: `Winner ${12 + index}`, + year: 2025, +})) + +describe('home winner selection', () => { + it('uses a non-empty CMS selection without appending winners from the fallback year', () => { + const featured = selected2026Winners.map(({ id }) => id) + const winners = resolveHomeWinners(featured, [...selected2026Winners, ...published2025Winners], []) + + expect(winners).toEqual(selected2026Winners) + expect(winners.some((winner) => winner.year === 2025)).toBe(false) + }) + + it('falls back to at most eight 2025 winners when the CMS selection is empty', () => { + const winners = resolveHomeWinners([], published2025Winners, []) + + expect(winners).toEqual(published2025Winners.slice(0, 8)) + }) +})