From f735c3484116f5c6d4e50752896ab26f095822d7 Mon Sep 17 00:00:00 2001 From: Greg Bugyis Date: Tue, 27 Sep 2022 20:33:15 +0300 Subject: [PATCH] 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 --- package.json | 1 + src/nft/components/explore/ActivityFeed.tsx | 9 +- src/nft/components/explore/Cells/Cells.css.ts | 46 +++++++ src/nft/components/explore/Cells/Cells.tsx | 102 +++++++++++++++ .../components/explore/CollectionTable.tsx | 84 +++++++++++++ src/nft/components/explore/Explore.css.ts | 100 ++++++++++++++- src/nft/components/explore/Table.tsx | 117 ++++++++++++++++++ .../explore/TrendingCollections.tsx | 97 +++++++++++++++ src/nft/pages/explore/index.tsx | 2 + yarn.lock | 7 ++ 10 files changed, 555 insertions(+), 10 deletions(-) create mode 100644 src/nft/components/explore/Cells/Cells.css.ts create mode 100644 src/nft/components/explore/Cells/Cells.tsx create mode 100644 src/nft/components/explore/CollectionTable.tsx create mode 100644 src/nft/components/explore/Table.tsx create mode 100644 src/nft/components/explore/TrendingCollections.tsx diff --git a/package.json b/package.json index 351f69ac07..d1ca9a9337 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/nft/components/explore/ActivityFeed.tsx b/src/nft/components/explore/ActivityFeed.tsx index 4801a6d451..3e8dfa5de8 100644 --- a/src/nft/components/explore/ActivityFeed.tsx +++ b/src/nft/components/explore/ActivityFeed.tsx @@ -53,14 +53,7 @@ const ActivityFeed = ({ address }: { address: string }) => { {collectionActivity.events.map((activityEvent: ActivityEvent, index: number) => { - return ( - - ) + return })} diff --git a/src/nft/components/explore/Cells/Cells.css.ts b/src/nft/components/explore/Cells/Cells.css.ts new file mode 100644 index 0000000000..8019762cfd --- /dev/null +++ b/src/nft/components/explore/Cells/Cells.css.ts @@ -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', +}) diff --git a/src/nft/components/explore/Cells/Cells.tsx b/src/nft/components/explore/Cells/Cells.tsx new file mode 100644 index 0000000000..28efd34958 --- /dev/null +++ b/src/nft/components/explore/Cells/Cells.tsx @@ -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 ( + + {`${value.name} + {value.name} + {value.isVerified && ( + + + + )} + + ) +} + +export const WithCommaCell = ({ value }: CellProps) => {value.value ? putCommas(value.value) : '-'} + +export const EthCell = ({ value }: { value: number }) => ( + + {value ? <>{formatWeiToDecimal(value.toString())} ETH : '-'} + +) + +export const VolumeCell = ({ value }: CellProps) => ( + + {value.value ? <>{ethNumberStandardFormatter(value.value.toString())} ETH : '-'} + +) + +export const EthWithDayChange = ({ value }: CellProps) => ( + + + {value.change ? ( + 0 ? 'green' : 'accentFailure'} + fontWeight="normal" + fontSize="12" + position="relative" + > + {value.change > 0 && '+'} + {formatChange(value.change)}% + + ) : null} + +) + +export const WeiWithDayChange = ({ value }: CellProps) => ( + + + {value && value.value ? <>{formatWeiToDecimal(value.value.toString())} ETH : '-'} + + {value.change ? ( + 0 ? 'green' : 'accentFailure'} + fontWeight="normal" + fontSize="12" + position="relative" + > + {value.change > 0 && '+'} + {formatChange(value.change)}% + + ) : null} + +) + +export const CommaWithDayChange = ({ value }: CellProps) => ( + + + {value.change ? ( + 0 ? 'green' : 'accentFailure'} + fontWeight="normal" + fontSize="12" + position="relative" + > + {value.change > 0 && '+'} + {formatChange(value.change)}% + + ) : null} + +) diff --git a/src/nft/components/explore/CollectionTable.tsx b/src/nft/components/explore/CollectionTable.tsx new file mode 100644 index 0000000000..8b0cd82837 --- /dev/null +++ b/src/nft/components/explore/CollectionTable.tsx @@ -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[] = [ + { + Header: 'Collection', + accessor: 'collection', + Cell: CollectionTitleCell, + }, + { + id: ColumnHeaders.Volume, + Header: ColumnHeaders.Volume, + accessor: ({ volume }) => volume.value, + sortDescFirst: true, + Cell: function EthDayChanget(cell: CellProps) { + return + }, + }, + { + id: ColumnHeaders.Floor, + Header: ColumnHeaders.Floor, + accessor: ({ floor }) => floor.value, + sortDescFirst: true, + Cell: function weiDayChange(cell: CellProps) { + return + }, + }, + { + id: ColumnHeaders.Sales, + Header: ColumnHeaders.Sales, + accessor: 'sales', + sortDescFirst: true, + Cell: function withCommaCell(cell: CellProps) { + return + }, + }, + { + id: ColumnHeaders.Items, + Header: ColumnHeaders.Items, + accessor: 'totalSupply', + sortDescFirst: true, + Cell: function withCommaCell(cell: CellProps) { + return + }, + }, + { + Header: ColumnHeaders.Owners, + accessor: ({ owners }) => owners.value, + sortDescFirst: true, + Cell: function commaDayChange(cell: CellProps) { + return + }, + }, +] + +const CollectionTable = ({ data }: { data: CollectionTableColumn[] }) => { + return ( + <> + + + ) +} + +export default CollectionTable diff --git a/src/nft/components/explore/Explore.css.ts b/src/nft/components/explore/Explore.css.ts index ec1f99a9f8..c2c1c1449f 100644 --- a/src/nft/components/explore/Explore.css.ts +++ b/src/nft/components/explore/Explore.css.ts @@ -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' }) diff --git a/src/nft/components/explore/Table.tsx b/src/nft/components/explore/Table.tsx new file mode 100644 index 0000000000..2a54a67d38 --- /dev/null +++ b/src/nft/components/explore/Table.tsx @@ -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> { + columns: Column[] + data: CollectionTableColumn[] + hiddenColumns: IdType[] + classNames?: { + td: string + } +} + +export function Table>({ + columns, + data, + hiddenColumns, + classNames, + ...props +}: TableProps) { + 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 ( +
+ + {headerGroups.map((headerGroup) => ( + + {headerGroup.headers.map((column, index) => { + return ( + + ) + })} + + ))} + + + {rows.map((row, i) => { + prepareRow(row) + + return ( + navigate(`/nfts/collection/${row.original.collection.address}`)} + > + {row.cells.map((cell, cellIndex) => { + return ( + + ) + })} + + ) + })} + +
+ + {column.isSorted ? ( + column.isSortedDesc ? ( + + ) : ( + + ) + ) : ( + '' + )} + + + {column.render('Header')} + +
+ {cellIndex === 0 ? {i + 1} : null} + {cell.render('Cell')} +
+ ) +} diff --git a/src/nft/components/explore/TrendingCollections.tsx b/src/nft/components/explore/TrendingCollections.tsx new file mode 100644 index 0000000000..8744968a18 --- /dev/null +++ b/src/nft/components/explore/TrendingCollections.tsx @@ -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.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 ( + + + + + Trending Collections + + + + + {timeOptions.map((timeOption) => { + return ( + setTimePeriod(timeOption.value)} + > + {timeOption.label} + + ) + })} + + + {data ? :

Loading

}
+
+
+ ) +} + +export default TrendingCollections diff --git a/src/nft/pages/explore/index.tsx b/src/nft/pages/explore/index.tsx index 407be6b573..80b5313422 100644 --- a/src/nft/pages/explore/index.tsx +++ b/src/nft/pages/explore/index.tsx @@ -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 = () => { <> + ) } diff --git a/yarn.lock b/yarn.lock index fbf0659c18..bfe1ec2434 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"