├── .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 | 
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 =
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 |
59 | :
60 |
70 | }
71 |
72 | render() {
73 | return this.displayEditor(this.props.multiline)
74 | }
75 | }
76 |
77 | AutoSaveInput.propTypes = {
78 | handleUpdates: React.PropTypes.func.isRequired,
79 | field: React.PropTypes.string.isRequired,
80 | contentValue: React.PropTypes.string,
81 | placeholder: React.PropTypes.string,
82 | multiline: React.PropTypes.bool
83 | }
84 |
85 | AutoSaveInput.defaultProps = {
86 | contentValue: "" ,
87 | placeholder: "Write something...",
88 | multiline: false
89 | }
--------------------------------------------------------------------------------
/app/imports/components/forms/click_to_edit.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { AutoSaveInput } from './auto_save_input'
3 |
4 | export class ClickToEdit extends React.Component {
5 |
6 | constructor(props){
7 | super(props)
8 | this.state = {
9 | editMode: this.props.editMode
10 | }
11 | }
12 |
13 | toggleEditMode(){
14 | this.setState({ editMode: !this.state.editMode })
15 | }
16 |
17 | render() {
18 |
19 | return this.state.editMode?
20 |
21 | :
22 | {this.props.component}
23 | }
24 | }
25 |
26 | ClickToEdit.propTypes = {
27 | component: React.PropTypes.object.isRequired,
28 | editMode: React.PropTypes.bool
29 | }
30 |
31 | ClickToEdit.defaultProps = {
32 | editMode: false
33 | }
34 |
--------------------------------------------------------------------------------
/app/imports/components/forms/single_field_submit.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | //This is an example of an actual React Component, in that we are extending the Component method of React
4 | export class SingleFieldSubmit extends React.Component {
5 |
6 | //This is where we instantiate the component and can set any default values. Additionally we are passing in props from the parent component via constructor(props) and then passing those props into the Component itself using super(props)
7 | constructor(props){
8 | super(props)
9 |
10 | //here we are setting a default state for 'inputValue'
11 | //In React, state is private and only accessible from within a component
12 | this.state = {
13 | inputValue: this.props.inputValue
14 | }
15 | }
16 |
17 | //When called, this function passes in the 'e' DOM event object and gets the value that was entered via target.value and then updates inputValue state with that value.
18 | //In React, changing a field value is considered a change in component state.
19 | updateInputValue(e){
20 | this.setState({inputValue: e.target.value})
21 | }
22 |
23 | handleSubmit(e) {
24 | e.preventDefault()
25 | this.props.handleSubmit(this.state.inputValue)
26 | this.setState({inputValue: ""})
27 | }
28 |
29 | //Here, we see several event handlers provided by React, such as onSubmit. Additionally, note the use of 'className' which will translate to 'class' in the HTML. Even though the syntax below looks like HTML, it is in fact still JavaScript, where 'class' is a reserved word. (In fact, we use it above to instantiate this component)
30 | render() {
31 | return
39 | }
40 | }
41 | SingleFieldSubmit.propTypes = {
42 | handleSubmit: React.PropTypes.func.isRequired,
43 | placeholder: React.PropTypes.string
44 | }
45 |
46 | SingleFieldSubmit.defaultProps = {
47 | inputValue: "" ,
48 | placeholder: "New..."
49 | }
50 |
51 | //YOUR TURN: This is as far as a I got with the inline comments. Keep checking out the code and then try building your own app. Questions,comments, feedback? Ping me at @codechron
--------------------------------------------------------------------------------
/app/imports/components/layouts/app_header.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import classNames from 'classnames'
3 |
4 | export const AppHeader = props => {
5 |
6 | const
7 | hasLeftCol = props.leftCol? "helper-left-padding" : null
8 | ,
9 | middleColStyling = classNames("flex-main-content", hasLeftCol)
10 |
11 | return
12 |
{props.leftCol}
13 |
{props.middleCol}
14 |
{props.rightCol}
15 |
16 | }
17 |
18 | AppHeader.propTypes = {
19 | leftCol: React.PropTypes.object,
20 | middleCol: React.PropTypes.object,
21 | rightCol: React.PropTypes.object
22 | }
23 |
--------------------------------------------------------------------------------
/app/imports/components/lists/list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { FlowRouter } from 'meteor/kadira:flow-router'
3 | import { DeleteBtn } from '../buttons/delete_btn'
4 |
5 | //This is a somewhat more advanced but still 'stateless' component
6 | export const List = props => {
7 |
8 | //Here is an example of having a collection of optional component features
9 | //Each feature can be turned on by setting its prop to true (see 'List.propTypes' below.) We are "turning on" these features in the NotesListContainer.
10 | //Doing so, will call the function corresponding to the feature name, pass in any props needed, and return the corresponding content block.
11 | const listFeatures = {
12 | itemTitle: item => {item.title},
13 | linkItem: (item, linkRoute) => {item.title},
14 | deleteBtn: (item, handleDelete) =>
15 | }
16 |
17 | //This is where we generate our list, using the map() method, which returns an array that will be assigned to 'displayList' Here, we are using the ternary operator to check if a given optional feature, eg 'linkItem' has been turned on. See the top-level container component, in which turn these features on by returning true for the corresponding props.
18 | const
19 | displayList = props.collection.map((item) =>
20 |
30 |
31 | }
32 |
33 | //This is where we set the props which this component supports. One can think of this as a mini api for this component. A developer can quickly look at this and see how to interface with the compoenent.
34 | List.propTypes = {
35 | collection: React.PropTypes.array.isRequired,
36 | addItem: React.PropTypes.object,
37 | linkItem: React.PropTypes.bool,
38 | deleteItem: React.PropTypes.bool,
39 | linkRoute: React.PropTypes.string
40 | }
41 |
42 | //This is where we set default values for props. Props for which a default value is not set will default to null. However, for any props that are not required you shuold set a default value.
43 | List.defaultProps = {
44 | addItem: null,
45 | linkItem: false,
46 | linkRoute: null,
47 | deleteItem: false
48 | }
49 |
50 | //NEXT: head over to /imports/components/forms/single_field_submit.jsx
--------------------------------------------------------------------------------
/app/imports/components/pages/homepage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { AppHeader } from '../layouts/app_header'
3 | import { PageTitle } from '../content/page_title'
4 | import { NotesListContainer } from '/imports/containers/notes_list_container'
5 |
6 | //This is an example of a basic React component. Yes, it is actually just a normal (ES6) JS function. However, React is rewriting the parts that look like HTML using React.createElement() It is recommended that you write most of your components like this, and only use React.Component in cases where your component has state. (You should try to keep your components 'dumb' so only a minority of your components should have state.)
7 | export const Homepage = props => {
8 |
9 | //Here we are instantiating a PageTitle component and passing in a value for the 'title' prop. I recommend heading over to /content/page_title to see how this corresponds to the component itself.
10 | const pageTitle =
11 |
12 | //This what this component actually renders (or 'returns') in the UI
13 | //Here we are passing in the component into the AppHeader. Below that, we are rendering the
14 | //NEXT: Head over to the /imports/containers/notes_list_container.jsx
15 | return
16 |
17 |
18 |
19 |
20 |
21 | }
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/imports/components/pages/note_details_page.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { AppHeader } from '../layouts/app_header'
3 | import { PageTitle } from '../content/page_title'
4 | import { ClickToEdit } from '../forms/click_to_edit'
5 | import { AutoSaveInput } from '../forms/auto_save_input'
6 | import { ContentBlock } from '../content/content_block'
7 | import { IconBtn } from '../buttons/icon_btn'
8 | import { LoadingWrapper } from '../utility/loading_wrapper'
9 |
10 |
11 | //This component is a somewhat more advanced version of the homepage component
12 | export const NoteDetailsPage = props => {
13 |
14 | //Here, we are preparing all the components and data we will need to display on this page.
15 | //Note the use of 'props.subReady?' for the noteTitle and noteContent. We do this check to prevent these values from returning undefined. I'll be honest - I don't like the way I'm doing this check here - it just feels hacky, so I recommend doing your own research on how best to handle definining content values that are subscription-based.
16 | const
17 | backBtn = history.back()} />
18 | ,
19 | noteTitleValue = props.subReady? props.note.title : ""
20 | ,
21 | noteContentValue = props.subReady? props.note.content : ""
22 | ,
23 | pageTitle =
24 | ,
25 | pageContent =
26 | ,
27 | editableNoteTitle =
34 | ,
35 | editableNoteContent =
43 |
44 | return
45 |
50 |
51 |
52 |
53 |
54 | }
55 |
56 | //NEXT: head over to /imports/components/lists/list.jsx
--------------------------------------------------------------------------------
/app/imports/components/utility/loading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const Loading = () =>
Loading...
--------------------------------------------------------------------------------
/app/imports/components/utility/loading_wrapper.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Loading } from './loading'
3 |
4 |
5 | //this is a very simple yet useful component. It accepts a loading animation and a component and a subsReady boolean. The loading animation displays until subscriptions are ready. That's it.
6 | //If you wanted to make this component a little more fancy, you could support different animations, such as a tiny animation for smaller elements.
7 | export const LoadingWrapper = props => props.subReady? props.component :
8 |
9 | LoadingWrapper.propTypes = {
10 | component: React.PropTypes.object.isRequired,
11 | subReady: React.PropTypes.bool.isRequired
12 | }
13 |
14 | //NEXT: head over to /imports/components/pages/note_details_page.jsx
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/imports/containers/app_container.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createContainer } from 'meteor/react-meteor-data'
3 | import { LoginButtons } from '/imports/components/accounts/login_buttons'
4 |
5 | //This is a container or "data wrapper", where we connect Meteor's real-time reactive data sources with our React components.
6 | //In this case, we are passing it into a minimal "App" component, that simply passes along any props we pass into it. The props that are passed in are those that are returned from this container. The only reason this is created is because we need to accomodate the structure imposed by FlowRouter, which is one targeted more towards a template-based paradigm (ie one where you have static layouts with dynamic regions) rather than a component-based paradigm, which is what we are using in the form of React, in which you have a single top-down hierarchy.
7 |
8 | const App = props => props.page(props)
9 |
10 | //To clarify, this is the data container. The second argument of createContainer() accepts the component into which data is passed.
11 |
12 | export const AppContainer = createContainer(() => {
13 |
14 | //Here we providing a 'currentUser' prop that will update reactively depending on if a user is signed in. We are also defining and returning a login ui that can be used in child components.
15 | return {
16 | currentUser: Meteor.user(),
17 | loginButtons:
18 | }
19 | },
20 | App)
21 |
22 | //NEXT: Head back to the routes page.
--------------------------------------------------------------------------------
/app/imports/containers/note_details_container.js:
--------------------------------------------------------------------------------
1 | import { createContainer } from 'meteor/react-meteor-data'
2 | import { Meteor } from 'meteor/meteor'
3 | import { Note } from '/imports/collections/notes'
4 | import { NoteDetailsPage } from '../components/pages/note_details_page'
5 |
6 | export const NoteDetailsContainer = createContainer(props => {
7 |
8 | const
9 | sub = Meteor.subscribe('note.details', props.noteId)
10 | ,
11 | subReady = sub.ready()
12 | ,
13 | note = subReady? Note.findOne({ _id: props.noteId }) : {}
14 | ,
15 | handleUpdateNote = (collection, field, value) => {
16 | const doc = {}
17 | doc[field] = value
18 | collection.set(doc)
19 |
20 | Meteor.call('note.update', collection, (err, result) => {
21 | if (err) {
22 | console.log('error: ' + err.reason)
23 | }
24 | })
25 | }
26 |
27 | return {
28 | note,
29 | handleUpdateNote,
30 | subReady
31 | }
32 | },
33 | NoteDetailsPage
34 | )
--------------------------------------------------------------------------------
/app/imports/containers/notes_list_container.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createContainer } from 'meteor/react-meteor-data'
3 | import { Count } from 'meteor/tmeasday:publish-counts'
4 |
5 | //Here, we are importing our MongoDb collection and a data schema. Head over there to check that out and then come back here: /imports/collections/note.js and then come back here.
6 | import { Note } from '/imports/collections/notes'
7 |
8 | //Here, we are importing components we will be using inside the container.
9 | import { SingleFieldSubmit } from '/imports/components/forms/single_field_submit'
10 | import { List } from '/imports/components/lists/list'
11 | import { LoadingWrapper } from '/imports/components/utility/loading_wrapper'
12 |
13 | export const NotesListContainer = createContainer(props => {
14 |
15 | const
16 | //Here, we are creating a subcription to the 'notes.list' publication. See /imports/collections/server/publications.js - the 'server' directory is significant. It is one of Meteor's special directories, and by naming that way we ensure that code inside that directory will only run on the server.
17 | sub = Meteor.subscribe('notes.list')
18 | ,
19 | //Here we are defining a boolean for checking if our subscription is ready (ie we have connected to the server publication and are receiving our real-time subscription stream)
20 | subReady = sub.ready()
21 | ,
22 | //Here, we are defining our collection based on if the above subscription is ready or not. By ready, we mean that a connection has been established to the reactive data source. Until it is ready, we are simply returning an empty array. The reason we need to append the fetch() method to the end is because we want array and not a cursor. Fetch() handles that for us.
23 | notes = subReady? Note.find({}, { sort: { updatedAt: -1} }).fetch() : []
24 | ,
25 |
26 | notesCount = subReady? Counts.get('note_count') : null
27 | ,
28 | //Here, we are handling db operations relating to notes. This is an interface to a Meteor db operation. This file will run both on the client and the server side, which allows for the db operation to run 'optimistically' on the client side, but is retracted if it fails on the server side. What this means, in terms of user experience, is that when, for example, you enter some data, you see the update on your screen immediately, because the app optimistically assumes your change will be approved on the server side. In the unlikely event that you are doing something unapproved, the change would be reverted.
29 | handleCreate = title => {
30 | Meteor.call('note.create', title, (err, result) => {
31 | if (err) {
32 | console.log('error: ' + err.reason)
33 | }
34 | })
35 | }
36 | ,
37 | handleDelete = note => {
38 | Meteor.call('note.delete', note._id, (err, result) => {
39 | if (err) {
40 | console.log('error: ' + err.reason)
41 | }
42 | })
43 | }
44 | ,
45 | //Here we are setting props to be used in the List component below depending on if a user is signed in or not, ie if currentUser is truthy or not.
46 | addItemPlaceholder = notesCount === 0? "Type something and hit return to add a note..." : "New Note..."
47 | , addItem = props.currentUser? : null
48 | , deleteItem = props.currentUser? true : false
49 |
50 | , linkItem = props.currentUser? true : false
51 |
52 | , displayEmptyListMsg = () => {
53 | const
54 | msg = "Please sign in or register to add notes."
55 | , msgBlock =
{msg}
56 | , displayMsg = notesCount === 0 && props.currentUser === null
57 |
58 | return displayMsg? msgBlock : null
59 | }
60 | , list =
61 |
62 |
63 | //This is where we return, or make available data to child components. In this case, we are returning a list component with all the props set.
64 | //The single token object syntax (eg 'subReady') is a feature of ES6, and is shorthand for 'token:token' eg 'subReady:subReady'
65 | return {
66 | component: list,
67 | subReady
68 | }
69 | },
70 | //We are wrapping this container around a "loading wrapper" which will display a loading animation until data is ready to be displayed, and will then display the component we are returning.
71 | LoadingWrapper
72 | )
73 |
74 | //NEXT: Header over to the /imports/components/utility/loading_wrapper.jsx
75 |
--------------------------------------------------------------------------------
/app/imports/startup/client/index.js:
--------------------------------------------------------------------------------
1 | // This is what one can think of as an 'import manifest file'. It lists all the files we will want to import on startup to the client side. (Then, we import the files by importing *this* file - see /client/main.js)
2 | // The order of the imports matters. Here,for example, we are first loading our stylesheets. Those stylesheets, in turn, will be available to the routes (or pages) that we load next. In both the imports below, we are pointing to files and loading the entire file. It is not necessary to include the file name extension (ie. these files are actually named main.scss and routes.jsx), unless there are multiple files of the same name in the same directory.
3 |
4 | import '/imports/stylesheets/main'
5 | import './routes'
6 |
7 | //NEXT: Continue to /imports/stylesheets/main.scss
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/imports/startup/client/routes.jsx:
--------------------------------------------------------------------------------
1 | //Here we see examples of importing packages. By doing this, we gain access to some of the package's functionality in this file, by way of the token inside the curly braces. (Eg. we can then write things like FlowRouter.route...)
2 | //To import Meteor packages, we need to use the 'pseudo-global' prefix 'meteor/'
3 | import { FlowRouter } from 'meteor/kadira:flow-router'
4 |
5 | //Here we are importing an npm package. For those, there is no need for a prefix. The lack of curly braces tells us we are importing the default module exported from the 'react' package.
6 | import React from 'react'
7 | import { mount } from 'react-mounter'
8 |
9 | //Here we are importing 'modules' exported from other files. It may seem like a lot of work to have to import everything, but the benefits are significant. A HUGE advantage is that anyone can now look at a file and trace the source of any function, object, etc., which is great both for debugging and for people who are new to a project.
10 |
11 | //Here are are importing a reactive data container. Head over to /imports/containers/app_container.jsx to learn more
12 | import { AppContainer } from '/imports/containers/app_container'
13 | import { NoteDetailsContainer } from '/imports/containers/note_details_container'
14 | import { Homepage } from '/imports/components/pages/homepage'
15 |
16 | //Here we are defining the 'root' route, or the default view of the app.
17 | FlowRouter.route('/', {
18 | name: 'homepage',
19 | action() {
20 |
21 | //Inside the action() method, we tell FlowRouter what it should do when users access this route. Because we are using React, we need to use the 'mount' method (imported above from 'react-mounter') to render React components. The mount methods takes two parameters: a 'layout' and a collection of 'regions'. For our 'layout' we are using the "pass-through" App we just created above.
22 | mount(AppContainer, {
23 |
24 | //Here we are defining the region and its associated content for this route. We are naming the region 'page' since we want everything to be enclosed in a single component. The component that is being rendered is the "Homepage" component. The 'props => ' arrow function allows us to pass in attributes from a parent into this component. Notice the use of "...props" which effectively passes along all props that are passed to it into the component. It is the '...' part that does the magic here. To learn more, read about ES6 spread operators: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator
25 |
26 | page: props =>
27 | //The above part might look funny. This is JSX syntax and allows us to write HTML-like syntax. The react package we imported translates this into JavaScript.
28 | })
29 |
30 | //NEXT: head over to the homepage component: /imports/components/pages/homepage.jsx
31 | }
32 | })
33 |
34 | FlowRouter.route('/notes/:_id', {
35 | name: 'noteDetails',
36 | action(params) {
37 | mount(AppContainer, {
38 | page: props =>
39 | })
40 | }
41 | })
--------------------------------------------------------------------------------
/app/imports/startup/server/index.js:
--------------------------------------------------------------------------------
1 | import { Note } from '/imports/collections/notes'
2 | import '/imports/collections/server/publications'
--------------------------------------------------------------------------------
/app/imports/stylesheets/components/_buttons.scss:
--------------------------------------------------------------------------------
1 | .btn-main {
2 | width: 100%;
3 | max-width: $btn-main-max-width;
4 | }
5 |
6 | .icon-btn{
7 | background-color: transparent;
8 | border:none;
9 |
10 | margin:0;
11 | padding: 0;
12 | text-align: center;
13 |
14 | &:focus {
15 | outline: 0;
16 | }
17 | &.btn-small {
18 | .material-icons {
19 | font-size: $btn-small;
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/imports/stylesheets/components/_forms.scss:
--------------------------------------------------------------------------------
1 | form.c-single-field-submit {
2 | width: 100%;
3 | height: 100%;
4 |
5 | input {
6 | width: 100%;
7 | margin-left: 0;
8 | margin-right:0;
9 | line-height: 1.75;
10 | padding-left: .25em;
11 | padding-right: .25em;
12 | }
13 | }
14 |
15 | .c-content-editor {
16 | padding:0;
17 |
18 | textarea {
19 | padding:0;
20 | }
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/app/imports/stylesheets/components/_lists.scss:
--------------------------------------------------------------------------------
1 | .list {
2 | list-style-type: none;
3 | padding-left: 0;
4 |
5 | & > li {
6 |
7 | @include flex-row;
8 |
9 | padding-top: $list-item-padding;
10 | padding-bottom: $list-item-padding;
11 |
12 | &:first-child {
13 | padding-top:0;
14 | }
15 |
16 | &:last-child {
17 | border-bottom: none;
18 | padding-bottom:0;
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/app/imports/stylesheets/core/_base.scss:
--------------------------------------------------------------------------------
1 | // Apply a natural box layout model to all elements, but allowing components to change - http://www.paulirish.com/2012/box-sizing-border-box-ftw/
2 | html,body {
3 | box-sizing: border-box;
4 | }
5 | *, *:before, *:after { box-sizing: inherit; }
--------------------------------------------------------------------------------
/app/imports/stylesheets/core/_helpers.scss:
--------------------------------------------------------------------------------
1 | //Giving our classname this 'helper-' prefix makes it easier trace the class to this location for debugging purposes.
2 |
3 | .helper-centered {
4 | text-align: center;
5 | }
6 | .helper-full-width {
7 | width: 100%;
8 | }
9 | .helper-left-padding {
10 | padding-left: 1em;
11 | }
12 | .helper-top-padding {
13 | padding-top: 1em;
14 | }
15 |
16 | .helper-bottom-spacing {
17 | margin-bottom: 1em;
18 | }
19 | .helper-clickable {
20 | &:hover {
21 | cursor: pointer;
22 | }
23 | }
--------------------------------------------------------------------------------
/app/imports/stylesheets/core/_normalize.scss:
--------------------------------------------------------------------------------
1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /**
4 | * 1. Change the default font family in all browsers (opinionated).
5 | * 2. Prevent adjustments of font size after orientation changes in IE and iOS.
6 | */
7 |
8 | html {
9 | font-family: sans-serif; /* 1 */
10 | -ms-text-size-adjust: 100%; /* 2 */
11 | -webkit-text-size-adjust: 100%; /* 2 */
12 | }
13 |
14 | /**
15 | * Remove the margin in all browsers (opinionated).
16 | */
17 |
18 | body {
19 | margin: 0;
20 | }
21 |
22 | /* HTML5 display definitions
23 | ========================================================================== */
24 |
25 | /**
26 | * Add the correct display in IE 9-.
27 | * 1. Add the correct display in Edge, IE, and Firefox.
28 | * 2. Add the correct display in IE.
29 | */
30 |
31 | article,
32 | aside,
33 | details, /* 1 */
34 | figcaption,
35 | figure,
36 | footer,
37 | header,
38 | main, /* 2 */
39 | menu,
40 | nav,
41 | section,
42 | summary { /* 1 */
43 | display: block;
44 | }
45 |
46 | /**
47 | * Add the correct display in IE 9-.
48 | */
49 |
50 | audio,
51 | canvas,
52 | progress,
53 | video {
54 | display: inline-block;
55 | }
56 |
57 | /**
58 | * Add the correct display in iOS 4-7.
59 | */
60 |
61 | audio:not([controls]) {
62 | display: none;
63 | height: 0;
64 | }
65 |
66 | /**
67 | * Add the correct vertical alignment in Chrome, Firefox, and Opera.
68 | */
69 |
70 | progress {
71 | vertical-align: baseline;
72 | }
73 |
74 | /**
75 | * Add the correct display in IE 10-.
76 | * 1. Add the correct display in IE.
77 | */
78 |
79 | template, /* 1 */
80 | [hidden] {
81 | display: none;
82 | }
83 |
84 | /* Links
85 | ========================================================================== */
86 |
87 | /**
88 | * 1. Remove the gray background on active links in IE 10.
89 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
90 | */
91 |
92 | a {
93 | background-color: transparent; /* 1 */
94 | -webkit-text-decoration-skip: objects; /* 2 */
95 | }
96 |
97 | /**
98 | * Remove the outline on focused links when they are also active or hovered
99 | * in all browsers (opinionated).
100 | */
101 |
102 | a:active,
103 | a:hover {
104 | outline-width: 0;
105 | }
106 |
107 | /* Text-level semantics
108 | ========================================================================== */
109 |
110 | /**
111 | * 1. Remove the bottom border in Firefox 39-.
112 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
113 | */
114 |
115 | abbr[title] {
116 | border-bottom: none; /* 1 */
117 | text-decoration: underline; /* 2 */
118 | text-decoration: underline dotted; /* 2 */
119 | }
120 |
121 | /**
122 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6.
123 | */
124 |
125 | b,
126 | strong {
127 | font-weight: inherit;
128 | }
129 |
130 | /**
131 | * Add the correct font weight in Chrome, Edge, and Safari.
132 | */
133 |
134 | b,
135 | strong {
136 | font-weight: bolder;
137 | }
138 |
139 | /**
140 | * Add the correct font style in Android 4.3-.
141 | */
142 |
143 | dfn {
144 | font-style: italic;
145 | }
146 |
147 | /**
148 | * Correct the font size and margin on `h1` elements within `section` and
149 | * `article` contexts in Chrome, Firefox, and Safari.
150 | */
151 |
152 | h1 {
153 | font-size: 2em;
154 | margin: 0.67em 0;
155 | }
156 |
157 | /**
158 | * Add the correct background and color in IE 9-.
159 | */
160 |
161 | mark {
162 | background-color: #ff0;
163 | color: #000;
164 | }
165 |
166 | /**
167 | * Add the correct font size in all browsers.
168 | */
169 |
170 | small {
171 | font-size: 80%;
172 | }
173 |
174 | /**
175 | * Prevent `sub` and `sup` elements from affecting the line height in
176 | * all browsers.
177 | */
178 |
179 | sub,
180 | sup {
181 | font-size: 75%;
182 | line-height: 0;
183 | position: relative;
184 | vertical-align: baseline;
185 | }
186 |
187 | sub {
188 | bottom: -0.25em;
189 | }
190 |
191 | sup {
192 | top: -0.5em;
193 | }
194 |
195 | /* Embedded content
196 | ========================================================================== */
197 |
198 | /**
199 | * Remove the border on images inside links in IE 10-.
200 | */
201 |
202 | img {
203 | border-style: none;
204 | }
205 |
206 | /**
207 | * Hide the overflow in IE.
208 | */
209 |
210 | svg:not(:root) {
211 | overflow: hidden;
212 | }
213 |
214 | /* Grouping content
215 | ========================================================================== */
216 |
217 | /**
218 | * 1. Correct the inheritance and scaling of font size in all browsers.
219 | * 2. Correct the odd `em` font sizing in all browsers.
220 | */
221 |
222 | code,
223 | kbd,
224 | pre,
225 | samp {
226 | font-family: monospace, monospace; /* 1 */
227 | font-size: 1em; /* 2 */
228 | }
229 |
230 | /**
231 | * Add the correct margin in IE 8.
232 | */
233 |
234 | figure {
235 | margin: 1em 40px;
236 | }
237 |
238 | /**
239 | * 1. Add the correct box sizing in Firefox.
240 | * 2. Show the overflow in Edge and IE.
241 | */
242 |
243 | hr {
244 | box-sizing: content-box; /* 1 */
245 | height: 0; /* 1 */
246 | overflow: visible; /* 2 */
247 | }
248 |
249 | /* Forms
250 | ========================================================================== */
251 |
252 | /**
253 | * 1. Change font properties to `inherit` in all browsers (opinionated).
254 | * 2. Remove the margin in Firefox and Safari.
255 | */
256 |
257 | button,
258 | input,
259 | select,
260 | textarea {
261 | font: inherit; /* 1 */
262 | margin: 0; /* 2 */
263 | }
264 |
265 | /**
266 | * Restore the font weight unset by the previous rule.
267 | */
268 |
269 | optgroup {
270 | font-weight: bold;
271 | }
272 |
273 | /**
274 | * Show the overflow in IE.
275 | * 1. Show the overflow in Edge.
276 | */
277 |
278 | button,
279 | input { /* 1 */
280 | overflow: visible;
281 | }
282 |
283 | /**
284 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
285 | * 1. Remove the inheritance of text transform in Firefox.
286 | */
287 |
288 | button,
289 | select { /* 1 */
290 | text-transform: none;
291 | }
292 |
293 | /**
294 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
295 | * controls in Android 4.
296 | * 2. Correct the inability to style clickable types in iOS and Safari.
297 | */
298 |
299 | button,
300 | html [type="button"], /* 1 */
301 | [type="reset"],
302 | [type="submit"] {
303 | -webkit-appearance: button; /* 2 */
304 | }
305 |
306 | /**
307 | * Remove the inner border and padding in Firefox.
308 | */
309 |
310 | button::-moz-focus-inner,
311 | [type="button"]::-moz-focus-inner,
312 | [type="reset"]::-moz-focus-inner,
313 | [type="submit"]::-moz-focus-inner {
314 | border-style: none;
315 | padding: 0;
316 | }
317 |
318 | /**
319 | * Restore the focus styles unset by the previous rule.
320 | */
321 |
322 | button:-moz-focusring,
323 | [type="button"]:-moz-focusring,
324 | [type="reset"]:-moz-focusring,
325 | [type="submit"]:-moz-focusring {
326 | outline: 1px dotted ButtonText;
327 | }
328 |
329 | /**
330 | * Change the border, margin, and padding in all browsers (opinionated).
331 | */
332 |
333 | fieldset {
334 | border: 1px solid #c0c0c0;
335 | margin: 0 2px;
336 | padding: 0.35em 0.625em 0.75em;
337 | }
338 |
339 | /**
340 | * 1. Correct the text wrapping in Edge and IE.
341 | * 2. Correct the color inheritance from `fieldset` elements in IE.
342 | * 3. Remove the padding so developers are not caught out when they zero out
343 | * `fieldset` elements in all browsers.
344 | */
345 |
346 | legend {
347 | box-sizing: border-box; /* 1 */
348 | color: inherit; /* 2 */
349 | display: table; /* 1 */
350 | max-width: 100%; /* 1 */
351 | padding: 0; /* 3 */
352 | white-space: normal; /* 1 */
353 | }
354 |
355 | /**
356 | * Remove the default vertical scrollbar in IE.
357 | */
358 |
359 | textarea {
360 | overflow: auto;
361 | }
362 |
363 | /**
364 | * 1. Add the correct box sizing in IE 10-.
365 | * 2. Remove the padding in IE 10-.
366 | */
367 |
368 | [type="checkbox"],
369 | [type="radio"] {
370 | box-sizing: border-box; /* 1 */
371 | padding: 0; /* 2 */
372 | }
373 |
374 | /**
375 | * Correct the cursor style of increment and decrement buttons in Chrome.
376 | */
377 |
378 | [type="number"]::-webkit-inner-spin-button,
379 | [type="number"]::-webkit-outer-spin-button {
380 | height: auto;
381 | }
382 |
383 | /**
384 | * 1. Correct the odd appearance in Chrome and Safari.
385 | * 2. Correct the outline style in Safari.
386 | */
387 |
388 | [type="search"] {
389 | -webkit-appearance: textfield; /* 1 */
390 | outline-offset: -2px; /* 2 */
391 | }
392 |
393 | /**
394 | * Remove the inner padding and cancel buttons in Chrome and Safari on OS X.
395 | */
396 |
397 | [type="search"]::-webkit-search-cancel-button,
398 | [type="search"]::-webkit-search-decoration {
399 | -webkit-appearance: none;
400 | }
401 |
402 | /**
403 | * Correct the text style of placeholders in Chrome, Edge, and Safari.
404 | */
405 |
406 | ::-webkit-input-placeholder {
407 | color: inherit;
408 | opacity: 0.54;
409 | }
410 |
411 | /**
412 | * 1. Correct the inability to style clickable types in iOS and Safari.
413 | * 2. Change font properties to `inherit` in Safari.
414 | */
415 |
416 | ::-webkit-file-upload-button {
417 | -webkit-appearance: button; /* 1 */
418 | font: inherit; /* 2 */
419 | }
--------------------------------------------------------------------------------
/app/imports/stylesheets/core/_variables.scss:
--------------------------------------------------------------------------------
1 | //This is where all (or most) values that we define in our css is set. It basically works as a kind of config file, and some people do name this file '_config'
2 |
3 |
4 | // ******* LAYOUTS *******
5 |
6 | // App Container
7 | $app-container-max-width: 40em;
8 | $app-container-min-width: 15em;
9 | $app-container-left-right-padding: 2em;
10 |
11 | //App Header
12 | $app-header-height: 4em;
13 |
14 | //Main content
15 | $main-content-top-bottom-padding: 1em;
16 |
17 | // ******* COMPONENTS *******
18 |
19 | // Lists
20 | $list-item-padding: 1em;
21 | $li-margin-bottom: .35em;
22 |
23 | // Buttons
24 | $btn-main-max-width: 8em;
25 | $btn-dflt-size: 1.5em;
26 | $btn-small: 1em;
27 |
28 |
29 | // ******* COLORS *******
30 |
31 | /* Palette: materialpalette.com/indigo/light-blue */
32 | $primary-color-dark: #303F9F;
33 | $primary-color: #3F51B5;
34 | $primary-color-light: #C5CAE9;
35 | $primary-color-text: #FFFFFF;
36 | $accent-color: #03A9F4;
37 | $primary-text-color: #212121;
38 | $secondary-text-color: #727272;
39 | $divider-color: #B6B6B6;
40 |
41 | //Material UI Grayscale: http://www.materialui.co/colors
42 | $color-gray-10: #FAFAFA; //lightest
43 | $color-gray-9: #F5F5F5;
44 | $color-gray-8: #EEEEEE;
45 | $color-gray-7: #E0E0E0;
46 | $color-gray-6: #BDBDBD;
47 | $color-gray-5: #9E9E9E;
48 | $color-gray-4: #757575;
49 | $color-gray-3: #616161;
50 | $color-gray-2: #424242;
51 | $color-gray-1: #212121; //darkest
52 |
53 | // Base colors
54 | $base-bg-color: $color-gray-9;
55 | $base-font-color: $color-gray-1;
56 | $app-container-bg-color: #FFFFFF;
57 | $link-color: $primary-color;
58 | $divider-line: 1px solid $color-gray-7;
59 | $icon-color: $secondary-text-color;
60 | $icon-color-inverted: $color-gray-10;
61 |
62 | // ******* TEXT *******
63 |
64 | $base-font: "proxima-nova", 'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif;
65 | $base-font-size: 1.2em;
66 | $heading-1: 1.25em;
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/app/imports/stylesheets/layouts/_app_container.scss:
--------------------------------------------------------------------------------
1 | #app-container {
2 | max-width: $app-container-max-width;
3 | min-width: $app-container-min-width;
4 | margin: 0 auto;
5 | }
6 |
--------------------------------------------------------------------------------
/app/imports/stylesheets/layouts/_app_header.scss:
--------------------------------------------------------------------------------
1 | #app-header {
2 | min-height: $app-header-height;
3 | padding-left:$app-container-left-right-padding;
4 | padding-right:$app-container-left-right-padding;
5 | }
--------------------------------------------------------------------------------
/app/imports/stylesheets/layouts/_flex_layouts.scss:
--------------------------------------------------------------------------------
1 | // ******* FLEX MAIN CONTENT *******
2 | .flex-main-content {
3 | flex: 1;
4 | }
5 |
6 | //Here, we are first creating a 'mixin' or snippet of css that we can 'mix in' with other css. Then, we also define the mixin as a css class, so that we can use it as a stand-alone.
7 | // ******* FLEX COLUMN *******
8 | @mixin flex-column {
9 | display: flex;
10 | flex-flow: column;
11 | }
12 |
13 | .flex-column {
14 | @include flex-column;
15 | }
16 |
17 |
18 | // ******* FLEX ROW *******
19 | @mixin flex-row {
20 | display: flex;
21 | flex-flow: row nowrap;
22 |
23 | &.flex-centered {
24 | justify-content: center;
25 | }
26 | &.flex-vertical-middle {
27 | align-items:center;
28 | }
29 | }
30 |
31 | .flex-row {
32 | @include flex-row;
33 | }
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/imports/stylesheets/layouts/_full_height.scss:
--------------------------------------------------------------------------------
1 | //This is the basis for enabling an app layout that stretches the full height of the screen or viewport. To learn more, check out this article: http://coderchronicles.org/2016/04/12/create-a-full-screen-layout-for-mobile-web-apps/
2 |
3 | // ******* FULL HEIGHT VIEWPORT *******
4 | html {
5 | height: 100%; //While this is base styling, we are including it in the layout stylesheet because it is needed for a full height layout
6 | }
7 | body {
8 | min-height: 100%; // min-height is needed for pages that might scroll, ie they may contain _more_ than 100% of viewport height
9 | }
10 | #app-container {
11 | min-height: 100vh; //set the app container to fill the viewport height and exceed it if the content requires it
12 | @include flex-column;
13 | }
14 | #main-content {
15 | flex: 1;
16 | @include flex-column;
17 | }
18 |
--------------------------------------------------------------------------------
/app/imports/stylesheets/layouts/_main_content.scss:
--------------------------------------------------------------------------------
1 | #main-content {
2 | padding: $main-content-top-bottom-padding $app-container-left-right-padding;
3 | }
--------------------------------------------------------------------------------
/app/imports/stylesheets/main.scss:
--------------------------------------------------------------------------------
1 | //This is an example of a mini css framework. It's created from scratch, rather than using something like Bootstrap. One of the biggest mistakes you can make when learning to code is to rely on CSS frameworks. If you do that, you're not going to learn how to write your own css and how css actually works.
2 |
3 | // I think this is a pretty decent css structure. It is loosely based on the 'smaccs' structure - learn more at https://smacss.com/
4 |
5 | //Here, we have one 'master' stylesheet and then import other 'partial' stylesheets into this one. The order in which we import matters.
6 |
7 | // This css uses Sass, which is a CSS pre-processor. Learn more at http://sass-lang.com/
8 | // To use it, you'll need to install a Sass package. I recommend this one, which also gives you support for autoprefixing: https://github.com/fourseven/meteor-scss/
9 |
10 | // ******* CORE *******
11 | @import "core/normalize"; // "makes browsers render all elements more consistently and in line with modern standards" - https://necolas.github.io/normalize.css/
12 | @import "core/base";
13 | @import "core/variables";
14 | @import "core/helpers";
15 |
16 | // ******* LAYOUTS *******
17 | @import "layouts/flex_layouts";
18 | @import "layouts/full_height";
19 | @import "layouts/app_container";
20 | @import "layouts/app_header";
21 | @import "layouts/main_content";
22 |
23 | // ******* COMPONENTS *******
24 | @import "components/buttons";
25 | @import "components/forms";
26 | @import "components/lists";
27 |
28 | // ******* THEME *******
29 | @import "theme/text";
30 | @import "theme/colors";
31 |
32 | // ******* VENDOR (3rd party styling) *******
33 | @import "vendor/loader";
34 |
35 |
36 | //NEXT: Take a minute to browse through some of the css files, then let's look at our routes file, where we define the pages and URLs in our app. Go to /imports/startup/client/routes.jsx
--------------------------------------------------------------------------------
/app/imports/stylesheets/theme/_colors.scss:
--------------------------------------------------------------------------------
1 | // ******* GLOBAL *******
2 |
3 | body {
4 | background-color: $base-bg-color;
5 | color: $base-font-color;
6 | }
7 |
8 | #app-container {
9 | background-color: $app-container-bg-color;
10 | }
11 |
12 |
13 | // ******* TEXT *******
14 | body {
15 | color: $primary-text-color;
16 | }
17 | .help-text {
18 | color: $secondary-text-color;
19 | }
20 |
21 |
22 | // ******* LINKS *******
23 | a {
24 | color: $link-color;
25 |
26 | &:link, &:visited, &:active {
27 | color: $link-color;
28 | }
29 | }
30 |
31 |
32 | // ******* LISTS *******
33 |
34 | .list {
35 | & > li {
36 | border-bottom: $divider-line;
37 | }
38 | }
39 |
40 | // ******* FORMS *******
41 | .color-invisible-textarea {
42 | outline: none;
43 | resize: none;
44 | border:none;
45 | }
46 |
47 |
48 | // ******* BUTTONS *******
49 |
50 | .icon-btn {
51 | color: $icon-color;
52 |
53 | &:hover {
54 | color: darken($icon-color, 20%);
55 | }
56 | }
57 | .btn-main {
58 | background-color:$primary-color;
59 | color: $base-bg-color;
60 | }
61 | .color-inverted {
62 | background-color: $primary-color-dark ;
63 | color: $base-bg-color;
64 |
65 | .icon-btn {
66 | color: $icon-color-inverted;
67 |
68 | &:hover {
69 | color: darken($icon-color-inverted, 20%);
70 | }
71 | }
72 | }
73 |
74 | // ******* BACKGROUNDS/PILLS *******
75 |
76 | .clickable-pill {
77 | border-radius: .25em;
78 | background-color: $color-gray-8;
79 | padding: .5em;
80 | }
81 |
82 | // ******* BORDERS/DIVIDERS *******
83 |
84 | .color-top-divider {
85 | border-top: $divider-line;
86 | }
87 |
88 |
89 | // ******* VENDOR *******
90 | #app-header #login-buttons {
91 | .login-link-text {
92 | color: $base-bg-color;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/app/imports/stylesheets/theme/_text.scss:
--------------------------------------------------------------------------------
1 | // ******* BASE *******
2 | body {
3 | font-size: $base-font-size;
4 | font-family: $base-font;
5 | font-weight: 300;
6 | }
7 |
8 | // ******* HEADINGS *******
9 | h1 {
10 | font-size: $heading-1;
11 | }
12 |
13 |
14 | // ******* LINKS *******
15 | a {
16 | text-decoration: none;
17 |
18 | &:hover {
19 | text-decoration: underline;
20 | }
21 | }
22 |
23 | // ******* BUTTONS/ICONS *******
24 |
25 | .material-icons {
26 | font-size: $btn-dflt-size;
27 | }
28 |
29 | .text-btn{
30 | font-size: $btn-dflt-size;
31 | }
32 |
33 | .icon-btn{
34 |
35 | &.btn-small {
36 | .material-icons {
37 | font-size: $btn-small;
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/app/imports/stylesheets/vendor/_loader.scss:
--------------------------------------------------------------------------------
1 | /*
2 | Src: http://projects.lukehaas.me/css-loaders/
3 | */
4 |
5 | .vendor-loader,
6 | .vendor-loader:before,
7 | .vendor-loader:after {
8 | background: #f0eff1;
9 | -webkit-animation: load1 1s infinite ease-in-out;
10 | animation: load1 1s infinite ease-in-out;
11 | width: 1em;
12 | height: 4em;
13 | }
14 | .vendor-loader:before,
15 | .vendor-loader:after {
16 | position: absolute;
17 | top: 0;
18 | content: '';
19 | }
20 | .vendor-loader:before {
21 | left: -1.5em;
22 | -webkit-animation-delay: -0.32s;
23 | animation-delay: -0.32s;
24 | }
25 | .vendor-loader {
26 | color: #f0eff1;
27 | text-indent: -9999em;
28 | margin: 88px auto;
29 | position: relative;
30 | font-size: 11px;
31 | -webkit-transform: translateZ(0);
32 | -ms-transform: translateZ(0);
33 | transform: translateZ(0);
34 | -webkit-animation-delay: -0.16s;
35 | animation-delay: -0.16s;
36 | }
37 | .vendor-loader:after {
38 | left: 1.5em;
39 | }
40 | @-webkit-keyframes load1 {
41 | 0%,
42 | 80%,
43 | 100% {
44 | box-shadow: 0 0;
45 | height: 4em;
46 | }
47 | 40% {
48 | box-shadow: 0 -2em;
49 | height: 5em;
50 | }
51 | }
52 | @keyframes load1 {
53 | 0%,
54 | 80%,
55 | 100% {
56 | box-shadow: 0 0;
57 | height: 4em;
58 | }
59 | 40% {
60 | box-shadow: 0 -2em;
61 | height: 5em;
62 | }
63 | }
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app",
3 | "private": true,
4 | "scripts": {
5 | "start": "meteor run"
6 | },
7 | "dependencies": {
8 | "classnames": "^2.2.5",
9 | "lodash.debounce": "^4.0.6",
10 | "meteor-node-stubs": "~0.2.0",
11 | "react": "^15.1.0",
12 | "react-addons-pure-render-mixin": "^15.1.0",
13 | "react-dom": "^15.1.0",
14 | "react-markdown": "^2.3.0",
15 | "react-mounter": "^1.2.0"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/server/main.js:
--------------------------------------------------------------------------------
1 | import '/imports/startup/server/'
--------------------------------------------------------------------------------