Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
I
interface
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
LuckySwap
interface
Commits
4b1b6098
Unverified
Commit
4b1b6098
authored
Nov 04, 2022
by
aballerr
Committed by
GitHub
Nov 04, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: Details implementation (#5059)
* Details Page Update
parent
53a6acc1
Changes
25
Show whitespace changes
Inline
Side-by-side
Showing
25 changed files
with
1219 additions
and
542 deletions
+1219
-542
SuggestionRow.tsx
src/components/NavBar/SuggestionRow.tsx
+1
-1
Collection.ts
src/graphql/data/nft/Collection.ts
+3
-4
Details.ts
src/graphql/data/nft/Details.ts
+5
-6
Bag.tsx
src/nft/components/bag/Bag.tsx
+17
-1
Activity.css.ts
src/nft/components/collection/Activity.css.ts
+0
-1
Activity.tsx
src/nft/components/collection/Activity.tsx
+1
-1
ActivityCells.tsx
src/nft/components/collection/ActivityCells.tsx
+18
-6
CollectionStats.tsx
src/nft/components/collection/CollectionStats.tsx
+5
-5
AssetActivity.tsx
src/nft/components/details/AssetActivity.tsx
+182
-0
AssetDetails.css.ts
src/nft/components/details/AssetDetails.css.ts
+5
-8
AssetDetails.tsx
src/nft/components/details/AssetDetails.tsx
+418
-346
AssetPriceDetails.tsx
src/nft/components/details/AssetPriceDetails.tsx
+178
-52
DetailsContainer.tsx
src/nft/components/details/DetailsContainer.tsx
+132
-0
InfoContainer.tsx
src/nft/components/details/InfoContainer.tsx
+89
-0
Traits.css.ts
src/nft/components/details/Traits.css.ts
+0
-18
Traits.tsx
src/nft/components/details/Traits.tsx
+0
-62
TraitsContainer.tsx
src/nft/components/details/TraitsContainer.tsx
+97
-0
icons.tsx
src/nft/components/icons.tsx
+2
-3
Asset.tsx
src/nft/pages/asset/Asset.tsx
+23
-4
ActivityFetcher.ts
src/nft/queries/genie/ActivityFetcher.ts
+8
-4
SingleAssetFetcher.ts
src/nft/queries/genie/SingleAssetFetcher.ts
+10
-1
collection.ts
src/nft/types/collection/collection.ts
+2
-1
common.ts
src/nft/types/common/common.ts
+18
-14
fetchPrice.ts
src/nft/utils/fetchPrice.ts
+4
-3
putCommas.ts
src/nft/utils/putCommas.ts
+1
-1
No files found.
src/components/NavBar/SuggestionRow.tsx
View file @
4b1b6098
...
...
@@ -93,7 +93,7 @@ export const CollectionRow = ({
<
Box
className=
{
styles
.
primaryText
}
>
{
collection
.
name
}
</
Box
>
{
collection
.
isVerified
&&
<
VerifiedIcon
className=
{
styles
.
suggestionIcon
}
/>
}
</
Row
>
<
Box
className=
{
styles
.
secondaryText
}
>
{
putCommas
(
collection
.
stats
?.
total_supply
)
}
items
</
Box
>
<
Box
className=
{
styles
.
secondaryText
}
>
{
putCommas
(
collection
?.
stats
?.
total_supply
??
0
)
}
items
</
Box
>
</
Column
>
</
Row
>
{
collection
.
stats
?.
floor_price
?
(
...
...
src/graphql/data/nft/Collection.ts
View file @
4b1b6098
import
graphql
from
'
babel-plugin-relay/macro
'
import
{
Trait
}
from
'
nft/hooks/useCollectionFilters
'
import
{
GenieCollection
}
from
'
nft/types
'
import
{
GenieCollection
,
Trait
}
from
'
nft/types
'
import
{
useLazyLoadQuery
}
from
'
react-relay
'
import
{
CollectionQuery
}
from
'
./__generated__/CollectionQuery.graphql
'
...
...
@@ -122,8 +121,8 @@ export function useCollectionQuery(address: string): GenieCollection | undefined
:
{},
traits
,
// marketplaceCount: { marketplace: string; count: number }[], // TODO add when backend supports
imageUrl
:
queryCollection
?.
image
?.
url
,
twitter
:
queryCollection
?.
twitterName
??
undefined
,
imageUrl
:
queryCollection
?.
image
?.
url
??
''
,
twitter
Url
:
queryCollection
?.
twitterName
??
''
,
instagram
:
queryCollection
?.
instagramName
??
undefined
,
discordUrl
:
queryCollection
?.
discordUrl
??
undefined
,
externalUrl
:
queryCollection
?.
homepageUrl
??
undefined
,
...
...
src/graphql/data/nft/Details.ts
View file @
4b1b6098
import
{
parseEther
}
from
'
@ethersproject/units
'
import
graphql
from
'
babel-plugin-relay/macro
'
import
{
Trait
}
from
'
nft/hooks
'
import
{
CollectionInfoForAsset
,
GenieAsset
,
SellOrder
,
TokenType
}
from
'
nft/types
'
import
{
useLazyLoadQuery
}
from
'
react-relay
'
...
...
@@ -141,14 +140,14 @@ export function useDetailsQuery(address: string, tokenId: string): [GenieAsset,
})
:
undefined
,
},
owner
:
asset
?.
ownerAddress
??
undefined
,
owner
:
{
address
:
asset
?.
ownerAddress
??
''
}
,
creator
:
{
profile_img_url
:
asset
?.
creator
?.
profileImage
?.
url
,
address
:
asset
?.
creator
?.
address
,
profile_img_url
:
asset
?.
creator
?.
profileImage
?.
url
??
''
,
address
:
asset
?.
creator
?.
address
??
''
,
},
metadataUrl
:
asset
?.
metadataUrl
??
undefined
,
metadataUrl
:
asset
?.
metadataUrl
??
''
,
traits
:
asset
?.
traits
?.
map
((
trait
)
=>
{
return
{
trait_type
:
trait
.
name
??
undefined
,
trait_value
:
trait
.
value
??
undefined
}
as
Trait
return
{
trait_type
:
trait
.
name
??
''
,
trait_value
:
trait
.
value
??
''
}
}),
},
{
...
...
src/nft/components/bag/Bag.tsx
View file @
4b1b6098
...
...
@@ -29,6 +29,7 @@ import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTx
import
{
useCallback
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
'
react
'
import
{
useQuery
,
useQueryClient
}
from
'
react-query
'
import
{
useLocation
}
from
'
react-router-dom
'
import
styled
from
'
styled-components/macro
'
import
*
as
styles
from
'
./Bag.css
'
import
{
BagContent
}
from
'
./BagContent
'
...
...
@@ -41,6 +42,15 @@ interface SeparatorProps {
show
?:
boolean
}
const
DetailsPageBackground
=
styled
.
div
`
position: fixed;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(12px);
top: 72px;
width: 100%;
height: 100%;
`
const
ScrollingIndicator
=
({
top
,
show
}:
SeparatorProps
)
=>
(
<
Box
marginX=
"16"
...
...
@@ -82,6 +92,8 @@ const Bag = () => {
const
shouldShowBag
=
isNFTPage
||
isProfilePage
const
isMobile
=
useIsMobile
()
const
isDetailsPage
=
pathname
.
includes
(
'
/nfts/asset/
'
)
const
sendTransaction
=
useSendTransaction
((
state
)
=>
state
.
sendTransaction
)
const
transactionState
=
useSendTransaction
((
state
)
=>
state
.
state
)
const
setTransactionState
=
useSendTransaction
((
state
)
=>
state
.
setState
)
...
...
@@ -304,7 +316,11 @@ const Bag = () => {
<
ListingModal
/>
)
}
</
Column
>
{
isOpen
&&
<
Overlay
onClick=
{
()
=>
(
!
bagIsLocked
?
setModalIsOpen
(
false
)
:
undefined
)
}
/>
}
{
isDetailsPage
?
(
<
DetailsPageBackground
onClick=
{
toggleBag
}
/>
)
:
(
isOpen
&&
<
Overlay
onClick=
{
()
=>
(
!
bagIsLocked
?
setModalIsOpen
(
false
)
:
undefined
)
}
/>
)
}
</
Portal
>
)
:
null
}
</>
...
...
src/nft/components/collection/Activity.css.ts
View file @
4b1b6098
...
...
@@ -79,7 +79,6 @@ export const detailsName = style([
export
const
eventDetail
=
style
([
subhead
,
sprinkles
({
marginBottom
:
'
4
'
,
gap
:
'
8
'
,
}),
{
...
...
src/nft/components/collection/Activity.tsx
View file @
4b1b6098
...
...
@@ -48,7 +48,7 @@ const initialFilterState = {
[
ActivityEventType
.
CancelListing
]:
false
,
}
const
reduceFilters
=
(
state
:
typeof
initialFilterState
,
action
:
{
eventType
:
ActivityEventType
})
=>
{
export
const
reduceFilters
=
(
state
:
typeof
initialFilterState
,
action
:
{
eventType
:
ActivityEventType
})
=>
{
return
{
...
state
,
[
action
.
eventType
]:
!
state
[
action
.
eventType
]
}
}
...
...
src/nft/components/collection/ActivityCells.tsx
View file @
4b1b6098
...
...
@@ -10,6 +10,7 @@ import {
ActivityListingIcon
,
ActivitySaleIcon
,
ActivityTransferIcon
,
CancelListingIcon
,
RarityVerifiedIcon
,
}
from
'
nft/components/icons
'
import
{
...
...
@@ -157,7 +158,7 @@ export const AddressCell = ({ address, desktopLBreakpoint, chainId }: AddressCel
)
}
const
MarketplaceIcon
=
({
marketplace
}:
{
marketplace
:
Markets
})
=>
{
export
const
MarketplaceIcon
=
({
marketplace
}:
{
marketplace
:
Markets
})
=>
{
return
(
<
Box
as=
"img"
...
...
@@ -204,8 +205,9 @@ interface EventCellProps {
eventType
:
ActivityEventType
eventTimestamp
?:
number
eventTransactionHash
?:
string
eventOnly
?:
boolean
price
?:
string
isMobile
:
boolean
isMobile
?
:
boolean
}
const
renderEventIcon
=
(
eventType
:
ActivityEventType
)
=>
{
...
...
@@ -216,6 +218,8 @@ const renderEventIcon = (eventType: ActivityEventType) => {
return
<
ActivitySaleIcon
width=
{
16
}
height=
{
16
}
/>
case
ActivityEventType
.
Transfer
:
return
<
ActivityTransferIcon
width=
{
16
}
height=
{
16
}
/>
case
ActivityEventType
.
CancelListing
:
return
<
CancelListingIcon
width=
{
16
}
height=
{
16
}
/>
default
:
return
null
}
...
...
@@ -237,13 +241,20 @@ const eventColors = (eventType: ActivityEventType) => {
[
ActivityEventType
.
Listing
]:
'
gold
'
,
[
ActivityEventType
.
Sale
]:
'
green
'
,
[
ActivityEventType
.
Transfer
]:
'
violet
'
,
[
ActivityEventType
.
CancelListing
]:
'
error
'
,
[
ActivityEventType
.
CancelListing
]:
'
accentFailure
'
,
}
return
activityEvents
[
eventType
]
as
'
gold
'
|
'
green
'
|
'
violet
'
|
'
accentFailure
'
}
export
const
EventCell
=
({
eventType
,
eventTimestamp
,
eventTransactionHash
,
price
,
isMobile
}:
EventCellProps
)
=>
{
export
const
EventCell
=
({
eventType
,
eventTimestamp
,
eventTransactionHash
,
eventOnly
,
price
,
isMobile
,
}:
EventCellProps
)
=>
{
const
formattedPrice
=
useMemo
(()
=>
(
price
?
putCommas
(
formatEthPrice
(
price
))?.
toString
()
:
null
),
[
price
])
return
(
<
Column
height=
"full"
justifyContent=
"center"
gap=
"4"
>
...
...
@@ -251,7 +262,7 @@ export const EventCell = ({ eventType, eventTimestamp, eventTransactionHash, pri
{
renderEventIcon
(
eventType
)
}
{
ActivityEventTypeDisplay
[
eventType
]
}
</
Row
>
{
eventTimestamp
&&
isValidDate
(
eventTimestamp
)
&&
!
isMobile
&&
(
{
eventTimestamp
&&
isValidDate
(
eventTimestamp
)
&&
!
isMobile
&&
!
eventOnly
&&
(
<
Row
className=
{
styles
.
eventTime
}
>
{
getTimeDifference
(
eventTimestamp
.
toString
())
}
{
eventTransactionHash
&&
<
ExternalLinkIcon
transactionHash=
{
eventTransactionHash
}
/>
}
...
...
@@ -301,9 +312,10 @@ interface RankingProps {
rarity
:
TokenRarity
collectionName
:
string
rarityVerified
:
boolean
details
?:
boolean
}
const
Ranking
=
({
rarity
,
collectionName
,
rarityVerified
}:
RankingProps
)
=>
{
const
Ranking
=
({
details
,
rarity
,
collectionName
,
rarityVerified
}:
RankingProps
)
=>
{
const
rarityProviderLogo
=
getRarityProviderLogo
(
rarity
.
source
)
return
(
...
...
src/nft/components/collection/CollectionStats.tsx
View file @
4b1b6098
...
...
@@ -76,8 +76,8 @@ const MobileSocialsPopover = ({
</
Box
>
</
MobileSocialsIcon
>
)
:
null
}
{
collectionStats
.
twitter
?
(
<
MobileSocialsIcon
href=
{
'
https://twitter.com/
'
+
collectionStats
.
twitter
}
>
{
collectionStats
.
twitter
Url
?
(
<
MobileSocialsIcon
href=
{
'
https://twitter.com/
'
+
collectionStats
.
twitter
Url
}
>
<
Box
margin=
"auto"
paddingTop=
"6"
>
<
TwitterIcon
fill=
{
themeVars
.
colors
.
textSecondary
}
...
...
@@ -161,8 +161,8 @@ const CollectionName = ({
/>
</
SocialsIcon
>
)
:
null
}
{
collectionStats
.
twitter
?
(
<
SocialsIcon
href=
{
'
https://twitter.com/
'
+
collectionStats
.
twitter
}
>
{
collectionStats
.
twitter
Url
?
(
<
SocialsIcon
href=
{
'
https://twitter.com/
'
+
collectionStats
.
twitter
Url
}
>
<
TwitterIcon
fill=
{
themeVars
.
colors
.
textSecondary
}
color=
{
themeVars
.
colors
.
textSecondary
}
...
...
@@ -186,7 +186,7 @@ const CollectionName = ({
</
Row
>
{
isMobile
&&
(
collectionStats
.
discordUrl
||
collectionStats
.
twitter
||
collectionStats
.
twitter
Url
||
collectionStats
.
instagram
||
collectionStats
.
externalUrl
)
&&
(
<
MobileSocialsPopover
...
...
src/nft/components/details/AssetActivity.tsx
0 → 100644
View file @
4b1b6098
import
{
ActivityEventResponse
}
from
'
nft/types
'
import
{
shortenAddress
}
from
'
nft/utils/address
'
import
{
formatEthPrice
}
from
'
nft/utils/currency
'
import
{
getTimeDifference
}
from
'
nft/utils/date
'
import
{
putCommas
}
from
'
nft/utils/putCommas
'
import
styled
from
'
styled-components/macro
'
import
{
EventCell
}
from
'
../collection/ActivityCells
'
import
{
MarketplaceIcon
}
from
'
../collection/ActivityCells
'
const
TR
=
styled
.
tr
`
border-bottom:
${({
theme
})
=>
`1px solid
${
theme
.
backgroundOutline
}
`
}
;
width: 100%;
&:last-child {
border-bottom: none;
}
`
const
TH
=
styled
.
th
`
color:
${({
theme
})
=>
theme
.
textSecondary
}
;
font-weight: 600;
font-size: 14px;
line-height: 20px;
width: 20%;
@media (max-width: 960px) {
&:nth-child(4) {
display: none;
}
}
@media (max-width: 720px) {
&:nth-child(2) {
display: none;
}
}
`
const
Table
=
styled
.
table
`
border-collapse: collapse;
text-align: left;
width: 100%;
`
const
TD
=
styled
.
td
`
height: 56px;
padding: 8px 0px;
text-align: left;
vertical-align: middle;
width: 20%;
@media (max-width: 960px) {
&:nth-child(4) {
display: none;
}
}
@media (max-width: 720px) {
&:nth-child(2) {
display: none;
}
}
`
const
PriceContainer
=
styled
.
div
`
align-items: center;
display: flex;
gap: 8px;
`
const
Link
=
styled
.
a
`
color:
${({
theme
})
=>
theme
.
textPrimary
}
;
text-decoration: none;
&:hover {
opacity:
${({
theme
})
=>
theme
.
opacity
.
hover
}
;
}
&:active {
opacity:
${({
theme
})
=>
theme
.
opacity
.
click
}
;
}
transition:
${({
theme
:
{
transition
:
{
duration
,
timing
},
},
})
=>
`opacity
${
duration
.
medium
}
${
timing
.
ease
}
`
}
;
`
const
ActivityContainer
=
styled
.
div
`
max-height: 310px;
overflow: auto;
// Firefox scrollbar styling
scrollbar-width: thin;
scrollbar-color:
${({
theme
})
=>
`
${
theme
.
backgroundOutline
}
transparent`
}
;
// safari and chrome scrollbar styling
::-webkit-scrollbar {
background: transparent;
width: 4px;
}
::-webkit-scrollbar-thumb {
background:
${({
theme
})
=>
theme
.
backgroundOutline
}
;
border-radius: 8px;
}
`
const
AssetActivity
=
({
eventsData
}:
{
eventsData
:
ActivityEventResponse
|
undefined
})
=>
{
return
(
<
ActivityContainer
id=
"activityContainer"
>
<
Table
>
<
thead
>
<
TR
>
<
TH
>
Event
</
TH
>
<
TH
>
Price
</
TH
>
<
TH
>
By
</
TH
>
<
TH
>
To
</
TH
>
<
TH
>
Time
</
TH
>
</
TR
>
</
thead
>
<
tbody
>
{
eventsData
?.
events
&&
eventsData
.
events
.
map
((
event
,
index
)
=>
{
const
{
eventTimestamp
,
eventType
,
fromAddress
,
marketplace
,
price
,
toAddress
,
transactionHash
}
=
event
const
formattedPrice
=
price
?
putCommas
(
formatEthPrice
(
price
)).
toString
()
:
null
return
(
<
TR
key=
{
index
}
>
<
TD
>
<
EventCell
eventType=
{
eventType
}
eventTimestamp=
{
eventTimestamp
}
eventTransactionHash=
{
transactionHash
}
eventOnly
/>
</
TD
>
<
TD
>
{
formattedPrice
&&
(
<
PriceContainer
>
{
marketplace
&&
<
MarketplaceIcon
marketplace=
{
marketplace
}
/>
}
{
formattedPrice
}
ETH
</
PriceContainer
>
)
}
</
TD
>
<
TD
>
{
fromAddress
&&
(
<
Link
href=
{
`https://etherscan.io/address/${fromAddress}`
}
target=
"_blank"
rel=
"noopener noreferrer"
>
{
shortenAddress
(
fromAddress
,
2
,
4
)
}
</
Link
>
)
}
</
TD
>
<
TD
>
{
toAddress
&&
(
<
Link
href=
{
`https://etherscan.io/address/${toAddress}`
}
target=
"_blank"
rel=
"noopener noreferrer"
>
{
shortenAddress
(
toAddress
,
2
,
4
)
}
</
Link
>
)
}
</
TD
>
<
TD
>
{
eventTimestamp
&&
getTimeDifference
(
eventTimestamp
.
toString
())
}
</
TD
>
</
TR
>
)
})
}
</
tbody
>
</
Table
>
</
ActivityContainer
>
)
}
export
default
AssetActivity
src/nft/components/details/AssetDetails.css.ts
View file @
4b1b6098
...
...
@@ -6,19 +6,18 @@ import { sprinkles, vars } from '../../css/sprinkles.css'
export
const
image
=
style
([
sprinkles
({
borderRadius
:
'
20
'
,
height
:
'
full
'
,
alignSelf
:
'
center
'
}),
{
width
:
'
calc(90vh - 165px)
'
,
height
:
'
calc(90vh - 165px)
'
,
maxHeight
:
'
678px
'
,
maxWidth
:
'
678px
'
,
maxHeight
:
'
calc(90vh - 165px)
'
,
minHeight
:
400
,
maxWidth
:
780
,
boxShadow
:
`0px 20px 50px var(--shadow), 0px 10px 50px rgba(70, 115, 250, 0.2)`
,
'
@media
'
:
{
'
(max-width: 1024px)
'
:
{
maxHeight
:
'
64vh
'
,
maxWidth
:
'
64vh
'
,
},
'
(max-width: 640px)
'
:
{
minHeight
:
280
,
maxHeight
:
'
56vh
'
,
maxWidth
:
'
56vh
'
,
maxWidth
:
'
100%
'
,
},
},
},
...
...
@@ -81,8 +80,6 @@ export const columns = style([
])
export
const
column
=
style
({
maxWidth
:
'
50%
'
,
width
:
'
50%
'
,
alignSelf
:
'
center
'
,
'
@media
'
:
{
'
(max-width: 1024px)
'
:
{
...
...
src/nft/components/details/AssetDetails.tsx
View file @
4b1b6098
import
{
useWeb3React
}
from
'
@web3-react/core
'
import
{
sendAnalyticsEvent
}
from
'
analytics
'
import
{
EventName
,
PageName
}
from
'
analytics/constants
'
import
{
useTrace
}
from
'
analytics/Trace
'
import
clsx
from
'
clsx
'
import
Resource
from
'
components/Tokens/TokenDetails/Resource
'
import
{
MouseoverTooltip
}
from
'
components/Tooltip/index
'
import
useENSName
from
'
hooks/useENSName
'
import
{
AnimatedBox
,
Box
}
from
'
nft/components/Box
'
import
{
CollectionProfile
}
from
'
nft/components/details/CollectionProfile
'
import
{
Details
}
from
'
nft/components/details/Details
'
import
{
Traits
}
from
'
nft/components/details/Traits
'
import
{
Center
,
Column
,
Row
}
from
'
nft/components/Flex
'
import
{
CloseDropDownIcon
,
CornerDownLeftIcon
,
Eth2Icon
,
ShareIcon
,
SuspiciousIcon
}
from
'
nft/components/icons
'
import
{
ExpandableText
}
from
'
nft/components/layout/ExpandableText
'
import
{
badge
,
bodySmall
,
caption
,
headlineMedium
,
subhead
}
from
'
nft/css/common.css
'
import
{
themeVars
}
from
'
nft/css/sprinkles.css
'
import
{
useBag
}
from
'
nft/hooks
'
import
{
useTimeout
}
from
'
nft/hooks/useTimeout
'
import
{
CollectionInfoForAsset
,
Deprecated_SellOrder
,
GenieAsset
,
SellOrder
}
from
'
nft/types
'
import
{
useUsdPrice
}
from
'
nft/utils
'
import
{
Box
}
from
'
nft/components/Box
'
import
{
reduceFilters
}
from
'
nft/components/collection/Activity
'
import
{
LoadingSparkle
}
from
'
nft/components/common/Loading/LoadingSparkle
'
import
{
AssetPriceDetails
}
from
'
nft/components/details/AssetPriceDetails
'
import
{
Center
}
from
'
nft/components/Flex
'
import
{
VerifiedIcon
}
from
'
nft/components/icons
'
import
{
ActivityFetcher
}
from
'
nft/queries/genie/ActivityFetcher
'
import
{
ActivityEventResponse
,
ActivityEventType
}
from
'
nft/types
'
import
{
CollectionInfoForAsset
,
GenieAsset
,
GenieCollection
}
from
'
nft/types
'
import
{
shortenAddress
}
from
'
nft/utils/address
'
import
{
formatEthPrice
}
from
'
nft/utils/currency
'
import
{
isAssetOwnedByUser
}
from
'
nft/utils/isAssetOwnedByUser
'
import
{
isAudio
}
from
'
nft/utils/isAudio
'
import
{
isVideo
}
from
'
nft/utils/isVideo
'
import
{
fallbackProvider
,
rarityProviderLogo
}
from
'
nft/utils/rarity
'
import
{
toSignificant
}
from
'
nft/utils/toSignificant
'
import
qs
from
'
query-string
'
import
{
useEffect
,
useMemo
,
useState
}
from
'
reac
t
'
import
ReactMarkdown
from
'
react-markdown
'
import
{
Link
,
useLocation
,
useNavigate
}
from
'
react-router-dom
'
import
{
useSpring
}
from
'
react-spring
'
import
{
SUSPICIOUS_TEXT
}
from
'
../collection/Card
'
import
{
putCommas
}
from
'
nft/utils/putCommas
'
import
{
fallbackProvider
,
getRarityProviderLogo
}
from
'
nft/utils/rarity
'
import
{
useCallback
,
useMemo
,
useReducer
,
useState
}
from
'
react
'
import
InfiniteScroll
from
'
react-infinite-scroll-componen
t
'
import
{
useInfiniteQuery
,
useQuery
}
from
'
react-query
'
import
{
Link
as
RouterLink
}
from
'
react-router-dom
'
import
styled
,
{
css
}
from
'
styled-components/macro
'
import
AssetActivity
from
'
./AssetActivity
'
import
*
as
styles
from
'
./AssetDetails.css
'
import
DetailsContainer
from
'
./DetailsContainer
'
import
InfoContainer
from
'
./InfoContainer
'
import
TraitsContainer
from
'
./TraitsContainer
'
const
OpacityTransition
=
css
`
&:hover {
opacity:
${({
theme
})
=>
theme
.
opacity
.
hover
}
;
}
&:active {
opacity:
${({
theme
})
=>
theme
.
opacity
.
click
}
;
}
transition:
${({
theme
:
{
transition
:
{
duration
,
timing
},
},
})
=>
`opacity
${
duration
.
medium
}
${
timing
.
ease
}
`
}
;
`
const
CollectionHeader
=
styled
.
span
`
display: flex;
align-items: center;
font-size: 16px;
line-height: 24px;
color:
${({
theme
})
=>
theme
.
textPrimary
}
;
margin-top: 28px;
text-decoration: none;
${
OpacityTransition
}
;
`
const
AssetPriceDetailsContainer
=
styled
.
div
`
margin-top: 20px;
display: none;
@media (max-width: 960px) {
display: block;
}
`
const
AssetHeader
=
styled
.
div
`
display: flex;
align-items: center;
font-size: 36px;
line-height: 36px;
color:
${({
theme
})
=>
theme
.
textPrimary
}
;
margin-top: 8px;
`
const
MediaContainer
=
styled
.
div
`
display: flex;
justify-content: center;
`
const
Column
=
styled
.
div
`
display: flex;
flex-direction: column;
width: 100%;
max-width: 780px;
`
const
AddressTextLink
=
styled
.
a
`
display: inline-block;
color:
${({
theme
})
=>
theme
.
textSecondary
}
;
text-decoration: none;
max-width: 100%;
word-wrap: break-word;
${
OpacityTransition
}
;
`
const
SocialsContainer
=
styled
.
div
`
display: flex;
gap: 16px;
margin-top: 20px;
`
const
DescriptionText
=
styled
.
div
`
margin-top: 8px;
font-size: 14px;
line-height: 20px;
`
const
RarityWrap
=
styled
.
span
`
display: flex;
color:
${({
theme
})
=>
theme
.
textSecondary
}
;
padding: 2px 4px;
border-radius: 4px;
align-items: center;
gap: 4px;
`
const
EmptyActivitiesContainer
=
styled
.
div
`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
color:
${({
theme
})
=>
theme
.
textPrimary
}
;
font-size: 28px;
line-height: 36px;
padding: 56px 0px;
`
const
Link
=
styled
(
RouterLink
)
`
color:
${({
theme
})
=>
theme
.
accentAction
}
;
text-decoration: none;
font-size: 14px;
line-height: 16px;
margin-top: 12px;
cursor: pointer;
${
OpacityTransition
}
;
`
const
DefaultLink
=
styled
(
RouterLink
)
`
text-decoration: none;
`
const
ActivitySelectContainer
=
styled
.
div
`
display: flex;
gap: 8px;
margin-bottom: 34px;
overflow-x: auto;
// Firefox scrollbar styling
scrollbar-width: thin;
scrollbar-color:
${({
theme
})
=>
`
${
theme
.
backgroundOutline
}
transparent`
}
;
// safari and chrome scrollbar styling
::-webkit-scrollbar {
background: transparent;
height: 4px;
}
::-webkit-scrollbar-track {
margin-top: 40px;
}
::-webkit-scrollbar-thumb {
background:
${({
theme
})
=>
theme
.
backgroundOutline
}
;
border-radius: 8px;
}
@media (max-width: 720px) {
padding-bottom: 8px;
}
`
const
ContentNotAvailable
=
styled
.
div
`
display: flex;
background-color:
${({
theme
})
=>
theme
.
backgroundSurface
}
;
color:
${({
theme
})
=>
theme
.
textSecondary
}
;
font-size: 14px;
line-height: 20px;
align-items: center;
justify-content: center;
border-radius: 12px;
width: 450px;
height: 450px;
`
const
FilterBox
=
styled
.
div
<
{
isActive
?:
boolean
}
>
`
box-sizing: border-box;
background-color:
${({
theme
})
=>
theme
.
backgroundInteractive
}
;
color:
${({
theme
})
=>
theme
.
textPrimary
}
;
padding: 12px 16px;
border-radius: 12px;
cursor: pointer;
box-sizing: border-box;
border:
${({
isActive
,
theme
})
=>
(
isActive
?
`1px solid
${
theme
.
accentActive
}
`
:
undefined
)}
;
${
OpacityTransition
}
;
`
const
ByText
=
styled
.
span
`
font-size: 14px;
line-height: 20px;
`
const
Img
=
styled
.
img
`
background-color: white;
`
const
HoverImageContainer
=
styled
.
div
`
display: flex;
margin-right: 4px;
`
const
HoverContainer
=
styled
.
div
`
display: flex;
`
const
ContainerText
=
styled
.
span
`
font-size: 14px;
`
const
AudioPlayer
=
({
imageUrl
,
...
...
@@ -58,26 +237,11 @@ const AudioPlayer = ({
)
}
const
formatter
=
Intl
.
DateTimeFormat
(
'
en-GB
'
,
{
dateStyle
:
'
full
'
,
timeStyle
:
'
short
'
})
const
CountdownTimer
=
({
sellOrder
}:
{
sellOrder
:
Deprecated_SellOrder
|
SellOrder
})
=>
{
const
{
date
,
expires
}
=
useMemo
(()
=>
{
const
date
=
new
Date
((
sellOrder
as
Deprecated_SellOrder
).
orderClosingDate
??
(
sellOrder
as
SellOrder
).
endAt
)
return
{
date
,
expires
:
formatter
.
format
(
date
),
}
},
[
sellOrder
])
const
[
days
,
hours
,
minutes
,
seconds
]
=
useTimeout
(
date
)
return
(
<
MouseoverTooltip
text=
{
<
Box
fontSize=
"12"
>
Expires
{
expires
}
</
Box
>
}
>
<
Box
as=
"span"
fontWeight=
"normal"
className=
{
caption
}
color=
"textSecondary"
>
Expires:
{
days
!==
0
?
`${days} days`
:
''
}
{
hours
!==
0
?
`${hours} hours`
:
''
}
{
minutes
}
minutes
{
seconds
}{
'
'
}
seconds
</
Box
>
</
MouseoverTooltip
>
)
const
initialFilterState
=
{
[
ActivityEventType
.
Listing
]:
true
,
[
ActivityEventType
.
Sale
]:
true
,
[
ActivityEventType
.
Transfer
]:
false
,
[
ActivityEventType
.
CancelListing
]:
false
,
}
const
AssetView
=
({
...
...
@@ -112,46 +276,19 @@ enum MediaType {
interface
AssetDetailsProps
{
asset
:
GenieAsset
collection
:
CollectionInfoForAsset
collectionStats
:
GenieCollection
|
undefined
}
export
const
AssetDetails
=
({
asset
,
collection
}:
AssetDetailsProps
)
=>
{
const
{
pathname
,
search
}
=
useLocation
()
const
navigate
=
useNavigate
()
const
addAssetsToBag
=
useBag
((
state
)
=>
state
.
addAssetsToBag
)
const
removeAssetsFromBag
=
useBag
((
state
)
=>
state
.
removeAssetsFromBag
)
const
itemsInBag
=
useBag
((
state
)
=>
state
.
itemsInBag
)
const
bagExpanded
=
useBag
((
state
)
=>
state
.
bagExpanded
)
const
[
creatorAddress
,
setCreatorAddress
]
=
useState
(
''
)
const
[
ownerAddress
,
setOwnerAddress
]
=
useState
(
''
)
export
const
AssetDetails
=
({
asset
,
collection
,
collectionStats
}:
AssetDetailsProps
)
=>
{
const
[
dominantColor
]
=
useState
<
[
number
,
number
,
number
]
>
([
0
,
0
,
0
])
const
creatorEnsName
=
useENSName
(
creatorAddress
)
const
ownerEnsName
=
useENSName
(
ownerAddress
)
const
parsed
=
qs
.
parse
(
search
)
const
{
gridWidthOffset
}
=
useSpring
({
gridWidthOffset
:
bagExpanded
?
324
:
0
,
})
const
[
showTraits
,
setShowTraits
]
=
useState
(
true
)
const
[
isSelected
,
setSelected
]
=
useState
(
false
)
const
[
isOwned
,
setIsOwned
]
=
useState
(
false
)
const
{
account
:
address
,
provider
}
=
useWeb3React
()
const
trace
=
useTrace
({
page
:
PageName
.
NFT_DETAILS_PAGE
})
const
eventProperties
=
{
collection_address
:
asset
.
address
,
token_id
:
asset
.
tokenId
,
token_type
:
asset
.
tokenType
,
...
trace
,
}
const
{
rarityProvider
,
rarityLogo
}
=
useMemo
(
const
{
rarityProvider
}
=
useMemo
(
()
=>
asset
.
rarity
?
{
rarityProvider
:
asset
?.
rarity
?.
providers
?.
find
(
({
provider
:
_provider
})
=>
_provider
===
asset
.
rarity
?.
primaryProvider
),
rarityLogo
:
rarityProviderLogo
[
asset
.
rarity
.
primaryProvider
]
||
''
,
}
:
{},
[
asset
.
rarity
]
...
...
@@ -166,282 +303,217 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
return
MediaType
.
Image
},
[
asset
])
useEffect
(()
=>
{
if
(
asset
.
creator
)
setCreatorAddress
(
asset
.
creator
.
address
??
''
)
if
(
asset
.
owner
)
setOwnerAddress
(
asset
.
owner
)
},
[
asset
])
const
{
address
:
contractAddress
,
tokenId
:
token_id
}
=
asset
useEffect
(()
=>
{
setSelected
(
!!
itemsInBag
.
find
((
item
)
=>
item
.
asset
.
tokenId
===
asset
.
tokenId
&&
item
.
asset
.
address
===
asset
.
address
)
const
{
data
:
priceData
}
=
useQuery
<
ActivityEventResponse
>
(
[
'
collectionActivity
'
,
{
contractAddress
,
},
],
async
({
pageParam
=
''
})
=>
{
return
await
ActivityFetcher
(
contractAddress
,
{
token_id
,
eventTypes
:
[
ActivityEventType
.
Sale
],
},
pageParam
,
'
1
'
)
},
[
asset
,
itemsInBag
])
useEffect
(()
=>
{
if
(
provider
)
{
isAssetOwnedByUser
({
tokenId
:
asset
.
tokenId
,
userAddress
:
address
||
''
,
assetAddress
:
asset
.
address
,
tokenType
:
asset
.
tokenType
,
provider
,
}).
then
(
setIsOwned
)
},
{
getNextPageParam
:
(
lastPage
)
=>
{
return
lastPage
.
events
?.
length
===
25
?
lastPage
.
cursor
:
undefined
},
refetchInterval
:
15000
,
refetchIntervalInBackground
:
false
,
refetchOnWindowFocus
:
false
,
refetchOnMount
:
false
,
}
},
[
asset
,
address
,
provider
])
)
const
lastSalePrice
=
priceData
?.
events
[
0
]?.
price
??
null
const
formattedEthprice
=
formatEthPrice
(
lastSalePrice
??
''
)
||
0
const
formattedPrice
=
lastSalePrice
?
putCommas
(
formattedEthprice
).
toString
()
:
null
const
[
activeFilters
,
filtersDispatch
]
=
useReducer
(
reduceFilters
,
initialFilterState
)
const
USDPrice
=
useUsdPrice
(
asset
)
const
Filter
=
useCallback
(
function
ActivityFilter
({
eventType
}:
{
eventType
:
ActivityEventType
})
{
const
isActive
=
activeFilters
[
eventType
]
return
(
<
AnimatedBox
style=
{
{
// @ts-ignore
width
:
gridWidthOffset
.
to
((
x
)
=>
`calc(100% - ${x}px)`
),
}
}
className=
{
styles
.
container
}
>
<
div
className=
{
styles
.
columns
}
>
<
Column
className=
{
styles
.
column
}
>
{
assetMediaType
===
MediaType
.
Image
?
(
<
img
<
FilterBox
isActive=
{
isActive
}
onClick=
{
()
=>
filtersDispatch
({
eventType
})
}
>
{
eventType
===
ActivityEventType
.
CancelListing
?
'
Cancellations
'
:
eventType
.
charAt
(
0
)
+
eventType
.
slice
(
1
).
toLowerCase
()
+
'
s
'
}
</
FilterBox
>
)
},
[
activeFilters
]
)
const
{
data
:
eventsData
,
fetchNextPage
,
hasNextPage
,
isFetchingNextPage
,
isSuccess
,
}
=
useInfiniteQuery
<
ActivityEventResponse
>
(
[
'
collectionActivity
'
,
{
contractAddress
,
activeFilters
,
token_id
,
},
],
async
({
pageParam
=
''
})
=>
{
return
await
ActivityFetcher
(
contractAddress
,
{
token_id
,
eventTypes
:
Object
.
keys
(
activeFilters
)
.
map
((
key
)
=>
key
as
ActivityEventType
)
.
filter
((
key
)
=>
activeFilters
[
key
]),
},
pageParam
)
},
{
getNextPageParam
:
(
lastPage
)
=>
{
return
lastPage
.
events
?.
length
===
25
?
lastPage
.
cursor
:
undefined
},
refetchInterval
:
15000
,
refetchIntervalInBackground
:
false
,
refetchOnWindowFocus
:
false
,
refetchOnMount
:
false
,
}
)
const
rarity
=
asset
?.
rarity
?.
providers
?.
length
?
asset
?.
rarity
?.
providers
?.[
0
]
:
undefined
const
[
showHolder
,
setShowHolder
]
=
useState
(
false
)
const
rarityProviderLogo
=
getRarityProviderLogo
(
rarity
?.
provider
)
const
events
=
useMemo
(
()
=>
(
isSuccess
?
eventsData
?.
pages
.
map
((
page
)
=>
page
.
events
).
flat
()
:
null
),
[
isSuccess
,
eventsData
]
)
return
(
<
Column
>
<
MediaContainer
>
{
asset
.
imageUrl
===
undefined
||
showHolder
?
(
<
ContentNotAvailable
>
Content not available yet
</
ContentNotAvailable
>
)
:
assetMediaType
===
MediaType
.
Image
?
(
<
Img
className=
{
styles
.
image
}
src=
{
asset
.
imageUrl
}
alt=
{
asset
.
name
||
collection
.
collectionName
}
style=
{
{
[
'
--shadow
'
as
string
]:
`rgba(${dominantColor.join(', ')}, 0.5)`
}
}
onError=
{
()
=>
setShowHolder
(
true
)
}
/>
)
:
(
<
AssetView
asset=
{
asset
}
mediaType=
{
assetMediaType
}
dominantColor=
{
dominantColor
}
/>
)
}
</
Column
>
<
Column
className=
{
clsx
(
styles
.
column
,
styles
.
columnRight
)
}
width=
"full"
>
<
Column
>
<
Row
marginBottom=
"8"
alignItems=
"center"
textAlign=
"center"
justifyContent=
{
rarityProvider
?
'
space-between
'
:
'
flex-end
'
}
>
{
rarityProvider
&&
(
</
MediaContainer
>
<
DefaultLink
to=
{
`/nfts/collection/${asset.address}`
}
>
<
CollectionHeader
>
{
collection
.
collectionName
}
{
collectionStats
?.
isVerified
&&
<
VerifiedIcon
/>
}
</
CollectionHeader
>
</
DefaultLink
>
<
AssetHeader
>
{
asset
.
name
??
`${asset.collectionName} #${asset.tokenId}`
}
</
AssetHeader
>
<
AssetPriceDetailsContainer
>
<
AssetPriceDetails
asset=
{
asset
}
collection=
{
collection
}
/>
</
AssetPriceDetailsContainer
>
<
InfoContainer
primaryHeader=
"Traits"
defaultOpen
secondaryHeader=
{
rarityProvider
&&
rarity
&&
rarity
.
score
?
(
<
MouseoverTooltip
text=
{
<
Row
gap=
"4"
>
<
img
src=
{
rarityLogo
}
width=
{
16
}
alt=
{
rarityProvider
.
provider
}
/>
Ranking by
{
'
'
}
{
asset
.
rarity
?.
primaryProvider
===
'
Genie
'
?
fallbackProvider
:
asset
.
rarity
?.
primaryProvider
}
</
Row
>
<
HoverContainer
>
<
HoverImageContainer
>
<
img
src=
{
rarityProviderLogo
}
alt=
"cardLogo"
width=
{
16
}
/>
</
HoverImageContainer
>
<
ContainerText
>
{
collectionStats
?.
rarityVerified
?
`Verified by ${collectionStats?.name}`
:
`Ranking by ${rarity.provider === 'Genie' ? fallbackProvider : rarity.provider}`
}
</
ContainerText
>
</
HoverContainer
>
}
placement=
"top"
>
<
Center
paddingLeft=
"6"
paddingRight=
"4"
className=
{
badge
}
backgroundColor=
"backgroundSurface"
color=
"textPrimary"
borderRadius=
"4"
>
#
{
rarityProvider
.
rank
}
<
img
src=
"/nft/svgs/rarity.svg"
height=
{
15
}
width=
{
15
}
alt=
"Rarity rank"
/>
</
Center
>
<
RarityWrap
>
Rarity:
{
putCommas
(
rarity
.
score
)
}
</
RarityWrap
>
</
MouseoverTooltip
>
)
}
<
Row
gap=
"12"
>
<
Center
as=
"button"
padding=
"0"
border=
"none"
background=
"transparent"
cursor=
"pointer"
onClick=
{
async
()
=>
{
await
navigator
.
clipboard
.
writeText
(
`${window.location.hostname}/#${pathname}`
)
}
}
)
:
null
}
>
<
TraitsContainer
asset=
{
asset
}
/>
</
InfoContainer
>
<
InfoContainer
primaryHeader=
"Activity"
secondaryHeader=
{
formattedPrice
?
`Last Sale: ${formattedPrice} ETH`
:
undefined
}
>
<
ShareIcon
/>
<>
<
ActivitySelectContainer
>
<
Filter
eventType=
{
ActivityEventType
.
Listing
}
/>
<
Filter
eventType=
{
ActivityEventType
.
Sale
}
/>
<
Filter
eventType=
{
ActivityEventType
.
Transfer
}
/>
<
Filter
eventType=
{
ActivityEventType
.
CancelListing
}
/>
</
ActivitySelectContainer
>
{
events
&&
events
.
length
>
0
?
(
<
InfiniteScroll
next=
{
fetchNextPage
}
hasMore=
{
!!
hasNextPage
}
loader=
{
isFetchingNextPage
?
(
<
Center
>
<
LoadingSparkle
/>
</
Center
>
<
Center
as=
"button"
border=
"none"
width=
"32"
height=
"32"
padding=
"0"
background=
"transparent"
cursor=
"pointer"
onClick=
{
()
=>
{
if
(
!
parsed
.
origin
||
parsed
.
origin
===
'
collection
'
)
{
navigate
(
`/nfts/collection/${asset.address}`
)
}
else
if
(
parsed
.
origin
===
'
profile
'
)
{
navigate
(
'
/profile
'
,
undefined
)
}
else
if
(
parsed
.
origin
===
'
explore
'
)
{
navigate
(
`/nfts`
,
undefined
)
}
else
if
(
parsed
.
origin
===
'
activity
'
)
{
navigate
(
`/nfts/collection/${asset.address}/activity`
,
undefined
)
)
:
null
}
}
}
dataLength=
{
events
?.
length
??
0
}
scrollableTarget=
"activityContainer"
>
{
parsed
.
origin
?
(
<
CornerDownLeftIcon
width=
"28"
height=
"28"
/
>
<
AssetActivity
eventsData=
{
{
events
}
}
/>
</
InfiniteScroll
>
)
:
(
<
CloseDropDownIcon
color=
{
themeVars
.
colors
.
textSecondary
}
/>
)
}
</
Center
>
</
Row
>
</
Row
>
<
Row
as=
"h1"
marginTop=
"0"
marginBottom=
"12"
gap=
"2"
className=
{
headlineMedium
}
>
{
asset
.
susFlag
&&
(
<
Box
marginTop=
"8"
>
<
MouseoverTooltip
text=
{
<
Box
fontWeight=
"normal"
>
{
SUSPICIOUS_TEXT
}
</
Box
>
}
>
<
SuspiciousIcon
height=
"30"
width=
"30"
viewBox=
"0 0 16 17"
/>
</
MouseoverTooltip
>
</
Box
>
<
EmptyActivitiesContainer
>
<
div
>
No activities yet
</
div
>
<
Link
to=
{
`/nfts/collection/${asset.address}`
}
>
View collection items
</
Link
>
{
'
'
}
</
EmptyActivitiesContainer
>
)
}
{
asset
.
name
||
`${collection.collectionName} #${asset.tokenId}`
}
</
Row
>
{
collection
.
collectionDescription
?
(
<
ExpandableText
>
<
ReactMarkdown
allowedTypes=
{
[
'
link
'
,
'
paragraph
'
,
'
strong
'
,
'
code
'
,
'
emphasis
'
,
'
text
'
]
}
source=
{
collection
.
collectionDescription
}
/>
</
ExpandableText
>
)
:
null
}
<
Row
justifyContent=
{
{
sm
:
'
space-between
'
,
}
}
gap=
{
{
sm
:
'
unset
'
,
}
}
marginBottom=
"36"
>
{
ownerAddress
.
length
>
0
&&
(
<
a
</>
</
InfoContainer
>
<
InfoContainer
primaryHeader=
"Description"
secondaryHeader=
{
null
}
>
<>
<
ByText
>
By
</
ByText
>
{
asset
?.
creator
&&
asset
.
creator
?.
address
&&
(
<
AddressTextLink
href=
{
`https://etherscan.io/address/${asset.creator.address}`
}
target=
"_blank"
rel=
"noreferrer"
href=
{
`https://etherscan.io/address/${asset.owner}`
}
style=
{
{
textDecoration
:
'
none
'
}
}
rel=
"noopener noreferrer"
>
<
CollectionProfile
label=
"Owner"
avatarUrl=
""
name=
{
ownerEnsName
.
ENSName
??
shortenAddress
(
ownerAddress
,
0
,
4
)
}
/>
</
a
>
{
shortenAddress
(
asset
.
creator
.
address
,
2
,
4
)
}
</
AddressTextLink
>
)
}
<
Link
to=
{
`/nfts/collection/${asset.address}`
}
style=
{
{
textDecoration
:
'
none
'
}
}
>
<
CollectionProfile
label=
"Collection"
avatarUrl=
{
collection
.
collectionImageUrl
}
name=
{
collection
.
collectionName
}
isVerified=
{
collection
.
isVerified
}
/>
</
Link
>
{
creatorAddress
?
(
<
a
target=
"_blank"
rel=
"noreferrer"
href=
{
`https://etherscan.io/address/${creatorAddress}`
}
style=
{
{
textDecoration
:
'
none
'
}
}
>
<
CollectionProfile
label=
"Creator"
avatarUrl=
{
asset
.
creator
.
profile_img_url
}
name=
{
creatorEnsName
.
ENSName
??
shortenAddress
(
creatorAddress
,
0
,
4
)
}
isVerified
className=
{
styles
.
creator
}
/>
</
a
>
)
:
null
}
</
Row
>
</
Column
>
{
asset
.
priceInfo
&&
asset
.
sellorders
&&
!
isOwned
?
(
<
Row
marginTop=
"8"
marginBottom=
"40"
justifyContent=
"space-between"
borderRadius=
"12"
paddingTop=
"16"
paddingBottom=
"16"
paddingLeft=
"16"
paddingRight=
"24"
background=
"accentActiveSoft"
>
<
Column
justifyContent=
"flex-start"
gap=
"8"
>
<
Row
gap=
"12"
as=
"a"
target=
"_blank"
rel=
"norefferer"
>
<
a
href=
{
asset
.
sellorders
[
0
].
marketplaceUrl
}
rel=
"noreferrer"
target=
"_blank"
>
<
img
className=
{
styles
.
marketplace
}
src=
{
`/nft/svgs/marketplaces/${asset.sellorders[0].marketplace.toLowerCase()}.svg`
}
height=
{
16
}
width=
{
16
}
alt=
"Markeplace"
/>
</
a
>
<
Row
as=
"span"
className=
{
subhead
}
color=
"textPrimary"
>
{
formatEthPrice
(
asset
.
priceInfo
.
ETHPrice
)
}
<
Eth2Icon
/>
</
Row
>
{
USDPrice
&&
(
<
Box
as=
"span"
color=
"textSecondary"
className=
{
bodySmall
}
>
$
{
toSignificant
(
USDPrice
)
}
</
Box
>
)
}
</
Row
>
{
(
asset
.
sellorders
?.[
0
]
as
Deprecated_SellOrder
).
orderClosingDate
||
(
asset
.
sellorders
?.[
0
]
as
SellOrder
).
endAt
?
(
<
CountdownTimer
sellOrder=
{
asset
.
sellorders
[
0
]
}
/>
)
:
null
}
</
Column
>
<
Box
as=
"button"
paddingTop=
"14"
paddingBottom=
"14"
fontWeight=
"medium"
textAlign=
"center"
fontSize=
"14"
style=
{
{
width
:
'
244px
'
}
}
color=
{
isSelected
?
'
genieBlue
'
:
'
explicitWhite
'
}
border=
"none"
borderRadius=
"12"
background=
{
isSelected
?
'
explicitWhite
'
:
'
genieBlue
'
}
transition=
"250"
boxShadow=
{
{
hover
:
'
elevation
'
}
}
onClick=
{
()
=>
{
if
(
isSelected
)
{
removeAssetsFromBag
([
asset
])
}
else
{
addAssetsToBag
([
asset
])
sendAnalyticsEvent
(
EventName
.
NFT_BUY_ADDED
,
{
...
eventProperties
})
}
setSelected
((
x
)
=>
!
x
)
}
}
>
{
isSelected
?
'
Added to Bag
'
:
'
Buy Now
'
}
</
Box
>
</
Row
>
)
:
null
}
<
Row
gap=
"32"
marginBottom=
"20"
>
<
button
data
-
active=
{
showTraits
}
onClick=
{
()
=>
setShowTraits
(
true
)
}
className=
{
styles
.
tab
}
>
Traits
</
button
>
<
button
data
-
active=
{
!
showTraits
}
onClick=
{
()
=>
setShowTraits
(
false
)
}
className=
{
styles
.
tab
}
>
Details
</
button
>
</
Row
>
{
showTraits
?
(
<
Traits
collectionAddress=
{
asset
.
address
}
traits=
{
asset
.
traits
??
[]
}
/>
)
:
(
<
Details
contractAddress=
{
asset
.
address
}
tokenId=
{
asset
.
tokenId
}
tokenType=
{
asset
.
tokenType
}
blockchain=
"Ethereum"
metadataUrl=
{
asset
.
metadataUrl
}
totalSupply=
{
collection
.
totalSupply
}
/>
<
DescriptionText
>
{
collection
.
collectionDescription
}
</
DescriptionText
>
<
SocialsContainer
>
{
collectionStats
?.
externalUrl
&&
<
Resource
name=
"Website"
link=
{
`${collectionStats?.externalUrl}`
}
/>
}
{
collectionStats
?.
twitterUrl
&&
(
<
Resource
name=
"Twitter"
link=
{
`https://twitter.com/${collectionStats?.twitterUrl}`
}
/>
)
}
{
collectionStats
?.
discordUrl
&&
<
Resource
name=
"Discord"
link=
{
collectionStats
?.
discordUrl
}
/>
}
</
SocialsContainer
>
</>
</
InfoContainer
>
<
InfoContainer
primaryHeader=
"Details"
secondaryHeader=
{
null
}
>
<
DetailsContainer
asset=
{
asset
}
collection=
{
collection
}
/>
</
InfoContainer
>
</
Column
>
</
div
>
</
AnimatedBox
>
)
}
src/nft/components/details/AssetPriceDetails.tsx
View file @
4b1b6098
import
{
useWeb3React
}
from
'
@web3-react/core
'
import
{
sendAnalyticsEvent
}
from
'
analytics
'
import
{
EventName
,
PageName
}
from
'
analytics/constants
'
import
{
useTrace
}
from
'
analytics/Trace
'
import
useCopyClipboard
from
'
hooks/useCopyClipboard
'
import
{
CancelListingIcon
,
MinusIcon
,
PlusIcon
}
from
'
nft/components/icons
'
import
{
useBag
}
from
'
nft/hooks
'
import
{
CollectionInfoForAsset
,
Deprecated_SellOrder
,
GenieAsset
,
SellOrder
,
TokenType
}
from
'
nft/types
'
import
{
ethNumberStandardFormatter
,
formatEthPrice
,
getMarketplaceIcon
,
timeLeft
,
useUsdPrice
}
from
'
nft/utils
'
import
{
shortenAddress
}
from
'
nft/utils/address
'
import
{
useMemo
}
from
'
react
'
import
{
Upload
}
from
'
react-feather
'
import
{
useNavigate
}
from
'
react-router-dom
'
import
styled
,
{
useTheme
}
from
'
styled-components/macro
'
import
styled
,
{
css
,
useTheme
}
from
'
styled-components/macro
'
import
{
ThemedText
}
from
'
theme
'
interface
AssetPriceDetailsProps
{
...
...
@@ -16,8 +16,40 @@ interface AssetPriceDetailsProps {
collection
:
CollectionInfoForAsset
}
const
hoverState
=
css
`
:hover::after {
border-radius: 12px;
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
${({
theme
})
=>
theme
.
stateOverlayHover
}
;
z-index: 0;
}
:active::after {
border-radius: 12px;
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
${({
theme
})
=>
theme
.
stateOverlayPressed
}
;
z-index: 0;
}
`
const
Container
=
styled
.
div
`
margin-left: 86px;
width: 100%;
@media (min-width: 960px) {
position: fixed;
width: 360px;
margin-top: -6px;
}
`
const
BestPriceContainer
=
styled
.
div
`
...
...
@@ -28,7 +60,6 @@ const BestPriceContainer = styled.div`
background-color:
${({
theme
})
=>
theme
.
backgroundSurface
}
;
border: 1px solid
${({
theme
})
=>
theme
.
backgroundOutline
}
;
border-radius: 16px;
width: 320px;
`
const
HeaderRow
=
styled
.
div
`
...
...
@@ -59,11 +90,17 @@ const BuyNowButton = styled.div<{ assetInBag: boolean; margin: boolean; useAccen
margin-top:
${({
margin
})
=>
(
margin
?
'
12px
'
:
'
0px
'
)}
;
text-align: center;
cursor: pointer;
${
hoverState
}
`
const
BuyNowButtonContainer
=
styled
.
div
`
position: relative;
`
const
Erc1155BuyNowButton
=
styled
.
div
`
display:
flex
;
flex-direction: row
;
display:
grid
;
grid-template-columns: 1fr 1fr 1fr
;
width: 100%;
background-color:
${({
theme
})
=>
theme
.
backgroundSurface
}
;
border:
${({
theme
})
=>
`1px solid
${
theme
.
backgroundOutline
}
`
}
;
...
...
@@ -74,6 +111,9 @@ const Erc1155BuyNowButton = styled.div`
justify-content: space-between;
overflow-x: hidden;
`
const
Tertiary
=
styled
(
ThemedText
.
BodySecondary
)
`
color:
${({
theme
})
=>
theme
.
textTertiary
}
;
`
const
Erc1155BuyNowText
=
styled
.
div
`
display: flex;
...
...
@@ -88,12 +128,32 @@ const Erc1155ChangeButton = styled(Erc1155BuyNowText)<{ remove: boolean }>`
color:
${({
theme
,
remove
})
=>
(
remove
?
theme
.
accentFailure
:
theme
.
accentAction
)}
;
cursor: pointer;
:hover {
background-color:
${({
theme
,
remove
})
=>
(
remove
?
theme
.
accentFailure
:
theme
.
accentAction
)}
;
color:
${({
theme
})
=>
theme
.
textPrimary
}
;
${
hoverState
}
&:hover::after {
border-radius: 0px;
}
`
const
UploadLink
=
styled
.
a
`
color:
${({
theme
})
=>
theme
.
textSecondary
}
;
cursor: pointer;
&:hover {
opacity:
${({
theme
})
=>
theme
.
opacity
.
hover
}
;
}
&:active {
opacity:
${({
theme
})
=>
theme
.
opacity
.
click
}
;
}
transition:
${({
theme
:
{
transition
:
{
duration
,
timing
},
},
})
=>
`opacity
${
duration
.
medium
}
${
timing
.
ease
}
`
}
;
`
const
NotForSaleContainer
=
styled
.
div
`
display: flex;
flex-direction: column;
...
...
@@ -111,10 +171,40 @@ const DiscoveryContainer = styled.div`
align-items: center;
`
const
OwnerText
=
styled
.
a
`
font-size: 14px;
line-height: 20px;
color:
${({
theme
})
=>
theme
.
textSecondary
}
;
text-decoration: none;
&:hover {
opacity:
${({
theme
})
=>
theme
.
opacity
.
hover
}
;
}
&:active {
opacity:
${({
theme
})
=>
theme
.
opacity
.
click
}
;
}
transition:
${({
theme
:
{
transition
:
{
duration
,
timing
},
},
})
=>
`opacity
${
duration
.
medium
}
${
timing
.
ease
}
`
}
;
`
const
OwnerInformationContainer
=
styled
.
div
`
color:
${({
theme
})
=>
theme
.
textSecondary
}
;
display: flex;
justify-content: space-between;
padding: 0 8px;
margin-bottom: 20px;
`
export
const
OwnerContainer
=
({
asset
}:
{
asset
:
GenieAsset
})
=>
{
const
listing
=
asset
.
sellorders
&&
asset
.
sellorders
.
length
>
0
?
asset
.
sellorders
[
0
]
:
undefined
const
expirationDate
=
listing
?
new
Date
((
listing
as
Deprecated_SellOrder
).
orderClosingDate
??
(
listing
as
SellOrder
).
endAt
)
const
cheapestOrder
=
asset
.
sellorders
&&
asset
.
sellorders
.
length
>
0
?
asset
.
sellorders
[
0
]
:
undefined
const
expirationDate
=
cheapestOrder
?
new
Date
((
cheapestOrder
as
Deprecated_SellOrder
).
orderClosingDate
??
(
cheapestOrder
as
SellOrder
).
endAt
)
:
undefined
const
USDPrice
=
useUsdPrice
(
asset
)
...
...
@@ -190,26 +280,27 @@ export const NotForSale = ({ collection }: { collection: CollectionInfoForAsset
)
}
const
SubHeader
=
styled
(
ThemedText
.
SubHeader
)
`
color:
${({
theme
})
=>
theme
.
textPrimary
}
;
`
export
const
AssetPriceDetails
=
({
asset
,
collection
}:
AssetPriceDetailsProps
)
=>
{
const
{
account
}
=
useWeb3React
()
const
cheapestOrder
=
asset
.
sellorders
&&
asset
.
sellorders
.
length
>
0
?
asset
.
sellorders
[
0
]
:
undefined
const
expirationDate
=
cheapestOrder
?
new
Date
((
cheapestOrder
as
Deprecated_SellOrder
).
orderClosingDate
??
(
cheapestOrder
as
SellOrder
).
endAt
)
:
undefined
const
itemsInBag
=
useBag
((
s
)
=>
s
.
itemsInBag
)
const
addAssetsToBag
=
useBag
((
s
)
=>
s
.
addAssetsToBag
)
const
removeAssetsFromBag
=
useBag
((
s
)
=>
s
.
removeAssetsFromBag
)
const
toggleBag
=
useBag
((
s
)
=>
s
.
toggleBag
)
const
bagExpanded
=
useBag
((
s
)
=>
s
.
bagExpanded
)
const
USDPrice
=
useUsdPrice
(
asset
)
const
isErc1555
=
asset
.
tokenType
===
TokenType
.
ERC1155
const
trace
=
useTrace
({
page
:
PageName
.
NFT_DETAILS_PAGE
})
const
eventProperties
=
{
collection_address
:
asset
.
address
,
token_id
:
asset
.
tokenId
,
token_type
:
asset
.
tokenType
,
...
trace
,
}
const
[,
setCopied
]
=
useCopyClipboard
()
const
{
quantity
,
assetInBag
}
=
useMemo
(()
=>
{
return
{
...
...
@@ -222,8 +313,7 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
}
},
[
asset
,
itemsInBag
])
const
isOwner
=
asset
.
owner
&&
typeof
asset
.
owner
===
'
string
'
?
account
?.
toLowerCase
()
===
asset
.
owner
.
toLowerCase
()
:
false
const
isOwner
=
asset
.
owner
?
account
?.
toLowerCase
()
===
asset
.
owner
?.
address
?.
toLowerCase
()
:
false
if
(
isOwner
)
{
return
<
OwnerContainer
asset=
{
asset
}
/>
...
...
@@ -231,6 +321,28 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
return
(
<
Container
>
<
OwnerInformationContainer
>
<
OwnerText
target=
"_blank"
href=
{
`https://etherscan.io/address/${asset.owner.address}`
}
rel=
"noopener noreferrer"
>
{
asset
.
tokenType
===
'
ERC1155
'
?
(
''
)
:
(
<
span
>
Seller:
{
isOwner
?
'
you
'
:
asset
.
owner
.
address
&&
shortenAddress
(
asset
.
owner
.
address
,
2
,
4
)
}
</
span
>
)
}
</
OwnerText
>
<
UploadLink
onClick=
{
()
=>
{
setCopied
(
window
.
location
.
href
)
}
}
target=
"_blank"
>
<
Upload
size=
{
20
}
strokeWidth=
{
2
}
/>
</
UploadLink
>
</
OwnerInformationContainer
>
{
cheapestOrder
&&
asset
.
priceInfo
?
(
<
BestPriceContainer
>
<
HeaderRow
>
...
...
@@ -241,7 +353,7 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
</
HeaderRow
>
<
PriceRow
>
<
ThemedText
.
MediumHeader
fontSize=
{
'
28px
'
}
lineHeight=
{
'
36px
'
}
>
{
formatEthPrice
(
asset
.
priceInfo
.
ETHPrice
)
}
{
formatEthPrice
(
asset
.
priceInfo
.
ETHPrice
)
}
ETH
</
ThemedText
.
MediumHeader
>
{
USDPrice
&&
(
<
ThemedText
.
BodySecondary
lineHeight=
{
'
24px
'
}
>
...
...
@@ -249,34 +361,48 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
</
ThemedText
.
BodySecondary
>
)
}
</
PriceRow
>
{
expirationDate
&&
(
<
ThemedText
.
BodySecondary
fontSize=
{
'
14px
'
}
>
Sale ends:
{
timeLeft
(
expirationDate
)
}
</
ThemedText
.
BodySecondary
>
)
}
{
expirationDate
&&
<
Tertiary
fontSize=
{
'
14px
'
}
>
Sale ends:
{
timeLeft
(
expirationDate
)
}
</
Tertiary
>
}
<
div
>
{
!
isErc1555
||
!
assetInBag
?
(
<
BuyNowButtonContainer
>
<
BuyNowButton
assetInBag=
{
assetInBag
}
margin=
{
true
}
useAccentColor=
{
true
}
onClick=
{
()
=>
{
assetInBag
?
removeAssetsFromBag
([
asset
])
:
addAssetsToBag
([
asset
])
!
assetInBag
&&
sendAnalyticsEvent
(
EventName
.
NFT_BUY_ADDED
,
{
...
eventProperties
})
if
(
!
assetInBag
&&
!
isErc1555
&&
!
bagExpanded
)
{
toggleBag
()
}
}
}
>
<
ThemedText
.
SubHeader
lineHeight=
{
'
20px
'
}
>
{
assetInBag
?
'
Remove
'
:
'
Buy Now
'
}
</
ThemedText
.
SubHeader
>
<
SubHeader
lineHeight=
{
'
20px
'
}
>
<
span
>
{
assetInBag
?
'
Remove
'
:
'
Buy Now
'
}
</
span
>
</
SubHeader
>
</
BuyNowButton
>
</
BuyNowButtonContainer
>
)
:
(
<
Erc1155BuyNowButton
>
<
BuyNowButtonContainer
>
<
Erc1155ChangeButton
remove=
{
true
}
onClick=
{
()
=>
removeAssetsFromBag
([
asset
])
}
>
<
MinusIcon
width=
"20px"
height=
"20px"
/>
</
Erc1155ChangeButton
>
</
BuyNowButtonContainer
>
<
BuyNowButtonContainer
>
<
Erc1155BuyNowText
>
<
ThemedText
.
SubHeader
lineHeight=
{
'
20px
'
}
>
{
quantity
}
</
ThemedText
.
SubHeader
>
</
Erc1155BuyNowText
>
</
BuyNowButtonContainer
>
<
BuyNowButtonContainer
>
<
Erc1155ChangeButton
remove=
{
false
}
onClick=
{
()
=>
addAssetsToBag
([
asset
])
}
>
<
PlusIcon
width=
"20px"
height=
"20px"
/>
</
Erc1155ChangeButton
>
</
BuyNowButtonContainer
>
</
Erc1155BuyNowButton
>
)
}
</
div
>
</
BestPriceContainer
>
)
:
(
<
NotForSale
collection=
{
collection
}
/>
...
...
src/nft/components/details/DetailsContainer.tsx
0 → 100644
View file @
4b1b6098
import
useCopyClipboard
from
'
hooks/useCopyClipboard
'
import
{
CollectionInfoForAsset
,
GenieAsset
}
from
'
nft/types
'
import
{
putCommas
}
from
'
nft/utils
'
import
{
shortenAddress
}
from
'
nft/utils/address
'
import
{
useCallback
}
from
'
react
'
import
{
Copy
}
from
'
react-feather
'
import
styled
from
'
styled-components/macro
'
const
Details
=
styled
.
div
`
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-gap: 40px;
@media (max-width: 600px) {
grid-template-columns: 1fr 1fr 1fr;
}
@media (max-width: 450px) {
grid-template-columns: 1fr 1fr;
}
`
const
Header
=
styled
.
div
`
color:
${({
theme
})
=>
theme
.
textSecondary
}
;
font-size: 14px;
line-height: 20px;
`
const
Body
=
styled
.
div
`
color:
${({
theme
})
=>
theme
.
textPrimary
}
;
font-size: 14px;
line-height: 20px;
margin-top: 8px;
`
const
Center
=
styled
.
span
`
align-items: center;
cursor: pointer;
display: flex;
gap: 8px;
&:hover {
opacity:
${({
theme
})
=>
theme
.
opacity
.
hover
}
;
}
&:active {
opacity:
${({
theme
})
=>
theme
.
opacity
.
click
}
;
}
transition:
${({
theme
:
{
transition
:
{
duration
,
timing
},
},
})
=>
`opacity
${
duration
.
medium
}
${
timing
.
ease
}
`
}
;
`
const
CreatorLink
=
styled
.
a
`
color:
${({
theme
})
=>
theme
.
textPrimary
}
;
text-decoration: none;
&:hover {
opacity:
${({
theme
})
=>
theme
.
opacity
.
hover
}
;
}
&:active {
opacity:
${({
theme
})
=>
theme
.
opacity
.
click
}
;
}
transition:
${({
theme
:
{
transition
:
{
duration
,
timing
},
},
})
=>
`opacity
${
duration
.
medium
}
${
timing
.
ease
}
`
}
;
`
const
CopyIcon
=
styled
(
Copy
)
`
cursor: pointer;
`
const
GridItem
=
({
header
,
body
}:
{
header
:
string
;
body
:
React
.
ReactNode
})
=>
{
return
(
<
div
>
<
Header
>
{
header
}
</
Header
>
<
Body
>
{
body
}
</
Body
>
</
div
>
)
}
const
stringShortener
=
(
text
:
string
)
=>
`
${
text
.
substring
(
0
,
4
)}
...
${
text
.
substring
(
text
.
length
-
4
,
text
.
length
)}
`
const
DetailsContainer
=
({
asset
,
collection
}:
{
asset
:
GenieAsset
;
collection
:
CollectionInfoForAsset
})
=>
{
const
{
address
,
tokenId
,
tokenType
,
creator
}
=
asset
const
{
totalSupply
}
=
collection
const
[,
setCopied
]
=
useCopyClipboard
()
const
copy
=
useCallback
(()
=>
{
setCopied
(
address
??
''
)
},
[
address
,
setCopied
])
return
(
<
Details
>
<
GridItem
header=
"Contract address"
body=
{
<
Center
onClick=
{
copy
}
>
{
shortenAddress
(
address
,
2
,
4
)
}
<
CopyIcon
size=
{
13
}
/>
</
Center
>
}
/>
<
GridItem
header=
"Token ID"
body=
{
tokenId
.
length
>
9
?
stringShortener
(
tokenId
)
:
tokenId
}
/>
<
GridItem
header=
"Token standard"
body=
{
tokenType
}
/>
<
GridItem
header=
"Blockchain"
body=
"Ethereum"
/>
<
GridItem
header=
"Total supply"
body=
{
`${putCommas(totalSupply ?? 0)}`
}
/>
<
GridItem
header=
"Creator"
body=
{
creator
?.
address
&&
(
<
CreatorLink
href=
{
`https://etherscan.io/address/${creator.address}`
}
rel=
"noopener noreferrer"
target=
"_blank"
>
{
shortenAddress
(
creator
.
address
,
2
,
4
)
}
</
CreatorLink
>
)
}
/>
</
Details
>
)
}
export
default
DetailsContainer
src/nft/components/details/InfoContainer.tsx
0 → 100644
View file @
4b1b6098
import
{
useState
}
from
'
react
'
import
{
ChevronDown
,
ChevronUp
}
from
'
react-feather
'
import
styled
,
{
css
}
from
'
styled-components/macro
'
const
Header
=
styled
.
div
<
{
isOpen
:
boolean
}
>
`
display: flex;
border-radius:
${({
isOpen
})
=>
(
isOpen
?
'
16px 16px 0px 0px
'
:
'
16px
'
)}
;
justify-content: space-between;
background-color:
${({
theme
})
=>
theme
.
backgroundSurface
}
;
padding: 14px 20px;
cursor: pointer;
border: 1px solid
${({
theme
})
=>
theme
.
backgroundOutline
}
;
margin-top: 28px;
width: 100%;
align-items: center;
&:hover {
background-color:
${({
theme
})
=>
theme
.
stateOverlayHover
}
;
}
&:active {
background-color:
${({
theme
})
=>
theme
.
stateOverlayPressed
}
;
}
transition:
${({
theme
:
{
transition
:
{
duration
,
timing
},
},
})
=>
css
`background-color
${
duration
.
medium
}
${
timing
.
ease
}
`
}
;
`
const
PrimaryHeader
=
styled
.
span
`
display: flex;
align-items: center;
gap: 16px;
color:
${({
theme
})
=>
theme
.
textPrimary
}
;
font-weight: 500;
line-height: 28px;
font-size: 20px;
`
const
SecondaryHeader
=
styled
.
span
`
font-size: 12px;
color:
${({
theme
})
=>
theme
.
textSecondary
}
;
`
const
SecondaryHeaderContainer
=
styled
.
span
`
display: flex;
align-items: center;
justify-content: center;
gap: 32px;
color:
${({
theme
})
=>
theme
.
textPrimary
}
;
`
const
ContentContainer
=
styled
.
div
`
padding: 20px;
border: 1px solid
${({
theme
})
=>
theme
.
backgroundOutline
}
;
border-top: none;
border-radius: 0px 0px 16px 16px;
background-color:
${({
theme
})
=>
theme
.
backgroundSurface
}
; ;
`
const
InfoContainer
=
({
children
,
primaryHeader
,
secondaryHeader
,
defaultOpen
,
}:
{
children
:
JSX
.
Element
primaryHeader
:
string
secondaryHeader
:
React
.
ReactNode
defaultOpen
?:
boolean
})
=>
{
const
[
isOpen
,
setIsOpen
]
=
useState
(
!!
defaultOpen
)
return
(
<
div
>
<
Header
isOpen=
{
isOpen
}
onClick=
{
()
=>
setIsOpen
(
!
isOpen
)
}
>
<
PrimaryHeader
>
{
primaryHeader
}
<
SecondaryHeader
>
{
secondaryHeader
}
</
SecondaryHeader
>
</
PrimaryHeader
>
<
SecondaryHeaderContainer
>
{
isOpen
?
<
ChevronUp
/>
:
<
ChevronDown
/>
}
</
SecondaryHeaderContainer
>
</
Header
>
{
isOpen
&&
<
ContentContainer
>
{
children
}
</
ContentContainer
>
}
</
div
>
)
}
export
default
InfoContainer
src/nft/components/details/Traits.css.ts
deleted
100644 → 0
View file @
53a6acc1
import
{
style
}
from
'
@vanilla-extract/css
'
import
{
sprinkles
}
from
'
../../css/sprinkles.css
'
export
const
grid
=
style
([
sprinkles
({
gap
:
'
16
'
,
display
:
'
grid
'
}),
{
gridTemplateColumns
:
'
repeat(4, 1fr)
'
,
'
@media
'
:
{
'
(max-width: 1536px)
'
:
{
gridTemplateColumns
:
'
repeat(3, 1fr)
'
,
},
'
(max-width: 640px)
'
:
{
gridTemplateColumns
:
'
repeat(2, 1fr)
'
,
},
},
},
])
src/nft/components/details/Traits.tsx
deleted
100644 → 0
View file @
53a6acc1
import
{
Trait
}
from
'
nft/hooks
'
import
qs
from
'
query-string
'
import
{
badge
}
from
'
../../css/common.css
'
import
{
Box
}
from
'
../Box
'
import
{
Column
}
from
'
../Flex
'
import
*
as
styles
from
'
./Traits.css
'
const
TraitRow
:
React
.
FC
<
Trait
>
=
({
trait_type
,
trait_value
}:
Trait
)
=>
(
<
Column
backgroundColor=
"backgroundSurface"
padding=
"16"
gap=
"4"
borderRadius=
"12"
>
<
Box
as=
"span"
className=
{
badge
}
color=
"textSecondary"
whiteSpace=
"nowrap"
overflow=
"hidden"
textOverflow=
"ellipsis"
style=
{
{
textTransform
:
'
uppercase
'
}
}
maxWidth=
{
{
sm
:
'
120
'
,
md
:
'
160
'
}
}
>
{
trait_type
}
</
Box
>
<
Box
as=
"span"
color=
"textPrimary"
fontSize=
"16"
fontWeight=
"normal"
whiteSpace=
"nowrap"
overflow=
"hidden"
textOverflow=
"ellipsis"
maxWidth=
{
{
sm
:
'
120
'
,
md
:
'
160
'
}
}
>
{
trait_value
}
</
Box
>
</
Column
>
)
export
const
Traits
=
({
traits
,
collectionAddress
}:
{
traits
:
Trait
[];
collectionAddress
:
string
})
=>
(
<
div
className=
{
styles
.
grid
}
>
{
traits
.
length
===
0
?
'
No traits
'
:
traits
.
map
((
item
)
=>
{
const
params
=
qs
.
stringify
(
{
traits
:
[
`("${item.trait_type}","${item.trait_value}")`
]
},
{
arrayFormat
:
'
comma
'
,
}
)
return
(
<
a
key=
{
`${item.trait_type}-${item.trait_value}`
}
href=
{
`#/nfts/collection/${collectionAddress}?${params}`
}
style=
{
{
textDecoration
:
'
none
'
}
}
>
<
TraitRow
trait_type=
{
item
.
trait_type
}
trait_value=
{
item
.
trait_value
}
/>
</
a
>
)
})
}
</
div
>
)
src/nft/components/details/TraitsContainer.tsx
0 → 100644
View file @
4b1b6098
import
{
GenieAsset
,
Trait
}
from
'
nft/types
'
import
qs
from
'
query-string
'
import
{
useMemo
}
from
'
react
'
import
{
Link
}
from
'
react-router-dom
'
import
styled
from
'
styled-components/macro
'
const
Grid
=
styled
.
div
`
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 16px;
max-width: 780px;
@media (max-width: 960px) {
grid-template-columns: 1fr 1fr 1fr;
}
@media (max-width: 420px) {
grid-template-columns: 1fr 1fr;
}
`
const
GridItemContainer
=
styled
(
Link
)
`
background-color:
${({
theme
})
=>
theme
.
backgroundInteractive
}
;
border-radius: 12px;
cursor: pointer;
padding: 12px;
text-decoration: none;
&:hover {
opacity:
${({
theme
})
=>
theme
.
opacity
.
hover
}
;
}
&:active {
opacity:
${({
theme
})
=>
theme
.
opacity
.
click
}
;
}
transition:
${({
theme
:
{
transition
:
{
duration
,
timing
},
},
})
=>
`opacity
${
duration
.
medium
}
${
timing
.
ease
}
`
}
;
min-width: 0;
`
const
TraitType
=
styled
.
div
`
color:
${({
theme
})
=>
theme
.
textSecondary
}
;
font-weight: 600;
font-size: 10px;
line-height: 12px;
white-space: nowrap;
width: 100%;
`
const
TraitValue
=
styled
.
div
`
color:
${({
theme
})
=>
theme
.
textPrimary
}
;
font-size: 16px;
line-height: 24px;
margin-top: 4px;
display: inline-block;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
`
const
GridItem
=
({
trait
,
collectionAddress
}:
{
trait
:
Trait
;
collectionAddress
:
string
})
=>
{
const
{
trait_type
,
trait_value
}
=
trait
const
params
=
qs
.
stringify
(
{
traits
:
[
`("
${
trait_type
}
","
${
trait_value
}
")`
]
},
{
arrayFormat
:
'
comma
'
,
}
)
return
(
<
GridItemContainer
to=
{
`/nfts/collection/${collectionAddress}?${params}`
}
>
<
TraitType
>
{
trait_type
}
</
TraitType
>
<
TraitValue
>
{
trait_value
}
</
TraitValue
>
</
GridItemContainer
>
)
}
const
TraitsContainer
=
({
asset
}:
{
asset
:
GenieAsset
})
=>
{
const
traits
=
useMemo
(()
=>
asset
.
traits
?.
sort
((
a
,
b
)
=>
a
.
trait_type
.
localeCompare
(
b
.
trait_type
)),
[
asset
])
return
(
<
Grid
>
{
traits
?.
map
((
trait
)
=>
{
return
<
GridItem
key=
{
trait
.
trait_type
}
trait=
{
trait
}
collectionAddress=
{
asset
.
address
}
/>
})
}
</
Grid
>
)
}
export
default
TraitsContainer
src/nft/components/icons.tsx
View file @
4b1b6098
...
...
@@ -1495,11 +1495,10 @@ export const EmptyNFTWalletIcon = (props: SVGProps) => (
)
export
const
CancelListingIcon
=
(
props
:
SVGProps
)
=>
(
<
svg
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
{
...
props
}
>
<
svg
width=
"15"
height=
"15"
viewBox=
"0 0 15 15"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
{
...
props
}
>
<
path
d=
"M
71 31L75.36 35.36C76.85 36.8589 77.6863 38.8865 77.6863 41C77.6863 43.1135 76.85 45.1411 75.36 46.64L46.68 75.32C45.937 76.0638 45.0547 76.6539 44.0835 77.0565C43.1123 77.4591 42.0713 77.6663 41.02 77.6663C39.9687 77.6663 38.9277 77.4591 37.9565 77.0565C36.9853 76.6539 36.103 76.0638 35.36 75.32L31 71M47.8 7.8L41 1H1V41L7.8 47.8M77.6863 1L1.62987 77.0565M21 21H21.0333
"
d=
"M
12.6667 6L13.3933 6.72667C13.6417 6.97648 13.7811 7.31442 13.7811 7.66667C13.7811 8.01891 13.6417 8.35685 13.3933 8.60667L8.61333 13.3867C8.4895 13.5106 8.34245 13.609 8.18059 13.6761C8.01872 13.7432 7.84522 13.7777 7.67 13.7777C7.49478 13.7777 7.32128 13.7432 7.15941 13.6761C6.99755 13.609 6.8505 13.5106 6.72667 13.3867L6 12.6667M8.8 2.13333L7.66667 1H1V7.66667L2.13333 8.8M13.7811 1L1.10498 13.6761M4.33333 4.33333H4.33889
"
stroke=
"currentColor"
strokeWidth=
"2"
strokeLinecap=
"round"
strokeLinejoin=
"round"
/>
...
...
src/nft/pages/asset/Asset.tsx
View file @
4b1b6098
...
...
@@ -5,6 +5,7 @@ import { useDetailsQuery } from 'graphql/data/nft/Details'
import
{
AssetDetails
}
from
'
nft/components/details/AssetDetails
'
import
{
AssetPriceDetails
}
from
'
nft/components/details/AssetPriceDetails
'
import
{
fetchSingleAsset
}
from
'
nft/queries
'
import
{
CollectionStatsFetcher
}
from
'
nft/queries
'
import
{
useMemo
}
from
'
react
'
import
{
useQuery
}
from
'
react-query
'
import
{
useParams
}
from
'
react-router-dom
'
...
...
@@ -12,8 +13,20 @@ import styled from 'styled-components/macro'
const
AssetContainer
=
styled
.
div
`
display: flex;
padding-right: 116px;
padding-left: 116px;
width: 100%;
justify-content: center;
gap: 60px;
padding: 48px 40px 0 40px;
`
const
AssetPriceDetailsContainer
=
styled
.
div
`
min-width: 360px;
position: relative;
padding-right: 100px;
@media (max-width: 960px) {
display: none;
}
`
const
Asset
=
()
=>
{
...
...
@@ -37,6 +50,10 @@ const Asset = () => {
[
data
,
gqlData
,
isNftGraphQl
]
)
const
{
data
:
collectionStats
}
=
useQuery
([
'
collectionStats
'
,
contractAddress
],
()
=>
CollectionStatsFetcher
(
contractAddress
)
)
return
(
<>
<
Trace
...
...
@@ -46,8 +63,10 @@ const Asset = () => {
>
{
asset
&&
collection
?
(
<
AssetContainer
>
<
AssetDetails
collection=
{
collection
}
asset=
{
asset
}
/>
<
AssetDetails
collection=
{
collection
}
asset=
{
asset
}
collectionStats=
{
collectionStats
}
/>
<
AssetPriceDetailsContainer
>
<
AssetPriceDetails
collection=
{
collection
}
asset=
{
asset
}
/>
</
AssetPriceDetailsContainer
>
</
AssetContainer
>
)
:
(
<
div
>
Holder for loading ...
</
div
>
...
...
src/nft/queries/genie/ActivityFetcher.ts
View file @
4b1b6098
...
...
@@ -3,15 +3,19 @@ import { ActivityEventResponse, ActivityFilter } from '../../types'
export
const
ActivityFetcher
=
async
(
contractAddress
:
string
,
filters
?:
ActivityFilter
,
cursor
?:
string
cursor
?:
string
,
limit
?:
string
):
Promise
<
ActivityEventResponse
>
=>
{
const
filterParam
=
filters
&&
filters
.
eventTypes
?
`&
${
filters
.
eventTypes
?.
map
((
eventType
)
=>
`event_types[]=
${
eventType
}
`
).
join
(
'
&
'
)}
`
: ''
const url = `
$
{
process
.
env
.
REACT_APP_GENIE_V3_API_URL
}
/collections/
$
{
contractAddress
}
/activity
?
limit=25${filterParam}${cursor
?
`&cursor=${cursor}` : ''}
`
const tokenId = filters?.token_id ? `
&
token_id
=
$
{
filters
?.
token_id
}
` : ''
const url = `
$
{
process
.
env
.
REACT_APP_GENIE_V3_API_URL
}
/collections/
$
{
contractAddress
}
/activity
?
limit=$
{
limit
?
limit
:
'
25
'
}
$
{
filterParam
}
$
{
cursor
?
`&cursor=
${
cursor
}
`
:
''
}
$
{
tokenId
}
`
const r = await fetch(url, {
method: 'GET',
...
...
src/nft/queries/genie/SingleAssetFetcher.ts
View file @
4b1b6098
import
{
CollectionInfoForAsset
,
GenieAsset
}
from
'
../../types
'
interface
ReponseTrait
{
trait_type
:
string
value
:
string
}
export
const
fetchSingleAsset
=
async
({
contractAddress
,
tokenId
,
...
...
@@ -10,5 +15,9 @@ export const fetchSingleAsset = async ({
const
url
=
`
${
process
.
env
.
REACT_APP_GENIE_V3_API_URL
}
/assetDetails?address=
${
contractAddress
}
&tokenId=
${
tokenId
}
`
const
r
=
await
fetch
(
url
)
const
data
=
await
r
.
json
()
return
[
data
.
asset
[
0
],
data
.
collection
]
const
asset
=
data
.
asset
[
0
]
asset
.
traits
=
asset
.
traits
.
map
((
trait
:
ReponseTrait
)
=>
({
trait_type
:
trait
.
trait_type
,
trait_value
:
trait
.
value
}))
return
[
asset
,
data
.
collection
]
}
src/nft/types/collection/collection.ts
View file @
4b1b6098
...
...
@@ -51,7 +51,7 @@ export enum ActivityEventTypeDisplay {
'
LISTING
'
=
'
Listed
'
,
'
SALE
'
=
'
Sold
'
,
'
TRANSFER
'
=
'
Transferred
'
,
'
CANCEL_LISTING
'
=
'
Cancell
ed
'
,
'
CANCEL_LISTING
'
=
'
Cancell
ation
'
,
}
export
enum
OrderStatus
{
...
...
@@ -65,6 +65,7 @@ export interface ActivityFilter {
collectionAddress
?:
string
eventTypes
?:
ActivityEventType
[]
marketplaces
?:
Markets
[]
token_id
?:
string
}
export
interface
ActivityEventResponse
{
...
...
src/nft/types/common/common.ts
View file @
4b1b6098
import
{
Trait
}
from
'
nft/hooks/useCollectionFilters
'
import
{
Deprecated_SellOrder
,
SellOrder
}
from
'
../sell
'
export
interface
OpenSeaCollection
{
...
...
@@ -43,13 +41,6 @@ export interface OpenSeaAsset {
collection
?:
OpenSeaCollection
}
interface
OpenSeaUser
{
user
?:
null
profile_img_url
?:
string
address
?:
string
config
?:
string
}
export
enum
TokenType
{
ERC20
=
'
ERC20
'
,
ERC721
=
'
ERC721
'
,
...
...
@@ -77,6 +68,14 @@ export interface Rarity {
providers
?:
{
provider
:
string
;
rank
?:
number
;
url
?:
string
;
score
?:
number
}[]
}
export
interface
Trait
{
trait_type
:
string
trait_value
:
string
display_type
?:
any
max_value
?:
any
trait_count
?:
number
order
?:
any
}
export
interface
GenieAsset
{
id
?:
string
// This would be a random id created and assigned by front end
address
:
string
...
...
@@ -96,9 +95,14 @@ export interface GenieAsset {
totalCount
?:
number
// The totalCount from the query to /assets
collectionIsVerified
?:
boolean
rarity
?:
Rarity
owner
?:
string
creator
:
OpenSeaUser
metadataUrl
?:
string
owner
:
{
address
:
string
}
metadataUrl
:
string
creator
:
{
address
:
string
profile_img_url
:
string
}
traits
?:
Trait
[]
}
...
...
@@ -122,8 +126,8 @@ export interface GenieCollection {
}
traits
?:
Record
<
string
,
Trait
[]
>
marketplaceCount
?:
{
marketplace
:
string
;
count
:
number
}[]
imageUrl
?
:
string
twitter
?:
string
imageUrl
:
string
twitter
Url
?:
string
instagram
?:
string
discordUrl
?:
string
externalUrl
?:
string
...
...
src/nft/utils/fetchPrice.ts
View file @
4b1b6098
...
...
@@ -20,7 +20,8 @@ export const fetchPrice = async (currency: Currency = Currency.ETH): Promise<num
export
function
useUsdPrice
(
asset
:
GenieAsset
):
string
|
undefined
{
const
{
data
:
fetchedPriceData
}
=
useQuery
([
'
fetchPrice
'
,
{}],
()
=>
fetchPrice
(),
{})
return
fetchedPriceData
&&
asset
.
priceInfo
.
ETHPrice
?
(
parseFloat
(
formatEther
(
asset
.
priceInfo
.
ETHPrice
))
*
fetchedPriceData
).
toString
()
:
undefined
return
fetchedPriceData
&&
asset
?.
priceInfo
?.
ETHPrice
?
(
parseFloat
(
formatEther
(
asset
?.
priceInfo
?.
ETHPrice
))
*
fetchedPriceData
).
toString
()
:
''
}
src/nft/utils/putCommas.ts
View file @
4b1b6098
export
const
putCommas
=
(
value
?
:
number
)
=>
{
export
const
putCommas
=
(
value
:
number
)
=>
{
try
{
if
(
!
value
)
return
value
return
value
.
toString
().
replace
(
/
\B(?=(\d{3})
+
(?!\d))
/g
,
'
,
'
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment