11'use client'
22
3- import { useState } from 'react'
3+ import { Suspense , useCallback } from 'react'
4+ import { useRouter , useSearchParams } from 'next/navigation'
45import { Search , Download } from 'lucide-react'
56import { RequireWallet } from '@/components/layout/require-wallet'
67import { Button } from '@/components/ui/button'
78import { streamsToCSV , downloadCSV } from '@/lib/export'
89import { StreamCard } from '@/components/streams/stream-card'
910import { EmptyStreams } from '@/components/streams/empty-state'
10- import { StreamStatusBadge } from '@/components/streams/stream-status-badge'
1111import { Input } from '@/components/ui/input'
1212import { useStreams } from '@/hooks/use-streams'
1313import { useNow } from '@/hooks/use-now'
14- import { getStreamStatus , shortenAddress } from '@/lib/stream-utils'
14+ import { getStreamStatus } from '@/lib/stream-utils'
1515import type { StreamStatus } from '@/types/stream'
1616
1717const STATUS_FILTERS : { label : string ; value : StreamStatus | 'all' } [ ] = [
@@ -22,25 +22,53 @@ const STATUS_FILTERS: { label: string; value: StreamStatus | 'all' }[] = [
2222 { label : 'Cancelled' , value : 'cancelled' } ,
2323]
2424
25+ const TOKEN_OPTIONS = [ 'all' , 'XLM' , 'USDC' , 'EURC' ] as const
26+
2527function StreamsPage ( ) {
28+ const router = useRouter ( )
29+ const searchParams = useSearchParams ( )
2630 const { all } = useStreams ( )
2731 const now = useNow ( 5000 )
28- const [ search , setSearch ] = useState ( '' )
29- const [ statusFilter , setStatusFilter ] = useState < StreamStatus | 'all' > ( 'all' )
32+
33+ const search = searchParams . get ( 'q' ) ?? ''
34+ const statusFilter = ( searchParams . get ( 'status' ) ?? 'all' ) as StreamStatus | 'all'
35+ const tokenFilter = searchParams . get ( 'token' ) ?? 'all'
36+
37+ const setParam = useCallback (
38+ ( key : string , value : string ) => {
39+ const params = new URLSearchParams ( searchParams . toString ( ) )
40+ if ( ! value || value === 'all' ) {
41+ params . delete ( key )
42+ } else {
43+ params . set ( key , value )
44+ }
45+ router . replace ( `?${ params . toString ( ) } ` , { scroll : false } )
46+ } ,
47+ [ router , searchParams ] ,
48+ )
49+
50+ const clearFilters = useCallback ( ( ) => {
51+ router . replace ( '?' , { scroll : false } )
52+ } , [ router ] )
3053
3154 const filtered = all . filter ( ( s ) => {
3255 const matchesStatus =
3356 statusFilter === 'all' || getStreamStatus ( s , now ) === statusFilter
57+ const matchesToken =
58+ tokenFilter === 'all' ||
59+ s . token . symbol . toUpperCase ( ) === tokenFilter . toUpperCase ( )
3460 const q = search . toLowerCase ( )
3561 const matchesSearch =
3662 ! q ||
3763 s . id . includes ( q ) ||
3864 s . sender . toLowerCase ( ) . includes ( q ) ||
3965 s . recipient . toLowerCase ( ) . includes ( q ) ||
4066 s . token . symbol . toLowerCase ( ) . includes ( q )
41- return matchesStatus && matchesSearch
67+ return matchesStatus && matchesToken && matchesSearch
4268 } )
4369
70+ const hasFilters = search || statusFilter !== 'all' || tokenFilter !== 'all'
71+
4472 return (
4573 < div className = "space-y-6" >
4674 < div className = "flex items-center justify-between" >
@@ -66,21 +94,42 @@ function StreamsPage() {
6694 </ div >
6795
6896 { /* Filters */ }
69- < div className = "flex flex-col gap-3 sm:flex-row sm:items-center" >
70- < div className = "relative flex-1 sm:max-w-xs" >
71- < Search className = "pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
72- < Input
73- placeholder = "Search by address or token…"
74- value = { search }
75- onChange = { ( e ) => setSearch ( e . target . value ) }
76- className = "pl-9"
77- />
97+ < div className = "flex flex-col gap-3" >
98+ < div className = "flex flex-col gap-3 sm:flex-row sm:items-center" >
99+ < div className = "relative flex-1 sm:max-w-xs" >
100+ < Search className = "pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
101+ < Input
102+ placeholder = "Search by address or token…"
103+ value = { search }
104+ onChange = { ( e ) => setParam ( 'q' , e . target . value ) }
105+ className = "pl-9"
106+ />
107+ </ div >
108+ { /* Token filter */ }
109+ < div className = "flex flex-wrap gap-2" >
110+ { TOKEN_OPTIONS . map ( ( t ) => (
111+ < button
112+ key = { t }
113+ onClick = { ( ) => setParam ( 'token' , t ) }
114+ className = {
115+ 'rounded-full border px-3 py-1 text-xs font-medium transition-colors ' +
116+ ( tokenFilter === t
117+ ? 'border-primary bg-primary/10 text-primary'
118+ : 'border-border bg-card text-muted-foreground hover:text-foreground' )
119+ }
120+ >
121+ { t === 'all' ? 'All tokens' : t }
122+ </ button >
123+ ) ) }
124+ </ div >
78125 </ div >
126+
127+ { /* Status filter */ }
79128 < div className = "flex flex-wrap gap-2" >
80129 { STATUS_FILTERS . map ( ( f ) => (
81130 < button
82131 key = { f . value }
83- onClick = { ( ) => setStatusFilter ( f . value ) }
132+ onClick = { ( ) => setParam ( 'status' , f . value ) }
84133 className = {
85134 'rounded-full border px-3 py-1 text-xs font-medium transition-colors ' +
86135 ( statusFilter === f . value
@@ -96,11 +145,11 @@ function StreamsPage() {
96145
97146 { /* Results */ }
98147 { filtered . length === 0 ? (
99- search || statusFilter !== 'all' ? (
148+ hasFilters ? (
100149 < div className = "flex flex-col items-center justify-center rounded-2xl border border-dashed border-border bg-card/40 px-6 py-16 text-center" >
101150 < p className = "text-sm font-medium" > No streams match your filters</ p >
102151 < button
103- onClick = { ( ) => { setSearch ( '' ) ; setStatusFilter ( 'all' ) } }
152+ onClick = { clearFilters }
104153 className = "mt-2 text-xs text-primary hover:underline"
105154 >
106155 Clear filters
@@ -123,7 +172,9 @@ function StreamsPage() {
123172export default function StreamsRoute ( ) {
124173 return (
125174 < RequireWallet >
126- < StreamsPage />
175+ < Suspense >
176+ < StreamsPage />
177+ </ Suspense >
127178 </ RequireWallet >
128179 )
129180}
0 commit comments