11"use client" ;
22
33import Link from "next/link" ;
4- import { useEffect , useState } from "react" ;
4+ import { useCallback , useEffect , useRef , useState } from "react" ;
55import type { Bundle } from "@/app/api/bundles/route" ;
66
77export 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