@@ -9,6 +9,10 @@ export default function BundlesPage() {
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 [ searchLoading , setSearchLoading ] = useState ( false ) ;
1216
1317 useEffect ( ( ) => {
1418 const fetchLiveBundles = async ( ) => {
@@ -56,6 +60,59 @@ export default function BundlesPage() {
5660 return ( ) => clearInterval ( interval ) ;
5761 } , [ ] ) ;
5862
63+ useEffect ( ( ) => {
64+ const filterBundles = async ( ) => {
65+ if ( ! searchHash . trim ( ) ) {
66+ setFilteredLiveBundles ( liveBundles ) ;
67+ setFilteredAllBundles ( allBundles ) ;
68+ return ;
69+ }
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 ) ;
111+ } ;
112+
113+ filterBundles ( ) ;
114+ } , [ searchHash , liveBundles , allBundles ] ) ;
115+
59116 if ( loading ) {
60117 return (
61118 < div className = "flex flex-col gap-4 p-8" >
@@ -68,18 +125,40 @@ export default function BundlesPage() {
68125 return (
69126 < div className = "flex flex-col gap-8 p-8" >
70127 < div className = "flex flex-col gap-2" >
71- < h1 className = "text-2xl font-bold" > BundleStore (fka Mempool)</ h1 >
128+ < div className = "flex items-center justify-between" >
129+ < h1 className = "text-2xl font-bold" > BundleStore (fka Mempool)</ h1 >
130+ < div className = "flex items-center gap-2" >
131+ < input
132+ type = "text"
133+ placeholder = "Search by transaction hash..."
134+ value = { searchHash }
135+ onChange = { ( e ) => setSearchHash ( e . target . value ) }
136+ 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]"
137+ />
138+ { searchLoading && (
139+ < div className = "text-sm text-gray-500 animate-pulse" > Searching...</ div >
140+ ) }
141+ </ div >
142+ </ div >
72143 { error && (
73144 < div className = "text-sm text-red-600 dark:text-red-400" > { error } </ div >
74145 ) }
75146 </ div >
76147
77148 < div className = "flex flex-col gap-6" >
78- < section >
79- < h2 className = "text-xl font-semibold mb-4" > Live Bundles</ h2 >
80- { liveBundles . length > 0 ? (
81- < ul className = "space-y-2" >
82- { liveBundles . map ( ( bundle ) => (
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 ) => (
83162 < li key = { bundle . id } >
84163 < Link
85164 href = { `/bundles/${ bundle . id } ` }
@@ -110,16 +189,25 @@ export default function BundlesPage() {
110189 </ ul >
111190 ) : (
112191 < p className = "text-gray-600 dark:text-gray-400" >
113- No live bundles found.
192+ { searchHash . trim ( ) ? " No live bundles found matching this transaction hash." : "No live bundles found." }
114193 </ p >
115194 ) }
116- </ section >
195+ </ section >
196+ ) }
117197
118- < section >
119- < h2 className = "text-xl font-semibold mb-4" > All Bundles</ h2 >
120- { allBundles . length > 0 ? (
121- < ul className = "space-y-2" >
122- { allBundles . map ( ( bundleId ) => (
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 ) => (
123211 < li key = { bundleId } >
124212 < Link
125213 href = { `/bundles/${ bundleId } ` }
@@ -132,10 +220,11 @@ export default function BundlesPage() {
132220 </ ul >
133221 ) : (
134222 < p className = "text-gray-600 dark:text-gray-400" >
135- No bundles found in S3.
223+ { searchHash . trim ( ) ? " No bundles found in S3 matching this transaction hash." : "No bundles found in S3." }
136224 </ p >
137225 ) }
138- </ section >
226+ </ section >
227+ ) }
139228 </ div >
140229 </ div >
141230 ) ;
0 commit comments