homeflowjs 1.0.37 → 1.0.39

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.
@@ -54,7 +54,7 @@ export const setUserCredentials = (payload) => ({
54
54
  payload,
55
55
  });
56
56
 
57
- export const createUser = (payload) => (dispatch, getState) => {
57
+ export const createUser = (payload, recaptchaData = {}) => (dispatch, getState) => {
58
58
  const { user: { localUser }, app: { authenticityToken } } = getState();
59
59
 
60
60
  return fetch('/user.ljson', {
@@ -63,6 +63,7 @@ export const createUser = (payload) => (dispatch, getState) => {
63
63
  body: JSON.stringify({
64
64
  client: payload,
65
65
  authenticity_token: authenticityToken,
66
+ ...recaptchaData,
66
67
  }),
67
68
  })
68
69
  .then(async (response) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homeflowjs",
3
- "version": "1.0.37",
3
+ "version": "1.0.39",
4
4
  "sideEffects": [
5
5
  "modal/**/*",
6
6
  "user/default-profile/**/*",
@@ -73,6 +73,6 @@
73
73
  "jest": "^26.6.0",
74
74
  "msw": "^0.28.1",
75
75
  "react-autosuggest": "^10.0.2",
76
- "redux-mock-store": "^1.5.4"
76
+ "redux-mock-store": "^1.5.5"
77
77
  }
78
78
  }
@@ -13,21 +13,45 @@ export default function PropertyResultsPaginationItem({
13
13
  LeftChevron,
14
14
  RightChevron,
15
15
  }) {
16
- const activeClassName = (className) => (
17
- currentPage === pageNumber ? `${className}--active` : ''
18
- );
16
+ const activeClassName = (className) =>
17
+ currentPage === pageNumber ? `${className}--active` : '';
18
+
19
+ const pageLink = () => {
20
+ if (window.location.pathname.includes('page-')) {
21
+ return window.location.pathname.replace(/page-\d+/, `page-${pageNumber}`);
22
+ }
23
+ return `${window.location.pathname}/page-${pageNumber}`;
24
+ };
25
+
19
26
  return (
20
- <li className={`property-results-pagination__item ${activeClassName('property-results-pagination__item')}`}>
21
- <a href={`${window.location.pathname}/page-${pageNumber}`} className={`property-results-pagination__link ${activeClassName('property-results-pagination__link')}`}>
27
+ <li
28
+ className={`property-results-pagination__item ${activeClassName(
29
+ 'property-results-pagination__item'
30
+ )}`}
31
+ >
32
+ <a
33
+ href={pageLink()}
34
+ className={`property-results-pagination__link ${activeClassName(
35
+ 'property-results-pagination__link'
36
+ )}`}
37
+ >
22
38
  {previous && (
23
39
  <>
24
40
  {!noChevrons && <LeftChevron />}
25
- {!noPrevOrNextText && <span className="property-results-pagination__link-prev-text">Previous</span>}
41
+ {!noPrevOrNextText && (
42
+ <span className="property-results-pagination__link-prev-text">
43
+ Previous
44
+ </span>
45
+ )}
26
46
  </>
27
47
  )}
28
48
  {next && (
29
49
  <>
30
- {!noPrevOrNextText && <span className="property-results-pagination__link-next-text">Next</span>}
50
+ {!noPrevOrNextText && (
51
+ <span className="property-results-pagination__link-next-text">
52
+ Next
53
+ </span>
54
+ )}
31
55
  {!noChevrons && <RightChevron />}
32
56
  </>
33
57
  )}
@@ -22,78 +22,110 @@ export default function PropertyResultsPagination({
22
22
  current_page: currentPage,
23
23
  has_next_page: hasNextPage,
24
24
  has_prev_page: hasPrevPage,
25
- total_count: totalCount,
26
25
  page_count: pageCount,
27
26
  } = pagination;
28
27
 
29
28
  /**
30
- * Generates an array of pagination increments (the middle numbers in the pagination)
31
- * based on the current page and total page count.
32
- * For example the 2,3,4 and 5 in < 1 ... 2 - 3 - 4 - 5 ... 29 >
33
- *
29
+ * Generates an array of pagination with ellipsis '...' where appropriate
34
30
  * @example
35
31
  * currentPage = 1;
36
- * pageCount = 100;
37
- * 'Should output: [2, 3, 4, 5]'
32
+ * pageCount = 1;
33
+ * paginationIncrements = 4;
34
+ * 'Should output: [1]'
38
35
  *
39
36
  * currentPage = 1;
40
37
  * pageCount = 5;
41
- *'Should output: [2, 3, 4]'
38
+ * paginationIncrements = 4;
39
+ *'Should output: ['1', '2', '3', '4', '5']'
40
+ *
41
+ * currentPage = 1;
42
+ * pageCount = 3;
43
+ * paginationIncrements = 4;
44
+ *'Should output: ['1', '2','3']'
45
+ *
46
+ * currentPage = 1;
47
+ * pageCount = 100;
48
+ * paginationIncrements = 4;
49
+ *'Should output: ['1', '2', '3', '4', '5', '...', '100']'
42
50
  *
43
51
  * currentPage = 2;
44
52
  * pageCount = 100;
45
- *'Should output: [2, 3, 4, 5]'
53
+ * paginationIncrements = 4;
54
+ * 'Should output: ['1', '2', '3', '4', '5', '...', '100']'
46
55
  *
47
56
  * currentPage = 3;
48
57
  * pageCount = 100;
49
- *'Should output: [ 3, 4, 5, 6]'
58
+ * paginationIncrements = 4;
59
+ * 'Should output: ['1', '...', '3', '4', '5', '6', '...', '100']'
50
60
  *
51
- * currentPage = 100;
61
+ * currentPage = 99;
52
62
  * pageCount = 100;
53
- * 'Should output: [ 96, 97, 98, 99]'
63
+ * paginationIncrements = 4;
64
+ * 'Should output: ['1', '...', '96', '97', '98', '99', '100']'
54
65
  *
55
- * currentPage = 3;
56
- * pageCount = 3;
57
- * 'Should output: [ 2 ]'
66
+ * currentPage = 96;
67
+ * pageCount = 100;
68
+ * paginationIncrements = 4;
69
+ * 'Should output: ['1', '...', '96', '97', '98', '99', '100']'
58
70
  *
59
- * currentPage = 1;
60
- * pageCount = 3;
61
- * 'Should output: [ 2 ]'
62
- * @returns {number[]} An array of page numbers for pagination.
71
+ * currentPage = 100;
72
+ * pageCount = 100;
73
+ * paginationIncrements = 4;
74
+ * 'Should output: ['1', '...','96','97', '98', '99', '100']'
75
+ *
76
+ * @returns {string[]} An array of strings for pagination.
63
77
  */
64
- const getPaginationIncrements = () => {
65
- if (pageCount === 1) return [];
66
-
67
- const pagination = [];
68
- let startIncrement = currentPage;
69
- let endIncrement = currentPage + paginationIncrements;
70
- if (currentPage === 1) {
71
- startIncrement++;
72
- endIncrement++;
78
+ const generatePagination = () => {
79
+ if (pageCount === 1) return ['1'];
80
+
81
+ let pages = [];
82
+
83
+ if (pageCount <= paginationIncrements + 1) {
84
+ for (let i = 1; i <= pageCount; i++) {
85
+ pages.push(i.toString());
86
+ }
87
+ return pages;
73
88
  }
74
89
 
75
- if (endIncrement >= pageCount) {
76
- endIncrement = pageCount;
90
+ if (currentPage > 1 && currentPage !== 2) {
91
+ pages.push('1', '...');
77
92
  }
78
93
 
79
- // If on the final page
80
- if (currentPage === pageCount) {
81
- startIncrement = pageCount >= paginationIncrements
82
- ? currentPage - paginationIncrements : currentPage - (paginationIncrements - currentPage);
83
- endIncrement = currentPage;
94
+ if (currentPage === 2) {
95
+ pages.push('1');
84
96
  }
85
97
 
86
- for (let i = startIncrement; i < endIncrement; i++) {
87
- pagination.push(i);
98
+ const getStart = () => {
99
+ if (currentPage + paginationIncrements >= pageCount)
100
+ return pageCount - paginationIncrements;
101
+ return Math.max(currentPage, 1);
102
+ };
103
+
104
+ const getEnd = () => {
105
+ if (currentPage + paginationIncrements >= pageCount) return pageCount;
106
+ if (currentPage === 1) return paginationIncrements + 1;
107
+ return Math.min(currentPage + (paginationIncrements - 1), pageCount);
108
+ };
109
+
110
+ const start = getStart();
111
+ const end = getEnd();
112
+
113
+ for (let i = start; i <= end; i++) {
114
+ pages.push(i.toString());
115
+ }
116
+
117
+ if (end < pageCount) {
118
+ pages.push('...', pageCount.toString());
88
119
  }
89
- return pagination;
120
+
121
+ return pages;
90
122
  };
91
123
 
92
124
  return (
93
125
  <div className="property-results-pagination" {...otherProps}>
94
126
  <ul className="property-results-pagination__links">
95
127
  {/* Previous page */}
96
- {hasPrevPage && (!noChevrons && !noPrevOrNextText) && (
128
+ {hasPrevPage && !noChevrons && !noPrevOrNextText && (
97
129
  <PropertyResultsPaginationItem
98
130
  previous
99
131
  pageNumber={currentPage - 1}
@@ -102,45 +134,28 @@ export default function PropertyResultsPagination({
102
134
  />
103
135
  )}
104
136
 
105
- {/* First page */}
106
- {pageCount > 1 && (
107
- <PropertyResultsPaginationItem pageNumber={1} currentPage={currentPage} />
108
- )}
109
-
110
- {/* Ellipsis */}
111
- {totalCount > paginationIncrements && (
112
- <li className="property-results-pagination__item property-results-pagination__item--ellipsis">
113
- <span className="property-results-pagination__ellipsis">&hellip;</span>
114
- </li>
115
- )}
116
-
117
- {/* Pagination increments */}
118
- {getPaginationIncrements().map((pageNumber) => (
119
- <PropertyResultsPaginationItem
120
- key={pageNumber}
121
- pageNumber={pageNumber}
122
- currentPage={currentPage}
123
- />
124
- ))}
125
-
126
- {/* Ellipsis */}
127
- {totalCount > paginationIncrements && (
128
- <li className="property-results-pagination__item property-results-pagination__item--ellipsis">
129
- <span className="property-results-pagination__ellipsis">&hellip;</span>
130
- </li>
131
- )}
132
-
133
- {/* Total page count */}
134
- {showIfPageCountIsOne && pageCount !== 0 ? (
135
- <PropertyResultsPaginationItem pageNumber={pageCount} currentPage={currentPage} />
136
- ) : (
137
- pageCount > 1 && (
138
- <PropertyResultsPaginationItem pageNumber={pageCount} currentPage={currentPage} />
139
- )
140
- )}
141
-
142
- {/* Previous page */}
143
- {hasNextPage && (!noChevrons && !noPrevOrNextText) && (
137
+ {generatePagination().map((page) => {
138
+ if (page === '...') {
139
+ // Ellipsis
140
+ return (
141
+ <li className="property-results-pagination__item property-results-pagination__item--ellipsis">
142
+ <span className="property-results-pagination__ellipsis">
143
+ &hellip;
144
+ </span>
145
+ </li>
146
+ );
147
+ }
148
+ return (
149
+ <PropertyResultsPaginationItem
150
+ key={page}
151
+ pageNumber={parseInt(page, 10)}
152
+ currentPage={currentPage}
153
+ />
154
+ );
155
+ })}
156
+
157
+ {/* Next page */}
158
+ {hasNextPage && !noChevrons && !noPrevOrNextText && (
144
159
  <PropertyResultsPaginationItem
145
160
  next
146
161
  pageNumber={currentPage + 1}
@@ -1,11 +1,13 @@
1
1
  import React from 'react';
2
- import { render, screen, waitFor, cleanup } from '@testing-library/react';
3
- import userEvent from '@testing-library/user-event'
2
+ import { Provider } from 'react-redux';
3
+ import { HashRouter } from 'react-router-dom';
4
+ import { fireEvent, render } from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
4
6
  import { rest } from 'msw';
5
7
  import { setupServer } from 'msw/node';
8
+ import configureStore from 'redux-mock-store';
6
9
 
7
- import withHomeflowState, { resetStore } from '../../../app/with-homeflow-state';
8
- import store from '../../../store';
10
+ import withHomeflowState, { resetStore } from '../../../app/with-homeflow-state';
9
11
  import SignInForm from './sign-in-form.component';
10
12
  import UserProfileModal from '../user-profile/user-profile-modal.component';
11
13
 
@@ -57,7 +59,9 @@ describe('SignInForm Component', () => {
57
59
  });
58
60
 
59
61
  it('reports successful sign in', async () => {
60
- const { findByText, findAllByText, getByText, getAllByText } = render(withHomeflowState(UserProfileModal));
62
+ const {
63
+ findByText, findAllByText, getByText, getAllByText,
64
+ } = render(withHomeflowState(UserProfileModal));
61
65
 
62
66
  await findByText('Register or Sign in');
63
67
  userEvent.click(getByText('Register or Sign in'));
@@ -71,7 +75,7 @@ describe('SignInForm Component', () => {
71
75
  const { findByText, getByText } = render(withHomeflowState(SignInForm));
72
76
 
73
77
  server.use(
74
- rest.post('/session', (req, res, ctx) => res(ctx.status(401))),
78
+ rest.post('/session', (req, res, ctx) => res(ctx.status(401))),
75
79
  );
76
80
  jest.spyOn(console, 'error').mockImplementation(() => {});
77
81
 
@@ -81,7 +85,9 @@ describe('SignInForm Component', () => {
81
85
  });
82
86
 
83
87
  it('reports succesful sign out', async () => {
84
- const { findByText, findAllByText, getByText, getAllByText } = render(withHomeflowState(UserProfileModal));
88
+ const {
89
+ findByText, findAllByText, getByText, getAllByText,
90
+ } = render(withHomeflowState(UserProfileModal));
85
91
 
86
92
  await findByText('Register or Sign in');
87
93
  userEvent.click(getByText('Register or Sign in'));
@@ -98,7 +104,7 @@ describe('SignInForm Component', () => {
98
104
 
99
105
  it('clears sign-in credentials on sign-out', async () => {
100
106
  const {
101
- findByText, findAllByText, getAllByLabelText, getByText, getAllByText
107
+ findByText, findAllByText, getAllByLabelText, getByText, getAllByText,
102
108
  } = render(withHomeflowState(UserProfileModal));
103
109
 
104
110
  await findByText('Register or Sign in');
@@ -121,29 +127,34 @@ describe('SignInForm Component', () => {
121
127
  });
122
128
 
123
129
  it('populates new user data in profile edit page afte user registration', async () => {
124
- const {
125
- findByText, findAllByText, findByRole, getByLabelText, getAllByLabelText, getByRole, getByText, getAllByText
126
- } = render(withHomeflowState(UserProfileModal));
127
-
128
- await findByText('Register or Sign in');
129
- userEvent.click(getByText('Register or Sign in'));
130
- await findByText('Welcome, Guest.');
131
- userEvent.type(getByLabelText('First name'), user.first_name);
132
- userEvent.type(getByLabelText('Last name'), user.last_name);
133
- userEvent.type(getAllByLabelText('Email')[0], user.email);
134
- userEvent.type(getByLabelText('Phone'), user.pohne);
135
- userEvent.type(getAllByLabelText('Password')[0], 'password');
136
- userEvent.type(getByLabelText('Confirm password'), 'password');
137
- userEvent.click(getByRole('button', { name: 'Register' }));
130
+ const mockStore = configureStore([]);
131
+ const store = mockStore({
132
+ user: { currentUser: user, localUser: user },
133
+ app: { loading: { user: false }},
134
+ properties: { savedProperties: [] },
135
+ search: { savedSearches: [] },
136
+ });
137
+
138
+ const { findByText, getByDisplayValue } = render(
139
+ <Provider store={store}>
140
+ <HashRouter>
141
+ <UserProfileModal />
142
+ </HashRouter>
143
+ </Provider>,
144
+ );
138
145
 
139
- await findByText('Welcome, Test.');
140
- userEvent.click(getByText('Edit your profile'));
146
+ expect(findByText(`Welcome, ${user.first_name}`)).toBeTruthy();
147
+ expect(findByText(`${user.first_name} ${user.last_name}`)).toBeTruthy();
148
+ expect(findByText(user.email)).toBeTruthy();
149
+ expect(findByText(user.tel_home)).toBeTruthy();
150
+ expect(findByText('Edit your profile')).toBeTruthy();
141
151
 
142
- await findByRole('heading', { name: 'Edit Your Profile' });
152
+ const button = await findByText('Edit your profile');
153
+ fireEvent.click(button);
143
154
 
144
- expect(getByLabelText('First name')).toHaveValue(user.first_name);
145
- expect(getByLabelText('Last name')).toHaveValue(user.last_name);
146
- expect(getByLabelText('Email')).toHaveValue(user.email);
147
- expect(getByLabelText('Phone')).toHaveValue(user.tel_home);
155
+ getByDisplayValue(user.first_name);
156
+ getByDisplayValue(user.last_name);
157
+ getByDisplayValue(user.tel_home);
158
+ getByDisplayValue(user.email);
148
159
  });
149
160
  });
@@ -1,6 +1,7 @@
1
- import React from 'react';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
2
  import { connect } from 'react-redux';
3
3
  import PropTypes from 'prop-types';
4
+ import { useRecaptcha } from '../../hooks';
4
5
 
5
6
  import { setLocalUser, createUser } from '../../actions/user.actions';
6
7
  import { setLoading } from '../../actions/app.actions';
@@ -19,6 +20,9 @@ const UserRegisterForm = (props) => {
19
20
  ...otherProps
20
21
  } = props;
21
22
 
23
+ const formRef = useRef(null);
24
+ const [isRequestCompleted, setIsRequestCompleted] = useState(null);
25
+
22
26
  const validateForm = () => {
23
27
  if (localUser.password === localUser.password_confirmation) return true;
24
28
 
@@ -26,9 +30,8 @@ const UserRegisterForm = (props) => {
26
30
  return false;
27
31
  };
28
32
 
29
- const handleSubmit = (e) => {
30
- e.preventDefault();
31
- const { body } = Object.fromEntries(new FormData(e.target));
33
+ const onRecaptchaSubmit = (recaptchaData) => {
34
+ const { body } = Object.fromEntries(new FormData(formRef.current));
32
35
  const formData = {
33
36
  ...localUser,
34
37
  body,
@@ -39,37 +42,61 @@ const UserRegisterForm = (props) => {
39
42
  ...localUser.marketing_preferences, opt_in_marketing_statement: marketingStatement,
40
43
  };
41
44
 
45
+ createUser(userParams)
46
+ .then(() => {
47
+ setLoading({ userRegister: false });
48
+ notify('You have been successfully registered!', 'success');
49
+
50
+ if (redirectHash) {
51
+ window.location.hash = redirectHash;
52
+ }
53
+ })
54
+ .catch(() => {
55
+ if (userParams?.body) {
56
+ window.location.href = '/';
57
+ } else if(errors?.password){
58
+ // this could be used for any kind of errors but we only need it for password errors right now
59
+ errors.password.forEach((error) => notify(error, 'error', { duration: 8000 }))
60
+ } else {
61
+ notify(
62
+ 'Sorry, something went wrong. You may already have an account with this email address, if so, please try signing in instead of registering.',
63
+ 'error',
64
+ { duration: 8000 },
65
+ );
66
+ }
67
+ })
68
+ .finally(setIsRequestCompleted((prevState) => !prevState))
69
+ }
70
+
71
+ const { onRecaptchaRender, onResetRecaptcha } = useRecaptcha({
72
+ formID: 'registration-form',
73
+ formRef,
74
+ onRecaptchaSubmit,
75
+ });
76
+
77
+ const handleSubmit = (e) => {
78
+ e.preventDefault();
79
+
42
80
  if (validateForm()) {
43
81
  setLoading({ userRegister: true });
44
-
45
- createUser(userParams)
46
- .then(() => {
47
- setLoading({ userRegister: false });
48
- notify('You have been successfully registered!', 'success');
49
-
50
- if (redirectHash) {
51
- window.location.hash = redirectHash;
52
- }
53
- })
54
- .catch((errors) => {
55
- if (userParams?.body) {
56
- window.location.href = '/';
57
- } else if(errors?.password){
58
- // this could be used for any kind of errors but we only need it for password errors right now
59
- errors.password.forEach((error) => notify(error, 'error', { duration: 8000 }))
60
- } else {
61
- notify(
62
- 'Sorry, something went wrong. You may already have an account with this email address, if so, please try signing in instead of registering.',
63
- 'error',
64
- { duration: 8000 },
65
- );
66
- }
67
- });
82
+ setIsRequestCompleted(false);
83
+ onRecaptchaRender();
68
84
  }
69
85
  };
70
86
 
87
+ useEffect(() => {
88
+ if(isRequestCompleted) {
89
+ onResetRecaptcha();
90
+ }
91
+ }, [isRequestCompleted])
92
+
93
+
71
94
  return (
72
- <form onSubmit={handleSubmit} {...otherProps}>
95
+ <form
96
+ onSubmit={handleSubmit}
97
+ ref={formRef}
98
+ {...otherProps}
99
+ >
73
100
  <input
74
101
  type="text"
75
102
  name="body"