committed by
GitHub
parent
9fc9dc593a
commit
62b8a93caa
@@ -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.
|
||||
101
apps/www/app/api-v2/luma-events/route.tsx
Normal file
101
apps/www/app/api-v2/luma-events/route.tsx
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/**"]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user