├── .gitignore
├── .meteor
├── .finished-upgraders
├── .gitignore
├── .id
├── packages
├── platforms
├── release
└── versions
├── client
├── main.html
├── main.js
└── main.scss
├── imports
├── api
│ ├── notes.js
│ ├── notes.test.js
│ ├── users.js
│ └── users.test.js
├── client
│ └── styles
│ │ ├── _base.scss
│ │ ├── _main.scss
│ │ ├── _variables.scss
│ │ ├── components
│ │ ├── _boxed-view.scss
│ │ ├── _button.scss
│ │ ├── _checkbox.scss
│ │ ├── _editor.scss
│ │ ├── _header.scss
│ │ ├── _item.scss
│ │ └── _page-content.scss
│ │ └── mixins
│ │ └── _media-query-mixins.scss
├── fixtures
│ └── fixtures.js
├── routes
│ └── routes.js
├── startup
│ └── simple-schema-configuration.js
└── ui
│ ├── Dashboard.js
│ ├── Editor.js
│ ├── Editor.test.js
│ ├── Login.js
│ ├── Login.test.js
│ ├── NotFound.js
│ ├── NoteList.js
│ ├── NoteList.test.js
│ ├── NoteListEmptyItem.js
│ ├── NoteListHeader.js
│ ├── NoteListHeader.test.js
│ ├── NoteListItem.js
│ ├── NoteListItem.test.js
│ ├── PrivateHeader.js
│ ├── PrivateHeader.test.js
│ ├── Signup.js
│ └── Signup.test.js
├── package.json
├── public
└── images
│ ├── bars.svg
│ ├── favicon.png
│ └── x.svg
├── readme.md
└── server
└── main.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/.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.4.0-remove-old-dev-bundle-link
15 | 1.4.1-add-shell-server-package
16 |
--------------------------------------------------------------------------------
/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 |
--------------------------------------------------------------------------------
/.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 | 1x1sbsv1xme3tt17sw9id
8 |
--------------------------------------------------------------------------------
/.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.14 # The database Meteor supports right now
10 | blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views
11 | reactive-var@1.0.11 # Reactive variable for tracker
12 | jquery@1.11.10 # Helpful client-side library
13 | tracker@1.1.1 # Meteor's client-side reactive programming library
14 |
15 | standard-minifier-css@1.3.2 # CSS minifier run for production mode
16 | standard-minifier-js@1.2.1 # JS minifier run for production mode
17 | es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers.
18 | ecmascript@0.6.1 # Enable ECMAScript2015+ syntax in app code
19 | shell-server@0.2.1 # Server-side component of the `meteor shell` command
20 |
21 | accounts-password
22 | session
23 | fourseven:scss@=3.13.0
24 | practicalmeteor:mocha@=2.4.5_6
25 | react-meteor-data@=0.2.9
26 |
--------------------------------------------------------------------------------
/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@1.4.2.3
2 |
--------------------------------------------------------------------------------
/.meteor/versions:
--------------------------------------------------------------------------------
1 | accounts-base@1.2.14
2 | accounts-password@1.3.3
3 | allow-deny@1.0.5
4 | autoupdate@1.2.11
5 | babel-compiler@6.13.0
6 | babel-runtime@1.0.1
7 | base64@1.0.10
8 | binary-heap@1.0.10
9 | blaze@2.1.9
10 | blaze-html-templates@1.0.5
11 | blaze-tools@1.0.10
12 | boilerplate-generator@1.0.11
13 | caching-compiler@1.1.9
14 | caching-html-compiler@1.0.7
15 | callback-hook@1.0.10
16 | check@1.2.4
17 | coffeescript@1.0.17
18 | ddp@1.2.5
19 | ddp-client@1.2.9
20 | ddp-common@1.2.8
21 | ddp-rate-limiter@1.0.6
22 | ddp-server@1.2.10
23 | deps@1.0.12
24 | diff-sequence@1.0.7
25 | ecmascript@0.6.1
26 | ecmascript-runtime@0.3.15
27 | ejson@1.0.13
28 | email@1.1.18
29 | es5-shim@4.6.15
30 | fastclick@1.0.13
31 | fourseven:scss@3.13.0
32 | geojson-utils@1.0.10
33 | hot-code-push@1.0.4
34 | html-tools@1.0.11
35 | htmljs@1.0.11
36 | http@1.1.8
37 | id-map@1.0.9
38 | jquery@1.11.10
39 | launch-screen@1.0.12
40 | livedata@1.0.18
41 | localstorage@1.0.12
42 | logging@1.1.16
43 | meteor@1.6.0
44 | meteor-base@1.0.4
45 | minifier-css@1.2.15
46 | minifier-js@1.2.15
47 | minimongo@1.0.19
48 | mobile-experience@1.0.4
49 | mobile-status-bar@1.0.13
50 | modules@0.7.7
51 | modules-runtime@0.7.7
52 | mongo@1.1.14
53 | mongo-id@1.0.6
54 | npm-bcrypt@0.9.2
55 | npm-mongo@2.2.11_2
56 | observe-sequence@1.0.14
57 | ordered-dict@1.0.9
58 | practicalmeteor:chai@2.1.0_1
59 | practicalmeteor:loglevel@1.2.0_2
60 | practicalmeteor:mocha@2.4.5_6
61 | practicalmeteor:mocha-core@1.0.1
62 | practicalmeteor:sinon@1.14.1_2
63 | promise@0.8.8
64 | random@1.0.10
65 | rate-limit@1.0.6
66 | react-meteor-data@0.2.9
67 | reactive-dict@1.1.8
68 | reactive-var@1.0.11
69 | reload@1.1.11
70 | retry@1.0.9
71 | routepolicy@1.0.12
72 | service-configuration@1.0.11
73 | session@1.1.7
74 | sha@1.0.9
75 | shell-server@0.2.1
76 | spacebars@1.0.13
77 | spacebars-compiler@1.0.13
78 | srp@1.0.10
79 | standard-minifier-css@1.3.2
80 | standard-minifier-js@1.2.1
81 | templating@1.2.15
82 | templating-compiler@1.2.15
83 | templating-runtime@1.2.15
84 | templating-tools@1.0.5
85 | tmeasday:check-npm-versions@0.2.0
86 | tmeasday:test-reporter-helpers@0.2.1
87 | tracker@1.1.1
88 | ui@1.0.12
89 | underscore@1.0.10
90 | url@1.0.11
91 | webapp@1.3.12
92 | webapp-hashing@1.0.9
93 |
--------------------------------------------------------------------------------
/client/main.html:
--------------------------------------------------------------------------------
1 |
2 | Notes App
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/client/main.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import ReactDOM from 'react-dom';
3 | import { Tracker } from 'meteor/tracker';
4 | import { Session } from 'meteor/session';
5 | import { browserHistory } from 'react-router';
6 |
7 | import { routes, onAuthChange } from '../imports/routes/routes';
8 | import '../imports/startup/simple-schema-configuration.js';
9 |
10 | Tracker.autorun(() => {
11 | const isAuthenticated = !!Meteor.userId();
12 | const currentPagePrivacy = Session.get('currentPagePrivacy');
13 |
14 | onAuthChange(isAuthenticated, currentPagePrivacy);
15 | });
16 |
17 | Tracker.autorun(() => {
18 | const selectedNoteId = Session.get('selectedNoteId');
19 | Session.set('isNavOpen', false);
20 |
21 | if (selectedNoteId) {
22 | browserHistory.replace(`/dashboard/${selectedNoteId}`);
23 | }
24 | });
25 |
26 | Tracker.autorun(() => {
27 | const isNavOpen = Session.get('isNavOpen');
28 |
29 | document.body.classList.toggle('is-nav-open', isNavOpen);
30 | });
31 |
32 | Meteor.startup(() => {
33 | Session.set('selectedNoteId', undefined);
34 | Session.set('isNavOpen', false);
35 | ReactDOM.render(routes, document.getElementById('app'));
36 | });
37 |
--------------------------------------------------------------------------------
/client/main.scss:
--------------------------------------------------------------------------------
1 | @import './../imports/client/styles/main';
2 |
--------------------------------------------------------------------------------
/imports/api/notes.js:
--------------------------------------------------------------------------------
1 | import { Mongo } from 'meteor/mongo';
2 | import { Meteor } from 'meteor/meteor';
3 | import moment from 'moment';
4 | import SimpleSchema from 'simpl-schema';
5 |
6 | export const Notes = new Mongo.Collection('notes');
7 |
8 | if (Meteor.isServer) {
9 | Meteor.publish('notes', function () {
10 | return Notes.find({ userId: this.userId });
11 | });
12 | }
13 |
14 | Meteor.methods({
15 | 'notes.insert'() {
16 | if (!this.userId) {
17 | throw new Meteor.Error('not-authorized');
18 | }
19 |
20 | return Notes.insert({
21 | title: '',
22 | body: '',
23 | userId: this.userId,
24 | updatedAt: moment().valueOf()
25 | });
26 | },
27 | 'notes.remove'(_id) {
28 | if (!this.userId) {
29 | throw new Meteor.Error('not-authorized');
30 | }
31 |
32 | new SimpleSchema({
33 | _id: {
34 | type: String,
35 | min: 1
36 | }
37 | }).validate({ _id });
38 |
39 | Notes.remove({ _id, userId: this.userId });
40 | },
41 | 'notes.update'(_id, updates) {
42 | if (!this.userId) {
43 | throw new Meteor.Error('not-authorized');
44 | }
45 |
46 | new SimpleSchema({
47 | _id: {
48 | type: String,
49 | min: 1
50 | },
51 | title: {
52 | type: String,
53 | optional: true
54 | },
55 | body: {
56 | type: String,
57 | optional: true
58 | }
59 | }).validate({
60 | _id,
61 | ...updates
62 | });
63 |
64 | Notes.update({
65 | _id,
66 | userId: this.userId
67 | }, {
68 | $set: {
69 | updatedAt: moment().valueOf(),
70 | ...updates
71 | }
72 | });
73 | }
74 | });
75 |
--------------------------------------------------------------------------------
/imports/api/notes.test.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import expect from 'expect';
3 |
4 | import { Notes } from './notes';
5 |
6 | if (Meteor.isServer) {
7 | describe('notes', function () {
8 | const noteOne = {
9 | _id: 'testNoteId1',
10 | title: 'My Title',
11 | body: 'My body for note',
12 | updatedAt: 0,
13 | userId: 'testUserId1'
14 | };
15 | const noteTwo = {
16 | _id: 'testNoteId2',
17 | title: 'Things To Buy',
18 | body: 'Couch',
19 | updatedAt: 0,
20 | userId: 'testUserId2'
21 | };
22 |
23 | beforeEach(function () {
24 | Notes.remove({});
25 | Notes.insert(noteOne);
26 | Notes.insert(noteTwo);
27 | });
28 |
29 | it('should insert new note', function () {
30 | const userId = 'testid';
31 | const _id = Meteor.server.method_handlers['notes.insert'].apply({ userId });
32 |
33 | expect(Notes.findOne({ _id, userId })).toExist();
34 | });
35 |
36 | it('should not insert note if not authenticated', function () {
37 | expect(() => {
38 | Meteor.server.method_handlers['notes.insert']();
39 | }).toThrow();
40 | });
41 |
42 | it('should remove note', function () {
43 | Meteor.server.method_handlers['notes.remove'].apply({ userId: noteOne.userId }, [noteOne._id]);
44 |
45 | expect(Notes.findOne({ _id: noteOne._id})).toNotExist();
46 | });
47 |
48 | it('should not remove note if unauthenticated', function () {
49 | expect(() => {
50 | Meteor.server.method_handlers['notes.remove'].apply({}, [noteOne._id]);
51 | }).toThrow();
52 | });
53 |
54 | it('should not remove note if invalid _id', function () {
55 | expect(() => {
56 | Meteor.server.method_handlers['notes.remove'].apply({ userId: noteOne.userId});
57 | }).toThrow();
58 | });
59 |
60 | it('should update note', function () {
61 | const title = 'This is an updated title';
62 |
63 | Meteor.server.method_handlers['notes.update'].apply({
64 | userId: noteOne.userId
65 | }, [
66 | noteOne._id,
67 | { title }
68 | ]);
69 |
70 | const note = Notes.findOne(noteOne._id);
71 |
72 | expect(note.updatedAt).toBeGreaterThan(0);
73 | expect(note).toInclude({
74 | title,
75 | body: noteOne.body
76 | });
77 | });
78 |
79 | it('should throw error if extra updates provided', function () {
80 | expect(() => {
81 | Meteor.server.method_handlers['notes.update'].apply({
82 | userId: noteOne.userId
83 | }, [
84 | noteOne._id,
85 | { title: 'new title', name: 'Andrew' }
86 | ]);
87 | }).toThrow();
88 | });
89 |
90 | it('should not update note if user was not creator', function () {
91 | const title = 'This is an updated title';
92 |
93 | Meteor.server.method_handlers['notes.update'].apply({
94 | userId: 'testid'
95 | }, [
96 | noteOne._id,
97 | { title }
98 | ]);
99 |
100 | const note = Notes.findOne(noteOne._id);
101 |
102 | expect(note).toInclude(noteOne);
103 | });
104 |
105 | it('should not update note if unauthenticated', function () {
106 | expect(() => {
107 | Meteor.server.method_handlers['notes.update'].apply({}, [noteOne._id]);
108 | }).toThrow();
109 | });
110 |
111 | it('should not update note if invalid _id', function () {
112 | expect(() => {
113 | Meteor.server.method_handlers['notes.update'].apply({ userId: noteOne.userId});
114 | }).toThrow();
115 | });
116 |
117 | it('should return a users notes', function () {
118 | const res = Meteor.server.publish_handlers.notes.apply({ userId: noteOne.userId });
119 | const notes = res.fetch();
120 |
121 | expect(notes.length).toBe(1);
122 | expect(notes[0]).toEqual(noteOne);
123 | });
124 |
125 | it('should return no notes for user that has none', function () {
126 | const res = Meteor.server.publish_handlers.notes.apply({ userId: 'testid' });
127 | const notes = res.fetch();
128 |
129 | expect(notes.length).toBe(0);
130 | });
131 |
132 | });
133 | }
134 |
--------------------------------------------------------------------------------
/imports/api/users.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import SimpleSchema from 'simpl-schema';
3 | import { Accounts } from 'meteor/accounts-base';
4 |
5 | export const validateNewUser = (user) => {
6 | const email = user.emails[0].address;
7 |
8 | new SimpleSchema({
9 | email: {
10 | type: String,
11 | regEx: SimpleSchema.RegEx.Email
12 | }
13 | }).validate({ email });
14 |
15 | return true;
16 | };
17 |
18 | if (Meteor.isServer) {
19 | Accounts.validateNewUser(validateNewUser);
20 | }
21 |
--------------------------------------------------------------------------------
/imports/api/users.test.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import expect from 'expect';
3 |
4 | import { validateNewUser } from './users';
5 |
6 | if (Meteor.isServer) {
7 | describe('users', function () {
8 |
9 | it('should allow valid email address', function () {
10 | const testUser = {
11 | emails: [
12 | {
13 | address: 'Test@example.com'
14 | }
15 | ]
16 | };
17 | const res = validateNewUser(testUser);
18 |
19 | expect(res).toBe(true);
20 | });
21 |
22 | it('should reject invalid email', function () {
23 | const testUser = {
24 | emails: [
25 | {
26 | address: 'Testom'
27 | }
28 | ]
29 | };
30 |
31 | expect(() => {
32 | validateNewUser(testUser);
33 | }).toThrow();
34 | });
35 |
36 | });
37 | }
38 |
39 |
40 |
41 | // const add = (a, b) => {
42 | // if (typeof b !== 'number') {
43 | // return a + a;
44 | // }
45 | //
46 | // return a + b;
47 | // };
48 | //
49 | // const square = (a) => a * a;
50 | //
51 | // describe('add', function () {
52 | // it('should add two numbers', function () {
53 | // const res = add(11, 9);
54 | //
55 | // expect(res).toBe(20);
56 | // });
57 | //
58 | // it('should double a single number', function () {
59 | // const res = add(44);
60 | //
61 | // expect(res).toBe(88);
62 | // });
63 | // });
64 | //
65 | // describe('square', function () {
66 | // it('should square a number', function () {
67 | // const res = square(11);
68 | //
69 | // expect(res).toBe(121);
70 | // });
71 | // });
72 |
--------------------------------------------------------------------------------
/imports/client/styles/_base.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | html {
8 | font-size: 62.5%;
9 | }
10 |
11 | body {
12 | background-color: $body-background-color;
13 | color: $body-default-color;
14 | font-family: $body-default-font-family;
15 | font-size: $base-font-size;
16 | }
17 |
18 | h1 {
19 | font-weight: 300;
20 | margin-bottom: $space;
21 | }
22 |
23 | h2 {
24 | font-size: $large-font-size;
25 | margin-bottom: $space;
26 | }
27 |
28 | p {
29 | margin-bottom: $space;
30 | }
31 |
32 | a {
33 | color: $body-default-color;
34 | }
35 |
36 | input[type=text], input[type=email], input[type=password], textarea {
37 | border: 1px solid $input-border;
38 | color: $body-default-color;
39 | font-size: $base-font-size;
40 | font-family: $body-default-font-family;
41 | margin-bottom: $space;
42 | padding: $input-spacing;
43 | }
44 |
--------------------------------------------------------------------------------
/imports/client/styles/_main.scss:
--------------------------------------------------------------------------------
1 | @import './variables';
2 | @import './mixins/media-query-mixins';
3 | @import './base';
4 | @import './components/boxed-view';
5 | @import './components/button';
6 | @import './components/editor';
7 | @import './components/header';
8 | @import './components/page-content';
9 | @import './components/checkbox';
10 | @import './components/item';
11 |
--------------------------------------------------------------------------------
/imports/client/styles/_variables.scss:
--------------------------------------------------------------------------------
1 | // Colors
2 | $brand-primary: #397ab1;
3 | $grey: #dddddd;
4 | $light-grey: #f9f9f9;
5 | $dark-grey: #777777;
6 |
7 | // Body
8 | $body-background-color: #fafafa;
9 | $body-default-color: #333333;
10 | $body-default-font-family: Helvetica, Arial, sans-serif;
11 |
12 | // Spacing
13 | $space: 1.4rem;
14 | $large-space: 2.8rem;
15 | $site-max-width: 100rem;
16 |
17 | // Font sizes
18 | $base-font-size: 1.4rem;
19 | $large-font-size: 2.2rem;
20 |
21 | // Form inputs
22 | $input-border: $grey;
23 | $input-spacing: 1rem;
24 |
25 | // Boxed view
26 | $boxed-view-overlay-bg: $grey;
27 | $boxed-view-bg: white;
28 | $boxed-view-border-color: $grey;
29 |
30 | // Button
31 | $button-bg: $brand-primary;
32 | $button-color: white;
33 | $button-pill-color: $brand-primary;
34 | $button-secondary-bg: $light-grey;
35 | $button-secondary-color: $dark-grey;
36 | $button-secondary-border-color: $grey;
37 |
38 | // Header
39 | $header-bg: $brand-primary;
40 | $header-color: white;
41 | $header-height: 6rem;
42 |
43 | // Item
44 | $item-border-width: 4px;
45 |
46 | // Page Content
47 | $page-content-sidebar-width: 30rem;
48 | $page-content-main-width: calc(#{$site-max-width} - #{$page-content-sidebar-width});
49 | $page-content-height: calc(100vh - #{$header-height});
50 |
--------------------------------------------------------------------------------
/imports/client/styles/components/_boxed-view.scss:
--------------------------------------------------------------------------------
1 | .boxed-view {
2 | align-items: center;
3 | display: flex;
4 | justify-content: center;
5 | height: 100vh;
6 | width: 100vw;
7 | }
8 |
9 | .boxed-view--modal {
10 | background: fade-out($boxed-view-overlay-bg, .3);
11 | position: fixed;
12 | top: 0;
13 | left: 0;
14 | right: 0;
15 | bottom: 0;
16 | }
17 |
18 | .boxed-view__box {
19 | background-color: $boxed-view-bg;
20 | border: 1px solid $boxed-view-border-color;
21 | padding: 2.4rem;
22 | text-align: center;
23 | width: 24rem;
24 | }
25 |
26 | .boxed-view__form {
27 | display: flex;
28 | flex-direction: column;
29 | }
30 |
--------------------------------------------------------------------------------
/imports/client/styles/components/_button.scss:
--------------------------------------------------------------------------------
1 | .button {
2 | background-color: $button-bg;
3 | border: none;
4 | color: $button-color;
5 | cursor: pointer;
6 | font-size: $base-font-size;
7 | line-height: 1.2;
8 | margin-bottom: $space;
9 | padding: $input-spacing;
10 | text-transform: uppercase;
11 | }
12 |
13 | .button--link {
14 | display: inline-block;
15 | text-decoration: none;
16 | }
17 |
18 | .button--pill {
19 | background-color: transparent;
20 | border: 1px solid $button-pill-color;
21 | color: $button-pill-color;
22 | margin: 0 ($space / 2) 0 0;
23 | padding: .3rem .8rem;
24 | }
25 |
26 | .button--secondary {
27 | background: $button-secondary-bg;
28 | border: 1px solid $button-secondary-border-color;
29 | color: $button-secondary-color;
30 | }
31 |
32 | .button--link-text {
33 | background: none;
34 | margin: 0;
35 | padding: 0;
36 | text-decoration: underline;
37 | text-transform: none;
38 | }
39 |
--------------------------------------------------------------------------------
/imports/client/styles/components/_checkbox.scss:
--------------------------------------------------------------------------------
1 | .checkbox {
2 | display: block;
3 | margin-bottom: $space;
4 | }
5 |
6 | .checkbox__box {
7 | margin-right: $space / 2;
8 | }
9 |
--------------------------------------------------------------------------------
/imports/client/styles/components/_editor.scss:
--------------------------------------------------------------------------------
1 | .editor {
2 | background-color: white;
3 | border: 1px solid $grey;
4 | display: flex;
5 | flex-direction: column;
6 | padding: $large-space;
7 | width: 100%;
8 | }
9 |
10 | .editor__title {
11 | border: none;
12 | border-bottom: 2px solid $grey;
13 | font-size: $large-font-size;
14 | margin-bottom: $large-space;
15 | outline: none;
16 | padding: $input-spacing;
17 | }
18 |
19 | .editor__body {
20 | flex-grow: 1;
21 | font-weight: 300;
22 | margin-bottom: $large-space;
23 | outline: none;
24 | padding: $input-spacing;
25 | resize: none;
26 | -webkit-overflow-scrolling: touch;
27 | }
28 |
29 | .editor__message {
30 | text-align: center;
31 | margin: $large-space * 2;
32 | font-style: italic;
33 | }
34 |
--------------------------------------------------------------------------------
/imports/client/styles/components/_header.scss:
--------------------------------------------------------------------------------
1 | .header {
2 | background: $header-bg;
3 | color: $header-color;
4 | }
5 |
6 | .header__content {
7 | align-items: center;
8 | display: flex;
9 | height: $header-height;
10 | justify-content: space-between;
11 | max-width: $site-max-width;
12 | margin: 0 auto;
13 | padding: $space;
14 | }
15 |
16 | .header__title {
17 | margin: 0;
18 | }
19 |
20 | .header__nav-toggle {
21 | cursor: pointer;
22 | height: 2rem;
23 | width: 2rem;
24 |
25 | @include desktop {
26 | display: none;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/imports/client/styles/components/_item.scss:
--------------------------------------------------------------------------------
1 | // Item List
2 | .item-list {
3 | background-color: white;
4 | border: 1px solid $grey;
5 | overflow-y: scroll;
6 | width: 100%;
7 | -webkit-overflow-scrolling: touch;
8 | }
9 |
10 | .item-list__header {
11 | display: flex;
12 | flex-direction: column;
13 | padding: $space;
14 | }
15 |
16 | // Item
17 | .item {
18 | border-left: $item-border-width solid transparent;
19 | cursor: pointer;
20 | padding: $space;
21 | transition: background-color .3s ease, border-left .3s ease;
22 | }
23 |
24 | .item:hover {
25 | background-color: $light-grey;
26 | border-left: $item-border-width solid $grey;
27 | }
28 |
29 | .item--selected, .item--selected:hover {
30 | background-color: $light-grey;
31 | border-left: $item-border-width solid $brand-primary;
32 | }
33 |
34 | .item__title {
35 | font-weight: normal;
36 | font-size: $base-font-size;
37 | margin-bottom: .4rem;
38 | }
39 |
40 | .item--selected .item__title {
41 | font-weight: bold;
42 | }
43 |
44 | .item__subtitle {
45 | color: $dark-grey;
46 | margin: 0;
47 | }
48 |
49 | // Empty Item
50 | .empty-item {
51 | font-style: italic;
52 | padding: $space;
53 | text-align: center;
54 | }
55 |
--------------------------------------------------------------------------------
/imports/client/styles/components/_page-content.scss:
--------------------------------------------------------------------------------
1 | .page-content {
2 | display: flex;
3 | height: $page-content-height;
4 | margin: 0 auto;
5 | max-width: $site-max-width;
6 |
7 | @include desktop {
8 | padding: $large-space $space;
9 | }
10 | }
11 |
12 | .page-content__sidebar {
13 | display: flex;
14 | transition: left .3s ease;
15 | width: 100vw;
16 |
17 | position: fixed;
18 | top: $header-height;
19 | left: -100vw;
20 | bottom: 0;
21 | z-index: 1;
22 |
23 | @include desktop {
24 | display: flex;
25 | padding-right: $large-space;
26 | position: static;
27 | width: $page-content-sidebar-width;
28 | }
29 | }
30 |
31 | .is-nav-open .page-content__sidebar {
32 | left: 0;
33 | }
34 |
35 | .page-content__main {
36 | display: flex;
37 | width: 100%;
38 |
39 | @include desktop {
40 | width: $page-content-main-width;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/imports/client/styles/mixins/_media-query-mixins.scss:
--------------------------------------------------------------------------------
1 | // @mixin funkyBorder ($borderSize: 10px, $borderColor: orange) {
2 | // border: $borderSize solid $borderColor;
3 | //
4 | // * {
5 | // @content;
6 | // }
7 | // }
8 | //
9 | // @mixin funkyBackground ($backgroundColor: green) {
10 | // background-color: $backgroundColor;
11 | // }
12 |
13 | @mixin desktop {
14 | @media (min-width: 50rem) {
15 | @content;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/imports/fixtures/fixtures.js:
--------------------------------------------------------------------------------
1 | export const notes = [
2 | {
3 | _id: 'noteId1',
4 | title: 'Test title',
5 | body: '',
6 | updatedAt: 1486137505429,
7 | userId: 'userId1'
8 | }, {
9 | _id: 'noteId2',
10 | title: '',
11 | body: 'Something is here.',
12 | updatedAt: 1486137505429,
13 | userId: 'userId2'
14 | }
15 | ];
16 |
--------------------------------------------------------------------------------
/imports/routes/routes.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import React from 'react';
3 | import { Router, Route, browserHistory } from 'react-router';
4 | import { Session } from 'meteor/session';
5 |
6 | import Signup from '../ui/Signup';
7 | import Dashboard from '../ui/Dashboard';
8 | import NotFound from '../ui/NotFound';
9 | import Login from '../ui/Login';
10 |
11 | const onEnterNotePage = (nextState) => {
12 | Session.set('selectedNoteId', nextState.params.id);
13 | };
14 | const onLeaveNotePage = () => {
15 | Session.set('selectedNoteId', undefined);
16 | };
17 | export const onAuthChange = (isAuthenticated, currentPagePrivacy) => {
18 | const isUnauthenticatedPage = currentPagePrivacy === 'unauth';
19 | const isAuthenticatedPage = currentPagePrivacy === 'auth';
20 |
21 | if (isUnauthenticatedPage && isAuthenticated) {
22 | browserHistory.replace('/dashboard');
23 | } else if (isAuthenticatedPage && !isAuthenticated) {
24 | browserHistory.replace('/');
25 | }
26 | };
27 | export const globalOnChange = (prevState, nextState) => {
28 | globalOnEnter(nextState);
29 | };
30 | export const globalOnEnter = (nextState) => {
31 | const lastRoute = nextState.routes[nextState.routes.length - 1];
32 | Session.set('currentPagePrivacy', lastRoute.privacy);
33 | };
34 | export const routes = (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 |
--------------------------------------------------------------------------------
/imports/startup/simple-schema-configuration.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import SimpleSchema from 'simpl-schema';
3 |
4 | SimpleSchema.defineValidationErrorTransform((e) => {
5 | return new Meteor.Error(400, e.message)
6 | });
7 |
--------------------------------------------------------------------------------
/imports/ui/Dashboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import PrivateHeader from './PrivateHeader';
4 | import NoteList from './NoteList';
5 | import Editor from './Editor';
6 |
7 | export default () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/imports/ui/Editor.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createContainer } from 'meteor/react-meteor-data';
3 | import { Session } from 'meteor/session';
4 | import { Meteor } from 'meteor/meteor';
5 | import { browserHistory } from 'react-router';
6 |
7 | import { Notes } from '../api/notes';
8 |
9 | export class Editor extends React.Component {
10 | constructor(props) {
11 | super(props);
12 | this.state = {
13 | title: '',
14 | body: ''
15 | };
16 | }
17 | handleBodyChange(e) {
18 | const body = e.target.value;
19 | this.setState({ body });
20 | this.props.call('notes.update', this.props.note._id, { body });
21 | }
22 | handleTitleChange(e) {
23 | const title = e.target.value;
24 | this.setState({ title });
25 | this.props.call('notes.update', this.props.note._id, { title });
26 | }
27 | handleRemoval(){
28 | this.props.call('notes.remove', this.props.note._id);
29 | this.props.browserHistory.push('/dashboard');
30 | }
31 | componentDidUpdate(prevProps, prevState) {
32 | const currentNoteId = this.props.note ? this.props.note._id : undefined;
33 | const prevNoteId = prevProps.note ? prevProps.note._id : undefined;
34 |
35 | if (currentNoteId && currentNoteId !== prevNoteId) {
36 | this.setState({
37 | title: this.props.note.title,
38 | body: this.props.note.body
39 | });
40 | }
41 | }
42 | render() {
43 | if (this.props.note) {
44 | return (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | );
53 | } else {
54 | return (
55 |
56 |
57 | { this.props.selectedNoteId ? 'Note not found.' : 'Pick or create a note to get started.'}
58 |
59 |
60 | );
61 | }
62 | }
63 | };
64 |
65 | Editor.propTypes = {
66 | note: React.PropTypes.object,
67 | selectedNoteId: React.PropTypes.string,
68 | call: React.PropTypes.func.isRequired,
69 | browserHistory: React.PropTypes.object.isRequired
70 | };
71 |
72 | export default createContainer(() => {
73 | const selectedNoteId = Session.get('selectedNoteId');
74 |
75 | return {
76 | selectedNoteId,
77 | note: Notes.findOne(selectedNoteId),
78 | call: Meteor.call,
79 | browserHistory
80 | };
81 | }, Editor);
82 |
--------------------------------------------------------------------------------
/imports/ui/Editor.test.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import React from 'react';
3 | import expect from 'expect';
4 | import { mount } from 'enzyme';
5 |
6 | import { Editor } from './Editor';
7 | import { notes } from '../fixtures/fixtures';
8 |
9 | if (Meteor.isClient) {
10 | describe('Editor', function () {
11 | let browserHistory;
12 | let call;
13 |
14 | beforeEach(function () {
15 | call = expect.createSpy();
16 | browserHistory = {
17 | push: expect.createSpy()
18 | };
19 | });
20 |
21 | it('should render pick note mesesage', function () {
22 | const wrapper = mount();
23 | expect(wrapper.find('p').text()).toBe('Pick or create a note to get started.');
24 | });
25 |
26 | it('should render not found message', function () {
27 | const wrapper = mount();
28 | expect(wrapper.find('p').text()).toBe('Note not found.');
29 | });
30 |
31 | it('should remove note', function () {
32 | const wrapper = mount();
33 |
34 | wrapper.find('button').simulate('click');
35 |
36 | expect(browserHistory.push).toHaveBeenCalledWith('/dashboard');
37 | expect(call).toHaveBeenCalledWith('notes.remove', notes[0]._id);
38 | });
39 |
40 | it('should update the note body on textarea change', function () {
41 | const newBody = 'This is my new body text';
42 | const wrapper = mount();
43 |
44 | wrapper.find('textarea').simulate('change', {
45 | target: {
46 | value: newBody
47 | }
48 | });
49 |
50 | expect(wrapper.state('body')).toBe(newBody);
51 | expect(call).toHaveBeenCalledWith('notes.update', notes[0]._id, { body: newBody });
52 | });
53 |
54 | it('should update the note title on input change', function () {
55 | const newTitle = 'This is my new title text';
56 | const wrapper = mount();
57 |
58 | wrapper.find('input').simulate('change', {
59 | target: {
60 | value: newTitle
61 | }
62 | });
63 |
64 | expect(wrapper.state('title')).toBe(newTitle);
65 | expect(call).toHaveBeenCalledWith('notes.update', notes[0]._id, { title: newTitle });
66 | });
67 |
68 | it('should set state for new note', function () {
69 | const wrapper = mount();
70 |
71 | wrapper.setProps({
72 | selectedNoteId: notes[0]._id,
73 | note: notes[0]
74 | });
75 |
76 | expect(wrapper.state('title')).toBe(notes[0].title);
77 | expect(wrapper.state('body')).toBe(notes[0].body);
78 | });
79 |
80 | it('should not set state if note prop not provided', function () {
81 | const wrapper = mount();
82 |
83 | wrapper.setProps({
84 | selectedNoteId: notes[0]._id
85 | });
86 |
87 | expect(wrapper.state('title')).toBe('');
88 | expect(wrapper.state('body')).toBe('');
89 | });
90 |
91 | });
92 | }
93 |
--------------------------------------------------------------------------------
/imports/ui/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import { Meteor } from 'meteor/meteor';
4 | import { createContainer } from 'meteor/react-meteor-data';
5 |
6 | export class Login extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | error: ''
11 | };
12 | }
13 | onSubmit(e) {
14 | e.preventDefault();
15 |
16 | let email = this.refs.email.value.trim();
17 | let password = this.refs.password.value.trim();
18 |
19 | this.props.loginWithPassword({email}, password, (err) => {
20 | if (err) {
21 | this.setState({error: 'Unable to login. Check email and password.'});
22 | } else {
23 | this.setState({error: ''});
24 | }
25 | });
26 | }
27 | render() {
28 | return (
29 |
30 |
31 |
Login
32 |
33 | {this.state.error ?
{this.state.error}
: undefined}
34 |
35 |
40 |
41 |
Need an account?
42 |
43 |
44 | );
45 | }
46 | }
47 |
48 | Login.propTypes = {
49 | loginWithPassword: React.PropTypes.func.isRequired
50 | };
51 |
52 | export default createContainer(() => {
53 | return {
54 | loginWithPassword: Meteor.loginWithPassword
55 | };
56 | }, Login);
57 |
--------------------------------------------------------------------------------
/imports/ui/Login.test.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import React from 'react';
3 | import expect from 'expect';
4 | import { mount } from 'enzyme';
5 |
6 | import { Login } from './Login';
7 |
8 | if (Meteor.isClient) {
9 | describe('Login', function () {
10 |
11 | it('should show error messages', function () {
12 | const error = 'This is not working';
13 | const wrapper = mount( {}}/>);
14 |
15 | wrapper.setState({ error });
16 | expect(wrapper.find('p').text()).toBe(error);
17 |
18 | wrapper.setState({ error: '' });
19 | expect(wrapper.find('p').length).toBe(0);
20 | });
21 |
22 | it('should call loginWithPassword with the form data', function () {
23 | const email = 'andrew@test.com';
24 | const password = 'password123';
25 | const spy = expect.createSpy();
26 | const wrapper = mount();
27 |
28 | wrapper.ref('email').node.value = email;
29 | wrapper.ref('password').node.value = password;
30 | wrapper.find('form').simulate('submit');
31 |
32 | expect(spy.calls[0].arguments[0]).toEqual({ email });
33 | expect(spy.calls[0].arguments[1]).toBe(password);
34 | });
35 |
36 | it('should set loginWithPassword callback errors', function () {
37 | const spy = expect.createSpy();
38 | const wrapper = mount();
39 |
40 | wrapper.find('form').simulate('submit');
41 |
42 | spy.calls[0].arguments[2]({});
43 | expect(wrapper.state('error').length).toNotBe(0);
44 |
45 | spy.calls[0].arguments[2]();
46 | expect(wrapper.state('error').length).toBe(0);
47 | });
48 |
49 | });
50 | }
51 |
--------------------------------------------------------------------------------
/imports/ui/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 |
4 | export default () => {
5 | return (
6 |
7 |
8 |
404 - Page Not Found
9 |
We're unable to find that page.
10 |
HEAD HOME
11 |
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/imports/ui/NoteList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import { createContainer } from 'meteor/react-meteor-data';
4 | import { Session } from 'meteor/session';
5 |
6 | import { Notes } from '../api/notes';
7 | import NoteListHeader from './NoteListHeader';
8 | import NoteListItem from './NoteListItem';
9 | import NoteListEmptyItem from './NoteListEmptyItem';
10 |
11 | export const NoteList = (props) => {
12 | return (
13 |
14 |
15 | { props.notes.length === 0 ? : undefined }
16 | {props.notes.map((note) => {
17 | return ;
18 | })}
19 |
20 | );
21 | };
22 |
23 | NoteList.propTypes = {
24 | notes: React.PropTypes.array.isRequired
25 | };
26 |
27 | export default createContainer(() => {
28 | const selectedNoteId = Session.get('selectedNoteId');
29 |
30 | Meteor.subscribe('notes');
31 |
32 | return {
33 | notes: Notes.find({}, {
34 | sort: {
35 | updatedAt: -1
36 | }
37 | }).fetch().map((note) => {
38 | return {
39 | ...note,
40 | selected: note._id === selectedNoteId
41 | };
42 | })
43 | };
44 | }, NoteList);
45 |
--------------------------------------------------------------------------------
/imports/ui/NoteList.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import expect from 'expect';
3 | import { mount } from 'enzyme';
4 | import { Meteor } from 'meteor/meteor';
5 |
6 | import { notes } from '../fixtures/fixtures';
7 | import { NoteList } from './NoteList';
8 |
9 | if (Meteor.isClient) {
10 | describe('NoteList', function () {
11 |
12 | it('should render NoteListItem for each note', function () {
13 | const wrapper = mount();
14 |
15 | expect(wrapper.find('NoteListItem').length).toBe(2);
16 | expect(wrapper.find('NoteListEmptyItem').length).toBe(0);
17 | });
18 |
19 | it('should render NoteListEmptyItem if zero notes', function () {
20 | const wrapper = mount();
21 |
22 | expect(wrapper.find('NoteListItem').length).toBe(0);
23 | expect(wrapper.find('NoteListEmptyItem').length).toBe(1);
24 | });
25 |
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/imports/ui/NoteListEmptyItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NoteListEmptyItem = () => {
4 | return (
5 | Create a note to get started!
6 | );
7 | };
8 |
9 | export default NoteListEmptyItem;
10 |
--------------------------------------------------------------------------------
/imports/ui/NoteListHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import { createContainer } from 'meteor/react-meteor-data';
4 | import { Session } from 'meteor/session';
5 |
6 | export const NoteListHeader = (props) => {
7 | return (
8 |
9 |
16 |
17 | );
18 | };
19 |
20 | NoteListHeader.propTypes = {
21 | meteorCall: React.PropTypes.func.isRequired,
22 | Session: React.PropTypes.object.isRequired
23 | };
24 |
25 | export default createContainer(() => {
26 | return {
27 | meteorCall: Meteor.call,
28 | Session
29 | };
30 | }, NoteListHeader);
31 |
--------------------------------------------------------------------------------
/imports/ui/NoteListHeader.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import expect from 'expect';
3 | import { mount } from 'enzyme';
4 | import { Meteor } from 'meteor/meteor';
5 |
6 | import { NoteListHeader } from './NoteListHeader';
7 | import { notes } from '../fixtures/fixtures';
8 |
9 | if (Meteor.isClient) {
10 | describe('NoteListHeader', function () {
11 | let meteorCall;
12 | let Session;
13 |
14 | beforeEach(function () {
15 | meteorCall = expect.createSpy();
16 | Session = {
17 | set: expect.createSpy()
18 | }
19 | });
20 |
21 | it('should call meteorCall on click', function () {
22 | const wrapper = mount();
23 |
24 | wrapper.find('button').simulate('click');
25 | meteorCall.calls[0].arguments[1](undefined, notes[0]._id);
26 |
27 | expect(meteorCall.calls[0].arguments[0]).toBe('notes.insert');
28 | expect(Session.set).toHaveBeenCalledWith('selectedNoteId', notes[0]._id);
29 | });
30 |
31 | it('should not set session for failed insert', function () {
32 | const wrapper = mount();
33 |
34 | wrapper.find('button').simulate('click');
35 | meteorCall.calls[0].arguments[1]({}, undefined);
36 |
37 | expect(meteorCall.calls[0].arguments[0]).toBe('notes.insert');
38 | expect(Session.set).toNotHaveBeenCalled();
39 | });
40 |
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/imports/ui/NoteListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import moment from 'moment';
3 | import { Session } from 'meteor/session';
4 | import { createContainer } from 'meteor/react-meteor-data';
5 |
6 | export const NoteListItem = (props) => {
7 | const className = props.note.selected ? 'item item--selected' : 'item';
8 |
9 | return (
10 | {
11 | props.Session.set('selectedNoteId', props.note._id);
12 | }}>
13 |
{ props.note.title || 'Untitled note' }
14 |
{ moment(props.note.updatedAt).format('M/DD/YY') }
15 |
16 | );
17 | };
18 |
19 | NoteListItem.propTypes = {
20 | note: React.PropTypes.object.isRequired,
21 | Session: React.PropTypes.object.isRequired
22 | };
23 |
24 | export default createContainer(() => {
25 | return { Session };
26 | }, NoteListItem);
27 |
--------------------------------------------------------------------------------
/imports/ui/NoteListItem.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import expect from 'expect';
3 | import { Meteor } from 'meteor/meteor';
4 | import { mount } from 'enzyme';
5 |
6 | import { notes } from '../fixtures/fixtures';
7 | import { NoteListItem } from './NoteListItem';
8 |
9 | if (Meteor.isClient) {
10 | describe('NoteListItem', function () {
11 | let Session;
12 |
13 | beforeEach(() => {
14 | Session = {
15 | set: expect.createSpy()
16 | };
17 | });
18 |
19 | it('should render title and timestamp', function () {
20 | const wrapper = mount( );
21 |
22 | expect(wrapper.find('h5').text()).toBe(notes[0].title);
23 | expect(wrapper.find('p').text()).toBe('2/03/17');
24 | });
25 |
26 | it('should set default title if no title set', function () {
27 | const wrapper = mount( );
28 |
29 | expect(wrapper.find('h5').text()).toBe('Untitled note');
30 | });
31 |
32 | it('should call set on click', function () {
33 | const wrapper = mount( );
34 |
35 | wrapper.find('div').simulate('click');
36 |
37 | expect(Session.set).toHaveBeenCalledWith('selectedNoteId', notes[0]._id);
38 | });
39 |
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/imports/ui/PrivateHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Accounts } from 'meteor/accounts-base';
3 | import { createContainer } from 'meteor/react-meteor-data';
4 | import { Session } from 'meteor/session';
5 |
6 | export const PrivateHeader = (props) => {
7 | const navImageSrc = props.isNavOpen ? '/images/x.svg' : '/images/bars.svg';
8 |
9 | return (
10 |
11 |
12 |

13 |
{props.title}
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | PrivateHeader.propTypes = {
21 | title: React.PropTypes.string.isRequired,
22 | handleLogout: React.PropTypes.func.isRequired,
23 | isNavOpen: React.PropTypes.bool.isRequired,
24 | handleNavToggle: React.PropTypes.func.isRequired
25 | };
26 |
27 | export default createContainer(() => {
28 | return {
29 | handleLogout: () => Accounts.logout(),
30 | handleNavToggle: () => Session.set('isNavOpen', !Session.get('isNavOpen')),
31 | isNavOpen: Session.get('isNavOpen')
32 | };
33 | }, PrivateHeader);
34 |
--------------------------------------------------------------------------------
/imports/ui/PrivateHeader.test.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import React from 'react';
3 | import expect from 'expect';
4 | import { mount } from 'enzyme';
5 |
6 | import { PrivateHeader } from './PrivateHeader';
7 |
8 | if (Meteor.isClient) {
9 | describe('PrivateHeader', function () {
10 | it('should set button text to logout', function () {
11 | const wrapper = mount( {}}/> )
12 | const buttonText = wrapper.find('button').text();
13 |
14 | expect(buttonText).toBe('Logout');
15 | });
16 |
17 | it('should use title prop as h1 text', function () {
18 | const title = 'Test title here';
19 | const wrapper = mount( {}}/> );
20 | const actualTitle = wrapper.find('h1').text();
21 |
22 | expect(actualTitle).toBe(title);
23 | });
24 |
25 | // it('should call the function', function () {
26 | // const spy = expect.createSpy();
27 | // spy(3, 4, 123);
28 | // spy('Andrew');
29 | // expect(spy).toHaveBeenCalledWith('Andrew');
30 | // });
31 |
32 | it('should call handleLogout on click', function () {
33 | const spy = expect.createSpy();
34 | const wrapper = mount( );
35 |
36 | wrapper.find('button').simulate('click');
37 |
38 | expect(spy).toHaveBeenCalled();
39 | });
40 |
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/imports/ui/Signup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import { Accounts } from 'meteor/accounts-base';
4 | import { createContainer } from 'meteor/react-meteor-data';
5 |
6 | export class Signup extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | error: ''
11 | };
12 | }
13 | onSubmit(e) {
14 | e.preventDefault();
15 |
16 | let email = this.refs.email.value.trim();
17 | let password = this.refs.password.value.trim();
18 |
19 | if (password.length < 9) {
20 | return this.setState({error: 'Password must be more than 8 characters long'});
21 | }
22 |
23 | this.props.createUser({email, password}, (err) => {
24 | if (err) {
25 | this.setState({error: err.reason});
26 | } else {
27 | this.setState({error: ''});
28 | }
29 | });
30 | }
31 | render() {
32 | return (
33 |
34 |
35 |
Join
36 |
37 | {this.state.error ?
{this.state.error}
: undefined}
38 |
39 |
44 |
45 |
Have an account?
46 |
47 |
48 | );
49 | }
50 | }
51 |
52 | Signup.propTypes = {
53 | createUser: React.PropTypes.func.isRequired
54 | };
55 |
56 | export default createContainer(() => {
57 | return {
58 | createUser: Accounts.createUser
59 | };
60 | }, Signup);
61 |
--------------------------------------------------------------------------------
/imports/ui/Signup.test.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import React from 'react';
3 | import expect from 'expect';
4 | import { mount } from 'enzyme';
5 |
6 | import { Signup } from './Signup';
7 |
8 | if (Meteor.isClient) {
9 | describe('Signup', function () {
10 |
11 | it('should show error messages', function () {
12 | const error = 'This is not working';
13 | const wrapper = mount( {}}/>);
14 |
15 | wrapper.setState({ error });
16 | expect(wrapper.find('p').text()).toBe(error);
17 |
18 | wrapper.setState({ error: '' });
19 | expect(wrapper.find('p').length).toBe(0);
20 | });
21 |
22 | it('should call createUser with the form data', function () {
23 | const email = 'andrew@test.com';
24 | const password = 'password123';
25 | const spy = expect.createSpy();
26 | const wrapper = mount();
27 |
28 | wrapper.ref('email').node.value = email;
29 | wrapper.ref('password').node.value = password;
30 | wrapper.find('form').simulate('submit');
31 |
32 | expect(spy.calls[0].arguments[0]).toEqual({ email, password });
33 | });
34 |
35 | it('should set error if short password', function () {
36 | const email = 'andrew@test.com';
37 | const password = '123 ';
38 | const spy = expect.createSpy();
39 | const wrapper = mount();
40 |
41 | wrapper.ref('email').node.value = email;
42 | wrapper.ref('password').node.value = password;
43 | wrapper.find('form').simulate('submit');
44 |
45 | expect(wrapper.state('error').length).toBeGreaterThan(0);
46 | });
47 |
48 | it('should set createUser callback errors', function () {
49 | const password = 'password123!';
50 | const reason = 'This is why it failed';
51 | const spy = expect.createSpy();
52 | const wrapper = mount();
53 |
54 | wrapper.ref('password').node.value = password;
55 | wrapper.find('form').simulate('submit');
56 |
57 | spy.calls[0].arguments[1]({ reason });
58 | expect(wrapper.state('error')).toBe(reason);
59 |
60 | spy.calls[0].arguments[1]();
61 | expect(wrapper.state('error').length).toBe(0);
62 | });
63 |
64 | });
65 | }
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "short-lnk",
3 | "private": true,
4 | "scripts": {
5 | "start": "meteor run",
6 | "test": "meteor test --driver-package=practicalmeteor:mocha"
7 | },
8 | "dependencies": {
9 | "babel-runtime": "6.18.0",
10 | "meteor-node-stubs": "~0.2.0",
11 | "moment": "^2.17.1",
12 | "react": "^15.4.1",
13 | "react-addons-pure-render-mixin": "^15.4.1",
14 | "react-dom": "^15.4.1",
15 | "react-router": "^3.0.0",
16 | "simpl-schema": "0.0.3"
17 | },
18 | "engines": {
19 | "node": "4.6.2"
20 | },
21 | "devDependencies": {
22 | "enzyme": "^2.7.1",
23 | "expect": "^1.20.2",
24 | "react-addons-test-utils": "^15.4.1"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/public/images/bars.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andrewjmead/notes-meteor-course/29deffc68f096334988727dcf447dfa6435419e8/public/images/favicon.png
--------------------------------------------------------------------------------
/public/images/x.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Notes App
2 |
3 | This is a notes application built on Meteor and created for my Udemy course.
4 |
5 | ## Getting Started
6 |
7 | This app requires you to have Meteor installed on your machine. Then you can clone the repo and run the following:
8 |
9 | ```
10 | meteor npm install
11 | ```
12 |
13 | ```
14 | meteor
15 | ```
16 |
17 | ## Running the Tests
18 |
19 | Running the tests is easy. All you have to do is run the following command and view the reporter at localhost port 3000.
20 |
21 | ```
22 | npm test
23 | ```
24 |
--------------------------------------------------------------------------------
/server/main.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { WebApp } from 'meteor/webapp';
3 |
4 | import '../imports/api/users';
5 | import '../imports/api/notes';
6 | import '../imports/startup/simple-schema-configuration.js';
7 |
8 | Meteor.startup(() => {
9 |
10 | });
11 |
--------------------------------------------------------------------------------