From 58cda9cc7fb79ca9df6746de7f9662bc08dc156a Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Fri, 13 Oct 2017 12:29:40 +0200 Subject: initial commit --- src/components/auth/AuthLayout.js | 88 ++++++++++++++++ src/components/auth/Import.js | 168 +++++++++++++++++++++++++++++++ src/components/auth/Invite.js | 111 ++++++++++++++++++++ src/components/auth/Login.js | 161 +++++++++++++++++++++++++++++ src/components/auth/Password.js | 135 +++++++++++++++++++++++++ src/components/auth/Pricing.js | 130 ++++++++++++++++++++++++ src/components/auth/Signup.js | 206 ++++++++++++++++++++++++++++++++++++++ src/components/auth/Welcome.js | 69 +++++++++++++ 8 files changed, 1068 insertions(+) create mode 100644 src/components/auth/AuthLayout.js create mode 100644 src/components/auth/Import.js create mode 100644 src/components/auth/Invite.js create mode 100644 src/components/auth/Login.js create mode 100644 src/components/auth/Password.js create mode 100644 src/components/auth/Pricing.js create mode 100644 src/components/auth/Signup.js create mode 100644 src/components/auth/Welcome.js (limited to 'src/components/auth') diff --git a/src/components/auth/AuthLayout.js b/src/components/auth/AuthLayout.js new file mode 100644 index 000000000..2741b8a15 --- /dev/null +++ b/src/components/auth/AuthLayout.js @@ -0,0 +1,88 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react'; +import { RouteTransition } from 'react-router-transition'; +import { intlShape } from 'react-intl'; + +import Link from '../ui/Link'; +import InfoBar from '../ui/InfoBar'; + +import { oneOrManyChildElements, globalError as globalErrorPropType } from '../../prop-types'; +import globalMessages from '../../i18n/globalMessages'; + +@observer +export default class AuthLayout extends Component { + static propTypes = { + children: oneOrManyChildElements.isRequired, + pathname: PropTypes.string.isRequired, + error: globalErrorPropType.isRequired, + isOnline: PropTypes.bool.isRequired, + isAPIHealthy: PropTypes.bool.isRequired, + retryHealthCheck: PropTypes.func.isRequired, + isHealthCheckLoading: PropTypes.bool.isRequired, + }; + + static contextTypes = { + intl: intlShape, + }; + + render() { + const { + children, + pathname, + error, + isOnline, + isAPIHealthy, + retryHealthCheck, + isHealthCheckLoading, + } = this.props; + const { intl } = this.context; + + return ( +
+ {!isOnline && ( + + + {intl.formatMessage(globalMessages.notConnectedToTheInternet)} + + )} + {isOnline && !isAPIHealthy && ( + + + {intl.formatMessage(globalMessages.APIUnhealthy)} + + )} +
+ ({ + transform: `translateX(${styles.translateX}%)`, + opacity: styles.opacity, + })} + component="span" + > + {/* Inject globalError into children */} + {React.cloneElement(children, { + error, + })} + +
+ {/*
*/} + + + + + ); + } +} diff --git a/src/components/auth/Import.js b/src/components/auth/Import.js new file mode 100644 index 000000000..cf83aa9c8 --- /dev/null +++ b/src/components/auth/Import.js @@ -0,0 +1,168 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; +import { Link } from 'react-router'; +import classnames from 'classnames'; + +import Form from '../../lib/Form'; +import Toggle from '../ui/Toggle'; +import Button from '../ui/Button'; + +const messages = defineMessages({ + headline: { + id: 'import.headline', + defaultMessage: '!!!Import your Franz 4 services', + }, + notSupportedHeadline: { + id: 'import.notSupportedHeadline', + defaultMessage: '!!!Services not yet supported in Franz 5', + }, + submitButtonLabel: { + id: 'import.submit.label', + defaultMessage: '!!!Import {count} services', + }, + skipButtonLabel: { + id: 'import.skip.label', + defaultMessage: '!!!I want add services manually', + }, +}); + +@observer +export default class Import extends Component { + static propTypes = { + services: MobxPropTypes.arrayOrObservableArray.isRequired, + onSubmit: PropTypes.func.isRequired, + isSubmitting: PropTypes.bool.isRequired, + inviteRoute: PropTypes.string.isRequired, + }; + + static contextTypes = { + intl: intlShape, + }; + + prepareForm() { + const { services } = this.props; + + const config = { + fields: { + import: [...services.filter(s => s.recipe).map(s => ({ + add: { + default: true, + options: s, + }, + }))], + }, + }; + + return new Form(config, this.context.intl); + } + + submit(e) { + const { services } = this.props; + e.preventDefault(); + this.form.submit({ + onSuccess: (form) => { + const servicesImport = form.values().import + .map((value, i) => !value.add || services.filter(s => s.recipe)[i]) + .filter(s => typeof s !== 'boolean'); + + this.props.onSubmit({ services: servicesImport }); + }, + onError: () => {}, + }); + } + + render() { + this.form = this.prepareForm(); + const { intl } = this.context; + const { services, isSubmitting, inviteRoute } = this.props; + + const availableServices = services.filter(s => s.recipe); + const unavailableServices = services.filter(s => !s.recipe); + + return ( +
+
+
this.submit(e)}> + +

+ {intl.formatMessage(messages.headline)} +

+ + + {this.form.$('import').map((service, i) => ( + service.$('add').set(!service.$('add').value)} + > + + + + + ))} + +
+ + + + + {availableServices[i].name !== '' + ? availableServices[i].name + : availableServices[i].recipe.name} +
+ {unavailableServices.length > 0 && ( +
+ {intl.formatMessage(messages.notSupportedHeadline)} +

+ {services.filter(s => !s.recipe).map((service, i) => ( + + {service.name !== '' ? service.name : service.service} + {unavailableServices.length > i + 1 ? ', ' : ''} + + ))} +

+
+ )} + + {isSubmitting ? ( +
+
+ ); + } +} diff --git a/src/components/auth/Invite.js b/src/components/auth/Invite.js new file mode 100644 index 000000000..c1d815dcd --- /dev/null +++ b/src/components/auth/Invite.js @@ -0,0 +1,111 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; +import { Link } from 'react-router'; + +import Form from '../../lib/Form'; +import { email } from '../../helpers/validation-helpers'; +import Input from '../ui/Input'; +import Button from '../ui/Button'; + +const messages = defineMessages({ + headline: { + id: 'invite.headline.friends', + defaultMessage: '!!!Invite 3 of your friends or colleagues', + }, + nameLabel: { + id: 'invite.name.label', + defaultMessage: '!!!Name', + }, + emailLabel: { + id: 'invite.email.label', + defaultMessage: '!!!Email address', + }, + submitButtonLabel: { + id: 'invite.submit.label', + defaultMessage: '!!!Send invites', + }, + skipButtonLabel: { + id: 'invite.skip.label', + defaultMessage: '!!!I want to do this later', + }, +}); + +@observer +export default class Invite extends Component { + static propTypes = { + onSubmit: PropTypes.func.isRequired, + }; + + static contextTypes = { + intl: intlShape, + }; + + form = new Form({ + fields: { + invite: [...Array(3).fill({ + name: { + label: this.context.intl.formatMessage(messages.nameLabel), + // value: '', + placeholder: this.context.intl.formatMessage(messages.nameLabel), + }, + email: { + label: this.context.intl.formatMessage(messages.emailLabel), + // value: '', + validate: [email], + placeholder: this.context.intl.formatMessage(messages.emailLabel), + }, + })], + }, + }, this.context.intl); + + submit(e) { + e.preventDefault(); + this.form.submit({ + onSuccess: (form) => { + this.props.onSubmit({ invites: form.values().invite }); + }, + onError: () => {}, + }); + } + + render() { + const { form } = this; + const { intl } = this.context; + + return ( +
+
this.submit(e)}> + +

+ {intl.formatMessage(messages.headline)} +

+ {form.$('invite').map(invite => ( +
+
+ + +
+
+ ))} +
+ ); + } +} diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js new file mode 100644 index 000000000..015079f02 --- /dev/null +++ b/src/components/auth/Login.js @@ -0,0 +1,161 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; + +import Form from '../../lib/Form'; +import { required, email } from '../../helpers/validation-helpers'; +import Input from '../ui/Input'; +import Button from '../ui/Button'; +import Link from '../ui/Link'; + +import { globalError as globalErrorPropType } from '../../prop-types'; + +// import Appear from '../ui/effects/Appear'; + +const messages = defineMessages({ + headline: { + id: 'login.headline', + defaultMessage: '!!!Sign in', + }, + emailLabel: { + id: 'login.email.label', + defaultMessage: '!!!Email address', + }, + passwordLabel: { + id: 'login.password.label', + defaultMessage: '!!!Password', + }, + submitButtonLabel: { + id: 'login.submit.label', + defaultMessage: '!!!Sign in', + }, + invalidCredentials: { + id: 'login.invalidCredentials', + defaultMessage: '!!!Email or password not valid', + }, + tokenExpired: { + id: 'login.tokenExpired', + defaultMessage: '!!!Your session expired, please login again.', + }, + serverLogout: { + id: 'login.serverLogout', + defaultMessage: '!!!Your session expired, please login again.', + }, + signupLink: { + id: 'login.link.signup', + defaultMessage: '!!!Create a free account', + }, + passwordLink: { + id: 'login.link.password', + defaultMessage: '!!!Forgot password', + }, +}); + +@observer +export default class Login extends Component { + static propTypes = { + onSubmit: PropTypes.func.isRequired, + isSubmitting: PropTypes.bool.isRequired, + isTokenExpired: PropTypes.bool.isRequired, + isServerLogout: PropTypes.bool.isRequired, + signupRoute: PropTypes.string.isRequired, + passwordRoute: PropTypes.string.isRequired, + error: globalErrorPropType.isRequired, + }; + + static contextTypes = { + intl: intlShape, + }; + + form = new Form({ + fields: { + email: { + label: this.context.intl.formatMessage(messages.emailLabel), + value: '', + validate: [required, email], + }, + password: { + label: this.context.intl.formatMessage(messages.passwordLabel), + value: '', + validate: [required], + type: 'password', + }, + }, + }, this.context.intl); + + submit(e) { + e.preventDefault(); + this.form.submit({ + onSuccess: (form) => { + this.props.onSubmit(form.values()); + }, + onError: () => {}, + }); + } + + emailField = null; + + render() { + const { form } = this; + const { intl } = this.context; + const { + isSubmitting, + isTokenExpired, + isServerLogout, + signupRoute, + passwordRoute, + error, + } = this.props; + + return ( +
+
this.submit(e)}> + +

{intl.formatMessage(messages.headline)}

+ {isTokenExpired && ( +

{intl.formatMessage(messages.tokenExpired)}

+ )} + {isServerLogout && ( +

{intl.formatMessage(messages.serverLogout)}

+ )} + { this.emailField = element; }} + focus + /> + + {error.code === 'invalid-credentials' && ( +

{intl.formatMessage(messages.invalidCredentials)}

+ )} + {isSubmitting ? ( +
+ ); + } +} diff --git a/src/components/auth/Password.js b/src/components/auth/Password.js new file mode 100644 index 000000000..d2b196853 --- /dev/null +++ b/src/components/auth/Password.js @@ -0,0 +1,135 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; + +import Form from '../../lib/Form'; +import { required, email } from '../../helpers/validation-helpers'; +import Input from '../ui/Input'; +import Button from '../ui/Button'; +import Link from '../ui/Link'; +import Infobox from '../ui/Infobox'; + +const messages = defineMessages({ + headline: { + id: 'password.headline', + defaultMessage: '!!!Forgot password', + }, + emailLabel: { + id: 'password.email.label', + defaultMessage: '!!!Email address', + }, + submitButtonLabel: { + id: 'password.submit.label', + defaultMessage: '!!!Submit', + }, + successInfo: { + id: 'password.successInfo', + defaultMessage: '!!!Your new password was sent to your email address', + }, + noUser: { + id: 'password.noUser', + defaultMessage: '!!!No user affiliated with that email address', + }, + signupLink: { + id: 'password.link.signup', + defaultMessage: '!!!Create a free account', + }, + loginLink: { + id: 'password.link.login', + defaultMessage: '!!!Sign in to your account', + }, +}); + +@observer +export default class Password extends Component { + static propTypes = { + onSubmit: PropTypes.func.isRequired, + isSubmitting: PropTypes.bool.isRequired, + signupRoute: PropTypes.string.isRequired, + loginRoute: PropTypes.string.isRequired, + status: MobxPropTypes.arrayOrObservableArray.isRequired, + }; + + static contextTypes = { + intl: intlShape, + }; + + form = new Form({ + fields: { + email: { + label: this.context.intl.formatMessage(messages.emailLabel), + value: '', + validate: [required, email], + }, + }, + }, this.context.intl); + + submit(e) { + e.preventDefault(); + this.form.submit({ + onSuccess: (form) => { + this.props.onSubmit(form.values()); + }, + onError: () => {}, + }); + } + + render() { + const { form } = this; + const { intl } = this.context; + const { + isSubmitting, + signupRoute, + loginRoute, + status, + } = this.props; + + return ( +
+
this.submit(e)}> + +

{intl.formatMessage(messages.headline)}

+ {status.length > 0 && status.includes('sent') && ( + + {intl.formatMessage(messages.successInfo)} + + )} + + {status.length > 0 && status.includes('no-user') && ( +

{intl.formatMessage(messages.noUser)}

+ )} + {isSubmitting ? ( +
+ ); + } +} diff --git a/src/components/auth/Pricing.js b/src/components/auth/Pricing.js new file mode 100644 index 000000000..761561a89 --- /dev/null +++ b/src/components/auth/Pricing.js @@ -0,0 +1,130 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; +// import { Link } from 'react-router'; + +// import Button from '../ui/Button'; +import Loader from '../ui/Loader'; +import Appear from '../ui/effects/Appear'; +import SubscriptionForm from '../../containers/ui/SubscriptionFormScreen'; + +const messages = defineMessages({ + headline: { + id: 'pricing.headline', + defaultMessage: '!!!Support Franz', + }, + monthlySupportLabel: { + id: 'pricing.support.label', + defaultMessage: '!!!Select your support plan', + }, + submitButtonLabel: { + id: 'pricing.submit.label', + defaultMessage: '!!!Support the development of Franz', + }, + skipPayment: { + id: 'pricing.link.skipPayment', + defaultMessage: '!!!I don\'t want to support the development of Franz.', + }, +}); + +@observer +export default class Signup extends Component { + static propTypes = { + donor: MobxPropTypes.objectOrObservableObject.isRequired, + isLoading: PropTypes.bool.isRequired, + isLoadingUser: PropTypes.bool.isRequired, + onCloseSubscriptionWindow: PropTypes.func.isRequired, + skipAction: PropTypes.func.isRequired, + }; + + static contextTypes = { + intl: intlShape, + }; + + render() { + const { + donor, + isLoading, + isLoadingUser, + onCloseSubscriptionWindow, + skipAction, + } = this.props; + const { intl } = this.context; + + return ( +
+
+
+ +

{intl.formatMessage(messages.headline)}

+
+ {isLoadingUser && ( +

Loading

+ )} + {!isLoadingUser && ( + donor.amount ? ( + +

+ Thank you so much for your previous donation of $ {donor.amount}. +
+ Your support allowed us to get where we are today. +
+

+

+ As an early supporter, you get a lifetime premium supporter license without any + additional charges. +

+

+ However, If you want to keep supporting us, you are more than welcome to subscribe to a plan. +

+

+
+ ) : ( + +

+ We built Franz with a lot of effort, manpower and love, + to bring you the best messaging experience. +
+

+

+ Getting a Franz Premium Supporter License will allow us to keep improving Franz for you. +

+
+ ) + )} +

+ Thanks for being a hero. +

+

+ Stefan Malzner +

+
+ + + {intl.formatMessage(messages.monthlySupportLabel)} + + {/* + {intl.formatMessage(messages.skipPayment)} + */} + + +
+
+
+ ); + } +} diff --git a/src/components/auth/Signup.js b/src/components/auth/Signup.js new file mode 100644 index 000000000..71ca16111 --- /dev/null +++ b/src/components/auth/Signup.js @@ -0,0 +1,206 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; + +import Form from '../../lib/Form'; +import { required, email, minLength } from '../../helpers/validation-helpers'; +import Input from '../ui/Input'; +import Radio from '../ui/Radio'; +import Button from '../ui/Button'; +import Link from '../ui/Link'; + +import { globalError as globalErrorPropType } from '../../prop-types'; + +const messages = defineMessages({ + headline: { + id: 'signup.headline', + defaultMessage: '!!!Sign up', + }, + firstnameLabel: { + id: 'signup.firstname.label', + defaultMessage: '!!!Firstname', + }, + lastnameLabel: { + id: 'signup.lastname.label', + defaultMessage: '!!!Lastname', + }, + emailLabel: { + id: 'signup.email.label', + defaultMessage: '!!!Email address', + }, + companyLabel: { + id: 'signup.company.label', + defaultMessage: '!!!Company', + }, + passwordLabel: { + id: 'signup.password.label', + defaultMessage: '!!!Password', + }, + legalInfo: { + id: 'signup.legal.info', + defaultMessage: '!!!By creating a Franz account you accept the', + }, + terms: { + id: 'signup.legal.terms', + defaultMessage: '!!!Terms of service', + }, + privacy: { + id: 'signup.legal.privacy', + defaultMessage: '!!!Privacy Statement', + }, + submitButtonLabel: { + id: 'signup.submit.label', + defaultMessage: '!!!Create account', + }, + loginLink: { + id: 'signup.link.login', + defaultMessage: '!!!Already have an account, sign in?', + }, + emailDuplicate: { + id: 'signup.emailDuplicate', + defaultMessage: '!!!A user with that email address already exists', + }, +}); + +@observer +export default class Signup extends Component { + static propTypes = { + onSubmit: PropTypes.func.isRequired, + isSubmitting: PropTypes.bool.isRequired, + loginRoute: PropTypes.string.isRequired, + error: globalErrorPropType.isRequired, + }; + + static contextTypes = { + intl: intlShape, + }; + + form = new Form({ + fields: { + accountType: { + value: 'individual', + validate: [required], + options: [{ + value: 'individual', + label: 'Individual', + }, { + value: 'non-profit', + label: 'Non-Profit', + }, { + value: 'company', + label: 'Company', + }], + }, + firstname: { + label: this.context.intl.formatMessage(messages.firstnameLabel), + value: '', + validate: [required], + }, + lastname: { + label: this.context.intl.formatMessage(messages.lastnameLabel), + value: '', + validate: [required], + }, + email: { + label: this.context.intl.formatMessage(messages.emailLabel), + value: '', + validate: [required, email], + }, + organization: { + label: this.context.intl.formatMessage(messages.companyLabel), + value: '', // TODO: make required when accountType: company + }, + password: { + label: this.context.intl.formatMessage(messages.passwordLabel), + value: '', + validate: [required, minLength(6)], + type: 'password', + }, + }, + }, this.context.intl); + + submit(e) { + e.preventDefault(); + this.form.submit({ + onSuccess: (form) => { + this.props.onSubmit(form.values()); + }, + onError: () => {}, + }); + } + + render() { + const { form } = this; + const { intl } = this.context; + const { isSubmitting, loginRoute, error } = this.props; + + return ( +
+
+
this.submit(e)}> + +

{intl.formatMessage(messages.headline)}

+ +
+ + +
+ + + {form.$('accountType').value === 'company' && ( + + )} + {error.code === 'email-duplicate' && ( +

{intl.formatMessage(messages.emailDuplicate)}

+ )} + {isSubmitting ? ( +
+
+ ); + } +} diff --git a/src/components/auth/Welcome.js b/src/components/auth/Welcome.js new file mode 100644 index 000000000..06b10ecfe --- /dev/null +++ b/src/components/auth/Welcome.js @@ -0,0 +1,69 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; + +import Link from '../ui/Link'; + +const messages = defineMessages({ + signupButton: { + id: 'welcome.signupButton', + defaultMessage: '!!!Create a free account', + }, + loginButton: { + id: 'welcome.loginButton', + defaultMessage: '!!!Login to your account', + }, +}); + +@observer +export default class Login extends Component { + static propTypes = { + loginRoute: PropTypes.string.isRequired, + signupRoute: PropTypes.string.isRequired, + recipes: MobxPropTypes.arrayOrObservableArray.isRequired, + }; + + static contextTypes = { + intl: intlShape, + }; + + render() { + const { intl } = this.context; + const { + loginRoute, + signupRoute, + recipes, + } = this.props; + + return ( +
+
+ + {/* */} +
+

Franz

+
+
+
+ + {intl.formatMessage(messages.signupButton)} + + + {intl.formatMessage(messages.loginButton)} + +
+
+ {recipes.map(recipe => ( + + ))} +
+
+ ); + } +} -- cgit v1.2.3-54-g00ecf