mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Merge branch 'main' into feature/issue-1418-dark-mode-toggle
This commit is contained in:
commit
4ef734891a
@ -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:
|
||||||
|
73
.jest/__mocks__/MarketMetadata.ts
Normal file
73
.jest/__mocks__/MarketMetadata.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import siteContent from '../../content/site.json'
|
||||||
|
import appConfig from '../../app.config'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getOpcFeeForToken: jest.fn(),
|
||||||
|
siteContent,
|
||||||
|
appConfig,
|
||||||
|
opcFees: [
|
||||||
|
{
|
||||||
|
chainId: 1,
|
||||||
|
approvedTokens: [
|
||||||
|
'0x0642026e7f0b6ccac5925b4e7fa61384250e1701',
|
||||||
|
'0x967da4048cd07ab37855c090aaf366e4ce1b9f48'
|
||||||
|
],
|
||||||
|
swapApprovedFee: '0.001',
|
||||||
|
swapNotApprovedFee: '0.002'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chainId: 137,
|
||||||
|
approvedTokens: [
|
||||||
|
'0x282d8efce846a88b159800bd4130ad77443fa1a1',
|
||||||
|
'0xc5248aa0629c0b2d6a02834a5f172937ac83cbd3'
|
||||||
|
],
|
||||||
|
swapApprovedFee: '0.001',
|
||||||
|
swapNotApprovedFee: '0.002'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chainId: 56,
|
||||||
|
approvedTokens: ['0xdce07662ca8ebc241316a15b611c89711414dd1a'],
|
||||||
|
swapApprovedFee: '0.001',
|
||||||
|
swapNotApprovedFee: '0.002'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chainId: 246,
|
||||||
|
approvedTokens: ['0x593122aae80a6fc3183b2ac0c4ab3336debee528'],
|
||||||
|
swapApprovedFee: '0.001',
|
||||||
|
swapNotApprovedFee: '0.002'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chainId: 1285,
|
||||||
|
approvedTokens: ['0x99c409e5f62e4bd2ac142f17cafb6810b8f0baae'],
|
||||||
|
swapApprovedFee: '0.001',
|
||||||
|
swapNotApprovedFee: '0.002'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chainId: 3,
|
||||||
|
approvedTokens: ['0x5e8dcb2afa23844bcc311b00ad1a0c30025aade9'],
|
||||||
|
swapApprovedFee: '0.001',
|
||||||
|
swapNotApprovedFee: '0.002'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chainId: 4,
|
||||||
|
approvedTokens: [
|
||||||
|
'0x8967bcf84170c91b0d24d4302c2376283b0b3a07',
|
||||||
|
'0xd92e713d051c37ebb2561803a3b5fbabc4962431'
|
||||||
|
],
|
||||||
|
swapApprovedFee: '0.001',
|
||||||
|
swapNotApprovedFee: '0.002'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chainId: 80001,
|
||||||
|
approvedTokens: ['0xd8992ed72c445c35cb4a2be468568ed1079357c8'],
|
||||||
|
swapApprovedFee: '0.001',
|
||||||
|
swapNotApprovedFee: '0.002'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chainId: 1287,
|
||||||
|
approvedTokens: ['0xf6410bf5d773c7a41ebff972f38e7463fa242477'],
|
||||||
|
swapApprovedFee: '0.001',
|
||||||
|
swapNotApprovedFee: '0.002'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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',
|
||||||
|
@ -1,2 +1,7 @@
|
|||||||
import '@testing-library/jest-dom/extend-expect'
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
import './__mocks__/matchMedia'
|
import './__mocks__/matchMedia'
|
||||||
|
import marketMetadataMock from './__mocks__/MarketMetadata'
|
||||||
|
|
||||||
|
jest.mock('../../src/@context/MarketMetadata', () => ({
|
||||||
|
useMarketMetadata: () => marketMetadataMock
|
||||||
|
}))
|
||||||
|
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())
|
|
||||||
}
|
|
||||||
})
|
|
54
README.md
54
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)
|
||||||
@ -103,12 +103,12 @@ cp .env.example .env
|
|||||||
|
|
||||||
## 🦀 Data Sources
|
## 🦀 Data Sources
|
||||||
|
|
||||||
All displayed data in the app is presented around the concept of one data set, which is a combination of:
|
All displayed data in the app is presented around the concept of one asset, which is a combination of:
|
||||||
|
|
||||||
- metadata about a data set
|
- metadata about an asset
|
||||||
- the actual data set files
|
- the actual asset file
|
||||||
- the NFT which represents the data set
|
- the NFT which represents the asset
|
||||||
- the datatokens representing access rights to the data set files
|
- the datatokens representing access rights to the asset file
|
||||||
- financial data connected to these datatokens, either a fixed rate exchange contract or a dispenser for free assets
|
- financial data connected to these datatokens, either a fixed rate exchange contract or a dispenser for free assets
|
||||||
- calculations and conversions based on financial data
|
- calculations and conversions based on financial data
|
||||||
- metadata about publisher accounts
|
- metadata about publisher accounts
|
||||||
@ -117,7 +117,7 @@ All this data then comes from multiple sources:
|
|||||||
|
|
||||||
### Aquarius
|
### Aquarius
|
||||||
|
|
||||||
All initial data sets and their metadata (DDO) is retrieved client-side on run-time from the [Aquarius](https://github.com/oceanprotocol/aquarius) instance, defined in `app.config.js`. All app calls to Aquarius are done with 2 internal methods which mimic the same methods in ocean.js, but allow us:
|
All initial assets and their metadata (DDO) is retrieved client-side on run-time from the [Aquarius](https://github.com/oceanprotocol/aquarius) instance, defined in `app.config.js`. All app calls to Aquarius are done with 2 internal methods which mimic the same methods in ocean.js, but allow us:
|
||||||
|
|
||||||
- to cancel requests when components get unmounted in combination with [axios](https://github.com/axios/axios)
|
- to cancel requests when components get unmounted in combination with [axios](https://github.com/axios/axios)
|
||||||
- hit Aquarius as early as possible without relying on any ocean.js initialization
|
- hit Aquarius as early as possible without relying on any ocean.js initialization
|
||||||
@ -159,7 +159,7 @@ function Component() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
For components within a single data set view the `useAsset()` hook can be used, which in the background gets the respective metadata from Aquarius.
|
For components within a single asset view the `useAsset()` hook can be used, which in the background gets the respective metadata from Aquarius.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useAsset } from '@context/Asset'
|
import { useAsset } from '@context/Asset'
|
||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -232,7 +216,7 @@ function Component() {
|
|||||||
|
|
||||||
### Purgatory
|
### Purgatory
|
||||||
|
|
||||||
Based on [list-purgatory](https://github.com/oceanprotocol/list-purgatory) some data sets get additional data. Within most components this can be done with the internal `useAsset()` hook which fetches data from the [market-purgatory](https://github.com/oceanprotocol/market-purgatory) endpoint in the background.
|
Based on [list-purgatory](https://github.com/oceanprotocol/list-purgatory) some assets get additional data. Within most components this can be done with the internal `useAsset()` hook which fetches data from the [market-purgatory](https://github.com/oceanprotocol/market-purgatory) endpoint in the background.
|
||||||
|
|
||||||
For asset purgatory:
|
For asset purgatory:
|
||||||
|
|
||||||
@ -399,6 +383,12 @@ Additionally, we would also advise that your retain the text saying "Powered by
|
|||||||
|
|
||||||
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!
|
||||||
|
|
||||||
|
If you are looking to fork Ocean Market and create your own marketplace, you will find the following guides useful in our docs:
|
||||||
|
|
||||||
|
- [Forking Ocean Market](https://docs.oceanprotocol.com/building-with-ocean/build-a-marketplace/forking-ocean-market)
|
||||||
|
- [Customising your Market](https://docs.oceanprotocol.com/building-with-ocean/build-a-marketplace/customising-your-market)
|
||||||
|
- [Deploying your Market](https://docs.oceanprotocol.com/building-with-ocean/build-a-marketplace/deploying-market)
|
||||||
|
|
||||||
## 💰 Pricing Options
|
## 💰 Pricing Options
|
||||||
|
|
||||||
### Fixed Pricing
|
### Fixed Pricing
|
||||||
@ -409,7 +399,7 @@ To allow publishers to set pricing as "Fixed" you need to add the following envi
|
|||||||
|
|
||||||
To allow publishers to set pricing as "Free" you need to add the following environmental variable to your .env file: `NEXT_PUBLIC_ALLOW_FREE_PRICING="true"` (default).
|
To allow publishers to set pricing as "Free" you need to add the following environmental variable to your .env file: `NEXT_PUBLIC_ALLOW_FREE_PRICING="true"` (default).
|
||||||
|
|
||||||
This allocates the datatokens to the [dispenser contract](https://github.com/oceanprotocol/contracts/blob/main/contracts/dispenser/Dispenser.sol) which dispenses data tokens to users for free. Publishers in your market will now be able to offer their datasets to users for free (excluding gas costs).
|
This allocates the datatokens to the [dispenser contract](https://github.com/oceanprotocol/contracts/blob/main/contracts/dispenser/Dispenser.sol) which dispenses data tokens to users for free. Publishers in your market will now be able to offer their assets to users for free (excluding gas costs).
|
||||||
|
|
||||||
## ✅ GDPR Compliance
|
## ✅ GDPR Compliance
|
||||||
|
|
||||||
|
@ -62,7 +62,11 @@ module.exports = {
|
|||||||
'LINK'
|
'LINK'
|
||||||
],
|
],
|
||||||
|
|
||||||
// Config for https://github.com/donavon/use-dark-mode
|
// Tokens to fetch the spot prices from coingecko, against above currencies.
|
||||||
|
// Refers to Coingecko API tokenIds.
|
||||||
|
coingeckoTokenIds: ['ocean-protocol', 'h2o', 'ethereum', 'matic-network'],
|
||||||
|
|
||||||
|
// Config for https://github.com/oceanprotocol/use-dark-mode
|
||||||
darkModeConfig: {
|
darkModeConfig: {
|
||||||
classNameDark: 'dark',
|
classNameDark: 'dark',
|
||||||
classNameLight: 'light',
|
classNameLight: 'light',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"asset": {
|
"asset": {
|
||||||
"title": "Data Set In Purgatory",
|
"title": "Dataset In Purgatory",
|
||||||
"description": "Except for removing liquidity, no further actions are permitted on this dataset and it will not be returned in any search. For more details go to [list-purgatory](https://github.com/oceanprotocol/list-purgatory)."
|
"description": "Except for removing liquidity, no further actions are permitted on this dataset and it will not be returned in any search. For more details go to [list-purgatory](https://github.com/oceanprotocol/list-purgatory)."
|
||||||
},
|
},
|
||||||
"account": {
|
"account": {
|
||||||
|
11
netlify.toml
Normal file
11
netlify.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[[headers]]
|
||||||
|
for = "/_next/image/*"
|
||||||
|
|
||||||
|
[headers.values]
|
||||||
|
Content-Security-Policy= "upgrade-insecure-requests"
|
||||||
|
Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
|
||||||
|
X-XSS-Protection = "1; mode=block"
|
||||||
|
X-Frame-Options = "DENY"
|
||||||
|
X-Content-Type-Options = "nosniff"
|
||||||
|
Referrer-Policy = "strict-origin-when-cross-origin"
|
||||||
|
Permissions-Policy= "accelerometer=(self), ambient-light-sensor=(self), autoplay=(self), battery=(self), camera=(self), cross-origin-isolated=(self), display-capture=(self), document-domain=(self), encrypted-media=(self), execution-while-not-rendered=(self), execution-while-out-of-viewport=(self), fullscreen=(self), geolocation=(self), gyroscope=(self), keyboard-map=(self), magnetometer=(self), microphone=(self), midi=(self), navigation-override=(self), payment=(self), picture-in-picture=(self), publickey-credentials-get=(self), screen-wake-lock=(self), sync-xhr=(self), usb=(self), web-share=(self), xr-spatial-tracking=(self), clipboard-read=(self), clipboard-write=(self), gamepad=(self), speaker-selection=(self), conversion-measurement=(self), focus-without-user-activation=(self), hid=(self), idle-detection=(self), interest-cohort=(self), serial=(self), sync-script=(self), trust-token-redemption=(self), window-placement=(self), vertical-scroll=(self)"
|
61084
package-lock.json
generated
61084
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
74
package.json
74
package.json
@ -17,7 +17,7 @@
|
|||||||
"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.ropsten.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,28 +26,27 @@
|
|||||||
"@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",
|
||||||
"@oceanprotocol/lib": "^1.1.6",
|
"@oceanprotocol/lib": "^2.0.2",
|
||||||
"@oceanprotocol/typographies": "^0.1.0",
|
"@oceanprotocol/typographies": "^0.1.0",
|
||||||
"@storybook/theming": "^6.5.9",
|
"@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": "^9.0.11",
|
||||||
"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",
|
||||||
"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",
|
||||||
@ -65,58 +64,53 @@
|
|||||||
"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",
|
"@storybook/theming": "^6.5.9",
|
||||||
"@storybook/testing-library": "^0.0.11",
|
"@svgr/webpack": "^6.3.1",
|
||||||
"@storybook/testing-react": "^1.3.0",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@svgr/webpack": "^6.2.1",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@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.14",
|
||||||
"@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.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.31.0",
|
"@typescript-eslint/parser": "^5.38.0",
|
||||||
"@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.3",
|
||||||
"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.6.4",
|
||||||
"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.0.3",
|
||||||
"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",
|
||||||
|
@ -16,6 +16,7 @@ export interface AppConfig {
|
|||||||
consumeMarketOrderFee: string
|
consumeMarketOrderFee: string
|
||||||
consumeMarketFixedSwapFee: string
|
consumeMarketFixedSwapFee: string
|
||||||
currencies: string[]
|
currencies: string[]
|
||||||
|
coingeckoTokenIds: string[]
|
||||||
allowFixedPricing: string
|
allowFixedPricing: string
|
||||||
allowFreePricing: string
|
allowFreePricing: string
|
||||||
defaultPrivacyPolicySlug: string
|
defaultPrivacyPolicySlug: string
|
||||||
|
@ -14,6 +14,7 @@ import { MarketMetadataProviderValue, OpcFee } from './_types'
|
|||||||
import siteContent from '../../../content/site.json'
|
import siteContent from '../../../content/site.json'
|
||||||
import appConfig from '../../../app.config'
|
import appConfig from '../../../app.config'
|
||||||
import { fetchData, getQueryContext } from '@utils/subgraph'
|
import { fetchData, getQueryContext } from '@utils/subgraph'
|
||||||
|
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||||
|
|
||||||
const MarketMetadataContext = createContext({} as MarketMetadataProviderValue)
|
const MarketMetadataContext = createContext({} as MarketMetadataProviderValue)
|
||||||
|
|
||||||
@ -43,6 +44,11 @@ function MarketMetadataProvider({
|
|||||||
swapNotApprovedFee: response.data?.opc.swapNonOceanFee
|
swapNotApprovedFee: response.data?.opc.swapNonOceanFee
|
||||||
} as OpcFee)
|
} as OpcFee)
|
||||||
}
|
}
|
||||||
|
LoggerInstance.log('[MarketMetadata] Got new data.', {
|
||||||
|
opcFees: opcData,
|
||||||
|
siteContent,
|
||||||
|
appConfig
|
||||||
|
})
|
||||||
setOpcFees(opcData)
|
setOpcFees(opcData)
|
||||||
}
|
}
|
||||||
getOpcData()
|
getOpcData()
|
||||||
|
13
src/@context/Prices/_constants.ts
Normal file
13
src/@context/Prices/_constants.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Prices } from './_types'
|
||||||
|
import { coingeckoTokenIds } from '../../../app.config'
|
||||||
|
|
||||||
|
export const initialData: Prices = coingeckoTokenIds.map((tokenId) => ({
|
||||||
|
[tokenId]: {
|
||||||
|
eur: 0.0,
|
||||||
|
usd: 0.0,
|
||||||
|
eth: 0.0,
|
||||||
|
btc: 0.0
|
||||||
|
}
|
||||||
|
}))[0]
|
||||||
|
|
||||||
|
export const refreshInterval = 120000 // 120 sec.
|
9
src/@context/Prices/_types.ts
Normal file
9
src/@context/Prices/_types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface Prices {
|
||||||
|
[key: string]: {
|
||||||
|
[key: string]: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PricesValue {
|
||||||
|
prices: Prices
|
||||||
|
}
|
23
src/@context/Prices/_utils.ts
Normal file
23
src/@context/Prices/_utils.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// Deal with differences between token symbol & Coingecko API IDs
|
||||||
|
//
|
||||||
|
export function getCoingeckoTokenId(symbol: string) {
|
||||||
|
// can be OCEAN or mOCEAN
|
||||||
|
const isOcean = symbol?.toLowerCase().includes('ocean')
|
||||||
|
// can be H2O or H20
|
||||||
|
const isH2o = symbol?.toLowerCase().includes('h2')
|
||||||
|
const isEth = symbol?.toLowerCase() === 'eth'
|
||||||
|
const isMatic = symbol?.toLowerCase() === 'matic'
|
||||||
|
|
||||||
|
const priceTokenId = isOcean
|
||||||
|
? 'ocean-protocol'
|
||||||
|
: isH2o
|
||||||
|
? 'h2o'
|
||||||
|
: isEth
|
||||||
|
? 'ethereum'
|
||||||
|
: isMatic
|
||||||
|
? 'matic-network'
|
||||||
|
: symbol?.toLowerCase()
|
||||||
|
|
||||||
|
return priceTokenId
|
||||||
|
}
|
36
src/@context/Prices/index.test.tsx
Normal file
36
src/@context/Prices/index.test.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import * as SWR from 'swr'
|
||||||
|
import { renderHook } from '@testing-library/react'
|
||||||
|
import { PricesProvider, usePrices, getCoingeckoTokenId } from '.'
|
||||||
|
|
||||||
|
jest.spyOn(SWR, 'default').mockImplementation(() => ({
|
||||||
|
useSWR: { data: { 'ocean-protocol': { eur: '2' } } },
|
||||||
|
isValidating: false,
|
||||||
|
mutate: jest.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
const wrapper = ({ children }: { children: ReactElement }) => (
|
||||||
|
<PricesProvider>{children}</PricesProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
test('should correctly initialize data', async () => {
|
||||||
|
const { result } = renderHook(() => usePrices(), { wrapper })
|
||||||
|
|
||||||
|
expect(result.current.prices['ocean-protocol'].eur).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('useSWR is called', async () => {
|
||||||
|
const { result } = renderHook(() => usePrices(), { wrapper })
|
||||||
|
expect(SWR.default).toHaveBeenCalled()
|
||||||
|
|
||||||
|
// somehow the above spy seems to not fully work, but this assertion is the goal
|
||||||
|
// expect(result.current.prices['ocean-protocol'].eur).toBe('2')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should get correct Coingecko API ID for OCEAN', async () => {
|
||||||
|
const id1 = getCoingeckoTokenId('OCEAN')
|
||||||
|
expect(id1).toBe('ocean-protocol')
|
||||||
|
|
||||||
|
const id2 = getCoingeckoTokenId('mOCEAN')
|
||||||
|
expect(id2).toBe('ocean-protocol')
|
||||||
|
})
|
@ -9,24 +9,10 @@ import React, {
|
|||||||
import { fetchData } from '@utils/fetch'
|
import { fetchData } from '@utils/fetch'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||||
import { useMarketMetadata } from './MarketMetadata'
|
import { useMarketMetadata } from '../MarketMetadata'
|
||||||
|
import { Prices, PricesValue } from './_types'
|
||||||
interface Prices {
|
import { initialData, refreshInterval } from './_constants'
|
||||||
[key: string]: number
|
import { getCoingeckoTokenId } from './_utils'
|
||||||
}
|
|
||||||
|
|
||||||
interface PricesValue {
|
|
||||||
prices: Prices
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialData: Prices = {
|
|
||||||
eur: 0.0,
|
|
||||||
usd: 0.0,
|
|
||||||
eth: 0.0,
|
|
||||||
btc: 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshInterval = 120000 // 120 sec.
|
|
||||||
|
|
||||||
const PricesContext = createContext(null)
|
const PricesContext = createContext(null)
|
||||||
|
|
||||||
@ -36,23 +22,23 @@ export default function PricesProvider({
|
|||||||
children: ReactNode
|
children: ReactNode
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const { appConfig } = useMarketMetadata()
|
const { appConfig } = useMarketMetadata()
|
||||||
const tokenId = 'ocean-protocol'
|
|
||||||
|
|
||||||
const [prices, setPrices] = useState(initialData)
|
const [prices, setPrices] = useState(initialData)
|
||||||
const [url, setUrl] = useState('')
|
const [url, setUrl] = useState<string>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!appConfig) return
|
if (!appConfig) return
|
||||||
// comma-separated list
|
|
||||||
const currencies = appConfig.currencies.join(',')
|
const currencies = appConfig.currencies.join(',')
|
||||||
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${tokenId}&vs_currencies=${currencies}`
|
const tokenIds = appConfig.coingeckoTokenIds.join(',')
|
||||||
|
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${tokenIds}&vs_currencies=${currencies}`
|
||||||
setUrl(url)
|
setUrl(url)
|
||||||
}, [appConfig])
|
}, [appConfig])
|
||||||
|
|
||||||
const onSuccess = async (data: { [tokenId]: Prices }) => {
|
const onSuccess = async (data: Prices) => {
|
||||||
if (!data) return
|
if (!data) return
|
||||||
LoggerInstance.log('[prices] Got new OCEAN spot prices.', data[tokenId])
|
LoggerInstance.log('[prices] Got new spot prices.', data)
|
||||||
setPrices(data[tokenId])
|
setPrices(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch new prices periodically with swr
|
// Fetch new prices periodically with swr
|
||||||
@ -71,4 +57,4 @@ export default function PricesProvider({
|
|||||||
// Helper hook to access the provider values
|
// Helper hook to access the provider values
|
||||||
const usePrices = (): PricesValue => useContext(PricesContext)
|
const usePrices = (): PricesValue => useContext(PricesContext)
|
||||||
|
|
||||||
export { PricesProvider, usePrices }
|
export { PricesProvider, usePrices, getCoingeckoTokenId }
|
@ -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
|
@ -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'
|
||||||
@ -160,12 +160,15 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
|||||||
// Helper: Get user balance
|
// Helper: Get user balance
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
const getUserBalance = useCallback(async () => {
|
const getUserBalance = useCallback(async () => {
|
||||||
if (!accountId || !networkId || !web3) return
|
if (!accountId || !networkId || !web3 || !networkData) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const balance: UserBalance = {
|
const userBalance = web3.utils.fromWei(
|
||||||
eth: web3.utils.fromWei(await web3.eth.getBalance(accountId, 'latest'))
|
await web3.eth.getBalance(accountId, 'latest')
|
||||||
}
|
)
|
||||||
|
const key = networkData.nativeCurrency.symbol.toLowerCase()
|
||||||
|
const balance: UserBalance = { [key]: userBalance }
|
||||||
|
|
||||||
if (approvedBaseTokens?.length > 0) {
|
if (approvedBaseTokens?.length > 0) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
approvedBaseTokens.map(async (token) => {
|
approvedBaseTokens.map(async (token) => {
|
||||||
@ -186,27 +189,38 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
LoggerInstance.error('[web3] Error: ', error.message)
|
LoggerInstance.error('[web3] Error: ', error.message)
|
||||||
}
|
}
|
||||||
}, [accountId, approvedBaseTokens, networkId, web3])
|
}, [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(
|
LoggerInstance.log(
|
||||||
`[web3] ENS name found for ${accountId}:`,
|
`[web3] ENS name found for ${accountId}:`,
|
||||||
accountEns
|
profile.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (profile.avatar) {
|
||||||
|
setAccountEnsAvatar(profile.avatar)
|
||||||
|
LoggerInstance.log(
|
||||||
|
`[web3] ENS avatar found for ${accountId}:`,
|
||||||
|
profile.avatar
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setAccountEnsAvatar(null)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LoggerInstance.error('[web3] Error: ', error.message)
|
LoggerInstance.error('[web3] Error: ', error.message)
|
||||||
}
|
}
|
||||||
@ -272,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
|
||||||
@ -334,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 */
|
||||||
@ -399,6 +413,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
|||||||
web3ProviderInfo,
|
web3ProviderInfo,
|
||||||
accountId,
|
accountId,
|
||||||
accountEns,
|
accountEns,
|
||||||
|
accountEnsAvatar,
|
||||||
balance,
|
balance,
|
||||||
networkId,
|
networkId,
|
||||||
chainId,
|
chainId,
|
||||||
|
@ -10,6 +10,7 @@ export function getNetworkType(network: EthereumListsChain): string {
|
|||||||
// .network field, which is innexistent on https://chainid.network/chains.json
|
// .network field, which is innexistent on https://chainid.network/chains.json
|
||||||
// We hack in mainnet detection for moonriver.
|
// We hack in mainnet detection for moonriver.
|
||||||
if (
|
if (
|
||||||
|
network &&
|
||||||
!network.name.includes('Testnet') &&
|
!network.name.includes('Testnet') &&
|
||||||
!network.title?.includes('Testnet') &&
|
!network.title?.includes('Testnet') &&
|
||||||
network.name !== 'Moonbase Alpha'
|
network.name !== 'Moonbase Alpha'
|
||||||
|
@ -10,7 +10,7 @@ function useNftFactory(): NftFactory {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!web3 || !chainId) return
|
if (!web3 || !chainId) return
|
||||||
const config = getOceanConfig(chainId)
|
const config = getOceanConfig(chainId)
|
||||||
const factory = new NftFactory(config?.erc721FactoryAddress, web3)
|
const factory = new NftFactory(config?.nftFactoryAddress, web3)
|
||||||
setNftFactory(factory)
|
setNftFactory(factory)
|
||||||
}, [web3, chainId])
|
}, [web3, 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
|
|
||||||
}
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
|
1
src/@types/TokenBalance.d.ts
vendored
1
src/@types/TokenBalance.d.ts
vendored
@ -1,4 +1,3 @@
|
|||||||
interface UserBalance {
|
interface UserBalance {
|
||||||
eth: string
|
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
|
4
src/@types/viewModels/AccountTeaserVM.d.ts
vendored
4
src/@types/viewModels/AccountTeaserVM.d.ts
vendored
@ -1,4 +0,0 @@
|
|||||||
interface AccountTeaserVM {
|
|
||||||
address: string
|
|
||||||
nrSales: number
|
|
||||||
}
|
|
@ -23,7 +23,7 @@ import {
|
|||||||
|
|
||||||
const tokensPriceQuery = gql`
|
const tokensPriceQuery = gql`
|
||||||
query TokensPriceQuery($datatokenIds: [ID!], $account: String) {
|
query TokensPriceQuery($datatokenIds: [ID!], $account: String) {
|
||||||
tokens(where: { id_in: $datatokenIds }) {
|
tokens(first: 1000, where: { id_in: $datatokenIds }) {
|
||||||
id
|
id
|
||||||
symbol
|
symbol
|
||||||
name
|
name
|
||||||
|
@ -9,6 +9,11 @@ import {
|
|||||||
} 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 {
|
||||||
@ -397,6 +402,40 @@ export async function getTopPublishers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTopAssetsPublishers(
|
||||||
|
chainIds: number[],
|
||||||
|
nrItems = 9
|
||||||
|
): Promise<UserSales[]> {
|
||||||
|
const publishers: UserSales[] = []
|
||||||
|
|
||||||
|
const result = await getTopPublishers(chainIds, null)
|
||||||
|
const { topPublishers } = result.aggregations
|
||||||
|
|
||||||
|
for (let i = 0; i < topPublishers.buckets.length; i++) {
|
||||||
|
publishers.push({
|
||||||
|
id: topPublishers.buckets[i].key,
|
||||||
|
totalSales: parseInt(topPublishers.buckets[i].totalSales.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
publishers.sort((a, b) => b.totalSales - a.totalSales)
|
||||||
|
|
||||||
|
return publishers.slice(0, nrItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserSales(
|
||||||
|
accountId: string,
|
||||||
|
chainIds: number[]
|
||||||
|
): Promise<number> {
|
||||||
|
try {
|
||||||
|
const result = await getPublishedAssets(accountId, chainIds, null)
|
||||||
|
const { totalOrders } = result.aggregations
|
||||||
|
return totalOrders.value
|
||||||
|
} catch (error) {
|
||||||
|
LoggerInstance.error('Error getUserSales', error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getDownloadAssets(
|
export async function getDownloadAssets(
|
||||||
dtList: string[],
|
dtList: string[],
|
||||||
tokenOrders: OrdersData[],
|
tokenOrders: OrdersData[],
|
||||||
|
@ -9,7 +9,7 @@ export async function setMinterToPublisher(
|
|||||||
accountId: string,
|
accountId: string,
|
||||||
setError: (msg: string) => void
|
setError: (msg: string) => void
|
||||||
): Promise<TransactionReceipt> {
|
): Promise<TransactionReceipt> {
|
||||||
const dispenserInstance = new Dispenser(web3, dispenserAddress)
|
const dispenserInstance = new Dispenser(dispenserAddress, web3)
|
||||||
const status = await dispenserInstance.status(datatokenAddress)
|
const status = await dispenserInstance.status(datatokenAddress)
|
||||||
if (!status?.active) return
|
if (!status?.active) return
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,8 +25,8 @@ export async function getFixedBuyPrice(
|
|||||||
|
|
||||||
const config = getOceanConfig(chainId)
|
const config = getOceanConfig(chainId)
|
||||||
|
|
||||||
const fixed = new FixedRateExchange(web3, config.fixedRateExchangeAddress)
|
const fixed = new FixedRateExchange(config.fixedRateExchangeAddress, web3)
|
||||||
const estimatedPrice = await fixed.calcBaseInGivenOutDT(
|
const estimatedPrice = await fixed.calcBaseInGivenDatatokensOut(
|
||||||
accessDetails.addressOrId,
|
accessDetails.addressOrId,
|
||||||
'1',
|
'1',
|
||||||
consumeMarketFixedSwapFee
|
consumeMarketFixedSwapFee
|
||||||
|
@ -171,15 +171,6 @@ export async function setNFTMetadataAndTokenURI(
|
|||||||
metadataProofs: []
|
metadataProofs: []
|
||||||
}
|
}
|
||||||
|
|
||||||
const estGasSetMetadataAndTokenURI = await nft.estGasSetMetadataAndTokenURI(
|
|
||||||
asset.nftAddress,
|
|
||||||
accountId,
|
|
||||||
metadataAndTokenURI
|
|
||||||
)
|
|
||||||
LoggerInstance.log(
|
|
||||||
'[setNFTMetadataAndTokenURI] est Gas set metadata and token uri --',
|
|
||||||
estGasSetMetadataAndTokenURI
|
|
||||||
)
|
|
||||||
const setMetadataAndTokenURITx = await nft.setMetadataAndTokenURI(
|
const setMetadataAndTokenURITx = await nft.setMetadataAndTokenURI(
|
||||||
asset.nftAddress,
|
asset.nftAddress,
|
||||||
accountId,
|
accountId,
|
||||||
|
@ -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 {
|
||||||
|
@ -57,7 +57,9 @@ export async function order(
|
|||||||
_consumeMarketFee: {
|
_consumeMarketFee: {
|
||||||
consumeMarketFeeAddress: marketFeeAddress,
|
consumeMarketFeeAddress: marketFeeAddress,
|
||||||
consumeMarketFeeAmount: consumeMarketOrderFee,
|
consumeMarketFeeAmount: consumeMarketOrderFee,
|
||||||
consumeMarketFeeToken: asset.accessDetails.baseToken.address
|
consumeMarketFeeToken:
|
||||||
|
asset?.accessDetails?.baseToken?.address ||
|
||||||
|
'0x0000000000000000000000000000000000000000'
|
||||||
}
|
}
|
||||||
} as OrderParams
|
} as OrderParams
|
||||||
|
|
||||||
@ -66,6 +68,7 @@ export async function order(
|
|||||||
// this assumes all fees are in ocean
|
// this assumes all fees are in ocean
|
||||||
const txApprove = await approve(
|
const txApprove = await approve(
|
||||||
web3,
|
web3,
|
||||||
|
config,
|
||||||
accountId,
|
accountId,
|
||||||
asset.accessDetails.baseToken.address,
|
asset.accessDetails.baseToken.address,
|
||||||
asset.accessDetails.datatoken.address,
|
asset.accessDetails.datatoken.address,
|
||||||
@ -152,19 +155,21 @@ async function approveProviderFee(
|
|||||||
accountId: string,
|
accountId: string,
|
||||||
web3: Web3,
|
web3: Web3,
|
||||||
providerFeeAmount: string
|
providerFeeAmount: string
|
||||||
): Promise<string> {
|
): Promise<TransactionReceipt> {
|
||||||
|
const config = getOceanConfig(asset.chainId)
|
||||||
const baseToken =
|
const baseToken =
|
||||||
asset?.accessDetails?.type === 'free'
|
asset?.accessDetails?.type === 'free'
|
||||||
? getOceanConfig(asset.chainId).oceanTokenAddress
|
? getOceanConfig(asset.chainId).oceanTokenAddress
|
||||||
: asset?.accessDetails?.baseToken?.address
|
: asset?.accessDetails?.baseToken?.address
|
||||||
const txApproveWei = await approveWei(
|
const txApproveWei = await approveWei(
|
||||||
web3,
|
web3,
|
||||||
|
config,
|
||||||
accountId,
|
accountId,
|
||||||
baseToken,
|
baseToken,
|
||||||
asset?.accessDetails?.datatoken?.address,
|
asset?.accessDetails?.datatoken?.address,
|
||||||
providerFeeAmount
|
providerFeeAmount
|
||||||
)
|
)
|
||||||
return txApproveWei as string // thanks ocean.js
|
return txApproveWei
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startOrder(
|
async function startOrder(
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -29,11 +29,13 @@ export default function AssetType({
|
|||||||
{type === 'dataset' ? 'dataset' : 'algorithm'}
|
{type === 'dataset' ? 'dataset' : 'algorithm'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{totalSales ? (
|
{(totalSales || totalSales === 0) && (
|
||||||
<div className={styles.typeLabel}>
|
<div className={styles.typeLabel}>
|
||||||
{`${totalSales} ${totalSales === 1 ? 'sale' : 'sales'}`}
|
{totalSales < 0
|
||||||
|
? 'N/A'
|
||||||
|
: `${totalSales} ${totalSales === 1 ? 'sale' : 'sales'}`}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
|
||||||
import { usePrices } from '@context/Prices'
|
|
||||||
import { useWeb3 } from '@context/Web3'
|
|
||||||
import Web3 from 'web3'
|
|
||||||
import useNftFactory from '@hooks/contracts/useNftFactory'
|
|
||||||
import { NftFactory } from '@oceanprotocol/lib'
|
|
||||||
import Conversion from '@shared/Price/Conversion'
|
|
||||||
import { generateNftCreateData, NftMetadata } from '@utils/nft'
|
|
||||||
|
|
||||||
const getEstGasFee = async (
|
|
||||||
address: string,
|
|
||||||
nftFactory: NftFactory,
|
|
||||||
nftMetadata: NftMetadata,
|
|
||||||
ethToOceanConversionRate: number
|
|
||||||
): Promise<string> => {
|
|
||||||
if (!address || !nftFactory || !nftMetadata || !ethToOceanConversionRate)
|
|
||||||
return
|
|
||||||
|
|
||||||
const { web3 } = nftFactory
|
|
||||||
const nft = generateNftCreateData(nftMetadata, address)
|
|
||||||
|
|
||||||
const gasPrice = await web3.eth.getGasPrice()
|
|
||||||
const gasLimit = await nftFactory?.estGasCreateNFT(address, nft)
|
|
||||||
const gasFeeEth = Web3.utils.fromWei(
|
|
||||||
(+gasPrice * +gasLimit).toString(),
|
|
||||||
'ether'
|
|
||||||
)
|
|
||||||
const gasFeeOcean = (+gasFeeEth / +ethToOceanConversionRate).toString()
|
|
||||||
return gasFeeOcean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TxFee({
|
|
||||||
nftMetadata
|
|
||||||
}: {
|
|
||||||
nftMetadata: NftMetadata
|
|
||||||
}): ReactElement {
|
|
||||||
const { accountId } = useWeb3()
|
|
||||||
const { prices } = usePrices()
|
|
||||||
const nftFactory = useNftFactory()
|
|
||||||
const [gasFee, setGasFee] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const calculateGasFee = async () =>
|
|
||||||
setGasFee(
|
|
||||||
await getEstGasFee(
|
|
||||||
accountId,
|
|
||||||
nftFactory,
|
|
||||||
nftMetadata,
|
|
||||||
(prices as any)?.eth
|
|
||||||
)
|
|
||||||
)
|
|
||||||
calculateGasFee()
|
|
||||||
}, [accountId, nftFactory, nftMetadata, prices])
|
|
||||||
|
|
||||||
return gasFee ? (
|
|
||||||
<p>
|
|
||||||
Gas fee estimation for this artwork
|
|
||||||
<Conversion price={gasFee} />
|
|
||||||
</p>
|
|
||||||
) : accountId ? (
|
|
||||||
<p>
|
|
||||||
An error occurred while estimating the gas fee for this artwork, please
|
|
||||||
try again.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p>Please connect your wallet to get a gas fee estimate for this artwork</p>
|
|
||||||
)
|
|
||||||
}
|
|
@ -27,7 +27,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding: 0 calc(var(--spacer) / 4);
|
padding: calc(var(--spacer) / 4);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -5,8 +5,6 @@ import { useField } from 'formik'
|
|||||||
import React, { ReactElement, useEffect } from 'react'
|
import React, { ReactElement, useEffect } from 'react'
|
||||||
import Refresh from '@images/refresh.svg'
|
import Refresh from '@images/refresh.svg'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
import Tooltip from '@shared/atoms/Tooltip'
|
|
||||||
import TxFee from './TxFee'
|
|
||||||
|
|
||||||
export default function Nft(props: InputProps): ReactElement {
|
export default function Nft(props: InputProps): ReactElement {
|
||||||
const [field, meta, helpers] = useField(props.name)
|
const [field, meta, helpers] = useField(props.name)
|
||||||
@ -28,7 +26,6 @@ export default function Nft(props: InputProps): ReactElement {
|
|||||||
<figure className={styles.image}>
|
<figure className={styles.image}>
|
||||||
<img src={field?.value?.image_data} width="128" height="128" />
|
<img src={field?.value?.image_data} width="128" height="128" />
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<Tooltip content={<TxFee nftMetadata={field.value} />} />
|
|
||||||
<Button
|
<Button
|
||||||
style="text"
|
style="text"
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import React, { useEffect, useState, ReactElement } from 'react'
|
import React, { useEffect, useState, ReactElement } from 'react'
|
||||||
import styles from './Conversion.module.css'
|
import styles from './Conversion.module.css'
|
||||||
import classNames from 'classnames/bind'
|
|
||||||
import { formatCurrency, isCrypto } from '@coingecko/cryptoformat'
|
import { formatCurrency, isCrypto } from '@coingecko/cryptoformat'
|
||||||
import { useUserPreferences } from '@context/UserPreferences'
|
import { useUserPreferences } from '@context/UserPreferences'
|
||||||
import { usePrices } from '@context/Prices'
|
import { usePrices, getCoingeckoTokenId } from '@context/Prices'
|
||||||
|
|
||||||
const cx = classNames.bind(styles)
|
|
||||||
|
|
||||||
export default function Conversion({
|
export default function Conversion({
|
||||||
price,
|
price,
|
||||||
|
symbol,
|
||||||
className,
|
className,
|
||||||
hideApproximateSymbol
|
hideApproximateSymbol
|
||||||
}: {
|
}: {
|
||||||
price: string // expects price in OCEAN, not wei
|
price: number // expects price in OCEAN, not wei
|
||||||
|
symbol: string
|
||||||
className?: string
|
className?: string
|
||||||
hideApproximateSymbol?: boolean
|
hideApproximateSymbol?: boolean
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
@ -25,19 +24,16 @@ export default function Conversion({
|
|||||||
// isCrypto() only checks for BTC & ETH & unknown but seems sufficient for now
|
// isCrypto() only checks for BTC & ETH & unknown but seems sufficient for now
|
||||||
// const isFiat = /(EUR|USD|CAD|SGD|HKD|CNY|JPY|GBP|INR|RUB)/g.test(currency)
|
// const isFiat = /(EUR|USD|CAD|SGD|HKD|CNY|JPY|GBP|INR|RUB)/g.test(currency)
|
||||||
|
|
||||||
const styleClasses = cx({
|
// referring to Coingecko tokenId in Prices context provider
|
||||||
conversion: true,
|
const priceTokenId = getCoingeckoTokenId(symbol)
|
||||||
[className]: className
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!prices || !price || price === '0') {
|
if (!prices || !price || !priceTokenId || !prices[priceTokenId]) {
|
||||||
setPriceConverted('0.00')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversionValue = prices[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
|
||||||
@ -54,16 +50,16 @@ export default function Conversion({
|
|||||||
(match) => `<span>${match}</span>`
|
(match) => `<span>${match}</span>`
|
||||||
)
|
)
|
||||||
setPriceConverted(convertedFormattedHTMLstring)
|
setPriceConverted(convertedFormattedHTMLstring)
|
||||||
}, [price, prices, currency, locale, isFiat])
|
}, [price, prices, currency, locale, isFiat, priceTokenId])
|
||||||
|
|
||||||
return (
|
return Number(price) >= 0 ? (
|
||||||
<span
|
<span
|
||||||
className={styleClasses}
|
className={`${styles.conversion} ${className || ''}`}
|
||||||
title="Approximation based on the current selected base token spot price on Coingecko"
|
title="Approximation based on the current spot price on Coingecko"
|
||||||
>
|
>
|
||||||
{!hideApproximateSymbol && '≈ '}
|
{!hideApproximateSymbol && '≈ '}
|
||||||
<strong dangerouslySetInnerHTML={{ __html: priceConverted }} />{' '}
|
<strong dangerouslySetInnerHTML={{ __html: priceConverted }} />{' '}
|
||||||
{!isFiat && currency}
|
{!isFiat && currency}
|
||||||
</span>
|
</span>
|
||||||
)
|
) : null
|
||||||
}
|
}
|
||||||
|
@ -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,10 +37,10 @@ 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} />}
|
{conversion && <Conversion price={price} symbol={symbol} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -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} />)
|
||||||
|
})
|
@ -15,9 +15,7 @@ const Tag = ({ tag, noLinks }: { tag: string; noLinks?: boolean }) => {
|
|||||||
return noLinks ? (
|
return noLinks ? (
|
||||||
<span className={styles.tag}>{tag}</span>
|
<span className={styles.tag}>{tag}</span>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link href={`/search?tags=${urlEncodedTag}&sort=_score&sortOrder=desc`}>
|
||||||
href={`/search?tags=${urlEncodedTag}&sort=metadata.created&sortOrder=desc`}
|
|
||||||
>
|
|
||||||
<a className={styles.tag} title={tag}>
|
<a className={styles.tag} title={tag}>
|
||||||
{tag}
|
{tag}
|
||||||
</a>
|
</a>
|
||||||
|
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} />)
|
||||||
|
})
|
@ -7,7 +7,9 @@ export default function AssetStats() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className={styles.stats}>
|
<footer className={styles.stats}>
|
||||||
{!asset || !asset?.stats || asset?.stats?.orders === 0 ? (
|
{!asset || !asset?.stats || asset?.stats?.orders < 0 ? (
|
||||||
|
'N/A'
|
||||||
|
) : asset?.stats?.orders === 0 ? (
|
||||||
'No sales yet'
|
'No sales yet'
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -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}>
|
||||||
|
@ -16,6 +16,7 @@ import { toast } from 'react-toastify'
|
|||||||
import { useIsMounted } from '@hooks/useIsMounted'
|
import { useIsMounted } from '@hooks/useIsMounted'
|
||||||
import { useMarketMetadata } from '@context/MarketMetadata'
|
import { useMarketMetadata } from '@context/MarketMetadata'
|
||||||
import Alert from '@shared/atoms/Alert'
|
import Alert from '@shared/atoms/Alert'
|
||||||
|
import Loader from '@shared/atoms/Loader'
|
||||||
|
|
||||||
export default function Download({
|
export default function Download({
|
||||||
asset,
|
asset,
|
||||||
@ -41,6 +42,7 @@ export default function Download({
|
|||||||
const [hasDatatoken, setHasDatatoken] = useState(false)
|
const [hasDatatoken, setHasDatatoken] = useState(false)
|
||||||
const [statusText, setStatusText] = useState('')
|
const [statusText, setStatusText] = useState('')
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [isPriceLoading, setIsPriceLoading] = useState(false)
|
||||||
const [isOwned, setIsOwned] = useState(false)
|
const [isOwned, setIsOwned] = useState(false)
|
||||||
const [validOrderTx, setValidOrderTx] = useState('')
|
const [validOrderTx, setValidOrderTx] = useState('')
|
||||||
const [orderPriceAndFees, setOrderPriceAndFees] =
|
const [orderPriceAndFees, setOrderPriceAndFees] =
|
||||||
@ -64,8 +66,11 @@ export default function Download({
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
!orderPriceAndFees && setIsPriceLoading(true)
|
||||||
|
|
||||||
const _orderPriceAndFees = await getOrderPriceAndFees(asset, ZERO_ADDRESS)
|
const _orderPriceAndFees = await getOrderPriceAndFees(asset, ZERO_ADDRESS)
|
||||||
setOrderPriceAndFees(_orderPriceAndFees)
|
setOrderPriceAndFees(_orderPriceAndFees)
|
||||||
|
!orderPriceAndFees && setIsPriceLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
init()
|
init()
|
||||||
@ -84,6 +89,7 @@ export default function Download({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
(asset?.accessDetails?.type === 'fixed' && !orderPriceAndFees) ||
|
||||||
!isMounted ||
|
!isMounted ||
|
||||||
!accountId ||
|
!accountId ||
|
||||||
!asset?.accessDetails ||
|
!asset?.accessDetails ||
|
||||||
@ -112,7 +118,8 @@ export default function Download({
|
|||||||
hasDatatoken,
|
hasDatatoken,
|
||||||
accountId,
|
accountId,
|
||||||
isOwned,
|
isOwned,
|
||||||
isUnsupportedPricing
|
isUnsupportedPricing,
|
||||||
|
orderPriceAndFees
|
||||||
])
|
])
|
||||||
|
|
||||||
async function handleOrderOrDownload() {
|
async function handleOrderOrDownload() {
|
||||||
@ -184,12 +191,17 @@ export default function Download({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{isPriceLoading ? (
|
||||||
|
<Loader message="Calculating full price (including fees)" />
|
||||||
|
) : (
|
||||||
<Price
|
<Price
|
||||||
accessDetails={asset.accessDetails}
|
accessDetails={asset.accessDetails}
|
||||||
orderPriceAndFees={orderPriceAndFees}
|
orderPriceAndFees={orderPriceAndFees}
|
||||||
conversion
|
conversion
|
||||||
size="large"
|
size="large"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isInPurgatory && <PurchaseButton />}
|
{!isInPurgatory && <PurchaseButton />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -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'
|
||||||
@ -43,8 +44,8 @@ export default function Edit({
|
|||||||
const config = getOceanConfig(asset.chainId)
|
const config = getOceanConfig(asset.chainId)
|
||||||
|
|
||||||
const fixedRateInstance = new FixedRateExchange(
|
const fixedRateInstance = new FixedRateExchange(
|
||||||
web3,
|
config.fixedRateExchangeAddress,
|
||||||
config.fixedRateExchangeAddress
|
web3
|
||||||
)
|
)
|
||||||
|
|
||||||
const setPriceResp = await fixedRateInstance.setRate(
|
const setPriceResp = await fixedRateInstance.setRate(
|
||||||
|
@ -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,13 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,10 +1,11 @@
|
|||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
34
src/components/Asset/Edit/_validation.ts
Normal file
34
src/components/Asset/Edit/_validation.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const computeSettingsValidationSchema = Yup.object().shape({
|
||||||
|
allowAllPublishedAlgorithms: Yup.boolean().nullable(),
|
||||||
|
publisherTrustedAlgorithms: Yup.array().nullable(),
|
||||||
|
publisherTrustedAlgorithmPublishers: Yup.array().nullable()
|
||||||
|
})
|
@ -1,7 +1,6 @@
|
|||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import loadable from '@loadable/component'
|
import loadable from '@loadable/component'
|
||||||
import Badge from '@shared/atoms/Badge'
|
|
||||||
import Logo from '@shared/atoms/Logo'
|
import Logo from '@shared/atoms/Logo'
|
||||||
import UserPreferences from './UserPreferences'
|
import UserPreferences from './UserPreferences'
|
||||||
import Networks from './UserPreferences/Networks'
|
import Networks from './UserPreferences/Networks'
|
||||||
@ -9,8 +8,6 @@ import SearchBar from './SearchBar'
|
|||||||
import styles from './Menu.module.css'
|
import styles from './Menu.module.css'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useMarketMetadata } from '@context/MarketMetadata'
|
import { useMarketMetadata } from '@context/MarketMetadata'
|
||||||
import Tooltip from '@shared/atoms/Tooltip'
|
|
||||||
import Caret from '@images/caret.svg'
|
|
||||||
const Wallet = loadable(() => import('./Wallet'))
|
const Wallet = loadable(() => import('./Wallet'))
|
||||||
|
|
||||||
declare type MenuItem = {
|
declare type MenuItem = {
|
||||||
@ -34,7 +31,7 @@ function MenuLink({ item }: { item: MenuItem }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Menu(): ReactElement {
|
export default function Menu(): ReactElement {
|
||||||
const { appConfig, siteContent } = useMarketMetadata()
|
const { siteContent } = useMarketMetadata()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={styles.menu}>
|
<nav className={styles.menu}>
|
||||||
@ -45,31 +42,6 @@ export default function Menu(): ReactElement {
|
|||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
className={styles.badgeWrap}
|
|
||||||
content={
|
|
||||||
<div className={styles.versions}>
|
|
||||||
<a className={styles.link} href={appConfig.v3MarketUri}>
|
|
||||||
v3
|
|
||||||
</a>
|
|
||||||
<a className={styles.link} href="" aria-current aria-disabled>
|
|
||||||
v4
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
trigger="click focus"
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<Badge
|
|
||||||
className={styles.badge}
|
|
||||||
label={
|
|
||||||
<>
|
|
||||||
v4 <Caret aria-hidden="true" className={styles.caret} />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<ul className={styles.navigation}>
|
<ul className={styles.navigation}>
|
||||||
{siteContent?.menu.map((item: MenuItem) => (
|
{siteContent?.menu.map((item: MenuItem) => (
|
||||||
<li key={item.name}>
|
<li key={item.name}>
|
||||||
|
@ -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>
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.balance {
|
.balance {
|
||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-small);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -32,6 +32,14 @@
|
|||||||
margin-right: 0.4rem;
|
margin-right: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: var(--font-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversion strong {
|
||||||
|
font-weight: var(--font-weight-base);
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
margin-top: calc(var(--spacer) / 2);
|
margin-top: calc(var(--spacer) / 2);
|
||||||
|
@ -29,8 +29,7 @@ export default function Details(): ReactElement {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!networkId) return
|
if (!networkId) return
|
||||||
|
|
||||||
const symbol =
|
const symbol = networkData?.nativeCurrency.symbol
|
||||||
networkId === 2021000 ? 'GX' : networkData?.nativeCurrency.symbol
|
|
||||||
setMainCurrency(symbol)
|
setMainCurrency(symbol)
|
||||||
|
|
||||||
const oceanConfig = getOceanConfig(networkId)
|
const oceanConfig = getOceanConfig(networkId)
|
||||||
@ -49,11 +48,17 @@ export default function Details(): ReactElement {
|
|||||||
<li className={styles.balance} key={key}>
|
<li className={styles.balance} key={key}>
|
||||||
<span className={styles.symbol}>
|
<span className={styles.symbol}>
|
||||||
{key === 'eth' ? mainCurrency : key.toUpperCase()}
|
{key === 'eth' ? mainCurrency : key.toUpperCase()}
|
||||||
</span>{' '}
|
</span>
|
||||||
|
<span className={styles.value}>
|
||||||
{formatCurrency(Number(value), '', locale, false, {
|
{formatCurrency(Number(value), '', locale, false, {
|
||||||
significantFigures: 4
|
significantFigures: 4
|
||||||
})}
|
})}
|
||||||
{key === 'ocean' && <Conversion price={value} />}
|
</span>
|
||||||
|
<Conversion
|
||||||
|
className={styles.conversion}
|
||||||
|
price={Number(value)}
|
||||||
|
symbol={key}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import { useMarketMetadata } from '@context/MarketMetadata'
|
|||||||
|
|
||||||
const columns: TableOceanColumn<AssetExtended>[] = [
|
const columns: TableOceanColumn<AssetExtended>[] = [
|
||||||
{
|
{
|
||||||
name: 'Data Set',
|
name: 'Dataset',
|
||||||
selector: (row) => {
|
selector: (row) => {
|
||||||
const { metadata } = row
|
const { metadata } = row
|
||||||
return <AssetTitle title={metadata.name} asset={row} />
|
return <AssetTitle title={metadata.name} asset={row} />
|
||||||
@ -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) {
|
||||||
|
@ -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 {
|
@ -5,11 +5,11 @@ import Bookmarks from './Bookmarks'
|
|||||||
import { generateBaseQuery, queryMetadata } from '@utils/aquarius'
|
import { generateBaseQuery, queryMetadata } from '@utils/aquarius'
|
||||||
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
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 { useIsMounted } from '@hooks/useIsMounted'
|
||||||
import { useCancelToken } from '@hooks/useCancelToken'
|
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[]) {
|
function sortElements(items: Asset[], sorted: string[]) {
|
||||||
items.sort(function (a, b) {
|
items.sort(function (a, b) {
|
||||||
@ -136,7 +136,7 @@ export default function HomePage(): ReactElement {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PublishersWithMostSales title="Publishers With Most Sales" />
|
<TopSales title="Publishers With Most Sales" />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
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