diff options
author | 2017-10-13 12:29:40 +0200 | |
---|---|---|
committer | 2017-10-13 12:29:40 +0200 | |
commit | 58cda9cc7fb79ca9df6746de7f9662bc08dc156a (patch) | |
tree | 1211600c2a5d3b5f81c435c6896618111a611720 /src/components/ui | |
download | ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.tar.gz ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.tar.zst ferdium-app-58cda9cc7fb79ca9df6746de7f9662bc08dc156a.zip |
initial commit
Diffstat (limited to 'src/components/ui')
-rw-r--r-- | src/components/ui/AppLoader.js | 15 | ||||
-rw-r--r-- | src/components/ui/Button.js | 78 | ||||
-rw-r--r-- | src/components/ui/InfoBar.js | 88 | ||||
-rw-r--r-- | src/components/ui/Infobox.js | 87 | ||||
-rw-r--r-- | src/components/ui/Input.js | 148 | ||||
-rw-r--r-- | src/components/ui/Link.js | 78 | ||||
-rw-r--r-- | src/components/ui/Loader.js | 41 | ||||
-rw-r--r-- | src/components/ui/Radio.js | 89 | ||||
-rw-r--r-- | src/components/ui/SearchInput.js | 124 | ||||
-rw-r--r-- | src/components/ui/Select.js | 70 | ||||
-rw-r--r-- | src/components/ui/Subscription.js | 265 | ||||
-rw-r--r-- | src/components/ui/SubscriptionPopup.js | 84 | ||||
-rw-r--r-- | src/components/ui/Tabs/TabItem.js | 17 | ||||
-rw-r--r-- | src/components/ui/Tabs/Tabs.js | 69 | ||||
-rw-r--r-- | src/components/ui/Tabs/index.js | 6 | ||||
-rw-r--r-- | src/components/ui/Toggle.js | 67 | ||||
-rw-r--r-- | src/components/ui/effects/Appear.js | 51 |
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 @@ | |||
1 | import React from 'react'; | ||
2 | |||
3 | import Appear from '../../components/ui/effects/Appear'; | ||
4 | import Loader from '../../components/ui/Loader'; | ||
5 | |||
6 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import Loader from 'react-loader'; | ||
5 | import classnames from 'classnames'; | ||
6 | |||
7 | @observer | ||
8 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import classnames from 'classnames'; | ||
5 | import Loader from 'react-loader'; | ||
6 | |||
7 | // import { oneOrManyChildElements } from '../../prop-types'; | ||
8 | import Appear from '../ui/effects/Appear'; | ||
9 | |||
10 | @observer | ||
11 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import classnames from 'classnames'; | ||
5 | import Loader from 'react-loader'; | ||
6 | |||
7 | @observer | ||
8 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { Field } from 'mobx-react-form'; | ||
5 | import classnames from 'classnames'; | ||
6 | |||
7 | import { scorePassword as scorePasswordFunc } from '../../helpers/password-helpers'; | ||
8 | |||
9 | @observer | ||
10 | export 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 @@ | |||
1 | import { shell } from 'electron'; | ||
2 | import React, { Component } from 'react'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | import { inject, observer } from 'mobx-react'; | ||
5 | import { RouterStore } from 'mobx-react-router'; | ||
6 | import classnames from 'classnames'; | ||
7 | |||
8 | import { oneOrManyChildElements } from '../../prop-types'; | ||
9 | import { matchRoute } from '../../helpers/routing-helpers'; | ||
10 | |||
11 | // TODO: create container component for this component | ||
12 | |||
13 | @inject('stores') @observer | ||
14 | export 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 | |||
57 | Link.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 | |||
72 | Link.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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import Loader from 'react-loader'; | ||
4 | |||
5 | import { oneOrManyChildElements } from '../../prop-types'; | ||
6 | |||
7 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { Field } from 'mobx-react-form'; | ||
5 | import classnames from 'classnames'; | ||
6 | |||
7 | @observer | ||
8 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import classnames from 'classnames'; | ||
5 | import uuidv1 from 'uuid/v1'; | ||
6 | import { debounce } from 'lodash'; | ||
7 | |||
8 | @observer | ||
9 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import { Field } from 'mobx-react-form'; | ||
5 | import classnames from 'classnames'; | ||
6 | |||
7 | @observer | ||
8 | export 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 @@ | |||
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 Radio from '../ui/Radio'; | ||
8 | import Button from '../ui/Button'; | ||
9 | import Loader from '../ui/Loader'; | ||
10 | |||
11 | import { required } from '../../helpers/validation-helpers'; | ||
12 | |||
13 | const 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 | ||
99 | export 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 | | ||
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 @@ | |||
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 Webview from 'react-electron-web-view'; | ||
6 | |||
7 | import Button from '../ui/Button'; | ||
8 | |||
9 | const 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 | ||
21 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | |||
3 | import { oneOrManyChildElements } from '../../../prop-types'; | ||
4 | |||
5 | export 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import classnames from 'classnames'; | ||
5 | |||
6 | import { oneOrManyChildElements } from '../../../prop-types'; | ||
7 | |||
8 | @observer | ||
9 | export 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 @@ | |||
1 | import Tabs from './Tabs'; | ||
2 | import TabItem from './TabItem'; | ||
3 | |||
4 | export default Tabs; | ||
5 | |||
6 | export { 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 @@ | |||
1 | import React, { Component } from 'react'; | ||
2 | import PropTypes from 'prop-types'; | ||
3 | import { observer } from 'mobx-react'; | ||
4 | import classnames from 'classnames'; | ||
5 | import { Field } from 'mobx-react-form'; | ||
6 | |||
7 | @observer | ||
8 | export 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 */ | ||
2 | import React, { Component } from 'react'; | ||
3 | import PropTypes from 'prop-types'; | ||
4 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; | ||
5 | |||
6 | export 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 | } | ||