@abcagency/hc-ui-components 1.3.23 → 1.3.25

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.
Files changed (102) hide show
  1. package/package.json +4 -5
  2. package/src/.editorconfig +12 -0
  3. package/src/apis/hcApi.ts +109 -0
  4. package/src/bundleIndex.js +14 -0
  5. package/src/clientToken.js +9 -0
  6. package/src/components/HireControlMap.js +135 -0
  7. package/src/components/containers/accordions/filter-container.js +48 -0
  8. package/src/components/containers/accordions/filter-item-container.js +66 -0
  9. package/src/components/containers/accordions/map-accordion-item-container.js +70 -0
  10. package/src/components/containers/filter/commute-container.js +89 -0
  11. package/src/components/containers/filter/filter-container.js +76 -0
  12. package/src/components/containers/filter/filter-item-container.js +71 -0
  13. package/src/components/containers/filter/location-container.js +45 -0
  14. package/src/components/containers/filter/points-of-interest-container.js +33 -0
  15. package/src/components/containers/filter/points-of-interest-radio-item-container.js +35 -0
  16. package/src/components/containers/filter/search-container.js +50 -0
  17. package/src/components/containers/jobListing/listing-details-container.js +40 -0
  18. package/src/components/containers/list/item-list-container.tsx +81 -0
  19. package/src/components/containers/list/list-item/list-item-container.js +43 -0
  20. package/src/components/containers/maps/info-window-content-container.js +51 -0
  21. package/src/components/containers/maps/map-container.js +204 -0
  22. package/src/components/containers/maps/map-list-container.js +48 -0
  23. package/src/components/containers/maps/map-marker-container.js +78 -0
  24. package/src/components/modules/accordions/MapAccordionItem.js +30 -0
  25. package/src/components/modules/accordions/default.js +171 -0
  26. package/src/components/modules/accordions/filterItem.js +27 -0
  27. package/src/components/modules/accordions/filters.js +32 -0
  28. package/src/components/modules/buttons/button-group-apply.js +123 -0
  29. package/src/components/modules/buttons/commute-pill.js +22 -0
  30. package/src/components/modules/buttons/default.js +194 -0
  31. package/src/components/modules/buttons/items-pill.js +35 -0
  32. package/src/components/modules/buttons/pill-wrapper.js +27 -0
  33. package/src/components/modules/buttons/show-all-button.js +20 -0
  34. package/src/components/modules/cards/default.js +167 -0
  35. package/src/components/modules/cards/filter.js +56 -0
  36. package/src/components/modules/dialogs/apply-dialog.js +48 -0
  37. package/src/components/modules/filter/commute.js +108 -0
  38. package/src/components/modules/filter/index.js +55 -0
  39. package/src/components/modules/filter/item.js +48 -0
  40. package/src/components/modules/filter/location.js +48 -0
  41. package/src/components/modules/filter/radio-item.js +42 -0
  42. package/src/components/modules/filter/search.js +65 -0
  43. package/src/components/modules/filter/sort.js +83 -0
  44. package/src/components/modules/grid.js +54 -0
  45. package/src/components/modules/icon.js +33 -0
  46. package/src/components/modules/jobListing/listing-details.js +109 -0
  47. package/src/components/modules/list/field-mapper.js +114 -0
  48. package/src/components/modules/list/header-item.js +91 -0
  49. package/src/components/modules/list/header.js +49 -0
  50. package/src/components/modules/list/item-expand-card/index.js +22 -0
  51. package/src/components/modules/list/item-expand-card/recruiter-contact-nav.js +50 -0
  52. package/src/components/modules/list/item-expand-card/recruiter-details.js +68 -0
  53. package/src/components/modules/list/item-expand-card/recruiter-headshot.js +22 -0
  54. package/src/components/modules/list/item-list.tsx +84 -0
  55. package/src/components/modules/list/list-item/list-item.js +130 -0
  56. package/src/components/modules/maps/info-window-card.js +17 -0
  57. package/src/components/modules/maps/info-window-content.js +35 -0
  58. package/src/components/modules/maps/map-list.js +28 -0
  59. package/src/components/modules/maps/map-marker.js +29 -0
  60. package/src/components/modules/maps/map.js +76 -0
  61. package/src/components/modules/maps/place-marker.js +41 -0
  62. package/src/components/modules/maps/tabs.js +81 -0
  63. package/src/constants/eventTypes.js +13 -0
  64. package/src/constants/placeTypes.js +8 -0
  65. package/src/contexts/mapContext.tsx +129 -0
  66. package/src/contexts/mapListContext.tsx +311 -0
  67. package/src/contexts/placesContext.js +102 -0
  68. package/src/contexts/trackEventContext.js +14 -0
  69. package/src/enums/SectionType.ts +9 -0
  70. package/src/hooks/useList.js +89 -0
  71. package/src/index.js +3 -0
  72. package/src/services/configService.ts +16 -0
  73. package/src/services/googlePlacesNearbyService.ts +42 -0
  74. package/src/services/listingAggregatorService.ts +76 -0
  75. package/src/services/listingEntityService.ts +16 -0
  76. package/src/services/listingService.ts +40 -0
  77. package/src/services/recruiterService.ts +18 -0
  78. package/src/styles/bundle.css +268 -0
  79. package/src/styles/index.css +24 -0
  80. package/src/types/Address.ts +7 -0
  81. package/src/types/ContentSection.ts +9 -0
  82. package/src/types/GetListingParams.ts +8 -0
  83. package/src/types/LatLng.ts +4 -0
  84. package/src/types/ListingEntity.ts +11 -0
  85. package/src/types/ListingFields.ts +25 -0
  86. package/src/types/Listings.ts +32 -0
  87. package/src/types/Recruiter.ts +9 -0
  88. package/src/types/SimilarListing.ts +24 -0
  89. package/src/types/config/Colors.ts +8 -0
  90. package/src/types/config/MapConfig.ts +31 -0
  91. package/src/types/config/PointsOfInterestConfig.ts +13 -0
  92. package/src/types/config/SearchConfig.ts +4 -0
  93. package/src/util/arrayUtil.js +3 -0
  94. package/src/util/fieldMapper.js +22 -0
  95. package/src/util/filterUtil.js +239 -0
  96. package/src/util/loading.js +17 -0
  97. package/src/util/localStorageUtil.ts +34 -0
  98. package/src/util/mapIconUtil.js +180 -0
  99. package/src/util/mapUtil.js +91 -0
  100. package/src/util/sortUtil.js +33 -0
  101. package/src/util/stringUtils.js +6 -0
  102. package/src/util/urlFilterUtil.js +85 -0
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@abcagency/hc-ui-components",
3
- "version": "1.3.23",
3
+ "version": "1.3.25",
4
4
  "description": "UI Components for HireControl",
5
5
  "main": "dist/index.js",
6
6
  "files": [
7
- "dist"
7
+ "dist",
8
+ "src"
8
9
  ],
9
10
  "scripts": {
10
11
  "build": "cross-env NODE_ENV=production rollup -c",
@@ -30,9 +31,7 @@
30
31
  "ajv-keywords": "^5.1.0",
31
32
  "framer-motion": "^10.18.0",
32
33
  "fuse.js": "^7.0.0",
33
- "npm": "^10.8.1",
34
- "react": "^18.3.1",
35
- "react-dom": "^18.3.1",
34
+ "npm": "^10.8.1",
36
35
  "react-router-dom": "^6.23.1",
37
36
  "react-scripts": "5.0.1",
38
37
  "react-wrap-balancer": "^1.1.1",
@@ -0,0 +1,12 @@
1
+ # EditorConfig is awesome: http://EditorConfig.org
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ end_of_line = lf
7
+ indent_style = tab
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
@@ -0,0 +1,109 @@
1
+ import { getClientAuthKey } from '~/clientToken';
2
+ const baseURL = process.env.HC_API_BASE_URL as string;
3
+
4
+ interface MemoryStorage {
5
+ authToken: string | null;
6
+ tokenExpiration: string | null;
7
+ }
8
+
9
+ const memoryStorage: MemoryStorage = {
10
+ authToken: null,
11
+ tokenExpiration: null
12
+ };
13
+
14
+ function setStorage(key: keyof MemoryStorage, value: string): void {
15
+ try {
16
+ sessionStorage.setItem(key, value);
17
+ } catch (error) {
18
+ memoryStorage[key] = value;
19
+ }
20
+ }
21
+
22
+ function getStorage(key: keyof MemoryStorage): string | null {
23
+ try {
24
+ return sessionStorage.getItem(key) || memoryStorage[key];
25
+ } catch (error) {
26
+ return memoryStorage[key];
27
+ }
28
+ }
29
+
30
+ interface AuthResponse {
31
+ token: string;
32
+ expiration: string;
33
+ }
34
+
35
+ const login = async (): Promise<AuthResponse> => {
36
+ const clientAuthKey = getClientAuthKey();
37
+
38
+ try {
39
+ const response = await fetch(`${baseURL}/auth/login`, {
40
+ method: 'POST',
41
+ headers: {
42
+ 'Content-Type': 'application/json'
43
+ },
44
+ body: JSON.stringify({
45
+ clientAuthKey: clientAuthKey
46
+ })
47
+ });
48
+
49
+ if (!response.ok) {
50
+ throw new Error('Login failed');
51
+ }
52
+
53
+ const data = await response.json();
54
+
55
+ if (data.token && data.expiration) {
56
+ setStorage('authToken', data.token);
57
+ setStorage('tokenExpiration', data.expiration);
58
+ return { token: data.token, expiration: data.expiration };
59
+ } else {
60
+ throw new Error('Invalid login response');
61
+ }
62
+ } catch (error) {
63
+ console.error('Login failed:', error);
64
+ throw error;
65
+ }
66
+ };
67
+
68
+ const fetchWithAuth = async (url: string, options: RequestInit = {}): Promise<Response> => {
69
+ let token = getStorage('authToken');
70
+
71
+ const expirationDateTime = getStorage('tokenExpiration');
72
+ const currentTime = new Date();
73
+
74
+ if (!token || !expirationDateTime || new Date(expirationDateTime) <= currentTime) {
75
+ const authResponse = await login();
76
+ token = authResponse.token;
77
+ }
78
+
79
+ const headers = new Headers(options.headers || {});
80
+ headers.append('Authorization', `Bearer ${token}`);
81
+
82
+ const finalOptions = {
83
+ ...options,
84
+ headers
85
+ };
86
+
87
+ const response = await fetch(`${baseURL}${url}`, finalOptions);
88
+
89
+ if (!response.ok) throw new Error('Network response was not ok.');
90
+
91
+ return response;
92
+ };
93
+
94
+ export default {
95
+ get: async <T>(url: string): Promise<T> => {
96
+ const response = await fetchWithAuth(url);
97
+ return await response.json() as T;
98
+ },
99
+ post: async <T>(url: string, data: any): Promise<T> => {
100
+ const response = await fetchWithAuth(url, {
101
+ method: 'POST',
102
+ headers: {
103
+ 'Content-Type': 'application/json'
104
+ },
105
+ body: JSON.stringify(data)
106
+ });
107
+ return await response.json() as T;
108
+ }
109
+ };
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import HireControlMap from '~/components/HireControlMap';
4
+
5
+ const initializeComponent = (elementId, props) => {
6
+ const element = document.getElementById(elementId);
7
+ if (element) {
8
+ ReactDOM.render(<div className='hc-bundle'><HireControlMap {...props} /></div>, element);
9
+ } else {
10
+ console.error(`Element with id ${elementId} not found`);
11
+ }
12
+ };
13
+
14
+ export { HireControlMap, initializeComponent };
@@ -0,0 +1,9 @@
1
+ let clientAuthKey = null;
2
+
3
+ export const setClientAuthKey = key => {
4
+ clientAuthKey = key;
5
+ };
6
+
7
+ export const getClientAuthKey = () => {
8
+ return clientAuthKey;
9
+ };
@@ -0,0 +1,135 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { useLoadScript } from '@react-google-maps/api';
3
+
4
+ import Grid from '~/components/modules/grid';
5
+ import Filter from '~/components/containers/filter/filter-container';
6
+ import MapList from '~/components/containers/maps/map-list-container';
7
+
8
+ import { MapProvider } from '~/contexts/mapContext';
9
+ import { PlacesProvider } from '~/contexts/placesContext';
10
+ import { MapListProvider } from '~/contexts/mapListContext';
11
+ import { TrackEventProvider } from '~/contexts/trackEventContext';
12
+
13
+ import { getMapConfig } from '~/services/configService';
14
+ import { setClientAuthKey } from '~/clientToken.js';
15
+
16
+ import '../styles/index.css';
17
+
18
+ const libraries = ['places'];
19
+
20
+ export const HireControlMap = ({
21
+ clientToken,
22
+ navigateToDetails = null,
23
+ navigateToEasyApply = null,
24
+ Link = null,
25
+ linkFormat = '/jobs/[slug]',
26
+ trackEvent = (eventType, eventObj) => {console.log(eventType); console.log(eventObj);},
27
+ listings = [],
28
+ setFiltersUrl = null
29
+ }) => {
30
+ const [siteConfig, setSiteconfig] = useState(null);
31
+
32
+ useEffect(() => {
33
+ setClientAuthKey(clientToken);
34
+ const fetchSiteConfig = async () => {
35
+ try {
36
+ const configData = await getMapConfig(clientToken);
37
+ setSiteconfig(configData);
38
+ } catch (error) {
39
+ console.error('Failed to fetch site configuration:', error);
40
+ }
41
+ };
42
+
43
+ fetchSiteConfig();
44
+ }, [clientToken]);
45
+
46
+ const { isLoaded } = useLoadScript({
47
+ googleMapsApiKey: process.env.GOOGLE_MAPS_API_KEY,
48
+ version: 'quarterly',
49
+ libraries: libraries
50
+ });
51
+
52
+ return (
53
+ <div>
54
+ {isLoaded && siteConfig && (
55
+ <HomeBody
56
+ siteConfig={siteConfig}
57
+ navigateToDetails={navigateToDetails}
58
+ navigateToEasyApply={navigateToEasyApply}
59
+ Link={Link}
60
+ linkFormat={linkFormat}
61
+ trackEvent={trackEvent}
62
+ listings={listings}
63
+ setFiltersUrl={setFiltersUrl}
64
+ />
65
+ )}
66
+ </div>
67
+ );
68
+ };
69
+
70
+ const HomeBody = ({ siteConfig, navigateToDetails, navigateToEasyApply, Link, linkFormat, trackEvent, listings, setFiltersUrl }) => {
71
+ const resetFilters = false;
72
+
73
+ return (
74
+ <TrackEventProvider trackEvent={trackEvent}>
75
+ <MapListProvider
76
+ siteConfig={siteConfig}
77
+ resetFilters={resetFilters}
78
+ avigateToDetails={navigateToDetails}
79
+ navigateToEasyApply={navigateToEasyApply}
80
+ Link={Link}
81
+ linkFormat={linkFormat}
82
+ listings={listings}
83
+ setFiltersUrl={setFiltersUrl}
84
+ >
85
+ <MapProvider resetFilters={resetFilters}>
86
+ <PlacesProvider
87
+ placeMappings={siteConfig.pointsOfInterestConfig.placeMappings ?? {}}
88
+ markerColors={{
89
+ fillColor: siteConfig.colors.primary,
90
+ strokeColor: siteConfig.colors.primaryDark,
91
+ selectedFillColor: siteConfig.colors.secondary,
92
+ selectedStrokeColor: siteConfig.colors.secondaryDark,
93
+ placeMarkers: {
94
+ colors: siteConfig.pointsOfInterestConfig.placeMarkerColors,
95
+ size: siteConfig.pointsOfInterestConfig.placeMarkerSize
96
+ }
97
+ }}
98
+ >
99
+ <Grid
100
+ as='section'
101
+ id='job-search-interface'
102
+ columns='md:hc-grid-cols-[1fr_2.5fr] lg:hc-grid-cols-[1fr_3.5fr]'
103
+ gap='hc-gap-0'
104
+ autoRows={false}
105
+ className='hc-bundle hc-items-stretch hc-h-screen hc-min-h-[30rem] hc-divide-x hc-divide-uiAccent/20'
106
+ >
107
+ <Grid.Item className='hc-bg-gray-100'>
108
+ <Filter showMap={siteConfig.showMap} className='hc-hidden md:hc-block' />
109
+ </Grid.Item>
110
+ <MapList
111
+ markerConfigs={{
112
+ fillColor: siteConfig.colors.primary,
113
+ strokeColor: siteConfig.colors.primaryDark,
114
+ selectedFillColor: siteConfig.colors.secondary,
115
+ selectedStrokeColor: siteConfig.colors.secondaryDark,
116
+ placeMarkers: {
117
+ colors: siteConfig.pointsOfInterestConfig.placeMarkerColors,
118
+ size: siteConfig.pointsOfInterestConfig.placeMarkerSize
119
+ }
120
+ }}
121
+ showMap={siteConfig.showMap}
122
+ fieldsShown={siteConfig.fieldsShown}
123
+ specialFeatures={siteConfig.specialFeatures}
124
+ fieldNames={siteConfig.fieldNames}
125
+ placeMappings={siteConfig.pointsOfInterestConfig.placeMappings ?? {}}
126
+ />
127
+ </Grid>
128
+ </PlacesProvider>
129
+ </MapProvider>
130
+ </MapListProvider>
131
+ </TrackEventProvider>
132
+ );
133
+ };
134
+
135
+ export default HireControlMap;
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+ import { useMapList } from '~/contexts/mapListContext';
3
+ import AccordionFilters from '~/components/modules/accordions/filters';
4
+ import AccordionFilterItem from '~/components/containers/accordions/filter-item-container';
5
+
6
+ const AccordionFiltersContainer = ({
7
+ className,
8
+ defaultValue,
9
+ setDefaultValue,
10
+ setLocation,
11
+ setSelectedListItem,
12
+ SubcategoryRequireCategory
13
+ }) => {
14
+ const { filterOptions, selectedFilters, setSelectedFilters, siteConfig } = useMapList();
15
+
16
+ const handleSetSelectedFilters = prevFilters => {
17
+ setSelectedFilters(prevFilters);
18
+ setLocation(null);
19
+ setSelectedListItem(null);
20
+ };
21
+
22
+ return (
23
+ <AccordionFilters
24
+ className={className}
25
+ defaultValue={defaultValue}
26
+ filterOptions={filterOptions}
27
+ >
28
+ {filterOptions?.filters?.map(filter => {
29
+ if(filter.id === 'category' && SubcategoryRequireCategory === true && (!selectedFilters.categoryClass || Object.keys(selectedFilters.categoryClass).length < 1)){
30
+ return;
31
+ } else if (filter.id === 'category' && SubcategoryRequireCategory === true && filter.items.length > 0) {
32
+ filter.items = filter.items.filter(item => item.count > 0);
33
+ }
34
+ return (<AccordionFilterItem
35
+ key={filter.id}
36
+ filter={filter}
37
+ setDefaultValue={setDefaultValue}
38
+ selectedFilters={selectedFilters}
39
+ setSelectedFilters={handleSetSelectedFilters}
40
+ subcategoryRequireCategory={siteConfig.subcategoryRequireCategory}
41
+ />);
42
+ }
43
+ )}
44
+ </AccordionFilters>
45
+ );
46
+ };
47
+
48
+ export default AccordionFiltersContainer;
@@ -0,0 +1,66 @@
1
+ import React, { memo } from 'react';
2
+ import AccordionFilterItem from '~/components/modules/accordions/filterItem';
3
+ import FilterItem from '~/components/containers/filter/filter-item-container';
4
+ import ItemsPill from '~/components/modules/buttons/items-pill';
5
+
6
+ const FilterItemContainer = ({
7
+ filter,
8
+ setDefaultValue,
9
+ setSelectedFilters,
10
+ selectedFilters,
11
+ subcategoryRequireCategory = false
12
+ }) => {
13
+ const fieldKey = filter.id;
14
+ const activeItemsCount = selectedFilters != null && selectedFilters[fieldKey]
15
+ ? Object.keys(selectedFilters[fieldKey]).length
16
+ : 0;
17
+
18
+ const handleClearFilters = event => {
19
+ event.stopPropagation();
20
+ setSelectedFilters(prevFilters => {
21
+ const updatedFilters = { ...prevFilters };
22
+ if(subcategoryRequireCategory && fieldKey == 'categoryClass'){
23
+ delete updatedFilters['category'];
24
+ }
25
+ delete updatedFilters[fieldKey];
26
+ return updatedFilters;
27
+ });
28
+ };
29
+
30
+ const header = (
31
+ <>
32
+ {filter.title}
33
+ {activeItemsCount > 0 && (
34
+ <ItemsPill
35
+ activeItemsCount={activeItemsCount}
36
+ onClick={handleClearFilters}
37
+ />
38
+ )}
39
+ </>
40
+ );
41
+
42
+ const body = (
43
+ <>
44
+ {filter.items.sort().map(item => (
45
+ <FilterItem
46
+ key={item.name}
47
+ item={item}
48
+ field={filter.id}
49
+ selectedFilters={selectedFilters}
50
+ setSelectedFilters={setSelectedFilters}
51
+ />
52
+ ))}
53
+ </>
54
+ );
55
+
56
+ return (
57
+ <AccordionFilterItem
58
+ id={filter.id}
59
+ setDefaultValue={setDefaultValue}
60
+ header={header}
61
+ body={body}
62
+ />
63
+ );
64
+ };
65
+
66
+ export default memo(FilterItemContainer);
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { useMap } from '~/contexts/mapContext';
3
+ import { useMapList } from '~/contexts/mapListContext';
4
+ import { useTrackEvent } from '~/contexts/trackEventContext';
5
+ import { setStorageObject } from '~/util/localStorageUtil';
6
+ import MapAccordionItem from '~/components/modules/accordions/MapAccordionItem';
7
+ import ListItemContainer from '~/components/containers/list/list-item/list-item-container';
8
+
9
+ const MapAccordionItemContainer = ({
10
+ showMap,
11
+ item,
12
+ itemRefs,
13
+ fieldsShown,
14
+ itemExpandedContent,
15
+ specialFeatures,
16
+ isActive,
17
+ hasListItemSelected
18
+ }) => {
19
+ const { mapItems, recruiters } = useMapList();
20
+ const { selectItem } = useMap();
21
+ const { trackEvent, eventTypes } = useTrackEvent();
22
+
23
+ const setSelectedItemAndZoomMap = (item, isActive) => {
24
+ if (isActive) {
25
+ localStorage.removeItem("selectedListItem");
26
+ const location = mapItems.find(x => Object.prototype.hasOwnProperty.call(x.items, item.id)) || null;
27
+ selectItem(null, null, 9, { lat: 39.8283, lng: -98.5795 });
28
+ } else {
29
+ setStorageObject("selectedListItem", item);
30
+ const location = mapItems.find(x => Object.prototype.hasOwnProperty.call(x.items, item.id)) || null;
31
+ selectItem(item, location, 12, {
32
+ lat: location?.latitude,
33
+ lng: location?.longitude
34
+ });
35
+ }
36
+ };
37
+
38
+ const handleItemClick = item => {
39
+ trackEvent(eventTypes.JOB_LISTING_SELECTED, {
40
+ jobTitle: item.fields.position,
41
+ jobCategory: item.fields.category,
42
+ entityDisplayName: item?.mapDetails?.entityDisplayName
43
+ });
44
+
45
+ setSelectedItemAndZoomMap(item, isActive);
46
+ };
47
+
48
+ return (
49
+ <MapAccordionItem
50
+ item={item}
51
+ itemRefs={itemRefs}
52
+ itemExpandedContent={itemExpandedContent}
53
+ isActive={isActive}
54
+ recruiter={recruiters[item.recruiterId]}
55
+ >
56
+ <ListItemContainer
57
+ showMap={showMap}
58
+ item={item}
59
+ itemRefs={itemRefs}
60
+ fieldsShown={fieldsShown}
61
+ specialFeatures={specialFeatures}
62
+ isActive={isActive}
63
+ hasListItemSelected={hasListItemSelected}
64
+ onClick={() => handleItemClick(item)}
65
+ />
66
+ </MapAccordionItem>
67
+ );
68
+ };
69
+
70
+ export default MapAccordionItemContainer;
@@ -0,0 +1,89 @@
1
+ import React, { useRef, useState, useEffect } from 'react';
2
+ import usePlacesAutocomplete, { getGeocode, getLatLng } from 'use-places-autocomplete';
3
+
4
+ import { useMapList } from '~/contexts/mapListContext';
5
+ import { useTrackEvent } from '~/contexts/trackEventContext';
6
+ import { getStorageItem } from '~/util/localStorageUtil';
7
+ import FilterCommute from '~/components/modules/filter/commute';
8
+
9
+ const FilterCommuteContainer = ({ className }) => {
10
+ const {
11
+ ready,
12
+ suggestions: { status, data },
13
+ setValue,
14
+ clearSuggestions
15
+ } = usePlacesAutocomplete();
16
+
17
+ const [selected, setSelected] = useState(getStorageItem('selectedCommute', ''));
18
+ const inputRef = useRef(null);
19
+ const { setCommuteLocation, commuteLocation } = useMapList();
20
+ const [isCurrentLocation, setIsCurrentLocation] = useState(getStorageItem('isCurrentLocation', false));
21
+ const { trackEvent, eventTypes } = useTrackEvent();
22
+
23
+ useEffect(() => {
24
+ if (commuteLocation !== null && commuteLocation !== '') return;
25
+ setIsCurrentLocation(false);
26
+ localStorage.removeItem('isCurrentLocation');
27
+ localStorage.removeItem('selectedCommute');
28
+ setSelected("");
29
+ }, [commuteLocation]);
30
+
31
+ const handleSelect = async (val, isCurrLocation = false) => {
32
+ setValue(val, false);
33
+ setSelected(val);
34
+ localStorage.setItem('selectedCommute', val);
35
+ clearSuggestions();
36
+ if (isCurrLocation) return;
37
+ try {
38
+ const results = await getGeocode({ address: val });
39
+ const { lat, lng } = await getLatLng(results[0]);
40
+ trackEvent(eventTypes.COMMUTE_ORIGIN_ADDED, { commuteLocation: { lat, lng }, isCurrentLocation: false });
41
+ setCommuteLocation({ lat, lng });
42
+ } catch (error) {
43
+ // no-op
44
+ }
45
+ };
46
+
47
+ const fetchLocation = () => {
48
+ if (!navigator.geolocation) {
49
+ console.error("Geolocation is not supported by this browser.");
50
+ return;
51
+ }
52
+ navigator.geolocation.getCurrentPosition(
53
+ position => {
54
+ const location = {
55
+ lat: position.coords.latitude,
56
+ lng: position.coords.longitude
57
+ };
58
+ setCommuteLocation(location);
59
+ trackEvent(eventTypes.COMMUTE_ORIGIN_ADDED, { commuteLocation: location, isCurrentLocation: true });
60
+ handleSelect("Current Location");
61
+ },
62
+ error => {
63
+ console.error("Error fetching location", error);
64
+ }
65
+ );
66
+ };
67
+
68
+ return (
69
+ <FilterCommute
70
+ className={className}
71
+ ready={ready}
72
+ status={status}
73
+ data={data}
74
+ selected={selected}
75
+ isCurrentLocation={isCurrentLocation}
76
+ inputRef={inputRef}
77
+ handleSelect={handleSelect}
78
+ setValue={setValue}
79
+ setSelected={setSelected}
80
+ clearSuggestions={clearSuggestions}
81
+ commuteLocation={commuteLocation}
82
+ setIsCurrentLocation={setIsCurrentLocation}
83
+ fetchLocation={fetchLocation}
84
+ setCommuteLocation={setCommuteLocation}
85
+ />
86
+ );
87
+ };
88
+
89
+ export default FilterCommuteContainer;
@@ -0,0 +1,76 @@
1
+ import React, { useState } from 'react';
2
+ import { useMap } from '~/contexts/mapContext';
3
+ import { useMapList } from '~/contexts/mapListContext';
4
+ import { useTrackEvent } from '~/contexts/trackEventContext';
5
+ import Filter from '~/components/modules/filter/index';
6
+ import FilterSearch from '~/components/containers/filter/search-container';
7
+ import FiltersAccordion from '~/components/containers/accordions/filter-container';
8
+ import FilterLocations from '~/components/containers/filter/location-container';
9
+
10
+ const FilterContainer = ({
11
+ className,
12
+ showMap
13
+ }) => {
14
+ const { trackEvent, eventTypes } = useTrackEvent();
15
+ const [hasActiveFilters, setHasActiveFilters] = useState(false);
16
+ const [defaultValue, setDefaultValue] = useState(null);
17
+ const { setSelectedListItem, setLocation, filterReset } = useMap();
18
+ const {
19
+ filteredListings,
20
+ selectedFilters,
21
+ setSelectedFilters,
22
+ setMobileTab,
23
+ handleSettingFavorites,
24
+ setQuery,
25
+ siteConfig
26
+ } = useMapList();
27
+
28
+ const handleReset = () => {
29
+ trackEvent(eventTypes.FILTERS_RESET, { filtersRemoved: selectedFilters });
30
+ filterReset();
31
+ setSelectedFilters({});
32
+ setQuery(null);
33
+ handleSettingFavorites(null);
34
+ };
35
+
36
+ return (
37
+ <Filter
38
+ className={className}
39
+ showMap={showMap}
40
+ hasActiveFilters={hasActiveFilters}
41
+ setHasActiveFilters={setHasActiveFilters}
42
+ defaultValue={defaultValue}
43
+ setDefaultValue={setDefaultValue}
44
+ setSelectedListItem={setSelectedListItem}
45
+ setLocation={setLocation}
46
+ filteredListings={filteredListings}
47
+ selectedFilters={selectedFilters}
48
+ setSelectedFilters={setSelectedFilters}
49
+ setMobileTab={setMobileTab}
50
+ handleReset={handleReset}
51
+ siteConfig={siteConfig}
52
+ >
53
+ <FiltersAccordion
54
+ setHasActiveFilters={setHasActiveFilters}
55
+ defaultValue={defaultValue}
56
+ setDefaultValue={value => { setDefaultValue(value === defaultValue ? "" : value); }}
57
+ setLocation={setLocation}
58
+ setSelectedListItem={setSelectedListItem}
59
+ SubcategoryRequireCategory={siteConfig.subcategoryRequireCategory}
60
+ />
61
+ <FilterSearch />
62
+ {siteConfig.hideLocations !== true &&
63
+ <FilterLocations
64
+ setHasActiveFilters={setHasActiveFilters}
65
+ defaultValue={defaultValue}
66
+ showMap={showMap}
67
+ setDefaultValue={value => { setDefaultValue(value === defaultValue ? "" : value); }}
68
+ setLocation={setLocation}
69
+ setSelectedListItem={setSelectedListItem}
70
+ />
71
+ }
72
+ </Filter>
73
+ );
74
+ };
75
+
76
+ export default FilterContainer;