├── .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 | 
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 |
39 | I believe these two libraries can serve as cornerstones to build a modern web application on, each
40 | addressing an important aspect of web development:
41 | {" "}
42 |
47 | React{" "}
48 |
49 | as the V (view), and
50 | {" "}
51 |
56 | {" "}Redux{" "}
57 |
58 | {" "}
59 | as the
60 | predictable state container.
61 |
62 |
63 |
64 | Along with other great libraries, such as
65 | {" "}
66 |
71 | {" "}React-Router
72 |
73 | {" "}
74 | for routing,
75 |
76 | {" "}Babel{" "}
77 |
78 | {" "}
79 | for next-gen Javascript, and
80 |
85 | {" "}Webpack
86 |
87 | {" "}
88 | for bundling and devtools, web development has never been more fun
89 | and productive.
90 |
91 |
92 | );
93 | };
94 |
95 | export default About;
96 |
--------------------------------------------------------------------------------
/src/containers/app/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | /////////////////////////////////////////////////////////////////////////
5 | // BrowserRouter would be preferred over HashRouter, but BrowserRouter
6 | // would require configuring the server. So we will use HashRouter here.
7 | // Please change to BrowserRouter if you have your own backend server.
8 | ///////////////////////////////////////////////////////////////////////////
9 | import { HashRouter as Router, Route, Switch } from "react-router-dom";
10 |
11 | import { connect } from "react-redux";
12 | import Header from "../../components/header/Header";
13 | import Footer from "../../components/footer/Footer";
14 | import Login from "../login/Login";
15 | import PrivateRoute from "../misc/PrivateRoute";
16 | import Home from "../home/Home";
17 | import UsersPage from "../user/UsersPage";
18 | import ReposPage from "../repo/ReposPage";
19 | import About from "../about/About";
20 | import NotFound from "../misc/NotFound";
21 |
22 | import { logout } from "../../actions/auth";
23 |
24 | import "./app.css";
25 |
26 | class App extends Component {
27 | handleLogout() {
28 | const { user } = this.props;
29 | this.props.dispatch(logout(user));
30 | }
31 |
32 | render() {
33 | const { user } = this.props;
34 | const isAuthenticated = true && user;
35 | return (
36 |
37 |
99 | When I started the project, I had to wrestle with Webpack and Babel to have the dev/build process work well.
100 | {" "}
101 | Recently I ported the starter kit to use
102 | {" "}
103 |
108 | create-react-app
109 |
110 | . I hope you will enjoy the "zero build configuration" experience as much as I do.
111 |
214 | This design pattern makes even more sense when using React along with Redux,
215 | {" "}
216 | where top-level smart components (a.k.a. containers in this codebase such as
217 | {" "}
218 | UsersPage
219 | {" "}
220 | and
221 | {" "}
222 | ReposPage
223 | ) subscribe to Redux state and
224 | {" "}
225 | dispatch Redux actions, while low level components (such as
226 | {" "}
227 | User
228 | ,
229 | {" "}
230 | Repo
231 | , and
232 | {" "}
233 | Header
234 | ) read data and invoke
235 | {" "}
236 | callbacks passed in as props.
237 |
238 |
239 |
240 |
Async Data fetching with caching and pagination
241 |
242 | The UsersPage and ReposPage
243 | {" "}
244 | would show most followed Github users (with 1000+ followers) and most starred
245 | {" "}
246 | Github repos (with 10000+ stars). The async actions (see
247 | users
248 | , and
249 | {" "}
250 | repos
251 | {" "}
252 | under actions) fetch data from the following Github APIs:
253 | {" "}
254 |
255 |
256 | https://api.github.com/search/users?q=followers:>1000&order=desc&page=1
257 |
258 |
259 |
260 | https://api.github.com/search/repositories?q=stars:>10000&order=desc&page=1
261 |
262 |
263 |
264 | The fetched data are stored with the page number as the lookup key, so that the local copy can be
265 | {" "}
266 | shown without the need to re-fetch the same data remotely each time. However cached data can be
267 | {" "}
268 | invalidated if desired.
269 |
270 |
271 |
272 |
Data fetching error handling
273 |
274 | You can test this by disabling your internet connection. Or even better, you can page through
275 | {" "}
276 | UsersPage
277 | {" "}
278 | or
279 | {" "}
280 | ReposPage
281 | {" "}
282 | very quickly and hopefully invoke Github's API
283 | {" "}
284 | rate limit for your IP address.
285 |
286 |
287 | The application would fail gracefully with the error message if data fetching (for a particular page) fails.
288 | {" "}
289 | However, the application can still show cached data for other pages, which is very desirable behavior.
290 |
291 |
292 |
293 |
Authentication and Page Restrictions
294 |
295 | Certain UI pages (
296 | UsersPage
297 | {" "}
298 | and
299 | {" "}
300 | ReposPage
301 | ) are restricted. You can only access them
302 | {" "}
303 | after signing in to the Application. When accessing restricted pages without signing in first,
304 | {" "}
305 | the application would redirect to the
306 | {" "}
307 | Login
308 | {" "}
309 | page. You can log in as user "admin"
310 | {" "}
311 | and password "password". The authentication is based on JSON Web Token (JWT).
312 |
313 |
314 |
WebSocket
315 |
316 | A "server alerts/notifications" use case is implemented to showcase Socket.IO.
317 | {" "}
318 | Whenever a client logs in/out of the application using the API server,
319 | the API server will notify currently connected clients via Socket.IO.
320 | {" "}
321 | You can test this use case by opening the the web app in two browsers side by side,
322 | and then log in/out the webapp in one browser, and observe the messages in the other browser.
323 | {" "}
324 | The messages are pushed from the server to the clients in "real time",
325 | and show up as
326 | {" "}
327 | Alerts
328 | {" "}
329 | in the header section of the web app.
330 |
331 |
332 |
Non-Univeral
333 |
334 | Most people probably would listed this under "issues" or "wish list" instead,
335 | {" "}
336 | since these days a web application is not "cutting edge" or "cool" if it's not universal (isomorphic).
337 | {" "}
338 | However there are many cases where server-side rendering is simply not required or applicable
339 | {" "}
340 | (e.g. Java backend instead of Node).
341 |