1
0
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:
mihaisc 2022-11-08 19:17:22 +02:00 committed by GitHub
parent 81341cd914
commit c302122795
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 184 additions and 22 deletions

View File

@ -5,7 +5,7 @@ export const assetAquarius: Asset = {
id: 'did:op:6654b0793765b269696cec8d2f0d077d9bbcdd3c4f033d941ab9684e8ad06630',
nftAddress: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
version: '4.1.0',
chainId: 5,
chainId: 1,
metadata: {
created: '2022-09-29T11:30:26Z',
updated: '2022-09-29T11:30:26Z',

View File

@ -1,10 +1,12 @@
import { useRef, useEffect, useCallback } from 'react'
import axios, { CancelToken } from 'axios'
export const useCancelToken = (): (() => CancelToken) => {
const axiosSource = useRef(null)
const newCancelToken = useCallback(() => {
axiosSource.current = axios.CancelToken.source()
return axiosSource.current.token
return axiosSource?.current?.token
}, [])
useEffect(

4
src/@types/Analytics.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
interface PageViews {
count: number
did: string
}

View File

@ -5,5 +5,6 @@ import { Asset } from '@oceanprotocol/lib'
declare global {
interface AssetExtended extends Asset {
accessDetails?: AccessDetails
views?: number
}
}

View File

@ -55,13 +55,13 @@ export function generateBaseQuery(
...(baseQueryParams.filters || []),
baseQueryParams.chainIds
? getFilterTerm('chainId', baseQueryParams.chainIds)
: [],
: '',
getFilterTerm('_index', 'aquarius'),
...(baseQueryParams.ignorePurgatory
? []
? ''
: [getFilterTerm('purgatory.state', false)]),
...(baseQueryParams.ignoreState
? []
? ''
: [
{
bool: {

View File

@ -1,3 +1,5 @@
import { Asset } from '@oceanprotocol/lib'
// Boolean value that will be true if we are inside a browser, false otherwise
export const isBrowser = typeof window !== 'undefined'
@ -14,3 +16,10 @@ export function removeItemFromArray<T>(arr: Array<T>, value: T): Array<T> {
}
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
}

View File

@ -89,6 +89,13 @@ export default function AssetTeaser({
: `${orders} ${orders === 1 ? 'sale' : 'sales'}`}
</span>
) : 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>
</a>
</Link>

View File

@ -1,15 +1,16 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import * as axios from 'axios'
import axios from 'axios'
import Publisher from './'
const account = '0x0000000000000000000000000000000000000000'
jest.mock('axios')
const axiosMock = axios as jest.Mocked<typeof axios>
describe('@shared/Publisher', () => {
test('should return correct markup by default', async () => {
;(axios as any).get.mockImplementationOnce(() =>
axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: 'jellymcjellyfish.eth' } })
)
@ -22,7 +23,7 @@ describe('@shared/Publisher', () => {
})
test('should truncate account by default', async () => {
;(axios as any).get.mockImplementationOnce(() =>
axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: null } })
)
@ -33,7 +34,7 @@ describe('@shared/Publisher', () => {
})
test('should return correct markup in minimal state', async () => {
;(axios as any).get.mockImplementationOnce(() =>
axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: null } })
)
@ -44,7 +45,7 @@ describe('@shared/Publisher', () => {
})
test('should return markup with empty account', async () => {
;(axios as any).get.mockImplementationOnce(() =>
axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: null } })
)

View 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
})
})

View 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>
)
}

View File

@ -1,29 +1,27 @@
import { useUserPreferences } from '@context/UserPreferences'
import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted'
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import { LoggerInstance } from '@oceanprotocol/lib'
import AssetList from '@shared/AssetList'
import Tooltip from '@shared/atoms/Tooltip'
import Markdown from '@shared/Markdown'
import { queryMetadata } from '@utils/aquarius'
import { sortAssets } from '@utils/index'
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
queryData,
tooltip
}: {
title: ReactElement | string
query: SearchQuery
action?: ReactElement
queryData?: string[]
tooltip?: string
}): ReactElement {
const { chainIds } = useUserPreferences()
const [result, setResult] = useState<PagedAssets>()
@ -52,7 +50,7 @@ export default function SectionQueryResult({
const result = await queryMetadata(query, newCancelToken())
if (!isMounted()) return
if (queryData && result?.totalResults > 0) {
const sortedAssets = sortElements(result.results, queryData)
const sortedAssets = sortAssets(result.results, queryData)
const overflow = sortedAssets.length - 6
sortedAssets.splice(sortedAssets.length - overflow, overflow)
result.results = sortedAssets
@ -69,7 +67,9 @@ export default function SectionQueryResult({
return (
<section className={styles.section}>
<h3>{title}</h3>
<h3>
{title} {tooltip && <Tooltip content={<Markdown text={tooltip} />} />}
</h3>
<AssetList
assets={result?.results}

View File

@ -13,6 +13,13 @@
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'] {
margin-top: var(--spacer);
}

View File

@ -9,12 +9,14 @@ import TopTags from './TopTags'
import SectionQueryResult from './SectionQueryResult'
import styles from './index.module.css'
import Allocations from './Allocations'
import MostViews from './MostViews'
export default function HomePage(): ReactElement {
const { chainIds } = useUserPreferences()
const [queryLatest, setQueryLatest] = useState<SearchQuery>()
const [queryMostSales, setQueryMostSales] = useState<SearchQuery>()
const [queryMostAllocation, setQueryMostAllocation] = useState<SearchQuery>()
useEffect(() => {
@ -66,7 +68,7 @@ export default function HomePage(): ReactElement {
/>
<SectionQueryResult title="Most Sales" query={queryMostSales} />
<MostViews />
<TopSales title="Publishers With Most Sales" />
<TopTags title="Top Tags By Sales" />