luma events (#37046)

integrate luma api to events page
This commit is contained in:
Francesco Sansalvadore
2025-07-11 17:44:07 +02:00
committed by GitHub
parent 9fc9dc593a
commit 62b8a93caa
5 changed files with 233 additions and 70 deletions

View File

@@ -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. Cant make it to the event? Dont worry, we'll send you a link to the recording.

View File

@@ -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 }
)
}
}

View File

@@ -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<HTMLInputElement>(null)
const [category, setCategory] = useState<string>('all')
const [searchTerm, setSearchTerm] = useState<string>('')
@@ -207,6 +209,22 @@ function EventFilters({ allEvents, setEvents, categories }: Props) {
{category === 'all' ? 'All' : startCase(category.replaceAll('-', ' '))}{' '}
</Button>
))}
{!!onDemandEvents?.length && (
<Button
key="on-demand"
type="outline"
size={is2XL ? 'tiny' : 'small'}
className="rounded-full"
iconRight={
<span className="text-foreground-lighter text-xs flex items-center h-[16px] self-center">
{onDemandEvents.length}
</span>
}
asChild
>
<Link href="#on-demand">On Demand</Link>
</Button>
)}
</div>
</AnimatePresence>
{!showSearchInput && (

View File

@@ -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<BlogPost[]>([])
const [isLoadingLuma, setIsLoadingLuma] = useState(true)
const [filteredEvents, setFilteredEvents] = useState<BlogPost[]>([])
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) {
<SectionContainer className="!py-0">
<EventsFilters
allEvents={allEvents}
events={events}
setEvents={setEvents}
onDemandEvents={onDemandEvents}
events={filteredEvents}
setEvents={setFilteredEvents}
categories={categories}
/>
<ol
<div
className={cn(
'grid -mx-2 sm:-mx-4 py-6 lg:py-6 grid-cols-1',
!events?.length && 'mx-0 sm:mx-0'
!filteredEvents?.length && 'mx-0 sm:mx-0'
)}
>
{events?.length ? (
events
{filteredEvents?.length ? (
filteredEvents
?.sort((a, b) => new Date(a.date!).getTime() - new Date(b.date!).getTime())
.map((event: BlogPost, idx: number) => (
<div
@@ -82,13 +164,19 @@ function Events({ events: allEvents, onDemandEvents, categories }: Props) {
</div>
))
) : (
<p className="text-sm py-2 sm:py-4 text-lighter col-span-full italic opacity-0 !scale-100 animate-fade-in">
No results found
</p>
<div className="flex flex-col items-center justify-center py-12">
{isLoadingLuma ? (
<div className="text-center">
<p className="text-foreground-muted">Loading events...</p>
</div>
) : (
<p className="text-foreground-muted">No results found.</p>
)}
</div>
)}
</ol>
</div>
</SectionContainer>
<SectionContainer>
<SectionContainer id="on-demand">
<div className="pt-8 border-t">
<h2 className="h3">On Demand</h2>
<p className="text-foreground-light">Replay selected events on your schedule</p>
@@ -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

View File

@@ -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/**"]
},