|
1 | 1 | import * as Sentry from '@sentry/nextjs' |
2 | | -import { NextRequest, NextResponse } from 'next/server' |
3 | 2 | import { DEFAULT_META_DESCRIPTION } from '~/lib/constants' |
| 3 | +import { NextRequest, NextResponse } from 'next/server' |
4 | 4 |
|
5 | 5 | export interface LumaGeoAddressJson { |
6 | 6 | city: string |
@@ -58,81 +58,90 @@ interface LumaResponse { |
58 | 58 | next_cursor?: string |
59 | 59 | } |
60 | 60 |
|
| 61 | +export type LumaCalendar = 'community' | 'hackathon' |
| 62 | + |
| 63 | +async function fetchLumaCalendar( |
| 64 | + apiKey: string, |
| 65 | + calendar: LumaCalendar, |
| 66 | + after: string | null, |
| 67 | + before: string | null |
| 68 | +) { |
| 69 | + const lumaUrl = new URL('https://public-api.lu.ma/public/v1/calendar/list-events') |
| 70 | + if (after) lumaUrl.searchParams.append('after', after) |
| 71 | + if (before) lumaUrl.searchParams.append('before', before) |
| 72 | + |
| 73 | + const response = await fetch(lumaUrl.toString(), { |
| 74 | + method: 'GET', |
| 75 | + headers: { |
| 76 | + accept: 'application/json', |
| 77 | + 'x-luma-api-key': apiKey, |
| 78 | + }, |
| 79 | + }) |
| 80 | + |
| 81 | + if (!response.ok) { |
| 82 | + throw new Error(`Luma API error (${calendar}): ${response.status} ${response.statusText}`) |
| 83 | + } |
| 84 | + |
| 85 | + const data: LumaResponse = await response.json() |
| 86 | + |
| 87 | + return data.entries |
| 88 | + .filter(({ event }) => event.visibility === 'public') |
| 89 | + .map(({ event }) => ({ |
| 90 | + id: event.api_id, |
| 91 | + calendar, |
| 92 | + start_at: event.start_at, |
| 93 | + end_at: event.end_at, |
| 94 | + name: event.name, |
| 95 | + city: event.geo_address_json?.city, |
| 96 | + country: event.geo_address_json?.country, |
| 97 | + url: event.url, |
| 98 | + timezone: event.timezone, |
| 99 | + cover_url: event.cover_url, |
| 100 | + description: event.description, |
| 101 | + hosts: event.hosts || [], |
| 102 | + })) |
| 103 | +} |
| 104 | + |
61 | 105 | export async function GET(request: NextRequest) { |
62 | 106 | try { |
63 | | - const lumaApiKey = process.env.LUMA_API_KEY |
| 107 | + const communityKey = process.env.LUMA_API_KEY |
| 108 | + const hackathonKey = process.env.LUMA_HACKATHONS_API_KEY |
64 | 109 |
|
65 | | - if (!lumaApiKey) { |
66 | | - console.error('LUMA_API_KEY environment variable is not set') |
| 110 | + if (!communityKey && !hackathonKey) { |
| 111 | + console.error('No Luma API keys configured (LUMA_API_KEY / LUMA_HACKATHONS_API_KEY)') |
67 | 112 | return NextResponse.json({ error: 'API configuration error' }, { status: 500 }) |
68 | 113 | } |
69 | 114 |
|
70 | | - // Extract query parameters from the request |
71 | 115 | const { searchParams } = new URL(request.url) |
72 | 116 | const after = searchParams.get('after') |
73 | 117 | const before = searchParams.get('before') |
74 | 118 |
|
75 | | - // Build the Luma API URL with query parameters |
76 | | - const lumaUrl = new URL('https://public-api.lu.ma/public/v1/calendar/list-events') |
| 119 | + const calendarFetches: Array<Promise<Awaited<ReturnType<typeof fetchLumaCalendar>>>> = [] |
| 120 | + if (communityKey) |
| 121 | + calendarFetches.push(fetchLumaCalendar(communityKey, 'community', after, before)) |
| 122 | + if (hackathonKey) |
| 123 | + calendarFetches.push(fetchLumaCalendar(hackathonKey, 'hackathon', after, before)) |
77 | 124 |
|
78 | | - if (after) { |
79 | | - lumaUrl.searchParams.append('after', after) |
80 | | - } |
81 | | - if (before) { |
82 | | - lumaUrl.searchParams.append('before', before) |
83 | | - } |
| 125 | + const results = await Promise.allSettled(calendarFetches) |
84 | 126 |
|
85 | | - // Fetch events from Luma API |
86 | | - const response = await fetch(lumaUrl.toString(), { |
87 | | - method: 'GET', |
88 | | - headers: { |
89 | | - accept: 'application/json', |
90 | | - 'x-luma-api-key': lumaApiKey, |
91 | | - }, |
92 | | - }) |
93 | | - |
94 | | - if (!response.ok) { |
95 | | - console.error('Luma API error:', response.status, response.statusText) |
96 | | - return NextResponse.json( |
97 | | - { |
98 | | - error: 'Failed to fetch events from Luma', |
99 | | - status: response.status, |
100 | | - }, |
101 | | - { status: response.status } |
102 | | - ) |
103 | | - } |
104 | | - |
105 | | - const data: LumaResponse = await response.json() |
106 | | - |
107 | | - const launchWeekEvents = data.entries |
108 | | - .filter(({ event }: { event: LumaPayloadEvent }) => event.visibility === 'public') |
109 | | - .map(({ event }: { event: LumaPayloadEvent }) => ({ |
110 | | - id: event.api_id, |
111 | | - start_at: event.start_at, |
112 | | - end_at: event.end_at, |
113 | | - name: event.name, |
114 | | - city: event.geo_address_json?.city, |
115 | | - country: event.geo_address_json?.country, |
116 | | - url: event.url, |
117 | | - timezone: event.timezone, |
118 | | - cover_url: event.cover_url, |
119 | | - description: event.description, |
120 | | - hosts: event.hosts || [], |
121 | | - })) |
| 127 | + const events = results |
| 128 | + .flatMap((result) => { |
| 129 | + if (result.status === 'fulfilled') return result.value |
| 130 | + Sentry.captureException(result.reason) |
| 131 | + console.error(result.reason) |
| 132 | + return [] |
| 133 | + }) |
122 | 134 | .sort((a, b) => new Date(a.start_at).getTime() - new Date(b.start_at).getTime()) |
123 | 135 |
|
124 | 136 | return NextResponse.json({ |
125 | 137 | success: true, |
126 | | - events: launchWeekEvents, |
127 | | - total: launchWeekEvents.length, |
128 | | - filters: { |
129 | | - after, |
130 | | - before, |
131 | | - }, |
| 138 | + events, |
| 139 | + total: events.length, |
| 140 | + filters: { after, before }, |
132 | 141 | }) |
133 | 142 | } catch (error) { |
134 | 143 | Sentry.captureException(error) |
135 | | - console.error('Error fetching meetups from Luma:', error) |
| 144 | + console.error('Error fetching events from Luma:', error) |
136 | 145 | return NextResponse.json( |
137 | 146 | { |
138 | 147 | error: 'Internal server error', |
|
0 commit comments