-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add filter to NFT page (#1639)
Co-authored-by: Nick <ekbatanifard@gmail.com>
- Loading branch information
1 parent
860e98f
commit c8c835e
Showing
8 changed files
with
346 additions
and
101 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
158 changes: 158 additions & 0 deletions
158
packages/extension-polkagate/src/fullscreen/nft/components/Filters.tsx
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,158 @@ | ||
// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
/* eslint-disable react/jsx-max-props-per-line */ | ||
|
||
import type { FilterAction, FilterSectionProps, FilterState, ItemInformation, SortAction, SortState } from '../utils/types'; | ||
|
||
import { Grid, Typography, useTheme } from '@mui/material'; | ||
import React, { useCallback, useEffect, useReducer, useState } from 'react'; | ||
|
||
import { selectableNetworks } from '@polkadot/networks'; | ||
import { isNumber } from '@polkadot/util'; | ||
|
||
import InputFilter from '../../../components/InputFilter'; | ||
import { usePrices } from '../../../hooks'; | ||
import useTranslation from '../../../hooks/useTranslation'; | ||
import NftFilter from './NftFilter'; | ||
|
||
const initialFilterState: FilterState = { | ||
collections: false, | ||
kusama: false, | ||
nft: false, | ||
polkadot: false, | ||
unique: false | ||
}; | ||
|
||
const initialSortState = { | ||
highPrice: false, | ||
lowPrice: false, | ||
newest: false, | ||
oldest: false | ||
}; | ||
|
||
const filterReducer = (state: FilterState, action: FilterAction): FilterState => { | ||
return { | ||
...state, | ||
[action.filter]: !state[action.filter] | ||
}; | ||
}; | ||
|
||
const sortReducer = (state: SortState, action: SortAction): SortState => { | ||
return { | ||
...state, | ||
[action.enable]: true, | ||
[action.unable]: false | ||
}; | ||
}; | ||
|
||
function Filters ({ items, setItemsToShow }: FilterSectionProps): React.ReactElement { | ||
const { t } = useTranslation(); | ||
const theme = useTheme(); | ||
const prices = usePrices(); | ||
|
||
const [filters, dispatchFilter] = useReducer(filterReducer, initialFilterState); | ||
const [searchedTxt, setSearchTxt] = useState<string | undefined>(); | ||
const [sort, dispatchSort] = useReducer(sortReducer, initialSortState); | ||
const [count, setCount] = useState<number>(); | ||
|
||
const onSearch = useCallback((text: string) => { | ||
setSearchTxt(text); | ||
}, []); | ||
|
||
const getDecimal = useCallback((chainName: string) => { | ||
return selectableNetworks.find(({ network }) => network.toLowerCase() === chainName)?.decimals[0]; | ||
}, []); | ||
|
||
const calculatePrice = useCallback((item: ItemInformation) => { | ||
if (!prices?.prices || !item.price) { | ||
return 0; | ||
} | ||
|
||
const currency = item.chainName.toLowerCase().includes('kusama') | ||
? 'kusama' | ||
: 'polkadot'; | ||
const decimal = getDecimal(currency) ?? 0; | ||
|
||
return (item.price / (10 ** decimal)) * prices.prices[currency].value; | ||
}, [getDecimal, prices]); | ||
|
||
const sortItems = useCallback((itemsToSort: ItemInformation[]) => { | ||
if (sort.highPrice) { | ||
return [...itemsToSort].sort((a, b) => calculatePrice(b) - calculatePrice(a)); | ||
} | ||
|
||
if (sort.lowPrice) { | ||
return [...itemsToSort].sort((a, b) => calculatePrice(a) - calculatePrice(b)); | ||
} | ||
|
||
return itemsToSort; | ||
}, [calculatePrice, sort]); | ||
|
||
useEffect(() => { | ||
if (!items?.length) { | ||
setItemsToShow(items); | ||
|
||
return; | ||
} | ||
|
||
try { | ||
let filtered = items.filter((item) => { | ||
const matchesSearch = !searchedTxt || | ||
item.chainName.toLowerCase().includes(searchedTxt.toLowerCase()) || | ||
item.collectionId?.toString().toLowerCase().includes(searchedTxt.toLowerCase()) || | ||
item.collectionName?.toLowerCase().includes(searchedTxt.toLowerCase()) || | ||
item.name?.toLowerCase().includes(searchedTxt.toLowerCase()) || | ||
item.itemId?.toString().toLowerCase().includes(searchedTxt.toLowerCase()); | ||
|
||
const matchesNetwork = (!filters.kusama && !filters.polkadot) || | ||
(filters.kusama && item.chainName.toLowerCase().includes('kusama')) || | ||
(filters.polkadot && item.chainName.toLowerCase().includes('polkadot')); | ||
|
||
const matchesType = (!filters.nft && !filters.unique) || | ||
(filters.nft && item.isNft) || | ||
(filters.unique && !item.isNft); | ||
|
||
const matchesCollection = !filters.collections || item.isCollection; | ||
|
||
return matchesSearch && matchesNetwork && matchesType && matchesCollection; | ||
}); | ||
|
||
setCount(filtered.length); | ||
|
||
// Apply sorting | ||
filtered = sortItems(filtered); | ||
|
||
setItemsToShow(filtered); | ||
} catch (error) { | ||
console.error('Error filtering items:', error); | ||
setItemsToShow(items); // Fallback to original items on error | ||
} | ||
}, [items, filters, searchedTxt, sortItems, setItemsToShow]); | ||
|
||
return ( | ||
<Grid alignItems='center' container item justifyContent={!isNumber(count) ? 'flex-end' : 'space-between'} sx={{ mb: '10px', mt: '20px' }}> | ||
{isNumber(count) && | ||
<Typography color='text.disabled' fontSize='16px' fontWeight={500}> | ||
{t('Items')}{`(${count})`} | ||
</Typography>} | ||
<Grid alignItems='center' columnGap='15px' container item width='fit-content'> | ||
<InputFilter | ||
autoFocus={false} | ||
onChange={onSearch} | ||
placeholder={t('🔍 Search')} | ||
theme={theme} | ||
// value={searchKeyword ?? ''} | ||
/> | ||
<NftFilter | ||
dispatchFilter={dispatchFilter} | ||
dispatchSort={dispatchSort} | ||
filters={filters} | ||
sort={sort} | ||
/> | ||
</Grid> | ||
</Grid> | ||
); | ||
} | ||
|
||
export default React.memo(Filters); |
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
153 changes: 153 additions & 0 deletions
153
packages/extension-polkagate/src/fullscreen/nft/components/NftFilter.tsx
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,153 @@ | ||
// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
/* eslint-disable react/jsx-max-props-per-line */ | ||
|
||
import type { FilterAction, FilterState, SortAction, SortState } from '../utils/types'; | ||
|
||
import { FilterAltOutlined as FilterIcon, FilterList as FilterListIcon, ImportExport as ImportExportIcon } from '@mui/icons-material'; | ||
import { Divider, FormControl, FormControlLabel, Grid, Popover, Radio, RadioGroup, Typography, useTheme } from '@mui/material'; | ||
import React, { useCallback } from 'react'; | ||
|
||
import Checkbox2 from '../../../components/Checkbox2'; | ||
import { useTranslation } from '../../../hooks'; | ||
|
||
interface Props { | ||
dispatchFilter: React.Dispatch<FilterAction>; | ||
filters: FilterState; | ||
dispatchSort: React.Dispatch<SortAction>; | ||
sort: SortState; | ||
} | ||
|
||
const Filters = React.memo(function Filters ({ dispatchFilter, dispatchSort, filters, sort }: Props) { | ||
const { t } = useTranslation(); | ||
const theme = useTheme(); | ||
|
||
const onFilters = useCallback((filter: keyof FilterState) => () => { | ||
dispatchFilter({ filter }); | ||
}, [dispatchFilter]); | ||
|
||
const onSort = useCallback((enable: keyof SortState, unable: keyof SortState) => () => { | ||
dispatchSort({ enable, unable }); | ||
}, [dispatchSort]); | ||
|
||
return ( | ||
<Grid alignItems='flex-start' container display='block' item sx={{ borderRadius: '10px', maxWidth: '300px', p: '10px 20px', width: 'max-content' }}> | ||
<Grid alignItems='center' container item> | ||
<FilterListIcon sx={{ color: 'secondary.light', height: '25px', mr: '10px', width: '25px' }} /> | ||
<Typography fontSize='16px' fontWeight={400}> | ||
{t('Filters')} | ||
</Typography> | ||
<Divider sx={{ bgcolor: 'divider', height: '2px', mt: '5px', width: '100%' }} /> | ||
<Checkbox2 | ||
checked={filters.collections} | ||
iconStyle={{ marginRight: '6px', width: '20px' }} | ||
label={t('Collections')} | ||
labelStyle={{ fontSize: '16px', fontWeight: 400 }} | ||
onChange={onFilters('collections')} | ||
style={{ mt: '15px', width: '100%' }} | ||
/> | ||
<Checkbox2 | ||
checked={filters.nft} | ||
iconStyle={{ marginRight: '6px', width: '20px' }} | ||
label={t('NFTs')} | ||
labelStyle={{ fontSize: '16px', fontWeight: 400 }} | ||
onChange={onFilters('nft')} | ||
style={{ mt: '15px', width: '100%' }} | ||
/> | ||
<Checkbox2 | ||
checked={filters.unique} | ||
iconStyle={{ marginRight: '6px', width: '20px' }} | ||
label={t('Uniques')} | ||
labelStyle={{ fontSize: '16px', fontWeight: 400 }} | ||
onChange={onFilters('unique')} | ||
style={{ mt: '15px', width: '100%' }} | ||
/> | ||
<Checkbox2 | ||
checked={filters.kusama} | ||
iconStyle={{ marginRight: '6px', width: '20px' }} | ||
label={t('Kusama Asset Hub')} | ||
labelStyle={{ fontSize: '16px', fontWeight: 400 }} | ||
onChange={onFilters('kusama')} | ||
style={{ mt: '15px', width: '100%' }} | ||
/> | ||
<Checkbox2 | ||
checked={filters.polkadot} | ||
iconStyle={{ marginRight: '6px', width: '20px' }} | ||
label={t('Polkadot Asset Hub')} | ||
labelStyle={{ fontSize: '16px', fontWeight: 400 }} | ||
onChange={onFilters('polkadot')} | ||
style={{ mt: '15px', width: '100%' }} | ||
/> | ||
</Grid> | ||
<Grid alignItems='center' container item mt='15px'> | ||
<ImportExportIcon sx={{ color: 'secondary.light', height: '30px', mr: '10px', width: '30px' }} /> | ||
<Typography fontSize='16px' fontWeight={400}> | ||
{t('Sort')} | ||
</Typography> | ||
<Divider sx={{ bgcolor: 'divider', height: '2px', mt: '5px', width: '100%' }} /> | ||
<FormControl fullWidth> | ||
<RadioGroup | ||
aria-labelledby='sort-price' | ||
name='sort-price' | ||
> | ||
<FormControlLabel checked={sort.highPrice} control={<Radio style={{ color: theme.palette.secondary.main }} />} label={t('Price: High to Low')} onClick={onSort('highPrice', 'lowPrice')} slotProps={{ typography: { fontWeight: 400 } }} value='highPrice' /> | ||
<FormControlLabel checked={sort.lowPrice} control={<Radio style={{ color: theme.palette.secondary.main }} />} label={t('Price: Low to High')} onClick={onSort('lowPrice', 'highPrice')} slotProps={{ typography: { fontWeight: 400 } }} value='lowPrice' /> | ||
</RadioGroup> | ||
</FormControl> | ||
</Grid> | ||
</Grid> | ||
); | ||
}); | ||
|
||
function NftFilters ({ dispatchFilter, dispatchSort, filters, sort }: Props): React.ReactElement { | ||
const theme = useTheme(); | ||
|
||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null); | ||
|
||
const handleClose = useCallback(() => { | ||
setAnchorEl(null); | ||
}, []); | ||
|
||
const handleClick = useCallback((event: React.MouseEvent<HTMLButtonElement>) => { | ||
setAnchorEl(event.currentTarget); | ||
}, []); | ||
|
||
const open = Boolean(anchorEl); | ||
const id = open ? 'simple-popover' : undefined; | ||
|
||
return ( | ||
<> | ||
<Grid aria-describedby={id} component='button' container item onClick={handleClick} sx={{ bgcolor: 'transparent', border: 'none', height: 'fit-content', p: 0, width: 'fit-content' }}> | ||
<FilterIcon sx={{ color: 'secondary.light', cursor: 'pointer', height: '30px', width: '30px' }} /> | ||
</Grid> | ||
<Popover | ||
PaperProps={{ | ||
sx: { backgroundImage: 'none', bgcolor: 'background.paper', border: '1px solid', borderColor: theme.palette.mode === 'dark' ? 'secondary.main' : 'transparent', borderRadius: '7px', boxShadow: theme.palette.mode === 'dark' ? '0px 4px 4px rgba(255, 255, 255, 0.25)' : '0px 0px 25px 0px rgba(0, 0, 0, 0.50)' } | ||
}} | ||
anchorEl={anchorEl} | ||
anchorOrigin={{ | ||
horizontal: 'right', | ||
vertical: 'bottom' | ||
}} | ||
id={id} | ||
onClose={handleClose} | ||
open={open} | ||
sx={{ mt: '5px' }} | ||
transformOrigin={{ | ||
horizontal: 'right', | ||
vertical: 'top' | ||
}} | ||
> | ||
<Filters | ||
dispatchFilter={dispatchFilter} | ||
dispatchSort={dispatchSort} | ||
filters={filters} | ||
sort={sort} | ||
/> | ||
</Popover> | ||
</> | ||
); | ||
} | ||
|
||
export default React.memo(NftFilters); |
Oops, something went wrong.