Skip to content

Commit fa79a2e

Browse files
committed
fix glitchy search bar
1 parent e9cd9e0 commit fa79a2e

File tree

1 file changed

+92
-87
lines changed

1 file changed

+92
-87
lines changed

ui/src/app/bundles/page.tsx

Lines changed: 92 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import Link from "next/link";
4-
import { useEffect, useState } from "react";
4+
import { useCallback, useEffect, useRef, useState } from "react";
55
import type { Bundle } from "@/app/api/bundles/route";
66

77
export default function BundlesPage() {
@@ -12,7 +12,46 @@ export default function BundlesPage() {
1212
const [searchHash, setSearchHash] = useState<string>("");
1313
const [filteredLiveBundles, setFilteredLiveBundles] = useState<Bundle[]>([]);
1414
const [filteredAllBundles, setFilteredAllBundles] = useState<string[]>([]);
15-
const [searchLoading, setSearchLoading] = useState(false);
15+
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
16+
17+
const filterBundles = useCallback(
18+
async (searchTerm: string, live: Bundle[], all: string[]) => {
19+
if (!searchTerm.trim()) {
20+
setFilteredLiveBundles(live);
21+
setFilteredAllBundles(all);
22+
return;
23+
}
24+
25+
// Filter live bundles immediately for better UX
26+
const liveBundlesWithTx = live.filter((bundle) =>
27+
bundle.txnHashes?.some((hash) =>
28+
hash.toLowerCase().includes(searchTerm.toLowerCase()),
29+
),
30+
);
31+
32+
let allBundlesWithTx: string[] = [];
33+
34+
try {
35+
const response = await fetch(`/api/txn/${searchTerm.trim()}`);
36+
37+
if (response.ok) {
38+
const txnData = await response.json();
39+
const bundleIds = txnData.bundle_ids || [];
40+
41+
allBundlesWithTx = all.filter((bundleId) =>
42+
bundleIds.includes(bundleId),
43+
);
44+
}
45+
} catch (err) {
46+
console.error("Error filtering bundles:", err);
47+
}
48+
49+
// Batch all state updates together to prevent jitter
50+
setFilteredLiveBundles(liveBundlesWithTx);
51+
setFilteredAllBundles(allBundlesWithTx);
52+
},
53+
[],
54+
);
1655

1756
useEffect(() => {
1857
const fetchLiveBundles = async () => {
@@ -61,57 +100,26 @@ export default function BundlesPage() {
61100
}, []);
62101

63102
useEffect(() => {
64-
const filterBundles = async () => {
65-
if (!searchHash.trim()) {
66-
setFilteredLiveBundles(liveBundles);
67-
setFilteredAllBundles(allBundles);
68-
return;
103+
if (debounceTimeoutRef.current) {
104+
clearTimeout(debounceTimeoutRef.current);
105+
}
106+
107+
if (!searchHash.trim()) {
108+
// No debounce for clearing search
109+
filterBundles(searchHash, liveBundles, allBundles);
110+
} else {
111+
// Debounce API calls for non-empty search
112+
debounceTimeoutRef.current = setTimeout(() => {
113+
filterBundles(searchHash, liveBundles, allBundles);
114+
}, 300);
115+
}
116+
117+
return () => {
118+
if (debounceTimeoutRef.current) {
119+
clearTimeout(debounceTimeoutRef.current);
69120
}
70-
71-
setSearchLoading(true);
72-
73-
try {
74-
const liveBundlesWithTx = liveBundles.filter(bundle =>
75-
bundle.txnHashes?.some(hash =>
76-
hash.toLowerCase().includes(searchHash.toLowerCase())
77-
)
78-
);
79-
80-
const response = await fetch(`/api/txn/${searchHash.trim()}`);
81-
82-
if (response.ok) {
83-
const txnData = await response.json();
84-
const bundleIds = txnData.bundle_ids || [];
85-
86-
const allBundlesWithTx = allBundles.filter(bundleId =>
87-
bundleIds.includes(bundleId)
88-
);
89-
90-
setFilteredLiveBundles(liveBundlesWithTx);
91-
setFilteredAllBundles(allBundlesWithTx);
92-
} else {
93-
setFilteredLiveBundles(liveBundles.filter(bundle =>
94-
bundle.txnHashes?.some(hash =>
95-
hash.toLowerCase().includes(searchHash.toLowerCase())
96-
)
97-
));
98-
setFilteredAllBundles([]);
99-
}
100-
} catch (err) {
101-
console.error("Error filtering bundles:", err);
102-
setFilteredLiveBundles(liveBundles.filter(bundle =>
103-
bundle.txnHashes?.some(hash =>
104-
hash.toLowerCase().includes(searchHash.toLowerCase())
105-
)
106-
));
107-
setFilteredAllBundles([]);
108-
}
109-
110-
setSearchLoading(false);
111121
};
112-
113-
filterBundles();
114-
}, [searchHash, liveBundles, allBundles]);
122+
}, [searchHash, liveBundles, allBundles, filterBundles]);
115123

116124
if (loading) {
117125
return (
@@ -135,9 +143,6 @@ export default function BundlesPage() {
135143
onChange={(e) => setSearchHash(e.target.value)}
136144
className="px-3 py-2 border rounded-lg bg-white/5 border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent placeholder-gray-500 dark:placeholder-gray-400 text-sm min-w-[300px]"
137145
/>
138-
{searchLoading && (
139-
<div className="text-sm text-gray-500 animate-pulse">Searching...</div>
140-
)}
141146
</div>
142147
</div>
143148
{error && (
@@ -146,19 +151,18 @@ export default function BundlesPage() {
146151
</div>
147152

148153
<div className="flex flex-col gap-6">
149-
{(filteredLiveBundles.length > 0 || !searchHash.trim()) && (
150-
<section>
151-
<h2 className="text-xl font-semibold mb-4">
152-
Live Bundles
153-
{searchHash.trim() && (
154-
<span className="text-sm font-normal text-gray-500 ml-2">
155-
({filteredLiveBundles.length} found)
156-
</span>
157-
)}
158-
</h2>
159-
{filteredLiveBundles.length > 0 ? (
160-
<ul className="space-y-2">
161-
{filteredLiveBundles.map((bundle) => (
154+
<section>
155+
<h2 className="text-xl font-semibold mb-4">
156+
Live Bundles
157+
{searchHash.trim() && (
158+
<span className="text-sm font-normal text-gray-500 ml-2">
159+
({filteredLiveBundles.length} found)
160+
</span>
161+
)}
162+
</h2>
163+
{filteredLiveBundles.length > 0 ? (
164+
<ul className="space-y-2">
165+
{filteredLiveBundles.map((bundle) => (
162166
<li key={bundle.id}>
163167
<Link
164168
href={`/bundles/${bundle.id}`}
@@ -189,25 +193,25 @@ export default function BundlesPage() {
189193
</ul>
190194
) : (
191195
<p className="text-gray-600 dark:text-gray-400">
192-
{searchHash.trim() ? "No live bundles found matching this transaction hash." : "No live bundles found."}
196+
{searchHash.trim()
197+
? "No live bundles found matching this transaction hash."
198+
: "No live bundles found."}
193199
</p>
194200
)}
195-
</section>
196-
)}
197-
198-
{(filteredAllBundles.length > 0 || !searchHash.trim()) && (
199-
<section>
200-
<h2 className="text-xl font-semibold mb-4">
201-
All Bundles
202-
{searchHash.trim() && (
203-
<span className="text-sm font-normal text-gray-500 ml-2">
204-
({filteredAllBundles.length} found)
205-
</span>
206-
)}
207-
</h2>
208-
{filteredAllBundles.length > 0 ? (
209-
<ul className="space-y-2">
210-
{filteredAllBundles.map((bundleId) => (
201+
</section>
202+
203+
<section>
204+
<h2 className="text-xl font-semibold mb-4">
205+
All Bundles
206+
{searchHash.trim() && (
207+
<span className="text-sm font-normal text-gray-500 ml-2">
208+
({filteredAllBundles.length} found)
209+
</span>
210+
)}
211+
</h2>
212+
{filteredAllBundles.length > 0 ? (
213+
<ul className="space-y-2">
214+
{filteredAllBundles.map((bundleId) => (
211215
<li key={bundleId}>
212216
<Link
213217
href={`/bundles/${bundleId}`}
@@ -220,11 +224,12 @@ export default function BundlesPage() {
220224
</ul>
221225
) : (
222226
<p className="text-gray-600 dark:text-gray-400">
223-
{searchHash.trim() ? "No bundles found in S3 matching this transaction hash." : "No bundles found in S3."}
227+
{searchHash.trim()
228+
? "No bundles found in S3 matching this transaction hash."
229+
: "No bundles found in S3."}
224230
</p>
225231
)}
226-
</section>
227-
)}
232+
</section>
228233
</div>
229234
</div>
230235
);

0 commit comments

Comments
 (0)