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.
- package/__tests__/instant-valuation.test.jsx +12 -1
- package/branches/branches-search-form/branches-search-input.component.jsx +57 -23
- package/instant-valuation/instant-valuation/instant-valuation.component.jsx +11 -2
- package/instant-valuation/result-step/result-step.component.jsx +17 -15
- package/instant-valuation/similar-properties-step/similar-properties-step.component.jsx +2 -1
- package/package.json +1 -1
- package/user/default-profile/account/register-form.component.jsx +1 -1
- package/user/default-profile/account/sign-in-form.component.jsx +1 -1
@@ -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 {
|
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, {
|
2
|
-
|
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={() =>
|
96
|
+
onSuggestionsClearRequested={() => {
|
97
|
+
setSuggestions([]);
|
98
|
+
if (label) setInputFocused(false);
|
99
|
+
}}
|
62
100
|
onSuggestionsFetchRequested={({ value }) => {
|
63
|
-
setBranchesSearch(value);
|
64
|
-
|
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
|
-
|
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({
|
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
@@ -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
|
|