diff --git a/apps/www/_events/2025-11-20-supabase-agency-webinar.mdx b/apps/www/_events/2025-11-20-supabase-agency-webinar.mdx index 25e633aeb1f..d3e034d306b 100644 --- a/apps/www/_events/2025-11-20-supabase-agency-webinar.mdx +++ b/apps/www/_events/2025-11-20-supabase-agency-webinar.mdx @@ -8,7 +8,7 @@ meta_description: >- Learn how leading agencies use Postgres expertise and strategic services to command premium pricing while competitors race to the bottom on AI-built MVPs. type: webinar -onDemand: false +onDemand: true date: '2025-11-20T07:00:00.000-08:00' timezone: America/Los_Angeles duration: 45 mins diff --git a/apps/www/app/events/context.tsx b/apps/www/app/events/context.tsx index 5f847f6afa7..eddb07b5d23 100644 --- a/apps/www/app/events/context.tsx +++ b/apps/www/app/events/context.tsx @@ -2,13 +2,14 @@ import { createContext, useContext, useState, useEffect, useMemo, ReactNode } from 'react' import { SupabaseEvent, SUPABASE_HOST } from '~/lib/eventsTypes' -import { cover } from 'three/src/extras/TextureUtils.js' interface EventsContextValue { // Events data allEvents: SupabaseEvent[] filteredEvents: SupabaseEvent[] + filteredOnDemandEvents: SupabaseEvent[] staticEvents: SupabaseEvent[] + onDemandEvents: SupabaseEvent[] lumaEvents: SupabaseEvent[] featuredEvent: SupabaseEvent | undefined @@ -30,9 +31,10 @@ const EventsContext = createContext(undefined) interface EventsProviderProps { children: ReactNode staticEvents: SupabaseEvent[] + onDemandEvents: SupabaseEvent[] } -export function EventsProvider({ children, staticEvents }: EventsProviderProps) { +export function EventsProvider({ children, staticEvents, onDemandEvents }: EventsProviderProps) { const [lumaEvents, setLumaEvents] = useState([]) const [isLoading, setIsLoading] = useState(true) const [searchQuery, setSearchQuery] = useState('') @@ -99,9 +101,12 @@ export function EventsProvider({ children, staticEvents }: EventsProviderProps) }, [staticEvents, lumaEvents]) // Calculate categories with counts + // - Webinar: count only upcoming webinars (not on-demand) + // - On-demand: count only on-demand events const categories = useMemo(() => { const categoryCounts: { [key: string]: number } = { all: 0 } + // Count upcoming events (excluding on-demand) allEvents.forEach((event) => { categoryCounts.all += 1 @@ -110,8 +115,15 @@ export function EventsProvider({ children, staticEvents }: EventsProviderProps) }) }) + // Count on-demand events separately + onDemandEvents.forEach((event) => { + categoryCounts.all += 1 + // Add to 'on-demand' category instead of 'webinar' + categoryCounts['on-demand'] = (categoryCounts['on-demand'] || 0) + 1 + }) + return categoryCounts - }, [allEvents]) + }, [allEvents, onDemandEvents]) // Toggle category selection const toggleCategory = (category: string) => { @@ -135,8 +147,13 @@ export function EventsProvider({ children, staticEvents }: EventsProviderProps) }) } - // Filter events by search query and category + // Filter upcoming events by search query and category const filteredEvents = useMemo(() => { + // If 'on-demand' is selected, don't show upcoming events + if (selectedCategories.includes('on-demand') && !selectedCategories.includes('all')) { + return [] + } + let filtered = allEvents // Filter by categories (multiple selection) @@ -167,6 +184,31 @@ export function EventsProvider({ children, staticEvents }: EventsProviderProps) }) }, [allEvents, selectedCategories, searchQuery]) + // Filter on-demand events separately by search query and category + const filteredOnDemandEvents = useMemo(() => { + // If specific categories are selected (not 'all' and not 'on-demand'), don't show on-demand events + if (!selectedCategories.includes('all') && !selectedCategories.includes('on-demand')) { + return [] + } + + let filtered = onDemandEvents + + // Filter by search query + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase() + filtered = filtered.filter((event) => { + const titleMatch = event.title?.toLowerCase().includes(query) + const descriptionMatch = event.description?.toLowerCase().includes(query) + const locationMatch = event.location?.toLowerCase().includes(query) + const tagsMatch = event.tags?.some((tag) => tag.toLowerCase().includes(query)) + + return titleMatch || descriptionMatch || locationMatch || tagsMatch + }) + } + + return filtered + }, [onDemandEvents, selectedCategories, searchQuery]) + // Featured event: nearest upcoming event, or if none, the most recent past event const featuredEvent = useMemo(() => { if (allEvents.length === 0) return undefined @@ -208,7 +250,9 @@ export function EventsProvider({ children, staticEvents }: EventsProviderProps) const value: EventsContextValue = { allEvents, filteredEvents, + filteredOnDemandEvents, staticEvents, + onDemandEvents, lumaEvents, isLoading, searchQuery, diff --git a/apps/www/app/events/page.tsx b/apps/www/app/events/page.tsx index b2e202162c9..0d674c52f32 100644 --- a/apps/www/app/events/page.tsx +++ b/apps/www/app/events/page.tsx @@ -10,7 +10,7 @@ export const metadata: Metadata = { export default async function EventsPage() { // This needs to be server-side as we use FS api. - const staticEvents = await getStaticEvents() + const { upcomingEvents, onDemandEvents } = await getStaticEvents() - return + return } diff --git a/apps/www/components/Events/new/EventBanner.tsx b/apps/www/components/Events/new/EventBanner.tsx index 66aae718c2f..2b91e00114b 100644 --- a/apps/www/components/Events/new/EventBanner.tsx +++ b/apps/www/components/Events/new/EventBanner.tsx @@ -3,7 +3,7 @@ import { CalendarIcon, MapPinIcon } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' -import { cn, Button } from 'ui' +import { cn, Button, Badge } from 'ui' import { useEvents } from '~/app/events/context' import { formatHosts } from '~/lib/eventsUtils' @@ -17,7 +17,9 @@ export function EventBanner() { if (!featuredEvent) return null return ( -
+
@@ -140,14 +142,20 @@ const LocationWidget = ({ location }: { location?: string }) => { const CoverImage = ({ url }: { url?: string }) => { if (!url) return ( -
+
+ + Upcoming +
) return (
Event Cover + + Upcoming +
) } diff --git a/apps/www/components/Events/new/EventClientRenderer.tsx b/apps/www/components/Events/new/EventClientRenderer.tsx index 21568b7fdd4..248c0b852ee 100644 --- a/apps/www/components/Events/new/EventClientRenderer.tsx +++ b/apps/www/components/Events/new/EventClientRenderer.tsx @@ -7,9 +7,15 @@ import { EventBanner } from '~/components/Events/new/EventBanner' import { EventsProvider } from '~/app/events/context' import { EventGallery } from './EventGallery' -export function EventClientRenderer({ staticEvents }: { staticEvents: SupabaseEvent[] }) { +export function EventClientRenderer({ + staticEvents, + onDemandEvents, +}: { + staticEvents: SupabaseEvent[] + onDemandEvents: SupabaseEvent[] +}) { return ( - +

diff --git a/apps/www/components/Events/new/EventList.tsx b/apps/www/components/Events/new/EventList.tsx index 83fe3f71d13..41e4033274f 100644 --- a/apps/www/components/Events/new/EventList.tsx +++ b/apps/www/components/Events/new/EventList.tsx @@ -16,7 +16,7 @@ const CATEGORIES_FILTERS = [ ] export function EventList() { - const { isLoading, filteredEvents } = useEvents() + const { isLoading, filteredEvents, filteredOnDemandEvents } = useEvents() const getCategoryLabel = (value: string) => { const category = CATEGORIES_FILTERS.find((cat) => cat.value === value) @@ -27,7 +27,7 @@ export function EventList() { return } - // Group events by date + // Group upcoming events by date const eventsByDate = filteredEvents.reduce( (acc, event) => { const eventDate = new Date(event.date).toLocaleDateString('en-US', { @@ -46,6 +46,9 @@ export function EventList() { {} as Record ) + const hasUpcomingEvents = Object.keys(eventsByDate).length > 0 + const hasOnDemandEvents = filteredOnDemandEvents.length > 0 + return (
{Object.entries(eventsByDate).map(([date, events], index) => ( @@ -99,8 +102,55 @@ export function EventList() {
))} - {/* emtpy state */} - {Object.keys(eventsByDate).length === 0 && ( + {/* On-demand events section */} + {hasOnDemandEvents && ( +
+
+ +

On Demand

+ +
+ {filteredOnDemandEvents.map((event, idx) => ( +
+ + +
+

{event.title}

+ +
+
+ {event.hosts[0]?.avatar_url && ( + {event.hosts[0].name + )} +
+ Hosted by {formatHosts(event.hosts).displayText} +
+
+ + {event.categories.map((tag, idx) => ( + {getCategoryLabel(tag)} + ))} +
+ ))} +
+
+ )} + + {/* empty state */} + {!hasUpcomingEvents && !hasOnDemandEvents && (

Oops! No events found.

diff --git a/apps/www/lib/events.ts b/apps/www/lib/events.ts index 9a05822af50..f686c08e93b 100644 --- a/apps/www/lib/events.ts +++ b/apps/www/lib/events.ts @@ -252,7 +252,11 @@ export const getStaticEvents = async (): Promise<{ thumb: post.thumb || '', cover_url: (post as any).cover_url || '', path: post.path || '', - url: post.url || '', + // For webinars, use internal path; for other events, use external link if available + url: + (post as any).type === 'webinar' + ? post.url || post.path || '' + : post.link?.href || post.url || post.path || '', tags: post.tags || [], categories: post.categories || [], timezone: (post as any).timezone || 'America/Los_Angeles',