commit f356f3ba293ef05243aed9e17b0ed2d80484c0c1 Author: Matthias Kretschmann Date: Wed Oct 16 17:02:04 2019 +0200 initial commit 🏄‍♀️ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08fee36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +npm-debug.log +.DS_Store +dist +package-lock.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..121d245 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "none", + "tabWidth": 2 +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2354bce --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# IPFS + +> 🏄‍♀️ Ocean Protocol's public IPFS Node, setup to be a public gateway, and to provide some access to its HTTP API for everyone. +> [ipfs.oceanprotocol.com](https://ipfs.oceanprotocol.com) diff --git a/config.js b/config.js new file mode 100644 index 0000000..4a03b66 --- /dev/null +++ b/config.js @@ -0,0 +1,24 @@ +export const title = 'Ocean Protocol 💖 IPFS' +export const description = `Ocean Protocol's public IPFS Node, setup to be a public gateway, and to provide some access to its HTTP API for everyone.
Learn More →` + +export const ipfsGateway = 'https://ipfs.oceanprotocol.com' +export const ipfsNodeUri = 'https://ipfs.oceanprotocol.com:443' + +export const links = [ + { + name: 'Homepage', + url: 'https://oceanprotocol.com' + }, + { + name: 'Blog', + url: 'https://blog.oceanprotocol.com' + }, + { + name: 'Twitter', + url: 'https://twitter.com/oceanprotocol' + }, + { + name: 'GitHub', + url: 'https://github.com/oceanprotocol' + } +] diff --git a/package.json b/package.json new file mode 100644 index 0000000..c2ab98e --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "@oceanprotocol/ipfs", + "version": "1.0.0", + "description": "Ocean Protocol's Public IPFS Node.", + "scripts": { + "start": "webpack-dev-server", + "build": "NODE_ENV=production webpack" + }, + "author": "Matthias Kretschmann ", + "license": "Apache-2.0", + "dependencies": { + "@oceanprotocol/art": "^2.2.0", + "@oceanprotocol/typographies": "^0.1.0", + "ipfs-http-client": "^38.2.0", + "react-dropzone": "^10.1.10" + }, + "devDependencies": { + "@babel/core": "^7.6.4", + "@babel/plugin-proposal-class-properties": "^7.5.5", + "@babel/polyfill": "^7.6.0", + "@babel/preset-env": "^7.6.3", + "@babel/preset-react": "^7.6.3", + "@svgr/webpack": "^4.3.3", + "babel-loader": "^8.0.6", + "copy-webpack-plugin": "^5.0.4", + "css-loader": "^3.2.0", + "eslint": "^6.5.1", + "eslint-config-oceanprotocol": "^1.5.0", + "eslint-config-prettier": "^6.4.0", + "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-react": "^7.16.0", + "mini-css-extract-plugin": "^0.8.0", + "prettier": "^1.18.2", + "react": "^16.10.2", + "react-dom": "^16.10.2", + "style-loader": "^1.0.0", + "webpack": "^4.41.2", + "webpack-cli": "^3.3.9", + "webpack-dev-server": "^3.8.2" + }, + "eslintConfig": { + "extends": [ + "oceanprotocol", + "oceanprotocol/react", + "prettier/react", + "prettier/standard", + "plugin:prettier/recommended" + ], + "plugins": [ + "prettier" + ] + } +} diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..5f155bc --- /dev/null +++ b/src/App.css @@ -0,0 +1,46 @@ +@import 'styles/_variables.css'; + +.app { + padding: var(--spacer); + background: var(--brand-black); + height: 100%; + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +@media screen and (min-width: 640px) { + .app { + height: auto; + min-height: calc(100vh - (var(--page-frame) * 2) - (var(--spacer) * 2)); + } +} + +.header { + width: 100%; + margin-top: var(--spacer); +} + +.appTitle { + margin-top: var(--spacer); + font-size: var(--font-size-h2); +} + +@media screen and (min-width: 640px) { + .appTitle { + font-size: var(--font-size-h1); + } +} + +.appDescription { + font-size: var(--font-size-large); + color: var(--brand-grey-lighter); + max-width: 40rem; + margin-left: auto; + margin-right: auto; +} + +.logo { + width: 80px; + height: 80px; +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..a31dad7 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,25 @@ +import '@babel/polyfill' +import React from 'react' +import Add from './components/Add' +import Logo from '@oceanprotocol/art/logo/logo-white.svg' +import './styles/global.css' +import './App.css' +import { title, description } from '../config' +import Footer from './components/Footer' + +export default function App() { + return ( +
+
+ +

{title}

+

+

+ +
+
+ ) +} diff --git a/src/components/Add.css b/src/components/Add.css new file mode 100644 index 0000000..572e711 --- /dev/null +++ b/src/components/Add.css @@ -0,0 +1,4 @@ +.add { + max-width: 40rem; + width: 100%; +} diff --git a/src/components/Add.jsx b/src/components/Add.jsx new file mode 100644 index 0000000..a129379 --- /dev/null +++ b/src/components/Add.jsx @@ -0,0 +1,38 @@ +import React, { useState } from 'react' +import { saveToIpfs } from '../ipfs' +import { ipfsGateway } from '../../config' +import Dropzone from './Dropzone' +import './Add.css' +import Spinner from './Spinner' + +export default function Add() { + const [fileHash, setFileHash] = useState(null) + const [loading, setLoading] = useState(false) + + const handleCaptureFile = async files => { + setLoading(true) + const cid = await saveToIpfs(files) + setFileHash(cid) + setLoading(false) + } + + return ( +
+ {loading ? ( + + ) : fileHash ? ( +
+ + {fileHash} + +
+ ) : ( + + )} +
+ ) +} diff --git a/src/components/Dropzone.css b/src/components/Dropzone.css new file mode 100644 index 0000000..7e2ef48 --- /dev/null +++ b/src/components/Dropzone.css @@ -0,0 +1,31 @@ +@import '../styles/_variables.css'; + +.dropzone { + border: 0.2rem dashed var(--brand-grey-light); + border-radius: calc(var(--border-radius) * 2); + padding: calc(var(--spacer) * 2); + transition: 0.2s ease-out; + cursor: pointer; +} + +.dropzone:hover, +.dropzone:focus, +.dropzone:active { + border-color: var(--brand-grey-lighter); + outline: 0; +} + +.dropzone.dragover { + border-color: var(--brand-pink); +} + +.dropzone.disabled { + opacity: 0.5; + pointer-events: none; +} + +.dropzone p { + text-align: center; + margin-bottom: 0; + color: var(--brand-grey-light); +} diff --git a/src/components/Dropzone.jsx b/src/components/Dropzone.jsx new file mode 100644 index 0000000..6121e0f --- /dev/null +++ b/src/components/Dropzone.jsx @@ -0,0 +1,45 @@ +import React, { useCallback } from 'react' +import PropTypes from 'prop-types' +import { useDropzone } from 'react-dropzone' +import './Dropzone.css' + +Dropzone.propTypes = { + handleOnDrop: PropTypes.func.isRequired, + disabled: PropTypes.bool, + multiple: PropTypes.bool +} + +export default function Dropzone({ handleOnDrop, disabled, multiple }) { + const onDrop = useCallback(acceptedFiles => handleOnDrop(acceptedFiles), [ + handleOnDrop + ]) + + const { + getRootProps, + getInputProps, + isDragActive, + isDragReject + } = useDropzone({ onDrop }) + + return ( +
+ +

+ {isDragActive && !isDragReject + ? `Drop it like it's hot!` + : multiple + ? `Drag 'n' drop some files here, or click to select files` + : `Drag 'n' drop a file here, or click to select a file`} + {} +

+
+ ) +} diff --git a/src/components/Footer.css b/src/components/Footer.css new file mode 100644 index 0000000..996ccf9 --- /dev/null +++ b/src/components/Footer.css @@ -0,0 +1,31 @@ +@import '../styles/_variables.css'; + +.footer { + width: 100%; + font-size: var(--font-size-mini); + padding: var(--spacer) 0; + padding-bottom: 0; + display: flex; + justify-content: space-between; + align-self: flex-end; + text-align: left; + margin-top: var(--spacer); +} + +.footer, +.footer a { + color: var(--brand-grey-light); +} + +.footer > div { + flex: 1 1 48%; +} + +.footer > div:last-child { + text-align: right; +} + +.footer > div:last-child a { + display: inline-block; + margin-left: var(--spacer); +} diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx new file mode 100644 index 0000000..410b0d7 --- /dev/null +++ b/src/components/Footer.jsx @@ -0,0 +1,22 @@ +import React from 'react' +import { links } from '../../config' +import './Footer.css' + +export default function Footer() { + return ( + + ) +} diff --git a/src/components/Spinner.css b/src/components/Spinner.css new file mode 100644 index 0000000..c8eafe3 --- /dev/null +++ b/src/components/Spinner.css @@ -0,0 +1,36 @@ +@import '../styles/_variables.css'; + +.spinner { + position: relative; + text-align: center; + margin-top: calc(var(--spacer) * var(--line-height)); + margin-bottom: calc(var(--spacer) / 2); + line-height: 1.3; +} + +.spinner:before { + content: ''; + box-sizing: border-box; + position: absolute; + top: 0; + left: 50%; + width: 20px; + height: 20px; + margin-top: -20px; + margin-left: -10px; + border-radius: 50%; + border: 2px solid var(--brand-purple); + border-top-color: var(--brand-violet); + animation: spinner 0.6s linear infinite; +} + +.spinnerMessage { + color: var(--brand-grey-light); + padding-top: calc(var(--spacer) / 4); +} + +@keyframes spinner { + to { + transform: rotate(360deg); + } +} diff --git a/src/components/Spinner.jsx b/src/components/Spinner.jsx new file mode 100644 index 0000000..0c08a95 --- /dev/null +++ b/src/components/Spinner.jsx @@ -0,0 +1,6 @@ +import React from 'react' +import './Spinner.css' + +export default function Spinner() { + return
+} diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..8edb07c --- /dev/null +++ b/src/index.html @@ -0,0 +1,13 @@ + + + + + + + Ocean Protocol 💖 IPFS + + +
+ + + diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..b3d0515 --- /dev/null +++ b/src/index.js @@ -0,0 +1,5 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App' + +ReactDOM.render(, document.getElementById('root')) diff --git a/src/ipfs.js b/src/ipfs.js new file mode 100644 index 0000000..0004735 --- /dev/null +++ b/src/ipfs.js @@ -0,0 +1,35 @@ +import ipfsClient from 'ipfs-http-client' +import { ipfsNodeUri } from '../config' + +export async function saveToIpfs(files) { + const { hostname, port, protocol } = new URL(ipfsNodeUri) + + const ipfsConfig = { + protocol: protocol.replace(':', ''), + host: hostname, + port: port || '443' + } + + const ipfs = ipfsClient(ipfsConfig) + + const file = [...files][0] + let ipfsId + const fileDetails = { + path: file.name, + content: file + } + const options = { + wrapWithDirectory: true, + progress: prog => console.log(`received: ${prog}`) + } + + try { + const response = await ipfs.add(fileDetails, options) + + // CID of wrapping directory is returned last + ipfsId = `${response[response.length - 1].hash}/${fileDetails.path}` + return ipfsId + } catch (error) { + console.error(error.message) + } +} diff --git a/src/styles/_variables.css b/src/styles/_variables.css new file mode 100644 index 0000000..1067ee2 --- /dev/null +++ b/src/styles/_variables.css @@ -0,0 +1,51 @@ +:root { + --brand-white: #fff; + --brand-black: #141414; + --brand-pink: #ff4092; + --brand-purple: #7b1173; + --brand-violet: #e000cf; + --brand-blue: #11597b; + + --brand-grey: #41474e; + --brand-grey-light: #8b98a9; + --brand-grey-dark: #303030; + --brand-grey-lighter: #e2e2e2; + + --green: #5fb359; + --red: #d80606; + --orange: #b35f36; + --yellow: #eac146; + + --font-family-base: 'Sharp Sans', -apple-system, BlinkMacSystemFont, + 'Segoe UI', Helvetica, Arial, sans-serif; + --font-family-title: 'Sharp Sans Display', -apple-system, BlinkMacSystemFont, + 'Segoe UI', Helvetica, Arial, sans-serif; + --font-family-monospace: 'Fira Code', 'Fira Mono', Menlo, Monaco, Consolas, + 'Courier New', monospace; + + --font-size-root: 16px; + --font-size-base: 1rem; + --font-size-large: 1.2rem; + --font-size-small: 0.85rem; + --font-size-mini: 0.65rem; + --font-size-text: $font-size-base; + --font-size-label: $font-size-base; + --font-size-title: 1.4rem; + --font-size-h1: 3rem; + --font-size-h2: 1.7rem; + --font-size-h3: 1.4rem; + --font-size-h4: $font-size-large; + --font-size-h5: 1.1rem; + --font-weight-base: 500; + --font-weight-bold: 600; + --line-height: 1.6; + + --spacer: 2rem; + --page-frame: 0.75rem; + + --break-point--small: 640px; + --break-point--medium: 860px; + --break-point--large: 1140px; + --break-point--huge: 1400px; + --border-radius: 0.2rem; +} diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..530846a --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,90 @@ +@import '../../node_modules/@oceanprotocol/typographies/css/ocean-typo.css'; +@import '_variables.css'; + +html, +body, +#root { + height: 100%; + max-height: calc(100vh - (var(--page-frame) * 2)); +} + +html { + height: 100%; + font-size: var(--font-size-root); +} + +body { + color: var(--brand-grey-light); + font-size: var(--font-size-base); + font-family: var(--font-family-base); + font-weight: var(--font-weight-base); + line-height: var(--line-height); + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + position: relative; + margin: 0; + text-align: center; +} + +@media screen and (min-width: 640px) { + body { + padding: var(--page-frame); + } +} + +a { + text-decoration: none; + color: var(--brand-pink); + transition: 0.2s ease-out; +} + +p { + margin: 0; + margin-bottom: calc(var(--spacer) / var(--line-height)); +} + +h1, +h2, +h3, +h4, +h5 { + font-family: var(--font-family-title); + color: var(--brand-white); + line-height: 1.2; + font-weight: var(--font-weight-bold); +} + +h1 { + font-size: var(--font-size-h1); +} + +h2 { + font-size: var(--font-size-h2); +} + +h3 { + font-size: var(--font-size-h3); +} + +h4 { + font-size: var(--font-size-h4); +} + +h5 { + font-size: var(--font-size-h5); +} + +figure, +img, +svg, +video, +audio, +embed, +canvas, +picture { + max-width: 100%; + height: auto; + margin: 0 auto; + display: block; +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..3eedeb6 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,68 @@ +const path = require('path') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const CopyPlugin = require('copy-webpack-plugin') + +const defaultInclude = [path.resolve(__dirname, 'src')] +const isDevelopment = process.env.NODE_ENV !== 'production' + +module.exports = { + mode: isDevelopment ? 'development' : 'production', + entry: path.resolve(__dirname, 'src', 'index.js'), + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'bundle.js' + }, + module: { + rules: [ + { + test: /\.(js|jsx)$/, + include: defaultInclude, + use: [ + { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-env', '@babel/preset-react'], + plugins: ['@babel/plugin-proposal-class-properties'] + } + } + ] + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + include: defaultInclude + }, + { + test: /\.(jpe?g|png|gif)$/, + use: ['file-loader?name=img/[name]__[hash:base64:5].[ext]'], + include: defaultInclude + }, + { + test: /\.svg$/, + use: [ + { + loader: '@svgr/webpack', + options: { + icon: true + } + } + ] + } + ] + }, + resolve: { + extensions: ['*', '.js', '.jsx'] + }, + node: { + fs: 'empty', + net: 'empty', + tls: 'empty' + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: isDevelopment ? '[name].css' : '[name].[hash].css', + chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css' + }), + new CopyPlugin([{ from: './src/index.html', to: './', flatten: true }]) + ] +}