mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
ENS integration, the right way (#1410)
* prototype getting ENS names the right decentralized way * get all profile metadata with ENS * refactor account display in context of top sales list * support almost all default text records * refactor text record fetching * more web3 calls reduction * package cleanup * add Publisher component test, mock out ens utils * remove mock to run @utils/ens directly * add Avatar stories * cleanup * rebase fixes * profile loading tweaks * fixes * merge cleanup * remove @ensdomains/ensjs * fetch ENS data from proxy * update avatar tests * tweak error catching for all axios fetches * test tweaks * api path fix * fetching fixes * account switching tweaks * remove unused methods * add ENS fetching tests * jest timeout tweak * update readme
This commit is contained in:
parent
5b2bb3045e
commit
92b7063b3d
@ -53,7 +53,8 @@
|
||||
"object": true,
|
||||
"array": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"testing-library/no-node-access": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
30
README.md
30
README.md
@ -16,7 +16,7 @@
|
||||
- [🦀 Data Sources](#-data-sources)
|
||||
- [Aquarius](#aquarius)
|
||||
- [Ocean Protocol Subgraph](#ocean-protocol-subgraph)
|
||||
- [3Box](#3box)
|
||||
- [ENS](#ens)
|
||||
- [Purgatory](#purgatory)
|
||||
- [Network Metadata](#network-metadata)
|
||||
- [👩🎤 Storybook](#-storybook)
|
||||
@ -194,37 +194,21 @@ function Component() {
|
||||
}
|
||||
```
|
||||
|
||||
### 3Box
|
||||
### ENS
|
||||
|
||||
Publishers can create a profile on [3Box Hub](https://www.3box.io/hub) and when found, it will be displayed in the app.
|
||||
Publishers can fill their account's [ENS domain](https://ens.domains) profile and when found, it will be displayed in the app.
|
||||
|
||||
For this our own [3box-proxy](https://github.com/oceanprotocol/3box-proxy) is used, within the app the utility method `get3BoxProfile()` can be used to get all info:
|
||||
For this our own [ens-proxy](https://github.com/oceanprotocol/ens-proxy) is used, within the app the utility method `getEnsProfile()` is called as part of the `useProfile()` hook:
|
||||
|
||||
```tsx
|
||||
import get3BoxProfile from '@utils/profile'
|
||||
import { useProfile } from '@context/Profile'
|
||||
|
||||
function Component() {
|
||||
const [profile, setProfile] = useState<Profile>()
|
||||
const { profile } = useProfile()
|
||||
|
||||
useEffect(() => {
|
||||
if (!account) return
|
||||
const source = axios.CancelToken.source()
|
||||
|
||||
async function get3Box() {
|
||||
const profile = await get3BoxProfile(account, source.token)
|
||||
if (!profile) return
|
||||
|
||||
setProfile(profile)
|
||||
}
|
||||
get3Box()
|
||||
|
||||
return () => {
|
||||
source.cancel()
|
||||
}
|
||||
}, [account])
|
||||
return (
|
||||
<div>
|
||||
{profile.emoji} {profile.name}
|
||||
{profile.avatar} {profile.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
11
package-lock.json
generated
11
package-lock.json
generated
@ -30,7 +30,6 @@
|
||||
"gray-matter": "^4.0.3",
|
||||
"is-url-superb": "^6.1.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"myetherwallet-blockies": "^0.1.1",
|
||||
@ -28113,11 +28112,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jwt-decode": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
||||
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||
},
|
||||
"node_modules/keccak": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz",
|
||||
@ -63001,11 +62995,6 @@
|
||||
"integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==",
|
||||
"dev": true
|
||||
},
|
||||
"jwt-decode": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
||||
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||
},
|
||||
"keccak": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz",
|
||||
|
@ -43,7 +43,6 @@
|
||||
"gray-matter": "^4.0.3",
|
||||
"is-url-superb": "^6.1.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.omit": "^4.5.0",
|
||||
"myetherwallet-blockies": "^0.1.1",
|
||||
|
@ -7,15 +7,18 @@ import React, {
|
||||
useCallback,
|
||||
ReactNode
|
||||
} from 'react'
|
||||
import { getUserSales, getUserTokenOrders } from '@utils/subgraph'
|
||||
import { useUserPreferences } from './UserPreferences'
|
||||
import { getUserTokenOrders } from '@utils/subgraph'
|
||||
import { useUserPreferences } from '../UserPreferences'
|
||||
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { getDownloadAssets, getPublishedAssets } from '@utils/aquarius'
|
||||
import { accountTruncate } from '@utils/web3'
|
||||
import {
|
||||
getDownloadAssets,
|
||||
getPublishedAssets,
|
||||
getUserSales
|
||||
} from '@utils/aquarius'
|
||||
import axios, { CancelToken } from 'axios'
|
||||
import get3BoxProfile from '@utils/profile'
|
||||
import web3 from 'web3'
|
||||
import { useMarketMetadata } from './MarketMetadata'
|
||||
import { useMarketMetadata } from '../MarketMetadata'
|
||||
import { getEnsProfile } from '@utils/ens'
|
||||
|
||||
interface ProfileProviderValue {
|
||||
profile: Profile
|
||||
@ -32,6 +35,14 @@ const ProfileContext = createContext({} as ProfileProviderValue)
|
||||
|
||||
const refreshInterval = 10000 // 10 sec.
|
||||
|
||||
const clearedProfile: Profile = {
|
||||
name: null,
|
||||
avatar: null,
|
||||
url: null,
|
||||
description: null,
|
||||
links: null
|
||||
}
|
||||
|
||||
function ProfileProvider({
|
||||
accountId,
|
||||
accountEns,
|
||||
@ -56,9 +67,9 @@ function ProfileProvider({
|
||||
}, [accountId])
|
||||
|
||||
//
|
||||
// User profile: ENS + 3Box
|
||||
// User profile: ENS
|
||||
//
|
||||
const [profile, setProfile] = useState<Profile>()
|
||||
const [profile, setProfile] = useState<Profile>({ name: accountEns })
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountEns) return
|
||||
@ -66,53 +77,22 @@ function ProfileProvider({
|
||||
}, [accountId, accountEns])
|
||||
|
||||
useEffect(() => {
|
||||
const clearedProfile: Profile = {
|
||||
name: null,
|
||||
accountEns: null,
|
||||
image: null,
|
||||
description: null,
|
||||
links: null
|
||||
}
|
||||
|
||||
if (!accountId || !isEthAddress) {
|
||||
if (
|
||||
!accountId ||
|
||||
accountId === '0x0000000000000000000000000000000000000000' ||
|
||||
!isEthAddress
|
||||
) {
|
||||
setProfile(clearedProfile)
|
||||
return
|
||||
}
|
||||
|
||||
const cancelTokenSource = axios.CancelToken.source()
|
||||
|
||||
async function getInfo() {
|
||||
setProfile({ name: accountEns || accountTruncate(accountId), accountEns })
|
||||
|
||||
const profile3Box = await get3BoxProfile(
|
||||
accountId,
|
||||
cancelTokenSource.token
|
||||
)
|
||||
if (profile3Box) {
|
||||
const { name, emoji, description, image, links } = profile3Box
|
||||
const newName = `${emoji || ''} ${name || accountTruncate(accountId)}`
|
||||
const newProfile = {
|
||||
name: newName,
|
||||
image,
|
||||
description,
|
||||
links
|
||||
}
|
||||
setProfile((prevState) => ({
|
||||
...prevState,
|
||||
...newProfile
|
||||
}))
|
||||
LoggerInstance.log('[profile] Found and set 3box profile.', newProfile)
|
||||
} else {
|
||||
// setProfile(clearedProfile)
|
||||
LoggerInstance.log('[profile] No 3box profile found.')
|
||||
}
|
||||
const profile = await getEnsProfile(accountId)
|
||||
setProfile(profile)
|
||||
LoggerInstance.log(`[profile] ENS metadata for ${accountId}:`, profile)
|
||||
}
|
||||
getInfo()
|
||||
|
||||
return () => {
|
||||
cancelTokenSource.cancel()
|
||||
}
|
||||
}, [accountId, accountEns, isEthAddress])
|
||||
}, [accountId, isEthAddress])
|
||||
|
||||
//
|
||||
// PUBLISHED ASSETS
|
@ -13,7 +13,7 @@ import { infuraProjectId as infuraId } from '../../app.config'
|
||||
import WalletConnectProvider from '@walletconnect/web3-provider'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { isBrowser } from '@utils/index'
|
||||
import { getEnsName } from '@utils/ens'
|
||||
import { getEnsProfile } from '@utils/ens'
|
||||
import useNetworkMetadata, {
|
||||
getNetworkDataById,
|
||||
getNetworkDisplayName,
|
||||
@ -32,6 +32,7 @@ interface Web3ProviderValue {
|
||||
web3ProviderInfo: IProviderInfo
|
||||
accountId: string
|
||||
accountEns: string
|
||||
accountEnsAvatar: string
|
||||
balance: UserBalance
|
||||
networkId: number
|
||||
chainId: number
|
||||
@ -54,8 +55,6 @@ const web3ModalTheme = {
|
||||
hover: 'var(--background-highlight)'
|
||||
}
|
||||
|
||||
// HEADS UP! We inline-require some packages so the SSR build does not break.
|
||||
// We only need them client-side.
|
||||
const providerOptions = isBrowser
|
||||
? {
|
||||
walletconnect: {
|
||||
@ -99,6 +98,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
const [isTestnet, setIsTestnet] = useState<boolean>()
|
||||
const [accountId, setAccountId] = useState<string>()
|
||||
const [accountEns, setAccountEns] = useState<string>()
|
||||
const [accountEnsAvatar, setAccountEnsAvatar] = useState<string>()
|
||||
const [web3Loading, setWeb3Loading] = useState<boolean>(true)
|
||||
const [balance, setBalance] = useState<UserBalance>({
|
||||
eth: '0'
|
||||
@ -192,24 +192,35 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
}, [accountId, approvedBaseTokens, networkId, web3, networkData])
|
||||
|
||||
// -----------------------------------
|
||||
// Helper: Get user ENS name
|
||||
// Helper: Get user ENS info
|
||||
// -----------------------------------
|
||||
const getUserEnsName = useCallback(async () => {
|
||||
const getUserEns = useCallback(async () => {
|
||||
if (!accountId) return
|
||||
|
||||
try {
|
||||
// const accountEns = await getEnsNameWithWeb3(
|
||||
// accountId,
|
||||
// web3Provider,
|
||||
// `${networkId}`
|
||||
// )
|
||||
const accountEns = await getEnsName(accountId)
|
||||
setAccountEns(accountEns)
|
||||
accountEns &&
|
||||
const profile = await getEnsProfile(accountId)
|
||||
|
||||
if (!profile) {
|
||||
setAccountEns(null)
|
||||
setAccountEnsAvatar(null)
|
||||
return
|
||||
}
|
||||
|
||||
setAccountEns(profile.name)
|
||||
LoggerInstance.log(
|
||||
`[web3] ENS name found for ${accountId}:`,
|
||||
accountEns
|
||||
profile.name
|
||||
)
|
||||
|
||||
if (profile.avatar) {
|
||||
setAccountEnsAvatar(profile.avatar)
|
||||
LoggerInstance.log(
|
||||
`[web3] ENS avatar found for ${accountId}:`,
|
||||
profile.avatar
|
||||
)
|
||||
} else {
|
||||
setAccountEnsAvatar(null)
|
||||
}
|
||||
} catch (error) {
|
||||
LoggerInstance.error('[web3] Error: ', error.message)
|
||||
}
|
||||
@ -275,11 +286,11 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
}, [getUserBalance])
|
||||
|
||||
// -----------------------------------
|
||||
// Get and set user ENS name
|
||||
// Get and set user ENS info
|
||||
// -----------------------------------
|
||||
useEffect(() => {
|
||||
getUserEnsName()
|
||||
}, [getUserEnsName])
|
||||
getUserEns()
|
||||
}, [getUserEns])
|
||||
|
||||
// -----------------------------------
|
||||
// Get and set network metadata
|
||||
@ -337,7 +348,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
// -----------------------------------
|
||||
async function logout() {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
if (web3 && web3.currentProvider && (web3.currentProvider as any).close) {
|
||||
if ((web3?.currentProvider as any)?.close) {
|
||||
await (web3.currentProvider as any).close()
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
@ -402,6 +413,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
web3ProviderInfo,
|
||||
accountId,
|
||||
accountEns,
|
||||
accountEnsAvatar,
|
||||
balance,
|
||||
networkId,
|
||||
chainId,
|
||||
|
32
src/@types/Profile.d.ts
vendored
32
src/@types/Profile.d.ts
vendored
@ -1,36 +1,12 @@
|
||||
interface ProfileLink {
|
||||
name: string
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
did?: string
|
||||
name?: string
|
||||
accountEns?: string
|
||||
name: string
|
||||
url?: string
|
||||
avatar?: string
|
||||
description?: string
|
||||
emoji?: string
|
||||
image?: string
|
||||
links?: ProfileLink[]
|
||||
}
|
||||
|
||||
interface ResponseData3Box {
|
||||
name: string
|
||||
description: string
|
||||
website: string
|
||||
status?: 'error'
|
||||
/* eslint-disable camelcase */
|
||||
proof_did: string
|
||||
proof_twitter: string
|
||||
proof_github: string
|
||||
/* eslint-enable camelcase */
|
||||
emoji: string
|
||||
job: string
|
||||
employer: string
|
||||
location: string
|
||||
memberSince: string
|
||||
image: {
|
||||
contentUrl: {
|
||||
[key: string]: string
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
4
src/@types/viewModels/AccountTeaserVM.d.ts
vendored
4
src/@types/viewModels/AccountTeaserVM.d.ts
vendored
@ -1,4 +0,0 @@
|
||||
interface AccountTeaserVM {
|
||||
address: string
|
||||
nrSales: number
|
||||
}
|
@ -9,6 +9,11 @@ import {
|
||||
} from '../@types/aquarius/SearchQuery'
|
||||
import { transformAssetToAssetSelection } from './assetConvertor'
|
||||
|
||||
export interface UserSales {
|
||||
id: string
|
||||
totalSales: number
|
||||
}
|
||||
|
||||
export const MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS = 476
|
||||
|
||||
export function escapeEsReservedCharacters(value: string): string {
|
||||
@ -397,6 +402,40 @@ export async function getTopPublishers(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTopAssetsPublishers(
|
||||
chainIds: number[],
|
||||
nrItems = 9
|
||||
): Promise<UserSales[]> {
|
||||
const publishers: UserSales[] = []
|
||||
|
||||
const result = await getTopPublishers(chainIds, null)
|
||||
const { topPublishers } = result.aggregations
|
||||
|
||||
for (let i = 0; i < topPublishers.buckets.length; i++) {
|
||||
publishers.push({
|
||||
id: topPublishers.buckets[i].key,
|
||||
totalSales: parseInt(topPublishers.buckets[i].totalSales.value)
|
||||
})
|
||||
}
|
||||
|
||||
publishers.sort((a, b) => b.totalSales - a.totalSales)
|
||||
|
||||
return publishers.slice(0, nrItems)
|
||||
}
|
||||
|
||||
export async function getUserSales(
|
||||
accountId: string,
|
||||
chainIds: number[]
|
||||
): Promise<number> {
|
||||
try {
|
||||
const result = await getPublishedAssets(accountId, chainIds, null)
|
||||
const { totalOrders } = result.aggregations
|
||||
return totalOrders.value
|
||||
} catch (error) {
|
||||
LoggerInstance.error('Error getUserSales', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDownloadAssets(
|
||||
dtList: string[],
|
||||
tokenOrders: OrdersData[],
|
||||
|
64
src/@utils/ens.test.ts
Normal file
64
src/@utils/ens.test.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { getEnsName, getEnsAddress, getEnsProfile } from './ens'
|
||||
|
||||
describe('@utils/ens', () => {
|
||||
jest.setTimeout(10000)
|
||||
jest.retryTimes(2)
|
||||
|
||||
test('getEnsName', async () => {
|
||||
const ensName = await getEnsName(
|
||||
'0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0'
|
||||
)
|
||||
expect(ensName).toBe('jellymcjellyfish.eth')
|
||||
})
|
||||
|
||||
test('getEnsName with invalid address', async () => {
|
||||
const ensName = await getEnsName('0x123')
|
||||
expect(ensName).toBeUndefined()
|
||||
})
|
||||
|
||||
test('getEnsName with empty address', async () => {
|
||||
const ensName = await getEnsName('')
|
||||
expect(ensName).toBeUndefined()
|
||||
})
|
||||
|
||||
test('getEnsName with undefined address', async () => {
|
||||
const ensName = await getEnsName(undefined)
|
||||
expect(ensName).toBeUndefined()
|
||||
})
|
||||
|
||||
test('getEnsAddress', async () => {
|
||||
const ensAddress = await getEnsAddress('jellymcjellyfish.eth')
|
||||
expect(ensAddress).toBe('0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0')
|
||||
})
|
||||
|
||||
test('getEnsAddress with invalid address', async () => {
|
||||
const ensAddress = await getEnsAddress('0x123')
|
||||
expect(ensAddress).toBeUndefined()
|
||||
})
|
||||
|
||||
test('getEnsAddress with empty address', async () => {
|
||||
const ensAddress = await getEnsAddress('')
|
||||
expect(ensAddress).toBeUndefined()
|
||||
})
|
||||
|
||||
test('getEnsProfile', async () => {
|
||||
const ensProfile = await getEnsProfile(
|
||||
'0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0'
|
||||
)
|
||||
expect(ensProfile).toEqual({
|
||||
avatar:
|
||||
'https://metadata.ens.domains/mainnet/avatar/jellymcjellyfish.eth',
|
||||
links: [
|
||||
{ key: 'url', value: 'https://oceanprotocol.com' },
|
||||
{ key: 'com.twitter', value: 'oceanprotocol' },
|
||||
{ key: 'com.github', value: 'oceanprotocol' }
|
||||
],
|
||||
name: 'jellymcjellyfish.eth'
|
||||
})
|
||||
})
|
||||
|
||||
test('getEnsProfile with empty address', async () => {
|
||||
const ensProfile = await getEnsProfile('')
|
||||
expect(ensProfile).toBeUndefined()
|
||||
})
|
||||
})
|
@ -1,52 +1,24 @@
|
||||
import { gql, OperationContext, OperationResult } from 'urql'
|
||||
import { fetchData } from './subgraph'
|
||||
import { fetchData } from './fetch'
|
||||
|
||||
// make sure to only query for domains owned by account, so domains
|
||||
// solely set by 3rd parties like *.gitcoin.eth won't show up
|
||||
const UserEnsNames = gql<any>`
|
||||
query UserEnsDomains($accountId: String!) {
|
||||
domains(where: { resolvedAddress: $accountId, owner: $accountId }) {
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const UserEnsAddress = gql<any>`
|
||||
query UserEnsDomainsAddress($name: String!) {
|
||||
domains(where: { name: $name }) {
|
||||
resolvedAddress {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const ensSubgraphQueryContext: OperationContext = {
|
||||
url: `https://api.thegraph.com/subgraphs/name/ensdomains/ens`,
|
||||
requestPolicy: 'cache-and-network'
|
||||
}
|
||||
const apiUrl = 'https://ens-proxy.oceanprotocol.com/api'
|
||||
|
||||
export async function getEnsName(accountId: string): Promise<string> {
|
||||
const response: OperationResult<any> = await fetchData(
|
||||
UserEnsNames,
|
||||
{ accountId: accountId.toLowerCase() },
|
||||
ensSubgraphQueryContext
|
||||
)
|
||||
if (!response?.data?.domains?.length) return
|
||||
if (!accountId || accountId === '') return
|
||||
|
||||
// Default order of response.data.domains seems to be by creation time, from oldest to newest.
|
||||
// Pick the last one as that is what direct web3 calls do.
|
||||
const { name } = response.data.domains.slice(-1)[0]
|
||||
return name
|
||||
const data = await fetchData(`${apiUrl}/name?accountId=${accountId}`)
|
||||
return data?.name
|
||||
}
|
||||
|
||||
export async function getEnsAddress(ensName: string): Promise<string> {
|
||||
const response: OperationResult<any> = await fetchData(
|
||||
UserEnsAddress,
|
||||
{ name: ensName },
|
||||
ensSubgraphQueryContext
|
||||
)
|
||||
if (!response?.data?.domains?.length) return
|
||||
const { id } = response.data.domains[0].resolvedAddress
|
||||
return id
|
||||
export async function getEnsAddress(accountId: string): Promise<string> {
|
||||
if (!accountId || accountId === '' || !accountId.includes('.')) return
|
||||
|
||||
const data = await fetchData(`${apiUrl}/address?name=${accountId}`)
|
||||
return data?.address
|
||||
}
|
||||
|
||||
export async function getEnsProfile(accountId: string): Promise<Profile> {
|
||||
if (!accountId || accountId === '') return
|
||||
|
||||
const data = await fetchData(`${apiUrl}/profile?address=${accountId}`)
|
||||
return data?.profile
|
||||
}
|
||||
|
@ -1,15 +1,24 @@
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import axios, { AxiosResponse } from 'axios'
|
||||
|
||||
export async function fetchData(url: string): Promise<AxiosResponse['data']> {
|
||||
try {
|
||||
const response = await axios(url)
|
||||
|
||||
if (response.status !== 200) {
|
||||
return console.error('Non-200 response: ' + response.status)
|
||||
}
|
||||
|
||||
return response.data
|
||||
return response?.data
|
||||
} catch (error) {
|
||||
console.error('Error parsing json: ' + error.message)
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
LoggerInstance.error(`Non-200 response from ${url}:`, error.response)
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
LoggerInstance.error('No response with:', error.request)
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
LoggerInstance.error('Error in setting up request:', error.message)
|
||||
}
|
||||
LoggerInstance.error(error.message)
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,5 @@
|
||||
import { Decimal } from 'decimal.js'
|
||||
|
||||
export function isValidNumber(value: any): boolean {
|
||||
const isUndefinedValue = typeof value === 'undefined'
|
||||
const isNullValue = value === null
|
||||
const isNaNValue = isNaN(Number(value))
|
||||
const isEmptyString = value === ''
|
||||
|
||||
return !isUndefinedValue && !isNullValue && !isNaNValue && !isEmptyString
|
||||
}
|
||||
|
||||
// Run decimal.js comparison
|
||||
// http://mikemcl.github.io/decimal.js/#cmp
|
||||
export function compareAsBN(balance: string, price: string): boolean {
|
||||
|
@ -1,15 +0,0 @@
|
||||
export function getBuyDTFeedback(dtSymbol: string): { [key: number]: string } {
|
||||
return {
|
||||
1: '1/3 Approving OCEAN ...',
|
||||
2: `2/3 Buying ${dtSymbol} ...`,
|
||||
3: `3/3 ${dtSymbol} bought.`
|
||||
}
|
||||
}
|
||||
|
||||
export function getSellDTFeedback(dtSymbol: string): { [key: number]: string } {
|
||||
return {
|
||||
1: '1/3 Approving OCEAN ...',
|
||||
2: `2/3 Selling ${dtSymbol} ...`,
|
||||
3: `3/3 ${dtSymbol} sold.`
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
import axios, { AxiosResponse, CancelToken } from 'axios'
|
||||
import jwtDecode from 'jwt-decode'
|
||||
|
||||
// https://docs.3box.io/api/rest-api
|
||||
const apiUri = 'https://3box.oceanprotocol.com'
|
||||
const ipfsUrl = 'https://infura-ipfs.io'
|
||||
|
||||
function decodeProof(proofJWT: string) {
|
||||
if (!proofJWT) return
|
||||
const proof = jwtDecode(proofJWT) as any
|
||||
return proof
|
||||
}
|
||||
|
||||
function getLinks(
|
||||
website: string,
|
||||
twitterProof: string,
|
||||
githubProof: string
|
||||
): ProfileLink[] {
|
||||
// Conditionally add links if they exist
|
||||
const links = [
|
||||
...(website ? [{ name: 'Website', value: website }] : []),
|
||||
...(twitterProof
|
||||
? [
|
||||
{
|
||||
name: 'Twitter',
|
||||
value: decodeProof(twitterProof).claim.twitter_handle
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(githubProof
|
||||
? [{ name: 'GitHub', value: githubProof.split('/')[3] }]
|
||||
: [])
|
||||
]
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
function transformResponse({
|
||||
name,
|
||||
description,
|
||||
website,
|
||||
emoji,
|
||||
image,
|
||||
/* eslint-disable camelcase */
|
||||
proof_twitter,
|
||||
proof_github,
|
||||
proof_did
|
||||
}: ResponseData3Box) {
|
||||
/* eslint-enable camelcase */
|
||||
const links = getLinks(website, proof_twitter, proof_github)
|
||||
|
||||
const profile: Profile = {
|
||||
did: decodeProof(proof_did).iss,
|
||||
// Conditionally add profile items if they exist
|
||||
...(name && { name }),
|
||||
...(description && { description }),
|
||||
...(emoji && { emoji }),
|
||||
...(image && {
|
||||
image: `${ipfsUrl}/ipfs/${
|
||||
image.map(
|
||||
(img: { contentUrl: { [key: string]: string } }) =>
|
||||
img.contentUrl['/']
|
||||
)[0]
|
||||
}`
|
||||
}),
|
||||
...(links.length && { links })
|
||||
}
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
export default async function get3BoxProfile(
|
||||
accountId: string,
|
||||
cancelToken: CancelToken
|
||||
): Promise<Profile> {
|
||||
try {
|
||||
const response = (await axios(`${apiUri}/profile/${accountId}`, {
|
||||
cancelToken
|
||||
})) as AxiosResponse<ResponseData3Box>
|
||||
|
||||
if (
|
||||
!response ||
|
||||
!response.data ||
|
||||
response.status !== 200 ||
|
||||
response.data.status === 'error'
|
||||
)
|
||||
return
|
||||
|
||||
// LoggerInstance.log(`3Box profile found for ${accountId}`, response.data)
|
||||
const profile = transformResponse(response.data)
|
||||
return profile
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (error) {}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import { Purgatory } from '@oceanprotocol/lib'
|
||||
import { fetchData } from './fetch'
|
||||
|
||||
const purgatoryUrl = 'https://market-purgatory.oceanprotocol.com/api/'
|
||||
|
@ -6,16 +6,6 @@ import { AssetPreviousOrder } from '../@types/subgraph/AssetPreviousOrder'
|
||||
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
|
||||
import { OpcFeesQuery as OpcFeesData } from '../@types/subgraph/OpcFeesQuery'
|
||||
|
||||
import { getPublishedAssets, getTopPublishers } from '@utils/aquarius'
|
||||
export interface UserLiquidity {
|
||||
price: string
|
||||
oceanBalance: string
|
||||
}
|
||||
|
||||
export interface PriceList {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
const PreviousOrderQuery = gql`
|
||||
query AssetPreviousOrder($id: String!, $account: String!) {
|
||||
orders(
|
||||
@ -153,29 +143,6 @@ export async function getOpcFees(chainId: number) {
|
||||
return opcFees
|
||||
}
|
||||
|
||||
export async function getPreviousOrders(
|
||||
id: string,
|
||||
account: string,
|
||||
assetTimeout: string
|
||||
): Promise<string> {
|
||||
const variables = { id, account }
|
||||
const fetchedPreviousOrders: OperationResult<AssetPreviousOrder> =
|
||||
await fetchData(PreviousOrderQuery, variables, null)
|
||||
if (fetchedPreviousOrders.data?.orders?.length === 0) return null
|
||||
if (assetTimeout === '0') {
|
||||
return fetchedPreviousOrders?.data?.orders[0]?.tx
|
||||
} else {
|
||||
const expiry =
|
||||
fetchedPreviousOrders?.data?.orders[0]?.createdTimestamp * 1000 +
|
||||
Number(assetTimeout) * 1000
|
||||
if (Date.now() <= expiry) {
|
||||
return fetchedPreviousOrders?.data?.orders[0]?.tx
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserTokenOrders(
|
||||
accountId: string,
|
||||
chainIds: number[]
|
||||
@ -201,40 +168,6 @@ export async function getUserTokenOrders(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserSales(
|
||||
accountId: string,
|
||||
chainIds: number[]
|
||||
): Promise<number> {
|
||||
try {
|
||||
const result = await getPublishedAssets(accountId, chainIds, null)
|
||||
const { totalOrders } = result.aggregations
|
||||
return totalOrders.value
|
||||
} catch (error) {
|
||||
LoggerInstance.error('Error getUserSales', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTopAssetsPublishers(
|
||||
chainIds: number[],
|
||||
nrItems = 9
|
||||
): Promise<AccountTeaserVM[]> {
|
||||
const publishers: AccountTeaserVM[] = []
|
||||
|
||||
const result = await getTopPublishers(chainIds, null)
|
||||
const { topPublishers } = result.aggregations
|
||||
|
||||
for (let i = 0; i < topPublishers.buckets.length; i++) {
|
||||
publishers.push({
|
||||
address: topPublishers.buckets[i].key,
|
||||
nrSales: parseInt(topPublishers.buckets[i].totalSales.value)
|
||||
})
|
||||
}
|
||||
|
||||
publishers.sort((a, b) => b.nrSales - a.nrSales)
|
||||
|
||||
return publishers.slice(0, nrItems)
|
||||
}
|
||||
|
||||
export async function getOpcsApprovedTokens(
|
||||
chainId: number
|
||||
): Promise<TokenInfo[]> {
|
||||
|
@ -1,57 +0,0 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import styles from './index.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 (
|
||||
<div className={styles.loaderWrap}>
|
||||
<Loader />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
declare type AccountListProps = {
|
||||
accounts: AccountTeaserVM[]
|
||||
isLoading: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function AccountList({
|
||||
accounts,
|
||||
isLoading,
|
||||
className
|
||||
}: AccountListProps): ReactElement {
|
||||
const { chainIds } = useUserPreferences()
|
||||
|
||||
const styleClasses = cx({
|
||||
accountList: true,
|
||||
[className]: className
|
||||
})
|
||||
|
||||
return accounts && (isLoading === undefined || isLoading === false) ? (
|
||||
<>
|
||||
<div className={styleClasses}>
|
||||
{accounts.length > 0 ? (
|
||||
accounts.map((account, index) => (
|
||||
<AccountTeaser
|
||||
accountTeaserVM={account}
|
||||
key={index + 1}
|
||||
place={index + 1}
|
||||
/>
|
||||
))
|
||||
) : chainIds.length === 0 ? (
|
||||
<div className={styles.empty}>No network selected.</div>
|
||||
) : (
|
||||
<div className={styles.empty}>No results found.</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<LoaderArea />
|
||||
)
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
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<Profile>()
|
||||
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 (
|
||||
<Link href={`/profile/${accountTeaserVM.address}`}>
|
||||
<a className={styles.teaser}>
|
||||
{place && <span className={styles.place}>{place}</span>}
|
||||
<Blockies
|
||||
accountId={accountTeaserVM.address}
|
||||
className={styles.blockies}
|
||||
image={profile?.image}
|
||||
/>
|
||||
<div>
|
||||
<Dotdotdot tagName="h4" clamp={2} className={styles.name}>
|
||||
{profile?.name
|
||||
? profile?.name
|
||||
: accountTruncate(accountTeaserVM.address)}
|
||||
</Dotdotdot>
|
||||
<p className={styles.sales}>
|
||||
<span>{accountTeaserVM.nrSales}</span>
|
||||
{`${accountTeaserVM.nrSales === 1 ? ' sale' : ' sales'}`}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
.add {
|
||||
color: var(--brand-pink);
|
||||
}
|
||||
|
||||
.linksExternal {
|
||||
composes: linksExternal from './index.module.css';
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import External from '@images/external.svg'
|
||||
import styles from './Add.module.css'
|
||||
|
||||
export default function Add(): ReactElement {
|
||||
return (
|
||||
<a
|
||||
className={styles.add}
|
||||
href="https://www.3box.io/hub"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Add profile on 3Box <External className={styles.linksExternal} />
|
||||
</a>
|
||||
)
|
||||
}
|
@ -7,10 +7,3 @@
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.linksExternal {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
display: inline-block;
|
||||
fill: var(--color-secondary);
|
||||
}
|
||||
|
56
src/components/@shared/Publisher/index.test.tsx
Normal file
56
src/components/@shared/Publisher/index.test.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as axios from 'axios'
|
||||
import Publisher from './'
|
||||
|
||||
const account = '0x0000000000000000000000000000000000000000'
|
||||
|
||||
jest.mock('axios')
|
||||
|
||||
describe('Publisher', () => {
|
||||
test('should return correct markup by default', async () => {
|
||||
;(axios as any).get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { name: 'jellymcjellyfish.eth' } })
|
||||
)
|
||||
|
||||
render(<Publisher account={account} />)
|
||||
|
||||
const element = await screen.findByRole('link')
|
||||
expect(element).toBeInTheDocument()
|
||||
expect(element).toContainHTML('<a')
|
||||
expect(element).toHaveAttribute('href', `/profile/${account}`)
|
||||
})
|
||||
|
||||
test('should truncate account by default', async () => {
|
||||
;(axios as any).get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { name: null } })
|
||||
)
|
||||
|
||||
render(<Publisher account={account} />)
|
||||
|
||||
const element = await screen.findByText('0x…00000000')
|
||||
expect(element).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('should return correct markup in minimal state', async () => {
|
||||
;(axios as any).get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { name: null } })
|
||||
)
|
||||
|
||||
render(<Publisher minimal account={account} />)
|
||||
|
||||
const element = await screen.findByText('0x…00000000')
|
||||
expect(element).not.toHaveAttribute('href')
|
||||
})
|
||||
|
||||
test('should return markup with empty account', async () => {
|
||||
;(axios as any).get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { name: null } })
|
||||
)
|
||||
|
||||
render(<Publisher account={null} />)
|
||||
|
||||
const element = await screen.findByRole('link')
|
||||
expect(element).toBeInTheDocument()
|
||||
})
|
||||
})
|
@ -1,72 +1,47 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import styles from './index.module.css'
|
||||
import classNames from 'classnames/bind'
|
||||
import Link from 'next/link'
|
||||
import get3BoxProfile from '@utils/profile'
|
||||
import { accountTruncate } from '@utils/web3'
|
||||
import axios from 'axios'
|
||||
import { getEnsName } from '@utils/ens'
|
||||
import { useIsMounted } from '@hooks/useIsMounted'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
export interface PublisherProps {
|
||||
account: string
|
||||
minimal?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function Publisher({
|
||||
account,
|
||||
minimal,
|
||||
className
|
||||
}: {
|
||||
account: string
|
||||
minimal?: boolean
|
||||
className?: string
|
||||
}): ReactElement {
|
||||
}: PublisherProps): ReactElement {
|
||||
const isMounted = useIsMounted()
|
||||
const [profile, setProfile] = useState<Profile>()
|
||||
const [name, setName] = useState('')
|
||||
const [accountEns, setAccountEns] = useState<string>()
|
||||
const [name, setName] = useState(accountTruncate(account))
|
||||
|
||||
useEffect(() => {
|
||||
if (!account) return
|
||||
if (!account || account === '') return
|
||||
|
||||
// set default name on hook
|
||||
// to avoid side effect (UI not updating on account's change)
|
||||
setName(accountTruncate(account))
|
||||
|
||||
const source = axios.CancelToken.source()
|
||||
|
||||
async function getExternalName() {
|
||||
// ENS
|
||||
const accountEns = await getEnsName(account)
|
||||
if (accountEns && isMounted()) {
|
||||
setAccountEns(accountEns)
|
||||
setName(accountEns)
|
||||
}
|
||||
|
||||
// 3box
|
||||
const profile = await get3BoxProfile(account, source.token)
|
||||
if (!profile) return
|
||||
setProfile(profile)
|
||||
const { name, emoji } = profile
|
||||
name && setName(`${emoji || ''} ${name}`)
|
||||
}
|
||||
getExternalName()
|
||||
|
||||
return () => {
|
||||
source.cancel()
|
||||
}
|
||||
}, [account, isMounted])
|
||||
|
||||
const styleClasses = cx({
|
||||
publisher: true,
|
||||
[className]: className
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={styleClasses}>
|
||||
<div className={`${styles.publisher} ${className || ''}`}>
|
||||
{minimal ? (
|
||||
name
|
||||
) : (
|
||||
<>
|
||||
<Link href={`/profile/${accountEns || account}`}>
|
||||
<Link href={`/profile/${account}`}>
|
||||
<a title="Show profile page.">{name}</a>
|
||||
</Link>
|
||||
</>
|
||||
|
@ -1,4 +1,4 @@
|
||||
.blockies {
|
||||
.avatar {
|
||||
width: var(--font-size-large);
|
||||
height: var(--font-size-large);
|
||||
border-radius: 50%;
|
31
src/components/@shared/atoms/Avatar/index.stories.tsx
Normal file
31
src/components/@shared/atoms/Avatar/index.stories.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react'
|
||||
|
||||
import Avatar, { AvatarProps } from '@shared/atoms/Avatar'
|
||||
|
||||
export default {
|
||||
title: 'Component/@shared/atoms/Avatar',
|
||||
component: Avatar
|
||||
} as ComponentMeta<typeof Avatar>
|
||||
|
||||
const Template: ComponentStory<typeof Avatar> = (args) => <Avatar {...args} />
|
||||
|
||||
interface Props {
|
||||
args: AvatarProps
|
||||
}
|
||||
|
||||
export const DefaultWithBlockies: Props = Template.bind({})
|
||||
DefaultWithBlockies.args = {
|
||||
accountId: '0x1234567890123456789012345678901234567890'
|
||||
}
|
||||
|
||||
export const CustomSource: Props = Template.bind({})
|
||||
CustomSource.args = {
|
||||
accountId: '0x1234567890123456789012345678901234567890',
|
||||
src: 'http://placekitten.com/g/300/300'
|
||||
}
|
||||
|
||||
export const Empty: Props = Template.bind({})
|
||||
Empty.args = {
|
||||
accountId: null
|
||||
}
|
19
src/components/@shared/atoms/Avatar/index.test.tsx
Normal file
19
src/components/@shared/atoms/Avatar/index.test.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import testRender from '../../../../../.jest/testRender'
|
||||
import Avatar from '@shared/atoms/Avatar'
|
||||
import { DefaultWithBlockies, CustomSource, Empty } from './index.stories'
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
describe('Avatar', () => {
|
||||
testRender(<Avatar {...DefaultWithBlockies.args} />)
|
||||
|
||||
it('renders without crashing with custom source', () => {
|
||||
const { container } = render(<Avatar {...CustomSource.args} />)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders empty without crashing', () => {
|
||||
const { container } = render(<Avatar {...Empty.args} />)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
24
src/components/@shared/atoms/Avatar/index.tsx
Normal file
24
src/components/@shared/atoms/Avatar/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { toDataUrl } from 'myetherwallet-blockies'
|
||||
import React, { ReactElement } from 'react'
|
||||
import styles from './index.module.css'
|
||||
|
||||
export interface AvatarProps {
|
||||
accountId: string
|
||||
src?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function Avatar({
|
||||
accountId,
|
||||
src,
|
||||
className
|
||||
}: AvatarProps): ReactElement {
|
||||
return (
|
||||
<img
|
||||
className={`${className || ''} ${styles.avatar} `}
|
||||
src={src || (accountId ? toDataUrl(accountId) : '')}
|
||||
alt="Avatar"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import React from 'react'
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react'
|
||||
|
||||
import Blockies, { BlockiesProps } from '@shared/atoms/Blockies'
|
||||
|
||||
export default {
|
||||
title: 'Component/@shared/atoms/Blockies',
|
||||
component: Blockies
|
||||
} as ComponentMeta<typeof Blockies>
|
||||
|
||||
const Template: ComponentStory<typeof Blockies> = (args) => (
|
||||
<Blockies {...args} />
|
||||
)
|
||||
|
||||
interface Props {
|
||||
args: BlockiesProps
|
||||
}
|
||||
|
||||
export const Default: Props = Template.bind({})
|
||||
Default.args = {
|
||||
accountId: '0x1xxxxxxxxxx3Exxxxxx7xxxxxxxxxxxxF1fd'
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import React from 'react'
|
||||
import testRender from '../../../../../.jest/testRender'
|
||||
import Blockies from '@shared/atoms/Blockies'
|
||||
import { Default } from './index.stories'
|
||||
|
||||
describe('Blockies', () => {
|
||||
testRender(<Blockies {...Default.args} />)
|
||||
})
|
@ -1,28 +0,0 @@
|
||||
import { toDataUrl } from 'myetherwallet-blockies'
|
||||
import React, { ReactElement } from 'react'
|
||||
import styles from './index.module.css'
|
||||
|
||||
export interface BlockiesProps {
|
||||
accountId: string
|
||||
className?: string
|
||||
image?: string
|
||||
}
|
||||
|
||||
export default function Blockies({
|
||||
accountId,
|
||||
className,
|
||||
image
|
||||
}: BlockiesProps): ReactElement {
|
||||
if (!accountId) return null
|
||||
|
||||
const blockies = toDataUrl(accountId)
|
||||
|
||||
return (
|
||||
<img
|
||||
className={`${className || ''} ${styles.blockies} `}
|
||||
src={image || blockies}
|
||||
alt="Blockies"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
}
|
@ -4,12 +4,13 @@ import { accountTruncate } from '@utils/web3'
|
||||
import Loader from '@shared/atoms/Loader'
|
||||
import styles from './Account.module.css'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import Blockies from '@shared/atoms/Blockies'
|
||||
import Avatar from '@shared/atoms/Avatar'
|
||||
|
||||
// Forward ref for Tippy.js
|
||||
// eslint-disable-next-line
|
||||
const Account = React.forwardRef((props, ref: any) => {
|
||||
const { accountId, accountEns, web3Modal, connect } = useWeb3()
|
||||
const { accountId, accountEns, accountEnsAvatar, web3Modal, connect } =
|
||||
useWeb3()
|
||||
|
||||
async function handleActivation(e: FormEvent<HTMLButtonElement>) {
|
||||
// prevent accidentially submitting a form the button might be in
|
||||
@ -30,7 +31,7 @@ const Account = React.forwardRef((props, ref: any) => {
|
||||
ref={ref}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<Blockies accountId={accountId} />
|
||||
<Avatar accountId={accountId} src={accountEnsAvatar} />
|
||||
<span className={styles.address} title={accountId}>
|
||||
{accountTruncate(accountEns || accountId)}
|
||||
</span>
|
||||
|
@ -1,4 +1,4 @@
|
||||
.blockies {
|
||||
.avatar {
|
||||
aspect-ratio: 1/1;
|
||||
width: calc(var(--font-size-large) * 2) !important;
|
||||
height: calc(var(--font-size-large) * 2) !important;
|
||||
@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
.teaser {
|
||||
composes: box from '../atoms/Box.module.css';
|
||||
composes: box from '@shared/atoms/Box.module.css';
|
||||
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2);
|
||||
color: var(--color-secondary);
|
||||
position: relative;
|
53
src/components/Home/TopSales/Account/index.tsx
Normal file
53
src/components/Home/TopSales/Account/index.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import Dotdotdot from 'react-dotdotdot'
|
||||
import Link from 'next/link'
|
||||
import styles from './index.module.css'
|
||||
import { accountTruncate } from '@utils/web3'
|
||||
import Avatar from '../../../@shared/atoms/Avatar'
|
||||
import { getEnsProfile } from '@utils/ens'
|
||||
import { UserSales } from '@utils/aquarius'
|
||||
|
||||
declare type AccountProps = {
|
||||
account: UserSales
|
||||
place?: number
|
||||
}
|
||||
|
||||
export default function Account({
|
||||
account,
|
||||
place
|
||||
}: AccountProps): ReactElement {
|
||||
const [profile, setProfile] = useState<Profile>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!account?.id) return
|
||||
|
||||
async function getProfileData() {
|
||||
const profile = await getEnsProfile(account.id)
|
||||
if (!profile) return
|
||||
setProfile(profile)
|
||||
}
|
||||
getProfileData()
|
||||
}, [account?.id])
|
||||
|
||||
return (
|
||||
<Link href={`/profile/${profile?.name || account.id}`}>
|
||||
<a className={styles.teaser}>
|
||||
{place && <span className={styles.place}>{place}</span>}
|
||||
<Avatar
|
||||
accountId={account.id}
|
||||
className={styles.avatar}
|
||||
src={profile?.avatar}
|
||||
/>
|
||||
<div>
|
||||
<Dotdotdot tagName="h4" clamp={2} className={styles.name}>
|
||||
{profile?.name ? profile?.name : accountTruncate(account.id)}
|
||||
</Dotdotdot>
|
||||
<p className={styles.sales}>
|
||||
<span>{account.totalSales}</span>
|
||||
{`${account.totalSales === 1 ? ' sale' : ' sales'}`}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
11
src/components/Home/TopSales/AccountList/index.module.css
Normal file
11
src/components/Home/TopSales/AccountList/index.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
.list {
|
||||
composes: assetList from '@shared/AssetList/index.module.css';
|
||||
}
|
||||
|
||||
.loaderWrap {
|
||||
composes: loaderWrap from '@shared/AssetList/index.module.css';
|
||||
}
|
||||
|
||||
.empty {
|
||||
composes: empty from '@shared/AssetList/index.module.css';
|
||||
}
|
43
src/components/Home/TopSales/AccountList/index.tsx
Normal file
43
src/components/Home/TopSales/AccountList/index.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import styles from './index.module.css'
|
||||
import Loader from '../../../@shared/atoms/Loader'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import Account from 'src/components/Home/TopSales/Account'
|
||||
import { UserSales } from '@utils/aquarius'
|
||||
|
||||
function LoaderArea() {
|
||||
return (
|
||||
<div className={styles.loaderWrap}>
|
||||
<Loader />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
declare type AccountListProps = {
|
||||
accounts: UserSales[]
|
||||
isLoading: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function AccountList({
|
||||
accounts,
|
||||
isLoading
|
||||
}: AccountListProps): ReactElement {
|
||||
const { chainIds } = useUserPreferences()
|
||||
const emptyText =
|
||||
chainIds.length === 0 ? 'No network selected.' : 'No results found.'
|
||||
|
||||
return isLoading ? (
|
||||
<LoaderArea />
|
||||
) : (
|
||||
<div className={styles.list}>
|
||||
{accounts?.length > 0 ? (
|
||||
accounts.map((account, index) => (
|
||||
<Account account={account} key={index} place={index + 1} />
|
||||
))
|
||||
) : (
|
||||
<div className={styles.empty}>{emptyText}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
3
src/components/Home/TopSales/index.module.css
Normal file
3
src/components/Home/TopSales/index.module.css
Normal file
@ -0,0 +1,3 @@
|
||||
.section {
|
||||
composes: section from '../index.module.css';
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import AccountList from '@shared/AccountList/AccountList'
|
||||
import { getTopAssetsPublishers } from '@utils/subgraph'
|
||||
import AccountList from 'src/components/Home/TopSales/AccountList'
|
||||
import { getTopAssetsPublishers, UserSales } from '@utils/aquarius'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import styles from './index.module.css'
|
||||
|
||||
export default function PublishersWithMostSales({
|
||||
export default function TopSales({
|
||||
title,
|
||||
action
|
||||
}: {
|
||||
@ -13,14 +13,14 @@ export default function PublishersWithMostSales({
|
||||
action?: ReactElement
|
||||
}): ReactElement {
|
||||
const { chainIds } = useUserPreferences()
|
||||
const [result, setResult] = useState<AccountTeaserVM[]>([])
|
||||
const [result, setResult] = useState<UserSales[]>([])
|
||||
const [loading, setLoading] = useState<boolean>()
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
setLoading(true)
|
||||
if (chainIds.length === 0) {
|
||||
const result: AccountTeaserVM[] = []
|
||||
const result: UserSales[] = []
|
||||
setResult(result)
|
||||
setLoading(false)
|
||||
} else {
|
@ -5,11 +5,11 @@ import Bookmarks from './Bookmarks'
|
||||
import { generateBaseQuery, queryMetadata } from '@utils/aquarius'
|
||||
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import styles from './index.module.css'
|
||||
import { useIsMounted } from '@hooks/useIsMounted'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import { SortTermOptions } from '../../@types/aquarius/SearchQuery'
|
||||
import PublishersWithMostSales from './PublishersWithMostSales'
|
||||
import TopSales from './TopSales'
|
||||
import styles from './index.module.css'
|
||||
|
||||
function sortElements(items: Asset[], sorted: string[]) {
|
||||
items.sort(function (a, b) {
|
||||
@ -136,7 +136,7 @@ export default function HomePage(): ReactElement {
|
||||
}
|
||||
/>
|
||||
|
||||
<PublishersWithMostSales title="Publishers With Most Sales" />
|
||||
<TopSales title="Publishers With Most Sales" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -4,9 +4,10 @@ import ExplorerLink from '@shared/ExplorerLink'
|
||||
import NetworkName from '@shared/NetworkName'
|
||||
import Jellyfish from '@oceanprotocol/art/creatures/jellyfish/jellyfish-grid.svg'
|
||||
import Copy from '@shared/atoms/Copy'
|
||||
import Blockies from '@shared/atoms/Blockies'
|
||||
import Avatar from '@shared/atoms/Avatar'
|
||||
import styles from './Account.module.css'
|
||||
import { useProfile } from '@context/Profile'
|
||||
import { accountTruncate } from '@utils/web3'
|
||||
|
||||
export default function Account({
|
||||
accountId
|
||||
@ -19,28 +20,27 @@ export default function Account({
|
||||
return (
|
||||
<div className={styles.account}>
|
||||
<figure className={styles.imageWrap}>
|
||||
{profile?.image ? (
|
||||
<img
|
||||
src={profile?.image}
|
||||
{accountId ? (
|
||||
<Avatar
|
||||
accountId={accountId}
|
||||
src={profile?.avatar}
|
||||
className={styles.image}
|
||||
width="96"
|
||||
height="96"
|
||||
/>
|
||||
) : accountId ? (
|
||||
<Blockies accountId={accountId} className={styles.image} />
|
||||
) : (
|
||||
<Jellyfish className={styles.image} />
|
||||
)}
|
||||
</figure>
|
||||
|
||||
<div>
|
||||
<h3 className={styles.name}>{profile?.name}</h3>
|
||||
<h3 className={styles.name}>
|
||||
{profile?.name || accountTruncate(accountId)}
|
||||
</h3>
|
||||
{accountId && (
|
||||
<code
|
||||
className={styles.accountId}
|
||||
title={profile?.accountEns ? accountId : null}
|
||||
title={profile?.name ? accountId : null}
|
||||
>
|
||||
{profile?.accountEns || accountId} <Copy text={accountId} />
|
||||
{accountId} <Copy text={accountId} />
|
||||
</code>
|
||||
)}
|
||||
<p>
|
||||
|
@ -24,5 +24,8 @@
|
||||
}
|
||||
|
||||
.linksExternal {
|
||||
composes: linksExternal from '@shared/Publisher/index.module.css';
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
display: inline-block;
|
||||
fill: var(--color-secondary);
|
||||
}
|
||||
|
@ -6,6 +6,39 @@ import { useProfile } from '@context/Profile'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
function getLinkData(link: ProfileLink): { href: string; label: string } {
|
||||
let href, label
|
||||
|
||||
switch (link.key) {
|
||||
case 'url':
|
||||
href = link.value
|
||||
label = 'Website'
|
||||
break
|
||||
case 'com.twitter':
|
||||
href = `https://twitter.com/${link.value}`
|
||||
label = 'Twitter'
|
||||
break
|
||||
case 'com.github':
|
||||
href = `https://github.com/${link.value}`
|
||||
label = 'GitHub'
|
||||
break
|
||||
case 'org.telegram':
|
||||
href = `https://telegram.org/${link.value}`
|
||||
label = 'Telegram'
|
||||
break
|
||||
case 'com.discord':
|
||||
href = `https://discordapp.com/users/${link.value}`
|
||||
label = 'Discord'
|
||||
break
|
||||
case 'com.reddit':
|
||||
href = `https://reddit.com/u/${link.value}`
|
||||
label = 'Reddit'
|
||||
break
|
||||
}
|
||||
|
||||
return { href, label }
|
||||
}
|
||||
|
||||
export default function PublisherLinks({
|
||||
className
|
||||
}: {
|
||||
@ -21,22 +54,17 @@ export default function PublisherLinks({
|
||||
return (
|
||||
<div className={styleClasses}>
|
||||
{' — '}
|
||||
{profile?.links?.map((link) => {
|
||||
const href =
|
||||
link.name === 'Twitter'
|
||||
? `https://twitter.com/${link.value}`
|
||||
: link.name === 'GitHub'
|
||||
? `https://github.com/${link.value}`
|
||||
: link.value.includes('http') // safeguard against urls without protocol defined
|
||||
? link.value
|
||||
: `//${link.value}`
|
||||
|
||||
return (
|
||||
<a href={href} key={link.name} target="_blank" rel="noreferrer">
|
||||
{link.name} <External className={styles.linksExternal} />
|
||||
{profile?.links?.map((link) => (
|
||||
<a
|
||||
href={getLinkData(link).href}
|
||||
key={link.key}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{getLinkData(link).label}{' '}
|
||||
<External className={styles.linksExternal} />
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -43,8 +43,8 @@ export default function AccountHeader({
|
||||
{isDescriptionTextClamped() ? (
|
||||
<span className={styles.more} onClick={toogleShowMore}>
|
||||
<LinkExternal
|
||||
url={`https://www.3box.io/${accountId}`}
|
||||
text="Read more on 3box"
|
||||
url={`https://app.ens.domains/name/${profile?.name}`}
|
||||
text="Read more on ENS"
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
@ -56,18 +56,9 @@ export default function AccountHeader({
|
||||
</div>
|
||||
<div className={styles.meta}>
|
||||
Profile data from{' '}
|
||||
{profile?.accountEns && (
|
||||
<>
|
||||
<LinkExternal
|
||||
url={`https://app.ens.domains/name/${profile.accountEns}`}
|
||||
url={`https://app.ens.domains/name/${profile?.name}`}
|
||||
text="ENS"
|
||||
/>{' '}
|
||||
&{' '}
|
||||
</>
|
||||
)}
|
||||
<LinkExternal
|
||||
url={`https://www.3box.io/${accountId}`}
|
||||
text="3Box Hub"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,7 +13,7 @@ import Navigation from './Navigation'
|
||||
import { Steps } from './Steps'
|
||||
import { FormPublishData } from './_types'
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import useNftFactory from '@hooks/contracts/useNftFactory'
|
||||
import useNftFactory from '@hooks/useNftFactory'
|
||||
import { ProviderInstance, LoggerInstance, DDO } from '@oceanprotocol/lib'
|
||||
import { getOceanConfig } from '@utils/ocean'
|
||||
import { validationSchema } from './_validation'
|
||||
|
@ -28,7 +28,7 @@ export default function PageProfile(): ReactElement {
|
||||
|
||||
const pathAccount = router.query.account as string
|
||||
|
||||
// Path has ETH addreess
|
||||
// Path has ETH address
|
||||
if (web3.utils.isAddress(pathAccount)) {
|
||||
const finalAccountId = pathAccount || accountId
|
||||
setFinalAccountId(finalAccountId)
|
||||
@ -40,6 +40,11 @@ export default function PageProfile(): ReactElement {
|
||||
// Path has ENS name
|
||||
setFinalAccountEns(pathAccount)
|
||||
const resolvedAccountId = await getEnsAddress(pathAccount)
|
||||
if (
|
||||
!resolvedAccountId ||
|
||||
resolvedAccountId === '0x0000000000000000000000000000000000000000'
|
||||
)
|
||||
return
|
||||
setFinalAccountId(resolvedAccountId)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user