├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── screenshot.png ├── server ├── README.md ├── package.json ├── server.js └── yarn.lock ├── src ├── actions │ ├── alerts.js │ ├── auth.js │ ├── repos.js │ └── users.js ├── components │ ├── footer │ │ ├── Footer.js │ │ └── footer.css │ ├── header │ │ ├── Alerts.js │ │ ├── Header.js │ │ ├── UserProfile.js │ │ ├── header.css │ │ └── logo.png │ ├── repo │ │ └── Repo.js │ └── user │ │ ├── User.js │ │ └── user.css ├── containers │ ├── about │ │ └── About.js │ ├── app │ │ ├── App.js │ │ └── app.css │ ├── home │ │ ├── Home.js │ │ └── home.css │ ├── login │ │ ├── Login.js │ │ └── login.css │ ├── misc │ │ ├── 404.gif │ │ ├── NotFound.js │ │ └── PrivateRoute.js │ ├── repo │ │ ├── ReposPage.js │ │ └── repo.css │ └── user │ │ ├── UsersPage.js │ │ └── user.css ├── index.css ├── index.js ├── logo.svg ├── reducers │ ├── alerts.js │ ├── auth.js │ ├── repos.js │ └── users.js ├── registerServiceWorker.js ├── store │ └── configureStore.js └── utils │ ├── apiUtils.js │ └── socketUtils.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | 13 | # vscode stuff 14 | *.d.ts 15 | .vscode/ 16 | typings.json 17 | jsconfig.json 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yunjun Mu 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [react-redux-starter-kit](http://cloudmu.github.io/react-redux-starter-kit/) 2 | 3 | This is yet another single page web application template using React. However, this project attempts to balance simplicity with developing a real web application that actually "does something useful". 4 | It demonstrates authentication, navigation, asynchronous data fetching, error handling, and caching and pagination, etc. using the technologies listed below. 5 | 6 | But first, the [demo!](http://cloudmu.github.io/react-redux-starter-kit/) It displays information retrieved from the Github API - most followed users and most starred repo's. 7 | Credentials are user *admin* and password *password*. 8 | 9 | Note the deployed demo may not always be up to date. Check out the latest project and [run the demo](#getting-started) yourself. 10 | Here's a screenshot: 11 | 12 | ![alt text](https://raw.githubusercontent.com/cloudmu/react-redux-starter-kit/master/screenshot.png "Screenshot") 13 | 14 | ## Technologies used: 15 | 16 | - [React](https://github.com/facebook/react) 17 | - [Redux](https://github.com/rackt/redux) 18 | - [React Router](https://github.com/rackt/react-router) 19 | - [Bootstrap](https://github.com/twbs/bootstrap) 20 | - [JSON Web Token](https://jwt.io/) 21 | - [Socket.IO](http://socket.io/) 22 | - [create-react-app](https://github.com/facebookincubator/create-react-app/) 23 | - [Babel](http://babeljs.io/) and [Webpack](http://webpack.github.io/) (now behind the scenes thanks to create-react-app) 24 | 25 | ## Feature highlights: 26 | 27 | #### Best React Practice - [Separating "smart" and "dumb" components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 28 | 29 | This design pattern makes even more sense when using React along with Redux, where top-level smart components (a.k.a. containers in this codebase such as `UsersPage` and `ReposPage`) subscribe to Redux state and dispatch Redux actions, while low level components (such as `User`, `Repo`, and `Header`) read data and invoke callbacks passed in as props. 30 | 31 | #### Async Data Fetching with Caching and Pagination 32 | 33 | The `UsersPage` and `ReposPage` would show most followed Github users (with 1000+ followers) and most starred Github repos (with 10000+ stars). The async actions (see `users` and `repos` under actions) fetch data from the following Github APIs: 34 | 35 | - `https://api.github.com/search/users?q=followers:>1000&order=desc&page=1` 36 | - `https://api.github.com/search/repositories?q=stars:>10000&order=desc&page=1` 37 | 38 | The fetched data are stored with the page number as the lookup key, so that the local copy can be shown without the need to re-fetch the same data remotely each time. However cached data can be invalidated if desired. 39 | 40 | #### Error Handling while Fetching Data 41 | 42 | You can test this by disabling your internet connection. Or even better, you can page through `UsersPage` or `ReposPage` very quickly and hopefully invoke Github's API rate limit for your IP address. 43 | The application would fail gracefully with the error message if data fetching (for a particular page) fails. However, the application can still show cached data for other pages, which is very desirable behavior. 44 | 45 | #### Authentication and Restricted Pages 46 | 47 | Certain UI pages (`UsersPage` and `ReposPage`) are only accessible after signing in to the application. When accessing restricted pages without signing in first, the application redirects to the `Login` page. The authentication is based on [JSON Web Token (JWT)](https://jwt.io/). 48 | 49 | #### Socket.IO 50 | 51 | A "server alerts/notifications" use case is implemented to showcase [Socket.IO](http://socket.io/). Whenever a client logs in/out of the application using the API server, the API server will notify currently connected clients via Socket.IO. You can test this use case by opening 52 | the web app in two browsers side by side, and then log in/out the webapp in one browser, and observe the messages in the other browser. The messages are pushed from the server to the clients in "real time", and show up as `Alerts` in the header section of the web app. 53 | 54 | ## What's New 55 | 56 | * I recently (Aug/2016) ported this project to use [create-react-app](https://github.com/facebookincubator/create-react-app). Enjoy configuration-free (fatigue-free) React! 57 | * A JWT based API server is added, thanks to the latest create-react-app feature [Proxying API Requests in Development](https://github.com/facebookincubator/create-react-app/blob/ef94b0561d5afb9b50b905fa5cd3f94e965c69c0/template/README.md#proxying-api-requests-in-development). 58 | * The async actions for restful API calls for authentication and fetching Github users and repos are now refactored to go through a common utility `callApi()`. 59 | * "server alerts/notifications" use case is implemented to showcase [Socket.IO](http://socket.io/). 60 | 61 | ## Wish List / Known Issues 62 | **Universal** 63 | 64 | Although it's "cool" to have universal (server-side, isomorphic) rendering these days, there are many situations (like this one) where that complexity is simply not useful or applicable (e.g. Java backend). 65 | 66 | ## Getting Started 67 | Thanks to [create-react-app](https://github.com/facebookincubator/create-react-app), we will have a configuration-free dev experience. 68 | 69 | To get started, please clone this git repository and then run `npm install` once under the project top-level directory. 70 | 71 | ``` 72 | git clone https://github.com/cloudmu/react-redux-starter-kit.git 73 | cd react-redux-starter-kit 74 | npm install 75 | ``` 76 | This will install the dependencies for the client side. 77 | 78 | **You’ll need to have Node installed on your machine**. (Node >= 6 and npm >= 3 are recommended). 79 | 80 | ## While You're Developing... 81 | Whenever you want to run/test the program, `cd` to the project top-level directory. Use these commands: 82 | 83 | ### `npm start` 84 | 85 | Runs the app in the development mode, using the Webpack-provided "development server".
86 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 87 | 88 | The page will reload if you make edits.
89 | You will also see any lint errors in the console.
90 | **Note The web app is up and running now, but some features (such as JWT-based authentication and server alerts/notifications) rely on an API Server. Be sure to run the [API Server](#an-api-server) as well.** 91 | 92 | ### `npm run build` 93 | 94 | Builds the app for production to the `build` folder.
95 | It correctly bundles React in production mode and optimizes the build for the best performance. 96 | 97 | The build is minified and the filenames include the hashes.
98 | Your app is ready to be deployed! 99 | 100 | ### `npm run eject` 101 | 102 | Note: `eject` is an advanced `create-react-app` tool. Read the [how-to](https://github.com/facebookincubator/create-react-app/blob/master/template/README.md) for details. 103 | 104 | ## An API Server 105 | The text and scripts above describe the client-side code that is displayed in the web browser. They rely on the Webpack-provided development server that runs on port 3000. 106 | 107 | This project also contains a separate [API server](https://github.com/cloudmu/react-redux-starter-kit/tree/master/server) that runs on a different port (3001) and handles authentication for certain UI pages using JWT based authentication. 108 | The client login/logout requests will be proxied to the API server as described in: 109 | [Proxying API Requests in Development](https://github.com/facebookincubator/create-react-app/blob/ef94b0561d5afb9b50b905fa5cd3f94e965c69c0/template/README.md#proxying-api-requests-in-development). 110 | 111 | In addition, the server will push notifications to the clients via [Socket.IO](http://socket.io/). 112 | 113 | First you need to open a separate command line window, and run `npm install` under the project's `server` directory. 114 | 115 | ``` 116 | cd react-redux-starter-kit 117 | cd server 118 | npm install 119 | ``` 120 | 121 | Then you can start the API server (under the project's server directory): 122 | 123 | ### `npm run server` 124 | 125 | This starts the API server on port 3001, which listens for authentication (login/logout) requests from the client, and pushes server notifications. At this point, the application is fully operating. 126 | 127 | ## How Do I ... ? 128 | 129 | This project was ported to use [create-react-app](https://github.com/facebookincubator/create-react-app) for handling all assets. 130 | Many questions are answered in its [how-to](https://github.com/facebookincubator/create-react-app/blob/master/template/README.md). 131 | 132 | ## Credits 133 | As a long-time backend developer (who writes preditive analytics and optimization algorithms), I would never have thought of posting a web application using Javascript on Github, were it not for the fateful summer 2015 when I stumbled upon a [30 minutes video](https://www.youtube.com/watch?v=xsSnOQynTHs) by [Dan Abramov](https://twitter.com/dan_abramov), and his inspiring work on [Redux](https://github.com/rackt/redux). 134 | Thank you. 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-starter-kit", 3 | "homepage": "https://cloudmu.github.com/react-redux-starter-kit", 4 | "version": "1.0.0", 5 | "description": "React/Redux Starter Kit with Zero Build Configuration", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/cloudmu/react-redux-starter-kit.git" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "reactjs", 13 | "redux", 14 | "react-router", 15 | "bootstrap", 16 | "webpack", 17 | "jwt", 18 | "auth0", 19 | "dashboard", 20 | "template", 21 | "boilerplate", 22 | "create-react-app", 23 | "websockets", 24 | "socket.io", 25 | "PWA", 26 | "Progressive Web Apps" 27 | 28 | ], 29 | "author": "twitter.com/_cloudmu (http://github.com/cloudmu)", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/cloudmu/react-redux-starter-kit/issues" 33 | }, 34 | "proxy": "http://localhost:3001", 35 | "devDependencies": { 36 | "react-scripts": "1.0.17" 37 | }, 38 | "dependencies": { 39 | "bootstrap": "^4.0.0-beta.2", 40 | "classnames": "^2.2.5", 41 | "font-awesome": "^4.7.0", 42 | "isomorphic-fetch": "^2.2.1", 43 | "jquery": "^3.2.1", 44 | "jwt-decode": "^2.2.0", 45 | "lodash": "^4.17.4", 46 | "popper.js": "^1.12.6", 47 | "prop-types": "^15.6.0", 48 | "react": "^16.0.0", 49 | "react-dom": "^16.0.0", 50 | "react-redux": "^5.0.6", 51 | "react-router-dom": "^4.2.2", 52 | "react-virtualized": "^9.12.0", 53 | "redux": "^3.7.2", 54 | "redux-logger": "^3.0.6", 55 | "redux-thunk": "^2.2.0", 56 | "socket.io-client": "^2.0.4" 57 | }, 58 | "scripts": { 59 | "start": "react-scripts start", 60 | "build": "react-scripts build", 61 | "test": "react-scripts test --env=jsdom", 62 | "eject": "react-scripts eject" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmu/react-redux-starter-kit/d14f4affb28070adc15a360888810e3e0067a15c/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 21 | React Redux Starter Kit 22 | 23 | 24 |
25 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Redux Starter Kit", 3 | "name": "React Redux Starter Kit", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "256x256", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmu/react-redux-starter-kit/d14f4affb28070adc15a360888810e3e0067a15c/screenshot.png -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Overview of the Server 3 | 4 | This is the API server to provide authentication API (Restful services) based [JWT](https://jwt.io/). 5 | It also shows how to push server notifications to clients via [Socket.IO](http://socket.io/). 6 | 7 | The simple implemenation show cases the following JWT and Socket.IO use cases: 8 | 9 | * Upon user authentication, the user profile is signed and the jwt token is returned as response to the client. 10 | It's expected the jwt token will be included in the subsequent client requests (as authorization header). 11 | 12 | * For subsequent client requests, the server will verify the jwt token extracted from the request headers. 13 | 14 | * Invalid requests are considered unauthorized access and should be rejected. 15 | 16 | * In addition, the server will push notifications (such as login/logout events) to the clients via [Socket.IO](http://socket.io/). 17 | 18 | ## Server Side Scripts 19 | In order for the JWT based authentication and the server notifications to work, you need to run this API server. The client login/logout requests will be proxied to 20 | the API server, thanks to the create-react-app feature [Proxying API Requests in Development](https://github.com/facebookincubator/create-react-app/blob/ef94b0561d5afb9b50b905fa5cd3f94e965c69c0/template/README.md#proxying-api-requests-in-development). 21 | 22 | In addition, the server will push notifications to the clients via [Socket.IO](http://socket.io/). 23 | 24 | Under this server directory, you can run (in a separate command line window): 25 | 26 | ### `npm install` 27 | This will install the dependencies for the server side. 28 | 29 | ### `npm run server` 30 | This will run the server on port 3001, which will be listening to the authentication requests (login/logout from the client), and pushes server notifications 31 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "This is for the api server", 3 | "name": "server-api", 4 | "dependencies": { 5 | "express": "^4.16.2", 6 | "body-parser": "^1.18.2", 7 | "jsonwebtoken": "^8.1.0", 8 | "socket.io": "^2.0.4" 9 | }, 10 | 11 | "scripts": { 12 | "server": "node server.js" 13 | } 14 | } -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a simple express server, to show basic authentication services (login and logout requests) 3 | * based JWT, and basic socket.io. 4 | * 5 | * Once a user is authenticated, a jwt token will be returned as response to the client. 6 | * It's expected the jwt token will be included in the subsequent client requests. The server 7 | * can then protect the services by verifying the jwt token in the subsequent API requests. 8 | * 9 | * The server will also broadcast the login/logout events to connected clients via socket.io. 10 | * 11 | */ 12 | var express = require("express"); 13 | var bodyParser = require("body-parser"); 14 | var jwt = require("jsonwebtoken"); 15 | var port = 3001; 16 | 17 | // Configure app to use bodyParser to parse json data 18 | var app = express(); 19 | var server = require("http").createServer(app); 20 | app.use(bodyParser.urlencoded({ extended: true })); 21 | app.use(bodyParser.json()); 22 | 23 | // and support socket io 24 | var io = require("socket.io")(server); 25 | 26 | // Test server is working (GET http://localhost:3001/api) 27 | app.get("/api/", function(req, res) { 28 | res.json({ message: "Hi, welcome to the server api!" }); 29 | }); 30 | 31 | // This should be well-guarded secret on the server (in a file or database). 32 | var JWT_SECRET = "JWT Rocks!"; 33 | 34 | // JWT based login service. 35 | app.post("/api/login", function(req, res) { 36 | console.log("Requesting /api/login ..."); 37 | 38 | const credentials = req.body; 39 | 40 | // In real world credentials should be authenticated against database. 41 | // For our purpose it's hard-coded: 42 | if (credentials.user === "admin" && credentials.password === "password") { 43 | // Once authenticated, the user profiles is signed and the jwt token is returned as response to the client. 44 | // It's expected the jwt token will be included in the subsequent client requests. 45 | const profile = { user: credentials.user, role: "ADMIN" }; 46 | const jwtToken = jwt.sign(profile, JWT_SECRET, { expiresIn: 5 * 60 }); // expires in 300 seconds (5 min) 47 | res.status(200).json({ 48 | id_token: jwtToken 49 | }); 50 | 51 | alertClients("info", `User '${credentials.user}' just logged in`); 52 | } else { 53 | res.status(401).json({ message: "Invalid user/password" }); 54 | 55 | alertClients("error", `User '${credentials.user}' just failed to login`); 56 | } 57 | }); 58 | 59 | // Alerts all clents via socket io. 60 | function alertClients(type, msg) { 61 | console.log("SocketIO alerting clients: ", msg); 62 | io.sockets.emit("alert", { message: msg, time: new Date(), type }); 63 | } 64 | 65 | /** 66 | * Util function to extract jwt token from the authorization header 67 | */ 68 | function extractToken(req) { 69 | if ( 70 | req.headers.authorization && 71 | req.headers.authorization.split(" ")[0] === "Bearer" 72 | ) { 73 | return req.headers.authorization.split(" ")[1]; 74 | } 75 | return null; 76 | } 77 | 78 | // Logout api. For illustration purpose we show how to check if the request is from an authorized user by 79 | // verifying the jwt token included in the request header. The same approach can be used to restrict access 80 | // to other (more intersting) API calls. 81 | app.post("/api/logout", function(req, res) { 82 | console.log("Requesting /api/logout ..."); 83 | 84 | var jwtToken = extractToken(req); 85 | try { 86 | var profile = jwt.verify(jwtToken, JWT_SECRET); 87 | res.status(200).json({ message: `User ${profile.user} logged out` }); 88 | 89 | alertClients("info", `User '${profile.user}' just logged out`); 90 | } catch (err) { 91 | console.log("jwt verify error", err); 92 | res.status(500).json({ message: "Invalid jwt token" }); 93 | 94 | alertClients("error", `JWT verify error`); 95 | } 96 | }); 97 | 98 | // Start the server 99 | server.listen(port); 100 | console.log("Server is listening on port " + port); 101 | -------------------------------------------------------------------------------- /server/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@1.3.3: 6 | version "1.3.3" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" 8 | dependencies: 9 | mime-types "~2.1.11" 10 | negotiator "0.6.1" 11 | 12 | accepts@~1.3.4: 13 | version "1.3.4" 14 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" 15 | dependencies: 16 | mime-types "~2.1.16" 17 | negotiator "0.6.1" 18 | 19 | after@0.8.2: 20 | version "0.8.2" 21 | resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" 22 | 23 | array-flatten@1.1.1: 24 | version "1.1.1" 25 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 26 | 27 | arraybuffer.slice@0.0.6: 28 | version "0.0.6" 29 | resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca" 30 | 31 | backo2@1.0.2: 32 | version "1.0.2" 33 | resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" 34 | 35 | base64-arraybuffer@0.1.5: 36 | version "0.1.5" 37 | resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" 38 | 39 | base64id@1.0.0: 40 | version "1.0.0" 41 | resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6" 42 | 43 | base64url@2.0.0, base64url@^2.0.0: 44 | version "2.0.0" 45 | resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" 46 | 47 | better-assert@~1.0.0: 48 | version "1.0.2" 49 | resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" 50 | dependencies: 51 | callsite "1.0.0" 52 | 53 | blob@0.0.4: 54 | version "0.0.4" 55 | resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" 56 | 57 | body-parser@1.18.2, body-parser@^1.18.2: 58 | version "1.18.2" 59 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" 60 | dependencies: 61 | bytes "3.0.0" 62 | content-type "~1.0.4" 63 | debug "2.6.9" 64 | depd "~1.1.1" 65 | http-errors "~1.6.2" 66 | iconv-lite "0.4.19" 67 | on-finished "~2.3.0" 68 | qs "6.5.1" 69 | raw-body "2.3.2" 70 | type-is "~1.6.15" 71 | 72 | buffer-equal-constant-time@1.0.1: 73 | version "1.0.1" 74 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 75 | 76 | bytes@3.0.0: 77 | version "3.0.0" 78 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" 79 | 80 | callsite@1.0.0: 81 | version "1.0.0" 82 | resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" 83 | 84 | component-bind@1.0.0: 85 | version "1.0.0" 86 | resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" 87 | 88 | component-emitter@1.2.1: 89 | version "1.2.1" 90 | resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" 91 | 92 | component-inherit@0.0.3: 93 | version "0.0.3" 94 | resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" 95 | 96 | content-disposition@0.5.2: 97 | version "0.5.2" 98 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 99 | 100 | content-type@~1.0.4: 101 | version "1.0.4" 102 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 103 | 104 | cookie-signature@1.0.6: 105 | version "1.0.6" 106 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 107 | 108 | cookie@0.3.1: 109 | version "0.3.1" 110 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 111 | 112 | debug@2.3.3: 113 | version "2.3.3" 114 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c" 115 | dependencies: 116 | ms "0.7.2" 117 | 118 | debug@2.6.9: 119 | version "2.6.9" 120 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 121 | dependencies: 122 | ms "2.0.0" 123 | 124 | debug@~2.6.4, debug@~2.6.6: 125 | version "2.6.7" 126 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" 127 | dependencies: 128 | ms "2.0.0" 129 | 130 | depd@1.1.1, depd@~1.1.1: 131 | version "1.1.1" 132 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" 133 | 134 | destroy@~1.0.4: 135 | version "1.0.4" 136 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 137 | 138 | ecdsa-sig-formatter@1.0.9: 139 | version "1.0.9" 140 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" 141 | dependencies: 142 | base64url "^2.0.0" 143 | safe-buffer "^5.0.1" 144 | 145 | ee-first@1.1.1: 146 | version "1.1.1" 147 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 148 | 149 | encodeurl@~1.0.1: 150 | version "1.0.1" 151 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" 152 | 153 | engine.io-client@~3.1.0: 154 | version "3.1.1" 155 | resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.1.1.tgz#415a9852badb14fa008fa3ef1e31608db6761325" 156 | dependencies: 157 | component-emitter "1.2.1" 158 | component-inherit "0.0.3" 159 | debug "~2.6.4" 160 | engine.io-parser "~2.1.1" 161 | has-cors "1.1.0" 162 | indexof "0.0.1" 163 | parsejson "0.0.3" 164 | parseqs "0.0.5" 165 | parseuri "0.0.5" 166 | ws "~2.3.1" 167 | xmlhttprequest-ssl "1.5.3" 168 | yeast "0.1.2" 169 | 170 | engine.io-parser@~2.1.0, engine.io-parser@~2.1.1: 171 | version "2.1.1" 172 | resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.1.1.tgz#e0fb3f0e0462f7f58bb77c1a52e9f5a7e26e4668" 173 | dependencies: 174 | after "0.8.2" 175 | arraybuffer.slice "0.0.6" 176 | base64-arraybuffer "0.1.5" 177 | blob "0.0.4" 178 | has-binary2 "~1.0.2" 179 | 180 | engine.io@~3.1.0: 181 | version "3.1.0" 182 | resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.1.0.tgz#5ca438e3ce9fdbc915c4a21c8dd9e1266706e57e" 183 | dependencies: 184 | accepts "1.3.3" 185 | base64id "1.0.0" 186 | cookie "0.3.1" 187 | debug "~2.6.4" 188 | engine.io-parser "~2.1.0" 189 | ws "~2.3.1" 190 | optionalDependencies: 191 | uws "~0.14.4" 192 | 193 | escape-html@~1.0.3: 194 | version "1.0.3" 195 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 196 | 197 | etag@~1.8.1: 198 | version "1.8.1" 199 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 200 | 201 | express@^4.16.2: 202 | version "4.16.2" 203 | resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" 204 | dependencies: 205 | accepts "~1.3.4" 206 | array-flatten "1.1.1" 207 | body-parser "1.18.2" 208 | content-disposition "0.5.2" 209 | content-type "~1.0.4" 210 | cookie "0.3.1" 211 | cookie-signature "1.0.6" 212 | debug "2.6.9" 213 | depd "~1.1.1" 214 | encodeurl "~1.0.1" 215 | escape-html "~1.0.3" 216 | etag "~1.8.1" 217 | finalhandler "1.1.0" 218 | fresh "0.5.2" 219 | merge-descriptors "1.0.1" 220 | methods "~1.1.2" 221 | on-finished "~2.3.0" 222 | parseurl "~1.3.2" 223 | path-to-regexp "0.1.7" 224 | proxy-addr "~2.0.2" 225 | qs "6.5.1" 226 | range-parser "~1.2.0" 227 | safe-buffer "5.1.1" 228 | send "0.16.1" 229 | serve-static "1.13.1" 230 | setprototypeof "1.1.0" 231 | statuses "~1.3.1" 232 | type-is "~1.6.15" 233 | utils-merge "1.0.1" 234 | vary "~1.1.2" 235 | 236 | finalhandler@1.1.0: 237 | version "1.1.0" 238 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" 239 | dependencies: 240 | debug "2.6.9" 241 | encodeurl "~1.0.1" 242 | escape-html "~1.0.3" 243 | on-finished "~2.3.0" 244 | parseurl "~1.3.2" 245 | statuses "~1.3.1" 246 | unpipe "~1.0.0" 247 | 248 | forwarded@~0.1.2: 249 | version "0.1.2" 250 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 251 | 252 | fresh@0.5.2: 253 | version "0.5.2" 254 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 255 | 256 | has-binary2@~1.0.2: 257 | version "1.0.2" 258 | resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.2.tgz#e83dba49f0b9be4d026d27365350d9f03f54be98" 259 | dependencies: 260 | isarray "2.0.1" 261 | 262 | has-cors@1.1.0: 263 | version "1.1.0" 264 | resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" 265 | 266 | http-errors@1.6.2, http-errors@~1.6.2: 267 | version "1.6.2" 268 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" 269 | dependencies: 270 | depd "1.1.1" 271 | inherits "2.0.3" 272 | setprototypeof "1.0.3" 273 | statuses ">= 1.3.1 < 2" 274 | 275 | iconv-lite@0.4.19: 276 | version "0.4.19" 277 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" 278 | 279 | indexof@0.0.1: 280 | version "0.0.1" 281 | resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" 282 | 283 | inherits@2.0.3: 284 | version "2.0.3" 285 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 286 | 287 | ipaddr.js@1.5.2: 288 | version "1.5.2" 289 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" 290 | 291 | isarray@2.0.1: 292 | version "2.0.1" 293 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" 294 | 295 | jsonwebtoken@^8.1.0: 296 | version "8.1.0" 297 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz#c6397cd2e5fd583d65c007a83dc7bb78e6982b83" 298 | dependencies: 299 | jws "^3.1.4" 300 | lodash.includes "^4.3.0" 301 | lodash.isboolean "^3.0.3" 302 | lodash.isinteger "^4.0.4" 303 | lodash.isnumber "^3.0.3" 304 | lodash.isplainobject "^4.0.6" 305 | lodash.isstring "^4.0.1" 306 | lodash.once "^4.0.0" 307 | ms "^2.0.0" 308 | xtend "^4.0.1" 309 | 310 | jwa@^1.1.4: 311 | version "1.1.5" 312 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" 313 | dependencies: 314 | base64url "2.0.0" 315 | buffer-equal-constant-time "1.0.1" 316 | ecdsa-sig-formatter "1.0.9" 317 | safe-buffer "^5.0.1" 318 | 319 | jws@^3.1.4: 320 | version "3.1.4" 321 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" 322 | dependencies: 323 | base64url "^2.0.0" 324 | jwa "^1.1.4" 325 | safe-buffer "^5.0.1" 326 | 327 | lodash.includes@^4.3.0: 328 | version "4.3.0" 329 | resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" 330 | 331 | lodash.isboolean@^3.0.3: 332 | version "3.0.3" 333 | resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" 334 | 335 | lodash.isinteger@^4.0.4: 336 | version "4.0.4" 337 | resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" 338 | 339 | lodash.isnumber@^3.0.3: 340 | version "3.0.3" 341 | resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" 342 | 343 | lodash.isplainobject@^4.0.6: 344 | version "4.0.6" 345 | resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" 346 | 347 | lodash.isstring@^4.0.1: 348 | version "4.0.1" 349 | resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" 350 | 351 | lodash.once@^4.0.0: 352 | version "4.1.1" 353 | resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" 354 | 355 | media-typer@0.3.0: 356 | version "0.3.0" 357 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 358 | 359 | merge-descriptors@1.0.1: 360 | version "1.0.1" 361 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 362 | 363 | methods@~1.1.2: 364 | version "1.1.2" 365 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 366 | 367 | mime-db@~1.26.0: 368 | version "1.26.0" 369 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff" 370 | 371 | mime-db@~1.27.0: 372 | version "1.27.0" 373 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" 374 | 375 | mime-db@~1.30.0: 376 | version "1.30.0" 377 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" 378 | 379 | mime-types@~2.1.11: 380 | version "2.1.14" 381 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee" 382 | dependencies: 383 | mime-db "~1.26.0" 384 | 385 | mime-types@~2.1.15: 386 | version "2.1.15" 387 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" 388 | dependencies: 389 | mime-db "~1.27.0" 390 | 391 | mime-types@~2.1.16: 392 | version "2.1.17" 393 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" 394 | dependencies: 395 | mime-db "~1.30.0" 396 | 397 | mime@1.4.1: 398 | version "1.4.1" 399 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" 400 | 401 | ms@0.7.2: 402 | version "0.7.2" 403 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" 404 | 405 | ms@2.0.0, ms@^2.0.0: 406 | version "2.0.0" 407 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 408 | 409 | negotiator@0.6.1: 410 | version "0.6.1" 411 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 412 | 413 | object-component@0.0.3: 414 | version "0.0.3" 415 | resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" 416 | 417 | on-finished@~2.3.0: 418 | version "2.3.0" 419 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 420 | dependencies: 421 | ee-first "1.1.1" 422 | 423 | parsejson@0.0.3: 424 | version "0.0.3" 425 | resolved "https://registry.yarnpkg.com/parsejson/-/parsejson-0.0.3.tgz#ab7e3759f209ece99437973f7d0f1f64ae0e64ab" 426 | dependencies: 427 | better-assert "~1.0.0" 428 | 429 | parseqs@0.0.5: 430 | version "0.0.5" 431 | resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" 432 | dependencies: 433 | better-assert "~1.0.0" 434 | 435 | parseuri@0.0.5: 436 | version "0.0.5" 437 | resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" 438 | dependencies: 439 | better-assert "~1.0.0" 440 | 441 | parseurl@~1.3.2: 442 | version "1.3.2" 443 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" 444 | 445 | path-to-regexp@0.1.7: 446 | version "0.1.7" 447 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 448 | 449 | proxy-addr@~2.0.2: 450 | version "2.0.2" 451 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" 452 | dependencies: 453 | forwarded "~0.1.2" 454 | ipaddr.js "1.5.2" 455 | 456 | qs@6.5.1: 457 | version "6.5.1" 458 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" 459 | 460 | range-parser@~1.2.0: 461 | version "1.2.0" 462 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 463 | 464 | raw-body@2.3.2: 465 | version "2.3.2" 466 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" 467 | dependencies: 468 | bytes "3.0.0" 469 | http-errors "1.6.2" 470 | iconv-lite "0.4.19" 471 | unpipe "1.0.0" 472 | 473 | safe-buffer@5.1.1: 474 | version "5.1.1" 475 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 476 | 477 | safe-buffer@^5.0.1, safe-buffer@~5.0.1: 478 | version "5.0.1" 479 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" 480 | 481 | send@0.16.1: 482 | version "0.16.1" 483 | resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3" 484 | dependencies: 485 | debug "2.6.9" 486 | depd "~1.1.1" 487 | destroy "~1.0.4" 488 | encodeurl "~1.0.1" 489 | escape-html "~1.0.3" 490 | etag "~1.8.1" 491 | fresh "0.5.2" 492 | http-errors "~1.6.2" 493 | mime "1.4.1" 494 | ms "2.0.0" 495 | on-finished "~2.3.0" 496 | range-parser "~1.2.0" 497 | statuses "~1.3.1" 498 | 499 | serve-static@1.13.1: 500 | version "1.13.1" 501 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719" 502 | dependencies: 503 | encodeurl "~1.0.1" 504 | escape-html "~1.0.3" 505 | parseurl "~1.3.2" 506 | send "0.16.1" 507 | 508 | setprototypeof@1.0.3: 509 | version "1.0.3" 510 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 511 | 512 | setprototypeof@1.1.0: 513 | version "1.1.0" 514 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" 515 | 516 | socket.io-adapter@~1.1.0: 517 | version "1.1.0" 518 | resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.0.tgz#c7aa46501dd556c2cb8a28af8ff95c0b5e1daa4c" 519 | dependencies: 520 | debug "2.3.3" 521 | 522 | socket.io-client@2.0.4: 523 | version "2.0.4" 524 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.0.4.tgz#0918a552406dc5e540b380dcd97afc4a64332f8e" 525 | dependencies: 526 | backo2 "1.0.2" 527 | base64-arraybuffer "0.1.5" 528 | component-bind "1.0.0" 529 | component-emitter "1.2.1" 530 | debug "~2.6.4" 531 | engine.io-client "~3.1.0" 532 | has-cors "1.1.0" 533 | indexof "0.0.1" 534 | object-component "0.0.3" 535 | parseqs "0.0.5" 536 | parseuri "0.0.5" 537 | socket.io-parser "~3.1.1" 538 | to-array "0.1.4" 539 | 540 | socket.io-parser@~3.1.1: 541 | version "3.1.2" 542 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.1.2.tgz#dbc2282151fc4faebbe40aeedc0772eba619f7f2" 543 | dependencies: 544 | component-emitter "1.2.1" 545 | debug "~2.6.4" 546 | has-binary2 "~1.0.2" 547 | isarray "2.0.1" 548 | 549 | socket.io@^2.0.4: 550 | version "2.0.4" 551 | resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.0.4.tgz#c1a4590ceff87ecf13c72652f046f716b29e6014" 552 | dependencies: 553 | debug "~2.6.6" 554 | engine.io "~3.1.0" 555 | socket.io-adapter "~1.1.0" 556 | socket.io-client "2.0.4" 557 | socket.io-parser "~3.1.1" 558 | 559 | "statuses@>= 1.3.1 < 2", statuses@~1.3.1: 560 | version "1.3.1" 561 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 562 | 563 | to-array@0.1.4: 564 | version "0.1.4" 565 | resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" 566 | 567 | type-is@~1.6.15: 568 | version "1.6.15" 569 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" 570 | dependencies: 571 | media-typer "0.3.0" 572 | mime-types "~2.1.15" 573 | 574 | ultron@~1.1.0: 575 | version "1.1.0" 576 | resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" 577 | 578 | unpipe@1.0.0, unpipe@~1.0.0: 579 | version "1.0.0" 580 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 581 | 582 | utils-merge@1.0.1: 583 | version "1.0.1" 584 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 585 | 586 | uws@~0.14.4: 587 | version "0.14.5" 588 | resolved "https://registry.yarnpkg.com/uws/-/uws-0.14.5.tgz#67aaf33c46b2a587a5f6666d00f7691328f149dc" 589 | 590 | vary@~1.1.2: 591 | version "1.1.2" 592 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 593 | 594 | ws@~2.3.1: 595 | version "2.3.1" 596 | resolved "https://registry.yarnpkg.com/ws/-/ws-2.3.1.tgz#6b94b3e447cb6a363f785eaf94af6359e8e81c80" 597 | dependencies: 598 | safe-buffer "~5.0.1" 599 | ultron "~1.1.0" 600 | 601 | xmlhttprequest-ssl@1.5.3: 602 | version "1.5.3" 603 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d" 604 | 605 | xtend@^4.0.1: 606 | version "4.0.1" 607 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 608 | 609 | yeast@0.1.2: 610 | version "0.1.2" 611 | resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" 612 | -------------------------------------------------------------------------------- /src/actions/alerts.js: -------------------------------------------------------------------------------- 1 | export const CONNECT_SUCCESS = "CONNECT_SUCCESS"; 2 | export const CONNECT_ERROR = "CONNECT_ERROR"; 3 | export const ALERT = "ALERT"; 4 | export const DISMISS = "DISMISS"; 5 | 6 | export function connectSuccess() { 7 | return { 8 | type: CONNECT_SUCCESS 9 | }; 10 | } 11 | 12 | export function connectError() { 13 | return { 14 | type: CONNECT_ERROR 15 | }; 16 | } 17 | 18 | export function alert(payload) { 19 | return { 20 | type: ALERT, 21 | payload 22 | }; 23 | } 24 | 25 | export function dismiss() { 26 | return { 27 | type: DISMISS 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/actions/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | callApi, 3 | ID_TOKEN, 4 | loadIdToken, 5 | setIdToken, 6 | removeIdToken, 7 | decodeUserProfile 8 | } from "../utils/apiUtils"; 9 | 10 | export const LOGIN_REQUEST = "LOGIN_REQUEST"; 11 | export const LOGIN_SUCCESS = "LOGIN_SUCCESS"; 12 | export const LOGIN_FAILURE = "LOGIN_FAILURE"; 13 | 14 | export const LOGOUT_REQUEST = "LOGOUT_REQUEST"; 15 | export const LOGOUT_SUCCESS = "LOGOUT_SUCCESS"; 16 | export const LOGOUT_FAILURE = "LOGOUT_FAILURE"; 17 | 18 | function loginRequest(user) { 19 | return { 20 | type: LOGIN_REQUEST, 21 | user 22 | }; 23 | } 24 | 25 | function loginSuccess(payload) { 26 | const idToken = payload[ID_TOKEN]; 27 | setIdToken(idToken); 28 | const profile = decodeUserProfile(idToken); 29 | return { 30 | type: LOGIN_SUCCESS, 31 | user: profile.user, 32 | role: profile.role 33 | }; 34 | } 35 | 36 | function loginFailure(error) { 37 | removeIdToken(); 38 | return { 39 | type: LOGIN_FAILURE, 40 | error 41 | }; 42 | } 43 | 44 | export function login(user, password) { 45 | const config = { 46 | method: "post", 47 | headers: { 48 | Accept: "application/json", 49 | "Content-Type": "application/json" 50 | }, 51 | body: JSON.stringify({ 52 | user, 53 | password 54 | }) 55 | }; 56 | 57 | return callApi( 58 | "/api/login", 59 | config, 60 | loginRequest(user), 61 | loginSuccess, 62 | loginFailure 63 | ); 64 | } 65 | 66 | function logoutRequest(user) { 67 | removeIdToken(); 68 | return { 69 | type: LOGOUT_REQUEST, 70 | user 71 | }; 72 | } 73 | 74 | function logoutSuccess(payload) { 75 | removeIdToken(); 76 | return { 77 | type: LOGOUT_SUCCESS, 78 | user: payload.user 79 | }; 80 | } 81 | 82 | function logoutFailure(error) { 83 | return { 84 | type: LOGOUT_FAILURE, 85 | error 86 | }; 87 | } 88 | 89 | export function logout(user) { 90 | const idToken = loadIdToken(); 91 | const config = { 92 | method: "post", 93 | headers: { 94 | Accept: "application/json", 95 | "Content-Type": "application/json", 96 | Authorization: `Bearer ${idToken}` 97 | }, 98 | body: JSON.stringify({ 99 | user 100 | }) 101 | }; 102 | 103 | return callApi( 104 | "/api/logout", 105 | config, 106 | logoutRequest, 107 | logoutSuccess, 108 | logoutFailure 109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /src/actions/repos.js: -------------------------------------------------------------------------------- 1 | import { callApi } from "../utils/apiUtils"; 2 | 3 | export const SELECT_REPOS_PAGE = "SELECT_REPOS_PAGE"; 4 | export const INVALIDATE_REPOS_PAGE = "INVALIDATE_REPOS_PAGE"; 5 | 6 | export const REPOS_REQUEST = "REPOS_REQUEST"; 7 | export const REPOS_SUCCESS = "REPOS_SUCCESS"; 8 | export const REPOS_FAILURE = "REPOS_FAILURE"; 9 | 10 | export function selectReposPage(page) { 11 | return { 12 | type: SELECT_REPOS_PAGE, 13 | page 14 | }; 15 | } 16 | 17 | export function invalidateReposPage(page) { 18 | return { 19 | type: INVALIDATE_REPOS_PAGE, 20 | page 21 | }; 22 | } 23 | 24 | function reposRequest(page) { 25 | return { 26 | type: REPOS_REQUEST, 27 | page 28 | }; 29 | } 30 | 31 | // This is a curried function that takes page as argument, 32 | // and expects payload as argument to be passed upon API call success. 33 | function reposSuccess(page) { 34 | return function(payload) { 35 | return { 36 | type: REPOS_SUCCESS, 37 | page, 38 | repos: payload.items, 39 | totalCount: payload.total_count 40 | }; 41 | }; 42 | } 43 | 44 | // This is a curried function that takes page as argument, 45 | // and expects error as argument to be passed upon API call failure. 46 | function reposFailure(page) { 47 | return function(error) { 48 | return { 49 | type: REPOS_FAILURE, 50 | page, 51 | error 52 | }; 53 | }; 54 | } 55 | 56 | const API_ROOT = "https://api.github.com"; 57 | 58 | function fetchTopRepos(page) { 59 | const url = `${API_ROOT}/search/repositories?q=stars:>10000&order=desc&page=${page}`; 60 | return callApi( 61 | url, 62 | null, 63 | reposRequest(page), 64 | reposSuccess(page), 65 | reposFailure(page) 66 | ); 67 | } 68 | 69 | function shouldFetchRepos(state, page) { 70 | // Check cache first 71 | const repos = state.reposByPage[page]; 72 | if (!repos) { 73 | // Not cached, should fetch 74 | return true; 75 | } 76 | 77 | if (repos.isFetching) { 78 | // Shouldn't fetch since fetching is running 79 | return false; 80 | } 81 | 82 | // Should fetch if cache was invalidate 83 | return repos.didInvalidate; 84 | } 85 | 86 | export function fetchTopReposIfNeeded(page) { 87 | return (dispatch, getState) => { 88 | if (shouldFetchRepos(getState(), page)) { 89 | return dispatch(fetchTopRepos(page)); 90 | } 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /src/actions/users.js: -------------------------------------------------------------------------------- 1 | import { callApi } from "../utils/apiUtils"; 2 | 3 | export const SELECT_USERS_PAGE = "SELECT_USERS_PAGE"; 4 | export const INVALIDATE_USERS_PAGE = "INVALIDATE_USERS_PAGE"; 5 | 6 | export const USERS_REQUEST = "USERS_REQUEST"; 7 | export const USERS_SUCCESS = "USERS_SUCCESS"; 8 | export const USERS_FAILURE = "USERS_FAILURE"; 9 | 10 | export function selectUsersPage(page) { 11 | return { 12 | type: SELECT_USERS_PAGE, 13 | page 14 | }; 15 | } 16 | 17 | export function invalidateUsersPage(page) { 18 | return { 19 | type: INVALIDATE_USERS_PAGE, 20 | page 21 | }; 22 | } 23 | 24 | function usersRequest(page) { 25 | return { 26 | type: USERS_REQUEST, 27 | page 28 | }; 29 | } 30 | 31 | // This is a curried function that takes page as argument, 32 | // and expects payload as argument to be passed upon API call success. 33 | function usersSuccess(page) { 34 | return function(payload) { 35 | return { 36 | type: USERS_SUCCESS, 37 | page, 38 | users: payload.items, 39 | totalCount: payload.total_count 40 | }; 41 | }; 42 | } 43 | 44 | // This is a curried function that takes page as argument, 45 | // and expects error as argument to be passed upon API call failure. 46 | function usersFailure(page) { 47 | return function(error) { 48 | return { 49 | type: USERS_FAILURE, 50 | page, 51 | error 52 | }; 53 | }; 54 | } 55 | 56 | const API_ROOT = "https://api.github.com"; 57 | 58 | function fetchTopUsers(page) { 59 | const url = `${API_ROOT}/search/users?q=followers:>1000&order=desc&page=${page}`; 60 | return callApi( 61 | url, 62 | null, 63 | usersRequest(page), 64 | usersSuccess(page), 65 | usersFailure(page) 66 | ); 67 | } 68 | 69 | function shouldFetchUsers(state, page) { 70 | // Check cache first 71 | const users = state.usersByPage[page]; 72 | if (!users) { 73 | // Not cached, should fetch 74 | return true; 75 | } 76 | 77 | if (users.isFetching) { 78 | // Shouldn't fetch since fetching is running 79 | return false; 80 | } 81 | 82 | // Should fetch if cache was invalidate 83 | return users.didInvalidate; 84 | } 85 | 86 | export function fetchTopUsersIfNeeded(page) { 87 | return (dispatch, getState) => { 88 | if (shouldFetchUsers(getState(), page)) { 89 | return dispatch(fetchTopUsers(page)); 90 | } 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /src/components/footer/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./footer.css"; 4 | 5 | const Footer = () => ( 6 | 29 | ); 30 | 31 | export default Footer; 32 | -------------------------------------------------------------------------------- /src/components/footer/footer.css: -------------------------------------------------------------------------------- 1 | /* Sticky footer styles 2 | -------------------------------------------------- */ 3 | html { 4 | position: relative; 5 | min-height: 100%; 6 | } 7 | body { 8 | /* Margin bottom by footer height */ 9 | margin-bottom: 3em; 10 | } 11 | .footer { 12 | position: absolute; 13 | bottom: 0; 14 | width: 100%; 15 | /* Set the fixed height of the footer here */ 16 | height: 3em; 17 | line-height: 3em; 18 | background-color: #f5f5f5; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/header/Alerts.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { dismiss } from "../../actions/alerts"; 5 | import connectToAlerts from "../../utils/socketUtils"; 6 | import classNames from "classnames"; 7 | 8 | class Alerts extends Component { 9 | dismiss = () => { 10 | const { dispatch } = this.props; 11 | dispatch(dismiss()); 12 | }; 13 | 14 | reconnect = () => { 15 | const { store } = this.context; 16 | connectToAlerts(store); 17 | }; 18 | 19 | alert = (type, message, time) => { 20 | const iconClass = classNames( 21 | "fa", 22 | { "fa-info-circle text-success": type === "info" }, 23 | { "fa-warning text-danger": type === "error" } 24 | ); 25 | const localTime = new Date(time); 26 | return ( 27 | 28 | 29 | {message} 30 | {" "} 31 | 32 | {localTime.toLocaleString()} 33 | 34 | 35 | ); 36 | }; 37 | 38 | render() { 39 | const { alerts, hasError } = this.props; 40 | const count = (alerts && alerts.length) || 0; 41 | const badge = count <= 1 ? `${count} new message` : `${count} new messages`; 42 | return ( 43 |
  • 44 | 55 | 114 |
  • 115 | ); 116 | } 117 | } 118 | 119 | Alerts.contextTypes = { 120 | store: PropTypes.object.isRequired 121 | }; 122 | 123 | Alerts.propTypes = { 124 | alerts: PropTypes.array.isRequired, 125 | hasError: PropTypes.bool.isRequired, 126 | dispatch: PropTypes.func.isRequired 127 | }; 128 | 129 | function mapStateToProps(state) { 130 | const { alerts } = state; 131 | return { alerts: alerts.alerts, hasError: alerts.hasError }; 132 | } 133 | 134 | export default connect(mapStateToProps)(Alerts); 135 | -------------------------------------------------------------------------------- /src/components/header/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | import { withRouter } from "react-router"; 5 | import UserProfile from "./UserProfile"; 6 | import Alerts from "./Alerts"; 7 | import "./header.css"; 8 | 9 | class Header extends Component { 10 | onLogoutClick = event => { 11 | event.preventDefault(); 12 | this.props.handleLogout(); 13 | this.props.history.replace("/login"); 14 | }; 15 | 16 | render() { 17 | const { user } = this.props; 18 | const pathname = this.props.history.location.pathname; 19 | const isLoginPage = pathname.indexOf("login") > -1; 20 | const isAboutPage = pathname.indexOf("about") > -1; 21 | const isUsersPage = pathname.indexOf("users") > -1; 22 | const isReposPage = pathname.indexOf("repos") > -1; 23 | 24 | return ( 25 | !isLoginPage && 26 | 69 | ); 70 | } 71 | } 72 | 73 | Header.propTypes = { 74 | user: PropTypes.string, 75 | handleLogout: PropTypes.func.isRequired 76 | }; 77 | 78 | export default withRouter(Header); 79 | -------------------------------------------------------------------------------- /src/components/header/UserProfile.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const UserProfile = ({ user, handleLogout }) => { 5 | return ( 6 |
  • 7 | 19 | 35 |
  • 36 | ); 37 | }; 38 | 39 | UserProfile.propTypes = { 40 | user: PropTypes.string, 41 | handleLogout: PropTypes.func.isRequired 42 | }; 43 | 44 | export default UserProfile; 45 | -------------------------------------------------------------------------------- /src/components/header/header.css: -------------------------------------------------------------------------------- 1 | 2 | .navbar-brand { 3 | position:relative; 4 | padding-left: 50px; 5 | } 6 | .brand { 7 | position:absolute; 8 | top:0px; 9 | left:5px; 10 | display:inline-block; 11 | background: #2d2d2d url('logo.png') no-repeat center center; 12 | width: 40px; 13 | height: 40px; 14 | background-size: 90%; 15 | border-radius:20px; 16 | } 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/header/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmu/react-redux-starter-kit/d14f4affb28070adc15a360888810e3e0067a15c/src/components/header/logo.png -------------------------------------------------------------------------------- /src/components/repo/Repo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const Repo = ({ repo, owner }) => ( 5 |
    6 |
    7 | 8 | {repo.name} 9 | 10 | Stars: {repo.stargazers_count} 11 |
    12 | 13 | 19 |
    20 | ); 21 | 22 | Repo.propTypes = { 23 | repo: PropTypes.shape({ 24 | name: PropTypes.string.isRequired, 25 | html_url: PropTypes.string.isRequired, 26 | stargazers_count: PropTypes.number.isRequired 27 | }).isRequired, 28 | owner: PropTypes.shape({ 29 | login: PropTypes.string.isRequired, 30 | avatar_url: PropTypes.string.isRequired, 31 | html_url: PropTypes.string.isRequired 32 | }).isRequired 33 | }; 34 | 35 | export default Repo; 36 | -------------------------------------------------------------------------------- /src/components/user/User.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const User = ({ user }) => { 5 | const { login, avatar_url, html_url } = user; 6 | const src = `https://ghbtns.com/github-btn.html?user=${login}&type=follow&count=true&size=large`; 7 | 8 | return ( 9 |
    10 | 15 | 16 |
    17 | avatar 18 |
    19 |
    20 |