first commit

This commit is contained in:
alexcos20 2020-05-07 09:03:30 +03:00
parent cf99b06222
commit ebd5ba9495
205 changed files with 34917 additions and 1 deletions

26
.env.example Normal file
View File

@ -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'

33
.eslintrc Normal file
View File

@ -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"
}
}
]
}

1
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1 @@
* @maxieprotocol @kremalicious @pfmescher @unjapones

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
node_modules
out
.DS_Store
.next
.idea
.env
.env.build
coverage
storybook-static
public/storybook
.artifacts

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"tabWidth": 2
}

16
.storybook/helpers.tsx Normal file
View File

@ -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>
)

7
.storybook/main.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
stories: [
'../src/components/**/*.stories.tsx',
'../src/styles/**/*.stories.tsx'
],
addons: []
}

27
.storybook/preview.js Normal file
View File

@ -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>
))

View File

@ -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
}

37
.travis.yml Normal file
View File

@ -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

201
LICENSE Normal file
View File

@ -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
View File

@ -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.
```

64
content/terms.md Normal file
View File

@ -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 Providers Confidential Information of the restrictions contained in this Agreement. Each Recipient shall limit access to the Providers 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 Providers 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 Users 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 Providers 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 Users 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.

12
jest.tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"jsx": "react",
"allowJs": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"noImplicitAny": true,
"sourceMap": true,
"target": "es5"
}
}

2
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />

109
next.config.js Normal file
View File

@ -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
}
})
)
)
)
)

23
now.json Normal file
View File

@ -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"
}
}

27025
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

94
package.json Normal file
View File

@ -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"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/icons/icon-48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
public/icons/icon-72x72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
public/icons/icon-96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

31
scripts/keeper.sh Normal file
View File

@ -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'

31
site.config.js Normal file
View File

@ -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'
}
]
}

34
src/@types/MetaData.d.ts vendored Normal file
View File

@ -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
}

11
src/@types/global.d.ts vendored Normal file
View File

@ -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'

22
src/Layout.module.css Normal file
View File

@ -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;
}

40
src/Layout.tsx Normal file
View File

@ -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>
)
}

View File

@ -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);
}

View File

@ -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" />
)

View File

@ -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>
)
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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

View File

@ -0,0 +1,3 @@
.label {
cursor: pointer;
}

View File

@ -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"
/>
)

View File

@ -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

View File

@ -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;
}

View File

@ -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>
)
}

View File

@ -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);
}

View File

@ -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}
/>
)

View File

@ -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>
</>
)
}

View File

@ -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';
}

View File

@ -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>
</>
)
}

View File

@ -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;
}

View File

@ -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>
)

View File

@ -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>
)
}

View File

@ -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);
}
}

View File

@ -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 />
)

View File

@ -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>
)
}

View File

@ -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

View File

@ -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);
}

View File

@ -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 />
}

View File

@ -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);
}

View File

@ -0,0 +1,8 @@
import React from 'react'
import Price from './Price'
export default {
title: 'Atoms/Price'
}
export const Normal = () => <Price price="126479107489300000000" />

View File

@ -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>
}

View File

@ -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);
}

View File

@ -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
}}
/>
)

View File

@ -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>
)
}

View File

@ -0,0 +1,3 @@
.filterSection {
margin-bottom: var(--spacer);
}

View File

@ -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>
)
}

View File

@ -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

View File

@ -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;
}

View File

@ -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" />

View File

@ -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} />
}

View File

@ -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);
}

View File

@ -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 />
)

View File

@ -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

View File

@ -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>
)
}

View File

@ -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);
}

View File

@ -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)} />

View File

@ -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

View File

@ -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);
}

View File

@ -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>
)
}

View File

@ -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;
}

View File

@ -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()}>
&times;
</button>
</div>
)
export default FileInfo

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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 }

View File

@ -0,0 +1,4 @@
.form {
composes: box from '../../atoms/Box.module.css';
margin-bottom: var(--spacer);
}

View File

@ -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>
)
}

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
import React from 'react'
import Menu from './Menu'
export default {
title: 'Molecules/Menu'
}
export const Normal = () => <Menu />

View File

@ -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>
)
}

View File

@ -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;
}

View File

@ -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! Ive been there. My folks were always on me to groom myself and wear underpants. What am I, the pope? Leelas gonna kill me."
/>
)

View File

@ -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>
)
}

View File

@ -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;
}

View File

@ -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} />
)

View File

@ -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
}

View File

@ -0,0 +1,9 @@
.error {
background-color: var(--color-danger);
}
.success {
background-color: var(--color-success);
}
.info {
background-color: var(--color-info);
}

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More