From c30212279505c8a3b36fd27d9c5dd26d3171267f Mon Sep 17 00:00:00 2001 From: mihaisc Date: Tue, 8 Nov 2022 19:17:22 +0200 Subject: [PATCH] Add most viewed (#1754) * add most viewed * Update src/components/Home/index.tsx Co-authored-by: Jamie Hewitt * add views in teasers * typo * add test * switch to axios * test tweaks * fix * add 30 days label Co-authored-by: Jamie Hewitt Co-authored-by: Matthias Kretschmann --- .jest/__fixtures__/assetAquarius.ts | 2 +- src/@hooks/useCancelToken.ts | 4 +- src/@types/Analytics.d.ts | 4 + src/@types/AssetExtended.d.ts | 1 + src/@utils/aquarius.ts | 6 +- src/@utils/index.ts | 9 +++ src/components/@shared/AssetTeaser/index.tsx | 7 ++ .../@shared/Publisher/index.test.tsx | 11 +-- src/components/Home/MostViews/index.test.tsx | 56 ++++++++++++++ src/components/Home/MostViews/index.tsx | 73 +++++++++++++++++++ src/components/Home/SectionQueryResult.tsx | 22 +++--- src/components/Home/index.module.css | 7 ++ src/components/Home/index.tsx | 4 +- 13 files changed, 184 insertions(+), 22 deletions(-) create mode 100644 src/@types/Analytics.d.ts create mode 100644 src/components/Home/MostViews/index.test.tsx create mode 100644 src/components/Home/MostViews/index.tsx diff --git a/.jest/__fixtures__/assetAquarius.ts b/.jest/__fixtures__/assetAquarius.ts index 1f25cda19..2e892b14f 100644 --- a/.jest/__fixtures__/assetAquarius.ts +++ b/.jest/__fixtures__/assetAquarius.ts @@ -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', diff --git a/src/@hooks/useCancelToken.ts b/src/@hooks/useCancelToken.ts index 35eb3d0a9..4875c2070 100644 --- a/src/@hooks/useCancelToken.ts +++ b/src/@hooks/useCancelToken.ts @@ -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( diff --git a/src/@types/Analytics.d.ts b/src/@types/Analytics.d.ts new file mode 100644 index 000000000..746ccef6d --- /dev/null +++ b/src/@types/Analytics.d.ts @@ -0,0 +1,4 @@ +interface PageViews { + count: number + did: string +} diff --git a/src/@types/AssetExtended.d.ts b/src/@types/AssetExtended.d.ts index 19a472e44..d6ee6ade1 100644 --- a/src/@types/AssetExtended.d.ts +++ b/src/@types/AssetExtended.d.ts @@ -5,5 +5,6 @@ import { Asset } from '@oceanprotocol/lib' declare global { interface AssetExtended extends Asset { accessDetails?: AccessDetails + views?: number } } diff --git a/src/@utils/aquarius.ts b/src/@utils/aquarius.ts index 96985125a..111851aaa 100644 --- a/src/@utils/aquarius.ts +++ b/src/@utils/aquarius.ts @@ -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: { diff --git a/src/@utils/index.ts b/src/@utils/index.ts index a07ba65fe..32249f4c5 100644 --- a/src/@utils/index.ts +++ b/src/@utils/index.ts @@ -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(arr: Array, value: T): Array { } 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 +} diff --git a/src/components/@shared/AssetTeaser/index.tsx b/src/components/@shared/AssetTeaser/index.tsx index f9d61504e..1b8b0d5ee 100644 --- a/src/components/@shared/AssetTeaser/index.tsx +++ b/src/components/@shared/AssetTeaser/index.tsx @@ -89,6 +89,13 @@ export default function AssetTeaser({ : `${orders} ${orders === 1 ? 'sale' : 'sales'}`} ) : null} + {asset.views && asset.views > 0 ? ( + + {asset.views < 0 + ? 'N/A' + : `${asset.views} ${asset.views === 1 ? 'view' : 'views'}`} + + ) : null} diff --git a/src/components/@shared/Publisher/index.test.tsx b/src/components/@shared/Publisher/index.test.tsx index 33ead400c..45641eba7 100644 --- a/src/components/@shared/Publisher/index.test.tsx +++ b/src/components/@shared/Publisher/index.test.tsx @@ -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 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 } }) ) diff --git a/src/components/Home/MostViews/index.test.tsx b/src/components/Home/MostViews/index.test.tsx new file mode 100644 index 000000000..8c61a3787 --- /dev/null +++ b/src/components/Home/MostViews/index.test.tsx @@ -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 +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() + 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() + await screen.findByText('No results found') + } catch (error) { + expect(error).toEqual({ message: 'Hello error' }) + } + + console.error = originalError + }) +}) diff --git a/src/components/Home/MostViews/index.tsx b/src/components/Home/MostViews/index.tsx new file mode 100644 index 000000000..08e711e7e --- /dev/null +++ b/src/components/Home/MostViews/index.tsx @@ -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() + const [mostViewed, setMostViewed] = useState([]) + const newCancelToken = useCancelToken() + + const getMostViewed = useCallback(async () => { + try { + setLoading(true) + const response: AxiosResponse = 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 ( +
+

+ Most Views last 30 days + +

+ + +
+ ) +} diff --git a/src/components/Home/SectionQueryResult.tsx b/src/components/Home/SectionQueryResult.tsx index 7efc796f1..7eed3103f 100644 --- a/src/components/Home/SectionQueryResult.tsx +++ b/src/components/Home/SectionQueryResult.tsx @@ -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() @@ -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 (
-

{title}

+

+ {title} {tooltip && } />} +

() const [queryMostSales, setQueryMostSales] = useState() + const [queryMostAllocation, setQueryMostAllocation] = useState() useEffect(() => { @@ -66,7 +68,7 @@ export default function HomePage(): ReactElement { /> - +