feat: collection asset cards (#4422)
* removing differentiating mobile and desktop collections * feat: asset cards * changed card to module + addressed other comments * todo
This commit is contained in:
parent
293e56758c
commit
c25971e5d2
@ -197,6 +197,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-feather": "^2.0.8",
|
||||
"react-ga4": "^1.4.1",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-is": "^17.0.2",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-popper": "^2.2.3",
|
||||
|
132
src/nft/components/collection/Card.css.ts
Normal file
132
src/nft/components/collection/Card.css.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { breakpoints, sprinkles, themeVars } from 'nft/css/sprinkles.css'
|
||||
|
||||
export const card = style([
|
||||
sprinkles({
|
||||
overflow: 'hidden',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '1px',
|
||||
}),
|
||||
{
|
||||
boxSizing: 'border-box',
|
||||
WebkitBoxSizing: 'border-box',
|
||||
'@media': {
|
||||
[`(max-width: ${breakpoints.tabletSm - 1}px)`]: {
|
||||
':hover': {
|
||||
borderColor: themeVars.colors.medGray,
|
||||
cursor: 'pointer',
|
||||
background: themeVars.colors.lightGrayOverlay,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export const notSelectedCard = style([
|
||||
card,
|
||||
sprinkles({
|
||||
backgroundColor: 'lightGray',
|
||||
borderColor: 'transparent',
|
||||
}),
|
||||
])
|
||||
|
||||
export const cardImageHover = style({
|
||||
transform: 'scale(1.15)',
|
||||
})
|
||||
|
||||
export const selectedCard = style([
|
||||
card,
|
||||
sprinkles({
|
||||
background: 'lightGrayOverlay',
|
||||
borderColor: 'medGray',
|
||||
}),
|
||||
])
|
||||
|
||||
export const button = style([
|
||||
sprinkles({
|
||||
display: 'flex',
|
||||
width: 'full',
|
||||
position: 'relative',
|
||||
paddingY: '8',
|
||||
marginTop: { mobile: '8', tabletSm: '10' },
|
||||
marginBottom: '12',
|
||||
borderRadius: '12',
|
||||
border: 'none',
|
||||
justifyContent: 'center',
|
||||
transition: '250',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
{
|
||||
lineHeight: '16px',
|
||||
},
|
||||
])
|
||||
|
||||
export const marketplaceIcon = style([
|
||||
sprinkles({
|
||||
display: 'inline-block',
|
||||
width: '16',
|
||||
height: '16',
|
||||
borderRadius: '4',
|
||||
flexShrink: '0',
|
||||
marginLeft: '8',
|
||||
}),
|
||||
{
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
])
|
||||
|
||||
export const erc1155ButtonRow = sprinkles({
|
||||
flexShrink: '0',
|
||||
gap: '12',
|
||||
marginTop: { mobile: '8', tabletSm: '10' },
|
||||
marginBottom: '12',
|
||||
})
|
||||
|
||||
export const erc1155QuantityText = style([
|
||||
sprinkles({
|
||||
color: 'blackBlue',
|
||||
}),
|
||||
{
|
||||
lineHeight: '20px',
|
||||
userSelect: 'none',
|
||||
},
|
||||
])
|
||||
|
||||
export const erc1155Button = sprinkles({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'white90',
|
||||
textAlign: 'center',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
borderRadius: 'round',
|
||||
cursor: 'pointer',
|
||||
padding: '0',
|
||||
transition: '250',
|
||||
})
|
||||
|
||||
export const erc1155PlusButton = style([
|
||||
erc1155Button,
|
||||
sprinkles({
|
||||
color: 'magicGradient',
|
||||
}),
|
||||
{
|
||||
':hover': {
|
||||
backgroundColor: themeVars.colors.magicGradient,
|
||||
color: themeVars.colors.blackBlue,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export const erc1155MinusButton = style([
|
||||
erc1155Button,
|
||||
sprinkles({
|
||||
color: 'error',
|
||||
}),
|
||||
{
|
||||
':hover': {
|
||||
backgroundColor: themeVars.colors.error,
|
||||
color: themeVars.colors.blackBlue,
|
||||
},
|
||||
},
|
||||
])
|
358
src/nft/components/collection/Card.tsx
Normal file
358
src/nft/components/collection/Card.tsx
Normal file
@ -0,0 +1,358 @@
|
||||
import clsx from 'clsx'
|
||||
import Column from 'components/Column'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Row } from 'nft/components/Flex'
|
||||
import { MinusIconLarge, PlusIconLarge } from 'nft/components/icons'
|
||||
import { body, subheadSmall } from 'nft/css/common.css'
|
||||
import { themeVars, vars } from 'nft/css/sprinkles.css'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { GenieAsset } from 'nft/types'
|
||||
import {
|
||||
createContext,
|
||||
MouseEvent,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import * as styles from './Card.css'
|
||||
|
||||
/* -------- ASSET CONTEXT -------- */
|
||||
export interface CardContextProps {
|
||||
asset: GenieAsset
|
||||
hovered: boolean
|
||||
selected: boolean
|
||||
href: string
|
||||
setHref: (href: string) => void
|
||||
}
|
||||
|
||||
const CardContext = createContext<CardContextProps | undefined>(undefined)
|
||||
|
||||
const useCardContext = () => {
|
||||
const context = useContext(CardContext)
|
||||
if (!context) throw new Error('Must use context inside of provider')
|
||||
return context
|
||||
}
|
||||
|
||||
const baseHref = (asset: GenieAsset) => `/#/nft/asset/${asset.address}/${asset.tokenId}?origin=collection`
|
||||
|
||||
/* -------- ASSET CARD -------- */
|
||||
interface CardProps {
|
||||
asset: GenieAsset
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const Container = ({ asset, children }: CardProps) => {
|
||||
const [hovered, toggleHovered] = useReducer((s) => !s, false)
|
||||
const [href, setHref] = useState(baseHref(asset))
|
||||
|
||||
const providerValue = useMemo(
|
||||
() => ({
|
||||
asset,
|
||||
selected: false,
|
||||
hovered,
|
||||
toggleHovered,
|
||||
href,
|
||||
setHref,
|
||||
}),
|
||||
[asset, hovered, href]
|
||||
)
|
||||
|
||||
const assetRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (hovered && assetRef.current?.matches(':hover') === false) toggleHovered()
|
||||
}, [hovered])
|
||||
|
||||
return (
|
||||
<CardContext.Provider value={providerValue}>
|
||||
<Box
|
||||
as="a"
|
||||
href={href ? href : baseHref(asset)}
|
||||
position={'relative'}
|
||||
ref={assetRef}
|
||||
borderRadius={'20'}
|
||||
className={styles.notSelectedCard}
|
||||
draggable={false}
|
||||
onMouseEnter={() => toggleHovered()}
|
||||
onMouseLeave={() => toggleHovered()}
|
||||
transition="250"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</CardContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/* -------- CARD IMAGE -------- */
|
||||
const Image = () => {
|
||||
const { hovered, asset } = useCardContext()
|
||||
const [noContent, setNoContent] = useState(!asset.smallImageUrl && !asset.imageUrl)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
{!noContent ? (
|
||||
<Box display="flex" overflow="hidden">
|
||||
<Box
|
||||
as={'img'}
|
||||
alt={asset.name || asset.tokenId}
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: 'auto',
|
||||
transition: 'transform 0.4s ease 0s',
|
||||
background: loaded
|
||||
? 'none'
|
||||
: `linear-gradient(270deg, ${themeVars.colors.medGray} 0%, ${themeVars.colors.lightGray} 100%)`,
|
||||
}}
|
||||
src={asset.imageUrl || asset.smallImageUrl}
|
||||
objectFit={'contain'}
|
||||
draggable={false}
|
||||
onError={() => setNoContent(true)}
|
||||
onLoad={() => {
|
||||
setLoaded(true)
|
||||
}}
|
||||
className={clsx(hovered && styles.cardImageHover)}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<NoContentContainer />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/* -------- CARD DETAILS CONTAINER -------- */
|
||||
interface CardDetailsContainerProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const DetailsContainer = ({ children }: CardDetailsContainerProps) => {
|
||||
return (
|
||||
<Row
|
||||
position="relative"
|
||||
paddingX="12"
|
||||
paddingTop="12"
|
||||
justifyContent="space-between"
|
||||
flexDirection="column"
|
||||
transition="250"
|
||||
>
|
||||
{children}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
const InfoContainer = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<Box overflow="hidden" width="full">
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const PrimaryRow = ({ children }: { children: ReactNode }) => <Row justifyContent="space-between">{children}</Row>
|
||||
|
||||
const PrimaryDetails = ({ children }: { children: ReactNode }) => (
|
||||
<Row overflow="hidden" whiteSpace="nowrap">
|
||||
{children}
|
||||
</Row>
|
||||
)
|
||||
|
||||
const PrimaryInfo = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<Box
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
color="blackBlue"
|
||||
fontWeight="medium"
|
||||
fontSize="14"
|
||||
style={{ lineHeight: '20px' }}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const SecondaryRow = ({ children }: { children: ReactNode }) => (
|
||||
<Row height="20" justifyContent="space-between" marginTop="6">
|
||||
{children}
|
||||
</Row>
|
||||
)
|
||||
|
||||
const SecondaryDetails = ({ children }: { children: ReactNode }) => <Row>{children}</Row>
|
||||
|
||||
const SecondaryInfo = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<Box
|
||||
color="blackBlue"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
fontSize="16"
|
||||
fontWeight="medium"
|
||||
style={{ lineHeight: '20px' }}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const TertiaryInfo = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<Box marginTop={'8'} color="darkGray">
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
interface ButtonProps {
|
||||
children: ReactNode
|
||||
selectedChildren: ReactNode
|
||||
onClick: (e: MouseEvent) => void
|
||||
onSelectedClick: (e: MouseEvent) => void
|
||||
}
|
||||
|
||||
const Button = ({ children, selectedChildren, onClick, onSelectedClick }: ButtonProps) => {
|
||||
const [buttonHovered, toggleButtonHovered] = useReducer((s) => !s, false)
|
||||
const { asset, selected, setHref } = useCardContext()
|
||||
const buttonRef = useRef<HTMLDivElement>(null)
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (buttonHovered && buttonRef.current?.matches(':hover') === false) toggleButtonHovered()
|
||||
}, [buttonHovered])
|
||||
|
||||
return (
|
||||
<>
|
||||
{!selected || asset.tokenType !== 'ERC1155' ? (
|
||||
<Box
|
||||
as="button"
|
||||
ref={buttonRef}
|
||||
color={
|
||||
buttonHovered || isMobile
|
||||
? 'explicitWhite'
|
||||
: selected
|
||||
? 'error'
|
||||
: asset.notForSale
|
||||
? 'placeholder'
|
||||
: 'blue400'
|
||||
}
|
||||
style={{
|
||||
background: `${
|
||||
buttonHovered || isMobile
|
||||
? selected
|
||||
? vars.color.error
|
||||
: vars.color.blue400
|
||||
: selected
|
||||
? '#FA2B391F'
|
||||
: '#4C82FB1F'
|
||||
}`,
|
||||
}}
|
||||
className={clsx(styles.button, subheadSmall)}
|
||||
onClick={(e) =>
|
||||
selected
|
||||
? onSelectedClick(e)
|
||||
: asset.notForSale
|
||||
? () => {
|
||||
return true
|
||||
}
|
||||
: onClick(e)
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
!asset.notForSale && setHref('')
|
||||
!buttonHovered && toggleButtonHovered()
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
!asset.notForSale && setHref(baseHref(asset))
|
||||
buttonHovered && toggleButtonHovered()
|
||||
}}
|
||||
transition="250"
|
||||
>
|
||||
{selected
|
||||
? selectedChildren
|
||||
: asset.notForSale
|
||||
? buttonHovered || isMobile
|
||||
? 'See details'
|
||||
: 'Not for sale'
|
||||
: children}
|
||||
</Box>
|
||||
) : (
|
||||
<Row className={styles.erc1155ButtonRow}>
|
||||
<Column
|
||||
as="button"
|
||||
className={styles.erc1155MinusButton}
|
||||
onClick={(e: MouseEvent<Element, globalThis.MouseEvent>) => onSelectedClick(e)}
|
||||
>
|
||||
<MinusIconLarge width="32" height="32" />
|
||||
</Column>
|
||||
<Box className={`${styles.erc1155QuantityText} ${subheadSmall}`}></Box>
|
||||
<Column
|
||||
as="button"
|
||||
className={styles.erc1155PlusButton}
|
||||
onClick={(e: MouseEvent<Element, globalThis.MouseEvent>) => onClick(e)}
|
||||
>
|
||||
<PlusIconLarge width="32" height="32" />
|
||||
</Column>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const MarketplaceIcon = ({ marketplace }: { marketplace: string }) => {
|
||||
return (
|
||||
<Box
|
||||
as="img"
|
||||
alt={marketplace}
|
||||
src={`/nft/svgs/marketplaces/${marketplace}.svg`}
|
||||
className={styles.marketplaceIcon}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const NoContentContainer = () => (
|
||||
<Box
|
||||
position="relative"
|
||||
width="full"
|
||||
style={{
|
||||
paddingTop: '100%',
|
||||
background: `linear-gradient(270deg, ${themeVars.colors.medGray} 0%, ${themeVars.colors.lightGray} 100%)`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
textAlign="center"
|
||||
left="1/2"
|
||||
top="1/2"
|
||||
style={{ transform: 'translate3d(-50%, -50%, 0)' }}
|
||||
fontWeight="normal"
|
||||
color="grey500"
|
||||
className={body}
|
||||
>
|
||||
Content not
|
||||
<br />
|
||||
available yet
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
export {
|
||||
Button,
|
||||
Container,
|
||||
DetailsContainer,
|
||||
Image,
|
||||
InfoContainer,
|
||||
MarketplaceIcon,
|
||||
PrimaryDetails,
|
||||
PrimaryInfo,
|
||||
PrimaryRow,
|
||||
SecondaryDetails,
|
||||
SecondaryInfo,
|
||||
SecondaryRow,
|
||||
TertiaryInfo,
|
||||
}
|
142
src/nft/components/collection/CollectionAsset.css.ts
Normal file
142
src/nft/components/collection/CollectionAsset.css.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
|
||||
import { sprinkles, vars } from '../../css/sprinkles.css'
|
||||
|
||||
export const assetInnerStyle = style([
|
||||
sprinkles({
|
||||
borderRadius: '20',
|
||||
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
{
|
||||
border: `4px solid ${vars.color.white}`,
|
||||
},
|
||||
])
|
||||
|
||||
export const hoverAsset = style({
|
||||
border: `4px solid ${vars.color.genieBlue}`,
|
||||
boxShadow: '0 4px 16px rgba(70,115,250,0.4)',
|
||||
})
|
||||
|
||||
export const assetSelected = style([
|
||||
sprinkles({
|
||||
borderRadius: '20',
|
||||
}),
|
||||
{
|
||||
border: `4px solid ${vars.color.genieBlue}`,
|
||||
},
|
||||
])
|
||||
|
||||
export const buy = style([
|
||||
{
|
||||
top: '-32px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
},
|
||||
sprinkles({ color: 'white', position: 'absolute', borderRadius: 'round' }),
|
||||
])
|
||||
|
||||
export const tokenQuantityHovered = style([
|
||||
{
|
||||
border: `4px solid ${vars.color.white}`,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
sprinkles({
|
||||
backgroundColor: 'genieBlue',
|
||||
borderRadius: 'round',
|
||||
}),
|
||||
])
|
||||
|
||||
export const tokenQuantity = style([
|
||||
{
|
||||
padding: '10px 17px',
|
||||
border: `4px solid ${vars.color.white}`,
|
||||
},
|
||||
sprinkles({
|
||||
color: 'genieBlue',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 'round',
|
||||
textAlign: 'center',
|
||||
}),
|
||||
])
|
||||
|
||||
export const plusIcon = style([
|
||||
{
|
||||
padding: '10px',
|
||||
border: `4px solid ${vars.color.white}`,
|
||||
},
|
||||
sprinkles({
|
||||
width: '28',
|
||||
backgroundColor: 'genieBlue',
|
||||
borderRadius: 'round',
|
||||
}),
|
||||
])
|
||||
|
||||
export const bagIcon = style([
|
||||
{
|
||||
width: '42px',
|
||||
padding: '9px',
|
||||
},
|
||||
sprinkles({
|
||||
borderRadius: 'round',
|
||||
backgroundColor: 'white',
|
||||
}),
|
||||
])
|
||||
|
||||
export const minusIcon = style([
|
||||
{
|
||||
width: '11px',
|
||||
padding: '19px 14px 8px 16px',
|
||||
},
|
||||
sprinkles({
|
||||
position: 'relative',
|
||||
}),
|
||||
])
|
||||
|
||||
export const plusQuantityIcon = style([
|
||||
{
|
||||
width: '12px',
|
||||
padding: '11px 16px 8px 12px',
|
||||
},
|
||||
sprinkles({
|
||||
position: 'relative',
|
||||
}),
|
||||
])
|
||||
|
||||
export const quantity = style([
|
||||
{
|
||||
padding: '9px 4px 8px',
|
||||
},
|
||||
sprinkles({
|
||||
position: 'relative',
|
||||
}),
|
||||
])
|
||||
|
||||
export const details = style({ float: 'right' })
|
||||
|
||||
export const marketplace = style({
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
bottom: '12px',
|
||||
})
|
||||
|
||||
export const placeholderImage = style({ width: '50%', padding: '25%' })
|
||||
export const ethIcon = style({ display: 'inline-block', marginBottom: '-3px', overflow: 'auto' })
|
||||
|
||||
export const rarityInfo = style({
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
backdropFilter: 'blur(6px)',
|
||||
})
|
||||
|
||||
export const iconToolTip = style([
|
||||
sprinkles({
|
||||
display: 'inline-block',
|
||||
overflow: 'auto',
|
||||
marginRight: '4',
|
||||
}),
|
||||
{
|
||||
marginBottom: '-3px',
|
||||
},
|
||||
])
|
57
src/nft/components/collection/CollectionAsset.tsx
Normal file
57
src/nft/components/collection/CollectionAsset.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import * as Card from 'nft/components/collection/Card'
|
||||
import { MouseEvent, useMemo } from 'react'
|
||||
|
||||
import { GenieAsset } from '../../types'
|
||||
import { formatWeiToDecimal } from '../../utils/currency'
|
||||
|
||||
export const CollectionAsset = ({ asset }: { asset: GenieAsset }) => {
|
||||
// ignore structure more will go inside
|
||||
const { notForSale } = useMemo(() => {
|
||||
if (asset) {
|
||||
return {
|
||||
notForSale: asset.notForSale || BigNumber.from(asset.currentEthPrice ? asset.currentEthPrice : 0).lt(0),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
notForSale: true,
|
||||
}
|
||||
}
|
||||
}, [asset])
|
||||
|
||||
return (
|
||||
<Card.Container asset={asset}>
|
||||
<Card.Image />
|
||||
<Card.DetailsContainer>
|
||||
<Card.InfoContainer>
|
||||
<Card.PrimaryRow>
|
||||
<Card.PrimaryDetails>
|
||||
<Card.PrimaryInfo>{asset.name ? asset.name : `#${asset.tokenId}`}</Card.PrimaryInfo>
|
||||
</Card.PrimaryDetails>
|
||||
</Card.PrimaryRow>
|
||||
<Card.SecondaryRow>
|
||||
<Card.SecondaryDetails>
|
||||
<Card.SecondaryInfo>
|
||||
{notForSale ? '' : `${formatWeiToDecimal(asset.currentEthPrice)} ETH`}
|
||||
</Card.SecondaryInfo>
|
||||
</Card.SecondaryDetails>
|
||||
{asset.tokenType !== 'ERC1155' && asset.marketplace && (
|
||||
<Card.MarketplaceIcon marketplace={asset.marketplace} />
|
||||
)}
|
||||
</Card.SecondaryRow>
|
||||
</Card.InfoContainer>
|
||||
<Card.Button
|
||||
selectedChildren={'Remove'}
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
}}
|
||||
onSelectedClick={(e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
{'Buy now'}
|
||||
</Card.Button>
|
||||
</Card.DetailsContainer>
|
||||
</Card.Container>
|
||||
)
|
||||
}
|
22
src/nft/components/collection/CollectionNfts.css.ts
Normal file
22
src/nft/components/collection/CollectionNfts.css.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
|
||||
import { sprinkles } from '../../css/sprinkles.css'
|
||||
|
||||
export const assetList = style([
|
||||
sprinkles({
|
||||
display: 'grid',
|
||||
marginTop: '24',
|
||||
gap: { mobile: '8', tablet: '12', tabletXl: '20' },
|
||||
}),
|
||||
{
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr) )',
|
||||
'@media': {
|
||||
'screen and (min-width: 708px)': {
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr) )',
|
||||
},
|
||||
'screen and (min-width: 1185px)': {
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(1fr, 280px) )',
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
83
src/nft/components/collection/CollectionNfts.tsx
Normal file
83
src/nft/components/collection/CollectionNfts.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import clsx from 'clsx'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { CollectionAsset } from 'nft/components/collection/CollectionAsset'
|
||||
import * as styles from 'nft/components/collection/CollectionNfts.css'
|
||||
import { Center } from 'nft/components/Flex'
|
||||
import { bodySmall, buttonTextMedium, header2 } from 'nft/css/common.css'
|
||||
import { AssetsFetcher } from 'nft/queries'
|
||||
import { useMemo } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import { useInfiniteQuery } from 'react-query'
|
||||
|
||||
interface CollectionNftsProps {
|
||||
contractAddress: string
|
||||
}
|
||||
|
||||
export const CollectionNfts = ({ contractAddress }: CollectionNftsProps) => {
|
||||
const {
|
||||
data: collectionAssets,
|
||||
isSuccess: AssetsFetchSuccess,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
} = useInfiniteQuery(
|
||||
[
|
||||
'collectionNfts',
|
||||
{
|
||||
contractAddress,
|
||||
},
|
||||
],
|
||||
async ({ pageParam = 0 }) => {
|
||||
return await AssetsFetcher({
|
||||
contractAddress,
|
||||
pageParam,
|
||||
})
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage, pages) => {
|
||||
return lastPage?.flat().length === 25 ? pages.length : null
|
||||
},
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchInterval: 5000,
|
||||
}
|
||||
)
|
||||
|
||||
const collectionNfts = useMemo(() => {
|
||||
if (!collectionAssets || !AssetsFetchSuccess) return undefined
|
||||
|
||||
return collectionAssets.pages.flat()
|
||||
}, [collectionAssets, AssetsFetchSuccess])
|
||||
|
||||
if (!collectionNfts) {
|
||||
// TODO: collection unavailable page
|
||||
return <div>No CollectionAssets</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<InfiniteScroll
|
||||
next={fetchNextPage}
|
||||
hasMore={hasNextPage ?? false}
|
||||
loader={hasNextPage ? <p>Loading from scroll...</p> : null}
|
||||
dataLength={collectionNfts.length}
|
||||
style={{ overflow: 'unset' }}
|
||||
>
|
||||
{collectionNfts.length > 0 ? (
|
||||
<div className={styles.assetList}>
|
||||
{collectionNfts.map((asset) => {
|
||||
return asset ? <CollectionAsset asset={asset} key={asset.address + asset.tokenId} /> : null
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<Center width="full" color="darkGray" style={{ height: '60vh' }}>
|
||||
<div style={{ display: 'block', textAlign: 'center' }}>
|
||||
<p className={header2}>No NFTS found</p>
|
||||
<Box className={clsx(bodySmall, buttonTextMedium)} color="blue" cursor="pointer">
|
||||
View full collection
|
||||
</Box>
|
||||
</div>
|
||||
</Center>
|
||||
)}
|
||||
</InfiniteScroll>
|
||||
)
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import { AnimatedBox, Box } from 'nft/components/Box'
|
||||
import { CollectionStats } from 'nft/components/collection/CollectionStats'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { CollectionProps } from 'nft/pages/collection/common'
|
||||
import * as styles from 'nft/pages/collection/common.css'
|
||||
|
||||
export const CollectionDesktop = ({ collectionStats }: CollectionProps) => {
|
||||
return (
|
||||
<Column width="full">
|
||||
<Box width="full" height="160">
|
||||
<Box
|
||||
as="img"
|
||||
maxHeight="full"
|
||||
width="full"
|
||||
src={collectionStats?.bannerImageUrl}
|
||||
className={`${styles.bannerImage}`}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{collectionStats && (
|
||||
<Row paddingLeft="32" paddingRight="32">
|
||||
<CollectionStats stats={collectionStats} isMobile={false} />
|
||||
</Row>
|
||||
)}
|
||||
<Row alignItems="flex-start" position="relative" paddingLeft="32" paddingRight="32">
|
||||
<AnimatedBox width="full">CollectionNfts</AnimatedBox>
|
||||
</Row>
|
||||
</Column>
|
||||
)
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import { AnimatedBox, Box } from 'nft/components/Box'
|
||||
import { CollectionStats } from 'nft/components/collection/CollectionStats'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { CollectionProps } from 'nft/pages/collection/common'
|
||||
import * as styles from 'nft/pages/collection/common.css'
|
||||
|
||||
export const CollectionMobile = ({ collectionStats }: CollectionProps) => {
|
||||
return (
|
||||
<Column width="full">
|
||||
<Box width="full" height="160">
|
||||
<Box
|
||||
as="img"
|
||||
maxHeight="full"
|
||||
width="full"
|
||||
src={collectionStats?.bannerImageUrl}
|
||||
className={`${styles.bannerImage}`}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{collectionStats && (
|
||||
<Row paddingLeft="32" paddingRight="32">
|
||||
<CollectionStats stats={collectionStats} isMobile={true} />
|
||||
</Row>
|
||||
)}
|
||||
<Row alignItems="flex-start" position="relative" paddingLeft="32" paddingRight="32">
|
||||
<AnimatedBox width="full">CollectionNfts</AnimatedBox>
|
||||
</Row>
|
||||
</Column>
|
||||
)
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { GenieCollection } from 'nft/types'
|
||||
|
||||
export interface CollectionProps {
|
||||
collectionStats: GenieCollection | undefined
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
import { AnimatedBox, Box } from 'nft/components/Box'
|
||||
import { CollectionNfts } from 'nft/components/collection/CollectionNfts'
|
||||
import { CollectionStats } from 'nft/components/collection/CollectionStats'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { useIsMobile } from 'nft/hooks/useIsMobile'
|
||||
import * as styles from 'nft/pages/collection/index.css'
|
||||
import { CollectionStatsFetcher } from 'nft/queries'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import { useIsMobile } from '../../hooks/useIsMobile'
|
||||
import { CollectionStatsFetcher } from '../../queries'
|
||||
import { CollectionDesktop } from './CollectionDesktop'
|
||||
import { CollectionMobile } from './CollectionMobile'
|
||||
|
||||
const Collection = () => {
|
||||
const { contractAddress } = useParams()
|
||||
|
||||
@ -15,10 +17,29 @@ const Collection = () => {
|
||||
CollectionStatsFetcher(contractAddress as string)
|
||||
)
|
||||
|
||||
return isMobile ? (
|
||||
<CollectionMobile collectionStats={collectionStats} />
|
||||
) : (
|
||||
<CollectionDesktop collectionStats={collectionStats} />
|
||||
return (
|
||||
<Column width="full">
|
||||
<Box width="full" height="160">
|
||||
<Box
|
||||
as="img"
|
||||
maxHeight="full"
|
||||
width="full"
|
||||
src={collectionStats?.bannerImageUrl}
|
||||
className={`${styles.bannerImage}`}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{collectionStats && (
|
||||
<Row paddingLeft="32" paddingRight="32">
|
||||
<CollectionStats stats={collectionStats} isMobile={isMobile} />
|
||||
</Row>
|
||||
)}
|
||||
<Row alignItems="flex-start" position="relative" paddingLeft="32" paddingRight="32">
|
||||
<AnimatedBox width="full">
|
||||
{contractAddress && <CollectionNfts contractAddress={contractAddress} />}
|
||||
</AnimatedBox>
|
||||
</Row>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
|
12
yarn.lock
12
yarn.lock
@ -14662,6 +14662,13 @@ react-ga4@^1.4.1:
|
||||
resolved "https://registry.yarnpkg.com/react-ga4/-/react-ga4-1.4.1.tgz#6ee2a2db115ed235b2f2092bc746b4eeeca9e206"
|
||||
integrity sha512-ioBMEIxd4ePw4YtaloTUgqhQGqz5ebDdC4slEpLgy2sLx1LuZBC9iYCwDymTXzcntw6K1dHX183ulP32nNdG7w==
|
||||
|
||||
react-infinite-scroll-component@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz#7e511e7aa0f728ac3e51f64a38a6079ac522407f"
|
||||
integrity sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==
|
||||
dependencies:
|
||||
throttle-debounce "^2.1.0"
|
||||
|
||||
react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.6:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
@ -16613,6 +16620,11 @@ throat@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
|
||||
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
|
||||
|
||||
throttle-debounce@^2.1.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2"
|
||||
integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==
|
||||
|
||||
throttleit@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
|
||||
|
Loading…
Reference in New Issue
Block a user