diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/app.js | 2 | ||||
-rw-r--r-- | src/components/auth/Invite.js | 121 | ||||
-rw-r--r-- | src/components/settings/navigation/SettingsNavigation.js | 11 | ||||
-rw-r--r-- | src/components/settings/services/ServiceError.js | 2 | ||||
-rw-r--r-- | src/containers/auth/InviteScreen.js | 2 | ||||
-rw-r--r-- | src/containers/settings/InviteScreen.js | 44 | ||||
-rw-r--r-- | src/i18n/locales/en-US.json | 3 | ||||
-rw-r--r-- | src/stores/UserStore.js | 12 | ||||
-rw-r--r-- | src/styles/invite.scss | 15 | ||||
-rw-r--r-- | src/styles/main.scss | 1 |
10 files changed, 184 insertions, 29 deletions
diff --git a/src/app.js b/src/app.js index 97be9f898..814bfacf1 100644 --- a/src/app.js +++ b/src/app.js | |||
@@ -27,6 +27,7 @@ import EditServiceScreen from './containers/settings/EditServiceScreen'; | |||
27 | import AccountScreen from './containers/settings/AccountScreen'; | 27 | import AccountScreen from './containers/settings/AccountScreen'; |
28 | import EditUserScreen from './containers/settings/EditUserScreen'; | 28 | import EditUserScreen from './containers/settings/EditUserScreen'; |
29 | import EditSettingsScreen from './containers/settings/EditSettingsScreen'; | 29 | import EditSettingsScreen from './containers/settings/EditSettingsScreen'; |
30 | import InviteSettingsScreen from './containers/settings/InviteScreen'; | ||
30 | import WelcomeScreen from './containers/auth/WelcomeScreen'; | 31 | import WelcomeScreen from './containers/auth/WelcomeScreen'; |
31 | import LoginScreen from './containers/auth/LoginScreen'; | 32 | import LoginScreen from './containers/auth/LoginScreen'; |
32 | import PasswordScreen from './containers/auth/PasswordScreen'; | 33 | import PasswordScreen from './containers/auth/PasswordScreen'; |
@@ -74,6 +75,7 @@ window.addEventListener('load', () => { | |||
74 | <Route path="/settings/user" component={AccountScreen} /> | 75 | <Route path="/settings/user" component={AccountScreen} /> |
75 | <Route path="/settings/user/edit" component={EditUserScreen} /> | 76 | <Route path="/settings/user/edit" component={EditUserScreen} /> |
76 | <Route path="/settings/app" component={EditSettingsScreen} /> | 77 | <Route path="/settings/app" component={EditSettingsScreen} /> |
78 | <Route path="/settings/invite" component={InviteSettingsScreen} /> | ||
77 | </Route> | 79 | </Route> |
78 | </Route> | 80 | </Route> |
79 | <Route path="/auth" component={AuthLayoutContainer}> | 81 | <Route path="/auth" component={AuthLayoutContainer}> |
diff --git a/src/components/auth/Invite.js b/src/components/auth/Invite.js index c6dca3a65..f1c16986b 100644 --- a/src/components/auth/Invite.js +++ b/src/components/auth/Invite.js | |||
@@ -3,13 +3,20 @@ import PropTypes from 'prop-types'; | |||
3 | import { observer } from 'mobx-react'; | 3 | import { observer } from 'mobx-react'; |
4 | import { defineMessages, intlShape } from 'react-intl'; | 4 | import { defineMessages, intlShape } from 'react-intl'; |
5 | import { Link } from 'react-router'; | 5 | import { Link } from 'react-router'; |
6 | import classnames from 'classnames'; | ||
6 | 7 | ||
8 | import Infobox from '../ui/Infobox'; | ||
9 | import Appear from '../ui/effects/Appear'; | ||
7 | import Form from '../../lib/Form'; | 10 | import Form from '../../lib/Form'; |
8 | import { email } from '../../helpers/validation-helpers'; | 11 | import { email } from '../../helpers/validation-helpers'; |
9 | import Input from '../ui/Input'; | 12 | import Input from '../ui/Input'; |
10 | import Button from '../ui/Button'; | 13 | import Button from '../ui/Button'; |
11 | 14 | ||
12 | const messages = defineMessages({ | 15 | const messages = defineMessages({ |
16 | settingsHeadline: { | ||
17 | id: 'settings.invite.headline', | ||
18 | defaultMessage: '!!!Invite Friends', | ||
19 | }, | ||
13 | headline: { | 20 | headline: { |
14 | id: 'invite.headline.friends', | 21 | id: 'invite.headline.friends', |
15 | defaultMessage: '!!!Invite 3 of your friends or colleagues', | 22 | defaultMessage: '!!!Invite 3 of your friends or colleagues', |
@@ -30,41 +37,77 @@ const messages = defineMessages({ | |||
30 | id: 'invite.skip.label', | 37 | id: 'invite.skip.label', |
31 | defaultMessage: '!!!I want to do this later', | 38 | defaultMessage: '!!!I want to do this later', |
32 | }, | 39 | }, |
40 | inviteSuccessInfo: { | ||
41 | id: 'invite.successInfo', | ||
42 | defaultMessage: '!!!Invitations sent successfully', | ||
43 | }, | ||
33 | }); | 44 | }); |
34 | 45 | ||
35 | @observer | 46 | @observer |
36 | export default class Invite extends Component { | 47 | export default class Invite extends Component { |
37 | static propTypes = { | 48 | static propTypes = { |
38 | onSubmit: PropTypes.func.isRequired, | 49 | onSubmit: PropTypes.func.isRequired, |
50 | embed: PropTypes.bool, | ||
51 | isInviteSuccessful: PropTypes.bool, | ||
52 | isLoadingInvite: PropTypes.bool, | ||
53 | }; | ||
54 | |||
55 | static defaultProps = { | ||
56 | embed: false, | ||
57 | isInviteSuccessful: false, | ||
58 | isLoadingInvite: false, | ||
39 | }; | 59 | }; |
40 | 60 | ||
41 | static contextTypes = { | 61 | static contextTypes = { |
42 | intl: intlShape, | 62 | intl: intlShape, |
43 | }; | 63 | }; |
44 | 64 | ||
45 | form = new Form({ | 65 | state = { showSuccessInfo: false }; |
46 | fields: { | 66 | |
47 | invite: [...Array(3).fill({ | 67 | componentWillMount() { |
48 | fields: { | 68 | const handlers = { |
49 | name: { | 69 | onChange: () => { |
50 | label: this.context.intl.formatMessage(messages.nameLabel), | 70 | this.setState({ showSuccessInfo: false }); |
51 | placeholder: this.context.intl.formatMessage(messages.nameLabel), | 71 | }, |
52 | }, | 72 | }; |
53 | email: { | 73 | |
54 | label: this.context.intl.formatMessage(messages.emailLabel), | 74 | this.form = new Form({ |
55 | placeholder: this.context.intl.formatMessage(messages.emailLabel), | 75 | fields: { |
56 | validators: [email], | 76 | invite: [...Array(3).fill({ |
77 | fields: { | ||
78 | name: { | ||
79 | label: this.context.intl.formatMessage(messages.nameLabel), | ||
80 | placeholder: this.context.intl.formatMessage(messages.nameLabel), | ||
81 | handlers, | ||
82 | // related: ['invite.0.email'], // path accepted but does not work | ||
83 | }, | ||
84 | email: { | ||
85 | label: this.context.intl.formatMessage(messages.emailLabel), | ||
86 | placeholder: this.context.intl.formatMessage(messages.emailLabel), | ||
87 | handlers, | ||
88 | validators: [email], | ||
89 | }, | ||
57 | }, | 90 | }, |
58 | }, | 91 | })], |
59 | })], | 92 | }, |
60 | }, | 93 | }, this.context.intl); |
61 | }, this.context.intl); | 94 | } |
95 | |||
96 | componentDidMount() { | ||
97 | document.querySelector('input:first-child').focus(); | ||
98 | } | ||
62 | 99 | ||
63 | submit(e) { | 100 | submit(e) { |
64 | e.preventDefault(); | 101 | e.preventDefault(); |
102 | |||
65 | this.form.submit({ | 103 | this.form.submit({ |
66 | onSuccess: (form) => { | 104 | onSuccess: (form) => { |
67 | this.props.onSubmit({ invites: form.values().invite }); | 105 | this.props.onSubmit({ invites: form.values().invite }); |
106 | |||
107 | this.form.clear(); | ||
108 | // this.form.$('invite.0.name').focus(); // path accepted but does not focus ;( | ||
109 | document.querySelector('input:first-child').focus(); | ||
110 | this.setState({ showSuccessInfo: true }); | ||
68 | }, | 111 | }, |
69 | onError: () => {}, | 112 | onError: () => {}, |
70 | }); | 113 | }); |
@@ -73,20 +116,38 @@ export default class Invite extends Component { | |||
73 | render() { | 116 | render() { |
74 | const { form } = this; | 117 | const { form } = this; |
75 | const { intl } = this.context; | 118 | const { intl } = this.context; |
119 | const { embed, isInviteSuccessful, isLoadingInvite } = this.props; | ||
76 | 120 | ||
77 | const atLeastOneEmailAddress = form.$('invite') | 121 | const atLeastOneEmailAddress = form.$('invite') |
78 | .map(invite => invite.$('email').value) | 122 | .map(invite => invite.$('email').value) |
79 | .some(emailValue => emailValue.trim() !== ''); | 123 | .some(emailValue => emailValue.trim() !== ''); |
80 | 124 | ||
81 | return ( | 125 | const sendButtonClassName = classnames({ |
82 | <div className="auth__container auth__container--signup"> | 126 | auth__button: true, |
127 | 'invite__embed--button': embed, | ||
128 | }); | ||
129 | |||
130 | const renderForm = ( | ||
131 | <div> | ||
132 | {this.state.showSuccessInfo && isInviteSuccessful && ( | ||
133 | <Appear> | ||
134 | <Infobox | ||
135 | type="success" | ||
136 | icon="checkbox-marked-circle-outline" | ||
137 | dismissable | ||
138 | > | ||
139 | {intl.formatMessage(messages.inviteSuccessInfo)} | ||
140 | </Infobox> | ||
141 | </Appear> | ||
142 | )} | ||
143 | |||
83 | <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> | 144 | <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> |
84 | <img | 145 | {!embed && (<img |
85 | src="./assets/images/logo.svg" | 146 | src="./assets/images/logo.svg" |
86 | className="auth__logo" | 147 | className="auth__logo" |
87 | alt="" | 148 | alt="" |
88 | /> | 149 | />)} |
89 | <h1> | 150 | <h1 className={embed && 'invite__embed'}> |
90 | {intl.formatMessage(messages.headline)} | 151 | {intl.formatMessage(messages.headline)} |
91 | </h1> | 152 | </h1> |
92 | {form.$('invite').map(invite => ( | 153 | {form.$('invite').map(invite => ( |
@@ -99,18 +160,30 @@ export default class Invite extends Component { | |||
99 | ))} | 160 | ))} |
100 | <Button | 161 | <Button |
101 | type="submit" | 162 | type="submit" |
102 | className="auth__button" | 163 | className={sendButtonClassName} |
103 | disabled={!atLeastOneEmailAddress} | 164 | disabled={!atLeastOneEmailAddress} |
104 | label={intl.formatMessage(messages.submitButtonLabel)} | 165 | label={intl.formatMessage(messages.submitButtonLabel)} |
166 | loaded={!isLoadingInvite} | ||
105 | /> | 167 | /> |
106 | <Link | 168 | {!embed && (<Link |
107 | to="/" | 169 | to="/" |
108 | className="franz-form__button franz-form__button--secondary auth__button auth__button--skip" | 170 | className="franz-form__button franz-form__button--secondary auth__button auth__button--skip" |
109 | > | 171 | > |
110 | {intl.formatMessage(messages.skipButtonLabel)} | 172 | {intl.formatMessage(messages.skipButtonLabel)} |
111 | </Link> | 173 | </Link>)} |
112 | </form> | 174 | </form> |
113 | </div> | 175 | </div> |
114 | ); | 176 | ); |
177 | |||
178 | return ( | ||
179 | <div className={!embed ? 'auth__container auth__container--signup' : 'settings__main'}> | ||
180 | {embed && ( | ||
181 | <div className="settings__header"> | ||
182 | <h1>{this.context.intl.formatMessage(messages.settingsHeadline)}</h1> | ||
183 | </div> | ||
184 | )} | ||
185 | {!embed ? <div>{renderForm}</div> : <div className="settings__body invite__form">{renderForm}</div>} | ||
186 | </div> | ||
187 | ); | ||
115 | } | 188 | } |
116 | } | 189 | } |
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js index fea8d682d..66539f324 100644 --- a/src/components/settings/navigation/SettingsNavigation.js +++ b/src/components/settings/navigation/SettingsNavigation.js | |||
@@ -21,6 +21,10 @@ const messages = defineMessages({ | |||
21 | id: 'settings.navigation.settings', | 21 | id: 'settings.navigation.settings', |
22 | defaultMessage: '!!!Settings', | 22 | defaultMessage: '!!!Settings', |
23 | }, | 23 | }, |
24 | inviteFriends: { | ||
25 | id: 'settings.navigation.inviteFriends', | ||
26 | defaultMessage: '!!!Invite Friends', | ||
27 | }, | ||
24 | logout: { | 28 | logout: { |
25 | id: 'settings.navigation.logout', | 29 | id: 'settings.navigation.logout', |
26 | defaultMessage: '!!!Logout', | 30 | defaultMessage: '!!!Logout', |
@@ -70,6 +74,13 @@ export default class SettingsNavigation extends Component { | |||
70 | > | 74 | > |
71 | {intl.formatMessage(messages.settings)} | 75 | {intl.formatMessage(messages.settings)} |
72 | </Link> | 76 | </Link> |
77 | <Link | ||
78 | to="/settings/invite" | ||
79 | className="settings-navigation__link" | ||
80 | activeClassName="is-active" | ||
81 | > | ||
82 | {intl.formatMessage(messages.inviteFriends)} | ||
83 | </Link> | ||
73 | <span className="settings-navigation__expander" /> | 84 | <span className="settings-navigation__expander" /> |
74 | <Link | 85 | <Link |
75 | to="/auth/logout" | 86 | to="/auth/logout" |
diff --git a/src/components/settings/services/ServiceError.js b/src/components/settings/services/ServiceError.js index 923053296..1f1512927 100644 --- a/src/components/settings/services/ServiceError.js +++ b/src/components/settings/services/ServiceError.js | |||
@@ -26,7 +26,7 @@ const messages = defineMessages({ | |||
26 | }); | 26 | }); |
27 | 27 | ||
28 | @observer | 28 | @observer |
29 | export default class EditServiceForm extends Component { | 29 | export default class ServiceError extends Component { |
30 | static contextTypes = { | 30 | static contextTypes = { |
31 | intl: intlShape, | 31 | intl: intlShape, |
32 | }; | 32 | }; |
diff --git a/src/containers/auth/InviteScreen.js b/src/containers/auth/InviteScreen.js index 51971f436..059888c99 100644 --- a/src/containers/auth/InviteScreen.js +++ b/src/containers/auth/InviteScreen.js | |||
@@ -12,9 +12,11 @@ export default class InviteScreen extends Component { | |||
12 | 12 | ||
13 | render() { | 13 | render() { |
14 | const { actions } = this.props; | 14 | const { actions } = this.props; |
15 | |||
15 | return ( | 16 | return ( |
16 | <Invite | 17 | <Invite |
17 | onSubmit={actions.user.invite} | 18 | onSubmit={actions.user.invite} |
19 | embed={false} | ||
18 | /> | 20 | /> |
19 | ); | 21 | ); |
20 | } | 22 | } |
diff --git a/src/containers/settings/InviteScreen.js b/src/containers/settings/InviteScreen.js new file mode 100644 index 000000000..5f341b1b3 --- /dev/null +++ b/src/containers/settings/InviteScreen.js | |||
@@ -0,0 +1,44 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { inject, observer } from 'mobx-react'; | ||
4 | |||
5 | import Invite from '../../components/auth/Invite'; | ||
6 | import { gaPage } from '../../lib/analytics'; | ||
7 | |||
8 | @inject('stores', 'actions') @observer | ||
9 | export default class InviteScreen extends Component { | ||
10 | componentDidMount() { | ||
11 | gaPage('Settings/Invite'); | ||
12 | } | ||
13 | |||
14 | componentWillUnmount() { | ||
15 | this.props.stores.user.inviteRequest.reset(); | ||
16 | } | ||
17 | |||
18 | render() { | ||
19 | const { actions } = this.props; | ||
20 | const { user } = this.props.stores; | ||
21 | |||
22 | return ( | ||
23 | <Invite | ||
24 | onSubmit={actions.user.invite} | ||
25 | isLoadingInvite={user.inviteRequest.isExecuting} | ||
26 | isInviteSuccessful={user.inviteRequest.wasExecuted && !user.inviteRequest.isError} | ||
27 | embed | ||
28 | /> | ||
29 | ); | ||
30 | } | ||
31 | } | ||
32 | |||
33 | InviteScreen.wrappedComponent.propTypes = { | ||
34 | actions: PropTypes.shape({ | ||
35 | user: PropTypes.shape({ | ||
36 | invite: PropTypes.func.isRequired, | ||
37 | }).isRequired, | ||
38 | }).isRequired, | ||
39 | stores: PropTypes.shape({ | ||
40 | user: PropTypes.shape({ | ||
41 | inviteRequest: PropTypes.object, | ||
42 | }).isRequired, | ||
43 | }).isRequired, | ||
44 | }; | ||
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 63d7ce60b..3c09fca3b 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json | |||
@@ -45,6 +45,7 @@ | |||
45 | "invite.name.label": "Name", | 45 | "invite.name.label": "Name", |
46 | "invite.email.label": "Email address", | 46 | "invite.email.label": "Email address", |
47 | "invite.skip.label": "I want to do this later", | 47 | "invite.skip.label": "I want to do this later", |
48 | "invite.successInfo": "Invitations sent successfully", | ||
48 | "subscription.submit.label": "I want to support the development of Franz", | 49 | "subscription.submit.label": "I want to support the development of Franz", |
49 | "subscription.paymentSessionError": "Could not initialize payment form", | 50 | "subscription.paymentSessionError": "Could not initialize payment form", |
50 | "subscription.includedFeatures": "Paid Franz Premium Supporter Account includes", | 51 | "subscription.includedFeatures": "Paid Franz Premium Supporter Account includes", |
@@ -87,10 +88,12 @@ | |||
87 | "settings.account.deleteAccount": "Delete account", | 88 | "settings.account.deleteAccount": "Delete account", |
88 | "settings.account.deleteInfo": "If you don't need your Franz account any longer, you can delete your account and all related data here.", | 89 | "settings.account.deleteInfo": "If you don't need your Franz account any longer, you can delete your account and all related data here.", |
89 | "settings.account.deleteEmailSent": "You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!", | 90 | "settings.account.deleteEmailSent": "You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!", |
91 | "settings.invite.headline": "Invite Friends", | ||
90 | "settings.navigation.availableServices": "Available services", | 92 | "settings.navigation.availableServices": "Available services", |
91 | "settings.navigation.yourServices": "Your services", | 93 | "settings.navigation.yourServices": "Your services", |
92 | "settings.navigation.account": "Account", | 94 | "settings.navigation.account": "Account", |
93 | "settings.navigation.settings": "Settings", | 95 | "settings.navigation.settings": "Settings", |
96 | "settings.navigation.inviteFriends": "Invite Friends", | ||
94 | "settings.navigation.logout": "Logout", | 97 | "settings.navigation.logout": "Logout", |
95 | "settings.recipes.headline": "Available services", | 98 | "settings.recipes.headline": "Available services", |
96 | "settings.recipes.mostPopular": "Most popular", | 99 | "settings.recipes.mostPopular": "Most popular", |
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js index 427b6454b..abec4df5d 100644 --- a/src/stores/UserStore.js +++ b/src/stores/UserStore.js | |||
@@ -161,13 +161,17 @@ export default class UserStore extends Store { | |||
161 | gaEvent('User', 'retrievePassword'); | 161 | gaEvent('User', 'retrievePassword'); |
162 | } | 162 | } |
163 | 163 | ||
164 | @action _invite({ invites }) { | 164 | @action async _invite({ invites }) { |
165 | const data = invites.filter(invite => invite.email !== ''); | 165 | const data = invites.filter(invite => invite.email !== ''); |
166 | 166 | ||
167 | this.inviteRequest.execute(data); | 167 | const response = await this.inviteRequest.execute(data)._promise; |
168 | 168 | ||
169 | // we do not wait for a server response before redirecting the user | 169 | this.actionStatus = response.status || []; |
170 | this.stores.router.push('/'); | 170 | |
171 | // we do not wait for a server response before redirecting the user ONLY DURING SIGNUP | ||
172 | if (this.stores.router.location.pathname.includes(this.INVITE_ROUTE)) { | ||
173 | this.stores.router.push('/'); | ||
174 | } | ||
171 | 175 | ||
172 | gaEvent('User', 'inviteUsers'); | 176 | gaEvent('User', 'inviteUsers'); |
173 | } | 177 | } |
diff --git a/src/styles/invite.scss b/src/styles/invite.scss new file mode 100644 index 000000000..bfb1a4b6b --- /dev/null +++ b/src/styles/invite.scss | |||
@@ -0,0 +1,15 @@ | |||
1 | .invite__form { | ||
2 | /* play with values to see different layouts */ | ||
3 | // display: flex; | ||
4 | align-items: center; | ||
5 | align-self: center; | ||
6 | justify-content: center; | ||
7 | } | ||
8 | |||
9 | .invite__embed { | ||
10 | text-align: center; | ||
11 | } | ||
12 | |||
13 | .invite__embed--button { | ||
14 | width: 100%; | ||
15 | } \ No newline at end of file | ||
diff --git a/src/styles/main.scss b/src/styles/main.scss index 261396f6f..446bdca14 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss | |||
@@ -27,6 +27,7 @@ $mdi-font-path: '../node_modules/mdi/fonts'; | |||
27 | @import './subscription.scss'; | 27 | @import './subscription.scss'; |
28 | @import './subscription-popup.scss'; | 28 | @import './subscription-popup.scss'; |
29 | @import './content-tabs.scss'; | 29 | @import './content-tabs.scss'; |
30 | @import './invite.scss'; | ||
30 | 31 | ||
31 | // form | 32 | // form |
32 | @import './input.scss'; | 33 | @import './input.scss'; |