-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: functional implementation of pool filter
- Loading branch information
Showing
11 changed files
with
396 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { Box, IconFilter, Text } from '@centrifuge/fabric' | ||
import * as React from 'react' | ||
import { useHistory, useLocation } from 'react-router-dom' | ||
import { SearchKeys } from './types' | ||
import { toKebabCase } from './utils' | ||
|
||
export type FilterMenuProps = { | ||
label: string | ||
options: string[] | ||
searchKey: SearchKeys['ASSET_CLASS' | 'POOL_STATUS'] | ||
} | ||
|
||
export function FilterMenu({ label, options, searchKey }: FilterMenuProps) { | ||
const history = useHistory() | ||
const { pathname, search } = useLocation() | ||
const [isOpen, setIsOpen] = React.useState(false) | ||
const id = React.useId() | ||
|
||
const form = React.useRef<HTMLFormElement>(null) | ||
|
||
const restSearchParams = React.useMemo(() => { | ||
const searchParams = new URLSearchParams(search) | ||
searchParams.delete(searchKey) | ||
return searchParams | ||
}, [search]) | ||
Check warning on line 25 in centrifuge-app/src/components/PoolFilter/FilterMenu.tsx GitHub Actions / build-app
|
||
|
||
const selectedOptions = React.useMemo(() => { | ||
const searchParams = new URLSearchParams(search) | ||
return searchParams.getAll(searchKey) | ||
}, [search]) | ||
Check warning on line 30 in centrifuge-app/src/components/PoolFilter/FilterMenu.tsx GitHub Actions / build-app
|
||
|
||
function handleChange() { | ||
const formData = new FormData(form.current ?? undefined) | ||
const entries = formData.getAll(searchKey) as string[] | ||
const searchParams = new URLSearchParams(entries.map((entry) => [searchKey, entry])) | ||
|
||
history.push({ | ||
pathname, | ||
search: `?${searchParams}${restSearchParams.size > 0 ? `&${restSearchParams}` : ''}`, | ||
}) | ||
} | ||
|
||
function deselectAll() { | ||
history.push({ | ||
pathname, | ||
search: restSearchParams.size > 0 ? `?${restSearchParams}` : '', | ||
}) | ||
} | ||
|
||
function selectAll() { | ||
const searchParams = new URLSearchParams(options.map((option) => [searchKey, toKebabCase(option)])) | ||
|
||
history.push({ | ||
pathname, | ||
search: `?${searchParams}${restSearchParams.size > 0 ? `&${restSearchParams}` : ''}`, | ||
}) | ||
} | ||
|
||
return ( | ||
<Box position="relative"> | ||
<Text as="button" id={`${id}-button`} aria-controls={`${id}-menu`} onClick={() => setIsOpen(!isOpen)}> | ||
{label} | ||
<IconFilter color={selectedOptions.length ? 'green' : 'black'} /> | ||
</Text> | ||
|
||
{isOpen && ( | ||
<Box as="form" ref={form} hidden={!isOpen} aria-labelledby={`${id}-button`} aria-expanded={!!isOpen}> | ||
<Box as="fieldset"> | ||
<Box as="legend" className="visually-hidden"> | ||
Filter {label} by: | ||
</Box> | ||
{options.map((option, index) => { | ||
const value = toKebabCase(option) | ||
const checked = selectedOptions.includes(value) | ||
|
||
return ( | ||
<Box as="label" key={`${value}${index}`} display="block"> | ||
<input type="checkbox" name={searchKey} value={value} onChange={handleChange} checked={checked} /> | ||
{option} | ||
</Box> | ||
) | ||
})} | ||
</Box> | ||
|
||
{selectedOptions.length === options.length ? ( | ||
<button type="button" onClick={() => deselectAll()}> | ||
Deselect all | ||
</button> | ||
) : ( | ||
<button type="button" onClick={() => selectAll()}> | ||
Select all | ||
</button> | ||
)} | ||
</Box> | ||
)} | ||
</Box> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { IconChevronDown, IconChevronUp, Stack, Text } from '@centrifuge/fabric' | ||
import * as React from 'react' | ||
import { useHistory, useLocation } from 'react-router-dom' | ||
import { SEARCH_KEYS } from './config' | ||
import { SortBy } from './types' | ||
|
||
export type SortButtonProps = { | ||
label: string | ||
searchKey: SortBy | ||
} | ||
|
||
export function SortButton({ label, searchKey }: SortButtonProps) { | ||
const history = useHistory() | ||
const { pathname, search } = useLocation() | ||
|
||
const sorting = React.useMemo(() => { | ||
const searchParams = new URLSearchParams(search) | ||
|
||
return { | ||
isActive: searchParams.get(SEARCH_KEYS.SORT_BY) === searchKey, | ||
direction: searchParams.get(SEARCH_KEYS.SORT), | ||
} | ||
}, [search]) | ||
Check warning on line 23 in centrifuge-app/src/components/PoolFilter/SortButton.tsx GitHub Actions / build-app
|
||
|
||
function handleClick() { | ||
const restSearchParams = new URLSearchParams(search) | ||
restSearchParams.delete(SEARCH_KEYS.SORT_BY) | ||
restSearchParams.delete(SEARCH_KEYS.SORT) | ||
|
||
const searchParams = new URLSearchParams({ | ||
[SEARCH_KEYS.SORT_BY]: searchKey, | ||
[SEARCH_KEYS.SORT]: sorting.direction === 'asc' ? 'desc' : 'asc', | ||
}) | ||
|
||
history.push({ | ||
pathname, | ||
search: `?${searchParams}${restSearchParams.size > 0 ? `&${restSearchParams}` : ''}`, | ||
}) | ||
} | ||
|
||
return ( | ||
<Text | ||
as="button" | ||
variant="body3" | ||
onClick={handleClick} | ||
color={sorting.isActive ? 'green' : 'blue'} | ||
aria-label={ | ||
!sorting.isActive | ||
? `Sort ${label}` | ||
: sorting.direction === 'asc' | ||
? `Sort ${label} descending` | ||
: `Sort ${label} ascending` | ||
} | ||
aria-live | ||
Check warning on line 54 in centrifuge-app/src/components/PoolFilter/SortButton.tsx GitHub Actions / build-app
|
||
> | ||
{label} | ||
|
||
<Stack as="span" width={14}> | ||
<IconChevronUp size={14} color={sorting.isActive && sorting.direction === 'asc' ? 'green' : 'gray'} /> | ||
<IconChevronDown size={14} color={sorting.isActive && sorting.direction === 'desc' ? 'green' : 'gray'} /> | ||
</Stack> | ||
</Text> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { config } from '../../config' | ||
import { FilterMenuProps } from './FilterMenu' | ||
import { SortButtonProps } from './SortButton' | ||
|
||
export const SEARCH_KEYS = { | ||
SORT_BY: 'sort-by', | ||
SORT: 'sort', | ||
ASSET_CLASS: 'asset-class', | ||
POOL_STATUS: 'pool-status', | ||
VALUE_LOCKED: 'value-locked', | ||
APR: 'apr', | ||
} as const | ||
|
||
export const poolFilterConfig = { | ||
assetClass: { | ||
label: 'Asset class', | ||
options: [...config.assetClasses, 'Credit'], // todo: is 'Credit' tinlke specific or is the config outdated? | ||
searchKey: SEARCH_KEYS.ASSET_CLASS, | ||
} as FilterMenuProps, | ||
poolStatus: { | ||
label: 'Pool status', | ||
options: ['Open for investments', 'Maker pool', 'Closed'], | ||
searchKey: SEARCH_KEYS.POOL_STATUS, | ||
} as FilterMenuProps, | ||
valueLocked: { | ||
label: 'Value locked', | ||
searchKey: SEARCH_KEYS.VALUE_LOCKED, | ||
} as SortButtonProps, | ||
apr: { | ||
label: 'APR', | ||
searchKey: SEARCH_KEYS.APR, | ||
} as SortButtonProps, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { Grid, Text } from '@centrifuge/fabric' | ||
import * as React from 'react' | ||
import { poolFilterConfig } from './config' | ||
import { FilterMenu } from './FilterMenu' | ||
import { SortButton } from './SortButton' | ||
|
||
export function PoolFilter() { | ||
return ( | ||
<Grid gridTemplateColumns="repeat(5, minmax(0, 1fr))" alignItems="start"> | ||
<Text>Pool name</Text> | ||
|
||
<FilterMenu {...poolFilterConfig.assetClass} /> | ||
|
||
<SortButton {...poolFilterConfig.valueLocked} /> | ||
|
||
<SortButton {...poolFilterConfig.apr} /> | ||
|
||
<FilterMenu {...poolFilterConfig.poolStatus} /> | ||
</Grid> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { SEARCH_KEYS } from './config' | ||
|
||
export type SortDirection = 'asc' | 'desc' | ||
export type SortBy = 'value-locked' | 'apr' | ||
export type FilterBy = 'asset-class' | 'pool-status' | ||
export type SearchKeys = typeof SEARCH_KEYS |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { PoolCardProps } from '../../pages/Pools' | ||
import { poolFilterConfig, SEARCH_KEYS } from './config' | ||
import { SortBy, SortDirection } from './types' | ||
|
||
export function toKebabCase(string: string) { | ||
return string.toLowerCase().split(' ').join('-') | ||
} | ||
|
||
export function filterPools(pools: PoolCardProps[], searchParams: URLSearchParams) { | ||
let filtered = pools | ||
const assetClasses = searchParams.getAll(poolFilterConfig.assetClass.searchKey) | ||
const poolStatuses = searchParams.getAll(poolFilterConfig.poolStatus.searchKey) | ||
const sortDirection = searchParams.get('sort') as SortDirection | ||
const sortBy = searchParams.get('sort-by') as SortBy | ||
|
||
if (assetClasses.length) { | ||
filtered = filtered.filter((pool) => assetClasses.includes(toKebabCase(pool.assetClass))) | ||
} | ||
|
||
if (poolStatuses.length) { | ||
filtered = filtered.filter((pool) => poolStatuses.includes(toKebabCase(pool.status))) | ||
} | ||
|
||
if (sortDirection && sortBy) { | ||
filtered = sortData(filtered, sortBy, sortDirection) | ||
} | ||
|
||
return filtered | ||
} | ||
|
||
const sortMap = { | ||
[SEARCH_KEYS.VALUE_LOCKED]: (item: PoolCardProps) => item.valueLocked.toNumber(), | ||
[SEARCH_KEYS.APR]: (item: PoolCardProps) => (item.apr ? item.apr.toDecimal().toNumber() : 0), | ||
} | ||
|
||
function sortData(data: PoolCardProps[], sortBy: SortBy, sortDirection: SortDirection) { | ||
if (sortMap.hasOwnProperty(sortBy)) { | ||
const sortFunction = sortMap[sortBy] | ||
|
||
data.sort((a, b) => { | ||
const valueA = sortFunction(a) | ||
const valueB = sortFunction(b) | ||
|
||
return sortDirection === 'asc' ? compareNumeric(valueA, valueB) : compareNumeric(valueB, valueA) | ||
}) | ||
} | ||
|
||
return data | ||
} | ||
|
||
function compareNumeric(a: number, b: number) { | ||
return a - b | ||
} |
Oops, something went wrong.