From 62b8a93caab409aaf54cd2feba7000335c96c8d8 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Fri, 11 Jul 2025 17:44:07 +0200 Subject: [PATCH] luma events (#37046) integrate luma api to events page --- ...2025-04-07-fauna-to-supabase-migration.mdx | 41 ------ apps/www/app/api-v2/luma-events/route.tsx | 101 +++++++++++++ apps/www/components/Events/EventsFilters.tsx | 22 ++- apps/www/pages/events/index.tsx | 136 ++++++++++++++---- turbo.json | 3 +- 5 files changed, 233 insertions(+), 70 deletions(-) delete mode 100644 apps/www/_events/2025-04-07-fauna-to-supabase-migration.mdx create mode 100644 apps/www/app/api-v2/luma-events/route.tsx diff --git a/apps/www/_events/2025-04-07-fauna-to-supabase-migration.mdx b/apps/www/_events/2025-04-07-fauna-to-supabase-migration.mdx deleted file mode 100644 index acd9922aefc..00000000000 --- a/apps/www/_events/2025-04-07-fauna-to-supabase-migration.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: 'Moving from Fauna to Supabase, a Live Tutorial' -meta_title: 'Moving from Fauna to Supabase, a Live Tutorial' -subtitle: 'A live tutorial that shows you how to move your data and applications to Supabase from Fauna' -meta_description: 'A live tutorial that shows you how to move your data and applications to Supabase from Fauna' -type: 'webinar' -onDemand: true -date: '2025-04-08T09:00:00.000-07:00' -timezone: 'America/Los_Angeles' -duration: '45 mins' -company: - { - name: 'Bree', - website_url: 'https://www.trybree.com', - logo: '/images/events/webinars/fauna-to-supabase-migration/bree.svg', - logo_light: '/images/events/webinars/fauna-to-supabase-migration/bree-light.svg', - } -categories: - - webinar -main_cta: - { - url: 'https://zoom.us/webinar/register/WN_26XTfO35SAC81jjS1ulWJw?amp_device_id=1bd3c92f-c97a-45ad-8aab-a508f950b536', - target: '_blank', - label: 'Register now', - } -speakers: 'caruso,ryanxjhan' ---- - -## Keep Your Fauna Applications by Migrating to Supabase - -The deadline to move off Fauna is approaching fast. In this 45-minute session, you'll learn how to move your data to Supabase, modify your FQL queries for SQL, and optimize your deployment. Bree has navigated this transition from Fauna to Supabase already and will be on hand to provide advice and best practices. This is a hands-on demonstration and will include plenty of time for Q&A. - -### Agenda - -- Concepts in Supabase and how they map to concepts in Fauna -- Using Postgres as a document store with JSONB -- Migrating your data -- Building queries using PostgREST and GraphQL -- Q&A - -Join us live to participate in the Q&A afterwards. Can’t make it to the event? Don’t worry, we'll send you a link to the recording. diff --git a/apps/www/app/api-v2/luma-events/route.tsx b/apps/www/app/api-v2/luma-events/route.tsx new file mode 100644 index 00000000000..db14d1a1323 --- /dev/null +++ b/apps/www/app/api-v2/luma-events/route.tsx @@ -0,0 +1,101 @@ +import { NextRequest, NextResponse } from 'next/server' + +export interface LumaEvent { + api_id: string + calendar_api_id: string + name: string + description: string + start_at: string + end_at: string + timezone: string + city: string + country: string + url: string + visibility: string +} + +interface LumaResponse { + entries: { event: LumaEvent }[] + has_more: boolean + next_cursor?: string +} + +export async function GET(request: NextRequest) { + try { + const lumaApiKey = process.env.LUMA_API_KEY + + if (!lumaApiKey) { + console.error('LUMA_API_KEY environment variable is not set') + return NextResponse.json({ error: 'API configuration error' }, { status: 500 }) + } + + // Extract query parameters from the request + const { searchParams } = new URL(request.url) + const after = searchParams.get('after') + const before = searchParams.get('before') + + // Build the Luma API URL with query parameters + const lumaUrl = new URL('https://public-api.lu.ma/public/v1/calendar/list-events') + + if (after) { + lumaUrl.searchParams.append('after', after) + } + if (before) { + lumaUrl.searchParams.append('before', before) + } + + // Fetch events from Luma API + const response = await fetch(lumaUrl.toString(), { + method: 'GET', + headers: { + accept: 'application/json', + 'x-luma-api-key': lumaApiKey, + }, + }) + + if (!response.ok) { + console.error('Luma API error:', response.status, response.statusText) + return NextResponse.json( + { + error: 'Failed to fetch events from Luma', + status: response.status, + }, + { status: response.status } + ) + } + + const data: LumaResponse = await response.json() + + const launchWeekEvents = data.entries + .filter(({ event }: { event: LumaEvent }) => event.visibility === 'public') + .map(({ event }: { event: LumaEvent }) => ({ + start_at: event.start_at, + end_at: event.end_at, + name: event.name, + city: event.city, + country: event.country, + url: event.url, + timezone: event.timezone, + })) + .sort((a, b) => new Date(a.start_at).getTime() - new Date(b.start_at).getTime()) + + return NextResponse.json({ + success: true, + events: launchWeekEvents, + total: launchWeekEvents.length, + filters: { + after, + before, + }, + }) + } catch (error) { + console.error('Error fetching meetups from Luma:', error) + return NextResponse.json( + { + error: 'Internal server error', + message: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ) + } +} diff --git a/apps/www/components/Events/EventsFilters.tsx b/apps/www/components/Events/EventsFilters.tsx index c8abd1991ad..006af0b7f77 100644 --- a/apps/www/components/Events/EventsFilters.tsx +++ b/apps/www/components/Events/EventsFilters.tsx @@ -3,9 +3,10 @@ import { AnimatePresence, motion } from 'framer-motion' import { startCase } from 'lodash' import { useSearchParams } from 'next/navigation' import { useRouter } from 'next/router' +import Link from 'next/link' import { useEffect, useRef, useState } from 'react' import { useKey } from 'react-use' -import type PostTypes from '~/types/post' +import type PostTypes from 'types/post' import { Button, @@ -23,6 +24,7 @@ interface Props { events?: PostTypes[] setEvents: (posts: any) => void categories: { [key: string]: number } + onDemandEvents?: PostTypes[] } /** @@ -30,7 +32,7 @@ interface Props { * search via category and reset q param if present */ -function EventFilters({ allEvents, setEvents, categories }: Props) { +function EventFilters({ allEvents, setEvents, categories, onDemandEvents }: Props) { const inputRef = useRef(null) const [category, setCategory] = useState('all') const [searchTerm, setSearchTerm] = useState('') @@ -207,6 +209,22 @@ function EventFilters({ allEvents, setEvents, categories }: Props) { {category === 'all' ? 'All' : startCase(category.replaceAll('-', ' '))}{' '} ))} + {!!onDemandEvents?.length && ( + + )} {!showSearchInput && ( diff --git a/apps/www/pages/events/index.tsx b/apps/www/pages/events/index.tsx index 9b5f4bfef09..fb4036d6202 100644 --- a/apps/www/pages/events/index.tsx +++ b/apps/www/pages/events/index.tsx @@ -1,17 +1,18 @@ -import { useState } from 'react' +import { useState, useEffect, useMemo } from 'react' import { useRouter } from 'next/router' import { NextSeo } from 'next-seo' -import { getSortedPosts } from '~/lib/posts' -import supabase from '~/lib/supabase' +import { getSortedPosts } from 'lib/posts' +import supabase from 'lib/supabase' import { cn } from 'ui' -import DefaultLayout from '~/components/Layouts/Default' -import EventListItem from '~/components/Events/EventListItem' -import EventsFilters from '~/components/Events/EventsFilters' -import SectionContainer from '~/components/Layouts/SectionContainer' +import DefaultLayout from 'components/Layouts/Default' +import EventListItem from 'components/Events/EventListItem' +import EventsFilters from 'components/Events/EventsFilters' +import SectionContainer from 'components/Layouts/SectionContainer' -import type BlogPost from '~/types/post' +import type BlogPost from 'types/post' +import type { LumaEvent } from 'app/api-v2/luma-events/route' interface Props { events: BlogPost[] @@ -19,10 +20,91 @@ interface Props { categories: { [key: string]: number } } -function Events({ events: allEvents, onDemandEvents, categories }: Props) { - const [events, setEvents] = useState(allEvents) +export default function Events({ + events: staticEvents, + onDemandEvents, + categories: staticCategories, +}: Props) { + const [lumaEvents, setLumaEvents] = useState([]) + const [isLoadingLuma, setIsLoadingLuma] = useState(true) + const [filteredEvents, setFilteredEvents] = useState([]) const router = useRouter() + // Fetch Luma events on client-side to avoid serverless maximum size limit error: https://vercel.com/guides/troubleshooting-function-250mb-limit + useEffect(() => { + const fetchLumaEvents = async () => { + try { + const afterDate = new Date().toISOString() + const url = new URL('/api-v2/luma-events', window.location.origin) + url.searchParams.append('after', afterDate) + + const res = await fetch(url.toString()) + const data = await res.json() + + if (data.success) { + const transformedEvents = data.events.map((event: LumaEvent) => { + let categories = [] + if (event.name.toLowerCase().includes('meetup')) categories.push('meetup') + + return { + slug: '', + type: 'event', + title: event?.name, + date: event?.start_at, + description: '', + thumb: '', + path: '', + url: event?.url ?? '', + tags: categories, + categories, + timezone: event?.timezone ?? 'America/Los_Angeles', + disable_page_build: true, + link: { + href: event?.url ?? '#', + target: '_blank', + }, + } as BlogPost + }) + setLumaEvents(transformedEvents) + } + } catch (error) { + console.error('Error fetching Luma events:', error) + } finally { + setIsLoadingLuma(false) + } + } + + fetchLumaEvents() + }, []) + + // Combine static and Luma events + const allEvents = useMemo(() => { + const combined = [...staticEvents, ...lumaEvents] + return combined.filter((event: BlogPost) => + event.end_date ? new Date(event.end_date!) >= new Date() : new Date(event.date!) >= new Date() + ) + }, [staticEvents, lumaEvents]) + + // Initialize filtered events when allEvents changes + useEffect(() => { + setFilteredEvents(allEvents) + }, [allEvents]) + + // Recalculate categories with Luma events included + const categories = useMemo(() => { + const updatedCategories = { ...staticCategories } + + lumaEvents.forEach((event) => { + updatedCategories.all = (updatedCategories.all || 0) + 1 + + event.categories?.forEach((category) => { + updatedCategories[category] = (updatedCategories[category] || 0) + 1 + }) + }) + + return updatedCategories + }, [staticCategories, lumaEvents]) + const meta_title = 'Supabase Events: webinars, talks, hackathons, and meetups' const meta_description = 'Join Supabase and the open-source community at the upcoming events.' @@ -59,19 +141,19 @@ function Events({ events: allEvents, onDemandEvents, categories }: Props) { - -
    - {events?.length ? ( - events + {filteredEvents?.length ? ( + filteredEvents ?.sort((a, b) => new Date(a.date!).getTime() - new Date(b.date!).getTime()) .map((event: BlogPost, idx: number) => (
    )) ) : ( -

    - No results found -

    +
    + {isLoadingLuma ? ( +
    +

    Loading events...

    +
    + ) : ( +

    No results found.

    + )} +
    )} -
+
- +

On Demand

Replay selected events on your schedule

@@ -164,10 +252,8 @@ export async function getStaticProps() { const categories = upcomingEvents.reduce( (acc: { [key: string]: number }, event: BlogPost) => { - // Increment the 'all' counter acc.all = (acc.all || 0) + 1 - // Increment the counter for each category event.categories?.forEach((category) => { acc[category] = (acc[category] || 0) + 1 }) @@ -185,5 +271,3 @@ export async function getStaticProps() { }, } } - -export default Events diff --git a/turbo.json b/turbo.json index f9094a0b43b..b1484553135 100644 --- a/turbo.json +++ b/turbo.json @@ -150,7 +150,8 @@ "AWS_SECRET_ACCESS_KEY", "FORCE_ASSET_CDN", "ASSET_CDN_S3_ENDPOINT", - "SITE_NAME" + "SITE_NAME", + "LUMA_API_KEY" ], "outputs": [".next/**", "!.next/cache/**", ".contentlayer/**"] },