Merge branch 'master' of https://github.com/cleverbeagle/pup
This commit is contained in:
commit
5b311446dc
108 changed files with 9797 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
settings-demo.json
|
||||
settings-staging.json
|
||||
settings-production.json
|
||||
17
.meteor/.finished-upgraders
Normal file
17
.meteor/.finished-upgraders
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# This file contains information which helps Meteor properly upgrade your
|
||||
# app when you run 'meteor update'. You should check it into version control
|
||||
# with your project.
|
||||
|
||||
notices-for-0.9.0
|
||||
notices-for-0.9.1
|
||||
0.9.4-platform-file
|
||||
notices-for-facebook-graph-api-2
|
||||
1.2.0-standard-minifiers-package
|
||||
1.2.0-meteor-platform-split
|
||||
1.2.0-cordova-changes
|
||||
1.2.0-breaking-changes
|
||||
1.3.0-split-minifiers-package
|
||||
1.4.0-remove-old-dev-bundle-link
|
||||
1.4.1-add-shell-server-package
|
||||
1.4.3-split-account-service-packages
|
||||
1.5-add-dynamic-import-package
|
||||
1
.meteor/.gitignore
vendored
Normal file
1
.meteor/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
local
|
||||
7
.meteor/.id
Normal file
7
.meteor/.id
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# This file contains a token that is unique to your project.
|
||||
# Check it into your repository along with the rest of this directory.
|
||||
# It can be used for purposes such as:
|
||||
# - ensuring you don't accidentally deploy one app on top of another
|
||||
# - providing package authors with aggregated statistics
|
||||
|
||||
16be20efyo0qb53r01o
|
||||
35
.meteor/packages
Normal file
35
.meteor/packages
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Meteor packages used by this project, one per line.
|
||||
# Check this file (and the other files in this directory) into your repository.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
meteor-base@1.1.0 # Packages every Meteor app needs to have
|
||||
mobile-experience@1.0.4 # Packages for a great mobile UX
|
||||
mongo@1.2.0 # The database Meteor supports right now
|
||||
reactive-var@1.0.11 # Reactive variable for tracker
|
||||
tracker@1.1.3 # Meteor's client-side reactive programming library
|
||||
|
||||
standard-minifier-css@1.3.4 # CSS minifier run for production mode
|
||||
standard-minifier-js@2.1.1 # JS minifier run for production mode
|
||||
es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers.
|
||||
ecmascript@0.8.2 # Enable ECMAScript2015+ syntax in app code
|
||||
shell-server@0.2.4 # Server-side component of the `meteor shell` command
|
||||
|
||||
react-meteor-data
|
||||
alanning:roles
|
||||
fourseven:scss
|
||||
twbs:bootstrap
|
||||
accounts-base@1.3.2
|
||||
accounts-password@1.4.0
|
||||
service-configuration@1.0.11
|
||||
accounts-facebook@1.2.1
|
||||
accounts-github@1.3.0
|
||||
accounts-google@1.2.0
|
||||
themeteorchef:bert
|
||||
fortawesome:fontawesome
|
||||
aldeed:collection2-core@2.0.1
|
||||
audit-argument-checks@1.0.7
|
||||
ddp-rate-limiter@1.0.7
|
||||
dynamic-import@0.1.1
|
||||
static-html
|
||||
2
.meteor/platforms
Normal file
2
.meteor/platforms
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
server
|
||||
browser
|
||||
1
.meteor/release
Normal file
1
.meteor/release
Normal file
|
|
@ -0,0 +1 @@
|
|||
METEOR@1.5.2
|
||||
104
.meteor/versions
Normal file
104
.meteor/versions
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
accounts-base@1.3.3
|
||||
accounts-facebook@1.2.1
|
||||
accounts-github@1.3.0
|
||||
accounts-google@1.2.0
|
||||
accounts-oauth@1.1.15
|
||||
accounts-password@1.4.0
|
||||
alanning:roles@1.2.16
|
||||
aldeed:collection2-core@2.0.1
|
||||
allow-deny@1.0.9
|
||||
audit-argument-checks@1.0.7
|
||||
autoupdate@1.3.12
|
||||
babel-compiler@6.20.0
|
||||
babel-runtime@1.0.1
|
||||
base64@1.0.10
|
||||
binary-heap@1.0.10
|
||||
blaze@2.3.2
|
||||
blaze-tools@1.0.10
|
||||
boilerplate-generator@1.2.0
|
||||
caching-compiler@1.1.9
|
||||
caching-html-compiler@1.1.2
|
||||
callback-hook@1.0.10
|
||||
check@1.2.5
|
||||
ddp@1.3.1
|
||||
ddp-client@2.1.3
|
||||
ddp-common@1.2.9
|
||||
ddp-rate-limiter@1.0.7
|
||||
ddp-server@2.0.2
|
||||
deps@1.0.12
|
||||
diff-sequence@1.0.7
|
||||
dynamic-import@0.1.3
|
||||
ecmascript@0.8.2
|
||||
ecmascript-runtime@0.4.1
|
||||
ecmascript-runtime-client@0.4.3
|
||||
ecmascript-runtime-server@0.4.1
|
||||
ejson@1.0.14
|
||||
email@1.2.3
|
||||
es5-shim@4.6.15
|
||||
facebook-oauth@1.3.2
|
||||
fastclick@1.0.13
|
||||
fortawesome:fontawesome@4.7.0
|
||||
fourseven:scss@4.5.4
|
||||
geojson-utils@1.0.10
|
||||
github-oauth@1.2.0
|
||||
google-oauth@1.2.4
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.0.11
|
||||
htmljs@1.0.11
|
||||
http@1.2.12
|
||||
id-map@1.0.9
|
||||
jquery@1.11.10
|
||||
launch-screen@1.1.1
|
||||
livedata@1.0.18
|
||||
localstorage@1.1.1
|
||||
logging@1.1.17
|
||||
meteor@1.7.2
|
||||
meteor-base@1.1.0
|
||||
minifier-css@1.2.16
|
||||
minifier-js@2.1.3
|
||||
minimongo@1.3.1
|
||||
mobile-experience@1.0.4
|
||||
mobile-status-bar@1.0.14
|
||||
modules@0.10.0
|
||||
modules-runtime@0.8.0
|
||||
mongo@1.2.2
|
||||
mongo-dev-server@1.0.1
|
||||
mongo-id@1.0.6
|
||||
npm-bcrypt@0.9.3
|
||||
npm-mongo@2.2.30
|
||||
oauth@1.1.13
|
||||
oauth2@1.1.11
|
||||
observe-sequence@1.0.16
|
||||
ordered-dict@1.0.9
|
||||
promise@0.9.0
|
||||
raix:eventemitter@0.1.3
|
||||
random@1.0.10
|
||||
rate-limit@1.0.8
|
||||
react-meteor-data@0.2.13
|
||||
reactive-dict@1.1.9
|
||||
reactive-var@1.0.11
|
||||
reload@1.1.11
|
||||
retry@1.0.9
|
||||
routepolicy@1.0.12
|
||||
service-configuration@1.0.11
|
||||
session@1.1.7
|
||||
sha@1.0.9
|
||||
shell-server@0.2.4
|
||||
spacebars@1.0.15
|
||||
spacebars-compiler@1.1.3
|
||||
srp@1.0.10
|
||||
standard-minifier-css@1.3.5
|
||||
standard-minifier-js@2.1.1
|
||||
static-html@1.2.2
|
||||
templating@1.3.2
|
||||
templating-compiler@1.3.3
|
||||
templating-runtime@1.3.2
|
||||
templating-tools@1.1.2
|
||||
themeteorchef:bert@2.1.3
|
||||
tmeasday:check-npm-versions@0.3.1
|
||||
tracker@1.1.3
|
||||
twbs:bootstrap@3.3.6
|
||||
underscore@1.0.10
|
||||
url@1.1.0
|
||||
webapp@1.3.19
|
||||
webapp-hashing@1.0.9
|
||||
69
History.md
Normal file
69
History.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
## v.NEXT
|
||||
|
||||
- Improve [#96](https://github.com/cleverbeagle/pup/issues/96). Fix miscellaneous ESLint errors (h/t [@cdolek](https://github.com/cdolek) for sniffing these out).
|
||||
- Improve [#95](https://github.com/cleverbeagle/pup/issues/95). Migrate from `createContainer` method to `withTracker` for data containers (h/t [@cdolek](https://github.com/cdolek) for sniffing these out).
|
||||
- Improve [#94](https://github.com/cleverbeagle/pup/issues/94). Bump Meteor to v1.6 and bump Atmosphere and NPM deps.
|
||||
- Improve [#53](https://github.com/cleverbeagle/pup/issues/53). Move method imports to be on both the client and the server to leverage Meteor's latency compensation by default (h/t [@zodern](https://github.com/zodern) for refactoring this).
|
||||
- Improve [#78](https://github.com/cleverbeagle/pup/issues/78). Deprecate `@cleverbeagle/dates` package in favor of a local file of date helpers (h/t [@zodern](https://github.com/zodern) for refactoring this).
|
||||
- Improve [#79](https://github.com/cleverbeagle/pup/issues/79). Add `babel-plugin-lodash` to reduce footprint of Lodash to only methods that are used in the project (h/t [@ninjaPixel](https://github.com/ninjaPixel) for finding/implementing this).
|
||||
- Improve [#80](https://github.com/cleverbeagle/pup/issues/80). Add exception to disable `max-len` ESLint rule (h/t [@ninjaPixel](https://github.com/ninjaPixel) for implementing this).
|
||||
- Improve [#60](https://github.com/cleverbeagle/pup/issues/60). Move to `babel-preset-env` from `babel-preset-es2015` (h/t [@pilarArr](https://github.com/pilarArr) for implementing this).
|
||||
- Fix [#59](https://github.com/cleverbeagle/pup/issues/59). Fix ESLint throwing a tantrum about line endings on Windows (h/t [@pilarArr](https://github.com/pilarArr) for implementing this).
|
||||
- Improve [#47](https://github.com/cleverbeagle/pup/issues/47). Move `getUserName()` function in `/imports/ui/layouts/App/App.js` into its own file (h/t [@pilarArr](https://github.com/pilarArr) for implementing this).
|
||||
- Improve [#46](https://github.com/cleverbeagle/pup/issues/46). Move the resend verification email bar into its own component (h/t [@pilarArr](https://github.com/pilarArr) for implementing this).
|
||||
|
||||
## v1.2.2, September 30th, 2017
|
||||
|
||||
- Fix [#66](https://github.com/cleverbeagle/pup/issues/66). Remove unnecessary `callback` argument causing trouble in `/imports/modules/server/send-email.js`.
|
||||
|
||||
- Fix [#74](https://github.com/cleverbeagle/pup/issues/74). Fix typo in `/imports/ui/components/Public/Public.js`.
|
||||
|
||||
## v1.2.1, September 24th, 2017
|
||||
|
||||
- Fix [#68](https://github.com/cleverbeagle/pup/issues/68). Update Meteor to v1.5.2 and bump Atmosphere dependencies to latest versions.
|
||||
|
||||
## v1.2.0, September 7th, 2017
|
||||
|
||||
- Fix [#31](https://github.com/cleverbeagle/pup/issues/31). Fix name not rendering for users logged in via OAuth with Google.
|
||||
- Fix [#32](https://github.com/cleverbeagle/pup/issues/32). Fix route props not being passed to rendered component in `<Authenticated />` and `<Public />`. h/t @pilarArr for the fix.
|
||||
- Improve [#37](https://github.com/cleverbeagle/pup/issues/37). Bump Atmosphere and NPM dependencies to latest versions. h/t @pilarArr for the refactor.
|
||||
- Improve [#40](https://github.com/cleverbeagle/pup/issues/40). Move `Meteor.logout()` call to be triggered by visiting the `/logout` route.
|
||||
- Add [#44](https://github.com/cleverbeagle/pup/issues/44). Add support for transactional emails with HTML and text templates via Handlebars.
|
||||
- Improve [#45](https://github.com/cleverbeagle/pup/issues/45). Clean up the UI/UX of the "Edit Profile" view for users who are logged in via OAuth.
|
||||
|
||||
## v1.1.1, August 6th, 2017
|
||||
|
||||
- Fix [#27](https://github.com/cleverbeagle/pup/issues/27). Fix incorrect method name in rate limiter list in `/imports/api/Users/server/methods.js`.
|
||||
- Fix [#28](https://github.com/cleverbeagle/pup/issues/28). Fix verify email message displaying for users who log in via OAuth.
|
||||
- Fix [#29](https://github.com/cleverbeagle/pup/issues/29). Fix `@cleverbeagle/seeder` and `@cleverbeagle/dates` dependencies breaking initial startup. Also add `core-js` dependency as it was breaking some apps on startup.
|
||||
|
||||
## v1.1.0, August 1st, 2017
|
||||
|
||||
- Improve [#16](https://github.com/cleverbeagle/pup/issues/16). Adds missing import for `/imports/startup/server/accounts/on-create-user.js` in `/imports/startup/server/accounts/index.js`.
|
||||
[Add documentation on usage of `Accounts.onCreateUser()`](/pup/v1/accounts/on-create-user-hook).
|
||||
|
||||
- Improve [#17](https://github.com/cleverbeagle/pup/issues/17). Move setting of user password in editing profile to client to ensure the user's current password is actually checked (implements Meteor's `Accounts.changePassword()` method).
|
||||
|
||||
- Add [#20](https://github.com/cleverbeagle/pup/issues/20). Add support for `lodash` package via NPM and swap usage of `capitalize()` method from `@cleverbeagle/strings` package with lodash's `_.capitalize()` method. Remove `@cleverbeagle/strings` from NPM dependencies.
|
||||
|
||||
- Add [#21](https://github.com/cleverbeagle/pup/issues/21). Add support for (non-blocking) email verification on user signup. [Add documentation on email verification process](/pup/v1/accounts/email-verification).
|
||||
|
||||
- Fix [#23](https://github.com/cleverbeagle/pup/issues/23). Fixes a React PropTypes warning showing up when accessing a non-existing document on `/documents/:_id/edit`. Also improve 404 handling on `/documents/:_id/edit` and `/documents/:_id` when no document is passed.
|
||||
|
||||
- Fix [#24](https://github.com/cleverbeagle/pup/issues/24). Remove unnecessary `_.pluck()` call obscuring the method names passed to the `rateLimit()` options object.
|
||||
|
||||
## v1.0.3, July 9th, 2017
|
||||
|
||||
- Fix [#14](https://github.com/cleverbeagle/pup/issues/14). Remove unnecessary `loggingIn` check that was forcing the page to re-render.
|
||||
|
||||
## v1.0.2, June 27th, 2017
|
||||
|
||||
- Fix [#12](https://github.com/cleverbeagle/pup/issues/12). Fixes prop type warning being thrown when transitioning to the `ViewDocument` page component from the `NewDocument` page component.
|
||||
|
||||
## v1.0.1, June 14th, 2017
|
||||
|
||||
- Fix [#1](https://github.com/cleverbeagle/pup/issues/1). h/t to [@sczk](https://github.com/sczk) for pointing out the fix.
|
||||
|
||||
## v1.0.0, June 13th, 2017
|
||||
|
||||
- Initial release of Pup.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
### Pup
|
||||
A boilerplate for products.
|
||||
|
||||
[Read the Documentation](http://cleverbeagle.com/pup)
|
||||
|
||||
---
|
||||
|
||||
Need help and want to stay accountable building your product? [Check out Clever Beagle](http://cleverbeagle.com).
|
||||
13
client/main.html
Normal file
13
client/main.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Pup</title>
|
||||
<meta name="description" content="A description for the application.">
|
||||
<meta name="viewport" content="initial-scale=1, minimal-ui, maximum-scale=1, minimum-scale=1" />
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png?v1" sizes="16x16 32x32 64x64">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-precomposed.png">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="react-root"></div>
|
||||
</body>
|
||||
1
client/main.js
Normal file
1
client/main.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import '../imports/startup/client';
|
||||
51
imports/api/Documents/Documents.js
Normal file
51
imports/api/Documents/Documents.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/* eslint-disable consistent-return */
|
||||
|
||||
import { Mongo } from 'meteor/mongo';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
|
||||
const Documents = new Mongo.Collection('Documents');
|
||||
|
||||
Documents.allow({
|
||||
insert: () => false,
|
||||
update: () => false,
|
||||
remove: () => false,
|
||||
});
|
||||
|
||||
Documents.deny({
|
||||
insert: () => true,
|
||||
update: () => true,
|
||||
remove: () => true,
|
||||
});
|
||||
|
||||
Documents.schema = new SimpleSchema({
|
||||
owner: {
|
||||
type: String,
|
||||
label: 'The ID of the user this document belongs to.',
|
||||
},
|
||||
createdAt: {
|
||||
type: String,
|
||||
label: 'The date this document was created.',
|
||||
autoValue() {
|
||||
if (this.isInsert) return (new Date()).toISOString();
|
||||
},
|
||||
},
|
||||
updatedAt: {
|
||||
type: String,
|
||||
label: 'The date this document was last updated.',
|
||||
autoValue() {
|
||||
if (this.isInsert || this.isUpdate) return (new Date()).toISOString();
|
||||
},
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
label: 'The title of the document.',
|
||||
},
|
||||
body: {
|
||||
type: String,
|
||||
label: 'The body of the document.',
|
||||
},
|
||||
});
|
||||
|
||||
Documents.attachSchema(Documents.schema);
|
||||
|
||||
export default Documents;
|
||||
53
imports/api/Documents/methods.js
Normal file
53
imports/api/Documents/methods.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import Documents from './Documents';
|
||||
import rateLimit from '../../modules/rate-limit';
|
||||
|
||||
Meteor.methods({
|
||||
'documents.insert': function documentsInsert(doc) {
|
||||
check(doc, {
|
||||
title: String,
|
||||
body: String,
|
||||
});
|
||||
|
||||
try {
|
||||
return Documents.insert({ owner: this.userId, ...doc });
|
||||
} catch (exception) {
|
||||
throw new Meteor.Error('500', exception);
|
||||
}
|
||||
},
|
||||
'documents.update': function documentsUpdate(doc) {
|
||||
check(doc, {
|
||||
_id: String,
|
||||
title: String,
|
||||
body: String,
|
||||
});
|
||||
|
||||
try {
|
||||
const documentId = doc._id;
|
||||
Documents.update(documentId, { $set: doc });
|
||||
return documentId; // Return _id so we can redirect to document after update.
|
||||
} catch (exception) {
|
||||
throw new Meteor.Error('500', exception);
|
||||
}
|
||||
},
|
||||
'documents.remove': function documentsRemove(documentId) {
|
||||
check(documentId, String);
|
||||
|
||||
try {
|
||||
return Documents.remove(documentId);
|
||||
} catch (exception) {
|
||||
throw new Meteor.Error('500', exception);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
rateLimit({
|
||||
methods: [
|
||||
'documents.insert',
|
||||
'documents.update',
|
||||
'documents.remove',
|
||||
],
|
||||
limit: 5,
|
||||
timeRange: 1000,
|
||||
});
|
||||
13
imports/api/Documents/server/publications.js
Normal file
13
imports/api/Documents/server/publications.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import Documents from '../Documents';
|
||||
|
||||
Meteor.publish('documents', function documents() {
|
||||
return Documents.find({ owner: this.userId });
|
||||
});
|
||||
|
||||
// Note: documents.view is also used when editing an existing document.
|
||||
Meteor.publish('documents.view', function documentsView(documentId) {
|
||||
check(documentId, String);
|
||||
return Documents.find({ _id: documentId, owner: this.userId });
|
||||
});
|
||||
30
imports/api/OAuth/server/methods.js
Normal file
30
imports/api/OAuth/server/methods.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import { ServiceConfiguration } from 'meteor/service-configuration';
|
||||
import rateLimit from '../../../modules/rate-limit';
|
||||
|
||||
Meteor.methods({
|
||||
'oauth.verifyConfiguration': function oauthVerifyConfiguration(services) {
|
||||
check(services, Array);
|
||||
|
||||
try {
|
||||
const verifiedServices = [];
|
||||
services.forEach((service) => {
|
||||
if (ServiceConfiguration.configurations.findOne({ service })) {
|
||||
verifiedServices.push(service);
|
||||
}
|
||||
});
|
||||
return verifiedServices.sort();
|
||||
} catch (exception) {
|
||||
throw new Meteor.Error('500', exception);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
rateLimit({
|
||||
methods: [
|
||||
'oauth.verifyConfiguration',
|
||||
],
|
||||
limit: 5,
|
||||
timeRange: 1000,
|
||||
});
|
||||
32
imports/api/Users/server/edit-profile.js
Normal file
32
imports/api/Users/server/edit-profile.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/* eslint-disable consistent-return */
|
||||
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
let action;
|
||||
|
||||
const updateUser = (userId, { emailAddress, profile }) => {
|
||||
try {
|
||||
Meteor.users.update(userId, {
|
||||
$set: {
|
||||
'emails.0.address': emailAddress,
|
||||
profile,
|
||||
},
|
||||
});
|
||||
} catch (exception) {
|
||||
action.reject(`[editProfile.updateUser] ${exception}`);
|
||||
}
|
||||
};
|
||||
|
||||
const editProfile = ({ userId, profile }, promise) => {
|
||||
try {
|
||||
action = promise;
|
||||
updateUser(userId, profile);
|
||||
action.resolve();
|
||||
} catch (exception) {
|
||||
action.reject(`[editProfile.handler] ${exception}`);
|
||||
}
|
||||
};
|
||||
|
||||
export default options =>
|
||||
new Promise((resolve, reject) =>
|
||||
editProfile(options, { resolve, reject }));
|
||||
37
imports/api/Users/server/methods.js
Normal file
37
imports/api/Users/server/methods.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import editProfile from './edit-profile';
|
||||
import rateLimit from '../../../modules/rate-limit';
|
||||
|
||||
Meteor.methods({
|
||||
'users.sendVerificationEmail': function usersSendVerificationEmail() {
|
||||
return Accounts.sendVerificationEmail(this.userId);
|
||||
},
|
||||
'users.editProfile': function usersEditProfile(profile) {
|
||||
check(profile, {
|
||||
emailAddress: String,
|
||||
profile: {
|
||||
name: {
|
||||
first: String,
|
||||
last: String,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return editProfile({ userId: this.userId, profile })
|
||||
.then(response => response)
|
||||
.catch((exception) => {
|
||||
throw new Meteor.Error('500', exception);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
rateLimit({
|
||||
methods: [
|
||||
'users.sendVerificationEmail',
|
||||
'users.editProfile',
|
||||
],
|
||||
limit: 5,
|
||||
timeRange: 1000,
|
||||
});
|
||||
11
imports/api/Users/server/publications.js
Normal file
11
imports/api/Users/server/publications.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
Meteor.publish('users.editProfile', function usersProfile() {
|
||||
return Meteor.users.find(this.userId, {
|
||||
fields: {
|
||||
emails: 1,
|
||||
profile: 1,
|
||||
services: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
25
imports/api/Users/server/send-welcome-email.js
Normal file
25
imports/api/Users/server/send-welcome-email.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import sendEmail from '../../../modules/server/send-email';
|
||||
import getOAuthProfile from '../../../modules/get-oauth-profile';
|
||||
|
||||
export default (options, user) => {
|
||||
const OAuthProfile = getOAuthProfile(options, user);
|
||||
|
||||
const applicationName = 'Application Name';
|
||||
const firstName = OAuthProfile ? OAuthProfile.name.first : options.profile.name.first;
|
||||
const emailAddress = OAuthProfile ? OAuthProfile.email : options.email;
|
||||
|
||||
return sendEmail({
|
||||
to: emailAddress,
|
||||
from: `${applicationName} <support@application.com>`,
|
||||
subject: `[${applicationName}] Welcome, ${firstName}!`,
|
||||
template: 'welcome',
|
||||
templateVars: {
|
||||
applicationName,
|
||||
firstName,
|
||||
welcomeUrl: Meteor.absoluteUrl('documents'), // e.g., returns http://localhost:3000/documents
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Meteor.Error('500', `${error}`);
|
||||
});
|
||||
};
|
||||
11
imports/api/Utility/server/methods.js
Normal file
11
imports/api/Utility/server/methods.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import getPrivateFile from '../../../modules/server/get-private-file';
|
||||
import parseMarkdown from '../../../modules/parse-markdown';
|
||||
|
||||
Meteor.methods({
|
||||
'utility.getPage': function utilityGetPage(fileName) {
|
||||
check(fileName, String);
|
||||
return parseMarkdown(getPrivateFile(`pages/${fileName}.md`));
|
||||
},
|
||||
});
|
||||
42
imports/modules/get-oauth-profile.js
Normal file
42
imports/modules/get-oauth-profile.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
const parseGoogleData = service => {
|
||||
return {
|
||||
email: service.email,
|
||||
name: {
|
||||
first: service.given_name,
|
||||
last: service.family_name,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const parseGithubData = (profile, service) => {
|
||||
const name = profile.name.split(' ');
|
||||
return {
|
||||
email: service.email,
|
||||
name: {
|
||||
first: name[0],
|
||||
last: name[1],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const parseFacebookData = service => {
|
||||
return {
|
||||
email: service.email,
|
||||
name: {
|
||||
first: service.first_name,
|
||||
last: service.last_name,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getDataForService = (profile, services) => {
|
||||
if (services.facebook) return parseFacebookData(services.facebook);
|
||||
if (services.github) return parseGithubData(profile, services.github);
|
||||
if (services.google) return parseGoogleData(services.google);
|
||||
};
|
||||
|
||||
export default (options, user) => {
|
||||
const isOAuth = !options.password;
|
||||
const serviceData = isOAuth ? getDataForService(options.profile, user.services) : null;
|
||||
return isOAuth ? serviceData : null;
|
||||
};
|
||||
8
imports/modules/parse-markdown.js
Normal file
8
imports/modules/parse-markdown.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Parser, HtmlRenderer } from 'commonmark';
|
||||
|
||||
export default (markdown, options) => {
|
||||
const reader = new Parser();
|
||||
const writer = options ? new HtmlRenderer(options) : new HtmlRenderer();
|
||||
const parsed = reader.parse(markdown);
|
||||
return writer.render(parsed);
|
||||
};
|
||||
11
imports/modules/rate-limit.js
Normal file
11
imports/modules/rate-limit.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
|
||||
|
||||
export default ({ methods, limit, timeRange }) => {
|
||||
if (Meteor.isServer) {
|
||||
DDPRateLimiter.addRule({
|
||||
name(name) { return methods.indexOf(name) > -1; },
|
||||
connectionId() { return true; },
|
||||
}, limit, timeRange);
|
||||
}
|
||||
};
|
||||
3
imports/modules/server/get-private-file.js
Normal file
3
imports/modules/server/get-private-file.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import fs from 'fs';
|
||||
|
||||
export default path => fs.readFileSync(`assets/app/${path}`, 'utf8');
|
||||
11
imports/modules/server/handlebars-email-to-html.js
Normal file
11
imports/modules/server/handlebars-email-to-html.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import handlebars from 'handlebars';
|
||||
import juice from 'juice';
|
||||
|
||||
export default (handlebarsMarkup, context, options) => {
|
||||
if (handlebarsMarkup && context) {
|
||||
const template = handlebars.compile(handlebarsMarkup);
|
||||
return options && !options.inlineCss ? template(context) : juice(template(context)); // Use juice to inline CSS <style></style> styles from <head> unless disabled.
|
||||
}
|
||||
|
||||
throw new Error('Please pass Handlebars markup to compile and a context object with data mapping to the Handlebars expressions used in your template.');
|
||||
};
|
||||
10
imports/modules/server/handlebars-email-to-text.js
Normal file
10
imports/modules/server/handlebars-email-to-text.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import handlebars from 'handlebars';
|
||||
|
||||
export default (handlebarsMarkup, context) => {
|
||||
if (handlebarsMarkup && context) {
|
||||
const template = handlebars.compile(handlebarsMarkup);
|
||||
return template(context);
|
||||
}
|
||||
|
||||
throw new Error('Please pass Handlebars markup to compile and a context object with data mapping to the Handlebars expressions used in your template.');
|
||||
};
|
||||
29
imports/modules/server/send-email.js
Normal file
29
imports/modules/server/send-email.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
import { Email } from 'meteor/email';
|
||||
import getPrivateFile from './get-private-file';
|
||||
import templateToText from './handlebars-email-to-text';
|
||||
import templateToHTML from './handlebars-email-to-html';
|
||||
|
||||
const sendEmail = (options, { resolve, reject }) => {
|
||||
try {
|
||||
Meteor.defer(() => {
|
||||
Email.send(options);
|
||||
resolve();
|
||||
});
|
||||
} catch (exception) {
|
||||
reject(exception);
|
||||
}
|
||||
};
|
||||
|
||||
export default ({ text, html, template, templateVars, ...rest }) => {
|
||||
if (text || html || template) {
|
||||
return new Promise((resolve, reject) => {
|
||||
sendEmail({
|
||||
...rest,
|
||||
text: template ? templateToText(getPrivateFile(`email-templates/${template}.txt`), (templateVars || {})) : text,
|
||||
html: template ? templateToHTML(getPrivateFile(`email-templates/${template}.html`), (templateVars || {})) : html,
|
||||
}, { resolve, reject });
|
||||
});
|
||||
}
|
||||
throw new Error('Please pass an HTML string, text, or template name to compile for your message\'s body.');
|
||||
};
|
||||
4
imports/modules/validate.js
Normal file
4
imports/modules/validate.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import $ from 'jquery';
|
||||
import 'jquery-validation';
|
||||
|
||||
export default (form, options) => $(form).validate(options);
|
||||
8
imports/startup/client/index.js
Normal file
8
imports/startup/client/index.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import App from '../../ui/layouts/App/App';
|
||||
|
||||
import '../../ui/stylesheets/app.scss';
|
||||
|
||||
Meteor.startup(() => render(<App />, document.getElementById('react-root')));
|
||||
59
imports/startup/server/accounts/email-templates.js
Normal file
59
imports/startup/server/accounts/email-templates.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import getPrivateFile from '../../../modules/server/get-private-file';
|
||||
import templateToHTML from '../../../modules/server/handlebars-email-to-html';
|
||||
import templateToText from '../../../modules/server/handlebars-email-to-text';
|
||||
|
||||
const name = 'Application Name';
|
||||
const email = '<support@application.com>';
|
||||
const from = `${name} ${email}`;
|
||||
const emailTemplates = Accounts.emailTemplates;
|
||||
|
||||
emailTemplates.siteName = name;
|
||||
emailTemplates.from = from;
|
||||
|
||||
emailTemplates.verifyEmail = {
|
||||
subject() {
|
||||
return `[${name}] Verify Your Email Address`;
|
||||
},
|
||||
html(user, url) {
|
||||
return templateToHTML(getPrivateFile('email-templates/verify-email.html'), {
|
||||
applicationName: name,
|
||||
firstName: user.profile.name.first,
|
||||
verifyUrl: url.replace('#/', '')
|
||||
});
|
||||
},
|
||||
text(user, url) {
|
||||
const urlWithoutHash = url.replace('#/', '');
|
||||
if (Meteor.isDevelopment) console.info(`Verify Email Link: ${urlWithoutHash}`); // eslint-disable-line
|
||||
return templateToText(getPrivateFile('email-templates/verify-email.txt'), {
|
||||
applicationName: name,
|
||||
firstName: user.profile.name.first,
|
||||
verifyUrl: urlWithoutHash,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
emailTemplates.resetPassword = {
|
||||
subject() {
|
||||
return `[${name}] Reset Your Password`;
|
||||
},
|
||||
html(user, url) {
|
||||
return templateToHTML(getPrivateFile('email-templates/reset-password.html'), {
|
||||
firstName: user.profile.name.first,
|
||||
applicationName: name,
|
||||
emailAddress: user.emails[0].address,
|
||||
resetUrl: url.replace('#/', ''),
|
||||
});
|
||||
},
|
||||
text(user, url) {
|
||||
const urlWithoutHash = url.replace('#/', '');
|
||||
if (Meteor.isDevelopment) console.info(`Reset Password Link: ${urlWithoutHash}`); // eslint-disable-line
|
||||
return templateToText(getPrivateFile('email-templates/reset-password.txt'), {
|
||||
firstName: user.profile.name.first,
|
||||
applicationName: name,
|
||||
emailAddress: user.emails[0].address,
|
||||
resetUrl: urlWithoutHash,
|
||||
});
|
||||
},
|
||||
};
|
||||
3
imports/startup/server/accounts/index.js
Normal file
3
imports/startup/server/accounts/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import './oauth';
|
||||
import './email-templates';
|
||||
import './on-create-user.js';
|
||||
13
imports/startup/server/accounts/oauth.js
Normal file
13
imports/startup/server/accounts/oauth.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
import { ServiceConfiguration } from 'meteor/service-configuration';
|
||||
|
||||
const OAuthSettings = Meteor.settings.private.OAuth;
|
||||
|
||||
if (OAuthSettings) {
|
||||
Object.keys(OAuthSettings).forEach((service) => {
|
||||
ServiceConfiguration.configurations.upsert(
|
||||
{ service },
|
||||
{ $set: OAuthSettings[service] },
|
||||
);
|
||||
});
|
||||
}
|
||||
9
imports/startup/server/accounts/on-create-user.js
Normal file
9
imports/startup/server/accounts/on-create-user.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Accounts } from 'meteor/accounts-base';
|
||||
import sendWelcomeEmail from '../../../api/Users/server/send-welcome-email';
|
||||
|
||||
Accounts.onCreateUser((options, user) => {
|
||||
const userToCreate = user;
|
||||
if (options.profile) userToCreate.profile = options.profile;
|
||||
sendWelcomeEmail(options, user);
|
||||
return userToCreate;
|
||||
});
|
||||
9
imports/startup/server/api.js
Normal file
9
imports/startup/server/api.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import '../../api/Documents/methods';
|
||||
import '../../api/Documents/server/publications';
|
||||
|
||||
import '../../api/OAuth/server/methods';
|
||||
|
||||
import '../../api/Users/server/methods';
|
||||
import '../../api/Users/server/publications';
|
||||
|
||||
import '../../api/Utility/server/methods';
|
||||
3
imports/startup/server/email.js
Normal file
3
imports/startup/server/email.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
if (Meteor.isDevelopment) process.env.MAIL_URL = Meteor.settings.private.MAIL_URL;
|
||||
54
imports/startup/server/fixtures.js
Normal file
54
imports/startup/server/fixtures.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import seeder from '@cleverbeagle/seeder';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Documents from '../../api/Documents/Documents';
|
||||
|
||||
const documentsSeed = userId => ({
|
||||
collection: Documents,
|
||||
environments: ['development', 'staging'],
|
||||
noLimit: true,
|
||||
modelCount: 5,
|
||||
model(dataIndex) {
|
||||
return {
|
||||
owner: userId,
|
||||
title: `Document #${dataIndex + 1}`,
|
||||
body: `This is the body of document #${dataIndex + 1}`,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
seeder(Meteor.users, {
|
||||
environments: ['development', 'staging'],
|
||||
noLimit: true,
|
||||
data: [{
|
||||
email: 'admin@admin.com',
|
||||
password: 'password',
|
||||
profile: {
|
||||
name: {
|
||||
first: 'Andy',
|
||||
last: 'Warhol',
|
||||
},
|
||||
},
|
||||
roles: ['admin'],
|
||||
data(userId) {
|
||||
return documentsSeed(userId);
|
||||
},
|
||||
}],
|
||||
modelCount: 5,
|
||||
model(index, faker) {
|
||||
const userCount = index + 1;
|
||||
return {
|
||||
email: `user+${userCount}@test.com`,
|
||||
password: 'password',
|
||||
profile: {
|
||||
name: {
|
||||
first: faker.name.firstName(),
|
||||
last: faker.name.lastName(),
|
||||
},
|
||||
},
|
||||
roles: ['user'],
|
||||
data(userId) {
|
||||
return documentsSeed(userId);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
4
imports/startup/server/index.js
Normal file
4
imports/startup/server/index.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import './accounts';
|
||||
import './api';
|
||||
import './fixtures';
|
||||
import './email';
|
||||
16
imports/ui/components/AccountPageFooter/AccountPageFooter.js
Normal file
16
imports/ui/components/AccountPageFooter/AccountPageFooter.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './AccountPageFooter.scss';
|
||||
|
||||
const AccountPageFooter = ({ children }) => (
|
||||
<div className="AccountPageFooter">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
AccountPageFooter.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default AccountPageFooter;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
@import '../../stylesheets/colors';
|
||||
|
||||
.AccountPageFooter {
|
||||
margin: 25px 0 0;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid $gray-lighter;
|
||||
}
|
||||
23
imports/ui/components/Authenticated/Authenticated.js
Normal file
23
imports/ui/components/Authenticated/Authenticated.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route, Redirect } from 'react-router-dom';
|
||||
|
||||
const Authenticated = ({ loggingIn, authenticated, component, path, exact, ...rest }) => (
|
||||
<Route
|
||||
path={path}
|
||||
exact={exact}
|
||||
render={props => (
|
||||
authenticated ?
|
||||
(React.createElement(component, { ...props, ...rest, loggingIn, authenticated })) :
|
||||
(<Redirect to="/login" />)
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
Authenticated.propTypes = {
|
||||
loggingIn: PropTypes.bool.isRequired,
|
||||
authenticated: PropTypes.bool.isRequired,
|
||||
component: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Authenticated;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
import { Nav, NavItem, NavDropdown, MenuItem } from 'react-bootstrap';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
const AuthenticatedNavigation = ({ name, history }) => (
|
||||
<div>
|
||||
<Nav>
|
||||
<LinkContainer to="/documents">
|
||||
<NavItem eventKey={1} href="/documents">Documents</NavItem>
|
||||
</LinkContainer>
|
||||
</Nav>
|
||||
<Nav pullRight>
|
||||
<NavDropdown eventKey={2} title={name} id="user-nav-dropdown">
|
||||
<LinkContainer to="/profile">
|
||||
<NavItem eventKey={2.1} href="/profile">Profile</NavItem>
|
||||
</LinkContainer>
|
||||
<MenuItem divider />
|
||||
<MenuItem eventKey={2.2} onClick={() => history.push('/logout')}>Logout</MenuItem>
|
||||
</NavDropdown>
|
||||
</Nav>
|
||||
</div>
|
||||
);
|
||||
|
||||
AuthenticatedNavigation.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default withRouter(AuthenticatedNavigation);
|
||||
16
imports/ui/components/Content/Content.js
Normal file
16
imports/ui/components/Content/Content.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/* eslint-disable react/no-danger */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './Content.scss';
|
||||
|
||||
const Content = ({ content }) => (
|
||||
<div className="Content" dangerouslySetInnerHTML={{ __html: content }} />
|
||||
);
|
||||
|
||||
Content.propTypes = {
|
||||
content: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Content;
|
||||
36
imports/ui/components/Content/Content.scss
Normal file
36
imports/ui/components/Content/Content.scss
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
@import '../../stylesheets/mixins';
|
||||
|
||||
.Content {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 30px 0 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
> *:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
.Content {
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
97
imports/ui/components/DocumentEditor/DocumentEditor.js
Normal file
97
imports/ui/components/DocumentEditor/DocumentEditor.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/* eslint-disable max-len, no-return-assign */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormGroup, ControlLabel, Button } from 'react-bootstrap';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Bert } from 'meteor/themeteorchef:bert';
|
||||
import validate from '../../../modules/validate';
|
||||
|
||||
class DocumentEditor extends React.Component {
|
||||
componentDidMount() {
|
||||
const component = this;
|
||||
validate(component.form, {
|
||||
rules: {
|
||||
title: {
|
||||
required: true,
|
||||
},
|
||||
body: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
title: {
|
||||
required: 'Need a title in here, Seuss.',
|
||||
},
|
||||
body: {
|
||||
required: 'This thneeds a body, please.',
|
||||
},
|
||||
},
|
||||
submitHandler() { component.handleSubmit(); },
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
const { history } = this.props;
|
||||
const existingDocument = this.props.doc && this.props.doc._id;
|
||||
const methodToCall = existingDocument ? 'documents.update' : 'documents.insert';
|
||||
const doc = {
|
||||
title: this.title.value.trim(),
|
||||
body: this.body.value.trim(),
|
||||
};
|
||||
|
||||
if (existingDocument) doc._id = existingDocument;
|
||||
|
||||
Meteor.call(methodToCall, doc, (error, documentId) => {
|
||||
if (error) {
|
||||
Bert.alert(error.reason, 'danger');
|
||||
} else {
|
||||
const confirmation = existingDocument ? 'Document updated!' : 'Document added!';
|
||||
this.form.reset();
|
||||
Bert.alert(confirmation, 'success');
|
||||
history.push(`/documents/${documentId}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { doc } = this.props;
|
||||
return (<form ref={form => (this.form = form)} onSubmit={event => event.preventDefault()}>
|
||||
<FormGroup>
|
||||
<ControlLabel>Title</ControlLabel>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
name="title"
|
||||
ref={title => (this.title = title)}
|
||||
defaultValue={doc && doc.title}
|
||||
placeholder="Oh, The Places You'll Go!"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<ControlLabel>Body</ControlLabel>
|
||||
<textarea
|
||||
className="form-control"
|
||||
name="body"
|
||||
ref={body => (this.body = body)}
|
||||
defaultValue={doc && doc.body}
|
||||
placeholder="Congratulations! Today is your day. You're off to Great Places! You're off and away!"
|
||||
/>
|
||||
</FormGroup>
|
||||
<Button type="submit" bsStyle="success">
|
||||
{doc && doc._id ? 'Save Changes' : 'Add Document'}
|
||||
</Button>
|
||||
</form>);
|
||||
}
|
||||
}
|
||||
|
||||
DocumentEditor.defaultProps = {
|
||||
doc: { title: '', body: '' },
|
||||
};
|
||||
|
||||
DocumentEditor.propTypes = {
|
||||
doc: PropTypes.object,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default DocumentEditor;
|
||||
27
imports/ui/components/Footer/Footer.js
Normal file
27
imports/ui/components/Footer/Footer.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import { year } from '@cleverbeagle/dates';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Grid } from 'react-bootstrap';
|
||||
|
||||
import './Footer.scss';
|
||||
|
||||
const copyrightYear = () => {
|
||||
const currentYear = year();
|
||||
return currentYear === '2017' ? '2017' : `2017-${currentYear}`;
|
||||
};
|
||||
|
||||
const Footer = () => (
|
||||
<div className="Footer">
|
||||
<Grid>
|
||||
<p className="pull-left">© {copyrightYear()} Application Name</p>
|
||||
<ul className="pull-right">
|
||||
<li><Link to="/terms">Terms<span className="hidden-xs"> of Service</span></Link></li>
|
||||
<li><Link to="/privacy">Privacy<span className="hidden-xs"> Policy</span></Link></li>
|
||||
</ul>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
|
||||
Footer.propTypes = {};
|
||||
|
||||
export default Footer;
|
||||
55
imports/ui/components/Footer/Footer.scss
Normal file
55
imports/ui/components/Footer/Footer.scss
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
@import '../../stylesheets/mixins';
|
||||
@import '../../stylesheets/colors';
|
||||
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-bottom: 80px;
|
||||
}
|
||||
|
||||
.Footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid $gray-lighter;
|
||||
padding: 20px 0;
|
||||
|
||||
p {
|
||||
color: $gray-light;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul li {
|
||||
float: left;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $gray-light;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:active,
|
||||
a:focus {
|
||||
text-decoration: none;
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
.Footer ul li:first-child {
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
10
imports/ui/components/Icon/Icon.js
Normal file
10
imports/ui/components/Icon/Icon.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Icon = ({ icon }) => (<i className={`fa fa-${icon}`} />);
|
||||
|
||||
Icon.propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
16
imports/ui/components/InputHint/InputHint.js
Normal file
16
imports/ui/components/InputHint/InputHint.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './InputHint.scss';
|
||||
|
||||
const InputHint = ({ children }) => (
|
||||
<div className="InputHint">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
InputHint.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default InputHint;
|
||||
9
imports/ui/components/InputHint/InputHint.scss
Normal file
9
imports/ui/components/InputHint/InputHint.scss
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
@import '../../stylesheets/colors';
|
||||
|
||||
.InputHint {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-style: italic;
|
||||
color: $gray-light;
|
||||
font-size: 13px;
|
||||
}
|
||||
56
imports/ui/components/Loading/Loading.js
Normal file
56
imports/ui/components/Loading/Loading.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
|
||||
const Loading = () => (
|
||||
<div className="Loading">
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="#4285F4">
|
||||
<g fill="none" fillRule="evenodd" strokeWidth="2">
|
||||
<circle cx="22" cy="22" r="1">
|
||||
<animate
|
||||
attributeName="r"
|
||||
begin="0s"
|
||||
dur="1.8s"
|
||||
values="1; 20"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.165, 0.84, 0.44, 1"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="stroke-opacity"
|
||||
begin="0s"
|
||||
dur="1.8s"
|
||||
values="1; 0"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.3, 0.61, 0.355, 1"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
<circle cx="22" cy="22" r="1">
|
||||
<animate
|
||||
attributeName="r"
|
||||
begin="-0.9s"
|
||||
dur="1.8s"
|
||||
values="1; 20"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.165, 0.84, 0.44, 1"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="stroke-opacity"
|
||||
begin="-0.9s"
|
||||
dur="1.8s"
|
||||
values="1; 0"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 1"
|
||||
keySplines="0.3, 0.61, 0.355, 1"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Loading;
|
||||
32
imports/ui/components/Navigation/Navigation.js
Normal file
32
imports/ui/components/Navigation/Navigation.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Navbar } from 'react-bootstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PublicNavigation from '../PublicNavigation/PublicNavigation';
|
||||
import AuthenticatedNavigation from '../AuthenticatedNavigation/AuthenticatedNavigation';
|
||||
|
||||
import './Navigation.scss';
|
||||
|
||||
const Navigation = props => (
|
||||
<Navbar>
|
||||
<Navbar.Header>
|
||||
<Navbar.Brand>
|
||||
<Link to="/">Pup</Link>
|
||||
</Navbar.Brand>
|
||||
<Navbar.Toggle />
|
||||
</Navbar.Header>
|
||||
<Navbar.Collapse>
|
||||
{!props.authenticated ? <PublicNavigation /> : <AuthenticatedNavigation {...props} />}
|
||||
</Navbar.Collapse>
|
||||
</Navbar>
|
||||
);
|
||||
|
||||
Navigation.defaultProps = {
|
||||
name: '',
|
||||
};
|
||||
|
||||
Navigation.propTypes = {
|
||||
authenticated: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default Navigation;
|
||||
6
imports/ui/components/Navigation/Navigation.scss
Normal file
6
imports/ui/components/Navigation/Navigation.scss
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.navbar {
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
}
|
||||
60
imports/ui/components/OAuthLoginButton/OAuthLoginButton.js
Normal file
60
imports/ui/components/OAuthLoginButton/OAuthLoginButton.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Bert } from 'meteor/themeteorchef:bert';
|
||||
import Icon from '../Icon/Icon';
|
||||
|
||||
import './OAuthLoginButton.scss';
|
||||
|
||||
const handleLogin = (service, callback) => {
|
||||
const options = {
|
||||
facebook: {
|
||||
requestPermissions: ['email'],
|
||||
loginStyle: 'popup',
|
||||
},
|
||||
github: {
|
||||
requestPermissions: ['user:email'],
|
||||
loginStyle: 'popup',
|
||||
},
|
||||
google: {
|
||||
requestPermissions: ['email', 'profile'],
|
||||
requestOfflineToken: true,
|
||||
loginStyle: 'popup',
|
||||
},
|
||||
}[service];
|
||||
|
||||
return {
|
||||
facebook: Meteor.loginWithFacebook,
|
||||
github: Meteor.loginWithGithub,
|
||||
google: Meteor.loginWithGoogle,
|
||||
}[service](options, callback);
|
||||
};
|
||||
|
||||
const serviceLabel = {
|
||||
facebook: <span><Icon icon="facebook-official" /> Log In with Facebook</span>,
|
||||
github: <span><Icon icon="github" /> Log In with GitHub</span>,
|
||||
google: <span><Icon icon="google" /> Log In with Google</span>,
|
||||
};
|
||||
|
||||
const OAuthLoginButton = ({ service, callback }) => (
|
||||
<button
|
||||
className={`OAuthLoginButton OAuthLoginButton-${service}`}
|
||||
type="button"
|
||||
onClick={() => handleLogin(service, callback)}
|
||||
>
|
||||
{serviceLabel[service]}
|
||||
</button>
|
||||
);
|
||||
|
||||
OAuthLoginButton.defaultProps = {
|
||||
callback: (error) => {
|
||||
if (error) Bert.alert(error.message, 'danger');
|
||||
},
|
||||
};
|
||||
|
||||
OAuthLoginButton.propTypes = {
|
||||
service: PropTypes.string.isRequired,
|
||||
callback: PropTypes.func,
|
||||
};
|
||||
|
||||
export default OAuthLoginButton;
|
||||
52
imports/ui/components/OAuthLoginButton/OAuthLoginButton.scss
Normal file
52
imports/ui/components/OAuthLoginButton/OAuthLoginButton.scss
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
@import '../../stylesheets/colors';
|
||||
|
||||
.OAuthLoginButton {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
background: $gray-lighter;
|
||||
border-radius: 3px;
|
||||
|
||||
i {
|
||||
margin-right: 3px;
|
||||
font-size: 18px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
&.OAuthLoginButton-facebook {
|
||||
background: $facebook;
|
||||
color: #fff;
|
||||
|
||||
&:hover { background: darken($facebook, 5%); }
|
||||
}
|
||||
|
||||
&.OAuthLoginButton-google {
|
||||
background: $google;
|
||||
color: #fff;
|
||||
|
||||
&:hover { background: darken($google, 5%); }
|
||||
}
|
||||
|
||||
&.OAuthLoginButton-github {
|
||||
background: $github;
|
||||
color: #fff;
|
||||
|
||||
&:hover { background: darken($github, 5%); }
|
||||
}
|
||||
|
||||
&:active {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.OAuthLoginButton + .OAuthLoginButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
42
imports/ui/components/OAuthLoginButtons/OAuthLoginButtons.js
Normal file
42
imports/ui/components/OAuthLoginButtons/OAuthLoginButtons.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import OAuthLoginButton from '../OAuthLoginButton/OAuthLoginButton';
|
||||
|
||||
import './OAuthLoginButtons.scss';
|
||||
|
||||
const OAuthLoginButtons = ({ services, emailMessage }) => (services.length ? (
|
||||
<div className={`OAuthLoginButtons ${emailMessage ? 'WithEmailMessage' : ''}`}>
|
||||
{services.map(service => <OAuthLoginButton key={service} service={service} />)}
|
||||
{emailMessage ? <p className="EmailMessage" style={{ marginLeft: `-${emailMessage.offset}px` }}>
|
||||
{emailMessage.text}
|
||||
</p> : ''}
|
||||
</div>
|
||||
) : <div />);
|
||||
|
||||
OAuthLoginButtons.propTypes = {
|
||||
services: PropTypes.array.isRequired,
|
||||
emailMessage: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const verificationComplete = new ReactiveVar(false);
|
||||
const verifiedServices = new ReactiveVar([]);
|
||||
|
||||
export default createContainer(({ services }) => {
|
||||
if (!verificationComplete.get()) {
|
||||
Meteor.call('oauth.verifyConfiguration', services, (error, response) => {
|
||||
if (error) {
|
||||
console.warn(error);
|
||||
} else {
|
||||
verifiedServices.set(response);
|
||||
verificationComplete.set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
services: verifiedServices.get(),
|
||||
};
|
||||
}, OAuthLoginButtons);
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
@import '../../stylesheets/colors';
|
||||
|
||||
.OAuthLoginButtons {
|
||||
margin-bottom: 25px;
|
||||
|
||||
&.WithEmailMessage {
|
||||
position: relative;
|
||||
border-bottom: 1px solid $gray-lighter;
|
||||
padding-bottom: 30px;
|
||||
|
||||
.EmailMessage {
|
||||
display: inline-block;
|
||||
background: #fff;
|
||||
padding: 0 10px;
|
||||
position: absolute;
|
||||
bottom: -19px;
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
imports/ui/components/PageHeader/PageHeader.js
Normal file
24
imports/ui/components/PageHeader/PageHeader.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './PageHeader.scss';
|
||||
|
||||
const PageHeader = ({ title, subtitle }) => (
|
||||
<div className="PageHeader">
|
||||
<div className="PageHeader-container">
|
||||
<h1>{title}</h1>
|
||||
{subtitle ? <p>{subtitle}</p> : ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
PageHeader.defaultProps = {
|
||||
subtitle: '',
|
||||
};
|
||||
|
||||
PageHeader.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string,
|
||||
};
|
||||
|
||||
export default PageHeader;
|
||||
47
imports/ui/components/PageHeader/PageHeader.scss
Normal file
47
imports/ui/components/PageHeader/PageHeader.scss
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
@import '../../stylesheets/mixins';
|
||||
@import '../../stylesheets/colors';
|
||||
|
||||
.PageHeader {
|
||||
border-bottom: 1px solid $gray-lighter;
|
||||
padding: 0px 0 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
color: $gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.PageHeader-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
.PageHeader {
|
||||
padding: 10px 0 30px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(desktop) {
|
||||
.PageHeader {
|
||||
padding: 20px 0 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
23
imports/ui/components/Public/Public.js
Normal file
23
imports/ui/components/Public/Public.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route, Redirect } from 'react-router-dom';
|
||||
|
||||
const Public = ({ loggingIn, authenticated, component, path, exact, ...rest }) => (
|
||||
<Route
|
||||
path={path}
|
||||
exact={exact}
|
||||
render={props => (
|
||||
!authenticated ?
|
||||
(React.createElement(component, { ...props, ...rest, loggingIn, authenticated })) :
|
||||
(<Redirect to="/documents" />)
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
Public.propTypes = {
|
||||
loggingIn: PropTypes.bool.isRequired,
|
||||
authenticated: PropTypes.bool.isRequired,
|
||||
component: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Public;
|
||||
16
imports/ui/components/PublicNavigation/PublicNavigation.js
Normal file
16
imports/ui/components/PublicNavigation/PublicNavigation.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
import { Nav, NavItem } from 'react-bootstrap';
|
||||
|
||||
const PublicNavigation = () => (
|
||||
<Nav pullRight>
|
||||
<LinkContainer to="/signup">
|
||||
<NavItem eventKey={1} href="/signup">Sign Up</NavItem>
|
||||
</LinkContainer>
|
||||
<LinkContainer to="/login">
|
||||
<NavItem eventKey={2} href="/login">Log In</NavItem>
|
||||
</LinkContainer>
|
||||
</Nav>
|
||||
);
|
||||
|
||||
export default PublicNavigation;
|
||||
109
imports/ui/layouts/App/App.js
Normal file
109
imports/ui/layouts/App/App.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/* eslint-disable jsx-a11y/no-href*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
|
||||
import { Grid, Alert, Button } from 'react-bootstrap';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import { Roles } from 'meteor/alanning:roles';
|
||||
import { Bert } from 'meteor/themeteorchef:bert';
|
||||
import Navigation from '../../components/Navigation/Navigation';
|
||||
import Authenticated from '../../components/Authenticated/Authenticated';
|
||||
import Public from '../../components/Public/Public';
|
||||
import Index from '../../pages/Index/Index';
|
||||
import Documents from '../../pages/Documents/Documents';
|
||||
import NewDocument from '../../pages/NewDocument/NewDocument';
|
||||
import ViewDocument from '../../pages/ViewDocument/ViewDocument';
|
||||
import EditDocument from '../../pages/EditDocument/EditDocument';
|
||||
import Signup from '../../pages/Signup/Signup';
|
||||
import Login from '../../pages/Login/Login';
|
||||
import Logout from '../../pages/Logout/Logout';
|
||||
import VerifyEmail from '../../pages/VerifyEmail/VerifyEmail';
|
||||
import RecoverPassword from '../../pages/RecoverPassword/RecoverPassword';
|
||||
import ResetPassword from '../../pages/ResetPassword/ResetPassword';
|
||||
import Profile from '../../pages/Profile/Profile';
|
||||
import NotFound from '../../pages/NotFound/NotFound';
|
||||
import Footer from '../../components/Footer/Footer';
|
||||
import Terms from '../../pages/Terms/Terms';
|
||||
import Privacy from '../../pages/Privacy/Privacy';
|
||||
import ExamplePage from '../../pages/ExamplePage/ExamplePage';
|
||||
|
||||
import './App.scss';
|
||||
|
||||
const handleResendVerificationEmail = (emailAddress) => {
|
||||
Meteor.call('users.sendVerificationEmail', (error) => {
|
||||
if (error) {
|
||||
Bert.alert(error.reason, 'danger');
|
||||
} else {
|
||||
Bert.alert(`Check ${emailAddress} for a verification link!`, 'success');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const App = props => (
|
||||
<Router>
|
||||
{!props.loading ? <div className="App">
|
||||
{props.userId && !props.emailVerified ? <Alert className="verify-email text-center"><p>Hey friend! Can you <strong>verify your email address</strong> ({props.emailAddress}) for us? <Button bsStyle="link" onClick={() => handleResendVerificationEmail(props.emailAddress)} href="#">Re-send verification email</Button></p></Alert> : ''}
|
||||
<Navigation {...props} />
|
||||
<Grid>
|
||||
<Switch>
|
||||
<Route exact name="index" path="/" component={Index} />
|
||||
<Authenticated exact path="/documents" component={Documents} {...props} />
|
||||
<Authenticated exact path="/documents/new" component={NewDocument} {...props} />
|
||||
<Authenticated exact path="/documents/:_id" component={ViewDocument} {...props} />
|
||||
<Authenticated exact path="/documents/:_id/edit" component={EditDocument} {...props} />
|
||||
<Authenticated exact path="/profile" component={Profile} {...props} />
|
||||
<Public path="/signup" component={Signup} {...props} />
|
||||
<Public path="/login" component={Login} {...props} />
|
||||
<Route path="/logout" component={Logout} {...props} />
|
||||
<Route name="verify-email" path="/verify-email/:token" component={VerifyEmail} />
|
||||
<Route name="recover-password" path="/recover-password" component={RecoverPassword} />
|
||||
<Route name="reset-password" path="/reset-password/:token" component={ResetPassword} />
|
||||
<Route name="terms" path="/terms" component={Terms} />
|
||||
<Route name="privacy" path="/privacy" component={Privacy} />
|
||||
<Route name="examplePage" path="/example-page" component={ExamplePage} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</Grid>
|
||||
<Footer />
|
||||
</div> : ''}
|
||||
</Router>
|
||||
);
|
||||
|
||||
App.defaultProps = {
|
||||
userId: '',
|
||||
emailAddress: '',
|
||||
};
|
||||
|
||||
App.propTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
userId: PropTypes.string,
|
||||
emailAddress: PropTypes.string,
|
||||
emailVerified: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const getUserName = name => ({
|
||||
string: name,
|
||||
object: `${name.first} ${name.last}`,
|
||||
}[typeof name]);
|
||||
|
||||
export default createContainer(() => {
|
||||
const loggingIn = Meteor.loggingIn();
|
||||
const user = Meteor.user();
|
||||
const userId = Meteor.userId();
|
||||
const loading = !Roles.subscription.ready();
|
||||
const name = user && user.profile && user.profile.name && getUserName(user.profile.name);
|
||||
const emailAddress = user && user.emails && user.emails[0].address;
|
||||
|
||||
return {
|
||||
loading,
|
||||
loggingIn,
|
||||
authenticated: !loggingIn && !!userId,
|
||||
name: name || emailAddress,
|
||||
roles: !loading && Roles.getRolesForUser(userId),
|
||||
userId,
|
||||
emailAddress,
|
||||
emailVerified: user && user.emails ? user && user.emails && user.emails[0].verified : true,
|
||||
};
|
||||
}, App);
|
||||
23
imports/ui/layouts/App/App.scss
Normal file
23
imports/ui/layouts/App/App.scss
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
@import '../../stylesheets/colors';
|
||||
|
||||
.App > .container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.App .verify-email {
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
border-bottom: 1px solid #e7e7e7;
|
||||
background: #fff;
|
||||
color: $gray-dark;
|
||||
border-radius: 0;
|
||||
|
||||
p {
|
||||
padding: 19px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
82
imports/ui/pages/Documents/Documents.js
Normal file
82
imports/ui/pages/Documents/Documents.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Table, Alert, Button } from 'react-bootstrap';
|
||||
import { timeago, monthDayYearAtTime } from '@cleverbeagle/dates';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import { Bert } from 'meteor/themeteorchef:bert';
|
||||
import DocumentsCollection from '../../../api/Documents/Documents';
|
||||
import Loading from '../../components/Loading/Loading';
|
||||
|
||||
import './Documents.scss';
|
||||
|
||||
const handleRemove = (documentId) => {
|
||||
if (confirm('Are you sure? This is permanent!')) {
|
||||
Meteor.call('documents.remove', documentId, (error) => {
|
||||
if (error) {
|
||||
Bert.alert(error.reason, 'danger');
|
||||
} else {
|
||||
Bert.alert('Document deleted!', 'success');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const Documents = ({ loading, documents, match, history }) => (!loading ? (
|
||||
<div className="Documents">
|
||||
<div className="page-header clearfix">
|
||||
<h4 className="pull-left">Documents</h4>
|
||||
<Link className="btn btn-success pull-right" to={`${match.url}/new`}>Add Document</Link>
|
||||
</div>
|
||||
{documents.length ? <Table responsive>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Created</th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{documents.map(({ _id, title, createdAt, updatedAt }) => (
|
||||
<tr key={_id}>
|
||||
<td>{title}</td>
|
||||
<td>{timeago(updatedAt)}</td>
|
||||
<td>{monthDayYearAtTime(createdAt)}</td>
|
||||
<td>
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
onClick={() => history.push(`${match.url}/${_id}`)}
|
||||
block
|
||||
>View</Button>
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
bsStyle="danger"
|
||||
onClick={() => handleRemove(_id)}
|
||||
block
|
||||
>Delete</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table> : <Alert bsStyle="warning">No documents yet!</Alert>}
|
||||
</div>
|
||||
) : <Loading />);
|
||||
|
||||
Documents.propTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
documents: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
match: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default createContainer(() => {
|
||||
const subscription = Meteor.subscribe('documents');
|
||||
return {
|
||||
loading: !subscription.ready(),
|
||||
documents: DocumentsCollection.find().fetch(),
|
||||
};
|
||||
}, Documents);
|
||||
3
imports/ui/pages/Documents/Documents.scss
Normal file
3
imports/ui/pages/Documents/Documents.scss
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.Documents table tbody tr td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
33
imports/ui/pages/EditDocument/EditDocument.js
Normal file
33
imports/ui/pages/EditDocument/EditDocument.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import Documents from '../../../api/Documents/Documents';
|
||||
import DocumentEditor from '../../components/DocumentEditor/DocumentEditor';
|
||||
import NotFound from '../NotFound/NotFound';
|
||||
|
||||
const EditDocument = ({ doc, history }) => (doc ? (
|
||||
<div className="EditDocument">
|
||||
<h4 className="page-header">{`Editing "${doc.title}"`}</h4>
|
||||
<DocumentEditor doc={doc} history={history} />
|
||||
</div>
|
||||
) : <NotFound />);
|
||||
|
||||
EditDocument.defaultProps = {
|
||||
doc: null,
|
||||
};
|
||||
|
||||
EditDocument.propTypes = {
|
||||
doc: PropTypes.object,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default createContainer(({ match }) => {
|
||||
const documentId = match.params._id;
|
||||
const subscription = Meteor.subscribe('documents.view', documentId);
|
||||
|
||||
return {
|
||||
loading: !subscription.ready(),
|
||||
doc: Documents.findOne(documentId),
|
||||
};
|
||||
}, EditDocument);
|
||||
14
imports/ui/pages/ExamplePage/ExamplePage.js
Normal file
14
imports/ui/pages/ExamplePage/ExamplePage.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import Page from '../Page/Page';
|
||||
|
||||
const ExamplePage = () => (
|
||||
<div className="ExamplePage">
|
||||
<Page
|
||||
title="My Example Page"
|
||||
subtitle="A subtitle for my example page."
|
||||
page="example-page"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ExamplePage;
|
||||
24
imports/ui/pages/Index/Index.js
Normal file
24
imports/ui/pages/Index/Index.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
|
||||
import './Index.scss';
|
||||
|
||||
const Index = () => (
|
||||
<div className="Index">
|
||||
<img
|
||||
src="https://s3-us-west-2.amazonaws.com/cleverbeagle-assets/graphics/email-icon.png"
|
||||
alt="Clever Beagle"
|
||||
/>
|
||||
<h1>Pup</h1>
|
||||
<p>A boilerplate for products.</p>
|
||||
<div>
|
||||
<Button href="http://cleverbeagle.com/pup">Read the Docs</Button>
|
||||
<Button href="https://github.com/cleverbeagle/pup"><i className="fa fa-star" /> Star on GitHub</Button>
|
||||
</div>
|
||||
<footer>
|
||||
<p>Need help and want to stay accountable building your product? <a href="http://cleverbeagle.com?utm_source=pupappindex&utm_campaign=oss">Check out Clever Beagle</a>.</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Index;
|
||||
75
imports/ui/pages/Index/Index.scss
Normal file
75
imports/ui/pages/Index/Index.scss
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
@import '../../stylesheets/mixins';
|
||||
@import '../../stylesheets/colors';
|
||||
|
||||
.Index {
|
||||
padding: 20px;
|
||||
background: $cb-blue;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
color: lighten($cb-blue, 25%);
|
||||
}
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
margin: 10px 0 0;
|
||||
|
||||
.btn:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
margin: 20px -20px -20px;
|
||||
border-top: 1px solid darken($cb-blue, 10%);
|
||||
padding: 20px;
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: lighten($cb-blue, 35%);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p a {
|
||||
color: lighten($cb-blue, 35%);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
.Index {
|
||||
padding: 30px;
|
||||
|
||||
footer {
|
||||
margin: 30px -30px -30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(desktop) {
|
||||
.Index {
|
||||
padding: 40px;
|
||||
|
||||
footer {
|
||||
margin: 40px -40px -40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
108
imports/ui/pages/Login/Login.js
Normal file
108
imports/ui/pages/Login/Login.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import React from 'react';
|
||||
import { Row, Col, FormGroup, ControlLabel, Button } from 'react-bootstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Bert } from 'meteor/themeteorchef:bert';
|
||||
import OAuthLoginButtons from '../../components/OAuthLoginButtons/OAuthLoginButtons';
|
||||
import AccountPageFooter from '../../components/AccountPageFooter/AccountPageFooter';
|
||||
import validate from '../../../modules/validate';
|
||||
|
||||
class Login extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const component = this;
|
||||
|
||||
validate(component.form, {
|
||||
rules: {
|
||||
emailAddress: {
|
||||
required: true,
|
||||
email: true,
|
||||
},
|
||||
password: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
emailAddress: {
|
||||
required: 'Need an email address here.',
|
||||
email: 'Is this email address correct?',
|
||||
},
|
||||
password: {
|
||||
required: 'Need a password here.',
|
||||
},
|
||||
},
|
||||
submitHandler() { component.handleSubmit(); },
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
const { history } = this.props;
|
||||
|
||||
Meteor.loginWithPassword(this.emailAddress.value, this.password.value, (error) => {
|
||||
if (error) {
|
||||
Bert.alert(error.reason, 'danger');
|
||||
} else {
|
||||
Bert.alert('Welcome back!', 'success');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div className="Login">
|
||||
<Row>
|
||||
<Col xs={12} sm={6} md={5} lg={4}>
|
||||
<h4 className="page-header">Log In</h4>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<OAuthLoginButtons
|
||||
services={['facebook', 'github', 'google']}
|
||||
emailMessage={{
|
||||
offset: 100,
|
||||
text: 'Log In with an Email Address',
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<form ref={form => (this.form = form)} onSubmit={event => event.preventDefault()}>
|
||||
<FormGroup>
|
||||
<ControlLabel>Email Address</ControlLabel>
|
||||
<input
|
||||
type="email"
|
||||
name="emailAddress"
|
||||
ref={emailAddress => (this.emailAddress = emailAddress)}
|
||||
className="form-control"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<ControlLabel className="clearfix">
|
||||
<span className="pull-left">Password</span>
|
||||
<Link className="pull-right" to="/recover-password">Forgot password?</Link>
|
||||
</ControlLabel>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
ref={password => (this.password = password)}
|
||||
className="form-control"
|
||||
/>
|
||||
</FormGroup>
|
||||
<Button type="submit" bsStyle="success">Log In</Button>
|
||||
<AccountPageFooter>
|
||||
<p>{'Don\'t have an account?'} <Link to="/signup">Sign Up</Link>.</p>
|
||||
</AccountPageFooter>
|
||||
</form>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
Login.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Login;
|
||||
32
imports/ui/pages/Logout/Logout.js
Normal file
32
imports/ui/pages/Logout/Logout.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import Icon from '../../components/Icon/Icon';
|
||||
|
||||
import './Logout.scss';
|
||||
|
||||
class Logout extends React.Component {
|
||||
componentDidMount() {
|
||||
Meteor.logout();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="Logout">
|
||||
<img
|
||||
src="https://s3-us-west-2.amazonaws.com/cleverbeagle-assets/graphics/email-icon.png"
|
||||
alt="Clever Beagle"
|
||||
/>
|
||||
<h1>Stay safe out there.</h1>
|
||||
<p>{'Don\'t forget to like and follow Clever Beagle elsewhere on the web:'}</p>
|
||||
<ul className="FollowUsElsewhere">
|
||||
<li><a href="https://facebook.com/cleverbeagle"><Icon icon="facebook-official" /></a></li>
|
||||
<li><a href="https://twitter.com/clvrbgl"><Icon icon="twitter" /></a></li>
|
||||
<li><a href="https://github.com/cleverbeagle"><Icon icon="github" /></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Logout.propTypes = {};
|
||||
|
||||
export default Logout;
|
||||
57
imports/ui/pages/Logout/Logout.scss
Normal file
57
imports/ui/pages/Logout/Logout.scss
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
@import '../../stylesheets/mixins';
|
||||
@import '../../stylesheets/colors';
|
||||
|
||||
.Logout {
|
||||
padding: 20px;
|
||||
background: $cb-blue;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
color: lighten($cb-blue, 25%);
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
ul li {
|
||||
float: left;
|
||||
font-size: 28px;
|
||||
line-height: 28px;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
.Logout {
|
||||
padding: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(desktop) {
|
||||
.Logout {
|
||||
padding: 40px;
|
||||
}
|
||||
}
|
||||
16
imports/ui/pages/NewDocument/NewDocument.js
Normal file
16
imports/ui/pages/NewDocument/NewDocument.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DocumentEditor from '../../components/DocumentEditor/DocumentEditor';
|
||||
|
||||
const NewDocument = ({ history }) => (
|
||||
<div className="NewDocument">
|
||||
<h4 className="page-header">New Document</h4>
|
||||
<DocumentEditor history={history} />
|
||||
</div>
|
||||
);
|
||||
|
||||
NewDocument.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default NewDocument;
|
||||
12
imports/ui/pages/NotFound/NotFound.js
Normal file
12
imports/ui/pages/NotFound/NotFound.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
|
||||
const NotFound = () => (
|
||||
<div className="NotFound">
|
||||
<Alert bsStyle="danger">
|
||||
<p><strong>Error [404]</strong>: {window.location.pathname} does not exist.</p>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NotFound;
|
||||
44
imports/ui/pages/Page/Page.js
Normal file
44
imports/ui/pages/Page/Page.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import PageHeader from '../../components/PageHeader/PageHeader';
|
||||
import Content from '../../components/Content/Content';
|
||||
|
||||
import './Page.scss';
|
||||
|
||||
const Page = ({ title, subtitle, content }) => (
|
||||
<div className="Page">
|
||||
<PageHeader title={title} subtitle={subtitle} />
|
||||
<Content content={content} />
|
||||
</div>
|
||||
);
|
||||
|
||||
Page.defaultProps = {
|
||||
subtitle: '',
|
||||
};
|
||||
|
||||
Page.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string,
|
||||
content: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const pageContent = new ReactiveVar('');
|
||||
|
||||
export default createContainer(({ content, page }) => {
|
||||
window.scrollTo(0, 0); // Force window to top of page.
|
||||
|
||||
Meteor.call('utility.getPage', page, (error, response) => {
|
||||
if (error) {
|
||||
console.warn(error);
|
||||
} else {
|
||||
pageContent.set(response);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
content: content || pageContent.get(),
|
||||
};
|
||||
}, Page);
|
||||
11
imports/ui/pages/Page/Page.scss
Normal file
11
imports/ui/pages/Page/Page.scss
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
@import '../../stylesheets/mixins';
|
||||
|
||||
.Page {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
.Page {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
14
imports/ui/pages/Privacy/Privacy.js
Normal file
14
imports/ui/pages/Privacy/Privacy.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import Page from '../Page/Page';
|
||||
|
||||
const Privacy = () => (
|
||||
<div className="Privacy">
|
||||
<Page
|
||||
title="Privacy Policy"
|
||||
subtitle="Last updated May 29th, 2017"
|
||||
page="privacy"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Privacy;
|
||||
230
imports/ui/pages/Profile/Profile.js
Normal file
230
imports/ui/pages/Profile/Profile.js
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Row, Col, FormGroup, ControlLabel, Button } from 'react-bootstrap';
|
||||
import _ from 'lodash';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Bert } from 'meteor/themeteorchef:bert';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import InputHint from '../../components/InputHint/InputHint';
|
||||
import validate from '../../../modules/validate';
|
||||
|
||||
import './Profile.scss';
|
||||
|
||||
class Profile extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getUserType = this.getUserType.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.renderOAuthUser = this.renderOAuthUser.bind(this);
|
||||
this.renderPasswordUser = this.renderPasswordUser.bind(this);
|
||||
this.renderProfileForm = this.renderProfileForm.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const component = this;
|
||||
|
||||
validate(component.form, {
|
||||
rules: {
|
||||
firstName: {
|
||||
required: true,
|
||||
},
|
||||
lastName: {
|
||||
required: true,
|
||||
},
|
||||
emailAddress: {
|
||||
required: true,
|
||||
email: true,
|
||||
},
|
||||
currentPassword: {
|
||||
required() {
|
||||
// Only required if newPassword field has a value.
|
||||
return component.newPassword.value.length > 0;
|
||||
},
|
||||
},
|
||||
newPassword: {
|
||||
required() {
|
||||
// Only required if currentPassword field has a value.
|
||||
return component.currentPassword.value.length > 0;
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
firstName: {
|
||||
required: 'What\'s your first name?',
|
||||
},
|
||||
lastName: {
|
||||
required: 'What\'s your last name?',
|
||||
},
|
||||
emailAddress: {
|
||||
required: 'Need an email address here.',
|
||||
email: 'Is this email address correct?',
|
||||
},
|
||||
currentPassword: {
|
||||
required: 'Need your current password if changing.',
|
||||
},
|
||||
newPassword: {
|
||||
required: 'Need your new password if changing.',
|
||||
},
|
||||
},
|
||||
submitHandler() { component.handleSubmit(); },
|
||||
});
|
||||
}
|
||||
|
||||
getUserType(user) {
|
||||
const userToCheck = user;
|
||||
delete userToCheck.services.resume;
|
||||
const service = Object.keys(userToCheck.services)[0];
|
||||
return service === 'password' ? 'password' : 'oauth';
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
const profile = {
|
||||
emailAddress: this.emailAddress.value,
|
||||
profile: {
|
||||
name: {
|
||||
first: this.firstName.value,
|
||||
last: this.lastName.value,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Meteor.call('users.editProfile', profile, (error) => {
|
||||
if (error) {
|
||||
Bert.alert(error.reason, 'danger');
|
||||
} else {
|
||||
Bert.alert('Profile updated!', 'success');
|
||||
}
|
||||
});
|
||||
|
||||
if (this.newPassword.value) {
|
||||
Accounts.changePassword(this.currentPassword.value, this.newPassword.value, (error) => {
|
||||
if (error) {
|
||||
Bert.alert(error.reason, 'danger');
|
||||
} else {
|
||||
this.currentPassword.value = '';
|
||||
this.newPassword.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderOAuthUser(loading, user) {
|
||||
return !loading ? (<div className="OAuthProfile">
|
||||
{Object.keys(user.services).map(service => (
|
||||
<div key={service} className={`LoggedInWith ${service}`}>
|
||||
<img src={`/${service}.svg`} alt={service} />
|
||||
<p>{`You're logged in with ${_.capitalize(service)} using the email address ${user.services[service].email}.`}</p>
|
||||
<Button
|
||||
className={`btn btn-${service}`}
|
||||
href={{
|
||||
facebook: 'https://www.facebook.com/settings',
|
||||
google: 'https://myaccount.google.com/privacy#personalinfo',
|
||||
github: 'https://github.com/settings/profile',
|
||||
}[service]}
|
||||
target="_blank"
|
||||
>Edit Profile on {_.capitalize(service)}</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>) : <div />;
|
||||
}
|
||||
|
||||
renderPasswordUser(loading, user) {
|
||||
return !loading ? (<div>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<FormGroup>
|
||||
<ControlLabel>First Name</ControlLabel>
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
defaultValue={user.profile.name.first}
|
||||
ref={firstName => (this.firstName = firstName)}
|
||||
className="form-control"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<FormGroup>
|
||||
<ControlLabel>Last Name</ControlLabel>
|
||||
<input
|
||||
type="text"
|
||||
name="lastName"
|
||||
defaultValue={user.profile.name.last}
|
||||
ref={lastName => (this.lastName = lastName)}
|
||||
className="form-control"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<FormGroup>
|
||||
<ControlLabel>Email Address</ControlLabel>
|
||||
<input
|
||||
type="email"
|
||||
name="emailAddress"
|
||||
defaultValue={user.emails[0].address}
|
||||
ref={emailAddress => (this.emailAddress = emailAddress)}
|
||||
className="form-control"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<ControlLabel>Current Password</ControlLabel>
|
||||
<input
|
||||
type="password"
|
||||
name="currentPassword"
|
||||
ref={currentPassword => (this.currentPassword = currentPassword)}
|
||||
className="form-control"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<ControlLabel>New Password</ControlLabel>
|
||||
<input
|
||||
type="password"
|
||||
name="newPassword"
|
||||
ref={newPassword => (this.newPassword = newPassword)}
|
||||
className="form-control"
|
||||
/>
|
||||
<InputHint>Use at least six characters.</InputHint>
|
||||
</FormGroup>
|
||||
<Button type="submit" bsStyle="success">Save Profile</Button>
|
||||
</div>) : <div />;
|
||||
}
|
||||
|
||||
renderProfileForm(loading, user) {
|
||||
return !loading ? ({
|
||||
password: this.renderPasswordUser,
|
||||
oauth: this.renderOAuthUser,
|
||||
}[this.getUserType(user)])(loading, user) : <div />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, user } = this.props;
|
||||
return (<div className="Profile">
|
||||
<Row>
|
||||
<Col xs={12} sm={6} md={4}>
|
||||
<h4 className="page-header">Edit Profile</h4>
|
||||
<form ref={form => (this.form = form)} onSubmit={event => event.preventDefault()}>
|
||||
{this.renderProfileForm(loading, user)}
|
||||
</form>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
Profile.propTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default createContainer(() => {
|
||||
const subscription = Meteor.subscribe('users.editProfile');
|
||||
|
||||
return {
|
||||
loading: !subscription.ready(),
|
||||
user: Meteor.user(),
|
||||
};
|
||||
}, Profile);
|
||||
46
imports/ui/pages/Profile/Profile.scss
Normal file
46
imports/ui/pages/Profile/Profile.scss
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
@import '../../stylesheets/colors';
|
||||
|
||||
.OAuthProfile {
|
||||
.LoggedInWith {
|
||||
padding: 20px;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
border: 1px solid $gray-lighter;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
&.github img {
|
||||
width: 125px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 20px 0 0 0;
|
||||
color: $gray-light;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top: 20px;
|
||||
|
||||
&.btn-facebook {
|
||||
background: $facebook;
|
||||
border-color: $facebook;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.btn-google {
|
||||
background: $google;
|
||||
border-color: $google;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.btn-github {
|
||||
background: $github;
|
||||
border-color: $github;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
imports/ui/pages/RecoverPassword/RecoverPassword.js
Normal file
83
imports/ui/pages/RecoverPassword/RecoverPassword.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
import { Row, Col, Alert, FormGroup, ControlLabel, Button } from 'react-bootstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Bert } from 'meteor/themeteorchef:bert';
|
||||
import AccountPageFooter from '../../components/AccountPageFooter/AccountPageFooter';
|
||||
import validate from '../../../modules/validate';
|
||||
|
||||
class RecoverPassword extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const component = this;
|
||||
|
||||
validate(component.form, {
|
||||
rules: {
|
||||
emailAddress: {
|
||||
required: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
emailAddress: {
|
||||
required: 'Need an email address here.',
|
||||
email: 'Is this email address correct?',
|
||||
},
|
||||
},
|
||||
submitHandler() { component.handleSubmit(); },
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
const { history } = this.props;
|
||||
const email = this.emailAddress.value;
|
||||
|
||||
Accounts.forgotPassword({ email }, (error) => {
|
||||
if (error) {
|
||||
Bert.alert(error.reason, 'danger');
|
||||
} else {
|
||||
Bert.alert(`Check ${email} for a reset link!`, 'success');
|
||||
history.push('/login');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div className="RecoverPassword">
|
||||
<Row>
|
||||
<Col xs={12} sm={6} md={5} lg={4}>
|
||||
<h4 className="page-header">Recover Password</h4>
|
||||
<Alert bsStyle="info">
|
||||
Enter your email address below to receive a link to reset your password.
|
||||
</Alert>
|
||||
<form ref={form => (this.form = form)} onSubmit={event => event.preventDefault()}>
|
||||
<FormGroup>
|
||||
<ControlLabel>Email Address</ControlLabel>
|
||||
<input
|
||||
type="email"
|
||||
name="emailAddress"
|
||||
ref={emailAddress => (this.emailAddress = emailAddress)}
|
||||
className="form-control"
|
||||
/>
|
||||
</FormGroup>
|
||||
<Button type="submit" bsStyle="success">Recover Password</Button>
|
||||
<AccountPageFooter>
|
||||
<p>Remember your password? <Link to="/login">Log In</Link>.</p>
|
||||
</AccountPageFooter>
|
||||
</form>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
RecoverPassword.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default RecoverPassword;
|
||||
99
imports/ui/pages/ResetPassword/ResetPassword.js
Normal file
99
imports/ui/pages/ResetPassword/ResetPassword.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Row, Col, Alert, FormGroup, ControlLabel, Button } from 'react-bootstrap';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Bert } from 'meteor/themeteorchef:bert';
|
||||
import validate from '../../../modules/validate';
|
||||
|
||||
class ResetPassword extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const component = this;
|
||||
|
||||
validate(component.form, {
|
||||
rules: {
|
||||
newPassword: {
|
||||
required: true,
|
||||
minlength: 6,
|
||||
},
|
||||
repeatNewPassword: {
|
||||
required: true,
|
||||
minlength: 6,
|
||||
equalTo: '[name="newPassword"]',
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
newPassword: {
|
||||
required: 'Enter a new password, please.',
|
||||
minlength: 'Use at least six characters, please.',
|
||||
},
|
||||
repeatNewPassword: {
|
||||
required: 'Repeat your new password, please.',
|
||||
equalTo: 'Hmm, your passwords don\'t match. Try again?',
|
||||
},
|
||||
},
|
||||
submitHandler() { component.handleSubmit(); },
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
const { match, history } = this.props;
|
||||
const token = match.params.token;
|
||||
|
||||
Accounts.resetPassword(token, this.newPassword.value, (error) => {
|
||||
if (error) {
|
||||
Bert.alert(error.reason, 'danger');
|
||||
} else {
|
||||
history.push('/documents');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div className="ResetPassword">
|
||||
<Row>
|
||||
<Col xs={12} sm={6} md={4}>
|
||||
<h4 className="page-header">Reset Password</h4>
|
||||
<Alert bsStyle="info">
|
||||
To reset your password, enter a new one below. You will be logged in
|
||||
with your new password.
|
||||
</Alert>
|
||||
<form ref={form => (this.form = form)} onSubmit={event => event.preventDefault()}>
|
||||
<FormGroup>
|
||||
<ControlLabel>New Password</ControlLabel>
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
ref={newPassword => (this.newPassword = newPassword)}
|
||||
name="newPassword"
|
||||
placeholder="New Password"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<ControlLabel>Repeat New Password</ControlLabel>
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
ref={repeatNewPassword => (this.repeatNewPassword = repeatNewPassword)}
|
||||
name="repeatNewPassword"
|
||||
placeholder="Repeat New Password"
|
||||
/>
|
||||
</FormGroup>
|
||||
<Button type="submit" bsStyle="success">Reset Password & Login</Button>
|
||||
</form>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
ResetPassword.propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ResetPassword;
|
||||
157
imports/ui/pages/Signup/Signup.js
Normal file
157
imports/ui/pages/Signup/Signup.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import React from 'react';
|
||||
import { Row, Col, FormGroup, ControlLabel, Button } from 'react-bootstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Bert } from 'meteor/themeteorchef:bert';
|
||||
import OAuthLoginButtons from '../../components/OAuthLoginButtons/OAuthLoginButtons';
|
||||
import InputHint from '../../components/InputHint/InputHint';
|
||||
import AccountPageFooter from '../../components/AccountPageFooter/AccountPageFooter';
|
||||
import validate from '../../../modules/validate';
|
||||
|
||||
class Signup extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const component = this;
|
||||
|
||||
validate(component.form, {
|
||||
rules: {
|
||||
firstName: {
|
||||
required: true,
|
||||
},
|
||||
lastName: {
|
||||
required: true,
|
||||
},
|
||||
emailAddress: {
|
||||
required: true,
|
||||
email: true,
|
||||
},
|
||||
password: {
|
||||
required: true,
|
||||
minlength: 6,
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
firstName: {
|
||||
required: 'What\'s your first name?',
|
||||
},
|
||||
lastName: {
|
||||
required: 'What\'s your last name?',
|
||||
},
|
||||
emailAddress: {
|
||||
required: 'Need an email address here.',
|
||||
email: 'Is this email address correct?',
|
||||
},
|
||||
password: {
|
||||
required: 'Need a password here.',
|
||||
minlength: 'Please use at least six characters.',
|
||||
},
|
||||
},
|
||||
submitHandler() { component.handleSubmit(); },
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
const { history } = this.props;
|
||||
|
||||
Accounts.createUser({
|
||||
email: this.emailAddress.value,
|
||||
password: this.password.value,
|
||||
profile: {
|
||||
name: {
|
||||
first: this.firstName.value,
|
||||
last: this.lastName.value,
|
||||
},
|
||||
},
|
||||
}, (error) => {
|
||||
if (error) {
|
||||
Bert.alert(error.reason, 'danger');
|
||||
} else {
|
||||
Meteor.call('users.sendVerificationEmail');
|
||||
Bert.alert('Welcome!', 'success');
|
||||
history.push('/documents');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div className="Signup">
|
||||
<Row>
|
||||
<Col xs={12} sm={6} md={5} lg={4}>
|
||||
<h4 className="page-header">Sign Up</h4>
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<OAuthLoginButtons
|
||||
services={['facebook', 'github', 'google']}
|
||||
emailMessage={{
|
||||
offset: 97,
|
||||
text: 'Sign Up with an Email Address',
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<form ref={form => (this.form = form)} onSubmit={event => event.preventDefault()}>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<FormGroup>
|
||||
<ControlLabel>First Name</ControlLabel>
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
ref={firstName => (this.firstName = firstName)}
|
||||
className="form-control"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<FormGroup>
|
||||
<ControlLabel>Last Name</ControlLabel>
|
||||
<input
|
||||
type="text"
|
||||
name="lastName"
|
||||
ref={lastName => (this.lastName = lastName)}
|
||||
className="form-control"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<FormGroup>
|
||||
<ControlLabel>Email Address</ControlLabel>
|
||||
<input
|
||||
type="email"
|
||||
name="emailAddress"
|
||||
ref={emailAddress => (this.emailAddress = emailAddress)}
|
||||
className="form-control"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<ControlLabel>Password</ControlLabel>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
ref={password => (this.password = password)}
|
||||
className="form-control"
|
||||
/>
|
||||
<InputHint>Use at least six characters.</InputHint>
|
||||
</FormGroup>
|
||||
<Button type="submit" bsStyle="success">Sign Up</Button>
|
||||
<AccountPageFooter>
|
||||
<p>Already have an account? <Link to="/login">Log In</Link>.</p>
|
||||
</AccountPageFooter>
|
||||
</form>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
Signup.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Signup;
|
||||
14
imports/ui/pages/Terms/Terms.js
Normal file
14
imports/ui/pages/Terms/Terms.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import Page from '../Page/Page';
|
||||
|
||||
const Terms = () => (
|
||||
<div className="Terms">
|
||||
<Page
|
||||
title="Terms of Service"
|
||||
subtitle="Last updated May 29th, 2017"
|
||||
page="terms"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Terms;
|
||||
43
imports/ui/pages/VerifyEmail/VerifyEmail.js
Normal file
43
imports/ui/pages/VerifyEmail/VerifyEmail.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Bert } from 'meteor/themeteorchef:bert';
|
||||
|
||||
class VerifyEmail extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { error: null };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { match, history } = this.props;
|
||||
Accounts.verifyEmail(match.params.token, (error) => {
|
||||
if (error) {
|
||||
Bert.alert(error.reason, 'danger');
|
||||
this.setState({ error: `${error.reason}. Please try again.` });
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
Bert.alert('All set, thanks!', 'success');
|
||||
history.push('/documents');
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div className="VerifyEmail">
|
||||
<Alert bsStyle={!this.state.error ? 'info' : 'danger'}>
|
||||
{!this.state.error ? 'Verifying...' : this.state.error}
|
||||
</Alert>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
VerifyEmail.propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default VerifyEmail;
|
||||
60
imports/ui/pages/ViewDocument/ViewDocument.js
Normal file
60
imports/ui/pages/ViewDocument/ViewDocument.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ButtonToolbar, ButtonGroup, Button } from 'react-bootstrap';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Bert } from 'meteor/themeteorchef:bert';
|
||||
import Documents from '../../../api/Documents/Documents';
|
||||
import NotFound from '../NotFound/NotFound';
|
||||
import Loading from '../../components/Loading/Loading';
|
||||
|
||||
const handleRemove = (documentId, history) => {
|
||||
if (confirm('Are you sure? This is permanent!')) {
|
||||
Meteor.call('documents.remove', documentId, (error) => {
|
||||
if (error) {
|
||||
Bert.alert(error.reason, 'danger');
|
||||
} else {
|
||||
Bert.alert('Document deleted!', 'success');
|
||||
history.push('/documents');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderDocument = (doc, match, history) => (doc ? (
|
||||
<div className="ViewDocument">
|
||||
<div className="page-header clearfix">
|
||||
<h4 className="pull-left">{ doc && doc.title }</h4>
|
||||
<ButtonToolbar className="pull-right">
|
||||
<ButtonGroup bsSize="small">
|
||||
<Button onClick={() => history.push(`${match.url}/edit`)}>Edit</Button>
|
||||
<Button onClick={() => handleRemove(doc._id, history)} className="text-danger">
|
||||
Delete
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ButtonToolbar>
|
||||
</div>
|
||||
{ doc && doc.body }
|
||||
</div>
|
||||
) : <NotFound />);
|
||||
|
||||
const ViewDocument = ({ loading, doc, match, history }) => (
|
||||
!loading ? renderDocument(doc, match, history) : <Loading />
|
||||
);
|
||||
|
||||
ViewDocument.propTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
doc: PropTypes.object,
|
||||
match: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default createContainer(({ match }) => {
|
||||
const documentId = match.params._id;
|
||||
const subscription = Meteor.subscribe('documents.view', documentId);
|
||||
|
||||
return {
|
||||
loading: !subscription.ready(),
|
||||
doc: Documents.findOne(documentId),
|
||||
};
|
||||
}, ViewDocument);
|
||||
3
imports/ui/stylesheets/app.scss
Normal file
3
imports/ui/stylesheets/app.scss
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@import './colors';
|
||||
@import './forms';
|
||||
@import './bootstrap-overrides';
|
||||
11
imports/ui/stylesheets/bootstrap-overrides.scss
vendored
Normal file
11
imports/ui/stylesheets/bootstrap-overrides.scss
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
@import './mixins';
|
||||
|
||||
.page-header {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
.page-header {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
20
imports/ui/stylesheets/colors.scss
Normal file
20
imports/ui/stylesheets/colors.scss
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
$primary: #337ab7;
|
||||
$success: #5cb85c;
|
||||
$info: #5bc0de;
|
||||
$warning: #f0ad4e;
|
||||
$danger: #d9534f;
|
||||
|
||||
$gray-darker: #222;
|
||||
$gray-dark: #333;
|
||||
$gray: #555;
|
||||
$gray-light: #777;
|
||||
$gray-lighter: #eee;
|
||||
|
||||
$facebook: #3b5998;
|
||||
$google: #ea4335;
|
||||
$github: $gray-dark;
|
||||
|
||||
$cb-blue: #4285F4;
|
||||
$cb-green: #00D490;
|
||||
$cb-yellow: #FFCF50;
|
||||
$cb-red: #DA5847;
|
||||
17
imports/ui/stylesheets/forms.scss
Normal file
17
imports/ui/stylesheets/forms.scss
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
@import './mixins';
|
||||
@import './colors';
|
||||
|
||||
form {
|
||||
label,
|
||||
.control-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label.error {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
23
imports/ui/stylesheets/mixins.scss
Normal file
23
imports/ui/stylesheets/mixins.scss
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
@mixin breakpoint($point) {
|
||||
@if $point == desktop-large {
|
||||
@media (min-width: 1200px) { @content; }
|
||||
} @else if $point == desktop {
|
||||
@media (min-width: 1024px) { @content; }
|
||||
} @else if $point == tablet {
|
||||
@media (min-width: 768px) { @content; }
|
||||
} @else if $point == handheld-large {
|
||||
@media (min-width: 480px) { @content; }
|
||||
} @else {
|
||||
@media (min-width: $point) { @content; }
|
||||
}
|
||||
}
|
||||
|
||||
@mixin ie {
|
||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
@content;
|
||||
}
|
||||
|
||||
@supports (-ms-accelerator:true) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
5591
package-lock.json
generated
Normal file
5591
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
86
package.json
Normal file
86
package.json
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"name": "pup",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "meteor --settings settings-development.json",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cleverbeagle/dates": "^0.5.1",
|
||||
"@cleverbeagle/seeder": "^1.2.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bcrypt": "^1.0.3",
|
||||
"commonmark": "^0.28.1",
|
||||
"core-js": "^2.5.1",
|
||||
"fs": "0.0.1-security",
|
||||
"handlebars": "^4.0.10",
|
||||
"jquery": "^2.2.4",
|
||||
"jquery-validation": "^1.17.0",
|
||||
"juice": "^4.1.1",
|
||||
"lodash": "^4.17.4",
|
||||
"meteor-node-stubs": "^0.2.11",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^15.6.1",
|
||||
"react-addons-pure-render-mixin": "^15.6.0",
|
||||
"react-bootstrap": "^0.31.2",
|
||||
"react-dom": "^15.6.1",
|
||||
"react-router-bootstrap": "^0.24.3",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"simpl-schema": "^0.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-jest": "^20.0.3",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"enzyme": "^2.9.1",
|
||||
"eslint": "^4.6.0",
|
||||
"eslint-config-airbnb": "^15.1.0",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"eslint-plugin-jsx-a11y": "^5.1.1",
|
||||
"eslint-plugin-meteor": "^4.1.4",
|
||||
"eslint-plugin-react": "^7.3.0",
|
||||
"jest": "^20.0.4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"meteor",
|
||||
"react"
|
||||
],
|
||||
"extends": [
|
||||
"airbnb",
|
||||
"plugin:meteor/recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"globals": {
|
||||
"expect": false
|
||||
},
|
||||
"rules": {
|
||||
"import/no-unresolved": 0,
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
"import/extensions": 0,
|
||||
"no-underscore-dangle": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"_id",
|
||||
"_ensureIndex",
|
||||
"_verifyEmailToken",
|
||||
"_resetPasswordToken",
|
||||
"_name"
|
||||
]
|
||||
}
|
||||
],
|
||||
"class-methods-use-this": 0,
|
||||
"react/jsx-filename-extension": 0,
|
||||
"react/forbid-prop-types": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
322
private/email-templates/reset-password.html
Normal file
322
private/email-templates/reset-password.html
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>[{{applicationName}}] Reset Your Password</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL
|
||||
A very basic CSS reset
|
||||
------------------------------------- */
|
||||
* {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
/* 1.6em * 14px = 22.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 22px;*/
|
||||
}
|
||||
|
||||
/* Let's make sure all tables have defaults */
|
||||
table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.body-wrap {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block !important;
|
||||
max-width: 600px !important;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
|
||||
.main {
|
||||
background-color: #fff;
|
||||
border: 1px solid #e9e9e9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding: 0 0 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
color: #999;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.footer p, .footer a, .footer td {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
color: #000;
|
||||
margin: 40px 0 0;
|
||||
line-height: 1.2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
/* 1.2em * 32px = 38.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 38px;*/
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
/* 1.2em * 24px = 28.8px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 29px;*/
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
/* 1.2em * 18px = 21.6px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 22px;*/
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p, ul, ol {
|
||||
margin-bottom: 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
p li, ul li, ol li {
|
||||
margin-left: 5px;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
LINKS & BUTTONS
|
||||
------------------------------------- */
|
||||
|
||||
a {
|
||||
color: #348eda;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
text-decoration: none;
|
||||
color: #FFF;
|
||||
background-color: #4285F4;
|
||||
border: solid #4285F4;
|
||||
border-width: 10px 20px;
|
||||
line-height: 2em;
|
||||
/* 2em * 14px = 28px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 28px;*/
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.aligncenter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alignright {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.alignleft {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
ALERTS
|
||||
Change the class depending on warning email, good email or bad email
|
||||
------------------------------------- */
|
||||
|
||||
.alert {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.alert a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 640px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body itemscope itemtype="http://schema.org/EmailMessage">
|
||||
|
||||
<table class="body-wrap">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="container" width="600">
|
||||
<div class="content">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/UpdateAction">
|
||||
<tr>
|
||||
<td class="content-wrap">
|
||||
<meta itemprop="name" content="Reset Password"/>
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
Hey, {{firstName}}!
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
A password reset has been requested for the account related to this email address ({{emailAddress}}).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
To reset the password, visit the following link:
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block" itemprop="handler" itemscope itemtype="http://schema.org/HttpActionHandler">
|
||||
<a href="{{resetUrl}}" class="btn-primary" itemprop="url">Reset Password</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
Cheers, <br />
|
||||
{{applicationName}} Team
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="footer">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="aligncenter content-block">Follow <a href="http://twitter.com/application">@application</a> on Twitter.</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div></div>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
10
private/email-templates/reset-password.txt
Normal file
10
private/email-templates/reset-password.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
Hey, {{firstName}}!
|
||||
|
||||
A password reset has been requested for the account related to this email address ({{emailAddress}}).
|
||||
|
||||
To reset the password, visit the following link:
|
||||
|
||||
[Reset Password]({{resetUrl}})
|
||||
|
||||
Cheers,
|
||||
{{applicationName}} Team
|
||||
317
private/email-templates/verify-email.html
Normal file
317
private/email-templates/verify-email.html
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>[{{applicationName}}] Verify Your Email Address</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL
|
||||
A very basic CSS reset
|
||||
------------------------------------- */
|
||||
* {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
/* 1.6em * 14px = 22.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 22px;*/
|
||||
}
|
||||
|
||||
/* Let's make sure all tables have defaults */
|
||||
table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.body-wrap {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block !important;
|
||||
max-width: 600px !important;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
|
||||
.main {
|
||||
background-color: #fff;
|
||||
border: 1px solid #e9e9e9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding: 0 0 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
color: #999;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.footer p, .footer a, .footer td {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
color: #000;
|
||||
margin: 40px 0 0;
|
||||
line-height: 1.2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
/* 1.2em * 32px = 38.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 38px;*/
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
/* 1.2em * 24px = 28.8px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 29px;*/
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
/* 1.2em * 18px = 21.6px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 22px;*/
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p, ul, ol {
|
||||
margin-bottom: 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
p li, ul li, ol li {
|
||||
margin-left: 5px;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
LINKS & BUTTONS
|
||||
------------------------------------- */
|
||||
|
||||
a {
|
||||
color: #348eda;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
text-decoration: none;
|
||||
color: #FFF;
|
||||
background-color: #4285F4;
|
||||
border: solid #4285F4;
|
||||
border-width: 10px 20px;
|
||||
line-height: 2em;
|
||||
/* 2em * 14px = 28px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 28px;*/
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.aligncenter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alignright {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.alignleft {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
ALERTS
|
||||
Change the class depending on warning email, good email or bad email
|
||||
------------------------------------- */
|
||||
|
||||
.alert {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.alert a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 640px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body itemscope itemtype="http://schema.org/EmailMessage">
|
||||
|
||||
<table class="body-wrap">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="container" width="600">
|
||||
<div class="content">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction">
|
||||
<tr>
|
||||
<td class="content-wrap">
|
||||
<meta itemprop="name" content="Verify Email Address"/>
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
Hey, {{firstName}}!
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
Can you do us a favor and verify your email address?
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block" itemprop="handler" itemscope itemtype="http://schema.org/HttpActionHandler">
|
||||
<a href="{{verifyUrl}}" class="btn-primary" itemprop="url">Verify Email Address</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
Thanks! <br />
|
||||
{{applicationName}} Team
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="footer">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="aligncenter content-block">Follow <a href="http://twitter.com/application">@application</a> on Twitter.</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div></div>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
8
private/email-templates/verify-email.txt
Normal file
8
private/email-templates/verify-email.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
Hey, {{firstName}}!
|
||||
|
||||
Can you do us a favor and verify your email address?
|
||||
|
||||
[Verify Email Address]({{verifyUrl}})
|
||||
|
||||
Thanks!
|
||||
{{applicationName}} Team
|
||||
322
private/email-templates/welcome.html
Normal file
322
private/email-templates/welcome.html
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Welcome! | {{applicationName}}</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL
|
||||
A very basic CSS reset
|
||||
------------------------------------- */
|
||||
* {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
/* 1.6em * 14px = 22.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 22px;*/
|
||||
}
|
||||
|
||||
/* Let's make sure all tables have defaults */
|
||||
table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.body-wrap {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block !important;
|
||||
max-width: 600px !important;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
|
||||
.main {
|
||||
background-color: #fff;
|
||||
border: 1px solid #e9e9e9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding: 0 0 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
color: #999;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.footer p, .footer a, .footer td {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
color: #000;
|
||||
margin: 40px 0 0;
|
||||
line-height: 1.2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
/* 1.2em * 32px = 38.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 38px;*/
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
/* 1.2em * 24px = 28.8px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 29px;*/
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
/* 1.2em * 18px = 21.6px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 22px;*/
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p, ul, ol {
|
||||
margin-bottom: 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
p li, ul li, ol li {
|
||||
margin-left: 5px;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
LINKS & BUTTONS
|
||||
------------------------------------- */
|
||||
|
||||
a {
|
||||
color: #348eda;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
text-decoration: none;
|
||||
color: #FFF;
|
||||
background-color: #4285F4;
|
||||
border: solid #4285F4;
|
||||
border-width: 10px 20px;
|
||||
line-height: 2em;
|
||||
/* 2em * 14px = 28px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 28px;*/
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.aligncenter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alignright {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.alignleft {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
ALERTS
|
||||
Change the class depending on warning email, good email or bad email
|
||||
------------------------------------- */
|
||||
|
||||
.alert {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.alert a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 640px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body itemscope itemtype="http://schema.org/EmailMessage">
|
||||
|
||||
<table class="body-wrap">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="container" width="600">
|
||||
<div class="content">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/JoinAction">
|
||||
<tr>
|
||||
<td class="content-wrap">
|
||||
<meta itemprop="name" content="Welcome"/>
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
Hey, {{firstName}}!
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
Welcome to {{applicationName}}. We're excited to have you on board.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
Ready to get started?
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block" itemprop="handler" itemscope itemtype="http://schema.org/HttpActionHandler">
|
||||
<a href="{{welcomeUrl}}" class="btn-primary" itemprop="url">View Your Documents</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
Cheers, <br />
|
||||
{{applicationName}} Team
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="footer">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="aligncenter content-block">Follow <a href="http://twitter.com/application">@application</a> on Twitter.</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div></div>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
10
private/email-templates/welcome.txt
Normal file
10
private/email-templates/welcome.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
Hey, {{firstName}}!
|
||||
|
||||
Welcome to Application Name. We're excited to have you on board.
|
||||
|
||||
Ready to get started?
|
||||
|
||||
[View Your Documents]({{welcomeUrl}})
|
||||
|
||||
Cheers,
|
||||
Application Name Team
|
||||
5
private/pages/example-page.md
Normal file
5
private/pages/example-page.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
### This is my Markdown page
|
||||
|
||||
I can type **any** Markdown I want into this file and it will ultimately be `parsed` as HTML by Pup's `utility.getPage` Method.
|
||||
|
||||
To learn more, you can [read about this Method here](http://cleverbeagle.com/pup/v1/the-basics/methods#utility-methods).
|
||||
18
private/pages/privacy.md
Normal file
18
private/pages/privacy.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
Your privacy is important to us.
|
||||
|
||||
It is Clever Beagle's policy to respect your privacy regarding any information we may collect while operating our website. Accordingly, we have developed this privacy policy in order for you to understand how we collect, use, communicate, disclose and otherwise make use of personal information. We have outlined our privacy policy below.
|
||||
|
||||
We will collect personal information by lawful and fair means and, where appropriate, with the knowledge or consent of the individual concerned.
|
||||
Before or at the time of collecting personal information, we will identify the purposes for which information is being collected.
|
||||
|
||||
We will collect and use personal information solely for fulfilling those purposes specified by us and for other ancillary purposes, unless we obtain the consent of the individual concerned or as required by law.
|
||||
|
||||
Personal data should be relevant to the purposes for which it is to be used, and, to the extent necessary for those purposes, should be accurate, complete, and up-to-date.
|
||||
|
||||
We will protect personal information by using reasonable security safeguards against loss or theft, as well as unauthorized access, disclosure, copying, use or modification.
|
||||
|
||||
We will make readily available to customers information about our policies and practices relating to the management of personal information.
|
||||
|
||||
We will only retain personal information for as long as necessary for the fulfillment of those purposes.
|
||||
|
||||
We are committed to conducting our business in accordance with these principles in order to ensure that the confidentiality of personal information is protected and maintained. Clever Beagle may change this privacy policy from time to time at Clever Beagle's sole discretion.
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue