diff options
author | Stefan Malzner <stefan@adlk.io> | 2017-10-13 12:29:40 +0200 |
---|---|---|
committer | Stefan Malzner <stefan@adlk.io> | 2017-10-13 12:29:40 +0200 |
commit | 58cda9cc7fb79ca9df6746de7f9662bc08dc156a (patch) | |
tree | 1211600c2a5d3b5f81c435c6896618111a611720 /src/components/auth | |
download | ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.tar.gz ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.tar.zst ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.zip |
initial commit
Diffstat (limited to 'src/components/auth')
-rw-r--r-- | src/components/auth/AuthLayout.js | 88 | ||||
-rw-r--r-- | src/components/auth/Import.js | 168 | ||||
-rw-r--r-- | src/components/auth/Invite.js | 111 | ||||
-rw-r--r-- | src/components/auth/Login.js | 161 | ||||
-rw-r--r-- | src/components/auth/Password.js | 135 | ||||
-rw-r--r-- | src/components/auth/Pricing.js | 130 | ||||
-rw-r--r-- | src/components/auth/Signup.js | 206 | ||||
-rw-r--r-- | src/components/auth/Welcome.js | 69 |
8 files changed, 1068 insertions, 0 deletions
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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { RouteTransition } from 'react-router-transition'; | ||
5 | import { intlShape } from 'react-intl'; | ||
6 | |||
7 | import Link from '../ui/Link'; | ||
8 | import InfoBar from '../ui/InfoBar'; | ||
9 | |||
10 | import { oneOrManyChildElements, globalError as globalErrorPropType } from '../../prop-types'; | ||
11 | import globalMessages from '../../i18n/globalMessages'; | ||
12 | |||
13 | @observer | ||
14 | export default class AuthLayout extends Component { | ||
15 | static propTypes = { | ||
16 | children: oneOrManyChildElements.isRequired, | ||
17 | pathname: PropTypes.string.isRequired, | ||
18 | error: globalErrorPropType.isRequired, | ||
19 | isOnline: PropTypes.bool.isRequired, | ||
20 | isAPIHealthy: PropTypes.bool.isRequired, | ||
21 | retryHealthCheck: PropTypes.func.isRequired, | ||
22 | isHealthCheckLoading: PropTypes.bool.isRequired, | ||
23 | }; | ||
24 | |||
25 | static contextTypes = { | ||
26 | intl: intlShape, | ||
27 | }; | ||
28 | |||
29 | render() { | ||
30 | const { | ||
31 | children, | ||
32 | pathname, | ||
33 | error, | ||
34 | isOnline, | ||
35 | isAPIHealthy, | ||
36 | retryHealthCheck, | ||
37 | isHealthCheckLoading, | ||
38 | } = this.props; | ||
39 | const { intl } = this.context; | ||
40 | |||
41 | return ( | ||
42 | <div className="auth"> | ||
43 | {!isOnline && ( | ||
44 | <InfoBar | ||
45 | type="warning" | ||
46 | > | ||
47 | <span className="mdi mdi-flash" /> | ||
48 | {intl.formatMessage(globalMessages.notConnectedToTheInternet)} | ||
49 | </InfoBar> | ||
50 | )} | ||
51 | {isOnline && !isAPIHealthy && ( | ||
52 | <InfoBar | ||
53 | type="danger" | ||
54 | ctaLabel="Try again" | ||
55 | ctaLoading={isHealthCheckLoading} | ||
56 | sticky | ||
57 | onClick={retryHealthCheck} | ||
58 | > | ||
59 | <span className="mdi mdi-flash" /> | ||
60 | {intl.formatMessage(globalMessages.APIUnhealthy)} | ||
61 | </InfoBar> | ||
62 | )} | ||
63 | <div className="auth__layout"> | ||
64 | <RouteTransition | ||
65 | pathname={pathname} | ||
66 | atEnter={{ opacity: 0 }} | ||
67 | atLeave={{ opacity: 0 }} | ||
68 | atActive={{ opacity: 1 }} | ||
69 | mapStyles={styles => ({ | ||
70 | transform: `translateX(${styles.translateX}%)`, | ||
71 | opacity: styles.opacity, | ||
72 | })} | ||
73 | component="span" | ||
74 | > | ||
75 | {/* Inject globalError into children */} | ||
76 | {React.cloneElement(children, { | ||
77 | error, | ||
78 | })} | ||
79 | </RouteTransition> | ||
80 | </div> | ||
81 | {/* </div> */} | ||
82 | <Link to="https://adlk.io" className="auth__adlk" target="_blank"> | ||
83 | <img src="./assets/images/adlk.svg" alt="" /> | ||
84 | </Link> | ||
85 | </div> | ||
86 | ); | ||
87 | } | ||
88 | } | ||
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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | import { Link } from 'react-router'; | ||
6 | import classnames from 'classnames'; | ||
7 | |||
8 | import Form from '../../lib/Form'; | ||
9 | import Toggle from '../ui/Toggle'; | ||
10 | import Button from '../ui/Button'; | ||
11 | |||
12 | const messages = defineMessages({ | ||
13 | headline: { | ||
14 | id: 'import.headline', | ||
15 | defaultMessage: '!!!Import your Franz 4 services', | ||
16 | }, | ||
17 | notSupportedHeadline: { | ||
18 | id: 'import.notSupportedHeadline', | ||
19 | defaultMessage: '!!!Services not yet supported in Franz 5', | ||
20 | }, | ||
21 | submitButtonLabel: { | ||
22 | id: 'import.submit.label', | ||
23 | defaultMessage: '!!!Import {count} services', | ||
24 | }, | ||
25 | skipButtonLabel: { | ||
26 | id: 'import.skip.label', | ||
27 | defaultMessage: '!!!I want add services manually', | ||
28 | }, | ||
29 | }); | ||
30 | |||
31 | @observer | ||
32 | export default class Import extends Component { | ||
33 | static propTypes = { | ||
34 | services: MobxPropTypes.arrayOrObservableArray.isRequired, | ||
35 | onSubmit: PropTypes.func.isRequired, | ||
36 | isSubmitting: PropTypes.bool.isRequired, | ||
37 | inviteRoute: PropTypes.string.isRequired, | ||
38 | }; | ||
39 | |||
40 | static contextTypes = { | ||
41 | intl: intlShape, | ||
42 | }; | ||
43 | |||
44 | prepareForm() { | ||
45 | const { services } = this.props; | ||
46 | |||
47 | const config = { | ||
48 | fields: { | ||
49 | import: [...services.filter(s => s.recipe).map(s => ({ | ||
50 | add: { | ||
51 | default: true, | ||
52 | options: s, | ||
53 | }, | ||
54 | }))], | ||
55 | }, | ||
56 | }; | ||
57 | |||
58 | return new Form(config, this.context.intl); | ||
59 | } | ||
60 | |||
61 | submit(e) { | ||
62 | const { services } = this.props; | ||
63 | e.preventDefault(); | ||
64 | this.form.submit({ | ||
65 | onSuccess: (form) => { | ||
66 | const servicesImport = form.values().import | ||
67 | .map((value, i) => !value.add || services.filter(s => s.recipe)[i]) | ||
68 | .filter(s => typeof s !== 'boolean'); | ||
69 | |||
70 | this.props.onSubmit({ services: servicesImport }); | ||
71 | }, | ||
72 | onError: () => {}, | ||
73 | }); | ||
74 | } | ||
75 | |||
76 | render() { | ||
77 | this.form = this.prepareForm(); | ||
78 | const { intl } = this.context; | ||
79 | const { services, isSubmitting, inviteRoute } = this.props; | ||
80 | |||
81 | const availableServices = services.filter(s => s.recipe); | ||
82 | const unavailableServices = services.filter(s => !s.recipe); | ||
83 | |||
84 | return ( | ||
85 | <div className="auth__scroll-container"> | ||
86 | <div className="auth__container auth__container--signup"> | ||
87 | <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> | ||
88 | <img | ||
89 | src="./assets/images/logo.svg" | ||
90 | className="auth__logo" | ||
91 | alt="" | ||
92 | /> | ||
93 | <h1> | ||
94 | {intl.formatMessage(messages.headline)} | ||
95 | </h1> | ||
96 | <table className="service-table available-services"> | ||
97 | <tbody> | ||
98 | {this.form.$('import').map((service, i) => ( | ||
99 | <tr | ||
100 | key={service.id} | ||
101 | className="service-table__row" | ||
102 | onClick={() => service.$('add').set(!service.$('add').value)} | ||
103 | > | ||
104 | <td className="service-table__toggle"> | ||
105 | <Toggle | ||
106 | field={service.$('add')} | ||
107 | showLabel={false} | ||
108 | /> | ||
109 | </td> | ||
110 | <td className="service-table__column-icon"> | ||
111 | <img | ||
112 | src={availableServices[i].custom_icon || availableServices[i].recipe.icons.svg} | ||
113 | className={classnames({ | ||
114 | 'service-table__icon': true, | ||
115 | 'has-custom-icon': availableServices[i].custom_icon, | ||
116 | })} | ||
117 | alt="" | ||
118 | /> | ||
119 | </td> | ||
120 | <td className="service-table__column-name"> | ||
121 | {availableServices[i].name !== '' | ||
122 | ? availableServices[i].name | ||
123 | : availableServices[i].recipe.name} | ||
124 | </td> | ||
125 | </tr> | ||
126 | ))} | ||
127 | </tbody> | ||
128 | </table> | ||
129 | {unavailableServices.length > 0 && ( | ||
130 | <div className="unavailable-services"> | ||
131 | <strong>{intl.formatMessage(messages.notSupportedHeadline)}</strong> | ||
132 | <p> | ||
133 | {services.filter(s => !s.recipe).map((service, i) => ( | ||
134 | <span key={service.id}> | ||
135 | {service.name !== '' ? service.name : service.service} | ||
136 | {unavailableServices.length > i + 1 ? ', ' : ''} | ||
137 | </span> | ||
138 | ))} | ||
139 | </p> | ||
140 | </div> | ||
141 | )} | ||
142 | |||
143 | {isSubmitting ? ( | ||
144 | <Button | ||
145 | className="auth__button is-loading" | ||
146 | label={`${intl.formatMessage(messages.submitButtonLabel)} ...`} | ||
147 | loaded={false} | ||
148 | disabled | ||
149 | /> | ||
150 | ) : ( | ||
151 | <Button | ||
152 | type="submit" | ||
153 | className="auth__button" | ||
154 | label={intl.formatMessage(messages.submitButtonLabel)} | ||
155 | /> | ||
156 | )} | ||
157 | <Link | ||
158 | to={inviteRoute} | ||
159 | className="franz-form__button franz-form__button--secondary auth__button auth__button--skip" | ||
160 | > | ||
161 | {intl.formatMessage(messages.skipButtonLabel)} | ||
162 | </Link> | ||
163 | </form> | ||
164 | </div> | ||
165 | </div> | ||
166 | ); | ||
167 | } | ||
168 | } | ||
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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | import { Link } from 'react-router'; | ||
6 | |||
7 | import Form from '../../lib/Form'; | ||
8 | import { email } from '../../helpers/validation-helpers'; | ||
9 | import Input from '../ui/Input'; | ||
10 | import Button from '../ui/Button'; | ||
11 | |||
12 | const messages = defineMessages({ | ||
13 | headline: { | ||
14 | id: 'invite.headline.friends', | ||
15 | defaultMessage: '!!!Invite 3 of your friends or colleagues', | ||
16 | }, | ||
17 | nameLabel: { | ||
18 | id: 'invite.name.label', | ||
19 | defaultMessage: '!!!Name', | ||
20 | }, | ||
21 | emailLabel: { | ||
22 | id: 'invite.email.label', | ||
23 | defaultMessage: '!!!Email address', | ||
24 | }, | ||
25 | submitButtonLabel: { | ||
26 | id: 'invite.submit.label', | ||
27 | defaultMessage: '!!!Send invites', | ||
28 | }, | ||
29 | skipButtonLabel: { | ||
30 | id: 'invite.skip.label', | ||
31 | defaultMessage: '!!!I want to do this later', | ||
32 | }, | ||
33 | }); | ||
34 | |||
35 | @observer | ||
36 | export default class Invite extends Component { | ||
37 | static propTypes = { | ||
38 | onSubmit: PropTypes.func.isRequired, | ||
39 | }; | ||
40 | |||
41 | static contextTypes = { | ||
42 | intl: intlShape, | ||
43 | }; | ||
44 | |||
45 | form = new Form({ | ||
46 | fields: { | ||
47 | invite: [...Array(3).fill({ | ||
48 | name: { | ||
49 | label: this.context.intl.formatMessage(messages.nameLabel), | ||
50 | // value: '', | ||
51 | placeholder: this.context.intl.formatMessage(messages.nameLabel), | ||
52 | }, | ||
53 | email: { | ||
54 | label: this.context.intl.formatMessage(messages.emailLabel), | ||
55 | // value: '', | ||
56 | validate: [email], | ||
57 | placeholder: this.context.intl.formatMessage(messages.emailLabel), | ||
58 | }, | ||
59 | })], | ||
60 | }, | ||
61 | }, this.context.intl); | ||
62 | |||
63 | submit(e) { | ||
64 | e.preventDefault(); | ||
65 | this.form.submit({ | ||
66 | onSuccess: (form) => { | ||
67 | this.props.onSubmit({ invites: form.values().invite }); | ||
68 | }, | ||
69 | onError: () => {}, | ||
70 | }); | ||
71 | } | ||
72 | |||
73 | render() { | ||
74 | const { form } = this; | ||
75 | const { intl } = this.context; | ||
76 | |||
77 | return ( | ||
78 | <div className="auth__container auth__container--signup"> | ||
79 | <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> | ||
80 | <img | ||
81 | src="./assets/images/logo.svg" | ||
82 | className="auth__logo" | ||
83 | alt="" | ||
84 | /> | ||
85 | <h1> | ||
86 | {intl.formatMessage(messages.headline)} | ||
87 | </h1> | ||
88 | {form.$('invite').map(invite => ( | ||
89 | <div className="grid" key={invite.key}> | ||
90 | <div className="grid__row"> | ||
91 | <Input field={invite.$('name')} showLabel={false} /> | ||
92 | <Input field={invite.$('email')} showLabel={false} /> | ||
93 | </div> | ||
94 | </div> | ||
95 | ))} | ||
96 | <Button | ||
97 | type="submit" | ||
98 | className="auth__button" | ||
99 | label={intl.formatMessage(messages.submitButtonLabel)} | ||
100 | /> | ||
101 | <Link | ||
102 | to="/" | ||
103 | className="franz-form__button franz-form__button--secondary auth__button auth__button--skip" | ||
104 | > | ||
105 | {intl.formatMessage(messages.skipButtonLabel)} | ||
106 | </Link> | ||
107 | </form> | ||
108 | </div> | ||
109 | ); | ||
110 | } | ||
111 | } | ||
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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | |||
6 | import Form from '../../lib/Form'; | ||
7 | import { required, email } from '../../helpers/validation-helpers'; | ||
8 | import Input from '../ui/Input'; | ||
9 | import Button from '../ui/Button'; | ||
10 | import Link from '../ui/Link'; | ||
11 | |||
12 | import { globalError as globalErrorPropType } from '../../prop-types'; | ||
13 | |||
14 | // import Appear from '../ui/effects/Appear'; | ||
15 | |||
16 | const messages = defineMessages({ | ||
17 | headline: { | ||
18 | id: 'login.headline', | ||
19 | defaultMessage: '!!!Sign in', | ||
20 | }, | ||
21 | emailLabel: { | ||
22 | id: 'login.email.label', | ||
23 | defaultMessage: '!!!Email address', | ||
24 | }, | ||
25 | passwordLabel: { | ||
26 | id: 'login.password.label', | ||
27 | defaultMessage: '!!!Password', | ||
28 | }, | ||
29 | submitButtonLabel: { | ||
30 | id: 'login.submit.label', | ||
31 | defaultMessage: '!!!Sign in', | ||
32 | }, | ||
33 | invalidCredentials: { | ||
34 | id: 'login.invalidCredentials', | ||
35 | defaultMessage: '!!!Email or password not valid', | ||
36 | }, | ||
37 | tokenExpired: { | ||
38 | id: 'login.tokenExpired', | ||
39 | defaultMessage: '!!!Your session expired, please login again.', | ||
40 | }, | ||
41 | serverLogout: { | ||
42 | id: 'login.serverLogout', | ||
43 | defaultMessage: '!!!Your session expired, please login again.', | ||
44 | }, | ||
45 | signupLink: { | ||
46 | id: 'login.link.signup', | ||
47 | defaultMessage: '!!!Create a free account', | ||
48 | }, | ||
49 | passwordLink: { | ||
50 | id: 'login.link.password', | ||
51 | defaultMessage: '!!!Forgot password', | ||
52 | }, | ||
53 | }); | ||
54 | |||
55 | @observer | ||
56 | export default class Login extends Component { | ||
57 | static propTypes = { | ||
58 | onSubmit: PropTypes.func.isRequired, | ||
59 | isSubmitting: PropTypes.bool.isRequired, | ||
60 | isTokenExpired: PropTypes.bool.isRequired, | ||
61 | isServerLogout: PropTypes.bool.isRequired, | ||
62 | signupRoute: PropTypes.string.isRequired, | ||
63 | passwordRoute: PropTypes.string.isRequired, | ||
64 | error: globalErrorPropType.isRequired, | ||
65 | }; | ||
66 | |||
67 | static contextTypes = { | ||
68 | intl: intlShape, | ||
69 | }; | ||
70 | |||
71 | form = new Form({ | ||
72 | fields: { | ||
73 | email: { | ||
74 | label: this.context.intl.formatMessage(messages.emailLabel), | ||
75 | value: '', | ||
76 | validate: [required, email], | ||
77 | }, | ||
78 | password: { | ||
79 | label: this.context.intl.formatMessage(messages.passwordLabel), | ||
80 | value: '', | ||
81 | validate: [required], | ||
82 | type: 'password', | ||
83 | }, | ||
84 | }, | ||
85 | }, this.context.intl); | ||
86 | |||
87 | submit(e) { | ||
88 | e.preventDefault(); | ||
89 | this.form.submit({ | ||
90 | onSuccess: (form) => { | ||
91 | this.props.onSubmit(form.values()); | ||
92 | }, | ||
93 | onError: () => {}, | ||
94 | }); | ||
95 | } | ||
96 | |||
97 | emailField = null; | ||
98 | |||
99 | render() { | ||
100 | const { form } = this; | ||
101 | const { intl } = this.context; | ||
102 | const { | ||
103 | isSubmitting, | ||
104 | isTokenExpired, | ||
105 | isServerLogout, | ||
106 | signupRoute, | ||
107 | passwordRoute, | ||
108 | error, | ||
109 | } = this.props; | ||
110 | |||
111 | return ( | ||
112 | <div className="auth__container"> | ||
113 | <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> | ||
114 | <img | ||
115 | src="./assets/images/logo.svg" | ||
116 | className="auth__logo" | ||
117 | alt="" | ||
118 | /> | ||
119 | <h1>{intl.formatMessage(messages.headline)}</h1> | ||
120 | {isTokenExpired && ( | ||
121 | <p className="error-message center">{intl.formatMessage(messages.tokenExpired)}</p> | ||
122 | )} | ||
123 | {isServerLogout && ( | ||
124 | <p className="error-message center">{intl.formatMessage(messages.serverLogout)}</p> | ||
125 | )} | ||
126 | <Input | ||
127 | field={form.$('email')} | ||
128 | ref={(element) => { this.emailField = element; }} | ||
129 | focus | ||
130 | /> | ||
131 | <Input | ||
132 | field={form.$('password')} | ||
133 | showPasswordToggle | ||
134 | /> | ||
135 | {error.code === 'invalid-credentials' && ( | ||
136 | <p className="error-message center">{intl.formatMessage(messages.invalidCredentials)}</p> | ||
137 | )} | ||
138 | {isSubmitting ? ( | ||
139 | <Button | ||
140 | className="auth__button is-loading" | ||
141 | buttonType="secondary" | ||
142 | label={`${intl.formatMessage(messages.submitButtonLabel)} ...`} | ||
143 | loaded={false} | ||
144 | disabled | ||
145 | /> | ||
146 | ) : ( | ||
147 | <Button | ||
148 | type="submit" | ||
149 | className="auth__button" | ||
150 | label={intl.formatMessage(messages.submitButtonLabel)} | ||
151 | /> | ||
152 | )} | ||
153 | </form> | ||
154 | <div className="auth__links"> | ||
155 | <Link to={signupRoute}>{intl.formatMessage(messages.signupLink)}</Link> | ||
156 | <Link to={passwordRoute}>{intl.formatMessage(messages.passwordLink)}</Link> | ||
157 | </div> | ||
158 | </div> | ||
159 | ); | ||
160 | } | ||
161 | } | ||
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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | |||
6 | import Form from '../../lib/Form'; | ||
7 | import { required, email } from '../../helpers/validation-helpers'; | ||
8 | import Input from '../ui/Input'; | ||
9 | import Button from '../ui/Button'; | ||
10 | import Link from '../ui/Link'; | ||
11 | import Infobox from '../ui/Infobox'; | ||
12 | |||
13 | const messages = defineMessages({ | ||
14 | headline: { | ||
15 | id: 'password.headline', | ||
16 | defaultMessage: '!!!Forgot password', | ||
17 | }, | ||
18 | emailLabel: { | ||
19 | id: 'password.email.label', | ||
20 | defaultMessage: '!!!Email address', | ||
21 | }, | ||
22 | submitButtonLabel: { | ||
23 | id: 'password.submit.label', | ||
24 | defaultMessage: '!!!Submit', | ||
25 | }, | ||
26 | successInfo: { | ||
27 | id: 'password.successInfo', | ||
28 | defaultMessage: '!!!Your new password was sent to your email address', | ||
29 | }, | ||
30 | noUser: { | ||
31 | id: 'password.noUser', | ||
32 | defaultMessage: '!!!No user affiliated with that email address', | ||
33 | }, | ||
34 | signupLink: { | ||
35 | id: 'password.link.signup', | ||
36 | defaultMessage: '!!!Create a free account', | ||
37 | }, | ||
38 | loginLink: { | ||
39 | id: 'password.link.login', | ||
40 | defaultMessage: '!!!Sign in to your account', | ||
41 | }, | ||
42 | }); | ||
43 | |||
44 | @observer | ||
45 | export default class Password extends Component { | ||
46 | static propTypes = { | ||
47 | onSubmit: PropTypes.func.isRequired, | ||
48 | isSubmitting: PropTypes.bool.isRequired, | ||
49 | signupRoute: PropTypes.string.isRequired, | ||
50 | loginRoute: PropTypes.string.isRequired, | ||
51 | status: MobxPropTypes.arrayOrObservableArray.isRequired, | ||
52 | }; | ||
53 | |||
54 | static contextTypes = { | ||
55 | intl: intlShape, | ||
56 | }; | ||
57 | |||
58 | form = new Form({ | ||
59 | fields: { | ||
60 | email: { | ||
61 | label: this.context.intl.formatMessage(messages.emailLabel), | ||
62 | value: '', | ||
63 | validate: [required, email], | ||
64 | }, | ||
65 | }, | ||
66 | }, this.context.intl); | ||
67 | |||
68 | submit(e) { | ||
69 | e.preventDefault(); | ||
70 | this.form.submit({ | ||
71 | onSuccess: (form) => { | ||
72 | this.props.onSubmit(form.values()); | ||
73 | }, | ||
74 | onError: () => {}, | ||
75 | }); | ||
76 | } | ||
77 | |||
78 | render() { | ||
79 | const { form } = this; | ||
80 | const { intl } = this.context; | ||
81 | const { | ||
82 | isSubmitting, | ||
83 | signupRoute, | ||
84 | loginRoute, | ||
85 | status, | ||
86 | } = this.props; | ||
87 | |||
88 | return ( | ||
89 | <div className="auth__container"> | ||
90 | <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> | ||
91 | <img | ||
92 | src="./assets/images/logo.svg" | ||
93 | className="auth__logo" | ||
94 | alt="" | ||
95 | /> | ||
96 | <h1>{intl.formatMessage(messages.headline)}</h1> | ||
97 | {status.length > 0 && status.includes('sent') && ( | ||
98 | <Infobox | ||
99 | type="success" | ||
100 | icon="checkbox-marked-circle-outline" | ||
101 | > | ||
102 | {intl.formatMessage(messages.successInfo)} | ||
103 | </Infobox> | ||
104 | )} | ||
105 | <Input | ||
106 | field={form.$('email')} | ||
107 | focus | ||
108 | /> | ||
109 | {status.length > 0 && status.includes('no-user') && ( | ||
110 | <p className="error-message center">{intl.formatMessage(messages.noUser)}</p> | ||
111 | )} | ||
112 | {isSubmitting ? ( | ||
113 | <Button | ||
114 | className="auth__button is-loading" | ||
115 | buttonType="secondary" | ||
116 | label={`${intl.formatMessage(messages.submitButtonLabel)} ...`} | ||
117 | loaded={false} | ||
118 | disabled | ||
119 | /> | ||
120 | ) : ( | ||
121 | <Button | ||
122 | type="submit" | ||
123 | className="auth__button" | ||
124 | label={intl.formatMessage(messages.submitButtonLabel)} | ||
125 | /> | ||
126 | )} | ||
127 | </form> | ||
128 | <div className="auth__links"> | ||
129 | <Link to={loginRoute}>{intl.formatMessage(messages.loginLink)}</Link> | ||
130 | <Link to={signupRoute}>{intl.formatMessage(messages.signupLink)}</Link> | ||
131 | </div> | ||
132 | </div> | ||
133 | ); | ||
134 | } | ||
135 | } | ||
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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | // import { Link } from 'react-router'; | ||
6 | |||
7 | // import Button from '../ui/Button'; | ||
8 | import Loader from '../ui/Loader'; | ||
9 | import Appear from '../ui/effects/Appear'; | ||
10 | import SubscriptionForm from '../../containers/ui/SubscriptionFormScreen'; | ||
11 | |||
12 | const messages = defineMessages({ | ||
13 | headline: { | ||
14 | id: 'pricing.headline', | ||
15 | defaultMessage: '!!!Support Franz', | ||
16 | }, | ||
17 | monthlySupportLabel: { | ||
18 | id: 'pricing.support.label', | ||
19 | defaultMessage: '!!!Select your support plan', | ||
20 | }, | ||
21 | submitButtonLabel: { | ||
22 | id: 'pricing.submit.label', | ||
23 | defaultMessage: '!!!Support the development of Franz', | ||
24 | }, | ||
25 | skipPayment: { | ||
26 | id: 'pricing.link.skipPayment', | ||
27 | defaultMessage: '!!!I don\'t want to support the development of Franz.', | ||
28 | }, | ||
29 | }); | ||
30 | |||
31 | @observer | ||
32 | export default class Signup extends Component { | ||
33 | static propTypes = { | ||
34 | donor: MobxPropTypes.objectOrObservableObject.isRequired, | ||
35 | isLoading: PropTypes.bool.isRequired, | ||
36 | isLoadingUser: PropTypes.bool.isRequired, | ||
37 | onCloseSubscriptionWindow: PropTypes.func.isRequired, | ||
38 | skipAction: PropTypes.func.isRequired, | ||
39 | }; | ||
40 | |||
41 | static contextTypes = { | ||
42 | intl: intlShape, | ||
43 | }; | ||
44 | |||
45 | render() { | ||
46 | const { | ||
47 | donor, | ||
48 | isLoading, | ||
49 | isLoadingUser, | ||
50 | onCloseSubscriptionWindow, | ||
51 | skipAction, | ||
52 | } = this.props; | ||
53 | const { intl } = this.context; | ||
54 | |||
55 | return ( | ||
56 | <div className="auth__scroll-container"> | ||
57 | <div className="auth__container auth__container--signup"> | ||
58 | <form className="franz-form auth__form"> | ||
59 | <img | ||
60 | src="./assets/images/sm.png" | ||
61 | className="auth__logo auth__logo--sm" | ||
62 | alt="" | ||
63 | /> | ||
64 | <h1>{intl.formatMessage(messages.headline)}</h1> | ||
65 | <div className="auth__letter"> | ||
66 | {isLoadingUser && ( | ||
67 | <p>Loading</p> | ||
68 | )} | ||
69 | {!isLoadingUser && ( | ||
70 | donor.amount ? ( | ||
71 | <span> | ||
72 | <p> | ||
73 | Thank you so much for your previous donation of <strong>$ {donor.amount}</strong>. | ||
74 | <br /> | ||
75 | Your support allowed us to get where we are today. | ||
76 | <br /> | ||
77 | </p> | ||
78 | <p> | ||
79 | As an early supporter, you get <strong>a lifetime premium supporter license</strong> without any | ||
80 | additional charges. | ||
81 | </p> | ||
82 | <p> | ||
83 | However, If you want to keep supporting us, you are more than welcome to subscribe to a plan. | ||
84 | <br /><br /> | ||
85 | </p> | ||
86 | </span> | ||
87 | ) : ( | ||
88 | <span> | ||
89 | <p> | ||
90 | We built Franz with a lot of effort, manpower and love, | ||
91 | to bring you the best messaging experience. | ||
92 | <br /> | ||
93 | </p> | ||
94 | <p> | ||
95 | Getting a Franz Premium Supporter License will allow us to keep improving Franz for you. | ||
96 | </p> | ||
97 | </span> | ||
98 | ) | ||
99 | )} | ||
100 | <p> | ||
101 | Thanks for being a hero. | ||
102 | </p> | ||
103 | <p> | ||
104 | <strong>Stefan Malzner</strong> | ||
105 | </p> | ||
106 | </div> | ||
107 | <Loader loaded={!isLoading}> | ||
108 | <Appear transitionName="slideDown"> | ||
109 | <span className="label">{intl.formatMessage(messages.monthlySupportLabel)}</span> | ||
110 | <SubscriptionForm | ||
111 | onCloseWindow={onCloseSubscriptionWindow} | ||
112 | showSkipOption | ||
113 | skipAction={skipAction} | ||
114 | hideInfo={Boolean(donor.amount)} | ||
115 | skipButtonLabel={intl.formatMessage(messages.skipPayment)} | ||
116 | /> | ||
117 | {/* <Link | ||
118 | to={inviteRoute} | ||
119 | className="franz-form__button franz-form__button--secondary auth__button auth__button--skip" | ||
120 | > | ||
121 | {intl.formatMessage(messages.skipPayment)} | ||
122 | </Link> */} | ||
123 | </Appear> | ||
124 | </Loader> | ||
125 | </form> | ||
126 | </div> | ||
127 | </div> | ||
128 | ); | ||
129 | } | ||
130 | } | ||
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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | |||
6 | import Form from '../../lib/Form'; | ||
7 | import { required, email, minLength } from '../../helpers/validation-helpers'; | ||
8 | import Input from '../ui/Input'; | ||
9 | import Radio from '../ui/Radio'; | ||
10 | import Button from '../ui/Button'; | ||
11 | import Link from '../ui/Link'; | ||
12 | |||
13 | import { globalError as globalErrorPropType } from '../../prop-types'; | ||
14 | |||
15 | const messages = defineMessages({ | ||
16 | headline: { | ||
17 | id: 'signup.headline', | ||
18 | defaultMessage: '!!!Sign up', | ||
19 | }, | ||
20 | firstnameLabel: { | ||
21 | id: 'signup.firstname.label', | ||
22 | defaultMessage: '!!!Firstname', | ||
23 | }, | ||
24 | lastnameLabel: { | ||
25 | id: 'signup.lastname.label', | ||
26 | defaultMessage: '!!!Lastname', | ||
27 | }, | ||
28 | emailLabel: { | ||
29 | id: 'signup.email.label', | ||
30 | defaultMessage: '!!!Email address', | ||
31 | }, | ||
32 | companyLabel: { | ||
33 | id: 'signup.company.label', | ||
34 | defaultMessage: '!!!Company', | ||
35 | }, | ||
36 | passwordLabel: { | ||
37 | id: 'signup.password.label', | ||
38 | defaultMessage: '!!!Password', | ||
39 | }, | ||
40 | legalInfo: { | ||
41 | id: 'signup.legal.info', | ||
42 | defaultMessage: '!!!By creating a Franz account you accept the', | ||
43 | }, | ||
44 | terms: { | ||
45 | id: 'signup.legal.terms', | ||
46 | defaultMessage: '!!!Terms of service', | ||
47 | }, | ||
48 | privacy: { | ||
49 | id: 'signup.legal.privacy', | ||
50 | defaultMessage: '!!!Privacy Statement', | ||
51 | }, | ||
52 | submitButtonLabel: { | ||
53 | id: 'signup.submit.label', | ||
54 | defaultMessage: '!!!Create account', | ||
55 | }, | ||
56 | loginLink: { | ||
57 | id: 'signup.link.login', | ||
58 | defaultMessage: '!!!Already have an account, sign in?', | ||
59 | }, | ||
60 | emailDuplicate: { | ||
61 | id: 'signup.emailDuplicate', | ||
62 | defaultMessage: '!!!A user with that email address already exists', | ||
63 | }, | ||
64 | }); | ||
65 | |||
66 | @observer | ||
67 | export default class Signup extends Component { | ||
68 | static propTypes = { | ||
69 | onSubmit: PropTypes.func.isRequired, | ||
70 | isSubmitting: PropTypes.bool.isRequired, | ||
71 | loginRoute: PropTypes.string.isRequired, | ||
72 | error: globalErrorPropType.isRequired, | ||
73 | }; | ||
74 | |||
75 | static contextTypes = { | ||
76 | intl: intlShape, | ||
77 | }; | ||
78 | |||
79 | form = new Form({ | ||
80 | fields: { | ||
81 | accountType: { | ||
82 | value: 'individual', | ||
83 | validate: [required], | ||
84 | options: [{ | ||
85 | value: 'individual', | ||
86 | label: 'Individual', | ||
87 | }, { | ||
88 | value: 'non-profit', | ||
89 | label: 'Non-Profit', | ||
90 | }, { | ||
91 | value: 'company', | ||
92 | label: 'Company', | ||
93 | }], | ||
94 | }, | ||
95 | firstname: { | ||
96 | label: this.context.intl.formatMessage(messages.firstnameLabel), | ||
97 | value: '', | ||
98 | validate: [required], | ||
99 | }, | ||
100 | lastname: { | ||
101 | label: this.context.intl.formatMessage(messages.lastnameLabel), | ||
102 | value: '', | ||
103 | validate: [required], | ||
104 | }, | ||
105 | email: { | ||
106 | label: this.context.intl.formatMessage(messages.emailLabel), | ||
107 | value: '', | ||
108 | validate: [required, email], | ||
109 | }, | ||
110 | organization: { | ||
111 | label: this.context.intl.formatMessage(messages.companyLabel), | ||
112 | value: '', // TODO: make required when accountType: company | ||
113 | }, | ||
114 | password: { | ||
115 | label: this.context.intl.formatMessage(messages.passwordLabel), | ||
116 | value: '', | ||
117 | validate: [required, minLength(6)], | ||
118 | type: 'password', | ||
119 | }, | ||
120 | }, | ||
121 | }, this.context.intl); | ||
122 | |||
123 | submit(e) { | ||
124 | e.preventDefault(); | ||
125 | this.form.submit({ | ||
126 | onSuccess: (form) => { | ||
127 | this.props.onSubmit(form.values()); | ||
128 | }, | ||
129 | onError: () => {}, | ||
130 | }); | ||
131 | } | ||
132 | |||
133 | render() { | ||
134 | const { form } = this; | ||
135 | const { intl } = this.context; | ||
136 | const { isSubmitting, loginRoute, error } = this.props; | ||
137 | |||
138 | return ( | ||
139 | <div className="auth__scroll-container"> | ||
140 | <div className="auth__container auth__container--signup"> | ||
141 | <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> | ||
142 | <img | ||
143 | src="./assets/images/logo.svg" | ||
144 | className="auth__logo" | ||
145 | alt="" | ||
146 | /> | ||
147 | <h1>{intl.formatMessage(messages.headline)}</h1> | ||
148 | <Radio field={form.$('accountType')} showLabel={false} /> | ||
149 | <div className="grid__row"> | ||
150 | <Input field={form.$('firstname')} focus /> | ||
151 | <Input field={form.$('lastname')} /> | ||
152 | </div> | ||
153 | <Input field={form.$('email')} /> | ||
154 | <Input | ||
155 | field={form.$('password')} | ||
156 | showPasswordToggle | ||
157 | scorePassword | ||
158 | /> | ||
159 | {form.$('accountType').value === 'company' && ( | ||
160 | <Input field={form.$('organization')} /> | ||
161 | )} | ||
162 | {error.code === 'email-duplicate' && ( | ||
163 | <p className="error-message center">{intl.formatMessage(messages.emailDuplicate)}</p> | ||
164 | )} | ||
165 | {isSubmitting ? ( | ||
166 | <Button | ||
167 | className="auth__button is-loading" | ||
168 | label={`${intl.formatMessage(messages.submitButtonLabel)} ...`} | ||
169 | loaded={false} | ||
170 | disabled | ||
171 | /> | ||
172 | ) : ( | ||
173 | <Button | ||
174 | type="submit" | ||
175 | className="auth__button" | ||
176 | label={intl.formatMessage(messages.submitButtonLabel)} | ||
177 | /> | ||
178 | )} | ||
179 | <p className="legal"> | ||
180 | {intl.formatMessage(messages.legalInfo)} | ||
181 | <br /> | ||
182 | <Link | ||
183 | to="http://meetfranz.com/terms" | ||
184 | target="_blank" | ||
185 | className="link" | ||
186 | > | ||
187 | {intl.formatMessage(messages.terms)} | ||
188 | </Link> | ||
189 | & | ||
190 | <Link | ||
191 | to="http://meetfranz.com/privacy" | ||
192 | target="_blank" | ||
193 | className="link" | ||
194 | > | ||
195 | {intl.formatMessage(messages.privacy)} | ||
196 | </Link>. | ||
197 | </p> | ||
198 | </form> | ||
199 | <div className="auth__links"> | ||
200 | <Link to={loginRoute}>{intl.formatMessage(messages.loginLink)}</Link> | ||
201 | </div> | ||
202 | </div> | ||
203 | </div> | ||
204 | ); | ||
205 | } | ||
206 | } | ||
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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer, PropTypes as MobxPropTypes } from 'mobx-react'; | ||
4 | import { defineMessages, intlShape } from 'react-intl'; | ||
5 | |||
6 | import Link from '../ui/Link'; | ||
7 | |||
8 | const messages = defineMessages({ | ||
9 | signupButton: { | ||
10 | id: 'welcome.signupButton', | ||
11 | defaultMessage: '!!!Create a free account', | ||
12 | }, | ||
13 | loginButton: { | ||
14 | id: 'welcome.loginButton', | ||
15 | defaultMessage: '!!!Login to your account', | ||
16 | }, | ||
17 | }); | ||
18 | |||
19 | @observer | ||
20 | export default class Login extends Component { | ||
21 | static propTypes = { | ||
22 | loginRoute: PropTypes.string.isRequired, | ||
23 | signupRoute: PropTypes.string.isRequired, | ||
24 | recipes: MobxPropTypes.arrayOrObservableArray.isRequired, | ||
25 | }; | ||
26 | |||
27 | static contextTypes = { | ||
28 | intl: intlShape, | ||
29 | }; | ||
30 | |||
31 | render() { | ||
32 | const { intl } = this.context; | ||
33 | const { | ||
34 | loginRoute, | ||
35 | signupRoute, | ||
36 | recipes, | ||
37 | } = this.props; | ||
38 | |||
39 | return ( | ||
40 | <div className="welcome"> | ||
41 | <div className="welcome__content"> | ||
42 | <img src="./assets/images/logo.svg" className="welcome__logo" alt="" /> | ||
43 | {/* <img src="./assets/images/welcome.png" className="welcome__services" alt="" /> */} | ||
44 | <div className="welcome__text"> | ||
45 | <h1>Franz</h1> | ||
46 | </div> | ||
47 | </div> | ||
48 | <div className="welcome__buttons"> | ||
49 | <Link to={signupRoute} className="button"> | ||
50 | {intl.formatMessage(messages.signupButton)} | ||
51 | </Link> | ||
52 | <Link to={loginRoute} className="button"> | ||
53 | {intl.formatMessage(messages.loginButton)} | ||
54 | </Link> | ||
55 | </div> | ||
56 | <div className="welcome__featured-services"> | ||
57 | {recipes.map(recipe => ( | ||
58 | <img | ||
59 | key={recipe.id} | ||
60 | src={recipe.icons.svg} | ||
61 | className="welcome__featured-service" | ||
62 | alt="" | ||
63 | /> | ||
64 | ))} | ||
65 | </div> | ||
66 | </div> | ||
67 | ); | ||
68 | } | ||
69 | } | ||