This commit is contained in:
vjrj 2017-11-12 13:44:10 +01:00
commit 5b311446dc
108 changed files with 9797 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules
settings-demo.json
settings-staging.json
settings-production.json

View 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
View file

@ -0,0 +1 @@
local

7
.meteor/.id Normal file
View 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
View 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
View file

@ -0,0 +1,2 @@
server
browser

1
.meteor/release Normal file
View file

@ -0,0 +1 @@
METEOR@1.5.2

104
.meteor/versions Normal file
View 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
View 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.

View file

@ -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
View 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
View file

@ -0,0 +1 @@
import '../imports/startup/client';

View 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;

View 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,
});

View 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 });
});

View 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,
});

View 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 }));

View 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,
});

View 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,
},
});
});

View 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}`);
});
};

View 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`));
},
});

View 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;
};

View 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);
};

View 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);
}
};

View file

@ -0,0 +1,3 @@
import fs from 'fs';
export default path => fs.readFileSync(`assets/app/${path}`, 'utf8');

View 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.');
};

View 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.');
};

View 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.');
};

View file

@ -0,0 +1,4 @@
import $ from 'jquery';
import 'jquery-validation';
export default (form, options) => $(form).validate(options);

View 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')));

View 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,
});
},
};

View file

@ -0,0 +1,3 @@
import './oauth';
import './email-templates';
import './on-create-user.js';

View 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] },
);
});
}

View 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;
});

View 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';

View file

@ -0,0 +1,3 @@
import { Meteor } from 'meteor/meteor';
if (Meteor.isDevelopment) process.env.MAIL_URL = Meteor.settings.private.MAIL_URL;

View 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);
},
};
},
});

View file

@ -0,0 +1,4 @@
import './accounts';
import './api';
import './fixtures';
import './email';

View 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;

View file

@ -0,0 +1,7 @@
@import '../../stylesheets/colors';
.AccountPageFooter {
margin: 25px 0 0;
padding-top: 20px;
border-top: 1px solid $gray-lighter;
}

View 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;

View file

@ -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);

View 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;

View 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;
}
}

View 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;

View 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">&copy; {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;

View 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;
}
}

View 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;

View 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;

View file

@ -0,0 +1,9 @@
@import '../../stylesheets/colors';
.InputHint {
display: block;
margin-top: 8px;
font-style: italic;
color: $gray-light;
font-size: 13px;
}

View 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;

View 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;

View file

@ -0,0 +1,6 @@
.navbar {
border-radius: 0;
border-left: none;
border-right: none;
border-top: none;
}

View 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;

View 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;
}

View 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);

View file

@ -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%;
}
}
}

View 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;

View 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;
}
}

View 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;

View 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;

View 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);

View 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;
}
}

View 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);

View file

@ -0,0 +1,3 @@
.Documents table tbody tr td {
vertical-align: middle;
}

View 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);

View 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;

View 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;

View 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;
}
}
}

View 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;

View 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;

View 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;
}
}

View 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;

View 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;

View 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);

View file

@ -0,0 +1,11 @@
@import '../../stylesheets/mixins';
.Page {
margin-bottom: 0px;
}
@include breakpoint(tablet) {
.Page {
margin-bottom: 30px;
}
}

View 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;

View 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);

View 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;
}
}
}
}

View 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;

View 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 &amp; Login</Button>
</form>
</Col>
</Row>
</div>);
}
}
ResetPassword.propTypes = {
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
export default ResetPassword;

View 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;

View 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;

View 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;

View 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);

View file

@ -0,0 +1,3 @@
@import './colors';
@import './forms';
@import './bootstrap-overrides';

View file

@ -0,0 +1,11 @@
@import './mixins';
.page-header {
margin-top: 0;
}
@include breakpoint(tablet) {
.page-header {
margin-top: 20px;
}
}

View 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;

View 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;
}
}

View 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

File diff suppressed because it is too large Load diff

86
package.json Normal file
View 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
}
}
}

View 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>

View 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

View 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>

View 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

View 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>

View 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

View 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
View 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