mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
fix filtering in events page (#40697)
* fix filtering in events page * make agency on-demand
This commit is contained in:
1 parent
b709428112
commit
7747ef0b29
7 files changed
+129
-17
No files matched your search
@@ -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
|
||||
|
||||
@@ -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<EventsContextValue | undefined>(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<SupabaseEvent[]>([])
|
||||
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,
|
||||
|
||||
@@ -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 <EventClientRenderer staticEvents={staticEvents.upcomingEvents} />
|
||||
return <EventClientRenderer staticEvents={upcomingEvents} onDemandEvents={onDemandEvents} />
|
||||
}
|
||||
@@ -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 (
|
||||
<section className={cn('grid md:grid-cols-[minmax(320px,35%),1fr] gap-6 lg:gap-12')}>
|
||||
<section
|
||||
className={cn('grid md:grid-cols-[minmax(320px,35%),1fr] items-start gap-6 lg:gap-12')}
|
||||
>
|
||||
<CoverImage url={featuredEvent.cover_url} />
|
||||
|
||||
<article className="flex flex-col md:justify-center gap-6 lg:py-2">
|
||||
@@ -140,14 +142,20 @@ const LocationWidget = ({ location }: { location?: string }) => {
|
||||
const CoverImage = ({ url }: { url?: string }) => {
|
||||
if (!url)
|
||||
return (
|
||||
<div className="w-full bg-surface-100 aspect-square border rounded-lg hidden md:grid place-items-center">
|
||||
<div className="w-full bg-surface-100 aspect-square border rounded-lg hidden md:grid place-items-center relative">
|
||||
<Logo />
|
||||
<Badge variant="brand" className="absolute bottom-4 right-4">
|
||||
Upcoming
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="w-full bg-surface-100 hidden md:block aspect-square border rounded-lg overflow-hidden relative">
|
||||
<img src={url} alt="Event Cover" className="object-cover object-center w-full" />
|
||||
<Badge variant="brand" className="absolute bottom-4 right-4">
|
||||
Upcoming
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<EventsProvider staticEvents={staticEvents}>
|
||||
<EventsProvider staticEvents={staticEvents} onDemandEvents={onDemandEvents}>
|
||||
<DefaultLayout className="flex flex-col">
|
||||
<SectionContainer className="border-x border-b !py-8">
|
||||
<h1 className="h3 !p-0 !m-0">
|
||||
|
||||
@@ -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 <EventListSkeleton />
|
||||
}
|
||||
|
||||
// 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<string, typeof filteredEvents>
|
||||
)
|
||||
|
||||
const hasUpcomingEvents = Object.keys(eventsByDate).length > 0
|
||||
const hasOnDemandEvents = filteredOnDemandEvents.length > 0
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-8 min-h-72">
|
||||
{Object.entries(eventsByDate).map(([date, events], index) => (
|
||||
@@ -99,8 +102,55 @@ export function EventList() {
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* emtpy state */}
|
||||
{Object.keys(eventsByDate).length === 0 && (
|
||||
{/* On-demand events section */}
|
||||
{hasOnDemandEvents && (
|
||||
<div className="flex flex-col gap-y-2 relative mt-8">
|
||||
<div className="absolute top-2 -left-[calc(48px+11px)] rounded-full size-1.5 bg-foreground-muted" />
|
||||
|
||||
<h3 className="text-foreground-light font-normal">On Demand</h3>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
{filteredOnDemandEvents.map((event, idx) => (
|
||||
<div
|
||||
key={`on-demand-${idx}-${event.url}`}
|
||||
className="bg-surface-100 border rounded-md p-3 flex justify-between items-start relative"
|
||||
>
|
||||
<Link
|
||||
className="inset-0 absolute"
|
||||
href={event.url}
|
||||
target={event.url.startsWith('http') ? '_blank' : '_self'}
|
||||
title="Go to event page"
|
||||
aria-hidden
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3>{event.title}</h3>
|
||||
|
||||
<div className="flex gap-2 items-center text-sm text-foreground-light">
|
||||
<div className="size-5 rounded-full border bg-gradient-to-br from-background-surface-100 to-background-surface-200 relative">
|
||||
{event.hosts[0]?.avatar_url && (
|
||||
<img
|
||||
src={event.hosts[0].avatar_url}
|
||||
alt={event.hosts[0].name || 'Host image'}
|
||||
className="absolute inset-0 w-full h-full object-cover rounded-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
Hosted by {formatHosts(event.hosts).displayText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{event.categories.map((tag, idx) => (
|
||||
<Badge key={`tag-${idx}`}>{getCategoryLabel(tag)}</Badge>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* empty state */}
|
||||
{!hasUpcomingEvents && !hasOnDemandEvents && (
|
||||
<div className="self-center text-muted my-auto flex flex-col items-center gap-y-4">
|
||||
<Rows3Icon className="size-8" />
|
||||
<p className="">Oops! No events found.</p>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user