@abcagency/hc-ui-components 1.0.0

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/dist/globals.css +3 -0
  2. package/dist/index.js +4644 -0
  3. package/dist/output.css +784 -0
  4. package/dist/services/globals.css +3 -0
  5. package/dist/services/listingService.js +606 -0
  6. package/package.json +38 -0
  7. package/postcss.config.js +15 -0
  8. package/rollup.config.js +67 -0
  9. package/src/apis/hcApi.js +68 -0
  10. package/src/clientToken.js +9 -0
  11. package/src/components/layout/footer.js +34 -0
  12. package/src/components/layout/header.js +23 -0
  13. package/src/components/layout/layout.js +36 -0
  14. package/src/components/modules/accordions/MapAccordionItem.js +69 -0
  15. package/src/components/modules/accordions/default.js +173 -0
  16. package/src/components/modules/accordions/filterItem.js +53 -0
  17. package/src/components/modules/accordions/filters.js +44 -0
  18. package/src/components/modules/animations/slidein.js +41 -0
  19. package/src/components/modules/buttons/button-group-apply.js +75 -0
  20. package/src/components/modules/buttons/commute-pill.js +21 -0
  21. package/src/components/modules/buttons/default.js +196 -0
  22. package/src/components/modules/buttons/items-pill.js +31 -0
  23. package/src/components/modules/buttons/pill-wrapper.js +26 -0
  24. package/src/components/modules/buttons/show-all-button.js +20 -0
  25. package/src/components/modules/cards/default.js +168 -0
  26. package/src/components/modules/cards/filter.js +55 -0
  27. package/src/components/modules/dialogs/apply-dialog.js +47 -0
  28. package/src/components/modules/filter/commute.js +149 -0
  29. package/src/components/modules/filter/index.js +86 -0
  30. package/src/components/modules/filter/item.js +77 -0
  31. package/src/components/modules/filter/location.js +69 -0
  32. package/src/components/modules/filter/points-of-interest.js +43 -0
  33. package/src/components/modules/filter/radio-item.js +51 -0
  34. package/src/components/modules/filter/search.js +89 -0
  35. package/src/components/modules/filter/search.js.rej +9 -0
  36. package/src/components/modules/filter/sort.js +83 -0
  37. package/src/components/modules/form.js +362 -0
  38. package/src/components/modules/grid.js +75 -0
  39. package/src/components/modules/icon.js +33 -0
  40. package/src/components/modules/jobListing/listing-details.js +87 -0
  41. package/src/components/modules/jumbotron.js +81 -0
  42. package/src/components/modules/maps/info-window-card.js +17 -0
  43. package/src/components/modules/maps/info-window-content.js +60 -0
  44. package/src/components/modules/maps/list/field-mapper.js +113 -0
  45. package/src/components/modules/maps/list/header-item.js +90 -0
  46. package/src/components/modules/maps/list/header.js +46 -0
  47. package/src/components/modules/maps/list/index.js +104 -0
  48. package/src/components/modules/maps/list/item-expand-card/index.js +21 -0
  49. package/src/components/modules/maps/list/item-expand-card/recruiter-contact-nav.js +48 -0
  50. package/src/components/modules/maps/list/item-expand-card/recruiter-details.js +67 -0
  51. package/src/components/modules/maps/list/item-expand-card/recruiter-headshot.js +22 -0
  52. package/src/components/modules/maps/list/list-item/index.js +133 -0
  53. package/src/components/modules/maps/map-list.js +73 -0
  54. package/src/components/modules/maps/map-marker.js +84 -0
  55. package/src/components/modules/maps/map.js +218 -0
  56. package/src/components/modules/maps/place-marker.js +41 -0
  57. package/src/components/modules/maps/tabs.js +79 -0
  58. package/src/components/modules/navigation/nav-link.js +65 -0
  59. package/src/components/modules/navigation/navbar.js +109 -0
  60. package/src/components/modules/navigation/skip-link.js +21 -0
  61. package/src/components/modules/navigation/social.js +29 -0
  62. package/src/components/modules/sections/default.js +59 -0
  63. package/src/components/modules/sections/sectionContext.js +4 -0
  64. package/src/components/modules/video-player.js +126 -0
  65. package/src/constants/placeTypes.js +8 -0
  66. package/src/contexts/mapContext.js +116 -0
  67. package/src/contexts/mapListContext.js +212 -0
  68. package/src/contexts/placesContext.js +98 -0
  69. package/src/hooks/useClickOutside.js +16 -0
  70. package/src/hooks/useEventListener.js +25 -0
  71. package/src/hooks/useEventTracker.js +19 -0
  72. package/src/hooks/useList.js +102 -0
  73. package/src/hooks/useRefScrollProgress.js +24 -0
  74. package/src/hooks/useScript.js +63 -0
  75. package/src/hooks/useScrollDirection.js +39 -0
  76. package/src/hooks/useSectionTracker.js +95 -0
  77. package/src/hooks/useUserAgent.js +43 -0
  78. package/src/hooks/useWindowSize.js +28 -0
  79. package/src/index.css +25 -0
  80. package/src/index.js +116 -0
  81. package/src/services/configService.js +16 -0
  82. package/src/services/googlePlacesNearbyService.js +33 -0
  83. package/src/services/listingAggregatorService.js +42 -0
  84. package/src/services/listingEntityService.js +14 -0
  85. package/src/services/listingService.js +28 -0
  86. package/src/services/recruiterService.js +17 -0
  87. package/src/styles/fonts.js +0 -0
  88. package/src/styles/globals.css +25 -0
  89. package/src/tailwind/preset.default.js +15 -0
  90. package/src/tailwind/tailwind.config.js +126 -0
  91. package/src/util/arrayUtil.js +3 -0
  92. package/src/util/fieldMapper.js +19 -0
  93. package/src/util/filterUtil.js +195 -0
  94. package/src/util/loading.js +17 -0
  95. package/src/util/localStorageUtil.js +27 -0
  96. package/src/util/mapIconUtil.js +179 -0
  97. package/src/util/mapUtil.js +91 -0
  98. package/src/util/page-head.js +62 -0
  99. package/src/util/provider.js +12 -0
  100. package/src/util/sortUtil.js +33 -0
  101. package/src/util/stringUtils.js +6 -0
  102. package/src/util/urlFilterUtil.js +91 -0
@@ -0,0 +1,109 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Link } from 'react-router-dom';
5
+
6
+ import NavLink from '~/components/modules/navigation/nav-link';
7
+ import Icon from '~/components/modules/icon';
8
+ import React from 'react'
9
+ import routes from '~/data/routes.json';
10
+ import site from '~/data/site.json';
11
+
12
+ const Navbar = ({ isPinned }) => {
13
+ const [isExpanded, toggleExpansion] = useState(false);
14
+
15
+ return (
16
+ <div
17
+ className={`
18
+ px-4 transition-all bg-white/20 border-b border-gray-400/20 backdrop-blur
19
+ ${isPinned ? 'mb:bg-transparent md:border-0 md:backdrop-blur-[unset]' : ''}
20
+ `}
21
+ >
22
+ <div
23
+ className={`
24
+ flex flex-wrap items-center justify-between py-2 px-6 container transition-all
25
+ ${isPinned ? 'md:translate-y-2 md:bg-white/20 md:border md:border-gray-400/20 md:!backdrop-blur md:rounded-full' : ''}
26
+ `}
27
+ >
28
+ <Link
29
+ href="/"
30
+ className="flex gap-2 items-center"
31
+ >
32
+ <Icon
33
+ icon="mdi:robot"
34
+ size="w-6 h-6"
35
+ />
36
+ <h1 className="text-gray-800 font-bold text-md no-underline">
37
+ {site.title}
38
+ </h1>
39
+ </Link>
40
+
41
+ <button
42
+ className={`
43
+ block md:hidden px-3 py-2 transition-colors hover:text-indigo-700 focus:text-indigo-700
44
+ ${isExpanded ? 'text-indigo-700' : 'text-gray-800'}
45
+ `}
46
+ onClick={() => toggleExpansion(!isExpanded)}
47
+ >
48
+ <svg
49
+ viewBox="0 0 20 20"
50
+ xmlns="http://www.w3.org/2000/svg"
51
+ className={`
52
+ block w-5 h-5 fill-current transition-transform
53
+ ${isExpanded ? `transform-gpu rotate-180` : ``}
54
+ `}
55
+ >
56
+ <title>Menu</title>
57
+ <rect
58
+ y="3"
59
+ width="20"
60
+ height="2"
61
+ className={`
62
+ transition
63
+ ${isExpanded ? `transform-gpu rotate-45 translate-y-[0] translate-x-[6px]` : ``}
64
+ `}
65
+ />
66
+ <rect
67
+ y="9"
68
+ width="20"
69
+ height="2"
70
+ className={`
71
+ transition
72
+ ${isExpanded ? `opacity-0` : ``}
73
+ `}
74
+ />
75
+ <rect
76
+ y="15"
77
+ width="20"
78
+ height="2"
79
+ className={`
80
+ transition
81
+ ${isExpanded ? `transform-gpu -rotate-45 translate-y-[6px] translate-x-[-8px]` : ``}
82
+ `}
83
+ />
84
+ </svg>
85
+ </button>
86
+
87
+ <nav
88
+ className={`
89
+ md:flex md:items-center md:gap-2 w-full md:w-auto pt-2 md:pt-0
90
+ ${isExpanded ? 'block' : 'hidden'}
91
+ `}
92
+ >
93
+ {routes.map(link => (
94
+ <NavLink.Anchor
95
+ key={link.title}
96
+ href={link.route}
97
+ className="block md:inline-block px-3.5 py-1.5 rounded-full border border-transparent text-xs text-gray-800 transition-colors hover:border-indigo-700 hover:text-indigo-700 focus:border-indigo-700 focus:text-indigo-700"
98
+ activeClassName="bg-indigo-700 !text-white"
99
+ >
100
+ {link.title}
101
+ </NavLink.Anchor>
102
+ ))}
103
+ </nav>
104
+ </div>
105
+ </div>
106
+ );
107
+ };
108
+
109
+ export default Navbar;
@@ -0,0 +1,21 @@
1
+ import React from 'react'
2
+
3
+ const SkipLink = ({
4
+ href = '#start-of-content',
5
+ className,
6
+ children = 'Skip to content'
7
+ }) => {
8
+ return (
9
+ <a
10
+ href={href}
11
+ className={`
12
+ sr-only focus:not-sr-only focus:absolute focus:py-2 focus:px-6 bg-gray-900 dark:bg-white text-white dark:text-gray-900 font-bold text-center transition-colors hover:bg-gray-700
13
+ ${className ?? ''}
14
+ `}
15
+ >
16
+ {children}
17
+ </a>
18
+ );
19
+ };
20
+
21
+ export default SkipLink;
@@ -0,0 +1,29 @@
1
+ import Button from '~/components/modules/buttons/default';
2
+ import React from 'react'
3
+ import site from '~/data/site.json';
4
+
5
+ const NavSocial = () => {
6
+ const socials = site.social;
7
+
8
+ return (
9
+ <nav className="flex justify-self-end gap-3">
10
+ {socials.map((data, index) => (
11
+ <Button.Anchor
12
+ key={`${index}`}
13
+ href={data.url}
14
+ variant="icon"
15
+ size="sq"
16
+ >
17
+ <span className="sr-only">{data.label}</span>
18
+
19
+ <Button.Icon
20
+ icon={data.icon}
21
+ size="w-4 h-4"
22
+ />
23
+ </Button.Anchor>
24
+ ))}
25
+ </nav>
26
+ );
27
+ };
28
+
29
+ export default NavSocial;
@@ -0,0 +1,59 @@
1
+ import { InView } from 'react-intersection-observer';
2
+ import React from 'react'
3
+ import useSectionTracker from '~/hooks/useSectionTracker';
4
+
5
+ const isBrowser = typeof window !== 'undefined';
6
+
7
+ const Section = ({
8
+ children,
9
+ className,
10
+ ...props
11
+ }) => {
12
+ const sectionIsIntersecting = useSectionTracker();
13
+
14
+ const onChange = (inView, entry) => {
15
+ if (inView) {
16
+ if (entry.intersectionRatio > 0 && sectionIsIntersecting) {
17
+ sectionIsIntersecting(entry.target.id, entry.intersectionRatio, entry.intersectionRatio * entry.boundingClientRect.height / (isBrowser ? window.innerHeight : 1));
18
+ }
19
+ }
20
+ };
21
+
22
+ return (
23
+ <InView
24
+ as="section"
25
+ threshold={[0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]}
26
+ onChange={onChange}
27
+ className={`
28
+ scroll-mt-20
29
+ ${className ?? ''}
30
+ `}
31
+ {...props}
32
+ >
33
+ {children}
34
+ </InView>
35
+ );
36
+ };
37
+
38
+ export const SectionTitle = ({
39
+ as = 'h2',
40
+ children,
41
+ className
42
+ }) => {
43
+ const Title = as;
44
+
45
+ return (
46
+ <Title
47
+ className={`
48
+ mb-4 text-2xl
49
+ ${className ?? ''}
50
+ `}
51
+ >
52
+ {children}
53
+ </Title>
54
+ );
55
+ };
56
+
57
+ Section.Title = SectionTitle;
58
+
59
+ export default Section;
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+
3
+ const reactThingy = React
4
+ export const SectionContext = reactThingy.createContext(null)
@@ -0,0 +1,126 @@
1
+ import React, { useRef, useState, useEffect } from 'react';
2
+ import ReactPlayer from 'react-player/lazy';
3
+
4
+ import Icon from '~/components/modules/icon';
5
+
6
+ /*
7
+ Responsive video player using react-player
8
+ @url: https://github.com/CookPete/react-player
9
+
10
+ @url: Video file URL
11
+ @placeholder: Video poster image
12
+ @playerOptions: Use to pass in any extra options to react-player
13
+ @containerClasses: Pass extra classes to the container element
14
+ @aspectRatio: Set player aspect ratio (Default: 16x9)
15
+ */
16
+
17
+ const isBrowser = typeof window !== 'undefined';
18
+
19
+ const VideoPlayer = ({
20
+ url,
21
+ imageAlt,
22
+ placeholder,
23
+ className,
24
+ containerClassName,
25
+ aspectRatio,
26
+ playOnLoad = false,
27
+ muted = false,
28
+ autoPlay = true,
29
+ ...playerOptions
30
+ }) => {
31
+ const [lightMode, setLightMode] = useState(placeholder);
32
+ const [isReady, setIsReady] = useState(false);
33
+ const [isPlaying, setIsPlaying] = useState(autoPlay);
34
+ const [played, setPlayed] = useState(0);
35
+ const [seekTo, setSeekTo] = useState(null);
36
+ const [pageLoaded, setPageLoaded] = useState(false);
37
+ const playerRef = useRef(null);
38
+ const divRef = useRef(null);
39
+
40
+ useEffect(() => {
41
+ if (isBrowser) {
42
+ setPageLoaded(true);
43
+ }
44
+ }, [autoPlay, setPageLoaded]);
45
+
46
+ useEffect(() => {
47
+ if (isReady && seekTo) {
48
+ playerRef.current.seekTo(parseFloat(seekTo), 'seconds');
49
+ setIsPlaying(true);
50
+ if (isBrowser && divRef.current) {
51
+ divRef.current.scrollIntoView({
52
+ behavior: 'smooth',
53
+ block: 'center'
54
+ });
55
+ }
56
+ setSeekTo(null);
57
+ }
58
+ }, [isReady, seekTo]);
59
+
60
+ return (
61
+ <div
62
+ className={`${containerClassName ?? ''}`}
63
+ >
64
+ <div
65
+ ref={divRef}
66
+ className="block relative w-full overflow-hidden"
67
+ >
68
+ {/*
69
+ Inline switch to set player aspect ratio
70
+ @default: 16x9
71
+ @usage: aspectRatio="21x9"
72
+ */}
73
+ {
74
+ {
75
+ '21x9': <span className="block pt-[42.86%]" aria-hidden="true"></span>,
76
+ '16x9': <span className="block pt-[56.25%]" aria-hidden="true"></span>,
77
+ '4x3': <span className="block pt-[75%]" aria-hidden="true"></span>,
78
+ '1x1': <span className="block pt-[100%]" aria-hidden="true"></span>
79
+ }[aspectRatio]
80
+ || <span className="block pt-[56.25%]" aria-hidden="true"></span>
81
+ }
82
+
83
+ {pageLoaded &&
84
+ <ReactPlayer
85
+ ref={playerRef}
86
+ className={`
87
+ absolute inset-y-0 left-0 w-full h-full border-0 my-0
88
+ ${className ?? ''}
89
+ `}
90
+ url={url}
91
+ light={playOnLoad ? false : lightMode === false ? lightMode : placeholder}
92
+ progressInterval={3000}
93
+ // eslint-disable-next-line no-unused-vars
94
+ onProgress={(played, loaded) => {
95
+ setPlayed(played);
96
+ if (isBrowser) {
97
+ document.querySelectorAll('button:focus').forEach(el => el.blur());
98
+ }
99
+ }}
100
+ onReady={() => { setIsReady(true); setIsPlaying(autoPlay);}}
101
+ onPlay={() => setIsPlaying(true)}
102
+ onPause={() => setIsPlaying(false)}
103
+ playing={isPlaying}
104
+ autoPlay={autoPlay}
105
+ width="100%"
106
+ height="100%"
107
+ muted={muted}
108
+ playIcon={
109
+ <button className="bg-blue-400 bg-opacity-70 p-1 rounded-full transition-colors hover:bg-opacity-90 focus:bg-opacity-90">
110
+ <span className="sr-only">Play</span>
111
+ <Icon
112
+ icon="fe:play"
113
+ sizeClasses="w-16 h-16"
114
+ className="text-white drop-shadow-md translate-x-1"
115
+ />
116
+ </button>
117
+ }
118
+ {...playerOptions}
119
+ />
120
+ }
121
+ </div>
122
+ </div>
123
+ );
124
+ };
125
+
126
+ export default VideoPlayer;
@@ -0,0 +1,8 @@
1
+ export const placeTypes = {
2
+ FOOD: "food",
3
+ STORE: "shopping",
4
+ TOURIST_ATTRACTION: "attractions",
5
+ TRANSIT_STATION: "transit",
6
+ SCHOOL: "schools",
7
+ PLACE_OF_WORSHIP: "worship"
8
+ };
@@ -0,0 +1,116 @@
1
+ import React, { createContext, useState, useContext, useEffect, useRef } from "react";
2
+ import { getStorageObject, setStorageObject } from "~/util/localStorageUtil";
3
+ const MapContext = createContext();
4
+
5
+ export const useMap = () => {
6
+ const context = useContext(MapContext);
7
+ if (!context) {
8
+ throw new Error("useMap must be used within a MapProvider");
9
+ }
10
+ return context;
11
+ };
12
+
13
+ export const MapProvider = ({ children, resetFilters }) => {
14
+ const [selectedListItem, setSelectedListItem] = useState(getStorageObject('selectedListItem'));
15
+ const [location, setLocation] = useState(getStorageObject('location'));
16
+ const [center, setCenter] = useState(getStorageObject("center", { lat: 39.8283, lng: -98.5795 }));
17
+ const [zoom, setZoom] = useState(getStorageObject("zoom",10));
18
+ const [commuteLocation, setCommuteLocation] = useState(getStorageObject('commuteLocation'));
19
+ const [selectedPlaces, setSelectedPlaces] = useState([]);
20
+ const [mapInteracted, setMapInteracted] = useState(false);
21
+ const [ firstLoadListItem ] = useState(getStorageObject('selectedListItem') ?? { id: "defaultId" });
22
+ const [travelTime, setTravelTime] = useState(null);
23
+ const userSetZoom = useRef(true);
24
+
25
+ useEffect(() =>{
26
+ setStorageObject("commuteLocation", commuteLocation);
27
+ if(!commuteLocation){
28
+ setTravelTime(null);
29
+ }
30
+ },[commuteLocation]);
31
+
32
+ useEffect(() => {
33
+ setStorageObject("selectedListItem", selectedListItem);
34
+ },[selectedListItem]);
35
+
36
+ useEffect(() => {
37
+ localStorage.setItem("zoom",zoom);
38
+ },[zoom]);
39
+
40
+ useEffect(() => {
41
+ if(location == null){
42
+ localStorage.removeItem("location");
43
+ }else{
44
+ setStorageObject("location", location);
45
+ }
46
+ },[location]);
47
+
48
+ useEffect(() => {
49
+ setStorageObject("center", center);
50
+ },[center]);
51
+
52
+ const selectItem = (item, itemLocation, zoom, center) => {
53
+ setSelectedListItem(item);
54
+ if(mapInteracted === false && itemLocation != null){
55
+ setLocation(itemLocation);
56
+ }
57
+ if(mapInteracted === false || itemLocation != null){
58
+ setLocation(itemLocation);
59
+ setCenter(center);
60
+ }
61
+ if(mapInteracted === false){
62
+ setZoom(zoom);
63
+
64
+ }
65
+
66
+ };
67
+
68
+ const filterReset = () => {
69
+ setSelectedPlaces({});
70
+ setSelectedListItem(null);
71
+ setLocation(null);
72
+ setZoom(8);
73
+ setMapInteracted(false);
74
+ };
75
+
76
+ if(resetFilters === true){
77
+ filterReset();
78
+ }
79
+ const selectLocationEntity = location => {
80
+
81
+ localStorage.removeItem("selectedListItem");
82
+ setTimeout(() => setLocation(location), 200);
83
+ setSelectedListItem(null);
84
+ };
85
+
86
+ return (
87
+ <MapContext.Provider
88
+ value={{
89
+ selectedListItem,
90
+ setSelectedListItem,
91
+ location,
92
+ center,
93
+ zoom,
94
+ setZoom,
95
+ selectItem,
96
+ commuteLocation,
97
+ setCommuteLocation,
98
+ setSelectedPlaces,
99
+ selectedPlaces,
100
+ selectLocationEntity,
101
+ setLocation,
102
+ setMapInteracted,
103
+ mapInteracted,
104
+ userSetZoom,
105
+ setLocation,
106
+ location,
107
+ firstLoadListItem,
108
+ travelTime,
109
+ setTravelTime,
110
+ filterReset
111
+ }}
112
+ >
113
+ {children}
114
+ </MapContext.Provider>
115
+ );
116
+ };
@@ -0,0 +1,212 @@
1
+ import React, { createContext, useState, useEffect, useContext } from 'react';
2
+ import { useLocation, useNavigate } from 'react-router-dom';
3
+ import fetchListings from '~/services/listingAggregatorService';
4
+ import {
5
+ generateFilterOptions,
6
+ applyFilters,
7
+ filterListingsByLocation
8
+ } from '~/util/filterUtil';
9
+ import { getStorageObject, setStorageObject } from '~/util/localStorageUtil';
10
+ import { updateURLWithFilters, hasFiltersInURL, filtersFromURL, hasQueryInUrl } from '~/util/urlFilterUtil';
11
+ import { getListingEntities } from "~/services/listingEntityService";
12
+
13
+ const MapListContext = createContext();
14
+
15
+ export const useMapList = () => useContext(MapListContext);
16
+
17
+ const getQuery = (location) => {
18
+ let query;
19
+ if (!hasQueryInUrl(location)) {
20
+ query = typeof window !== 'undefined' ? localStorage.getItem('query') : '';
21
+ } else {
22
+ query = filtersFromURL(location).query;
23
+ }
24
+ return query;
25
+ };
26
+
27
+ export const MapListProvider = ({ children, siteConfig, resetFilters }) => {
28
+ const location = useLocation();
29
+ const navigate = useNavigate();
30
+ const [allListings, setAllListings] = useState(getStorageObject("listings",[]));
31
+ const [filteredListings, setFilteredListings] = useState([]);
32
+ const [loading, setLoading] = useState(false);
33
+ const [mapItems, setMapItems] = useState(getStorageObject('mapItems', []));
34
+ const [query, setQuery] = useState(() => resetFilters ? null : getQuery(location));
35
+ const [listingEntities, setListingEntities] = useState(getStorageObject("listingEntities",null));
36
+ const [firstLoad, setFirstLoad] = useState(true);
37
+ const [commuteLocation, setCommuteLocation] = useState(null);
38
+ const [selectedFilters, setSelectedFilters] = useState(() => resetFilters ? {} : hasFiltersInURL(location) ? filtersFromURL(location).filters : getStorageObject('selectedFilters', {}));
39
+ const [filterOptions, setFilterOptions] = useState();
40
+ const [recruiters, setRecruiters] = useState(getStorageObject("recruiters", {}));
41
+ const [filterDialogIsOpen, setFilterDialogIsOpen] = useState(false);
42
+ const [mobileTab, setMobileTab] = useState("listTab");
43
+ const [favorites, setFavorites] = useState([]);
44
+ const [filterByFavorites, setFilterByFavorites] = useState(false);
45
+
46
+ useEffect(() => {
47
+ const loadedFavorites = JSON.parse(localStorage.getItem('favorites')) || [];
48
+ setFavorites(loadedFavorites);
49
+ }, []);
50
+
51
+ useEffect(() => {
52
+ if (commuteLocation === null || commuteLocation === '') return;
53
+ async function fetchEntities() {
54
+ const distinctEntityIds = [
55
+ ...new Set(allListings.map(listing => listing.entityId))
56
+ ];
57
+ try {
58
+ const fetchedEntities = await getListingEntities(
59
+ distinctEntityIds,
60
+ `${commuteLocation.lat}, ${commuteLocation.lng}`
61
+ );
62
+ setListingEntities(fetchedEntities);
63
+ var newFilteredListings = filteredListings;
64
+ for (var i = 0; i < allListings.length; i++) {
65
+ if (newFilteredListings[i].entityId != -1) {
66
+ newFilteredListings[i].fields.travelTime = fetchedEntities[newFilteredListings[i].entityId].travelTime;
67
+ }
68
+ }
69
+ for (var i = 0; i < newFilteredListings.length; i++) {
70
+ if (newFilteredListings[i].entityId != -1) {
71
+ newFilteredListings[i].fields.travelTime = fetchedEntities[newFilteredListings[i].entityId].travelTime;
72
+ }
73
+ }
74
+ setFilteredListings(newFilteredListings);
75
+ } catch (error) {
76
+ console.error("Failed to fetch listing entities:", error);
77
+ }
78
+ }
79
+
80
+ fetchEntities();
81
+ }, [commuteLocation, allListings, siteConfig.companyId]);
82
+
83
+ useEffect(() => {
84
+ const handleFetchListings = async () => {
85
+ if (!getStorageObject('listings', []).length > 0) {
86
+ setLoading(true);
87
+ }
88
+
89
+ try {
90
+ const {
91
+ listingsResult,
92
+ fetchedRecruiters,
93
+ fetchedEntities,
94
+ distinctItems
95
+ } = await fetchListings(query, siteConfig);
96
+ setAllListings(listingsResult);
97
+ setRecruiters(fetchedRecruiters);
98
+ setListingEntities(fetchedEntities);
99
+ setMapItems(distinctItems);
100
+ setStorageObject("mapItems", distinctItems);
101
+ setStorageObject("listingEntities", fetchedEntities);
102
+ setStorageObject("recruiters", fetchedRecruiters);
103
+ setStorageObject("listings", listingsResult);
104
+ } catch (error) {
105
+ console.log(error);
106
+ }
107
+ setLoading(false);
108
+ };
109
+ handleFetchListings();
110
+ }, [query, siteConfig]);
111
+
112
+ useEffect(() => {
113
+ const processListings = () => {
114
+ let { filteredListings, mapItems } = applyFilters(
115
+ allListings,
116
+ selectedFilters,
117
+ query,
118
+ listingEntities,
119
+ favorites,
120
+ siteConfig
121
+ );
122
+ if (filterByFavorites) {
123
+ filteredListings = filteredListings.filter(x => favorites.includes(x.id));
124
+ }
125
+ setFilteredListings(filteredListings);
126
+ if (firstLoad && hasFiltersInURL(location)) {
127
+ const { filters } = filtersFromURL(location);
128
+ setSelectedFilters(filters);
129
+ }
130
+ if (firstLoad && selectedFilters) {
131
+ updateURLWithFilters(selectedFilters,location, navigate, query);
132
+ } else if (Object.keys(selectedFilters).length === 0 && !firstLoad) {
133
+ localStorage.removeItem('selectedFilters');
134
+ updateURLWithFilters(selectedFilters,location, navigate, query);
135
+ } else if (!firstLoad) {
136
+ setStorageObject('selectedFilters', selectedFilters);
137
+ updateURLWithFilters(selectedFilters,location, navigate, query);
138
+ }
139
+ query != null ? localStorage.setItem('query', query) : localStorage.removeItem('query');
140
+ setMapItems(mapItems);
141
+
142
+ if (selectedFilters) {
143
+ const keys = Object.keys(selectedFilters);
144
+ const lastKey = keys[keys.length - 1];
145
+ const options = generateFilterOptions(
146
+ firstLoad ? allListings : filteredListings,
147
+ allListings,
148
+ siteConfig,
149
+ filterOptions,
150
+ lastKey,
151
+ favorites
152
+ );
153
+ if (options) {
154
+ setFilterOptions(options);
155
+ if (firstLoad) setFirstLoad(false);
156
+ }
157
+ }
158
+ };
159
+
160
+ processListings();
161
+ }, [selectedFilters, query, listingEntities, filterByFavorites, favorites]);
162
+
163
+ const handleFilterListingsByLocation = selectedLocation => {
164
+ const { filteredListings, mapItems } = filterListingsByLocation(
165
+ allListings,
166
+ selectedLocation,
167
+ listingEntities,
168
+ );
169
+ setFilteredListings(filteredListings);
170
+ //setMapItems(mapItems);
171
+ };
172
+
173
+ const handleSettingFavorites = newFavorites => {
174
+ if (newFavorites == null) {
175
+ localStorage.removeItem('favorites');
176
+ } else {
177
+ setFavorites(newFavorites);
178
+ localStorage.setItem('favorites', JSON.stringify(newFavorites));
179
+ }
180
+ };
181
+
182
+ return (
183
+ <MapListContext.Provider value={{
184
+ loading,
185
+ allListings,
186
+ filteredListings,
187
+ mapItems,
188
+ query,
189
+ setFilteredListings,
190
+ setQuery,
191
+ listingEntities,
192
+ selectedFilters,
193
+ setSelectedFilters,
194
+ filterOptions,
195
+ recruiters,
196
+ handleFilterListingsByLocation,
197
+ filterDialogIsOpen,
198
+ setFilterDialogIsOpen,
199
+ setMobileTab,
200
+ mobileTab,
201
+ siteConfig,
202
+ favorites,
203
+ handleSettingFavorites,
204
+ setFilterByFavorites,
205
+ filterByFavorites,
206
+ commuteLocation,
207
+ setCommuteLocation
208
+ }}>
209
+ {children}
210
+ </MapListContext.Provider>
211
+ );
212
+ };