├── .gitignore ├── code ├── .meteor │ ├── cordova-plugins │ ├── release │ ├── .gitignore │ ├── platforms │ ├── .id │ ├── .finished-upgraders │ ├── packages │ └── versions ├── client │ ├── main.js │ ├── stylesheets │ │ ├── module │ │ │ ├── _Signup.scss │ │ │ ├── _Login.scss │ │ │ ├── _PageHeader.scss │ │ │ └── _Loading.scss │ │ ├── base │ │ │ ├── _forms.scss │ │ │ ├── _extends.scss │ │ │ └── _animations.scss │ │ ├── state │ │ │ └── _Navbar.scss │ │ └── application.scss │ └── main.html ├── server │ └── main.js ├── settings-development.json ├── .gitignore ├── public │ ├── favicon.png │ └── apple-touch-icon-precomposed.png ├── imports │ ├── startup │ │ ├── server │ │ │ ├── api.js │ │ │ ├── index.js │ │ │ ├── browser-policy.js │ │ │ ├── fixtures.js │ │ │ └── accounts │ │ │ │ └── email-templates.js │ │ └── client │ │ │ └── index.js │ ├── modules │ │ ├── validation.js │ │ ├── rate-limit.js │ │ ├── recover-password.js │ │ ├── reset-password.js │ │ ├── login.js │ │ ├── document-editor.js │ │ └── signup.js │ ├── api │ │ └── documents │ │ │ ├── server │ │ │ └── publications.js │ │ │ ├── documents.tests.js │ │ │ ├── documents.js │ │ │ ├── methods.js │ │ │ └── methods.tests.js │ └── ui │ │ ├── pages │ │ ├── NotFound.js │ │ ├── NewDocument.js │ │ ├── EditDocument.js │ │ ├── Index.js │ │ ├── Public.js │ │ ├── Authenticated.js │ │ ├── Documents.js │ │ ├── RecoverPassword.js │ │ ├── ViewDocument.js │ │ ├── Login.js │ │ ├── ResetPassword.js │ │ └── Signup.js │ │ ├── components │ │ ├── PublicNavigation.js │ │ ├── DocumentsList.js │ │ ├── AppNavigation.js │ │ ├── AuthenticatedNavigation.js │ │ ├── Loading.js │ │ └── DocumentEditor.js │ │ ├── containers │ │ ├── DocumentsList.js │ │ ├── EditDocument.js │ │ └── ViewDocument.js │ │ └── layouts │ │ └── App.js ├── .editorconfig ├── README.md ├── tests │ ├── not-found.js │ ├── signup.js │ └── login.js └── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /code/.meteor/cordova-plugins: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.4.3.2 2 | -------------------------------------------------------------------------------- /code/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | dev_bundle 2 | local 3 | -------------------------------------------------------------------------------- /code/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /code/client/main.js: -------------------------------------------------------------------------------- 1 | import '/imports/startup/client'; 2 | -------------------------------------------------------------------------------- /code/server/main.js: -------------------------------------------------------------------------------- 1 | import '/imports/startup/server'; 2 | -------------------------------------------------------------------------------- /code/settings-development.json: -------------------------------------------------------------------------------- 1 | { 2 | "public": {}, 3 | "private": {} 4 | } 5 | -------------------------------------------------------------------------------- /code/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | settings-production.json 3 | npm-debug.log 4 | 5 | node_modules 6 | -------------------------------------------------------------------------------- /code/client/stylesheets/module/_Signup.scss: -------------------------------------------------------------------------------- 1 | .Signup form + p { 2 | margin-top: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /code/client/stylesheets/module/_Login.scss: -------------------------------------------------------------------------------- 1 | .Login label { 2 | display: block; 3 | @extend %clearfix; 4 | } 5 | -------------------------------------------------------------------------------- /code/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeteorchef/getting-started-with-react-router-v4/HEAD/code/public/favicon.png -------------------------------------------------------------------------------- /code/imports/startup/server/api.js: -------------------------------------------------------------------------------- 1 | import '../../api/documents/methods.js'; 2 | import '../../api/documents/server/publications.js'; 3 | -------------------------------------------------------------------------------- /code/imports/modules/validation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | import $ from 'jquery'; 4 | import 'jquery-validation'; 5 | -------------------------------------------------------------------------------- /code/imports/startup/server/index.js: -------------------------------------------------------------------------------- 1 | import './accounts/email-templates'; 2 | import './browser-policy'; 3 | import './fixtures'; 4 | import './api'; 5 | -------------------------------------------------------------------------------- /code/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themeteorchef/getting-started-with-react-router-v4/HEAD/code/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /code/client/stylesheets/base/_forms.scss: -------------------------------------------------------------------------------- 1 | form label.error { 2 | display: block; 3 | margin-top: 10px; 4 | margin-bottom: 0px; 5 | font-weight: normal; 6 | color: lighten( red, 20% ); 7 | } 8 | -------------------------------------------------------------------------------- /code/client/stylesheets/base/_extends.scss: -------------------------------------------------------------------------------- 1 | %clearfix { 2 | *zoom: 1; 3 | 4 | &:before, 5 | &:after { 6 | display: table; 7 | content: ""; 8 | } 9 | 10 | &:after { 11 | clear: both; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /code/imports/startup/server/browser-policy.js: -------------------------------------------------------------------------------- 1 | import { BrowserPolicy } from 'meteor/browser-policy-common'; 2 | // e.g., BrowserPolicy.content.allowOriginForAll( 's3.amazonaws.com' ); 3 | BrowserPolicy.content.allowFontOrigin("data:"); 4 | -------------------------------------------------------------------------------- /code/client/stylesheets/state/_Navbar.scss: -------------------------------------------------------------------------------- 1 | .navbar-default .navbar-nav > li > a.active, 2 | .navbar-default .navbar-nav > li > a.active:focus, 3 | .navbar-default .navbar-nav > li > a.active:hover { 4 | color: #555; 5 | background-color: #e7e7e7; 6 | } 7 | -------------------------------------------------------------------------------- /code/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /code/client/stylesheets/module/_PageHeader.scss: -------------------------------------------------------------------------------- 1 | .page-header { 2 | margin-top: 0px; 3 | 4 | h4 { 5 | margin-top: 7px; 6 | } 7 | } 8 | 9 | @media screen and (min-width: 768px) { 10 | .page-header { 11 | margin-top: 20px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /code/client/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | @import "base/extends"; 2 | @import "base/animations"; 3 | @import "base/forms"; 4 | 5 | @import "module/Loading"; 6 | @import "module/Login"; 7 | @import "module/PageHeader"; 8 | @import "module/Signup"; 9 | 10 | @import "state/Navbar"; 11 | -------------------------------------------------------------------------------- /code/client/stylesheets/base/_animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes rotate { 2 | from { transform: rotate( 0deg ); } 3 | to { transform: rotate( 360deg ); } 4 | } 5 | 6 | @-webkit-keyframes rotate { 7 | from { -webkit-transform: rotate( 0deg ); } 8 | to { -webkit-transform: rotate( 360deg ); } 9 | } 10 | -------------------------------------------------------------------------------- /code/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1ipv1tp12xzfzhba088c 8 | -------------------------------------------------------------------------------- /code/imports/api/documents/server/publications.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { check } from 'meteor/check'; 3 | import Documents from '../documents'; 4 | 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 | -------------------------------------------------------------------------------- /code/imports/ui/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert } from 'react-bootstrap'; 3 | 4 | const NotFound = () => ( 5 |
6 | 7 |

Error [404]: { window.location.pathname } does not exist.

8 |
9 |
10 | ); 11 | 12 | export default NotFound; 13 | -------------------------------------------------------------------------------- /code/README.md: -------------------------------------------------------------------------------- 1 | # The Meteor Chef - Base 2 | A starting point for Meteor apps. 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
Base Versionv4.14.0
Meteor Versionv1.4.3.2
16 | 17 | [Read the Documentation](http://themeteorchef.com/base) 18 | -------------------------------------------------------------------------------- /code/client/stylesheets/module/_Loading.scss: -------------------------------------------------------------------------------- 1 | .Loading { 2 | -webkit-animation-name: rotate; 3 | -webkit-animation-duration: 0.5s; 4 | -webkit-animation-iteration-count: infinite; 5 | -webkit-animation-timing-function: linear; 6 | animation-name: rotate; 7 | animation-duration: 0.5s; 8 | animation-iteration-count: infinite; 9 | animation-timing-function: linear; 10 | } 11 | -------------------------------------------------------------------------------- /code/imports/api/documents/documents.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* eslint-disable func-names, prefer-arrow-callback */ 3 | 4 | import { assert } from 'meteor/practicalmeteor:chai'; 5 | import Documents from './documents.js'; 6 | 7 | describe('Documents collection', function () { 8 | it('registers the collection with Mongo properly', function () { 9 | assert.equal(typeof Documents, 'object'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /code/imports/startup/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Meteor } from 'meteor/meteor'; 4 | import { Bert } from 'meteor/themeteorchef:bert'; 5 | import 'bootstrap/dist/css/bootstrap.min.css'; 6 | import App from '../../ui/layouts/App.js'; 7 | 8 | Bert.defaults.style = 'growl-top-right'; 9 | 10 | Meteor.startup(() => { 11 | render(, document.getElementById('react-root')); 12 | }); 13 | -------------------------------------------------------------------------------- /code/tests/not-found.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* eslint-disable func-names, prefer-arrow-callback */ 3 | 4 | describe('404 Error', function () { 5 | it('should render a 404 for a non-existent route @watch', function () { 6 | browser.url('http://localhost:3000/dididothat') 7 | .waitForExist('.alert-danger'); 8 | 9 | expect(browser.getText('.alert-danger p')).to.equal('Error [404]: /dididothat does not exist.'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /code/imports/ui/pages/NewDocument.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import DocumentEditor from '../components/DocumentEditor.js'; 3 | 4 | const NewDocument = ({ history }) => ( 5 |
6 |

New Document

7 | 8 |
9 | ); 10 | 11 | NewDocument.propTypes = { 12 | history: PropTypes.object, 13 | }; 14 | 15 | export default NewDocument; 16 | -------------------------------------------------------------------------------- /code/imports/ui/components/PublicNavigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import { Nav } from 'react-bootstrap'; 4 | 5 | const PublicNavigation = () => ( 6 | 14 | ); 15 | 16 | export default PublicNavigation; 17 | -------------------------------------------------------------------------------- /code/client/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Application Name 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /code/imports/ui/pages/EditDocument.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DocumentEditor from '../components/DocumentEditor'; 3 | import NotFound from './NotFound'; 4 | 5 | const EditDocument = (props) => { 6 | return props.doc ? ( 7 |
8 |

Editing "{ props.doc.title }"

9 | 10 |
11 | ) : ; 12 | }; 13 | 14 | EditDocument.propTypes = { 15 | doc: React.PropTypes.object, 16 | }; 17 | 18 | export default EditDocument; 19 | -------------------------------------------------------------------------------- /code/imports/ui/pages/Index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Jumbotron } from 'react-bootstrap'; 3 | 4 | const Index = () => ( 5 |
6 | 7 |

Base

8 |

A starting point for Meteor applications.

9 |

Read the Documentation

10 |

Currently at v4.14.0

11 |
12 |
13 | ); 14 | 15 | export default Index; 16 | -------------------------------------------------------------------------------- /code/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | 1.3.5-remove-old-dev-bundle-link 15 | 1.4.0-remove-old-dev-bundle-link 16 | 1.4.1-add-shell-server-package 17 | 1.4.3-split-account-service-packages 18 | -------------------------------------------------------------------------------- /code/imports/ui/containers/DocumentsList.js: -------------------------------------------------------------------------------- 1 | import { composeWithTracker } from 'react-komposer'; 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'; 6 | 7 | const composer = (params, onData) => { 8 | const subscription = Meteor.subscribe('documents.list'); 9 | if (subscription.ready()) { 10 | const documents = Documents.find().fetch(); 11 | onData(null, { documents }); 12 | } 13 | }; 14 | 15 | export default composeWithTracker(composer, Loading)(DocumentsList); 16 | -------------------------------------------------------------------------------- /code/imports/ui/pages/Public.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | 4 | const Public = ({ loggingIn, authenticated, component, ...rest }) => ( 5 | { 6 | if (loggingIn) return
; 7 | return !authenticated ? 8 | (React.createElement(component, { ...props, loggingIn, authenticated })) : 9 | (); 10 | }} /> 11 | ); 12 | 13 | Public.propTypes = { 14 | loggingIn: PropTypes.bool, 15 | authenticated: PropTypes.bool, 16 | component: PropTypes.func, 17 | }; 18 | 19 | export default Public; 20 | -------------------------------------------------------------------------------- /code/imports/ui/pages/Authenticated.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | 4 | const Authenticated = ({ loggingIn, authenticated, component, ...rest }) => ( 5 | { 6 | if (loggingIn) return
; 7 | return authenticated ? 8 | (React.createElement(component, { ...props, loggingIn, authenticated })) : 9 | (); 10 | }} /> 11 | ); 12 | 13 | Authenticated.propTypes = { 14 | loggingIn: PropTypes.bool, 15 | authenticated: PropTypes.bool, 16 | component: PropTypes.func, 17 | }; 18 | 19 | export default Authenticated; 20 | -------------------------------------------------------------------------------- /code/imports/modules/rate-limit.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; 3 | import { _ } from 'meteor/underscore'; 4 | 5 | const fetchMethodNames = methods => _.pluck(methods, 'name'); 6 | 7 | const assignLimits = ({ methods, limit, timeRange }) => { 8 | const methodNames = fetchMethodNames(methods); 9 | 10 | if (Meteor.isServer) { 11 | DDPRateLimiter.addRule({ 12 | name(name) { return _.contains(methodNames, name); }, 13 | connectionId() { return true; }, 14 | }, limit, timeRange); 15 | } 16 | }; 17 | 18 | export default function rateLimit(options) { return assignLimits(options); } 19 | -------------------------------------------------------------------------------- /code/imports/ui/containers/EditDocument.js: -------------------------------------------------------------------------------- 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 = ({ match }, onData) => { 8 | const documentId = match.params._id; 9 | const subscription = Meteor.subscribe('documents.view', documentId); 10 | 11 | if (subscription.ready()) { 12 | const doc = Documents.findOne(documentId); 13 | onData(null, { doc }); 14 | } 15 | }; 16 | 17 | export default composeWithTracker(composer, Loading)(EditDocument); 18 | -------------------------------------------------------------------------------- /code/imports/ui/containers/ViewDocument.js: -------------------------------------------------------------------------------- 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 = ({ match }, onData) => { 8 | const documentId = match.params._id; 9 | const subscription = Meteor.subscribe('documents.view', documentId); 10 | 11 | if (subscription.ready()) { 12 | const doc = Documents.findOne(documentId); 13 | onData(null, { doc }); 14 | } 15 | }; 16 | 17 | export default composeWithTracker(composer, Loading)(ViewDocument); 18 | -------------------------------------------------------------------------------- /code/imports/startup/server/fixtures.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { Roles } from 'meteor/alanning:roles'; 3 | import { Accounts } from 'meteor/accounts-base'; 4 | 5 | if (!Meteor.isProduction) { 6 | const users = [{ 7 | email: 'admin@admin.com', 8 | password: 'password', 9 | profile: { 10 | name: { first: 'Carl', last: 'Winslow' }, 11 | }, 12 | roles: ['admin'], 13 | }]; 14 | 15 | users.forEach(({ email, password, profile, roles }) => { 16 | const userExists = Meteor.users.findOne({ 'emails.address': email }); 17 | 18 | if (!userExists) { 19 | const userId = Accounts.createUser({ email, password, profile }); 20 | Roles.addUsersToRoles(userId, roles); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /code/imports/ui/components/DocumentsList.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { ListGroup, ListGroupItem, Alert } from 'react-bootstrap'; 3 | 4 | const handleNav = (history, _id) => { 5 | history.push(`/documents/${_id}`); 6 | }; 7 | 8 | const DocumentsList = ({ history, documents }) => ( 9 | documents.length > 0 ? 10 | {documents.map(({ _id, title }) => ( 11 | handleNav(history, _id) }> 12 | { title } 13 | 14 | ))} 15 | : 16 | No documents yet. 17 | ); 18 | 19 | DocumentsList.propTypes = { 20 | history: PropTypes.object, 21 | documents: PropTypes.array, 22 | }; 23 | 24 | export default DocumentsList; 25 | -------------------------------------------------------------------------------- /code/imports/ui/pages/Documents.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Row, Col, Button } from 'react-bootstrap'; 4 | import DocumentsList from '../containers/DocumentsList.js'; 5 | 6 | const Documents = ({ history }) => ( 7 |
8 | 9 | 10 |
11 |

Documents

12 | 13 | 17 | 18 |
19 | 20 | 21 |
22 |
23 | ); 24 | 25 | Documents.propTypes = { 26 | history: PropTypes.object, 27 | }; 28 | 29 | export default Documents; 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### Tutorial: Getting Started with React Router v4 2 | 3 | In this tutorial, we'll take a look at migrating from React Router v3 to v4 inside of The Meteor Chef's Base boilerplate. We'll learn how to define routes, handle authentication, and look at how things like navigation state and calling route methods have changed in the latest release. 4 | 5 | [Read this tutorial on The Meteor Chef](https://themeteorchef.com/tutorials/getting-started-with-react-router-v4) 6 | 7 | [Download the source (.zip)](https://github.com/themeteorchef/getting-started-with-react-router-v4/archive/master.zip) 8 | 9 | --- 10 | 11 | **Need help with this?** [Sign up for a Mentorship appointment](https://themeteorchef.com/mentorship?readme=getting-started-with-react-router-v4) and get 1-on-1 help. 12 | 13 | --- 14 | 15 | _The code for this tutorial is licensed under the [MIT License](http://opensource.org/licenses/MIT)_. 16 | -------------------------------------------------------------------------------- /code/imports/ui/components/AppNavigation.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Navbar } from 'react-bootstrap'; 3 | import { Link } from 'react-router-dom'; 4 | import PublicNavigation from './PublicNavigation.js'; 5 | import AuthenticatedNavigation from './AuthenticatedNavigation.js'; 6 | 7 | const renderNavigation = authenticated => 8 | (authenticated ? : ); 9 | 10 | const AppNavigation = ({ authenticated }) => ( 11 | 12 | 13 | 14 | Application Name 15 | 16 | 17 | 18 | 19 | { renderNavigation(authenticated) } 20 | 21 | 22 | ); 23 | 24 | AppNavigation.propTypes = { 25 | authenticated: PropTypes.bool, 26 | }; 27 | 28 | export default AppNavigation; 29 | -------------------------------------------------------------------------------- /code/imports/api/documents/documents.js: -------------------------------------------------------------------------------- 1 | import { Mongo } from 'meteor/mongo'; 2 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; 3 | import { Factory } from 'meteor/dburles:factory'; 4 | 5 | const Documents = new Mongo.Collection('Documents'); 6 | export default Documents; 7 | 8 | Documents.allow({ 9 | insert: () => false, 10 | update: () => false, 11 | remove: () => false, 12 | }); 13 | 14 | Documents.deny({ 15 | insert: () => true, 16 | update: () => true, 17 | remove: () => true, 18 | }); 19 | 20 | Documents.schema = new SimpleSchema({ 21 | title: { 22 | type: String, 23 | label: 'The title of the document.', 24 | }, 25 | body: { 26 | type: String, 27 | label: 'The body of the document.', 28 | }, 29 | }); 30 | 31 | Documents.attachSchema(Documents.schema); 32 | 33 | Factory.define('document', Documents, { 34 | title: () => 'Factory Title', 35 | body: () => 'Factory Body', 36 | }); 37 | -------------------------------------------------------------------------------- /code/imports/ui/components/AuthenticatedNavigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import { Nav, NavDropdown, MenuItem } from 'react-bootstrap'; 4 | import { Meteor } from 'meteor/meteor'; 5 | 6 | const handleLogout = () => Meteor.logout(); 7 | 8 | const userName = () => { 9 | const user = Meteor.user(); 10 | const name = user && user.profile ? user.profile.name : ''; 11 | return user ? `${name.first} ${name.last}` : ''; 12 | }; 13 | 14 | const AuthenticatedNavigation = () => ( 15 |
16 | 21 | 26 |
27 | ); 28 | 29 | export default AuthenticatedNavigation; 30 | -------------------------------------------------------------------------------- /code/imports/startup/server/accounts/email-templates.js: -------------------------------------------------------------------------------- 1 | import { Accounts } from 'meteor/accounts-base'; 2 | 3 | const name = 'Application Name'; 4 | const email = ''; 5 | const from = `${name} ${email}`; 6 | const emailTemplates = Accounts.emailTemplates; 7 | 8 | emailTemplates.siteName = name; 9 | emailTemplates.from = from; 10 | 11 | emailTemplates.resetPassword = { 12 | subject() { 13 | return `[${name}] Reset Your Password`; 14 | }, 15 | text(user, url) { 16 | const userEmail = user.emails[0].address; 17 | const urlWithoutHash = url.replace('#/', ''); 18 | 19 | return `A password reset has been requested for the account related to this 20 | address (${userEmail}). To reset the password, visit the following link: 21 | \n\n${urlWithoutHash}\n\n If you did not request this reset, please ignore 22 | this email. If you feel something is wrong, please contact our support team: 23 | ${email}.`; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /code/tests/signup.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* eslint-disable func-names, prefer-arrow-callback */ 3 | 4 | describe('Sign Up', function () { 5 | beforeEach(function () { 6 | server.execute(function () { 7 | const { Meteor } = require('meteor/meteor'); 8 | const user = Meteor.users.findOne({ 'emails.address': 'carl.winslow@abc.com' }); 9 | if (user) { 10 | Meteor.users.remove(user._id); 11 | } 12 | }); 13 | }); 14 | 15 | it('should create a new user and login with redirect to index @watch', function () { 16 | browser.url('http://localhost:3000/signup') 17 | .setValue('[name="firstName"]', 'Carl') 18 | .setValue('[name="lastName"]', 'Winslow') 19 | .setValue('[name="emailAddress"]', 'carl.winslow@abc.com') 20 | .setValue('[name="password"]', 'bigguy1989') 21 | .submitForm('form'); 22 | 23 | browser.waitForExist('.jumbotron'); 24 | expect(browser.getUrl()).to.equal('http://localhost:3000/'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /code/imports/ui/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loading = () => ( 4 | 14 | 22 | 26 | 27 | 28 | ); 29 | 30 | export default Loading; 31 | -------------------------------------------------------------------------------- /code/imports/api/documents/methods.js: -------------------------------------------------------------------------------- 1 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; 2 | import { ValidatedMethod } from 'meteor/mdg:validated-method'; 3 | import Documents from './documents'; 4 | import rateLimit from '../../modules/rate-limit.js'; 5 | 6 | export const upsertDocument = new ValidatedMethod({ 7 | name: 'documents.upsert', 8 | validate: new SimpleSchema({ 9 | _id: { type: String, optional: true }, 10 | title: { type: String, optional: true }, 11 | body: { type: String, optional: true }, 12 | }).validator(), 13 | run(document) { 14 | return Documents.upsert({ _id: document._id }, { $set: document }); 15 | }, 16 | }); 17 | 18 | export const removeDocument = new ValidatedMethod({ 19 | name: 'documents.remove', 20 | validate: new SimpleSchema({ 21 | _id: { type: String }, 22 | }).validator(), 23 | run({ _id }) { 24 | Documents.remove(_id); 25 | }, 26 | }); 27 | 28 | rateLimit({ 29 | methods: [ 30 | upsertDocument, 31 | removeDocument, 32 | ], 33 | limit: 5, 34 | timeRange: 1000, 35 | }); 36 | -------------------------------------------------------------------------------- /code/imports/modules/recover-password.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import { Accounts } from 'meteor/accounts-base'; 4 | import { Bert } from 'meteor/themeteorchef:bert'; 5 | import './validation.js'; 6 | 7 | let component; 8 | 9 | const handleRecovery = () => { 10 | Accounts.forgotPassword({ 11 | email: document.querySelector('[name="emailAddress"]').value, 12 | }, (error) => { 13 | if (error) { 14 | Bert.alert(error.reason, 'warning'); 15 | } else { 16 | Bert.alert('Check your inbox for a reset link!', 'success'); 17 | } 18 | }); 19 | }; 20 | 21 | const validate = () => { 22 | $(component.recoverPasswordForm).validate({ 23 | rules: { 24 | emailAddress: { 25 | required: true, 26 | email: true, 27 | }, 28 | }, 29 | messages: { 30 | emailAddress: { 31 | required: 'Need an email address here.', 32 | email: 'Is this email address legit?', 33 | }, 34 | }, 35 | submitHandler() { handleRecovery(); }, 36 | }); 37 | }; 38 | 39 | export default function handleRecoverPassword(options) { 40 | component = options.component; 41 | validate(); 42 | } 43 | -------------------------------------------------------------------------------- /code/tests/login.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* eslint-disable func-names, prefer-arrow-callback */ 3 | 4 | describe('Log In', function () { 5 | beforeEach(function () { 6 | server.execute(function () { 7 | const { Meteor } = require('meteor/meteor'); 8 | const user = Meteor.users.findOne({ 'emails.address': 'carl.winslow@abc.com' }); 9 | if (user) { 10 | Meteor.users.remove(user._id); 11 | } 12 | }); 13 | }); 14 | 15 | it('should allow us to login @watch', function () { 16 | server.execute(function () { 17 | const { Accounts } = require('meteor/accounts-base'); 18 | Accounts.createUser({ 19 | email: 'carl.winslow@abc.com', 20 | password: 'bigguy1989', 21 | profile: { 22 | name: { first: 'Carl', last: 'Winslow' }, 23 | }, 24 | }); 25 | }); 26 | 27 | browser.url('http://localhost:3000/login') 28 | .setValue('[name="emailAddress"]', 'carl.winslow@abc.com') 29 | .setValue('[name="password"]', 'bigguy1989') 30 | .submitForm('form'); 31 | 32 | browser.waitForExist('.jumbotron'); 33 | expect(browser.getUrl()).to.equal('http://localhost:3000/'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /code/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base@1.0.4 # Packages every Meteor app needs to have 8 | mobile-experience@1.0.4 # Packages for a great mobile UX 9 | mongo@1.1.16 # The database Meteor supports right now 10 | reactive-var@1.0.11 # Reactive variable for tracker 11 | session@1.1.7 12 | tracker@1.1.2 # Meteor's client-side reactive programming library 13 | 14 | standard-minifier-css@1.3.4 # CSS minifier run for production mode 15 | standard-minifier-js@1.2.3 # JS minifier run for production mode 16 | es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers. 17 | ecmascript@0.6.3 # Enable ECMAScript2015+ syntax in app code 18 | 19 | accounts-password@1.3.4 20 | accounts-base@1.2.15 21 | check@1.2.5 22 | audit-argument-checks@1.0.7 23 | browser-policy@1.1.0 24 | 25 | fourseven:scss@4.5.0 26 | aldeed:collection2 27 | alanning:roles 28 | themeteorchef:bert 29 | static-html@1.1.11 30 | xolvio:cleaner 31 | practicalmeteor:mocha 32 | xolvio:backdoor 33 | mdg:validated-method 34 | dburles:factory@1.0.0 35 | ddp-rate-limiter@1.0.7 36 | shell-server@0.2.3 37 | -------------------------------------------------------------------------------- /code/imports/ui/pages/RecoverPassword.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Col, Alert, FormGroup, FormControl, Button } from 'react-bootstrap'; 3 | import handleRecoverPassword from '../../modules/recover-password'; 4 | 5 | export default class RecoverPassword extends React.Component { 6 | componentDidMount() { 7 | handleRecoverPassword({ component: this }); 8 | } 9 | 10 | handleSubmit(event) { 11 | event.preventDefault(); 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 | 18 | 19 |

Recover Password

20 | 21 | Enter your email address below to receive a link to reset your password. 22 | 23 |
(this.recoverPasswordForm = form) } 25 | className="recover-password" 26 | onSubmit={ this.handleSubmit } 27 | > 28 | 29 | 35 | 36 | 37 |
38 | 39 |
40 |
41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /code/imports/modules/reset-password.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import { Accounts } from 'meteor/accounts-base'; 4 | import { Bert } from 'meteor/themeteorchef:bert'; 5 | import './validation.js'; 6 | 7 | let component; 8 | let token; 9 | 10 | const handleReset = () => { 11 | const password = document.querySelector('[name="newPassword"]').value; 12 | Accounts.resetPassword(token, password, (error) => { 13 | if (error) { 14 | Bert.alert(error.reason, 'danger'); 15 | } else { 16 | component.props.history.push('/'); 17 | Bert.alert('Password reset!', 'success'); 18 | } 19 | }); 20 | }; 21 | 22 | const validate = () => { 23 | $(component.resetPasswordForm).validate({ 24 | rules: { 25 | newPassword: { 26 | required: true, 27 | minlength: 6, 28 | }, 29 | repeatNewPassword: { 30 | required: true, 31 | minlength: 6, 32 | equalTo: '[name="newPassword"]', 33 | }, 34 | }, 35 | messages: { 36 | newPassword: { 37 | required: 'Enter a new password, please.', 38 | minlength: 'Use at least six characters, please.', 39 | }, 40 | repeatNewPassword: { 41 | required: 'Repeat your new password, please.', 42 | equalTo: 'Hmm, your passwords don\'t match. Try again?', 43 | }, 44 | }, 45 | submitHandler() { handleReset(); }, 46 | }); 47 | }; 48 | 49 | export default function handleResetPassword(options) { 50 | component = options.component; 51 | token = options.token; 52 | validate(); 53 | } 54 | -------------------------------------------------------------------------------- /code/imports/modules/login.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import { Meteor } from 'meteor/meteor'; 4 | import { Bert } from 'meteor/themeteorchef:bert'; 5 | import './validation.js'; 6 | 7 | let component; 8 | 9 | const login = () => { 10 | const email = document.querySelector('[name="emailAddress"]').value; 11 | const password = document.querySelector('[name="password"]').value; 12 | 13 | Meteor.loginWithPassword(email, password, (error) => { 14 | if (error) { 15 | Bert.alert(error.reason, 'warning'); 16 | } else { 17 | Bert.alert('Logged in!', 'success'); 18 | 19 | const { location } = component.props; 20 | if (location.state && location.state.nextPathname) { 21 | browserHistory.push(location.state.nextPathname); 22 | } else { 23 | // browserHistory.push('/'); 24 | } 25 | } 26 | }); 27 | }; 28 | 29 | const validate = () => { 30 | $(component.loginForm).validate({ 31 | rules: { 32 | emailAddress: { 33 | required: true, 34 | email: true, 35 | }, 36 | password: { 37 | required: true, 38 | }, 39 | }, 40 | messages: { 41 | emailAddress: { 42 | required: 'Need an email address here.', 43 | email: 'Is this email address legit?', 44 | }, 45 | password: { 46 | required: 'Need a password here.', 47 | }, 48 | }, 49 | submitHandler() { login(); }, 50 | }); 51 | }; 52 | 53 | export default function handleLogin(options) { 54 | component = options.component; 55 | validate(); 56 | } 57 | -------------------------------------------------------------------------------- /code/imports/modules/document-editor.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import { Bert } from 'meteor/themeteorchef:bert'; 4 | import { upsertDocument } from '../api/documents/methods.js'; 5 | import './validation.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, response) => { 20 | if (error) { 21 | Bert.alert(error.reason, 'danger'); 22 | } else { 23 | component.documentEditorForm.reset(); 24 | Bert.alert(confirmation, 'success'); 25 | component.props.history.push(`/documents/${response.insertedId || doc._id}`); 26 | } 27 | }); 28 | }; 29 | 30 | const validate = () => { 31 | $(component.documentEditorForm).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 documentEditor(options) { 53 | component = options.component; 54 | validate(); 55 | } 56 | -------------------------------------------------------------------------------- /code/imports/ui/components/DocumentEditor.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len, no-return-assign */ 2 | 3 | import React, { PropTypes } 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 | setTimeout(() => { document.querySelector('[name="title"]').focus(); }, 0); 11 | } 12 | 13 | render() { 14 | const { doc } = this.props; 15 | return (
(this.documentEditorForm = form) } 17 | onSubmit={ event => event.preventDefault() } 18 | > 19 | 20 | Title 21 | 27 | 28 | 29 | Body 30 | 36 | 37 | 40 |
); 41 | } 42 | } 43 | 44 | DocumentEditor.propTypes = { 45 | doc: PropTypes.object, 46 | }; 47 | -------------------------------------------------------------------------------- /code/imports/ui/pages/ViewDocument.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { ButtonToolbar, ButtonGroup, Button } from 'react-bootstrap'; 3 | import { Bert } from 'meteor/themeteorchef:bert'; 4 | import { removeDocument } from '../../api/documents/methods'; 5 | import NotFound from './NotFound'; 6 | 7 | const handleEdit = (history, _id) => { 8 | history.push(`/documents/${_id}/edit`); 9 | }; 10 | 11 | const handleRemove = (history, _id) => { 12 | if (confirm('Are you sure? This is permanent!')) { 13 | removeDocument.call({ _id }, (error) => { 14 | if (error) { 15 | Bert.alert(error.reason, 'danger'); 16 | } else { 17 | Bert.alert('Document deleted!', 'success'); 18 | history.push('/documents'); 19 | } 20 | }); 21 | } 22 | }; 23 | 24 | const ViewDocument = ({ doc, history }) => { 25 | return doc ? ( 26 |
27 |
28 |

{ doc && doc.title }

29 | 30 | 31 | 34 | 38 | 39 | 40 |
41 | { doc && doc.body } 42 |
43 | ) : ; 44 | }; 45 | 46 | ViewDocument.propTypes = { 47 | doc: PropTypes.object, 48 | history: PropTypes.object, 49 | }; 50 | 51 | export default ViewDocument; 52 | -------------------------------------------------------------------------------- /code/imports/api/documents/methods.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* eslint-disable func-names, prefer-arrow-callback */ 3 | 4 | import { Meteor } from 'meteor/meteor'; 5 | import { assert } from 'meteor/practicalmeteor:chai'; 6 | import { resetDatabase } from 'meteor/xolvio:cleaner'; 7 | import { Factory } from 'meteor/dburles:factory'; 8 | import Documents from './documents.js'; 9 | import { upsertDocument, removeDocument } from './methods.js'; 10 | 11 | describe('Documents methods', function () { 12 | beforeEach(function () { 13 | if (Meteor.isServer) { 14 | resetDatabase(); 15 | } 16 | }); 17 | 18 | it('inserts a document into the Documents collection', function () { 19 | upsertDocument.call({ 20 | title: 'You can\'t arrest me, I\'m the Cake Boss!', 21 | body: 'They went nuts!', 22 | }); 23 | 24 | const getDocument = Documents.findOne({ title: 'You can\'t arrest me, I\'m the Cake Boss!' }); 25 | assert.equal(getDocument.body, 'They went nuts!'); 26 | }); 27 | 28 | it('updates a document in the Documents collection', function () { 29 | const { _id } = Factory.create('document'); 30 | 31 | upsertDocument.call({ 32 | _id, 33 | title: 'You can\'t arrest me, I\'m the Cake Boss!', 34 | body: 'They went nuts!', 35 | }); 36 | 37 | const getDocument = Documents.findOne(_id); 38 | assert.equal(getDocument.title, 'You can\'t arrest me, I\'m the Cake Boss!'); 39 | }); 40 | 41 | it('removes a document from the Documents collection', function () { 42 | const { _id } = Factory.create('document'); 43 | removeDocument.call({ _id }); 44 | const getDocument = Documents.findOne(_id); 45 | assert.equal(getDocument, undefined); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /code/imports/ui/pages/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Row, Col, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; 4 | import handleLogin from '../../modules/login'; 5 | 6 | class Login extends React.Component { 7 | componentDidMount() { 8 | handleLogin({ component: this }); 9 | } 10 | 11 | handleSubmit(event) { 12 | event.preventDefault(); 13 | } 14 | 15 | render() { 16 | return ( 17 |
18 | 19 | 20 |

Login

21 |
(this.loginForm = form) } 23 | className="login" 24 | onSubmit={ this.handleSubmit } 25 | > 26 | 27 | Email Address 28 | 34 | 35 | 36 | 37 | Password 38 | Forgot Password? 39 | 40 | 46 | 47 | 48 |
49 | 50 |
51 |
52 | ); 53 | } 54 | } 55 | 56 | export default Login; 57 | -------------------------------------------------------------------------------- /code/imports/modules/signup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import { Accounts } from 'meteor/accounts-base'; 4 | import { Bert } from 'meteor/themeteorchef:bert'; 5 | import './validation.js'; 6 | 7 | let component; 8 | 9 | const getUserData = () => ({ 10 | email: document.querySelector('[name="emailAddress"]').value, 11 | password: document.querySelector('[name="password"]').value, 12 | profile: { 13 | name: { 14 | first: document.querySelector('[name="firstName"]').value, 15 | last: document.querySelector('[name="lastName"]').value, 16 | }, 17 | }, 18 | }); 19 | 20 | const signup = () => { 21 | const user = getUserData(); 22 | 23 | Accounts.createUser(user, (error) => { 24 | if (error) { 25 | Bert.alert(error.reason, 'danger'); 26 | } else { 27 | // browserHistory.push('/'); 28 | Bert.alert('Welcome!', 'success'); 29 | } 30 | }); 31 | }; 32 | 33 | const validate = () => { 34 | $(component.signupForm).validate({ 35 | rules: { 36 | firstName: { 37 | required: true, 38 | }, 39 | lastName: { 40 | required: true, 41 | }, 42 | emailAddress: { 43 | required: true, 44 | email: true, 45 | }, 46 | password: { 47 | required: true, 48 | minlength: 6, 49 | }, 50 | }, 51 | messages: { 52 | firstName: { 53 | required: 'First name?', 54 | }, 55 | lastName: { 56 | required: 'Last name?', 57 | }, 58 | emailAddress: { 59 | required: 'Need an email address here.', 60 | email: 'Is this email address legit?', 61 | }, 62 | password: { 63 | required: 'Need a password here.', 64 | minlength: 'Use at least six characters, please.', 65 | }, 66 | }, 67 | submitHandler() { signup(); }, 68 | }); 69 | }; 70 | 71 | export default function handleSignup(options) { 72 | component = options.component; 73 | validate(); 74 | } 75 | -------------------------------------------------------------------------------- /code/imports/ui/pages/ResetPassword.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Col, Alert, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; 3 | import handleResetPassword from '../../modules/reset-password'; 4 | 5 | export default class ResetPassword extends React.Component { 6 | componentDidMount() { 7 | handleResetPassword({ component: this, token: this.props.match.params.token }); 8 | } 9 | 10 | handleSubmit(event) { 11 | event.preventDefault(); 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 | 18 | 19 |

Reset Password

20 | 21 | To reset your password, enter a new one below. You will be logged in 22 | with your new password. 23 | 24 |
(this.resetPasswordForm = form) } 26 | className="reset-password" 27 | onSubmit={ this.handleSubmit } 28 | > 29 | 30 | New Password 31 | 37 | 38 | 39 | Repeat New Password 40 | 46 | 47 | 48 |
49 | 50 |
51 |
52 | ); 53 | } 54 | } 55 | 56 | ResetPassword.propTypes = { 57 | match: React.PropTypes.object, 58 | }; 59 | -------------------------------------------------------------------------------- /code/imports/ui/layouts/App.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; 3 | import { Grid } from 'react-bootstrap'; 4 | import { composeWithTracker } from 'react-komposer'; 5 | import { Meteor } from 'meteor/meteor'; 6 | import Public from '../pages/Public'; 7 | import Authenticated from '../pages/Authenticated'; 8 | import AppNavigation from '../components/AppNavigation'; 9 | import Index from '../pages/Index'; 10 | import Documents from '../pages/Documents'; 11 | import NewDocument from '../pages/NewDocument'; 12 | import EditDocument from '../containers/EditDocument'; 13 | import ViewDocument from '../containers/ViewDocument'; 14 | import Login from '../pages/Login'; 15 | import RecoverPassword from '../pages/RecoverPassword'; 16 | import ResetPassword from '../pages/ResetPassword'; 17 | import Signup from '../pages/Signup'; 18 | import NotFound from '../pages/NotFound'; 19 | 20 | const App = appProps => ( 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 | ); 41 | 42 | App.propTypes = { 43 | loggingIn: PropTypes.bool, 44 | authenticated: PropTypes.bool, 45 | }; 46 | 47 | const composer = (props, onData) => { 48 | const loggingIn = Meteor.loggingIn(); 49 | onData(null, { 50 | loggingIn, 51 | authenticated: !loggingIn && !!Meteor.userId(), 52 | }); 53 | }; 54 | 55 | export default composeWithTracker(composer)(App); 56 | -------------------------------------------------------------------------------- /code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name", 3 | "version": "1.0.0", 4 | "description": "Application description.", 5 | "scripts": { 6 | "start": "meteor --settings settings-development.json", 7 | "test": "meteor test --driver-package practicalmeteor:mocha --port 5000", 8 | "chimp-watch": "chimp --ddp=http://localhost:3000 --watch --mocha --path=tests", 9 | "chimp-test": "chimp --ddp=http://localhost:3000 --mocha --path=tests", 10 | "staging": "meteor deploy staging.meteor.com --settings settings-development.json", 11 | "production": "meteor deploy production.meteor.com --settings settings-production.json" 12 | }, 13 | "devDependencies": { 14 | "chimp": "^0.41.2", 15 | "eslint": "^3.8.1", 16 | "eslint-config-airbnb": "^12.0.0", 17 | "eslint-plugin-import": "^1.16.0", 18 | "eslint-plugin-jsx-a11y": "^2.2.3", 19 | "eslint-plugin-meteor": "^4.0.1", 20 | "eslint-plugin-react": "^6.4.1" 21 | }, 22 | "eslintConfig": { 23 | "parserOptions": { 24 | "ecmaFeatures": { 25 | "jsx": true 26 | } 27 | }, 28 | "plugins": [ 29 | "meteor", 30 | "react" 31 | ], 32 | "extends": [ 33 | "airbnb/base", 34 | "plugin:meteor/guide", 35 | "plugin:react/recommended" 36 | ], 37 | "env": { 38 | "browser": true 39 | }, 40 | "globals": { 41 | "server": false, 42 | "browser": false, 43 | "expect": false 44 | }, 45 | "rules": { 46 | "import/no-unresolved": 0, 47 | "import/no-extraneous-dependencies": 0, 48 | "import/extensions": 0, 49 | "no-underscore-dangle": [ 50 | "error", 51 | { 52 | "allow": [ 53 | "_id", 54 | "_ensureIndex", 55 | "_verifyEmailToken", 56 | "_resetPasswordToken", 57 | "_name" 58 | ] 59 | } 60 | ], 61 | "class-methods-use-this": 0 62 | } 63 | }, 64 | "dependencies": { 65 | "babel-runtime": "^6.18.0", 66 | "bcrypt": "^0.8.7", 67 | "bootstrap": "^3.3.7", 68 | "jquery": "^2.2.4", 69 | "jquery-validation": "^1.15.1", 70 | "react": "^15.3.2", 71 | "react-addons-pure-render-mixin": "^15.3.2", 72 | "react-bootstrap": "^0.30.5", 73 | "react-dom": "^15.3.2", 74 | "react-komposer": "^1.13.1", 75 | "react-router-bootstrap": "^0.23.1", 76 | "react-router-dom": "^4.0.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /code/imports/ui/pages/Signup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Row, Col, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; 4 | import handleSignup from '../../modules/signup'; 5 | 6 | export default class Signup extends React.Component { 7 | componentDidMount() { 8 | handleSignup({ component: this }); 9 | } 10 | 11 | handleSubmit(event) { 12 | event.preventDefault(); 13 | } 14 | 15 | render() { 16 | return ( 17 |
18 | 19 | 20 |

Sign Up

21 |
(this.signupForm = form) } 23 | onSubmit={ this.handleSubmit } 24 | > 25 | 26 | 27 | 28 | First Name 29 | 35 | 36 | 37 | 38 | 39 | Last Name 40 | 46 | 47 | 48 | 49 | 50 | Email Address 51 | 57 | 58 | 59 | Password 60 | 66 | 67 | 68 |
69 |

Already have an account? Log In.

70 | 71 |
72 |
73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /code/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.15 2 | accounts-password@1.3.4 3 | alanning:roles@1.2.16 4 | aldeed:collection2@2.10.0 5 | aldeed:collection2-core@1.2.0 6 | aldeed:schema-deny@1.1.0 7 | aldeed:schema-index@1.1.1 8 | aldeed:simple-schema@1.5.3 9 | allow-deny@1.0.5 10 | audit-argument-checks@1.0.7 11 | autoupdate@1.3.12 12 | babel-compiler@6.14.1 13 | babel-runtime@1.0.1 14 | base64@1.0.10 15 | binary-heap@1.0.10 16 | blaze@2.3.0 17 | blaze-tools@1.0.10 18 | boilerplate-generator@1.0.11 19 | browser-policy@1.1.0 20 | browser-policy-common@1.0.11 21 | browser-policy-content@1.1.0 22 | browser-policy-framing@1.1.0 23 | caching-compiler@1.1.9 24 | caching-html-compiler@1.1.0 25 | callback-hook@1.0.10 26 | check@1.2.5 27 | coffeescript@1.12.3_1 28 | dburles:factory@1.1.0 29 | ddp@1.2.5 30 | ddp-client@1.3.3 31 | ddp-common@1.2.8 32 | ddp-rate-limiter@1.0.7 33 | ddp-server@1.3.13 34 | deps@1.0.12 35 | diff-sequence@1.0.7 36 | ecmascript@0.6.3 37 | ecmascript-runtime@0.3.15 38 | ejson@1.0.13 39 | email@1.1.18 40 | es5-shim@4.6.15 41 | fastclick@1.0.13 42 | fortawesome:fontawesome@4.7.0 43 | fourseven:scss@4.5.0 44 | geojson-utils@1.0.10 45 | hot-code-push@1.0.4 46 | html-tools@1.0.11 47 | htmljs@1.0.11 48 | http@1.2.12 49 | id-map@1.0.9 50 | jquery@1.11.10 51 | launch-screen@1.1.1 52 | livedata@1.0.18 53 | localstorage@1.0.12 54 | logging@1.1.17 55 | mdg:validated-method@1.1.0 56 | mdg:validation-error@0.5.1 57 | meteor@1.6.1 58 | meteor-base@1.0.4 59 | minifier-css@1.2.16 60 | minifier-js@1.2.18 61 | minimongo@1.0.21 62 | mobile-experience@1.0.4 63 | mobile-status-bar@1.0.14 64 | modules@0.7.9 65 | modules-runtime@0.7.9 66 | mongo@1.1.16 67 | mongo-id@1.0.6 68 | npm-bcrypt@0.9.2 69 | npm-mongo@2.2.24 70 | observe-sequence@1.0.16 71 | ordered-dict@1.0.9 72 | practicalmeteor:chai@2.1.0_1 73 | practicalmeteor:loglevel@1.2.0_2 74 | practicalmeteor:mocha@2.4.5_6 75 | practicalmeteor:mocha-core@1.0.1 76 | practicalmeteor:sinon@1.14.1_2 77 | promise@0.8.8 78 | raix:eventemitter@0.1.3 79 | random@1.0.10 80 | rate-limit@1.0.7 81 | reactive-dict@1.1.8 82 | reactive-var@1.0.11 83 | reload@1.1.11 84 | retry@1.0.9 85 | routepolicy@1.0.12 86 | service-configuration@1.0.11 87 | session@1.1.7 88 | sha@1.0.9 89 | shell-server@0.2.3 90 | spacebars@1.0.13 91 | spacebars-compiler@1.1.0 92 | srp@1.0.10 93 | standard-minifier-css@1.3.4 94 | standard-minifier-js@1.2.3 95 | static-html@1.2.0 96 | templating@1.3.0 97 | templating-compiler@1.3.0 98 | templating-runtime@1.3.0 99 | templating-tools@1.1.0 100 | themeteorchef:bert@2.1.2 101 | tmeasday:test-reporter-helpers@0.2.1 102 | tracker@1.1.2 103 | ui@1.0.12 104 | underscore@1.0.10 105 | url@1.1.0 106 | webapp@1.3.14 107 | webapp-hashing@1.0.9 108 | xolvio:backdoor@0.2.1 109 | xolvio:cleaner@0.3.1 110 | --------------------------------------------------------------------------------