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',
|
||||
nftAddress: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
|
||||
version: '4.1.0',
|
||||
chainId: 5,
|
||||
chainId: 1,
|
||||
metadata: {
|
||||
created: '2022-09-29T11:30:26Z',
|
||||
updated: '2022-09-29T11:30:26Z',
|
||||
|
@ -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
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 {
|
||||
interface AssetExtended extends Asset {
|
||||
accessDetails?: AccessDetails
|
||||
views?: number
|
||||
}
|
||||
}
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 } })
|
||||
)
|
||||
|
||||
|
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 { 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}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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" />
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user