diff --git a/components/common/MintTypeSelector.tsx b/components/common/MintTypeSelector.tsx new file mode 100644 index 000000000..bd1e110d5 --- /dev/null +++ b/components/common/MintTypeSelector.tsx @@ -0,0 +1,52 @@ +import { ToggleGroupItem, ToggleGroupRoot } from 'components/primitives' +import { FC } from 'react' + +export const MintTypeOptions = ['any', 'free', 'paid'] as const +export type MintTypeOption = (typeof MintTypeOptions)[number] + +type Props = { + option: MintTypeOption + onOptionSelected: (option: MintTypeOption) => void +} + +const MintTypeSelector: FC = ({ onOptionSelected, option }) => { + return ( + + selectedOption && onOptionSelected(selectedOption as MintTypeOption) + } + > + {MintTypeOptions.map((option) => ( + + {option} + + ))} + + ) +} + +export default MintTypeSelector diff --git a/components/common/MintsPeriodDropdown.tsx b/components/common/MintsPeriodDropdown.tsx new file mode 100644 index 000000000..2a2094d83 --- /dev/null +++ b/components/common/MintsPeriodDropdown.tsx @@ -0,0 +1,95 @@ +import { faChevronDown } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { useTrendingMints } from '@reservoir0x/reservoir-kit-ui' +import { + DropdownMenuContent, + DropdownMenuItem, +} from 'components/primitives/Dropdown' +import { FC } from 'react' +import { Box, Button, Text } from '../primitives' + +export type MintsSortingOption = NonNullable< + Exclude[0], false | undefined>['period'] +> + +const sortingOptions: MintsSortingOption[] = [ + '5m', + '10m', + '30m', + '1h', + '6h', + '24h', +] + +const nameForSortingOption = (option: MintsSortingOption, compact: boolean) => { + switch (option) { + case '24h': + return compact ? '24h' : '24 hours' + case '6h': + return compact ? '6h' : '6 hours' + case '1h': + return compact ? '1h' : '1 hour' + case '30m': + return compact ? '30m' : '30 minutes' + case '10m': + return compact ? '10m' : '10 minutes' + case '5m': + return compact ? '5m' : '5 minutes' + } +} + +type Props = { + compact?: boolean + option: MintsSortingOption + onOptionSelected: (option: MintsSortingOption) => void +} + +const MintsPeriodDropdown: FC = ({ + compact = false, + option, + onOptionSelected, +}) => { + return ( + + + + + + {sortingOptions.map((optionItem) => ( + onOptionSelected(optionItem as MintsSortingOption)} + > + {nameForSortingOption(optionItem, false)} + + ))} + + + ) +} + +export default MintsPeriodDropdown diff --git a/components/navbar/HamburgerMenu.tsx b/components/navbar/HamburgerMenu.tsx index 571ca192d..1fc14c6a6 100644 --- a/components/navbar/HamburgerMenu.tsx +++ b/components/navbar/HamburgerMenu.tsx @@ -134,7 +134,7 @@ const HamburgerMenu = () => { Explore - + { pt: '24px', }} > - Trending + Trending Collections + + + Trending Mints + + + + { Explore - - Trending - - {/* + - - - NFTs - - + Trending - + - + - Trending Collections + Collections - + - Trending Mints + Mints - */} + {false && ( diff --git a/components/rankings/MintRankingsTable.tsx b/components/rankings/MintRankingsTable.tsx new file mode 100644 index 000000000..2545c18a3 --- /dev/null +++ b/components/rankings/MintRankingsTable.tsx @@ -0,0 +1,334 @@ +import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { useTrendingMints } from '@reservoir0x/reservoir-kit-ui' +import { OpenSeaVerified } from 'components/common/OpenSeaVerified' +import { NAVBAR_HEIGHT } from 'components/navbar' +import { + Box, + Flex, + FormatCryptoCurrency, + HeaderRow, + TableCell, + TableRow, + Text, +} from 'components/primitives' +import Img from 'components/primitives/Img' +import { useMarketplaceChain } from 'hooks' +import Link from 'next/link' +import { FC, useMemo } from 'react' +import { useMediaQuery } from 'react-responsive' +import optimizeImage from 'utils/optimizeImage' + +type Props = { + mints: NonNullable['data']> + loading?: boolean +} + +const gridColumns = { + gridTemplateColumns: '520px repeat(5, 0.5fr) 250px', + '@md': { + gridTemplateColumns: '420px 1fr 1fr 1fr', + }, + + '@lg': { + gridTemplateColumns: '360px repeat(5, 0.5fr) 250px', + }, + + '@xl': { + gridTemplateColumns: '520px repeat(5, 0.5fr) 250px', + }, +} + +export const MintRankingsTable: FC = ({ mints, loading }) => { + const isSmallDevice = useMediaQuery({ maxWidth: 900 }) + + return ( + <> + {!loading && mints && mints.length === 0 ? ( + + + + + No mints found + + ) : ( + + {isSmallDevice ? ( + + + Collection + + + Total Mints + + + ) : ( + + )} + + {mints?.map((mint, i) => ( + + ))} + + + )} + + ) +} + +type RankingsTableRowProps = { + mint: NonNullable['data']>[0] + rank: number +} + +const RankingsTableRow: FC = ({ mint, rank }) => { + const { routePrefix } = useMarketplaceChain() + const isSmallDevice = useMediaQuery({ maxWidth: 900 }) + + const collectionImage = useMemo(() => { + return optimizeImage(mint?.image || mint?.sampleImages?.[0], 250) + }, [mint.image]) + + const mintPrice = mint.mintPrice?.toString() + + const sampleImages: string[] = mint?.sampleImages || [] + + if (isSmallDevice) { + return ( + + + + {rank} + + Collection Image + + + + {mint?.name} + + + + + + + + + + {mint?.mintCount?.toLocaleString()} + + + + ) + } else { + return ( + + + + + + {rank} + + Collection Image + + + + {mint?.name} + + + + + + + + + {mintPrice !== '0' ? ( + + ) : ( + '-' + )} + + + + + + + + + + {mint?.mintCount?.toLocaleString()} + + + + + {mint?.oneHourCount?.toLocaleString()} + + + + {mint?.sixHourCount?.toLocaleString()} + + + + + {/** */} + {sampleImages.map((image: string, i) => { + if (image) { + return ( + + ) => { + e.currentTarget.style.visibility = 'hidden' + }} + /> + ) + } + return null + })} + + + + ) + } +} + +const headings = [ + 'Collection', + 'Mint Price', + 'Floor Price', + 'Total Mints', + '1H Mints', + '6h Mints', + 'Recent Mints', +] + +const TableHeading = () => ( + + {headings.map((heading, i) => ( + 3} + key={heading} + css={{ textAlign: i === headings.length - 1 ? 'right' : 'left' }} + > + + {heading} + + + ))} + +) diff --git a/pages/[chain]/mints/trending/index.tsx b/pages/[chain]/mints/trending/index.tsx new file mode 100644 index 000000000..199a2ef70 --- /dev/null +++ b/pages/[chain]/mints/trending/index.tsx @@ -0,0 +1,177 @@ +import { useTrendingMints } from '@reservoir0x/reservoir-kit-ui' +import { paths } from '@reservoir0x/reservoir-sdk' +import { Head } from 'components/Head' +import Layout from 'components/Layout' +import ChainToggle from 'components/common/ChainToggle' +import LoadingSpinner from 'components/common/LoadingSpinner' +import MintTypeSelector, { + MintTypeOption, +} from 'components/common/MintTypeSelector' +import MintsPeriodDropdown, { + MintsSortingOption, +} from 'components/common/MintsPeriodDropdown' +import { Box, Flex, Text } from 'components/primitives' +import { MintRankingsTable } from 'components/rankings/MintRankingsTable' +import { ChainContext } from 'context/ChainContextProvider' +import { useMounted } from 'hooks' +import { GetServerSideProps, InferGetServerSidePropsType, NextPage } from 'next' +import { useRouter } from 'next/router' +import { NORMALIZE_ROYALTIES } from 'pages/_app' +import { useContext, useEffect, useRef, useState } from 'react' +import { useMediaQuery } from 'react-responsive' +import supportedChains, { DefaultChain } from 'utils/chains' +import fetcher from 'utils/fetcher' + +type Props = InferGetServerSidePropsType + +const IndexPage: NextPage = ({ ssr }) => { + const router = useRouter() + const isSSR = typeof window === 'undefined' + const isMounted = useMounted() + const compactToggleNames = useMediaQuery({ query: '(max-width: 800px)' }) + const isSmallDevice = useMediaQuery({ maxWidth: 600 }) + + const [mintType, setMintType] = useState('any') + const [sortByPeriod, setSortByPeriod] = useState('24h') + + let mintQuery: Parameters['0'] = { + limit: 20, + period: sortByPeriod, + type: mintType, + } + + const { chain, switchCurrentChain } = useContext(ChainContext) + + useEffect(() => { + if (router.query.chain) { + let chainIndex: number | undefined + for (let i = 0; i < supportedChains.length; i++) { + if (supportedChains[i].routePrefix == router.query.chain) { + chainIndex = supportedChains[i].id + } + } + if (chainIndex !== -1 && chainIndex) { + switchCurrentChain(chainIndex) + } + } + }, [router.query]) + + const { data, isValidating } = useTrendingMints(mintQuery, chain.id, { + fallbackData: [ssr.mints], + }) + + let mints = data || [] + + return ( + + + + + + + Trending Mints + + + {!isSmallDevice && ( + + )} + { + setSortByPeriod(option) + }} + /> + + + {isSmallDevice && ( + + )} + + {isSSR || !isMounted ? null : ( + + )} + + {isValidating && ( + + + + )} + + + ) +} + +type MintsSchema = + paths['/collections/trending-mints/v1']['get']['responses']['200']['schema'] + +export const getServerSideProps: GetServerSideProps<{ + ssr: { + mints: MintsSchema + } +}> = async ({ res, params }) => { + const mintsQuery: paths['/collections/trending-mints/v1']['get']['parameters']['query'] = + { + period: '24h', + limit: 50, + } + + const chainPrefix = params?.chain || '' + + const chain = + supportedChains.find((chain) => chain.routePrefix === chainPrefix) || + DefaultChain + + const query = { ...mintsQuery, normalizeRoyalties: NORMALIZE_ROYALTIES } + + const response = await fetcher( + `${chain.reservoirBaseUrl}/collections/trending-mints/v1`, + query, + { + headers: { + 'x-api-key': process.env.RESERVOIR_API_KEY || '', + }, + } + ) + + res.setHeader( + 'Cache-Control', + 'public, s-maxage=30, stale-while-revalidate=60' + ) + + return { + props: { ssr: { mints: response.data } }, + } +} + +export default IndexPage