1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

Merge branch 'main' into feature/enforce-docker-containers

This commit is contained in:
Bogdan Fazakas 2022-10-10 13:23:44 +03:00
commit 7eb3f2f479
18 changed files with 331 additions and 216 deletions

14
package-lock.json generated
View File

@ -13,7 +13,7 @@
"@coingecko/cryptoformat": "^0.5.4",
"@loadable/component": "^5.15.2",
"@oceanprotocol/art": "^3.2.0",
"@oceanprotocol/lib": "^2.1.1",
"@oceanprotocol/lib": "^2.2.1",
"@oceanprotocol/typographies": "^0.1.0",
"@oceanprotocol/use-dark-mode": "^2.4.3",
"@tippyjs/react": "^4.2.6",
@ -4539,9 +4539,9 @@
"integrity": "sha512-Oe+oBRiu1dlco9PQ7eUYcTYi2Nua69S3TiSw62H46AIpwnFK8ORuO0Ny20No++KisBA9F+84b5lDn6kQy5Lt/Q=="
},
"node_modules/@oceanprotocol/lib": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-2.1.1.tgz",
"integrity": "sha512-N7NKnwVujJDn2X9MwxFu15x8VvTVEDqWuIZFY4s3NG0NbwXGEHptewKlAVhOkvm6jOGuCN3NXWqRUTvMFOWGbQ==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-2.2.1.tgz",
"integrity": "sha512-HNmT3DJJeyvFRwCbmgJucGpte90epIhgSy+68PSc83TLKRW2CF4N1mioMkoGxMwnK3rJzj6tEy4R9NKKLbdT5w==",
"dependencies": {
"@oceanprotocol/contracts": "^1.1.7",
"bignumber.js": "^9.1.0",
@ -44803,9 +44803,9 @@
"integrity": "sha512-Oe+oBRiu1dlco9PQ7eUYcTYi2Nua69S3TiSw62H46AIpwnFK8ORuO0Ny20No++KisBA9F+84b5lDn6kQy5Lt/Q=="
},
"@oceanprotocol/lib": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-2.1.1.tgz",
"integrity": "sha512-N7NKnwVujJDn2X9MwxFu15x8VvTVEDqWuIZFY4s3NG0NbwXGEHptewKlAVhOkvm6jOGuCN3NXWqRUTvMFOWGbQ==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-2.2.1.tgz",
"integrity": "sha512-HNmT3DJJeyvFRwCbmgJucGpte90epIhgSy+68PSc83TLKRW2CF4N1mioMkoGxMwnK3rJzj6tEy4R9NKKLbdT5w==",
"requires": {
"@oceanprotocol/contracts": "^1.1.7",
"bignumber.js": "^9.1.0",

View File

@ -26,7 +26,7 @@
"@coingecko/cryptoformat": "^0.5.4",
"@loadable/component": "^5.15.2",
"@oceanprotocol/art": "^3.2.0",
"@oceanprotocol/lib": "^2.1.1",
"@oceanprotocol/lib": "^2.2.1",
"@oceanprotocol/typographies": "^0.1.0",
"@oceanprotocol/use-dark-mode": "^2.4.3",
"@tippyjs/react": "^4.2.6",

View File

@ -6,7 +6,8 @@ export enum SortDirectionOptions {
export enum SortTermOptions {
Created = 'nft.created',
Relevance = '_score',
Stats = 'stats.orders'
Stats = 'stats.orders',
Allocated = 'stats.allocated'
}
// Note: could not figure out how to get `enum` to be ambiant

View File

@ -53,7 +53,9 @@ export function generateBaseQuery(
...baseQueryParams.nestedQuery,
filter: [
...(baseQueryParams.filters || []),
getFilterTerm('chainId', baseQueryParams.chainIds),
baseQueryParams.chainIds
? getFilterTerm('chainId', baseQueryParams.chainIds)
: [],
getFilterTerm('_index', 'aquarius'),
...(baseQueryParams.ignorePurgatory
? []

View File

@ -0,0 +1,45 @@
import { AllLocked } from 'src/@types/subgraph/AllLocked'
import { gql, OperationResult } from 'urql'
import { fetchData, getQueryContext } from './subgraph'
import axios from 'axios'
const AllLocked = gql`
query AllLocked {
veOCEANs(first: 1000) {
lockedAmount
}
}
`
interface TotalVe {
totalLocked: number
totalAllocated: number
}
export async function getTotalAllocatedAndLocked(): Promise<TotalVe> {
const totals = {
totalLocked: 0,
totalAllocated: 0
}
const queryContext = getQueryContext(1)
const response = await axios.post(`https://df-sql.oceandao.org/nftinfo`)
totals.totalAllocated = response.data?.reduce(
(previousValue: number, currentValue: { ve_allocated: any }) =>
previousValue + Number(currentValue.ve_allocated),
0
)
const fetchedLocked: OperationResult<AllLocked, any> = await fetchData(
AllLocked,
null,
queryContext
)
totals.totalLocked = fetchedLocked.data?.veOCEANs.reduce(
(previousValue, currentValue) =>
previousValue + Number(currentValue.lockedAmount),
0
)
return totals
}

View File

@ -1,4 +1,4 @@
import AssetTeaser from '@shared/AssetTeaser/AssetTeaser'
import AssetTeaser from '@shared/AssetTeaser'
import React, { ReactElement, useEffect, useState } from 'react'
import Pagination from '@shared/Pagination'
import styles from './index.module.css'

View File

@ -1,62 +0,0 @@
import React, { ReactElement } from 'react'
import Link from 'next/link'
import Dotdotdot from 'react-dotdotdot'
import Price from '@shared/Price'
import removeMarkdown from 'remove-markdown'
import Publisher from '@shared/Publisher'
import AssetType from '@shared/AssetType'
import NetworkName from '@shared/NetworkName'
import styles from './AssetTeaser.module.css'
import { getServiceByName } from '@utils/ddo'
declare type AssetTeaserProps = {
asset: AssetExtended
noPublisher?: boolean
}
export default function AssetTeaser({
asset,
noPublisher
}: AssetTeaserProps): ReactElement {
const { name, type, description } = asset.metadata
const { datatokens } = asset
const isCompute = Boolean(getServiceByName(asset, 'compute'))
const accessType = isCompute ? 'compute' : 'access'
const { owner } = asset.nft
const { orders } = asset.stats
return (
<article className={`${styles.teaser} ${styles[type]}`}>
<Link href={`/asset/${asset.id}`}>
<a className={styles.link}>
<header className={styles.header}>
<div className={styles.symbol}>{datatokens[0]?.symbol}</div>
<Dotdotdot tagName="h1" clamp={3} className={styles.title}>
{name.slice(0, 200)}
</Dotdotdot>
{!noPublisher && (
<Publisher account={owner} minimal className={styles.publisher} />
)}
</header>
<AssetType
type={type}
accessType={accessType}
className={styles.typeDetails}
totalSales={orders}
/>
<div className={styles.content}>
<Dotdotdot tagName="p" clamp={3}>
{removeMarkdown(description?.substring(0, 300) || '')}
</Dotdotdot>
</div>
<footer className={styles.foot}>
<Price accessDetails={asset.accessDetails} size="small" />
<NetworkName networkId={asset.chainId} className={styles.network} />
</footer>
</a>
</Link>
</article>
)
}

View File

@ -9,6 +9,8 @@
height: 100%;
color: var(--color-secondary);
position: relative;
padding-top: calc(var(--spacer) / 2);
padding-bottom: calc(var(--spacer) / 2);
/* for sticking footer to bottom */
display: flex;
flex-direction: column;
@ -18,8 +20,12 @@
background-color: var(--background-body);
}
.detailLine {
margin-bottom: calc(var(--spacer) / 2);
}
.content {
margin-top: calc(var(--spacer) / 2);
margin-top: calc(var(--spacer) / 3);
overflow-wrap: break-word;
hyphens: auto;
/* for sticking footer to bottom */
@ -27,7 +33,7 @@
}
.content p {
margin-bottom: calc(var(--spacer) / 4);
margin-bottom: calc(var(--spacer) / 3);
}
.title {
@ -37,36 +43,20 @@
overflow-wrap: break-word;
}
.publisher {
display: block;
}
.foot {
.footer {
margin-top: calc(var(--spacer) / 4);
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.foot p {
margin: 0;
}
.symbol {
display: block;
}
.typeDetails {
position: absolute;
top: calc(var(--spacer) / 3);
right: calc(var(--spacer) / 3);
width: auto;
.typeLabel {
font-size: var(--font-size-mini);
display: inline-block;
border-left: 1px solid var(--border-color);
padding-left: calc(var(--spacer) / 3.5);
margin-left: calc(var(--spacer) / 4);
}
.network {
font-size: var(--font-size-mini);
position: absolute;
right: calc(var(--spacer) / 3);
bottom: calc(var(--spacer) / 3);
.typeLabel:first-child {
border-left: none;
padding-left: 0;
margin-left: 0;
}

View File

@ -0,0 +1,84 @@
import React, { ReactElement } from 'react'
import Link from 'next/link'
import Dotdotdot from 'react-dotdotdot'
import Price from '@shared/Price'
import removeMarkdown from 'remove-markdown'
import Publisher from '@shared/Publisher'
import AssetType from '@shared/AssetType'
import NetworkName from '@shared/NetworkName'
import styles from './index.module.css'
import { getServiceByName } from '@utils/ddo'
import { formatPrice } from '@shared/Price/PriceUnit'
import { useUserPreferences } from '@context/UserPreferences'
declare type AssetTeaserProps = {
asset: AssetExtended
noPublisher?: boolean
}
export default function AssetTeaser({
asset,
noPublisher
}: AssetTeaserProps): ReactElement {
const { name, type, description } = asset.metadata
const { datatokens } = asset
const isCompute = Boolean(getServiceByName(asset, 'compute'))
const accessType = isCompute ? 'compute' : 'access'
const { owner } = asset.nft
const { orders, allocated } = asset.stats
const isUnsupportedPricing = asset?.accessDetails?.type === 'NOT_SUPPORTED'
const { locale } = useUserPreferences()
return (
<article className={`${styles.teaser} ${styles[type]}`}>
<Link href={`/asset/${asset.id}`}>
<a className={styles.link}>
<aside className={styles.detailLine}>
<AssetType
className={styles.typeLabel}
type={type}
accessType={accessType}
/>
<span className={styles.typeLabel}>{datatokens[0]?.symbol}</span>
<NetworkName
networkId={asset.chainId}
className={styles.typeLabel}
/>
</aside>
<header className={styles.header}>
<Dotdotdot tagName="h1" clamp={3} className={styles.title}>
{name.slice(0, 200)}
</Dotdotdot>
{!noPublisher && <Publisher account={owner} minimal />}
</header>
<div className={styles.content}>
<Dotdotdot tagName="p" clamp={3}>
{removeMarkdown(description?.substring(0, 300) || '')}
</Dotdotdot>
</div>
{isUnsupportedPricing ? (
<strong>No pricing schema available</strong>
) : (
<Price accessDetails={asset.accessDetails} size="small" />
)}
<footer className={styles.footer}>
{allocated && allocated > 0 ? (
<span className={styles.typeLabel}>
{allocated < 0
? ''
: `${formatPrice(allocated, locale)} veOCEAN`}
</span>
) : null}
{orders && orders > 0 ? (
<span className={styles.typeLabel}>
{orders < 0
? 'N/A'
: `${orders} ${orders === 1 ? 'sale' : 'sales'}`}
</span>
) : null}
</footer>
</a>
</Link>
</article>
)
}

View File

@ -7,13 +7,11 @@ import Lock from '@images/lock.svg'
export default function AssetType({
type,
accessType,
className,
totalSales
className
}: {
type: string
accessType: string
className?: string
totalSales?: number
}): ReactElement {
return (
<div className={className || null}>
@ -28,14 +26,6 @@ export default function AssetType({
<div className={styles.typeLabel}>
{type === 'dataset' ? 'dataset' : 'algorithm'}
</div>
{(totalSales || totalSales === 0) && (
<div className={styles.typeLabel}>
{totalSales < 0
? 'N/A'
: `${totalSales} ${totalSales === 1 ? 'sale' : 'sales'}`}
</div>
)}
</div>
)
}

View File

@ -20,6 +20,18 @@
}
}
.stat {
border-left: 1px solid var(--border-color);
padding-left: calc(var(--spacer) / 3.5);
margin-left: calc(var(--spacer) / 4);
}
.stat:first-child {
border-left: none;
padding-left: 0;
margin-left: 0;
}
.number {
font-weight: var(--font-weight-bold);
color: var(--font-color-heading);

View File

@ -1,21 +1,43 @@
import { useAsset } from '@context/Asset'
import React from 'react'
import { useUserPreferences } from '@context/UserPreferences'
import { formatPrice } from '@shared/Price/PriceUnit'
import React, { useEffect, useState } from 'react'
import styles from './index.module.css'
export default function AssetStats() {
const { locale } = useUserPreferences()
const { asset } = useAsset()
const [orders, setOrders] = useState(0)
const [allocated, setAllocated] = useState(0)
useEffect(() => {
if (!asset) return
const { orders, allocated } = asset.stats
setOrders(orders)
setAllocated(allocated)
}, [asset])
return (
<footer className={styles.stats}>
{!asset || !asset?.stats || asset?.stats?.orders < 0 ? (
{allocated && allocated > 0 ? (
<span className={styles.stat}>
<span className={styles.number}>
{formatPrice(allocated, locale)}
</span>
veOCEAN
</span>
) : null}
{!asset || !asset?.stats || orders < 0 ? (
'N/A'
) : asset?.stats?.orders === 0 ? (
) : orders === 0 ? (
'No sales yet'
) : (
<>
<span className={styles.number}>{asset.stats.orders}</span> sale
{asset.stats.orders === 1 ? '' : 's'}
</>
<span className={styles.stat}>
<span className={styles.number}>{orders}</span> sale
{orders === 1 ? '' : 's'}
</span>
)}
</footer>
)

View File

@ -1,3 +1,4 @@
import PriceUnit from '@shared/Price/PriceUnit'
import React, { ReactElement } from 'react'
import { StatsTotal } from './_types'
@ -8,9 +9,12 @@ export default function MarketStatsTotal({
}): ReactElement {
return (
<>
<strong>{total.orders}</strong> orders across{' '}
<strong>{total.nfts}</strong> assets with{' '}
<strong>{total.datatokens}</strong> different datatokens.
<PriceUnit price={total.orders} size="small" /> orders across{' '}
<PriceUnit price={total.nfts} size="small" /> assets with{' '}
<PriceUnit price={total.datatokens} size="small" /> different datatokens.{' '}
<PriceUnit price={total.veAllocated} symbol="veOCEAN" size="small" />{' '}
allocated.{' '}
<PriceUnit price={total.veLocked} symbol="OCEAN" size="small" /> locked.
</>
)
}

View File

@ -6,4 +6,6 @@ export interface StatsTotal {
nfts: number
datatokens: number
orders: number
veAllocated: number
veLocked: number
}

View File

@ -14,11 +14,14 @@ import { useMarketMetadata } from '@context/MarketMetadata'
import Tooltip from '@shared/atoms/Tooltip'
import Markdown from '@shared/Markdown'
import content from '../../../../content/footer.json'
import { getTotalAllocatedAndLocked } from '@utils/veAllocation'
const initialTotal: StatsTotal = {
nfts: 0,
datatokens: 0,
orders: 0
orders: 0,
veAllocated: 0,
veLocked: 0
}
export default function MarketStats(): ReactElement {
@ -34,15 +37,15 @@ export default function MarketStats(): ReactElement {
// Set the main chain ids we want to display stats for
//
useEffect(() => {
if (!networksList || !appConfig || !appConfig?.chainIdsSupported) return
if (!networksList || !appConfig || !appConfig?.chainIds) return
const mainChainIdsList = filterNetworksByType(
'mainnet',
appConfig.chainIdsSupported,
appConfig.chainIds,
networksList
)
setMainChainIds(mainChainIdsList)
}, [appConfig, appConfig?.chainIdsSupported, networksList])
}, [appConfig, appConfig?.chainIds, networksList])
//
// Helper: fetch data from subgraph
@ -68,6 +71,12 @@ export default function MarketStats(): ReactElement {
LoggerInstance.error('Error fetching global stats: ', error.message)
}
}
const veTotals = await getTotalAllocatedAndLocked()
total.veAllocated = veTotals.totalAllocated
total.veLocked = veTotals.totalLocked
setTotal(total)
setData(newData)
}, [mainChainIds])
@ -83,9 +92,7 @@ export default function MarketStats(): ReactElement {
//
useEffect(() => {
if (!data || !mainChainIds?.length) return
const newTotal: StatsTotal = {
...initialTotal // always start calculating beginning from initial 0 values
}
const newTotal: StatsTotal = total
for (const chainId of mainChainIds) {
try {

View File

@ -0,0 +1,83 @@
import { useUserPreferences } from '@context/UserPreferences'
import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted'
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import AssetList from '@shared/AssetList'
import { queryMetadata } from '@utils/aquarius'
import React, { ReactElement, useState, useEffect } from 'react'
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({
title,
query,
action,
queryData
}: {
title: ReactElement | string
query: SearchQuery
action?: ReactElement
queryData?: string[]
}): ReactElement {
const { chainIds } = useUserPreferences()
const [result, setResult] = useState<PagedAssets>()
const [loading, setLoading] = useState<boolean>()
const isMounted = useIsMounted()
const newCancelToken = useCancelToken()
useEffect(() => {
if (!query) return
async function init() {
if (chainIds.length === 0) {
const result: PagedAssets = {
results: [],
page: 0,
totalPages: 0,
totalResults: 0,
aggregations: undefined
}
setResult(result)
setLoading(false)
} else {
try {
setLoading(true)
const result = await queryMetadata(query, newCancelToken())
if (!isMounted()) return
if (queryData && result?.totalResults > 0) {
const sortedAssets = sortElements(result.results, queryData)
const overflow = sortedAssets.length - 6
sortedAssets.splice(sortedAssets.length - overflow, overflow)
result.results = sortedAssets
}
setResult(result)
setLoading(false)
} catch (error) {
LoggerInstance.error(error.message)
}
}
}
init()
}, [chainIds.length, isMounted, newCancelToken, query, queryData])
return (
<section className={styles.section}>
<h3>{title}</h3>
<AssetList
assets={result?.results}
showPagination={false}
isLoading={loading || !query}
/>
{action && action}
</section>
)
}

View File

@ -1,103 +1,24 @@
import React, { ReactElement, useEffect, useState } from 'react'
import AssetList from '@shared/AssetList'
import Button from '@shared/atoms/Button'
import Bookmarks from './Bookmarks'
import { generateBaseQuery, queryMetadata } from '@utils/aquarius'
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import { generateBaseQuery } from '@utils/aquarius'
import { useUserPreferences } from '@context/UserPreferences'
import { useIsMounted } from '@hooks/useIsMounted'
import { useCancelToken } from '@hooks/useCancelToken'
import { SortTermOptions } from '../../@types/aquarius/SearchQuery'
import TopSales from './TopSales'
import styles from './index.module.css'
function sortElements(items: Asset[], sorted: string[]) {
items.sort(function (a, b) {
return (
sorted.indexOf(a.services[0].datatokenAddress.toLowerCase()) -
sorted.indexOf(b.services[0].datatokenAddress.toLowerCase())
)
})
return items
}
function SectionQueryResult({
title,
query,
action,
queryData
}: {
title: ReactElement | string
query: SearchQuery
action?: ReactElement
queryData?: string[]
}) {
const { chainIds } = useUserPreferences()
const [result, setResult] = useState<PagedAssets>()
const [loading, setLoading] = useState<boolean>()
const isMounted = useIsMounted()
const newCancelToken = useCancelToken()
useEffect(() => {
if (!query) return
async function init() {
if (chainIds.length === 0) {
const result: PagedAssets = {
results: [],
page: 0,
totalPages: 0,
totalResults: 0,
aggregations: undefined
}
setResult(result)
setLoading(false)
} else {
try {
setLoading(true)
const result = await queryMetadata(query, newCancelToken())
if (!isMounted()) return
if (queryData && result?.totalResults > 0) {
const sortedAssets = sortElements(result.results, queryData)
const overflow = sortedAssets.length - 9
sortedAssets.splice(sortedAssets.length - overflow, overflow)
result.results = sortedAssets
}
setResult(result)
setLoading(false)
} catch (error) {
LoggerInstance.error(error.message)
}
}
}
init()
}, [chainIds.length, isMounted, newCancelToken, query, queryData])
return (
<section className={styles.section}>
<h3>{title}</h3>
<AssetList
assets={result?.results}
showPagination={false}
isLoading={loading || !query}
/>
{action && action}
</section>
)
}
import SectionQueryResult from './SectionQueryResult'
export default function HomePage(): ReactElement {
const [queryLatest, setQueryLatest] = useState<SearchQuery>()
const [queryMostSales, setQueryMostSales] = useState<SearchQuery>()
const [queryMostAllocation, setQueryMostAllocation] = useState<SearchQuery>()
const { chainIds } = useUserPreferences()
useEffect(() => {
const baseParams = {
chainIds,
esPaginationOptions: {
size: 9
size: 6
},
sortOptions: {
sortBy: SortTermOptions.Created
@ -108,13 +29,23 @@ export default function HomePage(): ReactElement {
const baseParamsSales = {
chainIds,
esPaginationOptions: {
size: 9
size: 6
},
sortOptions: {
sortBy: SortTermOptions.Stats
} as SortOptions
} as BaseQueryParams
setQueryMostSales(generateBaseQuery(baseParamsSales))
const baseParamsAllocation = {
chainIds,
esPaginationOptions: {
size: 6
},
sortOptions: {
sortBy: SortTermOptions.Allocated
} as SortOptions
} as BaseQueryParams
setQueryMostAllocation(generateBaseQuery(baseParamsAllocation))
}, [chainIds])
return (
@ -124,8 +55,15 @@ export default function HomePage(): ReactElement {
<Bookmarks />
</section>
<SectionQueryResult
title="Highest veOCEAN Allocations"
query={queryMostAllocation}
/>
<SectionQueryResult title="Most Sales" query={queryMostSales} />
<TopSales title="Publishers With Most Sales" />
<SectionQueryResult
title="Recently Published"
query={queryLatest}
@ -135,8 +73,6 @@ export default function HomePage(): ReactElement {
</Button>
}
/>
<TopSales title="Publishers With Most Sales" />
</>
)
}

View File

@ -26,7 +26,6 @@ export default function AvailableNetworks(): ReactElement {
{ title: 'Main', data: networksMain },
{ title: 'Test', data: networksTest }
]
const networkList = (networks: number[]) =>
networks.map((chainId) => (
<li key={chainId}>