├── .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 | [](https://github.com/airbnb/javascript)
2 | [](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 |
47 |
48 |
49 |
50 |
51 | {title}
52 |
53 |
61 |
62 |
63 | Meteor.userId():
{body}
64 |
65 |
66 | Meteor.user():
{' '}
67 |
68 | {' '}
69 | {JSON.stringify(Meteor.user(), null, 2)}
70 |
71 | Counter:
{' '}
72 |
73 | {' '}
74 | {JSON.stringify(counter, null, 2)}
75 |
76 |
77 |
78 |
85 |
86 |
87 |
88 |
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 |
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 |
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 |

64 |
65 |
66 |
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 |
60 | {countersReady && (
61 |
67 | )}
68 |
69 | {countersReady &&
}
70 |
71 |
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 |

65 |
66 |
67 |
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 |
--------------------------------------------------------------------------------