├── .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 |
141 |
142 |
143 | { this.fields.currentPassword = input; }} 147 | className="validate" 148 | required 149 | value={this.state.currentPassword} 150 | onChange={(e) => { this.onFieldChange('currentPassword', e.target.value); }} 151 | /> 152 | 153 |
154 |
155 |
156 |
157 | { this.fields.password = input; }} 161 | className="validate" 162 | required 163 | pattern=".{6,15}" 164 | value={this.state.password} 165 | onChange={(e) => { this.onFieldChange('password', e.target.value); }} 166 | /> 167 | 168 |
169 |
170 |
171 |
172 | { this.fields.password2 = input; }} 176 | className="validate" 177 | required 178 | pattern=".{6,}" 179 | value={this.state.password2} 180 | onChange={(e) => { this.onFieldChange('password2', e.target.value); }} 181 | /> 182 | 183 |
184 |
185 |
186 | 191 |
192 |
193 | 194 |
195 |
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 |
46 |
47 |
48 | { this.fields.email = input; }} 51 | type="email" 52 | className="validate" 53 | required 54 | /> 55 | 56 |
57 |
58 |
59 |
60 | 65 |
66 |
67 | 72 |
73 |
74 |
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 |
138 |

Login

139 |
140 |
141 |
142 | { this.fields.email = input; }} 145 | type="email" 146 | className="validate" 147 | value={this.state.email} 148 | onChange={(e) => { this.onFieldChange('email', e.target.value); }} 149 | /> 150 | 151 |
152 |
153 |
154 |
155 | { this.fields.password = input; }} 158 | type="password" 159 | className="validate" 160 | value={this.state.password} 161 | onChange={(e) => { this.onFieldChange('password', e.target.value); }} 162 | /> 163 | 164 | Forgot Password? 165 |
166 |
167 |
168 |
169 | 175 |
176 |
177 | 183 |
184 |
185 | {this.renderLoginWithServices()} 186 |
187 |
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 |
119 |
120 |
121 | { this.fields.email = input; }} 127 | value={this.state.email} 128 | onChange={(e) => { this.onFieldChange('email', e.target.value); }} 129 | /> 130 | 131 |
132 |
133 |
134 |
135 | { this.fields.password = input; }} 140 | value={this.state.password} 141 | onChange={(e) => { this.onFieldChange('password', e.target.value); }} 142 | /> 143 | 144 |
145 |
146 |
147 |
148 | { this.fields.password2 = input; }} 152 | value={this.state.password2} 153 | onChange={(e) => { this.onFieldChange('password2', e.target.value); }} 154 | /> 155 | 156 |
157 |
158 |
159 |
160 | 165 |
166 |
167 | 168 |
169 |
170 |
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 |
77 |
78 |
79 | { this.fields.password = input; }} 83 | className="validate" 84 | required 85 | pattern=".{6,15}" 86 | value={this.state.password} 87 | onChange={(e) => { this.onFieldChange('password', e.target.value); }} 88 | /> 89 | 90 |
91 |
92 |
93 |
94 | 99 |
100 |
101 | 102 |
103 |
104 |
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 | --------------------------------------------------------------------------------