11import * as React from 'react'
2- import { createColumnHelper , getCoreRowModel , useReactTable } from '@tanstack/react-table'
2+ import { useRouter } from 'next/router'
3+ import {
4+ ColumnFiltersState ,
5+ createColumnHelper ,
6+ getCoreRowModel ,
7+ getFilteredRowModel ,
8+ useReactTable
9+ } from '@tanstack/react-table'
310import { maxAgeForNext } from '~/api'
411import { getSimpleProtocolsPageData } from '~/api/categories/protocols'
512import { CSVDownloadButton } from '~/components/ButtonStyled/CsvButton'
13+ import { Icon } from '~/components/Icon'
614import { BasicLink } from '~/components/Link'
15+ import { SelectWithCombobox } from '~/components/SelectWithCombobox'
716import { VirtualTable } from '~/components/Table/Table'
817import { TokenLogo } from '~/components/TokenLogo'
18+ import { DEFI_SETTINGS_KEYS } from '~/contexts/LocalStorage'
19+ import { useDebounce } from '~/hooks/useDebounce'
920import Layout from '~/layout'
1021import { chainIconUrl , slug } from '~/utils'
1122import { withPerformanceLogging } from '~/utils/perf'
1223import { descriptions } from './categories'
1324
25+ const excludeChains = new Set ( [ ...DEFI_SETTINGS_KEYS , 'offers' , 'dcAndLsOverlap' , 'excludeParent' ] )
26+ const excludeCategories = new Set ( [ 'Bridge' , 'Canonical Bridge' ] )
1427export const getStaticProps = withPerformanceLogging ( 'top-protocols' , async ( ) => {
1528 const { protocols, chains } = await getSimpleProtocolsPageData ( [ 'name' , 'extraTvl' , 'chainTvls' , 'category' ] )
16- const topProtocolPerChainAndCategory = Object . fromEntries ( chains . map ( ( c ) => [ c , { } ] ) )
29+ const topProtocolPerChainAndCategory = { }
1730
18- protocols . forEach ( ( p ) => {
31+ for ( const p of protocols ) {
1932 const { chainTvls, category, name } = p
20- if ( [ 'Bridge' , 'Canonical Bridge' ] . includes ( category ) ) {
21- return
33+ if ( excludeCategories . has ( category ) ) {
34+ continue
2235 }
23- Object . entries ( chainTvls ?? { } ) . forEach ( ( [ chain , { tvl } ] : [ string , { tvl : number } ] ) => {
24- if ( topProtocolPerChainAndCategory [ chain ] === undefined ) {
25- return
36+ for ( const chain in chainTvls ) {
37+ if ( chain . includes ( '-' ) || excludeChains . has ( chain ) ) {
38+ continue
2639 }
40+ const tvl = chainTvls [ chain ] . tvl
41+ topProtocolPerChainAndCategory [ chain ] = topProtocolPerChainAndCategory [ chain ] ?? { }
2742
2843 const currentTopProtocol = topProtocolPerChainAndCategory [ chain ] [ category ]
2944
30- if ( currentTopProtocol === undefined || tvl > currentTopProtocol [ 1 ] ) {
45+ if ( currentTopProtocol == null || tvl > currentTopProtocol [ 1 ] ) {
3146 topProtocolPerChainAndCategory [ chain ] [ category ] = [ name , tvl ]
3247 }
33- } )
34- } )
48+ }
49+ }
3550
3651 const data = [ ]
3752 const uniqueCategories = new Set ( )
3853
39- chains . forEach ( ( chain ) => {
54+ for ( const chain of chains ) {
4055 const categories = topProtocolPerChainAndCategory [ chain ]
4156 const values = { }
4257
@@ -45,11 +60,12 @@ export const getStaticProps = withPerformanceLogging('top-protocols', async () =
4560 values [ cat ] = categories [ cat ] [ 0 ]
4661 }
4762 data . push ( { chain, ...values } )
48- } )
63+ }
4964
5065 return {
5166 props : {
5267 data,
68+ chains : data . map ( ( row ) => row . chain ) ,
5369 uniqueCategories : Array . from ( uniqueCategories )
5470 } ,
5571 revalidate : maxAgeForNext ( [ 22 ] )
@@ -58,14 +74,49 @@ export const getStaticProps = withPerformanceLogging('top-protocols', async () =
5874
5975const pageName = [ 'Top Protocols' ]
6076
61- export default function TopProtocols ( { data, uniqueCategories } ) {
62- const columns = React . useMemo ( ( ) => {
63- const columnHelper = createColumnHelper < any > ( )
77+ export default function TopProtocols ( { data, chains, uniqueCategories } ) {
78+ const columnHelper = React . useMemo ( ( ) => createColumnHelper < any > ( ) , [ ] )
79+
80+ const columnOptions = React . useMemo (
81+ ( ) => uniqueCategories . map ( ( cat ) => ( { name : cat , key : cat } ) ) ,
82+ [ uniqueCategories ]
83+ )
6484
85+ const router = useRouter ( )
86+ const { selectedChains, selectedColumns, columnVisibility } = React . useMemo ( ( ) => {
87+ const { chain, column } = router . query
88+ const selectedChains = chain ? ( Array . isArray ( chain ) ? chain : chain == 'All' ? chains : [ chain ] ) : chains
89+ const selectedColumns = column
90+ ? Array . isArray ( column )
91+ ? column
92+ : column == 'All'
93+ ? uniqueCategories
94+ : [ column ]
95+ : uniqueCategories
96+ const selectedColumnsSet = new Set ( selectedColumns )
97+ const columnVisibility = { }
98+ for ( const col of uniqueCategories ) {
99+ columnVisibility [ col ] = selectedColumnsSet . has ( col )
100+ }
101+ return { selectedChains, selectedColumns, columnVisibility }
102+ } , [ router . query , chains , uniqueCategories ] )
103+
104+ const columns = React . useMemo ( ( ) => {
65105 const baseColumns = [
66106 columnHelper . accessor ( 'chain' , {
67107 header : 'Chain' ,
68108 enableSorting : false ,
109+ filterFn : ( row , id , filterValue ) => {
110+ const value = ( row . getValue ( id ) as string ) ?? ''
111+ if ( ! filterValue || typeof filterValue !== 'object' ) {
112+ return true
113+ }
114+ const { search = '' , selected = [ ] } = filterValue as { search ?: string ; selected ?: string [ ] }
115+ const normalizedValue = value . toLowerCase ( )
116+ const matchesSearch = search ? normalizedValue . includes ( search . toLowerCase ( ) ) : true
117+ const matchesSelection = Array . isArray ( selected ) && selected . length > 0 ? selected . includes ( value ) : true
118+ return matchesSearch && matchesSelection
119+ } ,
69120 cell : ( info ) => {
70121 const chain = info . getValue ( )
71122 const rowIndex = info . row . index
@@ -106,28 +157,172 @@ export default function TopProtocols({ data, uniqueCategories }) {
106157 )
107158
108159 return [ ...baseColumns , ...categoryColumns ]
109- } , [ uniqueCategories ] )
160+ } , [ columnHelper , uniqueCategories ] )
161+
162+ const [ columnFilters , setColumnFilters ] = React . useState < ColumnFiltersState > ( [ ] )
163+ const [ searchValue , setSearchValue ] = React . useState ( '' )
164+ const debouncedSearch = useDebounce ( searchValue , 200 )
110165
111166 const table = useReactTable ( {
112167 data,
113168 columns,
114-
115- getCoreRowModel : getCoreRowModel ( )
169+ state : {
170+ columnFilters,
171+ columnVisibility
172+ } ,
173+ onColumnFiltersChange : setColumnFilters ,
174+ getCoreRowModel : getCoreRowModel ( ) ,
175+ getFilteredRowModel : getFilteredRowModel ( )
116176 } )
117177
178+ React . useEffect ( ( ) => {
179+ const column = table . getColumn ( 'chain' )
180+ if ( ! column ) return
181+
182+ column . setFilterValue ( { search : debouncedSearch , selected : selectedChains } )
183+ } , [ debouncedSearch , selectedChains , table ] )
184+
185+ const clearChainSelection = React . useCallback ( ( ) => {
186+ const { chain, ...queries } = router . query
187+ router . push (
188+ {
189+ pathname : router . pathname ,
190+ query : { ...queries , chain : 'None' }
191+ } ,
192+ undefined ,
193+ { shallow : true }
194+ )
195+ } , [ router ] )
196+
197+ const toggleAllChains = React . useCallback ( ( ) => {
198+ const { chain, ...queries } = router . query
199+ router . push (
200+ {
201+ pathname : router . pathname ,
202+ query : queries
203+ } ,
204+ undefined ,
205+ { shallow : true }
206+ )
207+ } , [ router ] )
208+
209+ const addChain = React . useCallback (
210+ ( newOptions : Array < string > ) => {
211+ const { chain, ...queries } = router . query
212+ router . push (
213+ {
214+ pathname : router . pathname ,
215+ query : {
216+ ...queries ,
217+ chain : newOptions
218+ }
219+ } ,
220+ undefined ,
221+ { shallow : true }
222+ )
223+ } ,
224+ [ router ]
225+ )
226+
227+ const selectOnlyOneChain = React . useCallback (
228+ ( chain : string ) => {
229+ const { chain : currentChain , ...queries } = router . query
230+ router . push (
231+ {
232+ pathname : router . pathname ,
233+ query : {
234+ ...queries ,
235+ chain : chain
236+ }
237+ } ,
238+ undefined ,
239+ { shallow : true }
240+ )
241+ } ,
242+ [ router ]
243+ )
244+
245+ const clearAllColumns = React . useCallback ( ( ) => {
246+ const { column, ...queries } = router . query
247+ router . push (
248+ {
249+ pathname : router . pathname ,
250+ query : {
251+ ...queries ,
252+ column : 'None'
253+ }
254+ } ,
255+ undefined ,
256+ { shallow : true }
257+ )
258+ } , [ router ] )
259+
260+ const toggleAllColumns = React . useCallback ( ( ) => {
261+ const { column, ...queries } = router . query
262+ router . push (
263+ {
264+ pathname : router . pathname ,
265+ query : queries
266+ } ,
267+ undefined ,
268+ { shallow : true }
269+ )
270+ } , [ router ] )
271+
272+ const addColumn = React . useCallback (
273+ ( newOptions : Array < string > ) => {
274+ const { column, ...queries } = router . query
275+ router . push (
276+ {
277+ pathname : router . pathname ,
278+ query : {
279+ ...queries ,
280+ column : newOptions
281+ }
282+ } ,
283+ undefined ,
284+ { shallow : true }
285+ )
286+ } ,
287+ [ router ]
288+ )
289+
290+ const addOnlyOneColumn = React . useCallback (
291+ ( newOption : string ) => {
292+ const { column, ...queries } = router . query
293+ router . push (
294+ {
295+ pathname : router . pathname ,
296+ query : {
297+ ...queries ,
298+ column : newOption
299+ }
300+ } ,
301+ undefined ,
302+ { shallow : true }
303+ )
304+ } ,
305+ [ router ]
306+ )
307+
118308 const prepareCsv = React . useCallback ( ( ) => {
119- const headers = [ 'Chain' , ...uniqueCategories ]
120- const csvData = data . map ( ( row ) => {
121- return {
122- Chain : row . chain ,
123- ...Object . fromEntries ( uniqueCategories . map ( ( cat ) => [ cat , row [ cat ] || '' ] ) )
309+ const visibleColumns = table . getAllLeafColumns ( ) . filter ( ( col ) => col . getIsVisible ( ) )
310+ const headers = visibleColumns . map ( ( col ) => {
311+ if ( typeof col . columnDef . header === 'string' ) {
312+ return col . columnDef . header
124313 }
314+ return col . id
125315 } )
126316
127- const rows = [ headers , ...csvData . map ( ( row ) => headers . map ( ( header ) => row [ header ] ) ) ]
317+ const dataRows = table . getFilteredRowModel ( ) . rows . map ( ( row ) =>
318+ visibleColumns . map ( ( col ) => {
319+ const value = row . getValue ( col . id )
320+ return value ?? ''
321+ } )
322+ )
128323
129- return { filename : 'top-protocols.csv' , rows : rows as ( string | number | boolean ) [ ] [ ] }
130- } , [ data , uniqueCategories ] )
324+ return { filename : 'top-protocols.csv' , rows : [ headers , ... dataRows ] as ( string | number | boolean ) [ ] [ ] }
325+ } , [ table ] )
131326
132327 return (
133328 < Layout
@@ -139,9 +334,68 @@ export default function TopProtocols({ data, uniqueCategories }) {
139334 >
140335 < div className = "flex flex-wrap items-center justify-between gap-2 rounded-md border border-(--cards-bg) bg-(--cards-bg) p-3" >
141336 < h1 className = "mr-auto text-xl font-semibold" > Protocols with highest TVL by chain on each category</ h1 >
142- < CSVDownloadButton prepareCsv = { prepareCsv } smol />
143337 </ div >
144- < VirtualTable instance = { table } />
338+ < div className = "isolate rounded-md border border-(--cards-border) bg-(--cards-bg)" >
339+ < div className = "flex flex-wrap items-center justify-end gap-2 p-2" >
340+ < label className = "relative mr-auto w-full sm:max-w-[280px]" >
341+ < span className = "sr-only" > Search chains</ span >
342+ < Icon
343+ name = "search"
344+ height = { 16 }
345+ width = { 16 }
346+ className = "absolute top-0 bottom-0 left-2 my-auto text-(--text-tertiary)"
347+ />
348+ < input
349+ value = { searchValue }
350+ onChange = { ( e ) => {
351+ setSearchValue ( e . target . value )
352+ } }
353+ placeholder = "Search..."
354+ className = "w-full rounded-md border border-(--form-control-border) bg-white p-1 pl-7 text-black max-sm:py-0.5 dark:bg-black dark:text-white"
355+ />
356+ </ label >
357+
358+ < div className = "flex items-center gap-2 max-sm:w-full max-sm:flex-col" >
359+ < SelectWithCombobox
360+ allValues = { chains }
361+ selectedValues = { selectedChains }
362+ setSelectedValues = { addChain }
363+ selectOnlyOne = { selectOnlyOneChain }
364+ toggleAll = { toggleAllChains }
365+ clearAll = { clearChainSelection }
366+ nestedMenu = { false }
367+ label = { 'Chains' }
368+ labelType = "smol"
369+ triggerProps = { {
370+ className :
371+ 'flex items-center justify-between gap-2 px-2 py-1.5 text-xs rounded-md cursor-pointer flex-nowrap relative border border-(--form-control-border) text-(--text-form) hover:bg-(--link-hover-bg) focus-visible:bg-(--link-hover-bg) font-medium w-full sm:w-auto'
372+ } }
373+ />
374+
375+ < SelectWithCombobox
376+ allValues = { columnOptions }
377+ selectedValues = { selectedColumns }
378+ setSelectedValues = { addColumn }
379+ selectOnlyOne = { addOnlyOneColumn }
380+ toggleAll = { toggleAllColumns }
381+ clearAll = { clearAllColumns }
382+ nestedMenu = { false }
383+ label = { 'Columns' }
384+ labelType = "smol"
385+ triggerProps = { {
386+ className :
387+ 'flex items-center justify-between gap-2 px-2 py-1.5 text-xs rounded-md cursor-pointer flex-nowrap relative border border-(--form-control-border) text-(--text-form) hover:bg-(--link-hover-bg) focus-visible:bg-(--link-hover-bg) font-medium w-full sm:w-auto'
388+ } }
389+ />
390+ < CSVDownloadButton prepareCsv = { prepareCsv } smol />
391+ </ div >
392+ </ div >
393+ { table . getFilteredRowModel ( ) . rows . length > 0 ? (
394+ < VirtualTable instance = { table } />
395+ ) : (
396+ < p className = "px-3 py-6 text-center text-(--text-primary)" > No results found</ p >
397+ ) }
398+ </ div >
145399 </ Layout >
146400 )
147401}
0 commit comments