From d5510ed75a536397a711c3957d90cd605709b0e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 15:08:50 +0000 Subject: [PATCH 1/4] =?UTF-8?q?Meetup-=C3=9Cbersicht:=20Teaser=20aus=20int?= =?UTF-8?q?ro,=20kommende=20Termine=20chronologisch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Karten der Meetup-Übersicht haben ihren Anreißer aus `description` gelesen. Dort steht inzwischen der immer gleiche Aufruf für Lightning-Talk-Speaker samt Agenda, sodass auf jeder Karte derselbe Text stand. Der eigentliche Teaser liegt in `intro`, das `getMeetups` bisher gar nicht abgefragt hat. `intro` wird jetzt mitgeladen und auf der Karte gezeigt; ist es leer – bei älteren Meetups, die ihren Text noch in `description` haben – wird weiterhin `description` verwendet. Beide Felder sind Rich Text und laufen durch `getPlainText`. Außerdem waren die kommenden Meetups nach der Sortierung der Abfrage absteigend, also mit dem am weitesten entfernten Termin oben. Sie werden nun aufsteigend sortiert, damit der nächste Termin zuerst steht; die vergangenen Meetups bleiben wie bisher absteigend. Der Wunsch kam aus dem Team. --- nuxt-app/components/MeetupCard.vue | 22 +++++++++++++++++----- nuxt-app/components/MeetupSection.vue | 2 +- nuxt-app/composables/useDirectus.ts | 1 + nuxt-app/pages/index.vue | 12 ++++++++---- nuxt-app/pages/meetup/index.vue | 12 ++++++++---- 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/nuxt-app/components/MeetupCard.vue b/nuxt-app/components/MeetupCard.vue index 9aa63ee1..ddaf455e 100644 --- a/nuxt-app/components/MeetupCard.vue +++ b/nuxt-app/components/MeetupCard.vue @@ -17,11 +17,11 @@ {{ meetup.title }} - +

- {{ description }} + {{ excerpt }}

@@ -48,7 +48,7 @@ export default defineComponent({ props: { meetup: { type: Object as PropType< - Pick + Pick >, required: true, }, @@ -61,11 +61,23 @@ export default defineComponent({ // Create href to meetup subpage const href = computed(() => `/meetup/${props.meetup.slug}`) - const description = computed(() => getPlainText(props.meetup.description)) + // The teaser prose belongs in `intro`; `description` carries the event details, and since + // late 2025 that is the recurring call for Lightning-Talk speakers plus the agenda, which + // reads as boilerplate on every card. Meetups created before then left `intro` empty and put + // their prose in `description`, so fall back to it rather than showing an empty card. + // + // Both fields are CMS rich text, so both go through `getPlainText`: it strips the markup and + // decodes entities, and it also turns an `intro` that is null or nothing but empty tags into + // an empty string, which is what the fallback tests. + const excerpt = computed(() => { + const intro = getPlainText(props.meetup.intro).trim() + + return intro || getPlainText(props.meetup.description) + }) return { href, - description, + excerpt, } }, }) diff --git a/nuxt-app/components/MeetupSection.vue b/nuxt-app/components/MeetupSection.vue index 77a77b57..6a21eda6 100644 --- a/nuxt-app/components/MeetupSection.vue +++ b/nuxt-app/components/MeetupSection.vue @@ -44,7 +44,7 @@ export default defineComponent({ props: { meetups: { type: Array as PropType< - Pick[] + Pick[] >, required: true, }, diff --git a/nuxt-app/composables/useDirectus.ts b/nuxt-app/composables/useDirectus.ts index dcb82792..5b9b097b 100644 --- a/nuxt-app/composables/useDirectus.ts +++ b/nuxt-app/composables/useDirectus.ts @@ -629,6 +629,7 @@ export function useDirectus() { 'start_on', 'end_on', 'title', + 'intro', 'description', 'cover_image.*', 'tags.tag.id', diff --git a/nuxt-app/pages/index.vue b/nuxt-app/pages/index.vue index 3a014ef2..a1b68aac 100644 --- a/nuxt-app/pages/index.vue +++ b/nuxt-app/pages/index.vue @@ -120,10 +120,14 @@ const { data: pageData } = useAsyncData(async () => { directus.getTestimonials(), ]) - const upcomingMeetups = meetups.filter((meetup) => { - const now = new Date() - return new Date(meetup.start_on) > now - }) + // The query sorts newest first, so upcoming meetups have to be reversed: the next date belongs at + // the top of the section, not the one furthest out. + const upcomingMeetups = meetups + .filter((meetup) => { + const now = new Date() + return new Date(meetup.start_on) > now + }) + .sort((a, b) => new Date(a.start_on).getTime() - new Date(b.start_on).getTime()) return { homePage, latestPodcasts, podcastCount, upcomingMeetups, testimonials } }) diff --git a/nuxt-app/pages/meetup/index.vue b/nuxt-app/pages/meetup/index.vue index 8513a89e..df682254 100644 --- a/nuxt-app/pages/meetup/index.vue +++ b/nuxt-app/pages/meetup/index.vue @@ -59,11 +59,15 @@ const { data: pageData } = useAsyncData(async () => { return new Date(meetup.start_on) < now }) - const upcomingMeetups = meetups.filter((meetup) => { - const now = new Date() + // The query sorts newest first, which is what the past list wants. Upcoming meetups have to run + // the other way round, so the next date is the one at the top instead of the one furthest out. + const upcomingMeetups = meetups + .filter((meetup) => { + const now = new Date() - return new Date(meetup.start_on) > now - }) + return new Date(meetup.start_on) > now + }) + .sort((a, b) => new Date(a.start_on).getTime() - new Date(b.start_on).getTime()) return { meetupPage, upcomingMeetups, pastMeetups, testimonials } }) From 816d86d61edf386517b156230f9dc90621fe2e18 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 15:16:40 +0000 Subject: [PATCH 2/4] =?UTF-8?q?Review-Anmerkungen:=20Prop-Typ=20vervollst?= =?UTF-8?q?=C3=A4ndigen,=20Zeitpunkt=20einmal=20bestimmen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der `meetups`-Prop von `MeetupSection` listete `id` und `slug` nicht auf, obwohl das Template `item.id` als Key nutzt und `MeetupCard` `slug` für den Link braucht. Der Typ war damit irreführend und hätte künftige Abweichungen in der Datenform verdeckt; beide Felder sind jetzt Teil des `Pick<>`. Außerdem wurde `new Date()` in den Filter-Callbacks pro Meetup neu gebildet. Der Zeitpunkt wird nun einmal vor den Filtern bestimmt, sodass vergangene und kommende Termine gegen dieselbe Referenzzeit verglichen werden. --- nuxt-app/components/MeetupSection.vue | 5 ++++- nuxt-app/pages/index.vue | 7 +++---- nuxt-app/pages/meetup/index.vue | 12 +++--------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/nuxt-app/components/MeetupSection.vue b/nuxt-app/components/MeetupSection.vue index 6a21eda6..80c51926 100644 --- a/nuxt-app/components/MeetupSection.vue +++ b/nuxt-app/components/MeetupSection.vue @@ -44,7 +44,10 @@ export default defineComponent({ props: { meetups: { type: Array as PropType< - Pick[] + Pick< + MeetupItem, + 'id' | 'slug' | 'start_on' | 'end_on' | 'title' | 'cover_image' | 'intro' | 'description' + >[] >, required: true, }, diff --git a/nuxt-app/pages/index.vue b/nuxt-app/pages/index.vue index a1b68aac..1796b279 100644 --- a/nuxt-app/pages/index.vue +++ b/nuxt-app/pages/index.vue @@ -120,13 +120,12 @@ const { data: pageData } = useAsyncData(async () => { directus.getTestimonials(), ]) + const now = new Date() + // The query sorts newest first, so upcoming meetups have to be reversed: the next date belongs at // the top of the section, not the one furthest out. const upcomingMeetups = meetups - .filter((meetup) => { - const now = new Date() - return new Date(meetup.start_on) > now - }) + .filter((meetup) => new Date(meetup.start_on) > now) .sort((a, b) => new Date(a.start_on).getTime() - new Date(b.start_on).getTime()) return { homePage, latestPodcasts, podcastCount, upcomingMeetups, testimonials } diff --git a/nuxt-app/pages/meetup/index.vue b/nuxt-app/pages/meetup/index.vue index df682254..3ac33eee 100644 --- a/nuxt-app/pages/meetup/index.vue +++ b/nuxt-app/pages/meetup/index.vue @@ -53,20 +53,14 @@ const { data: pageData } = useAsyncData(async () => { directus.getTestimonials(), ]) - const pastMeetups = meetups.filter((meetup) => { - const now = new Date() + const now = new Date() - return new Date(meetup.start_on) < now - }) + const pastMeetups = meetups.filter((meetup) => new Date(meetup.start_on) < now) // The query sorts newest first, which is what the past list wants. Upcoming meetups have to run // the other way round, so the next date is the one at the top instead of the one furthest out. const upcomingMeetups = meetups - .filter((meetup) => { - const now = new Date() - - return new Date(meetup.start_on) > now - }) + .filter((meetup) => new Date(meetup.start_on) > now) .sort((a, b) => new Date(a.start_on).getTime() - new Date(b.start_on).getTime()) return { meetupPage, upcomingMeetups, pastMeetups, testimonials } From a3880aa283eb74bffae5169018451f562e0568a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 15:22:35 +0000 Subject: [PATCH 3/4] Auswahl der kommenden Meetups in einen gemeinsamen Helper ziehen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Auswahl und Sortierung der kommenden Meetups stand wortgleich auf der Meetup-Übersicht und auf der Startseite. AGENTS.md verlangt, ein Muster, das an mehr als einer Stelle auftaucht, in eine gemeinsame Abstraktion zu ziehen — genau das war hier nicht der Fall. `helpers/meetupSchedule.ts` enthält jetzt `getUpcomingMeetups` (filtert auf Termine in der Zukunft und sortiert aufsteigend, damit der nächste Termin oben steht) und `getPastMeetups` (behält die Reihenfolge der Abfrage, die bereits absteigend sortiert). Beide bekommen die Referenzzeit als Argument, statt selbst die Uhr zu lesen, damit eine Seite mit beiden Listen alle Meetups gegen denselben Zeitpunkt vergleicht. Die Startseite blendet weiterhin selbst das hervorgehobene Element aus — das ist ihr eigenes Anliegen und gehört nicht in den Helper. Dazu ein Unit-Test, der die Sortierung, die Grenzfälle und das saubere Aufteilen in vergangen/kommend abdeckt. --- nuxt-app/helpers/index.ts | 1 + nuxt-app/helpers/meetupSchedule.ts | 43 ++++++++++++++++ nuxt-app/pages/index.vue | 9 +--- nuxt-app/pages/meetup/index.vue | 11 ++-- nuxt-app/test/meetupSchedule.test.ts | 76 ++++++++++++++++++++++++++++ 5 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 nuxt-app/helpers/meetupSchedule.ts create mode 100644 nuxt-app/test/meetupSchedule.test.ts diff --git a/nuxt-app/helpers/index.ts b/nuxt-app/helpers/index.ts index 68f63389..ad9fcd29 100644 --- a/nuxt-app/helpers/index.ts +++ b/nuxt-app/helpers/index.ts @@ -3,6 +3,7 @@ export * from './formatAudioTimestamp' export * from './getCookie' export * from './getHashCode' export * from './getMetaInfo' +export * from './meetupSchedule' export * from './normalizeExternalUrl' export * from './getTrimmedString' export * from './parseCmsDate' diff --git a/nuxt-app/helpers/meetupSchedule.ts b/nuxt-app/helpers/meetupSchedule.ts new file mode 100644 index 00000000..bccff36e --- /dev/null +++ b/nuxt-app/helpers/meetupSchedule.ts @@ -0,0 +1,43 @@ +import type { DirectusMeetupItem } from '~/types/directus' + +/** + * Selects and orders meetups by their date, so the meetup overview and the + * homepage derive their lists the same way. + * + * `getMeetups` sorts newest first, which is what a list of past meetups wants: + * the most recent one on top. Upcoming meetups have to run the other way + * round, otherwise the date furthest out is shown first and the next one sits + * at the bottom of the section. + * + * Both selections take `now` as an argument rather than reading the clock + * themselves, so a page that renders a past *and* an upcoming list compares + * every meetup against a single reference time. + */ +type ScheduledMeetup = Pick + +/** + * Upcoming meetups, soonest first. + * + * @param meetups The meetups to select from, in any order. + * @param now The reference time a meetup's `start_on` is compared against. + * + * @returns A new array holding the meetups that start after `now`, sorted ascending. + */ +export function getUpcomingMeetups(meetups: T[], now: Date): T[] { + return meetups + .filter((meetup) => new Date(meetup.start_on) > now) + .sort((a, b) => new Date(a.start_on).getTime() - new Date(b.start_on).getTime()) +} + +/** + * Past meetups, in the order they came in — the CMS query already sorts them + * newest first, which is the order the past list is shown in. + * + * @param meetups The meetups to select from. + * @param now The reference time a meetup's `start_on` is compared against. + * + * @returns A new array holding the meetups that started before `now`. + */ +export function getPastMeetups(meetups: T[], now: Date): T[] { + return meetups.filter((meetup) => new Date(meetup.start_on) < now) +} diff --git a/nuxt-app/pages/index.vue b/nuxt-app/pages/index.vue index 1796b279..6213cba6 100644 --- a/nuxt-app/pages/index.vue +++ b/nuxt-app/pages/index.vue @@ -98,6 +98,7 @@ import BrandLogoIcon from '~/assets/images/brand-logo.svg' import PrimaryPbButton from '~/components/PrimaryPbButton.vue' import TestimonialSlider from '~/components/TestimonialSlider.vue' import { useDirectus } from '~/composables/useDirectus' +import { getUpcomingMeetups } from '~/helpers' import { getAssetUrl } from '~/helpers/getAssetUrl' import { generatePodcastSeries } from '~/helpers/jsonLdGenerator' import { computed, type ComputedRef } from 'vue' @@ -120,13 +121,7 @@ const { data: pageData } = useAsyncData(async () => { directus.getTestimonials(), ]) - const now = new Date() - - // The query sorts newest first, so upcoming meetups have to be reversed: the next date belongs at - // the top of the section, not the one furthest out. - const upcomingMeetups = meetups - .filter((meetup) => new Date(meetup.start_on) > now) - .sort((a, b) => new Date(a.start_on).getTime() - new Date(b.start_on).getTime()) + const upcomingMeetups = getUpcomingMeetups(meetups, new Date()) return { homePage, latestPodcasts, podcastCount, upcomingMeetups, testimonials } }) diff --git a/nuxt-app/pages/meetup/index.vue b/nuxt-app/pages/meetup/index.vue index 3ac33eee..963602ea 100644 --- a/nuxt-app/pages/meetup/index.vue +++ b/nuxt-app/pages/meetup/index.vue @@ -39,6 +39,7 @@ import TestimonialSlider from '~/components/TestimonialSlider.vue' import { useLoadingScreen, usePageMeta } from '~/composables' import { useDirectus } from '~/composables/useDirectus' +import { getPastMeetups, getUpcomingMeetups } from '~/helpers' import type { DirectusMeetupItem, DirectusMeetupPage, DirectusTestimonialItem } from '~/types' import { computed, type ComputedRef } from 'vue' @@ -53,15 +54,11 @@ const { data: pageData } = useAsyncData(async () => { directus.getTestimonials(), ]) + // One reference time for both lists, so a meetup cannot fall out of either. const now = new Date() - const pastMeetups = meetups.filter((meetup) => new Date(meetup.start_on) < now) - - // The query sorts newest first, which is what the past list wants. Upcoming meetups have to run - // the other way round, so the next date is the one at the top instead of the one furthest out. - const upcomingMeetups = meetups - .filter((meetup) => new Date(meetup.start_on) > now) - .sort((a, b) => new Date(a.start_on).getTime() - new Date(b.start_on).getTime()) + const pastMeetups = getPastMeetups(meetups, now) + const upcomingMeetups = getUpcomingMeetups(meetups, now) return { meetupPage, upcomingMeetups, pastMeetups, testimonials } }) diff --git a/nuxt-app/test/meetupSchedule.test.ts b/nuxt-app/test/meetupSchedule.test.ts new file mode 100644 index 00000000..14e4b696 --- /dev/null +++ b/nuxt-app/test/meetupSchedule.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest' +import { getPastMeetups, getUpcomingMeetups } from '../helpers/meetupSchedule' + +// The ordering is the reason this exists. `getMeetups` sorts newest first, which put the meetup +// furthest out at the top of the upcoming section and the next one at the bottom. + +const NOW = new Date('2026-06-15T12:00:00Z') + +// Newest first, the order the CMS query returns. +const MEETUPS = [ + { slug: 'in-three-months', start_on: '2026-09-01T18:00:00Z' }, + { slug: 'next-week', start_on: '2026-06-22T18:00:00Z' }, + { slug: 'tomorrow', start_on: '2026-06-16T18:00:00Z' }, + { slug: 'last-month', start_on: '2026-05-10T18:00:00Z' }, + { slug: 'last-year', start_on: '2025-06-10T18:00:00Z' }, +] + +describe('getUpcomingMeetups', () => { + it('returns only meetups that start after the reference time', () => { + expect(getUpcomingMeetups(MEETUPS, NOW).map((meetup) => meetup.slug)).toEqual([ + 'tomorrow', + 'next-week', + 'in-three-months', + ]) + }) + + it('sorts soonest first, whatever order it is given', () => { + const reversed = [...MEETUPS].reverse() + + expect(getUpcomingMeetups(reversed, NOW).map((meetup) => meetup.slug)).toEqual([ + 'tomorrow', + 'next-week', + 'in-three-months', + ]) + }) + + it('leaves the input array untouched', () => { + const meetups = [...MEETUPS] + + getUpcomingMeetups(meetups, NOW) + + expect(meetups).toEqual(MEETUPS) + }) + + it('returns nothing when every meetup is in the past', () => { + expect(getUpcomingMeetups(MEETUPS, new Date('2027-01-01T00:00:00Z'))).toEqual([]) + }) +}) + +describe('getPastMeetups', () => { + it('returns only meetups that started before the reference time', () => { + expect(getPastMeetups(MEETUPS, NOW).map((meetup) => meetup.slug)).toEqual(['last-month', 'last-year']) + }) + + it('keeps the incoming order, which the CMS query already sorts newest first', () => { + expect(getPastMeetups(MEETUPS, new Date('2027-01-01T00:00:00Z')).map((meetup) => meetup.slug)).toEqual([ + 'in-three-months', + 'next-week', + 'tomorrow', + 'last-month', + 'last-year', + ]) + }) +}) + +describe('the two selections together', () => { + it('split the meetups without dropping or duplicating one', () => { + const upcoming = getUpcomingMeetups(MEETUPS, NOW) + const past = getPastMeetups(MEETUPS, NOW) + + expect(upcoming.length + past.length).toBe(MEETUPS.length) + expect([...upcoming, ...past].map((meetup) => meetup.slug).sort()).toEqual( + MEETUPS.map((meetup) => meetup.slug).sort() + ) + }) +}) From 7409d8532298339dceac3b55ba935d2d04edf24a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 15:28:58 +0000 Subject: [PATCH 4/4] =?UTF-8?q?Meetup=20am=20Referenzzeitpunkt=20z=C3=A4hl?= =?UTF-8?q?t=20als=20kommend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getUpcomingMeetups` verglich strikt mit `>`, `getPastMeetups` strikt mit `<`. Ein Meetup, dessen `start_on` genau dem Referenzzeitpunkt entspricht, fiel damit aus beiden Listen heraus und war für diesen Moment auf keiner der beiden Seiten zu sehen — genau das, was die gemeinsame Referenzzeit verhindern sollte. Die Grenze gehört nun zur kommenden Seite: ein Meetup, das exakt jetzt beginnt, hat noch nicht stattgefunden. Damit teilen die beiden Auswahlen die Meetups vollständig auf. Die gewählte Grenze steht ausdrücklich im Docblock, statt sich nur aus dem Operator zu ergeben. Der Test deckt den Gleichheitsfall jetzt in beiden Auswahlen und in der Aufteilungsprüfung ab; ohne die Änderung schlagen die beiden neuen Fälle auf der kommenden Seite fehl. --- nuxt-app/helpers/meetupSchedule.ts | 13 +++++++++-- nuxt-app/test/meetupSchedule.test.ts | 34 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/nuxt-app/helpers/meetupSchedule.ts b/nuxt-app/helpers/meetupSchedule.ts index bccff36e..2a709b24 100644 --- a/nuxt-app/helpers/meetupSchedule.ts +++ b/nuxt-app/helpers/meetupSchedule.ts @@ -12,6 +12,12 @@ import type { DirectusMeetupItem } from '~/types/directus' * Both selections take `now` as an argument rather than reading the clock * themselves, so a page that renders a past *and* an upcoming list compares * every meetup against a single reference time. + * + * The boundary belongs to the upcoming side: a meetup whose `start_on` is + * exactly `now` has not happened yet, so it counts as upcoming and not as + * past. The two selections therefore partition the meetups completely — every + * meetup lands in exactly one of the lists, and one starting at the very + * moment the page renders cannot fall out of both. */ type ScheduledMeetup = Pick @@ -21,11 +27,11 @@ type ScheduledMeetup = Pick * @param meetups The meetups to select from, in any order. * @param now The reference time a meetup's `start_on` is compared against. * - * @returns A new array holding the meetups that start after `now`, sorted ascending. + * @returns A new array holding the meetups that start at or after `now`, sorted ascending. */ export function getUpcomingMeetups(meetups: T[], now: Date): T[] { return meetups - .filter((meetup) => new Date(meetup.start_on) > now) + .filter((meetup) => new Date(meetup.start_on) >= now) .sort((a, b) => new Date(a.start_on).getTime() - new Date(b.start_on).getTime()) } @@ -33,6 +39,9 @@ export function getUpcomingMeetups(meetups: T[], now: * Past meetups, in the order they came in — the CMS query already sorts them * newest first, which is the order the past list is shown in. * + * Strictly before `now`: a meetup starting exactly at the reference time is + * upcoming, not past. + * * @param meetups The meetups to select from. * @param now The reference time a meetup's `start_on` is compared against. * diff --git a/nuxt-app/test/meetupSchedule.test.ts b/nuxt-app/test/meetupSchedule.test.ts index 14e4b696..aa956dba 100644 --- a/nuxt-app/test/meetupSchedule.test.ts +++ b/nuxt-app/test/meetupSchedule.test.ts @@ -15,6 +15,10 @@ const MEETUPS = [ { slug: 'last-year', start_on: '2025-06-10T18:00:00Z' }, ] +// A meetup starting at the exact reference time. Both selections used a strict comparison once, so +// this one fell out of the upcoming *and* the past list — invisible on the page for that one moment. +const STARTING_NOW = { slug: 'starting-now', start_on: NOW.toISOString() } + describe('getUpcomingMeetups', () => { it('returns only meetups that start after the reference time', () => { expect(getUpcomingMeetups(MEETUPS, NOW).map((meetup) => meetup.slug)).toEqual([ @@ -45,6 +49,15 @@ describe('getUpcomingMeetups', () => { it('returns nothing when every meetup is in the past', () => { expect(getUpcomingMeetups(MEETUPS, new Date('2027-01-01T00:00:00Z'))).toEqual([]) }) + + it('counts a meetup starting exactly at the reference time as upcoming', () => { + expect(getUpcomingMeetups([...MEETUPS, STARTING_NOW], NOW).map((meetup) => meetup.slug)).toEqual([ + 'starting-now', + 'tomorrow', + 'next-week', + 'in-three-months', + ]) + }) }) describe('getPastMeetups', () => { @@ -52,6 +65,13 @@ describe('getPastMeetups', () => { expect(getPastMeetups(MEETUPS, NOW).map((meetup) => meetup.slug)).toEqual(['last-month', 'last-year']) }) + it('does not count a meetup starting exactly at the reference time as past', () => { + expect(getPastMeetups([...MEETUPS, STARTING_NOW], NOW).map((meetup) => meetup.slug)).toEqual([ + 'last-month', + 'last-year', + ]) + }) + it('keeps the incoming order, which the CMS query already sorts newest first', () => { expect(getPastMeetups(MEETUPS, new Date('2027-01-01T00:00:00Z')).map((meetup) => meetup.slug)).toEqual([ 'in-three-months', @@ -73,4 +93,18 @@ describe('the two selections together', () => { MEETUPS.map((meetup) => meetup.slug).sort() ) }) + + it('still split cleanly when a meetup starts exactly at the reference time', () => { + // The regression: with a strict comparison on both sides, `starting-now` was in neither list. + const meetups = [...MEETUPS, STARTING_NOW] + const upcoming = getUpcomingMeetups(meetups, NOW) + const past = getPastMeetups(meetups, NOW) + + expect(upcoming.length + past.length).toBe(meetups.length) + expect([...upcoming, ...past].map((meetup) => meetup.slug).sort()).toEqual( + meetups.map((meetup) => meetup.slug).sort() + ) + expect(upcoming.map((meetup) => meetup.slug)).toContain('starting-now') + expect(past.map((meetup) => meetup.slug)).not.toContain('starting-now') + }) })