diff options
author | Stefan Malzner <stefan@adlk.io> | 2018-01-03 16:31:22 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-01-03 16:31:22 +0100 |
commit | 44f0b67f26b58a78e98843741e7eaac9b221ba37 (patch) | |
tree | 3f377e368e4d797f0bf1d832f524c11431245877 /src | |
parent | Merge pull request #506 from meetfranz/feature/reactivate-franz (diff) | |
parent | feat(Services): Improve user experience of service search (diff) | |
download | ferdium-app-44f0b67f26b58a78e98843741e7eaac9b221ba37.tar.gz ferdium-app-44f0b67f26b58a78e98843741e7eaac9b221ba37.tar.zst ferdium-app-44f0b67f26b58a78e98843741e7eaac9b221ba37.zip |
Merge pull request #533 from meetfranz/feature/ux-service-search
feat(Services): Improve user experience of service search
Diffstat (limited to 'src')
-rw-r--r-- | src/components/settings/recipes/RecipesDashboard.js | 20 | ||||
-rw-r--r-- | src/components/settings/services/ServicesDashboard.js | 42 | ||||
-rw-r--r-- | src/components/ui/SearchInput.js | 48 | ||||
-rw-r--r-- | src/containers/settings/ServicesScreen.js | 1 | ||||
-rw-r--r-- | src/i18n/locales/en-US.json | 1 | ||||
-rw-r--r-- | src/styles/searchInput.scss | 16 | ||||
-rw-r--r-- | src/styles/settings.scss | 22 |
7 files changed, 85 insertions, 65 deletions
diff --git a/src/components/settings/recipes/RecipesDashboard.js b/src/components/settings/recipes/RecipesDashboard.js index b6ade5da4..4610c69a5 100644 --- a/src/components/settings/recipes/RecipesDashboard.js +++ b/src/components/settings/recipes/RecipesDashboard.js | |||
@@ -16,6 +16,10 @@ const messages = defineMessages({ | |||
16 | id: 'settings.recipes.headline', | 16 | id: 'settings.recipes.headline', |
17 | defaultMessage: '!!!Available Services', | 17 | defaultMessage: '!!!Available Services', |
18 | }, | 18 | }, |
19 | searchService: { | ||
20 | id: 'settings.searchService', | ||
21 | defaultMessage: '!!!Search service', | ||
22 | }, | ||
19 | mostPopularRecipes: { | 23 | mostPopularRecipes: { |
20 | id: 'settings.recipes.mostPopular', | 24 | id: 'settings.recipes.mostPopular', |
21 | defaultMessage: '!!!Most popular', | 25 | defaultMessage: '!!!Most popular', |
@@ -81,13 +85,7 @@ export default class RecipesDashboard extends Component { | |||
81 | return ( | 85 | return ( |
82 | <div className="settings__main"> | 86 | <div className="settings__main"> |
83 | <div className="settings__header"> | 87 | <div className="settings__header"> |
84 | <SearchInput | 88 | <h1>{intl.formatMessage(messages.headline)}</h1> |
85 | className="settings__search-header" | ||
86 | defaultValue={intl.formatMessage(messages.headline)} | ||
87 | onChange={e => searchRecipes(e)} | ||
88 | onReset={() => resetSearch()} | ||
89 | throttle | ||
90 | /> | ||
91 | </div> | 89 | </div> |
92 | <div className="settings__body recipes"> | 90 | <div className="settings__body recipes"> |
93 | {serviceStatus.length > 0 && serviceStatus.includes('created') && ( | 91 | {serviceStatus.length > 0 && serviceStatus.includes('created') && ( |
@@ -101,7 +99,13 @@ export default class RecipesDashboard extends Component { | |||
101 | </Infobox> | 99 | </Infobox> |
102 | </Appear> | 100 | </Appear> |
103 | )} | 101 | )} |
104 | {/* {!searchNeedle && ( */} | 102 | <SearchInput |
103 | placeholder={intl.formatMessage(messages.searchService)} | ||
104 | onChange={e => searchRecipes(e)} | ||
105 | onReset={() => resetSearch()} | ||
106 | autoFocus | ||
107 | throttle | ||
108 | /> | ||
105 | <div className="recipes__navigation"> | 109 | <div className="recipes__navigation"> |
106 | <Link | 110 | <Link |
107 | to="/settings/recipes" | 111 | to="/settings/recipes" |
diff --git a/src/components/settings/services/ServicesDashboard.js b/src/components/settings/services/ServicesDashboard.js index 5f146b5f3..20e451f01 100644 --- a/src/components/settings/services/ServicesDashboard.js +++ b/src/components/settings/services/ServicesDashboard.js | |||
@@ -15,10 +15,18 @@ const messages = defineMessages({ | |||
15 | id: 'settings.services.headline', | 15 | id: 'settings.services.headline', |
16 | defaultMessage: '!!!Your services', | 16 | defaultMessage: '!!!Your services', |
17 | }, | 17 | }, |
18 | searchService: { | ||
19 | id: 'settings.searchService', | ||
20 | defaultMessage: '!!!Search service', | ||
21 | }, | ||
18 | noServicesAdded: { | 22 | noServicesAdded: { |
19 | id: 'settings.services.noServicesAdded', | 23 | id: 'settings.services.noServicesAdded', |
20 | defaultMessage: '!!!You haven\'t added any services yet.', | 24 | defaultMessage: '!!!You haven\'t added any services yet.', |
21 | }, | 25 | }, |
26 | noServiceFound: { | ||
27 | id: 'settings.recipes.nothingFound', | ||
28 | defaultMessage: '!!!Sorry, but no service matched your search term.', | ||
29 | }, | ||
22 | discoverServices: { | 30 | discoverServices: { |
23 | id: 'settings.services.discoverServices', | 31 | id: 'settings.services.discoverServices', |
24 | defaultMessage: '!!!Discover services', | 32 | defaultMessage: '!!!Discover services', |
@@ -53,7 +61,13 @@ export default class ServicesDashboard extends Component { | |||
53 | servicesRequestFailed: PropTypes.bool.isRequired, | 61 | servicesRequestFailed: PropTypes.bool.isRequired, |
54 | retryServicesRequest: PropTypes.func.isRequired, | 62 | retryServicesRequest: PropTypes.func.isRequired, |
55 | status: MobxPropTypes.arrayOrObservableArray.isRequired, | 63 | status: MobxPropTypes.arrayOrObservableArray.isRequired, |
64 | searchNeedle: PropTypes.string, | ||
56 | }; | 65 | }; |
66 | |||
67 | static defaultProps = { | ||
68 | searchNeedle: '', | ||
69 | } | ||
70 | |||
57 | static contextTypes = { | 71 | static contextTypes = { |
58 | intl: intlShape, | 72 | intl: intlShape, |
59 | }; | 73 | }; |
@@ -69,20 +83,24 @@ export default class ServicesDashboard extends Component { | |||
69 | servicesRequestFailed, | 83 | servicesRequestFailed, |
70 | retryServicesRequest, | 84 | retryServicesRequest, |
71 | status, | 85 | status, |
86 | searchNeedle, | ||
72 | } = this.props; | 87 | } = this.props; |
73 | const { intl } = this.context; | 88 | const { intl } = this.context; |
74 | 89 | ||
75 | return ( | 90 | return ( |
76 | <div className="settings__main"> | 91 | <div className="settings__main"> |
77 | <div className="settings__header"> | 92 | <div className="settings__header"> |
78 | <SearchInput | 93 | <h1>{intl.formatMessage(messages.headline)}</h1> |
79 | className="settings__search-header" | ||
80 | defaultValue={intl.formatMessage(messages.headline)} | ||
81 | onChange={needle => filterServices({ needle })} | ||
82 | onReset={() => resetFilter()} | ||
83 | /> | ||
84 | </div> | 94 | </div> |
85 | <div className="settings__body"> | 95 | <div className="settings__body"> |
96 | {!isLoading && ( | ||
97 | <SearchInput | ||
98 | placeholder={intl.formatMessage(messages.searchService)} | ||
99 | onChange={needle => filterServices({ needle })} | ||
100 | onReset={() => resetFilter()} | ||
101 | autoFocus | ||
102 | /> | ||
103 | )} | ||
86 | {!isLoading && servicesRequestFailed && ( | 104 | {!isLoading && servicesRequestFailed && ( |
87 | <div> | 105 | <div> |
88 | <Infobox | 106 | <Infobox |
@@ -121,7 +139,7 @@ export default class ServicesDashboard extends Component { | |||
121 | </Appear> | 139 | </Appear> |
122 | )} | 140 | )} |
123 | 141 | ||
124 | {!isLoading && services.length === 0 && ( | 142 | {!isLoading && services.length === 0 && !searchNeedle && ( |
125 | <div className="align-middle settings__empty-state"> | 143 | <div className="align-middle settings__empty-state"> |
126 | <p className="settings__empty-text"> | 144 | <p className="settings__empty-text"> |
127 | <span className="emoji"> | 145 | <span className="emoji"> |
@@ -132,6 +150,16 @@ export default class ServicesDashboard extends Component { | |||
132 | <Link to="/settings/recipes" className="button">{intl.formatMessage(messages.discoverServices)}</Link> | 150 | <Link to="/settings/recipes" className="button">{intl.formatMessage(messages.discoverServices)}</Link> |
133 | </div> | 151 | </div> |
134 | )} | 152 | )} |
153 | {!isLoading && services.length === 0 && searchNeedle && ( | ||
154 | <div className="align-middle settings__empty-state"> | ||
155 | <p className="settings__empty-text"> | ||
156 | <span className="emoji"> | ||
157 | <img src="./assets/images/emoji/dontknow.png" alt="" /> | ||
158 | </span> | ||
159 | {intl.formatMessage(messages.noServiceFound)} | ||
160 | </p> | ||
161 | </div> | ||
162 | )} | ||
135 | {isLoading ? ( | 163 | {isLoading ? ( |
136 | <Loader /> | 164 | <Loader /> |
137 | ) : ( | 165 | ) : ( |
diff --git a/src/components/ui/SearchInput.js b/src/components/ui/SearchInput.js index bca412cef..a94cde201 100644 --- a/src/components/ui/SearchInput.js +++ b/src/components/ui/SearchInput.js | |||
@@ -9,36 +9,46 @@ import { debounce } from 'lodash'; | |||
9 | export default class SearchInput extends Component { | 9 | export default class SearchInput extends Component { |
10 | static propTypes = { | 10 | static propTypes = { |
11 | value: PropTypes.string, | 11 | value: PropTypes.string, |
12 | defaultValue: PropTypes.string, | 12 | placeholder: PropTypes.string, |
13 | className: PropTypes.string, | 13 | className: PropTypes.string, |
14 | onChange: PropTypes.func, | 14 | onChange: PropTypes.func, |
15 | onReset: PropTypes.func, | 15 | onReset: PropTypes.func, |
16 | name: PropTypes.string, | 16 | name: PropTypes.string, |
17 | throttle: PropTypes.bool, | 17 | throttle: PropTypes.bool, |
18 | throttleDelay: PropTypes.number, | 18 | throttleDelay: PropTypes.number, |
19 | autoFocus: PropTypes.bool, | ||
19 | }; | 20 | }; |
20 | 21 | ||
21 | static defaultProps = { | 22 | static defaultProps = { |
22 | value: '', | 23 | value: '', |
23 | defaultValue: '', | 24 | placeholder: '', |
24 | className: '', | 25 | className: '', |
25 | name: uuidv1(), | 26 | name: uuidv1(), |
26 | throttle: false, | 27 | throttle: false, |
27 | throttleDelay: 250, | 28 | throttleDelay: 250, |
28 | onChange: () => null, | 29 | onChange: () => null, |
29 | onReset: () => null, | 30 | onReset: () => null, |
31 | autoFocus: false, | ||
30 | } | 32 | } |
31 | 33 | ||
32 | constructor(props) { | 34 | constructor(props) { |
33 | super(props); | 35 | super(props); |
34 | 36 | ||
35 | this.state = { | 37 | this.state = { |
36 | value: props.value || props.defaultValue, | 38 | value: props.value, |
37 | }; | 39 | }; |
38 | 40 | ||
39 | this.throttledOnChange = debounce(this.throttledOnChange, this.props.throttleDelay); | 41 | this.throttledOnChange = debounce(this.throttledOnChange, this.props.throttleDelay); |
40 | } | 42 | } |
41 | 43 | ||
44 | componentDidMount() { | ||
45 | const { autoFocus } = this.props; | ||
46 | |||
47 | if (autoFocus) { | ||
48 | this.input.focus(); | ||
49 | } | ||
50 | } | ||
51 | |||
42 | onChange(e) { | 52 | onChange(e) { |
43 | const { throttle, onChange } = this.props; | 53 | const { throttle, onChange } = this.props; |
44 | const { value } = e.target; | 54 | const { value } = e.target; |
@@ -52,26 +62,6 @@ export default class SearchInput extends Component { | |||
52 | } | 62 | } |
53 | } | 63 | } |
54 | 64 | ||
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) { | 65 | throttledOnChange(e) { |
76 | const { onChange } = this.props; | 66 | const { onChange } = this.props; |
77 | 67 | ||
@@ -79,8 +69,8 @@ export default class SearchInput extends Component { | |||
79 | } | 69 | } |
80 | 70 | ||
81 | reset() { | 71 | reset() { |
82 | const { defaultValue, onReset } = this.props; | 72 | const { onReset } = this.props; |
83 | this.setState({ value: defaultValue }); | 73 | this.setState({ value: '' }); |
84 | 74 | ||
85 | onReset(); | 75 | onReset(); |
86 | } | 76 | } |
@@ -88,7 +78,7 @@ export default class SearchInput extends Component { | |||
88 | input = null; | 78 | input = null; |
89 | 79 | ||
90 | render() { | 80 | render() { |
91 | const { className, name, defaultValue } = this.props; | 81 | const { className, name, placeholder } = this.props; |
92 | const { value } = this.state; | 82 | const { value } = this.state; |
93 | 83 | ||
94 | return ( | 84 | return ( |
@@ -101,18 +91,16 @@ export default class SearchInput extends Component { | |||
101 | <label | 91 | <label |
102 | htmlFor={name} | 92 | htmlFor={name} |
103 | className="mdi mdi-magnify" | 93 | className="mdi mdi-magnify" |
104 | onClick={() => this.onClick()} | ||
105 | /> | 94 | /> |
106 | <input | 95 | <input |
107 | name={name} | 96 | name={name} |
108 | type="text" | 97 | type="text" |
98 | placeholder={placeholder} | ||
109 | value={value} | 99 | value={value} |
110 | onChange={e => this.onChange(e)} | 100 | onChange={e => this.onChange(e)} |
111 | onClick={() => this.onClick()} | ||
112 | onBlur={() => this.onBlur()} | ||
113 | ref={(ref) => { this.input = ref; }} | 101 | ref={(ref) => { this.input = ref; }} |
114 | /> | 102 | /> |
115 | {value !== defaultValue && value.length > 0 && ( | 103 | {value.length > 0 && ( |
116 | <span | 104 | <span |
117 | className="mdi mdi-close-circle-outline" | 105 | className="mdi mdi-close-circle-outline" |
118 | onClick={() => this.reset()} | 106 | onClick={() => this.reset()} |
diff --git a/src/containers/settings/ServicesScreen.js b/src/containers/settings/ServicesScreen.js index 8cfe5efbf..12db1bcd3 100644 --- a/src/containers/settings/ServicesScreen.js +++ b/src/containers/settings/ServicesScreen.js | |||
@@ -53,6 +53,7 @@ export default class ServicesScreen extends Component { | |||
53 | goTo={router.push} | 53 | goTo={router.push} |
54 | servicesRequestFailed={services.allServicesRequest.wasExecuted && services.allServicesRequest.isError} | 54 | servicesRequestFailed={services.allServicesRequest.wasExecuted && services.allServicesRequest.isError} |
55 | retryServicesRequest={() => services.allServicesRequest.reload()} | 55 | retryServicesRequest={() => services.allServicesRequest.reload()} |
56 | searchNeedle={services.filterNeedle} | ||
56 | /> | 57 | /> |
57 | ); | 58 | ); |
58 | } | 59 | } |
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 567537d75..da5d4a143 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json | |||
@@ -66,6 +66,7 @@ | |||
66 | "sidebar.unmuteApp": "Enable notifications & audio", | 66 | "sidebar.unmuteApp": "Enable notifications & audio", |
67 | "services.welcome": "Welcome to Franz", | 67 | "services.welcome": "Welcome to Franz", |
68 | "services.getStarted": "Get started", | 68 | "services.getStarted": "Get started", |
69 | "settings.searchService": "Search service", | ||
69 | "settings.account.headline": "Account", | 70 | "settings.account.headline": "Account", |
70 | "settings.account.headlineSubscription": "Your subscription", | 71 | "settings.account.headlineSubscription": "Your subscription", |
71 | "settings.account.headlineUpgrade": "Upgrade your account & support Franz", | 72 | "settings.account.headlineUpgrade": "Upgrade your account & support Franz", |
diff --git a/src/styles/searchInput.scss b/src/styles/searchInput.scss index 28ff09fc4..633a31e09 100644 --- a/src/styles/searchInput.scss +++ b/src/styles/searchInput.scss | |||
@@ -1,4 +1,20 @@ | |||
1 | .search-input { | 1 | .search-input { |
2 | width: 100%; | 2 | width: 100%; |
3 | height: auto; | 3 | height: auto; |
4 | display: flex; | ||
5 | align-items: center; | ||
6 | padding: 0 10px; | ||
7 | border-radius: 30px; | ||
8 | background: $theme-gray-lightest; | ||
9 | padding: 5px 10px; | ||
10 | @extend %headline; | ||
11 | color: $theme-gray-light; | ||
12 | |||
13 | input { | ||
14 | padding-left: 10px; | ||
15 | background: none; | ||
16 | border: 0; | ||
17 | flex: 1; | ||
18 | color: $theme-gray-light; | ||
19 | } | ||
4 | } | 20 | } |
diff --git a/src/styles/settings.scss b/src/styles/settings.scss index b29ed5468..2da56c930 100644 --- a/src/styles/settings.scss +++ b/src/styles/settings.scss | |||
@@ -129,26 +129,8 @@ | |||
129 | } | 129 | } |
130 | } | 130 | } |
131 | 131 | ||
132 | .settings__search-header { | 132 | .search-input { |
133 | display: flex; | 133 | margin-bottom: 30px; |
134 | align-items: center; | ||
135 | padding: 0 10px; | ||
136 | border-radius: $theme-border-radius; | ||
137 | transition: background $theme-transition-time; | ||
138 | @extend %headline; | ||
139 | font-size: 22px; | ||
140 | |||
141 | &:hover { | ||
142 | background: darken($theme-gray-lighter, 5%); | ||
143 | } | ||
144 | |||
145 | input { | ||
146 | padding-left: 10px; | ||
147 | background: none; | ||
148 | border: 0; | ||
149 | flex: 1; | ||
150 | @extend %headline; | ||
151 | } | ||
152 | } | 134 | } |
153 | 135 | ||
154 | &__options { | 136 | &__options { |