Skip to content

Commit edfa362

Browse files
authored
feat: search by txn hash in UI (#28)
1 parent 6b91966 commit edfa362

File tree

1 file changed

+104
-10
lines changed

1 file changed

+104
-10
lines changed

ui/src/app/bundles/page.tsx

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,57 @@
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() {
88
const [liveBundles, setLiveBundles] = useState<Bundle[]>([]);
99
const [allBundles, setAllBundles] = useState<string[]>([]);
1010
const [loading, setLoading] = useState(true);
1111
const [error, setError] = useState<string | null>(null);
12+
const [searchHash, setSearchHash] = useState<string>("");
13+
const [filteredLiveBundles, setFilteredLiveBundles] = useState<Bundle[]>([]);
14+
const [filteredAllBundles, setFilteredAllBundles] = useState<string[]>([]);
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+
);
1255

1356
useEffect(() => {
1457
const fetchLiveBundles = async () => {
@@ -56,6 +99,28 @@ export default function BundlesPage() {
5699
return () => clearInterval(interval);
57100
}, []);
58101

102+
useEffect(() => {
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);
120+
}
121+
};
122+
}, [searchHash, liveBundles, allBundles, filterBundles]);
123+
59124
if (loading) {
60125
return (
61126
<div className="flex flex-col gap-4 p-8">
@@ -68,18 +133,36 @@ export default function BundlesPage() {
68133
return (
69134
<div className="flex flex-col gap-8 p-8">
70135
<div className="flex flex-col gap-2">
71-
<h1 className="text-2xl font-bold">BundleStore (fka Mempool)</h1>
136+
<div className="flex items-center justify-between">
137+
<h1 className="text-2xl font-bold">BundleStore (fka Mempool)</h1>
138+
<div className="flex items-center gap-2">
139+
<input
140+
type="text"
141+
placeholder="Search by transaction hash..."
142+
value={searchHash}
143+
onChange={(e) => setSearchHash(e.target.value)}
144+
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]"
145+
/>
146+
</div>
147+
</div>
72148
{error && (
73149
<div className="text-sm text-red-600 dark:text-red-400">{error}</div>
74150
)}
75151
</div>
76152

77153
<div className="flex flex-col gap-6">
78154
<section>
79-
<h2 className="text-xl font-semibold mb-4">Live Bundles</h2>
80-
{liveBundles.length > 0 ? (
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 ? (
81164
<ul className="space-y-2">
82-
{liveBundles.map((bundle) => (
165+
{filteredLiveBundles.map((bundle) => (
83166
<li key={bundle.id}>
84167
<Link
85168
href={`/bundles/${bundle.id}`}
@@ -110,16 +193,25 @@ export default function BundlesPage() {
110193
</ul>
111194
) : (
112195
<p className="text-gray-600 dark:text-gray-400">
113-
No live bundles found.
196+
{searchHash.trim()
197+
? "No live bundles found matching this transaction hash."
198+
: "No live bundles found."}
114199
</p>
115200
)}
116201
</section>
117202

118203
<section>
119-
<h2 className="text-xl font-semibold mb-4">All Bundles</h2>
120-
{allBundles.length > 0 ? (
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 ? (
121213
<ul className="space-y-2">
122-
{allBundles.map((bundleId) => (
214+
{filteredAllBundles.map((bundleId) => (
123215
<li key={bundleId}>
124216
<Link
125217
href={`/bundles/${bundleId}`}
@@ -132,7 +224,9 @@ export default function BundlesPage() {
132224
</ul>
133225
) : (
134226
<p className="text-gray-600 dark:text-gray-400">
135-
No bundles found in S3.
227+
{searchHash.trim()
228+
? "No bundles found in S3 matching this transaction hash."
229+
: "No bundles found in S3."}
136230
</p>
137231
)}
138232
</section>

0 commit comments

Comments
 (0)