1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

3Box publisher profiles (#264)

* install 3box

Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro>

* tinkering

* check tweak

* load library on init only

* add profile

Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro>

* get 3box profile

Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro>

* fix return type

Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro>

* remove console.log

* fix travis

Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro>

* fix eslit

Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro>

* fix travis

Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro>

* 3box data structure tweaks, prepare output in byline

* refactor

* new Publisher component

* tweaks

* remove data partners

* link/profile splitup

* profile tweaks

* component splitup

* lots of styling, add image

* affordance for publisher, refactor, server response tinkering

* use 3Box proxy

* open all 3box links in new tab/window

* mobile fixes

Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
This commit is contained in:
mihaisc 2020-11-27 13:04:35 +02:00 committed by GitHub
parent 35d9b6faec
commit dac47bf524
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 510 additions and 485 deletions

83
package-lock.json generated
View File

@ -3566,40 +3566,16 @@
"web3-eth-contract": "^1.3.0" "web3-eth-contract": "^1.3.0"
} }
}, },
"@oceanprotocol/list-datapartners": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@oceanprotocol/list-datapartners/-/list-datapartners-1.0.3.tgz",
"integrity": "sha512-MMyy81FvnRGwl2cQ4+cucq/YWjUTGzStHyAUVM6P2pFA8zMc3jouuWN2WSAjmvhxeKZU7jvJRwZCoi+miEYKjw=="
},
"@oceanprotocol/react": { "@oceanprotocol/react": {
"version": "0.3.21", "version": "0.3.22",
"resolved": "https://registry.npmjs.org/@oceanprotocol/react/-/react-0.3.21.tgz", "resolved": "https://registry.npmjs.org/@oceanprotocol/react/-/react-0.3.22.tgz",
"integrity": "sha512-3fY3oYbJ1I2bTZzWqaHbQvVCI9Xdn3Oa0BOeWPA0757wT6gOQzdMcE/zKAFqzI6L/4006kTQF3Ls0UnKVkocUA==", "integrity": "sha512-pPXi+4syzYWczfJXd292Wu7eJoaUZ1cjB7Js3TWE5E/YAZzQAYuaiUn9Lh5P8vHmxz8dl4Xn586jhRPWzyIRIw==",
"requires": { "requires": {
"@oceanprotocol/lib": "^0.9.18", "@oceanprotocol/lib": "^0.9.18",
"axios": "^0.21.0", "axios": "^0.21.0",
"decimal.js": "^10.2.1", "decimal.js": "^10.2.1",
"web3": "^1.3.0", "web3": "^1.3.0",
"web3modal": "^1.9.1" "web3modal": "^1.9.1"
},
"dependencies": {
"@oceanprotocol/lib": {
"version": "0.9.18",
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.9.18.tgz",
"integrity": "sha512-RsP4CjAnauI2kDH0923LOO3NhdKNB1y8WwpAviVwIwz9sYqsIIcac6MKXIm5oDeLNhmCIhJXbwvQehf17wRL5Q==",
"requires": {
"@ethereum-navigator/navigator": "^0.5.0",
"@oceanprotocol/contracts": "^0.5.7",
"decimal.js": "^10.2.0",
"fs": "0.0.1-security",
"lzma": "^2.3.2",
"node-fetch": "^2.6.1",
"save-file": "^2.3.1",
"uuid": "^8.3.0",
"web3": "^1.3.0",
"web3-eth-contract": "^1.3.0"
}
}
} }
}, },
"@oceanprotocol/typographies": { "@oceanprotocol/typographies": {
@ -5807,17 +5783,6 @@
"@types/reactcss": "*" "@types/reactcss": "*"
} }
}, },
"@types/react-datepicker": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-3.1.1.tgz",
"integrity": "sha512-vwNrgaIMJThvvwmtnA8jSVVJZ0FNgljQrq1jDA4MtYJIDmVmd9NNrFaXf9u2JqR2nS+8Kvi8OVs/tnAbUqZhHw==",
"dev": true,
"requires": {
"@types/react": "*",
"date-fns": "^2.0.1",
"popper.js": "^1.14.1"
}
},
"@types/react-helmet": { "@types/react-helmet": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.0.tgz",
@ -23236,6 +23201,11 @@
"resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz",
"integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==" "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ=="
}, },
"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": { "keccak": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz",
@ -26351,7 +26321,8 @@
"popper.js": { "popper.js": {
"version": "1.16.1", "version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
"dev": true
}, },
"portfinder": { "portfinder": {
"version": "1.0.28", "version": "1.0.28",
@ -27586,14 +27557,6 @@
"object-assign": "^4.1.1" "object-assign": "^4.1.1"
} }
}, },
"react-alice-carousel": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/react-alice-carousel/-/react-alice-carousel-2.0.2.tgz",
"integrity": "sha512-Uy9tOPAmwazPbq9uTygkk0zvCbMDWrtiBk7XnK5DTxkN3dLGxaCL3AKiCLLQOfnKbxZRGyXROLjlGYZ1aEpALg==",
"requires": {
"vanilla-swipe": "^2.2.0"
}
},
"react-chartjs-2": { "react-chartjs-2": {
"version": "2.11.1", "version": "2.11.1",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.11.1.tgz", "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.11.1.tgz",
@ -27628,18 +27591,6 @@
"shortid": "^2.2.15" "shortid": "^2.2.15"
} }
}, },
"react-datepicker": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-3.3.0.tgz",
"integrity": "sha512-QnIlBxDSWEGBi2X5P1BqWzvfnPFRKhtrsgAcujUVwyWeID/VatFaAOEjEjfD1bXR9FuSYVLlLR3j/vbG19hWOA==",
"requires": {
"classnames": "^2.2.6",
"date-fns": "^2.0.1",
"prop-types": "^15.7.2",
"react-onclickoutside": "^6.9.0",
"react-popper": "^1.3.4"
}
},
"react-dev-utils": { "react-dev-utils": {
"version": "10.2.1", "version": "10.2.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz",
@ -28288,11 +28239,6 @@
"warning": "^4.0.3" "warning": "^4.0.3"
} }
}, },
"react-onclickoutside": {
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz",
"integrity": "sha512-8ltIY3bC7oGhj2nPAvWOGi+xGFybPNhJM0V1H8hY/whNcXgmDeaeoCMPPd8VatrpTsUWjb/vGzrmu6SrXVty3A=="
},
"react-paginate": { "react-paginate": {
"version": "6.5.0", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/react-paginate/-/react-paginate-6.5.0.tgz", "resolved": "https://registry.npmjs.org/react-paginate/-/react-paginate-6.5.0.tgz",
@ -28305,6 +28251,7 @@
"version": "1.3.7", "version": "1.3.7",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz",
"integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==", "integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==",
"dev": true,
"requires": { "requires": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.1.2",
"create-react-context": "^0.3.0", "create-react-context": "^0.3.0",
@ -32889,7 +32836,8 @@
"typed-styles": { "typed-styles": {
"version": "0.0.7", "version": "0.0.7",
"resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz",
"integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==",
"dev": true
}, },
"typedarray": { "typedarray": {
"version": "0.0.6", "version": "0.0.6",
@ -33601,11 +33549,6 @@
"spdx-expression-parse": "^3.0.0" "spdx-expression-parse": "^3.0.0"
} }
}, },
"vanilla-swipe": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/vanilla-swipe/-/vanilla-swipe-2.2.0.tgz",
"integrity": "sha512-iNXEIpPTe2KMOzHyi0lKP9rSSHry+SoHAc0aBHH4xcJBjKX5cnw6DcxgT3OoWsqxHQ6O1wmG4PAG9BOzzR5jGQ=="
},
"varint": { "varint": {
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz", "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz",

View File

@ -25,7 +25,6 @@
"@loadable/component": "^5.14.1", "@loadable/component": "^5.14.1",
"@oceanprotocol/art": "^3.0.0", "@oceanprotocol/art": "^3.0.0",
"@oceanprotocol/lib": "^0.9.18", "@oceanprotocol/lib": "^0.9.18",
"@oceanprotocol/list-datapartners": "^1.0.3",
"@oceanprotocol/react": "^0.3.22", "@oceanprotocol/react": "^0.3.22",
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@sindresorhus/slugify": "^1.0.0", "@sindresorhus/slugify": "^1.0.0",
@ -62,14 +61,13 @@
"gatsby-transformer-sharp": "^2.5.21", "gatsby-transformer-sharp": "^2.5.21",
"intersection-observer": "^0.11.0", "intersection-observer": "^0.11.0",
"is-url-superb": "^4.0.0", "is-url-superb": "^4.0.0",
"jwt-decode": "^3.1.2",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0", "lodash.omit": "^4.5.0",
"query-string": "^6.13.7", "query-string": "^6.13.7",
"react": "^17.0.1", "react": "^17.0.1",
"react-alice-carousel": "^2.0.2",
"react-chartjs-2": "^2.11.1", "react-chartjs-2": "^2.11.1",
"react-data-table-component": "^6.11.5", "react-data-table-component": "^6.11.5",
"react-datepicker": "^3.3.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-dotdotdot": "^1.3.1", "react-dotdotdot": "^1.3.1",
"react-dropzone": "^11.2.3", "react-dropzone": "^11.2.3",
@ -104,7 +102,6 @@
"@types/lodash.omit": "^4.5.6", "@types/lodash.omit": "^4.5.6",
"@types/node": "^14.14.6", "@types/node": "^14.14.6",
"@types/react": "^16.9.56", "@types/react": "^16.9.56",
"@types/react-datepicker": "^3.1.1",
"@types/react-helmet": "^6.1.0", "@types/react-helmet": "^6.1.0",
"@types/react-modal": "^3.10.6", "@types/react-modal": "^3.10.6",
"@types/react-paginate": "^6.2.1", "@types/react-paginate": "^6.2.1",

View File

@ -1,24 +0,0 @@
.partner {
font-weight: var(--font-weight-bold);
margin-top: calc(var(--spacer) / 8);
margin-bottom: 0;
}
.badge {
composes: badge from '../atoms/Badge.module.css';
border-radius: 50%;
width: var(--font-size-h4);
height: var(--font-size-h4);
padding: 0.15rem;
vertical-align: middle;
margin-right: 0.2rem;
position: relative;
top: -0.1rem;
}
.badge svg {
fill: currentColor;
display: inline-block;
width: 100%;
height: 100%;
}

View File

@ -1,48 +0,0 @@
import React, { ReactElement } from 'react'
import { ReactComponent as PartnerIcon } from '../../images/partner.svg'
import styles from './Partner.module.css'
import classNames from 'classnames/bind'
import Tooltip from './Tooltip'
import { PartnerData } from '@oceanprotocol/list-datapartners/types'
const cx = classNames.bind(styles)
export function PartnerBadge(): ReactElement {
return (
<span className={styles.badge}>
<PartnerIcon />
</span>
)
}
export default function Partner({
partner,
className
}: {
partner: PartnerData
className?: string
}): ReactElement {
const styleClasses = cx({
partner: true,
[className]: className
})
return (
<span className={styleClasses}>
<Tooltip
content={
<>
Ocean Protocol{' '}
<a href="https://github.com/oceanprotocol/list-datapartners">
Data Partner
</a>
</>
}
placement="top"
>
<PartnerBadge />
{partner.name}
</Tooltip>
</span>
)
}

View File

@ -0,0 +1,7 @@
.add {
color: var(--brand-pink);
}
.linksExternal {
composes: linksExternal from './index.module.css';
}

View File

@ -0,0 +1,16 @@
import React, { ReactElement } from 'react'
import { ReactComponent as 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>
)
}

View File

@ -0,0 +1,71 @@
.profile {
background: var(--background-highlight);
border-radius: var(--border-radius);
padding: calc(var(--spacer) / 2);
margin-bottom: calc(var(--spacer) / 4);
}
@media (min-width: 40rem) {
.profile {
margin: calc(var(--spacer) / 4);
}
}
.profile p {
margin-bottom: calc(var(--spacer) / 4);
}
.profile code {
padding: 0;
color: var(--color-secondary);
font-size: var(--font-size-mini);
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
}
.header {
margin-bottom: calc(var(--spacer) / 4);
text-align: center;
}
.header::after {
content: '';
display: block;
margin: calc(var(--spacer) / 2) auto;
width: 20%;
height: 2px;
background: var(--border-color);
}
.image {
width: 48px;
height: 48px;
border-radius: 50%;
overflow: hidden;
display: inline-block;
margin-bottom: calc(var(--spacer) / 4);
border: 1px solid var(--border-color);
box-shadow: 0 6px 17px 0 var(--box-shadow-color);
}
.title {
font-size: var(--font-size-base);
margin-bottom: 0;
}
.description {
font-size: var(--font-size-small);
}
.profile p:last-child {
margin-bottom: 0;
}
.meta {
color: var(--color-secondary);
font-size: var(--font-size-mini);
text-align: right;
margin-left: calc(var(--spacer) / 4);
margin-right: calc(var(--spacer) / 4);
}

View File

@ -0,0 +1,51 @@
import React, { ReactElement } from 'react'
import styles from './ProfileDetails.module.css'
import { Profile } from '../../../models/Profile'
import EtherscanLink from '../EtherscanLink'
import PublisherLinks from './PublisherLinks'
export default function ProfileDetails({
profile,
networkId,
account
}: {
profile: Profile
networkId: number
account: string
}): ReactElement {
return (
<>
<div className={styles.profile}>
<header className={styles.header}>
{profile?.image && (
<div className={styles.image}>
<img src={profile.image} width="48" height="48" />
</div>
)}
<h3 className={styles.title}>
{profile?.emoji} {profile?.name}
</h3>
<EtherscanLink networkId={networkId} path={`address/${account}`}>
<code>{account}</code>
</EtherscanLink>
</header>
{profile?.description && (
<p className={styles.description}>{profile?.description}</p>
)}
<PublisherLinks links={profile?.links} />
</div>
<div className={styles.meta}>
Profile data from{' '}
<a
href={`https://www.3box.io/${account}`}
target="_blank"
rel="noreferrer"
>
3Box Hub
</a>
</div>
</>
)
}

View File

@ -0,0 +1,23 @@
.links,
.links a {
font-size: var(--font-size-small);
color: var(--color-secondary);
}
.links a {
margin-left: calc(var(--spacer) / 3);
color: inherit;
}
.links a:first-child {
margin-left: 0;
}
.links a:hover,
.links a:focus {
color: var(--brand-pink);
}
.linksExternal {
composes: linksExternal from './index.module.css';
}

View File

@ -0,0 +1,30 @@
import React, { ReactElement } from 'react'
import styles from './PublisherLinks.module.css'
import { ProfileLink } from '../../../models/Profile'
import { ReactComponent as External } from '../../../images/external.svg'
export default function PublisherLinks({
links
}: {
links: ProfileLink[]
}): ReactElement {
return (
<div className={styles.links}>
{' — '}
{links?.map((link: ProfileLink) => {
const href =
link.name === 'Twitter'
? `https://twitter.com/${link.value}`
: link.name === 'GitHub'
? `https://github.com/${link.value}`
: link.value
return (
<a href={href} key={link.name} target="_blank" rel="noreferrer">
{link.name} <External className={styles.linksExternal} />
</a>
)
})}
</div>
)
}

View File

@ -0,0 +1,47 @@
.publisher {
margin: 0;
}
@media (min-width: 32rem) {
.publisher {
display: inline-block;
}
}
.links {
display: inline;
}
.links a,
.links span {
margin-left: calc(var(--spacer) / 3);
font-size: var(--font-size-mini);
}
.links a:first-child,
.links span:first-child {
margin-left: 0;
}
.links a:hover,
.links a:focus {
color: var(--brand-pink);
}
.linksExternal {
width: 6px;
height: 6px;
display: inline-block;
fill: var(--color-secondary);
}
.detailsTrigger {
cursor: help;
}
.detailsTrigger svg {
width: 10px;
height: 10px;
position: relative;
bottom: -1px;
}

View File

@ -0,0 +1,99 @@
import React, { ReactElement, useEffect, useState } from 'react'
import styles from './index.module.css'
import classNames from 'classnames/bind'
import Tooltip from '../Tooltip'
import { Profile } from '../../../models/Profile'
import { Link } from 'gatsby'
import get3BoxProfile from '../../../utils/profile'
import EtherscanLink from '../EtherscanLink'
import { accountTruncate } from '../../../utils/wallet'
import axios from 'axios'
import { useOcean } from '@oceanprotocol/react'
import { ReactComponent as Info } from '../../../images/info.svg'
import ProfileDetails from './ProfileDetails'
import Add from './Add'
const cx = classNames.bind(styles)
export default function Publisher({
account,
minimal,
className
}: {
account: string
minimal?: boolean
className?: string
}): ReactElement {
const { networkId, accountId } = useOcean()
const [profile, setProfile] = useState<Profile>()
const [name, setName] = useState<string>()
const showAdd = account === accountId && !profile
useEffect(() => {
if (!account) return
setName(accountTruncate(account))
const source = axios.CancelToken.source()
async function get3Box() {
const profile = await get3BoxProfile(account, source.token)
if (!profile) return
setProfile(profile)
const { name } = profile
name && setName(name)
}
get3Box()
return () => {
source.cancel()
}
}, [account])
const styleClasses = cx({
publisher: true,
[className]: className
})
return (
<div className={styleClasses}>
{minimal ? (
name
) : (
<>
<Link
to={`/search/?owner=${account}`}
title="Show all data sets created by this account."
>
{name}
</Link>
<div className={styles.links}>
{' — '}
{profile && (
<Tooltip
placement="bottom"
content={
<ProfileDetails
profile={profile}
networkId={networkId}
account={account}
/>
}
>
<span className={styles.detailsTrigger}>
Profile <Info className={styles.linksExternal} />
</span>
</Tooltip>
)}
{showAdd && <Add />}
<EtherscanLink networkId={networkId} path={`address/${account}`}>
Etherscan
</EtherscanLink>
</div>
</>
)}
</div>
)
}

View File

@ -5,6 +5,7 @@
.content { .content {
composes: box from './Box.module.css'; composes: box from './Box.module.css';
padding: calc(var(--spacer) / 4); padding: calc(var(--spacer) / 4);
width: calc(100% - var(--spacer) / 3);
max-width: 25rem; max-width: 25rem;
font-size: var(--font-size-small); font-size: var(--font-size-small);
} }

View File

@ -41,7 +41,7 @@ export default function Tooltip({
function onMount() { function onMount() {
setSpring({ setSpring({
transform: 'scale(1) translateY(0)', ...animation.to,
onRest: (): void => null, onRest: (): void => null,
config: animation.config config: animation.config
}) })

View File

@ -2,25 +2,20 @@ import { DDO } from '@oceanprotocol/lib'
import { useOcean } from '@oceanprotocol/react' import { useOcean } from '@oceanprotocol/react'
import { Link } from 'gatsby' import { Link } from 'gatsby'
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import { useDataPartner } from '../../hooks/useDataPartner'
import { retrieveDDO } from '../../utils/aquarius' import { retrieveDDO } from '../../utils/aquarius'
import { PartnerBadge } from '../atoms/Partner'
import styles from './AssetListTitle.module.css' import styles from './AssetListTitle.module.css'
import axios from 'axios' import axios from 'axios'
export default function AssetListTitle({ export default function AssetListTitle({
ddo, ddo,
did, did,
title, title
owner
}: { }: {
ddo?: DDO ddo?: DDO
did?: string did?: string
title?: string title?: string
owner?: string
}): ReactElement { }): ReactElement {
const { config } = useOcean() const { config } = useOcean()
const { partner } = useDataPartner(owner)
const [assetTitle, setAssetTitle] = useState<string>(title) const [assetTitle, setAssetTitle] = useState<string>(title)
useEffect(() => { useEffect(() => {
@ -49,9 +44,7 @@ export default function AssetListTitle({
return ( return (
<h3 className={styles.title}> <h3 className={styles.title}>
<Link to={`/asset/${did || ddo.id}`}> <Link to={`/asset/${did || ddo.id}`}>{assetTitle}</Link>
{partner && <PartnerBadge />} {assetTitle}
</Link>
</h3> </h3>
) )
} }

View File

@ -6,9 +6,8 @@ import styles from './AssetTeaser.module.css'
import { DDO } from '@oceanprotocol/lib' import { DDO } from '@oceanprotocol/lib'
import removeMarkdown from 'remove-markdown' import removeMarkdown from 'remove-markdown'
import Tooltip from '../atoms/Tooltip' import Tooltip from '../atoms/Tooltip'
import Publisher from '../atoms/Publisher'
import { useMetadata } from '@oceanprotocol/react' import { useMetadata } from '@oceanprotocol/react'
import Partner from '../atoms/Partner'
import { useDataPartner } from '../../hooks/useDataPartner'
declare type AssetTeaserProps = { declare type AssetTeaserProps = {
ddo: DDO ddo: DDO
@ -16,8 +15,6 @@ declare type AssetTeaserProps = {
const AssetTeaser: React.FC<AssetTeaserProps> = ({ ddo }: AssetTeaserProps) => { const AssetTeaser: React.FC<AssetTeaserProps> = ({ ddo }: AssetTeaserProps) => {
const { owner } = useMetadata(ddo) const { owner } = useMetadata(ddo)
const { partner } = useDataPartner(owner)
const { attributes } = ddo.findServiceByType('metadata') const { attributes } = ddo.findServiceByType('metadata')
const { name } = attributes.main const { name } = attributes.main
const { dataTokenInfo } = ddo const { dataTokenInfo } = ddo
@ -34,7 +31,7 @@ const AssetTeaser: React.FC<AssetTeaserProps> = ({ ddo }: AssetTeaserProps) => {
{dataTokenInfo?.symbol} {dataTokenInfo?.symbol}
</Tooltip> </Tooltip>
<h1 className={styles.title}>{name}</h1> <h1 className={styles.title}>{name}</h1>
{partner && <Partner className={styles.partner} partner={partner} />} <Publisher account={owner} minimal />
{isCompute && <div className={styles.accessLabel}>Compute</div>} {isCompute && <div className={styles.accessLabel}>Compute</div>}
<div className={styles.content}> <div className={styles.content}>

View File

@ -54,13 +54,7 @@ const columns = [
name: 'Data Set', name: 'Data Set',
selector: function getAssetRow(row: DDO) { selector: function getAssetRow(row: DDO) {
const { attributes } = row.findServiceByType('metadata') const { attributes } = row.findServiceByType('metadata')
return ( return <AssetTitle title={attributes.main.name} ddo={row} />
<AssetTitle
title={attributes.main.name}
ddo={row}
owner={row.publicKey[0].owner}
/>
)
}, },
maxWidth: '45rem', maxWidth: '45rem',
grow: 1 grow: 1

View File

@ -1,31 +0,0 @@
.byline {
display: inline-block;
}
@media (min-width: 40rem) {
.bylineLinks {
display: inline;
}
}
.bylineLinks a {
margin-left: calc(var(--spacer) / 3);
color: inherit;
font-size: var(--font-size-mini);
}
.bylineLinks a:first-child {
margin-left: 0;
}
.bylineLinks a:hover,
.bylineLinks a:focus {
color: var(--brand-pink);
}
.bylineExternal {
width: 0.6em;
height: 0.6em;
display: inline-block;
fill: var(--brand-grey-light);
}

View File

@ -1,48 +0,0 @@
import React, { ReactElement } from 'react'
import styles from './Byline.module.css'
import { accountTruncate } from '../../../utils/wallet'
import Partner from '../../atoms/Partner'
import { ReactComponent as External } from '../../../images/external.svg'
import { Link } from 'gatsby'
import EtherscanLink from '../../atoms/EtherscanLink'
import { useOcean } from '@oceanprotocol/react'
import { useDataPartner } from '../../../hooks/useDataPartner'
export default function Byline({
owner,
prefix
}: {
owner: string
prefix?: string
}): ReactElement {
const { networkId } = useOcean()
const { partner } = useDataPartner(owner)
return (
<div className={styles.byline}>
{prefix}
<Link
to={`/search/?owner=${owner}`}
title="Show all data sets created by this account."
>
{partner ? (
<Partner partner={partner} />
) : (
owner && accountTruncate(owner)
)}
</Link>
<div className={styles.bylineLinks}>
{' — '}
{partner &&
Object.entries(partner.links).map(([key, value]) => (
<a href={value} key={key} target="_blank" rel="noreferrer">
{key} <External className={styles.bylineExternal} />
</a>
))}
<EtherscanLink networkId={networkId} path={`address/${owner}`}>
Etherscan
</EtherscanLink>
</div>
</div>
)
}

View File

@ -4,7 +4,7 @@ import MetaItem from './MetaItem'
import styles from './MetaFull.module.css' import styles from './MetaFull.module.css'
import { MetadataMarket } from '../../../@types/MetaData' import { MetadataMarket } from '../../../@types/MetaData'
import { DDO } from '@oceanprotocol/lib' import { DDO } from '@oceanprotocol/lib'
import Byline from './Byline' import Publisher from '../../atoms/Publisher'
export default function MetaFull({ export default function MetaFull({
ddo, ddo,
@ -18,8 +18,11 @@ export default function MetaFull({
return ( return (
<div className={styles.metaFull}> <div className={styles.metaFull}>
<MetaItem title="Author" content={metadata?.main.author} /> <MetaItem title="Data Author" content={metadata?.main.author} />
<MetaItem title="Owner" content={<Byline owner={publicKey[0].owner} />} /> <MetaItem
title="Owner"
content={<Publisher account={publicKey[0].owner} />}
/>
{metadata?.additionalInformation?.categories && ( {metadata?.additionalInformation?.categories && (
<MetaItem <MetaItem

View File

@ -34,10 +34,6 @@
margin-bottom: 0; margin-bottom: 0;
} }
.author {
font-weight: var(--font-weight-bold);
}
.datatoken a { .datatoken a {
color: var(--color-secondary); color: var(--color-secondary);
} }

View File

@ -9,10 +9,10 @@ import AssetActions from '../AssetActions'
import { DDO } from '@oceanprotocol/lib' import { DDO } from '@oceanprotocol/lib'
import { useUserPreferences } from '../../../providers/UserPreferences' import { useUserPreferences } from '../../../providers/UserPreferences'
import Pricing from './Pricing' import Pricing from './Pricing'
import { useMetadata, useOcean, usePricing } from '@oceanprotocol/react' import { useOcean, usePricing } from '@oceanprotocol/react'
import EtherscanLink from '../../atoms/EtherscanLink' import EtherscanLink from '../../atoms/EtherscanLink'
import Bookmark from './Bookmark' import Bookmark from './Bookmark'
import Byline from './Byline' import Publisher from '../../atoms/Publisher'
import { useAsset } from '../../../providers/Asset' import { useAsset } from '../../../providers/Asset'
export interface AssetContentProps { export interface AssetContentProps {
@ -27,7 +27,7 @@ export default function AssetContent({
}: AssetContentProps): ReactElement { }: AssetContentProps): ReactElement {
const { debug } = useUserPreferences() const { debug } = useUserPreferences()
const { accountId, networkId } = useOcean() const { accountId, networkId } = useOcean()
const { owner } = useMetadata(ddo) const { owner } = useAsset()
const { dtSymbol, dtName } = usePricing(ddo) const { dtSymbol, dtName } = usePricing(ddo)
const [showPricing, setShowPricing] = useState(false) const [showPricing, setShowPricing] = useState(false)
const { price } = useAsset() const { price } = useAsset()
@ -41,9 +41,6 @@ export default function AssetContent({
<div> <div>
{showPricing && <Pricing ddo={ddo} />} {showPricing && <Pricing ddo={ddo} />}
<div className={styles.content}> <div className={styles.content}>
<p className={styles.author} title="Author">
{metadata?.main.author}
</p>
{metadata?.additionalInformation?.categories?.length && ( {metadata?.additionalInformation?.categories?.length && (
<p> <p>
<Link <Link
@ -67,7 +64,7 @@ export default function AssetContent({
)} )}
</EtherscanLink> </EtherscanLink>
</p> </p>
<Byline owner={owner} prefix="Published by " /> Published By <Publisher account={owner} />
</aside> </aside>
<Markdown <Markdown

View File

@ -1,36 +0,0 @@
.assetCarousel [class*='alice-carousel__dots-item'] {
width: var(--font-size-mini);
height: var(--font-size-mini);
background: var(--color-secondary);
border: 1px solid var(--font-color-heading);
background: none;
}
.assetCarousel [class*='alice-carousel__dots-item']:hover,
.assetCarousel [class*='alice-carousel__dots-item __active'] {
background: var(--font-color-heading) !important;
}
.assetCarousel [class='alice-carousel__wrapper'] {
padding-bottom: calc(var(--spacer) / 2);
}
.assetCarousel [class='alice-carousel__stage'] > li article {
margin-left: calc(var(--spacer) / 2);
margin-right: calc(var(--spacer) / 2);
}
.assetCarousel [class='alice-carousel__dots'] {
margin-top: calc(var(--spacer) / 2);
}
.assetCarousel [class*='alice-carousel__stage-item'] *,
.assetCarousel [class*='alice-carousel__stage-item'] {
line-height: var(--line-height);
}
.empty {
color: var(--color-secondary);
font-size: var(--font-size-small);
font-style: italic;
}

View File

@ -1,59 +0,0 @@
import AssetTeaser from '../molecules/AssetTeaser'
import React from 'react'
import { QueryResult } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import styles from './AssetQueryCarousel.module.css'
import { DDO } from '@oceanprotocol/lib'
import classNames from 'classnames/bind'
import AliceCarousel from 'react-alice-carousel'
import 'react-alice-carousel/lib/alice-carousel.css'
const cx = classNames.bind(styles)
declare type AssetQueryCarouselProps = {
queryResult: QueryResult
className?: string
}
const responsive = {
0: { items: 1 },
600: { items: 2 },
1280: { items: 3 },
1600: { items: 4 },
2400: { items: 6 }
}
const AssetQueryCarousel: React.FC<AssetQueryCarouselProps> = ({
queryResult,
className
}) => {
const styleClasses = cx({
assetCarousel: true,
[className]: className
})
const items =
queryResult?.results.length > 0
? queryResult.results.map((ddo: DDO) => (
<AssetTeaser key={ddo.id} ddo={ddo} />
))
: [
<div className={styles.empty} key="empty">
No results found.
</div>
]
return (
<div className={styleClasses}>
<AliceCarousel
items={items}
responsive={responsive}
paddingLeft={10}
paddingRight={10}
disableButtonsControls
animationDuration={300}
/>
</div>
)
}
export default AssetQueryCarousel

View File

@ -28,8 +28,7 @@ const columns = [
{ {
name: 'Data Set', name: 'Data Set',
selector: function getAssetRow(row: Asset) { selector: function getAssetRow(row: Asset) {
const { owner } = row.ddo.publicKey[0] return <AssetTitle ddo={row.ddo} />
return <AssetTitle ddo={row.ddo} owner={owner} />
}, },
grow: 2 grow: 2
}, },

View File

@ -17,19 +17,6 @@
margin-top: var(--spacer); margin-top: var(--spacer);
} }
.listPartners {
composes: section;
}
.listPartners > div {
background: var(--background-highlight);
padding-top: var(--spacer);
padding-bottom: var(--spacer);
min-height: 360px;
margin-left: calc(-50vw + 50%);
margin-right: calc(-50vw + 50%);
}
.loaderWrap { .loaderWrap {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -8,33 +8,9 @@ import Loader from '../atoms/Loader'
import { useOcean } from '@oceanprotocol/react' import { useOcean } from '@oceanprotocol/react'
import Button from '../atoms/Button' import Button from '../atoms/Button'
import Bookmarks from '../molecules/Bookmarks' import Bookmarks from '../molecules/Bookmarks'
import listPartners from '@oceanprotocol/list-datapartners'
import Tooltip from '../atoms/Tooltip'
import AssetQueryCarousel from '../organisms/AssetQueryCarousel'
import axios from 'axios' import axios from 'axios'
import { queryMetadata } from '../../utils/aquarius' import { queryMetadata } from '../../utils/aquarius'
const partnerAccounts = listPartners
.map((partner) => partner.accounts.join(','))
.filter((account) => account !== '')
const searchAccounts = JSON.stringify(partnerAccounts)
.replace(/"/g, '')
.replace(/,/g, ' OR ')
.replace(/(\[|\])/g, '')
const queryPartners = {
page: 1,
offset: 100,
query: {
nativeSearch: 1,
query_string: {
query: `(publicKey.owner:${searchAccounts}) -isInPurgatory:true`
}
},
sort: { created: -1 }
}
const queryHighest = { const queryHighest = {
page: 1, page: 1,
offset: 9, offset: 9,
@ -116,68 +92,12 @@ function SectionQueryResult({
} }
export default function HomePage(): ReactElement { export default function HomePage(): ReactElement {
const { config } = useOcean()
const [queryResultPartners, setQueryResultPartners] = useState<QueryResult>()
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!config?.metadataCacheUri) return
const source = axios.CancelToken.source()
async function init() {
// TODO: remove any once ocean.js has nativeSearch typings
const queryResultPartners = await queryMetadata(
queryPartners as any,
config.metadataCacheUri,
source.token
)
setQueryResultPartners(queryResultPartners)
setLoading(false)
}
init()
return () => {
source.cancel()
}
}, [config?.metadataCacheUri])
return ( return (
<> <>
<Container narrow className={styles.searchWrap}> <Container narrow className={styles.searchWrap}>
<SearchBar size="large" /> <SearchBar size="large" />
</Container> </Container>
<section className={styles.listPartners}>
<h3>
Data Partners{' '}
<Tooltip
content={
<>
Ocean Protocol{' '}
<a href="https://github.com/oceanprotocol/list-datapartners">
Data Partners
</a>
</>
}
/>
</h3>
{loading ? (
<LoaderArea />
) : (
queryResultPartners && (
<AssetQueryCarousel queryResult={queryResultPartners} />
)
)}
{/* <Button
style="text"
to={`/search/?owner=${partnerAccounts?.toString()}`}
>
All data partner sets
</Button> */}
</section>
<section className={styles.section}> <section className={styles.section}>
<h3>Bookmarks</h3> <h3>Bookmarks</h3>
<Bookmarks /> <Bookmarks />

View File

@ -76,7 +76,7 @@
margin: calc(var(--spacer) * 2) auto; margin: calc(var(--spacer) * 2) auto;
max-width: 20%; max-width: 20%;
border: 0; border: 0;
border-top: 2px solid var(--brand-black); border-top: 2px solid var(--border-color);
} }
.content figure { .content figure {

View File

@ -1,28 +0,0 @@
import { useEffect, useState } from 'react'
import listPartners from '@oceanprotocol/list-datapartners'
import { PartnerData } from '@oceanprotocol/list-datapartners/types'
export function useDataPartner(
owner?: string
): {
partner: PartnerData
partnerAccounts: string[]
} {
const [partnerAccounts, setPartnerAccounts] = useState<string[]>()
const [partner, setPartner] = useState<PartnerData>()
useEffect(() => {
const accounts = [] as string[]
listPartners.map((partner) => accounts.push(...partner.accounts))
setPartnerAccounts(accounts)
if (!owner) return
const partner = listPartners.filter((partner) =>
partner.accounts.includes(owner)
)[0]
setPartner(partner)
}, [owner])
return { partner, partnerAccounts }
}

View File

@ -1,5 +0,0 @@
<svg width="21" height="24" viewBox="0 0 21 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13C17.897 13 17 13.896 17 15V17.5C17 18.327 16.327 19 15.5 19C14.673 19 14 18.327 14 17.5V9.119L10.5 2.119L7 9.119V17.5C7 18.327 6.327 19 5.5 19C4.673 19 4 18.327 4 17.5V15C4 13.896 3.102 13 2 13C0.898 13 0 13.896 0 15V17C0 17.276 0.224 17.5 0.5 17.5C0.776 17.5 1 17.276 1 17V15C1 14.449 1.448 14 2 14C2.552 14 3 14.449 3 15V17.5C3 18.879 4.121 20 5.5 20C6.879 20 8 18.879 8 17.5V16H9V20C9 21.654 7.654 23 6 23C4.262 23 3 21.844 3 20.25C3 19.974 2.776 19.75 2.5 19.75C2.224 19.75 2 19.974 2 20.25C2 22.388 3.72 24 6 24C8.206 24 10 22.206 10 20V16H11V20C11 22.169 12.889 24 15.125 24C17.406 24 19.063 22.423 19.063 20.25C19.063 19.974 18.839 19.75 18.563 19.75C18.287 19.75 18.063 19.974 18.063 20.25C18.063 21.844 16.828 23 15.125 23C13.431 23 12 21.626 12 20V16H13V17.5C13 18.879 14.121 20 15.5 20C16.879 20 18 18.879 18 17.5V15C18 14.449 18.448 14 19 14C19.552 14 20 14.449 20 15V16C20 16.276 20.224 16.5 20.5 16.5C20.776 16.5 21 16.276 21 16V15C21 13.896 20.103 13 19 13Z" />
<path d="M16.967 6.734L10.938 0.729004C10.933 0.737004 10.927 0.744004 10.923 0.751004C10.929 0.761004 10.941 0.765004 10.947 0.775004L15.059 9H16.032C17.041 9 17.379 8.547 17.49 8.276C17.602 8.007 17.682 7.447 16.967 6.734Z" />
<path d="M10.063 0.730004L4.061 6.732C3.348 7.446 3.43 8.006 3.541 8.276C3.653 8.547 3.991 9 5 9H5.941L10.052 0.776004C10.058 0.765004 10.071 0.761004 10.076 0.752004C10.073 0.744004 10.067 0.737004 10.063 0.730004Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

35
src/models/Profile.ts Normal file
View File

@ -0,0 +1,35 @@
export interface ProfileLink {
name: string
value: string
}
export interface Profile {
did: string
name?: string
description?: string
emoji?: string
image?: string
links?: ProfileLink[]
}
export 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
}
}[]
}

97
src/utils/profile.ts Normal file
View File

@ -0,0 +1,97 @@
import { Profile, ProfileLink, ResponseData3Box } from '../models/Profile'
import axios, { AxiosResponse, CancelToken } from 'axios'
import jwtDecode from 'jwt-decode'
import { Logger } from '@oceanprotocol/lib'
// https://docs.3box.io/api/rest-api
const apiUri = 'https://market-stats.oceanprotocol.com/api'
const ipfsUrl = 'https://ipfs.oceanprotocol.com'
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: AxiosResponse<ResponseData3Box> = await axios(
`${apiUri}/profile?address=${accountId}`,
{ cancelToken }
)
if (
!response ||
!response.data ||
response.status !== 200 ||
response.data.status === 'error'
)
return
Logger.log(`3Box profile found for ${accountId}`, response.data)
const profile = transformResponse(response.data)
return profile
// eslint-disable-next-line no-empty
} catch (error) {}
}

View File

@ -56,6 +56,7 @@ export function isDefaultNetwork(networkId: number): boolean {
} }
export function accountTruncate(account: string): string { export function accountTruncate(account: string): string {
if (!account) return
const middle = account.substring(6, 38) const middle = account.substring(6, 38)
const truncated = account.replace(middle, '…') const truncated = account.replace(middle, '…')
return truncated return truncated