Skip to content

Commit

Permalink
Merge pull request #81 from FaberVitale/fix/august-event-in-upcomng-e…
Browse files Browse the repository at this point in the history
…vents-page-79

fix: august event placeholder visible event in upcoming events page
  • Loading branch information
FaberVitale authored Aug 26, 2024
2 parents d479a69 + 3c3f336 commit 5d52561
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 15 deletions.
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ PUBLIC_MEETUP_PAGE_SIZE="10"
# They are intentionally SSR only
MEETUP_GRAPHQL_ENDPOINT='https://api.meetup.com/gql'
MEETUP_GROUP_ID='RomaJS'

# See https://github.com/Roma-JS/roma-js-on-astro/issues/80
# It's a comma-separated list. We use ISO LL format: 08 -> August
PUBLIC_MONTHS_WITHOUT_GENERATED_UPCOMING_EVENTS='08'
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ HomePage content is defined in:

Edit `enHpContent` to modify the english homepage and `itHpContent` to change the italian one.

#### Upcoming events

The upcoming events page is populated using the meetup api (`env.MEETUP_GRAPHQL_ENDPOINT`).

##### Placeholders

The [page generation logic](src/utils/meetup-events.ts) automatically adds placeholders after the current month.
You can use the env var `PUBLIC_MONTHS_WITHOUT_GENERATED_UPCOMING_EVENTS` to filter out placeholder months.

```bash
PUBLIC_MONTHS_WITHOUT_GENERATED_UPCOMING_EVENTS="08,10" # Hide August and October placeholders.
```

#### Deployment

##### Manual deploys
Expand Down
7 changes: 7 additions & 0 deletions src/@types/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ interface ImportMetaEnv {
readonly PUBLIC_DISCORD_INVITE_HREF: string;
readonly PUBLIC_TWITTER_PROFILE_HREF: string;
readonly PUBLIC_GITHUB_PROFILE_HREF: string;
/**
* Comma separated list of 1-index months for wich we do not generate
* an upcoming event.
*
* Does not filter events generated from external data sources.
*/
readonly PUBLIC_MONTHS_WITHOUT_GENERATED_UPCOMING_EVENTS: string;
readonly PUBLIC_SITE_URL: string;
readonly PUBLIC_URL_BASE: string;
readonly PUBLIC_GISCUS_REPO_NAME: string;
Expand Down
4 changes: 3 additions & 1 deletion src/pages/en/upcoming-events.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ const description = 'Upcoming events of RomaJS community';
const scheduledEvents = await fetchUpcomingRomajsEvents()
const upcomingEvents = computeUpcomingEvents(scheduledEvents, Date.now());
const upcomingEvents = computeUpcomingEvents(scheduledEvents, Date.now(), {
monthsWithoutGeneratedUpcomingEvents: import.meta.env.PUBLIC_MONTHS_WITHOUT_GENERATED_UPCOMING_EVENTS
});
---

<UpcomingEventsLayout
Expand Down
4 changes: 3 additions & 1 deletion src/pages/it/prossimi-eventi.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ const description = 'Tutti i prossimi eventi in programma del RomaJS';
const scheduledEvents = await fetchUpcomingRomajsEvents()
const upcomingEvents = computeUpcomingEvents(scheduledEvents, Date.now());
const upcomingEvents = computeUpcomingEvents(scheduledEvents, Date.now(), {
monthsWithoutGeneratedUpcomingEvents: import.meta.env.PUBLIC_MONTHS_WITHOUT_GENERATED_UPCOMING_EVENTS
});
---

<UpcomingEventsLayout
Expand Down
55 changes: 42 additions & 13 deletions src/utils/meetup-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
isBefore,
addMonths,
} from 'date-fns';
import { parseMonthsList } from './time';

export interface MeetupEventsApiResponse {
events: Pick<
Expand Down Expand Up @@ -78,27 +79,31 @@ export function computeScheduledEvents(
);
}

/**
* Given a list of scheduled events, returns a list of scheduled and placeholder
* events sorted by date in ascending order.
* @param scheduledUpcomingEvents Scheduled events
* @param curentEpochTimeMs built time epoch time i.e. Date.now().
* @see {@link UpcomingEvent}
*/
export function computeUpcomingEvents(
scheduledUpcomingEvents: MeetupEventType[] | undefined | null,
curentEpochTimeMs: number
): UpcomingEvent[] {
const scheduledEvents = computeScheduledEvents(scheduledUpcomingEvents);
export interface ComputeUpcomingEventsOptions {
monthsWithoutGeneratedUpcomingEvents?: string | undefined | null;
}

export function createPlaceholderEvents({
scheduledEvents,
curentEpochTimeMs,
monthsWithoutGeneratedUpcomingEvents,
}: {
scheduledEvents: ScheduledEvent[];
curentEpochTimeMs: number;
monthsWithoutGeneratedUpcomingEvents: ComputeUpcomingEventsOptions['monthsWithoutGeneratedUpcomingEvents'];
}) {
const meetupOfThisMonth = computeThirdWednesdayOfMonth(curentEpochTimeMs);
const isNowBeforeMeetup =
!isSameDay(curentEpochTimeMs, meetupOfThisMonth) &&
isBefore(curentEpochTimeMs, meetupOfThisMonth);

const placeholderMonths = isNowBeforeMeetup ? [0, 1, 2, 3] : [1, 2, 3];

const placeholderEvents = placeholderMonths
const { parsedMonths: monthsWithoutGeneratedEventsSet } = parseMonthsList(
monthsWithoutGeneratedUpcomingEvents
);

return placeholderMonths
.map((month): PlaceholderEvent => {
const date = computeThirdWednesdayOfMonth(
addMonths(new Date(curentEpochTimeMs), month)
Expand All @@ -112,11 +117,35 @@ export function computeUpcomingEvents(
})
.filter(
(placeholder) =>
!monthsWithoutGeneratedEventsSet.has(placeholder.date.getMonth()) &&
!scheduledEvents.some(
(scheduled) =>
isSameDay(scheduled.epochTimeMs, placeholder.epochTimeMs) // filter out placeholder if overlaps with a scheduled event
)
);
}

/**
* Given a list of scheduled events, returns a list of scheduled and placeholder
* events sorted by date in ascending order.
* @param scheduledUpcomingEvents Scheduled events
* @param curentEpochTimeMs built time epoch time i.e. Date.now().
* @param {ComputeUpcomingEventsOptions} options Additional options.
* @see {@link UpcomingEvent}
* @see {@link ComputeUpcomingEventsOptions}
*/
export function computeUpcomingEvents(
scheduledUpcomingEvents: MeetupEventType[] | undefined | null,
curentEpochTimeMs: number,
options?: ComputeUpcomingEventsOptions
): UpcomingEvent[] {
const scheduledEvents = computeScheduledEvents(scheduledUpcomingEvents);
const placeholderEvents = createPlaceholderEvents({
scheduledEvents,
curentEpochTimeMs,
monthsWithoutGeneratedUpcomingEvents:
options?.monthsWithoutGeneratedUpcomingEvents,
});

return (scheduledEvents as UpcomingEvent[])
.concat(placeholderEvents)
Expand Down
31 changes: 31 additions & 0 deletions src/utils/tests/meetup-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,36 @@ describe('meetup-events', () => {
]
`);
});

it('filters out the generated placeholders when monthsWithoutGeneratedUpcomingEvents is provided', () => {
const currentEpochTime = new Date('2024-03-09').getTime();
const [_, ...placeholders] = computeUpcomingEvents(
singleUpcomingEventList,
currentEpochTime,
{
monthsWithoutGeneratedUpcomingEvents: '04,6',
}
);

expect(placeholders).toHaveLength(1);
expect(new Date(placeholders[0].epochTimeMs).getMonth()).toBe(4);
});

it('does not filter out the scheduled events', () => {
const currentEpochTime = new Date('2024-03-09').getTime();
const events = computeUpcomingEvents(
singleUpcomingEventList,
currentEpochTime,
{
monthsWithoutGeneratedUpcomingEvents: '1,2,3,4,5,6,7,8,9,10,11,12',
}
);

expect(events).toHaveLength(1);
expect(events[0]).toHaveProperty('type', 'scheduled');
expect(new Date(events[0].epochTimeMs).getMonth()).toBe(
2 /* 0-based index */
);
});
});
});
69 changes: 69 additions & 0 deletions src/utils/tests/time.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { parseMonthsList, type ParseMonthsListOutput } from '../time';

describe('time', () => {
describe('parseMonthsWithoutGeneratedEvents', () => {
const createEmpyOutput = (): ParseMonthsListOutput => ({
parsedMonths: new Set(),
errors: [],
});

it.each([undefined, null, '', ' ', '\n'])(
'returns empty output (0 months & 0 errors) when input=%j',
(input) => {
expect(parseMonthsList(input)).toEqual(createEmpyOutput());
}
);

it.each([
{
input: '08',
parsedMonths: new Set([7]),
},
{
input: '8',
parsedMonths: new Set([7]),
},
{
input: '08,01,3,4',
parsedMonths: new Set([0, 2, 3, 7]),
},
{
input: ', 08 ,01,3,4,4,8,, , ,',
parsedMonths: new Set([0, 2, 3, 7]),
},
])(
'parses input=$input without errors and parsedMonths=$parsedMonths',
({ input, parsedMonths }) => {
const output = parseMonthsList(input);
expect(output.parsedMonths).toEqual(parsedMonths);
expect(output.errors).toHaveLength(0);
}
);

it.each([
{
input: '01,0,5,13,NaN',
expected: {
parsedMonths: new Set([0, 4]),
invalidMonths: ['0', '13', 'NaN'],
},
},
{
input: '-1',
expected: {
parsedMonths: new Set([]),
invalidMonths: ['-1'],
},
},
])(
'parses input=$input correctly when there is invalid input',
({ input, expected }) => {
const output = parseMonthsList(input);
expect(output.parsedMonths).toEqual(expected.parsedMonths);
expect(output.errors.map((r) => r.input).sort()).toEqual(
expected.invalidMonths.slice().sort()
);
}
);
});
});
56 changes: 56 additions & 0 deletions src/utils/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { parse as parseDate } from 'date-fns';

const monthFormat = 'LL';

export type ParseMonthsListOutput = {
/**
* Set of **0-based** index months.
*/
readonly parsedMonths: Set<number>;
readonly errors: { input: string; error: unknown }[];
};

/**
* Parses a comma separated list of months.
*
* Returns an output object containing a set of **0-based** index months.
*
* @param months a comma separated list of months.
* @returns {ParseMonthsListOutput} an output object
*/
export const parseMonthsList = (
months: string | undefined | null
): ParseMonthsListOutput => {
const formattedMonths =
months
?.trim()
.split(/\s*,\s*/)
.filter(Boolean) ?? [];
const referenceDate = new Date(new Date().getFullYear(), 0, 4); // A reference date with timezone-safe date of month.

const output: ParseMonthsListOutput = {
parsedMonths: new Set<number>(),
errors: [],
};

for (const formattedMonth of formattedMonths) {
try {
const parsedDate = parseDate(formattedMonth, monthFormat, referenceDate);

if (Number.isFinite(parsedDate.getTime())) {
output.parsedMonths.add(parsedDate.getMonth());
} else {
throw new Error(
`parseMonthsList: cannot parse date using format=${monthFormat}, input=${formattedMonth}`
);
}
} catch (err) {
output.errors.push({
input: formattedMonth,
error: err,
});
}
}

return output;
};

0 comments on commit 5d52561

Please sign in to comment.