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-dom": "^18.2.0",
|
||||||
"react-feather": "^2.0.8",
|
"react-feather": "^2.0.8",
|
||||||
"react-ga4": "^1.4.1",
|
"react-ga4": "^1.4.1",
|
||||||
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-is": "^17.0.2",
|
"react-is": "^17.0.2",
|
||||||
"react-markdown": "^4.3.1",
|
"react-markdown": "^4.3.1",
|
||||||
"react-popper": "^2.2.3",
|
"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 { useQuery } from 'react-query'
|
||||||
import { useParams } from 'react-router-dom'
|
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 Collection = () => {
|
||||||
const { contractAddress } = useParams()
|
const { contractAddress } = useParams()
|
||||||
|
|
||||||
@ -15,10 +17,29 @@ const Collection = () => {
|
|||||||
CollectionStatsFetcher(contractAddress as string)
|
CollectionStatsFetcher(contractAddress as string)
|
||||||
)
|
)
|
||||||
|
|
||||||
return isMobile ? (
|
return (
|
||||||
<CollectionMobile collectionStats={collectionStats} />
|
<Column width="full">
|
||||||
) : (
|
<Box width="full" height="160">
|
||||||
<CollectionDesktop collectionStats={collectionStats} />
|
<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"
|
resolved "https://registry.yarnpkg.com/react-ga4/-/react-ga4-1.4.1.tgz#6ee2a2db115ed235b2f2092bc746b4eeeca9e206"
|
||||||
integrity sha512-ioBMEIxd4ePw4YtaloTUgqhQGqz5ebDdC4slEpLgy2sLx1LuZBC9iYCwDymTXzcntw6K1dHX183ulP32nNdG7w==
|
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:
|
react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.6:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
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"
|
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
|
||||||
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
|
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:
|
throttleit@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
|
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
|
||||||
|
Loading…
Reference in New Issue
Block a user