mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Add most viewed (#1754)
* add most viewed * Update src/components/Home/index.tsx Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com> * add views in teasers * typo * add test * switch to axios * test tweaks * fix * add 30 days label Co-authored-by: Jamie Hewitt <jamie.hewitt15@gmail.com> Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
This commit is contained in:
parent
81341cd914
commit
c302122795
@ -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',
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -89,6 +89,13 @@ export default function AssetTeaser({
|
|||||||
: `${orders} ${orders === 1 ? 'sale' : 'sales'}`}
|
: `${orders} ${orders === 1 ? 'sale' : 'sales'}`}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
{asset.views && asset.views > 0 ? (
|
||||||
|
<span className={styles.typeLabel}>
|
||||||
|
{asset.views < 0
|
||||||
|
? 'N/A'
|
||||||
|
: `${asset.views} ${asset.views === 1 ? 'view' : 'views'}`}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</footer>
|
</footer>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -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 } })
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 views')
|
||||||
|
})
|
||||||
|
|
||||||
|
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