Commit ab43800d authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

refactor: add suspense loading states for Relay on Collection Page (#5021)

* add suspense loading states for Relay

* remove duplication

* cleanup

* add back in suspense wrapper

* rename to skeleton
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
parent 74accb2b
...@@ -6,6 +6,8 @@ import { useIsCollectionLoading } from 'nft/hooks' ...@@ -6,6 +6,8 @@ import { useIsCollectionLoading } from 'nft/hooks'
import * as styles from './ActivitySwitcher.css' import * as styles from './ActivitySwitcher.css'
export const ActivitySwitcherLoading = new Array(2).fill(<div className={styles.styledLoading} />)
export const ActivitySwitcher = ({ export const ActivitySwitcher = ({
showActivity, showActivity,
toggleActivity, toggleActivity,
...@@ -14,12 +16,11 @@ export const ActivitySwitcher = ({ ...@@ -14,12 +16,11 @@ export const ActivitySwitcher = ({
toggleActivity: () => void toggleActivity: () => void
}) => { }) => {
const isLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading) const isLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading)
const loadingVals = new Array(2).fill(<div className={styles.styledLoading} />)
return ( return (
<Row gap="24" marginBottom="28"> <Row gap="24" marginBottom="28">
{isLoading ? ( {isLoading ? (
loadingVals ActivitySwitcherLoading
) : ( ) : (
<> <>
<Box <Box
......
...@@ -13,9 +13,10 @@ import { CollectionSearch, FilterButton } from 'nft/components/collection' ...@@ -13,9 +13,10 @@ import { CollectionSearch, FilterButton } from 'nft/components/collection'
import { CollectionAsset } from 'nft/components/collection/CollectionAsset' import { CollectionAsset } from 'nft/components/collection/CollectionAsset'
import * as styles from 'nft/components/collection/CollectionNfts.css' import * as styles from 'nft/components/collection/CollectionNfts.css'
import { SortDropdown } from 'nft/components/common/SortDropdown' import { SortDropdown } from 'nft/components/common/SortDropdown'
import { Center, Row } from 'nft/components/Flex' import { Center, Column, Row } from 'nft/components/Flex'
import { NonRarityIcon, RarityIcon, SweepIcon } from 'nft/components/icons' import { NonRarityIcon, RarityIcon, SweepIcon } from 'nft/components/icons'
import { bodySmall, buttonTextMedium, headlineMedium } from 'nft/css/common.css' import { bodySmall, buttonTextMedium, headlineMedium } from 'nft/css/common.css'
import { loadingAsset } from 'nft/css/loading.css'
import { vars } from 'nft/css/sprinkles.css' import { vars } from 'nft/css/sprinkles.css'
import { import {
CollectionFilters, CollectionFilters,
...@@ -47,11 +48,6 @@ import { marketPlaceItems } from './MarketplaceSelect' ...@@ -47,11 +48,6 @@ import { marketPlaceItems } from './MarketplaceSelect'
import { Sweep } from './Sweep' import { Sweep } from './Sweep'
import { TraitChip } from './TraitChip' import { TraitChip } from './TraitChip'
const EmptyCollectionWrapper = styled.div`
display: block;
textalign: center;
`
interface CollectionNftsProps { interface CollectionNftsProps {
contractAddress: string contractAddress: string
collectionStats: GenieCollection collectionStats: GenieCollection
...@@ -65,6 +61,11 @@ const ActionsContainer = styled.div` ...@@ -65,6 +61,11 @@ const ActionsContainer = styled.div`
justify-content: space-between; justify-content: space-between;
` `
const EmptyCollectionWrapper = styled.div`
display: block;
textalign: center;
`
const ClearAllButton = styled.button` const ClearAllButton = styled.button`
color: ${({ theme }) => theme.textTertiary}; color: ${({ theme }) => theme.textTertiary};
padding-left: 8px; padding-left: 8px;
...@@ -115,7 +116,32 @@ export const LoadingButton = styled.div` ...@@ -115,7 +116,32 @@ export const LoadingButton = styled.div`
background-size: 400%; background-size: 400%;
` `
const DEFAULT_ASSET_QUERY_AMOUNT = 25 export const DEFAULT_ASSET_QUERY_AMOUNT = 25
const loadingAssets = <>{new Array(DEFAULT_ASSET_QUERY_AMOUNT).fill(<CollectionAssetLoading />)}</>
export const CollectionNftsLoading = () => (
<Box width="full" className={styles.assetList}>
{loadingAssets}
</Box>
)
export const CollectionNftsAndMenuLoading = () => (
<Column alignItems="flex-start" position="relative" width="full">
<Row marginY="12" gap="12">
<Box className={loadingAsset} borderRadius="12" width={{ sm: '44', md: '100' }} height="44" />
<Box
className={loadingAsset}
borderRadius="12"
height="44"
display={{ sm: 'none', md: 'flex' }}
style={{ width: '220px' }}
/>
<Box className={loadingAsset} borderRadius="12" height="44" width={{ sm: '276', md: '332' }} />
</Row>
<CollectionNftsLoading />
</Column>
)
export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerified }: CollectionNftsProps) => { export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerified }: CollectionNftsProps) => {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
...@@ -266,7 +292,6 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie ...@@ -266,7 +292,6 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
setIsCollectionNftsLoading(wrappedLoadingState) setIsCollectionNftsLoading(wrappedLoadingState)
}, [wrappedLoadingState, setIsCollectionNftsLoading]) }, [wrappedLoadingState, setIsCollectionNftsLoading])
const loadingAssets = useMemo(() => <>{new Array(DEFAULT_ASSET_QUERY_AMOUNT).fill(<CollectionAssetLoading />)}</>, [])
const hasRarity = getRarityStatus(rarityStatusCache, collectionStats?.address, collectionNfts) const hasRarity = getRarityStatus(rarityStatusCache, collectionStats?.address, collectionNfts)
const sortDropDownOptions: DropDownOption[] = useMemo( const sortDropDownOptions: DropDownOption[] = useMemo(
...@@ -515,16 +540,14 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie ...@@ -515,16 +540,14 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
<InfiniteScroll <InfiniteScroll
next={() => (isNftGraphQl ? loadNext(DEFAULT_ASSET_QUERY_AMOUNT) : fetchNextPage())} next={() => (isNftGraphQl ? loadNext(DEFAULT_ASSET_QUERY_AMOUNT) : fetchNextPage())}
hasMore={wrappedHasNext} hasMore={wrappedHasNext}
loader={wrappedHasNext ? loadingAssets : null} loader={wrappedHasNext && hasNfts ? loadingAssets : null}
dataLength={collectionNfts?.length ?? 0} dataLength={collectionNfts?.length ?? 0}
style={{ overflow: 'unset' }} style={{ overflow: 'unset' }}
className={hasNfts || wrappedLoadingState ? styles.assetList : undefined} className={hasNfts || wrappedLoadingState ? styles.assetList : undefined}
> >
{hasNfts ? ( {hasNfts ? (
Nfts Nfts
) : wrappedLoadingState ? ( ) : collectionNfts?.length === 0 ? (
loadingAssets
) : (
<Center width="full" color="textSecondary" style={{ height: '60vh' }}> <Center width="full" color="textSecondary" style={{ height: '60vh' }}>
<EmptyCollectionWrapper> <EmptyCollectionWrapper>
<p className={headlineMedium}>No NFTS found</p> <p className={headlineMedium}>No NFTS found</p>
...@@ -533,6 +556,10 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie ...@@ -533,6 +556,10 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
</Box> </Box>
</EmptyCollectionWrapper> </EmptyCollectionWrapper>
</Center> </Center>
) : isNftGraphQl ? (
<CollectionNftsLoading />
) : (
loadingAssets
)} )}
</InfiniteScroll> </InfiniteScroll>
</> </>
......
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { useIsMobile } from 'nft/hooks'
import { CollectionBannerLoading } from 'nft/pages/collection'
import { ActivitySwitcherLoading } from './ActivitySwitcher'
import { CollectionNftsAndMenuLoading } from './CollectionNfts'
import { CollectionStatsLoading } from './CollectionStats'
export const CollectionPageSkeleton = () => {
const isMobile = useIsMobile()
return (
<Column width="full">
<Box width="full" height="160">
<CollectionBannerLoading />
</Box>
<Column paddingX="32">
<CollectionStatsLoading isMobile={isMobile} />
<Row gap="24" marginBottom="28">
{ActivitySwitcherLoading}
</Row>
</Column>
<Box paddingX="48">
<CollectionNftsAndMenuLoading />
</Box>
</Column>
)
}
...@@ -29,6 +29,8 @@ export const baseCollectionImage = sprinkles({ ...@@ -29,6 +29,8 @@ export const baseCollectionImage = sprinkles({
borderStyle: 'solid', borderStyle: 'solid',
borderWidth: '4px', borderWidth: '4px',
borderColor: 'backgroundSurface', borderColor: 'backgroundSurface',
borderRadius: 'round',
position: 'absolute',
}) })
export const collectionImage = style([ export const collectionImage = style([
......
...@@ -198,6 +198,10 @@ const CollectionName = ({ ...@@ -198,6 +198,10 @@ const CollectionName = ({
) )
} }
const CollectionDescriptionLoading = () => (
<Box marginTop={{ sm: '12', md: '16' }} className={styles.descriptionLoading} />
)
const CollectionDescription = ({ description }: { description: string }) => { const CollectionDescription = ({ description }: { description: string }) => {
const [showReadMore, setShowReadMore] = useState(false) const [showReadMore, setShowReadMore] = useState(false)
const [readMore, toggleReadMore] = useReducer((state) => !state, false) const [readMore, toggleReadMore] = useReducer((state) => !state, false)
...@@ -218,7 +222,7 @@ const CollectionDescription = ({ description }: { description: string }) => { ...@@ -218,7 +222,7 @@ const CollectionDescription = ({ description }: { description: string }) => {
}, [descriptionRef, baseRef, isCollectionStatsLoading]) }, [descriptionRef, baseRef, isCollectionStatsLoading])
return isCollectionStatsLoading ? ( return isCollectionStatsLoading ? (
<Box marginTop={{ sm: '12', md: '16' }} className={styles.descriptionLoading}></Box> <CollectionDescriptionLoading />
) : ( ) : (
<Box ref={baseRef} marginTop={{ sm: '12', md: '16' }} style={{ maxWidth: '680px' }}> <Box ref={baseRef} marginTop={{ sm: '12', md: '16' }} style={{ maxWidth: '680px' }}>
<Box <Box
...@@ -249,6 +253,16 @@ const StatsItem = ({ children, label, isMobile }: { children: ReactNode; label: ...@@ -249,6 +253,16 @@ const StatsItem = ({ children, label, isMobile }: { children: ReactNode; label:
) )
} }
const statsLoadingSkeleton = (isMobile: boolean) =>
new Array(5).fill(
<>
<Box display="flex" flexDirection={isMobile ? 'row' : 'column'} alignItems="baseline" gap="2" height="min">
<div className={styles.statsLabelLoading} />
<span className={styles.statsValueLoading} />
</Box>
</>
)
const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMobile?: boolean } & BoxProps) => { const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMobile?: boolean } & BoxProps) => {
const uniqueOwnersPercentage = stats.stats const uniqueOwnersPercentage = stats.stats
? roundWholePercentage((stats.stats.num_owners / stats.stats.total_supply) * 100) ? roundWholePercentage((stats.stats.num_owners / stats.stats.total_supply) * 100)
...@@ -267,54 +281,79 @@ const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMob ...@@ -267,54 +281,79 @@ const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMob
stats.stats && stats.stats.one_day_floor_change ? Math.round(Math.abs(stats.stats.one_day_floor_change) * 100) : 0 stats.stats && stats.stats.one_day_floor_change ? Math.round(Math.abs(stats.stats.one_day_floor_change) * 100) : 0
const arrow = stats.stats && stats.stats.one_day_change ? getDeltaArrow(stats.stats.one_day_floor_change) : null const arrow = stats.stats && stats.stats.one_day_change ? getDeltaArrow(stats.stats.one_day_floor_change) : null
const statsLoadingSkeleton = new Array(5).fill(
<>
<Box display="flex" flexDirection={isMobile ? 'row' : 'column'} alignItems="baseline" gap="2" height="min">
<div className={styles.statsLabelLoading} />
<span className={styles.statsValueLoading} />
</Box>
</>
)
return ( return (
<Row gap={{ sm: '36', md: '60' }} {...props}> <Row gap={{ sm: '36', md: '60' }} {...props}>
{isCollectionStatsLoading && statsLoadingSkeleton} {isCollectionStatsLoading ? (
{stats.floorPrice ? ( statsLoadingSkeleton(isMobile ?? false)
<StatsItem label="Global floor" isMobile={isMobile ?? false}> ) : (
{floorPriceStr} ETH <>
</StatsItem> {stats.floorPrice ? (
) : null} <StatsItem label="Global floor" isMobile={isMobile ?? false}>
{stats.stats?.one_day_floor_change ? ( {floorPriceStr} ETH
<StatsItem label="24-Hour floor" isMobile={isMobile ?? false}> </StatsItem>
<PercentChange> ) : null}
{floorChangeStr}% {arrow} {stats.stats?.one_day_floor_change ? (
</PercentChange> <StatsItem label="24-Hour floor" isMobile={isMobile ?? false}>
</StatsItem> <PercentChange>
) : null} {floorChangeStr}% {arrow}
{stats.stats?.total_volume ? ( </PercentChange>
<StatsItem label="Total volume" isMobile={isMobile ?? false}> </StatsItem>
{totalVolumeStr} ETH ) : null}
</StatsItem> {stats.stats?.total_volume ? (
) : null} <StatsItem label="Total volume" isMobile={isMobile ?? false}>
{totalSupplyStr ? ( {totalVolumeStr} ETH
<StatsItem label="Items" isMobile={isMobile ?? false}> </StatsItem>
{totalSupplyStr} ) : null}
</StatsItem> {totalSupplyStr ? (
) : null} <StatsItem label="Items" isMobile={isMobile ?? false}>
{uniqueOwnersPercentage ? ( {totalSupplyStr}
<StatsItem label="Unique owners" isMobile={isMobile ?? false}> </StatsItem>
{uniqueOwnersPercentage}% ) : null}
</StatsItem> {uniqueOwnersPercentage ? (
) : null} <StatsItem label="Unique owners" isMobile={isMobile ?? false}>
{stats.stats?.total_listings && listedPercentageStr > 0 ? ( {uniqueOwnersPercentage}%
<StatsItem label="Listed" isMobile={isMobile ?? false}> </StatsItem>
{listedPercentageStr}% ) : null}
</StatsItem> {stats.stats?.total_listings && listedPercentageStr > 0 ? (
) : null} <StatsItem label="Listed" isMobile={isMobile ?? false}>
{listedPercentageStr}%
</StatsItem>
) : null}
</>
)}
</Row> </Row>
) )
} }
export const CollectionStatsLoading = ({ isMobile }: { isMobile: boolean }) => {
return (
<Column marginTop={isMobile ? '20' : '0'} position="relative" width="full">
<Box className={styles.collectionImageIsLoadingBackground} />
<Box className={styles.collectionImageIsLoading} />
<Box className={styles.statsText}>
<Box className={styles.nameTextLoading} />
{!isMobile && (
<>
<CollectionDescriptionLoading />
<Row gap={{ sm: '20', md: '60' }} marginTop="20">
{statsLoadingSkeleton(isMobile)}
</Row>
</>
)}
</Box>
{isMobile && (
<>
<Marquee>
<Row gap={{ sm: '20', md: '60' }} marginX="6" marginY="28">
{statsLoadingSkeleton(isMobile)}
</Row>
</Marquee>
</>
)}
</Column>
)
}
export const CollectionStats = ({ stats, isMobile }: { stats: GenieCollection; isMobile: boolean }) => { export const CollectionStats = ({ stats, isMobile }: { stats: GenieCollection; isMobile: boolean }) => {
const [collectionSocialsIsOpen, toggleCollectionSocials] = useReducer((state) => !state, false) const [collectionSocialsIsOpen, toggleCollectionSocials] = useReducer((state) => !state, false)
const isCollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading) const isCollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading)
......
...@@ -4,12 +4,13 @@ import { Trace } from 'analytics/Trace' ...@@ -4,12 +4,13 @@ import { Trace } from 'analytics/Trace'
import { MobileHoverBag } from 'nft/components/bag/MobileHoverBag' import { MobileHoverBag } from 'nft/components/bag/MobileHoverBag'
import { AnimatedBox, Box } from 'nft/components/Box' import { AnimatedBox, Box } from 'nft/components/Box'
import { Activity, ActivitySwitcher, CollectionNfts, CollectionStats, Filters } from 'nft/components/collection' import { Activity, ActivitySwitcher, CollectionNfts, CollectionStats, Filters } from 'nft/components/collection'
import { CollectionNftsAndMenuLoading } from 'nft/components/collection/CollectionNfts'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
import { useBag, useCollectionFilters, useFiltersExpanded, useIsCollectionLoading, useIsMobile } from 'nft/hooks' import { useBag, useCollectionFilters, useFiltersExpanded, useIsCollectionLoading, useIsMobile } from 'nft/hooks'
import * as styles from 'nft/pages/collection/index.css' import * as styles from 'nft/pages/collection/index.css'
import { CollectionStatsFetcher } from 'nft/queries' import { CollectionStatsFetcher } from 'nft/queries'
import { GenieCollection } from 'nft/types' import { GenieCollection } from 'nft/types'
import { useEffect } from 'react' import { Suspense, useEffect } from 'react'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { useLocation, useNavigate, useParams } from 'react-router-dom' import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { useSpring } from 'react-spring' import { useSpring } from 'react-spring'
...@@ -17,6 +18,8 @@ import { useSpring } from 'react-spring' ...@@ -17,6 +18,8 @@ import { useSpring } from 'react-spring'
const FILTER_WIDTH = 332 const FILTER_WIDTH = 332
const BAG_WIDTH = 324 const BAG_WIDTH = 324
export const CollectionBannerLoading = () => <Box height="full" width="full" className={styles.loadingBanner} />
const Collection = () => { const Collection = () => {
const { contractAddress } = useParams() const { contractAddress } = useParams()
const setIsCollectionStatsLoading = useIsCollectionLoading((state) => state.setIsCollectionStatsLoading) const setIsCollectionStatsLoading = useIsCollectionLoading((state) => state.setIsCollectionStatsLoading)
...@@ -77,7 +80,7 @@ const Collection = () => { ...@@ -77,7 +80,7 @@ const Collection = () => {
<Box width="full" height="276"> <Box width="full" height="276">
<Box width="full" height="276"> <Box width="full" height="276">
{isLoading ? ( {isLoading ? (
<Box height="full" width="full" className={styles.loadingBanner} /> <CollectionBannerLoading />
) : ( ) : (
<Box <Box
as="img" as="img"
...@@ -125,11 +128,13 @@ const Collection = () => { ...@@ -125,11 +128,13 @@ const Collection = () => {
) )
: contractAddress && : contractAddress &&
(isLoading || collectionStats !== undefined) && ( (isLoading || collectionStats !== undefined) && (
<CollectionNfts <Suspense fallback={<CollectionNftsAndMenuLoading />}>
collectionStats={collectionStats || ({} as GenieCollection)} <CollectionNfts
contractAddress={contractAddress} collectionStats={collectionStats || ({} as GenieCollection)}
rarityVerified={collectionStats?.rarityVerified} contractAddress={contractAddress}
/> rarityVerified={collectionStats?.rarityVerified}
/>
</Suspense>
)} )}
</AnimatedBox> </AnimatedBox>
</Row> </Row>
......
...@@ -6,6 +6,7 @@ import TopLevelModals from 'components/TopLevelModals' ...@@ -6,6 +6,7 @@ import TopLevelModals from 'components/TopLevelModals'
import { useFeatureFlagsIsLoaded } from 'featureFlags' import { useFeatureFlagsIsLoaded } from 'featureFlags'
import { NftVariant, useNftFlag } from 'featureFlags/flags/nft' import { NftVariant, useNftFlag } from 'featureFlags/flags/nft'
import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader' import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader'
import { CollectionPageSkeleton } from 'nft/components/collection/CollectionPageSkeleton'
import { lazy, Suspense, useEffect, useState } from 'react' import { lazy, Suspense, useEffect, useState } from 'react'
import { Navigate, Route, Routes, useLocation } from 'react-router-dom' import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
import { useIsDarkMode } from 'state/user/hooks' import { useIsDarkMode } from 'state/user/hooks'
...@@ -245,8 +246,22 @@ export default function App() { ...@@ -245,8 +246,22 @@ export default function App() {
<Route path="/profile" element={<Profile />} /> <Route path="/profile" element={<Profile />} />
<Route path="/nfts" element={<NftExplore />} /> <Route path="/nfts" element={<NftExplore />} />
<Route path="/nfts/asset/:contractAddress/:tokenId" element={<Asset />} /> <Route path="/nfts/asset/:contractAddress/:tokenId" element={<Asset />} />
<Route path="/nfts/collection/:contractAddress" element={<Collection />} /> <Route
<Route path="/nfts/collection/:contractAddress/activity" element={<Collection />} /> path="/nfts/collection/:contractAddress"
element={
<Suspense fallback={<CollectionPageSkeleton />}>
<Collection />
</Suspense>
}
/>
<Route
path="/nfts/collection/:contractAddress/activity"
element={
<Suspense fallback={<CollectionPageSkeleton />}>
<Collection />
</Suspense>
}
/>
</> </>
)} )}
</Routes> </Routes>
......
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