homeflowjs 0.10.16 → 0.10.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,11 +3,14 @@
3
3
  import React from 'react';
4
4
  import { Router } from 'react-router-dom';
5
5
  import { createHashHistory } from 'history';
6
- import { render, screen, fireEvent, waitFor } from '@testing-library/react';
6
+ import {
7
+ render, screen, fireEvent, waitFor,
8
+ } from '@testing-library/react';
7
9
  import { rest } from 'msw';
8
10
  import { setupServer } from 'msw/node';
9
11
 
10
12
  import InstantValuation from 'instant-valuation/instant-valuation/instant-valuation.component';
13
+ import ResultStep from '../instant-valuation/result-step/result-step.component';
11
14
 
12
15
  const server = setupServer(
13
16
  rest.get('/address_lookup', (req, res, ctx) => (
@@ -65,3 +68,11 @@ describe('Instant Valuation', () => {
65
68
  expect(message).toBeInTheDocument();
66
69
  });
67
70
  });
71
+
72
+ describe('Instant Valuation Results', () => {
73
+ it('renders error message if it can\'t provide an accurate valuation', () => {
74
+ render(<ResultStep valuation={{ price: null }} />);
75
+ const errorMessage = screen.getByTestId('valuation-error');
76
+ expect(errorMessage).toBeInTheDocument();
77
+ });
78
+ });
@@ -1,22 +1,28 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import { connect } from 'react-redux';
1
+ import React, {
2
+ useState, useRef, useEffect, useCallback,
3
+ } from 'react';
4
+ import { useSelector, useDispatch } from 'react-redux';
3
5
  import PropTypes from 'prop-types';
4
6
  import Autosuggest from 'react-autosuggest';
7
+ import debounce from 'lodash.debounce';
5
8
  import { setBranchesSearch } from '../../actions/branches.actions';
6
9
 
7
10
  const BranchesSearchInput = ({
8
- branchesSearch,
9
- setBranchesSearch,
10
11
  placeholder,
11
12
  isSelected,
12
13
  setIsSelected,
14
+ label,
15
+ isRequired,
13
16
  }) => {
17
+ const { branchesSearch } = useSelector((state) => state?.branches);
18
+ const dispatch = useDispatch();
14
19
  const [placeID, setPlaceID] = useState('');
15
20
  const [suggestions, setSuggestions] = useState([]);
21
+ const [inputFocused, setInputFocused] = useState(false);
16
22
 
17
23
  useEffect(() => {
18
24
  const searchedPlace = Homeflow.get('place');
19
- if (searchedPlace) setBranchesSearch(searchedPlace.name);
25
+ if (searchedPlace) dispatch(setBranchesSearch(searchedPlace.name));
20
26
  }, []);
21
27
 
22
28
  /**
@@ -47,21 +53,53 @@ const BranchesSearchInput = ({
47
53
  setSuggestions(suggestions);
48
54
  });
49
55
 
56
+ const debouncedGetSuggestions = useCallback(debounce(getSuggestions, 300), []);
57
+
50
58
  const renderSuggestion = (suggestion) => (
51
59
  <span className="react-autosuggest_span">{suggestion}</span>
52
60
  );
53
61
 
62
+ const updateLabelState = (reason) => {
63
+ if (label && (reason === 'input-focused' || reason === 'input-changed')) {
64
+ setInputFocused(true);
65
+ }
66
+ return true;
67
+ };
68
+
69
+ /**
70
+ * this follows the autosuggest library class naming convention (bem)
71
+ */
72
+ const labelClasses = () => {
73
+ if (inputFocused || branchesSearch) {
74
+ return 'react-autosuggest__label react-autosuggest__label--focused';
75
+ }
76
+ return 'react-autosuggest__label';
77
+ };
78
+
54
79
  return (
55
80
  <>
56
81
  <input type="hidden" name="place_id" value={placeID} />
57
82
  <input type="hidden" name="location" value={branchesSearch} />
83
+
84
+ {label && (
85
+ <label
86
+ className={labelClasses()}
87
+ htmlFor="react-autosuggest__input"
88
+ >
89
+ {label}
90
+ </label>
91
+ )}
92
+
58
93
  <Autosuggest
59
94
  ref={inputEl}
60
95
  suggestions={suggestions}
61
- onSuggestionsClearRequested={() => setSuggestions([])}
96
+ onSuggestionsClearRequested={() => {
97
+ setSuggestions([]);
98
+ if (label) setInputFocused(false);
99
+ }}
62
100
  onSuggestionsFetchRequested={({ value }) => {
63
- setBranchesSearch(value);
64
- getSuggestions(value);
101
+ dispatch(setBranchesSearch(value));
102
+ debouncedGetSuggestions(value);
65
103
  }}
66
104
  onSuggestionSelected={(_, { suggestion }) => {
67
105
  /**
@@ -69,17 +107,22 @@ const BranchesSearchInput = ({
69
107
  * countrywide themes, other themes will not use this.
70
108
  */
71
109
  if (setIsSelected) setIsSelected(false);
110
+ if (label) setInputFocused(true);
72
111
  return (
73
112
  setPlaceID(suggestion.place)
74
113
  );
75
114
  }}
76
115
  getSuggestionValue={(suggestion) => suggestion.label}
116
+ shouldRenderSuggestions={(_, reason) => updateLabelState(reason)}
77
117
  renderSuggestion={(suggestion) => renderSuggestion(suggestion.label)}
78
118
  inputProps={{
79
119
  placeholder,
120
+ name: 'react-autosuggest__input',
121
+ required: isRequired,
80
122
  value: branchesSearch,
123
+ // eslint-disable-next-line no-unused-vars
81
124
  onChange: (_, { newValue, method }) => {
82
- setBranchesSearch(newValue);
125
+ dispatch(setBranchesSearch(newValue));
83
126
  },
84
127
  }}
85
128
  highlightFirstSuggestion
@@ -89,9 +132,9 @@ const BranchesSearchInput = ({
89
132
  };
90
133
 
91
134
  BranchesSearchInput.propTypes = {
92
- branchesSearch: PropTypes.string.isRequired,
93
- setBranchesSearch: PropTypes.func.isRequired,
94
135
  placeholder: PropTypes.string,
136
+ label: PropTypes.string,
137
+ isRequired: PropTypes.bool,
95
138
  isSelected: PropTypes.oneOfType([
96
139
  PropTypes.bool,
97
140
  PropTypes.string,
@@ -104,19 +147,10 @@ BranchesSearchInput.propTypes = {
104
147
 
105
148
  BranchesSearchInput.defaultProps = {
106
149
  placeholder: '',
150
+ label: '',
107
151
  isSelected: '',
108
152
  setIsSelected: '',
153
+ isRequired: false,
109
154
  };
110
155
 
111
- const mapStateToProps = (state) => ({
112
- branchesSearch: state.branches.branchesSearch,
113
- });
114
-
115
- const mapDispatchToProps = {
116
- setBranchesSearch,
117
- };
118
-
119
- export default connect(
120
- mapStateToProps,
121
- mapDispatchToProps,
122
- )(BranchesSearchInput);
156
+ export default BranchesSearchInput;
@@ -15,6 +15,7 @@ import './instant-valuation.styles.scss';
15
15
 
16
16
  const INITIAL_STATE = {
17
17
  step: 1,
18
+ loading: false,
18
19
  addresses: [],
19
20
  message: null,
20
21
  similarProperties: [],
@@ -91,10 +92,11 @@ class InstantValuation extends Component {
91
92
  }
92
93
 
93
94
  getSimilarProperties() {
95
+ this.setState({ loading: true });
94
96
  const { search } = this.state;
95
97
  fetch(`/properties/similar_properties?bedrooms=${search.bedrooms}&postcode=${search.postcode}&channel=sales`)
96
98
  .then((response) => response.json())
97
- .then((json) => this.setState({ similarProperties: json }));
99
+ .then((json) => this.setState({ similarProperties: json, loading: false }));
98
100
  }
99
101
 
100
102
  getRecentSales() {
@@ -108,6 +110,7 @@ class InstantValuation extends Component {
108
110
  }
109
111
 
110
112
  getValuation() {
113
+ this.setState({ loading: true });
111
114
  const { search, lead } = this.state;
112
115
  const { properties, ...restOfSearch } = search;
113
116
 
@@ -144,7 +147,11 @@ class InstantValuation extends Component {
144
147
  .then((json) => {
145
148
  this.getRecentSales()
146
149
  .then((recentSalesJson) => {
147
- this.setState({ valuation: json, recentSales: recentSalesJson.recent_sales });
150
+ this.setState({
151
+ valuation: json,
152
+ recentSales: recentSalesJson.recent_sales,
153
+ loading: false,
154
+ });
148
155
 
149
156
  const event = new CustomEvent('formSubmission', {
150
157
  detail: {
@@ -247,6 +254,7 @@ class InstantValuation extends Component {
247
254
  similarProperties,
248
255
  valuation,
249
256
  recentSales,
257
+ loading,
250
258
  } = this.state;
251
259
 
252
260
  const StepComponent = stepComponents[step];
@@ -290,6 +298,7 @@ class InstantValuation extends Component {
290
298
  recentSales={recentSales}
291
299
  reset={this.reset}
292
300
  setMessage={this.setMessage}
301
+ loading={loading}
293
302
  />}
294
303
  </div>
295
304
  </div>
@@ -15,7 +15,23 @@ const ResultStep = ({
15
15
  valuation,
16
16
  recentSales,
17
17
  reset,
18
+ loading,
18
19
  }) => {
20
+ if (!valuation.price) {
21
+ return (
22
+ <div data-testid="valuation-error" id="no_results">
23
+ <p>
24
+ Unfortunately we do not have enough data to give you an accurate valuation. Your local
25
+ {' '}
26
+ {companyName}
27
+ {' '}
28
+ property expert will be in touch to arrange an accurate valuation taking
29
+ into account improvements to your property, the local market and more.
30
+ </p>
31
+ </div>
32
+ );
33
+ }
34
+
19
35
  const [selectedChannel, setSelectedChannel] = useState('sales');
20
36
 
21
37
  useEffect(() => {
@@ -66,24 +82,10 @@ const ResultStep = ({
66
82
  const companyName = Homeflow.get('company_name');
67
83
  const gmapsKey = Homeflow.get('theme_preferences').google_maps_api_key;
68
84
 
69
- if (!valuation) return <Loader className="hfjs-instant-val__loader" />;
85
+ if (!valuation || loading) return <Loader className="hfjs-instant-val__loader" />;
70
86
 
71
87
  const streetviewQuery = `size=900x900&location=${lead.full_address},${search.postcode}&key=${gmapsKey}`;
72
88
 
73
- if (valuation.price === 0) {
74
- return (
75
- <div id="no_results">
76
- <p>
77
- Unfortunately we do not have enough data to give you an accurate valuation. Your local
78
- {' '}
79
- {companyName}
80
- {' '}
81
- property expert will be in touch to arrange an accurate valuation taking
82
- into account improvements to your property, the local market and more.
83
- </p>
84
- </div>
85
- );
86
- }
87
89
 
88
90
  return (
89
91
  <div className="result-container">
@@ -11,6 +11,7 @@ const SimilarPropertiesStep = ({
11
11
  getSimilarProperties,
12
12
  toggleSimilarProperty,
13
13
  reset,
14
+ loading,
14
15
  }) => {
15
16
  useEffect(() => {
16
17
  getSimilarProperties();
@@ -24,7 +25,7 @@ const SimilarPropertiesStep = ({
24
25
 
25
26
  const { properties: selectedProperties } = search;
26
27
 
27
- if (!similarProperties.length) return <Loader className="hfjs-instant-val__loader" />;
28
+ if (!similarProperties.length || loading) return <Loader className="hfjs-instant-val__loader" />;
28
29
 
29
30
  return (
30
31
  <form id="similar-properties-form" onSubmit={handleSubmit}>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homeflowjs",
3
- "version": "0.10.16",
3
+ "version": "0.10.18",
4
4
  "description": "JavaScript toolkit for Homeflow themes",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -27,7 +27,7 @@ const RegisterForm = ({ localUser, registering }) => (
27
27
  </div>
28
28
 
29
29
  <div className="profile-register-form__group">
30
- <UserInput name="tel_home" required />
30
+ <UserInput name="tel_home" required pattern="^[+]?[0-9]{9,12}$" />
31
31
  <label htmlFor="user-input-tel_home" className={localUser.tel_home ? 'shrink' : ''}>Phone</label>
32
32
  </div>
33
33
 
@@ -10,7 +10,7 @@ const SignInForm = ({ userCredentials }) => (
10
10
  <UserSignInForm className="profile-sign-in">
11
11
  <fieldset>
12
12
  <div className="profile-sign-in__group">
13
- <SignInInput name="email" required />
13
+ <SignInInput name="email" required type="email"/>
14
14
  <label htmlFor="sign-in-input-email" className={userCredentials.email ? 'shrink' : ''}>Email</label>
15
15
  </div>
16
16