├── .gitignore
├── .meteor
├── .finished-upgraders
├── .gitignore
├── .id
├── packages
├── platforms
├── release
└── versions
├── README.md
├── client
├── main.html
├── main.js
└── main.scss
├── imports
├── api
│ ├── test
│ │ ├── methods.js
│ │ ├── server
│ │ │ ├── indexes.js
│ │ │ └── publications.js
│ │ └── tests.js
│ └── users
│ │ ├── methods.js
│ │ └── server
│ │ ├── helper.js
│ │ ├── indexes.js
│ │ └── publications.js
├── helpers
│ ├── call-with-promise.js
│ ├── react-loadable
│ │ ├── LoadableWrapper.js
│ │ └── loading.js
│ └── server
│ │ └── explainQuery.js
├── startup
│ ├── both
│ │ └── routes.js
│ ├── client
│ │ └── index.js
│ └── server
│ │ ├── database-indexes.js
│ │ ├── index.js
│ │ ├── register-api.js
│ │ ├── services.js
│ │ └── ssr-init.js
└── ui
│ ├── components
│ ├── accounts
│ │ ├── changePassword.js
│ │ ├── forgotPassword.js
│ │ ├── login.js
│ │ ├── login.scss
│ │ ├── profile
│ │ │ └── edit.js
│ │ ├── register.js
│ │ ├── registered.js
│ │ ├── resetPassword.js
│ │ └── verifyEmail.js
│ └── test
│ │ ├── simpleSchema.js
│ │ └── syncMethodCall.js
│ ├── helpers
│ ├── alerts.js
│ └── materialcss.js
│ ├── layouts
│ ├── admin
│ │ └── admin.js
│ └── site
│ │ ├── site.js
│ │ └── site.scss
│ └── pages
│ ├── accounts
│ ├── accounts.js
│ └── accounts.scss
│ ├── admin
│ └── dashboard
│ │ └── dashboard.js
│ ├── home
│ └── home.js
│ ├── notFound
│ └── notFound.js
│ └── test
│ └── test.js
├── package-lock.json
├── package.json
├── scss-config.json
└── server
└── main.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 | secb4ts17qw.vwcvkfg6cli
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.0.5 # Packages for a great mobile UX
9 | mongo@1.6.0-rc18.16 # 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 |
14 | standard-minifier-js@2.4.0-rc18.16 # JS minifier run for production mode
15 | es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers
16 | ecmascript@0.12.0-rc18.16 # Enable ECMAScript2015+ syntax in app code
17 | shell-server@0.4.0-rc18.16 # Server-side component of the `meteor shell` command
18 | server-render@0.3.1
19 | fourseven:scss
20 | dynamic-import@0.5.0-rc18.16
21 | react-meteor-data
22 | minifier-css@1.4.0-rc18.16
23 | juliancwirko:postcss
24 | accounts-base@1.4.3-rc18.16
25 | accounts-password@1.5.1
26 | email@1.2.3
27 | service-configuration@1.0.11
28 | accounts-facebook@1.3.2-rc18.16
29 | accounts-google@1.3.2-rc18.16
30 | underscore@1.0.10
31 |
--------------------------------------------------------------------------------
/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@1.8-rc.16
2 |
--------------------------------------------------------------------------------
/.meteor/versions:
--------------------------------------------------------------------------------
1 | accounts-base@1.4.3-rc18.16
2 | accounts-facebook@1.3.2-rc18.16
3 | accounts-google@1.3.2-rc18.16
4 | accounts-oauth@1.1.16-rc18.16
5 | accounts-password@1.5.1
6 | allow-deny@1.1.0
7 | autoupdate@1.5.0-rc18.16
8 | babel-compiler@7.2.0-rc18.16
9 | babel-runtime@1.3.0-rc18.16
10 | base64@1.0.11
11 | binary-heap@1.0.11-rc18.16
12 | blaze-tools@1.0.10
13 | boilerplate-generator@1.6.0-rc18.16
14 | caching-compiler@1.2.0-rc18.16
15 | caching-html-compiler@1.1.3
16 | callback-hook@1.1.0
17 | check@1.3.1
18 | ddp@1.4.0
19 | ddp-client@2.3.3
20 | ddp-common@1.4.0
21 | ddp-rate-limiter@1.0.7
22 | ddp-server@2.2.0
23 | deps@1.0.12
24 | diff-sequence@1.1.0
25 | dynamic-import@0.5.0-rc18.16
26 | ecmascript@0.12.0-rc18.16
27 | ecmascript-runtime@0.7.0
28 | ecmascript-runtime-client@0.8.0-rc18.16
29 | ecmascript-runtime-server@0.7.1
30 | ejson@1.1.0
31 | email@1.2.3
32 | es5-shim@4.8.0
33 | facebook-oauth@1.5.0
34 | fetch@0.1.0
35 | fourseven:scss@4.9.3
36 | geojson-utils@1.0.10
37 | google-oauth@1.2.6-rc18.16
38 | hot-code-push@1.0.4
39 | html-tools@1.0.11
40 | htmljs@1.0.11
41 | http@1.4.1
42 | id-map@1.1.0
43 | inter-process-messaging@0.1.0-rc18.16
44 | juliancwirko:postcss@1.3.0
45 | launch-screen@1.1.1
46 | livedata@1.0.18
47 | localstorage@1.2.0
48 | logging@1.1.20
49 | meteor@1.9.2
50 | meteor-base@1.4.0
51 | minifier-css@1.4.0-rc18.16
52 | minifier-js@2.4.0-rc18.16
53 | minimongo@1.4.5
54 | mobile-experience@1.0.5
55 | mobile-status-bar@1.0.14
56 | modern-browsers@0.1.2
57 | modules@0.13.0-rc18.16
58 | modules-runtime@0.10.2
59 | mongo@1.6.0-rc18.16
60 | mongo-decimal@0.1.0
61 | mongo-dev-server@1.1.0
62 | mongo-id@1.0.7
63 | npm-bcrypt@0.9.3
64 | npm-mongo@3.1.1-rc18.16
65 | oauth@1.2.3
66 | oauth2@1.2.1-rc18.16
67 | ordered-dict@1.1.0
68 | promise@0.11.1
69 | random@1.1.0
70 | rate-limit@1.0.9
71 | react-meteor-data@0.2.16
72 | reactive-var@1.0.11
73 | reload@1.2.0
74 | retry@1.1.0
75 | routepolicy@1.1.0-rc18.16
76 | server-render@0.3.1
77 | service-configuration@1.0.11
78 | sha@1.0.9
79 | shell-server@0.4.0-rc18.16
80 | socket-stream-client@0.2.2
81 | spacebars-compiler@1.1.3
82 | srp@1.0.12
83 | standard-minifier-js@2.4.0-rc18.16
84 | static-html@1.2.2
85 | templating-tools@1.1.2
86 | tmeasday:check-npm-versions@0.3.2
87 | tracker@1.2.0
88 | underscore@1.0.10
89 | url@1.2.0
90 | webapp@1.7.0-rc18.16
91 | webapp-hashing@1.0.9
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # meteor-react-ssr
2 | kind of boilerplate
3 |
4 | * meteor 1.7
5 | * react router v4
6 | * server side render
7 | * react-loadable - component based code splitting
8 | * react-helmet
9 | * postcss with autoprefixer
10 | * materializecss (template bootstrap)
11 | * eslint (coding styles)
12 |
13 | ## how to use this boilerplate
14 | - Clone: `git clone https://github.com/minhna/meteor-react-ssr.git`
15 | - Go inside the cloned directory
16 | - Remove the `.git` directory: `rm -rf .git`
17 | - Install npm libraries: `meteor npm install`
18 | - Run the meteor app: `meteor npm start`
19 |
20 | I created a `/test` page to test a couple of useful stuffs.
21 |
22 | ## demo
23 | http://meteor-ssr-loadable.minhnguyen.me/
24 |
--------------------------------------------------------------------------------
/client/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/client/main.js:
--------------------------------------------------------------------------------
1 | import '/imports/startup/client/index.js';
2 |
--------------------------------------------------------------------------------
/client/main.scss:
--------------------------------------------------------------------------------
1 | @import 'materialize.scss';
2 | @import '../imports/ui/layouts/site/site.scss';
3 |
--------------------------------------------------------------------------------
/imports/api/test/methods.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
3 |
4 | import Tests from './tests.js';
5 |
6 | Meteor.methods({
7 | 'test.task': async ({ index }) => {
8 | // console.log('do task with index: '+index);
9 |
10 | function waitXSeconds(x) {
11 | return new Promise((resolve) => {
12 | setTimeout(() => {
13 | resolve(x);
14 | }, x * 1000);
15 | });
16 | }
17 |
18 | await waitXSeconds(2);
19 | return index;
20 | },
21 |
22 | 'test.insert': ({ data }) => {
23 | const validationContext = Tests.schema.newContext();
24 | validationContext.validate(data);
25 | if (validationContext.isValid()) {
26 | const data2 = data;
27 | data2.createdAt = new Date();
28 | Tests.insert(data2);
29 | } else {
30 | // do something
31 | const errors = validationContext.validationErrors().map(elm => `${elm.name} is ${elm.type}, actual value: ${elm.value}`);
32 | throw new Meteor.Error('001', errors[0]);
33 | // console.log(validationContext.validationErrors());
34 | }
35 |
36 | return 'finished';
37 | },
38 | });
39 |
40 | const testInsertRule = {
41 | type: 'method',
42 | name: 'test.insert',
43 | };
44 | DDPRateLimiter.addRule(testInsertRule, 2, 10000);
45 |
--------------------------------------------------------------------------------
/imports/api/test/server/indexes.js:
--------------------------------------------------------------------------------
1 | import Tests from '../tests.js';
2 |
3 | Tests._ensureIndex({ _id: 1, owner: 1 });
4 |
--------------------------------------------------------------------------------
/imports/api/test/server/publications.js:
--------------------------------------------------------------------------------
1 | // All links-related publications
2 |
3 | import { Meteor } from 'meteor/meteor';
4 | // import { check, Match } from 'meteor/check';
5 |
6 | // for development only
7 | import { getIndexes, explainQuery } from '/imports/helpers/server/explainQuery.js';
8 |
9 | import Tests from '../tests.js';
10 |
11 | Meteor.publish('tests.all', function () {
12 | return Tests.find({});
13 | });
14 |
15 | Meteor.publish('tests.mine', function () {
16 | if (!this.userId) {
17 | return this.ready();
18 | }
19 |
20 | const query = {
21 | owner: this.userId,
22 | };
23 |
24 | const options = {
25 | fields: { createdAt: 0 },
26 | };
27 |
28 | // for development only
29 | getIndexes(Tests);
30 | explainQuery(Tests, query, options);
31 |
32 | return Tests.find(query, options);
33 | });
34 |
35 | Meteor.publish('tests.mine.noReactive', function () {
36 | if (!this.userId) {
37 | return this.ready();
38 | }
39 |
40 | const query = {
41 | owner: this.userId,
42 | };
43 |
44 | const options = {
45 | fields: { createdAt: 0 },
46 | };
47 |
48 | const tests = Tests.find(query, options);
49 | tests.forEach(test => this.added('tests', test._id, test));
50 | this.ready();
51 | });
52 |
--------------------------------------------------------------------------------
/imports/api/test/tests.js:
--------------------------------------------------------------------------------
1 | import SimpleSchema from 'simpl-schema';
2 | import { Mongo } from 'meteor/mongo';
3 |
4 | const Tests = new Mongo.Collection('tests');
5 |
6 | Tests.schema = new SimpleSchema({
7 | createdAt: {
8 | type: String,
9 | label: 'The date this document was created.',
10 | optional: true,
11 | },
12 | updatedAt: {
13 | type: String,
14 | label: 'The date this document was last updated.',
15 | optional: true,
16 | },
17 | title: {
18 | type: String,
19 | label: 'The title of the document.',
20 | },
21 | body: {
22 | type: String,
23 | label: 'The body of the document.',
24 | },
25 | favorites: {
26 | type: Array,
27 | label: 'Users who have favorited this document.',
28 | defaultValue: [],
29 | optional: true,
30 | },
31 | 'favorites.$': {
32 | type: String,
33 | label: 'A user who has favorited this document.',
34 | optional: true,
35 | },
36 | });
37 |
38 | export default Tests;
39 |
--------------------------------------------------------------------------------
/imports/api/users/methods.js:
--------------------------------------------------------------------------------
1 | // Methods related to users
2 |
3 | import { Meteor } from 'meteor/meteor';
4 | import { Email } from 'meteor/email';
5 | import { Match } from 'meteor/check';
6 | import { Accounts } from 'meteor/accounts-base';
7 | // import moment from 'moment';
8 |
9 | // import { Permissions } from '/imports/common/helpers/permissions';
10 | // import { Rules } from '/imports/api/rules/rules.js';
11 | // import { UserHelper } from '/imports/helpers/user.js';
12 |
13 | Meteor.methods({
14 | 'users.register': ({ email, password }) => {
15 | if (!Match.test(email, String)) {
16 | throw Meteor.Error('users.create.3', 'Invalid Email');
17 | }
18 | // check if email is existing
19 | if (Meteor.users.findOne({ email })) {
20 | throw Meteor.Error('users.create.4', 'Email is existing');
21 | }
22 |
23 | if (!Match.test(password, String)) {
24 | throw Meteor.Error('users.create.7', 'Invalid Password');
25 | }
26 |
27 | // console.log(email, password);
28 | if (Meteor.isServer) {
29 | const userId = Accounts.createUser({ email, password });
30 | if (userId) {
31 | // send confirmation email
32 | const { email: realEmail, user, token } =
33 | Accounts.generateVerificationToken(userId, email);
34 | const url = Meteor.absoluteUrl(`accounts/verify-email/${token}`);
35 | const options = Accounts.generateOptionsForEmail(realEmail, user, url, 'verifyEmail');
36 | Email.send(options);
37 |
38 | return userId;
39 | }
40 | }
41 |
42 | return false;
43 | },
44 |
45 | 'users.forgotPassword': ({ email }) => {
46 | if (!Match.test(email, String)) {
47 | throw Meteor.Error('users.forgotPassword.1', 'Invalid Email');
48 | }
49 |
50 | if (Meteor.isServer) {
51 | const userToReset = Accounts.findUserByEmail(email);
52 | if (userToReset) {
53 | // Accounts.sendResetPasswordEmail(user._id, email);
54 | const { email: realEmail, user, token } = Accounts.generateResetToken(userToReset._id, email, 'resetPassword');
55 | // console.log(realEmail, user, token);
56 | // send email
57 | const url = Meteor.absoluteUrl(`accounts/reset-password/${token}`);
58 | const options = Accounts.generateOptionsForEmail(realEmail, user, url, 'resetPassword');
59 | // console.log(options);
60 | Email.send(options);
61 | }
62 | }
63 |
64 | return true;
65 | },
66 | });
67 |
--------------------------------------------------------------------------------
/imports/api/users/server/helper.js:
--------------------------------------------------------------------------------
1 |
2 | const UsersHelper = {
3 | };
4 |
5 | export default UsersHelper;
6 |
--------------------------------------------------------------------------------
/imports/api/users/server/indexes.js:
--------------------------------------------------------------------------------
1 | // Meteor.users._ensureIndex({"profile.farmId": 1});
2 |
--------------------------------------------------------------------------------
/imports/api/users/server/publications.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | // import { check, Match } from 'meteor/check';
3 |
4 | Meteor.publish('users.current', function () {
5 | return Meteor.users.find({ _id: this.userId }, {
6 | fields: {
7 | profile: 1,
8 | roles: 1,
9 | },
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/imports/helpers/call-with-promise.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { Promise } from 'meteor/promise';
3 |
4 | const callWithPromise = (method, params) => {
5 | const methodPromise = new Promise((resolve, reject) => {
6 | Meteor.call(method, params, (error, result) => {
7 | if (error) reject(error);
8 | resolve(result);
9 | });
10 | });
11 | return methodPromise;
12 | };
13 |
14 | export default callWithPromise;
15 |
--------------------------------------------------------------------------------
/imports/helpers/react-loadable/LoadableWrapper.js:
--------------------------------------------------------------------------------
1 | import Loadable from 'react-loadable';
2 |
3 | import Loading from './loading.js';
4 |
5 | export default function LoadableWrapper(opts) {
6 | return Loadable({
7 | loading: Loading,
8 | delay: 200,
9 | ...opts,
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/imports/helpers/react-loadable/loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const Loading = (props) => {
5 | const {
6 | error, timedOut, pastDelay, retry,
7 | } = props;
8 | if (error) {
9 | return (
10 |
11 | {`Error! ${error.message || ''} `}
12 |
13 |
14 | );
15 | }
16 | if (timedOut) {
17 | return Taking a long time...
;
18 | }
19 | if (pastDelay) {
20 | return Loading...
;
21 | }
22 |
23 | return null;
24 | };
25 |
26 | Loading.propTypes = {
27 | error: PropTypes.shape({
28 | message: PropTypes.string,
29 | }),
30 | timedOut: PropTypes.bool,
31 | pastDelay: PropTypes.bool,
32 | retry: PropTypes.func,
33 | };
34 |
35 | Loading.defaultProps = {
36 | error: null,
37 | timedOut: false,
38 | pastDelay: false,
39 | retry: () => {},
40 | };
41 |
42 | export default Loading;
43 |
--------------------------------------------------------------------------------
/imports/helpers/server/explainQuery.js:
--------------------------------------------------------------------------------
1 |
2 | export const explainQuery = async (collection, query, options) => {
3 | console.log(`explainQuery on ${collection._name}:`);
4 | const raw = collection.rawCollection();
5 | const result = await raw.find(query, options).explain();
6 | console.log(JSON.stringify(result, null, 2));
7 | console.log('///////////////////////');
8 | };
9 |
10 | export const getIndexes = async (collection) => {
11 | console.log(`Indexes on ${collection._name}:`);
12 | const raw = collection.rawCollection();
13 | const result = await raw.indexes();
14 | console.log(JSON.stringify(result, null, 2));
15 | console.log('///////////////////////');
16 | };
17 |
--------------------------------------------------------------------------------
/imports/startup/both/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Switch } from 'react-router-dom';
3 |
4 | import LoadableWrapper from '/imports/helpers/react-loadable/LoadableWrapper.js';
5 |
6 | const LoadableAdminLayout = LoadableWrapper({
7 | loader: () => import('/imports/ui/layouts/admin/admin.js'),
8 | modules: ['/imports/ui/layouts/admin/admin.js'],
9 | });
10 | const LoadableSiteLayout = LoadableWrapper({
11 | loader: () => import('/imports/ui/layouts/site/site.js'),
12 | modules: ['/imports/ui/layouts/site/site.js'],
13 | });
14 |
15 | export default (
16 |
17 |
18 |
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/imports/startup/client/index.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import { Router } from 'react-router-dom';
5 | import createHistory from 'history/createBrowserHistory';
6 | import { onPageLoad } from 'meteor/server-render';
7 |
8 | if (Meteor.isClient) {
9 | import Materialize from 'materialize-css';
10 | // set global (that way we don´t need to import in every file)
11 | global.M = Materialize;
12 | global.Materialize = Materialize;
13 | }
14 |
15 | const history = createHistory();
16 |
17 | onPageLoad(async () => {
18 | const routes = (await import('../both/routes.js')).default;
19 | const App = () => (
20 |
21 | {routes}
22 |
23 | );
24 | ReactDOM.hydrate(, document.getElementById('app'));
25 | });
26 |
--------------------------------------------------------------------------------
/imports/startup/server/database-indexes.js:
--------------------------------------------------------------------------------
1 | import '/imports/api/test/server/indexes.js';
2 |
--------------------------------------------------------------------------------
/imports/startup/server/index.js:
--------------------------------------------------------------------------------
1 | import './database-indexes.js';
2 | import './register-api.js';
3 | import './ssr-init.js';
4 | import './services.js';
5 |
--------------------------------------------------------------------------------
/imports/startup/server/register-api.js:
--------------------------------------------------------------------------------
1 | import '/imports/api/test/methods.js';
2 | import '/imports/api/test/server/publications.js';
3 | import '/imports/api/users/methods.js';
4 | import '/imports/api/users/server/publications.js';
5 |
--------------------------------------------------------------------------------
/imports/startup/server/services.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { ServiceConfiguration } from 'meteor/service-configuration';
3 |
4 | Meteor.startup(function() {
5 | ServiceConfiguration.configurations.upsert(
6 | { service: 'facebook' },
7 | {
8 | $set: {
9 | loginStyle: 'popup',
10 | appId: 'THE_APP_ID', // See table below for correct property name!
11 | secret: 'THE_KEY',
12 | },
13 | },
14 | );
15 | ServiceConfiguration.configurations.upsert(
16 | { service: 'google' },
17 | {
18 | $set: {
19 | loginStyle: 'popup',
20 | clientId: 'THE_CLIENT_ID', // See table below for correct property name!
21 | secret: 'SOME_KEY',
22 | },
23 | },
24 | );
25 | });
26 |
--------------------------------------------------------------------------------
/imports/startup/server/ssr-init.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { renderToNodeStream, renderToString } from 'react-dom/server';
3 | import { onPageLoad } from 'meteor/server-render';
4 | import { StaticRouter } from 'react-router';
5 | import { Helmet } from 'react-helmet';
6 | import Loadable from 'react-loadable';
7 |
8 | onPageLoad(async (sink) => {
9 | const context = {};
10 |
11 | const routes = (await import('../both/routes.js')).default;
12 |
13 | const App = props => (
14 |
15 | {routes}
16 |
17 | );
18 |
19 | const modules = [];
20 | // const html = renderToNodeStream((
21 | const html = renderToString((
22 | { modules.push(moduleName); }}>
23 |
24 |
25 | ));
26 |
27 | // we have a list of modules here, hopefully Meteor will allow to add them to bundle
28 | // console.log(modules);
29 |
30 | sink.renderIntoElementById('app', html);
31 |
32 | const helmet = Helmet.renderStatic();
33 | sink.appendToHead(helmet.meta.toString());
34 | sink.appendToHead(helmet.title.toString());
35 | sink.appendToHead(helmet.link.toString());
36 | });
37 |
--------------------------------------------------------------------------------
/imports/ui/components/accounts/changePassword.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import { Accounts } from 'meteor/accounts-base';
4 |
5 | import { showError, showSuccess } from '/imports/ui/helpers/alerts.js';
6 | import MaterialHelper from '/imports/ui/helpers/materialcss.js';
7 |
8 | class ChangePasswordForm extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | currentPassword: '',
13 | password: '',
14 | password2: '',
15 | loading: false,
16 | };
17 |
18 | this.fields = {};
19 | }
20 |
21 | componentWillMount() {
22 | if (!Meteor.userId()) {
23 | // user must login to use this feature
24 | this.props.history.push('/accounts/login');
25 | Meteor.setTimeout(() => {
26 | showError('Please login first');
27 | }, 100);
28 | }
29 | }
30 |
31 | componentDidMount() {
32 |
33 | }
34 |
35 | onChangePassword(e) {
36 | e.preventDefault();
37 | const checkResult = MaterialHelper.checkAll(this.fields);
38 | // console.log(checkResult);
39 | if (checkResult !== true) {
40 | console.log(checkResult);
41 | return;
42 | }
43 |
44 | // check new password confirmation
45 | if (this.state.password !== this.state.password2) {
46 | showError('New password confirmation doesn\'t match');
47 | return;
48 | }
49 |
50 | if (this.state.loading === true) {
51 | console.log('loading');
52 | return;
53 | }
54 |
55 | this.setState({
56 | loading: true,
57 | });
58 |
59 | Accounts.changePassword(this.state.currentPassword, this.state.password, (err) => {
60 | this.setState({
61 | loading: false,
62 | });
63 |
64 | if (err) {
65 | if (err.message) {
66 | showError(err.message);
67 | } else {
68 | console.log(err);
69 | }
70 | } else {
71 | Meteor.logoutOtherClients();
72 | Meteor.logout((err2) => {
73 | if (err2) {
74 | if (err2.message) {
75 | showError(err2.message);
76 | } else {
77 | console.log(err2);
78 | }
79 | } else {
80 | // send user to login page
81 | this.props.history.push('/accounts/login');
82 | Meteor.setTimeout(() => {
83 | showSuccess('Password changed successfully. Please login again.');
84 | }, 100);
85 | }
86 | });
87 | }
88 | });
89 | }
90 |
91 | onCancel(e) {
92 | e.preventDefault();
93 | // send user to profile page
94 | this.props.history.push('/accounts/profile');
95 | }
96 |
97 | onFieldChange(field, value) {
98 | // console.log(field, value);
99 | const setObj = {};
100 | setObj[field] = value;
101 | this.setState(setObj, () => {
102 | if (field === 'password' || field === 'password2') {
103 | this.validatePassword();
104 | }
105 | });
106 | }
107 |
108 | validatePassword() {
109 | let passwordValid = true;
110 | let password2Valid = true;
111 | if (this.state.password === '') {
112 | passwordValid = false;
113 | } else {
114 | passwordValid = true;
115 | }
116 |
117 | if (this.state.password2 === '') {
118 | password2Valid = false;
119 | } else if (this.state.password2 !== this.state.password) {
120 | password2Valid = false;
121 | } else {
122 | password2Valid = true;
123 | }
124 |
125 | $(this.fields.password).addClass(passwordValid ? 'valid' : 'invalid');
126 | $(this.fields.password).removeClass(passwordValid ? 'invalid' : 'valid');
127 | $(this.fields.password2).addClass(password2Valid ? 'valid' : 'invalid');
128 | $(this.fields.password2).removeClass(password2Valid ? 'invalid' : 'valid');
129 | }
130 |
131 | render() {
132 | let submitBtnClass = 'waves-effect waves-light btn';
133 | if (this.state.loading) {
134 | submitBtnClass += ' disabled';
135 | }
136 |
137 | return (
138 |
139 |
Change Password
140 |
196 |
197 | );
198 | }
199 | }
200 |
201 | export default ChangePasswordForm;
202 |
--------------------------------------------------------------------------------
/imports/ui/components/accounts/forgotPassword.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import { showError } from '/imports/ui/helpers/alerts.js';
4 | import MaterialHelper from '/imports/ui/helpers/materialcss.js';
5 |
6 | class ForgotPasswordForm extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | emailSent: false,
11 | };
12 |
13 | this.fields = {};
14 | }
15 |
16 | onResetPasswordClick(e) {
17 | e.preventDefault();
18 | const isValid = MaterialHelper.checkAll(this.fields);
19 | if (isValid !== true) {
20 | return;
21 | }
22 |
23 | // call the method to send reset password link
24 | Meteor.call('users.forgotPassword', { email: $(this.fields.email)[0].value }, (error, result) => {
25 | if (error) {
26 | showError(error.message);
27 | }
28 | if (result) {
29 | this.setState({
30 | emailSent: true,
31 | });
32 | }
33 | });
34 | }
35 |
36 | onBackClick(e) {
37 | e.preventDefault();
38 | this.props.history.push('/accounts/login');
39 | }
40 |
41 | renderResetForm() {
42 | return (
43 |
44 |
Reset Password
45 |
75 |
76 | );
77 | }
78 |
79 | renderResultForm() {
80 | return (
81 |
82 |
Please check email
83 |
An email which has reset pasword link has sent to you. Please check your email.
84 |
85 | );
86 | }
87 |
88 | render() {
89 | return (
90 |
91 | {this.state.emailSent ? this.renderResultForm() : this.renderResetForm()}
92 |
93 | );
94 | }
95 | }
96 |
97 | export default ForgotPasswordForm;
98 |
--------------------------------------------------------------------------------
/imports/ui/components/accounts/login.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import React, { Component } from 'react';
3 | import { Link } from 'react-router-dom';
4 | import { withTracker } from 'meteor/react-meteor-data';
5 | import { Accounts } from 'meteor/accounts-base';
6 |
7 | import MaterialHelper from '/imports/ui/helpers/materialcss.js';
8 | import { showError } from '/imports/ui/helpers/alerts.js';
9 |
10 | class LoginForm extends Component {
11 | constructor(props) {
12 | super(props);
13 |
14 | this.state = {
15 | email: '',
16 | password: '',
17 | };
18 |
19 | this.fields = {};
20 | }
21 |
22 | onFieldChange(field, value) {
23 | // console.log(field, value);
24 | const setObj = {};
25 | setObj[field] = value;
26 |
27 | this.setState(setObj);
28 | }
29 |
30 | onRegister(e) {
31 | e.preventDefault();
32 | this.props.history.push('/accounts/register');
33 | }
34 |
35 | onLogin(e) {
36 | e.preventDefault();
37 |
38 | const checkResult = MaterialHelper.checkAll(this.fields);
39 | // console.log(checkResult);
40 | if (checkResult !== true) {
41 | // console.log(checkResult);
42 | return;
43 | }
44 |
45 | let redirectURL = '/';
46 | if (this.props.match.params && this.props.match.params.redirect) {
47 | redirectURL = decodeURIComponent(this.props.match.params.redirect);
48 | }
49 |
50 | Meteor.loginWithPassword({ email: this.state.email }, this.state.password, (err) => {
51 | if (err) {
52 | // console.log(err);
53 | showError(err.message);
54 | } else {
55 | // send user to redirect url
56 | this.props.history.push(redirectURL);
57 | }
58 | });
59 | }
60 |
61 | onLogout(e) {
62 | e.preventDefault();
63 | Meteor.logout((error) => {
64 | if (error) {
65 | showError(error.message);
66 | } else {
67 | // send user to login page
68 | this.props.history.push('/accounts/login');
69 | }
70 | });
71 | }
72 |
73 | onLoginWithFacebook(e) {
74 | e.preventDefault();
75 | Meteor.loginWithFacebook({
76 | requestPermissions: ['public_profile'],
77 | auth_type: 'rerequest',
78 | });
79 | }
80 |
81 | onLoginWithGoogle(e) {
82 | e.preventDefault();
83 | Meteor.loginWithGoogle({
84 | requestPermissions: [],
85 | }, (error) => {
86 | if (error) {
87 | showError(error.message);
88 | }
89 | });
90 | }
91 |
92 | renderLoginWithServices() {
93 | if (this.props.loginServicesConfigured) {
94 | return (
95 |
96 |
97 |
103 |
104 |
105 |
111 |
112 |
113 | );
114 | }
115 |
116 | return null;
117 | }
118 |
119 | render() {
120 | if (this.props.loggedIn === true) {
121 | return (
122 |
123 |
Login
124 |
You are already logged in.
125 |
126 |
132 |
133 |
134 | );
135 | }
136 | return (
137 |
188 | );
189 | }
190 | }
191 |
192 | export default withTracker((props) => {
193 | const returnObj = {
194 | loggedIn: false,
195 | loginServicesConfigured: false,
196 | };
197 |
198 | returnObj.loginServicesConfigured = Accounts.loginServicesConfigured();
199 |
200 | // prevent problem with server-render
201 | if (Meteor.isServer) {
202 | return returnObj;
203 | }
204 | // console.log(Meteor.userId());
205 | if (Meteor.userId()) {
206 | returnObj.loggedIn = true;
207 | }
208 |
209 | return returnObj;
210 | })(LoginForm);
211 |
--------------------------------------------------------------------------------
/imports/ui/components/accounts/login.scss:
--------------------------------------------------------------------------------
1 | .login-page-wrapper {
2 | .col.s12 {
3 | .btn {
4 | margin-bottom: 20px;
5 | }
6 | }
7 |
8 | .input-field {
9 | .input-field-helper {
10 | position: absolute;
11 | right: 10px;
12 | top: 3.2rem;
13 | font-size: 12px;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/imports/ui/components/accounts/profile/edit.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { withTracker } from 'meteor/react-meteor-data';
3 |
4 | import { UserHelper } from '/imports/helpers/user.js';
5 | import MaterialHelper from '/imports/ui/helpers/materialcss.js';
6 |
7 | import { SelectBox } from '/imports/ui/components/common/form.js';
8 |
9 | import Loading from '/imports/ui/components/common/loading.js';
10 |
11 | class ProfileEdit extends Component {
12 |
13 | constructor(props){
14 | super(props);
15 | this.state = {
16 | firstName: '',
17 | lastName: '',
18 | phone: '',
19 | phoneCode: '',
20 | email: '',
21 | country: '',
22 | loading: false
23 | }
24 | }
25 |
26 | _updateState(user){
27 | if(user){
28 | const country = user.profile ? CountriesHelper.getCountryByShortCode(user.profile.country) : null;
29 | const phone = UserHelper.getPhone(user);
30 | const email = UserHelper.getEmail(user);
31 |
32 | this.setState({
33 | firstName: user.profile ? user.profile.firstName : '',
34 | lastName: user.profile ? user.profile.lastName : '',
35 | phone: phone ? phone.number : '',
36 | phoneCode: country ? country.phoneCode: '',
37 | email: email.address,
38 | country: country ? country.shortCode : ''
39 | }, ()=>{
40 | Materialize.updateTextFields();
41 | this.onCountryChange(this.state.country);
42 | });
43 | }
44 | }
45 |
46 | componentWillMount(){
47 | this._updateState(this.props.user);
48 | }
49 |
50 | componentWillReceiveProps(nextProps){
51 | this._updateState(nextProps.user);
52 | }
53 |
54 | onFirstNameChange(e){
55 | e.preventDefault();
56 | this.setState({
57 | firstName: e.target.value
58 | });
59 | }
60 |
61 | onLastNameChange(e){
62 | e.preventDefault();
63 | this.setState({
64 | lastName: e.target.value
65 | });
66 | }
67 |
68 | onEmailChange(e){
69 | e.preventDefault();
70 | this.setState({
71 | email: e.target.value
72 | });
73 | }
74 |
75 | onCancel(e){
76 | e.preventDefault();
77 | //send user to profile page
78 | this.props.params.history.push('/accounts/profile');
79 | }
80 |
81 | onSave(e){
82 | const checkResult = MaterialHelper.checkAll(this.refs);
83 | // console.log(checkResult);
84 | if(checkResult !== true){
85 | console.log(checkResult);
86 | return;
87 | }
88 |
89 | if(this.state.loading === true){
90 | console.log('loading');
91 | return;
92 | }
93 |
94 | this.setState({
95 | loading: true
96 | });
97 |
98 | Meteor.call("users.updateProfile", {
99 | firstName: this.state.firstName,
100 | lastName: this.state.lastName,
101 | email: this.state.email,
102 | country: this.state.country,
103 | phone: this.state.phone
104 | }, (error, result)=>{
105 | this.setState({
106 | loading: false
107 | });
108 | if(error){
109 | showError(error.message);
110 | }
111 | else {
112 | //send user to profile page
113 | this.props.params.history.push('/accounts/profile');
114 | Meteor.setTimeout(function(){
115 | showSuccess('Profile updated successfully');
116 | }, 100);
117 | }
118 | });
119 | }
120 |
121 | onCountryChange(shortCode){
122 | //get the country phone code
123 | const country = CountriesHelper.getCountryByShortCode(shortCode);
124 | const phoneCode = country ? country.phoneCode : '';
125 | let currentPhone = this.state.phone;
126 | //fist, remove current phone code from current phone
127 | const pat = new RegExp('^\\+'+this.state.phoneCode);
128 | currentPhone = currentPhone.replace(pat, "");
129 | //then add new phone code to the begining
130 | currentPhone = '+' + phoneCode + currentPhone;
131 |
132 | this.setState({
133 | country: shortCode,
134 | phoneCode: country ? country.phoneCode : '',
135 | phone: currentPhone
136 | }, ()=>{
137 | Materialize.updateTextFields();
138 | });
139 | }
140 |
141 | onPhoneChange(e){
142 | e.preventDefault();
143 | const pat = new RegExp('^\\+'+this.state.phoneCode);
144 | //check if the value contains phone code
145 | if(this.state.phoneCode){
146 | if(!pat.test(e.target.value)){
147 | return;
148 | }
149 | }
150 | //check invalid char
151 | const pat2 = new RegExp('[^0-9\\+\\- ]');
152 | if(pat2.test(e.target.value)){
153 | return;
154 | }
155 |
156 | let value = e.target.value;
157 | //remove some multiple chars near by
158 | value = value.replace(/([\\-]+)/g, '-');
159 | value = value.replace(/(\s+)/g, ' ');
160 | //only allow + char at the begining
161 | value = value.replace(/(?!^)[\\+]/g, '');
162 |
163 | this.setState({
164 | phone: value
165 | });
166 | }
167 |
168 | renderCountrySelectOptions(){
169 | return CountriesHelper.list().map((country)=>{
170 | return (
171 | {text: country.name, value: country.shortCode}
172 | );
173 | })
174 | }
175 |
176 | renderUserInfo(){
177 | if(!this.props.user){
178 | return null;
179 | }
180 |
181 | return (
182 |
183 |
184 |
185 | {this.onFirstNameChange(e)}}/>
187 |
188 |
189 |
190 |
191 |
192 | {this.onLastNameChange(e)}}/>
194 |
195 |
196 |
197 |
198 |
199 | {this.onEmailChange(e)}}/>
201 |
202 |
203 |
204 |
205 |
206 | {this.refs.country = input}} />
209 |
210 |
211 |
212 |
213 |
214 | phone_android
215 | {this.onPhoneChange(e)}}/>
217 |
218 |
219 |
220 |
221 | )
222 | }
223 |
224 | render() {
225 | return (
226 |
227 |
Update Profile
228 | {this.renderUserInfo()}
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 | )
242 | }
243 | }
244 |
245 | export default withTracker((props)=>{
246 | let returnObj = {
247 | user: null,
248 | loadingUser: true
249 | }
250 |
251 | //load the user
252 | const userSub = Meteor.subscribe("users.current");
253 | if(userSub.ready()){
254 | returnObj.user = Meteor.users.findOne({_id: Meteor.userId()});
255 | returnObj.loadingUser = false;
256 | }
257 |
258 | return returnObj;
259 | })(ProfileEdit);
260 |
--------------------------------------------------------------------------------
/imports/ui/components/accounts/register.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 |
4 | import { showError } from '/imports/ui/helpers/alerts.js';
5 |
6 | import MaterialHelper from '/imports/ui/helpers/materialcss.js';
7 |
8 | class RegisterForm extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | email: '',
13 | password: '',
14 | password2: '',
15 | loading: false,
16 | };
17 |
18 | this.fields = {};
19 | }
20 |
21 | componentDidMount() {
22 |
23 | }
24 |
25 | onCreateAccount(e) {
26 | e.preventDefault();
27 | const checkResult = MaterialHelper.checkAll(this.fields);
28 | // console.log(checkResult);
29 | if (checkResult !== true) {
30 | // console.log(checkResult);
31 | return;
32 | }
33 |
34 | if (this.state.loading === true) {
35 | // console.log('loading');
36 | return;
37 | }
38 |
39 | this.setState({
40 | loading: true,
41 | });
42 |
43 | const { email, password } = this.state;
44 | Meteor.call('users.register', {
45 | email,
46 | password,
47 | }, (error, result) => {
48 | this.setState({
49 | loading: false,
50 | });
51 | if (error) {
52 | showError(error.message);
53 | // console.log(error);
54 | return;
55 | }
56 | if (result) {
57 | // login this user
58 | // Meteor.loginWithPassword(email, password, (err) => {
59 | // if (err) {
60 | // showError(err.message);
61 | // }
62 | // });
63 | // send user to login page
64 | this.props.history.push('/accounts/login');
65 | }
66 | });
67 | }
68 |
69 | onCancel(e) {
70 | e.preventDefault();
71 | // send user to login page
72 | this.props.history.push('/accounts/login');
73 | }
74 |
75 | onFieldChange(field, value) {
76 | // console.log(field, value);
77 | const setObj = {};
78 | setObj[field] = value;
79 | this.setState(setObj, () => {
80 | if (field === 'password' || field === 'password2') {
81 | this.validatePassword();
82 | }
83 | });
84 | }
85 |
86 | validatePassword() {
87 | let passwordValid = true;
88 | let password2Valid = true;
89 | if (this.state.password === '') {
90 | passwordValid = false;
91 | } else {
92 | passwordValid = true;
93 | }
94 |
95 | if (this.state.password2 === '') {
96 | password2Valid = false;
97 | } else if (this.state.password2 !== this.state.password) {
98 | password2Valid = false;
99 | } else {
100 | password2Valid = true;
101 | }
102 |
103 | $(this.fields.password).addClass(passwordValid ? 'valid' : 'invalid');
104 | $(this.fields.password).removeClass(passwordValid ? 'invalid' : 'valid');
105 | $(this.fields.password2).addClass(password2Valid ? 'valid' : 'invalid');
106 | $(this.fields.password2).removeClass(password2Valid ? 'invalid' : 'valid');
107 | }
108 |
109 | render() {
110 | let submitBtnClass = 'waves-effect waves-light btn';
111 | if (this.state.loading) {
112 | submitBtnClass += ' disabled';
113 | }
114 |
115 | return (
116 |
117 |
Create Account
118 |
171 |
172 | );
173 | }
174 | }
175 |
176 | export default RegisterForm;
177 |
--------------------------------------------------------------------------------
/imports/ui/components/accounts/registered.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class Registered extends Component {
4 |
5 | render() {
6 | return (
7 |
8 |
9 |
Email Confirmation
10 |
In order to complete your registration, please click the confirmation link in the email.
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | export default Registered;
18 |
--------------------------------------------------------------------------------
/imports/ui/components/accounts/resetPassword.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import { Accounts } from 'meteor/accounts-base';
4 |
5 | import { showError, showSuccess } from '/imports/ui/helpers/alerts.js';
6 | import MaterialHelper from '/imports/ui/helpers/materialcss.js';
7 |
8 | class ResetPasswordForm extends Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | password: '',
13 | loading: false,
14 | };
15 |
16 | this.fields = {};
17 | }
18 |
19 | componentDidMount() {
20 |
21 | }
22 |
23 | onResetPassword(e) {
24 | e.preventDefault();
25 | const checkResult = MaterialHelper.checkAll(this.fields);
26 | // console.log(checkResult);
27 | if (checkResult !== true) {
28 | console.log(checkResult);
29 | return;
30 | }
31 |
32 | if (this.state.loading === true) {
33 | console.log('loading');
34 | return;
35 | }
36 |
37 | this.setState({
38 | loading: true,
39 | });
40 |
41 | // call method to reset password
42 | const { token } = this.props.match.params;
43 |
44 | Accounts.resetPassword(token, this.state.password, (err) => {
45 | if (err) {
46 | showError(err.message);
47 | } else {
48 | // send user to login page
49 | this.props.history.push('/accounts/login');
50 | }
51 | });
52 | }
53 |
54 | onCancel(e) {
55 | e.preventDefault();
56 | // send user to profile page
57 | this.props.history.push('/accounts/login');
58 | }
59 |
60 | onFieldChange(field, value) {
61 | // console.log(field, value);
62 | const setObj = {};
63 | setObj[field] = value;
64 | this.setState(setObj);
65 | }
66 |
67 | render() {
68 | let submitBtnClass = 'waves-effect waves-light btn';
69 | if (this.state.loading) {
70 | submitBtnClass += ' disabled';
71 | }
72 |
73 | return (
74 |
75 |
Reset Password
76 |
105 |
106 | );
107 | }
108 | }
109 |
110 | export default ResetPasswordForm;
111 |
--------------------------------------------------------------------------------
/imports/ui/components/accounts/verifyEmail.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import { Accounts } from 'meteor/accounts-base';
4 |
5 | import { showError, showSuccess } from '/imports/ui/helpers/alerts.js';
6 |
7 | class VerifyEmail extends Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.state = {
12 | message: 'Loading...',
13 | };
14 | }
15 |
16 | componentWillMount() {
17 | const { token } = this.props.match.params;
18 | if (!token) {
19 | this.setState({
20 | message: 'Token was not found',
21 | });
22 | showError('Token was not found');
23 | }
24 | }
25 |
26 | componentDidMount() {
27 | const { token } = this.props.match.params;
28 | if (!token) {
29 | return;
30 | }
31 | // call meteor method
32 | Accounts.verifyEmail(token, (error) => {
33 | this.setState({
34 | message: 'Loaded',
35 | });
36 |
37 | // send user to login page
38 | this.props.history.push('/accounts/login');
39 | if (error) {
40 | Meteor.setTimeout(() => {
41 | showError(error.message);
42 | }, 100);
43 | } else {
44 | Meteor.setTimeout(() => {
45 | showSuccess('Accounts verified successfully');
46 | }, 100);
47 | }
48 | });
49 | }
50 |
51 | render() {
52 | return (
53 |
54 |
Email Verification
55 |
{this.state.message}
56 |
57 | );
58 | }
59 | }
60 |
61 | export default VerifyEmail;
62 |
--------------------------------------------------------------------------------
/imports/ui/components/test/simpleSchema.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import React, { Component } from 'react';
3 | import { withTracker } from 'meteor/react-meteor-data';
4 | import moment from 'moment';
5 |
6 | import Tests from '/imports/api/test/tests.js';
7 |
8 | class TestSimpleSchema extends Component {
9 | onTestFailInsert(e) {
10 | e.preventDefault();
11 | Meteor.call('test.insert', { data: { title: 'abc' } }, (error, result) => {
12 | if (error) {
13 | console.log('error', error);
14 | }
15 | if (result) {
16 | console.log(result);
17 | }
18 | });
19 | }
20 |
21 | onTestSuccessInsert(e) {
22 | e.preventDefault();
23 | Meteor.call('test.insert', { data: { title: 'abc', body: 'something in body' } }, (error, result) => {
24 | if (error) {
25 | console.log('error', error);
26 | }
27 | if (result) {
28 | console.log(result);
29 | }
30 | });
31 | }
32 |
33 | renderTestDataItems() {
34 | const { data } = this.props;
35 |
36 | return data.map(item =>
37 | (
38 |
39 |
40 | {item._id}
41 |
42 |
43 | {moment(item.createdAt).format('DD-MM-YY HH:mm:ss')}
44 |
45 |
46 | ));
47 | }
48 |
49 | renderTestData() {
50 | const { data, loading } = this.props;
51 |
52 | if (loading) {
53 | return Loading...
;
54 | }
55 |
56 | if (data.length === 0) {
57 | return null;
58 | }
59 |
60 | return (
61 |
62 |
Reactive data
63 | {this.renderTestDataItems()}
64 |
65 | );
66 | }
67 |
68 | render() {
69 | return (
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | {this.renderTestData()}
80 |
81 | );
82 | }
83 | }
84 |
85 | export default withTracker(() => {
86 | const returnObj = {
87 | data: [],
88 | loading: true,
89 | };
90 |
91 | let testSub;
92 | if (Meteor.isClient) {
93 | testSub = Meteor.subscribe('tests.all', {});
94 | }
95 | if (Meteor.isServer || (testSub && testSub.ready())) {
96 | returnObj.data = Tests.find({}, { sort: { createdAt: -1 } }).fetch();
97 | returnObj.loading = false;
98 | }
99 |
100 | return returnObj;
101 | })(TestSimpleSchema);
102 |
--------------------------------------------------------------------------------
/imports/ui/components/test/syncMethodCall.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import callWithPromise from '/imports/helpers/call-with-promise.js';
4 |
5 | class TestSyncMethodCall extends Component {
6 | onTest(e) {
7 | const callMethod = async () => {
8 | const result1 = await callWithPromise('test.task', { index: 1 });
9 | console.log(result1);
10 | const result2 = await callWithPromise('test.task', { index: 2 });
11 | console.log(result2);
12 | };
13 |
14 | callMethod();
15 | }
16 |
17 | render() {
18 | return (
19 |
20 |
21 |
22 | );
23 | }
24 | }
25 |
26 | export default TestSyncMethodCall;
27 |
--------------------------------------------------------------------------------
/imports/ui/helpers/alerts.js:
--------------------------------------------------------------------------------
1 | import Alert from 'react-s-alert';
2 |
3 | const showError = (text) => {
4 | if (text) {
5 | Alert.error(text, {
6 | timeout: 7000,
7 | });
8 | }
9 | };
10 |
11 | const showWarning = (text) => {
12 | if (text) {
13 | Alert.warning(text, {
14 | timeout: 7000,
15 | });
16 | }
17 | };
18 |
19 | const showInfo = (text) => {
20 | if (text) {
21 | Alert.info(text, {
22 | timeout: 5000,
23 | });
24 | }
25 | };
26 |
27 | const showSuccess = (text) => {
28 | if (text) {
29 | Alert.success(text, {
30 | timeout: 5000,
31 | });
32 | }
33 | };
34 |
35 | const playSunny = (text) => {
36 | if (text) {
37 | Alert.success(text, {
38 | beep: '/sounds/sunny.mp3',
39 | timeout: 5000,
40 | });
41 | }
42 | };
43 |
44 | export { Alert, showError, showWarning, showInfo, showSuccess, playSunny };
45 |
--------------------------------------------------------------------------------
/imports/ui/helpers/materialcss.js:
--------------------------------------------------------------------------------
1 | import { Materialize } from 'meteor/materialize:materialize';
2 |
3 | const MaterialHelper = {
4 | updateTextFields() {
5 | return Materialize.updateTextFields();
6 | },
7 |
8 | selectState(item) {
9 | // console.log(item, item.value);
10 | // console.log($(item).prop('required'));
11 | if ($(item).prop('required')) {
12 | // get the input mask
13 | const inputMask = $(item).siblings('input.select-dropdown');
14 |
15 | $(item).addClass(item.value ? 'valid' : 'invalid');
16 | $(item).removeClass(item.value ? 'invalid' : 'valid');
17 | $(inputMask).addClass(item.value ? 'valid' : 'invalid');
18 | $(inputMask).removeClass(item.value ? 'invalid' : 'valid');
19 | }
20 | },
21 |
22 | // Return true if success, else return a string of fields property
23 | checkAll(fields) {
24 | let fieldToFocus = null;
25 | Object.keys(fields).map((field) => {
26 | const item = fields[field];
27 | if (item && typeof item.checkValidity === 'function') {
28 | if (!item.checkValidity()) {
29 | if (fieldToFocus === null) {
30 | fieldToFocus = field;
31 | if (typeof item.focus === 'function') {
32 | item.focus();
33 | }
34 | }
35 |
36 | return false;
37 | }
38 | return true;
39 | }
40 |
41 | if (item instanceof HTMLElement) {
42 | if (!$(item)[0] || !$(item)[0].checkValidity) {
43 | return true;
44 | }
45 | // call html5 check validate
46 | const checkResult = $(item)[0].checkValidity();
47 | if (!checkResult) {
48 | // process the select box
49 | if ($(item).is('select')) {
50 | this.selectState(item);
51 | } else {
52 | $(item).addClass('invalid');
53 | $(item).removeClass('valid');
54 | }
55 |
56 | if (!fieldToFocus) {
57 | fieldToFocus = field;
58 | $(item).focus();
59 | }
60 | }
61 | } else {
62 | // console.log(item, 'is not html element');
63 | }
64 |
65 | return true;
66 | });
67 | if (fieldToFocus) {
68 | return fieldToFocus;
69 | }
70 |
71 | return true;
72 | },
73 | };
74 |
75 | export default MaterialHelper;
76 |
--------------------------------------------------------------------------------
/imports/ui/layouts/admin/admin.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Switch, Route } from 'react-router-dom';
3 | import { Helmet } from 'react-helmet';
4 |
5 | import NotFoundPage from '/imports/ui/pages/notFound/notFound.js';
6 | import LoadableWrapper from '/imports/helpers/react-loadable/LoadableWrapper.js';
7 |
8 | const LoadableDashboardPage = LoadableWrapper({
9 | loader: () => import('/imports/ui/pages/admin/dashboard/dashboard.js'),
10 | modules: ['/imports/ui/pages/admin/dashboard/dashboard.js'],
11 | });
12 |
13 | const AdminLayout = () => (
14 |
15 |
16 | Admin layout
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 |
29 | export default AdminLayout;
30 |
--------------------------------------------------------------------------------
/imports/ui/layouts/site/site.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import { Switch, Route } from 'react-router-dom';
4 | import { Helmet } from 'react-helmet';
5 |
6 | import { Alert } from '/imports/ui/helpers/alerts.js';
7 | import NotFoundPage from '/imports/ui/pages/notFound/notFound.js';
8 | import LoadableWrapper from '/imports/helpers/react-loadable/LoadableWrapper.js';
9 |
10 | const LoadableHomePage = LoadableWrapper({
11 | loader: () => import('/imports/ui/pages/home/home.js'),
12 | modules: ['/imports/ui/pages/home/home.js'],
13 | });
14 | const LoadableTestPage = LoadableWrapper({
15 | loader: () => import('/imports/ui/pages/test/test.js'),
16 | modules: ['/imports/ui/pages/test/test.js'],
17 | });
18 | const LoadableAccountPage = LoadableWrapper({
19 | loader: () => import('/imports/ui/pages/accounts/accounts.js'),
20 | modules: ['/imports/ui/pages/accounts/accounts.js'],
21 | });
22 |
23 | if (Meteor.isClient) {
24 | import 'react-s-alert/dist/s-alert-default.css';
25 | }
26 |
27 | const SiteLayout = () => (
28 |
29 |
30 | Site layout
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 |
46 | export default SiteLayout;
47 |
--------------------------------------------------------------------------------
/imports/ui/layouts/site/site.scss:
--------------------------------------------------------------------------------
1 | //some css go here
2 | h1 {
3 | opacity: 0.8;
4 | transition: all .5s;
5 | }
6 |
--------------------------------------------------------------------------------
/imports/ui/pages/accounts/accounts.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Meteor } from 'meteor/meteor';
3 | import { Switch, Route } from 'react-router-dom';
4 | // import { Permissions } from '/imports/common/helpers/permissions';
5 |
6 | import NotFoundPage from '/imports/ui/pages/notFound/notFound.js';
7 | import LoginForm from '/imports/ui/components/accounts/login';
8 | import ForgotPasswordForm from '/imports/ui/components/accounts/forgotPassword';
9 | import RegisterForm from '/imports/ui/components/accounts/register.js';
10 | import VerifyEmail from '/imports/ui/components/accounts/verifyEmail.js';
11 | import ChangePasswordForm from '/imports/ui/components/accounts/changePassword.js';
12 | import ResetPasswordForm from '/imports/ui/components/accounts/resetPassword.js';
13 |
14 | if (Meteor.isClient) {
15 | import './accounts.scss';
16 | }
17 |
18 | class AccountsPage extends Component {
19 | constructor(props) {
20 | super(props);
21 | this.state = {
22 | allowed: true,
23 | };
24 | }
25 |
26 | componentWillMount() {
27 |
28 | }
29 |
30 | render() {
31 | if (this.state.allowed) {
32 | return (
33 |
34 |
35 |
36 |
41 |
46 |
51 |
56 |
61 |
66 |
71 |
72 |
73 |
74 |
75 | );
76 | }
77 |
78 | return (
79 | ...loading...
80 | );
81 | }
82 | }
83 |
84 | export default AccountsPage;
85 |
--------------------------------------------------------------------------------
/imports/ui/pages/accounts/accounts.scss:
--------------------------------------------------------------------------------
1 | .login-page-wrapper {
2 | .col.s12 {
3 | .btn {
4 | margin-bottom: 20px;
5 | }
6 | }
7 |
8 | .input-field {
9 | .input-field-helper {
10 | position: absolute;
11 | right: 10px;
12 | top: 3.2rem;
13 | font-size: 12px;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/imports/ui/pages/admin/dashboard/dashboard.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import moment from 'moment';
3 |
4 | class AdminDashboardPage extends Component {
5 | goBack() {
6 | this.props.history.goBack();
7 | }
8 |
9 | render() {
10 | return (
11 |
12 | {moment().format('DD/MM/YYYY')}
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 | }
21 |
22 | export default AdminDashboardPage;
23 |
--------------------------------------------------------------------------------
/imports/ui/pages/home/home.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | class HomePage extends Component {
5 |
6 | render() {
7 | return (
8 |
9 |
Home Page
10 | admin
11 |
12 | );
13 | }
14 | }
15 |
16 | export default HomePage;
17 |
--------------------------------------------------------------------------------
/imports/ui/pages/notFound/notFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NotFoundPage = () => (
4 |
5 |
404 not found
6 |
7 | );
8 |
9 | export default NotFoundPage;
10 |
--------------------------------------------------------------------------------
/imports/ui/pages/test/test.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | import TestSimpleSchema from '/imports/ui/components/test/simpleSchema.js';
5 | import TestSyncMethodCall from '/imports/ui/components/test/syncMethodCall.js';
6 |
7 | class TestPage extends Component {
8 |
9 | render() {
10 | return (
11 |
12 |
Test Page
13 | admin
14 | Test sync method call
15 |
16 | test simpl-schema
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | export default TestPage;
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server-render",
3 | "private": true,
4 | "scripts": {
5 | "start": "meteor run --port=3300",
6 | "lint": "eslint .",
7 | "pretest": "npm run lint --silent"
8 | },
9 | "dependencies": {
10 | "@babel/runtime": "^7.0.0",
11 | "autoprefixer": "^7.2.6",
12 | "babel-runtime": "^6.26.0",
13 | "bcrypt": "^3.0.1",
14 | "history": "^4.7.2",
15 | "materialize-css": "^1.0.0",
16 | "meteor-node-stubs": "^0.3.3",
17 | "moment": "^2.22.2",
18 | "postcss-easy-import": "^3.0.0",
19 | "postcss-import": "^11.1.0",
20 | "postcss-nested": "^3.0.0",
21 | "postcss-scss": "^1.0.6",
22 | "postcss-simple-vars": "^4.1.0",
23 | "react": "^16.5.2",
24 | "react-dom": "^16.5.2",
25 | "react-helmet": "^5.2.0",
26 | "react-loadable": "^5.5.0",
27 | "react-router-dom": "^4.3.1",
28 | "react-s-alert": "^1.4.1",
29 | "simpl-schema": "^1.5.3"
30 | },
31 | "devDependencies": {
32 | "@meteorjs/eslint-config-meteor": "^1.0.5",
33 | "babel-eslint": "^8.2.6",
34 | "eslint": "^5.6.0",
35 | "eslint-config-airbnb": "^17.1.0",
36 | "eslint-import-resolver-meteor": "^0.4.0",
37 | "eslint-plugin-import": "^2.14.0",
38 | "eslint-plugin-jsx-a11y": "^6.1.1",
39 | "eslint-plugin-meteor": "^5.1.0",
40 | "eslint-plugin-react": "^7.11.1"
41 | },
42 | "postcss": {
43 | "plugins": {
44 | "postcss-easy-import": {
45 | "extensions": [
46 | ".css",
47 | ".scss",
48 | ".import.css"
49 | ],
50 | "prefix": "_"
51 | },
52 | "autoprefixer": {
53 | "browsers": [
54 | "last 2 versions"
55 | ]
56 | }
57 | },
58 | "parser": "postcss-scss"
59 | },
60 | "eslintConfig": {
61 | "env": {
62 | "node": true,
63 | "jquery": true
64 | },
65 | "extends": "@meteorjs/eslint-config-meteor",
66 | "rules": {
67 | "semi": 2,
68 | "jsx-a11y/anchor-is-valid": [
69 | "error",
70 | {
71 | "components": [
72 | "Link"
73 | ],
74 | "specialLink": [
75 | "hrefLeft",
76 | "hrefRight",
77 | "to"
78 | ],
79 | "aspects": [
80 | "noHref",
81 | "invalidHref",
82 | "preferButton"
83 | ]
84 | }
85 | ],
86 | "class-methods-use-this": [
87 | 0,
88 | {
89 | "exceptMethods": []
90 | }
91 | ]
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/scss-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "includePaths": [
3 | "{}/node_modules/materialize-css/sass/",
4 | "{}/node_modules/materialize-css/sass/components/forms"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/server/main.js:
--------------------------------------------------------------------------------
1 | import '/imports/startup/server/index.js';
2 |
--------------------------------------------------------------------------------