Commit 6e282a6d authored by Jack Short's avatar Jack Short Committed by GitHub

style: updating explore table (#5043)

* style: updating explore table

* pr comments

* addressing pr comments

* moved loading table to its own component

* removing clsx

* updating key for row

* updating spacing
parent d3a2e14d
...@@ -23,7 +23,7 @@ const BannerContainer = styled.div` ...@@ -23,7 +23,7 @@ const BannerContainer = styled.div`
height: 100%; height: 100%;
gap: 14px; gap: 14px;
margin-top: 4px; margin-top: 4px;
margin-bottom: 30px; margin-bottom: 6px;
} }
` `
......
...@@ -38,6 +38,7 @@ export const address = style([ ...@@ -38,6 +38,7 @@ export const address = style([
]) ])
export const verifiedBadge = sprinkles({ export const verifiedBadge = sprinkles({
marginLeft: '4',
display: 'inline-block', display: 'inline-block',
paddingTop: '4', paddingTop: '4',
height: '28', height: '28',
......
import { formatEther } from '@ethersproject/units'
import { SquareArrowDownIcon, SquareArrowUpIcon, VerifiedIcon } from 'nft/components/icons'
import { Denomination } from 'nft/types'
import { volumeFormatter } from 'nft/utils'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { ethNumberStandardFormatter, formatWeiToDecimal } from '../../../utils/currency' import { ethNumberStandardFormatter, formatWeiToDecimal } from '../../../utils/currency'
import { putCommas } from '../../../utils/putCommas'
import { formatChange } from '../../../utils/toSignificant' import { formatChange } from '../../../utils/toSignificant'
import { Box } from '../../Box' import { Box } from '../../Box'
import { Column, Row } from '../../Flex' import { Column, Row } from '../../Flex'
import { VerifiedIcon } from '../../icons'
import * as styles from './Cells.css' import * as styles from './Cells.css'
const CollectionNameContainer = styled.div`
display: flex;
padding: 14px 0px 14px 8px;
align-items: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`
const CollectionName = styled.div`
margin-left: 8px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`
const TruncatedSubHeader = styled(ThemedText.SubHeader)`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`
const RoundedImage = styled.div<{ src?: string }>`
height: 36px;
width: 36px;
border-radius: 36px;
background: ${({ src, theme }) => (src ? `url(${src})` : theme.backgroundModule)};
background-size: cover;
background-position: center;
flex-shrink: 0;
`
const ChangeCellContainer = styled.div<{ change: number }>`
display: flex;
color: ${({ theme, change }) => (change >= 0 ? theme.accentSuccess : theme.accentFailure)};
justify-content: end;
align-items: center;
gap: 2px;
`
const EthContainer = styled.div`
display: flex;
justify-content: end;
`
interface CellProps { interface CellProps {
value: { value: {
logo?: string logo?: string
...@@ -19,48 +69,87 @@ interface CellProps { ...@@ -19,48 +69,87 @@ interface CellProps {
export const CollectionTitleCell = ({ value }: CellProps) => { export const CollectionTitleCell = ({ value }: CellProps) => {
return ( return (
<Row as="span" style={{ marginLeft: '52px' }}> <CollectionNameContainer>
<img className={styles.logo} src={value.logo} alt={`${value.name} logo`} height={44} width={44} /> <RoundedImage src={value.logo} />
<span className={styles.title}>{value.name}</span> <CollectionName>
<TruncatedSubHeader>{value.name}</TruncatedSubHeader>
</CollectionName>
{value.isVerified && ( {value.isVerified && (
<span className={styles.verifiedBadge}> <span className={styles.verifiedBadge}>
<VerifiedIcon /> <VerifiedIcon />
</span> </span>
)} )}
</Row> </CollectionNameContainer>
) )
} }
export const WithCommaCell = ({ value }: CellProps) => <span>{value.value ? putCommas(value.value) : '-'}</span> export const DiscreteNumberCell = ({ value }: CellProps) => (
<span>{value.value ? volumeFormatter(value.value) : '-'}</span>
export const EthCell = ({ value }: { value: number }) => (
<Row justifyContent="flex-end" color="textPrimary">
{value ? <>{formatWeiToDecimal(value.toString(), true)} ETH</> : '-'}
</Row>
) )
export const VolumeCell = ({ value }: CellProps) => ( const getDenominatedValue = (denomination: Denomination, inWei: boolean, value?: number, usdPrice?: number) => {
<Row justifyContent="flex-end" color="textPrimary"> if (denomination === Denomination.ETH) return value
{value.value ? <>{ethNumberStandardFormatter(value.value.toString())} ETH</> : '-'} if (usdPrice && value) return usdPrice * (inWei ? parseFloat(formatEther(value)) : value)
</Row>
)
export const EthWithDayChange = ({ value }: CellProps) => ( return undefined
<Column gap="4"> }
<VolumeCell value={{ value: value.value }} />
{value.change ? ( export const EthCell = ({
<Box value,
as="span" denomination,
color={value.change > 0 ? 'green' : 'accentFailure'} usdPrice,
fontWeight="normal" }: {
fontSize="12" value?: number
position="relative" denomination: Denomination
> usdPrice?: number
{value.change > 0 && '+'} }) => {
{formatChange(value.change)}% const denominatedValue = getDenominatedValue(denomination, true, value, usdPrice)
</Box> const formattedValue = denominatedValue
) : null} ? denomination === Denomination.ETH
</Column> ? formatWeiToDecimal(denominatedValue.toString(), true) + ' ETH'
: ethNumberStandardFormatter(denominatedValue, true, false, true)
: '-'
return (
<EthContainer>
<ThemedText.BodyPrimary>{value ? formattedValue : '-'}</ThemedText.BodyPrimary>
</EthContainer>
)
}
export const VolumeCell = ({
value,
denomination,
usdPrice,
}: {
value?: number
denomination: Denomination
usdPrice?: number
}) => {
const denominatedValue = getDenominatedValue(denomination, false, value, usdPrice)
const formattedValue = denominatedValue
? denomination === Denomination.ETH
? ethNumberStandardFormatter(denominatedValue.toString(), false, false, true) + ' ETH'
: ethNumberStandardFormatter(denominatedValue, true, false, true)
: '-'
return (
<EthContainer>
<ThemedText.BodyPrimary>{value ? formattedValue : '-'}</ThemedText.BodyPrimary>
</EthContainer>
)
}
export const ChangeCell = ({ change }: { change?: number }) => (
<ChangeCellContainer change={change ?? 0}>
{!change || change > 0 ? (
<SquareArrowUpIcon width="20px" height="20px" />
) : (
<SquareArrowDownIcon width="20px" height="20px" />
)}
<ThemedText.BodyPrimary color="currentColor">{change ? Math.abs(Math.round(change)) : 0}%</ThemedText.BodyPrimary>
</ChangeCellContainer>
) )
export const WeiWithDayChange = ({ value }: CellProps) => ( export const WeiWithDayChange = ({ value }: CellProps) => (
...@@ -82,21 +171,3 @@ export const WeiWithDayChange = ({ value }: CellProps) => ( ...@@ -82,21 +171,3 @@ export const WeiWithDayChange = ({ value }: CellProps) => (
) : null} ) : null}
</Column> </Column>
) )
export const CommaWithDayChange = ({ value }: CellProps) => (
<Column gap="4">
<WithCommaCell value={value} />
{value.change ? (
<Box
as="span"
color={value.change > 0 ? 'green' : 'accentFailure'}
fontWeight="normal"
fontSize="12"
position="relative"
>
{value.change > 0 && '+'}
{formatChange(value.change)}%
</Box>
) : null}
</Column>
)
import { CellProps, Column } from 'react-table' import { BigNumber } from '@ethersproject/bignumber'
import { useMemo } from 'react'
import { CellProps, Column, Row } from 'react-table'
import { CollectionTableColumn } from '../../types' import { CollectionTableColumn } from '../../types'
import { import { ChangeCell, CollectionTitleCell, DiscreteNumberCell, EthCell, VolumeCell } from './Cells/Cells'
CollectionTitleCell,
CommaWithDayChange,
EthWithDayChange,
WeiWithDayChange,
WithCommaCell,
} from './Cells/Cells'
import { Table } from './Table' import { Table } from './Table'
export enum ColumnHeaders { export enum ColumnHeaders {
Volume = 'Volume', Volume = 'Volume',
VolumeChange = 'Volume change',
Floor = 'Floor', Floor = 'Floor',
FloorChange = 'Floor change',
Sales = 'Sales', Sales = 'Sales',
Items = 'Items', Items = 'Items',
Owners = 'Owners', Owners = 'Owners',
} }
const columns: Column<CollectionTableColumn>[] = [ const compareFloats = (a: number, b: number): 1 | -1 => {
{ return Math.round(a * 100000) >= Math.round(b * 100000) ? 1 : -1
Header: 'Collection', }
accessor: 'collection',
Cell: CollectionTitleCell,
},
{
id: ColumnHeaders.Volume,
Header: ColumnHeaders.Volume,
accessor: ({ volume }) => volume.value,
sortDescFirst: true,
Cell: function EthDayChanget(cell: CellProps<CollectionTableColumn>) {
return <EthWithDayChange value={cell.row.original.volume} />
},
},
{
id: ColumnHeaders.Floor,
Header: ColumnHeaders.Floor,
accessor: ({ floor }) => floor.value,
sortDescFirst: true,
Cell: function weiDayChange(cell: CellProps<CollectionTableColumn>) {
return <WeiWithDayChange value={cell.row.original.floor} />
},
},
{
id: ColumnHeaders.Sales,
Header: ColumnHeaders.Sales,
accessor: 'sales',
sortDescFirst: true,
Cell: function withCommaCell(cell: CellProps<CollectionTableColumn>) {
return <WithCommaCell value={{ value: cell.row.original.sales }} />
},
},
{
id: ColumnHeaders.Items,
Header: ColumnHeaders.Items,
accessor: 'totalSupply',
sortDescFirst: true,
Cell: function withCommaCell(cell: CellProps<CollectionTableColumn>) {
return <WithCommaCell value={{ value: cell.row.original.totalSupply }} />
},
},
{
Header: ColumnHeaders.Owners,
accessor: ({ owners }) => owners.value,
sortDescFirst: true,
Cell: function commaDayChange(cell: CellProps<CollectionTableColumn>) {
return <CommaWithDayChange value={cell.row.original.owners} />
},
},
]
const CollectionTable = ({ data }: { data: CollectionTableColumn[] }) => { const CollectionTable = ({ data }: { data: CollectionTableColumn[] }) => {
const floorSort = useMemo(() => {
return (rowA: Row<CollectionTableColumn>, rowB: Row<CollectionTableColumn>) => {
const aFloor = BigNumber.from(rowA.original.floor.value)
const bFloor = BigNumber.from(rowB.original.floor.value)
return aFloor.gte(bFloor) ? 1 : -1
}
}, [])
const floorChangeSort = useMemo(() => {
return (rowA: Row<CollectionTableColumn>, rowB: Row<CollectionTableColumn>) => {
return compareFloats(rowA.original.floor.change, rowB.original.floor.change)
}
}, [])
const volumeSort = useMemo(() => {
return (rowA: Row<CollectionTableColumn>, rowB: Row<CollectionTableColumn>) => {
return compareFloats(rowA.original.volume.value, rowB.original.volume.value)
}
}, [])
const volumeChangeSort = useMemo(() => {
return (rowA: Row<CollectionTableColumn>, rowB: Row<CollectionTableColumn>) => {
return compareFloats(rowA.original.volume.change, rowB.original.volume.change)
}
}, [])
const columns: Column<CollectionTableColumn>[] = useMemo(
() => [
{
Header: 'Collection name',
accessor: 'collection',
Cell: CollectionTitleCell,
disableSortBy: true,
},
{
id: ColumnHeaders.Floor,
Header: ColumnHeaders.Floor,
accessor: ({ floor }) => floor.value,
sortType: floorSort,
Cell: function ethCell(cell: CellProps<CollectionTableColumn>) {
return (
<EthCell
value={cell.row.original.floor.value}
denomination={cell.row.original.denomination}
usdPrice={cell.row.original.usdPrice}
/>
)
},
},
{
id: ColumnHeaders.FloorChange,
Header: ColumnHeaders.FloorChange,
accessor: ({ floor }) => floor.value,
sortDescFirst: true,
sortType: floorChangeSort,
Cell: function changeCell(cell: CellProps<CollectionTableColumn>) {
return <ChangeCell change={cell.row.original.floor.change} />
},
},
{
id: ColumnHeaders.Volume,
Header: ColumnHeaders.Volume,
accessor: ({ volume }) => volume.value,
sortDescFirst: true,
sortType: volumeSort,
Cell: function volumeCell(cell: CellProps<CollectionTableColumn>) {
return (
<VolumeCell
value={cell.row.original.volume.value}
denomination={cell.row.original.denomination}
usdPrice={cell.row.original.usdPrice}
/>
)
},
},
{
id: ColumnHeaders.VolumeChange,
Header: ColumnHeaders.VolumeChange,
accessor: ({ volume }) => volume.value,
sortDescFirst: true,
sortType: volumeChangeSort,
Cell: function changeCell(cell: CellProps<CollectionTableColumn>) {
return <ChangeCell change={cell.row.original.volume.change} />
},
},
{
id: ColumnHeaders.Items,
Header: ColumnHeaders.Items,
accessor: 'totalSupply',
sortDescFirst: true,
Cell: function discreteNumberCell(cell: CellProps<CollectionTableColumn>) {
return <DiscreteNumberCell value={{ value: cell.row.original.totalSupply }} />
},
},
{
Header: ColumnHeaders.Owners,
accessor: ({ owners }) => owners.value,
sortDescFirst: true,
Cell: function discreteNumberCell(cell: CellProps<CollectionTableColumn>) {
return <DiscreteNumberCell value={cell.row.original.owners} />
},
},
],
[floorChangeSort, floorSort, volumeChangeSort, volumeSort]
)
return ( return (
<> <>
<Table <Table
hiddenColumns={[ColumnHeaders.Volume, ColumnHeaders.Owners, ColumnHeaders.Items, ColumnHeaders.Sales]} smallHiddenColumns={[
ColumnHeaders.Items,
ColumnHeaders.FloorChange,
ColumnHeaders.Volume,
ColumnHeaders.VolumeChange,
ColumnHeaders.Owners,
]}
mediumHiddenColumns={[
ColumnHeaders.Items,
ColumnHeaders.FloorChange,
ColumnHeaders.VolumeChange,
ColumnHeaders.Owners,
]}
largeHiddenColumns={[ColumnHeaders.Items, ColumnHeaders.Owners]}
{...{ data, columns }} {...{ data, columns }}
/> />
</> </>
......
...@@ -8,7 +8,7 @@ export const section = style([ ...@@ -8,7 +8,7 @@ export const section = style([
paddingRight: { sm: '16', xl: '0' }, paddingRight: { sm: '16', xl: '0' },
}), }),
{ {
maxWidth: '1000px', maxWidth: '1200px',
margin: '0 auto', margin: '0 auto',
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
...@@ -155,6 +155,7 @@ export const table = style([ ...@@ -155,6 +155,7 @@ export const table = style([
borderSpacing: '0px 40px', borderSpacing: '0px 40px',
}, },
sprinkles({ sprinkles({
background: 'backgroundSurface',
width: 'full', width: 'full',
borderRadius: '12', borderRadius: '12',
borderStyle: 'none', borderStyle: 'none',
...@@ -178,15 +179,12 @@ export const th = style([ ...@@ -178,15 +179,12 @@ export const th = style([
}, },
}, },
sprinkles({ sprinkles({
color: { default: 'textSecondary', hover: 'textPrimary' }, color: { default: 'textSecondary' },
cursor: 'pointer',
paddingTop: '12', paddingTop: '12',
paddingBottom: '12', paddingBottom: '12',
}), }),
]) ])
export const tr = sprinkles({ cursor: 'pointer' })
export const rank = sprinkles({ export const rank = sprinkles({
color: 'textSecondary', color: 'textSecondary',
position: 'absolute', position: 'absolute',
...@@ -198,7 +196,6 @@ export const rank = sprinkles({ ...@@ -198,7 +196,6 @@ export const rank = sprinkles({
export const td = style([ export const td = style([
body, body,
{ {
verticalAlign: 'middle',
selectors: { selectors: {
'&:nth-last-child(1)': { '&:nth-last-child(1)': {
paddingRight: '20px', paddingRight: '20px',
...@@ -207,15 +204,32 @@ export const td = style([ ...@@ -207,15 +204,32 @@ export const td = style([
}, },
sprinkles({ sprinkles({
maxWidth: '160', maxWidth: '160',
paddingTop: '10', paddingY: '8',
paddingBottom: '10', textAlign: 'right',
position: 'relative',
}),
])
export const loadingTd = style([
body,
{
selectors: {
'&:nth-last-child(1)': {
paddingRight: '20px',
},
},
},
sprinkles({
maxWidth: '160',
paddingY: '8',
textAlign: 'right', textAlign: 'right',
position: 'relative', position: 'relative',
}), }),
]) ])
export const trendingOptions = sprinkles({ export const trendingOptions = sprinkles({
marginBottom: '32', marginTop: '36',
marginBottom: '20',
height: '44', height: '44',
borderRadius: '12', borderRadius: '12',
borderWidth: '2px', borderWidth: '2px',
......
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import clsx from 'clsx' import clsx from 'clsx'
import { LoadingBubble } from 'components/Tokens/loading'
import { useWindowSize } from 'hooks/useWindowSize'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Column, IdType, useSortBy, useTable } from 'react-table' import { Column, ColumnInstance, HeaderGroup, IdType, useSortBy, useTable } from 'react-table'
import { isMobile } from 'utils/userAgent' import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { Box } from '../../components/Box' import { Box } from '../../components/Box'
import { CollectionTableColumn } from '../../types' import { CollectionTableColumn } from '../../types'
...@@ -13,49 +13,126 @@ import { ArrowRightIcon } from '../icons' ...@@ -13,49 +13,126 @@ import { ArrowRightIcon } from '../icons'
import { ColumnHeaders } from './CollectionTable' import { ColumnHeaders } from './CollectionTable'
import * as styles from './Explore.css' import * as styles from './Explore.css'
const RankCellContainer = styled.div`
display: flex;
align-items: center;
padding-left: 24px;
gap: 12px;
`
const StyledRow = styled.tr`
cursor: pointer;
:hover {
background: ${({ theme }) => theme.stateOverlayHover};
}
:active {
background: ${({ theme }) => theme.stateOverlayPressed};
}
`
const StyledLoadingRow = styled.tr`
height: 80px;
`
const StyledHeader = styled.th<{ isFirstHeader: boolean }>`
${({ isFirstHeader }) => !isFirstHeader && `cursor: pointer;`}
:hover {
${({ theme, isFirstHeader }) => !isFirstHeader && `opacity: ${theme.opacity.hover};`}
}
:active {
${({ theme, isFirstHeader }) => !isFirstHeader && `opacity: ${theme.opacity.click};`}
}
`
const StyledLoadingHolder = styled.div`
display: flex;
width: 100%;
justify-content: end;
align-items: center;
`
const StyledCollectionNameHolder = styled.div`
display: flex;
margin-left: 24px;
gap: 8px;
align-items: center;
`
const StyledImageHolder = styled(LoadingBubble)`
width: 36px;
height: 36px;
border-radius: 36px;
`
const StyledRankHolder = styled(LoadingBubble)`
width: 8px;
height: 16px;
margin-right: 12px;
`
const DEFAULT_TRENDING_TABLE_QUERY_AMOUNT = 10
interface TableProps<D extends Record<string, unknown>> { interface TableProps<D extends Record<string, unknown>> {
columns: Column<CollectionTableColumn>[] columns: Column<CollectionTableColumn>[]
data: CollectionTableColumn[] data: CollectionTableColumn[]
hiddenColumns: IdType<D>[] smallHiddenColumns: IdType<D>[]
mediumHiddenColumns: IdType<D>[]
largeHiddenColumns: IdType<D>[]
classNames?: { classNames?: {
td: string td: string
} }
} }
export function Table<D extends Record<string, unknown>>({ export function Table<D extends Record<string, unknown>>({
columns, columns,
data, data,
hiddenColumns, smallHiddenColumns,
mediumHiddenColumns,
largeHiddenColumns,
classNames, classNames,
...props ...props
}: TableProps<D>) { }: TableProps<D>) {
const { chainId } = useWeb3React() const theme = useTheme()
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow, setHiddenColumns } = useTable( const { width } = useWindowSize()
{
columns, const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow, setHiddenColumns, visibleColumns } =
data, useTable(
initialState: { {
sortBy: [ columns,
{ data,
desc: true, initialState: {
id: ColumnHeaders.Volume, sortBy: [
}, {
], desc: true,
id: ColumnHeaders.Volume,
},
],
},
...props,
}, },
...props, useSortBy
}, )
useSortBy
)
const navigate = useNavigate() const navigate = useNavigate()
useEffect(() => { useEffect(() => {
if (hiddenColumns && isMobile) { if (!width) return
setHiddenColumns(hiddenColumns)
if (width < theme.breakpoint.sm) {
setHiddenColumns(smallHiddenColumns)
} else if (width < theme.breakpoint.md) {
setHiddenColumns(mediumHiddenColumns)
} else if (width < theme.breakpoint.lg) {
setHiddenColumns(largeHiddenColumns)
} else { } else {
setHiddenColumns([]) setHiddenColumns([])
} }
}, [hiddenColumns, setHiddenColumns]) }, [width, setHiddenColumns, columns, smallHiddenColumns, mediumHiddenColumns, largeHiddenColumns, theme.breakpoint])
if (data.length === 0) {
return <LoadingTable headerGroups={headerGroups} visibleColumns={visibleColumns} {...getTableProps()} />
}
return ( return (
<table {...getTableProps()} className={styles.table}> <table {...getTableProps()} className={styles.table}>
...@@ -64,13 +141,14 @@ export function Table<D extends Record<string, unknown>>({ ...@@ -64,13 +141,14 @@ export function Table<D extends Record<string, unknown>>({
<tr {...headerGroup.getHeaderGroupProps()} key={headerGroup.id}> <tr {...headerGroup.getHeaderGroupProps()} key={headerGroup.id}>
{headerGroup.headers.map((column, index) => { {headerGroup.headers.map((column, index) => {
return ( return (
<th <StyledHeader
className={styles.th} className={styles.th}
{...column.getHeaderProps(column.getSortByToggleProps())} {...column.getHeaderProps(column.getSortByToggleProps())}
style={{ style={{
textAlign: index === 0 ? 'left' : 'right', textAlign: index === 0 ? 'left' : 'right',
paddingLeft: index === 0 ? '52px' : 0, paddingLeft: index === 0 ? '52px' : 0,
}} }}
isFirstHeader={index === 0}
key={index} key={index}
> >
<Box as="span" color="accentAction" position="relative"> <Box as="span" color="accentAction" position="relative">
...@@ -87,7 +165,7 @@ export function Table<D extends Record<string, unknown>>({ ...@@ -87,7 +165,7 @@ export function Table<D extends Record<string, unknown>>({
<Box as="span" paddingLeft={column.isSorted ? '18' : '0'}> <Box as="span" paddingLeft={column.isSorted ? '18' : '0'}>
{column.render('Header')} {column.render('Header')}
</Box> </Box>
</th> </StyledHeader>
) )
})} })}
</tr> </tr>
...@@ -98,32 +176,101 @@ export function Table<D extends Record<string, unknown>>({ ...@@ -98,32 +176,101 @@ export function Table<D extends Record<string, unknown>>({
prepareRow(row) prepareRow(row)
return ( return (
<TraceEvent <StyledRow
events={[Event.onClick]} {...row.getRowProps()}
name={EventName.NFT_TRENDING_ROW_SELECTED} key={row.id}
properties={{ collection_address: row.original.collection.address, chain_id: chainId }} onClick={() => navigate(`/nfts/collection/${row.original.collection.address}`)}
element={ElementName.NFT_TRENDING_ROW}
key={i}
> >
<tr {row.cells.map((cell, cellIndex) => {
className={styles.tr} return (
{...row.getRowProps()} <td className={clsx(styles.td, classNames?.td)} {...cell.getCellProps()} key={cellIndex}>
key={i} {cellIndex === 0 ? (
onClick={() => navigate(`/nfts/collection/${row.original.collection.address}`)} <RankCellContainer>
> <ThemedText.BodySecondary fontSize="14px" lineHeight="20px">
{row.cells.map((cell, cellIndex) => { {i + 1}
return ( </ThemedText.BodySecondary>
<td className={clsx(styles.td, classNames?.td)} {...cell.getCellProps()} key={cellIndex}> {cell.render('Cell')}
{cellIndex === 0 ? <span className={styles.rank}>{i + 1}</span> : null} </RankCellContainer>
{cell.render('Cell')} ) : (
</td> cell.render('Cell')
) )}
})} </td>
</tr> )
</TraceEvent> })}
</StyledRow>
) )
})} })}
</tbody> </tbody>
</table> </table>
) )
} }
interface LoadingTableProps {
headerGroups: HeaderGroup<CollectionTableColumn>[]
visibleColumns: ColumnInstance<CollectionTableColumn>[]
}
function LoadingTable({ headerGroups, visibleColumns, ...props }: LoadingTableProps) {
return (
<table {...props} className={styles.table}>
<thead className={styles.thead}>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()} key={headerGroup.id}>
{headerGroup.headers.map((column, index) => {
return (
<StyledHeader
className={styles.th}
{...column.getHeaderProps(column.getSortByToggleProps())}
style={{
textAlign: index === 0 ? 'left' : 'right',
paddingLeft: index === 0 ? '52px' : 0,
}}
isFirstHeader={index === 0}
key={index}
>
<Box as="span" color="accentAction" position="relative">
{column.isSorted ? (
column.isSortedDesc ? (
<ArrowRightIcon style={{ transform: 'rotate(90deg)', position: 'absolute' }} />
) : (
<ArrowRightIcon style={{ transform: 'rotate(-90deg)', position: 'absolute' }} />
)
) : (
''
)}
</Box>
<Box as="span" paddingLeft={column.isSorted ? '18' : '0'}>
{column.render('Header')}
</Box>
</StyledHeader>
)
})}
</tr>
))}
</thead>
<tbody {...props}>
{[...Array(DEFAULT_TRENDING_TABLE_QUERY_AMOUNT)].map((_, index) => (
<StyledLoadingRow key={index}>
{[...Array(visibleColumns.length)].map((_, cellIndex) => {
return (
<td className={styles.loadingTd} key={cellIndex}>
{cellIndex === 0 ? (
<StyledCollectionNameHolder>
<StyledRankHolder />
<StyledImageHolder />
<LoadingBubble />
</StyledCollectionNameHolder>
) : (
<StyledLoadingHolder>
<LoadingBubble />
</StyledLoadingHolder>
)}
</td>
)
})}
</StyledLoadingRow>
))}
</tbody>
</table>
)
}
import clsx from 'clsx' import ms from 'ms.macro'
import { CollectionTableColumn, Denomination, TimePeriod, VolumeType } from 'nft/types'
import { fetchPrice } from 'nft/utils'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { Box } from '../../components/Box'
import { Column, Row } from '../../components/Flex'
import { headlineMedium } from '../../css/common.css'
import { fetchTrendingCollections } from '../../queries' import { fetchTrendingCollections } from '../../queries'
import { CollectionTableColumn, TimePeriod, VolumeType } from '../../types'
import CollectionTable from './CollectionTable' import CollectionTable from './CollectionTable'
import * as styles from './Explore.css'
const timeOptions: { label: string; value: TimePeriod }[] = [ const timeOptions: { label: string; value: TimePeriod }[] = [
{ label: '24 hour', value: TimePeriod.OneDay }, { label: '1D', value: TimePeriod.OneDay },
{ label: '7 day', value: TimePeriod.SevenDays }, { label: '1W', value: TimePeriod.SevenDays },
{ label: '30 day', value: TimePeriod.ThirtyDays }, { label: '1M', value: TimePeriod.ThirtyDays },
{ label: 'All time', value: TimePeriod.AllTime }, { label: 'All', value: TimePeriod.AllTime },
] ]
const ExploreContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
max-width: 1200px;
`
const StyledHeader = styled.div`
color: ${({ theme }) => theme.textPrimary};
font-size: 36px;
line-height: 44px;
weight: 500;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
font-size: 20px;
line-height: 28px;
}
`
const FiltersRow = styled.div`
display: flex;
justify-content: space-between;
margin-top: 36px;
margin-bottom: 20px;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
margin-bottom: 16px;
margin-top: 16px;
}
`
const Filter = styled.div`
display: flex;
outline: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 16px;
padding: 4px;
`
const Selector = styled.div<{ active: boolean }>`
padding: 8px 12px;
border-radius: 12px;
background: ${({ active, theme }) => (active ? theme.backgroundInteractive : 'none')};
cursor: pointer;
:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
:active {
opacity: ${({ theme }) => theme.opacity.click};
}
`
const StyledSelectorText = styled(ThemedText.SubHeader)<{ active: boolean }>`
color: ${({ theme, active }) => (active ? theme.textPrimary : theme.textSecondary)};
`
const TrendingCollections = () => { const TrendingCollections = () => {
const [timePeriod, setTimePeriod] = useState<TimePeriod>(TimePeriod.OneDay) const [timePeriod, setTimePeriod] = useState<TimePeriod>(TimePeriod.OneDay)
const [isEthToggled, setEthToggled] = useState(true)
const { isSuccess, data } = useQuery( const { isSuccess, data } = useQuery(
['trendingCollections', timePeriod], ['trendingCollections', timePeriod],
...@@ -33,6 +90,13 @@ const TrendingCollections = () => { ...@@ -33,6 +90,13 @@ const TrendingCollections = () => {
} }
) )
const { data: usdPrice } = useQuery(['fetchPrice', {}], () => fetchPrice(), {
refetchOnReconnect: false,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchInterval: ms`1m`,
})
const trendingCollections = useMemo(() => { const trendingCollections = useMemo(() => {
if (isSuccess && data) { if (isSuccess && data) {
return data.map((d) => ({ return data.map((d) => ({
...@@ -58,39 +122,46 @@ const TrendingCollections = () => { ...@@ -58,39 +122,46 @@ const TrendingCollections = () => {
}, },
sales: d.sales, sales: d.sales,
totalSupply: d.totalSupply, totalSupply: d.totalSupply,
denomination: isEthToggled ? Denomination.ETH : Denomination.USD,
usdPrice,
})) }))
} else return [] as CollectionTableColumn[] } else return [] as CollectionTableColumn[]
}, [data, isSuccess]) }, [data, isSuccess, isEthToggled, usdPrice])
return ( return (
<Box width="full" className={styles.section}> <ExploreContainer>
<Column width="full"> <StyledHeader>Trending NFT collections</StyledHeader>
<Row> <FiltersRow>
<Box as="h2" className={headlineMedium} marginTop="88"> <Filter>
Trending Collections {timeOptions.map((timeOption) => {
</Box> return (
</Row> <Selector
<Row> key={timeOption.value}
<Box className={styles.trendingOptions}> active={timeOption.value === timePeriod}
{timeOptions.map((timeOption) => { onClick={() => setTimePeriod(timeOption.value)}
return ( >
<span <StyledSelectorText lineHeight="20px" active={timeOption.value === timePeriod}>
className={clsx(
styles.trendingOption,
timeOption.value === timePeriod && styles.trendingOptionActive
)}
key={timeOption.value}
onClick={() => setTimePeriod(timeOption.value)}
>
{timeOption.label} {timeOption.label}
</span> </StyledSelectorText>
) </Selector>
})} )
</Box> })}
</Row> </Filter>
<Row paddingBottom="52">{data ? <CollectionTable data={trendingCollections} /> : <p>Loading</p>}</Row> <Filter onClick={() => setEthToggled(!isEthToggled)}>
</Column> <Selector active={isEthToggled}>
</Box> <StyledSelectorText lineHeight="20px" active={isEthToggled}>
ETH
</StyledSelectorText>
</Selector>
<Selector active={!isEthToggled}>
<StyledSelectorText lineHeight="20px" active={!isEthToggled}>
USD
</StyledSelectorText>
</Selector>
</Filter>
</FiltersRow>
<CollectionTable data={trendingCollections} />
</ExploreContainer>
) )
} }
......
...@@ -10,6 +10,14 @@ const ExploreContainer = styled.div` ...@@ -10,6 +10,14 @@ const ExploreContainer = styled.div`
align-items: center; align-items: center;
width: 100%; width: 100%;
padding: 16px; padding: 16px;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
gap: 16px;
}
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
gap: 0px;
}
` `
const NftExplore = () => { const NftExplore = () => {
......
...@@ -52,6 +52,11 @@ export interface TrendingCollection { ...@@ -52,6 +52,11 @@ export interface TrendingCollection {
sales: number sales: number
} }
export enum Denomination {
ETH = 'ETH',
USD = 'USD',
}
export interface CollectionTableColumn { export interface CollectionTableColumn {
collection: { collection: {
name: string name: string
...@@ -74,4 +79,6 @@ export interface CollectionTableColumn { ...@@ -74,4 +79,6 @@ export interface CollectionTableColumn {
} }
sales: number sales: number
totalSupply: number totalSupply: number
denomination: Denomination
usdPrice?: number
} }
...@@ -44,7 +44,8 @@ export const numberToWei = (amount: number) => { ...@@ -44,7 +44,8 @@ export const numberToWei = (amount: number) => {
export const ethNumberStandardFormatter = ( export const ethNumberStandardFormatter = (
amount: string | number | undefined, amount: string | number | undefined,
includeDollarSign = false, includeDollarSign = false,
removeZeroes = false removeZeroes = false,
roundToNearestWholeNumber = false
): string => { ): string => {
if (!amount) return '-' if (!amount) return '-'
...@@ -53,8 +54,14 @@ export const ethNumberStandardFormatter = ( ...@@ -53,8 +54,14 @@ export const ethNumberStandardFormatter = (
if (amountInDecimals === 0) return '-' if (amountInDecimals === 0) return '-'
if (amountInDecimals < 0.0001) return `< ${conditionalDollarSign}0.00001` if (amountInDecimals < 0.0001) return `< ${conditionalDollarSign}0.00001`
if (amountInDecimals < 1) return `${conditionalDollarSign}${amountInDecimals.toFixed(3)}` if (amountInDecimals < 1) return `${conditionalDollarSign}${parseFloat(amountInDecimals.toFixed(3))}`
const formattedPrice = (removeZeroes ? parseFloat(amountInDecimals.toFixed(2)) : amountInDecimals.toFixed(2)) const formattedPrice = (
removeZeroes
? parseFloat(amountInDecimals.toFixed(2))
: roundToNearestWholeNumber
? Math.round(amountInDecimals)
: amountInDecimals.toFixed(2)
)
.toString() .toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ',') .replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return conditionalDollarSign + formattedPrice return conditionalDollarSign + formattedPrice
...@@ -62,5 +69,5 @@ export const ethNumberStandardFormatter = ( ...@@ -62,5 +69,5 @@ export const ethNumberStandardFormatter = (
export const formatWeiToDecimal = (amount: string, removeZeroes = false) => { export const formatWeiToDecimal = (amount: string, removeZeroes = false) => {
if (!amount) return '-' if (!amount) return '-'
return ethNumberStandardFormatter(formatEther(amount), false, removeZeroes) return ethNumberStandardFormatter(formatEther(amount), false, removeZeroes, false)
} }
...@@ -8,6 +8,7 @@ export * from './fetchPrice' ...@@ -8,6 +8,7 @@ export * from './fetchPrice'
export * from './isAudio' export * from './isAudio'
export * from './isVideo' export * from './isVideo'
export * from './listNfts' export * from './listNfts'
export * from './numbers'
export * from './putCommas' export * from './putCommas'
export * from './rarity' export * from './rarity'
export * from './roundAndPluralize' export * from './roundAndPluralize'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment