├── .gitignore ├── Dockerfile ├── backend ├── daemon.js ├── log.js ├── options.js ├── routes.js └── updater.js ├── frontend ├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin │ └── webpack-dev-server.js ├── build │ ├── karma.js │ ├── webpack-dev-server.js │ └── webpack │ │ ├── _base.js │ │ ├── development.js │ │ ├── development_hot.js │ │ └── production.js ├── config │ └── index.js ├── karma.conf.js ├── karma.entry.js ├── package.json ├── src │ ├── components │ │ ├── .keep │ │ ├── nodes.js │ │ └── nodes_stats.js │ ├── containers │ │ └── Root.js │ ├── history.js │ ├── index.html │ ├── index.js │ ├── layouts │ │ └── CoreLayout.js │ ├── reducers │ │ ├── counter.js │ │ └── index.js │ ├── routes │ │ └── index.js │ ├── stores │ │ └── index.js │ ├── styles │ │ ├── _base.scss │ │ ├── content.scss │ │ ├── core.scss │ │ ├── navigation.scss │ │ ├── nodes.scss │ │ ├── notes.scss │ │ ├── vendor │ │ │ └── _normalize.scss │ │ └── wrapper.scss │ ├── utils │ │ └── index.js │ └── views │ │ ├── AboutView.js │ │ ├── HomeView.js │ │ ├── HomeView.spec.js │ │ ├── Navigation.js │ │ ├── NodesAddView.js │ │ ├── NodesView.js │ │ ├── PinShowView.js │ │ └── PinView.js └── webpack.config.js ├── package.json ├── pin_content_in_cluster.sh ├── readme.md ├── server.js ├── start_cluster.sh └── stop_cluster.sh /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | saved_daemons.json 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:4.2.1 2 | 3 | WORKDIR /src 4 | RUN npm install -g npm@latest 5 | ADD package.json package.json 6 | RUN mkdir -p /src/frontend 7 | ADD frontend/package.json frontend/package.json 8 | 9 | RUN apk add --update make gcc g++ python git 10 | 11 | RUN npm install; 12 | RUN cd frontend;npm install; 13 | #RUN cd frontend;SKIP_SASS_BINARY_DOWNLOAD_FOR_CI=true npm install node-sass@beta 14 | 15 | ADD . . 16 | EXPOSE 3000 17 | CMD STATIC=true npm start 18 | -------------------------------------------------------------------------------- /backend/daemon.js: -------------------------------------------------------------------------------- 1 | var ipfsAPI = require('ipfs-api') 2 | var Options = require('./options') 3 | 4 | var Daemon = function(multiaddr) { 5 | if(multiaddr === undefined) { 6 | throw new Error('You need to define a multiaddr!') 7 | } 8 | this.multiaddr = multiaddr 9 | this.id = "NotYetFound" 10 | this.ipfs = ipfsAPI(this.multiaddr) 11 | this.alive = false 12 | this.tries = 0 13 | // TODO hashes => model with state machine 14 | this.pinned = [] // already pinned 15 | this.pinning = [] // while pinning 16 | this.to_pin = [] // to be pinned 17 | } 18 | 19 | Daemon.prototype = { 20 | pin_unpinned_hashes: function() { 21 | if(this.to_pin.length > 0) { 22 | this.to_pin.forEach((hash_to_pin) => { 23 | const index_of_hash = this.to_pin.indexOf(hash_to_pin) 24 | const hash_currently_pinning = this.to_pin.splice(index_of_hash, 1)[0] 25 | 26 | this.pinning.push(hash_currently_pinning); 27 | 28 | this.ipfs.pin.add(hash_to_pin, (err, res) => { 29 | if(err !== null) { 30 | throw err 31 | } 32 | if(err === null && res.Pinned) { 33 | var pinned_hash = res.Pinned[0] 34 | const index_of_pinned_hash = this.pinning.indexOf(pinned_hash) 35 | this.pinning.splice(index_of_pinned_hash, 1) 36 | this.pinned.push(pinned_hash) 37 | } 38 | }) 39 | }) 40 | } 41 | }, 42 | is_alive: function(callback) { 43 | this.ipfs.id(function(err, res) { 44 | if(err) { 45 | this.alive = false 46 | this.tries++ 47 | callback(this.alive, "NotYetFound") 48 | return 49 | } 50 | if(res.ID !== undefined) { 51 | this.alive = true 52 | this.id = res.ID 53 | this.tries = 0 54 | callback(this.alive, this.id) 55 | return 56 | } 57 | }) 58 | } 59 | } 60 | 61 | module.exports = Daemon 62 | -------------------------------------------------------------------------------- /backend/log.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'); 2 | 3 | var log = new (winston.Logger)({ 4 | transports: [ 5 | new (winston.transports.Console)({ 6 | colorize: true, 7 | timestamp: true 8 | }) 9 | ] 10 | }); 11 | 12 | module.exports = log 13 | -------------------------------------------------------------------------------- /backend/options.js: -------------------------------------------------------------------------------- 1 | var Options = { 2 | daemon_api_calls_timeout: 5000, 3 | daemon_alive_retries: 5, 4 | daemon_check_interval: 5000 5 | } 6 | 7 | module.exports = Options 8 | -------------------------------------------------------------------------------- /backend/routes.js: -------------------------------------------------------------------------------- 1 | var Daemon = require('./daemon') 2 | var Log = require('./log') 3 | var _ = require('underscore') 4 | 5 | module.exports = function(Updater) { 6 | return [ 7 | { 8 | method: 'post', 9 | path: '/daemons', 10 | func: (req, res) => { 11 | const body = req.body 12 | var new_daemon = new Daemon(body.multiaddr) 13 | Updater.daemons.push(new_daemon) 14 | // TODO send back id? 15 | res.send(new_daemon) 16 | } 17 | }, 18 | { 19 | method: 'get', 20 | path: '/daemons', 21 | func: (req, res) => { 22 | const daemons = Updater.daemons.map((daemon) => { 23 | return daemon 24 | }) 25 | res.send(Updater.daemons) 26 | } 27 | }, 28 | { 29 | method: 'post', 30 | path: '/pin/:hash', 31 | func: (req, res) => { 32 | const hash = req.params.hash 33 | Log.info('Gonna pin ' + hash + ' on all hosts') 34 | Log.info('Number of daemons to pin on: ' + Updater.daemons.length) 35 | Updater.daemons = Updater.daemons.map((daemon) => { 36 | if(daemon.pinned.indexOf(hash) === -1) { 37 | daemon.to_pin = _.uniq(daemon.to_pin.concat([hash])) 38 | daemon.pinned = _.uniq(daemon.pinned) 39 | } 40 | return daemon; 41 | }) 42 | res.send(JSON.stringify(true)) 43 | } 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /backend/updater.js: -------------------------------------------------------------------------------- 1 | var Options = require('./options') 2 | var Log = require('./log') 3 | var _ = require('lodash') 4 | var fs = require('fs') 5 | var Daemon = require('./daemon') 6 | 7 | var initial_daemons = []; 8 | try { 9 | initial_daemons = JSON.parse(fs.readFileSync('saved_daemons.json')) 10 | initial_daemons = initial_daemons.map((daemon) => { 11 | var created_daemon = new Daemon(daemon.multiaddr) 12 | created_daemon.id = daemon.id 13 | created_daemon.alive = daemon.alive 14 | created_daemon.tries = daemon.tries 15 | created_daemon.pinned = daemon.pinned 16 | //TODO find a way to restore pinning 17 | created_daemon.pinning = [] 18 | created_daemon.to_pin = daemon.to_pin 19 | return created_daemon 20 | }) 21 | } catch(err) { 22 | if(err.code === 'ENOENT') { 23 | Log.info('### No saved deamons found! Starting from the beginning...') 24 | fs.writeFileSync('saved_daemons.json', JSON.stringify(initial_daemons)) 25 | } else { 26 | throw err 27 | } 28 | } 29 | 30 | var Updater = { 31 | interval: null, 32 | daemons: initial_daemons, 33 | addDaemons: function(daemons) { 34 | this.daemons = this.daemons.concat(daemons) 35 | this.daemons = _.uniq(this.daemons, 'multiaddr'); 36 | fs.writeFileSync('saved_daemons.json', JSON.stringify(this.daemons)) 37 | }, 38 | start: function() { 39 | this.interval = setInterval(() => { 40 | Log.info('Checking if daemons are alive') 41 | this.daemons.forEach((daemon) => { 42 | if(daemon.tries >= Options.daemon_alive_retries) { 43 | this.daemons = _.reject(this.daemons, (d) => { 44 | return d.multiaddr === daemon.multiaddr 45 | }) 46 | } 47 | daemon.is_alive((alive, id) => { 48 | Log.info(daemon.id + ' is alive? ' + alive) 49 | daemon.alive = alive 50 | daemon.id = id 51 | if(alive) { 52 | daemon.tries = 0 53 | daemon.pin_unpinned_hashes() 54 | } else { 55 | daemon.tries = daemon.tries + 1 56 | } 57 | fs.writeFileSync('saved_daemons.json', JSON.stringify(this.daemons)) 58 | }) 59 | }) 60 | }, Options.daemon_check_interval) 61 | }, 62 | stop: function() { 63 | clearInterval(this.interval) 64 | } 65 | } 66 | 67 | module.exports = Updater 68 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage" : 0 3 | } 4 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | # Indentation style 8 | # Possible values - tab, space 9 | indent_style = space 10 | 11 | # Indentation size in single-spaced characters 12 | # Possible values - an integer, tab 13 | indent_size = 2 14 | 15 | # Line ending file format 16 | # Possible values - lf, crlf, cr 17 | end_of_line = lf 18 | 19 | # File character encoding 20 | # Possible values - latin1, utf-8, utf-16be, utf-16le 21 | charset = utf-8 22 | 23 | # Denotes whether to trim whitespace at the end of lines 24 | # Possible values - true, false 25 | trim_trailing_whitespace = true 26 | 27 | # Denotes whether file should end with a newline 28 | # Possible values - true, false 29 | insert_final_newline = true 30 | -------------------------------------------------------------------------------- /frontend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "parser": "babel-eslint", 7 | "ecmaFeatures": { 8 | "classes": true, 9 | "jsx": true 10 | }, 11 | "globals": { 12 | "__DEV__" : false, 13 | "__PROD__" : false, 14 | "__DEBUG__" : false, 15 | "__DEBUG_NW__" : false 16 | }, 17 | "rules": { 18 | "semi": 0, 19 | "indent": 0, 20 | "space-before-function-paren": 0, 21 | "no-extra-semi": 1, 22 | "space-after-keywords": 0, 23 | "jsx-quotes": 1, 24 | "react/display-name": 0, 25 | "react/forbid-prop-types": 0, 26 | "react/jsx-boolean-value": 1, 27 | "react/jsx-closing-bracket-location": 1, 28 | "react/jsx-curly-spacing": 1, 29 | "react/jsx-indent-props": 0, 30 | "react/jsx-no-literals": 0, 31 | "react/jsx-max-props-per-line": 1, 32 | "react/jsx-no-duplicate-props": 1, 33 | "react/jsx-no-undef": 1, 34 | "react/jsx-sort-prop-types": 1, 35 | "react/jsx-sort-props": 1, 36 | "react/jsx-uses-react": 1, 37 | "react/jsx-uses-vars": 1, 38 | "react/no-danger": 1, 39 | "react/no-did-mount-set-state": 1, 40 | "react/no-did-update-set-state": 1, 41 | "react/no-direct-mutation-state": 1, 42 | "react/no-multi-comp": 1, 43 | "react/no-set-state": 1, 44 | "react/no-unknown-property": 1, 45 | "react/prefer-es6-class": 1, 46 | "react/prop-types": 1, 47 | "react/react-in-jsx-scope": 1, 48 | "react/require-extension": 1, 49 | "react/self-closing-comp": 1, 50 | "react/sort-comp": 1, 51 | "react/wrap-multilines": 0 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.log 3 | 4 | node_modules 5 | 6 | dist 7 | -------------------------------------------------------------------------------- /frontend/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.0" 4 | 5 | install: 6 | - npm install 7 | 8 | script: 9 | - NODE_ENV=production npm run deploy 10 | -------------------------------------------------------------------------------- /frontend/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.16.0 5 | ------ 6 | 7 | ### Features 8 | * Adds redux-router (thanks to [dougvk](https://github.com/dougvk)) 9 | * Adds redux-thunk middleware 10 | * Adds loaders for font files (thanks to [nodkz](https://github.com/nodkz)) 11 | * Adds url loader 12 | * Upgrades React dependencies to stable `^0.14.0` 13 | * Upgrades react-redux to `^4.0.0` 14 | 15 | ### Improvements 16 | * Cleans up unused configuration settings 17 | * configureStore no longer relies on a global variable to determine whether or not to enable devtool middleware 18 | * Removes unused invariant and ImmutableJS vendor dependencies 19 | * Removes unused webpack-clean plugin 20 | * Tweaks .js loader configuration to make it easier to add json-loader 21 | * Updates counter example to demonstrate `mapDispatchToProps` 22 | * Force `components` directory inclusion 23 | * Documentation improvements 24 | 25 | 0.15.2 26 | ------ 27 | 28 | ### Fixes 29 | * Remove unused/broken "minify" property provided to HtmlWebpackPlugin configuration. 30 | 31 | 0.15.1 32 | ------ 33 | 34 | ### Fixes 35 | * Dev server now loads the correct Webpack configuration with HMR enabled. 36 | * Redbox-React error catcher is now loaded correctly in development. 37 | 38 | 0.15.0 39 | ------ 40 | 41 | ### Fixes 42 | * HMR is now longer enabled for simple compilations. You can now compile development builds that won't constantly ping a non-existent dev server. 43 | * react-transform-hmr now only runs when HMR is enabled. 44 | 45 | ### Improvements 46 | * Unit tests now only run in watch mode when explicitly requested. This makes it much more convenient to run tests on any environment without having to struggle with the `singleRun` flag in Karma. 47 | * There is now only a single webpack configuration (rather than one for the client and one for the server). As a result, configuration has once again been split into a base configuration which is then extended based on the current `NODE_ENV`. 48 | 49 | ### Deprecations 50 | * Removed Koa server (sad days). 51 | 52 | 0.14.0 53 | ------ 54 | 55 | #### Features 56 | * Replaces `react-transform-webpack-hmr` with its replacement `react-transform-hmr`. Thanks to [daviferreira](https://github.com/daviferreira). 57 | * Replaces `delicate-error-reporter` with `redbox-react`. Thanks to [bulby97](https://github.com/bulby97). 58 | * Created a `no-server` branch [here](https://github.com/davezuko/react-redux-starter-kit/tree/no-server) to make it easier for users who don't care about Koa. 59 | 60 | #### Improvements 61 | * Renames `client` directory to `src` to be more intuitive. 62 | * `inline-source-map` has been replaced by `source-map` as the default webpack devTool to reduce build sizes. 63 | * Refactors configuration file to focus on commonly-configured options rather than mixing them with internal configuration. 64 | * Swaps `dev` and `dev:debug` so debug tools are now enabled by default and can be disabled instead with `dev:no-debug`. 65 | * Repositions Redux devtools so they no longer block errors displayed by `redbox-react`. 66 | * Adds explicit directory references to some `import` statements to clarify which are from from `npm` and which are local. 67 | 68 | #### Fixes 69 | * Fixes naming in `HomeView` where `mapStateToProps` was incorrectly written as `mapDispatchToProps`. 70 | 71 | #### Deprecations 72 | * Removes local test utilities (in `~/src/utils/test`). 73 | 74 | 0.13.0 75 | ------ 76 | 77 | #### Features 78 | * Adds `react-transform-catch-errors` along with `delicate-error-reporter`. Thanks [bulby97](https://github.com/bulby97) for this! 79 | 80 | #### Fixes 81 | * ExtractTextPlugin is once again production only. This fixes an issue where styles wouldn't be hot reloaded with Webpack. 82 | 83 | 0.12.0 84 | ------ 85 | 86 | #### Features 87 | * Upgrades react-router to `^3.0.0`. This is the only reason for the minor-level version bump. 88 | * Webpack now uses OccurrenceOrderPlugin to produce consistent bundle hashes. 89 | 90 | #### Fixes 91 | * Adds `history` to vendor dependencies to fix HMR caused by upgrade to react-router `1.0.0-rc` 92 | 93 | #### Improvements 94 | * Server no longer modifies initial counter state by default. 95 | * Adds invariant error in route rendering method to enforce router state definition through props. 96 | 97 | 0.11.0 98 | ------ 99 | 100 | #### Features 101 | * Upgrades all React dependencies to `0.14.0-rc1` 102 | * Upgrades react-router to `1.0.0-rc` 103 | * Updates client and server rendering accordingly 104 | * Adds Sinon-Chai for improved assertions and function spies 105 | * Adds option to disable eslint when in development 106 | 107 | #### Improvements 108 | * Improved example unit tests using react-addons-test-utils and Sinon Chai 109 | 110 | 0.10.0 111 | ------ 112 | 113 | #### Features 114 | * Initial state can now be injected from the server (still WIP). 115 | * Adds react-addons-test-utils as a devDependency. 116 | 117 | #### Improvements 118 | * Eslint no longer prevents webpack from bundling in development mode if an error is emitted. 119 | * See: https://github.com/MoOx/eslint-loader/issues/23 120 | * Updates all `.jsx` files to `.js`. (https://github.com/davezuko/react-redux-starter-kit/issues/37) 121 | * Updates all React component file names to be ProperCased. 122 | 123 | 0.9.0 124 | ----- 125 | 126 | #### Features 127 | * Koa server now uses gzip middleware. 128 | 129 | #### Improvements 130 | * Switches out react-hot-loader in favor of [react-transform-webpack-hmr](https://github.com/gaearon/react-transform-webpack-hmr). 131 | * Eslint configuration now uses Airbnb's configuration (slightly softened). 132 | * Migrates all actual development dependencies to devDependencies in `package.json`. 133 | * Example store and view are now more intuitive (simple counter display). 134 | * CSS-loader dependency upgraded from `0.16.0` to `0.17.0`. 135 | 136 | #### Deprecations 137 | * Removes unnecessary object-assign dependency. 138 | 139 | 0.8.0 140 | ----- 141 | 142 | #### Improvements 143 | * All build-, server-, and client-related code is now ES6. 144 | * Significantly refactors how client and server webpack configs are built. 145 | * `reducers/index.js` now exports combined root reducer. 146 | * Client application code now lives in `~/client` instead of `~/src` in order to conform to Redux standards. 147 | 148 | #### Fixes 149 | * Redux store now explicitly handles HMR. 150 | 151 | #### Changes 152 | * Webpack compiler configurations are no longer merged on top of a base default configuration. This can become unwieldy and even though explicitly writing each configuration file out is more verbose, it ends up being more maintainable. 153 | 154 | #### Deprecations 155 | * Quiet mode has been removed (`npm run dev:quiet`). 156 | 157 | 0.7.0 158 | ----- 159 | #### New Features 160 | * Support for redux-devtools in separate window with `dev:debugnw` 161 | - Thanks to [mlusetti](https://github.com/mlusetti) 162 | 163 | #### Improvements 164 | * Upgrades react to `0.14.0-beta3` 165 | * Upgrades react to `0.14.0-beta3` 166 | * Upgrades redux to `^2.0.0` 167 | * Upgrades redux-devtools to `^2.0.0` 168 | * Upgrades react-redux to `^2.0.0` 169 | 170 | #### Fixes 171 | * Configuration file name trimming on Windows machines 172 | - Thanks to [nuragic](https://github.com/nuragic) 173 | 174 | 0.6.0 175 | ----- 176 | 177 | #### Fixes 178 | * Fixes potential spacing issues when Webpack tries to load a config file. 179 | - Thanks to [nuragic](https://github.com/nuragic) for his [PR](https://github.com/davezuko/react-redux-starter-kit/pull/32) 180 | 181 | #### Improvements 182 | * Upgrades koa to `1.0.0` 183 | * Upgrades react-redux to `1.0.0` 184 | * Upgrades object-assign to `0.4.0` 185 | 186 | 0.5.0 187 | ----- 188 | 189 | #### Improvements 190 | * Restructures src directory so filenames are more identifiable. 191 | 192 | #### Breaking Changes 193 | * Removes action-creators alias as it's unlikely to be used. 194 | 195 | 0.4.0 196 | ----- 197 | 198 | #### Improvements 199 | * Cleans up/removes example code per https://github.com/davezuko/react-redux-starter-kit/issues/20 200 | 201 | 0.3.1 202 | ----- 203 | 204 | #### Fixes 205 | * https://github.com/davezuko/react-redux-starter-kit/issues/19 206 | - Invalid initialStates from server-side router will now yield to the next middleware. 207 | 208 | 0.3.0 209 | ----- 210 | 211 | #### Improvements 212 | * Bumps Redux version to first major release. 213 | * Bumps Redux-devtools version to first major release. 214 | 215 | #### Fixes 216 | * Fixes broken hot-reload in `:debug` mode. 217 | - Temporarily fixed by moving `redux-devtools` into the vendor bundle. 218 | 219 | 0.2.0 220 | ----- 221 | 222 | #### Improvements 223 | * Weakens various eslint rules that were too opinionated. 224 | - notable: `one-var` and `key-spacing`. 225 | 226 | Thanks to [StevenLangbroek](https://github.com/StevenLangbroek) for the following: 227 | * Adds alias `utils` to reference `~/src/utils` 228 | * Adds `createConstants` utility. 229 | * Adds `createReducer` utility. 230 | * Refactors `todos` reducer to use a function map rather than switch statements. 231 | 232 | #### Fixes 233 | * Nested routes are now loaded correctly in react-router when using BrowserHistory. 234 | * Bundle compilation now fails if an eslint error is encountered when running a production build. 235 | - Thanks [clearjs](https://github.com/clearjs) 236 | * Upgrades all outdated dependencies. 237 | - Karma, eslint, babel, sass-loader, and a handful more. 238 | -------------------------------------------------------------------------------- /frontend/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 David Zukowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | React Redux Starter Kit 2 | ======================= 3 | 4 | [![Join the chat at https://gitter.im/davezuko/react-redux-starter-kit](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/davezuko/react-redux-starter-kit?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | [![Build Status](https://travis-ci.org/davezuko/react-redux-starter-kit.svg?branch=master)](https://travis-ci.org/davezuko/react-redux-starter-kit?branch=master) 6 | [![dependencies](https://david-dm.org/davezuko/react-redux-starter-kit.svg)](https://david-dm.org/davezuko/react-redux-starter-kit) 7 | 8 | Starter kit to get you up and running with a bunch of awesome new front-end technologies, all on top of a configurable, feature-rich Webpack build system that's already setup to provide unit testing, linting, hot reloading, sass imports with CSS extraction, and a whole lot more. Check out the full feature list below! 9 | 10 | Redux, React-Router, and React are constantly releasing new API changes. If you'd like to help keep this boilerplate up to date, please contribute or create a new issue if you think this starter kit is missing something! 11 | 12 | **Where'd the server go?**. This starter kit used to come packaged with a Koa server to perform basic server-side rendering. However, despite that, universal rendering remained secondary to the goal of this starter kit, so it made more sense to remove that aspect; after all, do one thing and do it well. 13 | 14 | Table of Contents 15 | ----------------- 16 | 1. [Requirements](#requirements) 17 | 1. [Features](#features) 18 | 1. [Getting Started](#getting-started) 19 | 1. [Usage](#usage) 20 | 1. [Structure](#structure) 21 | 1. [Webpack](#webpack) 22 | 1. [Styles](#styles) 23 | 1. [Testing](#testing) 24 | 1. [Utilities](#utilities) 25 | 1. [Troubleshooting](#troubleshooting) 26 | 27 | Requirements 28 | ------------ 29 | 30 | Node `^4.0.0` 31 | 32 | Features 33 | -------- 34 | 35 | * [React](https://github.com/facebook/react) (`^0.14.0`) 36 | * Includes react-addons-test-utils (`^0.14.0`) 37 | * [React-Router](https://github.com/rackt/react-router) (`1.0.0-rc1`) 38 | * [Redux](https://github.com/gaearon/redux) (`^3.0.0`) 39 | * redux-router (`^1.0.0-beta3`) 40 | * react-redux (`^4.0.0`) 41 | * redux-devtools 42 | * use `npm run dev:nw` to display in a separate window. 43 | * redux-thunk middleware 44 | * [Karma](https://github.com/karma-runner/karma) 45 | * Mocha w/ Chai and Sinon-Chai 46 | * PhantomJS 47 | * [Babel](https://github.com/babel/babel) 48 | * `react-transform-hmr` for hot reloading 49 | * `react-transform-catch-errors` with `redbox-react` for more visible error reporting 50 | * Uses babel runtime rather than inline transformations 51 | * [Webpack](https://github.com/webpack/webpack) 52 | * Splits app code from vendor dependencies 53 | * webpack-dev-server 54 | * sass-loader with CSS extraction 55 | * eslint-loader 56 | * Uses [Airbnb's eslint config](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb) (with some softened rules) 57 | * Configured to fail production builds on error 58 | * Pre-configured folder aliases and globals 59 | 60 | Getting Started 61 | --------------- 62 | 63 | Just clone the repo and install the necessary node modules: 64 | 65 | ```shell 66 | $ git clone https://github.com/davezuko/react-redux-starter-kit.git ReduxStarterApp 67 | $ cd ReduxStarterApp 68 | $ npm install # Install Node modules listed in ./package.json (may take a while the first time) 69 | $ npm start # Compile and launch 70 | ``` 71 | 72 | Usage 73 | ----- 74 | 75 | #### `npm run dev` also `npm start` 76 | Runs the webpack build system just like in `compile` but enables HMR. The webpack dev server can be found at `localhost:3000`. 77 | 78 | #### `npm run dev:nw` 79 | Same as `npm run dev` but opens the debug tools in a new window. 80 | 81 | **Note:** you'll need to allow popups in Chrome, or you'll see an error: [issue 110](https://github.com/davezuko/react-redux-starter-kit/issues/110) 82 | 83 | #### `npm run dev:no-debug` 84 | Same as `npm run dev` but disables devtools. 85 | 86 | #### `npm run compile` 87 | Runs the Webpack build system with your current NODE_ENV and compiles the application to disk (`~/dist`). Production builds will fail on eslint errors (but not on warnings). 88 | 89 | #### `npm run test` 90 | Runs unit tests with Karma. 91 | 92 | #### `npm run test:dev` 93 | Same as `npm run test`, but will watch for changes and re-run tests. 94 | 95 | #### `npm run deploy` 96 | Helper script to run tests and then, on success, compile your application. 97 | 98 | ### Configuration 99 | 100 | Basic project configuration can be found in `~/config/index.js`. Here you'll be able to redefine your src and dist directories, as well as tweak what ports Webpack and WebpackDevServer run on. 101 | 102 | Structure 103 | --------- 104 | 105 | The folder structure provided is only meant to serve as a guide, it is by no means prescriptive. It is something that has worked very well for me and my team, but use only what makes sense to you. 106 | 107 | ``` 108 | . 109 | ├── bin # Build/Start scripts 110 | ├── build # All build-related configuration 111 | │ ├── webpack # Environment-specific configuration files for Webpack 112 | ├── config # Project configuration settings 113 | └── src # Application source code 114 | ├── components # Generic React Components (generally Dumb components) 115 | ├── containers # Components that provide context (e.g. Redux Providers) 116 | ├── layouts # Components that dictate major page structure 117 | ├── reducers # Redux reducers 118 | ├── routes # Application route definitions 119 | ├── stores # Redux store configuration 120 | ├── utils # Generic utilities 121 | ├── views # Components that live at a route 122 | └── index.js # Application bootstrap and rendering 123 | ``` 124 | 125 | ### Components vs. Views vs. Layouts 126 | 127 | **TL;DR:** They're all components. 128 | 129 | This distinction may not be important for you, but as an explanation: A **Layout** is something that describes an entire page structure, such as a fixed navigation, viewport, sidebar, and footer. Most applications will probably only have one layout, but keeping these components separate makes their intent clear. **Views** are components that live at routes, and are generally rendered within a **Layout**. What this ends up meaning is that, with this structure, nearly everything inside of **Components** ends up being a dumb component. 130 | 131 | Webpack 132 | ------- 133 | 134 | ### Configuration 135 | The webpack compiler configuration is located in `~/build/webpack`. When the webpack dev server runs, only the client compiler will be used. When webpack itself is run to compile to disk, both the client and server configurations will be used. Settings that are bundle agnostic should be defined in `~/config/index.js` and imported where needed. 136 | 137 | ### Vendor Bundle 138 | You can redefine which packages to treat as vendor dependencies by editing `vendor_dependencies` in `~/config/index.js`. These default to: 139 | 140 | ```js 141 | [ 142 | 'history', 143 | 'react', 144 | 'react-redux', 145 | 'react-router', 146 | 'redux-router', 147 | 'redux', 148 | 'redux-devtools', 149 | 'redux-devtools/lib/react' 150 | ] 151 | ``` 152 | 153 | ### Aliases 154 | As mentioned in features, the default Webpack configuration provides some globals and aliases to make your life easier. These can be used as such: 155 | 156 | ```js 157 | import MyComponent from '../../components/my-component'; // without alias 158 | import MyComponent from 'components/my-component'; // with alias 159 | 160 | // Available aliases: 161 | actions => '~/src/actions' 162 | components => '~/src/components' 163 | constants => '~/src/constants' 164 | containers => '~/src/containers' 165 | layouts => '~/src/layouts' 166 | reducers => '~/src/reducers' 167 | routes => '~/src/routes' 168 | services => '~/src/services' 169 | styles => '~/src/styles' 170 | utils => '~/src/utils' 171 | views => '~/src/views' 172 | ``` 173 | 174 | ### Globals 175 | 176 | #### `__DEV__` 177 | True when `process.env.NODE_ENV` is `development` 178 | 179 | #### `__PROD__` 180 | True when `process.env.NODE_ENV` is `production` 181 | 182 | #### `__DEBUG__` 183 | True when the compiler is run with `--debug` (any environment). 184 | 185 | Styles 186 | ------ 187 | 188 | All `.scss` imports will be run through the sass-loader, extracted during production builds, and ignored during server builds. If you're requiring styles from a base styles directory (useful for generic, app-wide styles) in your JS, you can make use of the `styles` alias, e.g.: 189 | 190 | ```js 191 | // ~/src/components/some/nested/component/index.jsx 192 | import `styles/core.scss`; 193 | ``` 194 | 195 | Furthermore, this `styles` directory is aliased for sass imports, which further eliminates manual directory traversing. An example nested `.scss` file: 196 | 197 | ```scss 198 | // current path: ~/src/styles/some/nested/style.scss 199 | // what used to be this: 200 | @import '../../base'; 201 | 202 | // can now be this: 203 | @import 'base'; 204 | ``` 205 | 206 | Testing 207 | ------- 208 | 209 | To add a unit test, simply create `.spec.js` file anywhere in `~/src`. The entry point for Karma uses webpack's custom require to load all these files, and both Mocha and Chai will be available to you within your test without the need to import them. 210 | 211 | Utilities 212 | --------- 213 | 214 | This boilerplate comes with two simple utilities (thanks to [StevenLangbroek](https://github.com/StevenLangbroek)) to help speed up your Redux development process. In `~/client/utils` you'll find exports for `createConstants` and `createReducer`. The former is pretty much an even lazier `keyMirror`, so if you _really_ hate typing out those constants you may want to give it a shot. Check it out: 215 | 216 | ```js 217 | import { createConstants } from 'utils'; 218 | 219 | export default createConstants( 220 | 'TODO_CREATE', 221 | 'TODO_DESTROY', 222 | 'TODO_TOGGLE_COMPLETE' 223 | ); 224 | ``` 225 | 226 | The other utility, `create-reducer`, is designed to expedite creating reducers when they're defined via an object map rather than switch statements. As an example, what once looked like this: 227 | 228 | ```js 229 | import { TODO_CREATE } from 'constants/todo'; 230 | 231 | const initialState = []; 232 | const handlers = { 233 | [TODO_CREATE] : (state, payload) => { ... } 234 | }; 235 | 236 | export default function todo (state = initialState, action) { 237 | const handler = handlers[action.type]; 238 | 239 | return handler ? handler(state, action.payload) : state; 240 | } 241 | ``` 242 | 243 | Can now look like this: 244 | 245 | ```js 246 | import { TODO_CREATE } from 'constants/todo'; 247 | import { createReducer } from 'utils'; 248 | 249 | const initialState = []; 250 | 251 | export default createReducer(initialState, { 252 | [TODO_CREATE] : (state, payload) => { ... } 253 | }); 254 | ``` 255 | 256 | Troubleshooting 257 | --------------- 258 | 259 | Nothing yet. Having an issue? Report it and I'll get to it as soon as possible! 260 | -------------------------------------------------------------------------------- /frontend/bin/webpack-dev-server.js: -------------------------------------------------------------------------------- 1 | require('babel/register'); 2 | 3 | const devServer = require('../build/webpack-dev-server'), 4 | config = require('../config'); 5 | 6 | const port = config.get('webpack_port'); 7 | devServer.listen(port, 'localhost', function () { 8 | console.log('Webpack dev server running at localhost:' + port); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/build/karma.js: -------------------------------------------------------------------------------- 1 | import { argv } from 'yargs'; 2 | import config from '../config'; 3 | import webpackConfig from '../webpack.config'; 4 | 5 | const globals = config.get('globals'); 6 | const KARMA_ENTRY_FILE = 'karma.entry.js'; 7 | 8 | function makeDefaultConfig () { 9 | return { 10 | files : [ 11 | './node_modules/phantomjs-polyfill/bind-polyfill.js', 12 | './' + KARMA_ENTRY_FILE 13 | ], 14 | singleRun : !argv.watch, 15 | frameworks : ['mocha', 'sinon-chai'], 16 | preprocessors : { 17 | [KARMA_ENTRY_FILE] : ['webpack'], 18 | [`${config.get('dir_src')}/**/*.js`] : ['webpack'] 19 | }, 20 | reporters : ['spec'], 21 | browsers : ['PhantomJS'], 22 | webpack : { 23 | devtool : 'inline-source-map', 24 | resolve : webpackConfig.resolve, 25 | plugins : webpackConfig.plugins 26 | .filter(p => !p.__KARMA_IGNORE__), 27 | module : { 28 | loaders : webpackConfig.module.loaders 29 | } 30 | }, 31 | webpackMiddleware : { 32 | noInfo : true 33 | }, 34 | plugins : [ 35 | require('karma-webpack'), 36 | require('karma-mocha'), 37 | require('karma-sinon-chai'), 38 | require('karma-coverage'), 39 | require('karma-phantomjs-launcher'), 40 | require('karma-spec-reporter') 41 | ] 42 | }; 43 | } 44 | 45 | export default (karmaConfig) => karmaConfig.set(makeDefaultConfig()); 46 | -------------------------------------------------------------------------------- /frontend/build/webpack-dev-server.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import WebpackDevServer from 'webpack-dev-server'; 3 | import config from '../config'; 4 | import webpackConfig from './webpack/development_hot'; 5 | 6 | const paths = config.get('utils_paths'); 7 | 8 | const server = new WebpackDevServer(webpack(webpackConfig), { 9 | contentBase : paths.project(config.get('dir_src')), 10 | hot : true, 11 | quiet : false, 12 | noInfo : false, 13 | lazy : false, 14 | stats : { 15 | colors : true 16 | }, 17 | historyApiFallback : true 18 | }); 19 | 20 | export default server; 21 | -------------------------------------------------------------------------------- /frontend/build/webpack/_base.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import config from '../../config'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | 5 | const paths = config.get('utils_paths'); 6 | 7 | const webpackConfig = { 8 | name : 'client', 9 | target : 'web', 10 | entry : { 11 | app : [ 12 | paths.project(config.get('dir_src')) 13 | ], 14 | vendor : config.get('vendor_dependencies') 15 | }, 16 | output : { 17 | filename : '[name].[hash].js', 18 | path : paths.project(config.get('dir_dist')), 19 | publicPath : '/' 20 | }, 21 | plugins : [ 22 | new webpack.DefinePlugin(config.get('globals')), 23 | new webpack.optimize.OccurrenceOrderPlugin(), 24 | new webpack.DefinePlugin({ 25 | 'process.env': Object.keys(process.env).reduce(function(o, k) { 26 | o[k] = JSON.stringify(process.env[k]); 27 | return o; 28 | }, {}) 29 | }), 30 | new webpack.optimize.DedupePlugin(), 31 | new HtmlWebpackPlugin({ 32 | template : paths.src('index.html'), 33 | hash : true, 34 | filename : 'index.html', 35 | inject : 'body' 36 | }), 37 | new webpack.ProvidePlugin({ 38 | 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' 39 | }) 40 | ], 41 | resolve : { 42 | extensions : ['', '.js', '.jsx'], 43 | alias : config.get('utils_aliases') 44 | }, 45 | module : { 46 | preLoaders : [ 47 | { 48 | test : /\.(js|jsx)$/, 49 | loaders : ['eslint-loader'], 50 | exclude : /node_modules/ 51 | } 52 | ], 53 | loaders : [ 54 | { 55 | test : /\.(js|jsx)$/, 56 | exclude : /node_modules/, 57 | loader : 'babel', 58 | query : { 59 | stage : 0, 60 | optional : ['runtime'], 61 | env : { 62 | development : { 63 | plugins : ['react-transform'], 64 | extra : { 65 | 'react-transform' : { 66 | transforms : [{ 67 | transform : 'react-transform-catch-errors', 68 | imports : ['react', 'redbox-react'] 69 | }] 70 | } 71 | } 72 | } 73 | } 74 | } 75 | }, 76 | { 77 | test : /\.scss$/, 78 | loaders : [ 79 | 'style-loader', 80 | 'css-loader', 81 | 'autoprefixer?browsers=last 2 version', 82 | 'sass-loader?includePaths[]=' + paths.src('styles') 83 | ] 84 | }, 85 | { test: /\.woff(\?.*)?$/, loader: "url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff" }, 86 | { test: /\.woff2(\?.*)?$/, loader: "url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff2" }, 87 | { test: /\.ttf(\?.*)?$/, loader: "url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/octet-stream" }, 88 | { test: /\.eot(\?.*)?$/, loader: "file-loader?prefix=fonts/&name=[path][name].[ext]" }, 89 | { test: /\.svg(\?.*)?$/, loader: "url-loader?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=image/svg+xml" } 90 | ] 91 | }, 92 | eslint : { 93 | configFile : paths.project('.eslintrc') 94 | } 95 | }; 96 | 97 | // NOTE: this is a temporary workaround. I don't know how to get Karma 98 | // to include the vendor bundle that webpack creates, so to get around that 99 | // we remove the bundle splitting when webpack is used with Karma. 100 | const commonChunkPlugin = new webpack.optimize.CommonsChunkPlugin( 101 | 'vendor', '[name].[hash].js' 102 | ); 103 | commonChunkPlugin.__KARMA_IGNORE__ = true; 104 | webpackConfig.plugins.push(commonChunkPlugin); 105 | 106 | export default webpackConfig; 107 | -------------------------------------------------------------------------------- /frontend/build/webpack/development.js: -------------------------------------------------------------------------------- 1 | import webpackConfig from './_base'; 2 | 3 | webpackConfig.devtool = 'source-map'; 4 | webpackConfig.eslint.emitWarning = true; 5 | 6 | export default webpackConfig; 7 | -------------------------------------------------------------------------------- /frontend/build/webpack/development_hot.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import config from '../../config'; 3 | import webpackConfig from './development'; 4 | 5 | webpackConfig.entry.app.push( 6 | `webpack-dev-server/client?${config.get('webpack_public_path')}`, 7 | `webpack/hot/dev-server` 8 | ); 9 | 10 | webpackConfig.plugins.push( 11 | new webpack.HotModuleReplacementPlugin(), 12 | new webpack.NoErrorsPlugin() 13 | ); 14 | 15 | // We need to apply the react-transform HMR plugin to the Babel configuration, 16 | // but _only_ when HMR is enabled. Putting this in the default development 17 | // configuration will break other tasks such as test:unit because Webpack 18 | // HMR is not enabled there, and these transforms require it. 19 | webpackConfig.module.loaders = webpackConfig.module.loaders.map(loader => { 20 | if (/js(?!on)/.test(loader.test)) { 21 | loader.query.env.development.extra['react-transform'].transforms.push({ 22 | transform : 'react-transform-hmr', 23 | imports : ['react'], 24 | locals : ['module'] 25 | }); 26 | } 27 | 28 | return loader; 29 | }); 30 | 31 | export default webpackConfig; 32 | -------------------------------------------------------------------------------- /frontend/build/webpack/production.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 3 | import webpackConfig from './_base'; 4 | 5 | webpackConfig.module.loaders = webpackConfig.module.loaders.map(loader => { 6 | if (/css/.test(loader.test)) { 7 | const [first, ...rest] = loader.loaders; 8 | 9 | loader.loader = ExtractTextPlugin.extract(first, rest.join('!')); 10 | delete loader.loaders; 11 | } 12 | 13 | return loader; 14 | }); 15 | 16 | webpackConfig.plugins.push( 17 | new ExtractTextPlugin('[name].[contenthash].css'), 18 | new webpack.optimize.UglifyJsPlugin({ 19 | compress : { 20 | 'unused' : true, 21 | 'dead_code' : true 22 | } 23 | }) 24 | ); 25 | 26 | webpackConfig.eslint.failOnError = true; 27 | 28 | export default webpackConfig; 29 | -------------------------------------------------------------------------------- /frontend/config/index.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = (process.env.NODE_ENV || 'development').trim(); 2 | 3 | import path from 'path'; 4 | import { argv } from 'yargs'; 5 | 6 | const config = new Map(); 7 | 8 | // ------------------------------------ 9 | // User Configuration 10 | // ------------------------------------ 11 | // NOTE: Due to limitations with Webpack's custom require, which is used for 12 | // looking up all *.spec.js files, if you edit dir_src you must also edit 13 | // the path in ~/karma.entry.js. 14 | config.set('dir_src', 'src'); 15 | config.set('dir_dist', 'dist'); 16 | 17 | config.set('webpack_host', 'localhost'); 18 | config.set('webpack_port', process.env.PORT || 3001); 19 | 20 | config.set('vendor_dependencies', [ 21 | 'history', 22 | 'react', 23 | 'react-redux', 24 | 'react-router', 25 | 'redux', 26 | 'redux-router', 27 | 'redux-devtools', 28 | 'redux-devtools/lib/react' 29 | ]); 30 | 31 | /* ********************************************* 32 | ------------------------------------------------- 33 | 34 | All Internal Configuration Below 35 | Edit at Your Own Risk 36 | 37 | ------------------------------------------------- 38 | ************************************************/ 39 | // ------------------------------------ 40 | // Environment 41 | // ------------------------------------ 42 | config.set('env', process.env.NODE_ENV); 43 | config.set('api_path', process.env.API_PATH); 44 | config.set('globals', { 45 | 'process.env' : { 46 | 'NODE_ENV' : JSON.stringify(config.get('env')), 47 | 'API_PATH' : JSON.stringify(config.get('api_path')) 48 | }, 49 | 'NODE_ENV' : config.get('env'), 50 | '__DEV__' : config.get('env') === 'development', 51 | '__PROD__' : config.get('env') === 'production', 52 | '__DEBUG__' : config.get('env') === 'development' && !argv.no_debug, 53 | '__DEBUG_NW__' : !!argv.nw 54 | }); 55 | 56 | // ------------------------------------ 57 | // Webpack 58 | // ------------------------------------ 59 | config.set('webpack_public_path', 60 | `http://${config.get('webpack_host')}:${config.get('webpack_port')}/` 61 | ); 62 | 63 | // ------------------------------------ 64 | // Project 65 | // ------------------------------------ 66 | config.set('path_project', path.resolve(__dirname, '../')); 67 | 68 | // ------------------------------------ 69 | // Utilities 70 | // ------------------------------------ 71 | const paths = (() => { 72 | const base = [config.get('path_project')], 73 | resolve = path.resolve; 74 | 75 | const project = (...args) => resolve.apply(resolve, [...base, ...args]); 76 | 77 | return { 78 | project : project, 79 | src : project.bind(null, config.get('dir_src')), 80 | dist : project.bind(null, config.get('dir_dist')) 81 | }; 82 | })(); 83 | 84 | config.set('utils_paths', paths); 85 | config.set('utils_aliases', [ 86 | 'actions', 87 | 'components', 88 | 'constants', 89 | 'containers', 90 | 'layouts', 91 | 'reducers', 92 | 'routes', 93 | 'services', 94 | 'styles', 95 | 'utils', 96 | 'views' 97 | ].reduce((acc, x) => ((acc[x] = paths.src(x)) && acc), {})); 98 | 99 | export default config; 100 | -------------------------------------------------------------------------------- /frontend/karma.conf.js: -------------------------------------------------------------------------------- 1 | require('babel/register'); 2 | module.exports = require('./build/karma'); 3 | -------------------------------------------------------------------------------- /frontend/karma.entry.js: -------------------------------------------------------------------------------- 1 | // Require all ".spec.js" files in ~/src. 2 | var context = require.context('./src', true, /.+\.spec\.js$/); 3 | context.keys().forEach(context); 4 | module.exports = context; 5 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-starter-kit", 3 | "version": "0.16.0", 4 | "description": "Get started with React, Redux, and React-Router!", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf dist", 8 | "compile": "webpack", 9 | "start": "npm run dev", 10 | "dev": "node --harmony bin/webpack-dev-server", 11 | "dev:nw": "npm run dev -- --nw", 12 | "dev:no-debug": "npm run dev -- --no_debug", 13 | "test": "node ./node_modules/karma/bin/karma start", 14 | "test:dev": "npm run test -- --watch", 15 | "deploy": "npm run test && npm run compile" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/davezuko/react-redux-starter-kit.git" 20 | }, 21 | "author": "David Zukowski (http://zuko.me)", 22 | "license": "MIT", 23 | "dependencies": { 24 | "babel": "^5.8.23", 25 | "babel-eslint": "^4.1.3", 26 | "eslint": "^1.7.3", 27 | "eslint-config-standard": "^4.4.0", 28 | "eslint-plugin-react": "^3.6.3", 29 | "eslint-plugin-standard": "^1.3.1", 30 | "exports-loader": "^0.6.2", 31 | "history": "^1.9.0", 32 | "imports-loader": "^0.6.5", 33 | "react": "^0.14.0", 34 | "react-dom": "^0.14.0", 35 | "react-redux": "^4.0.0", 36 | "react-router": "1.0.0-rc1", 37 | "redux": "^3.0.0", 38 | "redux-router": "^1.0.0-beta3", 39 | "redux-thunk": "^1.0.0", 40 | "sass-loader": "^3.1.0", 41 | "whatwg-fetch": "^0.10.0", 42 | "yargs": "^3.18.0" 43 | }, 44 | "devDependencies": { 45 | "autoprefixer": "^6.0.3", 46 | "autoprefixer-loader": "^3.1.0", 47 | "babel-loader": "^5.0.0", 48 | "babel-plugin-react-transform": "^1.1.0", 49 | "babel-runtime": "^5.8.20", 50 | "css-loader": "^0.19.0", 51 | "eslint-config-airbnb": "0.0.8", 52 | "eslint-loader": "^1.0.0", 53 | "extract-text-webpack-plugin": "^0.8.0", 54 | "file-loader": "^0.8.4", 55 | "html-webpack-plugin": "^1.6.1", 56 | "karma": "^0.13.8", 57 | "karma-coverage": "^0.5.0", 58 | "karma-mocha": "^0.2.0", 59 | "karma-phantomjs-launcher": "^0.2.1", 60 | "karma-sinon-chai": "^1.0.0", 61 | "karma-spec-reporter": "0.0.20", 62 | "karma-webpack": "^1.7.0", 63 | "mocha": "^2.2.5", 64 | "node-sass": "^3.3.3", 65 | "phantomjs": "^1.9.17", 66 | "phantomjs-polyfill": "0.0.1", 67 | "react-addons-test-utils": "^0.14.0", 68 | "react-transform-catch-errors": "^0.1.2", 69 | "react-transform-hmr": "^1.0.0", 70 | "redbox-react": "^1.0.4", 71 | "redux-devtools": "tomatau/redux-devtools#e126f799bb81a61e4a2ce69fbc501e09b15b5f5d", 72 | "style-loader": "^0.12.1", 73 | "url-loader": "^0.5.6", 74 | "webpack": "^1.11.0", 75 | "webpack-dev-server": "^1.10.1" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /frontend/src/components/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorb/pincoop/10f5f672a4cd6308e85c1b99c454795fcb55fc5a/frontend/src/components/.keep -------------------------------------------------------------------------------- /frontend/src/components/nodes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import NodesStats from './nodes_stats' 3 | 4 | class Node extends React.Component { 5 | render() { 6 | const alive = this.props.alive ? 'Online' : 'Offline' 7 | let id = this.props.id 8 | if(id !== null || id !== undefined) { 9 | id = id.substr(0, 16) 10 | } 11 | const pinned = this.props.pinned.length 12 | const to_pin = this.props.to_pin.length 13 | const retries = this.props.retries 14 | const pinning = this.props.pinning.length 15 | 16 | let alive_classname = this.props.alive ? 'column green' : 'column red' 17 | let to_pin_classname = 'column green' 18 | if(to_pin > 3) { 19 | to_pin_classname = 'column orange' 20 | } 21 | if(to_pin > 10) { 22 | to_pin_classname = 'column red' 23 | } 24 | 25 | let retries_classname = 'column green' 26 | if(retries > 0) { 27 | retries_classname = 'column orange' 28 | } 29 | if(retries > 3) { 30 | retries_classname = 'column red' 31 | } 32 | if(retries === 5) { 33 | retries_classname = 'column red bold' 34 | alive_classname = 'column red bold' 35 | } 36 | return
37 |
38 | {id} 39 |
40 |
41 | {alive} 42 |
43 |
44 | {retries} 45 |
46 |
47 | {pinned} 48 |
49 |
50 | {pinning} 51 |
52 |
53 | {to_pin} 54 |
55 |
56 |
57 | } 58 | } 59 | 60 | class Nodes extends React.Component { 61 | constructor(props) { 62 | super(props) 63 | this.state = { 64 | nodes: [], 65 | interval: null 66 | } 67 | } 68 | getNodes() { 69 | fetch(process.env.API + 'api/daemons').then((res) => { 70 | return res.json() 71 | }).then((nodes) => { 72 | this.setState({nodes}) 73 | }) 74 | } 75 | componentDidMount() { 76 | let interval = setInterval(() => { 77 | this.getNodes() 78 | }, 1000) 79 | this.getNodes() 80 | this.setState({interval}) 81 | } 82 | componentWillUnmount() { 83 | clearInterval(this.state.interval) 84 | } 85 | render() { 86 | const nodes = this.state.nodes.map((node) => { 87 | return 95 | }) 96 | return
97 | 98 |

Node details

99 |
100 |
101 | ID 102 |
103 |
104 | Status 105 |
106 |
107 | Retries 108 |
109 |
110 | Pinned 111 |
112 |
113 | Pinning 114 |
115 |
116 | Left To Pin 117 |
118 |
119 | {nodes} 120 |
121 |
122 | } 123 | } 124 | 125 | export default Nodes 126 | -------------------------------------------------------------------------------- /frontend/src/components/nodes_stats.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class NodesStats extends React.Component { 4 | static propTypes = { 5 | nodes: React.PropTypes.array.isRequired 6 | } 7 | render() { 8 | let alive_nodes = 0 9 | let pinned_content = 0 10 | let pinning_content = 0 11 | let content_to_pin = 0 12 | this.props.nodes.forEach((node) => { 13 | if(node.alive) { 14 | alive_nodes++ 15 | } 16 | pinned_content = pinned_content + node.pinned.length 17 | pinning_content = pinning_content + node.pinning.length 18 | content_to_pin = content_to_pin + node.to_pin.length 19 | }) 20 | let column_style = { 21 | width: '25%', 22 | float: 'left' 23 | } 24 | let column_header_style = { 25 | ...column_style, 26 | fontWeight: 'bold' 27 | } 28 | return
29 |

Summary

30 |
31 |
Alive Nodes
32 |
Pinned
33 |
Pinning
34 |
To Pin
35 |
36 |
37 |
{alive_nodes}
38 |
{pinned_content}
39 |
{pinning_content}
40 |
{content_to_pin}
41 |
42 |
43 |
44 | } 45 | } 46 | 47 | export default NodesStats 48 | -------------------------------------------------------------------------------- /frontend/src/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import routes from '../routes'; 4 | // import { createDevToolsWindow } from '../utils'; 5 | // import { DevTools, LogMonitor, DebugPanel } from 'redux-devtools/lib/react'; 6 | import { ReduxRouter } from 'redux-router'; 7 | 8 | export default class Root extends React.Component { 9 | static propTypes = { 10 | debug: React.PropTypes.bool, 11 | debugExternal: React.PropTypes.bool, 12 | store: React.PropTypes.object.isRequired 13 | } 14 | 15 | constructor () { 16 | super(); 17 | } 18 | 19 | // renderDevTools () { 20 | // if (!this.props.debug) { 21 | // return null; 22 | // } 23 | 24 | // if (this.props.debugExternal) { 25 | // createDevToolsWindow(this.props.store); 26 | // return null; 27 | // } 28 | 29 | // return ( 30 | // 31 | // 32 | // 33 | // ); 34 | // } 35 | // {this.renderDevTools()} 36 | 37 | render () { 38 | return ( 39 |
40 | 41 | 42 | {routes} 43 | 44 | 45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/history.js: -------------------------------------------------------------------------------- 1 | import createBrowserHistory from 'history/lib/createBrowserHistory' 2 | export default createBrowserHistory() 3 | -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PinCoop 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Root from './containers/Root'; 4 | import configureStore from './stores'; 5 | 6 | const target = document.getElementById('root'); 7 | const store = configureStore(window.__INITIAL_STATE__, __DEBUG__); 8 | 9 | const node = ( 10 | 14 | ) 15 | 16 | ReactDOM.render(node, target); 17 | -------------------------------------------------------------------------------- /frontend/src/layouts/CoreLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'styles/core.scss'; 3 | import Navigation from '../views/Navigation' 4 | 5 | export default class CoreLayout extends React.Component { 6 | static propTypes = { 7 | children : React.PropTypes.element 8 | } 9 | 10 | constructor () { 11 | super(); 12 | } 13 | 14 | render () { 15 | return ( 16 |
17 | 18 |
19 | {this.props.children} 20 |
21 |
22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from 'utils'; 2 | 3 | // normally this would be imported from /constants, but in trying to keep 4 | // this starter kit as small as possible we'll just define it here. 5 | const COUNTER_INCREMENT = 'COUNTER_INCREMENT'; 6 | 7 | const initialState = 0; 8 | export default createReducer(initialState, { 9 | [COUNTER_INCREMENT] : (state) => state + 1 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import counter from './counter'; 3 | import { routerStateReducer } from 'redux-router'; 4 | 5 | export default combineReducers({ 6 | counter, 7 | router: routerStateReducer 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/src/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router, Route, IndexRoute } from 'react-router'; 2 | import React from 'react'; 3 | import CoreLayout from 'layouts/CoreLayout'; 4 | import HomeView from 'views/PinView'; 5 | import PinView from 'views/PinView'; 6 | import NodesView from 'views/NodesView'; 7 | import NodesAddView from 'views/NodesAddView'; 8 | import AboutView from 'views/AboutView'; 9 | import PinShowView from 'views/PinShowView'; 10 | 11 | import history from '../history' 12 | 13 | export default ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /frontend/src/stores/index.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux'; 2 | import rootReducer from '../reducers'; 3 | import { devTools } from 'redux-devtools'; 4 | import thunk from 'redux-thunk'; 5 | import routes from '../routes'; 6 | import { reduxReactRouter } from 'redux-router'; 7 | import createHistory from 'history/lib/createBrowserHistory'; 8 | 9 | export default function configureStore (initialState, debug = false) { 10 | let createStoreWithMiddleware; 11 | 12 | const middleware = applyMiddleware(thunk); 13 | 14 | if (debug) { 15 | createStoreWithMiddleware = compose(middleware, reduxReactRouter({ routes, createHistory }), devTools()); 16 | } else { 17 | createStoreWithMiddleware = compose(middleware, reduxReactRouter({ routes, createHistory })); 18 | } 19 | 20 | const store = createStoreWithMiddleware(createStore)( 21 | rootReducer, initialState 22 | ); 23 | if (module.hot) { 24 | module.hot.accept('../reducers', () => { 25 | const nextRootReducer = require('../reducers/index'); 26 | 27 | store.replaceReducer(nextRootReducer); 28 | }); 29 | } 30 | return store; 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | // Settings go here 2 | -------------------------------------------------------------------------------- /frontend/src/styles/content.scss: -------------------------------------------------------------------------------- 1 | .content .form label { 2 | font-size: 36px; 3 | color: #34495E; 4 | line-height: 36px; 5 | width: 100%; 6 | } 7 | 8 | .content .form input { 9 | background: #FDFDFD; 10 | border: 1px solid #979797; 11 | height: 36px; 12 | width: 100%; 13 | } 14 | 15 | .content button { 16 | border-radius: 3px; 17 | font-size: 36px; 18 | line-height: 36px; 19 | margin-top: 35px; 20 | border: 2px solid #3498DB; 21 | padding: 30px 0px 30px 0px; 22 | width: 60%; 23 | background-color: rgba(0,0,0,0); 24 | color: #3498DB; 25 | } 26 | 27 | .content button:hover { 28 | color: #ECF0F1; 29 | background: #3498DB; 30 | cursor: pointer; 31 | border: 2px solid #3498DB; 32 | } 33 | 34 | .content h6 { 35 | font-weight: bold; 36 | font-size: 30px; 37 | line-height: 30px; 38 | margin: 0px; 39 | margin-top: 50px; 40 | } 41 | 42 | .content.about { 43 | font-size: 24px; 44 | line-height: 30px; 45 | margin-top: 0px; 46 | width: 90%; 47 | margin-left: 5%; 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/styles/core.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | @import 'vendor/normalize'; 3 | 4 | body, html { 5 | margin: 0; 6 | padding: 0; 7 | font-family: 'Ubuntu Mono'; 8 | color: #34495E; 9 | background-color: #ECF0F1; 10 | } 11 | 12 | .scroller { 13 | position: absolute; 14 | width: 1px; 15 | height: 1px; 16 | bottom: -1px; 17 | } 18 | 19 | .clearfix { 20 | clear: both; 21 | } 22 | 23 | @import 'navigation'; 24 | @import 'wrapper'; 25 | @import 'content'; 26 | @import 'notes'; 27 | @import 'nodes'; 28 | -------------------------------------------------------------------------------- /frontend/src/styles/navigation.scss: -------------------------------------------------------------------------------- 1 | .navigation { 2 | width: 100%; 3 | } 4 | 5 | .navigation .brand { 6 | position: relative; 7 | width: 100px; 8 | float: left; 9 | font-weight: bold; 10 | font-size: 20px; 11 | line-height: 20px; 12 | margin-top: 30px; 13 | margin-left: 30px; 14 | } 15 | 16 | .navigation .beta { 17 | font-size: 10px; 18 | line-height: 10px; 19 | margin-left: 5px; 20 | position: absolute; 21 | color: #2980b9; 22 | } 23 | 24 | .navigation .brand a { 25 | color: #34495E; 26 | text-decoration: none; 27 | } 28 | 29 | .navigation .links { 30 | float: right; 31 | margin-top: 30px; 32 | } 33 | 34 | .navigation .links a { 35 | font-size: 20px; 36 | text-decoration: none; 37 | color: #34495E; 38 | margin-right: 30px; 39 | } 40 | 41 | .navigation .links a.active { 42 | font-weight: bold; 43 | border-bottom: 1px solid #34495E; 44 | } 45 | 46 | .navigation .links a:hover { 47 | border-bottom: 1px solid #34495E; 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/styles/nodes.scss: -------------------------------------------------------------------------------- 1 | .nodes { 2 | padding-bottom: 50px; 3 | } 4 | 5 | .nodes .column { 6 | width: 16.6%; 7 | float: left; 8 | text-align: center; 9 | } 10 | 11 | .nodes .column.header { 12 | font-weight: bold; 13 | } 14 | 15 | .node { 16 | border-top: 1px solid rgba(200, 200, 200, 1) 17 | } 18 | 19 | .green { 20 | color: #27AE60; 21 | } 22 | 23 | .red { 24 | color: #C0392B; 25 | } 26 | 27 | .orange { 28 | color: #F39C12; 29 | } 30 | 31 | .bold { 32 | font-weight: bold; 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/styles/notes.scss: -------------------------------------------------------------------------------- 1 | .notes { 2 | font-size: 24px; 3 | color: #34495E; 4 | line-height: 30px; 5 | margin-top: 50px; 6 | } 7 | 8 | .notes h3 { 9 | font-weight: bold; 10 | font-size: 24px; 11 | color: #34495E; 12 | line-height: 30px; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/styles/vendor/_normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } 428 | -------------------------------------------------------------------------------- /frontend/src/styles/wrapper.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | position: absolute; 3 | width: 50%; 4 | left: 50%; 5 | top: 20%; 6 | margin-left: -25%; 7 | text-align: center; 8 | } 9 | 10 | .wrapper .content { 11 | margin-top: 50px; 12 | } 13 | 14 | .wrapper .form { 15 | width: 60%; 16 | margin-left: 20%; 17 | } 18 | 19 | .wrapper a { 20 | color: #2980B9; 21 | text-decoration: none; 22 | } 23 | 24 | .wrapper h1 { 25 | font-weight: bold; 26 | font-size: 36px; 27 | color: #34495E; 28 | line-height: 36px; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/utils/index.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import ReactDOM from 'react-dom'; 3 | // import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react'; 4 | 5 | export function createConstants (...constants) { 6 | return constants.reduce((acc, constant) => { 7 | acc[constant] = constant; 8 | return acc; 9 | }, {}); 10 | } 11 | 12 | export function createReducer (initialState, reducerMap) { 13 | return (state = initialState, action) => { 14 | const reducer = reducerMap[action.type]; 15 | 16 | return reducer ? reducer(state, action.payload) : state; 17 | }; 18 | } 19 | 20 | // export function createDevToolsWindow (store) { 21 | // const win = window.open( 22 | // null, 23 | // 'redux-devtools', // give it a name so it reuses the same window 24 | // 'menubar=no,location=no,resizable=yes,scrollbars=no,status=no' 25 | // ); 26 | // // reload in case it's reusing the same window with the old content 27 | // win.location.reload(); 28 | // // wait a little bit for it to reload, then render 29 | // setTimeout(() => { 30 | // // Wait for the reload to prevent: 31 | // // "Uncaught Error: Invariant Violation: _registerComponent(...): Target container is not a DOM element." 32 | // win.document.write('
'); 33 | // ReactDOM.render( 34 | // 35 | // 36 | // 37 | // , win.document.getElementById('react-devtools-root')); 38 | // }, 10); 39 | // } 40 | -------------------------------------------------------------------------------- /frontend/src/views/AboutView.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class AboutView extends React.Component { 4 | render() { 5 | return
6 |
7 |

A open network of IPFS
nodes that all help each
other to rehost content

8 |
9 |
10 |
Our mission
11 |

12 | Bring together groups of nodes that are all sharing and rehosting content from each other. 13 |

14 |
About
15 |

16 | IPFS is a protocol that enables a permanent web. But without nodes rehosting the content, we are still doing the same as the web we have today. 17 |

18 |

19 | By bringing nodes together, we can enable a API for sharing content in an easy way. 20 |

21 |

22 | Contribute a node to rehost content or donate money for others to handle the maintaince. Either way, you help us build a permanent web. 23 |

24 |

25 | More information can be found in the Github Wiki or at the source itself. 26 |

27 |

28 | PinCoop was built and is maintained by
29 | Victor Bjelkholm (Twitter, Github)
30 | and a group of contributers 31 |

32 |
33 |
34 | } 35 | } 36 | 37 | export default AboutView 38 | -------------------------------------------------------------------------------- /frontend/src/views/HomeView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | 5 | // Normally you'd import your action creators, but I don't want to create 6 | // a file that you're just going to delete anyways! 7 | const actionCreators = { 8 | increment : () => ({ type : 'COUNTER_INCREMENT' }) 9 | }; 10 | 11 | // We define mapStateToProps and mapDispatchToProps where we'd normally use 12 | // the @connect decorator so the data requirements are clear upfront, but then 13 | // export the decorated component after the main class definition so 14 | // the component can be tested w/ and w/o being connected. 15 | // See: http://rackt.github.io/redux/docs/recipes/WritingTests.html 16 | const mapStateToProps = (state) => ({ 17 | counter : state.counter, 18 | routerState : state.router 19 | }); 20 | const mapDispatchToProps = (dispatch) => ({ 21 | actions : bindActionCreators(actionCreators, dispatch) 22 | }); 23 | export class HomeView extends React.Component { 24 | static propTypes = { 25 | actions : React.PropTypes.object, 26 | counter : React.PropTypes.number 27 | } 28 | 29 | constructor () { 30 | super(); 31 | } 32 | 33 | render () { 34 | return ( 35 |
36 |

Welcome to the React Redux Starter Kit

37 |

Sample Counter: {this.props.counter}

38 | 42 |
43 | ); 44 | } 45 | } 46 | 47 | export default connect(mapStateToProps, mapDispatchToProps)(HomeView); 48 | -------------------------------------------------------------------------------- /frontend/src/views/HomeView.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-addons-test-utils'; 3 | import { bindActionCreators } from 'redux'; 4 | import { HomeView } from './HomeView'; 5 | 6 | function shallowRender (component) { 7 | const renderer = TestUtils.createRenderer(); 8 | 9 | renderer.render(component); 10 | return renderer.getRenderOutput(); 11 | } 12 | 13 | function renderWithProps (props = {}) { 14 | return TestUtils.renderIntoDocument(); 15 | } 16 | 17 | function shallowRenderWithProps (props = {}) { 18 | return shallowRender() 19 | } 20 | 21 | describe('(View) Home', function () { 22 | let component, rendered, _props, _spies; 23 | 24 | beforeEach(function () { 25 | _spies = {}; 26 | _props = { 27 | actions : bindActionCreators({ 28 | increment : (_spies.increment = sinon.spy()) 29 | }, _spies.dispatch = sinon.spy()) 30 | }; 31 | 32 | component = shallowRenderWithProps(_props); 33 | rendered = renderWithProps(_props); 34 | }); 35 | 36 | it('(Meta) Should have a test that works with Chai expectations.', function () { 37 | expect(true).to.be.true; 38 | }); 39 | 40 | it('Should render as a
.', function () { 41 | expect(component.type).to.equal('div'); 42 | }); 43 | 44 | it('Should include an

with welcome text.', function () { 45 | const h1 = TestUtils.findRenderedDOMComponentWithTag(rendered, 'h1'); 46 | 47 | expect(h1).to.exist; 48 | expect(h1.textContent).to.match(/Welcome to the React Redux Starter Kit/); 49 | }); 50 | 51 | it('Should render with an

that includes Sample Counter text.', function () { 52 | const h2 = TestUtils.findRenderedDOMComponentWithTag(rendered, 'h2'); 53 | 54 | expect(h2).to.exist; 55 | expect(h2.textContent).to.match(/Sample Counter/); 56 | }); 57 | 58 | it('Should render props.counter at the end of the sample counter

.', function () { 59 | const h2 = TestUtils.findRenderedDOMComponentWithTag( 60 | renderWithProps({ ..._props, counter : 5 }), 'h2' 61 | ); 62 | 63 | expect(h2).to.exist; 64 | expect(h2.textContent).to.match(/5$/); 65 | }); 66 | 67 | it('Should render an "Increment" button.', function () { 68 | const btn = TestUtils.findRenderedDOMComponentWithTag(rendered, 'button'); 69 | 70 | expect(btn).to.exist; 71 | expect(btn.textContent).to.match(/Increment/); 72 | }); 73 | 74 | it('Should dispatch an action when "Increment" button is clicked.', function () { 75 | const btn = TestUtils.findRenderedDOMComponentWithTag(rendered, 'button'); 76 | 77 | _spies.dispatch.should.have.not.been.called; 78 | TestUtils.Simulate.click(btn); 79 | _spies.dispatch.should.have.been.called; 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /frontend/src/views/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | const routes = { 5 | 'Pin': '/pins', 6 | 'Nodes': '/nodes', 7 | 'Add your node': '/nodes/add', 8 | 'About': '/about', 9 | 'Source': 'https://github.com/victorbjelkholm/pincoop', 10 | } 11 | 12 | class Navigation extends React.Component { 13 | handleOnClick(page) { 14 | window.location.pathname = page 15 | } 16 | render() { 17 | const links = Object.keys(routes).map((key) => { 18 | const path = routes[key] 19 | const is_active = window.location.pathname === path; 20 | let className = null 21 | if(is_active) { 22 | className = "active" 23 | } 24 | return {key} 25 | }) 26 | return
27 |
28 | PinCoop 29 | BETA 30 |
31 |
32 | {links} 33 |
34 |
35 |
36 | } 37 | } 38 | 39 | export default Navigation 40 | -------------------------------------------------------------------------------- /frontend/src/views/NodesAddView.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class NodesAddView extends React.Component { 4 | constructor(props) { 5 | super(props) 6 | this.state = { 7 | multiaddr: null 8 | } 9 | } 10 | handleOnChange(ev) { 11 | const multiaddr = ev.target.value.trim() 12 | this.setState({multiaddr}) 13 | } 14 | handleOnClick() { 15 | const multiaddr = this.state.multiaddr 16 | this.setState({multiaddr: null}) 17 | const body = {multiaddr} 18 | 19 | fetch(process.env.API + 'api/daemons', { 20 | method: 'POST', 21 | body: JSON.stringify(body), 22 | headers: { 23 | 'Content-Type': 'application/json' 24 | } 25 | }).then(() => { 26 | this.props.history.replaceState(null, '/nodes') 27 | }) 28 | } 29 | render() { 30 | return
31 |
32 |

Help us help you. Donate a IPFS node and make sure the content stays online forever

33 |
34 |
35 |
36 | 37 | 38 | 39 | 40 |
41 |
42 | 43 |
44 |
45 |

How to setup?

46 |

47 | If you want to more about how to run the node to be able to 48 | support PinCoop, take a look at the node setup guide 49 |

50 |
51 |
52 | } 53 | } 54 | 55 | export default NodesAddView 56 | -------------------------------------------------------------------------------- /frontend/src/views/NodesView.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Nodes from '../components/nodes' 3 | 4 | class NodesView extends React.Component { 5 | render() { 6 | return
7 |
8 |

A live view of all nodes
currently in the network

9 |
10 |
11 | 12 |
13 |
14 | } 15 | } 16 | 17 | export default NodesView 18 | -------------------------------------------------------------------------------- /frontend/src/views/PinShowView.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class PinShowView extends React.Component { 4 | constructor(props) { 5 | super(props) 6 | this.state = { 7 | nodes: [], 8 | interval: null 9 | } 10 | } 11 | getNodes() { 12 | fetch(process.env.API + 'api/daemons').then((res) => { 13 | return res.json() 14 | }).then((nodes) => { 15 | this.setState({nodes}) 16 | }) 17 | } 18 | componentDidMount() { 19 | let interval = setInterval(() => { 20 | this.getNodes() 21 | }, 1000) 22 | this.getNodes() 23 | this.setState({interval}) 24 | } 25 | componentWillUnmount() { 26 | clearInterval(this.state.interval) 27 | } 28 | render() { 29 | let not_pinned = [] 30 | let pinned = [] 31 | let pinning = [] 32 | 33 | this.state.nodes.forEach((node) => { 34 | node.pinned.forEach((hash) => { 35 | if(hash === this.props.params.hash) { 36 | pinned.push(node) 37 | } 38 | }) 39 | node.to_pin.forEach((hash) => { 40 | if(hash === this.props.params.hash) { 41 | not_pinned.push(node) 42 | } 43 | }) 44 | node.pinning.forEach((hash) => { 45 | if(hash === this.props.params.hash) { 46 | pinning.push(node) 47 | } 48 | }) 49 | }) 50 | let pinned_render; 51 | let not_pinned_render; 52 | let pinning_render; 53 | if(pinned.length > 0) { 54 | pinned_render = pinned.map((node) => { 55 | return
{node.id}
56 | }) 57 | } else { 58 | pinned_render =
No node have pinned your content yet...
59 | } 60 | if(not_pinned.length > 0) { 61 | not_pinned_render = not_pinned.map((node) => { 62 | return
{node.id}
63 | }) 64 | } else { 65 | not_pinned_render =
Everyone already pinned your content!
66 | } 67 | if(pinning.length > 0) { 68 | pinning_render = pinning.map((node) => { 69 | return
{node.id}
70 | }) 71 | } else { 72 | pinning_render =
No pinning is currently in progress
73 | } 74 | let to_render =
75 |

Status of pinning

76 |

77 | Summary
78 | Not pinned: [{not_pinned.length}]
79 | Pinning: [{pinning.length}]
80 | Pinned: [{pinned.length}]
81 |

82 |

Nodes that have not pinned your content yet

83 | {not_pinned_render} 84 |

Nodes that are currently pinning

85 | {pinning_render} 86 |

Nodes that have pinned!

87 | {pinned_render} 88 |
89 |
90 |
91 |

92 | Add more content by going home! 93 |

94 |
95 | 96 | let warning = "" 97 | if(pinned.length === 0 && not_pinned.length === 0 && pinning.length === 0) { 98 | warning =
99 |

No one have pinned this hash
or is trying to pin it...

100 |

101 | Are you sure you've added this hash? 102 |

103 | Go Home 104 |
105 | to_render =
{warning}
106 | } 107 | 108 | return
109 | {to_render} 110 |
111 | } 112 | } 113 | 114 | export default PinShowView 115 | -------------------------------------------------------------------------------- /frontend/src/views/PinView.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import history from '../history' 4 | 5 | class PinView extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | if(window.location.pathname === '/') { 9 | this.props.history.replaceState(null, '/pins') 10 | } 11 | this.state = { 12 | hash: null 13 | } 14 | } 15 | handleOnChange(ev) { 16 | this.setState({ 17 | hash: ev.target.value.trim() 18 | }) 19 | } 20 | handleOnClick() { 21 | const hash = this.state.hash 22 | this.setState({hash: null}) 23 | fetch(process.env.API + 'api/pin/' + hash, {method: 'POST'}) 24 | .then(() => { 25 | this.props.history.replaceState(null, '/pins/'+hash) 26 | }) 27 | } 28 | render() { 29 | return
30 |
31 |

Have your content hosted
forever and free

32 |
33 |
34 |
35 | 36 | 37 |
38 |
39 | 40 |
41 |
42 |

Developer?

43 |

44 | If you rather use the API to pin content,
45 | take a look at the API documentation 46 |

47 |
48 |
49 | } 50 | } 51 | 52 | export default PinView 53 | -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | require('babel/register'); 2 | 3 | const config = require('./config'); 4 | module.exports = require('./build/webpack/' + config.get('env')); 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pincoop", 3 | "version": "1.0.0", 4 | "description": "Share your files with the world, host the world's files", 5 | "main": "api.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "api": "node server.js", 9 | "frontend:compile": "cd frontend;npm run compile", 10 | "frontend:clear:cache": "rm -rf frontend/dist", 11 | "start": "npm run frontend:clear:cache;npm run frontend:compile;npm run api" 12 | }, 13 | "keywords": [ 14 | "ipfs" 15 | ], 16 | "author": "Victor Bjelkholm ", 17 | "license": "MIT", 18 | "dependencies": { 19 | "babel": "^5.8.23", 20 | "babel-core": "^5.8.25", 21 | "babel-loader": "^5.3.2", 22 | "body-parser": "^1.14.1", 23 | "cors": "^2.7.1", 24 | "eslint": "^1.7.3", 25 | "eslint-config-standard": "^4.4.0", 26 | "eslint-loader": "^1.1.0", 27 | "eslint-plugin-standard": "^1.3.1", 28 | "express": "^4.13.3", 29 | "express-http-proxy": "^0.6.0", 30 | "flat-file-db": "^0.1.5", 31 | "html-webpack-plugin": "^1.6.2", 32 | "ipfs-api": "git://github.com/ipfs/node-ipfs-api.git#solid-tests", 33 | "lodash": "^3.10.1", 34 | "react": "^0.14.0", 35 | "react-dom": "^0.14.0", 36 | "react-hot-loader": "^1.3.0", 37 | "react-redux": "^4.0.0", 38 | "react-router": "^1.0.0-rc3", 39 | "react-tools": "^0.10.0", 40 | "react-transform": "0.0.3", 41 | "redux": "^3.0.4", 42 | "redux-devtools": "^2.1.5", 43 | "redux-router": "^1.0.0-beta3", 44 | "sha1": "^1.1.1", 45 | "underscore": "^1.8.3", 46 | "webpack": "^1.12.2", 47 | "webpack-dev-server": "^1.12.1", 48 | "winston": "^1.1.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pin_content_in_cluster.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -e 3 | 4 | echo "Preparing to pin content in cluster..." 5 | 6 | 7 | TO_PIN=( 8 | 'QmSR7ELqgrwQEWkmDFLjeeMeZBcRpzV7K8G1j9yvJnWyH9' 9 | 'QmbWJCHaKEFULo5FmmQgZSppfET7yThk3erBdYaYtkDRmd' 10 | 'Qmc46FrUcsMUrpYEJrkKB9MmaXMeWJHGoqTL7MPReR3UYM' 11 | 'QmdCwW6WVqzDnEjWFSkztSNaxDSPQuWH4BEqFgc96iuoGC' 12 | 'QmdyWKUVgbkB6wuB6z6adGAUZ56eKCcJiB3N45dvA6K1hN' 13 | 'QmZrggXgBBzgwcSAMBsNchH354Mi9pZDAEQRHshbkUMeRM' 14 | 'QmURv1xdQvzgLunCoxoxsRPWmEbjitMCNajdDNK1fQA45d' 15 | 'QmZZpLbHxoaw4feXyXMWw9BbLxusY1aJ57foF8echmSELD' 16 | 'QmSG4JpZ9JUXivoFKVaUH7KXfeB75bfidR4n5qjwDbkqH7' 17 | 'QmWBYHF9xdFApjUCeHtJVeSzekMuuUmNcGQsASTrSk78JH' 18 | 'QmbWJCHaKEFULo5FmmQgZSppfET7yThk3erBdYaYtkDRmd' 19 | 'QmSR7ELqgrwQEWkmDFLjeeMeZBcRpzV7K8G1j9yvJnWyH9' 20 | 'QmS9YJvLe1E3CSzqiNViuJgKgKeXJv3uZLZmyyVeW7AQY7' 21 | 'Qmbv3xms1rUV9VQ8x49rhC3bNaWSpuzLSAtX1CWQZrTPyZ' 22 | 'QmWi9e7UWhJKPixsEshdVBZRZD4eh6nNXBNQ4emFfYoNTh' 23 | 'QmdMmVi6REUjP9zbFYCFxJWxaoV77L9iLQcBLda3q4Tyk1' 24 | 'QmRegQwX27Uexh8DQaczTrdwVn4gMdwFReNEBJcPhLRwcq' 25 | 'QmWjdqHX71vGWyxV68QzfXRcPzufbkYxhVDNxcj6mkkPYo' 26 | 'QmXcpG9ke5GJVzdcsoGddj5w5BifPq5cdFSU9znCPT9q9X' 27 | 'QmU62zXAeYaFm3XviFgb1BxNarzxLLYhK9bUAVBWLwngJV' 28 | 'QmNXSX68cHrHR5XmN4ZNmKm6qPRE39TyJqSBXdpbKtoJDH' 29 | 'QmNNsxvnvSgpCCWDAtnkvJ2CRi1taQGDWL3gAt9JzPSWX4' 30 | 'QmVFqqVw1iBwoVFA8PPTeN3FR6kkVWqmnthgKFiVDe3AVr' 31 | 'QmTmtDi6fYjaZ1N9AC3SB3VkqdAkCp6nCqS3nncSmzGXK6' 32 | 'QmfVBTXAFMua7JcHNkG2Bt5m5L2UPjMn9wiEFdxrnpdWjB' 33 | 'QmaTrmJeHvucKdtZN6ttb53vsUXSaLfocono4dFFMgJxCs' 34 | 'QmSNSm1gCv8mHjapPTQRuCJVWyabredCcUUnNDmp6pYrLs' 35 | 'QmUWgc1vokX7WJREe5tbe7bP1uQtdDiwHgRWQrLDNYip49' 36 | 'QmWAatTSa5CU3yzio15p47jGrwW4dPDfSHCkrLjFjPs75s' 37 | 'QmPXHtwCheXJ7fRFv9DTA3UaRHr59v1D4XrdR9KWa9HfR2' 38 | 'QmXeBjJ9gdJ1gQ8sXg1A6BNeL2fsmv5rPAq6uxj9hynsY4' 39 | 'QmdcdMwNVAi1jr1hwnKCnp3dJsHcaZyGgwWrEARTNMVeXd' 40 | 'QmazRLjjzLYDnxVMEMEmKd5w8KxYMyaQ3wMakLcz5iKyNc' 41 | 'QmchUpLRQxTTvdG75K9HxMRfdgQ1cbv2rHnorEbjkRrbXB' 42 | 'QmbkawJipe5MFVDtpCGrG75y59Ancq6UYSorHfoyGhUJ43' 43 | 'QmVu51NWMctz5MV1cLnR2SmUeYZy7nSVQiKHsLQ1fzDkzk' 44 | 'QmY6anJVKjChjxnjXdSZVGqKGoYmM1xgcLzkLT1613Sjw7' 45 | ); 46 | 47 | # Debug 48 | # HOST="localhost:3000" 49 | # Production 50 | HOST="pincoop.xyz" 51 | 52 | 53 | for i in ${TO_PIN[@]}; do 54 | RND_NUM=0.$[ 1 + $[ RANDOM % 100 ]] 55 | echo "Pinning $i in $RND_NUM" 56 | sleep $RND_NUM 57 | curl --silent -X POST http://$HOST/api/pin/$i > /dev/null 58 | done 59 | 60 | echo "Done pinning!" 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## PinCoop 2 | 3 | DISCLAIMER: This project is in Beta stage and hugely unstable. Installation 4 | without docker is not guaranteed to succeed. Use at your own risk 5 | 6 | ### Introduction 7 | 8 | PinCoop is a set of API endpoints and WebUI for adding public daemons, retrieve a list 9 | of all of them and to add hashes to be pinned on those public daemons. 10 | 11 | ### Goal 12 | 13 | The goal is to provide a starting point for openly share hashes of interest, 14 | on a large set of open daemons contributed by the community. 15 | 16 | IPFS works the way that the more hosts pin a hash, the more resilient that 17 | hash becomes. By providing a starting point for open hosting of these hashes, 18 | I hope that we can share more resources with each others. 19 | 20 | All in a very open and transparent way. 21 | 22 | ### Contributing 23 | 24 | There are mainly three ways you can contribute. 25 | 26 | 1. Donate money to people willing to hosts node. As project owner, I'm willing to host nodes in a location of your chosing, and manage the node for use in PinCoop. Donate via Bitcoin `1MFZHmG3HAtQLHv3yvRn5VWRmF1Aa7u8FC` or Paypal `victorbjelkholm+paypal@gmail.com` and open an issue about your donation, so I can tell you the progress of setting up the node for PinCoop. 27 | 2. You can manage your own node and copy-paste the public multiaddr directly in http://pincoop.xyz/nodes/add. You'll need to set the API config in ~/.ipfs/config to listen to 0.0.0.0. Better security will come in the future. 28 | 3. Third way of contributing is to help with the software behind PinCoop. There are two parts, the backend that does polling of the nodes and sends API requests to the connected nodes, and the frontend which is a redux application. 29 | 30 | ### Installation 31 | 32 | Easiest is to use docker to run PinCoop in your own network. 33 | 34 | * `docker pull victorbjelkholm/pincoop` 35 | 36 | * `docker run -d -p your_port:3000 victorbjelkholm/pincoop` 37 | 38 | Replace `your_port` with the port you want the WebUI to be available on. 39 | 40 | It'll take a few seconds for the server to boot after running `docker run`, so 41 | just hang tight for a while before loading... 42 | 43 | ### Screenshots 44 | 45 | Some screenshots on how it looks currently 46 | 47 | ##### Homepage 48 | ![Homepage](http://i.imgur.com/amwccoW.png) 49 | 50 | ##### Pinning content 51 | ![Pinning content](http://i.imgur.com/naZTfK7.png) 52 | 53 | ##### Live nodes 54 | ![Live nodes](http://i.imgur.com/KDcfO4M.png) 55 | 56 | ##### Adding a node 57 | ![Adding a node](http://i.imgur.com/zY4Edbe.png) 58 | 59 | ##### About 60 | ![About](http://i.imgur.com/ADcHfDF.png) 61 | 62 | ### Endpoints 63 | 64 | `GET /daemons` 65 | 66 | Returns a list of currently added daemons with `alive` status, list of hashes 67 | that will be pinned and hashes that been pinned before 68 | 69 | `POST /daemons` 70 | 71 | Adds a new daemon to the list of daemons. The endpoint accepts JSON only, be sure 72 | to include the `multiaddr` attribute on the root object. Returned is the created 73 | daemon in the API. 74 | 75 | `POST /pin/:hash` 76 | 77 | Pins a new hash. This is an asynchronous operation, it always return true. Use 78 | `GET /daemons` or `GET /pin/:hash` to see the status of that pinning operation 79 | 80 | `GET /pin/:hash` 81 | 82 | See the status of a current pinning, made from `POST /pin/:hash` 83 | 84 | ### Installation 85 | 86 | * Clone the repository into your local/remote machine 87 | * Run `npm install` 88 | * Run `npm start` 89 | * Done! Now the API+Frontend is running on `PORT` from environment variables or if 90 | that is not set, port 3000 91 | 92 | ### Development 93 | 94 | The stack is as follows: 95 | * Express + middlewares for handling API requests 96 | * node-ipfs-api for handling requests to the daemons added to the service 97 | * Redux+React+Webpack for the frontend 98 | 99 | A local daemon is always added on startup! Don't worry if you're not running 100 | a daemon where the API is deployed, it'll soon be removed if it's not up. 101 | 102 | You want to contribute to the API/Server/Daemon logic? Take a look in the `/api` 103 | directory 104 | 105 | You want to make the frontend a bit nicer and more sense-looking? Take a look in 106 | the `/frontend` directory. 107 | 108 | Makes sense? No? Change it and send a PR :) 109 | 110 | ### Contributing 111 | 112 | You can contribute in a number of ways. Either, add your own public daemon to 113 | the service, so other people can pin content on your daemon, or, take a look 114 | at the issues in this repository and see if it's something you can fix. If 115 | you already looked at the code or used the service, open a new issue for everything 116 | that looks weird (I bet some parts are filled with weird stuff!). If someone 117 | is asking something you know, please answer them! This is a project we do together, 118 | so feel free to basically do whatever you want. 119 | 120 | 121 | ### License 122 | 123 | The MIT License (MIT) 124 | 125 | Copyright (c) 2015 Victor Bjelkholm 126 | 127 | Permission is hereby granted, free of charge, to any person obtaining a copy 128 | of this software and associated documentation files (the "Software"), to deal 129 | in the Software without restriction, including without limitation the rights 130 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 131 | copies of the Software, and to permit persons to whom the Software is 132 | furnished to do so, subject to the following conditions: 133 | 134 | The above copyright notice and this permission notice shall be included in 135 | all copies or substantial portions of the Software. 136 | 137 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 138 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 139 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 140 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 141 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 142 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 143 | THE SOFTWARE. 144 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser') 3 | var cors = require('cors') 4 | var proxy = require('express-http-proxy'); 5 | 6 | var Daemon = require('./backend/daemon') 7 | var Log = require('./backend/log') 8 | var Options = require('./backend/options') 9 | var Updater = require('./backend/updater') 10 | var Routes = require('./backend/routes')(Updater) 11 | 12 | var app = express(); 13 | app.use(bodyParser.json()) 14 | app.use(cors()); 15 | if(process.env.STATIC) { 16 | Log.info('Servering static content from frontend/dist') 17 | app.use(express.static('frontend/dist')) 18 | } 19 | 20 | if(!process.env.DEV) { 21 | app.use(function (req, res, next) { 22 | var originalUrl = req.originalUrl 23 | if (originalUrl.indexOf('/api/') !== -1) { 24 | return next(); 25 | } 26 | res.sendFile(__dirname + '/frontend/dist/index.html'); 27 | }) 28 | } 29 | 30 | var bootstrap = [ 31 | '/ip4/127.0.0.1/tcp/5001', 32 | '/ip4/128.199.46.197/tcp/5001' 33 | ] 34 | 35 | Updater.addDaemons(bootstrap.map((multiaddr) => { 36 | return new Daemon(multiaddr) 37 | })) 38 | 39 | Updater.start() 40 | 41 | Routes.forEach((route) => { 42 | app[route.method]('/api' + route.path, route.func) 43 | }) 44 | 45 | 46 | const PORT = process.env.PORT || 3000 47 | var server = app.listen(PORT, () => { 48 | var host = server.address().address; 49 | var port = server.address().port; 50 | 51 | Log.info('Example app listening at http://%s:%s', host, port); 52 | }); 53 | -------------------------------------------------------------------------------- /start_cluster.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -e 3 | 4 | echo "Checking if there is any old containers to remove..." 5 | 6 | ./stop_cluster.sh 7 | 8 | echo "Starting cluster of containers..." 9 | 10 | MULTIADDRS=(); 11 | 12 | # Number of nodes 13 | for NUM in {1..10} 14 | do 15 | echo "Starting container #$NUM" 16 | CONTAINER_ID=$(docker run --name "ipfs_cluster_instance_$NUM" -d eris/ipfs) 17 | echo "Container #$NUM got ID $CONTAINER_ID" 18 | # echo "Waiting for #$NUM to boot..." 19 | # sleep 5 20 | # #TODO Check if the ipfs daemon started, otherwise repeat 21 | # echo "Checking logs for #$NUM" 22 | # docker logs $CONTAINER_ID 23 | 24 | CONTAINER_IP=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' $CONTAINER_ID) 25 | echo "#$NUM have IP $CONTAINER_IP" 26 | 27 | MULTIADDR="/ip4/$CONTAINER_IP/tcp/5001" 28 | echo "#$NUM have multiaddr $MULTIADDR" 29 | MULTIADDRS=("${MULTIADDRS[@]}" "$MULTIADDR") 30 | 31 | echo "Adding $MULTIADDR to PinCoop" 32 | curl -X POST --silent localhost:3000/api/daemons -H "Content-Type: application/json" --data '{"multiaddr":"'$MULTIADDR'"}' > /dev/null 33 | sleep 1 34 | done 35 | 36 | echo "## ALL DONE!" 37 | -------------------------------------------------------------------------------- /stop_cluster.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | #set -e 3 | echo "### Stopping cluster of containers..." 4 | 5 | TO_STOP=$(docker ps --all --filter="name=ipfs_cluster_instance" -q) 6 | 7 | if [ -z "${TO_STOP}" ]; then 8 | echo "No containers found, exiting..." 9 | exit 0 10 | fi 11 | 12 | echo "### I'm stopping $TO_STOP" 13 | 14 | echo "" 15 | echo "### Killing containers..." 16 | docker kill $TO_STOP 17 | echo "" 18 | echo "### Removing containers..." 19 | docker rm $TO_STOP 20 | 21 | echo "" 22 | echo "" 23 | echo "### All done!" 24 | --------------------------------------------------------------------------------