├── .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 | 
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 |
163 |
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 |
33 | How To Use
34 |
35 |
36 |
37 | How To Use
38 |
39 |
40 |
41 | This web page was made using GraphQL Subscriptions to power a real-time app .
42 |
43 | The messaging client along with the analytics dashboard demonstrate the power of web sockets.
44 | In order to see the magic happen:
45 |
46 |
47 | Open up two different browser tabs side by side so you can see both.
48 |
49 |
50 | Add a message to either one of the sites.
51 |
52 |
53 | You should notice the Conversation History and Mesage Graph update automatically in both browsers as it's listening to changes from the server.
54 |
55 |
56 |
57 |
58 |
59 | If you like what you see, join our Slack channel today to learn more !
60 |
61 |
62 |
63 |
64 | Here's what we used to build this app:
65 |
66 |
67 | This React.js boilerplate helps developers create modern, performant, and clean web apps with the help of Scaphold.io.
68 |
69 |
70 |
71 |
72 | Leverage the simplicity and power of Socket.io and GraphQL to enable real-time functionality to create apps like messaging clients and analytics dashboards.
73 |
74 |
75 |
76 |
77 | Smoothe and creative components to fit the way you want your apps to be experienced.
78 |
79 |
80 |
81 |
82 | Webpack is a module bundler that helps you serve your application in any environment with hot reloading.
83 |
84 |
85 |
86 |
87 | If you have any questions, please contact support@scaphold.io .
88 |
89 |
90 |
91 |
92 | Close
93 |
94 |
95 |
96 |
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 |
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 | });
--------------------------------------------------------------------------------