feat: Trending Collections Table (#4694)

* Migrate Trending Collections: first pass

* Adding types for react-table

* Forgot to add yarn.lock

* Update sprinkles colors and add accentSuccess to match Figma

* Style cleanup

* Fix overlap on activity items and text wrapping on Value Prop

* Update header to new typography name

* Make entire table row link to collection

* Remove duplicated navigate() on table row

* Use borderStyle: none (sprinkle) instead of hidden

* Use common typography style for table header row

* Sprinkles for rank styles

* Sprinkles for TrendingOptions border styles

* Update color on trendingOption active state

* Restore useEffect to hide certain columns on mobile

* forgot to save one file

* Update accent color

* Use isMobile instead of breakpoint check
This commit is contained in:
Greg Bugyis 2022-09-27 20:33:15 +03:00 committed by GitHub
parent 1aa4afad5f
commit f735c34841
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 555 additions and 10 deletions

@ -80,6 +80,7 @@
"@types/react-dom": "^18.0.6",
"@types/react-redux": "^7.1.24",
"@types/react-router-dom": "^5.3.3",
"@types/react-table": "^7.7.12",
"@types/react-virtualized-auto-sizer": "^1.0.0",
"@types/react-window": "^1.8.2",
"@types/rebass": "^4.0.7",

@ -53,14 +53,7 @@ const ActivityFeed = ({ address }: { address: string }) => {
</Column>
<Column width="full" position="relative">
{collectionActivity.events.map((activityEvent: ActivityEvent, index: number) => {
return (
<ActivityRow
event={activityEvent}
index={index}
key={`${activityEvent.eventType}${activityEvent.tokenId}`}
current={current}
/>
)
return <ActivityRow event={activityEvent} index={index} key={index} current={current} />
})}
</Column>
</Box>

@ -0,0 +1,46 @@
import { style } from '@vanilla-extract/css'
import { body } from '../../../css/common.css'
import { sprinkles } from '../../../css/sprinkles.css'
export const logo = sprinkles({ borderRadius: '12' })
export const title = style([
body,
sprinkles({
color: 'textPrimary',
textAlign: 'left',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
paddingLeft: '12',
paddingRight: '2',
}),
])
export const openAddress = sprinkles({
minWidth: '20',
height: '20',
width: '20',
})
export const address = style([
title,
sprinkles({
marginLeft: '8',
alignItems: 'center',
minWidth: '0',
width: 'max',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}),
])
export const verifiedBadge = sprinkles({
display: 'inline-block',
paddingTop: '4',
height: '28',
width: '28',
textAlign: 'left',
})

@ -0,0 +1,102 @@
import { ethNumberStandardFormatter, formatWeiToDecimal } from '../../../utils/currency'
import { putCommas } from '../../../utils/putCommas'
import { formatChange } from '../../../utils/toSignificant'
import { Box } from '../../Box'
import { Column, Row } from '../../Flex'
import { VerifiedIcon } from '../../icons'
import * as styles from './Cells.css'
interface CellProps {
value: {
logo?: string
name?: string
address?: string
isVerified?: boolean
value?: number
change?: number
}
}
export const CollectionTitleCell = ({ value }: CellProps) => {
return (
<Row as="span" style={{ marginLeft: '52px' }}>
<img className={styles.logo} src={value.logo} alt={`${value.name} logo`} height={44} width={44} />
<span className={styles.title}>{value.name}</span>
{value.isVerified && (
<span className={styles.verifiedBadge}>
<VerifiedIcon />
</span>
)}
</Row>
)
}
export const WithCommaCell = ({ value }: CellProps) => <span>{value.value ? putCommas(value.value) : '-'}</span>
export const EthCell = ({ value }: { value: number }) => (
<Row justifyContent="flex-end" color="textPrimary">
{value ? <>{formatWeiToDecimal(value.toString())} ETH</> : '-'}
</Row>
)
export const VolumeCell = ({ value }: CellProps) => (
<Row justifyContent="flex-end" color="textPrimary">
{value.value ? <>{ethNumberStandardFormatter(value.value.toString())} ETH</> : '-'}
</Row>
)
export const EthWithDayChange = ({ value }: CellProps) => (
<Column gap="4">
<VolumeCell value={{ value: 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>
)
export const WeiWithDayChange = ({ value }: CellProps) => (
<Column gap="4">
<Row justifyContent="flex-end" color="textPrimary">
{value && value.value ? <>{formatWeiToDecimal(value.value.toString())} ETH</> : '-'}
</Row>
{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>
)
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>
)

@ -0,0 +1,84 @@
import { CellProps, Column } from 'react-table'
import { CollectionTableColumn } from '../../types'
import {
CollectionTitleCell,
CommaWithDayChange,
EthWithDayChange,
WeiWithDayChange,
WithCommaCell,
} from './Cells/Cells'
import { Table } from './Table'
export enum ColumnHeaders {
Volume = 'Volume',
Floor = 'Floor',
Sales = 'Sales',
Items = 'Items',
Owners = 'Owners',
}
const columns: Column<CollectionTableColumn>[] = [
{
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[] }) => {
return (
<>
<Table
hiddenColumns={[ColumnHeaders.Volume, ColumnHeaders.Owners, ColumnHeaders.Items, ColumnHeaders.Sales]}
{...{ data, columns }}
/>
</>
)
}
export default CollectionTable

@ -1,4 +1,5 @@
import { style } from '@vanilla-extract/css'
import { body, caption } from 'nft/css/common.css'
import { breakpoints, sprinkles } from 'nft/css/sprinkles.css'
export const section = style([
@ -30,11 +31,11 @@ export const bannerWrap = style([
export const bannerOverlay = style([
{
opacity: '0.7',
height: '386px',
},
sprinkles({
position: 'absolute',
opacity: '0.7',
width: 'full',
backgroundColor: 'grey900',
left: '0',
@ -204,7 +205,102 @@ export const valuePropContent = style([
width: '58%',
lineHeight: '36px',
},
[`(min-width: ${breakpoints.lg}px)`]: { width: '50%' },
[`(min-width: ${breakpoints.lg - 1}px)`]: { width: '50%' },
},
},
])
/* Base Table Styles */
export const table = style([
{
borderCollapse: 'collapse',
boxShadow: '0 0 0 1px rgba(153, 161, 189, 0.24)',
borderSpacing: '0px 40px',
},
sprinkles({
width: 'full',
borderRadius: '12',
borderStyle: 'none',
}),
])
export const thead = sprinkles({
marginRight: '12',
borderColor: 'outline',
borderWidth: '1px',
borderBottomStyle: 'solid',
})
export const th = style([
caption,
{
selectors: {
'&:nth-last-child(1)': {
paddingRight: '20px',
},
},
},
sprinkles({
color: { default: 'textSecondary', hover: 'textPrimary' },
cursor: 'pointer',
paddingTop: '12',
paddingBottom: '12',
}),
])
export const tr = sprinkles({ cursor: 'pointer' })
export const rank = sprinkles({
color: 'textSecondary',
position: 'absolute',
display: { md: 'inline-block', sm: 'none' },
left: '24',
top: '20',
})
export const td = style([
body,
{
verticalAlign: 'middle',
selectors: {
'&:nth-last-child(1)': {
paddingRight: '20px',
},
},
},
sprinkles({
maxWidth: '160',
paddingTop: '10',
paddingBottom: '10',
textAlign: 'right',
position: 'relative',
}),
])
export const trendingOptions = sprinkles({
marginBottom: '32',
height: '44',
borderRadius: '12',
borderWidth: '2px',
borderStyle: 'solid',
borderColor: 'outline',
})
/* Trending Colletion styles */
export const trendingOption = style([
{
marginTop: '-1px',
marginLeft: '-1px',
},
sprinkles({
paddingY: '14',
paddingX: '16',
borderRadius: '12',
fontSize: '12',
display: 'inline-block',
cursor: 'pointer',
}),
])
export const trendingOptionActive = sprinkles({ backgroundColor: 'accentActiveSoft' })

@ -0,0 +1,117 @@
import clsx from 'clsx'
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { Column, IdType, useSortBy, useTable } from 'react-table'
import { isMobile } from 'utils/userAgent'
import { Box } from '../../components/Box'
import { CollectionTableColumn } from '../../types'
import { ArrowRightIcon } from '../icons'
import { ColumnHeaders } from './CollectionTable'
import * as styles from './Explore.css'
interface TableProps<D extends Record<string, unknown>> {
columns: Column<CollectionTableColumn>[]
data: CollectionTableColumn[]
hiddenColumns: IdType<D>[]
classNames?: {
td: string
}
}
export function Table<D extends Record<string, unknown>>({
columns,
data,
hiddenColumns,
classNames,
...props
}: TableProps<D>) {
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow, setHiddenColumns } = useTable(
{
columns,
data,
initialState: {
sortBy: [
{
desc: true,
id: ColumnHeaders.Volume,
},
],
},
...props,
},
useSortBy
)
const navigate = useNavigate()
useEffect(() => {
if (hiddenColumns && isMobile) {
setHiddenColumns(hiddenColumns)
} else {
setHiddenColumns([])
}
}, [hiddenColumns, setHiddenColumns])
return (
<table {...getTableProps()} className={styles.table}>
<thead className={styles.thead}>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()} key={headerGroup.id}>
{headerGroup.headers.map((column, index) => {
return (
<th
className={styles.th}
{...column.getHeaderProps(column.getSortByToggleProps())}
style={{
textAlign: index === 0 ? 'left' : 'right',
paddingLeft: index === 0 ? '52px' : 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>
</th>
)
})}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map((row, i) => {
prepareRow(row)
return (
<tr
className={styles.tr}
{...row.getRowProps()}
key={i}
onClick={() => navigate(`/nfts/collection/${row.original.collection.address}`)}
>
{row.cells.map((cell, cellIndex) => {
return (
<td className={clsx(styles.td, classNames?.td)} {...cell.getCellProps()} key={cellIndex}>
{cellIndex === 0 ? <span className={styles.rank}>{i + 1}</span> : null}
{cell.render('Cell')}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
)
}

@ -0,0 +1,97 @@
import clsx from 'clsx'
import { useMemo, useState } from 'react'
import { useQuery } from 'react-query'
import { Box } from '../../components/Box'
import { Column, Row } from '../../components/Flex'
import { headlineMedium } from '../../css/common.css'
import { fetchTrendingCollections } from '../../queries'
import { CollectionTableColumn, TimePeriod, VolumeType } from '../../types'
import CollectionTable from './CollectionTable'
import * as styles from './Explore.css'
const timeOptions: { label: string; value: TimePeriod }[] = [
{ label: '24 hour', value: TimePeriod.OneDay },
{ label: '7 day', value: TimePeriod.SevenDays },
{ label: '30 day', value: TimePeriod.ThirtyDays },
{ label: 'All time', value: TimePeriod.AllTime },
]
const TrendingCollections = () => {
const [timePeriod, setTimePeriod] = useState<TimePeriod>(TimePeriod.OneDay)
const { isSuccess, data } = useQuery(
['trendingCollections', timePeriod],
() => {
return fetchTrendingCollections({ volumeType: 'eth', timePeriod, size: 100 })
},
{
refetchOnReconnect: false,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchInterval: 5000,
}
)
const trendingCollections = useMemo(() => {
if (isSuccess && data) {
return data.map((d) => ({
...d,
collection: {
name: d.name,
logo: d.imageUrl,
address: d.address,
isVerified: d.isVerified,
},
volume: {
value: d.volume,
change: d.volumeChange,
type: 'eth' as VolumeType,
},
floor: {
value: d.floor,
change: d.floorChange,
},
owners: {
value: d.owners,
change: d.ownersChange,
},
sales: d.sales,
totalSupply: d.totalSupply,
}))
} else return [] as CollectionTableColumn[]
}, [data, isSuccess])
return (
<Box width="full" className={styles.section}>
<Column width="full">
<Row>
<Box as="h2" className={headlineMedium} marginTop="88">
Trending Collections
</Box>
</Row>
<Row>
<Box className={styles.trendingOptions}>
{timeOptions.map((timeOption) => {
return (
<span
className={clsx(
styles.trendingOption,
timeOption.value === timePeriod && styles.trendingOptionActive
)}
key={timeOption.value}
onClick={() => setTimePeriod(timeOption.value)}
>
{timeOption.label}
</span>
)
})}
</Box>
</Row>
<Row paddingBottom="52">{data ? <CollectionTable data={trendingCollections} /> : <p>Loading</p>}</Row>
</Column>
</Box>
)
}
export default TrendingCollections

@ -1,4 +1,5 @@
import Banner from 'nft/components/explore/Banner'
import TrendingCollections from 'nft/components/explore/TrendingCollections'
import ValueProp from 'nft/components/explore/ValueProp'
const NftExplore = () => {
@ -6,6 +7,7 @@ const NftExplore = () => {
<>
<Banner />
<ValueProp />
<TrendingCollections />
</>
)
}

@ -3713,6 +3713,13 @@
"@types/history" "*"
"@types/react" "*"
"@types/react-table@^7.7.12":
version "7.7.12"
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.12.tgz#628011d3cb695b07c678704a61f2f1d5b8e567fd"
integrity sha512-bRUent+NR/WwtDGwI/BqhZ8XnHghwHw0HUKeohzB5xN3K2qKWYE5w19e7GCuOkL1CXD9Gi1HFy7TIm2AvgWUHg==
dependencies:
"@types/react" "*"
"@types/react-virtualized-auto-sizer@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz#b3187dae1dfc4c15880c9cfc5b45f2719ea6ebd4"