diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..f9ced93c --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +packages diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..91df3274 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,62 @@ +{ + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + }, + "allowImportExportEverywhere": true + }, + "plugins": [ + "meteor" + ], + "extends": [ + "airbnb", + "plugin:meteor/recommended" + ], + "rules": { + "import/extensions": "off", + "import/no-extraneous-dependencies": "off", + "import/prefer-default-export": "off", + "no-underscore-dangle": "off", + "class-methods-use-this": "off", + "meteor/no-session": "off", + "object-shorthand": [ + "error", + "always", + { + "avoidQuotes": false + } + ], + "meteor/eventmap-params": [ + "error", + { + "eventParamName": "event", + "templateInstanceParamName": "instance" + } + ], + "meteor/template-names": [ + "off" + ], + "react/jsx-filename-extension": [ + 1, + { + "extensions": [ + ".js", + ".jsx" + ] + } + ], + "react/forbid-prop-types": "off", + "jsx-a11y/no-static-element-interactions": "off", + "prefer-arrow-callback": "off" + }, + "settings": { + "import/resolver": "meteor" + }, + "globals": { + "window": true, + "document": true + } +} diff --git a/.gitignore b/.gitignore index e9964c3c..cde5f439 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,17 @@ -.DS_Store settings-production.json -npm-debug.log +# OSX +# +.DS_Store +.tmp/ + +# IntelliJ +# +.idea + +# node.js +# node_modules +npm-debug.log +yarn-error.log + diff --git a/.meteor/packages b/.meteor/packages index ba54c24f..d151087f 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -12,18 +12,17 @@ session@1.1.7 tracker@1.1.2 # Meteor's client-side reactive programming library standard-minifier-css@1.3.4 # CSS minifier run for production mode -standard-minifier-js@1.2.3 # JS minifier run for production mode +standard-minifier-js@2.0.0 # JS minifier run for production mode es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers. -ecmascript@0.6.3 # Enable ECMAScript2015+ syntax in app code +ecmascript@0.7.2 # Enable ECMAScript2015+ syntax in app code -accounts-password@1.3.4 -accounts-base@1.2.15 +accounts-password@1.3.5 +accounts-base@1.2.16 check@1.2.5 audit-argument-checks@1.0.7 browser-policy@1.1.0 fourseven:scss@4.5.0 -aldeed:collection2 alanning:roles themeteorchef:bert static-html@1.1.11 @@ -34,3 +33,4 @@ mdg:validated-method dburles:factory@1.0.0 ddp-rate-limiter@1.0.7 shell-server@0.2.3 +aldeed:collection2-core@2.0.0 diff --git a/.meteor/release b/.meteor/release index 31ce024f..605b4e1f 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.4.3.2 +METEOR@1.4.4.1 diff --git a/.meteor/versions b/.meteor/versions index e2954800..e73eb94f 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,19 +1,15 @@ -accounts-base@1.2.15 -accounts-password@1.3.4 +accounts-base@1.2.16 +accounts-password@1.3.5 alanning:roles@1.2.16 -aldeed:collection2@2.10.0 -aldeed:collection2-core@1.2.0 -aldeed:schema-deny@1.1.0 -aldeed:schema-index@1.1.1 -aldeed:simple-schema@1.5.3 +aldeed:collection2-core@2.0.0 allow-deny@1.0.5 audit-argument-checks@1.0.7 autoupdate@1.3.12 -babel-compiler@6.14.1 +babel-compiler@6.18.2 babel-runtime@1.0.1 base64@1.0.10 binary-heap@1.0.10 -blaze@2.3.0 +blaze@2.3.2 blaze-tools@1.0.10 boilerplate-generator@1.0.11 browser-policy@1.1.0 @@ -21,22 +17,22 @@ browser-policy-common@1.0.11 browser-policy-content@1.1.0 browser-policy-framing@1.1.0 caching-compiler@1.1.9 -caching-html-compiler@1.1.0 +caching-html-compiler@1.1.2 callback-hook@1.0.10 check@1.2.5 coffeescript@1.12.3_1 dburles:factory@1.1.0 ddp@1.2.5 -ddp-client@1.3.3 +ddp-client@1.3.4 ddp-common@1.2.8 ddp-rate-limiter@1.0.7 -ddp-server@1.3.13 +ddp-server@1.3.14 deps@1.0.12 diff-sequence@1.0.7 -ecmascript@0.6.3 +ecmascript@0.7.3 ecmascript-runtime@0.3.15 ejson@1.0.13 -email@1.1.18 +email@1.2.1 es5-shim@4.6.15 fastclick@1.0.13 fortawesome:fontawesome@4.7.0 @@ -53,16 +49,15 @@ livedata@1.0.18 localstorage@1.0.12 logging@1.1.17 mdg:validated-method@1.1.0 -mdg:validation-error@0.5.1 meteor@1.6.1 meteor-base@1.0.4 minifier-css@1.2.16 -minifier-js@1.2.18 +minifier-js@2.0.0 minimongo@1.0.21 mobile-experience@1.0.4 mobile-status-bar@1.0.14 -modules@0.7.9 -modules-runtime@0.7.9 +modules@0.8.2 +modules-runtime@0.7.10 mongo@1.1.16 mongo-id@1.0.6 npm-bcrypt@0.9.2 @@ -77,7 +72,7 @@ practicalmeteor:sinon@1.14.1_2 promise@0.8.8 raix:eventemitter@0.1.3 random@1.0.10 -rate-limit@1.0.7 +rate-limit@1.0.8 reactive-dict@1.1.8 reactive-var@1.0.11 reload@1.1.11 @@ -87,23 +82,24 @@ service-configuration@1.0.11 session@1.1.7 sha@1.0.9 shell-server@0.2.3 -spacebars@1.0.13 -spacebars-compiler@1.1.0 +spacebars@1.0.15 +spacebars-compiler@1.1.2 srp@1.0.10 standard-minifier-css@1.3.4 -standard-minifier-js@1.2.3 -static-html@1.2.0 -templating@1.3.0 -templating-compiler@1.3.0 -templating-runtime@1.3.0 -templating-tools@1.1.0 +standard-minifier-js@2.0.0 +static-html@1.2.2 +templating@1.3.2 +templating-compiler@1.3.2 +templating-runtime@1.3.2 +templating-tools@1.1.2 themeteorchef:bert@2.1.2 +tmeasday:check-npm-versions@0.3.1 tmeasday:test-reporter-helpers@0.2.1 tracker@1.1.2 -ui@1.0.12 +ui@1.0.13 underscore@1.0.10 url@1.1.0 -webapp@1.3.14 +webapp@1.3.15 webapp-hashing@1.0.9 xolvio:backdoor@0.2.1 xolvio:cleaner@0.3.1 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..a043f11c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,11 @@ +MIT + +--- + +Copyright (c) 2014-2017 The Meteor Chef + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index b57f0053..f04512d6 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ A starting point for Meteor apps. Base Version - v4.14.0 + v4.15.0 Meteor Version - v1.4.3.2 + v1.4.4.1 diff --git a/imports/api/documents/documents.js b/imports/api/documents/documents.js index 58c3ae4c..c280dc98 100644 --- a/imports/api/documents/documents.js +++ b/imports/api/documents/documents.js @@ -1,5 +1,5 @@ import { Mongo } from 'meteor/mongo'; -import { SimpleSchema } from 'meteor/aldeed:simple-schema'; +import SimpleSchema from 'simpl-schema'; import { Factory } from 'meteor/dburles:factory'; const Documents = new Mongo.Collection('Documents'); diff --git a/imports/api/documents/methods.js b/imports/api/documents/methods.js index 0ded4d74..7468c6e4 100644 --- a/imports/api/documents/methods.js +++ b/imports/api/documents/methods.js @@ -1,4 +1,4 @@ -import { SimpleSchema } from 'meteor/aldeed:simple-schema'; +import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import Documents from './documents'; import rateLimit from '../../modules/rate-limit.js'; diff --git a/imports/api/users/server/publications.js b/imports/api/users/server/publications.js new file mode 100644 index 00000000..7e247b18 --- /dev/null +++ b/imports/api/users/server/publications.js @@ -0,0 +1,13 @@ +import { Meteor } from 'meteor/meteor'; + +Meteor.publish('users.info', function userInfoPublish() { + if (!this.userId) { + return this.ready(); + } + + return Meteor.users.find(this.userId, { + fields: { + name: 1, + }, + }); +}); diff --git a/imports/ui/components/AppNavigation.js b/imports/client/ui/components/AppNavigation.js similarity index 60% rename from imports/ui/components/AppNavigation.js rename to imports/client/ui/components/AppNavigation.js index 319c9362..d2aa978d 100644 --- a/imports/ui/components/AppNavigation.js +++ b/imports/client/ui/components/AppNavigation.js @@ -1,8 +1,11 @@ +import { Meteor } from 'meteor/meteor'; import React from 'react'; import { Navbar } from 'react-bootstrap'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; +import { PropTypes } from 'prop-types'; import PublicNavigation from './PublicNavigation.js'; import AuthenticatedNavigation from './AuthenticatedNavigation.js'; +import container from '../../../modules/container'; const renderNavigation = hasUser => (hasUser ? : ); @@ -21,7 +24,12 @@ const AppNavigation = ({ hasUser }) => ( ); AppNavigation.propTypes = { - hasUser: React.PropTypes.object, + hasUser: PropTypes.object, }; -export default AppNavigation; +export default container((props, onData) => { + const subscription = Meteor.subscribe('users.info'); + if (subscription.ready()) { + onData(null, { hasUser: Meteor.user() }); + } +}, AppNavigation); diff --git a/imports/client/ui/components/AuthenticatedNavigation.js b/imports/client/ui/components/AuthenticatedNavigation.js new file mode 100644 index 00000000..866c69b9 --- /dev/null +++ b/imports/client/ui/components/AuthenticatedNavigation.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { withRouter } from 'react-router-dom'; +import { PropTypes } from 'prop-types'; +import { LinkContainer } from 'react-router-bootstrap'; +import { Nav, NavItem, NavDropdown, MenuItem } from 'react-bootstrap'; +import { Meteor } from 'meteor/meteor'; + +const userName = () => { + const user = Meteor.user(); + const name = user ? user.name : ''; + return user ? `${name.first} ${name.last}` : ''; +}; + +const AuthenticatedNavigation = ({ history }) => ( +
+ + +
+); + +AuthenticatedNavigation.defaultProps = { + history: null, +}; + +AuthenticatedNavigation.propTypes = { + history: PropTypes.object, +}; + +export default withRouter(AuthenticatedNavigation); diff --git a/imports/client/ui/components/DocumentEditor.js b/imports/client/ui/components/DocumentEditor.js new file mode 100644 index 00000000..3cea5821 --- /dev/null +++ b/imports/client/ui/components/DocumentEditor.js @@ -0,0 +1,57 @@ +/* eslint-disable max-len, no-return-assign */ + +import React, { Component } from 'react'; +import { FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; +import { withRouter } from 'react-router-dom'; +import { PropTypes } from 'prop-types'; +import documentEditor from '../../../modules/document-editor.js'; + +class DocumentEditor extends Component { + componentDidMount() { + documentEditor({ component: this }); + setTimeout(() => { document.querySelector('[name="title"]').focus(); }, 0); + } + + render() { + const { doc } = this.props; + + return ( +
(this.documentEditorForm = form)} + onSubmit={event => event.preventDefault()} + > + + Title + + + + Body + + + +
+ ); + } +} + +DocumentEditor.defaultProps = { + doc: null, +}; + +DocumentEditor.propTypes = { + doc: PropTypes.object, +}; + +export default withRouter(DocumentEditor); diff --git a/imports/client/ui/components/DocumentsList.js b/imports/client/ui/components/DocumentsList.js new file mode 100644 index 00000000..26f72ecd --- /dev/null +++ b/imports/client/ui/components/DocumentsList.js @@ -0,0 +1,38 @@ +import { Meteor } from 'meteor/meteor'; +import React from 'react'; +import { ListGroup, ListGroupItem, Alert } from 'react-bootstrap'; +import { withRouter } from 'react-router-dom'; +import { PropTypes } from 'prop-types'; +import Documents from '../../../api/documents/documents'; +import container from '../../../modules/container'; +import Loading from '../components/Loading'; + +const DocumentsList = ({ documents, history }) => ( + documents.length > 0 ? + {documents.map(({ _id, title }) => ( + history.push(`/documents/${_id}`)}> + { title } + + ))} + : + No documents yet. +); + +DocumentsList.defaultProps = { + documents: [], + history: null, +}; + +DocumentsList.propTypes = { + documents: PropTypes.array, + history: PropTypes.object, +}; + +export default withRouter(container((props, onData) => { + const subscription = Meteor.subscribe('documents.list'); + if (subscription.ready()) { + const documents = Documents.find().fetch(); + onData(null, { documents }); + } +}, DocumentsList, { loadingHandler: () => })); + diff --git a/imports/ui/components/Loading.js b/imports/client/ui/components/Loading.js similarity index 100% rename from imports/ui/components/Loading.js rename to imports/client/ui/components/Loading.js diff --git a/imports/ui/components/PublicNavigation.js b/imports/client/ui/components/PublicNavigation.js similarity index 72% rename from imports/ui/components/PublicNavigation.js rename to imports/client/ui/components/PublicNavigation.js index d04fc80f..a590f554 100644 --- a/imports/ui/components/PublicNavigation.js +++ b/imports/client/ui/components/PublicNavigation.js @@ -5,10 +5,10 @@ import { Nav, NavItem } from 'react-bootstrap'; const PublicNavigation = () => ( ); diff --git a/imports/client/ui/layouts/App.js b/imports/client/ui/layouts/App.js new file mode 100644 index 00000000..b7ec52f7 --- /dev/null +++ b/imports/client/ui/layouts/App.js @@ -0,0 +1,41 @@ +/* eslint-disable max-len */ + +import React from 'react'; +import { BrowserRouter, Route, Switch } from 'react-router-dom'; +import { Grid } from 'react-bootstrap'; +import PrivateRoute from '../../../modules/private-route'; +import AppNavigation from '../components/AppNavigation'; +import EditDocument from '../pages/EditDocument'; +import ViewDocument from '../pages/ViewDocument'; +import Documents from '../pages/Documents'; +import NewDocument from '../pages/NewDocument'; +import Index from '../pages/Index'; +import Login from '../pages/Login'; +import NotFound from '../pages/NotFound'; +import RecoverPassword from '../pages/RecoverPassword'; +import ResetPassword from '../pages/ResetPassword'; +import Signup from '../pages/Signup'; + +const App = () => ( + +
+ + + + + + + + + + + + + + + +
+
+); + +export default App; diff --git a/imports/ui/pages/Documents.js b/imports/client/ui/pages/Documents.js similarity index 81% rename from imports/ui/pages/Documents.js rename to imports/client/ui/pages/Documents.js index fbfc0001..43f496aa 100644 --- a/imports/ui/pages/Documents.js +++ b/imports/client/ui/pages/Documents.js @@ -1,12 +1,12 @@ import React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { Row, Col, Button } from 'react-bootstrap'; -import DocumentsList from '../containers/DocumentsList.js'; +import DocumentsList from '../components/DocumentsList'; const Documents = () => (
- +

Documents

diff --git a/imports/client/ui/pages/EditDocument.js b/imports/client/ui/pages/EditDocument.js new file mode 100644 index 00000000..75a06c70 --- /dev/null +++ b/imports/client/ui/pages/EditDocument.js @@ -0,0 +1,31 @@ +import { Meteor } from 'meteor/meteor'; +import React from 'react'; +import { withRouter } from 'react-router-dom'; +import { PropTypes } from 'prop-types'; +import Documents from '../../../api/documents/documents'; +import DocumentEditor from '../components/DocumentEditor'; +import NotFound from './NotFound'; +import container from '../../../modules/container'; +import Loading from '../components/Loading'; + +const EditDocument = ({ doc }) => (doc ? ( +
+

Editing "{doc.title}"

+ +
+) : ); + +EditDocument.propTypes = { + doc: PropTypes.object.isRequired, +}; + +export default withRouter(container((props, onData) => { + const documentId = props.match.params._id; + const subscription = Meteor.subscribe('documents.view', documentId); + + if (subscription.ready()) { + const doc = Documents.findOne(documentId); + onData(null, { doc }); + } +}, EditDocument, { loadingHandler: () => })); + diff --git a/imports/ui/pages/Index.js b/imports/client/ui/pages/Index.js similarity index 96% rename from imports/ui/pages/Index.js rename to imports/client/ui/pages/Index.js index a5aa841e..86e1e1bd 100644 --- a/imports/ui/pages/Index.js +++ b/imports/client/ui/pages/Index.js @@ -7,7 +7,7 @@ const Index = () => (

Base

A starting point for Meteor applications.

Read the Documentation

-

Currently at v4.14.0

+

Currently at v4.15.0

); diff --git a/imports/ui/pages/Login.js b/imports/client/ui/pages/Login.js similarity index 79% rename from imports/ui/pages/Login.js rename to imports/client/ui/pages/Login.js index dfaaefe1..14442e04 100644 --- a/imports/ui/pages/Login.js +++ b/imports/client/ui/pages/Login.js @@ -1,9 +1,9 @@ -import React from 'react'; -import { Link } from 'react-router'; +import React, { Component } from 'react'; +import { Link, withRouter } from 'react-router-dom'; import { Row, Col, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; -import handleLogin from '../../modules/login'; +import handleLogin from '../../../modules/login'; -export default class Login extends React.Component { +class Login extends Component { componentDidMount() { handleLogin({ component: this }); } @@ -16,12 +16,12 @@ export default class Login extends React.Component { return (
- +

Login

(this.loginForm = form) } + ref={form => (this.loginForm = form)} className="login" - onSubmit={ this.handleSubmit } + onSubmit={this.handleSubmit} > Email Address @@ -52,3 +52,5 @@ export default class Login extends React.Component { ); } } + +export default withRouter(Login); diff --git a/imports/ui/pages/NewDocument.js b/imports/client/ui/pages/NewDocument.js similarity index 100% rename from imports/ui/pages/NewDocument.js rename to imports/client/ui/pages/NewDocument.js diff --git a/imports/ui/pages/NotFound.js b/imports/client/ui/pages/NotFound.js similarity index 100% rename from imports/ui/pages/NotFound.js rename to imports/client/ui/pages/NotFound.js diff --git a/imports/ui/pages/RecoverPassword.js b/imports/client/ui/pages/RecoverPassword.js similarity index 74% rename from imports/ui/pages/RecoverPassword.js rename to imports/client/ui/pages/RecoverPassword.js index 8562a568..cebccc4f 100644 --- a/imports/ui/pages/RecoverPassword.js +++ b/imports/client/ui/pages/RecoverPassword.js @@ -1,8 +1,8 @@ -import React from 'react'; +import React, { Component } from 'react'; import { Row, Col, Alert, FormGroup, FormControl, Button } from 'react-bootstrap'; -import handleRecoverPassword from '../../modules/recover-password'; +import handleRecoverPassword from '../../../modules/recover-password'; -export default class RecoverPassword extends React.Component { +class RecoverPassword extends Component { componentDidMount() { handleRecoverPassword({ component: this }); } @@ -15,15 +15,15 @@ export default class RecoverPassword extends React.Component { return (
- +

Recover Password

Enter your email address below to receive a link to reset your password. (this.recoverPasswordForm = form) } + ref={form => (this.recoverPasswordForm = form)} className="recover-password" - onSubmit={ this.handleSubmit } + onSubmit={this.handleSubmit} > - +

Reset Password

- To reset your password, enter a new one below. You will be logged in - with your new password. + To reset your password, enter a new one below. You will be logged in with your new password. (this.resetPasswordForm = form) } + ref={form => (this.resetPasswordForm = form)} className="reset-password" - onSubmit={ this.handleSubmit } + onSubmit={this.handleSubmit} > New Password @@ -54,5 +57,7 @@ export default class ResetPassword extends React.Component { } ResetPassword.propTypes = { - params: React.PropTypes.object, + match: PropTypes.object.isRequired, }; + +export default withRouter(ResetPassword); diff --git a/imports/ui/pages/Signup.js b/imports/client/ui/pages/Signup.js similarity index 86% rename from imports/ui/pages/Signup.js rename to imports/client/ui/pages/Signup.js index 8895e3e6..bcee8376 100644 --- a/imports/ui/pages/Signup.js +++ b/imports/client/ui/pages/Signup.js @@ -1,9 +1,9 @@ -import React from 'react'; -import { Link } from 'react-router'; +import React, { Component } from 'react'; +import { Link, withRouter } from 'react-router-dom'; import { Row, Col, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; -import handleSignup from '../../modules/signup'; +import handleSignup from '../../../modules/signup'; -export default class Signup extends React.Component { +class Signup extends Component { componentDidMount() { handleSignup({ component: this }); } @@ -16,14 +16,14 @@ export default class Signup extends React.Component { return (
- +

Sign Up

(this.signupForm = form) } onSubmit={ this.handleSubmit } > - + First Name - + Last Name { + if (confirm('Are you sure? This is permanent!')) { + removeDocument.call({ _id }, (error) => { + if (error) { + Bert.alert(error.reason, 'danger'); + } else { + Bert.alert('Document deleted!', 'success'); + history.push('/documents'); + } + }); + } +}; + +const ViewDocument = ({ doc, history }) => ( + doc ? ( +
+
+

{ doc && doc.title }

+ + + + + + +
+ { doc && doc.body } +
+ ) : ( + + ) +); + +ViewDocument.defaultProps = { + doc: null, + history: null, +}; + +ViewDocument.propTypes = { + doc: React.PropTypes.object, + history: PropTypes.object, +}; + +export default withRouter(container((props, onData) => { + const documentId = props.match.params._id; + const subscription = Meteor.subscribe('documents.view', documentId); + + if (subscription.ready()) { + const doc = Documents.findOne(documentId); + onData(null, { doc }); + } +}, ViewDocument, { loadingHandler: () => })); + diff --git a/imports/modules/container.js b/imports/modules/container.js new file mode 100644 index 00000000..e3f9702d --- /dev/null +++ b/imports/modules/container.js @@ -0,0 +1,6 @@ +import { compose } from 'react-komposer'; +import getTrackerLoader from './get-tracker-loader'; + +export default function container(composer, Component, options = {}) { + return compose(getTrackerLoader(composer), options)(Component); +} diff --git a/imports/modules/document-editor.js b/imports/modules/document-editor.js index c51c3bf8..d5c09bd2 100644 --- a/imports/modules/document-editor.js +++ b/imports/modules/document-editor.js @@ -1,6 +1,5 @@ /* eslint-disable no-undef */ -import { browserHistory } from 'react-router'; import { Bert } from 'meteor/themeteorchef:bert'; import { upsertDocument } from '../api/documents/methods.js'; import './validation.js'; @@ -8,7 +7,7 @@ import './validation.js'; let component; const handleUpsert = () => { - const { doc } = component.props; + const { doc, history } = component.props; const confirmation = doc && doc._id ? 'Document updated!' : 'Document added!'; const upsert = { title: document.querySelector('[name="title"]').value.trim(), @@ -23,7 +22,7 @@ const handleUpsert = () => { } else { component.documentEditorForm.reset(); Bert.alert(confirmation, 'success'); - browserHistory.push(`/documents/${response.insertedId || doc._id}`); + history.push(`/documents/${response.insertedId || doc._id}`); } }); }; @@ -40,10 +39,10 @@ const validate = () => { }, messages: { title: { - required: 'Need a title in here, Seuss.', + required: 'Need a title in here, thanks!.', }, body: { - required: 'This thneeds a body, please.', + required: 'This needs a body, please.', }, }, submitHandler() { handleUpsert(); }, diff --git a/imports/modules/get-tracker-loader.js b/imports/modules/get-tracker-loader.js new file mode 100644 index 00000000..8f3b0864 --- /dev/null +++ b/imports/modules/get-tracker-loader.js @@ -0,0 +1,21 @@ +import { Tracker } from 'meteor/tracker'; + +const getTrackerLoader = reactiveMapper => ( + (props, onData, env) => { + let trackerCleanup = null; + + const handler = Tracker.nonreactive(() => + Tracker.autorun(() => { + trackerCleanup = reactiveMapper(props, onData, env); + }), + ); + + return () => { + if (typeof trackerCleanup === 'function') trackerCleanup(); + return handler.stop(); + }; + } +); + +export default getTrackerLoader; + diff --git a/imports/modules/login.js b/imports/modules/login.js index 6447a77e..f6a94121 100644 --- a/imports/modules/login.js +++ b/imports/modules/login.js @@ -1,6 +1,5 @@ /* eslint-disable no-undef */ -import { browserHistory } from 'react-router'; import { Meteor } from 'meteor/meteor'; import { Bert } from 'meteor/themeteorchef:bert'; import './validation.js'; @@ -17,11 +16,11 @@ const login = () => { } else { Bert.alert('Logged in!', 'success'); - const { location } = component.props; + const { location, history } = component.props; if (location.state && location.state.nextPathname) { - browserHistory.push(location.state.nextPathname); + history.push(location.state.nextPathname); } else { - browserHistory.push('/'); + history.push('/'); } } }); diff --git a/imports/modules/private-route.js b/imports/modules/private-route.js new file mode 100644 index 00000000..53e2011a --- /dev/null +++ b/imports/modules/private-route.js @@ -0,0 +1,36 @@ +import { Meteor } from 'meteor/meteor'; +import React from 'react'; +import { Route, Redirect } from 'react-router-dom'; +import { PropTypes } from 'prop-types'; + +const PrivateRoute = ({ component: Component, ...rest }) => ( + ( + (!Meteor.loggingIn() && !Meteor.userId()) ? ( + + ) : ( + + ) + )} + /> +); + +PrivateRoute.defaultProps = { + location: null, +}; + +PrivateRoute.propTypes = { + component: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.func, + ]).isRequired, + location: PropTypes.object, +}; + +export default PrivateRoute; diff --git a/imports/modules/reset-password.js b/imports/modules/reset-password.js index 11b1aaae..d010804a 100644 --- a/imports/modules/reset-password.js +++ b/imports/modules/reset-password.js @@ -1,20 +1,20 @@ /* eslint-disable no-undef */ -import { browserHistory } from 'react-router'; import { Accounts } from 'meteor/accounts-base'; import { Bert } from 'meteor/themeteorchef:bert'; import './validation.js'; let component; -let token; const handleReset = () => { + const { history, match } = component.props; const password = document.querySelector('[name="newPassword"]').value; - Accounts.resetPassword(token, password, (error) => { + + Accounts.resetPassword(match.params.token, password, (error) => { if (error) { Bert.alert(error.reason, 'danger'); } else { - browserHistory.push('/'); + history.push('/'); Bert.alert('Password reset!', 'success'); } }); @@ -49,6 +49,5 @@ const validate = () => { export default function handleResetPassword(options) { component = options.component; - token = options.token; validate(); } diff --git a/imports/modules/signup.js b/imports/modules/signup.js index ea24dca4..e7631e03 100644 --- a/imports/modules/signup.js +++ b/imports/modules/signup.js @@ -1,6 +1,5 @@ /* eslint-disable no-undef */ -import { browserHistory } from 'react-router'; import { Accounts } from 'meteor/accounts-base'; import { Bert } from 'meteor/themeteorchef:bert'; import './validation.js'; @@ -25,7 +24,8 @@ const signup = () => { if (error) { Bert.alert(error.reason, 'danger'); } else { - browserHistory.push('/'); + const { history } = component.props; + history.push('/'); Bert.alert('Welcome!', 'success'); } }); diff --git a/imports/startup/client/index.js b/imports/startup/client/index.js index dde07191..f7ccb052 100644 --- a/imports/startup/client/index.js +++ b/imports/startup/client/index.js @@ -1,5 +1,15 @@ +import { Meteor } from 'meteor/meteor'; +import React from 'react'; +import { render } from 'react-dom'; import { Bert } from 'meteor/themeteorchef:bert'; import 'bootstrap/dist/css/bootstrap.min.css'; -import './routes.js'; +import App from '../../client/ui/layouts/App.js'; Bert.defaults.style = 'growl-top-right'; + +Meteor.startup(() => { + render( + , + document.getElementById('react-root'), + ); +}); diff --git a/imports/startup/client/routes.js b/imports/startup/client/routes.js deleted file mode 100644 index b7c71a26..00000000 --- a/imports/startup/client/routes.js +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint-disable max-len */ - -import React from 'react'; -import { render } from 'react-dom'; -import { Router, Route, IndexRoute, browserHistory } from 'react-router'; -import { Meteor } from 'meteor/meteor'; -import App from '../../ui/layouts/App.js'; -import Documents from '../../ui/pages/Documents.js'; -import NewDocument from '../../ui/pages/NewDocument.js'; -import EditDocument from '../../ui/containers/EditDocument.js'; -import ViewDocument from '../../ui/containers/ViewDocument.js'; -import Index from '../../ui/pages/Index.js'; -import Login from '../../ui/pages/Login.js'; -import NotFound from '../../ui/pages/NotFound.js'; -import RecoverPassword from '../../ui/pages/RecoverPassword.js'; -import ResetPassword from '../../ui/pages/ResetPassword.js'; -import Signup from '../../ui/pages/Signup.js'; - -const authenticate = (nextState, replace) => { - if (!Meteor.loggingIn() && !Meteor.userId()) { - replace({ - pathname: '/login', - state: { nextPathname: nextState.location.pathname }, - }); - } -}; - -Meteor.startup(() => { - render( - - - - - - - - - - - - - - , - document.getElementById('react-root') - ); -}); diff --git a/imports/startup/server/accounts/on-create-user.js b/imports/startup/server/accounts/on-create-user.js new file mode 100644 index 00000000..8c5326b1 --- /dev/null +++ b/imports/startup/server/accounts/on-create-user.js @@ -0,0 +1,16 @@ +import { Accounts } from 'meteor/accounts-base'; + +Accounts.onCreateUser((options, user) => { + const profile = options.profile; + const newUser = user; + + if (profile) { + newUser.name = { first: profile.name.first, last: profile.name.last }; + } + + if (options.email !== 'admin@admin.com') { + newUser.roles = ['registered']; + } + + return newUser; +}); diff --git a/imports/startup/server/api.js b/imports/startup/server/api.js index d284bbba..212f1dba 100644 --- a/imports/startup/server/api.js +++ b/imports/startup/server/api.js @@ -1,2 +1,3 @@ import '../../api/documents/methods.js'; import '../../api/documents/server/publications.js'; +import '../../api/users/server/publications.js'; diff --git a/imports/startup/server/index.js b/imports/startup/server/index.js index 800e1e63..b6217ebd 100644 --- a/imports/startup/server/index.js +++ b/imports/startup/server/index.js @@ -1,4 +1,5 @@ import './accounts/email-templates'; +import './accounts/on-create-user'; import './browser-policy'; import './fixtures'; import './api'; diff --git a/imports/ui/components/AuthenticatedNavigation.js b/imports/ui/components/AuthenticatedNavigation.js deleted file mode 100644 index 87a0c38a..00000000 --- a/imports/ui/components/AuthenticatedNavigation.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { browserHistory } from 'react-router'; -import { LinkContainer } from 'react-router-bootstrap'; -import { Nav, NavItem, NavDropdown, MenuItem } from 'react-bootstrap'; -import { Meteor } from 'meteor/meteor'; - -const handleLogout = () => Meteor.logout(() => browserHistory.push('/login')); - -const userName = () => { - const user = Meteor.user(); - const name = user && user.profile ? user.profile.name : ''; - return user ? `${name.first} ${name.last}` : ''; -}; - -const AuthenticatedNavigation = () => ( -
- - -
-); - -export default AuthenticatedNavigation; diff --git a/imports/ui/components/DocumentEditor.js b/imports/ui/components/DocumentEditor.js deleted file mode 100644 index 94f0ac6b..00000000 --- a/imports/ui/components/DocumentEditor.js +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint-disable max-len, no-return-assign */ - -import React from 'react'; -import { FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; -import documentEditor from '../../modules/document-editor.js'; - -export default class DocumentEditor extends React.Component { - componentDidMount() { - documentEditor({ component: this }); - setTimeout(() => { document.querySelector('[name="title"]').focus(); }, 0); - } - - render() { - const { doc } = this.props; - return ( (this.documentEditorForm = form) } - onSubmit={ event => event.preventDefault() } - > - - Title - - - - Body - - - - ); - } -} - -DocumentEditor.propTypes = { - doc: React.PropTypes.object, -}; diff --git a/imports/ui/components/DocumentsList.js b/imports/ui/components/DocumentsList.js deleted file mode 100644 index 2c36e26b..00000000 --- a/imports/ui/components/DocumentsList.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { browserHistory } from 'react-router'; -import { ListGroup, ListGroupItem, Alert } from 'react-bootstrap'; - -const handleNav = (_id) => { - browserHistory.push(`/documents/${_id}`); -} - -const DocumentsList = ({ documents }) => ( - documents.length > 0 ? - {documents.map(({ _id, title }) => ( - handleNav(_id) }> - { title } - - ))} - : - No documents yet. -); - -DocumentsList.propTypes = { - documents: React.PropTypes.array, -}; - -export default DocumentsList; diff --git a/imports/ui/containers/AppNavigation.js b/imports/ui/containers/AppNavigation.js deleted file mode 100644 index 65f743d9..00000000 --- a/imports/ui/containers/AppNavigation.js +++ /dev/null @@ -1,7 +0,0 @@ -import { composeWithTracker } from 'react-komposer'; -import { Meteor } from 'meteor/meteor'; -import AppNavigation from '../components/AppNavigation.js'; - -const composer = (props, onData) => onData(null, { hasUser: Meteor.user() }); - -export default composeWithTracker(composer, {}, {}, { pure: false })(AppNavigation); diff --git a/imports/ui/containers/DocumentsList.js b/imports/ui/containers/DocumentsList.js deleted file mode 100644 index 248d63ea..00000000 --- a/imports/ui/containers/DocumentsList.js +++ /dev/null @@ -1,15 +0,0 @@ -import { composeWithTracker } from 'react-komposer'; -import { Meteor } from 'meteor/meteor'; -import Documents from '../../api/documents/documents.js'; -import DocumentsList from '../components/DocumentsList.js'; -import Loading from '../components/Loading.js'; - -const composer = (params, onData) => { - const subscription = Meteor.subscribe('documents.list'); - if (subscription.ready()) { - const documents = Documents.find().fetch(); - onData(null, { documents }); - } -}; - -export default composeWithTracker(composer, Loading)(DocumentsList); diff --git a/imports/ui/containers/EditDocument.js b/imports/ui/containers/EditDocument.js deleted file mode 100644 index e37ed704..00000000 --- a/imports/ui/containers/EditDocument.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { composeWithTracker } from 'react-komposer'; -import Documents from '../../api/documents/documents.js'; -import EditDocument from '../pages/EditDocument.js'; -import Loading from '../components/Loading.js'; - -const composer = ({ params }, onData) => { - const subscription = Meteor.subscribe('documents.view', params._id); - - if (subscription.ready()) { - const doc = Documents.findOne(params._id); - onData(null, { doc }); - } -}; - -export default composeWithTracker(composer, Loading)(EditDocument); diff --git a/imports/ui/containers/ViewDocument.js b/imports/ui/containers/ViewDocument.js deleted file mode 100644 index 3af4aeab..00000000 --- a/imports/ui/containers/ViewDocument.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { composeWithTracker } from 'react-komposer'; -import Documents from '../../api/documents/documents.js'; -import ViewDocument from '../pages/ViewDocument.js'; -import Loading from '../components/Loading.js'; - -const composer = ({ params }, onData) => { - const subscription = Meteor.subscribe('documents.view', params._id); - - if (subscription.ready()) { - const doc = Documents.findOne(params._id); - onData(null, { doc }); - } -}; - -export default composeWithTracker(composer, Loading)(ViewDocument); diff --git a/imports/ui/layouts/App.js b/imports/ui/layouts/App.js deleted file mode 100644 index 1a8c89c1..00000000 --- a/imports/ui/layouts/App.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { Grid } from 'react-bootstrap'; -import AppNavigation from '../containers/AppNavigation.js'; - -const App = ({ children }) => ( -
- - - { children } - -
-); - -App.propTypes = { - children: React.PropTypes.node, -}; - -export default App; diff --git a/imports/ui/pages/EditDocument.js b/imports/ui/pages/EditDocument.js deleted file mode 100644 index c736d6bf..00000000 --- a/imports/ui/pages/EditDocument.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import DocumentEditor from '../components/DocumentEditor'; -import NotFound from './NotFound'; - -const EditDocument = ({ doc }) => { - return doc ? ( -
-

Editing "{ doc.title }"

- -
- ) : ; -}; - -EditDocument.propTypes = { - doc: React.PropTypes.object, -}; - -export default EditDocument; diff --git a/imports/ui/pages/ViewDocument.js b/imports/ui/pages/ViewDocument.js deleted file mode 100644 index 21d29c6a..00000000 --- a/imports/ui/pages/ViewDocument.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import { ButtonToolbar, ButtonGroup, Button } from 'react-bootstrap'; -import { browserHistory } from 'react-router'; -import { Bert } from 'meteor/themeteorchef:bert'; -import { removeDocument } from '../../api/documents/methods'; -import NotFound from './NotFound'; - -const handleEdit = (_id) => { - browserHistory.push(`/documents/${_id}/edit`); -}; - -const handleRemove = (_id) => { - if (confirm('Are you sure? This is permanent!')) { - removeDocument.call({ _id }, (error) => { - if (error) { - Bert.alert(error.reason, 'danger'); - } else { - Bert.alert('Document deleted!', 'success'); - browserHistory.push('/documents'); - } - }); - } -}; - -const ViewDocument = ({ doc }) => { - return doc ? ( -
-
-

{ doc && doc.title }

- - - - - - -
- { doc && doc.body } -
- ) : ; -}; - -ViewDocument.propTypes = { - doc: React.PropTypes.object, -}; - -export default ViewDocument; diff --git a/package.json b/package.json index 140497a8..74973a48 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "application-name", "version": "1.0.0", "description": "Application description.", + "license": "MIT", "scripts": { "start": "meteor --settings settings-development.json", "test": "meteor test --driver-package practicalmeteor:mocha --port 5000", @@ -10,69 +11,32 @@ "staging": "meteor deploy staging.meteor.com --settings settings-development.json", "production": "meteor deploy production.meteor.com --settings settings-production.json" }, + "dependencies": { + "babel-runtime": "^6.23.0", + "bcrypt": "^1.0.2", + "bootstrap": "^3.3.7", + "jquery": "^2.2.4", + "jquery-validation": "^1.15.1", + "meteor-node-stubs": "^0.2.6", + "prop-types": "^15.5.8", + "react": "^15.5.4", + "react-addons-pure-render-mixin": "^15.5.2", + "react-bootstrap": "^0.30.9", + "react-dom": "^15.5.4", + "react-komposer": "^2.0.0", + "react-router-bootstrap": "^0.24.2", + "react-router-dom": "^4.1.1", + "simpl-schema": "^0.2.3" + }, "devDependencies": { + "babel-eslint": "^7.2.2", "chimp": "^0.41.2", "eslint": "^3.8.1", "eslint-config-airbnb": "^12.0.0", "eslint-plugin-import": "^1.16.0", "eslint-plugin-jsx-a11y": "^2.2.3", "eslint-plugin-meteor": "^4.0.1", - "eslint-plugin-react": "^6.4.1" - }, - "eslintConfig": { - "parserOptions": { - "ecmaFeatures": { - "jsx": true - } - }, - "plugins": [ - "meteor", - "react" - ], - "extends": [ - "airbnb/base", - "plugin:meteor/guide", - "plugin:react/recommended" - ], - "env": { - "browser": true - }, - "globals": { - "server": false, - "browser": false, - "expect": false - }, - "rules": { - "import/no-unresolved": 0, - "import/no-extraneous-dependencies": 0, - "import/extensions": 0, - "no-underscore-dangle": [ - "error", - { - "allow": [ - "_id", - "_ensureIndex", - "_verifyEmailToken", - "_resetPasswordToken", - "_name" - ] - } - ], - "class-methods-use-this": 0 - } - }, - "dependencies": { - "babel-runtime": "^6.18.0", - "bcrypt": "^0.8.7", - "bootstrap": "^3.3.7", - "jquery": "^2.2.4", - "jquery-validation": "^1.15.1", - "react": "^15.3.2", - "react-addons-pure-render-mixin": "^15.3.2", - "react-bootstrap": "^0.30.5", - "react-dom": "^15.3.2", - "react-komposer": "^1.13.1", - "react-router": "^3.0.0", - "react-router-bootstrap": "^0.23.1" + "eslint-plugin-react": "^6.4.1", + "eslint-import-resolver-meteor": "^0.4.0" } }