wip Pup 1.0.0

This commit is contained in:
cleverbeagle 2017-05-23 20:05:46 -05:00
commit cdc15f019d
48 changed files with 1100 additions and 0 deletions

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

View file

@ -0,0 +1,22 @@
import React, { PropTypes } from 'react';
import { Route, Redirect } from 'react-router-dom';
const Authenticated = ({ loggingIn, authenticated, component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />;
return authenticated ?
(React.createElement(component, { ...props, 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,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import { LinkContainer } from 'react-router-bootstrap';
import { Nav, NavItem, NavDropdown, MenuItem } from 'react-bootstrap';
import { Meteor } from 'meteor/meteor';
const AuthenticatedNavigation = ({ name }) => (
<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={() => Meteor.logout()}>Logout</MenuItem>
</NavDropdown>
</Nav>
</div>
);
AuthenticatedNavigation.propTypes = {
name: PropTypes.string.isRequired,
};
export default AuthenticatedNavigation;

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,33 @@
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 = ({ authenticated, name }) => (
<Navbar>
<Navbar.Header>
<Navbar.Brand>
<Link to="/">Pup</Link>
</Navbar.Brand>
<Navbar.Toggle />
</Navbar.Header>
<Navbar.Collapse>
{!authenticated ? <PublicNavigation /> : <AuthenticatedNavigation name={name} />}
</Navbar.Collapse>
</Navbar>
);
Navigation.defaultProps = {
name: PropTypes.string,
};
Navigation.propTypes = {
authenticated: PropTypes.bool.isRequired,
name: PropTypes.string,
};
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,63 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Meteor } from 'meteor/meteor';
import { Bert } from 'meteor/themeteorchef:bert';
import './OAuthLoginButton.scss';
const handleLogin = (service, options, callback) => {
const defaultOptions = {
facebook: {
requestPermissions: ['email'],
loginStyle: 'redirect',
},
google: {
requestPermissions: ['email'],
requestOfflineToken: true,
loginStyle: 'redirect',
},
github: {
requestPermissions: ['user:email'],
loginStyle: 'redirect',
},
}[service];
const defaultCallback = (error) => {
if (error) Bert.alert(error.reason, 'danger');
};
return {
facebook: Meteor.loginWithFacebook,
google: Meteor.loginWithGoogle,
github: Meteor.loginWithGithub,
}[service](options || defaultOptions, callback || defaultCallback);
};
const serviceLabel = {
facebook: <span><i className="fa fa-facebook" /> Log In with Facebook</span>,
google: <span><i className="fa fa-google" /> Log In with Google</span>,
github: <span><i className="fa fa-github" /> Log In with GitHub</span>,
};
const OAuthLoginButton = ({ service, options, callback }) => (
<button
className={`OAuthLoginButton OAuthLoginButton-${service}`}
type="button"
onClick={() => handleLogin(service, options, callback)}
>
{serviceLabel[service]}
</button>
);
OAuthLoginButton.defaultProps = {
options: PropTypes.object,
callback: PropTypes.func,
};
OAuthLoginButton.propTypes = {
service: PropTypes.string.isRequired,
options: PropTypes.object, // eslint-disable-line react/forbid-prop-types
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,22 @@
import React, { PropTypes } from 'react';
import { Route, Redirect } from 'react-router-dom';
const Public = ({ loggingIn, authenticated, component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />;
return !authenticated ?
(React.createElement(component, { ...props, 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,61 @@
import React from 'react';
import PropTypes from 'prop-types';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import { Grid } from 'react-bootstrap';
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import { Roles } from 'meteor/alanning:roles';
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 RecoverPassword from '../../pages/RecoverPassword/RecoverPassword';
import ResetPassword from '../../pages/ResetPassword/ResetPassword';
import NotFound from '../../pages/NotFound/NotFound';
const App = props => (
<Router>
{!props.loading ? <div className="App">
<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} />
<Public path="/signup" component={Signup} {...props} />
<Public path="/login" component={Login} {...props} />
<Route name="recover-password" path="/recover-password" component={RecoverPassword} />
<Route name="reset-password" path="/reset-password/:token" component={ResetPassword} />
<Route component={NotFound} />
</Switch>
</Grid>
</div> : ''}
</Router>
);
App.propTypes = {
loading: PropTypes.bool.isRequired,
};
export default createContainer(() => {
const loggingIn = Meteor.loggingIn();
const user = Meteor.user();
const userId = Meteor.userId();
const loading = !Roles.subscription.ready();
return {
loading,
loggingIn,
authenticated: !loggingIn && !!userId,
name: user && user.profile ? `${user.profile.name.first} ${user.profile.name.last}` : '',
roles: !loading && Roles.getRolesForUser(userId),
};
}, App);

View file

View file

View file

@ -0,0 +1,18 @@
import React from 'react';
import { Jumbotron } from 'react-bootstrap';
import './Index.scss';
const Index = () => (
<div className="Index">
<Jumbotron>
<img
src="https://s3-us-west-2.amazonaws.com/cleverbeagle-assets/graphics/email-icon.png"
alt="Clever Beagle"
/>
<p>Need help building your app? Check out our mentorship service.</p>
</Jumbotron>
</div>
);
export default Index;

View file

View file

@ -0,0 +1,106 @@
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 OAuthLoginButton from '../../components/OAuthLoginButton/OAuthLoginButton';
import validate from '../../../modules/validate';
import './Login.scss';
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');
history.push('/documents');
}
});
}
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}>
<FormGroup className="OAuthLoginButtons">
<OAuthLoginButton service="facebook" />
<OAuthLoginButton service="google" />
<OAuthLoginButton service="github" />
</FormGroup>
</Col>
</Row>
<form ref={form => (this.form = form)} onSubmit={event => event.preventDefault()}>
<p className="LoginWithEmail">Log In with an Email Address</p>
<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"
/>
</FormGroup>
<Button type="submit" bsStyle="success">Log In</Button>
<p className="DontHaveAnAccount">
{'Don\'t have an account?'} <Link to="/signup">Sign Up</Link>.
</p>
</form>
</Col>
</Row>
</div>);
}
}
Login.propTypes = {
history: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
export default Login;

View file

@ -0,0 +1,47 @@
@import '../../stylesheets/mixins';
@import '../../stylesheets/colors';
.Login {
.page-header {
margin-top: 0;
}
form {
position: relative;
border-top: 1px solid $gray-lighter;
margin-top: 15px;
padding-top: 25px;
.LoginWithEmail {
display: inline-block;
background: #fff;
padding: 0 10px;
position: absolute;
top: -12px;
left: 50%;
margin-left: -106px;
}
.DontHaveAnAccount {
margin-top: 25px;
padding-top: 20px;
border-top: 1px solid $gray-lighter;
}
}
}
@include breakpoint(tablet) {
.Login {
.page-header {
margin-top: 10px;
}
}
}
@include breakpoint(desktop) {
.Login {
.page-header {
margin-top: 20px;
}
}
}

View file

View file

View file

@ -0,0 +1,155 @@
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 { Accounts } from 'meteor/accounts-base';
import { Bert } from 'meteor/themeteorchef:bert';
import OAuthLoginButton from '../../components/OAuthLoginButton/OAuthLoginButton';
import InputHint from '../../components/InputHint/InputHint';
import validate from '../../../modules/validate';
import './Signup.scss';
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 {
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}>
<FormGroup className="OAuthLoginButtons">
<OAuthLoginButton service="facebook" />
<OAuthLoginButton service="google" />
<OAuthLoginButton service="github" />
</FormGroup>
</Col>
</Row>
<form ref={form => (this.form = form)} onSubmit={event => event.preventDefault()}>
<p className="SignupWithEmail">Sign Up with an Email Address</p>
<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>
<p className="HaveAnAccount">
Already have an account? <Link to="/login">Log In</Link>.
</p>
</form>
</Col>
</Row>
</div>);
}
}
Signup.propTypes = {
history: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
export default Signup;

View file

@ -0,0 +1,47 @@
@import '../../stylesheets/mixins';
@import '../../stylesheets/colors';
.Signup {
.page-header {
margin-top: 0;
}
form {
position: relative;
border-top: 1px solid $gray-lighter;
margin-top: 15px;
padding-top: 25px;
.SignupWithEmail {
display: inline-block;
background: #fff;
padding: 0 10px;
position: absolute;
top: -12px;
left: 50%;
margin-left: -106px;
}
.HaveAnAccount {
margin-top: 25px;
padding-top: 20px;
border-top: 1px solid $gray-lighter;
}
}
}
@include breakpoint(tablet) {
.Signup {
.page-header {
margin-top: 10px;
}
}
}
@include breakpoint(desktop) {
.Signup {
.page-header {
margin-top: 20px;
}
}
}

View file

@ -0,0 +1,15 @@
$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;

View file

@ -0,0 +1,12 @@
@import './mixins';
@import './colors';
form {
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;
}
}

View file

@ -0,0 +1,2 @@
@import './colors';
@import './forms';