├── .gitignore ├── LICENSE ├── README.md └── app ├── .gitignore ├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── packages ├── platforms ├── release └── versions ├── client ├── head.html └── main.js ├── imports ├── collections │ ├── notes.js │ └── server │ │ └── publications.js ├── components │ ├── accounts │ │ └── login_buttons.jsx │ ├── buttons │ │ ├── delete_btn.jsx │ │ └── icon_btn.jsx │ ├── content │ │ ├── content_block.jsx │ │ └── page_title.jsx │ ├── forms │ │ ├── auto_save_input.jsx │ │ ├── click_to_edit.jsx │ │ └── single_field_submit.jsx │ ├── layouts │ │ └── app_header.jsx │ ├── lists │ │ └── list.jsx │ ├── pages │ │ ├── homepage.jsx │ │ └── note_details_page.jsx │ └── utility │ │ ├── loading.jsx │ │ └── loading_wrapper.jsx ├── containers │ ├── app_container.js │ ├── note_details_container.js │ └── notes_list_container.jsx ├── startup │ ├── client │ │ ├── index.js │ │ └── routes.jsx │ └── server │ │ └── index.js └── stylesheets │ ├── components │ ├── _buttons.scss │ ├── _forms.scss │ └── _lists.scss │ ├── core │ ├── _base.scss │ ├── _helpers.scss │ ├── _normalize.scss │ └── _variables.scss │ ├── layouts │ ├── _app_container.scss │ ├── _app_header.scss │ ├── _flex_layouts.scss │ ├── _full_height.scss │ └── _main_content.scss │ ├── main.scss │ ├── theme │ ├── _colors.scss │ └── _text.scss │ └── vendor │ └── _loader.scss ├── package.json └── server └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore top-level tmp directory only 2 | /tmp 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 The Coder Chronicles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Basic Note-taking App Built with Meteor and React 2 | 3 | ![note app views](https://cloud.githubusercontent.com/assets/819213/16540828/f38cfbee-403f-11e6-993d-683a5da0a298.png) 4 | 5 | This is the app we built during an [introductory workshop](http://www.meetup.com/BeginnerProgrammers/events/230617884/?rv=cr1&_af=event&_af_eid=230617884&https=off) on Meteor and React JS. This app can be a great starting point for a beginner programmer who wants to take their JS knowledge to the next level. 6 | 7 | Just [clone the repo](https://help.github.com/articles/cloning-a-repository/), and follow the comments in the code. Start at [/app/client/main.js](https://github.com/CodeChron/meteor-react-tutorial-notes-app/blob/master/app/client/main.js). I recommend trying to build your own version of the app while reviewing the code and the comments. 8 | 9 | 10 | ## Run the app 11 | 1. Make sure you have [Meteor installed](https://www.meteor.com/install). 12 | 2. Cd into the ``app`` directory. 13 | 2. Install npm packages: ```meteor npm install``` 14 | 3. Run the app: ```meteor``` 15 | 4. View the app in your browser at ```http://localhost:3000``` 16 | 17 | ## Some topics covered 18 | - Eager vs Lazy Loading 19 | - ES6 Modules (import/export) 20 | - Publications and Subcriptions 21 | - Creating your own mini CSS framework 22 | - Creating React components using plain functions vs with React.Component 23 | - Setting propTypes and defaultProps 24 | - Defining and updating React component state 25 | 26 | and more... 27 | 28 | ### Getting caught up using git branches. 29 | 1. Clone the repo 30 | 2. Pull down all branches: ```git fetch --all``` 31 | 3. View available branches: ```git branch -a``` 32 | 4. Check out a specific branch: ```git checkout remotes/origin/[branchname]``` eg ```git checkout remotes/origin/01-setup``` 33 | 34 | 35 | ## Questions, Comments, Feedback? 36 | Leave a comment at the associated [blog post](http://coderchronicles.org/2016/06/10/build-a-simple-note-taking-app-with-meteor-and-react/) or [ping me](https://twitter.com/codechron) on Twitter. 37 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /app/.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 | -------------------------------------------------------------------------------- /app/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /app/.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 | 4xoxzi17qoczq11c9n17 8 | -------------------------------------------------------------------------------- /app/.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 # Packages every Meteor app needs to have 8 | mobile-experience # Packages for a great mobile UX 9 | mongo # The database Meteor supports right now 10 | blaze-html-templates # Compile .html files into Meteor Blaze views 11 | reactive-var # Reactive variable for tracker 12 | jquery # Helpful client-side library 13 | tracker # Meteor's client-side reactive programming library 14 | 15 | standard-minifier-js # JS minifier run for production mode 16 | es5-shim # ECMAScript 5 compatibility for older browsers. 17 | ecmascript # Enable ECMAScript2015+ syntax in app code 18 | 19 | fourseven:scss 20 | seba:minifiers-autoprefixer 21 | kadira:flow-router 22 | jagi:astronomy 23 | react-meteor-data 24 | msavin:mongol 25 | accounts-password 26 | accounts-ui 27 | tmeasday:publish-counts 28 | -------------------------------------------------------------------------------- /app/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /app/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3.4.1 2 | -------------------------------------------------------------------------------- /app/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.8 2 | accounts-password@1.1.11 3 | accounts-ui@1.1.9 4 | accounts-ui-unstyled@1.1.12 5 | allow-deny@1.0.5 6 | autoupdate@1.2.10 7 | babel-compiler@6.8.3 8 | babel-runtime@0.1.9_1 9 | base64@1.0.9 10 | binary-heap@1.0.9 11 | blaze@2.1.8 12 | blaze-html-templates@1.0.4 13 | blaze-tools@1.0.9 14 | boilerplate-generator@1.0.9 15 | caching-compiler@1.0.5_1 16 | caching-html-compiler@1.0.6 17 | callback-hook@1.0.9 18 | check@1.2.3 19 | ddp@1.2.5 20 | ddp-client@1.2.8_1 21 | ddp-common@1.2.6 22 | ddp-rate-limiter@1.0.5 23 | ddp-server@1.2.8_1 24 | deps@1.0.12 25 | diff-sequence@1.0.6 26 | ecmascript@0.4.6_1 27 | ecmascript-runtime@0.2.11_1 28 | ejson@1.0.12 29 | email@1.0.14_1 30 | es5-shim@4.5.12_1 31 | fastclick@1.0.12 32 | fourseven:scss@3.4.3 33 | geojson-utils@1.0.9 34 | hot-code-push@1.0.4 35 | html-tools@1.0.10 36 | htmljs@1.0.10 37 | http@1.1.7 38 | id-map@1.0.8 39 | jagi:astronomy@2.0.1 40 | jquery@1.11.9 41 | kadira:flow-router@2.12.1 42 | launch-screen@1.0.12 43 | less@2.6.3_1 44 | livedata@1.0.18 45 | localstorage@1.0.11 46 | logging@1.0.13_1 47 | mdg:validation-error@0.5.1 48 | meteor@1.1.15_1 49 | meteor-base@1.0.4 50 | meteortoys:toykit@3.0.4 51 | minifier-css@1.1.12_1 52 | minifier-js@1.1.12_1 53 | minimongo@1.0.17 54 | mobile-experience@1.0.4 55 | mobile-status-bar@1.0.12 56 | modules@0.6.4 57 | modules-runtime@0.6.4_1 58 | mongo@1.1.9_1 59 | mongo-id@1.0.5 60 | msavin:mongol@2.0.1 61 | npm-bcrypt@0.8.7 62 | npm-mongo@1.4.44_1 63 | observe-sequence@1.0.12 64 | ordered-dict@1.0.8 65 | promise@0.7.2_1 66 | random@1.0.10 67 | rate-limit@1.0.5 68 | react-meteor-data@0.2.9 69 | reactive-dict@1.1.8 70 | reactive-var@1.0.10 71 | reload@1.1.10 72 | retry@1.0.8 73 | routepolicy@1.0.11 74 | seba:minifiers-autoprefixer@1.0.1 75 | service-configuration@1.0.10 76 | session@1.1.6 77 | sha@1.0.8 78 | spacebars@1.0.12 79 | spacebars-compiler@1.0.12 80 | srp@1.0.9 81 | standard-minifier-js@1.0.7_1 82 | templating@1.1.12_1 83 | templating-tools@1.0.4 84 | tmeasday:check-npm-versions@0.2.0 85 | tmeasday:publish-counts@0.7.3 86 | tracker@1.0.14 87 | ui@1.0.11 88 | underscore@1.0.9 89 | url@1.0.10 90 | webapp@1.2.9_1 91 | webapp-hashing@1.0.9 92 | -------------------------------------------------------------------------------- /app/client/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /app/client/main.js: -------------------------------------------------------------------------------- 1 | // By default, Meteor loads files 'eagerly', meaning that all files inside your app directory will be loaded and accessible when the app starts up. One disadvantage of this is, if you have a large app with lots of files, you are likely loading much more than you actually need on startup, which in turn can lead to a bad user experience (what's worse than a slow loading app?) With Meteor 1.3, we can address this with 'lazy' loading, in which files are not loaded by default. To take advantage of this, we need to place files in a special directory named "imports" and then import the files we need. The example below uses ES6 module syntax to import files on startup. 2 | 3 | //Here, we are pointing to a directory in the imports directory. For this to work, we need an index.js file in that directory. 4 | 5 | import '/imports/startup/client' 6 | 7 | //NEXT: continue to /imports/startup/client/index.js 8 | -------------------------------------------------------------------------------- /app/imports/collections/notes.js: -------------------------------------------------------------------------------- 1 | import { Mongo } from 'meteor/mongo' 2 | import { Meteor } from 'meteor/meteor' 3 | import { Class } from 'meteor/jagi:astronomy' 4 | 5 | //Here we are creating a collection - just with this one line, we can conduct db operations. It's one of the great examples of the power of Meteor. 6 | const Notes = new Mongo.Collection('notes') 7 | 8 | //By default a Mongo db collection does not impose any type of structure. I can insert a value into a field named 'foo' and if the field doesn't exist, Mongo will just create it. By using this package, Astronomy - http://jagi.github.io/meteor-astronomy/ - we can explicitly model our data. I highly recommend using this or some other modeling/schema package. 9 | export const Note = Class.create({ 10 | name: 'Note', 11 | collection: Notes, 12 | fields: { 13 | title: String, 14 | content: { 15 | type: String, 16 | default: '' 17 | }, 18 | updatedAt: Date 19 | } 20 | }) 21 | 22 | //Here, we are defining the db operations that are supported for the Notes collection. We can insert, update and delete a note. (We can also view notes - see publications) 23 | Meteor.methods({ 24 | 25 | //The naming of these methods are purely a convention, ie we could call this 'foo' if we wanted. However, it's key to have a good naming convention here, since Meteor.methods is not specific to Notes. In other words, if were to just call this the 'create' method, that could become a problem if we had another collection and also wanted a create method there as well. Therefore we prefix it with the collection name to ensure uniqueness. 26 | 'note.create': title => { 27 | 28 | //This syntax is based on use of the Astronomy package. 29 | const note = new Note() 30 | 31 | //Here we are only passing in as little data as possible from the client, the title, and then creating everything else internally, in this case just the date value. This is a good practice, as you never want to trust the client. 32 | note.set({ 33 | title, 34 | updatedAt: new Date() 35 | }) 36 | note.save() 37 | return note 38 | } 39 | , 40 | 'note.update': note => { 41 | note.set({ 42 | updatedAt: new Date() 43 | }) 44 | note.save() 45 | return note 46 | } 47 | , 48 | 'note.delete': id => Note.remove(id) 49 | }) 50 | 51 | //NEXT: Head back to /imports/components/containers/homepage_container.js -------------------------------------------------------------------------------- /app/imports/collections/server/publications.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor' 2 | import { Note } from '../notes' 3 | 4 | //When publishing data to the server, you should always be specific about the fields you are publishing, to prevent unintentionally publishing, say, an internal id. Here is an example of creating a field definition for each publication. '1' (ie 'true') means that the field will be published. 5 | const 6 | notesListFields = { 7 | title: 1, 8 | updatedAt: 1 9 | }, 10 | noteDetailsFields = { 11 | title: 1, 12 | content: 1 13 | } 14 | 15 | //Here, we are using the same naming convention as with Meteor.methods. See /imports/collections/notes.js 16 | Meteor.publish('notes.list', function() { 17 | 18 | //Here we are using the 'tmeasday:publish-counts' package to reactively get the number of notes published. This allows us to display a message if there are no notes. 19 | Counts.publish(this, 'note_count', Note.find(), { noReady: true }) 20 | 21 | //By including a 'field' parameter in our query, the data that is published will include only the fields listed. Without it, all fields would be published, which is basically a security risk. 22 | return Note.find({}, { fields: notesListFields }) 23 | }) 24 | 25 | 26 | //Here, we are technically only publishing one document. However, you can't use findOne() to do that. You must always publish a cursor, which is why use find() Learn more about cursors: https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/ 27 | Meteor.publish('note.details', function(id) { 28 | return Note.find({ _id: id }, { fields: noteDetailsFields }) 29 | }) -------------------------------------------------------------------------------- /app/imports/components/accounts/login_buttons.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Blaze } from 'meteor/blaze' 4 | 5 | //Here we are using Meteor's out-of-the box login ui. As it happens, this ui is created using Blaze, so this is an example of how to integrate that with React. 6 | export class LoginButtons extends React.Component{ 7 | 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | componentDidMount() { 13 | // Use Meteor Blaze to render login buttons 14 | this.view = Blaze.renderWithData(Template.loginButtons, {align: this.props.align}, 15 | ReactDOM.findDOMNode(this.refs.container)); 16 | } 17 | 18 | componentWillUnmount(){ 19 | Blaze.remove(this.view); 20 | } 21 | 22 | render() { 23 | // Just render a placeholder container that will be filled in 24 | return ; 25 | } 26 | } 27 | 28 | LoginButtons.propTypes = { 29 | align: React.PropTypes.string 30 | } 31 | 32 | LoginButtons.defaultProps = { 33 | align: "left" 34 | } -------------------------------------------------------------------------------- /app/imports/components/buttons/delete_btn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconBtn } from './icon_btn' 3 | 4 | export const DeleteBtn = props => { 5 | 6 | const handleDelete = (item, msg, handler) => { 7 | const confirmDelete = confirm(msg) 8 | if (confirmDelete) { handler } 9 | } 10 | 11 | return handleDelete(props.itemToDelete, props.confirmMsg, props.handleDelete(props.itemToDelete))} 12 | {...props} 13 | /> 14 | } 15 | 16 | DeleteBtn.propTypes = { 17 | itemToDelete: React.PropTypes.object.isRequired, 18 | handleDelete: React.PropTypes.func.isRequired, 19 | confirmMsg: React.PropTypes.string, 20 | } 21 | 22 | DeleteBtn.defaultProps = { 23 | icon: "delete", 24 | title: "Delete...", 25 | confirmMsg: "Really delete this?" 26 | } -------------------------------------------------------------------------------- /app/imports/components/buttons/icon_btn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | 4 | export const IconBtn = props => 5 | 13 | 14 | IconBtn.propTypes = { 15 | handleClick: React.PropTypes.func.isRequired, 16 | icon: React.PropTypes.string.isRequired, 17 | title: React.PropTypes.string, 18 | size: React.PropTypes.string 19 | } -------------------------------------------------------------------------------- /app/imports/components/content/content_block.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactMarkdown from 'react-markdown' 3 | 4 | export const ContentBlock = props => { 5 | 6 | const 7 | emptyMsg =
{props.emptyMsg}
8 | , 9 | handleMarkDown = (useMarkdown, content) => 10 | useMarkdown? 11 |
12 | : 13 |
{content}
14 | 15 | return props.contentValue === ""? 16 | emptyMsg 17 | : 18 | handleMarkDown(props.useMarkdown, props.contentValue) 19 | 20 | } 21 | 22 | ContentBlock.propTypes = { 23 | contentValue: React.PropTypes.string.isRequired, 24 | useMarkdown: React.PropTypes.bool, 25 | emptyMsg: React.PropTypes.string 26 | } 27 | 28 | ContentBlock.defaultProps = { 29 | useMarkdown: false, 30 | emptyMsg: "Empty item." 31 | } -------------------------------------------------------------------------------- /app/imports/components/content/page_title.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const PageTitle = props =>

{props.title}

4 | 5 | PageTitle.propTypes = { 6 | title: React.PropTypes.string.isRequired 7 | } -------------------------------------------------------------------------------- /app/imports/components/forms/auto_save_input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import debounce from 'lodash.debounce' 3 | 4 | export class AutoSaveInput extends React.Component { 5 | 6 | constructor(props){ 7 | super(props) 8 | this.state = { 9 | contentValue: this.props.contentValue 10 | } 11 | } 12 | 13 | handleSubmit(e) { 14 | e.preventDefault() 15 | this.props.doneEditing() 16 | } 17 | 18 | handleUpdates(updatedValue){ 19 | 20 | const 21 | updateInterval = 250, 22 | options = { 'maxWait': 2000 }, 23 | autoSaveChanges = debounce(updatedValue => 24 | this.props.handleUpdates(this.props.note, this.props.field, updatedValue), 25 | updateInterval, 26 | options 27 | ) 28 | 29 | autoSaveChanges(updatedValue) 30 | } 31 | 32 | handleOnChange(e) { 33 | const updatedValue = e.target.value 34 | this.setState({contentValue: updatedValue}) 35 | this.handleUpdates(updatedValue) 36 | } 37 | 38 | handleOnBlur() { 39 | this.props.doneEditing? this.props.doneEditing() : null 40 | } 41 | 42 | displayEditor(multiLineEditor){ 43 | return multiLineEditor? 44 |
45 |