mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Merge branch 'main' into fix/issue-1069-c2d-unsupported-networks
This commit is contained in:
commit
1649e075e2
@ -5,7 +5,7 @@ export const assetAquarius: Asset = {
|
|||||||
id: 'did:op:6654b0793765b269696cec8d2f0d077d9bbcdd3c4f033d941ab9684e8ad06630',
|
id: 'did:op:6654b0793765b269696cec8d2f0d077d9bbcdd3c4f033d941ab9684e8ad06630',
|
||||||
nftAddress: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
|
nftAddress: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
|
||||||
version: '4.1.0',
|
version: '4.1.0',
|
||||||
chainId: 5,
|
chainId: 1,
|
||||||
metadata: {
|
metadata: {
|
||||||
created: '2022-09-29T11:30:26Z',
|
created: '2022-09-29T11:30:26Z',
|
||||||
updated: '2022-09-29T11:30:26Z',
|
updated: '2022-09-29T11:30:26Z',
|
||||||
|
1272
package-lock.json
generated
1272
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,12 @@
|
|||||||
import { useRef, useEffect, useCallback } from 'react'
|
import { useRef, useEffect, useCallback } from 'react'
|
||||||
import axios, { CancelToken } from 'axios'
|
import axios, { CancelToken } from 'axios'
|
||||||
|
|
||||||
export const useCancelToken = (): (() => CancelToken) => {
|
export const useCancelToken = (): (() => CancelToken) => {
|
||||||
const axiosSource = useRef(null)
|
const axiosSource = useRef(null)
|
||||||
|
|
||||||
const newCancelToken = useCallback(() => {
|
const newCancelToken = useCallback(() => {
|
||||||
axiosSource.current = axios.CancelToken.source()
|
axiosSource.current = axios.CancelToken.source()
|
||||||
return axiosSource.current.token
|
return axiosSource?.current?.token
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
4
src/@types/Analytics.d.ts
vendored
Normal file
4
src/@types/Analytics.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
interface PageViews {
|
||||||
|
count: number
|
||||||
|
did: string
|
||||||
|
}
|
1
src/@types/AssetExtended.d.ts
vendored
1
src/@types/AssetExtended.d.ts
vendored
@ -5,5 +5,6 @@ import { Asset } from '@oceanprotocol/lib'
|
|||||||
declare global {
|
declare global {
|
||||||
interface AssetExtended extends Asset {
|
interface AssetExtended extends Asset {
|
||||||
accessDetails?: AccessDetails
|
accessDetails?: AccessDetails
|
||||||
|
views?: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,13 +55,13 @@ export function generateBaseQuery(
|
|||||||
...(baseQueryParams.filters || []),
|
...(baseQueryParams.filters || []),
|
||||||
baseQueryParams.chainIds
|
baseQueryParams.chainIds
|
||||||
? getFilterTerm('chainId', baseQueryParams.chainIds)
|
? getFilterTerm('chainId', baseQueryParams.chainIds)
|
||||||
: [],
|
: '',
|
||||||
getFilterTerm('_index', 'aquarius'),
|
getFilterTerm('_index', 'aquarius'),
|
||||||
...(baseQueryParams.ignorePurgatory
|
...(baseQueryParams.ignorePurgatory
|
||||||
? []
|
? ''
|
||||||
: [getFilterTerm('purgatory.state', false)]),
|
: [getFilterTerm('purgatory.state', false)]),
|
||||||
...(baseQueryParams.ignoreState
|
...(baseQueryParams.ignoreState
|
||||||
? []
|
? ''
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
bool: {
|
bool: {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Asset } from '@oceanprotocol/lib'
|
||||||
|
|
||||||
// Boolean value that will be true if we are inside a browser, false otherwise
|
// Boolean value that will be true if we are inside a browser, false otherwise
|
||||||
export const isBrowser = typeof window !== 'undefined'
|
export const isBrowser = typeof window !== 'undefined'
|
||||||
|
|
||||||
@ -14,3 +16,10 @@ export function removeItemFromArray<T>(arr: Array<T>, value: T): Array<T> {
|
|||||||
}
|
}
|
||||||
return arr
|
return arr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sortAssets(items: Asset[], sorted: string[]) {
|
||||||
|
items.sort(function (a, b) {
|
||||||
|
return sorted?.indexOf(a.id) - sorted?.indexOf(b.id)
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
@ -82,10 +82,13 @@ export function generateNftCreateData(
|
|||||||
|
|
||||||
export function decodeTokenURI(tokenURI: string): NftMetadata {
|
export function decodeTokenURI(tokenURI: string): NftMetadata {
|
||||||
if (!tokenURI) return undefined
|
if (!tokenURI) return undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nftMeta = JSON.parse(
|
const nftMeta = tokenURI.includes('data:application/json')
|
||||||
Buffer.from(tokenURI.replace(tokenUriPrefix, ''), 'base64').toString()
|
? (JSON.parse(
|
||||||
) as NftMetadata
|
Buffer.from(tokenURI.replace(tokenUriPrefix, ''), 'base64').toString()
|
||||||
|
) as NftMetadata)
|
||||||
|
: ({ image: tokenURI } as NftMetadata)
|
||||||
|
|
||||||
return nftMeta
|
return nftMeta
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,5 +1,20 @@
|
|||||||
|
import { formatCurrency } from '@coingecko/cryptoformat'
|
||||||
import { Decimal } from 'decimal.js'
|
import { Decimal } from 'decimal.js'
|
||||||
|
|
||||||
|
export function formatNumber(
|
||||||
|
price: number,
|
||||||
|
locale: string,
|
||||||
|
decimals?: string
|
||||||
|
): string {
|
||||||
|
return formatCurrency(price, '', locale, false, {
|
||||||
|
// Not exactly clear what `significant figures` are for this library,
|
||||||
|
// but setting this seems to give us the formatting we want.
|
||||||
|
// See https://github.com/oceanprotocol/market/issues/70
|
||||||
|
significantFigures: 4,
|
||||||
|
...(decimals && { decimalPlaces: Number(decimals) })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Run decimal.js comparison
|
// Run decimal.js comparison
|
||||||
// http://mikemcl.github.io/decimal.js/#cmp
|
// http://mikemcl.github.io/decimal.js/#cmp
|
||||||
export function compareAsBN(balance: string, price: string): boolean {
|
export function compareAsBN(balance: string, price: string): boolean {
|
||||||
|
@ -8,7 +8,8 @@ import {
|
|||||||
OrderParams,
|
OrderParams,
|
||||||
ProviderComputeInitialize,
|
ProviderComputeInitialize,
|
||||||
ProviderFees,
|
ProviderFees,
|
||||||
ProviderInstance
|
ProviderInstance,
|
||||||
|
ProviderInitialize
|
||||||
} from '@oceanprotocol/lib'
|
} from '@oceanprotocol/lib'
|
||||||
import Web3 from 'web3'
|
import Web3 from 'web3'
|
||||||
import { getOceanConfig } from './ocean'
|
import { getOceanConfig } from './ocean'
|
||||||
@ -20,6 +21,26 @@ import {
|
|||||||
} from '../../app.config'
|
} from '../../app.config'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
|
async function initializeProvider(
|
||||||
|
asset: AssetExtended,
|
||||||
|
accountId: string,
|
||||||
|
providerFees?: ProviderFees
|
||||||
|
): Promise<ProviderInitialize> {
|
||||||
|
if (providerFees) return
|
||||||
|
try {
|
||||||
|
const provider = await ProviderInstance.initialize(
|
||||||
|
asset.id,
|
||||||
|
asset.services[0].id,
|
||||||
|
0,
|
||||||
|
accountId,
|
||||||
|
asset.services[0].serviceEndpoint
|
||||||
|
)
|
||||||
|
return provider
|
||||||
|
} catch (error) {
|
||||||
|
LoggerInstance.log('[Initialize Provider] Error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param web3
|
* @param web3
|
||||||
* @param asset
|
* @param asset
|
||||||
@ -40,15 +61,11 @@ export async function order(
|
|||||||
const datatoken = new Datatoken(web3)
|
const datatoken = new Datatoken(web3)
|
||||||
const config = getOceanConfig(asset.chainId)
|
const config = getOceanConfig(asset.chainId)
|
||||||
|
|
||||||
const initializeData =
|
const initializeData = await initializeProvider(
|
||||||
!providerFees &&
|
asset,
|
||||||
(await ProviderInstance.initialize(
|
accountId,
|
||||||
asset.id,
|
providerFees
|
||||||
asset.services[0].id,
|
)
|
||||||
0,
|
|
||||||
accountId,
|
|
||||||
asset.services[0].serviceEndpoint
|
|
||||||
))
|
|
||||||
|
|
||||||
const orderParams = {
|
const orderParams = {
|
||||||
consumer: computeConsumerAddress || accountId,
|
consumer: computeConsumerAddress || accountId,
|
||||||
@ -130,15 +147,11 @@ export async function reuseOrder(
|
|||||||
providerFees?: ProviderFees
|
providerFees?: ProviderFees
|
||||||
): Promise<TransactionReceipt> {
|
): Promise<TransactionReceipt> {
|
||||||
const datatoken = new Datatoken(web3)
|
const datatoken = new Datatoken(web3)
|
||||||
const initializeData =
|
const initializeData = await initializeProvider(
|
||||||
!providerFees &&
|
asset,
|
||||||
(await ProviderInstance.initialize(
|
accountId,
|
||||||
asset.id,
|
providerFees
|
||||||
asset.services[0].id,
|
)
|
||||||
0,
|
|
||||||
accountId,
|
|
||||||
asset.services[0].serviceEndpoint
|
|
||||||
))
|
|
||||||
|
|
||||||
const tx = await datatoken.reuseOrder(
|
const tx = await datatoken.reuseOrder(
|
||||||
asset.accessDetails.datatoken.address,
|
asset.accessDetails.datatoken.address,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { AllLocked } from 'src/@types/subgraph/AllLocked'
|
import { AllLockedQuery } from 'src/@types/subgraph/AllLockedQuery'
|
||||||
import { OwnAllocations } from 'src/@types/subgraph/OwnAllocations'
|
import { OwnAllocationsQuery } from 'src/@types/subgraph/OwnAllocationsQuery'
|
||||||
import { NftOwnAllocation } from 'src/@types/subgraph/NftOwnAllocation'
|
import { NftOwnAllocationQuery } from 'src/@types/subgraph/NftOwnAllocationQuery'
|
||||||
import { OceanLocked } from 'src/@types/subgraph/OceanLocked'
|
import { OceanLockedQuery } from 'src/@types/subgraph/OceanLockedQuery'
|
||||||
import { gql, OperationResult } from 'urql'
|
import { gql, OperationResult } from 'urql'
|
||||||
import { fetchData, getQueryContext } from './subgraph'
|
import { fetchData, getQueryContext } from './subgraph'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
@ -12,11 +12,11 @@ import {
|
|||||||
NetworkType
|
NetworkType
|
||||||
} from '@hooks/useNetworkMetadata'
|
} from '@hooks/useNetworkMetadata'
|
||||||
import { getAssetsFromNftList } from './aquarius'
|
import { getAssetsFromNftList } from './aquarius'
|
||||||
import { chainIdsSupported } from 'app.config'
|
import { chainIdsSupported } from '../../app.config'
|
||||||
import { Asset } from '@oceanprotocol/lib'
|
import { Asset } from '@oceanprotocol/lib'
|
||||||
|
|
||||||
const AllLocked = gql`
|
const AllLocked = gql`
|
||||||
query AllLocked {
|
query AllLockedQuery {
|
||||||
veOCEANs(first: 1000) {
|
veOCEANs(first: 1000) {
|
||||||
lockedAmount
|
lockedAmount
|
||||||
}
|
}
|
||||||
@ -24,7 +24,7 @@ const AllLocked = gql`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const OwnAllocations = gql`
|
const OwnAllocations = gql`
|
||||||
query OwnAllocations($address: String) {
|
query OwnAllocationsQuery($address: String) {
|
||||||
veAllocations(where: { allocationUser: $address }) {
|
veAllocations(where: { allocationUser: $address }) {
|
||||||
id
|
id
|
||||||
nftAddress
|
nftAddress
|
||||||
@ -33,7 +33,7 @@ const OwnAllocations = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
const NftOwnAllocation = gql`
|
const NftOwnAllocation = gql`
|
||||||
query NftOwnAllocation($address: String, $nftAddress: String) {
|
query NftOwnAllocationQuery($address: String, $nftAddress: String) {
|
||||||
veAllocations(
|
veAllocations(
|
||||||
where: { allocationUser: $address, nftAddress: $nftAddress }
|
where: { allocationUser: $address, nftAddress: $nftAddress }
|
||||||
) {
|
) {
|
||||||
@ -42,7 +42,7 @@ const NftOwnAllocation = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
const OceanLocked = gql`
|
const OceanLocked = gql`
|
||||||
query OceanLocked($address: ID!) {
|
query OceanLockedQuery($address: ID!) {
|
||||||
veOCEAN(id: $address) {
|
veOCEAN(id: $address) {
|
||||||
id
|
id
|
||||||
lockedAmount
|
lockedAmount
|
||||||
@ -87,7 +87,7 @@ export async function getNftOwnAllocation(
|
|||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
const veNetworkId = getVeChainNetworkId(networkId)
|
const veNetworkId = getVeChainNetworkId(networkId)
|
||||||
const queryContext = getQueryContext(veNetworkId)
|
const queryContext = getQueryContext(veNetworkId)
|
||||||
const fetchedAllocation: OperationResult<NftOwnAllocation, any> =
|
const fetchedAllocation: OperationResult<NftOwnAllocationQuery, any> =
|
||||||
await fetchData(
|
await fetchData(
|
||||||
NftOwnAllocation,
|
NftOwnAllocation,
|
||||||
{
|
{
|
||||||
@ -115,7 +115,7 @@ export async function getTotalAllocatedAndLocked(): Promise<TotalVe> {
|
|||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
const fetchedLocked: OperationResult<AllLocked, any> = await fetchData(
|
const fetchedLocked: OperationResult<AllLockedQuery, any> = await fetchData(
|
||||||
AllLocked,
|
AllLocked,
|
||||||
null,
|
null,
|
||||||
queryContext
|
queryContext
|
||||||
@ -136,11 +136,12 @@ export async function getLocked(
|
|||||||
const veNetworkIds = getVeChainNetworkIds(networkIds)
|
const veNetworkIds = getVeChainNetworkIds(networkIds)
|
||||||
for (let i = 0; i < veNetworkIds.length; i++) {
|
for (let i = 0; i < veNetworkIds.length; i++) {
|
||||||
const queryContext = getQueryContext(veNetworkIds[i])
|
const queryContext = getQueryContext(veNetworkIds[i])
|
||||||
const fetchedLocked: OperationResult<OceanLocked, any> = await fetchData(
|
const fetchedLocked: OperationResult<OceanLockedQuery, any> =
|
||||||
OceanLocked,
|
await fetchData(
|
||||||
{ address: userAddress.toLowerCase() },
|
OceanLocked,
|
||||||
queryContext
|
{ address: userAddress.toLowerCase() },
|
||||||
)
|
queryContext
|
||||||
|
)
|
||||||
|
|
||||||
fetchedLocked.data?.veOCEAN?.lockedAmount &&
|
fetchedLocked.data?.veOCEAN?.lockedAmount &&
|
||||||
(total += Number(fetchedLocked.data?.veOCEAN?.lockedAmount))
|
(total += Number(fetchedLocked.data?.veOCEAN?.lockedAmount))
|
||||||
@ -157,7 +158,7 @@ export async function getOwnAllocations(
|
|||||||
const veNetworkIds = getVeChainNetworkIds(networkIds)
|
const veNetworkIds = getVeChainNetworkIds(networkIds)
|
||||||
for (let i = 0; i < veNetworkIds.length; i++) {
|
for (let i = 0; i < veNetworkIds.length; i++) {
|
||||||
const queryContext = getQueryContext(veNetworkIds[i])
|
const queryContext = getQueryContext(veNetworkIds[i])
|
||||||
const fetchedAllocations: OperationResult<OwnAllocations, any> =
|
const fetchedAllocations: OperationResult<OwnAllocationsQuery, any> =
|
||||||
await fetchData(
|
await fetchData(
|
||||||
OwnAllocations,
|
OwnAllocations,
|
||||||
{ address: userAddress.toLowerCase() },
|
{ address: userAddress.toLowerCase() },
|
||||||
|
@ -69,11 +69,13 @@ export default function AssetList({
|
|||||||
|
|
||||||
const styleClasses = `${styles.assetList} ${className || ''}`
|
const styleClasses = `${styles.assetList} ${className || ''}`
|
||||||
|
|
||||||
return assetsWithPrices && !loading ? (
|
return loading ? (
|
||||||
|
<LoaderArea />
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className={styleClasses}>
|
<div className={styleClasses}>
|
||||||
{assetsWithPrices.length > 0 ? (
|
{assetsWithPrices?.length > 0 ? (
|
||||||
assetsWithPrices.map((assetWithPrice) => (
|
assetsWithPrices?.map((assetWithPrice) => (
|
||||||
<AssetTeaser
|
<AssetTeaser
|
||||||
asset={assetWithPrice}
|
asset={assetWithPrice}
|
||||||
key={assetWithPrice.id}
|
key={assetWithPrice.id}
|
||||||
@ -95,7 +97,5 @@ export default function AssetList({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
|
||||||
<LoaderArea />
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: calc(var(--spacer) / 12);
|
margin-top: calc(var(--spacer) / 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
.typeLabel {
|
.typeLabel {
|
||||||
|
@ -8,8 +8,8 @@ import AssetType from '@shared/AssetType'
|
|||||||
import NetworkName from '@shared/NetworkName'
|
import NetworkName from '@shared/NetworkName'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
import { getServiceByName } from '@utils/ddo'
|
import { getServiceByName } from '@utils/ddo'
|
||||||
import { formatPrice } from '@shared/Price/PriceUnit'
|
|
||||||
import { useUserPreferences } from '@context/UserPreferences'
|
import { useUserPreferences } from '@context/UserPreferences'
|
||||||
|
import { formatNumber } from '@utils/numbers'
|
||||||
|
|
||||||
export declare type AssetTeaserProps = {
|
export declare type AssetTeaserProps = {
|
||||||
asset: AssetExtended
|
asset: AssetExtended
|
||||||
@ -77,16 +77,37 @@ export default function AssetTeaser({
|
|||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
{allocated && allocated > 0 ? (
|
{allocated && allocated > 0 ? (
|
||||||
<span className={styles.typeLabel}>
|
<span className={styles.typeLabel}>
|
||||||
{allocated < 0
|
{allocated < 0 ? (
|
||||||
? ''
|
''
|
||||||
: `${formatPrice(allocated, locale)} veOCEAN`}
|
) : (
|
||||||
|
<>
|
||||||
|
<strong>{formatNumber(allocated, locale, '0')}</strong>{' '}
|
||||||
|
veOCEAN
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{orders && orders > 0 ? (
|
{orders && orders > 0 ? (
|
||||||
<span className={styles.typeLabel}>
|
<span className={styles.typeLabel}>
|
||||||
{orders < 0
|
{orders < 0 ? (
|
||||||
? 'N/A'
|
'N/A'
|
||||||
: `${orders} ${orders === 1 ? 'sale' : 'sales'}`}
|
) : (
|
||||||
|
<>
|
||||||
|
<strong>{orders}</strong> {orders === 1 ? 'sale' : 'sales'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{asset.views && asset.views > 0 ? (
|
||||||
|
<span className={styles.typeLabel}>
|
||||||
|
{asset.views < 0 ? (
|
||||||
|
'N/A'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<strong>{asset.views}</strong>{' '}
|
||||||
|
{asset.views === 1 ? 'view' : 'views'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -1,17 +1,8 @@
|
|||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import { formatCurrency } from '@coingecko/cryptoformat'
|
|
||||||
import Conversion from './Conversion'
|
import Conversion from './Conversion'
|
||||||
import styles from './PriceUnit.module.css'
|
import styles from './PriceUnit.module.css'
|
||||||
import { useUserPreferences } from '@context/UserPreferences'
|
import { useUserPreferences } from '@context/UserPreferences'
|
||||||
|
import { formatNumber } from '@utils/numbers'
|
||||||
export function formatPrice(price: number, locale: string): string {
|
|
||||||
return formatCurrency(price, '', locale, false, {
|
|
||||||
// Not exactly clear what `significant figures` are for this library,
|
|
||||||
// but setting this seems to give us the formatting we want.
|
|
||||||
// See https://github.com/oceanprotocol/market/issues/70
|
|
||||||
significantFigures: 4
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PriceUnit({
|
export default function PriceUnit({
|
||||||
price,
|
price,
|
||||||
@ -19,7 +10,8 @@ export default function PriceUnit({
|
|||||||
size = 'small',
|
size = 'small',
|
||||||
conversion,
|
conversion,
|
||||||
symbol,
|
symbol,
|
||||||
type
|
type,
|
||||||
|
decimals
|
||||||
}: {
|
}: {
|
||||||
price: number
|
price: number
|
||||||
type?: string
|
type?: string
|
||||||
@ -27,6 +19,7 @@ export default function PriceUnit({
|
|||||||
size?: 'small' | 'mini' | 'large'
|
size?: 'small' | 'mini' | 'large'
|
||||||
conversion?: boolean
|
conversion?: boolean
|
||||||
symbol?: string
|
symbol?: string
|
||||||
|
decimals?: string
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const { locale } = useUserPreferences()
|
const { locale } = useUserPreferences()
|
||||||
|
|
||||||
@ -37,7 +30,7 @@ export default function PriceUnit({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{Number.isNaN(price) ? '-' : formatPrice(price, locale)}{' '}
|
{Number.isNaN(price) ? '-' : formatNumber(price, locale, decimals)}{' '}
|
||||||
<span className={styles.symbol}>{symbol}</span>
|
<span className={styles.symbol}>{symbol}</span>
|
||||||
</div>
|
</div>
|
||||||
{conversion && <Conversion price={price} symbol={symbol} />}
|
{conversion && <Conversion price={price} symbol={symbol} />}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import * as axios from 'axios'
|
import axios from 'axios'
|
||||||
import Publisher from './'
|
import Publisher from './'
|
||||||
|
|
||||||
const account = '0x0000000000000000000000000000000000000000'
|
const account = '0x0000000000000000000000000000000000000000'
|
||||||
|
|
||||||
jest.mock('axios')
|
jest.mock('axios')
|
||||||
|
const axiosMock = axios as jest.Mocked<typeof axios>
|
||||||
|
|
||||||
describe('@shared/Publisher', () => {
|
describe('@shared/Publisher', () => {
|
||||||
test('should return correct markup by default', async () => {
|
test('should return correct markup by default', async () => {
|
||||||
;(axios as any).get.mockImplementationOnce(() =>
|
axiosMock.get.mockImplementationOnce(() =>
|
||||||
Promise.resolve({ data: { name: 'jellymcjellyfish.eth' } })
|
Promise.resolve({ data: { name: 'jellymcjellyfish.eth' } })
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ describe('@shared/Publisher', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should truncate account by default', async () => {
|
test('should truncate account by default', async () => {
|
||||||
;(axios as any).get.mockImplementationOnce(() =>
|
axiosMock.get.mockImplementationOnce(() =>
|
||||||
Promise.resolve({ data: { name: null } })
|
Promise.resolve({ data: { name: null } })
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ describe('@shared/Publisher', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should return correct markup in minimal state', async () => {
|
test('should return correct markup in minimal state', async () => {
|
||||||
;(axios as any).get.mockImplementationOnce(() =>
|
axiosMock.get.mockImplementationOnce(() =>
|
||||||
Promise.resolve({ data: { name: null } })
|
Promise.resolve({ data: { name: null } })
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ describe('@shared/Publisher', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should return markup with empty account', async () => {
|
test('should return markup with empty account', async () => {
|
||||||
;(axios as any).get.mockImplementationOnce(() =>
|
axiosMock.get.mockImplementationOnce(() =>
|
||||||
Promise.resolve({ data: { name: null } })
|
Promise.resolve({ data: { name: null } })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,4 +24,9 @@ describe('Tags', () => {
|
|||||||
it('renders WithoutLinks', () => {
|
it('renders WithoutLinks', () => {
|
||||||
render(<Tags {...argsWithoutLinks} />)
|
render(<Tags {...argsWithoutLinks} />)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders with faulty tags', () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
render(<Tags items={'tags' as any} />)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -30,6 +30,9 @@ export default function Tags({
|
|||||||
className,
|
className,
|
||||||
noLinks
|
noLinks
|
||||||
}: TagsProps): ReactElement {
|
}: TagsProps): ReactElement {
|
||||||
|
// safeguard against faults in the metadata
|
||||||
|
if (!(items instanceof Array)) return null
|
||||||
|
|
||||||
max = max || items.length
|
max = max || items.length
|
||||||
const remainder = items.length - max
|
const remainder = items.length - max
|
||||||
// filter out empty array items, and restrict to `max`
|
// filter out empty array items, and restrict to `max`
|
||||||
|
@ -17,7 +17,8 @@ const DefaultTrigger = React.forwardRef((props, ref: any) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default function Tooltip(props: TippyProps): ReactElement {
|
export default function Tooltip(props: TippyProps): ReactElement {
|
||||||
const { content, children, trigger, disabled, className, placement } = props
|
const { className, ...restProps } = props
|
||||||
|
const { content, children, trigger, disabled, placement } = props
|
||||||
const [styles, api] = useSpring(() => animation.from)
|
const [styles, api] = useSpring(() => animation.from)
|
||||||
|
|
||||||
function onMount() {
|
function onMount() {
|
||||||
@ -60,7 +61,7 @@ export default function Tooltip(props: TippyProps): ReactElement {
|
|||||||
onMount={onMount}
|
onMount={onMount}
|
||||||
onHide={onHide}
|
onHide={onHide}
|
||||||
// animation
|
// animation
|
||||||
{...props}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<div className={styleClasses}>{children || <DefaultTrigger />}</div>
|
<div className={styleClasses}>{children || <DefaultTrigger />}</div>
|
||||||
</Tippy>
|
</Tippy>
|
||||||
|
@ -2,7 +2,7 @@ import { useAsset } from '@context/Asset'
|
|||||||
import { useUserPreferences } from '@context/UserPreferences'
|
import { useUserPreferences } from '@context/UserPreferences'
|
||||||
import { useWeb3 } from '@context/Web3'
|
import { useWeb3 } from '@context/Web3'
|
||||||
import Tooltip from '@shared/atoms/Tooltip'
|
import Tooltip from '@shared/atoms/Tooltip'
|
||||||
import { formatPrice } from '@shared/Price/PriceUnit'
|
import { formatNumber } from '@utils/numbers'
|
||||||
import { getNftOwnAllocation } from '@utils/veAllocation'
|
import { getNftOwnAllocation } from '@utils/veAllocation'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
@ -33,8 +33,8 @@ export default function AssetStats() {
|
|||||||
{asset?.stats?.allocated && asset?.stats?.allocated > 0 ? (
|
{asset?.stats?.allocated && asset?.stats?.allocated > 0 ? (
|
||||||
<span className={styles.stat}>
|
<span className={styles.stat}>
|
||||||
<span className={styles.number}>
|
<span className={styles.number}>
|
||||||
{formatPrice(asset.stats.allocated, locale)}
|
{formatNumber(asset.stats.allocated, locale, '0')}
|
||||||
</span>
|
</span>{' '}
|
||||||
veOCEAN
|
veOCEAN
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
143
src/components/Asset/AssetActions/ButtonBuy/index.test.tsx
Normal file
143
src/components/Asset/AssetActions/ButtonBuy/index.test.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import ButtonBuy, { ButtonBuyProps } from './'
|
||||||
|
|
||||||
|
const downloadProps: ButtonBuyProps = {
|
||||||
|
action: 'download',
|
||||||
|
disabled: false,
|
||||||
|
hasPreviousOrder: false,
|
||||||
|
hasDatatoken: false,
|
||||||
|
btSymbol: 'btSymbol',
|
||||||
|
dtSymbol: 'dtSymbol',
|
||||||
|
dtBalance: '100000000000',
|
||||||
|
assetTimeout: '1 day',
|
||||||
|
assetType: 'Dataset',
|
||||||
|
stepText: 'TEST',
|
||||||
|
priceType: 'fixed',
|
||||||
|
isConsumable: true,
|
||||||
|
isBalanceSufficient: true,
|
||||||
|
consumableFeedback: 'TEST: consumableFeedback'
|
||||||
|
}
|
||||||
|
|
||||||
|
const computeProps: ButtonBuyProps = {
|
||||||
|
action: 'compute',
|
||||||
|
disabled: false,
|
||||||
|
hasPreviousOrder: false,
|
||||||
|
hasDatatoken: true,
|
||||||
|
btSymbol: 'btSymbol',
|
||||||
|
dtSymbol: 'dtSymbol',
|
||||||
|
dtBalance: '100000000000',
|
||||||
|
assetTimeout: '1 day',
|
||||||
|
assetType: 'algorithm',
|
||||||
|
hasPreviousOrderSelectedComputeAsset: false,
|
||||||
|
hasDatatokenSelectedComputeAsset: true,
|
||||||
|
dtSymbolSelectedComputeAsset: 'dtSymbol',
|
||||||
|
dtBalanceSelectedComputeAsset: 'dtBalance',
|
||||||
|
selectedComputeAssetType: 'selectedComputeAssetType',
|
||||||
|
stepText: ' ',
|
||||||
|
isLoading: false,
|
||||||
|
type: 'submit',
|
||||||
|
priceType: 'fixed',
|
||||||
|
algorithmPriceType: 'free',
|
||||||
|
isBalanceSufficient: true,
|
||||||
|
isConsumable: true,
|
||||||
|
consumableFeedback: 'consumableFeedback',
|
||||||
|
isAlgorithmConsumable: true,
|
||||||
|
hasProviderFee: false,
|
||||||
|
retry: false
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Asset/AssetActions/ButtonBuy', () => {
|
||||||
|
// TESTS FOR LOADING
|
||||||
|
it('Renders Buy button without crashing', () => {
|
||||||
|
render(<ButtonBuy {...downloadProps} isLoading />)
|
||||||
|
const button = screen.getByText('TEST')
|
||||||
|
expect(button).toContainHTML('<Loader')
|
||||||
|
})
|
||||||
|
|
||||||
|
// TESTS FOR DOWNLOAD
|
||||||
|
it('Renders Buy button without crashing', () => {
|
||||||
|
render(<ButtonBuy {...downloadProps} />)
|
||||||
|
const button = screen.getByText('Buy for 1 day')
|
||||||
|
expect(button).toContainHTML('<button')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders Buy button without crashing when hasPreviousOrder=true', () => {
|
||||||
|
render(<ButtonBuy {...downloadProps} hasPreviousOrder />)
|
||||||
|
const button = screen.getByText('Download')
|
||||||
|
expect(button).toContainHTML('<button')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders retry button for download without crashing', () => {
|
||||||
|
render(<ButtonBuy {...downloadProps} retry />)
|
||||||
|
const button = screen.getByText('Retry')
|
||||||
|
expect(button).toContainHTML('<button')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders get button for free download without crashing', () => {
|
||||||
|
render(<ButtonBuy {...downloadProps} priceType="free" hasPreviousOrder />)
|
||||||
|
const button = screen.getByText('Download')
|
||||||
|
expect(button).toContainHTML('<button')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders "Get" button for free assets without crashing', () => {
|
||||||
|
render(<ButtonBuy {...downloadProps} priceType="free" />)
|
||||||
|
const button = screen.getByText('Get')
|
||||||
|
expect(button).toContainHTML('<button')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders Buy button without crashing', () => {
|
||||||
|
render(
|
||||||
|
<ButtonBuy
|
||||||
|
{...downloadProps}
|
||||||
|
assetTimeout="Forever"
|
||||||
|
isConsumable={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
const button = screen.getByText('Buy')
|
||||||
|
expect(button).toContainHTML('<button')
|
||||||
|
})
|
||||||
|
|
||||||
|
// TESTS FOR COMPUTE
|
||||||
|
it('Renders "Buy Compute Job" button for compute without crashing', () => {
|
||||||
|
render(<ButtonBuy {...computeProps} />)
|
||||||
|
const button = screen.getByText('Buy Compute Job')
|
||||||
|
expect(button).toContainHTML('<button')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders "Buy Compute Job" button for compute without crashing', () => {
|
||||||
|
render(<ButtonBuy {...computeProps} hasDatatokenSelectedComputeAsset />)
|
||||||
|
const button = screen.getByText('Buy Compute Job')
|
||||||
|
expect(button).toContainHTML('<button')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders "Start Compute Job" button', () => {
|
||||||
|
render(
|
||||||
|
<ButtonBuy
|
||||||
|
{...computeProps}
|
||||||
|
hasPreviousOrder
|
||||||
|
hasPreviousOrderSelectedComputeAsset
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
const button = screen.getByText('Start Compute Job')
|
||||||
|
expect(button).toContainHTML('<button')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders "Order Compute Job" button', () => {
|
||||||
|
render(<ButtonBuy {...computeProps} priceType="free" hasProviderFee />)
|
||||||
|
const button = screen.getByText('Order Compute Job')
|
||||||
|
expect(button).toContainHTML('<button')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders "Order Compute Job" button', () => {
|
||||||
|
render(<ButtonBuy {...computeProps} priceType="free" hasProviderFee />)
|
||||||
|
const button = screen.getByText('Order Compute Job')
|
||||||
|
expect(button).toContainHTML('<button')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders "retry" button for compute without crashing', () => {
|
||||||
|
render(<ButtonBuy {...computeProps} retry />)
|
||||||
|
const button = screen.getByText('Retry')
|
||||||
|
expect(button).toContainHTML('<button')
|
||||||
|
})
|
||||||
|
})
|
@ -5,7 +5,7 @@ import Loader from '../../../@shared/atoms/Loader'
|
|||||||
import { useWeb3 } from '@context/Web3'
|
import { useWeb3 } from '@context/Web3'
|
||||||
import Web3 from 'web3'
|
import Web3 from 'web3'
|
||||||
|
|
||||||
interface ButtonBuyProps {
|
export interface ButtonBuyProps {
|
||||||
action: 'download' | 'compute'
|
action: 'download' | 'compute'
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
hasPreviousOrder: boolean
|
hasPreviousOrder: boolean
|
||||||
@ -32,6 +32,7 @@ interface ButtonBuyProps {
|
|||||||
isAlgorithmConsumable?: boolean
|
isAlgorithmConsumable?: boolean
|
||||||
isSupportedOceanNetwork?: boolean
|
isSupportedOceanNetwork?: boolean
|
||||||
hasProviderFee?: boolean
|
hasProviderFee?: boolean
|
||||||
|
retry?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConsumeHelpText(
|
function getConsumeHelpText(
|
||||||
@ -168,24 +169,26 @@ export default function ButtonBuy({
|
|||||||
priceType,
|
priceType,
|
||||||
algorithmPriceType,
|
algorithmPriceType,
|
||||||
isAlgorithmConsumable,
|
isAlgorithmConsumable,
|
||||||
isSupportedOceanNetwork,
|
hasProviderFee,
|
||||||
hasProviderFee
|
retry,
|
||||||
|
isSupportedOceanNetwork
|
||||||
}: ButtonBuyProps): ReactElement {
|
}: ButtonBuyProps): ReactElement {
|
||||||
const { web3 } = useWeb3()
|
const { web3 } = useWeb3()
|
||||||
const buttonText =
|
const buttonText = retry
|
||||||
action === 'download'
|
? 'Retry'
|
||||||
? hasPreviousOrder
|
: action === 'download'
|
||||||
? 'Download'
|
? hasPreviousOrder
|
||||||
: priceType === 'free'
|
? 'Download'
|
||||||
? 'Get'
|
: priceType === 'free'
|
||||||
: `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}`
|
? 'Get'
|
||||||
: hasPreviousOrder &&
|
: `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}`
|
||||||
hasPreviousOrderSelectedComputeAsset &&
|
: hasPreviousOrder &&
|
||||||
!hasProviderFee
|
hasPreviousOrderSelectedComputeAsset &&
|
||||||
? 'Start Compute Job'
|
!hasProviderFee
|
||||||
: priceType === 'free' && algorithmPriceType === 'free'
|
? 'Start Compute Job'
|
||||||
? 'Order Compute Job'
|
: priceType === 'free' && algorithmPriceType === 'free'
|
||||||
: `Buy Compute Job`
|
? 'Order Compute Job'
|
||||||
|
: `Buy Compute Job`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
|
@ -43,7 +43,8 @@ export default function FormStartCompute({
|
|||||||
datasetOrderPriceAndFees,
|
datasetOrderPriceAndFees,
|
||||||
algoOrderPriceAndFees,
|
algoOrderPriceAndFees,
|
||||||
providerFeeAmount,
|
providerFeeAmount,
|
||||||
validUntil
|
validUntil,
|
||||||
|
retry
|
||||||
}: {
|
}: {
|
||||||
algorithms: AssetSelectionAsset[]
|
algorithms: AssetSelectionAsset[]
|
||||||
ddoListAlgorithms: Asset[]
|
ddoListAlgorithms: Asset[]
|
||||||
@ -71,6 +72,7 @@ export default function FormStartCompute({
|
|||||||
algoOrderPriceAndFees?: OrderPriceAndFees
|
algoOrderPriceAndFees?: OrderPriceAndFees
|
||||||
providerFeeAmount?: string
|
providerFeeAmount?: string
|
||||||
validUntil?: string
|
validUntil?: string
|
||||||
|
retry: boolean
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const { siteContent } = useMarketMetadata()
|
const { siteContent } = useMarketMetadata()
|
||||||
const { accountId, balance, isSupportedOceanNetwork } = useWeb3()
|
const { accountId, balance, isSupportedOceanNetwork } = useWeb3()
|
||||||
@ -301,6 +303,7 @@ export default function FormStartCompute({
|
|||||||
}
|
}
|
||||||
isSupportedOceanNetwork={isSupportedOceanNetwork}
|
isSupportedOceanNetwork={isSupportedOceanNetwork}
|
||||||
hasProviderFee={providerFeeAmount && providerFeeAmount !== '0'}
|
hasProviderFee={providerFeeAmount && providerFeeAmount !== '0'}
|
||||||
|
retry={retry}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
@ -100,6 +100,7 @@ export default function Compute({
|
|||||||
const [refetchJobs, setRefetchJobs] = useState(false)
|
const [refetchJobs, setRefetchJobs] = useState(false)
|
||||||
const [isLoadingJobs, setIsLoadingJobs] = useState(false)
|
const [isLoadingJobs, setIsLoadingJobs] = useState(false)
|
||||||
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
|
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
|
||||||
|
const [retry, setRetry] = useState<boolean>(false)
|
||||||
|
|
||||||
const hasDatatoken = Number(dtBalance) >= 1
|
const hasDatatoken = Number(dtBalance) >= 1
|
||||||
const isComputeButtonDisabled =
|
const isComputeButtonDisabled =
|
||||||
@ -299,7 +300,8 @@ export default function Compute({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newError = error
|
const newError = error
|
||||||
if (!newError) return
|
if (!newError) return
|
||||||
toast.error(newError)
|
const errorMsg = newError + '. Please retry.'
|
||||||
|
toast.error(errorMsg)
|
||||||
}, [error])
|
}, [error])
|
||||||
|
|
||||||
async function startJob(): Promise<void> {
|
async function startJob(): Promise<void> {
|
||||||
@ -395,6 +397,7 @@ export default function Compute({
|
|||||||
initPriceAndFees()
|
initPriceAndFees()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error.message)
|
setError(error.message)
|
||||||
|
setRetry(true)
|
||||||
LoggerInstance.error(`[compute] ${error.message} `)
|
LoggerInstance.error(`[compute] ${error.message} `)
|
||||||
} finally {
|
} finally {
|
||||||
setIsOrdering(false)
|
setIsOrdering(false)
|
||||||
@ -484,6 +487,7 @@ export default function Compute({
|
|||||||
algoOrderPriceAndFees={algoOrderPriceAndFees}
|
algoOrderPriceAndFees={algoOrderPriceAndFees}
|
||||||
providerFeeAmount={providerFeeAmount}
|
providerFeeAmount={providerFeeAmount}
|
||||||
validUntil={computeValidUntil}
|
validUntil={computeValidUntil}
|
||||||
|
retry={retry}
|
||||||
/>
|
/>
|
||||||
</Formik>
|
</Formik>
|
||||||
)}
|
)}
|
||||||
|
@ -48,6 +48,7 @@ export default function Download({
|
|||||||
const [isOrderDisabled, setIsOrderDisabled] = useState(false)
|
const [isOrderDisabled, setIsOrderDisabled] = useState(false)
|
||||||
const [orderPriceAndFees, setOrderPriceAndFees] =
|
const [orderPriceAndFees, setOrderPriceAndFees] =
|
||||||
useState<OrderPriceAndFees>()
|
useState<OrderPriceAndFees>()
|
||||||
|
const [retry, setRetry] = useState<boolean>(false)
|
||||||
|
|
||||||
const isUnsupportedPricing = asset?.accessDetails?.type === 'NOT_SUPPORTED'
|
const isUnsupportedPricing = asset?.accessDetails?.type === 'NOT_SUPPORTED'
|
||||||
|
|
||||||
@ -155,9 +156,10 @@ export default function Download({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LoggerInstance.error(error)
|
LoggerInstance.error(error)
|
||||||
|
setRetry(true)
|
||||||
const message = isOwned
|
const message = isOwned
|
||||||
? 'Failed to download file!'
|
? 'Failed to download file!'
|
||||||
: 'An error occurred. Check console for more information.'
|
: 'An error occurred, please retry. Check console for more information.'
|
||||||
toast.error(message)
|
toast.error(message)
|
||||||
}
|
}
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
@ -181,6 +183,7 @@ export default function Download({
|
|||||||
isConsumable={asset.accessDetails?.isPurchasable}
|
isConsumable={asset.accessDetails?.isPurchasable}
|
||||||
isBalanceSufficient={isBalanceSufficient}
|
isBalanceSufficient={isBalanceSufficient}
|
||||||
consumableFeedback={consumableFeedback}
|
consumableFeedback={consumableFeedback}
|
||||||
|
retry={retry}
|
||||||
isSupportedOceanNetwork={isSupportedOceanNetwork}
|
isSupportedOceanNetwork={isSupportedOceanNetwork}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
.wrapper img {
|
.wrapper img {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 128px;
|
width: 128px;
|
||||||
height: 128px;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
|
@ -14,11 +14,13 @@ const openSeaTestNetworks = [4]
|
|||||||
|
|
||||||
export default function NftTooltip({
|
export default function NftTooltip({
|
||||||
nft,
|
nft,
|
||||||
|
nftImage,
|
||||||
address,
|
address,
|
||||||
chainId,
|
chainId,
|
||||||
isBlockscoutExplorer
|
isBlockscoutExplorer
|
||||||
}: {
|
}: {
|
||||||
nft: NftMetadata
|
nft: NftMetadata
|
||||||
|
nftImage: string
|
||||||
address: string
|
address: string
|
||||||
chainId: number
|
chainId: number
|
||||||
isBlockscoutExplorer: boolean
|
isBlockscoutExplorer: boolean
|
||||||
@ -39,7 +41,7 @@ export default function NftTooltip({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
{nft && <img src={nft.image_data || nft.image} alt={nft?.name} />}
|
{nftImage && <img src={nftImage} alt={nft?.name} />}
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
{nft && <h5>{nft.name}</h5>}
|
{nft && <h5>{nft.name}</h5>}
|
||||||
{address && (
|
{address && (
|
||||||
|
@ -4,14 +4,19 @@
|
|||||||
border-right: 1px solid var(--border-color);
|
border-right: 1px solid var(--border-color);
|
||||||
width: calc(var(--spacer) * 2);
|
width: calc(var(--spacer) * 2);
|
||||||
height: calc(var(--spacer) * 2);
|
height: calc(var(--spacer) * 2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nftImage img,
|
|
||||||
.nftImage > svg:first-of-type {
|
.nftImage > svg:first-of-type {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nftImage img {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.nftImage > svg:first-of-type {
|
.nftImage > svg:first-of-type {
|
||||||
transform: scale(0.7);
|
transform: scale(0.7);
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ export default function Nft({
|
|||||||
content={
|
content={
|
||||||
<NftTooltip
|
<NftTooltip
|
||||||
nft={nftMetadata}
|
nft={nftMetadata}
|
||||||
|
nftImage={nftImage}
|
||||||
address={asset?.nftAddress}
|
address={asset?.nftAddress}
|
||||||
chainId={asset?.chainId}
|
chainId={asset?.chainId}
|
||||||
isBlockscoutExplorer={isBlockscoutExplorer}
|
isBlockscoutExplorer={isBlockscoutExplorer}
|
||||||
|
@ -23,7 +23,6 @@ export default function RelatedAssets(): ReactElement {
|
|||||||
!asset?.nft ||
|
!asset?.nft ||
|
||||||
!asset?.metadata
|
!asset?.metadata
|
||||||
) {
|
) {
|
||||||
setIsLoading(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,11 +30,17 @@ export default function RelatedAssets(): ReactElement {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tagQuery = generateBaseQuery(
|
let tagResults: Asset[] = []
|
||||||
generateQuery(chainIds, asset.nftAddress, 4, asset.metadata.tags)
|
|
||||||
)
|
// safeguard against faults in the metadata
|
||||||
const tagResults = (await queryMetadata(tagQuery, newCancelToken()))
|
if (asset.metadata.tags instanceof Array) {
|
||||||
?.results
|
const tagQuery = generateBaseQuery(
|
||||||
|
generateQuery(chainIds, asset.nftAddress, 4, asset.metadata.tags)
|
||||||
|
)
|
||||||
|
|
||||||
|
tagResults = (await queryMetadata(tagQuery, newCancelToken()))
|
||||||
|
?.results
|
||||||
|
}
|
||||||
|
|
||||||
if (tagResults.length === 4) {
|
if (tagResults.length === 4) {
|
||||||
setRelatedAssets(tagResults)
|
setRelatedAssets(tagResults)
|
||||||
|
@ -124,8 +124,19 @@ export default function MarketStats(): ReactElement {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<PriceUnit price={total.veLocked} symbol="OCEAN" size="small" /> locked.{' '}
|
<PriceUnit
|
||||||
<PriceUnit price={total.veAllocated} symbol="veOCEAN" size="small" />{' '}
|
decimals="0"
|
||||||
|
price={total.veLocked}
|
||||||
|
symbol="OCEAN"
|
||||||
|
size="small"
|
||||||
|
/>{' '}
|
||||||
|
locked.{' '}
|
||||||
|
<PriceUnit
|
||||||
|
decimals="0"
|
||||||
|
price={total.veAllocated}
|
||||||
|
symbol="veOCEAN"
|
||||||
|
size="small"
|
||||||
|
/>{' '}
|
||||||
allocated.
|
allocated.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
56
src/components/Home/MostViews/index.test.tsx
Normal file
56
src/components/Home/MostViews/index.test.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import React from 'react'
|
||||||
|
import MostViews from '.'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { queryMetadata } from '@utils/aquarius'
|
||||||
|
import { assetAquarius } from '../../../../.jest/__fixtures__/assetAquarius'
|
||||||
|
|
||||||
|
jest.mock('axios')
|
||||||
|
jest.mock('@utils/aquarius')
|
||||||
|
|
||||||
|
const axiosMock = axios as jest.Mocked<typeof axios>
|
||||||
|
const queryMetadataMock = queryMetadata as jest.Mock
|
||||||
|
|
||||||
|
const queryMetadataBaseReturn: PagedAssets = {
|
||||||
|
results: [assetAquarius],
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
totalResults: 1,
|
||||||
|
aggregations: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components/Home/MostViews', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders without crashing', async () => {
|
||||||
|
axiosMock.get.mockImplementation(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: [{ count: 666, did: assetAquarius.id }]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
queryMetadataMock.mockResolvedValue(queryMetadataBaseReturn)
|
||||||
|
render(<MostViews />)
|
||||||
|
await screen.findByText('666')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('catches errors', async () => {
|
||||||
|
queryMetadataMock.mockImplementation(() => {
|
||||||
|
throw new Error('Hello error')
|
||||||
|
})
|
||||||
|
|
||||||
|
// prevent console error from showing up in test log
|
||||||
|
const originalError = console.error
|
||||||
|
console.error = jest.fn()
|
||||||
|
|
||||||
|
try {
|
||||||
|
render(<MostViews />)
|
||||||
|
await screen.findByText('No results found')
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toEqual({ message: 'Hello error' })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error = originalError
|
||||||
|
})
|
||||||
|
})
|
73
src/components/Home/MostViews/index.tsx
Normal file
73
src/components/Home/MostViews/index.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React, { ReactElement, useCallback, useEffect, useState } from 'react'
|
||||||
|
import styles from '../index.module.css'
|
||||||
|
import {
|
||||||
|
generateBaseQuery,
|
||||||
|
getFilterTerm,
|
||||||
|
queryMetadata
|
||||||
|
} from '@utils/aquarius'
|
||||||
|
import { useCancelToken } from '@hooks/useCancelToken'
|
||||||
|
import Tooltip from '@shared/atoms/Tooltip'
|
||||||
|
import AssetList from '@shared/AssetList'
|
||||||
|
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||||
|
import { sortAssets } from '@utils/index'
|
||||||
|
import axios, { AxiosResponse } from 'axios'
|
||||||
|
|
||||||
|
export default function MostViews(): ReactElement {
|
||||||
|
const [loading, setLoading] = useState<boolean>()
|
||||||
|
const [mostViewed, setMostViewed] = useState<AssetExtended[]>([])
|
||||||
|
const newCancelToken = useCancelToken()
|
||||||
|
|
||||||
|
const getMostViewed = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const response: AxiosResponse<PageViews[]> = await axios.get(
|
||||||
|
'https://market-analytics.oceanprotocol.com/pages?limit=6',
|
||||||
|
{ cancelToken: newCancelToken() }
|
||||||
|
)
|
||||||
|
const dids = response?.data?.map((x: PageViews) => x.did)
|
||||||
|
const assetsWithViews: AssetExtended[] = []
|
||||||
|
const baseParams = {
|
||||||
|
esPaginationOptions: { size: 6 },
|
||||||
|
filters: [getFilterTerm('_id', dids)]
|
||||||
|
} as BaseQueryParams
|
||||||
|
const query = generateBaseQuery(baseParams)
|
||||||
|
const result = await queryMetadata(query, newCancelToken())
|
||||||
|
|
||||||
|
if (result?.totalResults > 0) {
|
||||||
|
const sortedAssets = sortAssets(result.results, dids)
|
||||||
|
const overflow = sortedAssets.length - 6
|
||||||
|
sortedAssets.splice(sortedAssets.length - overflow, overflow)
|
||||||
|
sortedAssets.forEach((asset) => {
|
||||||
|
assetsWithViews.push({
|
||||||
|
...asset,
|
||||||
|
views: response.data.filter((x) => x.did === asset.id)?.[0]?.count
|
||||||
|
})
|
||||||
|
})
|
||||||
|
setMostViewed(assetsWithViews)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
LoggerInstance.error(error.message)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [newCancelToken])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getMostViewed()
|
||||||
|
}, [getMostViewed])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h3>
|
||||||
|
Most Views <span>last 30 days</span>
|
||||||
|
<Tooltip content="Assets from all supported chains. Not affected by your selected networks." />
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<AssetList
|
||||||
|
assets={mostViewed}
|
||||||
|
showPagination={false}
|
||||||
|
isLoading={loading}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
@ -1,29 +1,27 @@
|
|||||||
import { useUserPreferences } from '@context/UserPreferences'
|
import { useUserPreferences } from '@context/UserPreferences'
|
||||||
import { useCancelToken } from '@hooks/useCancelToken'
|
import { useCancelToken } from '@hooks/useCancelToken'
|
||||||
import { useIsMounted } from '@hooks/useIsMounted'
|
import { useIsMounted } from '@hooks/useIsMounted'
|
||||||
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||||
import AssetList from '@shared/AssetList'
|
import AssetList from '@shared/AssetList'
|
||||||
|
import Tooltip from '@shared/atoms/Tooltip'
|
||||||
|
import Markdown from '@shared/Markdown'
|
||||||
import { queryMetadata } from '@utils/aquarius'
|
import { queryMetadata } from '@utils/aquarius'
|
||||||
|
import { sortAssets } from '@utils/index'
|
||||||
import React, { ReactElement, useState, useEffect } from 'react'
|
import React, { ReactElement, useState, useEffect } from 'react'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
|
|
||||||
function sortElements(items: Asset[], sorted: string[]) {
|
|
||||||
items.sort(function (a, b) {
|
|
||||||
return sorted.indexOf(a.nftAddress) - sorted.indexOf(b.nftAddress)
|
|
||||||
})
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SectionQueryResult({
|
export default function SectionQueryResult({
|
||||||
title,
|
title,
|
||||||
query,
|
query,
|
||||||
action,
|
action,
|
||||||
queryData
|
queryData,
|
||||||
|
tooltip
|
||||||
}: {
|
}: {
|
||||||
title: ReactElement | string
|
title: ReactElement | string
|
||||||
query: SearchQuery
|
query: SearchQuery
|
||||||
action?: ReactElement
|
action?: ReactElement
|
||||||
queryData?: string[]
|
queryData?: string[]
|
||||||
|
tooltip?: string
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const { chainIds } = useUserPreferences()
|
const { chainIds } = useUserPreferences()
|
||||||
const [result, setResult] = useState<PagedAssets>()
|
const [result, setResult] = useState<PagedAssets>()
|
||||||
@ -52,7 +50,7 @@ export default function SectionQueryResult({
|
|||||||
const result = await queryMetadata(query, newCancelToken())
|
const result = await queryMetadata(query, newCancelToken())
|
||||||
if (!isMounted()) return
|
if (!isMounted()) return
|
||||||
if (queryData && result?.totalResults > 0) {
|
if (queryData && result?.totalResults > 0) {
|
||||||
const sortedAssets = sortElements(result.results, queryData)
|
const sortedAssets = sortAssets(result.results, queryData)
|
||||||
const overflow = sortedAssets.length - 6
|
const overflow = sortedAssets.length - 6
|
||||||
sortedAssets.splice(sortedAssets.length - overflow, overflow)
|
sortedAssets.splice(sortedAssets.length - overflow, overflow)
|
||||||
result.results = sortedAssets
|
result.results = sortedAssets
|
||||||
@ -69,7 +67,9 @@ export default function SectionQueryResult({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<h3>{title}</h3>
|
<h3>
|
||||||
|
{title} {tooltip && <Tooltip content={<Markdown text={tooltip} />} />}
|
||||||
|
</h3>
|
||||||
|
|
||||||
<AssetList
|
<AssetList
|
||||||
assets={result?.results}
|
assets={result?.results}
|
||||||
|
@ -13,6 +13,13 @@
|
|||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section h3 span {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
color: var(--color-secondary);
|
||||||
|
font-family: var(--font-family-base);
|
||||||
|
font-weight: var(--font-weight-base);
|
||||||
|
}
|
||||||
|
|
||||||
.section [class*='button'] {
|
.section [class*='button'] {
|
||||||
margin-top: var(--spacer);
|
margin-top: var(--spacer);
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,14 @@ import TopTags from './TopTags'
|
|||||||
import SectionQueryResult from './SectionQueryResult'
|
import SectionQueryResult from './SectionQueryResult'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
import Allocations from './Allocations'
|
import Allocations from './Allocations'
|
||||||
|
import MostViews from './MostViews'
|
||||||
|
|
||||||
export default function HomePage(): ReactElement {
|
export default function HomePage(): ReactElement {
|
||||||
const { chainIds } = useUserPreferences()
|
const { chainIds } = useUserPreferences()
|
||||||
|
|
||||||
const [queryLatest, setQueryLatest] = useState<SearchQuery>()
|
const [queryLatest, setQueryLatest] = useState<SearchQuery>()
|
||||||
const [queryMostSales, setQueryMostSales] = useState<SearchQuery>()
|
const [queryMostSales, setQueryMostSales] = useState<SearchQuery>()
|
||||||
|
|
||||||
const [queryMostAllocation, setQueryMostAllocation] = useState<SearchQuery>()
|
const [queryMostAllocation, setQueryMostAllocation] = useState<SearchQuery>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -66,7 +68,7 @@ export default function HomePage(): ReactElement {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SectionQueryResult title="Most Sales" query={queryMostSales} />
|
<SectionQueryResult title="Most Sales" query={queryMostSales} />
|
||||||
|
<MostViews />
|
||||||
<TopSales title="Publishers With Most Sales" />
|
<TopSales title="Publishers With Most Sales" />
|
||||||
<TopTags title="Top Tags By Sales" />
|
<TopTags title="Top Tags By Sales" />
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user