Commit c42d4eeaceb50a4195d854a48fe6122f086724e5

Authored by themeteorchef
1 parent 23c8a4c3c3
Exists in master

handful of changes

- Remove dependency on faker NPM package.
- Consolidate insert and update methods into single upsert method.
- Add /edit and /new pages for editing and creating documents.
- Move all exports to default exports (linter thing).
- Add a module for editing and creating documents.
- Update method tests for documents collection to use new upsert method.
- Oh, I'm sure there's something else...
... ... @@ -4,13 +4,13 @@ alanning:roles@1.2.15
4 4 aldeed:collection2@2.10.0
5 5 aldeed:collection2-core@1.2.0
6 6 aldeed:schema-deny@1.1.0
7   -aldeed:schema-index@1.1.0
  7 +aldeed:schema-index@1.1.1
8 8 aldeed:simple-schema@1.5.3
9 9 allow-deny@1.0.5
10 10 audit-argument-checks@1.0.7
11 11 autoupdate@1.3.12
12 12 babel-compiler@6.13.0
13   -babel-runtime@0.1.12
  13 +babel-runtime@0.1.13
14 14 base64@1.0.10
15 15 binary-heap@1.0.10
16 16 blaze@2.1.9
... ... @@ -40,7 +40,7 @@ email@1.1.18
40 40 es5-shim@4.6.15
41 41 fastclick@1.0.13
42 42 fortawesome:fontawesome@4.6.3
43   -fourseven:scss@3.10.0
  43 +fourseven:scss@3.10.1
44 44 geojson-utils@1.0.10
45 45 hot-code-push@1.0.4
46 46 html-tools@1.0.11
... ...
imports/api/documents/documents.js
1   -if (Meteor.isClient) import faker from 'faker';
2 1 import { Mongo } from 'meteor/mongo';
3 2 import { SimpleSchema } from 'meteor/aldeed:simple-schema';
4 3 import { Factory } from 'meteor/dburles:factory';
5 4  
6   -export const Documents = new Mongo.Collection('Documents');
  5 +const Documents = new Mongo.Collection('Documents');
  6 +export default Documents;
7 7  
8 8 Documents.allow({
9 9 insert: () => false,
... ... @@ -22,10 +22,15 @@ Documents.schema = new SimpleSchema({
22 22 type: String,
23 23 label: 'The title of the document.',
24 24 },
  25 + body: {
  26 + type: String,
  27 + label: 'The body of the document.',
  28 + },
25 29 });
26 30  
27 31 Documents.attachSchema(Documents.schema);
28 32  
29 33 Factory.define('document', Documents, {
30   - title: () => faker.hacker.phrase(),
  34 + title: () => 'Factory Title',
  35 + body: () => 'Factory Body',
31 36 });
... ...
imports/api/documents/documents.tests.js
... ... @@ -2,7 +2,7 @@
2 2 /* eslint-disable func-names, prefer-arrow-callback */
3 3  
4 4 import { assert } from 'meteor/practicalmeteor:chai';
5   -import { Documents } from './documents.js';
  5 +import Documents from './documents.js';
6 6  
7 7 describe('Documents collection', function () {
8 8 it('registers the collection with Mongo properly', function () {
... ...
imports/api/documents/methods.js
1 1 import { SimpleSchema } from 'meteor/aldeed:simple-schema';
2 2 import { ValidatedMethod } from 'meteor/mdg:validated-method';
3   -import { Documents } from './documents';
  3 +import Documents from './documents';
4 4 import { rateLimit } from '../../modules/rate-limit.js';
5 5  
6   -export const insertDocument = new ValidatedMethod({
7   - name: 'documents.insert',
  6 +export const upsertDocument = new ValidatedMethod({
  7 + name: 'documents.upsert',
8 8 validate: new SimpleSchema({
9   - title: { type: String },
  9 + _id: { type: String, optional: true },
  10 + title: { type: String, optional: true },
  11 + body: { type: String, optional: true },
10 12 }).validator(),
11 13 run(document) {
12   - Documents.insert(document);
13   - },
14   -});
15   -
16   -export const updateDocument = new ValidatedMethod({
17   - name: 'documents.update',
18   - validate: new SimpleSchema({
19   - _id: { type: String },
20   - 'update.title': { type: String, optional: true },
21   - }).validator(),
22   - run({ _id, update }) {
23   - Documents.update(_id, { $set: update });
  14 + return Documents.upsert({ _id: document._id }, { $set: document });
24 15 },
25 16 });
26 17  
... ... @@ -36,8 +27,7 @@ export const removeDocument = new ValidatedMethod({
36 27  
37 28 rateLimit({
38 29 methods: [
39   - insertDocument,
40   - updateDocument,
  30 + upsertDocument,
41 31 removeDocument,
42 32 ],
43 33 limit: 5,
... ...
imports/api/documents/methods.tests.js
... ... @@ -5,8 +5,8 @@ import { Meteor } from 'meteor/meteor';
5 5 import { assert } from 'meteor/practicalmeteor:chai';
6 6 import { resetDatabase } from 'meteor/xolvio:cleaner';
7 7 import { Factory } from 'meteor/dburles:factory';
8   -import { Documents } from './documents.js';
9   -import { insertDocument, updateDocument, removeDocument } from './methods.js';
  8 +import Documents from './documents.js';
  9 +import { upsertDocument, removeDocument } from './methods.js';
10 10  
11 11 describe('Documents methods', function () {
12 12 beforeEach(function () {
... ... @@ -16,19 +16,22 @@ describe('Documents methods', function () {
16 16 });
17 17  
18 18 it('inserts a document into the Documents collection', function () {
19   - insertDocument.call({ title: 'You can\'t arrest me, I\'m the Cake Boss!' });
  19 + upsertDocument.call({
  20 + title: 'You can\'t arrest me, I\'m the Cake Boss!',
  21 + body: 'They went nuts!',
  22 + });
  23 +
20 24 const getDocument = Documents.findOne({ title: 'You can\'t arrest me, I\'m the Cake Boss!' });
21   - assert.equal(getDocument.title, 'You can\'t arrest me, I\'m the Cake Boss!');
  25 + assert.equal(getDocument.body, 'They went nuts!');
22 26 });
23 27  
24 28 it('updates a document in the Documents collection', function () {
25 29 const { _id } = Factory.create('document');
26 30  
27   - updateDocument.call({
  31 + upsertDocument.call({
28 32 _id,
29   - update: {
30   - title: 'You can\'t arrest me, I\'m the Cake Boss!',
31   - },
  33 + title: 'You can\'t arrest me, I\'m the Cake Boss!',
  34 + body: 'They went nuts!',
32 35 });
33 36  
34 37 const getDocument = Documents.findOne(_id);
... ...
imports/api/documents/server/publications.js
1 1 import { Meteor } from 'meteor/meteor';
2   -import { Documents } from '../documents';
  2 +import { check } from 'meteor/check';
  3 +import Documents from '../documents';
3 4  
4   -Meteor.publish('documents', () => Documents.find());
  5 +Meteor.publish('documents.list', () => Documents.find());
  6 +
  7 +Meteor.publish('documents.view', (_id) => {
  8 + check(_id, String);
  9 + return Documents.find(_id);
  10 +});
  11 +
  12 +Meteor.publish('documents.edit', (_id) => {
  13 + check(_id, String);
  14 + return Documents.find(_id);
  15 +});
... ...
imports/modules/document-editor.js
... ... @@ -0,0 +1,55 @@
  1 +import $ from 'jquery';
  2 +import 'jquery-validation';
  3 +import { browserHistory } from 'react-router';
  4 +import { Bert } from 'meteor/themeteorchef:bert';
  5 +import { upsertDocument } from '../api/documents/methods.js';
  6 +
  7 +let component;
  8 +
  9 +const handleUpsert = () => {
  10 + const { doc } = component.props;
  11 + const confirmation = doc && doc._id ? 'Document updated!' : 'Document added!';
  12 + const upsert = {
  13 + title: document.querySelector('[name="title"]').value.trim(),
  14 + body: document.querySelector('[name="body"]').value.trim(),
  15 + };
  16 +
  17 + if (doc && doc._id) upsert._id = doc._id;
  18 +
  19 + upsertDocument.call(upsert, (error, { insertedId }) => {
  20 + if (error) {
  21 + Bert.alert(error.reason, 'danger');
  22 + } else {
  23 + component.form.reset();
  24 + Bert.alert(confirmation, 'success');
  25 + browserHistory.push(`/documents/${insertedId || doc._id}`);
  26 + }
  27 + });
  28 +};
  29 +
  30 +const validate = () => {
  31 + $(component.form).validate({
  32 + rules: {
  33 + title: {
  34 + required: true,
  35 + },
  36 + body: {
  37 + required: true,
  38 + },
  39 + },
  40 + messages: {
  41 + title: {
  42 + required: 'Need a title in here, Seuss.',
  43 + },
  44 + body: {
  45 + required: 'This thneeds a body, please.',
  46 + },
  47 + },
  48 + submitHandler() { handleUpsert(); },
  49 + });
  50 +};
  51 +
  52 +export default function handleLogin(options) {
  53 + component = options.component;
  54 + validate();
  55 +}
... ...
imports/modules/login.js
... ... @@ -27,7 +27,7 @@ const login = () => {
27 27 };
28 28  
29 29 const validate = () => {
30   - $(component.refs.login).validate({
  30 + $('.login').validate({
31 31 rules: {
32 32 emailAddress: {
33 33 required: true,
... ...
imports/startup/client/routes.js
  1 +/* eslint-disable max-len */
  2 +
1 3 import React from 'react';
2 4 import { render } from 'react-dom';
3 5 import { Router, Route, IndexRoute, browserHistory } from 'react-router';
4 6 import { Meteor } from 'meteor/meteor';
5   -import { App } from '../../ui/layouts/App.js';
6   -import { Documents } from '../../ui/pages/Documents.js';
7   -import { Index } from '../../ui/pages/Index.js';
8   -import { Login } from '../../ui/pages/Login.js';
9   -import { NotFound } from '../../ui/pages/NotFound.js';
10   -import { RecoverPassword } from '../../ui/pages/RecoverPassword.js';
11   -import { ResetPassword } from '../../ui/pages/ResetPassword.js';
12   -import { Signup } from '../../ui/pages/Signup.js';
  7 +import App from '../../ui/layouts/App.js';
  8 +import Documents from '../../ui/pages/Documents.js';
  9 +import NewDocument from '../../ui/pages/NewDocument.js';
  10 +import EditDocument from '../../ui/containers/EditDocument.js';
  11 +import ViewDocument from '../../ui/containers/ViewDocument.js';
  12 +import Index from '../../ui/pages/Index.js';
  13 +import Login from '../../ui/pages/Login.js';
  14 +import NotFound from '../../ui/pages/NotFound.js';
  15 +import RecoverPassword from '../../ui/pages/RecoverPassword.js';
  16 +import ResetPassword from '../../ui/pages/ResetPassword.js';
  17 +import Signup from '../../ui/pages/Signup.js';
13 18  
14 19 const requireAuth = (nextState, replace) => {
15 20 if (!Meteor.loggingIn() && !Meteor.userId()) {
... ... @@ -26,6 +31,9 @@ Meteor.startup(() => {
26 31 <Route path="/" component={ App }>
27 32 <IndexRoute name="index" component={ Index } onEnter={ requireAuth } />
28 33 <Route name="documents" path="/documents" component={ Documents } onEnter={ requireAuth } />
  34 + <Route name="newDocument" path="/documents/new" component={ NewDocument } onEnter={ requireAuth } />
  35 + <Route name="editDocument" path="/documents/:_id/edit" component={ EditDocument } onEnter={ requireAuth } />
  36 + <Route name="editDocument" path="/documents/:_id" component={ ViewDocument } onEnter={ requireAuth } />
29 37 <Route name="login" path="/login" component={ Login } />
30 38 <Route name="recover-password" path="/recover-password" component={ RecoverPassword } />
31 39 <Route name="reset-password" path="/reset-password/:token" component={ ResetPassword } />
... ...
imports/ui/components/AppNavigation.js
1 1 import React from 'react';
2 2 import { Navbar } from 'react-bootstrap';
3 3 import { Link } from 'react-router';
4   -import { PublicNavigation } from './PublicNavigation.js';
5   -import { AuthenticatedNavigation } from './AuthenticatedNavigation.js';
  4 +import PublicNavigation from './PublicNavigation.js';
  5 +import AuthenticatedNavigation from './AuthenticatedNavigation.js';
6 6  
7 7 const renderNavigation = hasUser => (hasUser ? <AuthenticatedNavigation /> : <PublicNavigation />);
8 8  
... ...
imports/ui/components/DocumentEditor.js
... ... @@ -0,0 +1,45 @@
  1 +/* eslint-disable max-len, no-return-assign */
  2 +
  3 +import React from 'react';
  4 +import { FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap';
  5 +import documentEditor from '../../modules/document-editor.js';
  6 +
  7 +export default class DocumentEditor extends React.Component {
  8 + componentDidMount() {
  9 + documentEditor({ component: this });
  10 + }
  11 +
  12 + render() {
  13 + const { doc } = this.props;
  14 + return (<form
  15 + ref={ form => this.form = form }
  16 + onSubmit={ event => event.preventDefault() }
  17 + >
  18 + <FormGroup>
  19 + <ControlLabel>Title</ControlLabel>
  20 + <FormControl
  21 + type="text"
  22 + name="title"
  23 + defaultValue={ doc && doc.title }
  24 + placeholder="Oh, The Places You'll Go!"
  25 + />
  26 + </FormGroup>
  27 + <FormGroup>
  28 + <ControlLabel>Body</ControlLabel>
  29 + <FormControl
  30 + componentClass="textarea"
  31 + name="body"
  32 + defaultValue={ doc && doc.body }
  33 + placeholder="Congratulations! Today is your day. You're off to Great Places! You're off and away!"
  34 + />
  35 + </FormGroup>
  36 + <Button type="submit" bsStyle="success">
  37 + { doc && doc._id ? 'Save Changes' : 'Add Document' }
  38 + </Button>
  39 + </form>);
  40 + }
  41 +}
  42 +
  43 +DocumentEditor.propTypes = {
  44 + doc: React.PropTypes.object,
  45 +};
... ...
imports/ui/components/DocumentsList.js
1 1 import React from 'react';
2   -import { ListGroup, Alert } from 'react-bootstrap';
3   -import { Document } from './Document.js';
  2 +import { ListGroup, ListGroupItem, Alert } from 'react-bootstrap';
4 3  
5 4 const DocumentsList = ({ documents }) => (
6   - documents.length > 0 ? <ListGroup className="documents-list">
7   - {documents.map(doc => (
8   - <Document key={ doc._id } document={ doc } />
  5 + documents.length > 0 ? <ListGroup className="DocumentsList">
  6 + {documents.map(({ _id, title }) => (
  7 + <ListGroupItem key={ _id } href={`/documents/${_id}`}>{ title }</ListGroupItem>
9 8 ))}
10 9 </ListGroup> :
11 10 <Alert bsStyle="warning">No documents yet.</Alert>
... ...
imports/ui/containers/AppNavigation.js
1 1 import { composeWithTracker } from 'react-komposer';
2 2 import { Meteor } from 'meteor/meteor';
3   -import { AppNavigation } from '../components/AppNavigation.js';
  3 +import AppNavigation from '../components/AppNavigation.js';
4 4  
5 5 const composer = (props, onData) => onData(null, { hasUser: Meteor.user() });
6 6  
... ...
imports/ui/containers/DocumentsList.js
1 1 import { composeWithTracker } from 'react-komposer';
2 2 import { Meteor } from 'meteor/meteor';
3   -import { Documents } from '../../api/documents/documents.js';
4   -import { DocumentsList } from '../components/DocumentsList.js';
5   -import { Loading } from '../components/Loading.js';
  3 +import Documents from '../../api/documents/documents.js';
  4 +import DocumentsList from '../components/DocumentsList.js';
  5 +import Loading from '../components/Loading.js';
6 6  
7 7 const composer = (params, onData) => {
8   - const subscription = Meteor.subscribe('documents');
  8 + const subscription = Meteor.subscribe('documents.list');
9 9 if (subscription.ready()) {
10 10 const documents = Documents.find().fetch();
11 11 onData(null, { documents });
... ...
imports/ui/containers/EditDocument.js
... ... @@ -0,0 +1,16 @@
  1 +import { Meteor } from 'meteor/meteor';
  2 +import { composeWithTracker } from 'react-komposer';
  3 +import Documents from '../../api/documents/documents.js';
  4 +import EditDocument from '../pages/EditDocument.js';
  5 +import { Loading } from '../components/Loading.js';
  6 +
  7 +const composer = ({ params }, onData) => {
  8 + const subscription = Meteor.subscribe('documents.edit', params._id);
  9 +
  10 + if (subscription.ready()) {
  11 + const doc = Documents.findOne();
  12 + onData(null, { doc });
  13 + }
  14 +};
  15 +
  16 +export default composeWithTracker(composer, Loading)(EditDocument);
... ...
imports/ui/containers/ViewDocument.js
... ... @@ -0,0 +1,16 @@
  1 +import { Meteor } from 'meteor/meteor';
  2 +import { composeWithTracker } from 'react-komposer';
  3 +import Documents from '../../api/documents/documents.js';
  4 +import ViewDocument from '../pages/ViewDocument.js';
  5 +import { Loading } from '../components/Loading.js';
  6 +
  7 +const composer = ({ params }, onData) => {
  8 + const subscription = Meteor.subscribe('documents.view', params._id);
  9 +
  10 + if (subscription.ready()) {
  11 + const doc = Documents.findOne();
  12 + onData(null, { doc });
  13 + }
  14 +};
  15 +
  16 +export default composeWithTracker(composer, Loading)(ViewDocument);
... ...
imports/ui/pages/EditDocument.js
... ... @@ -0,0 +1,15 @@
  1 +import React from 'react';
  2 +import DocumentEditor from '../components/DocumentEditor.js';
  3 +
  4 +const EditDocument = ({ doc }) => (
  5 + <div className="EditDocument">
  6 + <h4 className="page-header">Editing "{ doc.title }"</h4>
  7 + <DocumentEditor doc={ doc } />
  8 + </div>
  9 +);
  10 +
  11 +EditDocument.propTypes = {
  12 + doc: React.PropTypes.object,
  13 +};
  14 +
  15 +export default EditDocument;
... ...
imports/ui/pages/NewDocument.js
... ... @@ -0,0 +1,11 @@
  1 +import React from 'react';
  2 +import DocumentEditor from '../components/DocumentEditor.js';
  3 +
  4 +const NewDocument = () => (
  5 + <div className="NewDocument">
  6 + <h4 className="page-header">New Document</h4>
  7 + <DocumentEditor />
  8 + </div>
  9 +);
  10 +
  11 +export default NewDocument;
... ...
imports/ui/pages/RecoverPassword.js
1 1 import React from 'react';
2 2 import { Row, Col, Alert, FormGroup, FormControl, Button } from 'react-bootstrap';
3   -import { handleRecoverPassword } from '../../modules/recover-password';
  3 +import handleRecoverPassword from '../../modules/recover-password';
4 4  
5 5 export default class RecoverPassword extends React.Component {
6 6 componentDidMount() {
7 7 handleRecoverPassword({ component: this });
8 8 }
9 9  
10   - handleSubmit() {
11   - this.preventDefault();
  10 + handleSubmit(event) {
  11 + event.preventDefault();
12 12 }
13 13  
14 14 render() {
... ...
imports/ui/pages/ResetPassword.js
1 1 import React from 'react';
2 2 import { Row, Col, Alert, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap';
3   -import { handleResetPassword } from '../../modules/reset-password';
  3 +import handleResetPassword from '../../modules/reset-password';
4 4  
5 5 export default class ResetPassword extends React.Component {
6 6 componentDidMount() {
7 7 handleResetPassword({ component: this, token: this.props.params.token });
8 8 }
9 9  
10   - handleSubmit() {
11   - this.preventDefault();
  10 + handleSubmit(event) {
  11 + event.preventDefault();
12 12 }
13 13  
14 14 render() {
... ...
imports/ui/pages/ViewDocument.js
... ... @@ -0,0 +1,39 @@
  1 +import React from 'react';
  2 +import { ButtonToolbar, ButtonGroup, Button } from 'react-bootstrap';
  3 +import { browserHistory } from 'react-router';
  4 +import { Bert } from 'meteor/themeteorchef:bert';
  5 +import { removeDocument } from '../../api/documents/methods.js';
  6 +
  7 +const handleRemove = (_id) => {
  8 + if (confirm('Are you sure? This is permanent!')) {
  9 + removeDocument.call({ _id }, (error) => {
  10 + if (error) {
  11 + Bert.alert(error.reason, 'danger');
  12 + } else {
  13 + Bert.alert('Document deleted!', 'success');
  14 + browserHistory.push('/documents');
  15 + }
  16 + });
  17 + }
  18 +};
  19 +
  20 +const ViewDocument = ({ doc }) => (
  21 + <div className="ViewDocument">
  22 + <div className="page-header clearfix">
  23 + <h4 className="pull-left">{ doc.title }</h4>
  24 + <ButtonToolbar className="pull-right">
  25 + <ButtonGroup bsSize="small">
  26 + <Button href={`/documents/${doc._id}/edit`}>Edit</Button>
  27 + <Button onClick={ () => handleRemove(doc._id) } className="text-danger">Delete</Button>
  28 + </ButtonGroup>
  29 + </ButtonToolbar>
  30 + </div>
  31 + { doc.body }
  32 + </div>
  33 +);
  34 +
  35 +ViewDocument.propTypes = {
  36 + doc: React.PropTypes.object.isRequired,
  37 +};
  38 +
  39 +export default ViewDocument;
... ...
imports/ui/pages/documents.js
1 1 import React from 'react';
2   -import { Row, Col } from 'react-bootstrap';
  2 +import { Row, Col, Button } from 'react-bootstrap';
3 3 import DocumentsList from '../containers/DocumentsList.js';
4   -import { AddDocument } from '../components/AddDocument.js';
5 4  
6 5 const Documents = () => (
7 6 <Row>
8 7 <Col xs={ 12 }>
9   - <h4 className="page-header">Documents</h4>
10   - <AddDocument />
  8 + <div className="page-header clearfix">
  9 + <h4 className="pull-left">Documents</h4>
  10 + <Button
  11 + bsStyle="success"
  12 + className="pull-right"
  13 + href="/documents/new"
  14 + >New Document</Button>
  15 + </div>
11 16 <DocumentsList />
12 17 </Col>
13 18 </Row>
... ...
imports/ui/pages/login.js
1 1 import React from 'react';
2 2 import { Link } from 'react-router';
3 3 import { Row, Col, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap';
4   -import { handleLogin } from '../../modules/login';
  4 +import handleLogin from '../../modules/login';
5 5  
6 6 export default class Login extends React.Component {
7 7 componentDidMount() {
8 8 handleLogin({ component: this });
9 9 }
10 10  
11   - handleSubmit() {
12   - this.preventDefault();
  11 + handleSubmit(event) {
  12 + event.preventDefault();
13 13 }
14 14  
15 15 render() {
... ...
imports/ui/pages/signup.js
1 1 import React from 'react';
2 2 import { Link } from 'react-router';
3 3 import { Row, Col, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap';
4   -import { handleSignup } from '../../modules/signup';
  4 +import handleSignup from '../../modules/signup';
5 5  
6 6 export default class Signup extends React.Component {
7 7 componentDidMount() {
8 8 handleSignup({ component: this });
9 9 }
10 10  
11   - handleSubmit() {
12   - this.preventDefault();
  11 + handleSubmit(event) {
  12 + event.preventDefault();
13 13 }
14 14  
15 15 render() {
... ...
... ... @@ -17,8 +17,7 @@
17 17 "eslint-plugin-import": "^1.16.0",
18 18 "eslint-plugin-jsx-a11y": "^2.2.3",
19 19 "eslint-plugin-meteor": "^4.0.1",
20   - "eslint-plugin-react": "^6.4.1",
21   - "faker": "^3.1.0"
  20 + "eslint-plugin-react": "^6.4.1"
22 21 },
23 22 "eslintConfig": {
24 23 "parserOptions": {
... ... @@ -58,13 +57,14 @@
58 57 "_name"
59 58 ]
60 59 }
61   - ]
  60 + ],
  61 + "class-methods-use-this": 0
62 62 }
63 63 },
64 64 "dependencies": {
65 65 "bcrypt": "^0.8.7",
66 66 "bootstrap": "^3.3.7",
67   - "jquery": "^3.1.1",
  67 + "jquery": "^2.2.4",
68 68 "jquery-validation": "^1.15.1",
69 69 "react": "^15.3.2",
70 70 "react-addons-pure-render-mixin": "^15.3.2",
... ...