homeflowjs 0.9.21 → 0.9.24

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.
@@ -0,0 +1,52 @@
1
+ import ArticlesActionTypes from './articles.types';
2
+ import { setLoading } from './app.actions';
3
+
4
+ export const setArticles = (payload) => ({
5
+ type: ArticlesActionTypes.SET_ARTICLES,
6
+ payload,
7
+ });
8
+
9
+ export const loadNextPage = () => (dispatch, getState) => {
10
+ if (!getState().articles.pagination.has_next_page) return null;
11
+
12
+ dispatch(setLoading({ articles: true }));
13
+
14
+ const currentArticles = [...getState().articles.articles];
15
+ const page = getState().articles.pagination.current_page + 1;
16
+ const currentTopic = getState().articles.topics.current_topic;
17
+ let url = `/articles.ljson?page=${page}`;
18
+ if (currentTopic) url = `${url}&topic=${currentTopic}`;
19
+
20
+ return fetch(url)
21
+ .then((response) => {
22
+ if (response.ok) return response.json();
23
+
24
+ throw Object.assign(
25
+ new Error('There\'s been an error fetching articles'),
26
+ { code: 404 },
27
+ );
28
+ })
29
+ .then((json) => {
30
+ if (json.articles.length > 0) {
31
+ const updateArticlesStore = {
32
+ articles: [...currentArticles, ...json.articles],
33
+ pagination: json.pagination,
34
+ topics: json.topics,
35
+ };
36
+ dispatch(setArticles(updateArticlesStore));
37
+ dispatch(setLoading({ articles: false }));
38
+
39
+ return true;
40
+ }
41
+
42
+ throw Object.assign(
43
+ new Error('No more articles found'),
44
+ { code: 404 },
45
+ );
46
+ }).catch((error) => {
47
+ console.error(error);
48
+ dispatch(setLoading({ articles: false }));
49
+
50
+ return false;
51
+ });
52
+ };
@@ -0,0 +1,5 @@
1
+ const ArticlesActionTypes = {
2
+ SET_ARTICLES: 'SET_ARTICLES',
3
+ };
4
+
5
+ export default ArticlesActionTypes;
@@ -1,88 +1,24 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import Loader from '../shared/loader.component';
3
+ import { useSelector } from 'react-redux';
4
4
 
5
5
  const ArticlesGrid = ({
6
- handleIsLoading,
7
- pageSize,
8
6
  gridClass,
9
- articleItem,
10
- loadMoreButtonContainerClass,
11
- loadMoreButtonClass,
12
- loadMoreButtonText,
13
- // eslint-disable-next-line react/prop-types
14
- customLoader,
7
+ ArticleItem,
8
+ noArticlesClass,
15
9
  }) => {
16
- const [pagination, setPagination] = useState({});
17
- const [articles, setArticles] = useState([]);
18
- const [page, setPage] = useState(1);
19
- const [showMore, setShowMore] = useState(true);
20
- const [isLoading, setIsLoading] = useState(true);
21
- const [loadingMore, setLoadingMore] = useState(false);
10
+ const storeArticles = useSelector((state) => state.articles.articles) || [];
22
11
 
23
- const agent = `${window.location.protocol}//${window.location.host}`;
24
- const path = window.location.pathname;
25
- const topicPath = '/articles/topic-';
26
-
27
- const topic = path.includes(topicPath)
28
- ? `&topic=${path.slice(path.indexOf(topicPath) + topicPath.length).replace('#/', '')}`
29
- : '';
30
- const url = `${agent}/articles.ljson?page=${page}&page_size=${pageSize}${topic}`;
31
-
32
- const loader = () => {
33
- if (customLoader) return customLoader;
34
- return <Loader containerClass={loadMoreButtonContainerClass} />;
35
- };
36
-
37
- const fetchArticles = (firstRender = false) => {
38
- fetch(url)
39
- .then((response) => response.json())
40
- .then((json) => {
41
- setArticles((prev) => [...prev, ...json.articles]);
42
- setPagination(json.pagination);
43
- setShowMore(json.pagination.has_next_page);
44
- setPage(json.pagination.current_page + 1);
45
- setIsLoading(false);
46
- setLoadingMore(false);
47
- if (handleIsLoading && firstRender) handleIsLoading(false);
48
- })
49
- .catch((e) => console.log('error', e));
50
- };
51
-
52
- const loadMore = () => {
53
- if (pagination.has_next_page) {
54
- setLoadingMore(true);
55
- fetchArticles();
56
- }
57
- return false;
58
- };
59
-
60
- useEffect(() => {
61
- if (handleIsLoading) handleIsLoading(true);
62
- fetchArticles(true);
63
- }, []);
12
+ if (!storeArticles.length) {
13
+ return (
14
+ <div className={noArticlesClass}>No articles found...</div>
15
+ );
16
+ }
64
17
 
65
18
  return (
66
19
  <>
67
20
  <div className={gridClass}>
68
- {articles.map((article) => articleItem(article))}
69
- </div>
70
- <div className={loadMoreButtonContainerClass}>
71
- {(showMore && !isLoading) && (
72
- <>
73
- {loadingMore ? (
74
- loader()
75
- ) : (
76
- <button
77
- type="button"
78
- onClick={loadMore}
79
- className={loadMoreButtonClass}
80
- >
81
- {loadMoreButtonText}
82
- </button>
83
- )}
84
- </>
85
- )}
21
+ {storeArticles.map((article) => <ArticleItem key={article.id} article={article} />)}
86
22
  </div>
87
23
  </>
88
24
  );
@@ -90,20 +26,12 @@ const ArticlesGrid = ({
90
26
 
91
27
  ArticlesGrid.propTypes = {
92
28
  gridClass: PropTypes.string.isRequired,
93
- articleItem: PropTypes.elementType.isRequired,
94
- handleIsLoading: PropTypes.elementType,
95
- loadMoreButtonClass: PropTypes.string,
96
- loadMoreButtonText: PropTypes.string,
97
- loadMoreButtonContainerClass: PropTypes.string,
98
- pageSize: PropTypes.number,
29
+ ArticleItem: PropTypes.elementType.isRequired,
30
+ noArticlesClass: PropTypes.string,
99
31
  };
100
32
 
101
33
  ArticlesGrid.defaultProps = {
102
- loadMoreButtonClass: '',
103
- loadMoreButtonContainerClass: '',
104
- loadMoreButtonText: 'Load more',
105
- handleIsLoading: null,
106
- pageSize: 12,
34
+ noArticlesClass: '',
107
35
  };
108
36
 
109
37
  export default ArticlesGrid;
@@ -0,0 +1,76 @@
1
+ import React, { useState } from 'react';
2
+ import { useSelector } from 'react-redux';
3
+ import PropTypes from 'prop-types';
4
+
5
+ const TopicSelector = ({
6
+ containerClass,
7
+ children,
8
+ buttonClassShowTopics,
9
+ buttonClassHideTopics,
10
+ listWrapperClass,
11
+ ulClass,
12
+ listAnchorTagClass,
13
+ }) => {
14
+ const [showTopics, setShowTopics] = useState(false);
15
+ const storeTopics = useSelector((state) => state.articles.topics.topics_list) || [];
16
+ const iterableTopics = [...storeTopics, 'all topics'];
17
+
18
+ const toggleTopics = (e) => {
19
+ e.preventDefault();
20
+ setShowTopics((prev) => !prev);
21
+ };
22
+
23
+ return (
24
+ <div className={containerClass}>
25
+ <button
26
+ type="button"
27
+ className={` ${showTopics ? buttonClassShowTopics : buttonClassHideTopics}`}
28
+ onClick={(e) => toggleTopics(e)}
29
+ >
30
+ {children}
31
+ </button>
32
+ <div
33
+ className={listWrapperClass}
34
+ style={{ display: showTopics ? 'block' : 'none' }}
35
+ >
36
+ <ul tabIndex={-1} className={ulClass}>
37
+ {iterableTopics.map((topic) => (
38
+ <li key={topic}>
39
+ <a
40
+ className={listAnchorTagClass}
41
+ href={
42
+ topic === 'all topics'
43
+ ? '/articles'
44
+ : `/articles/topic-${topic}`
45
+ }
46
+ >
47
+ {topic}
48
+ </a>
49
+ </li>
50
+ ))}
51
+ </ul>
52
+ </div>
53
+ </div>
54
+ );
55
+ };
56
+
57
+ TopicSelector.propTypes = {
58
+ containerClass: PropTypes.string,
59
+ children: PropTypes.node.isRequired,
60
+ buttonClassShowTopics: PropTypes.string,
61
+ buttonClassHideTopics: PropTypes.string,
62
+ listWrapperClass: PropTypes.string,
63
+ ulClass: PropTypes.string,
64
+ listAnchorTagClass: PropTypes.string,
65
+ };
66
+
67
+ TopicSelector.defaultProps = {
68
+ containerClass: '',
69
+ buttonClassShowTopics: '',
70
+ buttonClassHideTopics: '',
71
+ listWrapperClass: '',
72
+ ulClass: '',
73
+ listAnchorTagClass: '',
74
+ };
75
+
76
+ export default TopicSelector;
package/articles/index.js CHANGED
@@ -1,5 +1,9 @@
1
1
  import ArticlesGrid from './articles-grid.component';
2
+ import LoadMoreButton from './load-more-button.component';
3
+ import TopicSelector from './articles-topic-selector.component';
2
4
 
3
5
  export {
4
6
  ArticlesGrid,
7
+ LoadMoreButton,
8
+ TopicSelector,
5
9
  };
@@ -0,0 +1,62 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useSelector, useDispatch } from 'react-redux';
4
+ import Loader from '../shared/loader.component';
5
+ import { loadNextPage } from '../actions/articles.actions';
6
+
7
+ const LoadMoreButton = ({
8
+ CustomLoader, loadMoreButtonContainerClass, children, loadMoreButtonClass, ...otherProps
9
+ }) => {
10
+ const [loading, setLoading] = useState(false);
11
+ const [error, setError] = useState(false);
12
+ const hasNextPage = useSelector((state) => state.articles?.pagination?.has_next_page);
13
+ const storeArticles = useSelector((state) => state.articles.articles) || [];
14
+ const dispatch = useDispatch();
15
+ const loadNextArticlesPage = () => dispatch(loadNextPage());
16
+
17
+ const loadMore = (e) => {
18
+ e.currentTarget.blur();
19
+ setLoading(true);
20
+ loadNextArticlesPage()
21
+ .then((didLoadNextPage) => {
22
+ // if there's an error fetching articles, the load more button won't render (line 33)
23
+ if (!didLoadNextPage) setError(true);
24
+ setLoading(false);
25
+ });
26
+ };
27
+
28
+ if (loading) {
29
+ if (CustomLoader) return <CustomLoader />;
30
+ return <Loader height="24px" />;
31
+ }
32
+
33
+ if (!hasNextPage || !storeArticles.length || error) return null;
34
+
35
+ return (
36
+ <div className={loadMoreButtonContainerClass}>
37
+ <button
38
+ type="button"
39
+ onClick={(e) => loadMore(e)}
40
+ className={loadMoreButtonClass}
41
+ {...otherProps}
42
+ >
43
+ {children}
44
+ </button>
45
+ </div>
46
+ );
47
+ };
48
+
49
+ LoadMoreButton.propTypes = {
50
+ CustomLoader: PropTypes.elementType,
51
+ loadMoreButtonContainerClass: PropTypes.string,
52
+ children: PropTypes.node.isRequired,
53
+ loadMoreButtonClass: PropTypes.string,
54
+ };
55
+
56
+ LoadMoreButton.defaultProps = {
57
+ CustomLoader: null,
58
+ loadMoreButtonContainerClass: '',
59
+ loadMoreButtonClass: '',
60
+ };
61
+
62
+ export default LoadMoreButton;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homeflowjs",
3
- "version": "0.9.21",
3
+ "version": "0.9.24",
4
4
  "description": "JavaScript toolkit for Homeflow themes",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -12,6 +12,7 @@ const INITIAL_STATE = {
12
12
  properties: false,
13
13
  propertiesMap: true,
14
14
  googleMaps: true,
15
+ articles: false,
15
16
  },
16
17
  themePreferences: {},
17
18
  themeSettings: Homeflow.get('theme_settings_defaults') || {},
@@ -45,6 +46,6 @@ const appReducer = (state = INITIAL_STATE, action) => {
45
46
  default:
46
47
  return state;
47
48
  }
48
- }
49
+ };
49
50
 
50
51
  export default appReducer;
@@ -0,0 +1,23 @@
1
+ import ArticlesActionTypes from '../actions/articles.types';
2
+
3
+ const INITIAL_STATE = {
4
+ articles: [],
5
+ pagination: {},
6
+ topics: {},
7
+ };
8
+
9
+ const articlesReducer = (state = INITIAL_STATE, action) => {
10
+ switch (action.type) {
11
+ case ArticlesActionTypes.SET_ARTICLES:
12
+ return {
13
+ ...state,
14
+ articles: action.payload.articles,
15
+ pagination: action.payload.pagination,
16
+ topics: action.payload.topics,
17
+ };
18
+ default:
19
+ return state;
20
+ }
21
+ };
22
+
23
+ export default articlesReducer;
package/reducers/index.js CHANGED
@@ -6,6 +6,7 @@ import appReducer from './app.reducer';
6
6
  // import companyPreferencesReducer from './company-preferences.reducer';
7
7
  import propertiesReducer from './properties.reducer';
8
8
  import branchesReducer from './branches.reducer';
9
+ import articlesReducer from './articles.reducer';
9
10
 
10
11
  const combined = combineReducers({
11
12
  app: appReducer,
@@ -14,12 +15,13 @@ const combined = combineReducers({
14
15
  // companyPreferences: companyPreferencesReducer,
15
16
  properties: propertiesReducer,
16
17
  branches: branchesReducer,
18
+ articles: articlesReducer,
17
19
  });
18
20
 
19
21
  export default (state, action) => {
20
- if (action.type == 'RESET_STORE') {
22
+ if (action.type === 'RESET_STORE') {
21
23
  return combined(undefined, action);
22
- } else {
23
- return combined(state, action);
24
24
  }
25
+
26
+ return combined(state, action);
25
27
  };
@@ -27,7 +27,7 @@ const propertiesReducer = (state = INITIAL_STATE, action) => {
27
27
  return {
28
28
  ...state,
29
29
  properties: state.properties ? [...state.properties, ...action.payload] : action.payload,
30
- }
30
+ };
31
31
  case PropertiesActionTypes.SET_PAGINATION:
32
32
  return {
33
33
  ...state,
@@ -47,7 +47,8 @@ const propertiesReducer = (state = INITIAL_STATE, action) => {
47
47
  const propertyId = parseInt(action.payload.propertyId, 10);
48
48
  const newSavedProperties = [...state.savedProperties];
49
49
 
50
- const index = newSavedProperties.findIndex(({ property_id: searchId }) => searchId === propertyId);
50
+ const index = newSavedProperties
51
+ .findIndex(({ property_id: searchId }) => searchId === propertyId);
51
52
 
52
53
  if (index > -1) {
53
54
  newSavedProperties.splice(index, 1);
@@ -63,7 +64,8 @@ const propertiesReducer = (state = INITIAL_STATE, action) => {
63
64
  }
64
65
 
65
66
  // need to find the property from the propertiesReducer and add it here
66
- const property = state.properties.find(({ property_id: searchId }) => searchId === propertyId);
67
+ const property = state
68
+ .properties.find(({ property_id: searchId }) => searchId === propertyId);
67
69
 
68
70
  newSavedProperties.push(property || state.property);
69
71
 
@@ -84,6 +86,6 @@ const propertiesReducer = (state = INITIAL_STATE, action) => {
84
86
  default:
85
87
  return state;
86
88
  }
87
- }
89
+ };
88
90
 
89
91
  export default propertiesReducer;
@@ -5,11 +5,12 @@ import { setSearchField } from '../../actions/search.actions';
5
5
 
6
6
  // GenericSearchField for anything not already available as a component
7
7
 
8
- const GenericSelect = ({ name, value, children, setSearchField }) => (
8
+ const GenericSelect = ({ name, value, children, setSearchField, ...otherProps }) => (
9
9
  <select
10
10
  name={name}
11
11
  value={value}
12
12
  onChange={e => setSearchField({ [e.target.name]: e.target.value })}
13
+ {...otherProps}
13
14
  >
14
15
  {children}
15
16
  </select>
@@ -108,11 +108,11 @@ const PriceSelect = (props) => {
108
108
  const selectedPrice = type === 'min' ? minPrice : maxPrice;
109
109
 
110
110
  if (type === 'max' && minPrice) {
111
- prices = prices.filter(price => price >= minPrice);
111
+ prices = prices.filter(price => price > minPrice);
112
112
  }
113
113
 
114
114
  if (type === 'min' && maxPrice) {
115
- prices = prices.filter(price => price <= maxPrice);
115
+ prices = prices.filter(price => price < maxPrice);
116
116
  }
117
117
 
118
118
  if (reactSelect) {
@@ -44,7 +44,7 @@ describe('PriceSelect', () => {
44
44
  const wrapper = mount(<PriceSelect {...testProps} />);
45
45
 
46
46
  // includes the default unselected option
47
- expect(wrapper.find('option').length).toEqual(3);
47
+ expect(wrapper.find('option').length).toEqual(2);
48
48
  });
49
49
 
50
50
  it('removes higher prices for min when selected for max', () => {
@@ -60,7 +60,7 @@ describe('PriceSelect', () => {
60
60
  const wrapper = mount(<PriceSelect {...testProps} />);
61
61
 
62
62
  // includes the default unselected option
63
- expect(wrapper.find('option').length).toEqual(4);
63
+ expect(wrapper.find('option').length).toEqual(3);
64
64
  });
65
65
 
66
66
  it('calls the setSearchField prop on change', () => {
package/store.js CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable import/no-cycle */
1
2
  import { createStore, applyMiddleware } from 'redux';
2
3
  import thunk from 'redux-thunk';
3
4
  import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
@@ -5,7 +6,10 @@ import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
5
6
  import rootReducer from './reducers';
6
7
  import { setThemePreferences, setThemeSettings, setAuthenticityToken } from './actions/app.actions';
7
8
  import { fetchUser } from './actions/user.actions';
8
- import { setProperties, setPagination, setProperty, setPropertyLinksAsync } from './actions/properties.actions';
9
+ import {
10
+ setProperties, setPagination, setProperty, setPropertyLinksAsync,
11
+ } from './actions/properties.actions';
12
+ import { setArticles } from './actions/articles.actions';
9
13
  // import { fetchCompanyPreferences } from './actions/company-preferences.actions';
10
14
  // import { fetchCompanyPreferences } from './actions/company-preferences.actions';
11
15
 
@@ -42,4 +46,9 @@ if (Homeflow.get('property')) {
42
46
  store.dispatch(setPropertyLinksAsync());
43
47
  }
44
48
 
49
+ // setting articles, pagination and topics on articles index page
50
+ if (Homeflow.get('articles')) {
51
+ store.dispatch(setArticles(Homeflow.get('articles')));
52
+ }
53
+
45
54
  export default store;