Mike Yim
04/17/2025, 6:04 PMimport React, { useEffect, useRef } from 'react';
import TypesenseInstantSearchAdapter from 'typesense-instantsearch-adapter';
import { InstantSearch, useInfiniteHits, useRefinementList } from 'react-instantsearch';
import { SearchBox } from '@/components/ui/search-box';
import dayjs from 'dayjs';
import { useUserLocationQuery } from '@utc/data/query/user';
import { EventCard, EventCardSkeleton } from './event-card';
interface EventSearchProps {
isUpcoming?: boolean;
}
export function EventSearch({ isUpcoming = true }: EventSearchProps) {
const { isPending, error, data } = useUserLocationQuery();
if (isPending) {
return (
<div className="mt-6 grid gap-x-5 gap-y-5 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{Array.from({ length: 8 }).map((_, index) => (
<EventCardSkeleton key={index} />
))}
</div>
);
}
if (error) {
return null;
}
// Current timestamp for comparison
const currentTimestamp = dayjs().unix();
// Filter based on isUpcoming flag
const filterQuery = isUpcoming
? `endAtUnix:>${currentTimestamp}&&published:true`
: `endAtUnix:<=${currentTimestamp}&&published:true`;
const typesense = new TypesenseInstantSearchAdapter({
server: {
apiKey: import.meta.env.VITE_TYPESENSE_SEARCH_ONLY_API_KEY!,
nodes: [
{
host: import.meta.env.VITE_TYPESENSE_HOST!,
port: parseInt(import.meta.env.VITE_TYPESENSE_PORT!),
protocol: import.meta.env.VITE_TYPESENSE_PROTOCOL!,
},
],
},
geoLocationField: 'place.location',
additionalSearchParameters: {
query_by: 'title,place.formattedAddress,host.username',
sort_by: `place.location(${data.userLocation.lat},${data.userLocation.lng}):asc,${isUpcoming ? 'startAtUnix:asc' : 'endAtUnix:desc'}`,
per_page: 8,
filter_by: filterQuery,
},
});
return (
<div>
<InstantSearch searchClient={typesense.searchClient} indexName="event">
{/* Search input */}
<div className="mb-4 flex">
<SearchBox placeholder="Search events..." className="w-full max-w-md" />
</div>
{/* Event results */}
<EventList />
</InstantSearch>
</div>
);
}
function EventList() {
const { items, isLastPage, showMore } = useInfiniteHits({});
const sentinelRef = useRef(null);
useEffect(() => {
if (sentinelRef.current !== null) {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !isLastPage) {
showMore()
}
});
});
observer.observe(sentinelRef.current);
return () => {
observer.disconnect();
};
}
}, [isLastPage, showMore]);
return (
<div>
<div className="mt-6 grid gap-x-5 gap-y-5 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{items.map((event) => (
<EventCard key={event.objectID} event={event} />
))}
</div>
<div ref={sentinelRef} aria-hidden="true" />
{items.length === 0 && (
<div className="flex flex-col items-center justify-center py-12">
<h3>No events found</h3>
<p className="text-muted-foreground mt-2">Check back later for updates</p>
</div>
)}
</div>
);
}