aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/ui')
-rw-r--r--src/components/ui/AppLoader.js15
-rw-r--r--src/components/ui/Button.js78
-rw-r--r--src/components/ui/InfoBar.js88
-rw-r--r--src/components/ui/Infobox.js87
-rw-r--r--src/components/ui/Input.js148
-rw-r--r--src/components/ui/Link.js78
-rw-r--r--src/components/ui/Loader.js41
-rw-r--r--src/components/ui/Radio.js89
-rw-r--r--src/components/ui/SearchInput.js124
-rw-r--r--src/components/ui/Select.js70
-rw-r--r--src/components/ui/Subscription.js265
-rw-r--r--src/components/ui/SubscriptionPopup.js84
-rw-r--r--src/components/ui/Tabs/TabItem.js17
-rw-r--r--src/components/ui/Tabs/Tabs.js69
-rw-r--r--src/components/ui/Tabs/index.js6
-rw-r--r--src/components/ui/Toggle.js67
-rw-r--r--src/components/ui/effects/Appear.js51
17 files changed, 1377 insertions, 0 deletions
diff --git a/src/components/ui/AppLoader.js b/src/components/ui/AppLoader.js
new file mode 100644
index 000000000..64a212969
--- /dev/null
+++ b/src/components/ui/AppLoader.js
@@ -0,0 +1,15 @@
1import React from 'react';
2
3import Appear from '../../components/ui/effects/Appear';
4import Loader from '../../components/ui/Loader';
5
6export default function () {
7 return (
8 <div className="app-loader">
9 <Appear>
10 <h1 className="app-loader__title">Franz</h1>
11 <Loader />
12 </Appear>
13 </div>
14 );
15}
diff --git a/src/components/ui/Button.js b/src/components/ui/Button.js
new file mode 100644
index 000000000..07e94192f
--- /dev/null
+++ b/src/components/ui/Button.js
@@ -0,0 +1,78 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import Loader from 'react-loader';
5import classnames from 'classnames';
6
7@observer
8export default class Button extends Component {
9 static propTypes = {
10 className: PropTypes.string,
11 label: PropTypes.string.isRequired,
12 disabled: PropTypes.bool,
13 onClick: PropTypes.func,
14 type: PropTypes.string,
15 buttonType: PropTypes.string,
16 loaded: PropTypes.bool,
17 htmlForm: PropTypes.string,
18 };
19
20 static defaultProps = {
21 className: null,
22 disabled: false,
23 onClick: () => {},
24 type: 'button',
25 buttonType: '',
26 loaded: true,
27 htmlForm: '',
28 };
29
30 element = null;
31
32 render() {
33 const {
34 label,
35 className,
36 disabled,
37 onClick,
38 type,
39 buttonType,
40 loaded,
41 htmlForm,
42 } = this.props;
43
44 const buttonProps = {
45 className: classnames({
46 'franz-form__button': true,
47 [`franz-form__button--${buttonType}`]: buttonType,
48 [`${className}`]: className,
49 }),
50 type,
51 };
52
53 if (disabled) {
54 buttonProps.disabled = true;
55 }
56
57 if (onClick) {
58 buttonProps.onClick = onClick;
59 }
60
61 if (htmlForm) {
62 buttonProps.form = htmlForm;
63 }
64
65 return (
66 <button {...buttonProps}>
67 <Loader
68 loaded={loaded}
69 lines={10}
70 scale={0.4}
71 color={buttonType === '' ? '#FFF' : '#373a3c'}
72 component="span"
73 />
74 {label}
75 </button>
76 );
77 }
78}
diff --git a/src/components/ui/InfoBar.js b/src/components/ui/InfoBar.js
new file mode 100644
index 000000000..aea2bd888
--- /dev/null
+++ b/src/components/ui/InfoBar.js
@@ -0,0 +1,88 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import classnames from 'classnames';
5import Loader from 'react-loader';
6
7// import { oneOrManyChildElements } from '../../prop-types';
8import Appear from '../ui/effects/Appear';
9
10@observer
11export default class InfoBar extends Component {
12 static propTypes = {
13 // eslint-disable-next-line
14 children: PropTypes.any.isRequired,
15 onClick: PropTypes.func,
16 type: PropTypes.string,
17 className: PropTypes.string,
18 ctaLabel: PropTypes.string,
19 ctaLoading: PropTypes.bool,
20 position: PropTypes.string,
21 sticky: PropTypes.bool,
22 onHide: PropTypes.func,
23 };
24
25 static defaultProps = {
26 onClick: () => null,
27 type: 'primary',
28 className: '',
29 ctaLabel: '',
30 ctaLoading: false,
31 position: 'bottom',
32 sticky: false,
33 onHide: () => null,
34 };
35
36 render() {
37 const {
38 children,
39 type,
40 className,
41 ctaLabel,
42 ctaLoading,
43 onClick,
44 position,
45 sticky,
46 onHide,
47 } = this.props;
48
49 let transitionName = 'slideUp';
50 if (position === 'top') {
51 transitionName = 'slideDown';
52 }
53
54 return (
55 <Appear
56 transitionName={transitionName}
57 className={classnames({
58 'info-bar': true,
59 [`info-bar--${type}`]: true,
60 [`info-bar--${position}`]: true,
61 [`${className}`]: true,
62 })}
63 >
64 <div onClick={onClick} className="info-bar__content">
65 {children}
66 {ctaLabel && (
67 <button className="info-bar__cta">
68 <Loader
69 loaded={!ctaLoading}
70 lines={10}
71 scale={0.3}
72 color="#FFF"
73 component="span"
74 />
75 {ctaLabel}
76 </button>
77 )}
78 </div>
79 {!sticky && (
80 <button
81 className="info-bar__close mdi mdi-close"
82 onClick={onHide}
83 />
84 )}
85 </Appear>
86 );
87 }
88}
diff --git a/src/components/ui/Infobox.js b/src/components/ui/Infobox.js
new file mode 100644
index 000000000..2d063c7ef
--- /dev/null
+++ b/src/components/ui/Infobox.js
@@ -0,0 +1,87 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import classnames from 'classnames';
5import Loader from 'react-loader';
6
7@observer
8export default class Infobox extends Component {
9 static propTypes = {
10 children: PropTypes.any.isRequired, // eslint-disable-line
11 icon: PropTypes.string,
12 type: PropTypes.string,
13 ctaOnClick: PropTypes.func,
14 ctaLabel: PropTypes.string,
15 ctaLoading: PropTypes.bool,
16 dismissable: PropTypes.bool,
17 };
18
19 static defaultProps = {
20 icon: '',
21 type: 'primary',
22 dismissable: false,
23 ctaOnClick: () => null,
24 ctaLabel: '',
25 ctaLoading: false,
26 };
27
28 state = {
29 dismissed: false,
30 };
31
32 render() {
33 const {
34 children,
35 icon,
36 type,
37 ctaLabel,
38 ctaLoading,
39 ctaOnClick,
40 dismissable,
41 } = this.props;
42
43 if (this.state.dismissed) {
44 return null;
45 }
46
47 return (
48 <div
49 className={classnames({
50 infobox: true,
51 [`infobox--${type}`]: type,
52 'infobox--default': !type,
53 })}
54 >
55 {icon && (
56 <i className={`mdi mdi-${icon}`} />
57 )}
58 <div className="infobox__content">
59 {children}
60 </div>
61 {ctaLabel && (
62 <button
63 className="infobox__cta"
64 onClick={ctaOnClick}
65 >
66 <Loader
67 loaded={!ctaLoading}
68 lines={10}
69 scale={0.3}
70 color="#FFF"
71 component="span"
72 />
73 {ctaLabel}
74 </button>
75 )}
76 {dismissable && (
77 <button
78 onClick={() => this.setState({
79 dismissed: true,
80 })}
81 className="infobox__delete mdi mdi-close"
82 />
83 )}
84 </div>
85 );
86 }
87}
diff --git a/src/components/ui/Input.js b/src/components/ui/Input.js
new file mode 100644
index 000000000..0bb9f23bf
--- /dev/null
+++ b/src/components/ui/Input.js
@@ -0,0 +1,148 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { Field } from 'mobx-react-form';
5import classnames from 'classnames';
6
7import { scorePassword as scorePasswordFunc } from '../../helpers/password-helpers';
8
9@observer
10export default class Input extends Component {
11 static propTypes = {
12 field: PropTypes.instanceOf(Field).isRequired,
13 className: PropTypes.string,
14 focus: PropTypes.bool,
15 showPasswordToggle: PropTypes.bool,
16 showLabel: PropTypes.bool,
17 scorePassword: PropTypes.bool,
18 prefix: PropTypes.string,
19 suffix: PropTypes.string,
20 };
21
22 static defaultProps = {
23 className: null,
24 focus: false,
25 showPasswordToggle: false,
26 showLabel: true,
27 scorePassword: false,
28 prefix: '',
29 suffix: '',
30 };
31
32 state = {
33 showPassword: false,
34 passwordScore: 0,
35 }
36
37 componentDidMount() {
38 if (this.props.focus) {
39 this.focus();
40 }
41 }
42
43 onChange(e) {
44 const { field, scorePassword } = this.props;
45
46 field.onChange(e);
47
48 if (scorePassword) {
49 this.setState({ passwordScore: scorePasswordFunc(field.value) });
50 }
51 }
52
53 focus() {
54 this.inputElement.focus();
55 }
56
57 inputElement = null;
58
59 render() {
60 const {
61 field,
62 className,
63 showPasswordToggle,
64 showLabel,
65 scorePassword,
66 prefix,
67 suffix,
68 } = this.props;
69
70 const { passwordScore } = this.state;
71
72 let type = field.type;
73 if (type === 'password' && this.state.showPassword) {
74 type = 'text';
75 }
76
77 return (
78 <div
79 className={classnames({
80 'franz-form__field': true,
81 'has-error': field.error,
82 [`${className}`]: className,
83 })}
84 >
85 <div className="franz-form__input-wrapper">
86 {prefix && (
87 <span className="franz-form__input-prefix">{prefix}</span>
88 )}
89 <input
90 id={field.id}
91 type={type}
92 className="franz-form__input"
93 name={field.name}
94 value={field.value}
95 placeholder={field.placeholder}
96 onChange={e => this.onChange(e)}
97 onBlur={field.onBlur}
98 onFocus={field.onFocus}
99 ref={(element) => { this.inputElement = element; }}
100 />
101 {suffix && (
102 <span className="franz-form__input-suffix">{suffix}</span>
103 )}
104 {showPasswordToggle && (
105 <button
106 type="button"
107 className={classnames({
108 'franz-form__input-modifier': true,
109 mdi: true,
110 'mdi-eye': !this.state.showPassword,
111 'mdi-eye-off': this.state.showPassword,
112 })}
113 onClick={() => this.setState({ showPassword: !this.state.showPassword })}
114 tabIndex="-1"
115 />
116 )}
117 {scorePassword && (
118 <div className="franz-form__password-score">
119 {/* <progress value={this.state.passwordScore} max="100" /> */}
120 <meter
121 value={passwordScore < 5 ? 5 : passwordScore}
122 low="30"
123 high="75"
124 optimum="100"
125 max="100"
126 />
127 </div>
128 )}
129 </div>
130 {field.label && showLabel && (
131 <label
132 className="franz-form__label"
133 htmlFor={field.name}
134 >
135 {field.label}
136 </label>
137 )}
138 {field.error && (
139 <div
140 className="franz-form__error"
141 >
142 {field.error}
143 </div>
144 )}
145 </div>
146 );
147 }
148}
diff --git a/src/components/ui/Link.js b/src/components/ui/Link.js
new file mode 100644
index 000000000..f5da921fa
--- /dev/null
+++ b/src/components/ui/Link.js
@@ -0,0 +1,78 @@
1import { shell } from 'electron';
2import React, { Component } from 'react';
3import PropTypes from 'prop-types';
4import { inject, observer } from 'mobx-react';
5import { RouterStore } from 'mobx-react-router';
6import classnames from 'classnames';
7
8import { oneOrManyChildElements } from '../../prop-types';
9import { matchRoute } from '../../helpers/routing-helpers';
10
11// TODO: create container component for this component
12
13@inject('stores') @observer
14export default class Link extends Component {
15 onClick(e) {
16 if (this.props.target === '_blank') {
17 e.preventDefault();
18 shell.openExternal(this.props.to);
19 }
20 }
21
22 render() {
23 const {
24 children,
25 stores,
26 to,
27 className,
28 activeClassName,
29 strictFilter,
30 } = this.props;
31 const { router } = stores;
32
33 let filter = `${to}(*action)`;
34 if (strictFilter) {
35 filter = `${to}`;
36 }
37
38 const match = matchRoute(filter, router.location.pathname);
39
40 const linkClasses = classnames({
41 [`${className}`]: true,
42 [`${activeClassName}`]: match,
43 });
44
45 return (
46 <a
47 href={router.history.createHref(to)}
48 className={linkClasses}
49 onClick={e => this.onClick(e)}
50 >
51 {children}
52 </a>
53 );
54 }
55}
56
57Link.wrappedComponent.propTypes = {
58 stores: PropTypes.shape({
59 router: PropTypes.instanceOf(RouterStore).isRequired,
60 }).isRequired,
61 children: PropTypes.oneOfType([
62 oneOrManyChildElements,
63 PropTypes.string,
64 ]).isRequired,
65 to: PropTypes.string.isRequired,
66 className: PropTypes.string,
67 activeClassName: PropTypes.string,
68 strictFilter: PropTypes.bool,
69 target: PropTypes.string,
70};
71
72Link.wrappedComponent.defaultProps = {
73 className: '',
74 activeClassName: '',
75 strictFilter: false,
76 target: '',
77 openInBrowser: false,
78};
diff --git a/src/components/ui/Loader.js b/src/components/ui/Loader.js
new file mode 100644
index 000000000..e4fbd96a2
--- /dev/null
+++ b/src/components/ui/Loader.js
@@ -0,0 +1,41 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import Loader from 'react-loader';
4
5import { oneOrManyChildElements } from '../../prop-types';
6
7export default class LoaderComponent extends Component {
8 static propTypes = {
9 children: oneOrManyChildElements,
10 loaded: PropTypes.bool,
11 className: PropTypes.string,
12 };
13
14 static defaultProps = {
15 children: null,
16 loaded: false,
17 className: '',
18 };
19
20 render() {
21 const {
22 children,
23 loaded,
24 className,
25 } = this.props;
26
27 return (
28 <Loader
29 loaded={loaded}
30 // lines={10}
31 width={4}
32 scale={0.6}
33 color="#373a3c"
34 component="span"
35 className={className}
36 >
37 {children}
38 </Loader>
39 );
40 }
41}
diff --git a/src/components/ui/Radio.js b/src/components/ui/Radio.js
new file mode 100644
index 000000000..b54cfc820
--- /dev/null
+++ b/src/components/ui/Radio.js
@@ -0,0 +1,89 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { Field } from 'mobx-react-form';
5import classnames from 'classnames';
6
7@observer
8export default class Radio extends Component {
9 static propTypes = {
10 field: PropTypes.instanceOf(Field).isRequired,
11 className: PropTypes.string,
12 focus: PropTypes.bool,
13 showLabel: PropTypes.bool,
14 };
15
16 static defaultProps = {
17 className: null,
18 focus: false,
19 showLabel: true,
20 };
21
22 componentDidMount() {
23 if (this.props.focus) {
24 this.focus();
25 }
26 }
27
28 focus() {
29 this.inputElement.focus();
30 }
31
32 inputElement = null;
33
34 render() {
35 const {
36 field,
37 className,
38 showLabel,
39 } = this.props;
40
41 return (
42 <div
43 className={classnames({
44 'franz-form__field': true,
45 'has-error': field.error,
46 [`${className}`]: className,
47 })}
48 >
49 {field.label && showLabel && (
50 <label
51 className="franz-form__label"
52 htmlFor={field.name}
53 >
54 {field.label}
55 </label>
56 )}
57 <div className="franz-form__radio-wrapper">
58 {field.options.map(type => (
59 <label
60 key={type.value}
61 htmlFor={`${field.id}-${type.value}`}
62 className={classnames({
63 'franz-form__radio': true,
64 'is-selected': field.value === type.value,
65 })}
66 >
67 <input
68 id={`${field.id}-${type.value}`}
69 type="radio"
70 name="type"
71 value={type.value}
72 onChange={field.onChange}
73 checked={field.value === type.value}
74 />
75 {type.label}
76 </label>
77 ))}
78 </div>
79 {field.error && (
80 <div
81 className="franz-form__error"
82 >
83 {field.error}
84 </div>
85 )}
86 </div>
87 );
88 }
89}
diff --git a/src/components/ui/SearchInput.js b/src/components/ui/SearchInput.js
new file mode 100644
index 000000000..bca412cef
--- /dev/null
+++ b/src/components/ui/SearchInput.js
@@ -0,0 +1,124 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import classnames from 'classnames';
5import uuidv1 from 'uuid/v1';
6import { debounce } from 'lodash';
7
8@observer
9export default class SearchInput extends Component {
10 static propTypes = {
11 value: PropTypes.string,
12 defaultValue: PropTypes.string,
13 className: PropTypes.string,
14 onChange: PropTypes.func,
15 onReset: PropTypes.func,
16 name: PropTypes.string,
17 throttle: PropTypes.bool,
18 throttleDelay: PropTypes.number,
19 };
20
21 static defaultProps = {
22 value: '',
23 defaultValue: '',
24 className: '',
25 name: uuidv1(),
26 throttle: false,
27 throttleDelay: 250,
28 onChange: () => null,
29 onReset: () => null,
30 }
31
32 constructor(props) {
33 super(props);
34
35 this.state = {
36 value: props.value || props.defaultValue,
37 };
38
39 this.throttledOnChange = debounce(this.throttledOnChange, this.props.throttleDelay);
40 }
41
42 onChange(e) {
43 const { throttle, onChange } = this.props;
44 const { value } = e.target;
45 this.setState({ value });
46
47 if (throttle) {
48 e.persist();
49 this.throttledOnChange(value);
50 } else {
51 onChange(value);
52 }
53 }
54
55 onClick() {
56 const { defaultValue } = this.props;
57 const { value } = this.state;
58
59 if (value === defaultValue) {
60 this.setState({ value: '' });
61 }
62
63 this.input.focus();
64 }
65
66 onBlur() {
67 const { defaultValue } = this.props;
68 const { value } = this.state;
69
70 if (value === '') {
71 this.setState({ value: defaultValue });
72 }
73 }
74
75 throttledOnChange(e) {
76 const { onChange } = this.props;
77
78 onChange(e);
79 }
80
81 reset() {
82 const { defaultValue, onReset } = this.props;
83 this.setState({ value: defaultValue });
84
85 onReset();
86 }
87
88 input = null;
89
90 render() {
91 const { className, name, defaultValue } = this.props;
92 const { value } = this.state;
93
94 return (
95 <div
96 className={classnames([
97 className,
98 'search-input',
99 ])}
100 >
101 <label
102 htmlFor={name}
103 className="mdi mdi-magnify"
104 onClick={() => this.onClick()}
105 />
106 <input
107 name={name}
108 type="text"
109 value={value}
110 onChange={e => this.onChange(e)}
111 onClick={() => this.onClick()}
112 onBlur={() => this.onBlur()}
113 ref={(ref) => { this.input = ref; }}
114 />
115 {value !== defaultValue && value.length > 0 && (
116 <span
117 className="mdi mdi-close-circle-outline"
118 onClick={() => this.reset()}
119 />
120 )}
121 </div>
122 );
123 }
124}
diff --git a/src/components/ui/Select.js b/src/components/ui/Select.js
new file mode 100644
index 000000000..2a877af3e
--- /dev/null
+++ b/src/components/ui/Select.js
@@ -0,0 +1,70 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { Field } from 'mobx-react-form';
5import classnames from 'classnames';
6
7@observer
8export default class Select extends Component {
9 static propTypes = {
10 field: PropTypes.instanceOf(Field).isRequired,
11 className: PropTypes.string,
12 showLabel: PropTypes.bool,
13 };
14
15 static defaultProps = {
16 className: null,
17 focus: false,
18 showLabel: true,
19 };
20
21 render() {
22 const {
23 field,
24 className,
25 showLabel,
26 } = this.props;
27
28 return (
29 <div
30 className={classnames({
31 'franz-form__field': true,
32 'has-error': field.error,
33 [`${className}`]: className,
34 })}
35 >
36 {field.label && showLabel && (
37 <label
38 className="franz-form__label"
39 htmlFor={field.name}
40 >
41 {field.label}
42 </label>
43 )}
44 <select
45 onChange={field.onChange}
46 id={field.id}
47 defaultValue={field.value}
48 className="franz-form__select"
49 >
50 {field.options.map(type => (
51 <option
52 key={type.value}
53 value={type.value}
54 // selected={field.value === }
55 >
56 {type.label}
57 </option>
58 ))}
59 </select>
60 {field.error && (
61 <div
62 className="franz-form__error"
63 >
64 {field.error}
65 </div>
66 )}
67 </div>
68 );
69 }
70}
diff --git a/src/components/ui/Subscription.js b/src/components/ui/Subscription.js
new file mode 100644
index 000000000..ada5cc3e0
--- /dev/null
+++ b/src/components/ui/Subscription.js
@@ -0,0 +1,265 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer, PropTypes as MobxPropTypes } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5
6import Form from '../../lib/Form';
7import Radio from '../ui/Radio';
8import Button from '../ui/Button';
9import Loader from '../ui/Loader';
10
11import { required } from '../../helpers/validation-helpers';
12
13const messages = defineMessages({
14 submitButtonLabel: {
15 id: 'subscription.submit.label',
16 defaultMessage: '!!!Support the development of Franz',
17 },
18 paymentSessionError: {
19 id: 'subscription.paymentSessionError',
20 defaultMessage: '!!!Could not initialize payment form',
21 },
22 typeFree: {
23 id: 'subscription.type.free',
24 defaultMessage: '!!!free',
25 },
26 typeMonthly: {
27 id: 'subscription.type.month',
28 defaultMessage: '!!!month',
29 },
30 typeYearly: {
31 id: 'subscription.type.year',
32 defaultMessage: '!!!year',
33 },
34 typeMining: {
35 id: 'subscription.type.mining',
36 defaultMessage: '!!!Support Franz with processing power',
37 },
38 includedFeatures: {
39 id: 'subscription.includedFeatures',
40 defaultMessage: '!!!The Franz Premium Supporter Account includes',
41 },
42 features: {
43 unlimitedServices: {
44 id: 'subscription.features.unlimitedServices',
45 defaultMessage: '!!!Add unlimited services',
46 },
47 onpremise: {
48 id: 'subscription.features.onpremise',
49 defaultMessage: '!!!Add on-premise/hosted services like HipChat',
50 },
51 customServices: {
52 id: 'subscription.features.customServices',
53 defaultMessage: '!!!Add your custom services',
54 },
55 encryptedSync: {
56 id: 'subscription.features.encryptedSync',
57 defaultMessage: '!!!Encrypted session synchronization',
58 },
59 vpn: {
60 id: 'subscription.features.vpn',
61 defaultMessage: '!!!Proxy & VPN support',
62 },
63 ads: {
64 id: 'subscription.features.ads',
65 defaultMessage: '!!!No ads, ever!',
66 },
67 comingSoon: {
68 id: 'subscription.features.comingSoon',
69 defaultMessage: '!!!coming soon',
70 },
71 },
72 miningHeadline: {
73 id: 'subscription.mining.headline',
74 defaultMessage: '!!!How does this work?',
75 },
76 experimental: {
77 id: 'subscription.mining.experimental',
78 defaultMessage: '!!!experimental',
79 },
80 miningDetail1: {
81 id: 'subscription.mining.line1',
82 defaultMessage: '!!!By enabling "Support with processing power", Franz will use about 20-50% of your CPU to mine cryptocurrency Monero which equals approximately $ 5/year.',
83 },
84 miningDetail2: {
85 id: 'subscription.mining.line2',
86 defaultMessage: '!!!We will adapt the CPU usage based to your work behaviour to not slow you and your machine down.',
87 },
88 miningDetail3: {
89 id: 'subscription.mining.line3',
90 defaultMessage: '!!!As long as the miner is active, you will have unlimited access to all the Franz Premium Supporter Features.',
91 },
92 miningMoreInfo: {
93 id: 'subscription.mining.moreInformation',
94 defaultMessage: '!!!Get more information about this plan',
95 },
96});
97
98@observer
99export default class SubscriptionForm extends Component {
100 static propTypes = {
101 plan: MobxPropTypes.objectOrObservableObject.isRequired,
102 isLoading: PropTypes.bool.isRequired,
103 handlePayment: PropTypes.func.isRequired,
104 retryPlanRequest: PropTypes.func.isRequired,
105 isCreatingHostedPage: PropTypes.bool.isRequired,
106 error: PropTypes.bool.isRequired,
107 showSkipOption: PropTypes.bool,
108 skipAction: PropTypes.func,
109 skipButtonLabel: PropTypes.string,
110 hideInfo: PropTypes.bool.isRequired,
111 openExternalUrl: PropTypes.func.isRequired,
112 };
113
114 static defaultProps ={
115 content: '',
116 showSkipOption: false,
117 skipAction: () => null,
118 skipButtonLabel: '',
119 }
120
121 static contextTypes = {
122 intl: intlShape,
123 };
124
125 componentWillMount() {
126 this.form = this.prepareForm();
127 }
128
129 prepareForm() {
130 const { intl } = this.context;
131
132 const form = {
133 fields: {
134 paymentTier: {
135 value: 'year',
136 validate: [required],
137 options: [{
138 value: 'month',
139 label: `$ ${Object.hasOwnProperty.call(this.props.plan, 'month')
140 ? `${this.props.plan.month.price} / ${intl.formatMessage(messages.typeMonthly)}`
141 : 'monthly'}`,
142 }, {
143 value: 'year',
144 label: `$ ${Object.hasOwnProperty.call(this.props.plan, 'year')
145 ? `${this.props.plan.year.price} / ${intl.formatMessage(messages.typeYearly)}`
146 : 'yearly'}`,
147 }, {
148 value: 'mining',
149 label: intl.formatMessage(messages.typeMining),
150 }],
151 },
152 },
153 };
154
155 if (this.props.showSkipOption) {
156 form.fields.paymentTier.options.unshift({
157 value: 'skip',
158 label: `$ 0 / ${intl.formatMessage(messages.typeFree)}`,
159 });
160 }
161
162 return new Form(form, this.context.intl);
163 }
164
165 render() {
166 const {
167 isLoading,
168 isCreatingHostedPage,
169 handlePayment,
170 retryPlanRequest,
171 error,
172 showSkipOption,
173 skipAction,
174 skipButtonLabel,
175 hideInfo,
176 openExternalUrl,
177 } = this.props;
178 const { intl } = this.context;
179
180 if (error) {
181 return (
182 <Button
183 label="Reload"
184 onClick={retryPlanRequest}
185 isLoaded={!isLoading}
186 />
187 );
188 }
189
190 return (
191 <Loader loaded={!isLoading}>
192 <Radio field={this.form.$('paymentTier')} showLabel={false} className="paymentTiers" />
193 {!hideInfo && (
194 <div className="subscription__premium-info">
195 {this.form.$('paymentTier').value !== 'mining' && (
196 <div>
197 <p>
198 <strong>{intl.formatMessage(messages.includedFeatures)}</strong>
199 </p>
200 <div className="subscription">
201 <ul className="subscription__premium-features">
202 <li>{intl.formatMessage(messages.features.onpremise)}</li>
203 <li>
204 {intl.formatMessage(messages.features.encryptedSync)}
205 <span className="badge">{intl.formatMessage(messages.features.comingSoon)}</span>
206 </li>
207 <li>
208 {intl.formatMessage(messages.features.customServices)}
209 <span className="badge">{intl.formatMessage(messages.features.comingSoon)}</span>
210 </li>
211 <li>
212 {intl.formatMessage(messages.features.vpn)}
213 <span className="badge">{intl.formatMessage(messages.features.comingSoon)}</span>
214 </li>
215 <li>
216 {intl.formatMessage(messages.features.ads)}
217 </li>
218 </ul>
219 </div>
220 </div>
221 )}
222 {this.form.$('paymentTier').value === 'mining' && (
223 <div className="subscription mining-details">
224 <p>
225 <strong>{intl.formatMessage(messages.miningHeadline)}</strong>
226 &nbsp;
227 <span className="badge">{intl.formatMessage(messages.experimental)}</span>
228 </p>
229 <p>{intl.formatMessage(messages.miningDetail1)}</p>
230 <p>{intl.formatMessage(messages.miningDetail2)}</p>
231 <p>{intl.formatMessage(messages.miningDetail3)}</p>
232 <p>
233 <button
234 onClick={() => openExternalUrl({ url: 'http://meetfranz.com/mining' })}
235 >
236 {intl.formatMessage(messages.miningMoreInfo)}
237 </button>
238 </p>
239 </div>
240 )}
241 </div>
242 )}
243 <div>
244 {error.code === 'no-payment-session' && (
245 <p className="error-message center">{intl.formatMessage(messages.paymentSessionError)}</p>
246 )}
247 </div>
248 {showSkipOption && this.form.$('paymentTier').value === 'skip' ? (
249 <Button
250 label={skipButtonLabel}
251 className="auth__button"
252 onClick={skipAction}
253 />
254 ) : (
255 <Button
256 label={intl.formatMessage(messages.submitButtonLabel)}
257 className="auth__button"
258 loaded={!isCreatingHostedPage}
259 onClick={() => handlePayment(this.form.$('paymentTier').value)}
260 />
261 )}
262 </Loader>
263 );
264 }
265}
diff --git a/src/components/ui/SubscriptionPopup.js b/src/components/ui/SubscriptionPopup.js
new file mode 100644
index 000000000..72b6ccd98
--- /dev/null
+++ b/src/components/ui/SubscriptionPopup.js
@@ -0,0 +1,84 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl';
5import Webview from 'react-electron-web-view';
6
7import Button from '../ui/Button';
8
9const messages = defineMessages({
10 buttonCancel: {
11 id: 'subscriptionPopup.buttonCancel',
12 defaultMessage: '!!!Cancel',
13 },
14 buttonDone: {
15 id: 'subscriptionPopup.buttonDone',
16 defaultMessage: '!!!Done',
17 },
18});
19
20@observer
21export default class SubscriptionPopup extends Component {
22 static propTypes = {
23 url: PropTypes.string.isRequired,
24 closeWindow: PropTypes.func.isRequired,
25 completeCheck: PropTypes.func.isRequired,
26 isCompleted: PropTypes.bool.isRequired,
27 };
28
29 static contextTypes = {
30 intl: intlShape,
31 };
32
33 state = {
34 isFakeLoading: false,
35 };
36
37 // We delay the window closing a bit in order to give
38 // the Recurly webhook a few seconds to do it's magic
39 delayedCloseWindow() {
40 this.setState({
41 isFakeLoading: true,
42 });
43
44 setTimeout(() => {
45 this.props.closeWindow();
46 }, 4000);
47 }
48
49 render() {
50 const { url, closeWindow, completeCheck, isCompleted } = this.props;
51 const { intl } = this.context;
52
53 return (
54 <div className="subscription-popup">
55 <div className="subscription-popup__content">
56 <Webview
57 className="subscription-popup__webview"
58
59 autosize
60 src={url}
61 disablewebsecurity
62 onDidNavigate={completeCheck}
63 // onNewWindow={(event, url, frameName, options) =>
64 // openWindow({ event, url, frameName, options })}
65 />
66 </div>
67 <div className="subscription-popup__toolbar franz-form">
68 <Button
69 label={intl.formatMessage(messages.buttonCancel)}
70 buttonType="secondary"
71 onClick={closeWindow}
72 disabled={isCompleted}
73 />
74 <Button
75 label={intl.formatMessage(messages.buttonDone)}
76 onClick={() => this.delayedCloseWindow()}
77 disabled={!isCompleted}
78 loaded={!this.state.isFakeLoading}
79 />
80 </div>
81 </div>
82 );
83 }
84}
diff --git a/src/components/ui/Tabs/TabItem.js b/src/components/ui/Tabs/TabItem.js
new file mode 100644
index 000000000..9ff9f009e
--- /dev/null
+++ b/src/components/ui/Tabs/TabItem.js
@@ -0,0 +1,17 @@
1import React, { Component } from 'react';
2
3import { oneOrManyChildElements } from '../../../prop-types';
4
5export default class TabItem extends Component {
6 static propTypes = {
7 children: oneOrManyChildElements.isRequired,
8 }
9
10 render() {
11 const { children } = this.props;
12
13 return (
14 <div>{children}</div>
15 );
16 }
17}
diff --git a/src/components/ui/Tabs/Tabs.js b/src/components/ui/Tabs/Tabs.js
new file mode 100644
index 000000000..50397f9bb
--- /dev/null
+++ b/src/components/ui/Tabs/Tabs.js
@@ -0,0 +1,69 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import classnames from 'classnames';
5
6import { oneOrManyChildElements } from '../../../prop-types';
7
8@observer
9export default class Tab extends Component {
10 static propTypes = {
11 children: oneOrManyChildElements.isRequired,
12 active: PropTypes.number,
13 };
14
15 static defaultProps = {
16 active: 0,
17 };
18
19 componentWillMount() {
20 this.setState({ active: this.props.active });
21 }
22
23 switchTab(index) {
24 this.setState({ active: index });
25 }
26
27 render() {
28 const { children: childElements } = this.props;
29 const children = childElements.filter(c => !!c);
30
31 if (children.length === 1) {
32 return <div>{children}</div>;
33 }
34
35 return (
36 <div className="content-tabs">
37 <div className="content-tabs__tabs">
38 {React.Children.map(children, (child, i) => (
39 <button
40 key={i}
41 className={classnames({
42 'content-tabs__item': true,
43 'is-active': this.state.active === i,
44 })}
45 onClick={() => this.switchTab(i)}
46 type="button"
47 >
48 {child.props.title}
49 </button>
50 ))}
51 </div>
52 <div className="content-tabs__content">
53 {React.Children.map(children, (child, i) => (
54 <div
55 key={i}
56 className={classnames({
57 'content-tabs__item': true,
58 'is-active': this.state.active === i,
59 })}
60 type="button"
61 >
62 {child}
63 </div>
64 ))}
65 </div>
66 </div>
67 );
68 }
69}
diff --git a/src/components/ui/Tabs/index.js b/src/components/ui/Tabs/index.js
new file mode 100644
index 000000000..e4adb62c7
--- /dev/null
+++ b/src/components/ui/Tabs/index.js
@@ -0,0 +1,6 @@
1import Tabs from './Tabs';
2import TabItem from './TabItem';
3
4export default Tabs;
5
6export { TabItem };
diff --git a/src/components/ui/Toggle.js b/src/components/ui/Toggle.js
new file mode 100644
index 000000000..62d46393e
--- /dev/null
+++ b/src/components/ui/Toggle.js
@@ -0,0 +1,67 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react';
4import classnames from 'classnames';
5import { Field } from 'mobx-react-form';
6
7@observer
8export default class Toggle extends Component {
9 static propTypes = {
10 field: PropTypes.instanceOf(Field).isRequired,
11 className: PropTypes.string,
12 showLabel: PropTypes.bool,
13 };
14
15 static defaultProps = {
16 className: '',
17 showLabel: true,
18 };
19
20 onChange(e) {
21 const { field } = this.props;
22
23 field.onChange(e);
24 }
25
26 render() {
27 const {
28 field,
29 className,
30 showLabel,
31 } = this.props;
32
33 if (field.value === '' && field.default !== '') {
34 field.value = field.default;
35 }
36
37 return (
38 <div
39 className={classnames([
40 'franz-form__field',
41 'franz-form__toggle-wrapper',
42 className,
43 ])}
44 >
45 <label
46 htmlFor={field.id}
47 className={classnames({
48 'franz-form__toggle': true,
49 'is-active': field.value,
50 })}
51 >
52 <div className="franz-form__toggle-button" />
53 <input
54 type="checkbox"
55 id={field.id}
56 name={field.name}
57 value={field.name}
58 checked={field.value}
59 onChange={e => this.onChange(e)}
60 />
61 </label>
62 {field.error && <div className={field.error}>{field.error}</div>}
63 {field.label && showLabel && <label className="franz-form__label" htmlFor={field.id}>{field.label}</label>}
64 </div>
65 );
66 }
67}
diff --git a/src/components/ui/effects/Appear.js b/src/components/ui/effects/Appear.js
new file mode 100644
index 000000000..1255fce2e
--- /dev/null
+++ b/src/components/ui/effects/Appear.js
@@ -0,0 +1,51 @@
1/* eslint-disable react/no-did-mount-set-state */
2import React, { Component } from 'react';
3import PropTypes from 'prop-types';
4import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
5
6export default class Appear extends Component {
7 static propTypes = {
8 children: PropTypes.any.isRequired, // eslint-disable-line
9 transitionName: PropTypes.string,
10 className: PropTypes.string,
11 };
12
13 static defaultProps = {
14 transitionName: 'fadeIn',
15 className: '',
16 };
17
18 state = {
19 mounted: false,
20 };
21
22 componentDidMount() {
23 this.setState({ mounted: true });
24 }
25
26 render() {
27 const {
28 children,
29 transitionName,
30 className,
31 } = this.props;
32
33 if (!this.state.mounted) {
34 return null;
35 }
36
37 return (
38 <ReactCSSTransitionGroup
39 transitionName={transitionName}
40 transitionAppear
41 transitionLeave
42 transitionAppearTimeout={1500}
43 transitionEnterTimeout={1500}
44 transitionLeaveTimeout={1500}
45 className={className}
46 >
47 {children}
48 </ReactCSSTransitionGroup>
49 );
50 }
51}