Skip to content

Commit 3636702

Browse files
committed
add Algolia's InstantSearch
1 parent bceb449 commit 3636702

File tree

7 files changed

+575
-113
lines changed

7 files changed

+575
-113
lines changed

components/ApiCard.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React from "react";
2-
import { ApiCardModel } from "../models/ApiCardModel";
32
import { Card, CardContent, CardHeader } from "./ui/card";
43
import Link from "next/link";
54
import Image from "next/image";
@@ -43,7 +42,7 @@ export default function ApiCard({ model }: { model: ApiCard }) {
4342
}}
4443
/>
4544
<p className="leading-[1.2] overflow-hidden text-ellipsis h-[calc(1em*1.2*3)] text-sm text-gray-700">
46-
{model.cardDescription}
45+
{model.description}
4746
</p>
4847
</div>
4948
</CardContent>

components/ApiGrid.tsx

Lines changed: 91 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,89 @@
1-
import React from "react";
2-
1+
import React, { useEffect, useRef, useCallback } from "react";
2+
import { useInfiniteHits, useInstantSearch } from "react-instantsearch";
33
import Card from "./ApiCard";
4-
54
import { CardSkeleton } from "@/components/ui/CardSkeleton";
6-
import { ApiCard } from "@/types/api";
5+
import { cleanDescription } from "@/utils/textProcessing";
76

87
interface ApiGridProps {
9-
cards: ApiCard[];
108
searchTerm: string;
11-
loading: boolean;
12-
loadingMore: boolean;
13-
hasMore: boolean;
149
gridColumns: number;
1510
pageSize: number;
16-
observerRef: React.RefObject<HTMLDivElement | null>;
1711
}
12+
const transformItems = (items: any[]) => {
13+
console.log("Transforming items:", items);
14+
return items.map((item) => ({
15+
...item,
16+
name: item.name || item.objectID,
17+
description: cleanDescription(item.description || ""),
18+
title: item.title || item.name || "",
19+
categories: item.categories ? item.categories.split(",") : [],
20+
tags: item.tags ? item.tags.split(",") : [],
21+
contact: item.contact || "",
22+
license: item.license || "",
23+
logoUrl: item.logoUrl || "",
24+
swaggerUrl: item.swaggerUrl || "",
25+
swaggerYamlUrl: item.swaggerYamlUrl || "",
26+
externalUrl: item.externalUrl || "",
27+
version: item.version || "",
28+
added: item.added || "",
29+
updated: item.updated || "",
30+
}));
31+
};
32+
33+
export function ApiGrid({ searchTerm, gridColumns, pageSize }: ApiGridProps) {
34+
const { status, error } = useInstantSearch({ catchError: true });
35+
const { hits, isLastPage, showMore, results } = useInfiniteHits({
36+
transformItems,
37+
showPrevious: false,
38+
});
39+
40+
const loading = status === "loading";
41+
const stalled = status === "stalled";
42+
const hasError = status === "error";
43+
const initialLoading = (loading || stalled) && hits.length === 0;
44+
const loadingMore = (loading || stalled) && hits.length > 0;
45+
const hasMore = !isLastPage && !hasError;
46+
47+
const observerRef = useRef<HTMLDivElement | null>(null);
48+
49+
const handleIntersection = useCallback(
50+
(entries: IntersectionObserverEntry[]) => {
51+
if (entries[0].isIntersecting && hasMore && !loading && !stalled) {
52+
showMore();
53+
}
54+
},
55+
[hasMore, loading, stalled, showMore]
56+
);
57+
58+
useEffect(() => {
59+
const observer = observerRef.current;
60+
if (!observer) return;
61+
62+
const intersectionObserver = new IntersectionObserver(handleIntersection, {
63+
threshold: 0.1,
64+
rootMargin: "100px",
65+
});
66+
67+
intersectionObserver.observe(observer);
68+
69+
return () => {
70+
intersectionObserver.disconnect();
71+
};
72+
}, [handleIntersection]);
73+
74+
if (hasError && error) {
75+
return (
76+
<section id="apis-list" className="cards">
77+
<div className="col-span-full text-center py-6 bg-red-50 rounded-lg border border-red-100">
78+
<p className="text-red-600">Error loading APIs: {error.message}</p>
79+
</div>
80+
</section>
81+
);
82+
}
1883

19-
export function ApiGrid({
20-
cards,
21-
searchTerm,
22-
loading,
23-
loadingMore,
24-
hasMore,
25-
gridColumns,
26-
pageSize,
27-
observerRef,
28-
}: ApiGridProps) {
2984
return (
3085
<section id="apis-list" className="cards">
31-
{loading ? (
86+
{initialLoading ? (
3287
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 mt-4">
3388
{Array.from({ length: Math.min(pageSize, gridColumns * 2) }).map(
3489
(_, index) => (
@@ -38,32 +93,34 @@ export function ApiGrid({
3893
</div>
3994
) : (
4095
<>
96+
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 mt-4">
97+
{hits.length > 0 ? (
98+
hits.map((hit, index) => {
99+
console.log("Rendering hit:", hit);
100+
return <Card key={`${hit.objectID}-${index}`} model={hit} />;
101+
})
102+
) : (
103+
<div className="col-span-full text-center py-6 bg-gray-50 rounded-lg border border-gray-100">
104+
No APIs found matching &quot;{searchTerm}&quot;
105+
</div>
106+
)}
107+
</div>
108+
41109
{loadingMore && (
42110
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 mt-4">
43-
{Array.from({ length: Math.min(pageSize, gridColumns * 2) }).map(
111+
{Array.from({ length: Math.min(pageSize, gridColumns) }).map(
44112
(_, index) => (
45113
<CardSkeleton key={`skeleton-more-${index}`} />
46114
)
47115
)}
48116
</div>
49117
)}
50-
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4">
51-
{cards.length > 0 ? (
52-
cards.map((card, index) => (
53-
<Card key={`${card.name}-${index}`} model={card} />
54-
))
55-
) : (
56-
<div className="col-span-full text-center py-6 bg-gray-50 rounded-lg border border-gray-100">
57-
No APIs found matching &quot;{searchTerm}&quot;
58-
</div>
59-
)}
60-
</div>
61118
</>
62119
)}
63120

64-
<div ref={observerRef} className="h-10 mt-4" />
121+
<div ref={observerRef} className="h-10 mt-4" aria-hidden="true" />
65122

66-
{!hasMore && cards.length > 0 && (
123+
{!hasMore && hits.length > 0 && !hasError && (
67124
<div className="text-center py-6 text-gray-500">
68125
That's all the APIs! 🎉
69126
</div>

components/SearchClientComponent.tsx

Lines changed: 34 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ import React, { useEffect, Suspense } from "react";
44
import { useSearchParams } from "next/navigation";
55
import { useGridLayout } from "@/hooks/useGridLayout";
66
import { useApiSearch } from "@/hooks/useApiSearch";
7-
import { useInfiniteScroll } from "@/hooks/useInfiniteScroll";
8-
import { SearchSection } from "@/components/SearchSection";
9-
import { ApiGrid } from "@/components/ApiGrid";
107
import { LoadingSkeleton } from "@/components/LoadingSkeleton";
8+
import { ApiGrid } from "@/components/ApiGrid";
9+
import { liteClient as algoliasearch } from "algoliasearch/lite";
10+
import { InstantSearch, Hits } from "react-instantsearch";
11+
import { SearchSection } from "@/components/SearchSection";
12+
13+
const searchClient = algoliasearch(
14+
"D29MLR0AMY",
15+
"03da9918f8ebfdb40e9b37cfd43ed8c4"
16+
);
1117

1218
interface SearchClientComponentProps {
1319
repoStarCounts: Record<string, number>;
@@ -20,54 +26,15 @@ function SearchClientComponentInner({
2026
}: SearchClientComponentProps) {
2127
const searchParams = useSearchParams();
2228
const initialSearchTerm = searchParams?.get("q") || "";
23-
2429
const initialCombinedSearchTerm = providerSlug
2530
? `${providerSlug} ${initialSearchTerm}`.trim()
2631
: initialSearchTerm;
2732

2833
const { gridColumns, pageSize } = useGridLayout();
29-
30-
const {
31-
searchTerm,
32-
setSearchTerm,
33-
allApiCards,
34-
loading,
35-
setLoading,
36-
loadingMore,
37-
hasMore,
38-
loadMoreApis,
39-
resetSearch,
40-
totalCount,
41-
} = useApiSearch(initialCombinedSearchTerm, pageSize);
42-
43-
const observerRef = useInfiniteScroll({
44-
hasMore,
45-
loadingMore,
46-
loading,
47-
loadMore: loadMoreApis,
48-
}) as React.RefObject<HTMLDivElement>;
49-
50-
useEffect(() => {
51-
resetSearch(initialCombinedSearchTerm);
52-
}, [pageSize, initialCombinedSearchTerm, resetSearch]);
53-
54-
useEffect(() => {
55-
const handleSearchChange = async () => {
56-
const combinedSearchTerm = providerSlug
57-
? `${providerSlug}:${searchTerm}`.trim()
58-
: searchTerm;
59-
60-
await resetSearch(combinedSearchTerm);
61-
};
62-
63-
const debounceTimer = setTimeout(handleSearchChange, 300);
64-
return () => clearTimeout(debounceTimer);
65-
}, [searchTerm, initialCombinedSearchTerm, providerSlug, resetSearch]);
66-
67-
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
68-
setSearchTerm(e.target.value);
69-
setLoading(true);
70-
};
34+
const { searchTerm, setSearchTerm, resetSearch, totalCount } = useApiSearch(
35+
initialCombinedSearchTerm,
36+
pageSize
37+
);
7138

7239
useEffect(() => {
7340
Object.entries(repoStarCounts).forEach(([name, stars]) => {
@@ -80,25 +47,30 @@ function SearchClientComponentInner({
8047
});
8148
}, [repoStarCounts]);
8249

50+
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
51+
setSearchTerm(e.target.value);
52+
};
53+
8354
return (
8455
<div className="container mx-auto px-4 relative">
8556
<div className="relative z-10">
86-
<SearchSection
87-
searchTerm={searchTerm}
88-
apiCount={totalCount}
89-
onSearchChange={handleSearch}
90-
/>
91-
92-
<ApiGrid
93-
cards={allApiCards}
94-
searchTerm={searchTerm}
95-
loading={loading}
96-
loadingMore={loadingMore}
97-
hasMore={hasMore}
98-
gridColumns={gridColumns}
99-
pageSize={pageSize}
100-
observerRef={observerRef}
101-
/>
57+
<InstantSearch
58+
indexName="test_apis_guru"
59+
searchClient={searchClient}
60+
initialUiState={{
61+
test_apis_guru: {
62+
sortBy: "test_apis_guru_by_name_asc",
63+
},
64+
}}
65+
>
66+
<SearchSection searchTerm={searchTerm} apiCount={totalCount} />
67+
68+
<ApiGrid
69+
gridColumns={gridColumns}
70+
pageSize={pageSize}
71+
searchTerm={searchTerm}
72+
/>
73+
</InstantSearch>
10274
</div>
10375
</div>
10476
);

components/SearchSection.tsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
import React from "react";
2-
import { Input } from "@/components/ui/input";
1+
import React, { useEffect } from "react";
2+
import { SearchBox, useInstantSearch } from "react-instantsearch";
33

44
interface SearchSectionProps {
55
searchTerm: string;
66
apiCount: number;
7-
onSearchChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
7+
88
}
99

1010
export function SearchSection({
1111
searchTerm,
1212
apiCount,
13-
onSearchChange,
13+
1414
}: SearchSectionProps) {
15+
const { results } = useInstantSearch({});
16+
useEffect(() => {
17+
console.log(results);
18+
}, [results]);
1519
return (
1620
<div id="search" className="mb-8 max-w-3xl mx-auto">
1721
<div className="relative">
18-
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
22+
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none z-10">
1923
<svg
2024
className="h-5 w-5 text-gray-400"
2125
fill="none"
@@ -30,13 +34,16 @@ export function SearchSection({
3034
/>
3135
</svg>
3236
</div>
33-
<Input
34-
id="search-input"
35-
type="search"
36-
placeholder={`Search through ${apiCount.toLocaleString()} APIs...`}
37-
value={searchTerm}
38-
onChange={onSearchChange}
39-
className="w-full pl-12 pr-4 py-6 text-lg border-2 border-gray-200 rounded-xl focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all duration-200 shadow-sm hover:shadow-md"
37+
<SearchBox
38+
placeholder={`Search through APIs...`}
39+
classNames={{
40+
form: "relative",
41+
input:
42+
"w-full pl-12 pr-4 py-2 text-lg border-2 border-gray-200 rounded-xl transition-all duration-200 shadow-sm hover:shadow-md",
43+
submit: "hidden",
44+
reset: "hidden",
45+
loadingIndicator: "hidden", // Hide the loading indicator
46+
}}
4047
/>
4148
</div>
4249
{searchTerm && (

0 commit comments

Comments
 (0)