mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Merge branch 'main' into feature/vedf
This commit is contained in:
commit
9a9348548a
@ -53,7 +53,8 @@
|
|||||||
"object": true,
|
"object": true,
|
||||||
"array": false
|
"array": false
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"testing-library/no-node-access": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
36
.github/workflows/ci.yml
vendored
36
.github/workflows/ci.yml
vendored
@ -23,13 +23,13 @@ jobs:
|
|||||||
node: ['16']
|
node: ['16']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
- name: Cache node_modules
|
- name: Cache node_modules
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@ -37,7 +37,7 @@ jobs:
|
|||||||
key: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
restore-keys: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}-
|
restore-keys: ${{ runner.os }}-${{ matrix.node }}-build-${{ env.cache-name }}-
|
||||||
|
|
||||||
- run: npm ci --legacy-peer-deps
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@ -50,13 +50,13 @@ jobs:
|
|||||||
node: ['16']
|
node: ['16']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
- name: Cache node_modules
|
- name: Cache node_modules
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@ -64,11 +64,11 @@ jobs:
|
|||||||
key: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
restore-keys: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}-
|
restore-keys: ${{ runner.os }}-${{ matrix.node }}-test-${{ env.cache-name }}-
|
||||||
|
|
||||||
- run: npm ci --legacy-peer-deps
|
- run: npm ci
|
||||||
- run: npm test
|
- run: npm test
|
||||||
|
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ runner.os }}
|
name: coverage-${{ runner.os }}
|
||||||
path: coverage/
|
path: coverage/
|
||||||
@ -79,12 +79,12 @@ jobs:
|
|||||||
if: ${{ success() && github.actor != 'dependabot[bot]' }}
|
if: ${{ success() && github.actor != 'dependabot[bot]' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '16'
|
||||||
- name: Cache node_modules
|
- name: Cache node_modules
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@ -92,11 +92,11 @@ jobs:
|
|||||||
key: ${{ runner.os }}-coverage-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-coverage-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
restore-keys: ${{ runner.os }}-coverage-${{ env.cache-name }}-
|
restore-keys: ${{ runner.os }}-coverage-${{ env.cache-name }}-
|
||||||
|
|
||||||
- uses: actions/download-artifact@v2
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ runner.os }}
|
name: coverage-${{ runner.os }}
|
||||||
|
|
||||||
- run: npm ci --legacy-peer-deps
|
- run: npm ci
|
||||||
- run: npm run codegen:apollo
|
- run: npm run codegen:apollo
|
||||||
|
|
||||||
- uses: paambaati/codeclimate-action@v3.0.0
|
- uses: paambaati/codeclimate-action@v3.0.0
|
||||||
@ -113,13 +113,13 @@ jobs:
|
|||||||
node: ['16']
|
node: ['16']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
- name: Cache node_modules
|
- name: Cache node_modules
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@ -127,6 +127,6 @@ jobs:
|
|||||||
key: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
restore-keys: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}-
|
restore-keys: ${{ runner.os }}-${{ matrix.node }}-storybook-${{ env.cache-name }}-
|
||||||
|
|
||||||
- run: npm ci --legacy-peer-deps
|
- run: npm ci
|
||||||
- run: npm run pregenerate
|
- run: npm run pregenerate
|
||||||
- run: npm run storybook:build
|
- run: npm run storybook:build
|
||||||
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@ -35,11 +35,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@ -50,7 +50,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@ -64,4 +64,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v2
|
||||||
|
6
.github/workflows/deploy.yml
vendored
6
.github/workflows/deploy.yml
vendored
@ -10,9 +10,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v3
|
||||||
- run: npm ci --legacy-peer-deps
|
- run: npm ci
|
||||||
|
|
||||||
- run: npm run build:static
|
- run: npm run build:static
|
||||||
env:
|
env:
|
||||||
|
@ -14,7 +14,7 @@ const customJestConfig = {
|
|||||||
moduleDirectories: ['node_modules', '<rootDir>/src'],
|
moduleDirectories: ['node_modules', '<rootDir>/src'],
|
||||||
testEnvironment: 'jest-environment-jsdom',
|
testEnvironment: 'jest-environment-jsdom',
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'\\.svg': '<rootDir>/.jest/__mocks__/svgrMock.tsx',
|
'^.+\\.(svg)$': '<rootDir>/.jest/__mocks__/svgrMock.tsx',
|
||||||
// '^@/components/(.*)$': '<rootDir>/components/$1',
|
// '^@/components/(.*)$': '<rootDir>/components/$1',
|
||||||
'@shared(.*)$': '<rootDir>/src/components/@shared/$1',
|
'@shared(.*)$': '<rootDir>/src/components/@shared/$1',
|
||||||
'@hooks/(.*)$': '<rootDir>/src/@hooks/$1',
|
'@hooks/(.*)$': '<rootDir>/src/@hooks/$1',
|
||||||
|
12
.jest/testRender.ts
Normal file
12
.jest/testRender.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { ReactElement } from 'react'
|
||||||
|
|
||||||
|
const testRender = (component: ReactElement): void => {
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const { container } = render(component)
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default testRender
|
@ -1,14 +0,0 @@
|
|||||||
import initStoryshots from '@storybook/addon-storyshots'
|
|
||||||
import { render, waitFor } from '@testing-library/react'
|
|
||||||
|
|
||||||
// Stories are render-tested with @testing-library/react,
|
|
||||||
// overwriting default snapshot testing behavior
|
|
||||||
initStoryshots({
|
|
||||||
asyncJest: true,
|
|
||||||
test: async ({ story, done }) => {
|
|
||||||
const storyElement = story.render()
|
|
||||||
// render the story with @testing-library/react
|
|
||||||
render(storyElement)
|
|
||||||
await waitFor(() => done())
|
|
||||||
}
|
|
||||||
})
|
|
40
README.md
40
README.md
@ -16,7 +16,7 @@
|
|||||||
- [🦀 Data Sources](#-data-sources)
|
- [🦀 Data Sources](#-data-sources)
|
||||||
- [Aquarius](#aquarius)
|
- [Aquarius](#aquarius)
|
||||||
- [Ocean Protocol Subgraph](#ocean-protocol-subgraph)
|
- [Ocean Protocol Subgraph](#ocean-protocol-subgraph)
|
||||||
- [3Box](#3box)
|
- [ENS](#ens)
|
||||||
- [Purgatory](#purgatory)
|
- [Purgatory](#purgatory)
|
||||||
- [Network Metadata](#network-metadata)
|
- [Network Metadata](#network-metadata)
|
||||||
- [👩🎤 Storybook](#-storybook)
|
- [👩🎤 Storybook](#-storybook)
|
||||||
@ -86,7 +86,7 @@ npm start
|
|||||||
|
|
||||||
To use the app together with MetaMask, importing one of the accounts auto-generated by the Ganache container is the easiest way to have test ETH available. All of them have 100 ETH by default. Upon start, the `ocean_ganache_1` container will print out the private keys of multiple accounts in its logs. Pick one of them and import into MetaMask.
|
To use the app together with MetaMask, importing one of the accounts auto-generated by the Ganache container is the easiest way to have test ETH available. All of them have 100 ETH by default. Upon start, the `ocean_ganache_1` container will print out the private keys of multiple accounts in its logs. Pick one of them and import into MetaMask.
|
||||||
|
|
||||||
To fully test all [The Graph](https://thegraph.com) integrations, you have to run your own local Graph node with our [`ocean-subgraph`](https://github.com/oceanprotocol/ocean-subgraph) deployed to it. Barge does not include a local subgraph so by default, the `subgraphUri` is hardcoded to the Rinkeby subgraph in our [`getDevelopmentConfig` function](https://github.com/oceanprotocol/market/blob/d0b1534d105e5dcb3790c65d4bb04ff1d2dbc575/src/utils/ocean.ts#L31).
|
To fully test all [The Graph](https://thegraph.com) integrations, you have to run your own local Graph node with our [`ocean-subgraph`](https://github.com/oceanprotocol/ocean-subgraph) deployed to it. Barge does not include a local subgraph so by default, the `subgraphUri` is hardcoded to the Goerli subgraph in our [`getDevelopmentConfig` function](https://github.com/oceanprotocol/market/blob/d0b1534d105e5dcb3790c65d4bb04ff1d2dbc575/src/utils/ocean.ts#L31).
|
||||||
|
|
||||||
> Cleaning all Docker images so they are fetched freshly is often a good idea to make sure no issues are caused by old or stale images: `docker system prune --all --volumes`
|
> Cleaning all Docker images so they are fetched freshly is often a good idea to make sure no issues are caused by old or stale images: `docker system prune --all --volumes`
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ The `app.config.js` file is setup to prioritize environment variables for settin
|
|||||||
For local development, you can use a `.env` file:
|
For local development, you can use a `.env` file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# modify env variables, Rinkeby is enabled by default when using those files
|
# modify env variables, Goerli is enabled by default when using those files
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -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
|
```tsx
|
||||||
import get3BoxProfile from '@utils/profile'
|
import { useProfile } from '@context/Profile'
|
||||||
|
|
||||||
function Component() {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{profile.emoji} {profile.name}
|
{profile.avatar} {profile.name}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -332,7 +316,7 @@ npm run jest
|
|||||||
|
|
||||||
A coverage report is automatically shown in console whenever `npm run jest` is called. Generated reports are sent to CodeClimate during CI runs.
|
A coverage report is automatically shown in console whenever `npm run jest` is called. Generated reports are sent to CodeClimate during CI runs.
|
||||||
|
|
||||||
During local development you can continously get coverage report feedback in your console by running Jest in watch mode:
|
During local development you can continuously get coverage report feedback in your console by running Jest in watch mode:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run jest:watch
|
npm run jest:watch
|
||||||
@ -395,7 +379,7 @@ We encourage you to fork this repository and create your own data marketplace. W
|
|||||||
- The Ocean Protocol logo is a trademark of the Ocean Protocol Foundation and must be removed from forked versions of the market.
|
- The Ocean Protocol logo is a trademark of the Ocean Protocol Foundation and must be removed from forked versions of the market.
|
||||||
- The name "Ocean Market" is also copyright protected and should be changed to the name of your market.
|
- The name "Ocean Market" is also copyright protected and should be changed to the name of your market.
|
||||||
|
|
||||||
Additionally, we would also advise that your retain the text saying "Powered by Ocean Protocol" on your forked version of the marketplace in order to give credit for the development work done by the Ocean Protocol team.
|
Additionally, we would also advise that you retain the text saying "Powered by Ocean Protocol" on your forked version of the marketplace in order to give credit for the development work done by the Ocean Protocol team.
|
||||||
|
|
||||||
Everything else is made open according to the apache2 license. We look forward to seeing your data marketplace!
|
Everything else is made open according to the apache2 license. We look forward to seeing your data marketplace!
|
||||||
|
|
||||||
@ -427,7 +411,7 @@ Feel free to adopt our provided privacy policies to your needs. Per default we c
|
|||||||
|
|
||||||
### Privacy Preference Center
|
### Privacy Preference Center
|
||||||
|
|
||||||
Additionally, Ocean Market provides a privacy preference center for you to use. This feature is disabled per default since we do not use cookies requiring consent on our deployment of the market. However, if you need to add some functionality depending on cookies, you can simply enable this feature by changing the value of the `NEXT_PUBLIC_PRIVACY_PREFERENCE_CENTER` environmental variable to `"true"` in your `.env` file. This will enable a customizable cookie banner stating the use of your individual cookies. The content of this banner can be adjusted within the `content/gdpr.json` file. If no `optionalCookies` are provided, the privacy preference center will be set to a simpler version displaying only the `title`, `text` and `close`-button. This can be used to inform the user about the use of essential cookies, where no consent is needed. The privacy preference center supports two different styling options: `'small'` and `'default'`. Setting the style propertie to `'small'` will display a smaller cookie banner to the user at first, only showing the default styled privacy preference center upon the user's customization request.
|
Additionally, Ocean Market provides a privacy preference center for you to use. This feature is disabled per default since we do not use cookies requiring consent on our deployment of the market. However, if you need to add some functionality depending on cookies, you can simply enable this feature by changing the value of the `NEXT_PUBLIC_PRIVACY_PREFERENCE_CENTER` environmental variable to `"true"` in your `.env` file. This will enable a customizable cookie banner stating the use of your individual cookies. The content of this banner can be adjusted within the `content/gdpr.json` file. If no `optionalCookies` are provided, the privacy preference center will be set to a simpler version displaying only the `title`, `text` and `close`-button. This can be used to inform the user about the use of essential cookies, where no consent is needed. The privacy preference center supports two different styling options: `'small'` and `'default'`. Setting the style property to `'small'` will display a smaller cookie banner to the user at first, only showing the default styled privacy preference center upon the user's customization request.
|
||||||
|
|
||||||
Now your market users will be provided with additional options to toggle the use of your configured cookie consent categories. You can always retrieve the current consent status per category with the provided `useConsent()` hook. See below, how you can set your own custom cookies depending on the market user's consent. Feel free to adjust the provided utility functions for cookie usage provided in the `src/utils/cookies.ts` file to your needs.
|
Now your market users will be provided with additional options to toggle the use of your configured cookie consent categories. You can always retrieve the current consent status per category with the provided `useConsent()` hook. See below, how you can set your own custom cookies depending on the market user's consent. Feel free to adjust the provided utility functions for cookie usage provided in the `src/utils/cookies.ts` file to your needs.
|
||||||
|
|
||||||
|
@ -9,20 +9,12 @@ module.exports = {
|
|||||||
process.env.NEXT_PUBLIC_METADATACACHE_URI ||
|
process.env.NEXT_PUBLIC_METADATACACHE_URI ||
|
||||||
'https://v4.aquarius.oceanprotocol.com',
|
'https://v4.aquarius.oceanprotocol.com',
|
||||||
|
|
||||||
v3MetadataCacheUri:
|
|
||||||
process.env.NEXT_PUBLIC_V3_METADATACACHE_URI ||
|
|
||||||
'https://aquarius.oceanprotocol.com',
|
|
||||||
|
|
||||||
v3MarketUri:
|
|
||||||
process.env.NEXT_PUBLIC_V3_MARKET_URI ||
|
|
||||||
'https://v3.market.oceanprotocol.com',
|
|
||||||
|
|
||||||
// List of chainIds which metadata cache queries will return by default.
|
// List of chainIds which metadata cache queries will return by default.
|
||||||
// This preselects the Chains user preferences.
|
// This preselects the Chains user preferences.
|
||||||
chainIds: [1, 137, 56, 246, 1285],
|
chainIds: [1, 137, 56, 246, 1285],
|
||||||
|
|
||||||
// List of all supported chainIds. Used to populate the Chains user preferences list.
|
// List of all supported chainIds. Used to populate the Chains user preferences list.
|
||||||
chainIdsSupported: [1, 137, 56, 246, 1285, 3, 4, 80001, 1287],
|
chainIdsSupported: [1, 137, 56, 246, 1285, 5, 80001, 1287],
|
||||||
|
|
||||||
infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx',
|
infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx',
|
||||||
|
|
||||||
@ -66,7 +58,7 @@ module.exports = {
|
|||||||
// Refers to Coingecko API tokenIds.
|
// Refers to Coingecko API tokenIds.
|
||||||
coingeckoTokenIds: ['ocean-protocol', 'h2o', 'ethereum', 'matic-network'],
|
coingeckoTokenIds: ['ocean-protocol', 'h2o', 'ethereum', 'matic-network'],
|
||||||
|
|
||||||
// Config for https://github.com/donavon/use-dark-mode
|
// Config for https://github.com/oceanprotocol/use-dark-mode
|
||||||
darkModeConfig: {
|
darkModeConfig: {
|
||||||
classNameDark: 'dark',
|
classNameDark: 'dark',
|
||||||
classNameLight: 'light',
|
classNameLight: 'light',
|
||||||
|
@ -59,6 +59,13 @@
|
|||||||
"placeholder": "e.g. Mrs McJellyfish",
|
"placeholder": "e.g. Mrs McJellyfish",
|
||||||
"help": "Give proper attribution for your dataset.",
|
"help": "Give proper attribution for your dataset.",
|
||||||
"required": false
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tags",
|
||||||
|
"label": "New Tags",
|
||||||
|
"type": "tags",
|
||||||
|
"placeholder": "e.g. logistics",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -39,8 +39,8 @@
|
|||||||
{
|
{
|
||||||
"name": "tags",
|
"name": "tags",
|
||||||
"label": "Tags",
|
"label": "Tags",
|
||||||
"placeholder": "e.g. logistics, ai",
|
"type": "tags",
|
||||||
"help": "Separate tags with comma."
|
"placeholder": "e.g. logistics"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "dockerImage",
|
"name": "dockerImage",
|
||||||
|
44503
package-lock.json
generated
44503
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
82
package.json
82
package.json
@ -17,8 +17,8 @@
|
|||||||
"format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json}' --write",
|
"format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json}' --write",
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"deploy:s3": "bash scripts/deploy-s3.sh",
|
"deploy:s3": "bash scripts/deploy-s3.sh",
|
||||||
"postinstall": "husky install",
|
"postinstall": "husky install && rm -r node_modules/apollo-language-server/node_modules/graphql",
|
||||||
"codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.ropsten.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/",
|
"codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.goerli.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/",
|
||||||
"storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet",
|
"storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet",
|
||||||
"storybook:build": "cross-env NODE_ENV=test build-storybook"
|
"storybook:build": "cross-env NODE_ENV=test build-storybook"
|
||||||
},
|
},
|
||||||
@ -26,27 +26,32 @@
|
|||||||
"@coingecko/cryptoformat": "^0.5.4",
|
"@coingecko/cryptoformat": "^0.5.4",
|
||||||
"@loadable/component": "^5.15.2",
|
"@loadable/component": "^5.15.2",
|
||||||
"@oceanprotocol/art": "^3.2.0",
|
"@oceanprotocol/art": "^3.2.0",
|
||||||
|
<<<<<<< HEAD
|
||||||
"@oceanprotocol/lib": "^2.0.0",
|
"@oceanprotocol/lib": "^2.0.0",
|
||||||
|
=======
|
||||||
|
"@oceanprotocol/lib": "^2.2.1",
|
||||||
|
>>>>>>> main
|
||||||
"@oceanprotocol/typographies": "^0.1.0",
|
"@oceanprotocol/typographies": "^0.1.0",
|
||||||
|
"@oceanprotocol/use-dark-mode": "^2.4.3",
|
||||||
"@tippyjs/react": "^4.2.6",
|
"@tippyjs/react": "^4.2.6",
|
||||||
"@urql/exchange-refocus": "^0.2.5",
|
"@urql/exchange-refocus": "^1.0.0",
|
||||||
"@walletconnect/web3-provider": "^1.7.8",
|
"@walletconnect/web3-provider": "^1.8.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.2",
|
||||||
"date-fns": "^2.29.1",
|
"date-fns": "^2.29.3",
|
||||||
"decimal.js": "^10.3.1",
|
"decimal.js": "^10.3.1",
|
||||||
"dom-confetti": "^0.2.2",
|
"dom-confetti": "^0.2.2",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"filesize": "^9.0.1",
|
"filesize": "^10.0.5",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"is-url-superb": "^6.1.0",
|
"is-url-superb": "^6.1.0",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"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",
|
||||||
|
"match-sorter": "^6.3.1",
|
||||||
"myetherwallet-blockies": "^0.1.1",
|
"myetherwallet-blockies": "^0.1.1",
|
||||||
"next": "^12.1.6",
|
"next": "12.3.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-clipboard.js": "^2.0.16",
|
"react-clipboard.js": "^2.0.16",
|
||||||
@ -55,6 +60,7 @@
|
|||||||
"react-dotdotdot": "^1.3.1",
|
"react-dotdotdot": "^1.3.1",
|
||||||
"react-modal": "^3.15.1",
|
"react-modal": "^3.15.1",
|
||||||
"react-paginate": "^8.1.3",
|
"react-paginate": "^8.1.3",
|
||||||
|
"react-select": "^5.4.0",
|
||||||
"react-spring": "^9.5.2",
|
"react-spring": "^9.5.2",
|
||||||
"react-tabs": "^5.1.0",
|
"react-tabs": "^5.1.0",
|
||||||
"react-toastify": "^9.0.4",
|
"react-toastify": "^9.0.4",
|
||||||
@ -64,58 +70,52 @@
|
|||||||
"remove-markdown": "^0.5.0",
|
"remove-markdown": "^0.5.0",
|
||||||
"slugify": "^1.6.5",
|
"slugify": "^1.6.5",
|
||||||
"swr": "^1.3.0",
|
"swr": "^1.3.0",
|
||||||
"urql": "^2.2.1",
|
"urql": "^3.0.3",
|
||||||
"use-dark-mode": "^2.3.1",
|
"web3": "^1.8.0",
|
||||||
"web3": "^1.7.4",
|
"web3modal": "^1.9.9",
|
||||||
"web3modal": "^1.9.8",
|
|
||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-essentials": "^6.5.7",
|
"@storybook/addon-essentials": "^6.5.12",
|
||||||
"@storybook/addon-storyshots": "^6.5.9",
|
"@storybook/builder-webpack5": "^6.5.12",
|
||||||
"@storybook/builder-webpack5": "^6.5.9",
|
"@storybook/manager-webpack5": "^6.5.12",
|
||||||
"@storybook/manager-webpack5": "^6.5.7",
|
"@storybook/react": "^6.5.12",
|
||||||
"@storybook/react": "^6.5.7",
|
"@svgr/webpack": "^6.3.1",
|
||||||
"@storybook/testing-library": "^0.0.11",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@storybook/testing-react": "^1.3.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@svgr/webpack": "^6.2.1",
|
|
||||||
"@testing-library/jest-dom": "^5.16.4",
|
|
||||||
"@testing-library/react": "^13.3.0",
|
|
||||||
"@types/js-cookie": "^3.0.2",
|
"@types/js-cookie": "^3.0.2",
|
||||||
"@types/loadable__component": "^5.13.4",
|
"@types/loadable__component": "^5.13.4",
|
||||||
"@types/lodash.debounce": "^4.0.7",
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
"@types/lodash.omit": "^4.5.7",
|
"@types/lodash.omit": "^4.5.7",
|
||||||
"@types/node": "^17.0.41",
|
"@types/node": "^18.7.18",
|
||||||
"@types/react": "^18.0.14",
|
"@types/react": "^18.0.21",
|
||||||
"@types/react-dom": "^18.0.5",
|
"@types/react-dom": "^18.0.5",
|
||||||
"@types/react-modal": "^3.13.1",
|
"@types/react-modal": "^3.13.1",
|
||||||
"@types/react-paginate": "^7.1.1",
|
"@types/react-paginate": "^7.1.1",
|
||||||
"@types/remove-markdown": "^0.3.1",
|
"@types/remove-markdown": "^0.3.1",
|
||||||
"@types/yup": "^0.29.14",
|
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.31.0",
|
"@typescript-eslint/parser": "^5.38.1",
|
||||||
"@typescript-eslint/parser": "^5.27.1",
|
|
||||||
"apollo": "^2.34.0",
|
"apollo": "^2.34.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.21.0",
|
"eslint": "^8.23.1",
|
||||||
"eslint-config-oceanprotocol": "^2.0.3",
|
"eslint-config-oceanprotocol": "^2.0.4",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-jest-dom": "^4.0.2",
|
"eslint-plugin-jest-dom": "^4.0.2",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.30.0",
|
"eslint-plugin-react": "^7.31.8",
|
||||||
"eslint-plugin-react-hooks": "^4.5.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-testing-library": "^5.5.1",
|
"eslint-plugin-testing-library": "^5.7.0",
|
||||||
"file-loader": "^6.2.0",
|
|
||||||
"https-browserify": "^1.0.0",
|
"https-browserify": "^1.0.0",
|
||||||
"husky": "^8.0.1",
|
"husky": "^8.0.1",
|
||||||
"jest": "^28.1.2",
|
"jest": "^29.1.2",
|
||||||
"jest-environment-jsdom": "^28.1.2",
|
"jest-environment-jsdom": "^29.0.3",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.7.1",
|
||||||
"pretty-quick": "^3.1.3",
|
"pretty-quick": "^3.1.3",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"serve": "^13.0.2",
|
"serve": "^14.0.1",
|
||||||
"stream-http": "^3.2.0",
|
"stream-http": "^3.2.0",
|
||||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||||
"typescript": "^4.7.3"
|
"typescript": "^4.8.3"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -9,7 +9,7 @@ import React, {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { Config, LoggerInstance, Purgatory } from '@oceanprotocol/lib'
|
import { Config, LoggerInstance, Purgatory } from '@oceanprotocol/lib'
|
||||||
import { CancelToken } from 'axios'
|
import { CancelToken } from 'axios'
|
||||||
import { checkV3Asset, retrieveAsset } from '@utils/aquarius'
|
import { retrieveAsset } from '@utils/aquarius'
|
||||||
import { useWeb3 } from './Web3'
|
import { useWeb3 } from './Web3'
|
||||||
import { useCancelToken } from '@hooks/useCancelToken'
|
import { useCancelToken } from '@hooks/useCancelToken'
|
||||||
import { getOceanConfig, getDevelopmentConfig } from '@utils/ocean'
|
import { getOceanConfig, getDevelopmentConfig } from '@utils/ocean'
|
||||||
@ -25,7 +25,6 @@ export interface AssetProviderValue {
|
|||||||
owner: string
|
owner: string
|
||||||
error?: string
|
error?: string
|
||||||
isAssetNetwork: boolean
|
isAssetNetwork: boolean
|
||||||
isV3Asset: boolean
|
|
||||||
isOwner: boolean
|
isOwner: boolean
|
||||||
oceanConfig: Config
|
oceanConfig: Config
|
||||||
loading: boolean
|
loading: boolean
|
||||||
@ -53,7 +52,6 @@ function AssetProvider({
|
|||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [isAssetNetwork, setIsAssetNetwork] = useState<boolean>()
|
const [isAssetNetwork, setIsAssetNetwork] = useState<boolean>()
|
||||||
const [isV3Asset, setIsV3Asset] = useState<boolean>()
|
|
||||||
const [oceanConfig, setOceanConfig] = useState<Config>()
|
const [oceanConfig, setOceanConfig] = useState<Config>()
|
||||||
|
|
||||||
const newCancelToken = useCancelToken()
|
const newCancelToken = useCancelToken()
|
||||||
@ -71,7 +69,6 @@ function AssetProvider({
|
|||||||
const asset = await retrieveAsset(did, token)
|
const asset = await retrieveAsset(did, token)
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
setIsV3Asset(await checkV3Asset(did, token))
|
|
||||||
setError(
|
setError(
|
||||||
`\`${did}\`` +
|
`\`${did}\`` +
|
||||||
'\n\nWe could not find an asset for this DID in the cache. If you just published a new asset, wait some seconds and refresh this page.'
|
'\n\nWe could not find an asset for this DID in the cache. If you just published a new asset, wait some seconds and refresh this page.'
|
||||||
@ -96,7 +93,6 @@ function AssetProvider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTitle(`This asset has been flagged as "${state}" by the publisher`)
|
setTitle(`This asset has been flagged as "${state}" by the publisher`)
|
||||||
setIsV3Asset(await checkV3Asset(did, token))
|
|
||||||
setError(`\`${did}\`` + `\n\nPublisher Address: ${asset.nft.owner}`)
|
setError(`\`${did}\`` + `\n\nPublisher Address: ${asset.nft.owner}`)
|
||||||
LoggerInstance.error(`[asset] Failed getting asset for ${did}`, asset)
|
LoggerInstance.error(`[asset] Failed getting asset for ${did}`, asset)
|
||||||
return
|
return
|
||||||
@ -208,7 +204,6 @@ function AssetProvider({
|
|||||||
loading,
|
loading,
|
||||||
fetchAsset,
|
fetchAsset,
|
||||||
isAssetNetwork,
|
isAssetNetwork,
|
||||||
isV3Asset,
|
|
||||||
isOwner,
|
isOwner,
|
||||||
oceanConfig
|
oceanConfig
|
||||||
} as AssetProviderValue
|
} as AssetProviderValue
|
||||||
|
@ -26,8 +26,6 @@ export interface AppConfig {
|
|||||||
classNameLight: string
|
classNameLight: string
|
||||||
storageKey: string
|
storageKey: string
|
||||||
}
|
}
|
||||||
v3MetadataCacheUri: string
|
|
||||||
v3MarketUri: string
|
|
||||||
}
|
}
|
||||||
export interface SiteContent {
|
export interface SiteContent {
|
||||||
siteTitle: string
|
siteTitle: string
|
||||||
|
@ -7,15 +7,18 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
ReactNode
|
ReactNode
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { getUserSales, getUserTokenOrders } from '@utils/subgraph'
|
import { getUserTokenOrders } from '@utils/subgraph'
|
||||||
import { useUserPreferences } from './UserPreferences'
|
import { useUserPreferences } from '../UserPreferences'
|
||||||
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
||||||
import { getDownloadAssets, getPublishedAssets } from '@utils/aquarius'
|
import {
|
||||||
import { accountTruncate } from '@utils/web3'
|
getDownloadAssets,
|
||||||
|
getPublishedAssets,
|
||||||
|
getUserSales
|
||||||
|
} from '@utils/aquarius'
|
||||||
import axios, { CancelToken } from 'axios'
|
import axios, { CancelToken } from 'axios'
|
||||||
import get3BoxProfile from '@utils/profile'
|
|
||||||
import web3 from 'web3'
|
import web3 from 'web3'
|
||||||
import { useMarketMetadata } from './MarketMetadata'
|
import { useMarketMetadata } from '../MarketMetadata'
|
||||||
|
import { getEnsProfile } from '@utils/ens'
|
||||||
|
|
||||||
interface ProfileProviderValue {
|
interface ProfileProviderValue {
|
||||||
profile: Profile
|
profile: Profile
|
||||||
@ -32,6 +35,14 @@ const ProfileContext = createContext({} as ProfileProviderValue)
|
|||||||
|
|
||||||
const refreshInterval = 10000 // 10 sec.
|
const refreshInterval = 10000 // 10 sec.
|
||||||
|
|
||||||
|
const clearedProfile: Profile = {
|
||||||
|
name: null,
|
||||||
|
avatar: null,
|
||||||
|
url: null,
|
||||||
|
description: null,
|
||||||
|
links: null
|
||||||
|
}
|
||||||
|
|
||||||
function ProfileProvider({
|
function ProfileProvider({
|
||||||
accountId,
|
accountId,
|
||||||
accountEns,
|
accountEns,
|
||||||
@ -56,9 +67,9 @@ function ProfileProvider({
|
|||||||
}, [accountId])
|
}, [accountId])
|
||||||
|
|
||||||
//
|
//
|
||||||
// User profile: ENS + 3Box
|
// User profile: ENS
|
||||||
//
|
//
|
||||||
const [profile, setProfile] = useState<Profile>()
|
const [profile, setProfile] = useState<Profile>({ name: accountEns })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!accountEns) return
|
if (!accountEns) return
|
||||||
@ -66,53 +77,22 @@ function ProfileProvider({
|
|||||||
}, [accountId, accountEns])
|
}, [accountId, accountEns])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const clearedProfile: Profile = {
|
if (
|
||||||
name: null,
|
!accountId ||
|
||||||
accountEns: null,
|
accountId === '0x0000000000000000000000000000000000000000' ||
|
||||||
image: null,
|
!isEthAddress
|
||||||
description: null,
|
) {
|
||||||
links: null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!accountId || !isEthAddress) {
|
|
||||||
setProfile(clearedProfile)
|
setProfile(clearedProfile)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelTokenSource = axios.CancelToken.source()
|
|
||||||
|
|
||||||
async function getInfo() {
|
async function getInfo() {
|
||||||
setProfile({ name: accountEns || accountTruncate(accountId), accountEns })
|
const profile = await getEnsProfile(accountId)
|
||||||
|
setProfile(profile)
|
||||||
const profile3Box = await get3BoxProfile(
|
LoggerInstance.log(`[profile] ENS metadata for ${accountId}:`, profile)
|
||||||
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.')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
getInfo()
|
getInfo()
|
||||||
|
}, [accountId, isEthAddress])
|
||||||
return () => {
|
|
||||||
cancelTokenSource.cancel()
|
|
||||||
}
|
|
||||||
}, [accountId, accountEns, isEthAddress])
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// PUBLISHED ASSETS
|
// PUBLISHED ASSETS
|
@ -133,6 +133,14 @@ function UserPreferencesProvider({
|
|||||||
setBookmarks(newPinned)
|
setBookmarks(newPinned)
|
||||||
}, [bookmarks])
|
}, [bookmarks])
|
||||||
|
|
||||||
|
// chainIds old data migration
|
||||||
|
// remove deprecated networks from user-saved chainIds
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chainIds.includes(3) && !chainIds.includes(4)) return
|
||||||
|
const newChainIds = chainIds.filter((id) => id !== 3 && id !== 4)
|
||||||
|
setChainIds(newChainIds)
|
||||||
|
}, [chainIds])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserPreferencesContext.Provider
|
<UserPreferencesContext.Provider
|
||||||
value={
|
value={
|
||||||
|
@ -13,7 +13,7 @@ import { infuraProjectId as infuraId } from '../../app.config'
|
|||||||
import WalletConnectProvider from '@walletconnect/web3-provider'
|
import WalletConnectProvider from '@walletconnect/web3-provider'
|
||||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||||
import { isBrowser } from '@utils/index'
|
import { isBrowser } from '@utils/index'
|
||||||
import { getEnsName } from '@utils/ens'
|
import { getEnsProfile } from '@utils/ens'
|
||||||
import useNetworkMetadata, {
|
import useNetworkMetadata, {
|
||||||
getNetworkDataById,
|
getNetworkDataById,
|
||||||
getNetworkDisplayName,
|
getNetworkDisplayName,
|
||||||
@ -32,6 +32,7 @@ interface Web3ProviderValue {
|
|||||||
web3ProviderInfo: IProviderInfo
|
web3ProviderInfo: IProviderInfo
|
||||||
accountId: string
|
accountId: string
|
||||||
accountEns: string
|
accountEns: string
|
||||||
|
accountEnsAvatar: string
|
||||||
balance: UserBalance
|
balance: UserBalance
|
||||||
networkId: number
|
networkId: number
|
||||||
chainId: number
|
chainId: number
|
||||||
@ -54,8 +55,6 @@ const web3ModalTheme = {
|
|||||||
hover: 'var(--background-highlight)'
|
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
|
const providerOptions = isBrowser
|
||||||
? {
|
? {
|
||||||
walletconnect: {
|
walletconnect: {
|
||||||
@ -99,6 +98,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
|||||||
const [isTestnet, setIsTestnet] = useState<boolean>()
|
const [isTestnet, setIsTestnet] = useState<boolean>()
|
||||||
const [accountId, setAccountId] = useState<string>()
|
const [accountId, setAccountId] = useState<string>()
|
||||||
const [accountEns, setAccountEns] = useState<string>()
|
const [accountEns, setAccountEns] = useState<string>()
|
||||||
|
const [accountEnsAvatar, setAccountEnsAvatar] = useState<string>()
|
||||||
const [web3Loading, setWeb3Loading] = useState<boolean>(true)
|
const [web3Loading, setWeb3Loading] = useState<boolean>(true)
|
||||||
const [balance, setBalance] = useState<UserBalance>({
|
const [balance, setBalance] = useState<UserBalance>({
|
||||||
eth: '0'
|
eth: '0'
|
||||||
@ -192,24 +192,35 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
|||||||
}, [accountId, approvedBaseTokens, networkId, web3, networkData])
|
}, [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
|
if (!accountId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// const accountEns = await getEnsNameWithWeb3(
|
const profile = await getEnsProfile(accountId)
|
||||||
// accountId,
|
|
||||||
// web3Provider,
|
if (!profile) {
|
||||||
// `${networkId}`
|
setAccountEns(null)
|
||||||
// )
|
setAccountEnsAvatar(null)
|
||||||
const accountEns = await getEnsName(accountId)
|
return
|
||||||
setAccountEns(accountEns)
|
}
|
||||||
accountEns &&
|
|
||||||
|
setAccountEns(profile.name)
|
||||||
|
LoggerInstance.log(
|
||||||
|
`[web3] ENS name found for ${accountId}:`,
|
||||||
|
profile.name
|
||||||
|
)
|
||||||
|
|
||||||
|
if (profile.avatar) {
|
||||||
|
setAccountEnsAvatar(profile.avatar)
|
||||||
LoggerInstance.log(
|
LoggerInstance.log(
|
||||||
`[web3] ENS name found for ${accountId}:`,
|
`[web3] ENS avatar found for ${accountId}:`,
|
||||||
accountEns
|
profile.avatar
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
setAccountEnsAvatar(null)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LoggerInstance.error('[web3] Error: ', error.message)
|
LoggerInstance.error('[web3] Error: ', error.message)
|
||||||
}
|
}
|
||||||
@ -275,11 +286,11 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
|||||||
}, [getUserBalance])
|
}, [getUserBalance])
|
||||||
|
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
// Get and set user ENS name
|
// Get and set user ENS info
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUserEnsName()
|
getUserEns()
|
||||||
}, [getUserEnsName])
|
}, [getUserEns])
|
||||||
|
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
// Get and set network metadata
|
// Get and set network metadata
|
||||||
@ -337,7 +348,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
|||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
async function logout() {
|
async function logout() {
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* 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()
|
await (web3.currentProvider as any).close()
|
||||||
}
|
}
|
||||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||||
@ -402,6 +413,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
|||||||
web3ProviderInfo,
|
web3ProviderInfo,
|
||||||
accountId,
|
accountId,
|
||||||
accountEns,
|
accountEns,
|
||||||
|
accountEnsAvatar,
|
||||||
balance,
|
balance,
|
||||||
networkId,
|
networkId,
|
||||||
chainId,
|
chainId,
|
||||||
|
32
src/@types/Profile.d.ts
vendored
32
src/@types/Profile.d.ts
vendored
@ -1,36 +1,12 @@
|
|||||||
interface ProfileLink {
|
interface ProfileLink {
|
||||||
name: string
|
key: string
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Profile {
|
interface Profile {
|
||||||
did?: string
|
name: string
|
||||||
name?: string
|
url?: string
|
||||||
accountEns?: string
|
avatar?: string
|
||||||
description?: string
|
description?: string
|
||||||
emoji?: string
|
|
||||||
image?: string
|
|
||||||
links?: ProfileLink[]
|
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
|
|
||||||
}
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
|
@ -6,7 +6,8 @@ export enum SortDirectionOptions {
|
|||||||
export enum SortTermOptions {
|
export enum SortTermOptions {
|
||||||
Created = 'nft.created',
|
Created = 'nft.created',
|
||||||
Relevance = '_score',
|
Relevance = '_score',
|
||||||
Stats = 'stats.orders'
|
Stats = 'stats.orders',
|
||||||
|
Allocated = 'stats.allocated'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: could not figure out how to get `enum` to be ambiant
|
// Note: could not figure out how to get `enum` to be ambiant
|
||||||
|
4
src/@types/aquarius/TagsList.d.ts
vendored
Normal file
4
src/@types/aquarius/TagsList.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
interface AggregatedTag {
|
||||||
|
doc_count: number
|
||||||
|
key: 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
|
|
||||||
}
|
|
@ -2,13 +2,18 @@ import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
|||||||
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
|
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
|
||||||
import axios, { CancelToken, AxiosResponse } from 'axios'
|
import axios, { CancelToken, AxiosResponse } from 'axios'
|
||||||
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
|
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
|
||||||
import { metadataCacheUri, v3MetadataCacheUri } from '../../app.config'
|
import { metadataCacheUri } from '../../app.config'
|
||||||
import {
|
import {
|
||||||
SortDirectionOptions,
|
SortDirectionOptions,
|
||||||
SortTermOptions
|
SortTermOptions
|
||||||
} from '../@types/aquarius/SearchQuery'
|
} from '../@types/aquarius/SearchQuery'
|
||||||
import { transformAssetToAssetSelection } from './assetConvertor'
|
import { transformAssetToAssetSelection } from './assetConvertor'
|
||||||
|
|
||||||
|
export interface UserSales {
|
||||||
|
id: string
|
||||||
|
totalSales: number
|
||||||
|
}
|
||||||
|
|
||||||
export const MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS = 476
|
export const MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS = 476
|
||||||
|
|
||||||
export function escapeEsReservedCharacters(value: string): string {
|
export function escapeEsReservedCharacters(value: string): string {
|
||||||
@ -39,13 +44,18 @@ export function generateBaseQuery(
|
|||||||
): SearchQuery {
|
): SearchQuery {
|
||||||
const generatedQuery = {
|
const generatedQuery = {
|
||||||
from: baseQueryParams.esPaginationOptions?.from || 0,
|
from: baseQueryParams.esPaginationOptions?.from || 0,
|
||||||
size: baseQueryParams.esPaginationOptions?.size || 1000,
|
size:
|
||||||
|
baseQueryParams.esPaginationOptions?.size >= 0
|
||||||
|
? baseQueryParams.esPaginationOptions?.size
|
||||||
|
: 1000,
|
||||||
query: {
|
query: {
|
||||||
bool: {
|
bool: {
|
||||||
...baseQueryParams.nestedQuery,
|
...baseQueryParams.nestedQuery,
|
||||||
filter: [
|
filter: [
|
||||||
...(baseQueryParams.filters || []),
|
...(baseQueryParams.filters || []),
|
||||||
getFilterTerm('chainId', baseQueryParams.chainIds),
|
baseQueryParams.chainIds
|
||||||
|
? getFilterTerm('chainId', baseQueryParams.chainIds)
|
||||||
|
: [],
|
||||||
getFilterTerm('_index', 'aquarius'),
|
getFilterTerm('_index', 'aquarius'),
|
||||||
...(baseQueryParams.ignorePurgatory
|
...(baseQueryParams.ignorePurgatory
|
||||||
? []
|
? []
|
||||||
@ -140,28 +150,6 @@ export async function retrieveAsset(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkV3Asset(
|
|
||||||
did: string,
|
|
||||||
cancelToken: CancelToken
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const response: AxiosResponse<Asset> = await axios.get(
|
|
||||||
`${v3MetadataCacheUri}/api/v1/aquarius/assets/ddo/${did}`,
|
|
||||||
{ cancelToken }
|
|
||||||
)
|
|
||||||
if (!response || response.status !== 200 || !response.data) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isCancel(error)) {
|
|
||||||
LoggerInstance.log(error.message)
|
|
||||||
} else {
|
|
||||||
LoggerInstance.error(error.message)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAssetsNames(
|
export async function getAssetsNames(
|
||||||
didList: string[],
|
didList: string[],
|
||||||
cancelToken: CancelToken
|
cancelToken: CancelToken
|
||||||
@ -397,6 +385,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(
|
export async function getDownloadAssets(
|
||||||
dtList: string[],
|
dtList: string[],
|
||||||
tokenOrders: OrdersData[],
|
tokenOrders: OrdersData[],
|
||||||
@ -439,3 +461,47 @@ export async function getDownloadAssets(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTagsList(
|
||||||
|
chainIds: number[],
|
||||||
|
cancelToken: CancelToken
|
||||||
|
): Promise<string[]> {
|
||||||
|
const baseQueryParams = {
|
||||||
|
chainIds,
|
||||||
|
esPaginationOptions: { from: 0, size: 0 }
|
||||||
|
} as BaseQueryParams
|
||||||
|
const query = {
|
||||||
|
...generateBaseQuery(baseQueryParams),
|
||||||
|
aggs: {
|
||||||
|
tags: {
|
||||||
|
terms: {
|
||||||
|
field: 'metadata.tags.keyword',
|
||||||
|
size: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<SearchResponse> = await axios.post(
|
||||||
|
`${metadataCacheUri}/api/aquarius/assets/query`,
|
||||||
|
{ ...query },
|
||||||
|
{ cancelToken }
|
||||||
|
)
|
||||||
|
if (response?.status !== 200 || !response?.data) return
|
||||||
|
const { buckets }: { buckets: AggregatedTag[] } =
|
||||||
|
response.data.aggregations.tags
|
||||||
|
|
||||||
|
const tagsList = buckets
|
||||||
|
.filter((tag) => tag.key !== '')
|
||||||
|
.map((tag) => tag.key)
|
||||||
|
|
||||||
|
return tagsList.sort()
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isCancel(error)) {
|
||||||
|
LoggerInstance.log(error.message)
|
||||||
|
} else {
|
||||||
|
LoggerInstance.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 './fetch'
|
||||||
import { fetchData } from './subgraph'
|
|
||||||
|
|
||||||
// make sure to only query for domains owned by account, so domains
|
const apiUrl = 'https://ens-proxy.oceanprotocol.com/api'
|
||||||
// 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'
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getEnsName(accountId: string): Promise<string> {
|
export async function getEnsName(accountId: string): Promise<string> {
|
||||||
const response: OperationResult<any> = await fetchData(
|
if (!accountId || accountId === '') return
|
||||||
UserEnsNames,
|
|
||||||
{ accountId: accountId.toLowerCase() },
|
|
||||||
ensSubgraphQueryContext
|
|
||||||
)
|
|
||||||
if (!response?.data?.domains?.length) return
|
|
||||||
|
|
||||||
// Default order of response.data.domains seems to be by creation time, from oldest to newest.
|
const data = await fetchData(`${apiUrl}/name?accountId=${accountId}`)
|
||||||
// Pick the last one as that is what direct web3 calls do.
|
return data?.name
|
||||||
const { name } = response.data.domains.slice(-1)[0]
|
|
||||||
return name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEnsAddress(ensName: string): Promise<string> {
|
export async function getEnsAddress(accountId: string): Promise<string> {
|
||||||
const response: OperationResult<any> = await fetchData(
|
if (!accountId || accountId === '' || !accountId.includes('.')) return
|
||||||
UserEnsAddress,
|
|
||||||
{ name: ensName },
|
const data = await fetchData(`${apiUrl}/address?name=${accountId}`)
|
||||||
ensSubgraphQueryContext
|
return data?.address
|
||||||
)
|
}
|
||||||
if (!response?.data?.domains?.length) return
|
|
||||||
const { id } = response.data.domains[0].resolvedAddress
|
export async function getEnsProfile(accountId: string): Promise<Profile> {
|
||||||
return id
|
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'
|
import axios, { AxiosResponse } from 'axios'
|
||||||
|
|
||||||
export async function fetchData(url: string): Promise<AxiosResponse['data']> {
|
export async function fetchData(url: string): Promise<AxiosResponse['data']> {
|
||||||
try {
|
try {
|
||||||
const response = await axios(url)
|
const response = await axios(url)
|
||||||
|
return response?.data
|
||||||
if (response.status !== 200) {
|
|
||||||
return console.error('Non-200 response: ' + response.status)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data
|
|
||||||
} catch (error) {
|
} 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'
|
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
|
// Run decimal.js comparison
|
||||||
// http://mikemcl.github.io/decimal.js/#cmp
|
// http://mikemcl.github.io/decimal.js/#cmp
|
||||||
export function compareAsBN(balance: string, price: string): boolean {
|
export function compareAsBN(balance: string, price: string): boolean {
|
||||||
|
@ -24,7 +24,7 @@ export function getDevelopmentConfig(): Config {
|
|||||||
// fixedRateExchangeAddress: contractAddresses.development?.FixedRateExchange,
|
// fixedRateExchangeAddress: contractAddresses.development?.FixedRateExchange,
|
||||||
// metadataContractAddress: contractAddresses.development?.Metadata,
|
// metadataContractAddress: contractAddresses.development?.Metadata,
|
||||||
// oceanTokenAddress: contractAddresses.development?.Ocean,
|
// oceanTokenAddress: contractAddresses.development?.Ocean,
|
||||||
// There is no subgraph in barge so we hardcode the Rinkeby one for now
|
// There is no subgraph in barge so we hardcode the Goerli one for now
|
||||||
subgraphUri: 'https://v4.subgraph.rinkeby.oceanprotocol.com'
|
subgraphUri: 'https://v4.subgraph.goerli.oceanprotocol.com'
|
||||||
} as Config
|
} as Config
|
||||||
}
|
}
|
||||||
|
@ -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'
|
import { fetchData } from './fetch'
|
||||||
|
|
||||||
const purgatoryUrl = 'https://market-purgatory.oceanprotocol.com/api/'
|
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 { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
|
||||||
import { OpcFeesQuery as OpcFeesData } from '../@types/subgraph/OpcFeesQuery'
|
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`
|
const PreviousOrderQuery = gql`
|
||||||
query AssetPreviousOrder($id: String!, $account: String!) {
|
query AssetPreviousOrder($id: String!, $account: String!) {
|
||||||
orders(
|
orders(
|
||||||
@ -153,29 +143,6 @@ export async function getOpcFees(chainId: number) {
|
|||||||
return opcFees
|
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(
|
export async function getUserTokenOrders(
|
||||||
accountId: string,
|
accountId: string,
|
||||||
chainIds: number[]
|
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(
|
export async function getOpcsApprovedTokens(
|
||||||
chainId: number
|
chainId: number
|
||||||
): Promise<TokenInfo[]> {
|
): Promise<TokenInfo[]> {
|
||||||
|
9
src/@utils/url.test.ts
Normal file
9
src/@utils/url.test.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { sanitizeUrl } from './url'
|
||||||
|
|
||||||
|
describe('@utils/url', () => {
|
||||||
|
test('sanitizeUrl', () => {
|
||||||
|
expect(sanitizeUrl('http://example.com')).toBe('http://example.com')
|
||||||
|
expect(sanitizeUrl('https://example.com')).toBe('https://example.com')
|
||||||
|
expect(sanitizeUrl('ftp://example.com')).toBe('about:blank')
|
||||||
|
})
|
||||||
|
})
|
@ -1,10 +1,5 @@
|
|||||||
export function sanitizeUrl(url: string) {
|
export function sanitizeUrl(url: string) {
|
||||||
const u = decodeURI(url).trim().toLowerCase()
|
const u = decodeURI(url).trim().toLowerCase()
|
||||||
if (
|
const isAllowedUrlScheme = u.startsWith('http://') || u.startsWith('https://')
|
||||||
u.startsWith('javascript:') ||
|
return isAllowedUrlScheme ? url : 'about:blank'
|
||||||
u.startsWith('data:') ||
|
|
||||||
u.startsWith('vbscript:')
|
|
||||||
)
|
|
||||||
return 'about:blank'
|
|
||||||
return url
|
|
||||||
}
|
}
|
||||||
|
45
src/@utils/veAllocation.ts
Normal file
45
src/@utils/veAllocation.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { AllLocked } from 'src/@types/subgraph/AllLocked'
|
||||||
|
import { gql, OperationResult } from 'urql'
|
||||||
|
import { fetchData, getQueryContext } from './subgraph'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const AllLocked = gql`
|
||||||
|
query AllLocked {
|
||||||
|
veOCEANs(first: 1000) {
|
||||||
|
lockedAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
interface TotalVe {
|
||||||
|
totalLocked: number
|
||||||
|
totalAllocated: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTotalAllocatedAndLocked(): Promise<TotalVe> {
|
||||||
|
const totals = {
|
||||||
|
totalLocked: 0,
|
||||||
|
totalAllocated: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryContext = getQueryContext(1)
|
||||||
|
|
||||||
|
const response = await axios.post(`https://df-sql.oceandao.org/nftinfo`)
|
||||||
|
totals.totalAllocated = response.data?.reduce(
|
||||||
|
(previousValue: number, currentValue: { ve_allocated: any }) =>
|
||||||
|
previousValue + Number(currentValue.ve_allocated),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
const fetchedLocked: OperationResult<AllLocked, any> = await fetchData(
|
||||||
|
AllLocked,
|
||||||
|
null,
|
||||||
|
queryContext
|
||||||
|
)
|
||||||
|
totals.totalLocked = fetchedLocked.data?.veOCEANs.reduce(
|
||||||
|
(previousValue, currentValue) =>
|
||||||
|
previousValue + Number(currentValue.lockedAmount),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
return totals
|
||||||
|
}
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -37,7 +37,7 @@ export default function AssetComputeSelection({
|
|||||||
</Dotdotdot>
|
</Dotdotdot>
|
||||||
</div>
|
</div>
|
||||||
<PriceUnit
|
<PriceUnit
|
||||||
price={asset.price}
|
price={Number(asset.price)}
|
||||||
size="small"
|
size="small"
|
||||||
className={styles.price}
|
className={styles.price}
|
||||||
/>
|
/>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import AssetTeaser from '@shared/AssetTeaser/AssetTeaser'
|
import AssetTeaser from '@shared/AssetTeaser'
|
||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import Pagination from '@shared/Pagination'
|
import Pagination from '@shared/Pagination'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import React, { ReactElement } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import Dotdotdot from 'react-dotdotdot'
|
|
||||||
import Price from '@shared/Price'
|
|
||||||
import removeMarkdown from 'remove-markdown'
|
|
||||||
import Publisher from '@shared/Publisher'
|
|
||||||
import AssetType from '@shared/AssetType'
|
|
||||||
import NetworkName from '@shared/NetworkName'
|
|
||||||
import styles from './AssetTeaser.module.css'
|
|
||||||
import { getServiceByName } from '@utils/ddo'
|
|
||||||
|
|
||||||
declare type AssetTeaserProps = {
|
|
||||||
asset: AssetExtended
|
|
||||||
noPublisher?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AssetTeaser({
|
|
||||||
asset,
|
|
||||||
noPublisher
|
|
||||||
}: AssetTeaserProps): ReactElement {
|
|
||||||
const { name, type, description } = asset.metadata
|
|
||||||
const { datatokens } = asset
|
|
||||||
const isCompute = Boolean(getServiceByName(asset, 'compute'))
|
|
||||||
const accessType = isCompute ? 'compute' : 'access'
|
|
||||||
const { owner } = asset.nft
|
|
||||||
const { orders } = asset.stats
|
|
||||||
return (
|
|
||||||
<article className={`${styles.teaser} ${styles[type]}`}>
|
|
||||||
<Link href={`/asset/${asset.id}`}>
|
|
||||||
<a className={styles.link}>
|
|
||||||
<header className={styles.header}>
|
|
||||||
<div className={styles.symbol}>{datatokens[0]?.symbol}</div>
|
|
||||||
<Dotdotdot tagName="h1" clamp={3} className={styles.title}>
|
|
||||||
{name.slice(0, 200)}
|
|
||||||
</Dotdotdot>
|
|
||||||
{!noPublisher && (
|
|
||||||
<Publisher account={owner} minimal className={styles.publisher} />
|
|
||||||
)}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<AssetType
|
|
||||||
type={type}
|
|
||||||
accessType={accessType}
|
|
||||||
className={styles.typeDetails}
|
|
||||||
totalSales={orders}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.content}>
|
|
||||||
<Dotdotdot tagName="p" clamp={3}>
|
|
||||||
{removeMarkdown(description?.substring(0, 300) || '')}
|
|
||||||
</Dotdotdot>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer className={styles.foot}>
|
|
||||||
<Price accessDetails={asset.accessDetails} size="small" />
|
|
||||||
<NetworkName networkId={asset.chainId} className={styles.network} />
|
|
||||||
</footer>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</article>
|
|
||||||
)
|
|
||||||
}
|
|
@ -9,6 +9,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding-top: calc(var(--spacer) / 2);
|
||||||
|
padding-bottom: calc(var(--spacer) / 2);
|
||||||
/* for sticking footer to bottom */
|
/* for sticking footer to bottom */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -18,8 +20,12 @@
|
|||||||
background-color: var(--background-body);
|
background-color: var(--background-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detailLine {
|
||||||
|
margin-bottom: calc(var(--spacer) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
margin-top: calc(var(--spacer) / 2);
|
margin-top: calc(var(--spacer) / 3);
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
/* for sticking footer to bottom */
|
/* for sticking footer to bottom */
|
||||||
@ -27,7 +33,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content p {
|
.content p {
|
||||||
margin-bottom: calc(var(--spacer) / 4);
|
margin-bottom: calc(var(--spacer) / 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@ -37,36 +43,20 @@
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.publisher {
|
.footer {
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.foot {
|
|
||||||
margin-top: calc(var(--spacer) / 4);
|
margin-top: calc(var(--spacer) / 4);
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.foot p {
|
.typeLabel {
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.symbol {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.typeDetails {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(var(--spacer) / 3);
|
|
||||||
right: calc(var(--spacer) / 3);
|
|
||||||
width: auto;
|
|
||||||
font-size: var(--font-size-mini);
|
font-size: var(--font-size-mini);
|
||||||
|
display: inline-block;
|
||||||
|
border-left: 1px solid var(--border-color);
|
||||||
|
padding-left: calc(var(--spacer) / 3.5);
|
||||||
|
margin-left: calc(var(--spacer) / 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.network {
|
.typeLabel:first-child {
|
||||||
font-size: var(--font-size-mini);
|
border-left: none;
|
||||||
position: absolute;
|
padding-left: 0;
|
||||||
right: calc(var(--spacer) / 3);
|
margin-left: 0;
|
||||||
bottom: calc(var(--spacer) / 3);
|
|
||||||
}
|
}
|
84
src/components/@shared/AssetTeaser/index.tsx
Normal file
84
src/components/@shared/AssetTeaser/index.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import Dotdotdot from 'react-dotdotdot'
|
||||||
|
import Price from '@shared/Price'
|
||||||
|
import removeMarkdown from 'remove-markdown'
|
||||||
|
import Publisher from '@shared/Publisher'
|
||||||
|
import AssetType from '@shared/AssetType'
|
||||||
|
import NetworkName from '@shared/NetworkName'
|
||||||
|
import styles from './index.module.css'
|
||||||
|
import { getServiceByName } from '@utils/ddo'
|
||||||
|
import { formatPrice } from '@shared/Price/PriceUnit'
|
||||||
|
import { useUserPreferences } from '@context/UserPreferences'
|
||||||
|
|
||||||
|
declare type AssetTeaserProps = {
|
||||||
|
asset: AssetExtended
|
||||||
|
noPublisher?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AssetTeaser({
|
||||||
|
asset,
|
||||||
|
noPublisher
|
||||||
|
}: AssetTeaserProps): ReactElement {
|
||||||
|
const { name, type, description } = asset.metadata
|
||||||
|
const { datatokens } = asset
|
||||||
|
const isCompute = Boolean(getServiceByName(asset, 'compute'))
|
||||||
|
const accessType = isCompute ? 'compute' : 'access'
|
||||||
|
const { owner } = asset.nft
|
||||||
|
const { orders, allocated } = asset.stats
|
||||||
|
const isUnsupportedPricing = asset?.accessDetails?.type === 'NOT_SUPPORTED'
|
||||||
|
const { locale } = useUserPreferences()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className={`${styles.teaser} ${styles[type]}`}>
|
||||||
|
<Link href={`/asset/${asset.id}`}>
|
||||||
|
<a className={styles.link}>
|
||||||
|
<aside className={styles.detailLine}>
|
||||||
|
<AssetType
|
||||||
|
className={styles.typeLabel}
|
||||||
|
type={type}
|
||||||
|
accessType={accessType}
|
||||||
|
/>
|
||||||
|
<span className={styles.typeLabel}>{datatokens[0]?.symbol}</span>
|
||||||
|
<NetworkName
|
||||||
|
networkId={asset.chainId}
|
||||||
|
className={styles.typeLabel}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<Dotdotdot tagName="h1" clamp={3} className={styles.title}>
|
||||||
|
{name.slice(0, 200)}
|
||||||
|
</Dotdotdot>
|
||||||
|
{!noPublisher && <Publisher account={owner} minimal />}
|
||||||
|
</header>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<Dotdotdot tagName="p" clamp={3}>
|
||||||
|
{removeMarkdown(description?.substring(0, 300) || '')}
|
||||||
|
</Dotdotdot>
|
||||||
|
</div>
|
||||||
|
{isUnsupportedPricing ? (
|
||||||
|
<strong>No pricing schema available</strong>
|
||||||
|
) : (
|
||||||
|
<Price accessDetails={asset.accessDetails} size="small" />
|
||||||
|
)}
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
{allocated && allocated > 0 ? (
|
||||||
|
<span className={styles.typeLabel}>
|
||||||
|
{allocated < 0
|
||||||
|
? ''
|
||||||
|
: `${formatPrice(allocated, locale)} veOCEAN`}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{orders && orders > 0 ? (
|
||||||
|
<span className={styles.typeLabel}>
|
||||||
|
{orders < 0
|
||||||
|
? 'N/A'
|
||||||
|
: `${orders} ${orders === 1 ? 'sale' : 'sales'}`}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</footer>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
@ -7,13 +7,11 @@ import Lock from '@images/lock.svg'
|
|||||||
export default function AssetType({
|
export default function AssetType({
|
||||||
type,
|
type,
|
||||||
accessType,
|
accessType,
|
||||||
className,
|
className
|
||||||
totalSales
|
|
||||||
}: {
|
}: {
|
||||||
type: string
|
type: string
|
||||||
accessType: string
|
accessType: string
|
||||||
className?: string
|
className?: string
|
||||||
totalSales?: number
|
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
return (
|
return (
|
||||||
<div className={className || null}>
|
<div className={className || null}>
|
||||||
@ -28,12 +26,6 @@ export default function AssetType({
|
|||||||
<div className={styles.typeLabel}>
|
<div className={styles.typeLabel}>
|
||||||
{type === 'dataset' ? 'dataset' : 'algorithm'}
|
{type === 'dataset' ? 'dataset' : 'algorithm'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{totalSales ? (
|
|
||||||
<div className={styles.typeLabel}>
|
|
||||||
{`${totalSales} ${totalSales === 1 ? 'sale' : 'sales'}`}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import filesize from 'filesize'
|
import { filesize } from 'filesize'
|
||||||
import classNames from 'classnames/bind'
|
|
||||||
import cleanupContentType from '@utils/cleanupContentType'
|
import cleanupContentType from '@utils/cleanupContentType'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
import Loader from '@shared/atoms/Loader'
|
import Loader from '@shared/atoms/Loader'
|
||||||
import { FileInfo } from '@oceanprotocol/lib'
|
import { FileInfo } from '@oceanprotocol/lib'
|
||||||
|
|
||||||
const cx = classNames.bind(styles)
|
|
||||||
|
|
||||||
function LoaderArea() {
|
function LoaderArea() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.loaderWrap}>
|
<div className={styles.loaderWrap}>
|
||||||
@ -27,11 +24,9 @@ export default function FileIcon({
|
|||||||
small?: boolean
|
small?: boolean
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const styleClasses = cx({
|
const styleClasses = `${styles.file} ${small ? styles.small : ''} ${
|
||||||
file: true,
|
className || ''
|
||||||
small,
|
}`
|
||||||
[className]: className
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={styleClasses}>
|
<ul className={styleClasses}>
|
||||||
@ -42,7 +37,7 @@ export default function FileIcon({
|
|||||||
<li>{cleanupContentType(file.contentType)}</li>
|
<li>{cleanupContentType(file.contentType)}</li>
|
||||||
<li>
|
<li>
|
||||||
{file.contentLength && file.contentLength !== '0'
|
{file.contentLength && file.contentLength !== '0'
|
||||||
? filesize(Number(file.contentLength))
|
? filesize(Number(file.contentLength)).toString()
|
||||||
: ''}
|
: ''}
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
|
@ -108,7 +108,7 @@ export default function AssetSelection({
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<PriceUnit
|
<PriceUnit
|
||||||
price={asset.price}
|
price={Number(asset.price)}
|
||||||
type={asset.price === '0' ? 'free' : undefined}
|
type={asset.price === '0' ? 'free' : undefined}
|
||||||
size="small"
|
size="small"
|
||||||
className={styles.price}
|
className={styles.price}
|
||||||
|
@ -11,6 +11,7 @@ import AssetSelection, {
|
|||||||
} from '../FormFields/AssetSelection'
|
} from '../FormFields/AssetSelection'
|
||||||
import Nft from '../FormFields/Nft'
|
import Nft from '../FormFields/Nft'
|
||||||
import InputRadio from './InputRadio'
|
import InputRadio from './InputRadio'
|
||||||
|
import TagsAutoComplete from './TagsAutoComplete'
|
||||||
|
|
||||||
const cx = classNames.bind(styles)
|
const cx = classNames.bind(styles)
|
||||||
|
|
||||||
@ -121,6 +122,8 @@ export default function InputElement({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case 'tags':
|
||||||
|
return <TagsAutoComplete {...field} {...props} />
|
||||||
default:
|
default:
|
||||||
return prefix || postfix ? (
|
return prefix || postfix ? (
|
||||||
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>
|
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>
|
||||||
|
101
src/components/@shared/FormInput/TagsAutoComplete.module.css
Normal file
101
src/components/@shared/FormInput/TagsAutoComplete.module.css
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
.select [class$='control'] {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: var(--background-content);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family-base);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
cursor: text;
|
||||||
|
min-height: 43px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select [class$='control']:hover {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select [class$='control']:focus-within {
|
||||||
|
border-color: var(--font-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select [class$='ValueContainer'] {
|
||||||
|
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select [class$='Input'] {
|
||||||
|
margin: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family-base);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select input {
|
||||||
|
color: var(--font-color-heading) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select [class$='menu'] {
|
||||||
|
background-color: var(--background-highlight);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select [class$='option'] {
|
||||||
|
color: var(--font-color-heading);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family-base);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select [class$='option']:active {
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select [class$='multiValue'],
|
||||||
|
.select [class$='multiValue'] > *,
|
||||||
|
.select [class$='multiValue']:hover > * {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: var(--background-highlight);
|
||||||
|
color: var(--font-color-text);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-family: var(--font-family-base);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select [class$='multiValue'] > div[role$='button'],
|
||||||
|
.select [class$='indicatorContainer'] svg {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select [class$='placeholder'] {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
color: var(--color-secondary);
|
||||||
|
font-weight: var(--font-weight-base);
|
||||||
|
transition: 0.2s ease-out;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select [class$='menu'] {
|
||||||
|
background-color: var(--background-content);
|
||||||
|
margin-top: -2px;
|
||||||
|
border: 1px solid var(--font-color-text);
|
||||||
|
border-top-color: var(--border-color);
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select [class$='menu'] [class$='option']:hover,
|
||||||
|
.select [class$='menu'] [class$='option']:focus-within {
|
||||||
|
background-color: var(--font-color-heading);
|
||||||
|
color: var(--background-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select [class$='NoOptionsMessage'] {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
color: var(--color-secondary);
|
||||||
|
text-align: left;
|
||||||
|
}
|
92
src/components/@shared/FormInput/TagsAutoComplete.tsx
Normal file
92
src/components/@shared/FormInput/TagsAutoComplete.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
|
import CreatableSelect from 'react-select/creatable'
|
||||||
|
import { OnChangeValue } from 'react-select'
|
||||||
|
import { useField } from 'formik'
|
||||||
|
import { InputProps } from '.'
|
||||||
|
import { getTagsList } from '@utils/aquarius'
|
||||||
|
import { chainIds } from 'app.config'
|
||||||
|
import { useCancelToken } from '@hooks/useCancelToken'
|
||||||
|
import styles from './TagsAutoComplete.module.css'
|
||||||
|
import { matchSorter } from 'match-sorter'
|
||||||
|
|
||||||
|
interface AutoCompleteOption {
|
||||||
|
readonly value: string
|
||||||
|
readonly label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TagsAutoComplete({
|
||||||
|
...props
|
||||||
|
}: InputProps): ReactElement {
|
||||||
|
const { name, placeholder } = props
|
||||||
|
const [tagsList, setTagsList] = useState<AutoCompleteOption[]>()
|
||||||
|
const [matchedTagsList, setMatchedTagsList] = useState<AutoCompleteOption[]>(
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const [field, meta, helpers] = useField(name)
|
||||||
|
const [input, setInput] = useState<string>()
|
||||||
|
|
||||||
|
const newCancelToken = useCancelToken()
|
||||||
|
|
||||||
|
const generateAutocompleteOptions = (
|
||||||
|
options: string[]
|
||||||
|
): AutoCompleteOption[] => {
|
||||||
|
return options?.map((tag) => ({
|
||||||
|
value: tag,
|
||||||
|
label: tag
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTags = !field.value
|
||||||
|
? undefined
|
||||||
|
: generateAutocompleteOptions(field.value)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const generateTagsList = async () => {
|
||||||
|
const tags = await getTagsList(chainIds, newCancelToken())
|
||||||
|
const autocompleteOptions = generateAutocompleteOptions(tags)
|
||||||
|
setTagsList(autocompleteOptions)
|
||||||
|
}
|
||||||
|
generateTagsList()
|
||||||
|
}, [newCancelToken])
|
||||||
|
|
||||||
|
const handleChange = (userInput: OnChangeValue<AutoCompleteOption, true>) => {
|
||||||
|
const normalizedInput = userInput.map((input) => input.value)
|
||||||
|
helpers.setValue(normalizedInput)
|
||||||
|
helpers.setTouched(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOptionsFilter = (
|
||||||
|
options: AutoCompleteOption[],
|
||||||
|
input: string
|
||||||
|
): void => {
|
||||||
|
setInput(input)
|
||||||
|
const matchedTagsList = matchSorter(options, input, { keys: ['value'] })
|
||||||
|
setMatchedTagsList(matchedTagsList)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CreatableSelect
|
||||||
|
components={{
|
||||||
|
DropdownIndicator: () => null,
|
||||||
|
IndicatorSeparator: () => null
|
||||||
|
}}
|
||||||
|
className={styles.select}
|
||||||
|
defaultValue={defaultTags}
|
||||||
|
hideSelectedOptions
|
||||||
|
isMulti
|
||||||
|
isClearable={false}
|
||||||
|
noOptionsMessage={() =>
|
||||||
|
'Start typing to get suggestions based on tags from all published assets.'
|
||||||
|
}
|
||||||
|
onChange={(value: AutoCompleteOption[]) => handleChange(value)}
|
||||||
|
onInputChange={(value) => handleOptionsFilter(tagsList, value)}
|
||||||
|
openMenuOnClick
|
||||||
|
options={!input || input?.length < 1 ? [] : matchedTagsList}
|
||||||
|
placeholder={placeholder}
|
||||||
|
theme={(theme) => ({
|
||||||
|
...theme,
|
||||||
|
colors: { ...theme.colors, primary25: 'var(--border-color)' }
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -71,9 +71,7 @@ function checkError(
|
|||||||
parsedFieldName: string[],
|
parsedFieldName: string[],
|
||||||
field: FieldInputProps<any>
|
field: FieldInputProps<any>
|
||||||
) {
|
) {
|
||||||
if (form?.errors === {}) {
|
if (
|
||||||
return false
|
|
||||||
} else if (
|
|
||||||
(form?.touched?.[parsedFieldName[0]]?.[parsedFieldName[1]] &&
|
(form?.touched?.[parsedFieldName[0]]?.[parsedFieldName[1]] &&
|
||||||
form?.errors?.[parsedFieldName[0]]?.[parsedFieldName[1]]) ||
|
form?.errors?.[parsedFieldName[0]]?.[parsedFieldName[1]]) ||
|
||||||
(form?.touched[field.name] &&
|
(form?.touched[field.name] &&
|
||||||
@ -140,11 +138,13 @@ export default function Input(props: Partial<InputProps>): ReactElement {
|
|||||||
</Label>
|
</Label>
|
||||||
<InputElement size={size} {...field} {...props} />
|
<InputElement size={size} {...field} {...props} />
|
||||||
{help && prominentHelp && <FormHelp>{help}</FormHelp>}
|
{help && prominentHelp && <FormHelp>{help}</FormHelp>}
|
||||||
{isFormikField && hasFormikError && (
|
|
||||||
|
{field?.name !== 'files' && isFormikField && hasFormikError && (
|
||||||
<div className={styles.error}>
|
<div className={styles.error}>
|
||||||
<ErrorMessage name={field.name} />
|
<ErrorMessage name={field.name} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{disclaimer && (
|
{disclaimer && (
|
||||||
<Disclaimer visible={disclaimerVisible}>{disclaimer}</Disclaimer>
|
<Disclaimer visible={disclaimerVisible}>{disclaimer}</Disclaimer>
|
||||||
)}
|
)}
|
||||||
|
@ -10,7 +10,7 @@ export default function Conversion({
|
|||||||
className,
|
className,
|
||||||
hideApproximateSymbol
|
hideApproximateSymbol
|
||||||
}: {
|
}: {
|
||||||
price: string // expects price in OCEAN, not wei
|
price: number // expects price in OCEAN, not wei
|
||||||
symbol: string
|
symbol: string
|
||||||
className?: string
|
className?: string
|
||||||
hideApproximateSymbol?: boolean
|
hideApproximateSymbol?: boolean
|
||||||
@ -28,18 +28,12 @@ export default function Conversion({
|
|||||||
const priceTokenId = getCoingeckoTokenId(symbol)
|
const priceTokenId = getCoingeckoTokenId(symbol)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (!prices || !price || !priceTokenId || !prices[priceTokenId]) {
|
||||||
!prices ||
|
|
||||||
!price ||
|
|
||||||
price === '0' ||
|
|
||||||
!priceTokenId ||
|
|
||||||
!prices[priceTokenId]
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversionValue = prices[priceTokenId][currency.toLowerCase()]
|
const conversionValue = prices[priceTokenId][currency.toLowerCase()]
|
||||||
const converted = conversionValue * Number(price)
|
const converted = conversionValue * price
|
||||||
const convertedFormatted = formatCurrency(
|
const convertedFormatted = formatCurrency(
|
||||||
converted,
|
converted,
|
||||||
// No passing of `currency` for non-fiat so symbol conversion
|
// No passing of `currency` for non-fiat so symbol conversion
|
||||||
@ -58,7 +52,7 @@ export default function Conversion({
|
|||||||
setPriceConverted(convertedFormattedHTMLstring)
|
setPriceConverted(convertedFormattedHTMLstring)
|
||||||
}, [price, prices, currency, locale, isFiat, priceTokenId])
|
}, [price, prices, currency, locale, isFiat, priceTokenId])
|
||||||
|
|
||||||
return Number(price) > 0 ? (
|
return Number(price) >= 0 ? (
|
||||||
<span
|
<span
|
||||||
className={`${styles.conversion} ${className || ''}`}
|
className={`${styles.conversion} ${className || ''}`}
|
||||||
title="Approximation based on the current spot price on Coingecko"
|
title="Approximation based on the current spot price on Coingecko"
|
||||||
|
@ -3,10 +3,9 @@ import { formatCurrency } from '@coingecko/cryptoformat'
|
|||||||
import Conversion from './Conversion'
|
import Conversion from './Conversion'
|
||||||
import styles from './PriceUnit.module.css'
|
import styles from './PriceUnit.module.css'
|
||||||
import { useUserPreferences } from '@context/UserPreferences'
|
import { useUserPreferences } from '@context/UserPreferences'
|
||||||
import Badge from '@shared/atoms/Badge'
|
|
||||||
|
|
||||||
export function formatPrice(price: string, locale: string): string {
|
export function formatPrice(price: number, locale: string): string {
|
||||||
return formatCurrency(Number(price), '', locale, false, {
|
return formatCurrency(price, '', locale, false, {
|
||||||
// Not exactly clear what `significant figures` are for this library,
|
// Not exactly clear what `significant figures` are for this library,
|
||||||
// but setting this seems to give us the formatting we want.
|
// but setting this seems to give us the formatting we want.
|
||||||
// See https://github.com/oceanprotocol/market/issues/70
|
// See https://github.com/oceanprotocol/market/issues/70
|
||||||
@ -22,7 +21,7 @@ export default function PriceUnit({
|
|||||||
symbol,
|
symbol,
|
||||||
type
|
type
|
||||||
}: {
|
}: {
|
||||||
price: string
|
price: number
|
||||||
type?: string
|
type?: string
|
||||||
className?: string
|
className?: string
|
||||||
size?: 'small' | 'mini' | 'large'
|
size?: 'small' | 'mini' | 'large'
|
||||||
@ -38,7 +37,7 @@ export default function PriceUnit({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{Number.isNaN(Number(price)) ? '-' : formatPrice(price, locale)}{' '}
|
{Number.isNaN(price) ? '-' : formatPrice(price, locale)}{' '}
|
||||||
<span className={styles.symbol}>{symbol}</span>
|
<span className={styles.symbol}>{symbol}</span>
|
||||||
</div>
|
</div>
|
||||||
{conversion && <Conversion price={price} symbol={symbol} />}
|
{conversion && <Conversion price={price} symbol={symbol} />}
|
||||||
|
@ -16,10 +16,11 @@ export default function Price({
|
|||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const isSupported =
|
const isSupported =
|
||||||
accessDetails?.type === 'fixed' || accessDetails?.type === 'free'
|
accessDetails?.type === 'fixed' || accessDetails?.type === 'free'
|
||||||
|
const price = `${orderPriceAndFees?.price || accessDetails?.price}`
|
||||||
|
|
||||||
return isSupported ? (
|
return isSupported ? (
|
||||||
<PriceUnit
|
<PriceUnit
|
||||||
price={`${orderPriceAndFees?.price || accessDetails?.price}`}
|
price={Number(price)}
|
||||||
symbol={accessDetails.baseToken?.symbol}
|
symbol={accessDetails.baseToken?.symbol}
|
||||||
className={className}
|
className={className}
|
||||||
size={size}
|
size={size}
|
||||||
|
@ -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;
|
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 React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
import classNames from 'classnames/bind'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import get3BoxProfile from '@utils/profile'
|
|
||||||
import { accountTruncate } from '@utils/web3'
|
import { accountTruncate } from '@utils/web3'
|
||||||
import axios from 'axios'
|
|
||||||
import { getEnsName } from '@utils/ens'
|
import { getEnsName } from '@utils/ens'
|
||||||
import { useIsMounted } from '@hooks/useIsMounted'
|
import { useIsMounted } from '@hooks/useIsMounted'
|
||||||
|
|
||||||
const cx = classNames.bind(styles)
|
export interface PublisherProps {
|
||||||
|
account: string
|
||||||
|
minimal?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function Publisher({
|
export default function Publisher({
|
||||||
account,
|
account,
|
||||||
minimal,
|
minimal,
|
||||||
className
|
className
|
||||||
}: {
|
}: PublisherProps): ReactElement {
|
||||||
account: string
|
|
||||||
minimal?: boolean
|
|
||||||
className?: string
|
|
||||||
}): ReactElement {
|
|
||||||
const isMounted = useIsMounted()
|
const isMounted = useIsMounted()
|
||||||
const [profile, setProfile] = useState<Profile>()
|
const [name, setName] = useState(accountTruncate(account))
|
||||||
const [name, setName] = useState('')
|
|
||||||
const [accountEns, setAccountEns] = useState<string>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!account) return
|
if (!account || account === '') return
|
||||||
|
|
||||||
// set default name on hook
|
// set default name on hook
|
||||||
// to avoid side effect (UI not updating on account's change)
|
// to avoid side effect (UI not updating on account's change)
|
||||||
setName(accountTruncate(account))
|
setName(accountTruncate(account))
|
||||||
|
|
||||||
const source = axios.CancelToken.source()
|
|
||||||
|
|
||||||
async function getExternalName() {
|
async function getExternalName() {
|
||||||
// ENS
|
|
||||||
const accountEns = await getEnsName(account)
|
const accountEns = await getEnsName(account)
|
||||||
if (accountEns && isMounted()) {
|
if (accountEns && isMounted()) {
|
||||||
setAccountEns(accountEns)
|
|
||||||
setName(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()
|
getExternalName()
|
||||||
|
|
||||||
return () => {
|
|
||||||
source.cancel()
|
|
||||||
}
|
|
||||||
}, [account, isMounted])
|
}, [account, isMounted])
|
||||||
|
|
||||||
const styleClasses = cx({
|
|
||||||
publisher: true,
|
|
||||||
[className]: className
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styleClasses}>
|
<div className={`${styles.publisher} ${className || ''}`}>
|
||||||
{minimal ? (
|
{minimal ? (
|
||||||
name
|
name
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Link href={`/profile/${accountEns || account}`}>
|
<Link href={`/profile/${account}`}>
|
||||||
<a title="Show profile page.">{name}</a>
|
<a title="Show profile page.">{name}</a>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
|
7
src/components/@shared/atoms/Alert/index.test.tsx
Normal file
7
src/components/@shared/atoms/Alert/index.test.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import testRender from '../../../../../.jest/testRender'
|
||||||
|
import Alert from '@shared/atoms/Alert'
|
||||||
|
|
||||||
|
describe('Alert', () => {
|
||||||
|
testRender(<Alert text="Alert text" state="info" />)
|
||||||
|
})
|
@ -1,4 +1,4 @@
|
|||||||
.blockies {
|
.avatar {
|
||||||
width: var(--font-size-large);
|
width: var(--font-size-large);
|
||||||
height: var(--font-size-large);
|
height: var(--font-size-large);
|
||||||
border-radius: 50%;
|
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"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
7
src/components/@shared/atoms/Badge/index.test.tsx
Normal file
7
src/components/@shared/atoms/Badge/index.test.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import testRender from '../../../../../.jest/testRender'
|
||||||
|
import Badge from '@shared/atoms/Badge'
|
||||||
|
|
||||||
|
describe('Badge', () => {
|
||||||
|
testRender(<Badge label="Badge text" />)
|
||||||
|
})
|
@ -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,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"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
8
src/components/@shared/atoms/Container/index.test.tsx
Normal file
8
src/components/@shared/atoms/Container/index.test.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import testRender from '../../../../../.jest/testRender'
|
||||||
|
import Container from '@shared/atoms/Container'
|
||||||
|
import { Default } from './index.stories'
|
||||||
|
|
||||||
|
describe('Container', () => {
|
||||||
|
testRender(<Container {...Default.args} />)
|
||||||
|
})
|
@ -1,12 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render, act, screen, fireEvent } from '@testing-library/react'
|
import { render, act, screen, fireEvent } from '@testing-library/react'
|
||||||
import { Default } from './index.stories'
|
import { Default } from './index.stories'
|
||||||
|
import Copy from '.'
|
||||||
|
|
||||||
jest.useFakeTimers()
|
jest.useFakeTimers()
|
||||||
|
|
||||||
describe('Copy', () => {
|
describe('Copy', () => {
|
||||||
test('should change class on click', () => {
|
test('should change class on click', () => {
|
||||||
render(<Default {...Default.args} />)
|
render(<Copy {...Default.args} />)
|
||||||
|
|
||||||
const element = screen.getByTitle('Copy to clipboard')
|
const element = screen.getByTitle('Copy to clipboard')
|
||||||
fireEvent.click(element)
|
fireEvent.click(element)
|
||||||
@ -14,7 +15,7 @@ describe('Copy', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('should remove class after timer end', () => {
|
test('should remove class after timer end', () => {
|
||||||
render(<Default {...Default.args} />)
|
render(<Copy {...Default.args} />)
|
||||||
|
|
||||||
const element = screen.getByTitle('Copy to clipboard')
|
const element = screen.getByTitle('Copy to clipboard')
|
||||||
fireEvent.click(element)
|
fireEvent.click(element)
|
||||||
|
8
src/components/@shared/atoms/Loader/index.test.tsx
Normal file
8
src/components/@shared/atoms/Loader/index.test.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import testRender from '../../../../../.jest/testRender'
|
||||||
|
import Loader from '@shared/atoms/Loader'
|
||||||
|
import { Default } from './index.stories'
|
||||||
|
|
||||||
|
describe('Loader', () => {
|
||||||
|
testRender(<Loader {...Default.args} />)
|
||||||
|
})
|
8
src/components/@shared/atoms/Logo/index.test.tsx
Normal file
8
src/components/@shared/atoms/Logo/index.test.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import testRender from '../../../../../.jest/testRender'
|
||||||
|
import Logo from '@shared/atoms/Logo'
|
||||||
|
import { Default } from './index.stories'
|
||||||
|
|
||||||
|
describe('Logo', () => {
|
||||||
|
testRender(<Logo {...Default.args} />)
|
||||||
|
})
|
8
src/components/@shared/atoms/Status/index.test.tsx
Normal file
8
src/components/@shared/atoms/Status/index.test.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import testRender from '../../../../../.jest/testRender'
|
||||||
|
import Status from '@shared/atoms/Status'
|
||||||
|
import { Default } from './index.stories'
|
||||||
|
|
||||||
|
describe('Status', () => {
|
||||||
|
testRender(<Status {...Default.args} />)
|
||||||
|
})
|
8
src/components/@shared/atoms/Time/index.test.tsx
Normal file
8
src/components/@shared/atoms/Time/index.test.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import testRender from '../../../../../.jest/testRender'
|
||||||
|
import Time from '@shared/atoms/Time'
|
||||||
|
import { Default } from './index.stories'
|
||||||
|
|
||||||
|
describe('Time', () => {
|
||||||
|
testRender(<Time {...Default.args} />)
|
||||||
|
})
|
@ -20,6 +20,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
border-left: 1px solid var(--border-color);
|
||||||
|
padding-left: calc(var(--spacer) / 3.5);
|
||||||
|
margin-left: calc(var(--spacer) / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat:first-child {
|
||||||
|
border-left: none;
|
||||||
|
padding-left: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.number {
|
.number {
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
color: var(--font-color-heading);
|
color: var(--font-color-heading);
|
||||||
|
@ -1,19 +1,43 @@
|
|||||||
import { useAsset } from '@context/Asset'
|
import { useAsset } from '@context/Asset'
|
||||||
import React from 'react'
|
import { useUserPreferences } from '@context/UserPreferences'
|
||||||
|
import { formatPrice } from '@shared/Price/PriceUnit'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
|
|
||||||
export default function AssetStats() {
|
export default function AssetStats() {
|
||||||
|
const { locale } = useUserPreferences()
|
||||||
const { asset } = useAsset()
|
const { asset } = useAsset()
|
||||||
|
const [orders, setOrders] = useState(0)
|
||||||
|
const [allocated, setAllocated] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!asset) return
|
||||||
|
|
||||||
|
const { orders, allocated } = asset.stats
|
||||||
|
|
||||||
|
setOrders(orders)
|
||||||
|
setAllocated(allocated)
|
||||||
|
}, [asset])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className={styles.stats}>
|
<footer className={styles.stats}>
|
||||||
{!asset || !asset?.stats || asset?.stats?.orders === 0 ? (
|
{allocated && allocated > 0 ? (
|
||||||
|
<span className={styles.stat}>
|
||||||
|
<span className={styles.number}>
|
||||||
|
{formatPrice(allocated, locale)}
|
||||||
|
</span>
|
||||||
|
veOCEAN
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{!asset || !asset?.stats || orders < 0 ? (
|
||||||
|
'N/A'
|
||||||
|
) : orders === 0 ? (
|
||||||
'No sales yet'
|
'No sales yet'
|
||||||
) : (
|
) : (
|
||||||
<>
|
<span className={styles.stat}>
|
||||||
<span className={styles.number}>{asset.stats.orders}</span> sale
|
<span className={styles.number}>{orders}</span> sale
|
||||||
{asset.stats.orders === 1 ? '' : 's'}
|
{orders === 1 ? '' : 's'}
|
||||||
</>
|
</span>
|
||||||
)}
|
)}
|
||||||
</footer>
|
</footer>
|
||||||
)
|
)
|
||||||
|
@ -45,7 +45,7 @@ function Row({
|
|||||||
<div className={styles.type}>{type}</div>
|
<div className={styles.type}>{type}</div>
|
||||||
<div>
|
<div>
|
||||||
<PriceUnit
|
<PriceUnit
|
||||||
price={hasPreviousOrder || hasDatatoken ? '0' : `${price}`}
|
price={hasPreviousOrder || hasDatatoken ? 0 : Number(price)}
|
||||||
symbol={symbol}
|
symbol={symbol}
|
||||||
size="small"
|
size="small"
|
||||||
className={styles.price}
|
className={styles.price}
|
||||||
@ -81,7 +81,7 @@ export default function PriceOutput({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.priceComponent}>
|
<div className={styles.priceComponent}>
|
||||||
You will pay{' '}
|
You will pay{' '}
|
||||||
<PriceUnit price={`${totalPrice}`} symbol={symbol} size="small" />
|
<PriceUnit price={Number(totalPrice)} symbol={symbol} size="small" />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
<div className={styles.calculation}>
|
<div className={styles.calculation}>
|
||||||
|
@ -12,10 +12,8 @@ import { useUserPreferences } from '@context/UserPreferences'
|
|||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
import Web3Feedback from '@shared/Web3Feedback'
|
import Web3Feedback from '@shared/Web3Feedback'
|
||||||
import { useCancelToken } from '@hooks/useCancelToken'
|
import { useCancelToken } from '@hooks/useCancelToken'
|
||||||
import {
|
import { getComputeSettingsInitialValues } from './_constants'
|
||||||
getComputeSettingsInitialValues,
|
import { computeSettingsValidationSchema } from './_validation'
|
||||||
computeSettingsValidationSchema
|
|
||||||
} from './_constants'
|
|
||||||
import content from '../../../../content/pages/editComputeDataset.json'
|
import content from '../../../../content/pages/editComputeDataset.json'
|
||||||
import { getServiceByName } from '@utils/ddo'
|
import { getServiceByName } from '@utils/ddo'
|
||||||
import { setMinterToPublisher, setMinterToDispenser } from '@utils/dispenser'
|
import { setMinterToPublisher, setMinterToDispenser } from '@utils/dispenser'
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feedback h3 {
|
.feedback h3 {
|
||||||
|
@ -7,7 +7,8 @@ import {
|
|||||||
Asset,
|
Asset,
|
||||||
Service
|
Service
|
||||||
} from '@oceanprotocol/lib'
|
} from '@oceanprotocol/lib'
|
||||||
import { validationSchema, getInitialValues } from './_constants'
|
import { validationSchema } from './_validation'
|
||||||
|
import { getInitialValues } from './_constants'
|
||||||
import { MetadataEditForm } from './_types'
|
import { MetadataEditForm } from './_types'
|
||||||
import { useWeb3 } from '@context/Web3'
|
import { useWeb3 } from '@context/Web3'
|
||||||
import { useUserPreferences } from '@context/UserPreferences'
|
import { useUserPreferences } from '@context/UserPreferences'
|
||||||
@ -72,7 +73,8 @@ export default function Edit({
|
|||||||
name: values.name,
|
name: values.name,
|
||||||
description: values.description,
|
description: values.description,
|
||||||
links: linksTransformed,
|
links: linksTransformed,
|
||||||
author: values.author
|
author: values.author,
|
||||||
|
tags: values.tags
|
||||||
}
|
}
|
||||||
|
|
||||||
asset?.accessDetails?.type === 'fixed' &&
|
asset?.accessDetails?.type === 'fixed' &&
|
||||||
|
@ -1,20 +1,7 @@
|
|||||||
import { FileInfo, Metadata, ServiceComputeOptions } from '@oceanprotocol/lib'
|
import { Metadata, ServiceComputeOptions } from '@oceanprotocol/lib'
|
||||||
import { secondsToString } from '@utils/ddo'
|
import { secondsToString } from '@utils/ddo'
|
||||||
import * as Yup from 'yup'
|
|
||||||
import { ComputeEditForm, MetadataEditForm } from './_types'
|
import { ComputeEditForm, MetadataEditForm } from './_types'
|
||||||
|
|
||||||
export const validationSchema = Yup.object().shape({
|
|
||||||
name: Yup.string()
|
|
||||||
.min(4, (param) => `Title must be at least ${param.min} characters`)
|
|
||||||
.required('Required'),
|
|
||||||
description: Yup.string().required('Required').min(10),
|
|
||||||
price: Yup.number().required('Required'),
|
|
||||||
links: Yup.array<any[]>().nullable(),
|
|
||||||
files: Yup.array<FileInfo[]>().nullable(),
|
|
||||||
timeout: Yup.string().required('Required'),
|
|
||||||
author: Yup.string().nullable()
|
|
||||||
})
|
|
||||||
|
|
||||||
export function getInitialValues(
|
export function getInitialValues(
|
||||||
metadata: Metadata,
|
metadata: Metadata,
|
||||||
timeout: number,
|
timeout: number,
|
||||||
@ -24,19 +11,14 @@ export function getInitialValues(
|
|||||||
name: metadata?.name,
|
name: metadata?.name,
|
||||||
description: metadata?.description,
|
description: metadata?.description,
|
||||||
price,
|
price,
|
||||||
links: metadata?.links,
|
links: metadata?.links as any,
|
||||||
files: '',
|
files: [{ url: '', type: '' }],
|
||||||
timeout: secondsToString(timeout),
|
timeout: secondsToString(timeout),
|
||||||
author: metadata?.author
|
author: metadata?.author,
|
||||||
|
tags: metadata?.tags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const computeSettingsValidationSchema = Yup.object().shape({
|
|
||||||
allowAllPublishedAlgorithms: Yup.boolean().nullable(),
|
|
||||||
publisherTrustedAlgorithms: Yup.array().nullable(),
|
|
||||||
publisherTrustedAlgorithmPublishers: Yup.array().nullable()
|
|
||||||
})
|
|
||||||
|
|
||||||
export function getComputeSettingsInitialValues({
|
export function getComputeSettingsInitialValues({
|
||||||
publisherTrustedAlgorithms,
|
publisherTrustedAlgorithms,
|
||||||
publisherTrustedAlgorithmPublishers
|
publisherTrustedAlgorithmPublishers
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
|
import { FileInfo } from '@oceanprotocol/lib'
|
||||||
export interface MetadataEditForm {
|
export interface MetadataEditForm {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
timeout: string
|
timeout: string
|
||||||
price?: string
|
price?: string
|
||||||
links?: string | any[]
|
files: FileInfo[]
|
||||||
files: string | any[]
|
links?: FileInfo[]
|
||||||
author?: string
|
author?: string
|
||||||
|
tags?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComputeEditForm {
|
export interface ComputeEditForm {
|
||||||
|
35
src/components/Asset/Edit/_validation.ts
Normal file
35
src/components/Asset/Edit/_validation.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { FileInfo } from '@oceanprotocol/lib'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
|
export const validationSchema = Yup.object().shape({
|
||||||
|
name: Yup.string()
|
||||||
|
.min(4, (param) => `Title must be at least ${param.min} characters`)
|
||||||
|
.required('Required'),
|
||||||
|
description: Yup.string().required('Required').min(10),
|
||||||
|
price: Yup.number().required('Required'),
|
||||||
|
files: Yup.array<FileInfo[]>()
|
||||||
|
.of(
|
||||||
|
Yup.object().shape({
|
||||||
|
url: Yup.string().url('Must be a valid URL.'),
|
||||||
|
valid: Yup.boolean().isTrue()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.nullable(),
|
||||||
|
links: Yup.array<FileInfo[]>()
|
||||||
|
.of(
|
||||||
|
Yup.object().shape({
|
||||||
|
url: Yup.string().url('Must be a valid URL.'),
|
||||||
|
valid: Yup.boolean().isTrue()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.nullable(),
|
||||||
|
timeout: Yup.string().required('Required'),
|
||||||
|
author: Yup.string().nullable(),
|
||||||
|
tags: Yup.array<string[]>().nullable()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const computeSettingsValidationSchema = Yup.object().shape({
|
||||||
|
allowAllPublishedAlgorithms: Yup.boolean().nullable(),
|
||||||
|
publisherTrustedAlgorithms: Yup.array().nullable(),
|
||||||
|
publisherTrustedAlgorithmPublishers: Yup.array().nullable()
|
||||||
|
})
|
@ -5,29 +5,25 @@ import Alert from '@shared/atoms/Alert'
|
|||||||
import Loader from '@shared/atoms/Loader'
|
import Loader from '@shared/atoms/Loader'
|
||||||
import { useAsset } from '@context/Asset'
|
import { useAsset } from '@context/Asset'
|
||||||
import AssetContent from './AssetContent'
|
import AssetContent from './AssetContent'
|
||||||
import { v3MarketUri } from 'app.config'
|
|
||||||
|
|
||||||
export default function AssetDetails({ uri }: { uri: string }): ReactElement {
|
export default function AssetDetails({ uri }: { uri: string }): ReactElement {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { asset, title, error, isInPurgatory, loading, isV3Asset } = useAsset()
|
const { asset, title, error, isInPurgatory, loading } = useAsset()
|
||||||
const [pageTitle, setPageTitle] = useState<string>()
|
const [pageTitle, setPageTitle] = useState<string>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isV3Asset) {
|
|
||||||
router.push(`${v3MarketUri}${uri}`)
|
|
||||||
}
|
|
||||||
if (!asset || error) {
|
if (!asset || error) {
|
||||||
setPageTitle(title || 'Could not retrieve asset')
|
setPageTitle(title || 'Could not retrieve asset')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setPageTitle(isInPurgatory ? '' : title)
|
setPageTitle(isInPurgatory ? '' : title)
|
||||||
}, [asset, error, isInPurgatory, isV3Asset, router, title, uri])
|
}, [asset, error, isInPurgatory, router, title, uri])
|
||||||
|
|
||||||
return asset && pageTitle !== undefined && !loading ? (
|
return asset && pageTitle !== undefined && !loading ? (
|
||||||
<Page title={pageTitle} uri={uri}>
|
<Page title={pageTitle} uri={uri}>
|
||||||
<AssetContent asset={asset} />
|
<AssetContent asset={asset} />
|
||||||
</Page>
|
</Page>
|
||||||
) : error && isV3Asset === false ? (
|
) : error ? (
|
||||||
<Page title={pageTitle} noPageHeader uri={uri}>
|
<Page title={pageTitle} noPageHeader uri={uri}>
|
||||||
<Alert title={pageTitle} text={error} state={'error'} />
|
<Alert title={pageTitle} text={error} state={'error'} />
|
||||||
</Page>
|
</Page>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import PriceUnit from '@shared/Price/PriceUnit'
|
||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import { StatsTotal } from './_types'
|
import { StatsTotal } from './_types'
|
||||||
|
|
||||||
@ -8,9 +9,12 @@ export default function MarketStatsTotal({
|
|||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<strong>{total.orders}</strong> orders across{' '}
|
<PriceUnit price={total.orders} size="small" /> orders across{' '}
|
||||||
<strong>{total.nfts}</strong> assets with{' '}
|
<PriceUnit price={total.nfts} size="small" /> assets with{' '}
|
||||||
<strong>{total.datatokens}</strong> different datatokens.
|
<PriceUnit price={total.datatokens} size="small" /> different datatokens.{' '}
|
||||||
|
<PriceUnit price={total.veAllocated} symbol="veOCEAN" size="small" />{' '}
|
||||||
|
allocated.{' '}
|
||||||
|
<PriceUnit price={total.veLocked} symbol="OCEAN" size="small" /> locked.
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -6,4 +6,6 @@ export interface StatsTotal {
|
|||||||
nfts: number
|
nfts: number
|
||||||
datatokens: number
|
datatokens: number
|
||||||
orders: number
|
orders: number
|
||||||
|
veAllocated: number
|
||||||
|
veLocked: number
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,14 @@ import { useMarketMetadata } from '@context/MarketMetadata'
|
|||||||
import Tooltip from '@shared/atoms/Tooltip'
|
import Tooltip from '@shared/atoms/Tooltip'
|
||||||
import Markdown from '@shared/Markdown'
|
import Markdown from '@shared/Markdown'
|
||||||
import content from '../../../../content/footer.json'
|
import content from '../../../../content/footer.json'
|
||||||
|
import { getTotalAllocatedAndLocked } from '@utils/veAllocation'
|
||||||
|
|
||||||
const initialTotal: StatsTotal = {
|
const initialTotal: StatsTotal = {
|
||||||
nfts: 0,
|
nfts: 0,
|
||||||
datatokens: 0,
|
datatokens: 0,
|
||||||
orders: 0
|
orders: 0,
|
||||||
|
veAllocated: 0,
|
||||||
|
veLocked: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MarketStats(): ReactElement {
|
export default function MarketStats(): ReactElement {
|
||||||
@ -34,15 +37,15 @@ export default function MarketStats(): ReactElement {
|
|||||||
// Set the main chain ids we want to display stats for
|
// Set the main chain ids we want to display stats for
|
||||||
//
|
//
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!networksList || !appConfig || !appConfig?.chainIdsSupported) return
|
if (!networksList || !appConfig || !appConfig?.chainIds) return
|
||||||
|
|
||||||
const mainChainIdsList = filterNetworksByType(
|
const mainChainIdsList = filterNetworksByType(
|
||||||
'mainnet',
|
'mainnet',
|
||||||
appConfig.chainIdsSupported,
|
appConfig.chainIds,
|
||||||
networksList
|
networksList
|
||||||
)
|
)
|
||||||
setMainChainIds(mainChainIdsList)
|
setMainChainIds(mainChainIdsList)
|
||||||
}, [appConfig, appConfig?.chainIdsSupported, networksList])
|
}, [appConfig, appConfig?.chainIds, networksList])
|
||||||
|
|
||||||
//
|
//
|
||||||
// Helper: fetch data from subgraph
|
// Helper: fetch data from subgraph
|
||||||
@ -68,6 +71,12 @@ export default function MarketStats(): ReactElement {
|
|||||||
LoggerInstance.error('Error fetching global stats: ', error.message)
|
LoggerInstance.error('Error fetching global stats: ', error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const veTotals = await getTotalAllocatedAndLocked()
|
||||||
|
total.veAllocated = veTotals.totalAllocated
|
||||||
|
total.veLocked = veTotals.totalLocked
|
||||||
|
setTotal(total)
|
||||||
|
|
||||||
setData(newData)
|
setData(newData)
|
||||||
}, [mainChainIds])
|
}, [mainChainIds])
|
||||||
|
|
||||||
@ -83,9 +92,7 @@ export default function MarketStats(): ReactElement {
|
|||||||
//
|
//
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data || !mainChainIds?.length) return
|
if (!data || !mainChainIds?.length) return
|
||||||
const newTotal: StatsTotal = {
|
const newTotal: StatsTotal = total
|
||||||
...initialTotal // always start calculating beginning from initial 0 values
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const chainId of mainChainIds) {
|
for (const chainId of mainChainIds) {
|
||||||
try {
|
try {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { ReactElement, ChangeEvent } from 'react'
|
import React, { ReactElement, ChangeEvent } from 'react'
|
||||||
import { DarkMode } from 'use-dark-mode'
|
import { DarkMode } from '@oceanprotocol/use-dark-mode'
|
||||||
import FormHelp from '@shared/FormInput/Help'
|
import FormHelp from '@shared/FormInput/Help'
|
||||||
import Label from '@shared/FormInput/Label'
|
import Label from '@shared/FormInput/Label'
|
||||||
import Moon from '@images/moon.svg'
|
import Moon from '@images/moon.svg'
|
||||||
|
@ -5,7 +5,7 @@ import styles from './index.module.css'
|
|||||||
import Currency from './Currency'
|
import Currency from './Currency'
|
||||||
import Debug from './Debug'
|
import Debug from './Debug'
|
||||||
import Caret from '@images/caret.svg'
|
import Caret from '@images/caret.svg'
|
||||||
import useDarkMode from 'use-dark-mode'
|
import useDarkMode from '@oceanprotocol/use-dark-mode'
|
||||||
import Appearance from './Appearance'
|
import Appearance from './Appearance'
|
||||||
import TokenApproval from './TokenApproval'
|
import TokenApproval from './TokenApproval'
|
||||||
import { useMarketMetadata } from '@context/MarketMetadata'
|
import { useMarketMetadata } from '@context/MarketMetadata'
|
||||||
|
@ -4,12 +4,13 @@ import { accountTruncate } from '@utils/web3'
|
|||||||
import Loader from '@shared/atoms/Loader'
|
import Loader from '@shared/atoms/Loader'
|
||||||
import styles from './Account.module.css'
|
import styles from './Account.module.css'
|
||||||
import { useWeb3 } from '@context/Web3'
|
import { useWeb3 } from '@context/Web3'
|
||||||
import Blockies from '@shared/atoms/Blockies'
|
import Avatar from '@shared/atoms/Avatar'
|
||||||
|
|
||||||
// Forward ref for Tippy.js
|
// Forward ref for Tippy.js
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const Account = React.forwardRef((props, ref: any) => {
|
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>) {
|
async function handleActivation(e: FormEvent<HTMLButtonElement>) {
|
||||||
// prevent accidentially submitting a form the button might be in
|
// prevent accidentially submitting a form the button might be in
|
||||||
@ -30,7 +31,7 @@ const Account = React.forwardRef((props, ref: any) => {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={(e) => e.preventDefault()}
|
onClick={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<Blockies accountId={accountId} />
|
<Avatar accountId={accountId} src={accountEnsAvatar} />
|
||||||
<span className={styles.address} title={accountId}>
|
<span className={styles.address} title={accountId}>
|
||||||
{accountTruncate(accountEns || accountId)}
|
{accountTruncate(accountEns || accountId)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -56,7 +56,7 @@ export default function Details(): ReactElement {
|
|||||||
</span>
|
</span>
|
||||||
<Conversion
|
<Conversion
|
||||||
className={styles.conversion}
|
className={styles.conversion}
|
||||||
price={value}
|
price={Number(value)}
|
||||||
symbol={key}
|
symbol={key}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
@ -48,7 +48,7 @@ export default function Bookmarks(): ReactElement {
|
|||||||
const newCancelToken = useCancelToken()
|
const newCancelToken = useCancelToken()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!appConfig?.metadataCacheUri || bookmarks === []) return
|
if (!appConfig?.metadataCacheUri || bookmarks?.length === 0) return
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
if (!bookmarks?.length) {
|
if (!bookmarks?.length) {
|
||||||
|
83
src/components/Home/SectionQueryResult.tsx
Normal file
83
src/components/Home/SectionQueryResult.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { useUserPreferences } from '@context/UserPreferences'
|
||||||
|
import { useCancelToken } from '@hooks/useCancelToken'
|
||||||
|
import { useIsMounted } from '@hooks/useIsMounted'
|
||||||
|
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
||||||
|
import AssetList from '@shared/AssetList'
|
||||||
|
import { queryMetadata } from '@utils/aquarius'
|
||||||
|
import React, { ReactElement, useState, useEffect } from 'react'
|
||||||
|
import styles from './index.module.css'
|
||||||
|
|
||||||
|
function sortElements(items: Asset[], sorted: string[]) {
|
||||||
|
items.sort(function (a, b) {
|
||||||
|
return sorted.indexOf(a.nftAddress) - sorted.indexOf(b.nftAddress)
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SectionQueryResult({
|
||||||
|
title,
|
||||||
|
query,
|
||||||
|
action,
|
||||||
|
queryData
|
||||||
|
}: {
|
||||||
|
title: ReactElement | string
|
||||||
|
query: SearchQuery
|
||||||
|
action?: ReactElement
|
||||||
|
queryData?: string[]
|
||||||
|
}): ReactElement {
|
||||||
|
const { chainIds } = useUserPreferences()
|
||||||
|
const [result, setResult] = useState<PagedAssets>()
|
||||||
|
const [loading, setLoading] = useState<boolean>()
|
||||||
|
const isMounted = useIsMounted()
|
||||||
|
const newCancelToken = useCancelToken()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!query) return
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
if (chainIds.length === 0) {
|
||||||
|
const result: PagedAssets = {
|
||||||
|
results: [],
|
||||||
|
page: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
totalResults: 0,
|
||||||
|
aggregations: undefined
|
||||||
|
}
|
||||||
|
setResult(result)
|
||||||
|
setLoading(false)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
const result = await queryMetadata(query, newCancelToken())
|
||||||
|
if (!isMounted()) return
|
||||||
|
if (queryData && result?.totalResults > 0) {
|
||||||
|
const sortedAssets = sortElements(result.results, queryData)
|
||||||
|
const overflow = sortedAssets.length - 6
|
||||||
|
sortedAssets.splice(sortedAssets.length - overflow, overflow)
|
||||||
|
result.results = sortedAssets
|
||||||
|
}
|
||||||
|
setResult(result)
|
||||||
|
setLoading(false)
|
||||||
|
} catch (error) {
|
||||||
|
LoggerInstance.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
init()
|
||||||
|
}, [chainIds.length, isMounted, newCancelToken, query, queryData])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
|
||||||
|
<AssetList
|
||||||
|
assets={result?.results}
|
||||||
|
showPagination={false}
|
||||||
|
isLoading={loading || !query}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{action && action}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
.blockies {
|
.avatar {
|
||||||
aspect-ratio: 1/1;
|
aspect-ratio: 1/1;
|
||||||
width: calc(var(--font-size-large) * 2) !important;
|
width: calc(var(--font-size-large) * 2) !important;
|
||||||
height: calc(var(--font-size-large) * 2) !important;
|
height: calc(var(--font-size-large) * 2) !important;
|
||||||
@ -8,7 +8,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.teaser {
|
.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);
|
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2);
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
position: relative;
|
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 { useUserPreferences } from '@context/UserPreferences'
|
||||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||||
import AccountList from '@shared/AccountList/AccountList'
|
import AccountList from 'src/components/Home/TopSales/AccountList'
|
||||||
import { getTopAssetsPublishers } from '@utils/subgraph'
|
import { getTopAssetsPublishers, UserSales } from '@utils/aquarius'
|
||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
|
|
||||||
export default function PublishersWithMostSales({
|
export default function TopSales({
|
||||||
title,
|
title,
|
||||||
action
|
action
|
||||||
}: {
|
}: {
|
||||||
@ -13,14 +13,14 @@ export default function PublishersWithMostSales({
|
|||||||
action?: ReactElement
|
action?: ReactElement
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const { chainIds } = useUserPreferences()
|
const { chainIds } = useUserPreferences()
|
||||||
const [result, setResult] = useState<AccountTeaserVM[]>([])
|
const [result, setResult] = useState<UserSales[]>([])
|
||||||
const [loading, setLoading] = useState<boolean>()
|
const [loading, setLoading] = useState<boolean>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function init() {
|
async function init() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
if (chainIds.length === 0) {
|
if (chainIds.length === 0) {
|
||||||
const result: AccountTeaserVM[] = []
|
const result: UserSales[] = []
|
||||||
setResult(result)
|
setResult(result)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
} else {
|
} else {
|
@ -1,103 +1,24 @@
|
|||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import AssetList from '@shared/AssetList'
|
|
||||||
import Button from '@shared/atoms/Button'
|
import Button from '@shared/atoms/Button'
|
||||||
import Bookmarks from './Bookmarks'
|
import Bookmarks from './Bookmarks'
|
||||||
import { generateBaseQuery, queryMetadata } from '@utils/aquarius'
|
import { generateBaseQuery } from '@utils/aquarius'
|
||||||
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
|
||||||
import { useUserPreferences } from '@context/UserPreferences'
|
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 { 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[]) {
|
import SectionQueryResult from './SectionQueryResult'
|
||||||
items.sort(function (a, b) {
|
|
||||||
return (
|
|
||||||
sorted.indexOf(a.services[0].datatokenAddress.toLowerCase()) -
|
|
||||||
sorted.indexOf(b.services[0].datatokenAddress.toLowerCase())
|
|
||||||
)
|
|
||||||
})
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
function SectionQueryResult({
|
|
||||||
title,
|
|
||||||
query,
|
|
||||||
action,
|
|
||||||
queryData
|
|
||||||
}: {
|
|
||||||
title: ReactElement | string
|
|
||||||
query: SearchQuery
|
|
||||||
action?: ReactElement
|
|
||||||
queryData?: string[]
|
|
||||||
}) {
|
|
||||||
const { chainIds } = useUserPreferences()
|
|
||||||
const [result, setResult] = useState<PagedAssets>()
|
|
||||||
const [loading, setLoading] = useState<boolean>()
|
|
||||||
const isMounted = useIsMounted()
|
|
||||||
const newCancelToken = useCancelToken()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!query) return
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
if (chainIds.length === 0) {
|
|
||||||
const result: PagedAssets = {
|
|
||||||
results: [],
|
|
||||||
page: 0,
|
|
||||||
totalPages: 0,
|
|
||||||
totalResults: 0,
|
|
||||||
aggregations: undefined
|
|
||||||
}
|
|
||||||
setResult(result)
|
|
||||||
setLoading(false)
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
const result = await queryMetadata(query, newCancelToken())
|
|
||||||
if (!isMounted()) return
|
|
||||||
if (queryData && result?.totalResults > 0) {
|
|
||||||
const sortedAssets = sortElements(result.results, queryData)
|
|
||||||
const overflow = sortedAssets.length - 9
|
|
||||||
sortedAssets.splice(sortedAssets.length - overflow, overflow)
|
|
||||||
result.results = sortedAssets
|
|
||||||
}
|
|
||||||
setResult(result)
|
|
||||||
setLoading(false)
|
|
||||||
} catch (error) {
|
|
||||||
LoggerInstance.error(error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
init()
|
|
||||||
}, [chainIds.length, isMounted, newCancelToken, query, queryData])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className={styles.section}>
|
|
||||||
<h3>{title}</h3>
|
|
||||||
|
|
||||||
<AssetList
|
|
||||||
assets={result?.results}
|
|
||||||
showPagination={false}
|
|
||||||
isLoading={loading || !query}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{action && action}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HomePage(): ReactElement {
|
export default function HomePage(): ReactElement {
|
||||||
const [queryLatest, setQueryLatest] = useState<SearchQuery>()
|
const [queryLatest, setQueryLatest] = useState<SearchQuery>()
|
||||||
const [queryMostSales, setQueryMostSales] = useState<SearchQuery>()
|
const [queryMostSales, setQueryMostSales] = useState<SearchQuery>()
|
||||||
|
const [queryMostAllocation, setQueryMostAllocation] = useState<SearchQuery>()
|
||||||
const { chainIds } = useUserPreferences()
|
const { chainIds } = useUserPreferences()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const baseParams = {
|
const baseParams = {
|
||||||
chainIds,
|
chainIds,
|
||||||
esPaginationOptions: {
|
esPaginationOptions: {
|
||||||
size: 9
|
size: 6
|
||||||
},
|
},
|
||||||
sortOptions: {
|
sortOptions: {
|
||||||
sortBy: SortTermOptions.Created
|
sortBy: SortTermOptions.Created
|
||||||
@ -108,13 +29,23 @@ export default function HomePage(): ReactElement {
|
|||||||
const baseParamsSales = {
|
const baseParamsSales = {
|
||||||
chainIds,
|
chainIds,
|
||||||
esPaginationOptions: {
|
esPaginationOptions: {
|
||||||
size: 9
|
size: 6
|
||||||
},
|
},
|
||||||
sortOptions: {
|
sortOptions: {
|
||||||
sortBy: SortTermOptions.Stats
|
sortBy: SortTermOptions.Stats
|
||||||
} as SortOptions
|
} as SortOptions
|
||||||
} as BaseQueryParams
|
} as BaseQueryParams
|
||||||
setQueryMostSales(generateBaseQuery(baseParamsSales))
|
setQueryMostSales(generateBaseQuery(baseParamsSales))
|
||||||
|
const baseParamsAllocation = {
|
||||||
|
chainIds,
|
||||||
|
esPaginationOptions: {
|
||||||
|
size: 6
|
||||||
|
},
|
||||||
|
sortOptions: {
|
||||||
|
sortBy: SortTermOptions.Allocated
|
||||||
|
} as SortOptions
|
||||||
|
} as BaseQueryParams
|
||||||
|
setQueryMostAllocation(generateBaseQuery(baseParamsAllocation))
|
||||||
}, [chainIds])
|
}, [chainIds])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -124,8 +55,15 @@ export default function HomePage(): ReactElement {
|
|||||||
<Bookmarks />
|
<Bookmarks />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<SectionQueryResult
|
||||||
|
title="Highest veOCEAN Allocations"
|
||||||
|
query={queryMostAllocation}
|
||||||
|
/>
|
||||||
|
|
||||||
<SectionQueryResult title="Most Sales" query={queryMostSales} />
|
<SectionQueryResult title="Most Sales" query={queryMostSales} />
|
||||||
|
|
||||||
|
<TopSales title="Publishers With Most Sales" />
|
||||||
|
|
||||||
<SectionQueryResult
|
<SectionQueryResult
|
||||||
title="Recently Published"
|
title="Recently Published"
|
||||||
query={queryLatest}
|
query={queryLatest}
|
||||||
@ -135,8 +73,6 @@ export default function HomePage(): ReactElement {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PublishersWithMostSales title="Publishers With Most Sales" />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,10 @@ import ExplorerLink from '@shared/ExplorerLink'
|
|||||||
import NetworkName from '@shared/NetworkName'
|
import NetworkName from '@shared/NetworkName'
|
||||||
import Jellyfish from '@oceanprotocol/art/creatures/jellyfish/jellyfish-grid.svg'
|
import Jellyfish from '@oceanprotocol/art/creatures/jellyfish/jellyfish-grid.svg'
|
||||||
import Copy from '@shared/atoms/Copy'
|
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 styles from './Account.module.css'
|
||||||
import { useProfile } from '@context/Profile'
|
import { useProfile } from '@context/Profile'
|
||||||
|
import { accountTruncate } from '@utils/web3'
|
||||||
|
|
||||||
export default function Account({
|
export default function Account({
|
||||||
accountId
|
accountId
|
||||||
@ -19,28 +20,27 @@ export default function Account({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.account}>
|
<div className={styles.account}>
|
||||||
<figure className={styles.imageWrap}>
|
<figure className={styles.imageWrap}>
|
||||||
{profile?.image ? (
|
{accountId ? (
|
||||||
<img
|
<Avatar
|
||||||
src={profile?.image}
|
accountId={accountId}
|
||||||
|
src={profile?.avatar}
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
width="96"
|
|
||||||
height="96"
|
|
||||||
/>
|
/>
|
||||||
) : accountId ? (
|
|
||||||
<Blockies accountId={accountId} className={styles.image} />
|
|
||||||
) : (
|
) : (
|
||||||
<Jellyfish className={styles.image} />
|
<Jellyfish className={styles.image} />
|
||||||
)}
|
)}
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className={styles.name}>{profile?.name}</h3>
|
<h3 className={styles.name}>
|
||||||
|
{profile?.name || accountTruncate(accountId)}
|
||||||
|
</h3>
|
||||||
{accountId && (
|
{accountId && (
|
||||||
<code
|
<code
|
||||||
className={styles.accountId}
|
className={styles.accountId}
|
||||||
title={profile?.accountEns ? accountId : null}
|
title={profile?.name ? accountId : null}
|
||||||
>
|
>
|
||||||
{profile?.accountEns || accountId} <Copy text={accountId} />
|
{accountId} <Copy text={accountId} />
|
||||||
</code>
|
</code>
|
||||||
)}
|
)}
|
||||||
<p>
|
<p>
|
||||||
|
@ -24,5 +24,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.linksExternal {
|
.linksExternal {
|
||||||
composes: linksExternal from '@shared/Publisher/index.module.css';
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
display: inline-block;
|
||||||
|
fill: var(--color-secondary);
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user