diff --git a/src/@types/viewModels/AccountTeaserVM.d.ts b/src/@types/viewModels/AccountTeaserVM.d.ts new file mode 100644 index 000000000..0367993da --- /dev/null +++ b/src/@types/viewModels/AccountTeaserVM.d.ts @@ -0,0 +1,4 @@ +interface AccountTeaserVM { + address: string + nrSales: number +} diff --git a/src/@utils/subgraph.ts b/src/@utils/subgraph.ts index 90f36122d..ccfee7972 100644 --- a/src/@utils/subgraph.ts +++ b/src/@utils/subgraph.ts @@ -24,6 +24,10 @@ import { PoolShares_poolShares as PoolShare } from '../@types/apollo/PoolShares' import { OrdersData_tokenOrders as OrdersData } from '../@types/apollo/OrdersData' +import { + UserSalesQuery_users as UserSales, + UserSalesQuery as UsersSalesList +} from '../@types/apollo/UserSalesQuery' export interface UserLiquidity { price: string @@ -165,19 +169,6 @@ const HighestLiquidityAssets = gql` } ` -const TotalAccountOrders = gql` - query TotalAccountOrders($datatokenId_in: [String!]) { - tokenOrders(where: { datatokenId_in: $datatokenId_in }) { - payer { - id - } - datatokenId { - id - } - } - } -` - const UserSharesQuery = gql` query UserSharesQuery($user: String, $pools: [String!]) { poolShares(where: { userAddress: $user, poolId_in: $pools }) { @@ -259,6 +250,20 @@ const UserSalesQuery = gql` } ` +const TopSalesQuery = gql` + query TopSalesQuery { + users( + first: 20 + orderBy: nrSales + orderDirection: desc + where: { nrSales_not: 0 } + ) { + id + nrSales + } + } +` + export function getSubgraphUri(chainId: number): string { const config = getOceanConfig(chainId) return config.subgraphUri @@ -723,3 +728,38 @@ export async function getUserSales( Logger.log(error.message) } } + +export async function getTopAssetsPublishers( + chainIds: number[], + nrItems = 9 +): Promise { + const publisherSales: AccountTeaserVM[] = [] + + for (const chain of chainIds) { + const queryContext = getQueryContext(Number(chain)) + const fetchedUsers: OperationResult = await fetchData( + TopSalesQuery, + null, + queryContext + ) + for (let i = 0; i < fetchedUsers.data.users.length; i++) { + const publishersIndex = publisherSales.findIndex( + (user) => fetchedUsers.data.users[i].id === user.address + ) + if (publishersIndex === -1) { + const publisher: AccountTeaserVM = { + address: fetchedUsers.data.users[i].id, + nrSales: fetchedUsers.data.users[i].nrSales + } + publisherSales.push(publisher) + } else { + publisherSales[publishersIndex].nrSales += + publisherSales[publishersIndex].nrSales + } + } + } + + publisherSales.sort((a, b) => b.nrSales - a.nrSales) + + return publisherSales.slice(0, nrItems) +} diff --git a/src/components/@shared/AccountList/AccountList.tsx b/src/components/@shared/AccountList/AccountList.tsx new file mode 100644 index 000000000..bef2e323f --- /dev/null +++ b/src/components/@shared/AccountList/AccountList.tsx @@ -0,0 +1,57 @@ +import React, { ReactElement } from 'react' +import styles from './AssetList.module.css' +import classNames from 'classnames/bind' +import Loader from '../atoms/Loader' +import { useUserPreferences } from '@context/UserPreferences' +import AccountTeaser from '@shared/AccountTeaser/AccountTeaser' + +const cx = classNames.bind(styles) + +function LoaderArea() { + return ( +
+ +
+ ) +} + +declare type AccountListProps = { + accounts: AccountTeaserVM[] + isLoading: boolean + className?: string +} + +export default function AccountList({ + accounts, + isLoading, + className +}: AccountListProps): ReactElement { + const { chainIds } = useUserPreferences() + + const styleClasses = cx({ + assetList: true, + [className]: className + }) + + return accounts && (isLoading === undefined || isLoading === false) ? ( + <> +
+ {accounts.length > 0 ? ( + accounts.map((account, index) => ( + + )) + ) : chainIds.length === 0 ? ( +
No network selected.
+ ) : ( +
No results found.
+ )} +
+ + ) : ( + + ) +} diff --git a/src/components/@shared/AccountTeaser/AccountTeaser.module.css b/src/components/@shared/AccountTeaser/AccountTeaser.module.css new file mode 100644 index 000000000..47af5f397 --- /dev/null +++ b/src/components/@shared/AccountTeaser/AccountTeaser.module.css @@ -0,0 +1,46 @@ +.blockies { + aspect-ratio: 1/1; + width: 13%; + height: 13%; + border-radius: 50%; + margin-left: 0; + margin-right: calc(var(--spacer) / 4); +} + +.teaser { + max-width: 40rem; + height: 100%; +} + +.link { + composes: box from '../atoms/Box.module.css'; + padding: calc(var(--spacer) / 2) !important; + font-size: var(--font-size-mini); + height: 90%; + color: var(--color-secondary); + position: relative; + display: flex; + align-items: center; +} + +.link span { + font-size: var(--font-size-large); + margin-right: calc(var(--spacer) / 3); +} +.name { + margin-bottom: 0; + font-size: var(--font-size-base) !important; + padding-top: calc(var(--spacer) / 4); +} + +.header { + display: flex; + flex-direction: row; + align-items: center; +} + +.sales { + font-size: small; + margin-top: -5px !important; + margin-bottom: clac(var(--spacer) / 2); +} diff --git a/src/components/@shared/AccountTeaser/AccountTeaser.tsx b/src/components/@shared/AccountTeaser/AccountTeaser.tsx new file mode 100644 index 000000000..7de6685c6 --- /dev/null +++ b/src/components/@shared/AccountTeaser/AccountTeaser.tsx @@ -0,0 +1,66 @@ +import React, { ReactElement, useEffect, useState } from 'react' +import Dotdotdot from 'react-dotdotdot' +import Link from 'next/link' +import styles from './AccountTeaser.module.css' +import Blockies from '../atoms/Blockies' +import { useCancelToken } from '@hooks/useCancelToken' +import get3BoxProfile from '@utils/profile' +import { accountTruncate } from '@utils/web3' + +declare type AccountTeaserProps = { + accountTeaserVM: AccountTeaserVM + place?: number +} + +export default function AccountTeaser({ + accountTeaserVM, + place +}: AccountTeaserProps): ReactElement { + const [profile, setProfile] = useState() + const newCancelToken = useCancelToken() + + useEffect(() => { + if (!accountTeaserVM) return + async function getProfileData() { + const profile = await get3BoxProfile( + accountTeaserVM.address, + newCancelToken() + ) + if (!profile) return + setProfile(profile) + } + getProfileData() + }, [accountTeaserVM, newCancelToken]) + + return ( +
+ +
+ {place && {place}} + {profile?.image ? ( + + ) : ( + + )} +
+ +

+ {profile?.name + ? profile?.name + : accountTruncate(accountTeaserVM.address)} +

+
+

+ {`${accountTeaserVM.nrSales} ${ + accountTeaserVM.nrSales === 1 ? 'sale' : 'sales' + }`} +

+
+
+ +
+ ) +} diff --git a/src/components/@shared/AssetList/index.tsx b/src/components/@shared/AssetList/index.tsx index 5a9fc8e12..78340d0f3 100644 --- a/src/components/@shared/AssetList/index.tsx +++ b/src/components/@shared/AssetList/index.tsx @@ -1,5 +1,5 @@ import AssetTeaser from '@shared/AssetTeaser/AssetTeaser' -import React, { useEffect, useState } from 'react' +import React, { ReactElement, useEffect, useState } from 'react' import Pagination from '@shared/Pagination' import styles from './index.module.css' import classNames from 'classnames/bind' @@ -29,7 +29,7 @@ declare type AssetListProps = { noPublisher?: boolean } -const AssetList: React.FC = ({ +export default function AssetList({ assets, showPagination, page, @@ -38,7 +38,7 @@ const AssetList: React.FC = ({ onPageChange, className, noPublisher -}) => { +}: AssetListProps): ReactElement { const { chainIds } = useUserPreferences() const [assetsWithPrices, setAssetsWithPrices] = useState() const [loading, setLoading] = useState(isLoading) @@ -105,5 +105,3 @@ const AssetList: React.FC = ({ ) } - -export default AssetList diff --git a/src/components/@shared/AssetTeaser/AssetTeaser.tsx b/src/components/@shared/AssetTeaser/AssetTeaser.tsx index 516be30a3..7615b4a28 100644 --- a/src/components/@shared/AssetTeaser/AssetTeaser.tsx +++ b/src/components/@shared/AssetTeaser/AssetTeaser.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { ReactElement } from 'react' import Link from 'next/link' import Dotdotdot from 'react-dotdotdot' import Price from '@shared/Price' @@ -15,11 +15,11 @@ declare type AssetTeaserProps = { noPublisher?: boolean } -const AssetTeaser: React.FC = ({ +export default function AssetTeaser({ ddo, price, noPublisher -}: AssetTeaserProps) => { +}: AssetTeaserProps): ReactElement { const { name, type, description } = ddo.metadata const { dataTokenInfo } = ddo const isCompute = Boolean(getServiceByName(ddo, 'compute')) @@ -61,5 +61,3 @@ const AssetTeaser: React.FC = ({ ) } - -export default AssetTeaser diff --git a/src/components/@shared/atoms/Tags.tsx b/src/components/@shared/atoms/Tags.tsx index d9d68de75..686789646 100644 --- a/src/components/@shared/atoms/Tags.tsx +++ b/src/components/@shared/atoms/Tags.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { ReactElement } from 'react' import Link from 'next/link' import styles from './Tags.module.css' @@ -23,13 +23,13 @@ const Tag = ({ tag, noLinks }: { tag: string; noLinks?: boolean }) => { ) } -const Tags: React.FC = ({ +export default function Tags({ items, max, showMore, className, noLinks -}) => { +}: TagsProps): ReactElement { max = max || items.length const remainder = items.length - max // filter out empty array items, and restrict to `max` @@ -48,5 +48,3 @@ const Tags: React.FC = ({ ) } - -export default Tags diff --git a/src/components/Home/PublishersWithMostSales.tsx b/src/components/Home/PublishersWithMostSales.tsx new file mode 100644 index 000000000..94b495fd4 --- /dev/null +++ b/src/components/Home/PublishersWithMostSales.tsx @@ -0,0 +1,45 @@ +import { useUserPreferences } from '@context/UserPreferences' +import AccountList from '@shared/AccountList/AccountList' +import { getTopAssetsPublishers } from '@utils/subgraph' +import React, { ReactElement, useEffect, useState } from 'react' +import styles from './Home.module.css' + +export default function PublishersWithMostSales({ + title, + action +}: { + title: ReactElement | string + action?: ReactElement +}): ReactElement { + const { chainIds } = useUserPreferences() + const [result, setResult] = useState([]) + const [loading, setLoading] = useState() + + useEffect(() => { + async function init() { + if (chainIds.length === 0) { + const result: AccountTeaserVM[] = [] + setResult(result) + setLoading(false) + } else { + try { + setLoading(true) + const publishers = await getTopAssetsPublishers(chainIds) + setResult(publishers) + setLoading(false) + } catch (error) { + // Logger.error(error.message) + } + } + } + init() + }, [chainIds]) + + return ( +
+

{title}

+ + {action && action} +
+ ) +}