├── .babelrc ├── .eslintrc ├── .gitignore ├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── packages ├── platforms ├── release └── versions ├── .prettierrc ├── README.md ├── client ├── main.html └── main.js ├── imports ├── api │ ├── counters │ │ ├── counters.js │ │ ├── counters.tests.js │ │ ├── hooks.js │ │ ├── methods.js │ │ ├── methods.tests.js │ │ ├── publications.js │ │ └── publications.tests.js │ ├── remote │ │ ├── ddp.js │ │ └── users.js │ └── users │ │ ├── hooks.js │ │ ├── methods.js │ │ ├── methods.tests.js │ │ ├── publications.js │ │ ├── publications.tests.js │ │ ├── users.js │ │ └── users.tests.js ├── startup │ ├── both │ │ └── index.js │ ├── client │ │ ├── index.js │ │ └── styles │ │ │ ├── custom.scss │ │ │ └── main.scss │ └── server │ │ ├── accounts.js │ │ ├── browser-policy.js │ │ ├── fixtures.js │ │ ├── index.js │ │ └── register-api.js └── ui │ ├── components │ ├── Alert │ │ ├── Alert.js │ │ ├── Alert.scss │ │ └── index.js │ ├── Button │ │ ├── Button.js │ │ ├── Button.scss │ │ └── index.js │ ├── Modal │ │ ├── Modal.js │ │ ├── Modal.scss │ │ └── index.js │ ├── Navbar │ │ ├── Navbar.js │ │ ├── Navbar.scss │ │ └── index.js │ ├── Spinner │ │ ├── Spinner.js │ │ ├── Spinner.scss │ │ └── index.js │ └── Text │ │ ├── Text.js │ │ ├── Text.scss │ │ └── index.js │ ├── layouts │ └── App.js │ └── pages │ ├── Landing │ ├── Landing.js │ ├── Landing.scss │ └── index.js │ ├── Login │ ├── Login.js │ ├── Login.scss │ └── index.js │ ├── Not-Found │ ├── Not-Found.js │ ├── Not-Found.scss │ └── index.js │ ├── Profile │ ├── Profile.js │ ├── Profile.scss │ └── index.js │ ├── PropsRoute │ ├── PropsRoute.js │ ├── PropsRoute.scss │ └── index.js │ ├── RecoverPassword │ ├── RecoverPassword.js │ ├── RecoverPassword.scss │ └── index.js │ ├── ResetPassword │ ├── ResetPassword.js │ ├── ResetPassword.scss │ └── index.js │ └── Signup │ ├── Signup.js │ ├── Signup.scss │ └── index.js ├── package-lock.json ├── package.json ├── private └── README.md ├── public └── README.md ├── server └── main.js └── tests └── enzyme-config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "UNIT_TESTING": { 4 | "presets": ["es2015", "@babel/preset-env", "react"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@meteorjs/eslint-config-meteor", "plugin:prettier/recommended"], 3 | "rules": { 4 | "jsx-a11y/label-has-for": false, 5 | "no-console": 0, 6 | "consistent-return": 0, 7 | "no-plusplus": 0, 8 | "jsx-a11y/anchor-is-valid": 0, 9 | "no-underscore-dangle": 0, 10 | "prettier/prettier": "error" 11 | }, 12 | "plugins": ["prettier"], 13 | "env": { 14 | "jest": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/# Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # mup 64 | .deploy 65 | 66 | # macOS 67 | .DS_Store 68 | -------------------------------------------------------------------------------- /.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 | 1.4.3-split-account-service-packages 17 | 1.5-add-dynamic-import-package 18 | 1.7-split-underscore-from-meteor-base 19 | 1.8.3-split-jquery-from-blaze 20 | -------------------------------------------------------------------------------- /.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 | zns8wk5fgpg8.ymmfw63codp 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.4.0 # Packages every Meteor app needs to have 8 | mobile-experience@1.1.0 # Packages for a great mobile UX 9 | mongo@1.9.0 # The database Meteor supports right now 10 | static-html # Define static page content in .html files 11 | reactive-var@1.0.11 # Reactive variable for tracker 12 | tracker@1.2.0 # Meteor's client-side reactive programming library 13 | session@1.2.0 14 | accounts-password@1.6.0 15 | 16 | # standard-minifier-css@1.4.0 # CSS minifier run for production mode 17 | juliancwirko:postcss # CSS minifier + postcss processing https://guide.meteor.com/build-tool.html#postcss 18 | standard-minifier-js@2.6.0 # JS minifier run for production mode 19 | es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers 20 | ecmascript@0.14.2 # Enable ECMAScript2015+ syntax in app code 21 | shell-server@0.5.0 # Server-side component of the `meteor shell` command 22 | 23 | practicalmeteor:chai 24 | johanbrook:publication-collector # Test a Meteor publication by collecting its output 25 | fourseven:scss 26 | fortawesome:fontawesome 27 | 28 | browser-policy@1.1.0 29 | aldeed:collection2@3.0.0 30 | # msavin:mongol 31 | 32 | mdg:validated-method 33 | didericis:callpromise-mixin 34 | lacosta:method-hooks 35 | tunifight:loggedin-mixin 36 | underscore@1.0.10 37 | react-meteor-data 38 | alanning:roles 39 | jquery 40 | matb33:collection-hooks 41 | meteortesting:mocha 42 | mizzao:user-status 43 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.10.1 2 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.6.0 2 | accounts-password@1.6.0 3 | alanning:roles@3.2.2 4 | aldeed:collection2@3.0.6 5 | allow-deny@1.1.0 6 | autoupdate@1.6.0 7 | babel-compiler@7.5.2 8 | babel-runtime@1.5.0 9 | base64@1.0.12 10 | binary-heap@1.0.11 11 | blaze-tools@1.0.10 12 | boilerplate-generator@1.7.0 13 | browser-policy@1.1.0 14 | browser-policy-common@1.0.11 15 | browser-policy-content@1.1.0 16 | browser-policy-framing@1.1.0 17 | caching-compiler@1.2.1 18 | caching-html-compiler@1.1.3 19 | callback-hook@1.3.0 20 | check@1.3.1 21 | coffeescript@1.0.17 22 | ddp@1.4.0 23 | ddp-client@2.3.3 24 | ddp-common@1.4.0 25 | ddp-rate-limiter@1.0.7 26 | ddp-server@2.3.1 27 | deps@1.0.12 28 | didericis:callpromise-mixin@0.0.1 29 | diff-sequence@1.1.1 30 | dynamic-import@0.5.1 31 | ecmascript@0.14.2 32 | ecmascript-runtime@0.7.0 33 | ecmascript-runtime-client@0.10.0 34 | ecmascript-runtime-server@0.9.0 35 | ejson@1.1.1 36 | email@1.2.3 37 | es5-shim@4.8.0 38 | fetch@0.1.1 39 | fortawesome:fontawesome@4.7.0 40 | fourseven:scss@4.12.0 41 | geojson-utils@1.0.10 42 | hot-code-push@1.0.4 43 | html-tools@1.0.11 44 | htmljs@1.0.11 45 | http@1.4.2 46 | id-map@1.1.0 47 | inter-process-messaging@0.1.1 48 | johanbrook:publication-collector@1.1.0 49 | jquery@3.0.0 50 | juliancwirko:postcss@1.3.0 51 | lacosta:method-hooks@1.5.4 52 | launch-screen@1.2.0 53 | livedata@1.0.18 54 | lmieulet:meteor-coverage@1.1.4 55 | localstorage@1.2.0 56 | logging@1.1.20 57 | matb33:collection-hooks@1.0.1 58 | mdg:validated-method@1.2.0 59 | meteor@1.9.3 60 | meteor-base@1.4.0 61 | meteorhacks:picker@1.0.3 62 | meteortesting:browser-tests@1.3.3 63 | meteortesting:mocha@1.1.5 64 | meteortesting:mocha-core@7.0.1 65 | minifier-css@1.5.0 66 | minifier-js@2.6.0 67 | minimongo@1.5.0 68 | mizzao:timesync@0.5.1 69 | mizzao:user-status@1.0.0 70 | mobile-experience@1.1.0 71 | mobile-status-bar@1.1.0 72 | modern-browsers@0.1.5 73 | modules@0.15.0 74 | modules-runtime@0.12.0 75 | mongo@1.9.0 76 | mongo-decimal@0.1.1 77 | mongo-dev-server@1.1.0 78 | mongo-id@1.0.7 79 | npm-bcrypt@0.9.3 80 | npm-mongo@3.7.0 81 | ordered-dict@1.1.0 82 | practicalmeteor:chai@2.1.0_1 83 | promise@0.11.2 84 | raix:eventemitter@1.0.0 85 | random@1.2.0 86 | rate-limit@1.0.9 87 | react-meteor-data@2.0.1 88 | reactive-dict@1.3.0 89 | reactive-var@1.0.11 90 | reload@1.3.0 91 | retry@1.1.0 92 | routepolicy@1.1.0 93 | service-configuration@1.0.11 94 | session@1.2.0 95 | sha@1.0.9 96 | shell-server@0.5.0 97 | socket-stream-client@0.2.3 98 | spacebars-compiler@1.1.3 99 | srp@1.0.12 100 | standard-minifier-js@2.6.0 101 | static-html@1.2.2 102 | templating-tools@1.1.2 103 | tmeasday:check-npm-versions@0.3.2 104 | tracker@1.2.0 105 | tunifight:loggedin-mixin@0.1.0 106 | underscore@1.0.10 107 | url@1.2.0 108 | webapp@1.9.0 109 | webapp-hashing@1.0.9 110 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![eslint: airbnb](https://img.shields.io/badge/eslint-airbnb-blue.svg)](https://github.com/airbnb/javascript) 2 | [![prettier](https://img.shields.io/badge/-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 3 | ## A Meteor 1.9, React 16, React Router 5, Bootstrap 4 template 4 | 5 | Based off the official meteor scaffolding, with accounts, login and a demo collection that persists on login/logout. 6 | 7 | Current routes setup: 8 | 9 | - landing (index route) 10 | - login 11 | - signup 12 | - profile 13 | - recover-password 14 | - reset-password 15 | - not-found 16 | 17 | ## Quick start 18 | Clone repository: 19 | ``` 20 | git clone https://github.com/johnwils/meteor-react-template.git 21 | ``` 22 | Install packages: 23 | ``` 24 | meteor npm install 25 | ``` 26 | Start Meteor: 27 | ``` 28 | meteor 29 | ``` 30 | 31 | Navigate to [http://localhost:3000](http://localhost:3000) in any browser. 32 | 33 | 34 | ## Routing and redirects 35 | React Router 5 `props` are accessible in every top level 'page' component. This allows any page to access react router's 'redirect' functions and url params, etc. These can be passed onto any further components. 36 | 37 | Also React Router's `withProps` HOC provides the same functionality to any component. 38 | 39 | When logged in, users are redirected to the '/profile' route. 40 | 41 | When logged out, users are redirect to the '/login' route. 42 | 43 | ## Folder structure 44 | 45 | The folder structure is modular, developer friendly, easy to navigate and follows the import structure of the official Meteor docs. 46 | 47 | ### Pages 48 | Each 'route' is represented by a folder in the 'pages' directory. Most data fetching is done at this top page level. These pages are the 'smart' or 'container' components. They fetch data and pass it as props to presentational components. 49 | 50 | ### Components 51 | Reusable components in the 'components' directory are 'dumb' or ''presentational' components. These are mostly functional, stateless components. If a component requires data, it is passed as props from it's page component. 52 | 53 | *Note:* Meteor's reactive `withTracker` can also fetch data in any sub component (if really needed). 54 | 55 | ### API 56 | The 'api' folder contains 1 folder per collection (all methods and publications for each endpoint are exclusive to each folder). This makes it easy to maintain each collection endpoint. All collections use `aldeed:collection2` to enable schema validation on inserts. Both collections and methods use `simpl-schema` to validate parameters. 57 | 58 | #### Methods 59 | Methods use MDG's [mdg:validated-method](https://atmospherejs.com/mdg/validated-method). The benefits of validated methods over regular methods are listed here: [https://atmospherejs.com/mdg/validated-method#benefits-of-validatedmethod](https://atmospherejs.com/mdg/validated-method#benefits-of-validatedmethod) 60 | 61 | ##### Validated Method Mixins: 62 | 63 | The following mixins are used with methods: 64 | 65 | - [didericis:callpromise-mixin](https://atmospherejs.com/didericis/callpromise-mixin) is used to return a promise to the client instead of a callback. Async/await code is used on the client for handling methods. 66 | 67 | - [lacosta:method-hooks](https://atmospherejs.com/lacosta/method-hooks) provides before and after hooks when methods are called. 68 | 69 | - [tunifight:loggedin-mixin](https://atmospherejs.com/tunifight/loggedin-mixin) is used to only allow logged-in users to call methods. 70 | 71 | ## Roles 72 | Basic roles are defined using `alanning:roles`. 73 | 74 | The first user created is 'admin' and subsequent users are 'user'. 75 | 76 | ## SCSS 77 | SCSS is also locally scoped to each page/component folder. This makes managing styles easy, as .scss files are in the same folder as the component file. 78 | 79 | *Note:* most styling can be done via 'classes' using the Bootstrap API (see below) 80 | 81 | ### Global styles 82 | There is 1 main.scss file that imports Bootstrap and 1 custom.scss to override default styles. An app-wide custom theme can be setup easily in custom.scss. 83 | ## Bootstrap 4 84 | Bootstrap is being used directly on elements (adding to the 'class' or 'className') using the [v4 api](https://getbootstrap.com/docs/4.0/components/buttons/). This includes (so far) navbar, collapsed navbar, login/signup cards, search bar, dropdown menu and a modal. The api is well documented and easy to use. This approach limits the dependency on common external bootstrap packages. 85 | 86 | ## Autoprefixer 87 | Meteor's built-in css minify tool is replaced with `juliancwirko:postcss` ([mentioned](https://guide.meteor.com/build-tool.html#postcss) in the meteor docs). This package minifies CSS plus it makes use of a postcss entry in package.json to apply autoprefixer for wider browser support. 88 | 89 | ## Responsive layout 90 | The grid from Bootstrap 4 ensures the layout is responsive on desktop and mobile. The navbar, modal and login/signup cards are good examples to check out on mobile. 91 | 92 | ## Testing 93 | 94 | ### Server tests 95 | Mocha is used to run tests and log test results on the server. 96 | Chai is used as an expectation / assertion library. 97 | 98 | To run server tests on the example Counters methods and publications run: 99 | 100 | ``` 101 | npm run test-server 102 | ``` 103 | 104 | The server tests are in `imports/api/counters/` 105 | 106 | ### Client tests (todo) 107 | Jest is used on the client to test React components. 108 | Enzyme is used to help test, assert, manipulate, and traverse React components. 109 | 110 | ## ESLint 111 | 112 | ESLint is used to enforce consistent styling. 113 | 114 | Airbnb and Prettier style presets are used. 115 | 116 | To clean the app run: 117 | ``` 118 | npm run prettier 119 | ``` 120 | 121 | This will conform files in the 'imports', 'client' and 'server' folders to the style presets. 122 | 123 | ## Connecting this template to an existing meteor backend 124 | A ddp connection can be made to an existing meteor server, following steps in [Meteor's official docs](https://docs.meteor.com/api/connections.html#DDP-connect) 125 | 126 | The ddp connection enables access to the existing server's methods, collections and publications. 127 | 128 | **Links**: 129 | 130 | [Splitting into multiple Meteor apps](https://guide.meteor.com/structure.html#splitting-your-app) 131 | 132 | [Meteor multi app accounts](https://github.com/tmeasday/multi-app-accounts) 133 | 134 | 164 | 165 | ## What is not included? 166 | There is no state management such as [Redux](https://github.com/reactjs/redux) or [MobX](https://github.com/mobxjs/mobx). This is partly because this template is so small and state is locally managed in components as needed. Also the Meteor collections reactively update the UI when changed. However, any state management tool can be easily added to the top level App component to provide a global store. 167 | -------------------------------------------------------------------------------- /client/main.html: -------------------------------------------------------------------------------- 1 | 2 | Meteor, Bootstrap 4, React Router 4 - Template 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | // Client entry point, imports all client code 2 | 3 | import '/imports/startup/client'; 4 | import '/imports/startup/both'; 5 | -------------------------------------------------------------------------------- /imports/api/counters/counters.js: -------------------------------------------------------------------------------- 1 | // Collection definition 2 | 3 | import { Mongo } from 'meteor/mongo'; 4 | import SimpleSchema from 'simpl-schema'; 5 | 6 | // define collection 7 | const Counters = new Mongo.Collection('counters'); 8 | 9 | // define schema 10 | const Schema = new SimpleSchema({ 11 | _id: { 12 | type: String, 13 | }, 14 | count: { 15 | type: SimpleSchema.Integer, 16 | }, 17 | }); 18 | 19 | // attach schema 20 | Counters.attachSchema(Schema); 21 | 22 | export default Counters; 23 | -------------------------------------------------------------------------------- /imports/api/counters/counters.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, no-underscore-dangle */ 2 | // Tests for the behavior of the collection 3 | // https://guide.meteor.com/testing.html 4 | 5 | import { Meteor } from 'meteor/meteor'; 6 | import { assert } from 'meteor/practicalmeteor:chai'; 7 | import Counters from './counters.js'; 8 | 9 | if (Meteor.isServer) { 10 | describe('counters collection', function() { 11 | it('inserts correctly', function() { 12 | const counterId = Counters.insert({ 13 | _id: this.userId, 14 | count: 0, 15 | }); 16 | const added = Counters.find({ _id: counterId }); 17 | const collectionName = added._getCollectionName(); 18 | const count = added.count(); 19 | 20 | assert.equal(collectionName, 'counters'); 21 | assert.equal(count, 1); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /imports/api/counters/hooks.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /** 3 | * Collection Hooks 4 | * https://github.com/matb33/meteor-collection-hooks 5 | */ 6 | 7 | import { Meteor } from 'meteor/meteor'; 8 | import Counters from './counters'; 9 | -------------------------------------------------------------------------------- /imports/api/counters/methods.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Meteor methods 3 | */ 4 | 5 | import { Meteor } from 'meteor/meteor'; 6 | import { Random } from 'meteor/random'; 7 | import SimpleSchema from 'simpl-schema'; 8 | import { ValidatedMethod } from 'meteor/mdg:validated-method'; 9 | import { LoggedInMixin } from 'meteor/tunifight:loggedin-mixin'; 10 | import { MethodHooks } from 'meteor/lacosta:method-hooks'; 11 | import { CallPromiseMixin } from 'meteor/didericis:callpromise-mixin'; 12 | 13 | import Counters from './counters.js'; 14 | 15 | /** **************** Helpers **************** */ 16 | 17 | const mixins = [LoggedInMixin, MethodHooks, CallPromiseMixin]; 18 | 19 | // not logged in error message 20 | const checkLoggedInError = { 21 | error: 'notLogged', 22 | message: 'You need to be logged in to call this method', 23 | reason: 'You need to login', 24 | }; 25 | 26 | /** **************** Methods **************** */ 27 | 28 | /** 29 | * countersIncrease 30 | */ 31 | 32 | // eslint-disable-next-line no-unused-vars, arrow-body-style 33 | const beforeHookExample = (methodArgs, methodOptions) => { 34 | // console.log('countersIncrease before hook'); 35 | // perform tasks 36 | return methodArgs; 37 | }; 38 | // eslint-disable-next-line no-unused-vars, arrow-body-style 39 | const afterHookExample = (methodArgs, returnValue, methodOptions) => { 40 | // console.log('countersIncrease: after hook:'); 41 | // perform tasks 42 | return returnValue; 43 | }; 44 | 45 | export const countersIncrease = new ValidatedMethod({ 46 | name: 'counters.increase', 47 | mixins, 48 | beforeHooks: [beforeHookExample], 49 | afterHooks: [afterHookExample], 50 | checkLoggedInError, 51 | validate: new SimpleSchema({ 52 | _id: { 53 | type: String, 54 | optional: false, 55 | }, 56 | }).validator(), 57 | run({ _id }) { 58 | // console.log('counters.increase', _id); 59 | if (Meteor.isServer) { 60 | // secure code - not available on the client 61 | } 62 | // call code on client and server (optimistic UI) 63 | return Counters.update( 64 | { _id }, 65 | { 66 | $inc: { 67 | count: 1, 68 | }, 69 | } 70 | ); 71 | }, 72 | }); 73 | 74 | /** 75 | * used for example test in methods.tests.js 76 | */ 77 | export const countersInsert = new ValidatedMethod({ 78 | name: 'counters.insert', 79 | mixin: [CallPromiseMixin], 80 | validate: null, 81 | run() { 82 | const _id = Random.id(); 83 | // console.log('counters.insert', _id); 84 | const counterId = Counters.insert({ 85 | _id, 86 | count: Number(0), 87 | }); 88 | return counterId; 89 | }, 90 | }); 91 | -------------------------------------------------------------------------------- /imports/api/counters/methods.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // Tests for methods 3 | // https://guide.meteor.com/testing.html 4 | 5 | import { Meteor } from 'meteor/meteor'; 6 | import { assert } from 'meteor/practicalmeteor:chai'; 7 | import Counters from './counters.js'; 8 | import { countersInsert, countersIncrease } from './methods.js'; 9 | 10 | if (Meteor.isServer) { 11 | describe('counters method', function() { 12 | before(function() { 13 | Counters.remove({}); 14 | Meteor.users.remove({}); 15 | }); 16 | 17 | // use same counter id for all tests 18 | let counterId = null; 19 | 20 | it('can add a counter', async function(done) { 21 | assert.equal(Counters.find().count(), 0); 22 | countersInsert.call((err, result) => { 23 | if (err) { 24 | console.log(err); 25 | return done(); 26 | } 27 | counterId = result; 28 | assert.equal(Counters.find().count(), 1); 29 | return done(); 30 | }); 31 | }); 32 | 33 | it('can increase a counter', async function() { 34 | assert.equal(Counters.findOne(counterId).count, 0); 35 | // create user and assign to 'user' role 36 | const stubbedUserId = Accounts.createUser({ 37 | email: 'test@user.com', 38 | password: 'test', 39 | }); 40 | Roles.addUsersToRoles(stubbedUserId, 'user'); 41 | await countersIncrease.run.call( 42 | { userId: stubbedUserId }, 43 | { _id: counterId } 44 | ); 45 | assert.equal(Counters.findOne(counterId).count, 1); 46 | }); 47 | 48 | it('cannot increase a counter if not in "user" role', async function() { 49 | const counter = Counters.findOne(counterId); 50 | // should still be 1 from previous test 51 | assert.equal(counter.count, 1); 52 | // create user *without* assigning a role 53 | const stubbedUserId = Accounts.createUser({ 54 | email: 'not@in.com', 55 | password: 'user-role', 56 | }); 57 | assert.throws( 58 | () => 59 | countersIncrease.run.call( 60 | { userId: stubbedUserId }, 61 | { _id: counterId } 62 | ), 63 | Error, 64 | 'You are not allowed to call this method [not-allowed]' 65 | ); 66 | // should remain 1 67 | assert.equal(counter.count, 1); 68 | }); 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /imports/api/counters/publications.js: -------------------------------------------------------------------------------- 1 | // Publications send to the client 2 | 3 | import { Meteor } from 'meteor/meteor'; 4 | import { Roles } from 'meteor/alanning:roles'; 5 | import Counters from './counters.js'; 6 | 7 | if (Meteor.isServer) { 8 | Meteor.publish('counters.all', function() { 9 | if (Roles.userIsInRole(this.userId, 'admin')) { 10 | return Counters.find(); 11 | } 12 | return this.ready(); 13 | }); 14 | 15 | Meteor.publish('counters.user', function() { 16 | if (!this.userId) { 17 | return this.ready(); 18 | } 19 | return Counters.find({ _id: this.userId }); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /imports/api/counters/publications.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // Tests for publications 3 | // https://guide.meteor.com/testing.html 4 | 5 | import { Random } from 'meteor/random'; 6 | import { assert } from 'meteor/practicalmeteor:chai'; 7 | import { PublicationCollector } from 'meteor/johanbrook:publication-collector'; 8 | 9 | import Counters from './counters.js'; 10 | import './publications.js'; 11 | 12 | if (Meteor.isServer) { 13 | describe('counters publications', function() { 14 | before(function() { 15 | Counters.remove({}); 16 | _.times(7, () => { 17 | Counters.insert({ 18 | _id: Random.id(), 19 | count: 0, 20 | }); 21 | }); 22 | }); 23 | 24 | describe('counters.all', function() { 25 | it('sends all counters', function(done) { 26 | const collector = new PublicationCollector(); 27 | collector.collect('counters.all', () => { 28 | assert.notEqual(Counters.find().count(), 6); 29 | assert.equal(Counters.find().count(), 7); 30 | assert.notEqual(Counters.find().count(), 8); 31 | done(); 32 | }); 33 | }); 34 | }); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /imports/api/remote/ddp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Client DDP Connection 3 | * Connect to existing meteor server using ddp 4 | * 5 | * See Profile component in 'pages' directory for HOC data fetching example 6 | */ 7 | 8 | import { DDP } from 'meteor/ddp-client'; 9 | 10 | // establish ddp connection 11 | const remoteUrl = ''; 12 | const Remote = DDP.connect(remoteUrl); 13 | Remote.onReconnect = (...args) => console.log('reconnected to ddp...', args); 14 | 15 | export default Remote; 16 | 17 | // example: call a remote server method (use in place of Meteor.call) 18 | /* 19 | Remote.call('someMethod', (err) => { 20 | // 'someMethod' is run on the remote meteor server 21 | if (err) { 22 | return console.log('error calling method over ddp'); 23 | } 24 | console.log('successfully called method over ddp!'); 25 | }); 26 | */ 27 | -------------------------------------------------------------------------------- /imports/api/remote/users.js: -------------------------------------------------------------------------------- 1 | // Remote Collection definition 2 | 3 | import { Meteor } from 'meteor/meteor'; 4 | import Remote from './ddp'; 5 | 6 | const Users = new Meteor.Collection('users', { connection: Remote }); 7 | 8 | export default Users; 9 | -------------------------------------------------------------------------------- /imports/api/users/hooks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Collection Hooks 3 | */ 4 | 5 | import { Meteor } from 'meteor/meteor'; 6 | import { Roles } from 'meteor/alanning:roles'; 7 | 8 | Meteor.users.after.insert(function(userId, doc) { 9 | if (!userId && Meteor.users.find().count() === 0) { 10 | console.log("new admin registered, added to 'admin' role", doc._id); 11 | return Roles.addUsersToRoles(doc._id, ['admin'], Roles.GLOBAL_GROUP); 12 | } 13 | if (!userId) { 14 | console.log("new user registered, added to 'user' role", doc._id); 15 | return Roles.addUsersToRoles(doc._id, ['user'], Roles.GLOBAL_GROUP); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /imports/api/users/methods.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /** 3 | * Meteor methods 4 | */ 5 | 6 | import { Meteor } from 'meteor/meteor'; 7 | import { check } from 'meteor/check'; 8 | -------------------------------------------------------------------------------- /imports/api/users/methods.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // Tests for methods 3 | // https://guide.meteor.com/testing.html 4 | 5 | import { Meteor } from 'meteor/meteor'; 6 | import { assert } from 'meteor/practicalmeteor:chai'; 7 | import './methods.js'; 8 | -------------------------------------------------------------------------------- /imports/api/users/publications.js: -------------------------------------------------------------------------------- 1 | // Publications to the client 2 | 3 | import { Meteor } from 'meteor/meteor'; 4 | import { Roles } from 'meteor/alanning:roles'; 5 | 6 | if (Meteor.isServer) { 7 | // all users publication (admin only) 8 | Meteor.publish('users.all', function() { 9 | if (Roles.userIsInRole(this.userId, 'admin')) { 10 | return Meteor.users.find(); 11 | } 12 | return this.ready(); 13 | }); 14 | 15 | // current logged in user publication 16 | Meteor.publish('user', function() { 17 | if (this.userId) { 18 | return Meteor.users.find( 19 | { _id: this.userId }, 20 | { 21 | fields: { 22 | emails: 1, 23 | profile: 1, 24 | status: 1, 25 | }, 26 | } 27 | ); 28 | } 29 | return this.ready(); 30 | }); 31 | 32 | // example friends publication 33 | // Meteor.publish('users.friends', function() { 34 | // if (this.userId) { 35 | // const user = Meteor.users.findOne(this.userId); 36 | // if (user.friendIds) { 37 | // return Meteor.users.find( 38 | // { _id: { $inc: user.friendIds } }, 39 | // { 40 | // fields: { 41 | // emails: 1, 42 | // profile: 1, 43 | // status: 1, 44 | // }, 45 | // }, 46 | // ); 47 | // } 48 | // return this.ready(); 49 | // } 50 | // return this.ready(); 51 | // }); 52 | } 53 | -------------------------------------------------------------------------------- /imports/api/users/publications.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // Tests for publications 3 | // 4 | // https://guide.meteor.com/testing.html 5 | 6 | import { assert } from 'meteor/practicalmeteor:chai'; 7 | import { PublicationCollector } from 'meteor/johanbrook:publication-collector'; 8 | import './publications.js'; 9 | -------------------------------------------------------------------------------- /imports/api/users/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Deny write access on users collection from client 3 | */ 4 | 5 | import { Meteor } from 'meteor/meteor'; 6 | 7 | // This fixes default writable profile field: 8 | // https://guide.meteor.com/accounts.html#dont-use-profile 9 | Meteor.users.deny({ 10 | update() { 11 | return true; 12 | }, 13 | }); 14 | 15 | Meteor.startup(() => { 16 | const roles = ['admin', 'user']; 17 | roles.forEach(role => Roles.createRole(role, { unlessExists: true })); 18 | }); 19 | -------------------------------------------------------------------------------- /imports/api/users/users.tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // Tests for the behavior of the collection 3 | // https://guide.meteor.com/testing.html 4 | 5 | import { Meteor } from 'meteor/meteor'; 6 | import { assert } from 'meteor/practicalmeteor:chai'; 7 | -------------------------------------------------------------------------------- /imports/startup/both/index.js: -------------------------------------------------------------------------------- 1 | // Import modules used by both client and server 2 | // e.g. useraccounts configuration file. 3 | 4 | // import counter collection and common methods 5 | import '../../api/users/methods'; 6 | import '../../api/users/users'; 7 | 8 | // import counter collection and common methods 9 | import '../../api/counters/counters'; 10 | import '../../api/counters/methods'; 11 | -------------------------------------------------------------------------------- /imports/startup/client/index.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | 5 | import '@popperjs/core'; 6 | import 'bootstrap'; 7 | import './styles/main.scss'; 8 | 9 | // connect to ddp (uncomment when url is set in ddp.js) 10 | // import '../../api/remote/ddp'; 11 | 12 | // import client routes 13 | import App from '../../ui/layouts/App'; 14 | 15 | // mount app 16 | Meteor.startup(() => { 17 | render(, document.getElementById('react-root')); 18 | }); 19 | -------------------------------------------------------------------------------- /imports/startup/client/styles/custom.scss: -------------------------------------------------------------------------------- 1 | // custom variable overrides 2 | 3 | // $body-bg: #000; 4 | // $body-color: #111; 5 | 6 | $theme-colors: ( 7 | // 'primary': '', 8 | // 'danger': '', 9 | // 'secondary':'', 10 | // 'success':'', 11 | // 'info':'', 12 | // 'warning':'', 13 | // 'light':'', 14 | // 'dark':'' 15 | ); 16 | 17 | // mongol 18 | #Mongol { 19 | background-color: black; 20 | } 21 | -------------------------------------------------------------------------------- /imports/startup/client/styles/main.scss: -------------------------------------------------------------------------------- 1 | // variable overides 2 | @import "./custom.scss"; 3 | 4 | // Bootstrap and its default variables 5 | @import "{}/node_modules/bootstrap/scss/bootstrap.scss"; 6 | -------------------------------------------------------------------------------- /imports/startup/server/accounts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Accounts Setup 3 | */ 4 | 5 | import { Accounts } from 'meteor/accounts-base'; 6 | import Counters from '../../api/counters/counters.js'; 7 | 8 | Accounts.onCreateUser((options, user) => { 9 | // init counter at 0 10 | Counters.insert({ 11 | _id: user._id, 12 | count: Number(0), 13 | }); 14 | return user; 15 | }); 16 | -------------------------------------------------------------------------------- /imports/startup/server/browser-policy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Browser Policy 3 | * Set security-related policies to be enforced by newer browsers. 4 | * These policies help prevent and mitigate common attacks like 5 | * cross-site scripting and clickjacking. 6 | */ 7 | 8 | import { BrowserPolicy } from 'meteor/browser-policy-common'; 9 | 10 | /** 11 | * allowed images 12 | */ 13 | const allowImageOrigin = ['via.placeholder.com']; 14 | allowImageOrigin.forEach(o => BrowserPolicy.content.allowImageOrigin(o)); 15 | 16 | /** 17 | * allowed scripts 18 | */ 19 | // const allowScriptOrigin = ['']; 20 | // allowScriptOrigin.forEach(o => BrowserPolicy.content.allowScriptOrigin(o)); 21 | 22 | /** 23 | * allowed styles 24 | */ 25 | // const allowStyleOrigin = ['']; 26 | // allowStyleOrigin.forEach(o => BrowserPolicy.content.allowStyleOrigin(o)); 27 | -------------------------------------------------------------------------------- /imports/startup/server/fixtures.js: -------------------------------------------------------------------------------- 1 | // Fill the DB with example data on startup 2 | 3 | import { Meteor } from 'meteor/meteor'; 4 | import Counters from '../../api/counters/counters.js'; 5 | 6 | Meteor.startup(() => { 7 | // check if db is empty, fill with fake data for testing 8 | }); 9 | -------------------------------------------------------------------------------- /imports/startup/server/index.js: -------------------------------------------------------------------------------- 1 | // Import server startup through a single index entry point 2 | 3 | import './accounts.js'; 4 | import './browser-policy.js'; 5 | import './fixtures.js'; 6 | import './register-api.js'; 7 | -------------------------------------------------------------------------------- /imports/startup/server/register-api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Register each api 3 | * import private server methods and server publications 4 | */ 5 | 6 | // users api 7 | import '../../api/users/publications.js'; 8 | import '../../api/users/hooks.js'; 9 | 10 | // counters api (example) 11 | import '../../api/counters/methods.js'; 12 | import '../../api/counters/publications.js'; 13 | 14 | // import another api 15 | -------------------------------------------------------------------------------- /imports/ui/components/Alert/Alert.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import './Alert.scss'; 5 | 6 | const Alert = ({ errMsg }) => ( 7 |
8 | {errMsg} 9 |
10 | ); 11 | 12 | Alert.propTypes = { 13 | errMsg: PropTypes.string.isRequired, 14 | }; 15 | 16 | export default Alert; 17 | -------------------------------------------------------------------------------- /imports/ui/components/Alert/Alert.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwils/meteor-react-template/b5227b8becf3644cea1a60d0dfb469f182215e56/imports/ui/components/Alert/Alert.scss -------------------------------------------------------------------------------- /imports/ui/components/Alert/index.js: -------------------------------------------------------------------------------- 1 | import Alert from './Alert.js'; 2 | 3 | export default Alert; 4 | -------------------------------------------------------------------------------- /imports/ui/components/Button/Button.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import React from 'react'; 3 | 4 | import { countersIncrease } from '../../../api/counters/methods'; 5 | 6 | import './Button.scss'; 7 | 8 | const handlePress = () => countersIncrease.call({ _id: Meteor.userId() }); 9 | 10 | const Button = () => ( 11 | 14 | ); 15 | 16 | export default Button; 17 | -------------------------------------------------------------------------------- /imports/ui/components/Button/Button.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwils/meteor-react-template/b5227b8becf3644cea1a60d0dfb469f182215e56/imports/ui/components/Button/Button.scss -------------------------------------------------------------------------------- /imports/ui/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import Button from './Button.js'; 2 | 3 | export default Button; 4 | -------------------------------------------------------------------------------- /imports/ui/components/Modal/Modal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A basic bootstrap 4 modal 3 | * jw 4 | */ 5 | 6 | import { Meteor } from 'meteor/meteor'; 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | 10 | import './Modal.scss'; 11 | 12 | export const Button = ({ target, type, title }) => ( 13 | 21 | ); 22 | 23 | Button.propTypes = { 24 | target: PropTypes.string.isRequired, 25 | title: PropTypes.string.isRequired, 26 | type: PropTypes.oneOf([ 27 | 'primary', 28 | 'secondary', 29 | 'success', 30 | 'danger', 31 | 'warning', 32 | 'info', 33 | 'light', 34 | 'dark', 35 | ]).isRequired, 36 | }; 37 | 38 | const Modal = ({ target, title, body, counter }) => ( 39 | 89 | ); 90 | 91 | Modal.propTypes = { 92 | target: PropTypes.string.isRequired, 93 | title: PropTypes.string.isRequired, 94 | body: PropTypes.string.isRequired, 95 | counter: PropTypes.shape({ 96 | _id: PropTypes.string, 97 | count: PropTypes.number, 98 | }).isRequired, 99 | }; 100 | 101 | export default Modal; 102 | -------------------------------------------------------------------------------- /imports/ui/components/Modal/Modal.scss: -------------------------------------------------------------------------------- 1 | .modal-01 { 2 | pre { 3 | color: #e83e8c; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /imports/ui/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | import Modal from './Modal.js'; 2 | 3 | export default Modal; 4 | -------------------------------------------------------------------------------- /imports/ui/components/Navbar/Navbar.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { NavLink } from 'react-router-dom'; 5 | 6 | import './Navbar.scss'; 7 | 8 | const PublicNav = () => [ 9 |
  • 10 | 11 | Login 12 | 13 |
  • , 14 |
  • 15 | 16 | Signup 17 | 18 |
  • , 19 | ]; 20 | 21 | const SearchBar = () => ( 22 |
    23 | 29 | 32 | 33 | ); 34 | 35 | const LoggedInNav = () => ( 36 | <> 37 | 38 |
  • 39 | 40 | 43 | 44 |
  • 45 |
  • 46 |
    47 |
  • 48 |
  • 49 | Meteor.logout()}> 50 | 53 | 54 |
  • 55 | 56 | ); 57 | 58 | const Status = ({ loggedIn }) => ( 59 |
    60 | {loggedIn ? ( 61 | 62 | 63 | 64 | ) : ( 65 | 66 | 67 | 68 | )} 69 |
    70 | ); 71 | 72 | Status.propTypes = { 73 | loggedIn: PropTypes.bool.isRequired, 74 | }; 75 | 76 | const Navbar = ({ loggedIn }) => ( 77 | 99 | ); 100 | 101 | Navbar.propTypes = { 102 | loggedIn: PropTypes.bool.isRequired, 103 | }; 104 | 105 | export default Navbar; 106 | -------------------------------------------------------------------------------- /imports/ui/components/Navbar/Navbar.scss: -------------------------------------------------------------------------------- 1 | .navbar { 2 | background-color: #444; 3 | * { 4 | :hover, 5 | :active { 6 | text-decoration: none; 7 | } 8 | } 9 | a { 10 | color: #666; 11 | } 12 | .active { 13 | color: #007bff; 14 | } 15 | .navbar-brand a { 16 | color: #666; 17 | } 18 | .dropdown-item:active { 19 | color: #212529; 20 | background-color: #fff; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /imports/ui/components/Navbar/index.js: -------------------------------------------------------------------------------- 1 | import Navbar from './Navbar.js'; 2 | 3 | export default Navbar; 4 | -------------------------------------------------------------------------------- /imports/ui/components/Spinner/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './Spinner.scss'; 4 | 5 | const Spinner = () => ( 6 |
    7 |
    8 | 9 |
    10 |
    11 | ); 12 | 13 | export default Spinner; 14 | -------------------------------------------------------------------------------- /imports/ui/components/Spinner/Spinner.scss: -------------------------------------------------------------------------------- 1 | .login-spinner { 2 | position: fixed; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | top: 0; 7 | width: 100%; 8 | height: 100%; 9 | z-index: 1000; 10 | background-color: white; 11 | i { 12 | font-size: 44px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /imports/ui/components/Spinner/index.js: -------------------------------------------------------------------------------- 1 | import Spinner from './Spinner.js'; 2 | 3 | export default Spinner; 4 | -------------------------------------------------------------------------------- /imports/ui/components/Text/Text.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import './Text.scss'; 5 | 6 | const Text = ({ count }) => ( 7 |
    Button pressed {count} times.
    8 | ); 9 | 10 | Text.propTypes = { 11 | count: PropTypes.number.isRequired, 12 | }; 13 | 14 | export default Text; 15 | -------------------------------------------------------------------------------- /imports/ui/components/Text/Text.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwils/meteor-react-template/b5227b8becf3644cea1a60d0dfb469f182215e56/imports/ui/components/Text/Text.scss -------------------------------------------------------------------------------- /imports/ui/components/Text/index.js: -------------------------------------------------------------------------------- 1 | import Text from './Text.js'; 2 | 3 | export default Text; 4 | -------------------------------------------------------------------------------- /imports/ui/layouts/App.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-named-default, react/destructuring-assignment */ 2 | 3 | // import packages 4 | import { Meteor } from 'meteor/meteor'; 5 | import { withTracker } from 'meteor/react-meteor-data'; 6 | import React from 'react'; 7 | import PropTypes from 'prop-types'; 8 | import { BrowserRouter as Router, Switch } from 'react-router-dom'; 9 | 10 | // import navbar 11 | import Navbar from '../components/Navbar'; 12 | 13 | // import routes 14 | import Landing from '../pages/Landing'; 15 | import Login from '../pages/Login'; 16 | import Signup from '../pages/Signup'; 17 | import Profile from '../pages/Profile'; 18 | import NotFound from '../pages/Not-Found'; 19 | import RecoverPassword from '../pages/RecoverPassword'; 20 | import ResetPassword from '../pages/ResetPassword'; 21 | 22 | // import Spinner 23 | import Spinner from '../components/Spinner'; 24 | 25 | // import hoc to pass additional props to routes 26 | import PropsRoute from '../pages/PropsRoute'; 27 | 28 | const App = props => ( 29 | 30 |
    31 | 32 | {props.loggingIn && } 33 | 34 | 35 | 36 | 37 | 38 | 39 | 44 | 49 | 50 | 51 |
    52 |
    53 | ); 54 | 55 | App.propTypes = { 56 | loggingIn: PropTypes.bool.isRequired, 57 | userReady: PropTypes.bool.isRequired, 58 | loggedIn: PropTypes.bool.isRequired, 59 | }; 60 | 61 | export default withTracker(() => { 62 | const userSub = Meteor.subscribe('user'); 63 | const user = Meteor.user(); 64 | const userReady = userSub.ready() && !!user; 65 | const loggingIn = Meteor.loggingIn(); 66 | const loggedIn = !loggingIn && userReady; 67 | return { 68 | loggingIn, 69 | userReady, 70 | loggedIn, 71 | }; 72 | })(App); 73 | -------------------------------------------------------------------------------- /imports/ui/pages/Landing/Landing.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import './Landing.scss'; 5 | 6 | class Landing extends React.Component { 7 | componentDidMount() { 8 | if (this.props.loggedIn) { 9 | return this.props.history.push('/profile'); 10 | } 11 | } 12 | 13 | shouldComponentUpdate(nextProps) { 14 | if (nextProps.loggedIn) { 15 | nextProps.history.push('/profile'); 16 | return false; 17 | } 18 | return true; 19 | } 20 | 21 | render() { 22 | if (this.props.loggedIn) { 23 | return null; 24 | } 25 | return ( 26 |
    27 |

    Landing Page

    28 |
    29 | ); 30 | } 31 | } 32 | 33 | Landing.propTypes = { 34 | loggedIn: PropTypes.bool.isRequired, 35 | history: PropTypes.shape({ 36 | push: PropTypes.func.isRequired, 37 | }).isRequired, 38 | }; 39 | 40 | export default Landing; 41 | -------------------------------------------------------------------------------- /imports/ui/pages/Landing/Landing.scss: -------------------------------------------------------------------------------- 1 | .landing-page { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 80vh; 6 | font-size: 44px; 7 | h1 { 8 | color: #333; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /imports/ui/pages/Landing/index.js: -------------------------------------------------------------------------------- 1 | import Landing from './Landing.js'; 2 | 3 | export default Landing; 4 | -------------------------------------------------------------------------------- /imports/ui/pages/Login/Login.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { NavLink } from 'react-router-dom'; 5 | 6 | // import components 7 | import Alert from '../../components/Alert'; 8 | 9 | // import styles 10 | import './Login.scss'; 11 | 12 | class Login extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | email: '', 17 | password: '', 18 | errMsg: null, 19 | }; 20 | this.handleSubmit = this.handleSubmit.bind(this); 21 | } 22 | 23 | componentDidMount() { 24 | if (this.props.loggedIn) { 25 | return this.props.history.push('/profile'); 26 | } 27 | } 28 | 29 | shouldComponentUpdate(nextProps) { 30 | if (nextProps.loggedIn) { 31 | nextProps.history.push('/profile'); 32 | return false; 33 | } 34 | return true; 35 | } 36 | 37 | handleSubmit(e) { 38 | e.preventDefault(); 39 | const { email, password } = this.state; 40 | Meteor.loginWithPassword(email, password, err => { 41 | if (err) { 42 | this.setState({ errMsg: err.reason }); 43 | return console.log(err); 44 | } 45 | }); 46 | } 47 | render() { 48 | if (this.props.loggedIn) { 49 | return null; 50 | } 51 | 52 | const { errMsg } = this.state; 53 | return ( 54 |
    55 |
    56 |
    57 |
    58 |
    59 | logo 64 |
    65 |
    66 |
    67 |

    Login

    68 |
    69 |
    70 | 71 | 72 | this.setState({ email: e.target.value })} 79 | required 80 | /> 81 |
    82 | 83 |
    84 |
    85 | 86 |
    87 | this.setState({ password: e.target.value })} 94 | required 95 | /> 96 | Forgot Password? 97 |
    98 |
    99 | 105 | {errMsg && } 106 |
    107 |
    108 | Don't have an account?{' '} 109 | Create one 110 |
    111 |
    112 |
    113 |
    114 |
    115 | © {new Date().getFullYear()} 116 |
    117 |
    118 |
    119 | ); 120 | } 121 | } 122 | 123 | export default Login; 124 | 125 | Login.propTypes = { 126 | loggedIn: PropTypes.bool.isRequired, 127 | history: PropTypes.shape({ 128 | push: PropTypes.func.isRequired, 129 | }).isRequired, 130 | }; 131 | -------------------------------------------------------------------------------- /imports/ui/pages/Login/Login.scss: -------------------------------------------------------------------------------- 1 | .login-page { 2 | margin-top: 15px; 3 | .form-signin { 4 | width: 100%; 5 | max-width: 330px; 6 | padding: 15px; 7 | margin: 0 auto; 8 | } 9 | .form-signin .checkbox { 10 | font-weight: 400; 11 | } 12 | .form-signin .form-control { 13 | position: relative; 14 | box-sizing: border-box; 15 | height: auto; 16 | padding: 10px; 17 | font-size: 16px; 18 | } 19 | .form-signin .form-control:focus { 20 | z-index: 2; 21 | } 22 | .form-signin input[type='email'] { 23 | margin-bottom: -1px; 24 | border-bottom-right-radius: 0; 25 | border-bottom-left-radius: 0; 26 | } 27 | .form-signin input[type='password'] { 28 | margin-bottom: 10px; 29 | border-top-left-radius: 0; 30 | border-top-right-radius: 0; 31 | } 32 | .spread-container { 33 | display: flex; 34 | justify-content: space-between; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /imports/ui/pages/Login/index.js: -------------------------------------------------------------------------------- 1 | import Login from './Login.js'; 2 | 3 | export default Login; 4 | -------------------------------------------------------------------------------- /imports/ui/pages/Not-Found/Not-Found.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './Not-Found.scss'; 4 | 5 | const NotFound = () => ( 6 |
    7 |
    8 | Page Not Found 9 |
    10 |
    11 | ); 12 | 13 | export default NotFound; 14 | -------------------------------------------------------------------------------- /imports/ui/pages/Not-Found/Not-Found.scss: -------------------------------------------------------------------------------- 1 | .not-found-page { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 100vh; 6 | font-size: 44px; 7 | h1 { 8 | color: #333; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /imports/ui/pages/Not-Found/index.js: -------------------------------------------------------------------------------- 1 | import NotFound from './Not-Found.js'; 2 | 3 | export default NotFound; 4 | -------------------------------------------------------------------------------- /imports/ui/pages/Profile/Profile.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { withTracker } from 'meteor/react-meteor-data'; 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | // collection 7 | import Counters from '../../../api/counters/counters'; 8 | 9 | // remote example (if using ddp) 10 | /* 11 | import Remote from '../../../api/remote/ddp'; 12 | import Users from '../../../api/remote/users'; 13 | */ 14 | 15 | // components 16 | import Modal, { Button } from '../../components/Modal/Modal'; 17 | import AddCountButton from '../../components/Button'; 18 | import Text from '../../components/Text'; 19 | 20 | import './Profile.scss'; 21 | 22 | class Profile extends React.Component { 23 | componentDidMount() { 24 | if (!this.props.loggedIn) { 25 | return this.props.history.push('/login'); 26 | } 27 | } 28 | 29 | shouldComponentUpdate(nextProps) { 30 | if (!nextProps.loggedIn) { 31 | nextProps.history.push('/login'); 32 | return false; 33 | } 34 | return true; 35 | } 36 | 37 | render() { 38 | const { 39 | loggedIn, 40 | // remote example (if using ddp) 41 | // usersReady, 42 | // users, 43 | countersReady, 44 | counter, 45 | } = this.props; 46 | 47 | // eslint-disable-line 48 | // remote example (if using ddp) 49 | /* 50 | console.log('usersReady', usersReady); 51 | console.log('users', users); 52 | */ 53 | if (!loggedIn) { 54 | return null; 55 | } 56 | return ( 57 |
    58 |

    Profile Page

    59 |
    72 | ); 73 | } 74 | } 75 | 76 | Profile.defaultProps = { 77 | // users: null, remote example (if using ddp) 78 | counter: null, 79 | }; 80 | 81 | Profile.propTypes = { 82 | loggedIn: PropTypes.bool.isRequired, 83 | history: PropTypes.shape({ 84 | push: PropTypes.func.isRequired, 85 | }).isRequired, 86 | // remote example (if using ddp) 87 | // usersReady: PropTypes.bool.isRequired, 88 | // users: Meteor.user() ? PropTypes.array.isRequired : () => null, 89 | countersReady: PropTypes.bool.isRequired, 90 | counter: PropTypes.shape({ 91 | _id: PropTypes.string, 92 | count: PropTypes.number, 93 | }), 94 | }; 95 | 96 | export default withTracker(() => { 97 | // remote example (if using ddp) 98 | /* 99 | const usersSub = Remote.subscribe('users.friends'); // publication needs to be set on remote server 100 | const users = Users.find().fetch(); 101 | const usersReady = usersSub.ready() && !!users; 102 | */ 103 | 104 | // counters example 105 | const countersSub = Meteor.subscribe('counters.user'); 106 | const counter = Counters.findOne({ _id: Meteor.userId() }); 107 | const countersReady = countersSub.ready() && !!counter; 108 | return { 109 | // remote example (if using ddp) 110 | // usersReady, 111 | // users, 112 | countersReady, 113 | counter, 114 | }; 115 | })(Profile); 116 | -------------------------------------------------------------------------------- /imports/ui/pages/Profile/Profile.scss: -------------------------------------------------------------------------------- 1 | .profile-page { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | height: 80vh; 7 | h1 { 8 | font-size: 44px; 9 | color: #333; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /imports/ui/pages/Profile/index.js: -------------------------------------------------------------------------------- 1 | import Profile from './Profile.js'; 2 | 3 | export default Profile; 4 | -------------------------------------------------------------------------------- /imports/ui/pages/PropsRoute/PropsRoute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * is used in place of 3 | * This allows additional props to be passed in 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import { Route } from 'react-router-dom'; 8 | 9 | const renderMergedProps = (component, ...rest) => { 10 | const finalProps = Object.assign({}, ...rest); 11 | return React.createElement(component, finalProps); 12 | }; 13 | 14 | const PropsRoute = ({ component, ...rest }) => ( 15 | renderMergedProps(component, routeProps, rest)} 18 | /> 19 | ); 20 | 21 | PropsRoute.propTypes = { 22 | component: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired 23 | }; 24 | 25 | export default PropsRoute; 26 | -------------------------------------------------------------------------------- /imports/ui/pages/PropsRoute/PropsRoute.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnwils/meteor-react-template/b5227b8becf3644cea1a60d0dfb469f182215e56/imports/ui/pages/PropsRoute/PropsRoute.scss -------------------------------------------------------------------------------- /imports/ui/pages/PropsRoute/index.js: -------------------------------------------------------------------------------- 1 | import PropsRoute from './PropsRoute.js'; 2 | 3 | export default PropsRoute; 4 | -------------------------------------------------------------------------------- /imports/ui/pages/RecoverPassword/RecoverPassword.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './RecoverPassword.scss'; 4 | 5 | const RecoverPassword = () => ( 6 |
    7 |

    Recover Password Page

    8 |
    9 | ); 10 | 11 | export default RecoverPassword; 12 | -------------------------------------------------------------------------------- /imports/ui/pages/RecoverPassword/RecoverPassword.scss: -------------------------------------------------------------------------------- 1 | .recover-password-page { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 80vh; 6 | font-size: 44px; 7 | h1 { 8 | color: #333; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /imports/ui/pages/RecoverPassword/index.js: -------------------------------------------------------------------------------- 1 | import RecoverPassword from './RecoverPassword.js'; 2 | 3 | export default RecoverPassword; 4 | -------------------------------------------------------------------------------- /imports/ui/pages/ResetPassword/ResetPassword.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './ResetPassword.scss'; 4 | 5 | const RecoverPassword = () => ( 6 |
    7 |

    Recover Password Page

    8 |
    9 | ); 10 | 11 | export default RecoverPassword; 12 | -------------------------------------------------------------------------------- /imports/ui/pages/ResetPassword/ResetPassword.scss: -------------------------------------------------------------------------------- 1 | .reset-password-page { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 80vh; 6 | font-size: 44px; 7 | h1 { 8 | color: #333; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /imports/ui/pages/ResetPassword/index.js: -------------------------------------------------------------------------------- 1 | import ResetPassword from './ResetPassword.js'; 2 | 3 | export default ResetPassword; 4 | -------------------------------------------------------------------------------- /imports/ui/pages/Signup/Signup.js: -------------------------------------------------------------------------------- 1 | import { Accounts } from 'meteor/accounts-base'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { NavLink } from 'react-router-dom'; 5 | 6 | // import components 7 | import Alert from '../../components/Alert'; 8 | 9 | // import styles 10 | import './Signup.scss'; 11 | 12 | class Signup extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | email: '', 17 | password: '', 18 | errMsg: '', 19 | }; 20 | this.handleSubmit = this.handleSubmit.bind(this); 21 | } 22 | 23 | componentDidMount() { 24 | if (this.props.loggedIn) { 25 | return this.props.history.push('/profile'); 26 | } 27 | } 28 | 29 | shouldComponentUpdate(nextProps) { 30 | if (nextProps.loggedIn) { 31 | nextProps.history.push('/profile'); 32 | return false; 33 | } 34 | return true; 35 | } 36 | 37 | handleSubmit(e) { 38 | e.preventDefault(); 39 | const { email, password } = this.state; 40 | Accounts.createUser({ email, password }, err => { 41 | if (err) { 42 | this.setState({ errMsg: err.reason }); 43 | return console.log(err); 44 | } 45 | }); 46 | } 47 | 48 | render() { 49 | if (this.props.loggedIn) { 50 | return null; 51 | } 52 | 53 | const { errMsg } = this.state; 54 | return ( 55 |
    56 |
    57 |
    58 |
    59 |
    60 | logo 65 |
    66 |
    67 |
    68 |

    Sign up

    69 |
    70 |
    71 | 72 | 73 | this.setState({ email: e.target.value })} 80 | required 81 | /> 82 |
    83 | 84 |
    85 | 86 | this.setState({ password: e.target.value })} 93 | required 94 | /> 95 |
    96 |
    97 | 101 |
    102 |
    103 | 109 | {errMsg && } 110 |
    111 |
    112 | Already have an account? Login 113 |
    114 |
    115 |
    116 |
    117 |
    118 | © {new Date().getFullYear()} 119 |
    120 |
    121 |
    122 | ); 123 | } 124 | } 125 | 126 | Signup.propTypes = { 127 | loggedIn: PropTypes.bool.isRequired, 128 | history: PropTypes.shape({ 129 | push: PropTypes.func.isRequired, 130 | }).isRequired, 131 | }; 132 | 133 | export default Signup; 134 | -------------------------------------------------------------------------------- /imports/ui/pages/Signup/Signup.scss: -------------------------------------------------------------------------------- 1 | .signup-page { 2 | margin-top: 15px; 3 | .form-signin { 4 | width: 100%; 5 | max-width: 330px; 6 | padding: 15px; 7 | margin: 0 auto; 8 | } 9 | .form-signin .checkbox { 10 | font-weight: 400; 11 | } 12 | .form-signin .form-control { 13 | position: relative; 14 | box-sizing: border-box; 15 | height: auto; 16 | padding: 10px; 17 | font-size: 16px; 18 | } 19 | .form-signin .form-control:focus { 20 | z-index: 2; 21 | } 22 | .form-signin input[type='email'] { 23 | margin-bottom: -1px; 24 | border-bottom-right-radius: 0; 25 | border-bottom-left-radius: 0; 26 | } 27 | .form-signin input[type='password'] { 28 | margin-bottom: 10px; 29 | border-top-left-radius: 0; 30 | border-top-right-radius: 0; 31 | } 32 | .spread-container { 33 | display: flex; 34 | justify-content: space-between; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /imports/ui/pages/Signup/index.js: -------------------------------------------------------------------------------- 1 | import Signup from './Signup.js'; 2 | 3 | export default Signup; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meteor-bootstrap4-react-router4-template", 3 | "private": true, 4 | "scripts": { 5 | "start": "meteor run", 6 | "test": "npm run test-server && npm run test-client", 7 | "test-server": "TEST_CLIENT=0 SERVER_TEST_REPORTER=\"list\" meteor test --once --driver-package meteortesting:mocha --port 3100", 8 | "test-client": "NODE_ENV=UNIT_TESTING jest", 9 | "prettier": "prettier --write \"client/**/*.js\" \"imports/**/*.js\" \"server/**/*.js\" --single-quote true --trailing-comma es5" 10 | }, 11 | "jest": { 12 | "setupTestFrameworkScriptFile": "./tests/enzyme-config.js", 13 | "moduleNameMapper": { 14 | "^.+\\.(css|less|scss)$": "babel-jest" 15 | } 16 | }, 17 | "dependencies": { 18 | "@babel/runtime": "^7.8.7", 19 | "@popperjs/core": "^2.1.1", 20 | "bcrypt": "^4.0.1", 21 | "bootstrap": "^4.4.1", 22 | "jquery": "^3.4.1", 23 | "lodash": "^4.17.15", 24 | "meteor-node-stubs": "^1.0.0", 25 | "popper.js": "^1.16.1", 26 | "prop-types": "^15.7.2", 27 | "react": "^16.13.0", 28 | "react-dom": "^16.13.0", 29 | "react-router-dom": "^5.1.2", 30 | "recompose": "^0.30.0", 31 | "simpl-schema": "^1.5.7" 32 | }, 33 | "devDependencies": { 34 | "@babel/preset-env": "^7.8.7", 35 | "@meteorjs/eslint-config-meteor": "^1.0.5", 36 | "autoprefixer": "^9.7.4", 37 | "babel-core": "^6.26.3", 38 | "babel-eslint": "^10.1.0", 39 | "babel-jest": "^25.1.0", 40 | "babel-preset-react": "^6.24.1", 41 | "chromedriver": "^80.0.1", 42 | "enzyme": "^3.11.0", 43 | "enzyme-adapter-react-16": "^1.15.2", 44 | "eslint": "^6.8.0", 45 | "eslint-config-airbnb": "^18.1.0", 46 | "eslint-config-airbnb-base": "^14.1.0", 47 | "eslint-config-prettier": "^6.10.0", 48 | "eslint-import-resolver-meteor": "^0.4.0", 49 | "eslint-plugin-import": "^2.20.1", 50 | "eslint-plugin-jsx-a11y": "^6.2.3", 51 | "eslint-plugin-meteor": "^6.0.0", 52 | "eslint-plugin-prettier": "^3.1.2", 53 | "eslint-plugin-react": "^7.19.0", 54 | "eslint-plugin-react-hooks": "^2.5.0", 55 | "jest": "^25.1.0", 56 | "prettier": "^1.19.1", 57 | "regenerator-runtime": "^0.13.5" 58 | }, 59 | "postcss": { 60 | "plugins": { 61 | "autoprefixer": { 62 | "browserlist": [ 63 | "last 2 versions" 64 | ] 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /private/README.md: -------------------------------------------------------------------------------- 1 | **private folder** 2 | 3 | All files inside a top-level directory called `private/` are only accessible from server code and can be loaded via the [`Assets`](http://docs.meteor.com/#/full/assets_getText) API. This can be used for private data files and any files that are in your project directory that you don't want to be accessible from the outside. 4 | -------------------------------------------------------------------------------- /public/README.md: -------------------------------------------------------------------------------- 1 | **public folder** 2 | 3 | All files inside a top-level directory called `public/` are accessible on the client. 4 | 5 | For example a file located here: `public/some-image.png` 6 | 7 | Is accessible here: **http://domain.com/some-image.png** 8 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | // Server entry point, imports all server code 2 | 3 | import '/imports/startup/server'; 4 | import '/imports/startup/both'; 5 | -------------------------------------------------------------------------------- /tests/enzyme-config.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | --------------------------------------------------------------------------------