diff --git a/netlify/functions/event-reminders-hourly/index.ts b/netlify/functions/event-reminders-hourly/index.ts index d1f87f28..186a6635 100644 --- a/netlify/functions/event-reminders-hourly/index.ts +++ b/netlify/functions/event-reminders-hourly/index.ts @@ -197,77 +197,74 @@ export default async (req: Request) => { emoji: true, }, }, - ...filteredList.reduce[]>( - (list, event) => { - const eventDate = DateTime.fromISO(event.startDateLocalized); + ...filteredList.reduce[]>((list, event) => { + const eventDate = DateTime.fromISO(event.startDateLocalized); - const titleBlock: Record = { - type: 'section', + const titleBlock: Record = { + type: 'section', + text: { + type: 'mrkdwn', + text: `*${ + event.title + }*\n`, + }, + }; + + if ( + event.eventJoinLink && + event.eventJoinLink.substring(0, 4) === 'http' + ) { + titleBlock.accessory = { + type: 'button', text: { - type: 'mrkdwn', - text: `*${ - event.title - }*\n`, + 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', + }, + ]; + }, []), ], }; diff --git a/netlify/functions/join-coffee.ts b/netlify/functions/join-coffee.ts index 1758dace..14fb3da6 100644 --- a/netlify/functions/join-coffee.ts +++ b/netlify/functions/join-coffee.ts @@ -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); }; diff --git a/netlify/functions/slack/index.ts b/netlify/functions/slack/index.ts index af521086..702a4c52 100644 --- a/netlify/functions/slack/index.ts +++ b/netlify/functions/slack/index.ts @@ -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 }); diff --git a/src/data/events.ts b/src/data/events.ts index a6b319ca..610af78f 100644 --- a/src/data/events.ts +++ b/src/data/events.ts @@ -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; @@ -48,92 +19,121 @@ export interface EventItem extends SolspaceEventResponse { } export type EventsResponse = Array; -function createEventsQuery( - calendars: Pick[], +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 { + 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 => { - 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[] }; - }>(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,