diff options
Diffstat (limited to 'src/components/auth/Invite.tsx')
-rw-r--r-- | src/components/auth/Invite.tsx | 213 |
1 files changed, 213 insertions, 0 deletions
diff --git a/src/components/auth/Invite.tsx b/src/components/auth/Invite.tsx new file mode 100644 index 000000000..7723ea1ac --- /dev/null +++ b/src/components/auth/Invite.tsx | |||
@@ -0,0 +1,213 @@ | |||
1 | import { Component } from 'react'; | ||
2 | import { observer } from 'mobx-react'; | ||
3 | import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; | ||
4 | import { Link } from 'react-router-dom'; | ||
5 | import classnames from 'classnames'; | ||
6 | import { noop } from 'lodash'; | ||
7 | import Infobox from '../ui/Infobox'; | ||
8 | import Appear from '../ui/effects/Appear'; | ||
9 | import Form from '../../lib/Form'; | ||
10 | import { email, required } from '../../helpers/validation-helpers'; | ||
11 | import Input from '../ui/input/index'; | ||
12 | import Button from '../ui/button'; | ||
13 | import { H1 } from '../ui/headline'; | ||
14 | |||
15 | const messages = defineMessages({ | ||
16 | settingsHeadline: { | ||
17 | id: 'settings.invite.headline', | ||
18 | defaultMessage: 'Invite Friends', | ||
19 | }, | ||
20 | headline: { | ||
21 | id: 'invite.headline.friends', | ||
22 | defaultMessage: 'Invite 3 of your friends or colleagues', | ||
23 | }, | ||
24 | nameLabel: { | ||
25 | id: 'invite.name.label', | ||
26 | defaultMessage: 'Name', | ||
27 | }, | ||
28 | emailLabel: { | ||
29 | id: 'invite.email.label', | ||
30 | defaultMessage: 'Email address', | ||
31 | }, | ||
32 | submitButtonLabel: { | ||
33 | id: 'invite.submit.label', | ||
34 | defaultMessage: 'Send invites', | ||
35 | }, | ||
36 | skipButtonLabel: { | ||
37 | id: 'invite.skip.label', | ||
38 | defaultMessage: 'I want to do this later', | ||
39 | }, | ||
40 | inviteSuccessInfo: { | ||
41 | id: 'invite.successInfo', | ||
42 | defaultMessage: 'Invitations sent successfully', | ||
43 | }, | ||
44 | }); | ||
45 | |||
46 | interface IProps extends WrappedComponentProps { | ||
47 | onSubmit: (...args: any[]) => void; | ||
48 | embed?: boolean; | ||
49 | isInviteSuccessful?: boolean; | ||
50 | isLoadingInvite?: boolean; | ||
51 | } | ||
52 | |||
53 | interface IState { | ||
54 | showSuccessInfo: boolean; | ||
55 | } | ||
56 | |||
57 | @observer | ||
58 | class Invite extends Component<IProps, IState> { | ||
59 | form: Form; | ||
60 | |||
61 | constructor(props: IProps) { | ||
62 | super(props); | ||
63 | |||
64 | this.state = { showSuccessInfo: false }; | ||
65 | this.form = new Form({ | ||
66 | fields: { | ||
67 | invite: [ | ||
68 | ...Array.from({ length: 3 }).fill({ | ||
69 | fields: { | ||
70 | name: { | ||
71 | label: this.props.intl.formatMessage(messages.nameLabel), | ||
72 | placeholder: this.props.intl.formatMessage(messages.nameLabel), | ||
73 | onChange: () => { | ||
74 | this.setState({ showSuccessInfo: false }); | ||
75 | }, | ||
76 | validators: [required], | ||
77 | // related: ['invite.0.email'], // path accepted but does not work | ||
78 | }, | ||
79 | email: { | ||
80 | label: this.props.intl.formatMessage(messages.emailLabel), | ||
81 | placeholder: this.props.intl.formatMessage(messages.emailLabel), | ||
82 | onChange: () => { | ||
83 | this.setState({ showSuccessInfo: false }); | ||
84 | }, | ||
85 | validators: [email], | ||
86 | }, | ||
87 | }, | ||
88 | }), | ||
89 | // TODO - [TS DEBT] need to fix this type once mobx-react-form is updated to next version | ||
90 | ] as any, | ||
91 | }, | ||
92 | }); | ||
93 | } | ||
94 | |||
95 | componentDidMount() { | ||
96 | const selector: HTMLElement | null = | ||
97 | document.querySelector('input:first-child'); | ||
98 | if (selector) { | ||
99 | selector.focus(); | ||
100 | } | ||
101 | } | ||
102 | |||
103 | submit(e) { | ||
104 | e.preventDefault(); | ||
105 | |||
106 | this.form?.submit({ | ||
107 | onSuccess: form => { | ||
108 | this.props.onSubmit({ invites: form.values().invite }); | ||
109 | this.form?.clear(); | ||
110 | // this.form.$('invite.0.name').focus(); // path accepted but does not focus ;( | ||
111 | |||
112 | const selector: HTMLElement | null = | ||
113 | document.querySelector('input:first-child'); | ||
114 | if (selector) { | ||
115 | selector.focus(); | ||
116 | } | ||
117 | |||
118 | this.setState({ showSuccessInfo: true }); | ||
119 | }, | ||
120 | onError: () => {}, | ||
121 | }); | ||
122 | } | ||
123 | |||
124 | render() { | ||
125 | const { form } = this; | ||
126 | const { intl } = this.props; | ||
127 | const { | ||
128 | embed = false, | ||
129 | isInviteSuccessful = false, | ||
130 | isLoadingInvite = false, | ||
131 | } = this.props; | ||
132 | |||
133 | const atLeastOneEmailAddress = form | ||
134 | .$('invite') | ||
135 | .map(invite => invite.$('email').value) | ||
136 | .some(emailValue => emailValue.trim() !== ''); | ||
137 | |||
138 | const sendButtonClassName = classnames({ | ||
139 | auth__button: true, | ||
140 | 'invite__embed--button': embed, | ||
141 | }); | ||
142 | |||
143 | const renderForm = ( | ||
144 | <> | ||
145 | {this.state.showSuccessInfo && isInviteSuccessful && ( | ||
146 | <Appear> | ||
147 | <Infobox | ||
148 | type="success" | ||
149 | icon="checkbox-marked-circle-outline" | ||
150 | dismissible | ||
151 | > | ||
152 | {intl.formatMessage(messages.inviteSuccessInfo)} | ||
153 | </Infobox> | ||
154 | </Appear> | ||
155 | )} | ||
156 | |||
157 | <form className="franz-form auth__form" onSubmit={e => this.submit(e)}> | ||
158 | {!embed && ( | ||
159 | <img src="./assets/images/logo.svg" className="auth__logo" alt="" /> | ||
160 | )} | ||
161 | <H1 className={embed ? 'invite__embed' : ''}> | ||
162 | {intl.formatMessage(messages.headline)} | ||
163 | </H1> | ||
164 | {form.$('invite').map(invite => ( | ||
165 | <div className="grid" key={invite.key}> | ||
166 | <div className="grid__row"> | ||
167 | <Input {...invite.$('name').bind()} showLabel={false} /> | ||
168 | <Input {...invite.$('email').bind()} showLabel={false} /> | ||
169 | </div> | ||
170 | </div> | ||
171 | ))} | ||
172 | <Button | ||
173 | type="submit" | ||
174 | className={sendButtonClassName} | ||
175 | disabled={!atLeastOneEmailAddress} | ||
176 | label={intl.formatMessage(messages.submitButtonLabel)} | ||
177 | loaded={!isLoadingInvite} | ||
178 | onClick={noop} | ||
179 | /> | ||
180 | {!embed && ( | ||
181 | <Link | ||
182 | to="/" | ||
183 | className="franz-form__button franz-form__button--secondary auth__button auth__button--skip" | ||
184 | > | ||
185 | {intl.formatMessage(messages.skipButtonLabel)} | ||
186 | </Link> | ||
187 | )} | ||
188 | </form> | ||
189 | </> | ||
190 | ); | ||
191 | |||
192 | return ( | ||
193 | <div | ||
194 | className={ | ||
195 | !embed ? 'auth__container auth__container--signup' : 'settings__main' | ||
196 | } | ||
197 | > | ||
198 | {embed && ( | ||
199 | <div className="settings__header"> | ||
200 | <H1>{intl.formatMessage(messages.settingsHeadline)}</H1> | ||
201 | </div> | ||
202 | )} | ||
203 | {!embed ? ( | ||
204 | <div>{renderForm}</div> | ||
205 | ) : ( | ||
206 | <div className="settings__body invite__form">{renderForm}</div> | ||
207 | )} | ||
208 | </div> | ||
209 | ); | ||
210 | } | ||
211 | } | ||
212 | |||
213 | export default injectIntl(Invite); | ||