Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 60 additions & 63 deletions netlify/functions/event-reminders-hourly/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,77 +197,74 @@ export default async (req: Request) => {
emoji: true,
},
},
...filteredList.reduce<Record<string, unknown>[]>(
(list, event) => {
const eventDate = DateTime.fromISO(event.startDateLocalized);
...filteredList.reduce<Record<string, unknown>[]>((list, event) => {
const eventDate = DateTime.fromISO(event.startDateLocalized);

const titleBlock: Record<string, unknown> = {
type: 'section',
const titleBlock: Record<string, unknown> = {
type: 'section',
text: {
type: 'mrkdwn',
text: `*${
event.title
}*\n<!date^${eventDate.toSeconds()}^{date_long_pretty} {time}|${eventDate.toFormat(
'EEEE, fff',
)}>`,
},
};

if (
event.eventJoinLink &&
event.eventJoinLink.substring(0, 4) === 'http'
) {
titleBlock.accessory = {
type: 'button',
text: {
type: 'mrkdwn',
text: `*${
event.title
}*\n<!date^${eventDate.toSeconds()}^{date_long_pretty} {time}|${eventDate.toFormat(
'EEEE, fff',
)}>`,
type: 'plain_text',
text: 'Join Event',
emoji: true,
},
value: `join_event_${event.id}`,
url: event.eventJoinLink,
action_id: 'button-join-event',
};
}

if (
event.eventJoinLink &&
event.eventJoinLink.substring(0, 4) === 'http'
) {
titleBlock.accessory = {
type: 'button',
text: {
type: 'plain_text',
text: 'Join Event',
emoji: true,
},
value: `join_event_${event.id}`,
url: event.eventJoinLink,
action_id: 'button-join-event',
};
}

return [
...list,
titleBlock,
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Location:* ${event.eventJoinLink}`,
},
return [
...list,
titleBlock,
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Location:* ${event.eventJoinLink}`,
},
...(event.eventZoomHostCode
? [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Host Code:* ${event.eventZoomHostCode}`,
},
},
...(event.eventZoomHostCode
? [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Host Code:* ${event.eventZoomHostCode}`,
},
]
: []),
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Announcement posted to:* <#${
event.eventSlackAnnouncementsChannelId ||
DEFAULT_SLACK_EVENT_CHANNEL
}>`,
},
},
{
type: 'divider',
},
]
: []),
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Announcement posted to:* <#${
event.eventSlackAnnouncementsChannelId ||
DEFAULT_SLACK_EVENT_CHANNEL
}>`,
},
];
},
[],
),
},
{
type: 'divider',
},
];
}, []),
],
};

Expand Down
4 changes: 3 additions & 1 deletion netlify/functions/join-coffee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export default async (req: Request) => {
console.log(`Joining ${day}: ${code}`);

const target =
day === 'tuesday' ? requireEnv('ZOOM_TUESDAYS') : requireEnv('ZOOM_THURSDAYS');
day === 'tuesday'
? requireEnv('ZOOM_TUESDAYS')
: requireEnv('ZOOM_THURSDAYS');

return Response.redirect(target, 302);
};
6 changes: 5 additions & 1 deletion netlify/functions/slack/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ class NetlifyReceiver implements Receiver {
});
}

const isValid = verifySlackRequest(rawBody, req.headers, this.signingSecret);
const isValid = verifySlackRequest(
rawBody,
req.headers,
this.signingSecret,
);
if (!isValid.valid) {
console.log('Failed validation:', isValid.reason);
return new Response(isValid.reason, { status: 401 });
Expand Down
172 changes: 86 additions & 86 deletions src/data/events.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,16 @@
'use server';

import { unstable_cache } from 'next/cache';
import { GraphQLClient, gql } from 'graphql-request';
import { DateTime } from 'luxon';
import { sanitizeHtml } from '@/util/sanitizeCmsData';
import { ics, google, outlook } from 'calendar-link';

const calendarsQuery = gql`
query getCalendars {
solspace_calendar {
calendars {
handle
}
}
}
`;

/**
* Defining the interface for the SolspaceCalendar object.
* @link https://docs.solspace.com/craft/calendar/v3/developer/graphql.html#calendar-interface
*/
interface SolspaceCalendar {
id: number;
uid: string;
name: string;
handle: string;
description: string;
color: string;
lighterColor: string;
darkerColor: string;
icsHash: string;
allowRepeatingEvents: boolean;
}
interface SolspaceEventResponse {
id: number | string;
export interface EventItem {
id: string;
title: string;
startDateLocalized: string;
endDateLocalized: string;
eventCalendarDescription: string;
}
export interface EventItem extends SolspaceEventResponse {
eventCalendarLinks: {
google: string;
outlook: string;
Expand All @@ -48,92 +19,121 @@ export interface EventItem extends SolspaceEventResponse {
}
export type EventsResponse = Array<EventItem>;

function createEventsQuery(
calendars: Pick<SolspaceCalendar, 'handle'>[],
interface GoogleCalendarEvent {
id: string;
summary?: string;
description?: string;
start: { dateTime?: string; date?: string };
end: { dateTime?: string; date?: string };
status?: string;
}

interface GoogleCalendarResponse {
items: GoogleCalendarEvent[];
}

async function fetchCalendarEvents(
calendarId: string,
apiKey: string,
rangeStart: string,
rangeEnd: string,
limit: number,
) {
return gql`
query getEvents {
solspace_calendar {
events(rangeStart: "${rangeStart}", rangeEnd: "${rangeEnd}", limit: ${limit}) {
id
title
startDateLocalized
endDateLocalized
${calendars.map(
({ handle }) => `
... on ${handle}_Event {
eventCalendarDescription
id
}
`,
)}
}
}
): Promise<GoogleCalendarEvent[]> {
const params = new URLSearchParams({
key: apiKey,
timeMin: rangeStart,
timeMax: rangeEnd,
maxResults: String(limit),
singleEvents: 'true',
orderBy: 'startTime',
});

const url = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?${params}`;
const res = await fetch(url, { next: { revalidate: 43200 } });

if (!res.ok) {
console.error(
`Failed to fetch calendar ${calendarId}: ${res.status} ${res.statusText}`,
);
return [];
}
`;

const data: GoogleCalendarResponse = await res.json();
return data.items ?? [];
}

export const getEvents = unstable_cache(
async ({ limit }: { limit: number }): Promise<EventsResponse> => {
const rangeStart = DateTime.now().toUTC().set({ hour: 0 }).toISO();
const rangeStart = DateTime.now().toUTC().set({ hour: 0 }).toISO()!;
const rangeEnd = DateTime.now()
.toUTC()
.set({ hour: 0 })
.plus({ days: 30 })
.toISO();
.toISO()!;

if (!(process.env.CMS_URL && process.env.CMS_TOKEN)) {
const apiKey = process.env.GOOGLE_CALENDAR;
const calendarIds = process.env.GOOGLE_CALENDAR_IDS;

if (!apiKey || !calendarIds) {
const fakeData = await import('./mocks/events');
return fakeData.createEventsData({ limit, rangeEnd, rangeStart });
}

const graphQLClient = new GraphQLClient(`${process.env.CMS_URL}/api`, {
headers: {
Authorization: `bearer ${process.env.CMS_TOKEN}`,
},
});
const ids = calendarIds
.split(',')
.map((id) => id.trim())
.filter(Boolean);

try {
const {
solspace_calendar: { calendars },
} = await graphQLClient.request<{
solspace_calendar: { calendars: Pick<SolspaceCalendar, 'handle'>[] };
}>(calendarsQuery);

const {
solspace_calendar: { events },
} = await graphQLClient.request<{
solspace_calendar: { events: EventsResponse };
}>(createEventsQuery(calendars, rangeStart, rangeEnd, limit));
const allEventsNested = await Promise.all(
ids.map((calendarId) =>
fetchCalendarEvents(calendarId, apiKey, rangeStart, rangeEnd, limit),
),
);

const allEvents = allEventsNested
.flat()
.filter((e) => e.status !== 'cancelled')
.sort((a, b) => {
const aStart = a.start.dateTime ?? a.start.date ?? '';
const bStart = b.start.dateTime ?? b.start.date ?? '';
return aStart.localeCompare(bStart);
})
.slice(0, limit);

return await Promise.all(
events.map(async (event) => {
const sanitizedDescription = await sanitizeHtml(
event.eventCalendarDescription,
);
allEvents.map(async (event) => {
const startDate = event.start.dateTime ?? event.start.date ?? '';
const endDate = event.end.dateTime ?? event.end.date ?? '';
const title = event.summary ?? 'Virtual Coffee Event';
const rawDescription = event.description ?? '';

const sanitizedDescription = await sanitizeHtml(rawDescription);

const calendarLinkGoogle = google({
title: event.title,
start: event.startDateLocalized,
end: event.endDateLocalized,
title,
start: startDate,
end: endDate,
description: sanitizedDescription,
});
const calendarLinkOutlook = outlook({
title: event.title,
start: event.startDateLocalized,
end: event.endDateLocalized,
title,
start: startDate,
end: endDate,
description: sanitizedDescription,
});
const calendarLinkIcs = ics({
title: event.title,
start: event.startDateLocalized,
end: event.endDateLocalized,
title,
start: startDate,
end: endDate,
description: sanitizedDescription,
});

return {
...event,
id: event.id,
title,
startDateLocalized: startDate,
endDateLocalized: endDate,
eventCalendarDescription: sanitizedDescription,
eventCalendarLinks: {
google: calendarLinkGoogle,
Expand Down