first commit
|
@ -0,0 +1,26 @@
|
|||
#Spree config
|
||||
NODE_URI='http://localhost:8545'
|
||||
AQUARIUS_URI='http://aquarius:5000'
|
||||
BRIZO_URI='http://localhost:8030'
|
||||
BRIZO_ADDRESS='0x068ed00cf0441e4829d9784fcbe7b9e26d4bd8d0'
|
||||
SECRET_STORE_URI='http://localhost:12001'
|
||||
FAUCET_URI='https://localhost:3001'
|
||||
RATING_URI='http://localhost:8000'
|
||||
|
||||
#Nile dexFreight
|
||||
#NODE_URI='https://nile.dev-ocean.com'
|
||||
#AQUARIUS_URI='https://aquarius.nile.dexfreight.dev-ocean.com'
|
||||
#BRIZO_URI='https://brizo.nile.dexfreight.dev-ocean.com'
|
||||
#BRIZO_ADDRESS='0xeD792C5FcC8bF3322a6ba89A6e51eF0B6fB3C530'
|
||||
#SECRET_STORE_URI='https://secret-store.nile.dev-ocean.com'
|
||||
#FAUCET_URI='https://faucet.nile.dev-ocean.com'
|
||||
#RATING_URI='https://rating.nile.dexfreight.dev-ocean.com'
|
||||
|
||||
#Pacific dexFreight
|
||||
#NODE_URI='https://pacific.oceanprotocol.com'
|
||||
#AQUARIUS_URI='https://aquarius.pacific.dexfreight.dev-ocean.com'
|
||||
#BRIZO_URI='https://brizo.pacific.dexfreight.dev-ocean.com'
|
||||
#BRIZO_ADDRESS='0xeD792C5FcC8bF3322a6ba89A6e51eF0B6fB3C530'
|
||||
#SECRET_STORE_URI='https://secret-store.oceanprotocol.com'
|
||||
#FAUCET_URI='https://faucet.oceanprotocol.com'
|
||||
#RATING_URI='https://rating.pacific.dexfreight.dev-ocean.com'
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"extends": ["eslint:recommended", "prettier"],
|
||||
"env": { "es6": true, "browser": true, "node": true },
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts", "**/*.tsx"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": ["./tsconfig.json"]
|
||||
},
|
||||
"extends": [
|
||||
"oceanprotocol",
|
||||
"oceanprotocol/react",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier/react",
|
||||
"prettier/standard",
|
||||
"prettier/@typescript-eslint"
|
||||
],
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"rules": {
|
||||
"react/prop-types": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
* @maxieprotocol @kremalicious @pfmescher @unjapones
|
|
@ -0,0 +1,11 @@
|
|||
node_modules
|
||||
out
|
||||
.DS_Store
|
||||
.next
|
||||
.idea
|
||||
.env
|
||||
.env.build
|
||||
coverage
|
||||
storybook-static
|
||||
public/storybook
|
||||
.artifacts
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react'
|
||||
|
||||
export const Center = ({ children }: { children: any }) => (
|
||||
<div
|
||||
style={{
|
||||
height: '100vh',
|
||||
maxWidth: '35rem',
|
||||
margin: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
stories: [
|
||||
'../src/components/**/*.stories.tsx',
|
||||
'../src/styles/**/*.stories.tsx'
|
||||
],
|
||||
addons: []
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react'
|
||||
import { addDecorator } from '@storybook/react'
|
||||
import WebFont from 'webfontloader'
|
||||
|
||||
WebFont.load({
|
||||
google: {
|
||||
families: ['Montserrat:400,400i,600']
|
||||
}
|
||||
})
|
||||
|
||||
// Import global css with custom properties once for all stories.
|
||||
// Needed because in Next.js we impoprt that file only once too,
|
||||
// in src/_app.tsx which does not get loaded by Storybook
|
||||
import '../src/styles/global.css'
|
||||
|
||||
// Wrapper for all stories previews
|
||||
addDecorator(storyFn => (
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
width: '100%',
|
||||
padding: '2rem'
|
||||
}}
|
||||
>
|
||||
{storyFn()}
|
||||
</div>
|
||||
))
|
|
@ -0,0 +1,66 @@
|
|||
// Make CSS modules work
|
||||
// https://github.com/storybookjs/storybook/issues/4306#issuecomment-517951264
|
||||
const setCssModulesRule = rule => {
|
||||
const nextRule = rule
|
||||
const cssLoader = rule.use[1]
|
||||
|
||||
const nextOptions = {
|
||||
...cssLoader.options,
|
||||
modules: {
|
||||
localIdentName: '[name]__[local]___[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
|
||||
cssLoader.options = nextOptions
|
||||
return nextRule
|
||||
}
|
||||
|
||||
module.exports = async ({ config, mode }) => {
|
||||
const cssRules = config.module.rules.map(rule => {
|
||||
const isCssRule = rule.test.toString().indexOf('css') !== -1
|
||||
let nextRule = rule
|
||||
|
||||
if (isCssRule) {
|
||||
nextRule = setCssModulesRule(rule)
|
||||
}
|
||||
return nextRule
|
||||
})
|
||||
|
||||
config.module.rules = cssRules
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.(ts|tsx)$/,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
presets: [['react-app', { flow: false, typescript: true }]]
|
||||
}
|
||||
})
|
||||
|
||||
config.resolve.extensions.push('.ts', '.tsx')
|
||||
|
||||
config.node = {
|
||||
fs: 'empty'
|
||||
}
|
||||
|
||||
// Handle SVGs
|
||||
// Don't use Storybook's default SVG Configuration
|
||||
config.module.rules = config.module.rules.map(rule => {
|
||||
if (rule.test.toString().includes('svg')) {
|
||||
const test = rule.test
|
||||
.toString()
|
||||
.replace('svg|', '')
|
||||
.replace(/\//g, '')
|
||||
return { ...rule, test: new RegExp(test) }
|
||||
} else {
|
||||
return rule
|
||||
}
|
||||
})
|
||||
|
||||
// Use SVG Configuration for SVGR yourself
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
use: ['@svgr/webpack']
|
||||
})
|
||||
|
||||
return config
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
dist: xenial
|
||||
language: node_js
|
||||
node_js: node
|
||||
|
||||
cache:
|
||||
npm: true
|
||||
directories:
|
||||
- .next/cache
|
||||
|
||||
before_script:
|
||||
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
||||
- chmod +x ./cc-test-reporter
|
||||
- './cc-test-reporter before-build'
|
||||
# startup Barge with local Spree network
|
||||
# - git clone https://github.com/oceanprotocol/barge
|
||||
# - cd barge
|
||||
# - export AQUARIUS_VERSION=v1.0.7
|
||||
# - export BRIZO_VERSION=v0.9.3
|
||||
# - export KEEPER_VERSION=v0.13.2
|
||||
# - export EVENTS_HANDLER_VERSION=v0.4.5
|
||||
# - export KEEPER_OWNER_ROLE_ADDRESS="0xe2DD09d719Da89e5a3D0F2549c7E24566e947260"
|
||||
# - rm -rf "${HOME}/.ocean/keeper-contracts/artifacts"
|
||||
# - bash -x start_ocean.sh --no-commons --no-dashboard 2>&1 > start_ocean.log &
|
||||
# - cd ..
|
||||
- cp .env.example .env && cp .env.example .env.build
|
||||
# overwrite AQUARIUS_URI from above .env files, which default to Spree
|
||||
- export AQUARIUS_URI='https://aquarius.pacific.dexfreight.dev-ocean.com'
|
||||
|
||||
script:
|
||||
# will run `npm ci` automatically here
|
||||
# - ./scripts/keeper.sh
|
||||
- npm test
|
||||
- './cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT'
|
||||
- npm run build
|
||||
|
||||
notifications:
|
||||
email: false
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
231
README.md
|
@ -1 +1,230 @@
|
|||
# ocean_marketplace
|
||||
[![banner](https://raw.githubusercontent.com/oceanprotocol/art/master/github/repo-banner%402x.png)](https://oceanprotocol.com)
|
||||
|
||||
<h1 align="center">dexFreight Marketplace</h1>
|
||||
|
||||
> 🚚 Data marketplace for dexFreight.
|
||||
|
||||
[![Build Status](https://travis-ci.com/oceanprotocol/dexfreight.svg?branch=master)](https://travis-ci.com/oceanprotocol/dexfreight)
|
||||
[![Now deployment](https://flat.badgen.net/badge/now/auto-deployment/21c4dd?icon=now)](https://zeit.co/oceanprotocol/dexfreight)
|
||||
[![Maintainability](https://api.codeclimate.com/v1/badges/d114f94f75e6efd2ee71/maintainability)](https://codeclimate.com/repos/5e3933869a31771fd800011c/maintainability)
|
||||
[![Test Coverage](https://api.codeclimate.com/v1/badges/d114f94f75e6efd2ee71/test_coverage)](https://codeclimate.com/repos/5e3933869a31771fd800011c/test_coverage)
|
||||
[![js oceanprotocol](https://img.shields.io/badge/js-oceanprotocol-7b1173.svg)](https://github.com/oceanprotocol/eslint-config-oceanprotocol)
|
||||
|
||||
**Table of Contents**
|
||||
|
||||
- [🤓 Resources](#-resources)
|
||||
- [🏄 Get Started](#-get-started)
|
||||
- [Local Spree components with Barge](#local-spree-components-with-barge)
|
||||
- [🦑 Environment variables](#-environment-variables)
|
||||
- [🎨 Storybook](#-storybook)
|
||||
- [✨ Code Style](#-code-style)
|
||||
- [👩🔬 Testing](#-testing)
|
||||
- [🛳 Production](#-production)
|
||||
- [⬆️ Deployment](#️-deployment)
|
||||
- [Manual Deployment](#manual-deployment)
|
||||
- [🏗 Ocean Protocol Infrastructure](#-ocean-protocol-infrastructure)
|
||||
- [🏛 License](#-license)
|
||||
|
||||
## 🤓 Resources
|
||||
|
||||
- [UI Design: Figma Mock Up](https://www.figma.com/file/K38ZsQjzndyp2YFJCLxIN7/dexFreight-Marketplace)
|
||||
- [Planning: ZenHub Board](https://app.zenhub.com/workspaces/dexfreight-marketplace-5e2f201751116794cf4f2e75/board?repos=236508929)
|
||||
|
||||
## 🏄 Get Started
|
||||
|
||||
The app is a React app built with [Next.js](https://nextjs.org) + TypeScript + CSS modules and will connect to Ocean components in Pacific by default.
|
||||
|
||||
To start local development:
|
||||
|
||||
```bash
|
||||
git clone git@github.com:oceanprotocol/dexfreight.git
|
||||
cd dexfreight
|
||||
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
This will launch the app under [localhost:3000](http://localhost:3000).
|
||||
|
||||
Depending on your configuration, you might have to increase the amount of `inotify` watchers:
|
||||
|
||||
```
|
||||
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
|
||||
```
|
||||
|
||||
### Local Spree components with Barge
|
||||
|
||||
If you prefer to connect to locally running components instead of remote connections to Ocean's network, you can spin up [`barge`](https://github.com/oceanprotocol/barge) and use a local Spree network in another terminal before running `npm start`:
|
||||
|
||||
```bash
|
||||
git clone git@github.com:oceanprotocol/barge.git
|
||||
cd barge
|
||||
|
||||
# startup with local Spree node
|
||||
./start_ocean.sh --no-commons
|
||||
```
|
||||
|
||||
This will take some time on first start, and at the end you need to copy the generated contract artifacts out of the Docker container. To do so, use this script from the root of the app folder:
|
||||
|
||||
```bash
|
||||
./scripts/keeper.sh
|
||||
```
|
||||
|
||||
The script will wait for all contracts to be generated in the `keeper-contracts` Docker container, then will copy the artifacts in place into `node_modules/@oceanprotocol/keeper-contracts/artifacts/`.
|
||||
|
||||
Finally, set environment variables to use those local connections in `.env` & `.env.build` in the app:
|
||||
|
||||
```bash
|
||||
# modify env variables, Spree is enabled by default when using those files
|
||||
cp .env.example .env && cp .env.example .env.build
|
||||
```
|
||||
|
||||
## 🦑 Environment variables
|
||||
|
||||
The `./src/config/ocean.ts` file is setup to prioritize environment variables for setting each Ocean component endpoint. By setting environment variables, you can easily switch between Ocean networks the app connects to, without directly modifying `./src/config/ocean.ts`.
|
||||
|
||||
For local development, you can use a `.env` & `.env.build` file:
|
||||
|
||||
```bash
|
||||
# modify env variables, Spree is enabled by default when using those files
|
||||
cp .env.example .env && cp .env.example .env.build
|
||||
```
|
||||
|
||||
For a Now deployment, all environment variables defining the Ocean component endpoints need to be added with `now secrets` to `oceanprotocol` org based on the `@` variable names defined in `now.json`, e.g.:
|
||||
|
||||
```bash
|
||||
now switch
|
||||
now secrets add aquarius_uri https://aquarius.pacific.dexfreight.dev-ocean.com
|
||||
```
|
||||
|
||||
Adding the env vars like that will provide them during both, build & run time.
|
||||
|
||||
## 🎨 Storybook
|
||||
|
||||
[Storybook](https://storybook.js.org) is set up for this project and is used for UI development of components. Stories are created inside `src/components/` alongside each component in the form of `ComponentName.stories.tsx`.
|
||||
|
||||
To run the Storybook server, execute in your Terminal:
|
||||
|
||||
```bash
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
This will launch the Storybook UI with all stories loaded under [localhost:4000](http://localhost:4000).
|
||||
|
||||
Every deployment run will build and deploy the exported storybook under `/storybook/`, e.g. the current one matching `master` under [https://dexfreight-ten.now.sh/storybook/](https://dexfreight-ten.now.sh/storybook/).
|
||||
|
||||
## ✨ Code Style
|
||||
|
||||
For linting and auto-formatting you can use from the root of the project:
|
||||
|
||||
```bash
|
||||
# lint all js with eslint
|
||||
npm run lint
|
||||
|
||||
# auto format all js & css with prettier, taking all configs into account
|
||||
npm run format
|
||||
```
|
||||
|
||||
## 👩🔬 Testing
|
||||
|
||||
Test suite for unit tests is setup with [Jest](https://jestjs.io) as a test runner and:
|
||||
|
||||
- [react-testing-library](https://github.com/kentcdodds/react-testing-library) for all React components
|
||||
- [node-mocks-http](https://github.com/howardabrams/node-mocks-http) for all `src/pages/api/` routes
|
||||
|
||||
> Note: fully testing Next.js API routes should be part of integration tests. There are [various problems](https://spectrum.chat/next-js/general/api-routes-unit-testing~aa868f97-3a7d-45fe-97e5-3f0408f0022d) with fully testing them so a proper unit test suite for them should be setup.
|
||||
|
||||
To run all linting and unit tests:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
For local development, you can start the test runner in a watch mode.
|
||||
|
||||
```bash
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
For analyzing the generated JavaScript bundle sizes you can use:
|
||||
|
||||
```bash
|
||||
npm run analyze
|
||||
```
|
||||
|
||||
## 🛳 Production
|
||||
|
||||
To create a production build, run from the root of the project:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# serve production build
|
||||
npm run serve
|
||||
```
|
||||
|
||||
## ⬆️ Deployment
|
||||
|
||||
Every branch or Pull Request is automatically deployed by [Now](https://zeit.co/now) with their GitHub integration. A link to a deployment will appear under each Pull Request.
|
||||
|
||||
The latest deployment of the `master` branch is automatically aliased to `xxx`.
|
||||
|
||||
### Manual Deployment
|
||||
|
||||
If needed, app can be deployed manually. Make sure to switch to Ocean Protocol org before deploying:
|
||||
|
||||
```bash
|
||||
# first run
|
||||
now login
|
||||
now switch
|
||||
|
||||
# deploy
|
||||
now
|
||||
# switch alias to new deployment
|
||||
now alias
|
||||
```
|
||||
|
||||
## 🏗 Ocean Protocol Infrastructure
|
||||
|
||||
The following Aquarius & Brizo instances specifically for dexFreight marketplace are deployed in Ocean Protocol's AWS K8:
|
||||
|
||||
**Nile (Staging)**
|
||||
|
||||
- K8 namespace: `dexfreight-nile`
|
||||
- `aquarius.nile.dexfreight.dev-ocean.com`
|
||||
- `brizo.nile.dexfreight.dev-ocean.com`
|
||||
|
||||
Edit command with `kubectl`, e.g.:
|
||||
|
||||
```bash
|
||||
kubectl edit deployment -n dexfreight-nile aquarius
|
||||
```
|
||||
|
||||
**Pacific (Production)**
|
||||
|
||||
- K8 namespace: `dexfreight-pacific`
|
||||
- `aquarius.pacific.dexfreight.dev-ocean.com`
|
||||
- `brizo.pacific.dexfreight.dev-ocean.com`
|
||||
|
||||
Edit command with `kubectl`, e.g.:
|
||||
|
||||
```bash
|
||||
kubectl edit deployment -n dexfreight-pacific aquarius
|
||||
```
|
||||
|
||||
## 🏛 License
|
||||
|
||||
```text
|
||||
Copyright 2019 Ocean Protocol Foundation Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
```
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
# Terms and Conditions
|
||||
|
||||
Thanks for using our product and services. By using our products and services you are agreeing to the terms. Please read them carefully.
|
||||
|
||||
_Latest Revision: February 20, 2020_
|
||||
|
||||
## Definitions
|
||||
|
||||
“Users” mean buyers and sellers of data in the marketplace.
|
||||
“Data seller or provider” means individual or an institution offering to sell the data in the marketplace.
|
||||
“Data buyer or recipient” means individual or an institution offering to buy the data from the marketplace.
|
||||
“Marketplace” means the web-based logistics data marketplace via which users can sell and buy logistics data.
|
||||
|
||||
## Your agreement
|
||||
|
||||
By using this Marketplace, you agree to be bound by, and to comply with, these Terms and Conditions. If you do not agree to these Terms and Conditions, please do not use it.
|
||||
|
||||
## Toxicity
|
||||
|
||||
You shall not use the Marketplace to advertise, sell, or exchange any data, products or services relating to illegal or illicit activities, including, without limitation, pornographic products or services, illegal drug products or services, or illegal weapons.
|
||||
|
||||
## Data Sharing
|
||||
|
||||
The data recipient or buyer will not release data to a third party without prior approval from the data provider or seller. The data recipient will not share, publish, or otherwise release any findings or conclusions derived from analysis of data obtained from the data provider or data seller without prior approval from the data seller. Neither dexFreight nor BigChainDB is responsible and guarantee quality of data provided by the sellers.
|
||||
|
||||
## Delivery of the Data
|
||||
|
||||
BigchainDB/dexFreight does not host in its premises the data being offered or already offered in the Marketplace by the data provider. If the provider selects IPFS as a medium to host data, then the data is stored in distributed data storage which neither dexFreight nor BigchainDB has control over or the ability to manage.
|
||||
|
||||
## Right to Remove Published Metadata
|
||||
|
||||
BigchainDB/dexFreight holds the right to remove published data if found in violation of these terms and conditions. We also reserve the right to remove published data that are deemed out of scope of the Marketplace. That means data published by the sellers has to be logistics data and consistent with the data categories mentioned in the Marketplace.
|
||||
|
||||
## Confidentiality
|
||||
|
||||
The Recipient shall: (a) protect the Confidential Information of the Provider with at least the same degree of care with which it protects its own confidential or proprietary information, but not less than a reasonable degree of care, and (b) instruct its employees and all other parties who are authorized to have access to the Provider’s Confidential Information of the restrictions contained in this Agreement. Each Recipient shall limit access to the Provider’s Confidential Information to its own employees, agents, contractors, , and consultants strictly with a "need to know"; provided, however, that such parties have executed an agreement with the Recipient with confidentiality provisions at least as restrictive as those contained herein. The parties hereby undertake to ensure the individual compliance of such employees, agents, contractors, and consultants with the terms hereof and shall be responsible for any actions of such employees, agents, contractors, and consultants. The Recipient shall, as soon as reasonably practical after discovery, report to the Provider any unauthorized use of, disclosure of or access to the Provider’s Confidential Information, subject to any reasonable restrictions placed on the timing of such notice by a law enforcement or regulatory agency investigating the incident; and take all reasonable measures to prevent any further unauthorized disclosure or access.
|
||||
|
||||
## Indemnification
|
||||
|
||||
Users of the Marketplace shall defend, indemnify and hold harmless dexFreight and BigChainDB from and against any and all claims, demands, judgments, liability, damages, losses, costs, and expenses, including reasonable attorneys' fees, arising out of or resulting from the User’s or its Client's or Third Party Service Provider's misuse or unauthorized use of the Marketplace . In addition, both Data Provider and Data Recipients will hold each other harmless from and against any and all claims, demands, judgements, liability, damages, losses, costs, and expenses as a result of using Provider’s data.
|
||||
|
||||
## Warranty Disclaimer / Limitation of Liability
|
||||
|
||||
The Logistics Data Marketplace (referred to as Marketplace from here on) may be subject to transcription and transmission errors, accordingly, the Marketplace is provided on an "as is," "as available" basis. Any use or reliance upon the Marketplace by Users shall be at its own risk. EXCEPT AS SET FORTH IN THIS SECTION, NEITHER BigchainDB/dexFreight NOR THE DATA PROVIDER MAKES ANY WARRANTIES, EXPRESS OR IMPLIED, HEREUNDER WITH RESPECT TO THE SERVICES, DATA, OR THE MEDIA ON WHICH THE DATA IS PROVIDED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF ACCURACY, COMPLETENESS, CURRENTNESS, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. BigchainDB/dexFreight AND THE DATA PROVIDERS AGGREGATE LIABILITY TO DATA RECIPIENT OR ANY THIRD PARTY, WHETHER FOR NEGLIGENCE, BREACH OF WARRANTY, OR ANY OTHER CAUSE OF ACTION, SHALL BE LIMITED TO THE PRICE PAID FOR THE PRODUCT OR SERVICES TO WHICH THE INCIDENT RELATES. IN NO EVENT SHALL the BigchainDB/dexFreight OR DATA PROVIDER BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES, WHETHER OR NOT FORESEEABLE AND HOWEVER ARISING, INCLUDING BUT NOT LIMITED TO LOST INCOME OR LOST REVENUE, WHETHER BASED IN CONTRACT, TORT OR ANY OTHER THEORY.
|
||||
|
||||
## Audit / Non-Compliance
|
||||
|
||||
BigchainDB/dexFreight may monitor your use of the Marketplace. BigchainDB/dexFreight reserves the right, in its sole discretion, to immediately suspend your use of the Marketplace in the event of any suspected or actual violation of the terms of this Agreement. In the event an audit reveals that you are not in compliance with the terms and conditions of this Agreement, you shall be responsible for the costs of the audit, as well as any and all damages resulting from such non-compliance including, without limitation, any special, incidental, indirect, or consequential damages whatsoever (including punitive damages and damages for loss of goodwill).
|
||||
|
||||
## Regulations
|
||||
|
||||
Users shall comply with all Federal and State laws and regulations governing the confidentiality of the information that is the subject of this Agreement.
|
||||
|
||||
## Intended Use
|
||||
|
||||
BigchainDB/dexFreight reserves the right to review and pre-approve the User’s intended use of the Marketplace.
|
||||
|
||||
## Force Majeure
|
||||
|
||||
BigchainDB/dexFreight shall not be liable for any losses arising out of the delay or interruption of its performance of the Marketplace due to any act of God, act of governmental authority, act of public enemy, war, riot, flood, civil commotion, insurrection, severe weather conditions, or any other cause beyond the reasonable control of the party.
|
||||
|
||||
By accessing and using this Marketplace, you accept and agree to be bound by the terms and conditions of this agreement. In addition, when using the Marketplace, you shall be subject to any posted guidelines or rules applicable to using the services. Any participation in this Marketplace will constitute acceptance of this agreement. If you do not agree to abide by the above, please do not use the Marketplace.
|
||||
|
||||
These terms and conditions are subject to change.
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"target": "es5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
|
@ -0,0 +1,109 @@
|
|||
const webpack = require('webpack')
|
||||
require('dotenv').config()
|
||||
|
||||
// Returns environment variables as an object
|
||||
const env = Object.keys(process.env).reduce((acc, curr) => {
|
||||
acc[`process.env.${curr}`] = JSON.stringify(process.env[curr])
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const withSvgr = (nextConfig = {}) => ({
|
||||
webpack(config, options) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
use: [
|
||||
{
|
||||
loader: '@svgr/webpack',
|
||||
options: {
|
||||
icon: true
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
if (typeof nextConfig.webpack === 'function') {
|
||||
return nextConfig.webpack(config, options)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const withFsFix = (nextConfig = {}) => ({
|
||||
webpack(config, options) {
|
||||
// Fixes npm packages that depend on `fs` module
|
||||
// https://github.com/zeit/next.js/issues/7755#issuecomment-508633125
|
||||
// or https://github.com/zeit/next.js/issues/7755
|
||||
if (!options.isServer) {
|
||||
config.node = {
|
||||
fs: 'empty'
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof nextConfig.webpack === 'function') {
|
||||
return nextConfig.webpack(config, options)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
})
|
||||
|
||||
const withGlobalConstants = (nextConfig = {}) => ({
|
||||
webpack(config, options) {
|
||||
// Allows to create global constants which can be configured at compile
|
||||
// time (in this case they are the environment variables)
|
||||
config.plugins.push(new webpack.DefinePlugin(env))
|
||||
|
||||
if (typeof nextConfig.webpack === 'function') {
|
||||
return nextConfig.webpack(config, options)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
})
|
||||
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true'
|
||||
})
|
||||
|
||||
const withMarkdown = (nextConfig = {}) => ({
|
||||
webpack(config, options) {
|
||||
config.module.rules.push({
|
||||
test: /\.md$/,
|
||||
loader: 'raw-loader'
|
||||
})
|
||||
|
||||
if (typeof nextConfig.webpack === 'function') {
|
||||
return nextConfig.webpack(config, options)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = withBundleAnalyzer(
|
||||
withSvgr(
|
||||
withFsFix(
|
||||
withMarkdown(
|
||||
withGlobalConstants({
|
||||
exportPathMap: (defaultPathMap, { dev }) => {
|
||||
// In dev environment return defaultPathMas as it is
|
||||
if (dev) {
|
||||
return defaultPathMap
|
||||
}
|
||||
|
||||
// pages we know about beforehand
|
||||
const paths = {
|
||||
'/': { page: '/' },
|
||||
'/publish': { page: '/publish' },
|
||||
'/explore': { page: '/explore' }
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "dexfreight",
|
||||
"build": {
|
||||
"env": {
|
||||
"NODE_URI": "@node_uri",
|
||||
"AQUARIUS_URI": "@aquarius_uri",
|
||||
"BRIZO_URI": "@brizo_uri",
|
||||
"BRIZO_ADDRESS": "@brizo_address",
|
||||
"SECRET_STORE_URI": "@secret_store_uri",
|
||||
"FAUCET_URI": "@faucet_uri",
|
||||
"RATING_URI": "@rating_uri"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"NODE_URI": "@node_uri",
|
||||
"AQUARIUS_URI": "@aquarius_uri",
|
||||
"BRIZO_URI": "@brizo_uri",
|
||||
"BRIZO_ADDRESS": "@brizo_address",
|
||||
"SECRET_STORE_URI": "@secret_store_uri",
|
||||
"FAUCET_URI": "@faucet_uri",
|
||||
"RATING_URI": "@rating_uri"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"name": "dexfreight",
|
||||
"description": "Data marketplace for dexFreight.",
|
||||
"version": "0.0.1",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"start": "next dev",
|
||||
"export": "next export",
|
||||
"build": "npm run storybook:build && next build",
|
||||
"serve": "next start",
|
||||
"jest": "NODE_ENV=test jest -c tests/unit/jest.config.js",
|
||||
"test": "npm run lint && npm run jest",
|
||||
"test:watch": "npm run lint && npm run jest -- --watch",
|
||||
"lint": "eslint --ignore-path .gitignore --ext .js --ext .ts --ext .tsx .",
|
||||
"format": "prettier --ignore-path .gitignore **/**/*.{css,yml,js,jsx,ts,tsx,json} --write",
|
||||
"analyze": "ANALYZE=true next build",
|
||||
"storybook": "start-storybook -p 4000 -c .storybook",
|
||||
"storybook:build": "build-storybook -c .storybook -o public/storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oceanprotocol/squid": "2.0.0-beta.4",
|
||||
"axios": "^0.19.2",
|
||||
"date-fns": "^2.11.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"filesize": "^6.1.0",
|
||||
"is-url-superb": "^3.0.0",
|
||||
"next": "^9.3.2",
|
||||
"next-seo": "^4.4.0",
|
||||
"next-svgr": "^0.0.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"numeral": "^2.0.6",
|
||||
"react": "^16.12.0",
|
||||
"react-datepicker": "^2.14.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-dotdotdot": "^1.3.1",
|
||||
"react-jsonschema-form": "^1.8.1",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-paginate": "^6.3.2",
|
||||
"react-rating": "^2.0.4",
|
||||
"react-toastify": "^5.5.0",
|
||||
"shortid": "^2.2.15",
|
||||
"slugify": "^1.4.0",
|
||||
"use-debounce": "^3.4.0",
|
||||
"web3connect": "^1.0.0-beta.33"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.7",
|
||||
"@next/bundle-analyzer": "^9.3.0",
|
||||
"@storybook/addon-storyshots": "^5.3.17",
|
||||
"@storybook/react": "^5.3.17",
|
||||
"@testing-library/jest-dom": "^5.1.1",
|
||||
"@testing-library/react": "^10.0.1",
|
||||
"@testing-library/react-hooks": "^3.2.1",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/node": "^13.9.1",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/numeral": "0.0.26",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-datepicker": "^2.11.0",
|
||||
"@types/react-jsonschema-form": "^1.7.0",
|
||||
"@types/react-paginate": "^6.2.1",
|
||||
"@types/shortid": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^2.23.0",
|
||||
"@typescript-eslint/parser": "^2.23.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-preset-react-app": "^9.1.1",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-oceanprotocol": "^1.5.0",
|
||||
"eslint-config-prettier": "^6.10.0",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^25.1.0",
|
||||
"node-mocks-http": "^1.8.1",
|
||||
"prettier": "^1.19.1",
|
||||
"react-test-renderer": "^16.12.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"typescript": "^3.8.3",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/oceanprotocol/dexfreight"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Wait for contracts migration and extract Keeper artifacts
|
||||
|
||||
RETRY_COUNT=0
|
||||
COMMAND_STATUS=1
|
||||
|
||||
printf '\n\e[33m◯ Waiting for contracts to be generated...\e[0m\n'
|
||||
|
||||
mkdir -p artifacts
|
||||
|
||||
until [ $COMMAND_STATUS -eq 0 ] || [ $RETRY_COUNT -eq 120 ]; do
|
||||
keeper_contracts_docker_id=$(docker container ls | grep keeper-contracts | awk '{print $1}')
|
||||
docker cp ${keeper_contracts_docker_id}:/keeper-contracts/artifacts/ready ./artifacts/ > /dev/null 2>&1
|
||||
COMMAND_STATUS=$?
|
||||
sleep 5
|
||||
(( RETRY_COUNT=RETRY_COUNT+1 ))
|
||||
done
|
||||
|
||||
printf '\e[32m✔ Found new contract artifacts.\e[0m\n'
|
||||
|
||||
rm -rf ./artifacts/
|
||||
|
||||
if [ $COMMAND_STATUS -ne 0 ]; then
|
||||
echo "Waited for more than two minutes, but keeper contracts have not been migrated yet. Did you run an Ethereum RPC client and the migration script?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker cp "${keeper_contracts_docker_id}":/keeper-contracts/artifacts/. ./node_modules/@oceanprotocol/keeper-contracts/artifacts/
|
||||
|
||||
printf '\e[32m✔ Copied new contract artifacts.\e[0m\n'
|
|
@ -0,0 +1,31 @@
|
|||
module.exports = {
|
||||
title: 'Logistics Data Marketplace',
|
||||
description: `Easily buy and sell logistics data from around the world.`,
|
||||
url: 'https://dexfreight.oceanprotocol.com',
|
||||
copyright:
|
||||
'All Rights Reserved. Powered by [dexFreight](https://dexfreight.io) & [Ocean Protocol](https://oceanprotocol.com)',
|
||||
refundPolicy: [
|
||||
'Data can be challenged within 2 days after purchase.',
|
||||
'The marketplace decides if you are eligible for refund.'
|
||||
],
|
||||
assetTerms: [
|
||||
{
|
||||
name: 'Personal Identifiable Information',
|
||||
value: 'This offer contains no personal data'
|
||||
},
|
||||
{
|
||||
name: 'Regions where data can be used',
|
||||
value: 'Worldwide'
|
||||
}
|
||||
],
|
||||
menu: [
|
||||
{
|
||||
name: 'Explore',
|
||||
link: '/explore'
|
||||
},
|
||||
{
|
||||
name: 'Publish',
|
||||
link: '/publish'
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { MetaData, AdditionalInformation, Curation } from '@oceanprotocol/squid'
|
||||
|
||||
declare type DeliveryType = 'files' | 'api' | 'subscription'
|
||||
|
||||
declare type Granularity =
|
||||
| 'hourly'
|
||||
| 'daily'
|
||||
| 'weekly'
|
||||
| 'monthly'
|
||||
| 'annually'
|
||||
| 'Not updated periodically'
|
||||
| ''
|
||||
|
||||
export interface Sample {
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface AdditionalInformationDexFreight extends AdditionalInformation {
|
||||
description: string // required for dexFreight
|
||||
categories: [string] // required for dexFreight, lock to one category only
|
||||
links?: Sample[] // redefine existing key, cause not specific enough in Squid
|
||||
deliveryType: DeliveryType
|
||||
termsAndConditions: boolean
|
||||
dateRange?: [string, string]
|
||||
granularity?: Granularity
|
||||
supportName?: string
|
||||
supportEmail?: string
|
||||
}
|
||||
|
||||
export interface MetaDataDexFreight extends MetaData {
|
||||
additionalInformation: AdditionalInformationDexFreight
|
||||
curation: Curation
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
declare module '*.svg' {
|
||||
import * as React from 'react'
|
||||
export const ReactComponent: React.FunctionComponent<React.SVGProps<
|
||||
SVGSVGElement
|
||||
>>
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare type Nullable<T> = T | null
|
||||
declare module '*.md'
|
|
@ -0,0 +1,22 @@
|
|||
.app {
|
||||
height: 100%;
|
||||
|
||||
/* sticky footer technique */
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: calc(var(--spacer) * 2) calc(var(--spacer) / 1.5);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: var(--layout-max-width);
|
||||
|
||||
/* sticky footer technique */
|
||||
flex: 1;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import React, { ReactNode } from 'react'
|
||||
import Head from 'next/head'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import styles from './Layout.module.css'
|
||||
import Header from './components/organisms/Header'
|
||||
import Footer from './components/organisms/Footer'
|
||||
import PageHeader from './components/molecules/PageHeader'
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
noPageHeader
|
||||
}: {
|
||||
children: ReactNode
|
||||
title?: string
|
||||
description?: string
|
||||
noPageHeader?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.app}>
|
||||
<Head>
|
||||
<link rel="icon" href="/icons/icon-96x96.png" />
|
||||
<link rel="apple-touch-icon" href="icons/icon-256x256.png" />
|
||||
<meta name="theme-color" content="#ca2935" />
|
||||
</Head>
|
||||
|
||||
<NextSeo title={title} description={description} />
|
||||
|
||||
<Header />
|
||||
<main className={styles.main}>
|
||||
{title && !noPageHeader && (
|
||||
<PageHeader title={title} description={description} />
|
||||
)}
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
.alert {
|
||||
composes: box from './Box.module.css';
|
||||
max-width: 40rem;
|
||||
margin: auto;
|
||||
border-width: 0;
|
||||
border-left-width: 0.5rem;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.alert,
|
||||
.title {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-large);
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* States */
|
||||
.error {
|
||||
border-color: var(--color-danger);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.success {
|
||||
border-color: var(--color-success);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.info {
|
||||
border-color: var(--color-info);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.warning {
|
||||
border-color: var(--color-warning);
|
||||
color: var(--color-warning);
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react'
|
||||
import { Center } from '../../../.storybook/helpers'
|
||||
import { Alert } from './Alert'
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Alert',
|
||||
decorators: [(storyFn: any) => <Center>{storyFn()}</Center>]
|
||||
}
|
||||
|
||||
export const Error = () => (
|
||||
<Alert title="Title" text="I am the alert text." state="error" />
|
||||
)
|
||||
|
||||
export const Warning = () => (
|
||||
<Alert title="Title" text="I am the alert text." state="warning" />
|
||||
)
|
||||
|
||||
export const Info = () => (
|
||||
<Alert title="Title" text="I am the alert text." state="info" />
|
||||
)
|
||||
|
||||
export const Success = () => (
|
||||
<Alert title="Title" text="I am the alert text." state="success" />
|
||||
)
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react'
|
||||
import styles from './Alert.module.css'
|
||||
|
||||
export function Alert({
|
||||
title,
|
||||
text,
|
||||
state
|
||||
}: {
|
||||
title: string
|
||||
text: string
|
||||
state: 'error' | 'warning' | 'info' | 'success'
|
||||
}) {
|
||||
return (
|
||||
<div className={`${styles.alert} ${styles[state]}`}>
|
||||
<h3 className={styles.title}>{title}</h3>
|
||||
<p className={styles.text}>{text}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
.box {
|
||||
background: var(--color-white);
|
||||
padding: var(--spacer);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--color-grey-light);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
a.box:hover,
|
||||
a.box:focus {
|
||||
outline: 0;
|
||||
border-color: var(--color-grey);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
transform: translate3d(0, -2px, 0);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react'
|
||||
import styles from './Box.module.css'
|
||||
import { Center } from '../../../.storybook/helpers'
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Box',
|
||||
decorators: [(storyFn: any) => <Center>{storyFn()}</Center>]
|
||||
}
|
||||
|
||||
export const Normal = () => <div className={styles.box}>Hello Fancy Box</div>
|
|
@ -0,0 +1,64 @@
|
|||
.button {
|
||||
border: 2px solid var(--color-secondary);
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
padding: calc(var(--spacer) / 5) var(--spacer);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
border-radius: var(--border-radius);
|
||||
transition: 0.2s ease-out;
|
||||
color: var(--color-white);
|
||||
background: var(--color-secondary);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.button:hover,
|
||||
.button:focus {
|
||||
color: var(--color-dark);
|
||||
background: transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button:active {
|
||||
color: var(--color-white);
|
||||
background: var(--color-dark);
|
||||
border-color: var(--color-dark);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.primary:hover,
|
||||
.primary:focus {
|
||||
}
|
||||
|
||||
.primary:active {
|
||||
}
|
||||
|
||||
.link {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
background: 0;
|
||||
padding: 0;
|
||||
color: var(--color-primary);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-base);
|
||||
font-family: inherit;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react'
|
||||
import Button from './Button'
|
||||
import { Center } from '../../../.storybook/helpers'
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Button',
|
||||
decorators: [(storyFn: any) => <Center>{storyFn()}</Center>]
|
||||
}
|
||||
|
||||
export const Normal = () => <Button>Hello Button</Button>
|
||||
|
||||
export const Primary = () => <Button primary>Hello Button</Button>
|
||||
|
||||
export const Link = () => <Button link>Hello Button</Button>
|
|
@ -0,0 +1,44 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import Link from 'next/link'
|
||||
import styles from './Button.module.css'
|
||||
|
||||
declare type ButtonProps = {
|
||||
children: string | ReactElement
|
||||
className?: string
|
||||
primary?: boolean
|
||||
link?: boolean
|
||||
href?: string
|
||||
size?: string
|
||||
onClick?: any
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const Button = ({
|
||||
primary,
|
||||
link,
|
||||
href,
|
||||
size,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ButtonProps) => {
|
||||
const classes = primary
|
||||
? `${styles.button} ${styles.primary}`
|
||||
: link
|
||||
? `${styles.button} ${styles.link}`
|
||||
: styles.button
|
||||
|
||||
return href ? (
|
||||
<Link href={href}>
|
||||
<a className={`${classes} ${className}`} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button className={`${classes} ${className}`} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
|
@ -0,0 +1,3 @@
|
|||
.label {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react'
|
||||
import Checkbox from './Checkbox'
|
||||
import { Center } from '../../../.storybook/helpers'
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Checkbox',
|
||||
decorators: [(storyFn: any) => <Center>{storyFn()}</Center>]
|
||||
}
|
||||
|
||||
export const Checked = () => (
|
||||
<Checkbox
|
||||
name="someName"
|
||||
checked
|
||||
onChange={() => null}
|
||||
label="Example checkbox"
|
||||
/>
|
||||
)
|
||||
|
||||
export const Unchecked = () => (
|
||||
<Checkbox
|
||||
name="someName"
|
||||
checked={false}
|
||||
onChange={() => null}
|
||||
label="Example checkbox"
|
||||
/>
|
||||
)
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react'
|
||||
import styles from './Checkbox.module.css'
|
||||
|
||||
interface CheckboxProps {
|
||||
name: string
|
||||
checked: boolean
|
||||
onChange?: (evt: React.ChangeEvent) => void
|
||||
label: string
|
||||
}
|
||||
|
||||
const Checkbox: React.FC<CheckboxProps> = ({
|
||||
name,
|
||||
checked,
|
||||
onChange,
|
||||
label
|
||||
}) => {
|
||||
return (
|
||||
<label className={styles.label}>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={name}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
className={styles.checkbox}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
export default Checkbox
|
|
@ -0,0 +1,20 @@
|
|||
.file {
|
||||
background: var(--color-secondary);
|
||||
background-size: 100%;
|
||||
padding: var(--spacer) calc(var(--spacer) / 2);
|
||||
margin-right: calc(var(--spacer) / 1.5);
|
||||
height: 7.5rem;
|
||||
width: 6rem;
|
||||
/* cut the corner */
|
||||
clip-path: polygon(85% 0, 100% 15%, 100% 100%, 0 100%, 0 0);
|
||||
}
|
||||
|
||||
.file li {
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.file li.empty {
|
||||
font-size: var(--font-size-mini);
|
||||
opacity: 0.75;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react'
|
||||
import { File as FileMetaData } from '@oceanprotocol/squid'
|
||||
import filesize from 'filesize'
|
||||
import cleanupContentType from '../../utils/cleanupContentType'
|
||||
import styles from './File.module.css'
|
||||
|
||||
export default function File({ file }: { file: FileMetaData }) {
|
||||
if (!file) return null
|
||||
|
||||
return (
|
||||
<ul className={styles.file}>
|
||||
{file.contentType || file.contentLength ? (
|
||||
<>
|
||||
<li>{cleanupContentType(file.contentType)}</li>
|
||||
<li>
|
||||
{file.contentLength && file.contentLength !== '0'
|
||||
? filesize(Number(file.contentLength))
|
||||
: ''}
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<li className={styles.empty}>No file info available</li>
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
.dateRange {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0 var(--spacer);
|
||||
height: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
composes: checkbox from '../../molecules/Form/FieldTemplate.module.css';
|
||||
margin-top: calc(var(--spacer) / 8);
|
||||
}
|
||||
|
||||
.label {
|
||||
composes: label from '../../molecules/Form/FieldTemplate.module.css';
|
||||
font-size: var(--font-size-small);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react'
|
||||
import { Center } from '../../../../.storybook/helpers'
|
||||
import DateRangeWidget from './DateRangeWidget'
|
||||
import { PublishFormSchema } from '../../../models/PublishForm'
|
||||
|
||||
export default {
|
||||
title: 'Atoms/DateRangeWidget',
|
||||
decorators: [(storyFn: () => React.FC) => <Center>{storyFn()}</Center>]
|
||||
}
|
||||
|
||||
export const DateRange = () => (
|
||||
<DateRangeWidget
|
||||
schema={PublishFormSchema}
|
||||
id="1"
|
||||
autofocus={false}
|
||||
disabled={false}
|
||||
label="Date Range"
|
||||
formContext={{}}
|
||||
readonly={false}
|
||||
value="[]"
|
||||
onBlur={() => {
|
||||
/* */
|
||||
}}
|
||||
onFocus={() => {
|
||||
/* */
|
||||
}}
|
||||
onChange={() => {
|
||||
/* */
|
||||
}}
|
||||
options={{}}
|
||||
required={false}
|
||||
/>
|
||||
)
|
|
@ -0,0 +1,87 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { WidgetProps } from 'react-jsonschema-form'
|
||||
import dynamic from 'next/dynamic'
|
||||
import styles from './DateRangeWidget.module.css'
|
||||
import { toStringNoMS } from '../../../utils'
|
||||
|
||||
// lazy load this module, it's huge
|
||||
const LazyDatePicker = dynamic(() => import('react-datepicker'))
|
||||
|
||||
export function getWidgetValue(
|
||||
date1: Date,
|
||||
date2: Date,
|
||||
range: boolean
|
||||
): string {
|
||||
let [initial, final] = [toStringNoMS(date1), toStringNoMS(date2)]
|
||||
|
||||
if (!range) {
|
||||
final = initial
|
||||
}
|
||||
|
||||
return JSON.stringify([initial, final])
|
||||
}
|
||||
|
||||
export default function DateRangeWidget(props: WidgetProps) {
|
||||
const { onChange } = props
|
||||
const [startDate, setStartDate] = useState<Date>(new Date())
|
||||
const [endDate, setEndDate] = useState<Date>(new Date())
|
||||
const [range, setRange] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// If the range checkbox is clicked we update the value of the picker
|
||||
onChange(getWidgetValue(startDate, endDate, range))
|
||||
}, [range])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.dateRange}>
|
||||
{range ? (
|
||||
<>
|
||||
<LazyDatePicker
|
||||
selected={startDate}
|
||||
onChange={(date: Date) => {
|
||||
setStartDate(date)
|
||||
onChange(getWidgetValue(date, endDate, range))
|
||||
}}
|
||||
startDate={startDate}
|
||||
selectsStart
|
||||
endDate={endDate}
|
||||
/>
|
||||
<div className={styles.separator}>–</div>
|
||||
<LazyDatePicker
|
||||
selected={endDate}
|
||||
selectsEnd
|
||||
onChange={(date: Date) => {
|
||||
setEndDate(date)
|
||||
onChange(getWidgetValue(startDate, date, range))
|
||||
}}
|
||||
minDate={startDate}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<LazyDatePicker
|
||||
selected={startDate}
|
||||
onChange={(date: Date) => {
|
||||
setStartDate(date)
|
||||
onChange(getWidgetValue(date, date, range))
|
||||
}}
|
||||
startDate={startDate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.checkbox}>
|
||||
<input
|
||||
id="range"
|
||||
type="checkbox"
|
||||
onChange={ev => setRange(ev.target.checked)}
|
||||
checked={range}
|
||||
/>
|
||||
<label className={styles.label} htmlFor="range">
|
||||
Date Range
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
.terms {
|
||||
padding: calc(var(--spacer) / 2);
|
||||
border: 1px solid var(--color-grey-light);
|
||||
background-color: var(--color-grey-dimmed);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
font-size: var(--font-size-small);
|
||||
|
||||
max-height: 250px;
|
||||
/* smooth overflow scrolling for pre-iOS 13 */
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.terms h1 {
|
||||
font-size: var(--font-size-base);
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.terms h2 {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.label {
|
||||
composes: label from '../../molecules/Form/FieldTemplate.module.css';
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.req {
|
||||
composes: req from '../../molecules/Form/FieldTemplate.module.css';
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react'
|
||||
import { WidgetProps } from 'react-jsonschema-form'
|
||||
import styles from './TermsWidget.module.css'
|
||||
import Markdown from '../Markdown'
|
||||
import terms from '../../../../content/terms.md'
|
||||
|
||||
export default function TermsWidget(props: WidgetProps) {
|
||||
const {
|
||||
id,
|
||||
value,
|
||||
disabled,
|
||||
readonly,
|
||||
label,
|
||||
autofocus,
|
||||
onBlur,
|
||||
onFocus,
|
||||
onChange,
|
||||
required
|
||||
// DescriptionField
|
||||
} = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Markdown text={terms} className={styles.terms} />
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={required ? `${styles.label} ${styles.req}` : styles.label}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
checked={typeof value === 'undefined' ? false : value}
|
||||
disabled={disabled || readonly}
|
||||
autoFocus={autofocus}
|
||||
onChange={event => onChange(event.target.checked)}
|
||||
onBlur={onBlur && (event => onBlur(id, event.target.checked))}
|
||||
onFocus={onFocus && (event => onFocus(id, event.target.checked))}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
.item {
|
||||
display: list-item;
|
||||
margin-top: calc(var(--spacer) / 8);
|
||||
color: var(--color-grey);
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.item span {
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
.ulItem {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
.olItem {
|
||||
list-style-type: decimal;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react'
|
||||
import { ListItem } from './Lists'
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Lists'
|
||||
}
|
||||
|
||||
export const Unordered = () => (
|
||||
<ul>
|
||||
<ListItem>Hello You</ListItem>
|
||||
<ListItem>Hello You</ListItem>
|
||||
<ListItem>Hello You</ListItem>
|
||||
</ul>
|
||||
)
|
||||
|
||||
export const Ordered = () => (
|
||||
<ol>
|
||||
<ListItem ol>Hello You</ListItem>
|
||||
<ListItem ol>Hello You</ListItem>
|
||||
<ListItem ol>Hello You</ListItem>
|
||||
</ol>
|
||||
)
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react'
|
||||
import styles from './Lists.module.css'
|
||||
|
||||
export function ListItem({ children, ol }: { children: any; ol?: boolean }) {
|
||||
const classes = ol
|
||||
? `${styles.item} ${styles.olItem}`
|
||||
: `${styles.item} ${styles.ulItem}`
|
||||
|
||||
return (
|
||||
<li className={classes}>
|
||||
<span>{children}</span>
|
||||
</li>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
.loaderWrap {
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-grey);
|
||||
}
|
||||
|
||||
.loader,
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
background: var(--color-grey);
|
||||
border-radius: var(--border-radius);
|
||||
animation: load1 0.7s infinite ease-in-out;
|
||||
font-size: var(--font-size-base);
|
||||
width: 0.3em;
|
||||
height: 1em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 0.6em auto;
|
||||
position: relative;
|
||||
transform: translateZ(0);
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
.loaderHorizontal {
|
||||
composes: loader;
|
||||
display: inline-block;
|
||||
margin: 0 1.5em 0 0.7em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.loader:before {
|
||||
left: -0.6em;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.loader:after {
|
||||
left: 0.6em;
|
||||
}
|
||||
|
||||
@keyframes load1 {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
40% {
|
||||
transform: scaleY(1.3);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react'
|
||||
import Loader from './Loader'
|
||||
import { Center } from '../../../.storybook/helpers'
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Loader',
|
||||
decorators: [(storyFn: any) => <Center>{storyFn()}</Center>]
|
||||
}
|
||||
|
||||
export const Normal = () => <Loader />
|
||||
|
||||
export const WithMessage = () => (
|
||||
<Loader message="Crunching all the tech for you..." />
|
||||
)
|
||||
|
||||
export const WithMessageHorizontal = () => (
|
||||
<Loader message="Crunching all the tech for you..." isHorizontal />
|
||||
)
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react'
|
||||
import styles from './Loader.module.css'
|
||||
|
||||
export default function Loader({
|
||||
message,
|
||||
isHorizontal
|
||||
}: {
|
||||
message?: string
|
||||
isHorizontal?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.loaderWrap}>
|
||||
<span
|
||||
className={isHorizontal ? styles.loaderHorizontal : styles.loader}
|
||||
/>
|
||||
{message || null}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
const Markdown = ({
|
||||
text,
|
||||
className
|
||||
}: {
|
||||
text: string
|
||||
className?: string
|
||||
}) => {
|
||||
// fix react-markdown \n transformation
|
||||
// https://github.com/rexxars/react-markdown/issues/105#issuecomment-351585313
|
||||
const textCleaned = text.replace(/\\n/g, '\n ')
|
||||
|
||||
return <ReactMarkdown source={textCleaned} className={className} />
|
||||
}
|
||||
|
||||
export default Markdown
|
|
@ -0,0 +1,24 @@
|
|||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: var(--color-primary);
|
||||
position: fixed;
|
||||
z-index: 99;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0.2rem;
|
||||
}
|
||||
|
||||
#nprogress .peg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 10px var(--color-primary), 0 0 5px var(--color-primary);
|
||||
opacity: 1;
|
||||
transform: rotate(3deg) translate(0px, -4px);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import NProgress, { NProgressOptions } from 'nprogress'
|
||||
import Router from 'next/router'
|
||||
|
||||
//
|
||||
// Component loosely taken from, but highly refactored
|
||||
// https://github.com/sergiodxa/next-nprogress/blob/master/src/component.js
|
||||
//
|
||||
|
||||
declare type NProgressContainerProps = {
|
||||
showAfterMs?: number
|
||||
options?: NProgressOptions
|
||||
}
|
||||
|
||||
export default function NProgressContainer({
|
||||
showAfterMs = 300,
|
||||
options
|
||||
}: NProgressContainerProps) {
|
||||
let timer: NodeJS.Timeout
|
||||
|
||||
function routeChangeStart() {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(NProgress.start, showAfterMs)
|
||||
}
|
||||
|
||||
function routeChangeEnd() {
|
||||
clearTimeout(timer)
|
||||
NProgress.done()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (options) {
|
||||
NProgress.configure(options)
|
||||
}
|
||||
|
||||
Router.events.on('routeChangeStart', routeChangeStart)
|
||||
Router.events.on('routeChangeComplete', routeChangeEnd)
|
||||
Router.events.on('routeChangeError', routeChangeEnd)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
Router.events.off('routeChangeStart', routeChangeStart)
|
||||
Router.events.off('routeChangeComplete', routeChangeEnd)
|
||||
Router.events.off('routeChangeError', routeChangeEnd)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div />
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
.price {
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-large);
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
.price span {
|
||||
font-weight: var(--font-weight-base);
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react'
|
||||
import Price from './Price'
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Price'
|
||||
}
|
||||
|
||||
export const Normal = () => <Price price="126479107489300000000" />
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react'
|
||||
import Web3 from 'web3'
|
||||
import styles from './Price.module.css'
|
||||
|
||||
export default function Price({
|
||||
price,
|
||||
className
|
||||
}: {
|
||||
price: string
|
||||
className?: string
|
||||
}) {
|
||||
const classes = className ? `${styles.price} ${className}` : styles.price
|
||||
const isFree = price === '0'
|
||||
const displayPrice = isFree ? (
|
||||
'Free'
|
||||
) : (
|
||||
<>
|
||||
<span>OCEAN</span> {Web3.utils.fromWei(price)}
|
||||
</>
|
||||
)
|
||||
|
||||
return <div className={classes}>{displayPrice}</div>
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
.ratings {
|
||||
display: flex;
|
||||
margin-left: calc(var(--spacer) / -8);
|
||||
font-weight: var(--font-weight-base);
|
||||
}
|
||||
|
||||
/* Handle half stars our own way */
|
||||
.ratings [style*='width:'] {
|
||||
width: 100% !important;
|
||||
clip-path: polygon(0 0, 60% 0, 60% 100%, 0% 100%);
|
||||
}
|
||||
|
||||
.ratings [style*='width:100%'],
|
||||
.ratings [style*='width: 100%'] {
|
||||
width: 100% !important;
|
||||
clip-path: none !important;
|
||||
}
|
||||
|
||||
.ratings [style*='width:0%'],
|
||||
.ratings [style*='width: 0%'] {
|
||||
width: 0 !important;
|
||||
clip-path: none !important;
|
||||
}
|
||||
|
||||
.star {
|
||||
margin-left: calc(var(--spacer) / 8);
|
||||
}
|
||||
|
||||
.star svg {
|
||||
fill: none;
|
||||
stroke: var(--color-grey);
|
||||
}
|
||||
|
||||
.full {
|
||||
composes: star;
|
||||
}
|
||||
|
||||
.full svg {
|
||||
fill: var(--color-primary);
|
||||
stroke: var(--color-primary);
|
||||
}
|
||||
|
||||
.ratingVotes {
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-small);
|
||||
padding-left: 5px;
|
||||
color: var(--color-grey);
|
||||
}
|
||||
|
||||
.readonly {
|
||||
composes: ratings;
|
||||
}
|
||||
|
||||
.readonly .full svg {
|
||||
fill: var(--color-secondary);
|
||||
stroke: var(--color-secondary);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react'
|
||||
import Rating from './Rating'
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Rating'
|
||||
}
|
||||
|
||||
export const Normal = () => (
|
||||
<Rating
|
||||
readonly
|
||||
curation={{
|
||||
rating: 3,
|
||||
numVotes: 300
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
export const WithFraction = () => (
|
||||
<Rating
|
||||
readonly
|
||||
curation={{
|
||||
rating: 3.3,
|
||||
numVotes: 300
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
export const Interactive = () => (
|
||||
<Rating
|
||||
onClick={(value: any) => null}
|
||||
curation={{
|
||||
rating: 3.3,
|
||||
numVotes: 300
|
||||
}}
|
||||
/>
|
||||
)
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react'
|
||||
import ReactRating from 'react-rating'
|
||||
import Star from '../../images/star.svg'
|
||||
import { Curation } from '@oceanprotocol/squid'
|
||||
import styles from './Rating.module.css'
|
||||
|
||||
export default function Rating({
|
||||
curation,
|
||||
readonly,
|
||||
isLoading,
|
||||
onClick
|
||||
}: {
|
||||
curation: Curation | undefined
|
||||
readonly?: boolean
|
||||
isLoading?: boolean
|
||||
onClick?: (value: any) => void
|
||||
}) {
|
||||
let numVotes = 0
|
||||
let rating = 0
|
||||
|
||||
if (!curation) return null
|
||||
;({ numVotes, rating } = curation)
|
||||
|
||||
// if it's readonly then the fraction is 10 to show the average rating proper. When you select the rating you select from 1 to 5
|
||||
const fractions = readonly ? 2 : 1
|
||||
|
||||
return (
|
||||
<div className={`${readonly ? styles.readonly : styles.ratings}`}>
|
||||
<ReactRating
|
||||
emptySymbol={
|
||||
<div className={styles.star}>
|
||||
<Star />
|
||||
</div>
|
||||
}
|
||||
fullSymbol={
|
||||
<div className={styles.full}>
|
||||
<Star />
|
||||
</div>
|
||||
}
|
||||
initialRating={rating}
|
||||
readonly={readonly || isLoading || false}
|
||||
onClick={onClick}
|
||||
fractions={fractions}
|
||||
/>
|
||||
|
||||
<span className={styles.ratingVotes}>
|
||||
{rating} {readonly ? `(${numVotes})` : ''}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.filterSection {
|
||||
margin-bottom: var(--spacer);
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react'
|
||||
import SearchFilterSection from './SearchFilterSection'
|
||||
import { Center } from '../../../.storybook/helpers'
|
||||
|
||||
export default {
|
||||
title: 'Atoms/SearchFilterSection',
|
||||
decorators: [(storyFn: any) => <Center>{storyFn()}</Center>]
|
||||
}
|
||||
|
||||
export const WithTitle = () => {
|
||||
return (
|
||||
<SearchFilterSection title="Search filter title">
|
||||
<p>Example search filter content, in this case a paragraph.</p>
|
||||
</SearchFilterSection>
|
||||
)
|
||||
}
|
||||
|
||||
export const WithoutTitle = () => {
|
||||
return (
|
||||
<SearchFilterSection>
|
||||
<p>Example search filter content, in this case a paragraph.</p>
|
||||
</SearchFilterSection>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react'
|
||||
|
||||
import styles from './SearchFilterSection.module.css'
|
||||
|
||||
const SearchFilterSection = ({
|
||||
title,
|
||||
children
|
||||
}: {
|
||||
title?: string
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.filterSection}>
|
||||
{title ? <h4>{title}</h4> : null}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchFilterSection
|
|
@ -0,0 +1,28 @@
|
|||
/* default: success, green circle */
|
||||
.status {
|
||||
width: var(--font-size-small);
|
||||
height: var(--font-size-small);
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
background: var(--color-success);
|
||||
}
|
||||
|
||||
/* yellow triangle */
|
||||
.warning {
|
||||
composes: status;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: calc(var(--font-size-small) / 1.7) solid transparent;
|
||||
border-right: calc(var(--font-size-small) / 1.7) solid transparent;
|
||||
border-bottom: var(--font-size-small) solid var(--color-warning);
|
||||
}
|
||||
|
||||
/* red square */
|
||||
.error {
|
||||
composes: status;
|
||||
border-radius: 0;
|
||||
background: var(--color-danger);
|
||||
text-transform: capitalize;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react'
|
||||
import Status from './Status'
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Status'
|
||||
}
|
||||
|
||||
export const Default = () => <Status />
|
||||
|
||||
export const Warning = () => <Status state="warning" />
|
||||
|
||||
export const Error = () => <Status state="error" />
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react'
|
||||
import styles from './Status.module.css'
|
||||
|
||||
export default function Status({ state }: { state?: string }) {
|
||||
const classes =
|
||||
state === 'error'
|
||||
? styles.error
|
||||
: state === 'warning'
|
||||
? styles.warning
|
||||
: styles.status
|
||||
|
||||
return <i className={classes} />
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.tag {
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding: 0.2rem 1.2rem 0.2rem 1.2rem;
|
||||
margin-left: calc(var(--spacer) / 16);
|
||||
margin-right: calc(var(--spacer) / 16);
|
||||
margin-bottom: calc(var(--spacer) / 8);
|
||||
text-align: center;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--color-grey-light);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tag:hover:not(span),
|
||||
.tag:focus:not(span) {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.tag:first-of-type {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.more {
|
||||
font-size: var(--font-size-mini);
|
||||
margin-left: calc(var(--spacer) / 8);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react'
|
||||
import Tags from './Tags'
|
||||
|
||||
const items = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6']
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Tags'
|
||||
}
|
||||
|
||||
export const Default = () => <Tags items={items} />
|
||||
|
||||
export const DefaultNoLinks = () => <Tags items={items} noLinks />
|
||||
|
||||
export const WithMaxItemsEq3 = () => <Tags items={items} max={3} />
|
||||
|
||||
export const WithMaxItemsEq3AndShowMore = () => (
|
||||
<Tags items={items} max={3} showMore />
|
||||
)
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import shortid from 'shortid'
|
||||
import slugify from 'slugify'
|
||||
import styles from './Tags.module.css'
|
||||
|
||||
declare type TagsProps = {
|
||||
items: string[]
|
||||
max?: number
|
||||
showMore?: boolean
|
||||
className?: string
|
||||
noLinks?: boolean
|
||||
}
|
||||
|
||||
const Tag = ({ tag, noLinks }: { tag: string; noLinks?: boolean }) => {
|
||||
// TODO: we should slugify all tags upon publish, so only
|
||||
// slug-style tags should be allowed.
|
||||
const cleanTag = slugify(tag).toLowerCase()
|
||||
|
||||
return noLinks ? (
|
||||
<span className={styles.tag}>{cleanTag}</span>
|
||||
) : (
|
||||
<Link href={`/search?tags=${tag}`}>
|
||||
<a className={styles.tag} title={cleanTag}>
|
||||
{cleanTag}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const Tags: React.FC<TagsProps> = ({
|
||||
items,
|
||||
max,
|
||||
showMore,
|
||||
className,
|
||||
noLinks
|
||||
}) => {
|
||||
max = max || items.length
|
||||
const remainder = items.length - max
|
||||
const tags = items.slice(0, max)
|
||||
const shouldShowMore = showMore && remainder > 0
|
||||
const classes = className ? `${styles.tags} ${className}` : styles.tags
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{tags &&
|
||||
tags.map(tag => (
|
||||
<Tag tag={tag} noLinks={noLinks} key={shortid.generate()} />
|
||||
))}
|
||||
{shouldShowMore && (
|
||||
<span className={styles.more}>{`+ ${items.length - max} more`}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tags
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react'
|
||||
import { format, formatDistance } from 'date-fns'
|
||||
|
||||
export default function Time({
|
||||
date,
|
||||
relative
|
||||
}: {
|
||||
date: string
|
||||
relative?: boolean
|
||||
}) {
|
||||
const dateNew = new Date(date)
|
||||
const dateIso = dateNew.toISOString()
|
||||
|
||||
return (
|
||||
<time
|
||||
title={relative ? format(dateNew, 'MMMM d, yyyy') : undefined}
|
||||
dateTime={dateIso}
|
||||
>
|
||||
{relative
|
||||
? formatDistance(dateNew, Date.now(), { addSuffix: true })
|
||||
: format(dateNew, 'MMMM d, yyyy')}
|
||||
</time>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
.teaser {
|
||||
max-width: 50rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.link {
|
||||
composes: box from '../atoms/Box.module.css';
|
||||
font-size: var(--font-size-small);
|
||||
height: 100%;
|
||||
color: var(--color-dark);
|
||||
|
||||
/* for sticking footer to bottom */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
|
||||
/* for sticking footer to bottom */
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-h4);
|
||||
color: var(--color-primary);
|
||||
margin-bottom: calc(var(--spacer) / 6);
|
||||
}
|
||||
|
||||
.tags > * {
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
.foot {
|
||||
color: var(--color-secondary);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-top: calc(var(--spacer) / 1.5);
|
||||
border-top: 1px solid var(--color-grey-light);
|
||||
padding-top: calc(var(--spacer) / 3);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.foot p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.price {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
p.copyright {
|
||||
width: 100%;
|
||||
font-weight: var(--font-weight-base);
|
||||
margin-bottom: 0;
|
||||
font-size: var(--font-size-mini);
|
||||
text-align: center;
|
||||
margin-top: calc(var(--spacer) / 3);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import AssetTeaser from '../molecules/AssetTeaser'
|
||||
import * as React from 'react'
|
||||
import { DDO } from '@oceanprotocol/squid'
|
||||
import ddo from '../../../tests/unit/__fixtures__/ddo'
|
||||
|
||||
export default {
|
||||
title: 'Molecules/Asset Teaser'
|
||||
}
|
||||
|
||||
export const Default = () => <AssetTeaser ddo={new DDO(ddo)} />
|
|
@ -0,0 +1,71 @@
|
|||
import React from 'react'
|
||||
import { DDO } from '@oceanprotocol/squid'
|
||||
import Link from 'next/link'
|
||||
import Dotdotdot from 'react-dotdotdot'
|
||||
import {
|
||||
AdditionalInformationDexFreight,
|
||||
MetaDataDexFreight
|
||||
} from '../../@types/MetaData'
|
||||
import { findServiceByType } from '../../utils'
|
||||
import Tags from '../atoms/Tags'
|
||||
import Price from '../atoms/Price'
|
||||
import styles from './AssetTeaser.module.css'
|
||||
import Rating from '../atoms/Rating'
|
||||
|
||||
declare type AssetTeaserProps = {
|
||||
ddo: Partial<DDO>
|
||||
}
|
||||
|
||||
const AssetTeaser: React.FC<AssetTeaserProps> = ({ ddo }: AssetTeaserProps) => {
|
||||
const { attributes } = findServiceByType(ddo, 'metadata')
|
||||
const { name, price } = attributes.main
|
||||
|
||||
let description
|
||||
let copyrightHolder
|
||||
let tags
|
||||
let categories
|
||||
|
||||
if (attributes && attributes.additionalInformation) {
|
||||
;({
|
||||
description,
|
||||
copyrightHolder,
|
||||
tags,
|
||||
categories
|
||||
} = attributes.additionalInformation as AdditionalInformationDexFreight)
|
||||
}
|
||||
|
||||
const { curation } = attributes as MetaDataDexFreight
|
||||
|
||||
return (
|
||||
<article className={styles.teaser}>
|
||||
<Link href="/asset/[did]" as={`/asset/${ddo.id}`}>
|
||||
<a className={styles.link}>
|
||||
<h1 className={styles.title}>{name}</h1>
|
||||
<Rating curation={curation} readonly />
|
||||
|
||||
<div className={styles.content}>
|
||||
<Dotdotdot tagName="p" clamp={3}>
|
||||
{description || ''}
|
||||
</Dotdotdot>
|
||||
|
||||
{tags && (
|
||||
<Tags className={styles.tags} items={tags} max={3} noLinks />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className={styles.foot}>
|
||||
{categories && <p className={styles.type}>{categories[0]}</p>}
|
||||
|
||||
<Price price={price} className={styles.price} />
|
||||
|
||||
<p className={styles.copyright}>
|
||||
Provided by <strong>{copyrightHolder}</strong>
|
||||
</p>
|
||||
</footer>
|
||||
</a>
|
||||
</Link>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export default AssetTeaser
|
|
@ -0,0 +1,143 @@
|
|||
.row {
|
||||
margin-bottom: var(--spacer);
|
||||
}
|
||||
|
||||
.input,
|
||||
.row input:not([type='radio']):not([type='checkbox']),
|
||||
.row select,
|
||||
.row textarea {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
/* font-weight: var(--font-weight-bold); */
|
||||
color: var(--color-dark);
|
||||
border: 1px solid var(--color-grey-light);
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
background: var(--color-white);
|
||||
padding: calc(var(--spacer) / 3);
|
||||
margin: 0;
|
||||
border-radius: var(--border-radius);
|
||||
transition: 0.2s ease-out;
|
||||
min-height: 43px;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.row input:focus:not([type='radio']):not([type='checkbox']),
|
||||
.row select:focus,
|
||||
.row textarea:focus {
|
||||
box-shadow: none;
|
||||
outline: 0;
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.input::placeholder,
|
||||
.row input::placeholder,
|
||||
.row textarea::placeholder {
|
||||
font-family: var(--font-family-base);
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-secondary);
|
||||
font-weight: var(--font-weight-base);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.input[readonly],
|
||||
.input[disabled],
|
||||
.row input[readonly],
|
||||
.row input[disabled] {
|
||||
background-color: var(--color-grey-light);
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.row textarea {
|
||||
min-height: 5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.row select {
|
||||
padding-right: 3rem;
|
||||
|
||||
/* custom arrow */
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
transparent 50%,
|
||||
var(--color-secondary) 50%
|
||||
),
|
||||
linear-gradient(135deg, var(--color-secondary) 50%, transparent 50%),
|
||||
linear-gradient(
|
||||
to right,
|
||||
var(--color-grey-light) 0px,
|
||||
var(--color-white) 1.5px
|
||||
);
|
||||
background-position: calc(100% - 18px) calc(1rem + 5px),
|
||||
calc(100% - 13px) calc(1rem + 5px), 100% 0;
|
||||
background-size: 5px 5px, 5px 5px, 2.5rem 4rem;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.checkbox label,
|
||||
.radio label,
|
||||
.row input[type='radio'] + span,
|
||||
.row input[type='checkbox'] + span {
|
||||
display: inline-block;
|
||||
font-weight: var(--font-weight-base);
|
||||
margin-bottom: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.row :global(.field-radio-group) {
|
||||
border: 1px solid var(--color-grey-light);
|
||||
padding: calc(var(--spacer) / 3);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.labelHolder {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: calc(var(--spacer) / 8);
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.req:after {
|
||||
content: '*';
|
||||
padding-left: calc(var(--spacer) / 8);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.help {
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-secondary);
|
||||
margin-top: 0.25rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.errors {
|
||||
padding-top: calc(var(--spacer) / 8);
|
||||
font-size: var(--font-size-mini);
|
||||
color: var(--color-primary);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.error input:not([type='radio']):not([type='checkbox']),
|
||||
.error select,
|
||||
.error textarea {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Size Modifiers */
|
||||
.large,
|
||||
.large::placeholder,
|
||||
.large + button {
|
||||
font-size: var(--font-size-large);
|
||||
}
|
||||
|
||||
.large {
|
||||
padding: calc(var(--spacer) / 2);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react'
|
||||
import { FieldTemplateProps } from 'react-jsonschema-form'
|
||||
import styles from './FieldTemplate.module.css'
|
||||
|
||||
const noLabelFields = ['root', 'root_termsAndConditions', 'root_files_0']
|
||||
|
||||
// Ref: https://react-jsonschema-form.readthedocs.io/en/latest/advanced-customization/#field-template
|
||||
export const FieldTemplate = ({
|
||||
id,
|
||||
label,
|
||||
rawHelp,
|
||||
required,
|
||||
rawErrors,
|
||||
children
|
||||
}: FieldTemplateProps) => {
|
||||
const noLabel = id !== noLabelFields.filter(f => id === f)[0]
|
||||
return (
|
||||
<section
|
||||
key={id}
|
||||
className={
|
||||
rawErrors !== undefined && rawErrors.length > 0
|
||||
? `${styles.row} ${styles.error}`
|
||||
: `${styles.row}`
|
||||
}
|
||||
>
|
||||
<div className={styles.labelHolder}>
|
||||
{noLabel && (
|
||||
<label
|
||||
className={
|
||||
required ? `${styles.label} ${styles.req}` : styles.label
|
||||
}
|
||||
htmlFor={id}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
|
||||
{rawErrors && <span className={styles.errors}>{rawErrors}</span>}
|
||||
{rawHelp && <div className={styles.help}>{rawHelp}</div>}
|
||||
</section>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
.info {
|
||||
border-radius: var(--border-radius);
|
||||
padding: calc(var(--spacer) / 2);
|
||||
border: 1px solid var(--color-grey-light);
|
||||
}
|
||||
|
||||
.url {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height);
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
padding-right: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.info ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info li {
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-small);
|
||||
margin-right: calc(var(--spacer) / 2);
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
position: absolute;
|
||||
top: -0.2rem;
|
||||
right: 0;
|
||||
font-size: var(--font-size-h3);
|
||||
cursor: pointer;
|
||||
color: var(--color-secondary);
|
||||
background-color: transparent;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react'
|
||||
import { File } from '@oceanprotocol/squid'
|
||||
import { prettySize } from '../../../../utils'
|
||||
import cleanupContentType from '../../../../utils/cleanupContentType'
|
||||
import styles from './Info.module.css'
|
||||
|
||||
const FileInfo = ({ info, removeItem }: { info: File; removeItem(): void }) => (
|
||||
<div className={styles.info}>
|
||||
<h3 className={styles.url}>{info.url}</h3>
|
||||
<ul>
|
||||
<li>URL confirmed</li>
|
||||
{info.contentLength && <li>{prettySize(+info.contentLength)}</li>}
|
||||
{info.contentType && <li>{cleanupContentType(info.contentType)}</li>}
|
||||
</ul>
|
||||
<button className={styles.removeButton} onClick={() => removeItem()}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default FileInfo
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react'
|
||||
import isUrl from 'is-url-superb'
|
||||
import Loader from '../../../atoms/Loader'
|
||||
import Button from '../../../atoms/Button'
|
||||
import styles from './index.module.css'
|
||||
|
||||
const FileInput = ({
|
||||
formData,
|
||||
handleButtonClick,
|
||||
isLoading,
|
||||
children,
|
||||
i
|
||||
}: {
|
||||
children: any
|
||||
i: number
|
||||
formData: string[]
|
||||
handleButtonClick(e: React.SyntheticEvent, data: string): void
|
||||
isLoading: boolean
|
||||
}) => (
|
||||
<>
|
||||
{children}
|
||||
{formData[i] && (
|
||||
<Button
|
||||
className={styles.addButton}
|
||||
onClick={(e: React.SyntheticEvent) => handleButtonClick(e, formData[i])}
|
||||
disabled={!isUrl(formData[i])}
|
||||
>
|
||||
{isLoading ? <Loader /> : 'Add File'}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
export default FileInput
|
|
@ -0,0 +1,16 @@
|
|||
.arrayField {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.arrayField > section {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
margin-top: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.error {
|
||||
border-color: var(--color-primary);
|
||||
text-transform: capitalize;
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import React, { useState } from 'react'
|
||||
import { ArrayFieldTemplateProps } from 'react-jsonschema-form'
|
||||
import { File } from '@oceanprotocol/squid'
|
||||
import { toast } from 'react-toastify'
|
||||
import useStoredValue from '../../../../hooks/useStoredValue'
|
||||
import { getFileInfo } from '../../../../utils'
|
||||
import FileInfo from './Info'
|
||||
import FileInput from './Input'
|
||||
import styles from './index.module.css'
|
||||
|
||||
const FILES_DATA_LOCAL_STORAGE_KEY = 'filesData'
|
||||
|
||||
const FileField = ({ items, formData }: ArrayFieldTemplateProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
// in order to access fileInfo as an array of objects upon formSubmit we need to keep it in localStorage
|
||||
const [fileInfo, setFileInfo] = useStoredValue<File[]>(
|
||||
FILES_DATA_LOCAL_STORAGE_KEY,
|
||||
[]
|
||||
)
|
||||
|
||||
const handleButtonClick = async (e: React.SyntheticEvent, url: string) => {
|
||||
// File example 'https://oceanprotocol.com/tech-whitepaper.pdf'
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const newFileInfo = await getFileInfo(url)
|
||||
newFileInfo && setFileInfo([newFileInfo])
|
||||
} catch (error) {
|
||||
toast.error('Could not fetch file info. Please check url and try again')
|
||||
console.error(error.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const removeItem = () => {
|
||||
setFileInfo([])
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map(({ children, key }, i) => (
|
||||
<div key={key} className={styles.arrayField}>
|
||||
{fileInfo[i] ? (
|
||||
<FileInfo info={fileInfo[i]} removeItem={removeItem} />
|
||||
) : (
|
||||
<FileInput
|
||||
formData={formData}
|
||||
handleButtonClick={handleButtonClick}
|
||||
i={i}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{children}
|
||||
</FileInput>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileField
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react'
|
||||
import { ObjectFieldTemplateProps } from 'react-jsonschema-form'
|
||||
|
||||
// Template to render form
|
||||
// https://react-jsonschema-form.readthedocs.io/en/latest/advanced-customization/#object-field-template
|
||||
const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => (
|
||||
<>
|
||||
<h3>{props.title}</h3>
|
||||
{props.properties.map(
|
||||
(element: { content: React.ReactElement }) => element.content
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
export { ObjectFieldTemplate }
|
|
@ -0,0 +1,4 @@
|
|||
.form {
|
||||
composes: box from '../../atoms/Box.module.css';
|
||||
margin-bottom: var(--spacer);
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
import React from 'react'
|
||||
import FormJsonSchema, {
|
||||
UiSchema,
|
||||
IChangeEvent,
|
||||
ISubmitEvent,
|
||||
ErrorSchema,
|
||||
AjvError
|
||||
} from 'react-jsonschema-form'
|
||||
import { JSONSchema6 } from 'json-schema'
|
||||
import Button from '../../atoms/Button'
|
||||
import styles from './index.module.css'
|
||||
|
||||
import { FieldTemplate } from './FieldTemplate'
|
||||
|
||||
import {
|
||||
customWidgets,
|
||||
PublishFormDataInterface
|
||||
} from '../../../models/PublishForm'
|
||||
// Overwrite default input fields
|
||||
/*
|
||||
AltDateTimeWidget
|
||||
AltDateWidget
|
||||
CheckboxWidget
|
||||
ColorWidget
|
||||
DateTimeWidget
|
||||
DateWidget
|
||||
EmailWidget
|
||||
FileWidget
|
||||
HiddenWidget
|
||||
RadioWidget
|
||||
RangeWidget
|
||||
SelectWidget
|
||||
CheckboxesWidget
|
||||
UpDownWidget
|
||||
TextareaWidget
|
||||
PasswordWidget
|
||||
TextWidget
|
||||
URLWidget
|
||||
*/
|
||||
|
||||
// Example of Custom Error
|
||||
// REF: react-jsonschema-form.readthedocs.io/en/latest/validation/#custom-error-messages
|
||||
export const transformErrors = (errors: AjvError[]) => {
|
||||
return errors.map((error: AjvError) => {
|
||||
if (error.property === '.termsAndConditions') {
|
||||
console.log('ERROR')
|
||||
error.message = 'Required Field'
|
||||
}
|
||||
return error
|
||||
})
|
||||
}
|
||||
|
||||
const validate = (formData: PublishFormDataInterface, errors: any) => {
|
||||
if (!formData.termsAndConditions) {
|
||||
errors.termsAndConditions.addError('Required Field')
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
export declare type FormProps = {
|
||||
buttonDisabled?: boolean
|
||||
children?: React.ReactNode
|
||||
schema: JSONSchema6
|
||||
uiSchema: UiSchema
|
||||
formData: PublishFormDataInterface
|
||||
onChange: (
|
||||
e: IChangeEvent<PublishFormDataInterface>,
|
||||
es?: ErrorSchema
|
||||
) => void
|
||||
onSubmit: (e: ISubmitEvent<PublishFormDataInterface>) => void
|
||||
onError: (e: AjvError) => void
|
||||
showErrorList?: boolean
|
||||
}
|
||||
|
||||
export default function Form({
|
||||
children,
|
||||
schema,
|
||||
uiSchema,
|
||||
formData,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onError,
|
||||
showErrorList,
|
||||
buttonDisabled
|
||||
}: FormProps) {
|
||||
return (
|
||||
<FormJsonSchema
|
||||
className={styles.form}
|
||||
schema={schema}
|
||||
formData={formData}
|
||||
uiSchema={uiSchema}
|
||||
onChange={(event: IChangeEvent<PublishFormDataInterface>) =>
|
||||
onChange(event)
|
||||
}
|
||||
onSubmit={(event: ISubmitEvent<PublishFormDataInterface>) =>
|
||||
onSubmit(event)
|
||||
}
|
||||
FieldTemplate={FieldTemplate}
|
||||
onError={onError}
|
||||
widgets={customWidgets}
|
||||
noHtml5Validate
|
||||
showErrorList={showErrorList}
|
||||
validate={validate} // REF: https://react-jsonschema-form.readthedocs.io/en/latest/validation/#custom-validation
|
||||
// liveValidate
|
||||
transformErrors={transformErrors}
|
||||
>
|
||||
<div>
|
||||
<Button disabled={buttonDisabled} primary>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
{children}
|
||||
</FormJsonSchema>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
.menu {
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-block;
|
||||
color: var(--color-secondary);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin: calc(var(--spacer) / 4) var(--spacer);
|
||||
}
|
||||
|
||||
.link:hover,
|
||||
.link:focus,
|
||||
.link:active {
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
.link.active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.link:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.link:last-child {
|
||||
margin-right: 0;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react'
|
||||
import Menu from './Menu'
|
||||
|
||||
export default {
|
||||
title: 'Molecules/Menu'
|
||||
}
|
||||
|
||||
export const Normal = () => <Menu />
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { menu } from '../../../site.config'
|
||||
import styles from './Menu.module.css'
|
||||
|
||||
declare type MenuItem = {
|
||||
name: string
|
||||
link: string
|
||||
}
|
||||
|
||||
function MenuLink({ item }: { item: MenuItem }) {
|
||||
const router = useRouter()
|
||||
const classes =
|
||||
router && router.pathname === item.link
|
||||
? `${styles.link} ${styles.active}`
|
||||
: styles.link
|
||||
|
||||
return (
|
||||
<Link key={item.name} href={item.link}>
|
||||
<a className={classes}>{item.name}</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Menu() {
|
||||
return (
|
||||
<nav className={styles.menu}>
|
||||
{menu.map((item: MenuItem) => (
|
||||
<MenuLink key={item.name} item={item} />
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
.header {
|
||||
margin-bottom: var(--spacer);
|
||||
max-width: 50rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: var(--font-size-large);
|
||||
margin-top: calc(var(--spacer) / 4);
|
||||
margin-bottom: 0;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react'
|
||||
import PageHeader from './PageHeader'
|
||||
|
||||
export default {
|
||||
title: 'Molecules/PageHeader'
|
||||
}
|
||||
|
||||
export const Normal = () => (
|
||||
<PageHeader
|
||||
title="The Cool Page Header"
|
||||
description="Blame the wizards! Hello, little man. I will destroy you! I’ve been there. My folks were always on me to groom myself and wear underpants. What am I, the pope? Leela’s gonna kill me."
|
||||
/>
|
||||
)
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react'
|
||||
import styles from './PageHeader.module.css'
|
||||
|
||||
export default function PageHeader({
|
||||
title,
|
||||
description
|
||||
}: {
|
||||
title: string
|
||||
description?: string
|
||||
}) {
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<h1 className={styles.title}>{title}</h1>
|
||||
{description && <p className={styles.description}>{description}</p>}
|
||||
</header>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
.pagination {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin-top: calc(var(--spacer) * 2);
|
||||
margin-bottom: var(--spacer);
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.number {
|
||||
text-align: center;
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 2);
|
||||
margin-left: -1px;
|
||||
margin-top: -1px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--color-grey-light);
|
||||
min-width: 3.5rem;
|
||||
}
|
||||
|
||||
li:first-child .number,
|
||||
:global(li.selected):nth-child(2) .number {
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
li:last-child .number {
|
||||
border-top-right-radius: var(--border-radius);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.number,
|
||||
.number:hover,
|
||||
.number:focus,
|
||||
.number:active {
|
||||
transform: none;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.number:hover {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.current,
|
||||
.prev,
|
||||
.next,
|
||||
.break {
|
||||
composes: number;
|
||||
}
|
||||
|
||||
.current {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.current,
|
||||
.current:hover,
|
||||
.current:focus,
|
||||
.current:active {
|
||||
color: var(--color-grey);
|
||||
}
|
||||
|
||||
.next {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.prevNextDisabled {
|
||||
opacity: 0;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react'
|
||||
import Pagination from './Pagination'
|
||||
|
||||
export default {
|
||||
title: 'Molecules/Pagination'
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
hrefBuilder: () => null as any,
|
||||
onPageChange: () => null as any
|
||||
}
|
||||
|
||||
export const Normal = () => (
|
||||
<Pagination totalPages={20} currentPage={1} {...defaultProps} />
|
||||
)
|
||||
|
||||
export const FewPages = () => (
|
||||
<Pagination totalPages={3} currentPage={1} {...defaultProps} />
|
||||
)
|
||||
|
||||
export const LotsOfPages = () => (
|
||||
<Pagination totalPages={300} currentPage={1} {...defaultProps} />
|
||||
)
|
|
@ -0,0 +1,57 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import ReactPaginate from 'react-paginate'
|
||||
import styles from './Pagination.module.css'
|
||||
|
||||
interface PaginationProps {
|
||||
totalPages: number
|
||||
currentPage: number
|
||||
onPageChange(selected: number): void
|
||||
hrefBuilder(pageIndex: number): void
|
||||
}
|
||||
|
||||
export default function Pagination({
|
||||
totalPages,
|
||||
currentPage,
|
||||
hrefBuilder,
|
||||
onPageChange
|
||||
}: PaginationProps) {
|
||||
const [smallViewport, setSmallViewport] = useState(true)
|
||||
|
||||
function viewportChange(mq: { matches: boolean }) {
|
||||
setSmallViewport(!mq.matches)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia('(min-width: 600px)')
|
||||
viewportChange(mq)
|
||||
mq.addListener(viewportChange)
|
||||
|
||||
return () => {
|
||||
mq.removeListener(viewportChange)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return totalPages > 1 ? (
|
||||
<ReactPaginate
|
||||
pageCount={totalPages}
|
||||
// react-pagination starts counting at 0, we start at 1
|
||||
initialPage={currentPage - 1}
|
||||
// adapt based on media query match
|
||||
marginPagesDisplayed={smallViewport ? 0 : 1}
|
||||
pageRangeDisplayed={smallViewport ? 3 : 6}
|
||||
onPageChange={data => onPageChange(data.selected)}
|
||||
hrefBuilder={pageIndex => hrefBuilder(pageIndex)}
|
||||
disableInitialCallback
|
||||
previousLabel="←"
|
||||
nextLabel="→"
|
||||
breakLabel="..."
|
||||
containerClassName={styles.pagination}
|
||||
pageLinkClassName={styles.number}
|
||||
activeLinkClassName={styles.current}
|
||||
previousLinkClassName={styles.prev}
|
||||
nextLinkClassName={styles.next}
|
||||
disabledClassName={styles.prevNextDisabled}
|
||||
breakLinkClassName={styles.break}
|
||||
/>
|
||||
) : null
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
.error {
|
||||
background-color: var(--color-danger);
|
||||
}
|
||||
.success {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
.info {
|
||||
background-color: var(--color-info);
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import Form from '../../molecules/Form/index'
|
||||
import {
|
||||
PublishFormSchema,
|
||||
PublishFormUiSchema,
|
||||
publishFormData,
|
||||
PublishFormDataInterface
|
||||
} from '../../../models/PublishForm'
|
||||
import useStoredValue from '../../../hooks/useStoredValue'
|
||||
import { MetaDataDexFreight } from '../../../@types/MetaData'
|
||||
import useOcean from '../../../hooks/useOcean'
|
||||
import useWeb3 from '../../../hooks/useWeb3'
|
||||
import { File, MetaData } from '@oceanprotocol/squid'
|
||||
import { isBrowser, toStringNoMS } from '../../../utils'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useRouter } from 'next/router'
|
||||
import styles from './PublishForm.module.css'
|
||||
import utils from 'web3-utils'
|
||||
import AssetModel from '../../../models/Asset'
|
||||
|
||||
declare type PublishFormProps = {}
|
||||
|
||||
const FILES_DATA_LOCAL_STORAGE_KEY = 'filesData'
|
||||
const PUBLISH_FORM_LOCAL_STORAGE_KEY = 'publishForm'
|
||||
|
||||
export function getFilesData() {
|
||||
let localFileData: File[] = []
|
||||
if (isBrowser) {
|
||||
const storedData = localStorage.getItem(FILES_DATA_LOCAL_STORAGE_KEY)
|
||||
if (storedData) {
|
||||
localFileData = localFileData.concat(JSON.parse(storedData) as File[])
|
||||
}
|
||||
}
|
||||
return localFileData
|
||||
}
|
||||
|
||||
export function clearFilesData() {
|
||||
if (isBrowser)
|
||||
localStorage.setItem(FILES_DATA_LOCAL_STORAGE_KEY, JSON.stringify([]))
|
||||
}
|
||||
|
||||
export function transformPublishFormToMetadata(
|
||||
data: PublishFormDataInterface
|
||||
): MetaDataDexFreight {
|
||||
const currentTime = toStringNoMS(new Date())
|
||||
|
||||
const {
|
||||
title,
|
||||
price,
|
||||
author,
|
||||
license,
|
||||
summary,
|
||||
category,
|
||||
holder,
|
||||
keywords,
|
||||
termsAndConditions,
|
||||
granularity,
|
||||
supportName,
|
||||
supportEmail,
|
||||
dateRange
|
||||
} = data
|
||||
|
||||
const metadata: MetaDataDexFreight = {
|
||||
main: {
|
||||
...AssetModel.main,
|
||||
name: title,
|
||||
price: utils.toWei(price.toString()),
|
||||
author,
|
||||
dateCreated: currentTime,
|
||||
datePublished: currentTime,
|
||||
files: getFilesData(),
|
||||
license
|
||||
},
|
||||
// ------- additional information -------
|
||||
additionalInformation: {
|
||||
...AssetModel.additionalInformation,
|
||||
description: summary,
|
||||
categories: [category],
|
||||
copyrightHolder: holder,
|
||||
tags: keywords?.split(','),
|
||||
termsAndConditions,
|
||||
granularity,
|
||||
supportName,
|
||||
supportEmail
|
||||
},
|
||||
// ------- curation -------
|
||||
curation: {
|
||||
...AssetModel.curation
|
||||
}
|
||||
}
|
||||
|
||||
if (dateRange) {
|
||||
const newDateRange = JSON.parse(dateRange)
|
||||
if (newDateRange.length > 1) {
|
||||
metadata.additionalInformation.dateRange = JSON.parse(dateRange)
|
||||
} else if (newDateRange.length === 1) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
metadata.main.dateCreated = newDateRange[0]
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
const PublishForm: React.FC<PublishFormProps> = () => {
|
||||
const [buttonDisabled, setButtonDisabled] = useState(false)
|
||||
const { web3, web3Connect } = useWeb3()
|
||||
const { ocean } = useOcean(web3)
|
||||
const router = useRouter()
|
||||
const [data, updateData] = useStoredValue(
|
||||
PUBLISH_FORM_LOCAL_STORAGE_KEY,
|
||||
publishFormData
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setButtonDisabled(!ocean)
|
||||
}, [ocean])
|
||||
|
||||
const handleChange = ({
|
||||
formData
|
||||
}: {
|
||||
formData: PublishFormDataInterface
|
||||
}) => {
|
||||
updateData(formData)
|
||||
}
|
||||
|
||||
const handleSubmit = async ({
|
||||
formData
|
||||
}: {
|
||||
formData: PublishFormDataInterface
|
||||
}) => {
|
||||
setButtonDisabled(true)
|
||||
const submittingToast = toast.info('submitting asset', {
|
||||
className: styles.info
|
||||
})
|
||||
|
||||
if (ocean == null) {
|
||||
await web3Connect.connect()
|
||||
}
|
||||
|
||||
if (ocean) {
|
||||
const asset = await ocean.assets.create(
|
||||
(transformPublishFormToMetadata(formData) as unknown) as MetaData,
|
||||
(await ocean.accounts.list())[0]
|
||||
)
|
||||
|
||||
// Reset the form to initial values
|
||||
|
||||
updateData(publishFormData)
|
||||
clearFilesData()
|
||||
setButtonDisabled(false)
|
||||
// User feedback and redirect
|
||||
toast.success('asset created successfully', {
|
||||
className: styles.success
|
||||
})
|
||||
toast.dismiss(submittingToast)
|
||||
router.push(`/asset/${asset.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = () =>
|
||||
toast.error('Please check form. There are some errors', {
|
||||
className: styles.error
|
||||
})
|
||||
|
||||
return (
|
||||
<Form
|
||||
schema={PublishFormSchema}
|
||||
uiSchema={PublishFormUiSchema}
|
||||
formData={data}
|
||||
onChange={handleChange}
|
||||
onSubmit={handleSubmit}
|
||||
onError={handleError}
|
||||
showErrorList={false}
|
||||
buttonDisabled={buttonDisabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default PublishForm
|