Commit c42d4eeaceb50a4195d854a48fe6122f086724e5
1 parent
23c8a4c3c3
Exists in
master
handful of changes
- Remove dependency on faker NPM package. - Consolidate insert and update methods into single upsert method. - Add /edit and /new pages for editing and creating documents. - Move all exports to default exports (linter thing). - Add a module for editing and creating documents. - Update method tests for documents collection to use new upsert method. - Oh, I'm sure there's something else...
Showing
25 changed files
with
294 additions
and
76 deletions
Show diff stats
.meteor/versions
1 | accounts-base@1.2.14 | 1 | accounts-base@1.2.14 |
2 | accounts-password@1.3.1 | 2 | accounts-password@1.3.1 |
3 | alanning:roles@1.2.15 | 3 | alanning:roles@1.2.15 |
4 | aldeed:collection2@2.10.0 | 4 | aldeed:collection2@2.10.0 |
5 | aldeed:collection2-core@1.2.0 | 5 | aldeed:collection2-core@1.2.0 |
6 | aldeed:schema-deny@1.1.0 | 6 | aldeed:schema-deny@1.1.0 |
7 | aldeed:schema-index@1.1.0 | 7 | aldeed:schema-index@1.1.1 |
8 | aldeed:simple-schema@1.5.3 | 8 | aldeed:simple-schema@1.5.3 |
9 | allow-deny@1.0.5 | 9 | allow-deny@1.0.5 |
10 | audit-argument-checks@1.0.7 | 10 | audit-argument-checks@1.0.7 |
11 | autoupdate@1.3.12 | 11 | autoupdate@1.3.12 |
12 | babel-compiler@6.13.0 | 12 | babel-compiler@6.13.0 |
13 | babel-runtime@0.1.12 | 13 | babel-runtime@0.1.13 |
14 | base64@1.0.10 | 14 | base64@1.0.10 |
15 | binary-heap@1.0.10 | 15 | binary-heap@1.0.10 |
16 | blaze@2.1.9 | 16 | blaze@2.1.9 |
17 | blaze-tools@1.0.10 | 17 | blaze-tools@1.0.10 |
18 | boilerplate-generator@1.0.11 | 18 | boilerplate-generator@1.0.11 |
19 | browser-policy@1.0.9 | 19 | browser-policy@1.0.9 |
20 | browser-policy-common@1.0.11 | 20 | browser-policy-common@1.0.11 |
21 | browser-policy-content@1.0.12 | 21 | browser-policy-content@1.0.12 |
22 | browser-policy-framing@1.0.12 | 22 | browser-policy-framing@1.0.12 |
23 | caching-compiler@1.1.8 | 23 | caching-compiler@1.1.8 |
24 | caching-html-compiler@1.0.7 | 24 | caching-html-compiler@1.0.7 |
25 | callback-hook@1.0.10 | 25 | callback-hook@1.0.10 |
26 | check@1.2.4 | 26 | check@1.2.4 |
27 | coffeescript@1.11.1_2 | 27 | coffeescript@1.11.1_2 |
28 | dburles:factory@1.1.0 | 28 | dburles:factory@1.1.0 |
29 | ddp@1.2.5 | 29 | ddp@1.2.5 |
30 | ddp-client@1.3.2 | 30 | ddp-client@1.3.2 |
31 | ddp-common@1.2.7 | 31 | ddp-common@1.2.7 |
32 | ddp-rate-limiter@1.0.6 | 32 | ddp-rate-limiter@1.0.6 |
33 | ddp-server@1.3.11 | 33 | ddp-server@1.3.11 |
34 | deps@1.0.12 | 34 | deps@1.0.12 |
35 | diff-sequence@1.0.7 | 35 | diff-sequence@1.0.7 |
36 | ecmascript@0.5.9 | 36 | ecmascript@0.5.9 |
37 | ecmascript-runtime@0.3.15 | 37 | ecmascript-runtime@0.3.15 |
38 | ejson@1.0.13 | 38 | ejson@1.0.13 |
39 | email@1.1.18 | 39 | email@1.1.18 |
40 | es5-shim@4.6.15 | 40 | es5-shim@4.6.15 |
41 | fastclick@1.0.13 | 41 | fastclick@1.0.13 |
42 | fortawesome:fontawesome@4.6.3 | 42 | fortawesome:fontawesome@4.6.3 |
43 | fourseven:scss@3.10.0 | 43 | fourseven:scss@3.10.1 |
44 | geojson-utils@1.0.10 | 44 | geojson-utils@1.0.10 |
45 | hot-code-push@1.0.4 | 45 | hot-code-push@1.0.4 |
46 | html-tools@1.0.11 | 46 | html-tools@1.0.11 |
47 | htmljs@1.0.11 | 47 | htmljs@1.0.11 |
48 | http@1.2.10 | 48 | http@1.2.10 |
49 | id-map@1.0.9 | 49 | id-map@1.0.9 |
50 | jquery@1.11.10 | 50 | jquery@1.11.10 |
51 | launch-screen@1.1.0 | 51 | launch-screen@1.1.0 |
52 | livedata@1.0.18 | 52 | livedata@1.0.18 |
53 | localstorage@1.0.12 | 53 | localstorage@1.0.12 |
54 | logging@1.1.16 | 54 | logging@1.1.16 |
55 | mdg:validated-method@1.1.0 | 55 | mdg:validated-method@1.1.0 |
56 | mdg:validation-error@0.5.1 | 56 | mdg:validation-error@0.5.1 |
57 | meteor@1.6.0 | 57 | meteor@1.6.0 |
58 | meteor-base@1.0.4 | 58 | meteor-base@1.0.4 |
59 | minifier-css@1.2.15 | 59 | minifier-css@1.2.15 |
60 | minifier-js@1.2.15 | 60 | minifier-js@1.2.15 |
61 | minimongo@1.0.18 | 61 | minimongo@1.0.18 |
62 | mobile-experience@1.0.4 | 62 | mobile-experience@1.0.4 |
63 | mobile-status-bar@1.0.13 | 63 | mobile-status-bar@1.0.13 |
64 | modules@0.7.7 | 64 | modules@0.7.7 |
65 | modules-runtime@0.7.7 | 65 | modules-runtime@0.7.7 |
66 | mongo@1.1.14 | 66 | mongo@1.1.14 |
67 | mongo-id@1.0.6 | 67 | mongo-id@1.0.6 |
68 | npm-bcrypt@0.9.2 | 68 | npm-bcrypt@0.9.2 |
69 | npm-mongo@2.2.11_2 | 69 | npm-mongo@2.2.11_2 |
70 | observe-sequence@1.0.14 | 70 | observe-sequence@1.0.14 |
71 | ordered-dict@1.0.9 | 71 | ordered-dict@1.0.9 |
72 | practicalmeteor:chai@2.1.0_1 | 72 | practicalmeteor:chai@2.1.0_1 |
73 | practicalmeteor:loglevel@1.2.0_2 | 73 | practicalmeteor:loglevel@1.2.0_2 |
74 | practicalmeteor:mocha@2.4.5_6 | 74 | practicalmeteor:mocha@2.4.5_6 |
75 | practicalmeteor:mocha-core@1.0.1 | 75 | practicalmeteor:mocha-core@1.0.1 |
76 | practicalmeteor:sinon@1.14.1_2 | 76 | practicalmeteor:sinon@1.14.1_2 |
77 | promise@0.8.8 | 77 | promise@0.8.8 |
78 | raix:eventemitter@0.1.3 | 78 | raix:eventemitter@0.1.3 |
79 | random@1.0.10 | 79 | random@1.0.10 |
80 | rate-limit@1.0.6 | 80 | rate-limit@1.0.6 |
81 | reactive-dict@1.1.8 | 81 | reactive-dict@1.1.8 |
82 | reactive-var@1.0.11 | 82 | reactive-var@1.0.11 |
83 | reload@1.1.11 | 83 | reload@1.1.11 |
84 | retry@1.0.9 | 84 | retry@1.0.9 |
85 | routepolicy@1.0.12 | 85 | routepolicy@1.0.12 |
86 | service-configuration@1.0.11 | 86 | service-configuration@1.0.11 |
87 | session@1.1.7 | 87 | session@1.1.7 |
88 | sha@1.0.9 | 88 | sha@1.0.9 |
89 | shell-server@0.2.1 | 89 | shell-server@0.2.1 |
90 | spacebars@1.0.13 | 90 | spacebars@1.0.13 |
91 | spacebars-compiler@1.0.13 | 91 | spacebars-compiler@1.0.13 |
92 | srp@1.0.10 | 92 | srp@1.0.10 |
93 | standard-minifier-css@1.3.2 | 93 | standard-minifier-css@1.3.2 |
94 | standard-minifier-js@1.2.1 | 94 | standard-minifier-js@1.2.1 |
95 | static-html@1.1.13 | 95 | static-html@1.1.13 |
96 | templating@1.2.15 | 96 | templating@1.2.15 |
97 | templating-compiler@1.2.15 | 97 | templating-compiler@1.2.15 |
98 | templating-runtime@1.2.15 | 98 | templating-runtime@1.2.15 |
99 | templating-tools@1.0.5 | 99 | templating-tools@1.0.5 |
100 | themeteorchef:bert@2.1.1 | 100 | themeteorchef:bert@2.1.1 |
101 | tmeasday:test-reporter-helpers@0.2.1 | 101 | tmeasday:test-reporter-helpers@0.2.1 |
102 | tracker@1.1.1 | 102 | tracker@1.1.1 |
103 | ui@1.0.12 | 103 | ui@1.0.12 |
104 | underscore@1.0.10 | 104 | underscore@1.0.10 |
105 | url@1.0.11 | 105 | url@1.0.11 |
106 | webapp@1.3.12 | 106 | webapp@1.3.12 |
107 | webapp-hashing@1.0.9 | 107 | webapp-hashing@1.0.9 |
108 | xolvio:backdoor@0.2.1 | 108 | xolvio:backdoor@0.2.1 |
109 | xolvio:cleaner@0.3.1 | 109 | xolvio:cleaner@0.3.1 |
110 | 110 |
imports/api/documents/documents.js
1 | if (Meteor.isClient) import faker from 'faker'; | ||
2 | import { Mongo } from 'meteor/mongo'; | 1 | import { Mongo } from 'meteor/mongo'; |
3 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; | 2 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; |
4 | import { Factory } from 'meteor/dburles:factory'; | 3 | import { Factory } from 'meteor/dburles:factory'; |
5 | 4 | ||
6 | export const Documents = new Mongo.Collection('Documents'); | 5 | const Documents = new Mongo.Collection('Documents'); |
6 | export default Documents; | ||
7 | 7 | ||
8 | Documents.allow({ | 8 | Documents.allow({ |
9 | insert: () => false, | 9 | insert: () => false, |
10 | update: () => false, | 10 | update: () => false, |
11 | remove: () => false, | 11 | remove: () => false, |
12 | }); | 12 | }); |
13 | 13 | ||
14 | Documents.deny({ | 14 | Documents.deny({ |
15 | insert: () => true, | 15 | insert: () => true, |
16 | update: () => true, | 16 | update: () => true, |
17 | remove: () => true, | 17 | remove: () => true, |
18 | }); | 18 | }); |
19 | 19 | ||
20 | Documents.schema = new SimpleSchema({ | 20 | Documents.schema = new SimpleSchema({ |
21 | title: { | 21 | title: { |
22 | type: String, | 22 | type: String, |
23 | label: 'The title of the document.', | 23 | label: 'The title of the document.', |
24 | }, | 24 | }, |
25 | body: { | ||
26 | type: String, | ||
27 | label: 'The body of the document.', | ||
28 | }, | ||
25 | }); | 29 | }); |
26 | 30 | ||
27 | Documents.attachSchema(Documents.schema); | 31 | Documents.attachSchema(Documents.schema); |
28 | 32 | ||
29 | Factory.define('document', Documents, { | 33 | Factory.define('document', Documents, { |
30 | title: () => faker.hacker.phrase(), | 34 | title: () => 'Factory Title', |
35 | body: () => 'Factory Body', | ||
31 | }); | 36 | }); |
imports/api/documents/documents.tests.js
1 | /* eslint-env mocha */ | 1 | /* eslint-env mocha */ |
2 | /* eslint-disable func-names, prefer-arrow-callback */ | 2 | /* eslint-disable func-names, prefer-arrow-callback */ |
3 | 3 | ||
4 | import { assert } from 'meteor/practicalmeteor:chai'; | 4 | import { assert } from 'meteor/practicalmeteor:chai'; |
5 | import { Documents } from './documents.js'; | 5 | import Documents from './documents.js'; |
6 | 6 | ||
7 | describe('Documents collection', function () { | 7 | describe('Documents collection', function () { |
8 | it('registers the collection with Mongo properly', function () { | 8 | it('registers the collection with Mongo properly', function () { |
9 | assert.equal(typeof Documents, 'object'); | 9 | assert.equal(typeof Documents, 'object'); |
10 | }); | 10 | }); |
11 | }); | 11 | }); |
12 | 12 |
imports/api/documents/methods.js
1 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; | 1 | import { SimpleSchema } from 'meteor/aldeed:simple-schema'; |
2 | import { ValidatedMethod } from 'meteor/mdg:validated-method'; | 2 | import { ValidatedMethod } from 'meteor/mdg:validated-method'; |
3 | import { Documents } from './documents'; | 3 | import Documents from './documents'; |
4 | import { rateLimit } from '../../modules/rate-limit.js'; | 4 | import { rateLimit } from '../../modules/rate-limit.js'; |
5 | 5 | ||
6 | export const insertDocument = new ValidatedMethod({ | 6 | export const upsertDocument = new ValidatedMethod({ |
7 | name: 'documents.insert', | 7 | name: 'documents.upsert', |
8 | validate: new SimpleSchema({ | 8 | validate: new SimpleSchema({ |
9 | title: { type: String }, | 9 | _id: { type: String, optional: true }, |
10 | title: { type: String, optional: true }, | ||
11 | body: { type: String, optional: true }, | ||
10 | }).validator(), | 12 | }).validator(), |
11 | run(document) { | 13 | run(document) { |
12 | Documents.insert(document); | 14 | return Documents.upsert({ _id: document._id }, { $set: document }); |
13 | }, | ||
14 | }); | ||
15 | |||
16 | export const updateDocument = new ValidatedMethod({ | ||
17 | name: 'documents.update', | ||
18 | validate: new SimpleSchema({ | ||
19 | _id: { type: String }, | ||
20 | 'update.title': { type: String, optional: true }, | ||
21 | }).validator(), | ||
22 | run({ _id, update }) { | ||
23 | Documents.update(_id, { $set: update }); | ||
24 | }, | 15 | }, |
25 | }); | 16 | }); |
26 | 17 | ||
27 | export const removeDocument = new ValidatedMethod({ | 18 | export const removeDocument = new ValidatedMethod({ |
28 | name: 'documents.remove', | 19 | name: 'documents.remove', |
29 | validate: new SimpleSchema({ | 20 | validate: new SimpleSchema({ |
30 | _id: { type: String }, | 21 | _id: { type: String }, |
31 | }).validator(), | 22 | }).validator(), |
32 | run({ _id }) { | 23 | run({ _id }) { |
33 | Documents.remove(_id); | 24 | Documents.remove(_id); |
34 | }, | 25 | }, |
35 | }); | 26 | }); |
36 | 27 | ||
37 | rateLimit({ | 28 | rateLimit({ |
38 | methods: [ | 29 | methods: [ |
39 | insertDocument, | 30 | upsertDocument, |
40 | updateDocument, | ||
41 | removeDocument, | 31 | removeDocument, |
42 | ], | 32 | ], |
43 | limit: 5, | 33 | limit: 5, |
44 | timeRange: 1000, | 34 | timeRange: 1000, |
imports/api/documents/methods.tests.js
1 | /* eslint-env mocha */ | 1 | /* eslint-env mocha */ |
2 | /* eslint-disable func-names, prefer-arrow-callback */ | 2 | /* eslint-disable func-names, prefer-arrow-callback */ |
3 | 3 | ||
4 | import { Meteor } from 'meteor/meteor'; | 4 | import { Meteor } from 'meteor/meteor'; |
5 | import { assert } from 'meteor/practicalmeteor:chai'; | 5 | import { assert } from 'meteor/practicalmeteor:chai'; |
6 | import { resetDatabase } from 'meteor/xolvio:cleaner'; | 6 | import { resetDatabase } from 'meteor/xolvio:cleaner'; |
7 | import { Factory } from 'meteor/dburles:factory'; | 7 | import { Factory } from 'meteor/dburles:factory'; |
8 | import { Documents } from './documents.js'; | 8 | import Documents from './documents.js'; |
9 | import { insertDocument, updateDocument, removeDocument } from './methods.js'; | 9 | import { upsertDocument, removeDocument } from './methods.js'; |
10 | 10 | ||
11 | describe('Documents methods', function () { | 11 | describe('Documents methods', function () { |
12 | beforeEach(function () { | 12 | beforeEach(function () { |
13 | if (Meteor.isServer) { | 13 | if (Meteor.isServer) { |
14 | resetDatabase(); | 14 | resetDatabase(); |
15 | } | 15 | } |
16 | }); | 16 | }); |
17 | 17 | ||
18 | it('inserts a document into the Documents collection', function () { | 18 | it('inserts a document into the Documents collection', function () { |
19 | insertDocument.call({ title: 'You can\'t arrest me, I\'m the Cake Boss!' }); | 19 | upsertDocument.call({ |
20 | title: 'You can\'t arrest me, I\'m the Cake Boss!', | ||
21 | body: 'They went nuts!', | ||
22 | }); | ||
23 | |||
20 | const getDocument = Documents.findOne({ title: 'You can\'t arrest me, I\'m the Cake Boss!' }); | 24 | const getDocument = Documents.findOne({ title: 'You can\'t arrest me, I\'m the Cake Boss!' }); |
21 | assert.equal(getDocument.title, 'You can\'t arrest me, I\'m the Cake Boss!'); | 25 | assert.equal(getDocument.body, 'They went nuts!'); |
22 | }); | 26 | }); |
23 | 27 | ||
24 | it('updates a document in the Documents collection', function () { | 28 | it('updates a document in the Documents collection', function () { |
25 | const { _id } = Factory.create('document'); | 29 | const { _id } = Factory.create('document'); |
26 | 30 | ||
27 | updateDocument.call({ | 31 | upsertDocument.call({ |
28 | _id, | 32 | _id, |
29 | update: { | 33 | title: 'You can\'t arrest me, I\'m the Cake Boss!', |
30 | title: 'You can\'t arrest me, I\'m the Cake Boss!', | 34 | body: 'They went nuts!', |
31 | }, | ||
32 | }); | 35 | }); |
33 | 36 | ||
34 | const getDocument = Documents.findOne(_id); | 37 | const getDocument = Documents.findOne(_id); |
35 | assert.equal(getDocument.title, 'You can\'t arrest me, I\'m the Cake Boss!'); | 38 | assert.equal(getDocument.title, 'You can\'t arrest me, I\'m the Cake Boss!'); |
36 | }); | 39 | }); |
37 | 40 | ||
38 | it('removes a document from the Documents collection', function () { | 41 | it('removes a document from the Documents collection', function () { |
39 | const { _id } = Factory.create('document'); | 42 | const { _id } = Factory.create('document'); |
40 | removeDocument.call({ _id }); | 43 | removeDocument.call({ _id }); |
41 | const getDocument = Documents.findOne(_id); | 44 | const getDocument = Documents.findOne(_id); |
42 | assert.equal(getDocument, undefined); | 45 | assert.equal(getDocument, undefined); |
43 | }); | 46 | }); |
44 | }); | 47 | }); |
imports/api/documents/server/publications.js
1 | import { Meteor } from 'meteor/meteor'; | 1 | import { Meteor } from 'meteor/meteor'; |
2 | import { Documents } from '../documents'; | 2 | import { check } from 'meteor/check'; |
3 | import Documents from '../documents'; | ||
3 | 4 | ||
4 | Meteor.publish('documents', () => Documents.find()); | 5 | Meteor.publish('documents.list', () => Documents.find()); |
6 | |||
7 | Meteor.publish('documents.view', (_id) => { | ||
8 | check(_id, String); | ||
9 | return Documents.find(_id); | ||
10 | }); | ||
11 | |||
12 | Meteor.publish('documents.edit', (_id) => { | ||
13 | check(_id, String); | ||
14 | return Documents.find(_id); | ||
15 | }); | ||
5 | 16 |
imports/modules/document-editor.js
File was created | 1 | import $ from 'jquery'; | |
2 | import 'jquery-validation'; | ||
3 | import { browserHistory } from 'react-router'; | ||
4 | import { Bert } from 'meteor/themeteorchef:bert'; | ||
5 | import { upsertDocument } from '../api/documents/methods.js'; | ||
6 | |||
7 | let component; | ||
8 | |||
9 | const handleUpsert = () => { | ||
10 | const { doc } = component.props; | ||
11 | const confirmation = doc && doc._id ? 'Document updated!' : 'Document added!'; | ||
12 | const upsert = { | ||
13 | title: document.querySelector('[name="title"]').value.trim(), | ||
14 | body: document.querySelector('[name="body"]').value.trim(), | ||
15 | }; | ||
16 | |||
17 | if (doc && doc._id) upsert._id = doc._id; | ||
18 | |||
19 | upsertDocument.call(upsert, (error, { insertedId }) => { | ||
20 | if (error) { | ||
21 | Bert.alert(error.reason, 'danger'); | ||
22 | } else { | ||
23 | component.form.reset(); | ||
24 | Bert.alert(confirmation, 'success'); | ||
25 | browserHistory.push(`/documents/${insertedId || doc._id}`); | ||
26 | } | ||
27 | }); | ||
28 | }; | ||
29 | |||
30 | const validate = () => { | ||
31 | $(component.form).validate({ | ||
32 | rules: { | ||
33 | title: { | ||
34 | required: true, | ||
35 | }, | ||
36 | body: { | ||
37 | required: true, | ||
38 | }, | ||
39 | }, | ||
40 | messages: { | ||
41 | title: { | ||
42 | required: 'Need a title in here, Seuss.', | ||
43 | }, | ||
44 | body: { | ||
45 | required: 'This thneeds a body, please.', | ||
46 | }, | ||
47 | }, | ||
48 | submitHandler() { handleUpsert(); }, | ||
49 | }); | ||
50 | }; | ||
51 | |||
52 | export default function handleLogin(options) { | ||
53 | component = options.component; | ||
54 | validate(); | ||
55 | } | ||
56 |
imports/modules/login.js
1 | import $ from 'jquery'; | 1 | import $ from 'jquery'; |
2 | import 'jquery-validation'; | 2 | import 'jquery-validation'; |
3 | import { browserHistory } from 'react-router'; | 3 | import { browserHistory } from 'react-router'; |
4 | import { Meteor } from 'meteor/meteor'; | 4 | import { Meteor } from 'meteor/meteor'; |
5 | import { Bert } from 'meteor/themeteorchef:bert'; | 5 | import { Bert } from 'meteor/themeteorchef:bert'; |
6 | 6 | ||
7 | let component; | 7 | let component; |
8 | 8 | ||
9 | const login = () => { | 9 | const login = () => { |
10 | const email = document.querySelector('[name="emailAddress"]').value; | 10 | const email = document.querySelector('[name="emailAddress"]').value; |
11 | const password = document.querySelector('[name="password"]').value; | 11 | const password = document.querySelector('[name="password"]').value; |
12 | 12 | ||
13 | Meteor.loginWithPassword(email, password, (error) => { | 13 | Meteor.loginWithPassword(email, password, (error) => { |
14 | if (error) { | 14 | if (error) { |
15 | Bert.alert(error.reason, 'warning'); | 15 | Bert.alert(error.reason, 'warning'); |
16 | } else { | 16 | } else { |
17 | Bert.alert('Logged in!', 'success'); | 17 | Bert.alert('Logged in!', 'success'); |
18 | 18 | ||
19 | const { location } = component.props; | 19 | const { location } = component.props; |
20 | if (location.state && location.state.nextPathname) { | 20 | if (location.state && location.state.nextPathname) { |
21 | browserHistory.push(location.state.nextPathname); | 21 | browserHistory.push(location.state.nextPathname); |
22 | } else { | 22 | } else { |
23 | browserHistory.push('/'); | 23 | browserHistory.push('/'); |
24 | } | 24 | } |
25 | } | 25 | } |
26 | }); | 26 | }); |
27 | }; | 27 | }; |
28 | 28 | ||
29 | const validate = () => { | 29 | const validate = () => { |
30 | $(component.refs.login).validate({ | 30 | $('.login').validate({ |
31 | rules: { | 31 | rules: { |
32 | emailAddress: { | 32 | emailAddress: { |
33 | required: true, | 33 | required: true, |
34 | email: true, | 34 | email: true, |
35 | }, | 35 | }, |
36 | password: { | 36 | password: { |
37 | required: true, | 37 | required: true, |
38 | }, | 38 | }, |
39 | }, | 39 | }, |
40 | messages: { | 40 | messages: { |
41 | emailAddress: { | 41 | emailAddress: { |
42 | required: 'Need an email address here.', | 42 | required: 'Need an email address here.', |
43 | email: 'Is this email address legit?', | 43 | email: 'Is this email address legit?', |
44 | }, | 44 | }, |
45 | password: { | 45 | password: { |
46 | required: 'Need a password here.', | 46 | required: 'Need a password here.', |
47 | }, | 47 | }, |
48 | }, | 48 | }, |
49 | submitHandler() { login(); }, | 49 | submitHandler() { login(); }, |
50 | }); | 50 | }); |
51 | }; | 51 | }; |
52 | 52 | ||
53 | export default function handleLogin(options) { | 53 | export default function handleLogin(options) { |
54 | component = options.component; | 54 | component = options.component; |
55 | validate(); | 55 | validate(); |
56 | } | 56 | } |
57 | 57 |
imports/startup/client/routes.js
1 | /* eslint-disable max-len */ | ||
2 | |||
1 | import React from 'react'; | 3 | import React from 'react'; |
2 | import { render } from 'react-dom'; | 4 | import { render } from 'react-dom'; |
3 | import { Router, Route, IndexRoute, browserHistory } from 'react-router'; | 5 | import { Router, Route, IndexRoute, browserHistory } from 'react-router'; |
4 | import { Meteor } from 'meteor/meteor'; | 6 | import { Meteor } from 'meteor/meteor'; |
5 | import { App } from '../../ui/layouts/App.js'; | 7 | import App from '../../ui/layouts/App.js'; |
6 | import { Documents } from '../../ui/pages/Documents.js'; | 8 | import Documents from '../../ui/pages/Documents.js'; |
7 | import { Index } from '../../ui/pages/Index.js'; | 9 | import NewDocument from '../../ui/pages/NewDocument.js'; |
8 | import { Login } from '../../ui/pages/Login.js'; | 10 | import EditDocument from '../../ui/containers/EditDocument.js'; |
9 | import { NotFound } from '../../ui/pages/NotFound.js'; | 11 | import ViewDocument from '../../ui/containers/ViewDocument.js'; |
10 | import { RecoverPassword } from '../../ui/pages/RecoverPassword.js'; | 12 | import Index from '../../ui/pages/Index.js'; |
11 | import { ResetPassword } from '../../ui/pages/ResetPassword.js'; | 13 | import Login from '../../ui/pages/Login.js'; |
12 | import { Signup } from '../../ui/pages/Signup.js'; | 14 | import NotFound from '../../ui/pages/NotFound.js'; |
15 | import RecoverPassword from '../../ui/pages/RecoverPassword.js'; | ||
16 | import ResetPassword from '../../ui/pages/ResetPassword.js'; | ||
17 | import Signup from '../../ui/pages/Signup.js'; | ||
13 | 18 | ||
14 | const requireAuth = (nextState, replace) => { | 19 | const requireAuth = (nextState, replace) => { |
15 | if (!Meteor.loggingIn() && !Meteor.userId()) { | 20 | if (!Meteor.loggingIn() && !Meteor.userId()) { |
16 | replace({ | 21 | replace({ |
17 | pathname: '/login', | 22 | pathname: '/login', |
18 | state: { nextPathname: nextState.location.pathname }, | 23 | state: { nextPathname: nextState.location.pathname }, |
19 | }); | 24 | }); |
20 | } | 25 | } |
21 | }; | 26 | }; |
22 | 27 | ||
23 | Meteor.startup(() => { | 28 | Meteor.startup(() => { |
24 | render( | 29 | render( |
25 | <Router history={ browserHistory }> | 30 | <Router history={ browserHistory }> |
26 | <Route path="/" component={ App }> | 31 | <Route path="/" component={ App }> |
27 | <IndexRoute name="index" component={ Index } onEnter={ requireAuth } /> | 32 | <IndexRoute name="index" component={ Index } onEnter={ requireAuth } /> |
28 | <Route name="documents" path="/documents" component={ Documents } onEnter={ requireAuth } /> | 33 | <Route name="documents" path="/documents" component={ Documents } onEnter={ requireAuth } /> |
34 | <Route name="newDocument" path="/documents/new" component={ NewDocument } onEnter={ requireAuth } /> | ||
35 | <Route name="editDocument" path="/documents/:_id/edit" component={ EditDocument } onEnter={ requireAuth } /> | ||
36 | <Route name="editDocument" path="/documents/:_id" component={ ViewDocument } onEnter={ requireAuth } /> | ||
29 | <Route name="login" path="/login" component={ Login } /> | 37 | <Route name="login" path="/login" component={ Login } /> |
30 | <Route name="recover-password" path="/recover-password" component={ RecoverPassword } /> | 38 | <Route name="recover-password" path="/recover-password" component={ RecoverPassword } /> |
31 | <Route name="reset-password" path="/reset-password/:token" component={ ResetPassword } /> | 39 | <Route name="reset-password" path="/reset-password/:token" component={ ResetPassword } /> |
32 | <Route name="signup" path="/signup" component={ Signup } /> | 40 | <Route name="signup" path="/signup" component={ Signup } /> |
33 | <Route path="*" component={ NotFound } /> | 41 | <Route path="*" component={ NotFound } /> |
34 | </Route> | 42 | </Route> |
35 | </Router>, | 43 | </Router>, |
36 | document.getElementById('react-root') | 44 | document.getElementById('react-root') |
37 | ); | 45 | ); |
38 | }); | 46 | }); |
39 | 47 |
imports/ui/components/AppNavigation.js
1 | import React from 'react'; | 1 | import React from 'react'; |
2 | import { Navbar } from 'react-bootstrap'; | 2 | import { Navbar } from 'react-bootstrap'; |
3 | import { Link } from 'react-router'; | 3 | import { Link } from 'react-router'; |
4 | import { PublicNavigation } from './PublicNavigation.js'; | 4 | import PublicNavigation from './PublicNavigation.js'; |
5 | import { AuthenticatedNavigation } from './AuthenticatedNavigation.js'; | 5 | import AuthenticatedNavigation from './AuthenticatedNavigation.js'; |
6 | 6 | ||
7 | const renderNavigation = hasUser => (hasUser ? <AuthenticatedNavigation /> : <PublicNavigation />); | 7 | const renderNavigation = hasUser => (hasUser ? <AuthenticatedNavigation /> : <PublicNavigation />); |
8 | 8 | ||
9 | const AppNavigation = ({ hasUser }) => ( | 9 | const AppNavigation = ({ hasUser }) => ( |
10 | <Navbar> | 10 | <Navbar> |
11 | <Navbar.Header> | 11 | <Navbar.Header> |
12 | <Navbar.Brand> | 12 | <Navbar.Brand> |
13 | <Link to="/">Application Name</Link> | 13 | <Link to="/">Application Name</Link> |
14 | </Navbar.Brand> | 14 | </Navbar.Brand> |
15 | <Navbar.Toggle /> | 15 | <Navbar.Toggle /> |
16 | </Navbar.Header> | 16 | </Navbar.Header> |
17 | <Navbar.Collapse> | 17 | <Navbar.Collapse> |
18 | { renderNavigation(hasUser) } | 18 | { renderNavigation(hasUser) } |
19 | </Navbar.Collapse> | 19 | </Navbar.Collapse> |
20 | </Navbar> | 20 | </Navbar> |
21 | ); | 21 | ); |
22 | 22 | ||
23 | AppNavigation.propTypes = { | 23 | AppNavigation.propTypes = { |
24 | hasUser: React.PropTypes.object, | 24 | hasUser: React.PropTypes.object, |
25 | }; | 25 | }; |
26 | 26 | ||
27 | export default AppNavigation; | 27 | export default AppNavigation; |
28 | 28 |
imports/ui/components/DocumentEditor.js
File was created | 1 | /* eslint-disable max-len, no-return-assign */ | |
2 | |||
3 | import React from 'react'; | ||
4 | import { FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; | ||
5 | import documentEditor from '../../modules/document-editor.js'; | ||
6 | |||
7 | export default class DocumentEditor extends React.Component { | ||
8 | componentDidMount() { | ||
9 | documentEditor({ component: this }); | ||
10 | } | ||
11 | |||
12 | render() { | ||
13 | const { doc } = this.props; | ||
14 | return (<form | ||
15 | ref={ form => this.form = form } | ||
16 | onSubmit={ event => event.preventDefault() } | ||
17 | > | ||
18 | <FormGroup> | ||
19 | <ControlLabel>Title</ControlLabel> | ||
20 | <FormControl | ||
21 | type="text" | ||
22 | name="title" | ||
23 | defaultValue={ doc && doc.title } | ||
24 | placeholder="Oh, The Places You'll Go!" | ||
25 | /> | ||
26 | </FormGroup> | ||
27 | <FormGroup> | ||
28 | <ControlLabel>Body</ControlLabel> | ||
29 | <FormControl | ||
30 | componentClass="textarea" | ||
31 | name="body" | ||
32 | defaultValue={ doc && doc.body } | ||
33 | placeholder="Congratulations! Today is your day. You're off to Great Places! You're off and away!" | ||
34 | /> | ||
35 | </FormGroup> | ||
36 | <Button type="submit" bsStyle="success"> | ||
37 | { doc && doc._id ? 'Save Changes' : 'Add Document' } | ||
38 | </Button> | ||
39 | </form>); | ||
40 | } | ||
41 | } | ||
42 | |||
43 | DocumentEditor.propTypes = { | ||
44 | doc: React.PropTypes.object, | ||
45 | }; | ||
46 |
imports/ui/components/DocumentsList.js
1 | import React from 'react'; | 1 | import React from 'react'; |
2 | import { ListGroup, Alert } from 'react-bootstrap'; | 2 | import { ListGroup, ListGroupItem, Alert } from 'react-bootstrap'; |
3 | import { Document } from './Document.js'; | ||
4 | 3 | ||
5 | const DocumentsList = ({ documents }) => ( | 4 | const DocumentsList = ({ documents }) => ( |
6 | documents.length > 0 ? <ListGroup className="documents-list"> | 5 | documents.length > 0 ? <ListGroup className="DocumentsList"> |
7 | {documents.map(doc => ( | 6 | {documents.map(({ _id, title }) => ( |
8 | <Document key={ doc._id } document={ doc } /> | 7 | <ListGroupItem key={ _id } href={`/documents/${_id}`}>{ title }</ListGroupItem> |
9 | ))} | 8 | ))} |
10 | </ListGroup> : | 9 | </ListGroup> : |
11 | <Alert bsStyle="warning">No documents yet.</Alert> | 10 | <Alert bsStyle="warning">No documents yet.</Alert> |
12 | ); | 11 | ); |
13 | 12 | ||
14 | DocumentsList.propTypes = { | 13 | DocumentsList.propTypes = { |
15 | documents: React.PropTypes.array, | 14 | documents: React.PropTypes.array, |
16 | }; | 15 | }; |
17 | 16 | ||
18 | export default DocumentsList; | 17 | export default DocumentsList; |
19 | 18 |
imports/ui/containers/AppNavigation.js
1 | import { composeWithTracker } from 'react-komposer'; | 1 | import { composeWithTracker } from 'react-komposer'; |
2 | import { Meteor } from 'meteor/meteor'; | 2 | import { Meteor } from 'meteor/meteor'; |
3 | import { AppNavigation } from '../components/AppNavigation.js'; | 3 | import AppNavigation from '../components/AppNavigation.js'; |
4 | 4 | ||
5 | const composer = (props, onData) => onData(null, { hasUser: Meteor.user() }); | 5 | const composer = (props, onData) => onData(null, { hasUser: Meteor.user() }); |
6 | 6 | ||
7 | export default composeWithTracker(composer, {}, {}, { pure: false })(AppNavigation); | 7 | export default composeWithTracker(composer, {}, {}, { pure: false })(AppNavigation); |
8 | 8 |
imports/ui/containers/DocumentsList.js
1 | import { composeWithTracker } from 'react-komposer'; | 1 | import { composeWithTracker } from 'react-komposer'; |
2 | import { Meteor } from 'meteor/meteor'; | 2 | import { Meteor } from 'meteor/meteor'; |
3 | import { Documents } from '../../api/documents/documents.js'; | 3 | import Documents from '../../api/documents/documents.js'; |
4 | import { DocumentsList } from '../components/DocumentsList.js'; | 4 | import DocumentsList from '../components/DocumentsList.js'; |
5 | import { Loading } from '../components/Loading.js'; | 5 | import Loading from '../components/Loading.js'; |
6 | 6 | ||
7 | const composer = (params, onData) => { | 7 | const composer = (params, onData) => { |
8 | const subscription = Meteor.subscribe('documents'); | 8 | const subscription = Meteor.subscribe('documents.list'); |
9 | if (subscription.ready()) { | 9 | if (subscription.ready()) { |
10 | const documents = Documents.find().fetch(); | 10 | const documents = Documents.find().fetch(); |
11 | onData(null, { documents }); | 11 | onData(null, { documents }); |
12 | } | 12 | } |
13 | }; | 13 | }; |
14 | 14 | ||
15 | export default composeWithTracker(composer, Loading)(DocumentsList); | 15 | export default composeWithTracker(composer, Loading)(DocumentsList); |
16 | 16 |
imports/ui/containers/EditDocument.js
File was created | 1 | import { Meteor } from 'meteor/meteor'; | |
2 | import { composeWithTracker } from 'react-komposer'; | ||
3 | import Documents from '../../api/documents/documents.js'; | ||
4 | import EditDocument from '../pages/EditDocument.js'; | ||
5 | import { Loading } from '../components/Loading.js'; | ||
6 | |||
7 | const composer = ({ params }, onData) => { | ||
8 | const subscription = Meteor.subscribe('documents.edit', params._id); | ||
9 | |||
10 | if (subscription.ready()) { | ||
11 | const doc = Documents.findOne(); | ||
12 | onData(null, { doc }); | ||
13 | } | ||
14 | }; | ||
15 | |||
16 | export default composeWithTracker(composer, Loading)(EditDocument); | ||
17 |
imports/ui/containers/ViewDocument.js
File was created | 1 | import { Meteor } from 'meteor/meteor'; | |
2 | import { composeWithTracker } from 'react-komposer'; | ||
3 | import Documents from '../../api/documents/documents.js'; | ||
4 | import ViewDocument from '../pages/ViewDocument.js'; | ||
5 | import { Loading } from '../components/Loading.js'; | ||
6 | |||
7 | const composer = ({ params }, onData) => { | ||
8 | const subscription = Meteor.subscribe('documents.view', params._id); | ||
9 | |||
10 | if (subscription.ready()) { | ||
11 | const doc = Documents.findOne(); | ||
12 | onData(null, { doc }); | ||
13 | } | ||
14 | }; | ||
15 | |||
16 | export default composeWithTracker(composer, Loading)(ViewDocument); | ||
17 |
imports/ui/pages/EditDocument.js
File was created | 1 | import React from 'react'; | |
2 | import DocumentEditor from '../components/DocumentEditor.js'; | ||
3 | |||
4 | const EditDocument = ({ doc }) => ( | ||
5 | <div className="EditDocument"> | ||
6 | <h4 className="page-header">Editing "{ doc.title }"</h4> | ||
7 | <DocumentEditor doc={ doc } /> | ||
8 | </div> | ||
9 | ); | ||
10 | |||
11 | EditDocument.propTypes = { | ||
12 | doc: React.PropTypes.object, | ||
13 | }; | ||
14 | |||
15 | export default EditDocument; | ||
16 |
imports/ui/pages/NewDocument.js
File was created | 1 | import React from 'react'; | |
2 | import DocumentEditor from '../components/DocumentEditor.js'; | ||
3 | |||
4 | const NewDocument = () => ( | ||
5 | <div className="NewDocument"> | ||
6 | <h4 className="page-header">New Document</h4> | ||
7 | <DocumentEditor /> | ||
8 | </div> | ||
9 | ); | ||
10 | |||
11 | export default NewDocument; | ||
12 |
imports/ui/pages/RecoverPassword.js
1 | import React from 'react'; | 1 | import React from 'react'; |
2 | import { Row, Col, Alert, FormGroup, FormControl, Button } from 'react-bootstrap'; | 2 | import { Row, Col, Alert, FormGroup, FormControl, Button } from 'react-bootstrap'; |
3 | import { handleRecoverPassword } from '../../modules/recover-password'; | 3 | import handleRecoverPassword from '../../modules/recover-password'; |
4 | 4 | ||
5 | export default class RecoverPassword extends React.Component { | 5 | export default class RecoverPassword extends React.Component { |
6 | componentDidMount() { | 6 | componentDidMount() { |
7 | handleRecoverPassword({ component: this }); | 7 | handleRecoverPassword({ component: this }); |
8 | } | 8 | } |
9 | 9 | ||
10 | handleSubmit() { | 10 | handleSubmit(event) { |
11 | this.preventDefault(); | 11 | event.preventDefault(); |
12 | } | 12 | } |
13 | 13 | ||
14 | render() { | 14 | render() { |
15 | return (<Row> | 15 | return (<Row> |
16 | <Col xs={ 12 } sm={ 6 } md={ 4 }> | 16 | <Col xs={ 12 } sm={ 6 } md={ 4 }> |
17 | <h4 className="page-header">Recover Password</h4> | 17 | <h4 className="page-header">Recover Password</h4> |
18 | <Alert bsStyle="info"> | 18 | <Alert bsStyle="info"> |
19 | Enter your email address below to receive a link to reset your password. | 19 | Enter your email address below to receive a link to reset your password. |
20 | </Alert> | 20 | </Alert> |
21 | <form ref="recoverPassword" className="recover-password" onSubmit={ this.handleSubmit }> | 21 | <form ref="recoverPassword" className="recover-password" onSubmit={ this.handleSubmit }> |
22 | <FormGroup> | 22 | <FormGroup> |
23 | <FormControl | 23 | <FormControl |
24 | type="email" | 24 | type="email" |
25 | ref="emailAddress" | 25 | ref="emailAddress" |
26 | name="emailAddress" | 26 | name="emailAddress" |
27 | placeholder="Email Address" | 27 | placeholder="Email Address" |
28 | /> | 28 | /> |
29 | </FormGroup> | 29 | </FormGroup> |
30 | <Button type="submit" bsStyle="success">Recover Password</Button> | 30 | <Button type="submit" bsStyle="success">Recover Password</Button> |
31 | </form> | 31 | </form> |
32 | </Col> | 32 | </Col> |
33 | </Row>); | 33 | </Row>); |
34 | } | 34 | } |
35 | } | 35 | } |
36 | 36 |
imports/ui/pages/ResetPassword.js
1 | import React from 'react'; | 1 | import React from 'react'; |
2 | import { Row, Col, Alert, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; | 2 | import { Row, Col, Alert, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; |
3 | import { handleResetPassword } from '../../modules/reset-password'; | 3 | import handleResetPassword from '../../modules/reset-password'; |
4 | 4 | ||
5 | export default class ResetPassword extends React.Component { | 5 | export default class ResetPassword extends React.Component { |
6 | componentDidMount() { | 6 | componentDidMount() { |
7 | handleResetPassword({ component: this, token: this.props.params.token }); | 7 | handleResetPassword({ component: this, token: this.props.params.token }); |
8 | } | 8 | } |
9 | 9 | ||
10 | handleSubmit() { | 10 | handleSubmit(event) { |
11 | this.preventDefault(); | 11 | event.preventDefault(); |
12 | } | 12 | } |
13 | 13 | ||
14 | render() { | 14 | render() { |
15 | return (<Row> | 15 | return (<Row> |
16 | <Col xs={ 12 } sm={ 6 } md={ 4 }> | 16 | <Col xs={ 12 } sm={ 6 } md={ 4 }> |
17 | <h4 className="page-header">Reset Password</h4> | 17 | <h4 className="page-header">Reset Password</h4> |
18 | <Alert bsStyle="info"> | 18 | <Alert bsStyle="info"> |
19 | To reset your password, enter a new one below. You will be logged in | 19 | To reset your password, enter a new one below. You will be logged in |
20 | with your new password. | 20 | with your new password. |
21 | </Alert> | 21 | </Alert> |
22 | <form ref="resetPassword" className="reset-password" onSubmit={ this.handleSubmit }> | 22 | <form ref="resetPassword" className="reset-password" onSubmit={ this.handleSubmit }> |
23 | <FormGroup> | 23 | <FormGroup> |
24 | <ControlLabel>New Password</ControlLabel> | 24 | <ControlLabel>New Password</ControlLabel> |
25 | <FormControl | 25 | <FormControl |
26 | type="password" | 26 | type="password" |
27 | ref="newPassword" | 27 | ref="newPassword" |
28 | name="newPassword" | 28 | name="newPassword" |
29 | placeholder="New Password" | 29 | placeholder="New Password" |
30 | /> | 30 | /> |
31 | </FormGroup> | 31 | </FormGroup> |
32 | <FormGroup> | 32 | <FormGroup> |
33 | <ControlLabel>Repeat New Password</ControlLabel> | 33 | <ControlLabel>Repeat New Password</ControlLabel> |
34 | <FormControl | 34 | <FormControl |
35 | type="password" | 35 | type="password" |
36 | ref="repeatNewPassword" | 36 | ref="repeatNewPassword" |
37 | name="repeatNewPassword" | 37 | name="repeatNewPassword" |
38 | placeholder="Repeat New Password" | 38 | placeholder="Repeat New Password" |
39 | /> | 39 | /> |
40 | </FormGroup> | 40 | </FormGroup> |
41 | <Button type="submit" bsStyle="success">Reset Password & Login</Button> | 41 | <Button type="submit" bsStyle="success">Reset Password & Login</Button> |
42 | </form> | 42 | </form> |
43 | </Col> | 43 | </Col> |
44 | </Row>); | 44 | </Row>); |
45 | } | 45 | } |
46 | } | 46 | } |
47 | 47 | ||
48 | ResetPassword.propTypes = { | 48 | ResetPassword.propTypes = { |
49 | params: React.PropTypes.object, | 49 | params: React.PropTypes.object, |
50 | }; | 50 | }; |
51 | 51 |
imports/ui/pages/ViewDocument.js
File was created | 1 | import React from 'react'; | |
2 | import { ButtonToolbar, ButtonGroup, Button } from 'react-bootstrap'; | ||
3 | import { browserHistory } from 'react-router'; | ||
4 | import { Bert } from 'meteor/themeteorchef:bert'; | ||
5 | import { removeDocument } from '../../api/documents/methods.js'; | ||
6 | |||
7 | const handleRemove = (_id) => { | ||
8 | if (confirm('Are you sure? This is permanent!')) { | ||
9 | removeDocument.call({ _id }, (error) => { | ||
10 | if (error) { | ||
11 | Bert.alert(error.reason, 'danger'); | ||
12 | } else { | ||
13 | Bert.alert('Document deleted!', 'success'); | ||
14 | browserHistory.push('/documents'); | ||
15 | } | ||
16 | }); | ||
17 | } | ||
18 | }; | ||
19 | |||
20 | const ViewDocument = ({ doc }) => ( | ||
21 | <div className="ViewDocument"> | ||
22 | <div className="page-header clearfix"> | ||
23 | <h4 className="pull-left">{ doc.title }</h4> | ||
24 | <ButtonToolbar className="pull-right"> | ||
25 | <ButtonGroup bsSize="small"> | ||
26 | <Button href={`/documents/${doc._id}/edit`}>Edit</Button> | ||
27 | <Button onClick={ () => handleRemove(doc._id) } className="text-danger">Delete</Button> | ||
28 | </ButtonGroup> | ||
29 | </ButtonToolbar> | ||
30 | </div> | ||
31 | { doc.body } | ||
32 | </div> | ||
33 | ); | ||
34 | |||
35 | ViewDocument.propTypes = { | ||
36 | doc: React.PropTypes.object.isRequired, | ||
37 | }; | ||
38 | |||
39 | export default ViewDocument; | ||
40 |
imports/ui/pages/documents.js
1 | import React from 'react'; | 1 | import React from 'react'; |
2 | import { Row, Col } from 'react-bootstrap'; | 2 | import { Row, Col, Button } from 'react-bootstrap'; |
3 | import DocumentsList from '../containers/DocumentsList.js'; | 3 | import DocumentsList from '../containers/DocumentsList.js'; |
4 | import { AddDocument } from '../components/AddDocument.js'; | ||
5 | 4 | ||
6 | const Documents = () => ( | 5 | const Documents = () => ( |
7 | <Row> | 6 | <Row> |
8 | <Col xs={ 12 }> | 7 | <Col xs={ 12 }> |
9 | <h4 className="page-header">Documents</h4> | 8 | <div className="page-header clearfix"> |
10 | <AddDocument /> | 9 | <h4 className="pull-left">Documents</h4> |
10 | <Button | ||
11 | bsStyle="success" | ||
12 | className="pull-right" | ||
13 | href="/documents/new" | ||
14 | >New Document</Button> | ||
15 | </div> | ||
11 | <DocumentsList /> | 16 | <DocumentsList /> |
12 | </Col> | 17 | </Col> |
13 | </Row> | 18 | </Row> |
14 | ); | 19 | ); |
15 | 20 | ||
16 | export default Documents; | 21 | export default Documents; |
imports/ui/pages/login.js
1 | import React from 'react'; | 1 | import React from 'react'; |
2 | import { Link } from 'react-router'; | 2 | import { Link } from 'react-router'; |
3 | import { Row, Col, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; | 3 | import { Row, Col, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; |
4 | import { handleLogin } from '../../modules/login'; | 4 | import handleLogin from '../../modules/login'; |
5 | 5 | ||
6 | export default class Login extends React.Component { | 6 | export default class Login extends React.Component { |
7 | componentDidMount() { | 7 | componentDidMount() { |
8 | handleLogin({ component: this }); | 8 | handleLogin({ component: this }); |
9 | } | 9 | } |
10 | 10 | ||
11 | handleSubmit() { | 11 | handleSubmit(event) { |
12 | this.preventDefault(); | 12 | event.preventDefault(); |
13 | } | 13 | } |
14 | 14 | ||
15 | render() { | 15 | render() { |
16 | return (<Row> | 16 | return (<Row> |
17 | <Col xs={ 12 } sm={ 6 } md={ 4 }> | 17 | <Col xs={ 12 } sm={ 6 } md={ 4 }> |
18 | <h4 className="page-header">Login</h4> | 18 | <h4 className="page-header">Login</h4> |
19 | <form ref="login" className="login" onSubmit={ this.handleSubmit }> | 19 | <form ref="login" className="login" onSubmit={ this.handleSubmit }> |
20 | <FormGroup> | 20 | <FormGroup> |
21 | <ControlLabel>Email Address</ControlLabel> | 21 | <ControlLabel>Email Address</ControlLabel> |
22 | <FormControl | 22 | <FormControl |
23 | type="email" | 23 | type="email" |
24 | ref="emailAddress" | 24 | ref="emailAddress" |
25 | name="emailAddress" | 25 | name="emailAddress" |
26 | placeholder="Email Address" | 26 | placeholder="Email Address" |
27 | /> | 27 | /> |
28 | </FormGroup> | 28 | </FormGroup> |
29 | <FormGroup> | 29 | <FormGroup> |
30 | <ControlLabel> | 30 | <ControlLabel> |
31 | <span className="pull-left">Password</span> | 31 | <span className="pull-left">Password</span> |
32 | <Link className="pull-right" to="/recover-password">Forgot Password?</Link> | 32 | <Link className="pull-right" to="/recover-password">Forgot Password?</Link> |
33 | </ControlLabel> | 33 | </ControlLabel> |
34 | <FormControl | 34 | <FormControl |
35 | type="password" | 35 | type="password" |
36 | ref="password" | 36 | ref="password" |
37 | name="password" | 37 | name="password" |
38 | placeholder="Password" | 38 | placeholder="Password" |
39 | /> | 39 | /> |
40 | </FormGroup> | 40 | </FormGroup> |
41 | <Button type="submit" bsStyle="success">Login</Button> | 41 | <Button type="submit" bsStyle="success">Login</Button> |
42 | </form> | 42 | </form> |
43 | </Col> | 43 | </Col> |
44 | </Row>); | 44 | </Row>); |
45 | } | 45 | } |
46 | } | 46 | } |
47 | 47 |
imports/ui/pages/signup.js
1 | import React from 'react'; | 1 | import React from 'react'; |
2 | import { Link } from 'react-router'; | 2 | import { Link } from 'react-router'; |
3 | import { Row, Col, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; | 3 | import { Row, Col, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; |
4 | import { handleSignup } from '../../modules/signup'; | 4 | import handleSignup from '../../modules/signup'; |
5 | 5 | ||
6 | export default class Signup extends React.Component { | 6 | export default class Signup extends React.Component { |
7 | componentDidMount() { | 7 | componentDidMount() { |
8 | handleSignup({ component: this }); | 8 | handleSignup({ component: this }); |
9 | } | 9 | } |
10 | 10 | ||
11 | handleSubmit() { | 11 | handleSubmit(event) { |
12 | this.preventDefault(); | 12 | event.preventDefault(); |
13 | } | 13 | } |
14 | 14 | ||
15 | render() { | 15 | render() { |
16 | return (<Row> | 16 | return (<Row> |
17 | <Col xs={ 12 } sm={ 6 } md={ 4 }> | 17 | <Col xs={ 12 } sm={ 6 } md={ 4 }> |
18 | <h4 className="page-header">Sign Up</h4> | 18 | <h4 className="page-header">Sign Up</h4> |
19 | <form ref="signup" className="signup" onSubmit={ this.handleSubmit }> | 19 | <form ref="signup" className="signup" onSubmit={ this.handleSubmit }> |
20 | <Row> | 20 | <Row> |
21 | <Col xs={ 6 } sm={ 6 }> | 21 | <Col xs={ 6 } sm={ 6 }> |
22 | <FormGroup> | 22 | <FormGroup> |
23 | <ControlLabel>First Name</ControlLabel> | 23 | <ControlLabel>First Name</ControlLabel> |
24 | <FormControl | 24 | <FormControl |
25 | type="text" | 25 | type="text" |
26 | ref="firstName" | 26 | ref="firstName" |
27 | name="firstName" | 27 | name="firstName" |
28 | placeholder="First Name" | 28 | placeholder="First Name" |
29 | /> | 29 | /> |
30 | </FormGroup> | 30 | </FormGroup> |
31 | </Col> | 31 | </Col> |
32 | <Col xs={ 6 } sm={ 6 }> | 32 | <Col xs={ 6 } sm={ 6 }> |
33 | <FormGroup> | 33 | <FormGroup> |
34 | <ControlLabel>Last Name</ControlLabel> | 34 | <ControlLabel>Last Name</ControlLabel> |
35 | <FormControl | 35 | <FormControl |
36 | type="text" | 36 | type="text" |
37 | ref="lastName" | 37 | ref="lastName" |
38 | name="lastName" | 38 | name="lastName" |
39 | placeholder="Last Name" | 39 | placeholder="Last Name" |
40 | /> | 40 | /> |
41 | </FormGroup> | 41 | </FormGroup> |
42 | </Col> | 42 | </Col> |
43 | </Row> | 43 | </Row> |
44 | <FormGroup> | 44 | <FormGroup> |
45 | <ControlLabel>Email Address</ControlLabel> | 45 | <ControlLabel>Email Address</ControlLabel> |
46 | <FormControl | 46 | <FormControl |
47 | type="text" | 47 | type="text" |
48 | ref="emailAddress" | 48 | ref="emailAddress" |
49 | name="emailAddress" | 49 | name="emailAddress" |
50 | placeholder="Email Address" | 50 | placeholder="Email Address" |
51 | /> | 51 | /> |
52 | </FormGroup> | 52 | </FormGroup> |
53 | <FormGroup> | 53 | <FormGroup> |
54 | <ControlLabel>Password</ControlLabel> | 54 | <ControlLabel>Password</ControlLabel> |
55 | <FormControl | 55 | <FormControl |
56 | type="password" | 56 | type="password" |
57 | ref="password" | 57 | ref="password" |
58 | name="password" | 58 | name="password" |
59 | placeholder="Password" | 59 | placeholder="Password" |
60 | /> | 60 | /> |
61 | </FormGroup> | 61 | </FormGroup> |
62 | <Button type="submit" bsStyle="success">Sign Up</Button> | 62 | <Button type="submit" bsStyle="success">Sign Up</Button> |
63 | </form> | 63 | </form> |
64 | <p>Already have an account? <Link to="/login">Log In</Link>.</p> | 64 | <p>Already have an account? <Link to="/login">Log In</Link>.</p> |
65 | </Col> | 65 | </Col> |
66 | </Row>); | 66 | </Row>); |
67 | } | 67 | } |
68 | } | 68 | } |
69 | 69 |
package.json
1 | { | 1 | { |
2 | "name": "application-name", | 2 | "name": "application-name", |
3 | "version": "1.0.0", | 3 | "version": "1.0.0", |
4 | "description": "Application description.", | 4 | "description": "Application description.", |
5 | "scripts": { | 5 | "scripts": { |
6 | "start": "meteor --settings settings-development.json", | 6 | "start": "meteor --settings settings-development.json", |
7 | "test": "meteor test --driver-package practicalmeteor:mocha --port 5000", | 7 | "test": "meteor test --driver-package practicalmeteor:mocha --port 5000", |
8 | "chimp-watch": "chimp --ddp=http://localhost:3000 --watch --mocha --path=tests", | 8 | "chimp-watch": "chimp --ddp=http://localhost:3000 --watch --mocha --path=tests", |
9 | "chimp-test": "chimp --ddp=http://localhost:3000 --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", | 10 | "staging": "meteor deploy staging.meteor.com --settings settings-development.json", |
11 | "production": "meteor deploy production.meteor.com --settings settings-production.json" | 11 | "production": "meteor deploy production.meteor.com --settings settings-production.json" |
12 | }, | 12 | }, |
13 | "devDependencies": { | 13 | "devDependencies": { |
14 | "chimp": "^0.41.2", | 14 | "chimp": "^0.41.2", |
15 | "eslint": "^3.8.1", | 15 | "eslint": "^3.8.1", |
16 | "eslint-config-airbnb": "^12.0.0", | 16 | "eslint-config-airbnb": "^12.0.0", |
17 | "eslint-plugin-import": "^1.16.0", | 17 | "eslint-plugin-import": "^1.16.0", |
18 | "eslint-plugin-jsx-a11y": "^2.2.3", | 18 | "eslint-plugin-jsx-a11y": "^2.2.3", |
19 | "eslint-plugin-meteor": "^4.0.1", | 19 | "eslint-plugin-meteor": "^4.0.1", |
20 | "eslint-plugin-react": "^6.4.1", | 20 | "eslint-plugin-react": "^6.4.1" |
21 | "faker": "^3.1.0" | ||
22 | }, | 21 | }, |
23 | "eslintConfig": { | 22 | "eslintConfig": { |
24 | "parserOptions": { | 23 | "parserOptions": { |
25 | "ecmaFeatures": { | 24 | "ecmaFeatures": { |
26 | "jsx": true | 25 | "jsx": true |
27 | } | 26 | } |
28 | }, | 27 | }, |
29 | "plugins": [ | 28 | "plugins": [ |
30 | "meteor", | 29 | "meteor", |
31 | "react" | 30 | "react" |
32 | ], | 31 | ], |
33 | "extends": [ | 32 | "extends": [ |
34 | "airbnb/base", | 33 | "airbnb/base", |
35 | "plugin:meteor/guide", | 34 | "plugin:meteor/guide", |
36 | "plugin:react/recommended" | 35 | "plugin:react/recommended" |
37 | ], | 36 | ], |
38 | "env": { | 37 | "env": { |
39 | "browser": true | 38 | "browser": true |
40 | }, | 39 | }, |
41 | "globals": { | 40 | "globals": { |
42 | "server": false, | 41 | "server": false, |
43 | "browser": false, | 42 | "browser": false, |
44 | "expect": false | 43 | "expect": false |
45 | }, | 44 | }, |
46 | "rules": { | 45 | "rules": { |
47 | "import/no-unresolved": 0, | 46 | "import/no-unresolved": 0, |
48 | "import/no-extraneous-dependencies": 0, | 47 | "import/no-extraneous-dependencies": 0, |
49 | "import/extensions": 0, | 48 | "import/extensions": 0, |
50 | "no-underscore-dangle": [ | 49 | "no-underscore-dangle": [ |
51 | "error", | 50 | "error", |
52 | { | 51 | { |
53 | "allow": [ | 52 | "allow": [ |
54 | "_id", | 53 | "_id", |
55 | "_ensureIndex", | 54 | "_ensureIndex", |
56 | "_verifyEmailToken", | 55 | "_verifyEmailToken", |
57 | "_resetPasswordToken", | 56 | "_resetPasswordToken", |
58 | "_name" | 57 | "_name" |
59 | ] | 58 | ] |
60 | } | 59 | } |
61 | ] | 60 | ], |
61 | "class-methods-use-this": 0 | ||
62 | } | 62 | } |
63 | }, | 63 | }, |
64 | "dependencies": { | 64 | "dependencies": { |
65 | "bcrypt": "^0.8.7", | 65 | "bcrypt": "^0.8.7", |
66 | "bootstrap": "^3.3.7", | 66 | "bootstrap": "^3.3.7", |
67 | "jquery": "^3.1.1", | 67 | "jquery": "^2.2.4", |
68 | "jquery-validation": "^1.15.1", | 68 | "jquery-validation": "^1.15.1", |
69 | "react": "^15.3.2", | 69 | "react": "^15.3.2", |
70 | "react-addons-pure-render-mixin": "^15.3.2", | 70 | "react-addons-pure-render-mixin": "^15.3.2", |
71 | "react-bootstrap": "^0.30.5", | 71 | "react-bootstrap": "^0.30.5", |
72 | "react-dom": "^15.3.2", | 72 | "react-dom": "^15.3.2", |
73 | "react-komposer": "^1.13.1", | 73 | "react-komposer": "^1.13.1", |
74 | "react-router": "^3.0.0", | 74 | "react-router": "^3.0.0", |
75 | "react-router-bootstrap": "^0.23.1" | 75 | "react-router-bootstrap": "^0.23.1" |
76 | } | 76 | } |
77 | } | 77 | } |