├── .babelrc ├── .gitignore ├── GraphQL_Subscriptions.gif ├── LICENSE ├── PATENTS ├── README.md ├── lib ├── config.js ├── index.html ├── js │ ├── app.js │ └── components │ │ └── App │ │ ├── App.js │ │ ├── Dashboard.js │ │ ├── Footer.js │ │ ├── Header.js │ │ ├── Message.js │ │ ├── MessageBoard.js │ │ └── MessageForm.js ├── server.js └── webpack.config.js ├── package.json └── src ├── config.js ├── index.html ├── js ├── app.js └── components │ └── App │ ├── App.js │ ├── Dashboard.js │ ├── Footer.js │ ├── Header.js │ ├── Message.js │ ├── MessageBoard.js │ └── MessageForm.js ├── server.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-0" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | data/schema.graphql 5 | .idea/ 6 | -------------------------------------------------------------------------------- /GraphQL_Subscriptions.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaphold-io/graphql-subscriptions-realtime-starter-kit/ef731a6dcf25e4cba0a71bb81032e208d898ce0f/GraphQL_Subscriptions.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For Relay Starter Kit software 4 | 5 | Copyright (c) 2013-2015, Facebook, Inc. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without modification, 9 | are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name Facebook nor the names of its contributors may be used to 19 | endorse or promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 26 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /PATENTS: -------------------------------------------------------------------------------- 1 | Additional Grant of Patent Rights Version 2 2 | 3 | "Software" means the Relay Starter Kit software distributed by Facebook, Inc. 4 | 5 | Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software 6 | ("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable 7 | (subject to the termination provision below) license under any Necessary 8 | Claims, to make, have made, use, sell, offer to sell, import, and otherwise 9 | transfer the Software. For avoidance of doubt, no license is granted under 10 | Facebook's rights in any patent claims that are infringed by (i) modifications 11 | to the Software made by you or any third party or (ii) the Software in 12 | combination with any software or other technology. 13 | 14 | The license granted hereunder will terminate, automatically and without notice, 15 | if you (or any of your subsidiaries, corporate affiliates or agents) initiate 16 | directly or indirectly, or take a direct financial interest in, any Patent 17 | Assertion: (i) against Facebook or any of its subsidiaries or corporate 18 | affiliates, (ii) against any party if such Patent Assertion arises in whole or 19 | in part from any software, technology, product or service of Facebook or any of 20 | its subsidiaries or corporate affiliates, or (iii) against any party relating 21 | to the Software. Notwithstanding the foregoing, if Facebook or any of its 22 | subsidiaries or corporate affiliates files a lawsuit alleging patent 23 | infringement against you in the first instance, and you respond by filing a 24 | patent infringement counterclaim in that lawsuit against that party that is 25 | unrelated to the Software, the license granted hereunder will not terminate 26 | under section (i) of this paragraph due to such counterclaim. 27 | 28 | A "Necessary Claim" is a claim of a patent owned by Facebook that is 29 | necessarily infringed by the Software standing alone. 30 | 31 | A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, 32 | or contributory infringement or inducement to infringe any patent, including a 33 | cross-claim or counterclaim. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Scaphold.io's GraphQL Subscriptions boilerplate 2 | 3 | Fork this boilerplate code to get started with GraphQL Subscriptions. 4 | 5 | **Demo:** 6 | 7 | ![GraphQL Subscriptions](https://s3.amazonaws.com/meshboard.scaphold.io/GraphQL_Subscriptions.gif) 8 | 9 | 10 | **Quickstart:** 11 | 12 | 1) Go to Scaphold.io (https://scaphold.io). 13 | 14 | 2) Create an account and dataset. 15 | 16 | 3) Change the URL in the API manager (config.js) in the boilerplate to point to your unique Scaphold.io API URL. 17 | 18 | 5) Install dependencies: ```npm install``` 19 | 20 | 4) Run with: ```npm start``` 21 | 22 | 23 | **Deployment:** 24 | 25 | *Note: For development, you only need to run ```npm start```* 26 | 27 | 1) Run ```npm run build``` to transpile ES6 code from the src/ directory to JavaScript in the lib/ directory. 28 | 29 | 2) Set the environment variable ```process.env.NODE_ENV = 'production'``` to let server.js know to run the code in the lib/ directory. 30 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Modify the config Scaphold URL to point to your specific app. 5 | * Find the URL at the top of the page on Scaphold.io once you've created an app. 6 | * Yup. It's that easy. 7 | */ 8 | 9 | var config = { 10 | scapholdAppId: "meshboard", 11 | scapholdUrl: "https://api.scaphold.io/graphql/meshboard", 12 | // scapholdUrl: "http://localhost:3000/graphql/meshboard", 13 | scapholdSubscriptionUrl: "https://subscribe.api.scaphold.io" 14 | // scapholdSubscriptionUrl: "http://localhost:3000" 15 | }; 16 | 17 | module.exports = config; -------------------------------------------------------------------------------- /lib/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Meshboard for Scaphold.io 12 | 13 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | 29 | 36 | 37 | -------------------------------------------------------------------------------- /lib/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('babel-polyfill'); 4 | 5 | var _react = require('react'); 6 | 7 | var _react2 = _interopRequireDefault(_react); 8 | 9 | var _reactDom = require('react-dom'); 10 | 11 | var _reactDom2 = _interopRequireDefault(_reactDom); 12 | 13 | var _config = require('./../config'); 14 | 15 | var _config2 = _interopRequireDefault(_config); 16 | 17 | var _apolloClient = require('apollo-client'); 18 | 19 | var _apolloClient2 = _interopRequireDefault(_apolloClient); 20 | 21 | var _reactApollo = require('react-apollo'); 22 | 23 | var _App = require('./components/App/App'); 24 | 25 | var _App2 = _interopRequireDefault(_App); 26 | 27 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 28 | 29 | var networkInterface = (0, _apolloClient.createNetworkInterface)(_config2.default.scapholdUrl); 30 | networkInterface.use([{ 31 | applyMiddleware: function applyMiddleware(req, next) { 32 | if (!req.options.headers) { 33 | req.options.headers = {}; // Create the header object if needed. 34 | } 35 | if (localStorage.getItem('token')) { 36 | req.options.headers.Authorization = 'Bearer ' + localStorage.getItem('token'); 37 | } 38 | next(); 39 | } 40 | }]); 41 | 42 | var client = new _apolloClient2.default({ 43 | networkInterface: networkInterface 44 | }); 45 | 46 | _reactDom2.default.render(_react2.default.createElement( 47 | _reactApollo.ApolloProvider, 48 | { client: client }, 49 | _react2.default.createElement(_App2.default, { messageBoardId: 'NzdkMTliMDYtNTkyZC00MWRlLThlOTctOWZjZDQ1YmU1ZGYxOjNlNzMzNWM1LWFjYWItNGVjOC1hZDFjLWFlYmE2NWNmMTIxNA==' }) 50 | ), document.getElementById('root')); -------------------------------------------------------------------------------- /lib/js/components/App/App.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _templateObject = _taggedTemplateLiteral(['\n mutation CreateUserQuery($user: _CreateUserInput!){\n createUser(input: $user) {\n token\n changedUser {\n id\n username\n city\n country\n ipAddress\n createdAtSecond\n createdAtMinute\n createdAtHour\n createdAtDay\n createdAtMonth\n createdAtYear\n }\n }\n }\n'], ['\n mutation CreateUserQuery($user: _CreateUserInput!){\n createUser(input: $user) {\n token\n changedUser {\n id\n username\n city\n country\n ipAddress\n createdAtSecond\n createdAtMinute\n createdAtHour\n createdAtDay\n createdAtMonth\n createdAtYear\n }\n }\n }\n']); 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _reactApollo = require('react-apollo'); 16 | 17 | var _graphqlTag = require('graphql-tag'); 18 | 19 | var _graphqlTag2 = _interopRequireDefault(_graphqlTag); 20 | 21 | var _config = require('../../../config'); 22 | 23 | var _config2 = _interopRequireDefault(_config); 24 | 25 | var _reactBootstrap = require('react-bootstrap'); 26 | 27 | var _Header = require('./Header'); 28 | 29 | var _Header2 = _interopRequireDefault(_Header); 30 | 31 | var _MessageBoard = require('./MessageBoard'); 32 | 33 | var _MessageBoard2 = _interopRequireDefault(_MessageBoard); 34 | 35 | var _Message = require('./Message'); 36 | 37 | var _Message2 = _interopRequireDefault(_Message); 38 | 39 | var _Dashboard = require('./Dashboard'); 40 | 41 | var _Dashboard2 = _interopRequireDefault(_Dashboard); 42 | 43 | var _Footer = require('./Footer'); 44 | 45 | var _Footer2 = _interopRequireDefault(_Footer); 46 | 47 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 48 | 49 | function _taggedTemplateLiteral(strings, raw) { return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); } 50 | 51 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 52 | 53 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 54 | 55 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 56 | 57 | var App = function (_React$Component) { 58 | _inherits(App, _React$Component); 59 | 60 | function App(props) { 61 | _classCallCheck(this, App); 62 | 63 | var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(App).call(this, props)); 64 | 65 | _this.state = { 66 | socket: null, 67 | messagesChannel: props.messageBoardId, 68 | usersChannel: "usersChannel", 69 | newUser: {}, 70 | newMessage: {}, 71 | currentUser: null 72 | }; 73 | return _this; 74 | } 75 | 76 | _createClass(App, [{ 77 | key: 'componentWillMount', 78 | value: function componentWillMount() { 79 | var _this2 = this; 80 | 81 | this.props.createUser().then(function (user) { 82 | localStorage.setItem('token', user.token); 83 | localStorage.setItem('currentUser', JSON.stringify(user.changedUser)); 84 | _this2.setState({ currentUser: user }); 85 | }); 86 | 87 | this.state.socket = io.connect(_config2.default.scapholdSubscriptionUrl, { query: 'apiKey=' + _config2.default.scapholdAppId + '&token=' + localStorage.getItem('token') }); 88 | 89 | this.state.socket.on('connect', function (data) { 90 | console.log("Connected!"); 91 | 92 | _this2.subscribeToUsers(); 93 | _this2.subscribeToMessages(); 94 | }); 95 | this.state.socket.on('error', function (err) { 96 | console.log("Error connecting! Uh oh"); 97 | console.log(err); 98 | }); 99 | this.state.socket.on('exception', function (exc) { 100 | console.log("Exception"); 101 | console.log(exc); 102 | }); 103 | 104 | this.state.socket.on("subscribed", function (data) { 105 | console.log("Subscribed"); 106 | console.log(data); 107 | }); 108 | 109 | this.state.socket.on(this.state.messagesChannel, function (data) { 110 | console.log("Received subscription update for channel", _this2.state.messagesChannel); 111 | console.log(data); 112 | _this2.setState({ newMessage: data.data.subscribeToMessages.changedMessage }); 113 | }); 114 | 115 | this.state.socket.on(this.state.usersChannel, function (data) { 116 | console.log("Received subscription update for channel", _this2.state.usersChannel); 117 | console.log(data); 118 | _this2.setState({ newUser: data.data.subscribeToUsers.changedUser }); 119 | }); 120 | } 121 | }, { 122 | key: 'subscribeToMessages', 123 | value: function subscribeToMessages() { 124 | var data = { 125 | query: 'subscription subscribeToMessagesQuery($data: _SubscribeToMessagesInput!) {\n subscribeToMessages(input: $data) {\n changedMessage {\n id\n author {\n id\n username\n city\n country\n createdAtSecond\n createdAtMinute\n createdAtHour\n createdAtDay\n createdAtMonth\n createdAtYear\n }\n content\n createdAt\n createdAtSecond\n createdAtMinute\n createdAtHour\n createdAtDay\n createdAtMonth\n createdAtYear\n }\n }\n }', 126 | variables: { 127 | "data": { 128 | "channel": this.state.messagesChannel, 129 | "transactionTypes": ["CREATE"], 130 | "filter": { 131 | "messageBoardId": this.props.messageBoardId 132 | } 133 | } 134 | } 135 | }; 136 | 137 | this.state.socket.emit("subscribe", data); 138 | } 139 | }, { 140 | key: 'subscribeToUsers', 141 | value: function subscribeToUsers() { 142 | var data = { 143 | query: 'subscription subscribeToUsersQuery($data: _SubscribeToUsersInput!) {\n subscribeToUsers(input: $data) {\n changedUser {\n id\n username\n city\n country\n createdAtSecond\n createdAtMinute\n createdAtHour\n createdAtDay\n createdAtMonth\n createdAtYear\n }\n }\n }', 144 | variables: { 145 | "data": { 146 | "channel": this.state.usersChannel, 147 | "transactionTypes": ["CREATE"] 148 | } 149 | } 150 | }; 151 | 152 | this.state.socket.emit("subscribe", data); 153 | } 154 | }, { 155 | key: 'render', 156 | value: function render() { 157 | 158 | var currentUserId = null; 159 | if (this.state.currentUser) { 160 | currentUserId = this.state.currentUser.changedUser.id; 161 | } 162 | 163 | return _react2.default.createElement( 164 | 'div', 165 | null, 166 | _react2.default.createElement(_Header2.default, null), 167 | _react2.default.createElement( 168 | 'div', 169 | { className: 'container' }, 170 | _react2.default.createElement( 171 | _reactBootstrap.Row, 172 | { style: styles.app }, 173 | _react2.default.createElement( 174 | 'h1', 175 | null, 176 | 'The Olympics Chat App' 177 | ), 178 | ' Brought to you by ', 179 | _react2.default.createElement( 180 | 'a', 181 | { target: '_blank', href: 'https://scaphold.io', style: styles.a }, 182 | 'Scaphold.io' 183 | ) 184 | ), 185 | _react2.default.createElement( 186 | _reactBootstrap.Row, 187 | { style: styles.app }, 188 | _react2.default.createElement( 189 | _reactBootstrap.Col, 190 | { sm: 5 }, 191 | _react2.default.createElement(_MessageBoard2.default, { messageBoardId: this.props.messageBoardId, userId: currentUserId, newMessage: this.state.newMessage }) 192 | ), 193 | _react2.default.createElement( 194 | _reactBootstrap.Col, 195 | { sm: 7 }, 196 | _react2.default.createElement(_Dashboard2.default, { newMessage: this.state.newMessage, newUser: this.state.newUser }) 197 | ) 198 | ) 199 | ), 200 | _react2.default.createElement(_Footer2.default, null) 201 | ); 202 | } 203 | }]); 204 | 205 | return App; 206 | }(_react2.default.Component); 207 | 208 | App.propTypes = { 209 | currentUser: _react2.default.PropTypes.func 210 | }; 211 | 212 | var CREATE_USER = (0, _graphqlTag2.default)(_templateObject); 213 | 214 | var componentWithUser = (0, _reactApollo.graphql)(CREATE_USER, { 215 | options: function options(ownProps) { 216 | return { 217 | variables: { 218 | user: { 219 | username: createNewUsername(), 220 | password: "password" 221 | } 222 | } 223 | }; 224 | }, 225 | props: function props(_ref) { 226 | var ownProps = _ref.ownProps; 227 | var mutate = _ref.mutate; 228 | return { 229 | createUser: function createUser() { 230 | var current = JSON.parse(localStorage.getItem('current')); 231 | var d = new Date(); 232 | return mutate({ 233 | variables: { 234 | user: { 235 | username: createNewUsername(), 236 | password: "password", 237 | city: current.city || "", 238 | country: current.country || "", 239 | ipAddress: current.ip || "", 240 | createdAtSecond: d.getUTCSeconds() ? d.getUTCSeconds() : 0, 241 | createdAtMinute: d.getUTCMinutes() ? d.getUTCMinutes() : 0, 242 | createdAtHour: d.getUTCHours() ? d.getUTCHours() : 0, 243 | createdAtDay: d.getUTCDate() ? d.getUTCDate() : 0, 244 | createdAtMonth: d.getUTCMonth() ? d.getUTCMonth() + 1 : 0, 245 | createdAtYear: d.getUTCFullYear() ? d.getUTCFullYear() : 0 246 | } 247 | } 248 | }).then(function (_ref2) { 249 | var data = _ref2.data; 250 | 251 | // Successfully created new user 252 | return data.createUser; 253 | }).catch(function (error) { 254 | console.log('There was an error sending the query', error); 255 | }); 256 | } 257 | }; 258 | } 259 | }); 260 | 261 | var createNewUsername = function createNewUsername() { 262 | var sub = ((1 + Math.random()) * 0x10000 | 0).toString(16).substring(1); 263 | return (sub + sub + "-" + sub + "-4" + sub.substr(0, 3) + "-" + sub + "-" + sub + sub + sub).toLowerCase(); 264 | }; 265 | 266 | exports.default = componentWithUser(App); 267 | 268 | 269 | var styles = { 270 | app: { 271 | margin: '40px 0' 272 | }, 273 | a: { 274 | color: '#1daaa0' 275 | } 276 | }; -------------------------------------------------------------------------------- /lib/js/components/App/Dashboard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _templateObject = _taggedTemplateLiteral(['\n query {\n viewer {\n allUsers {\n totalCount: count {\n reduction\n }\n countryCount: count (groupBy: "country") {\n group\n reduction\n }\n groupedHourly: count (groupBy: "createdAtHour") {\n group\n reduction\n }\n }\n allMessages {\n totalCount: count {\n reduction\n }\n groupedHourly: count (groupBy: "createdAtHour") {\n group\n reduction\n }\n }\n }\n }\n'], ['\n query {\n viewer {\n allUsers {\n totalCount: count {\n reduction\n }\n countryCount: count (groupBy: "country") {\n group\n reduction\n }\n groupedHourly: count (groupBy: "createdAtHour") {\n group\n reduction\n }\n }\n allMessages {\n totalCount: count {\n reduction\n }\n groupedHourly: count (groupBy: "createdAtHour") {\n group\n reduction\n }\n }\n }\n }\n']); 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _reactApollo = require('react-apollo'); 16 | 17 | var _graphqlTag = require('graphql-tag'); 18 | 19 | var _graphqlTag2 = _interopRequireDefault(_graphqlTag); 20 | 21 | var _reactBootstrap = require('react-bootstrap'); 22 | 23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 24 | 25 | function _taggedTemplateLiteral(strings, raw) { return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); } 26 | 27 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 28 | 29 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 30 | 31 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 32 | 33 | var LineChart = require("react-chartjs").Line; 34 | 35 | var Dashboard = function (_React$Component) { 36 | _inherits(Dashboard, _React$Component); 37 | 38 | function Dashboard(props) { 39 | _classCallCheck(this, Dashboard); 40 | 41 | var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Dashboard).call(this, props)); 42 | 43 | _this.state = { 44 | totalUsers: 0, 45 | totalMessages: 0, 46 | oldUser: {}, 47 | oldMessage: {}, 48 | userMapping: [], 49 | messageMapping: [], 50 | countryCount: [] 51 | }; 52 | _this.getUserMapping = _this.getUserMapping.bind(_this); 53 | _this.getMessageMapping = _this.getMessageMapping.bind(_this); 54 | _this.addToCountry = _this.addToCountry.bind(_this); 55 | return _this; 56 | } 57 | 58 | _createClass(Dashboard, [{ 59 | key: 'componentWillReceiveProps', 60 | value: function componentWillReceiveProps(props) { 61 | var _this2 = this; 62 | 63 | setTimeout(function () { 64 | if (_this2.state.userMapping.length && props.newUser.id && _this2.state.oldUser !== props.newUser) { 65 | // Update when new user comes in 66 | _this2.state.totalUsers++; 67 | _this2.setState({ 68 | userMapping: _this2.addNewUser(_this2.state.userMapping), 69 | totalUsers: _this2.state.totalUsers, 70 | countryCount: _this2.addToCountry(_this2.state.countryCount), 71 | oldUser: _this2.props.newUser 72 | }); 73 | } 74 | if (props.newMessage.id && _this2.state.oldMessage !== props.newMessage) { 75 | // Update when new message comes in 76 | _this2.state.totalMessages++; 77 | _this2.setState({ 78 | messageMapping: _this2.addNewMessage(_this2.state.messageMapping), 79 | totalMessages: _this2.state.totalMessages, 80 | oldMessage: _this2.props.newMessage 81 | }); 82 | } 83 | if (props.data.viewer && _this2.state.totalUsers == 0 && _this2.state.totalMessages == 0) { 84 | // Initialize states 85 | _this2.setState({ 86 | totalUsers: _this2.props.data.viewer.allUsers.totalCount[0].reduction, 87 | totalMessages: _this2.props.data.viewer.allMessages.totalCount[0].reduction, 88 | countryCount: _this2.props.data.viewer.allUsers.countryCount, 89 | userMapping: _this2.getUserMapping(), 90 | messageMapping: _this2.getMessageMapping() 91 | }); 92 | } 93 | }, this.props.wait); 94 | } 95 | }, { 96 | key: 'getUserMapping', 97 | value: function getUserMapping() { 98 | var userMapping = []; 99 | if (this.props.data.viewer) { 100 | var j = void 0; 101 | if (!this.props.data.viewer.allUsers.groupedHourly[0].group) { 102 | j = 2; 103 | } else { 104 | j = 1; 105 | } 106 | for (var i = 0; i < 24; i++) { 107 | var grouping = this.props.data.viewer.allUsers.groupedHourly[j - 1]; 108 | var num = void 0; 109 | if (grouping && grouping.group != i) { 110 | num = 0; 111 | } else if (grouping) { 112 | num = grouping.reduction; 113 | j++; 114 | } 115 | var newObj = { hour: i, num: num || 0 }; 116 | userMapping.push(newObj); 117 | } 118 | } 119 | return userMapping; 120 | } 121 | }, { 122 | key: 'addNewUser', 123 | value: function addNewUser(userMapping) { 124 | var hour = this.props.newUser.createdAtHour; 125 | userMapping[hour].num++; 126 | return userMapping; 127 | } 128 | }, { 129 | key: 'getMessageMapping', 130 | value: function getMessageMapping() { 131 | var messageMapping = []; 132 | if (this.props.data.viewer) { 133 | var j = void 0; 134 | if (!this.props.data.viewer.allMessages.groupedHourly[0].group) { 135 | j = 2; 136 | } else { 137 | j = 1; 138 | } 139 | for (var i = 0; i < 24; i++) { 140 | var grouping = this.props.data.viewer.allMessages.groupedHourly[j - 1]; 141 | var num = void 0; 142 | if (grouping && grouping.group != i) { 143 | num = 0; 144 | } else if (grouping) { 145 | num = grouping.reduction; 146 | j++; 147 | } 148 | var newObj = { hour: i, num: num || 0 }; 149 | messageMapping.push(newObj); 150 | } 151 | } 152 | return messageMapping; 153 | } 154 | }, { 155 | key: 'addNewMessage', 156 | value: function addNewMessage(messageMapping) { 157 | var hour = this.props.newMessage.createdAtHour; 158 | messageMapping[hour].num++; 159 | return messageMapping; 160 | } 161 | }, { 162 | key: 'addToCountry', 163 | value: function addToCountry(countryCount) { 164 | var country = this.props.newUser.country; 165 | countryCount.forEach(function (countryGroup) { 166 | if (countryGroup.group == country) { 167 | countryGroup.reduction++; 168 | } 169 | }); 170 | return countryCount; 171 | } 172 | }, { 173 | key: 'render', 174 | value: function render() { 175 | 176 | var userChartData = { 177 | labels: this.state.userMapping.map(function (item) { 178 | return item.hour; 179 | }), 180 | datasets: [{ 181 | fillColor: "rgba(151,187,205,0.2)", 182 | strokeColor: "#1daaa0", 183 | pointColor: "#1daaa0", 184 | pointStrokeColor: "#fff", 185 | pointHighlightFill: "#fff", 186 | pointHighlightStroke: "#1daaa0", 187 | data: this.state.userMapping.map(function (item) { 188 | return item.num; 189 | }) 190 | }] 191 | }; 192 | var messageChartData = { 193 | labels: this.state.messageMapping.map(function (item) { 194 | return item.hour; 195 | }), 196 | datasets: [{ 197 | fillColor: "rgba(220,220,220,0.2)", 198 | strokeColor: "#1daaa0", 199 | pointColor: "#1daaa0", 200 | pointStrokeColor: "#fff", 201 | pointHighlightFill: "#fff", 202 | pointHighlightStroke: "#1daaa0", 203 | data: this.state.messageMapping.map(function (item) { 204 | return item.num; 205 | }) 206 | }] 207 | }; 208 | var chartOptions = {}; 209 | 210 | var countryComponent = _react2.default.createElement( 211 | 'div', 212 | null, 213 | 'Count by Country: 0' 214 | ); 215 | if (this.props.data.viewer) { 216 | countryComponent = _react2.default.createElement( 217 | 'div', 218 | null, 219 | 'Count by Country: ', 220 | this.state.countryCount.map(function (item, i) { 221 | return _react2.default.createElement( 222 | 'div', 223 | { key: i }, 224 | item.group ? item.group : 'Other', 225 | ' - ', 226 | item.reduction 227 | ); 228 | }) 229 | ); 230 | } 231 | 232 | return _react2.default.createElement( 233 | 'div', 234 | null, 235 | _react2.default.createElement( 236 | 'h3', 237 | null, 238 | 'Chat Dashboard' 239 | ), 240 | _react2.default.createElement( 241 | 'div', 242 | null, 243 | _react2.default.createElement( 244 | _reactBootstrap.Row, 245 | { style: styles.numbersRow }, 246 | _react2.default.createElement( 247 | _reactBootstrap.Col, 248 | { sm: 4 }, 249 | _react2.default.createElement( 250 | 'div', 251 | { style: styles.numbersRow.numbers }, 252 | this.props.data.viewer ? this.state.totalUsers : '0' 253 | ), 254 | _react2.default.createElement( 255 | 'div', 256 | null, 257 | 'Total Users' 258 | ) 259 | ), 260 | _react2.default.createElement( 261 | _reactBootstrap.Col, 262 | { sm: 4 }, 263 | _react2.default.createElement( 264 | 'div', 265 | { style: styles.numbersRow.numbers }, 266 | this.props.data.viewer ? this.state.totalMessages : '0' 267 | ), 268 | _react2.default.createElement( 269 | 'div', 270 | null, 271 | 'Total Messages' 272 | ) 273 | ), 274 | _react2.default.createElement( 275 | _reactBootstrap.Col, 276 | { sm: 4 }, 277 | countryComponent 278 | ) 279 | ), 280 | _react2.default.createElement('hr', null), 281 | _react2.default.createElement( 282 | 'div', 283 | null, 284 | _react2.default.createElement( 285 | 'h4', 286 | { style: styles.graph }, 287 | 'Users vs. Hour of the Day (UTC)' 288 | ), 289 | _react2.default.createElement(LineChart, { data: userChartData, options: chartOptions, height: '300%', width: '650%' }), 290 | _react2.default.createElement( 291 | 'h4', 292 | { style: styles.graph }, 293 | 'Messages vs. Hour of the Day (UTC)' 294 | ), 295 | _react2.default.createElement(LineChart, { data: messageChartData, options: chartOptions, height: '300%', width: '650%' }) 296 | ) 297 | ) 298 | ); 299 | } 300 | }]); 301 | 302 | return Dashboard; 303 | }(_react2.default.Component); 304 | 305 | Dashboard.propTypes = { 306 | data: _react2.default.PropTypes.object 307 | }; 308 | 309 | var GET_ALL_DATA = (0, _graphqlTag2.default)(_templateObject); 310 | 311 | var componentWithAllData = (0, _reactApollo.graphql)(GET_ALL_DATA); 312 | 313 | exports.default = componentWithAllData(Dashboard); 314 | 315 | 316 | var styles = { 317 | numbersRow: { 318 | margin: '20px 20px', 319 | textAlign: 'center', 320 | numbers: { 321 | fontSize: '30px' 322 | } 323 | }, 324 | graph: { 325 | marginTop: '40px' 326 | } 327 | }; -------------------------------------------------------------------------------- /lib/js/components/App/Footer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _reactBootstrap = require('react-bootstrap'); 14 | 15 | var _reactFontawesome = require('react-fontawesome'); 16 | 17 | var _reactFontawesome2 = _interopRequireDefault(_reactFontawesome); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 20 | 21 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 22 | 23 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 24 | 25 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 26 | 27 | var Footer = function (_React$Component) { 28 | _inherits(Footer, _React$Component); 29 | 30 | function Footer() { 31 | _classCallCheck(this, Footer); 32 | 33 | return _possibleConstructorReturn(this, Object.getPrototypeOf(Footer).apply(this, arguments)); 34 | } 35 | 36 | _createClass(Footer, [{ 37 | key: 'render', 38 | value: function render() { 39 | return _react2.default.createElement( 40 | 'p', 41 | { style: styles.footer }, 42 | 'Made with ', 43 | _react2.default.createElement(_reactFontawesome2.default, { name: 'heart' }), 44 | ' from the Scaphold team' 45 | ); 46 | } 47 | }]); 48 | 49 | return Footer; 50 | }(_react2.default.Component); 51 | 52 | exports.default = Footer; 53 | 54 | 55 | var styles = { 56 | footer: { 57 | textAlign: 'center', 58 | paddingTop: 20, 59 | color: '#777', 60 | borderTop: '1px, solid, #e5e5e5' 61 | } 62 | }; -------------------------------------------------------------------------------- /lib/js/components/App/Header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _reactBootstrap = require('react-bootstrap'); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 18 | 19 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 20 | 21 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 22 | 23 | var Header = function (_React$Component) { 24 | _inherits(Header, _React$Component); 25 | 26 | function Header(props) { 27 | _classCallCheck(this, Header); 28 | 29 | var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Header).call(this, props)); 30 | 31 | _this.state = { 32 | showModal: false 33 | }; 34 | 35 | _this.open = _this.open.bind(_this); 36 | _this.close = _this.close.bind(_this); 37 | return _this; 38 | } 39 | 40 | _createClass(Header, [{ 41 | key: 'close', 42 | value: function close() { 43 | this.setState({ showModal: false }); 44 | } 45 | }, { 46 | key: 'open', 47 | value: function open() { 48 | this.setState({ showModal: true }); 49 | } 50 | }, { 51 | key: 'render', 52 | value: function render() { 53 | 54 | return _react2.default.createElement( 55 | _reactBootstrap.Navbar, 56 | { style: styles.navbar }, 57 | _react2.default.createElement( 58 | _reactBootstrap.Navbar.Header, 59 | null, 60 | _react2.default.createElement( 61 | _reactBootstrap.Navbar.Brand, 62 | null, 63 | _react2.default.createElement( 64 | 'a', 65 | { href: '/' }, 66 | 'Scaphold' 67 | ) 68 | ) 69 | ), 70 | _react2.default.createElement( 71 | _reactBootstrap.Nav, 72 | { pullRight: true }, 73 | _react2.default.createElement( 74 | _reactBootstrap.NavItem, 75 | { onClick: this.open }, 76 | 'How To Use' 77 | ), 78 | _react2.default.createElement( 79 | _reactBootstrap.Modal, 80 | { show: this.state.showModal, onHide: this.close }, 81 | _react2.default.createElement( 82 | _reactBootstrap.Modal.Header, 83 | { closeButton: true }, 84 | _react2.default.createElement( 85 | _reactBootstrap.Modal.Title, 86 | null, 87 | 'How To Use' 88 | ) 89 | ), 90 | _react2.default.createElement( 91 | _reactBootstrap.Modal.Body, 92 | null, 93 | _react2.default.createElement( 94 | 'p', 95 | null, 96 | 'This web page was made using ', 97 | _react2.default.createElement( 98 | 'b', 99 | null, 100 | 'GraphQL Subscriptions' 101 | ), 102 | ' to power a ', 103 | _react2.default.createElement( 104 | 'b', 105 | null, 106 | 'real-time app' 107 | ), 108 | '.' 109 | ), 110 | _react2.default.createElement( 111 | 'p', 112 | { style: styles.marketing.p }, 113 | 'The messaging client along with the analytics dashboard demonstrate the power of web sockets. In order to see the magic happen:', 114 | _react2.default.createElement( 115 | 'ol', 116 | null, 117 | _react2.default.createElement( 118 | 'li', 119 | null, 120 | 'Open up two different browser tabs side by side so you can see both.' 121 | ), 122 | _react2.default.createElement( 123 | 'li', 124 | null, 125 | 'Add a message to either one of the sites.' 126 | ), 127 | _react2.default.createElement( 128 | 'li', 129 | null, 130 | 'You should notice the Conversation History and Mesage Graph update automatically in both browsers as it\'s listening to changes from the server.' 131 | ) 132 | ) 133 | ), 134 | _react2.default.createElement( 135 | 'p', 136 | { style: styles.marketing.p }, 137 | 'If you like what you see, ', 138 | _react2.default.createElement( 139 | 'a', 140 | { target: '_blank', href: 'https://scapholdslackin.herokuapp.com', style: styles.marketing.a }, 141 | 'join our Slack channel today to learn more' 142 | ), 143 | '!' 144 | ), 145 | _react2.default.createElement('hr', null), 146 | _react2.default.createElement( 147 | 'p', 148 | null, 149 | 'Here\'s what we used to build this app:' 150 | ), 151 | _react2.default.createElement( 152 | 'h4', 153 | { style: styles.marketing.h4 }, 154 | _react2.default.createElement( 155 | 'a', 156 | { target: '_blank', href: 'https://facebook.github.io/react/', style: styles.marketing.a }, 157 | 'React.js Boilerplate' 158 | ) 159 | ), 160 | _react2.default.createElement( 161 | 'p', 162 | { style: styles.marketing.p }, 163 | 'This React.js boilerplate helps developers create modern, performant, and clean web apps with the help of Scaphold.io.' 164 | ), 165 | _react2.default.createElement('hr', null), 166 | _react2.default.createElement( 167 | 'h4', 168 | { style: styles.marketing.h4 }, 169 | _react2.default.createElement( 170 | 'a', 171 | { target: '_blank', href: 'http://socket.io/', style: styles.marketing.a }, 172 | 'Socket.io' 173 | ) 174 | ), 175 | _react2.default.createElement( 176 | 'p', 177 | { style: styles.marketing.p }, 178 | 'Leverage the simplicity and power of Socket.io and GraphQL to enable real-time functionality to create apps like messaging clients and analytics dashboards.' 179 | ), 180 | _react2.default.createElement('hr', null), 181 | _react2.default.createElement( 182 | 'h4', 183 | { style: styles.marketing.h4 }, 184 | _react2.default.createElement( 185 | 'a', 186 | { target: '_blank', href: 'https://react-bootstrap.github.io/', style: styles.marketing.a }, 187 | 'React-Bootstrap' 188 | ) 189 | ), 190 | _react2.default.createElement( 191 | 'p', 192 | { style: styles.marketing.p }, 193 | 'Smoothe and creative components to fit the way you want your apps to be experienced.' 194 | ), 195 | _react2.default.createElement('hr', null), 196 | _react2.default.createElement( 197 | 'h4', 198 | { style: styles.marketing.h4 }, 199 | _react2.default.createElement( 200 | 'a', 201 | { target: '_blank', href: 'https://webpack.github.io/docs/list-of-tutorials.html', style: styles.marketing.a }, 202 | 'Webpack' 203 | ) 204 | ), 205 | _react2.default.createElement( 206 | 'p', 207 | { style: styles.marketing.p }, 208 | 'Webpack is a module bundler that helps you serve your application in any environment with hot reloading.' 209 | ), 210 | _react2.default.createElement('hr', null), 211 | _react2.default.createElement( 212 | 'p', 213 | { style: styles.marketing.p }, 214 | 'If you have any questions, please contact ', 215 | _react2.default.createElement( 216 | 'a', 217 | { href: 'mailto:support@scaphold.io', style: styles.marketing.a }, 218 | 'support@scaphold.io' 219 | ), 220 | '.' 221 | ) 222 | ), 223 | _react2.default.createElement( 224 | _reactBootstrap.Modal.Footer, 225 | null, 226 | _react2.default.createElement( 227 | _reactBootstrap.Button, 228 | { onClick: this.close }, 229 | 'Close' 230 | ) 231 | ) 232 | ) 233 | ) 234 | ); 235 | } 236 | }]); 237 | 238 | return Header; 239 | }(_react2.default.Component); 240 | 241 | exports.default = Header; 242 | 243 | 244 | var styles = { 245 | navbar: { 246 | marginBottom: 0 247 | }, 248 | marketing: { 249 | margin: '40px 0', 250 | p: { 251 | marginTop: 28 252 | }, 253 | h4: { 254 | marginTop: 28 255 | }, 256 | a: { 257 | color: '#1daaa0' 258 | } 259 | } 260 | }; -------------------------------------------------------------------------------- /lib/js/components/App/Message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _reactBootstrap = require('react-bootstrap'); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 18 | 19 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 20 | 21 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 22 | 23 | var Message = function (_React$Component) { 24 | _inherits(Message, _React$Component); 25 | 26 | function Message(props) { 27 | _classCallCheck(this, Message); 28 | 29 | return _possibleConstructorReturn(this, Object.getPrototypeOf(Message).call(this, props)); 30 | } 31 | 32 | _createClass(Message, [{ 33 | key: 'render', 34 | value: function render() { 35 | 36 | var youComponent = _react2.default.createElement('span', null); 37 | if (this.props.message && this.props.message.author && this.props.userId == this.props.message.author.id) { 38 | youComponent = _react2.default.createElement( 39 | 'span', 40 | null, 41 | '(You)' 42 | ); 43 | } 44 | 45 | var metadataComponent = _react2.default.createElement('div', null); 46 | if (this.props.message.author) { 47 | metadataComponent = _react2.default.createElement( 48 | 'div', 49 | null, 50 | _react2.default.createElement( 51 | 'span', 52 | { style: styles.message.metadata.left }, 53 | ' Posted on ', 54 | this.props.message.author.createdAtMonth, 55 | '/', 56 | this.props.message.author.createdAtDay, 57 | ' at ', 58 | this.props.message.createdAtHour, 59 | ':', 60 | this.props.message.createdAtMinute, 61 | ':', 62 | this.props.message.createdAtSecond, 63 | ' ' 64 | ), 65 | _react2.default.createElement( 66 | 'span', 67 | { style: styles.message.metadata.right }, 68 | ' from ', 69 | this.props.message.author.city, 70 | ', ', 71 | this.props.message.author.country, 72 | ' ', 73 | youComponent 74 | ) 75 | ); 76 | } 77 | 78 | return _react2.default.createElement( 79 | 'div', 80 | { className: 'message', style: styles.message }, 81 | _react2.default.createElement( 82 | 'div', 83 | null, 84 | _react2.default.createElement( 85 | 'b', 86 | null, 87 | this.props.message.content 88 | ) 89 | ), 90 | metadataComponent 91 | ); 92 | } 93 | }]); 94 | 95 | return Message; 96 | }(_react2.default.Component); 97 | 98 | exports.default = Message; 99 | 100 | 101 | var styles = { 102 | message: { 103 | margin: '10px 10px', 104 | metadata: { 105 | left: { 106 | fontSize: '8px', 107 | textAlign: 'left' 108 | }, 109 | right: { 110 | fontSize: '8px', 111 | textAlign: 'right' 112 | } 113 | } 114 | } 115 | }; -------------------------------------------------------------------------------- /lib/js/components/App/MessageBoard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _templateObject = _taggedTemplateLiteral(['\n query GetMessageBoardQuery ($boardId: ID!) {\n getMessageBoard(id: $boardId) {\n id\n name\n messages (first: 10, orderBy: "-createdAt") {\n edges {\n node {\n id\n author {\n id\n username\n city\n country\n createdAtSecond\n createdAtMinute\n createdAtHour\n createdAtDay\n createdAtMonth\n createdAtYear\n }\n content\n createdAt\n createdAtSecond\n createdAtMinute\n createdAtHour\n createdAtDay\n createdAtMonth\n createdAtYear\n }\n }\n }\n }\n }\n'], ['\n query GetMessageBoardQuery ($boardId: ID!) {\n getMessageBoard(id: $boardId) {\n id\n name\n messages (first: 10, orderBy: "-createdAt") {\n edges {\n node {\n id\n author {\n id\n username\n city\n country\n createdAtSecond\n createdAtMinute\n createdAtHour\n createdAtDay\n createdAtMonth\n createdAtYear\n }\n content\n createdAt\n createdAtSecond\n createdAtMinute\n createdAtHour\n createdAtDay\n createdAtMonth\n createdAtYear\n }\n }\n }\n }\n }\n']), 10 | _templateObject2 = _taggedTemplateLiteral(['\n mutation CreateMessageQuery($data: _CreateMessageInput!) {\n createMessage(input: $data) {\n changedMessage {\n id\n author {\n id\n username\n }\n content\n createdAt\n createdAtSecond\n createdAtMinute\n createdAtHour\n createdAtDay\n createdAtMonth\n createdAtYear\n }\n }\n }\n'], ['\n mutation CreateMessageQuery($data: _CreateMessageInput!) {\n createMessage(input: $data) {\n changedMessage {\n id\n author {\n id\n username\n }\n content\n createdAt\n createdAtSecond\n createdAtMinute\n createdAtHour\n createdAtDay\n createdAtMonth\n createdAtYear\n }\n }\n }\n']); 11 | 12 | var _react = require('react'); 13 | 14 | var _react2 = _interopRequireDefault(_react); 15 | 16 | var _reactApollo = require('react-apollo'); 17 | 18 | var _graphqlTag = require('graphql-tag'); 19 | 20 | var _graphqlTag2 = _interopRequireDefault(_graphqlTag); 21 | 22 | var _reactBootstrap = require('react-bootstrap'); 23 | 24 | var _Message = require('./Message'); 25 | 26 | var _Message2 = _interopRequireDefault(_Message); 27 | 28 | var _MessageForm = require('./MessageForm'); 29 | 30 | var _MessageForm2 = _interopRequireDefault(_MessageForm); 31 | 32 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 33 | 34 | function _taggedTemplateLiteral(strings, raw) { return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); } 35 | 36 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 37 | 38 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 39 | 40 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 41 | 42 | var MessageBoard = function (_React$Component) { 43 | _inherits(MessageBoard, _React$Component); 44 | 45 | function MessageBoard(props) { 46 | _classCallCheck(this, MessageBoard); 47 | 48 | var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(MessageBoard).call(this, props)); 49 | 50 | _this.state = { 51 | newMessage: {}, 52 | newMessages: [] 53 | }; 54 | 55 | _this.handleMessageSubmit = _this.handleMessageSubmit.bind(_this); 56 | return _this; 57 | } 58 | 59 | _createClass(MessageBoard, [{ 60 | key: 'componentWillReceiveProps', 61 | value: function componentWillReceiveProps(props) { 62 | if (props.newMessage.id) { 63 | var newMsgs = this.state.newMessages; 64 | newMsgs.push(props.newMessage); 65 | this.setState({ newMessages: newMsgs }); 66 | } 67 | } 68 | }, { 69 | key: 'handleMessageSubmit', 70 | value: function handleMessageSubmit(message) { 71 | this.props.createMessage(message.content); 72 | } 73 | }, { 74 | key: 'render', 75 | value: function render() { 76 | var _this2 = this; 77 | 78 | var conversationHeader = "Loading message title..."; 79 | var messages = "Loading message history..."; 80 | var newMessages = null; 81 | if (this.props.data.getMessageBoard && this.props.data.getMessageBoard.name) { 82 | conversationHeader = _react2.default.createElement( 83 | 'h3', 84 | null, 85 | ' Talk about the ', 86 | this.props.data.getMessageBoard.name, 87 | ' ' 88 | ); 89 | messages = this.props.data.getMessageBoard.messages.edges.map(function (message, i) { 90 | return _react2.default.createElement(_Message2.default, { 91 | key: i, 92 | message: message.node 93 | }); 94 | }); 95 | newMessages = this.state.newMessages.map(function (newMsg, i) { 96 | return _react2.default.createElement(_Message2.default, { 97 | key: i, 98 | message: newMsg, 99 | userId: _this2.props.userId 100 | }); 101 | }).reverse(); 102 | } 103 | 104 | return _react2.default.createElement( 105 | 'div', 106 | null, 107 | _react2.default.createElement( 108 | 'div', 109 | { className: 'messages' }, 110 | conversationHeader, 111 | _react2.default.createElement(_MessageForm2.default, { 112 | onMessageSubmit: this.handleMessageSubmit 113 | }), 114 | newMessages ? newMessages : "", 115 | messages 116 | ) 117 | ); 118 | } 119 | }]); 120 | 121 | return MessageBoard; 122 | }(_react2.default.Component); 123 | 124 | MessageBoard.propTypes = { 125 | data: _react2.default.PropTypes.object.isRequired, 126 | createMessage: _react2.default.PropTypes.func.isRequired, 127 | messageBoardId: _react2.default.PropTypes.string, 128 | userId: _react2.default.PropTypes.string 129 | }; 130 | 131 | var GET_MESSAGEBOARD = (0, _graphqlTag2.default)(_templateObject); 132 | 133 | var CREATE_MESSAGE = (0, _graphqlTag2.default)(_templateObject2); 134 | 135 | var componentWithMessageBoard = (0, _reactApollo.graphql)(GET_MESSAGEBOARD, { 136 | options: function options(ownProps) { 137 | return { 138 | variables: { 139 | boardId: ownProps.messageBoardId 140 | } 141 | }; 142 | } 143 | }); 144 | 145 | var componentWithCreateMessage = (0, _reactApollo.graphql)(CREATE_MESSAGE, { 146 | props: function props(_ref) { 147 | var ownProps = _ref.ownProps; 148 | var mutate = _ref.mutate; 149 | return { 150 | createMessage: function createMessage(content) { 151 | var d = new Date(); 152 | return mutate({ 153 | variables: { 154 | data: { 155 | authorId: ownProps.userId, 156 | messageBoardId: ownProps.messageBoardId, 157 | content: content, 158 | createdAtSecond: d.getUTCSeconds(), 159 | createdAtMinute: d.getUTCMinutes(), 160 | createdAtHour: d.getUTCHours(), 161 | createdAtDay: d.getUTCDate(), 162 | createdAtMonth: d.getUTCMonth() + 1, 163 | createdAtYear: d.getUTCFullYear() 164 | } 165 | } 166 | }).then(function (_ref2) { 167 | // console.log("SUCCESS"); 168 | 169 | var data = _ref2.data; 170 | }).catch(function (error) { 171 | // console.log("FAILED"); 172 | }); 173 | } 174 | }; 175 | } 176 | }); 177 | 178 | exports.default = componentWithCreateMessage(componentWithMessageBoard(MessageBoard)); 179 | 180 | 181 | var styles = {}; -------------------------------------------------------------------------------- /lib/js/components/App/MessageForm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _reactBootstrap = require('react-bootstrap'); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 18 | 19 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 20 | 21 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 22 | 23 | var MessageForm = function (_React$Component) { 24 | _inherits(MessageForm, _React$Component); 25 | 26 | function MessageForm(props) { 27 | _classCallCheck(this, MessageForm); 28 | 29 | var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(MessageForm).call(this, props)); 30 | 31 | _this.state = { 32 | content: '' 33 | }; 34 | 35 | _this.handleSubmit = _this.handleSubmit.bind(_this); 36 | _this.changeHandler = _this.changeHandler.bind(_this); 37 | return _this; 38 | } 39 | 40 | _createClass(MessageForm, [{ 41 | key: 'componentDidMount', 42 | value: function componentDidMount() {} 43 | }, { 44 | key: 'handleSubmit', 45 | value: function handleSubmit(e) { 46 | e.preventDefault(); 47 | var message = { 48 | content: this.state.content 49 | }; 50 | if (message.content == '') return; 51 | this.props.onMessageSubmit(message); 52 | this.setState({ content: '' }); 53 | } 54 | }, { 55 | key: 'changeHandler', 56 | value: function changeHandler(e) { 57 | this.setState({ content: e.target.value }); 58 | } 59 | }, { 60 | key: 'render', 61 | value: function render() { 62 | return _react2.default.createElement( 63 | 'div', 64 | { className: 'message_form', style: styles.form }, 65 | _react2.default.createElement( 66 | 'h4', 67 | null, 68 | 'Say something great!' 69 | ), 70 | _react2.default.createElement( 71 | 'form', 72 | { onSubmit: this.handleSubmit }, 73 | _react2.default.createElement('input', { 74 | style: styles.input, 75 | onChange: this.changeHandler, 76 | value: this.state.content 77 | }) 78 | ) 79 | ); 80 | } 81 | }]); 82 | 83 | return MessageForm; 84 | }(_react2.default.Component); 85 | 86 | exports.default = MessageForm; 87 | 88 | 89 | var styles = { 90 | input: { 91 | fontSize: '20px', 92 | width: '100%', 93 | resize: 'both', 94 | overflow: 'auto' 95 | }, 96 | form: { 97 | margin: '30px 30px' 98 | } 99 | }; -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _path = require('path'); 4 | 5 | var _path2 = _interopRequireDefault(_path); 6 | 7 | var _webpack = require('webpack'); 8 | 9 | var _webpack2 = _interopRequireDefault(_webpack); 10 | 11 | var _webpackDevServer = require('webpack-dev-server'); 12 | 13 | var _webpackDevServer2 = _interopRequireDefault(_webpackDevServer); 14 | 15 | var _express = require('express'); 16 | 17 | var _express2 = _interopRequireDefault(_express); 18 | 19 | var _config = require('./config'); 20 | 21 | var _config2 = _interopRequireDefault(_config); 22 | 23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 24 | 25 | var APP_PORT = 3001; 26 | 27 | var compiler = (0, _webpack2.default)({ 28 | entry: _path2.default.resolve(__dirname, 'js', 'app.js'), 29 | module: { 30 | loaders: [{ 31 | exclude: /node_modules/, 32 | loader: 'babel', 33 | test: /\.js$/ 34 | }] 35 | }, 36 | output: { filename: 'app.js', path: '/' } 37 | }); 38 | 39 | var contentBase = 'src/'; 40 | if (process.env.NODE_ENV === "production") { 41 | contentBase = 'lib/'; 42 | } 43 | 44 | var app = new _webpackDevServer2.default(compiler, { 45 | contentBase: contentBase, 46 | publicPath: '/js/', 47 | proxy: { '/graphql': _config2.default.scapholdUrl }, 48 | stats: { colors: true } 49 | }); 50 | // Serve static resources 51 | app.use('/', _express2.default.static(_path2.default.resolve(__dirname, '/'))); 52 | app.listen(APP_PORT, function () { 53 | console.log('App is now running on http://localhost:' + APP_PORT); 54 | }); -------------------------------------------------------------------------------- /lib/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var StaticSiteGeneratorPlugin = require('static-site-generator-webpack-plugin'); 4 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | var webpack = require('webpack'); 6 | 7 | var locals = { 8 | paths: ['/'] 9 | }; 10 | 11 | module.exports = { 12 | 13 | entry: { 14 | 'main': './server.js' 15 | }, 16 | 17 | plugins: [new StaticSiteGeneratorPlugin('main', locals.paths, locals), new webpack.NoErrorsPlugin() 18 | // new ExtractTextPlugin('style.css') 19 | ], 20 | 21 | output: { 22 | filename: 'server.js', //sets our output filename to index.js 23 | path: 'dist', //sets our output directory to dist/ 24 | libraryTarget: 'umd' //nodejs and StaticSiteGeneratorWebpackPlugin require UMD or CommonJS 25 | }, 26 | 27 | module: { 28 | loaders: [{ 29 | test: /\.jsx?$/, 30 | exclude: /node_modules/, 31 | loader: 'babel', 32 | query: { 33 | presets: ['es2015', 'react'] 34 | } 35 | }] 36 | }, 37 | 38 | watch: true 39 | 40 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-subscriptions-realtime-starter-kit", 3 | "private": true, 4 | "description": "Scaphold.io's Starter Kit for building real-time apps with GraphQL Subscriptions", 5 | "repository": "scaphold-io/graphql-subscriptions-realtime-starter-kit", 6 | "version": "0.1.0", 7 | "scripts": { 8 | "start": "babel-node ./src/server.js", 9 | "start-prod": "node ./lib/server.js", 10 | "build": "cp src/index.html lib/ && babel src --out-dir lib --sourceRoot src", 11 | "buildw": "cp src/index.html lib/ && babel src --out-dir lib -w --sourceRoot src" 12 | }, 13 | "dependencies": { 14 | "apollo-client": "^0.4.11", 15 | "babel-core": "^6.7.7", 16 | "babel-loader": "6.2.4", 17 | "babel-polyfill": "6.7.4", 18 | "babel-preset-es2015": "6.6.0", 19 | "babel-preset-react": "6.5.0", 20 | "babel-preset-stage-0": "6.5.0", 21 | "babel-relay-plugin": "0.8.1", 22 | "chart.js": "^1.1.1", 23 | "classnames": "2.2.4", 24 | "express": "^4.13.4", 25 | "graphql-tag": "^0.1.11", 26 | "isomorphic-fetch": "^2.2.1", 27 | "react": "^15.3.0", 28 | "react-apollo": "^0.4.5", 29 | "react-bootstrap": "^0.30.2", 30 | "react-chartjs": "^0.8.0", 31 | "react-dom": "^15.3.0", 32 | "react-fontawesome": "^1.1.0", 33 | "static-site-generator-webpack-plugin": "^2.1.0", 34 | "sync-request": "^2.0.1", 35 | "webpack": "1.13.0", 36 | "webpack-dev-server": "1.14.1" 37 | }, 38 | "devDependencies": { 39 | "babel-cli": "6.7.7", 40 | "babel-core": "^6.13.2", 41 | "babel-loader": "^6.2.4", 42 | "babel-preset-es2015": "^6.6.0", 43 | "babel-preset-react": "^6.5.0", 44 | "extract-text-webpack-plugin": "^1.0.1", 45 | "static-site-generator-webpack-plugin": "^2.1.0", 46 | "webpack": "^1.13.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Modify the config Scaphold URL to point to your specific app. 3 | * Find the URL at the top of the page on Scaphold.io once you've created an app. 4 | * Yup. It's that easy. 5 | */ 6 | 7 | var config = { 8 | scapholdAppId: "meshboard", 9 | scapholdUrl: "https://api.scaphold.io/graphql/meshboard", 10 | // scapholdUrl: "http://localhost:3000/graphql/meshboard", 11 | scapholdSubscriptionUrl: "https://subscribe.api.scaphold.io" 12 | // scapholdSubscriptionUrl: "http://localhost:3000" 13 | } 14 | 15 | module.exports = config; -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Meshboard for Scaphold.io 12 | 13 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | 29 | 36 | 37 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import config from './../config'; 5 | import ApolloClient, { createNetworkInterface } from 'apollo-client'; 6 | import { ApolloProvider } from 'react-apollo'; 7 | import App from './components/App/App'; 8 | 9 | const networkInterface = createNetworkInterface(config.scapholdUrl); 10 | networkInterface.use([{ 11 | applyMiddleware(req, next) { 12 | if (!req.options.headers) { 13 | req.options.headers = {}; // Create the header object if needed. 14 | } 15 | if (localStorage.getItem('token')) { 16 | req.options.headers.Authorization = `Bearer ${localStorage.getItem('token')}`; 17 | } 18 | next(); 19 | } 20 | }]); 21 | 22 | const client = new ApolloClient({ 23 | networkInterface 24 | }); 25 | 26 | ReactDOM.render( 27 | 28 | 29 | , 30 | document.getElementById('root') 31 | ); 32 | -------------------------------------------------------------------------------- /src/js/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'react-apollo'; 3 | import gql from 'graphql-tag'; 4 | import config from '../../../config'; 5 | import {Row, Col, Button, Jumbotron} from 'react-bootstrap'; 6 | import Header from './Header'; 7 | import MessageBoard from './MessageBoard'; 8 | import Message from './Message'; 9 | import Dashboard from './Dashboard'; 10 | import Footer from './Footer'; 11 | 12 | class App extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | socket: null, 17 | messagesChannel: props.messageBoardId, 18 | usersChannel: "usersChannel", 19 | newUser: {}, 20 | newMessage: {}, 21 | currentUser: null 22 | } 23 | } 24 | 25 | componentWillMount() { 26 | 27 | this.props.createUser().then(user => { 28 | localStorage.setItem('token', user.token); 29 | localStorage.setItem('currentUser', JSON.stringify(user.changedUser)); 30 | this.setState({currentUser: user}); 31 | }); 32 | 33 | this.state.socket = io.connect(config.scapholdSubscriptionUrl, {query: `apiKey=${config.scapholdAppId}&token=${localStorage.getItem('token')}`}); 34 | 35 | this.state.socket.on('connect', (data) => { 36 | console.log("Connected!"); 37 | 38 | this.subscribeToUsers(); 39 | this.subscribeToMessages(); 40 | }); 41 | this.state.socket.on('error', (err) => { 42 | console.log("Error connecting! Uh oh"); 43 | console.log(err); 44 | }); 45 | this.state.socket.on('exception', (exc) => { 46 | console.log("Exception"); 47 | console.log(exc); 48 | }) 49 | 50 | this.state.socket.on("subscribed", (data) => { 51 | console.log("Subscribed"); 52 | console.log(data); 53 | }) 54 | 55 | this.state.socket.on(this.state.messagesChannel, (data) => { 56 | console.log("Received subscription update for channel", this.state.messagesChannel); 57 | console.log(data); 58 | this.setState({newMessage: data.data.subscribeToMessages.changedMessage}); 59 | }); 60 | 61 | this.state.socket.on(this.state.usersChannel, (data) => { 62 | console.log("Received subscription update for channel", this.state.usersChannel); 63 | console.log(data); 64 | this.setState({newUser: data.data.subscribeToUsers.changedUser}); 65 | }); 66 | 67 | } 68 | 69 | subscribeToMessages() { 70 | let data = { 71 | query: `subscription subscribeToMessagesQuery($data: _SubscribeToMessagesInput!) { 72 | subscribeToMessages(input: $data) { 73 | changedMessage { 74 | id 75 | author { 76 | id 77 | username 78 | city 79 | country 80 | createdAtSecond 81 | createdAtMinute 82 | createdAtHour 83 | createdAtDay 84 | createdAtMonth 85 | createdAtYear 86 | } 87 | content 88 | createdAt 89 | createdAtSecond 90 | createdAtMinute 91 | createdAtHour 92 | createdAtDay 93 | createdAtMonth 94 | createdAtYear 95 | } 96 | } 97 | }`, 98 | variables: { 99 | "data": { 100 | "channel": this.state.messagesChannel, 101 | "transactionTypes": ["CREATE"], 102 | "filter": { 103 | "messageBoardId": this.props.messageBoardId 104 | } 105 | } 106 | } 107 | }; 108 | 109 | this.state.socket.emit("subscribe", data); 110 | } 111 | 112 | subscribeToUsers() { 113 | let data = { 114 | query: `subscription subscribeToUsersQuery($data: _SubscribeToUsersInput!) { 115 | subscribeToUsers(input: $data) { 116 | changedUser { 117 | id 118 | username 119 | city 120 | country 121 | createdAtSecond 122 | createdAtMinute 123 | createdAtHour 124 | createdAtDay 125 | createdAtMonth 126 | createdAtYear 127 | } 128 | } 129 | }`, 130 | variables: { 131 | "data": { 132 | "channel": this.state.usersChannel, 133 | "transactionTypes": ["CREATE"] 134 | } 135 | } 136 | }; 137 | 138 | this.state.socket.emit("subscribe", data); 139 | } 140 | 141 | render() { 142 | 143 | let currentUserId = null; 144 | if (this.state.currentUser) { 145 | currentUserId = this.state.currentUser.changedUser.id; 146 | } 147 | 148 | return ( 149 |
150 |
151 |
152 |

The Olympics Chat App

 Brought to you by Scaphold.io
153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 |
162 |
164 | ); 165 | } 166 | } 167 | 168 | App.propTypes = { 169 | currentUser: React.PropTypes.func 170 | }; 171 | 172 | const CREATE_USER = gql ` 173 | mutation CreateUserQuery($user: _CreateUserInput!){ 174 | createUser(input: $user) { 175 | token 176 | changedUser { 177 | id 178 | username 179 | city 180 | country 181 | ipAddress 182 | createdAtSecond 183 | createdAtMinute 184 | createdAtHour 185 | createdAtDay 186 | createdAtMonth 187 | createdAtYear 188 | } 189 | } 190 | } 191 | ` 192 | 193 | const componentWithUser = graphql(CREATE_USER, { 194 | options: (ownProps) => ({ 195 | variables: { 196 | user: { 197 | username: createNewUsername(), 198 | password: "password" 199 | } 200 | } 201 | }), 202 | props: ({ ownProps, mutate }) => ({ 203 | createUser() { 204 | let current = JSON.parse(localStorage.getItem('current')); 205 | let d = new Date(); 206 | return mutate({ 207 | variables: { 208 | user: { 209 | username: createNewUsername(), 210 | password: "password", 211 | city: current.city || "", 212 | country: current.country || "", 213 | ipAddress: current.ip || "", 214 | createdAtSecond: d.getUTCSeconds() ? d.getUTCSeconds() : 0, 215 | createdAtMinute: d.getUTCMinutes() ? d.getUTCMinutes() : 0, 216 | createdAtHour: d.getUTCHours() ? d.getUTCHours() : 0, 217 | createdAtDay: d.getUTCDate() ? d.getUTCDate() : 0, 218 | createdAtMonth: d.getUTCMonth() ? d.getUTCMonth()+1 : 0, 219 | createdAtYear: d.getUTCFullYear() ? d.getUTCFullYear() : 0 220 | } 221 | } 222 | }).then(({ data }) => { 223 | // Successfully created new user 224 | return data.createUser; 225 | }).catch((error) => { 226 | console.log('There was an error sending the query', error); 227 | }); 228 | } 229 | }) 230 | }); 231 | 232 | const createNewUsername = () => { 233 | let sub = (((1+Math.random())*0x10000)|0).toString(16).substring(1) 234 | return (sub + sub + "-" + sub + "-4" + sub.substr(0,3) + "-" + sub + "-" + sub + sub + sub).toLowerCase(); 235 | } 236 | 237 | export default componentWithUser(App); 238 | 239 | const styles = { 240 | app: { 241 | margin: `40px 0` 242 | }, 243 | a: { 244 | color: '#1daaa0' 245 | } 246 | }; 247 | -------------------------------------------------------------------------------- /src/js/components/App/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'react-apollo'; 3 | import gql from 'graphql-tag'; 4 | import {Row, Col} from 'react-bootstrap'; 5 | let LineChart = require("react-chartjs").Line; 6 | 7 | class Dashboard extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | totalUsers: 0, 13 | totalMessages: 0, 14 | oldUser: {}, 15 | oldMessage: {}, 16 | userMapping: [], 17 | messageMapping: [], 18 | countryCount: [] 19 | } 20 | this.getUserMapping = this.getUserMapping.bind(this); 21 | this.getMessageMapping = this.getMessageMapping.bind(this); 22 | this.addToCountry = this.addToCountry.bind(this); 23 | } 24 | 25 | componentWillReceiveProps(props) { 26 | setTimeout(() => { 27 | if (this.state.userMapping.length && props.newUser.id && this.state.oldUser !== props.newUser) { 28 | // Update when new user comes in 29 | this.state.totalUsers++; 30 | this.setState({ 31 | userMapping: this.addNewUser(this.state.userMapping), 32 | totalUsers: this.state.totalUsers, 33 | countryCount: this.addToCountry(this.state.countryCount), 34 | oldUser: this.props.newUser 35 | }); 36 | } 37 | if (props.newMessage.id && this.state.oldMessage !== props.newMessage) { 38 | // Update when new message comes in 39 | this.state.totalMessages++; 40 | this.setState({ 41 | messageMapping: this.addNewMessage(this.state.messageMapping), 42 | totalMessages: this.state.totalMessages, 43 | oldMessage: this.props.newMessage 44 | }); 45 | } 46 | if (props.data.viewer && this.state.totalUsers == 0 && this.state.totalMessages == 0) { 47 | // Initialize states 48 | this.setState({ 49 | totalUsers: this.props.data.viewer.allUsers.totalCount[0].reduction, 50 | totalMessages: this.props.data.viewer.allMessages.totalCount[0].reduction, 51 | countryCount: this.props.data.viewer.allUsers.countryCount, 52 | userMapping: this.getUserMapping(), 53 | messageMapping: this.getMessageMapping() 54 | }); 55 | } 56 | }, this.props.wait); 57 | } 58 | 59 | getUserMapping() { 60 | let userMapping = []; 61 | if (this.props.data.viewer) { 62 | let j; 63 | if (!this.props.data.viewer.allUsers.groupedHourly[0].group) { j = 2; } else { j = 1; } 64 | for (let i = 0; i < 24; i++) { 65 | let grouping = this.props.data.viewer.allUsers.groupedHourly[j-1]; 66 | let num; 67 | if (grouping && grouping.group != i) { 68 | num = 0; 69 | } else if (grouping) { 70 | num = grouping.reduction; 71 | j++; 72 | } 73 | let newObj = {hour: i, num: num || 0}; 74 | userMapping.push(newObj); 75 | } 76 | } 77 | return userMapping; 78 | } 79 | 80 | addNewUser(userMapping) { 81 | let hour = this.props.newUser.createdAtHour; 82 | userMapping[hour].num++; 83 | return userMapping; 84 | } 85 | 86 | getMessageMapping() { 87 | let messageMapping = []; 88 | if (this.props.data.viewer) { 89 | let j; 90 | if (!this.props.data.viewer.allMessages.groupedHourly[0].group) { j = 2 } else { j = 1 } 91 | for (let i = 0; i < 24; i++) { 92 | let grouping = this.props.data.viewer.allMessages.groupedHourly[j-1]; 93 | let num; 94 | if (grouping && grouping.group != i) { 95 | num = 0; 96 | } else if (grouping) { 97 | num = grouping.reduction; 98 | j++; 99 | } 100 | let newObj = {hour: i, num: num || 0}; 101 | messageMapping.push(newObj); 102 | } 103 | } 104 | return messageMapping; 105 | } 106 | 107 | addNewMessage(messageMapping) { 108 | let hour = this.props.newMessage.createdAtHour; 109 | messageMapping[hour].num++; 110 | return messageMapping; 111 | } 112 | 113 | addToCountry(countryCount) { 114 | let country = this.props.newUser.country; 115 | countryCount.forEach(countryGroup => { 116 | if (countryGroup.group == country) { 117 | countryGroup.reduction++; 118 | } 119 | }); 120 | return countryCount; 121 | } 122 | 123 | render() { 124 | 125 | let userChartData = { 126 | labels: this.state.userMapping.map(item => { return item.hour }), 127 | datasets: [ 128 | { 129 | fillColor: "rgba(151,187,205,0.2)", 130 | strokeColor: "#1daaa0", 131 | pointColor: "#1daaa0", 132 | pointStrokeColor: "#fff", 133 | pointHighlightFill: "#fff", 134 | pointHighlightStroke: "#1daaa0", 135 | data: this.state.userMapping.map(item => { return item.num }) 136 | } 137 | ] 138 | }; 139 | let messageChartData = { 140 | labels: this.state.messageMapping.map(item => { return item.hour }), 141 | datasets: [ 142 | { 143 | fillColor: "rgba(220,220,220,0.2)", 144 | strokeColor: "#1daaa0", 145 | pointColor: "#1daaa0", 146 | pointStrokeColor: "#fff", 147 | pointHighlightFill: "#fff", 148 | pointHighlightStroke: "#1daaa0", 149 | data: this.state.messageMapping.map(item => { return item.num }) 150 | } 151 | ] 152 | }; 153 | let chartOptions = {}; 154 | 155 | let countryComponent =
Count by Country: 0
156 | if (this.props.data.viewer) { 157 | countryComponent =
Count by Country: {this.state.countryCount.map((item, i) => { 158 | return
{item.group ? item.group : 'Other'} - {item.reduction}
159 | })}
160 | } 161 | 162 | return ( 163 |
164 |

Chat Dashboard

165 | 166 |
167 | 168 | 169 |
{this.props.data.viewer ? this.state.totalUsers : '0'}
170 |
Total Users
171 | 172 | 173 |
{this.props.data.viewer ? this.state.totalMessages : '0'}
174 |
Total Messages
175 | 176 | 177 | {countryComponent} 178 | 179 |
180 | 181 |
182 | 183 |
184 |

Users vs. Hour of the Day (UTC)

185 | 186 | 187 |

Messages vs. Hour of the Day (UTC)

188 | 189 |
190 | 191 |
192 | 193 |
194 | ); 195 | } 196 | } 197 | 198 | Dashboard.propTypes = { 199 | data: React.PropTypes.object 200 | } 201 | 202 | const GET_ALL_DATA = gql ` 203 | query { 204 | viewer { 205 | allUsers { 206 | totalCount: count { 207 | reduction 208 | } 209 | countryCount: count (groupBy: "country") { 210 | group 211 | reduction 212 | } 213 | groupedHourly: count (groupBy: "createdAtHour") { 214 | group 215 | reduction 216 | } 217 | } 218 | allMessages { 219 | totalCount: count { 220 | reduction 221 | } 222 | groupedHourly: count (groupBy: "createdAtHour") { 223 | group 224 | reduction 225 | } 226 | } 227 | } 228 | } 229 | ` 230 | 231 | const componentWithAllData = graphql(GET_ALL_DATA); 232 | 233 | export default componentWithAllData(Dashboard); 234 | 235 | const styles = { 236 | numbersRow: { 237 | margin: `20px 20px`, 238 | textAlign: `center`, 239 | numbers: { 240 | fontSize: `30px` 241 | } 242 | }, 243 | graph: { 244 | marginTop: '40px' 245 | } 246 | }; 247 | -------------------------------------------------------------------------------- /src/js/components/App/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Row, Col} from 'react-bootstrap'; 3 | import FontAwesome from 'react-fontawesome'; 4 | 5 | class Footer extends React.Component { 6 | render() { 7 | return ( 8 |

Made with from the Scaphold team

9 | ); 10 | } 11 | } 12 | 13 | export default Footer; 14 | 15 | const styles = { 16 | footer: { 17 | textAlign: 'center', 18 | paddingTop: 20, 19 | color: '#777', 20 | borderTop: '1px, solid, #e5e5e5' 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/js/components/App/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Navbar, Nav, NavItem, NavDropdown, MenuItem, Modal, OverlayTrigger, Button} from 'react-bootstrap'; 3 | 4 | class Header extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | showModal: false 9 | } 10 | 11 | this.open = this.open.bind(this); 12 | this.close = this.close.bind(this); 13 | } 14 | 15 | close() { 16 | this.setState({ showModal: false }); 17 | } 18 | 19 | open() { 20 | this.setState({ showModal: true }); 21 | } 22 | 23 | render() { 24 | 25 | return ( 26 | 27 | 28 | 29 | Scaphold 30 | 31 | 32 | 97 | 98 | ); 99 | } 100 | } 101 | 102 | export default Header; 103 | 104 | const styles = { 105 | navbar: { 106 | marginBottom: 0 107 | }, 108 | marketing: { 109 | margin: '40px 0', 110 | p: { 111 | marginTop: 28 112 | }, 113 | h4: { 114 | marginTop: 28 115 | }, 116 | a: { 117 | color: '#1daaa0' 118 | } 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /src/js/components/App/Message.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Row, Col} from 'react-bootstrap'; 3 | 4 | class Message extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | 12 | let youComponent = ; 13 | if (this.props.message && this.props.message.author && this.props.userId == this.props.message.author.id) { 14 | youComponent = (You) 15 | } 16 | 17 | let metadataComponent =
; 18 | if (this.props.message.author) { 19 | metadataComponent =
Posted on {this.props.message.author.createdAtMonth}/{this.props.message.author.createdAtDay} at {this.props.message.createdAtHour}:{this.props.message.createdAtMinute}:{this.props.message.createdAtSecond} from {this.props.message.author.city}, {this.props.message.author.country} {youComponent}
20 | } 21 | 22 | return ( 23 |
24 |
{this.props.message.content}
25 | {metadataComponent} 26 |
27 | ); 28 | } 29 | } 30 | 31 | export default Message; 32 | 33 | const styles = { 34 | message: { 35 | margin: '10px 10px', 36 | metadata: { 37 | left: { 38 | fontSize: '8px', 39 | textAlign: 'left' 40 | }, 41 | right: { 42 | fontSize: '8px', 43 | textAlign: 'right' 44 | } 45 | } 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/js/components/App/MessageBoard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'react-apollo'; 3 | import gql from 'graphql-tag'; 4 | import {Row, Col} from 'react-bootstrap'; 5 | import Message from './Message'; 6 | import MessageForm from './MessageForm'; 7 | 8 | class MessageBoard extends React.Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | newMessage: {}, 14 | newMessages: [] 15 | } 16 | 17 | this.handleMessageSubmit = this.handleMessageSubmit.bind(this); 18 | } 19 | 20 | componentWillReceiveProps(props) { 21 | if (props.newMessage.id) { 22 | let newMsgs = this.state.newMessages; 23 | newMsgs.push(props.newMessage); 24 | this.setState({newMessages: newMsgs}); 25 | } 26 | } 27 | 28 | handleMessageSubmit(message) { 29 | this.props.createMessage(message.content); 30 | } 31 | 32 | render() { 33 | let conversationHeader = "Loading message title..."; 34 | let messages = "Loading message history..."; 35 | let newMessages = null; 36 | if (this.props.data.getMessageBoard && this.props.data.getMessageBoard.name) { 37 | conversationHeader =

Talk about the {this.props.data.getMessageBoard.name}

; 38 | messages = this.props.data.getMessageBoard.messages.edges.map((message, i) => { 39 | return ( 40 | 44 | ); 45 | }); 46 | newMessages = this.state.newMessages.map((newMsg, i) => { 47 | return ( 48 | 53 | ) 54 | }).reverse(); 55 | } 56 | 57 | return ( 58 |
59 |
60 | {conversationHeader} 61 | 64 | {newMessages ? newMessages : ""} 65 | {messages} 66 |
67 |
68 | ); 69 | } 70 | } 71 | 72 | MessageBoard.propTypes = { 73 | data: React.PropTypes.object.isRequired, 74 | createMessage: React.PropTypes.func.isRequired, 75 | messageBoardId: React.PropTypes.string, 76 | userId: React.PropTypes.string 77 | }; 78 | 79 | const GET_MESSAGEBOARD = gql ` 80 | query GetMessageBoardQuery ($boardId: ID!) { 81 | getMessageBoard(id: $boardId) { 82 | id 83 | name 84 | messages (first: 10, orderBy: "-createdAt") { 85 | edges { 86 | node { 87 | id 88 | author { 89 | id 90 | username 91 | city 92 | country 93 | createdAtSecond 94 | createdAtMinute 95 | createdAtHour 96 | createdAtDay 97 | createdAtMonth 98 | createdAtYear 99 | } 100 | content 101 | createdAt 102 | createdAtSecond 103 | createdAtMinute 104 | createdAtHour 105 | createdAtDay 106 | createdAtMonth 107 | createdAtYear 108 | } 109 | } 110 | } 111 | } 112 | } 113 | ` 114 | 115 | const CREATE_MESSAGE = gql ` 116 | mutation CreateMessageQuery($data: _CreateMessageInput!) { 117 | createMessage(input: $data) { 118 | changedMessage { 119 | id 120 | author { 121 | id 122 | username 123 | } 124 | content 125 | createdAt 126 | createdAtSecond 127 | createdAtMinute 128 | createdAtHour 129 | createdAtDay 130 | createdAtMonth 131 | createdAtYear 132 | } 133 | } 134 | } 135 | ` 136 | 137 | const componentWithMessageBoard = graphql(GET_MESSAGEBOARD, { 138 | options: (ownProps) => ({ 139 | variables: { 140 | boardId: ownProps.messageBoardId 141 | } 142 | }) 143 | }) 144 | 145 | const componentWithCreateMessage = graphql(CREATE_MESSAGE, { 146 | props: ({ ownProps, mutate }) => ({ 147 | createMessage(content) { 148 | let d = new Date(); 149 | return mutate({ 150 | variables: { 151 | data: { 152 | authorId: ownProps.userId, 153 | messageBoardId: ownProps.messageBoardId, 154 | content: content, 155 | createdAtSecond: d.getUTCSeconds(), 156 | createdAtMinute: d.getUTCMinutes(), 157 | createdAtHour: d.getUTCHours(), 158 | createdAtDay: d.getUTCDate(), 159 | createdAtMonth: d.getUTCMonth()+1, 160 | createdAtYear: d.getUTCFullYear() 161 | } 162 | } 163 | }).then(({ data }) => { 164 | // console.log("SUCCESS"); 165 | }).catch((error) => { 166 | // console.log("FAILED"); 167 | }) 168 | } 169 | }) 170 | }) 171 | 172 | export default componentWithCreateMessage(componentWithMessageBoard(MessageBoard)); 173 | 174 | const styles = { 175 | }; 176 | -------------------------------------------------------------------------------- /src/js/components/App/MessageForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Row, Col} from 'react-bootstrap'; 3 | 4 | class MessageForm extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | content: '' 10 | } 11 | 12 | this.handleSubmit = this.handleSubmit.bind(this); 13 | this.changeHandler = this.changeHandler.bind(this); 14 | } 15 | 16 | componentDidMount() { 17 | } 18 | 19 | handleSubmit(e) { 20 | e.preventDefault(); 21 | let message = { 22 | content: this.state.content 23 | } 24 | if (message.content == '') return; 25 | this.props.onMessageSubmit(message); 26 | this.setState({ content: '' }); 27 | } 28 | 29 | changeHandler(e) { 30 | this.setState({ content : e.target.value }); 31 | } 32 | 33 | render() { 34 | return( 35 |
36 |

Say something great!

37 |
38 | 43 |
44 |
45 | ); 46 | } 47 | } 48 | 49 | export default MessageForm; 50 | 51 | const styles = { 52 | input: { 53 | fontSize: '20px', 54 | width: '100%', 55 | resize: 'both', 56 | overflow: 'auto' 57 | }, 58 | form: { 59 | margin: `30px 30px` 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import WebpackDevServer from 'webpack-dev-server'; 4 | import express from 'express'; 5 | import config from './config'; 6 | 7 | const APP_PORT = 3001; 8 | 9 | var compiler = webpack({ 10 | entry: path.resolve(__dirname, 'js', 'app.js'), 11 | module: { 12 | loaders: [ 13 | { 14 | exclude: /node_modules/, 15 | loader: 'babel', 16 | test: /\.js$/, 17 | } 18 | ] 19 | }, 20 | output: {filename: 'app.js', path: '/'} 21 | }); 22 | 23 | let contentBase = 'src/'; 24 | if (process.env.NODE_ENV === "production") { 25 | contentBase = 'lib/'; 26 | } 27 | 28 | var app = new WebpackDevServer(compiler, { 29 | contentBase: contentBase, 30 | publicPath: '/js/', 31 | proxy: { '/graphql': config.scapholdUrl }, 32 | stats: {colors: true} 33 | }); 34 | // Serve static resources 35 | app.use('/', express.static(path.resolve(__dirname, '/'))); 36 | app.listen(APP_PORT, () => { 37 | console.log(`App is now running on http://localhost:${APP_PORT}`); 38 | }); 39 | -------------------------------------------------------------------------------- /src/webpack.config.js: -------------------------------------------------------------------------------- 1 | let StaticSiteGeneratorPlugin = require('static-site-generator-webpack-plugin') 2 | let ExtractTextPlugin = require('extract-text-webpack-plugin') 3 | let webpack = require('webpack') 4 | 5 | const locals = { 6 | paths: [ 7 | '/' 8 | ] 9 | }; 10 | 11 | module.exports = ({ 12 | 13 | entry: { 14 | 'main': './server.js' 15 | }, 16 | 17 | plugins: [ 18 | new StaticSiteGeneratorPlugin('main', locals.paths, locals), 19 | new webpack.NoErrorsPlugin() 20 | // new ExtractTextPlugin('style.css') 21 | ], 22 | 23 | output: { 24 | filename: 'server.js', //sets our output filename to index.js 25 | path: 'dist', //sets our output directory to dist/ 26 | libraryTarget: 'umd' //nodejs and StaticSiteGeneratorWebpackPlugin require UMD or CommonJS 27 | }, 28 | 29 | module: { 30 | loaders: [ 31 | { 32 | test: /\.jsx?$/, 33 | exclude: /node_modules/, 34 | loader: 'babel', 35 | query: { 36 | presets: ['es2015', 'react'] 37 | } 38 | } 39 | ] 40 | }, 41 | 42 | watch: true 43 | 44 | }); --------------------------------------------------------------------------------