@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.
- package/dist/globals.css +3 -0
- package/dist/index.js +4644 -0
- package/dist/output.css +784 -0
- package/dist/services/globals.css +3 -0
- package/dist/services/listingService.js +606 -0
- package/package.json +38 -0
- package/postcss.config.js +15 -0
- package/rollup.config.js +67 -0
- package/src/apis/hcApi.js +68 -0
- package/src/clientToken.js +9 -0
- package/src/components/layout/footer.js +34 -0
- package/src/components/layout/header.js +23 -0
- package/src/components/layout/layout.js +36 -0
- package/src/components/modules/accordions/MapAccordionItem.js +69 -0
- package/src/components/modules/accordions/default.js +173 -0
- package/src/components/modules/accordions/filterItem.js +53 -0
- package/src/components/modules/accordions/filters.js +44 -0
- package/src/components/modules/animations/slidein.js +41 -0
- package/src/components/modules/buttons/button-group-apply.js +75 -0
- package/src/components/modules/buttons/commute-pill.js +21 -0
- package/src/components/modules/buttons/default.js +196 -0
- package/src/components/modules/buttons/items-pill.js +31 -0
- package/src/components/modules/buttons/pill-wrapper.js +26 -0
- package/src/components/modules/buttons/show-all-button.js +20 -0
- package/src/components/modules/cards/default.js +168 -0
- package/src/components/modules/cards/filter.js +55 -0
- package/src/components/modules/dialogs/apply-dialog.js +47 -0
- package/src/components/modules/filter/commute.js +149 -0
- package/src/components/modules/filter/index.js +86 -0
- package/src/components/modules/filter/item.js +77 -0
- package/src/components/modules/filter/location.js +69 -0
- package/src/components/modules/filter/points-of-interest.js +43 -0
- package/src/components/modules/filter/radio-item.js +51 -0
- package/src/components/modules/filter/search.js +89 -0
- package/src/components/modules/filter/search.js.rej +9 -0
- package/src/components/modules/filter/sort.js +83 -0
- package/src/components/modules/form.js +362 -0
- package/src/components/modules/grid.js +75 -0
- package/src/components/modules/icon.js +33 -0
- package/src/components/modules/jobListing/listing-details.js +87 -0
- package/src/components/modules/jumbotron.js +81 -0
- package/src/components/modules/maps/info-window-card.js +17 -0
- package/src/components/modules/maps/info-window-content.js +60 -0
- package/src/components/modules/maps/list/field-mapper.js +113 -0
- package/src/components/modules/maps/list/header-item.js +90 -0
- package/src/components/modules/maps/list/header.js +46 -0
- package/src/components/modules/maps/list/index.js +104 -0
- package/src/components/modules/maps/list/item-expand-card/index.js +21 -0
- package/src/components/modules/maps/list/item-expand-card/recruiter-contact-nav.js +48 -0
- package/src/components/modules/maps/list/item-expand-card/recruiter-details.js +67 -0
- package/src/components/modules/maps/list/item-expand-card/recruiter-headshot.js +22 -0
- package/src/components/modules/maps/list/list-item/index.js +133 -0
- package/src/components/modules/maps/map-list.js +73 -0
- package/src/components/modules/maps/map-marker.js +84 -0
- package/src/components/modules/maps/map.js +218 -0
- package/src/components/modules/maps/place-marker.js +41 -0
- package/src/components/modules/maps/tabs.js +79 -0
- package/src/components/modules/navigation/nav-link.js +65 -0
- package/src/components/modules/navigation/navbar.js +109 -0
- package/src/components/modules/navigation/skip-link.js +21 -0
- package/src/components/modules/navigation/social.js +29 -0
- package/src/components/modules/sections/default.js +59 -0
- package/src/components/modules/sections/sectionContext.js +4 -0
- package/src/components/modules/video-player.js +126 -0
- package/src/constants/placeTypes.js +8 -0
- package/src/contexts/mapContext.js +116 -0
- package/src/contexts/mapListContext.js +212 -0
- package/src/contexts/placesContext.js +98 -0
- package/src/hooks/useClickOutside.js +16 -0
- package/src/hooks/useEventListener.js +25 -0
- package/src/hooks/useEventTracker.js +19 -0
- package/src/hooks/useList.js +102 -0
- package/src/hooks/useRefScrollProgress.js +24 -0
- package/src/hooks/useScript.js +63 -0
- package/src/hooks/useScrollDirection.js +39 -0
- package/src/hooks/useSectionTracker.js +95 -0
- package/src/hooks/useUserAgent.js +43 -0
- package/src/hooks/useWindowSize.js +28 -0
- package/src/index.css +25 -0
- package/src/index.js +116 -0
- package/src/services/configService.js +16 -0
- package/src/services/googlePlacesNearbyService.js +33 -0
- package/src/services/listingAggregatorService.js +42 -0
- package/src/services/listingEntityService.js +14 -0
- package/src/services/listingService.js +28 -0
- package/src/services/recruiterService.js +17 -0
- package/src/styles/fonts.js +0 -0
- package/src/styles/globals.css +25 -0
- package/src/tailwind/preset.default.js +15 -0
- package/src/tailwind/tailwind.config.js +126 -0
- package/src/util/arrayUtil.js +3 -0
- package/src/util/fieldMapper.js +19 -0
- package/src/util/filterUtil.js +195 -0
- package/src/util/loading.js +17 -0
- package/src/util/localStorageUtil.js +27 -0
- package/src/util/mapIconUtil.js +179 -0
- package/src/util/mapUtil.js +91 -0
- package/src/util/page-head.js +62 -0
- package/src/util/provider.js +12 -0
- package/src/util/sortUtil.js +33 -0
- package/src/util/stringUtils.js +6 -0
- package/src/util/urlFilterUtil.js +91 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
2
|
+
import { markerIconProps } from '~/util/mapIconUtil';
|
|
3
|
+
import { useMap } from '~/contexts/mapContext';
|
|
4
|
+
import { searchNearbyPlaces } from '~/services/googlePlacesNearbyService';
|
|
5
|
+
const PlacesContext = createContext();
|
|
6
|
+
|
|
7
|
+
export const usePlaces = () => useContext(PlacesContext);
|
|
8
|
+
|
|
9
|
+
export const PlacesProvider = ({ children, placeMappings, markerColors }) => {
|
|
10
|
+
const { selectedPlaces, zoom, center } = useMap();
|
|
11
|
+
const [poiMarkers, setPoiMarkers] = useState({ markers: [], icon: null });
|
|
12
|
+
const [currentCenter, setCurrentCenter] = useState(center);
|
|
13
|
+
const [currentZoom, setCurrentZoom] = useState(zoom);
|
|
14
|
+
const [placesWindow, setPlacesWindow] = useState(false);
|
|
15
|
+
const [selectedPlaceMarker, setSelectedPlaceMarker] = useState(null);
|
|
16
|
+
|
|
17
|
+
const getRadiusForZoom = () => {
|
|
18
|
+
if (currentZoom >= 18) return 1000;
|
|
19
|
+
if (currentZoom <= 10) return 0;
|
|
20
|
+
|
|
21
|
+
let tempZoom = Math.pow(19 - currentZoom, 4.85);
|
|
22
|
+
let radius = tempZoom;
|
|
23
|
+
let minRadius = 1500;
|
|
24
|
+
let maxRadius = 800000;
|
|
25
|
+
|
|
26
|
+
if (radius < minRadius) radius = minRadius;
|
|
27
|
+
else if (radius > maxRadius) radius = maxRadius;
|
|
28
|
+
|
|
29
|
+
return radius;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!selectedPlaces || (!selectedPlaces.length > 0) || !center || currentZoom < 12) {
|
|
34
|
+
setPoiMarkers({ markers: [], icon: null });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const fetchPlaces = async () => {
|
|
38
|
+
let poiTypes = [];
|
|
39
|
+
const selectedPOICategories = selectedPlaces;
|
|
40
|
+
selectedPOICategories.forEach(pointOfInterest => {
|
|
41
|
+
poiTypes = poiTypes.concat(placeMappings[pointOfInterest]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const radius = getRadiusForZoom();
|
|
45
|
+
const location = { latitude: currentCenter.lat, longitude: currentCenter.lng };
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const response = await searchNearbyPlaces(poiTypes, location, radius);
|
|
49
|
+
const newMarkers = response.places.map(place => {
|
|
50
|
+
const getParentCategory = types => {
|
|
51
|
+
const selectedTypes = selectedPOICategories.reduce((acc, category) => {
|
|
52
|
+
return acc.concat(placeMappings[category]);
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
for (const type of types) {
|
|
56
|
+
if (!selectedTypes.includes(type)) continue;
|
|
57
|
+
for (const category in placeMappings) {
|
|
58
|
+
if (placeMappings[category].includes(type)) {
|
|
59
|
+
return category;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const icon = markerIconProps(markerColors.placeMarkers, getParentCategory(place.types));
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
position: { lat: place.location.latitude, lng: place.location.longitude },
|
|
70
|
+
title: place.displayName.text,
|
|
71
|
+
icon: icon
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
setPoiMarkers({ markers: newMarkers, icon: null });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Failed to fetch places:', error);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
fetchPlaces();
|
|
81
|
+
}, [selectedPlaces, currentZoom, currentCenter]);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<PlacesContext.Provider value={{
|
|
85
|
+
poiMarkers,
|
|
86
|
+
setCurrentCenter,
|
|
87
|
+
currentCenter,
|
|
88
|
+
setCurrentZoom,
|
|
89
|
+
currentZoom,
|
|
90
|
+
placesWindow,
|
|
91
|
+
setPlacesWindow,
|
|
92
|
+
selectedPlaceMarker,
|
|
93
|
+
setSelectedPlaceMarker
|
|
94
|
+
}}>
|
|
95
|
+
{children}
|
|
96
|
+
</PlacesContext.Provider>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import useEventListener from '~/hooks/useEventListener';
|
|
2
|
+
|
|
3
|
+
const isBrowser = typeof window !== "undefined";
|
|
4
|
+
|
|
5
|
+
const useClickOutside = (ref, cb) => {
|
|
6
|
+
useEventListener(
|
|
7
|
+
'click',
|
|
8
|
+
e => {
|
|
9
|
+
if (ref.current == null || ref.current.contains(e.target)) return;
|
|
10
|
+
cb(e);
|
|
11
|
+
},
|
|
12
|
+
isBrowser ? document : null
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default useClickOutside;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
const isBrowser = typeof window !== "undefined";
|
|
4
|
+
|
|
5
|
+
const useEventListener = (
|
|
6
|
+
eventType,
|
|
7
|
+
callback,
|
|
8
|
+
element = isBrowser ?? window
|
|
9
|
+
) => {
|
|
10
|
+
const callbackRef = useRef(callback);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
callbackRef.current = callback;
|
|
14
|
+
}, [callback]);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (element == null) return;
|
|
18
|
+
const handler = e => callbackRef.current(e);
|
|
19
|
+
element.addEventListener(eventType, handler);
|
|
20
|
+
|
|
21
|
+
return () => element.removeEventListener(eventType, handler);
|
|
22
|
+
}, [eventType, element]);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default useEventListener;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const isBrowser = typeof window !== 'undefined';
|
|
2
|
+
|
|
3
|
+
if (isBrowser) {
|
|
4
|
+
window.dataLayer = window.dataLayer || [];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const trackEvent = (category, action, label, value) => {
|
|
8
|
+
if (isBrowser && window.dataLayer) {
|
|
9
|
+
window.dataLayer.push({
|
|
10
|
+
'event': 'eventTracking',
|
|
11
|
+
'category': category,
|
|
12
|
+
'action': action,
|
|
13
|
+
'label': label,
|
|
14
|
+
'value': value
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default trackEvent;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useEffect, useState, useRef } from "react";
|
|
2
|
+
import { getStorageObject } from "~/util/localStorageUtil";
|
|
3
|
+
import { dynamicSort } from "~/util/sortUtil";
|
|
4
|
+
import { useMapList } from '~/contexts/mapListContext';
|
|
5
|
+
|
|
6
|
+
const getDefaultItemId = () => {
|
|
7
|
+
let item = getStorageObject("selectedListItem");
|
|
8
|
+
if(item?.expanded == true){
|
|
9
|
+
return item.id;
|
|
10
|
+
}else{
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const useListLogic = () => {
|
|
16
|
+
const [itemLimit, setItemLimit] = useState(100);
|
|
17
|
+
const [expandedId] = useState(getDefaultItemId());
|
|
18
|
+
const [sortSetting, setSortSetting] = useState(getStorageObject('sortSetting', null));
|
|
19
|
+
const [scrollPosition, setScrollPosition] = useState(getStorageObject('scrollPosition',0));
|
|
20
|
+
const loader = useRef(null);
|
|
21
|
+
const scrollContainerRef = useRef(null);
|
|
22
|
+
const itemRefs = useRef({});
|
|
23
|
+
const observer = useRef(null);
|
|
24
|
+
const { filteredListings, setFilteredListings } = useMapList();
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if(!sortSetting) return;
|
|
28
|
+
localStorage.setItem('sortSetting', JSON.stringify(sortSetting));
|
|
29
|
+
let listingFiltered = dynamicSort(filteredListings, sortSetting.field, sortSetting.type);
|
|
30
|
+
setFilteredListings(listingFiltered);
|
|
31
|
+
},[sortSetting]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
observer.current = new IntersectionObserver(handleObserver, {
|
|
35
|
+
root: scrollContainerRef.current,
|
|
36
|
+
rootMargin: "100px 0px",
|
|
37
|
+
threshold: 0.5
|
|
38
|
+
});
|
|
39
|
+
if (loader.current) {
|
|
40
|
+
observer.current.observe(loader.current);
|
|
41
|
+
}
|
|
42
|
+
return () => {
|
|
43
|
+
if (observer.current && loader.current) {
|
|
44
|
+
observer.current.unobserve(loader.current);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}, [loader.current, itemLimit, filteredListings.length]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
localStorage.setItem('scrollPosition', scrollPosition.toString());
|
|
51
|
+
}, [scrollPosition]);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const savedScrollPosition = scrollPosition;
|
|
55
|
+
if(parseInt(savedScrollPosition) > 3000){
|
|
56
|
+
setItemLimit(savedScrollPosition / 10);
|
|
57
|
+
}
|
|
58
|
+
if (savedScrollPosition && scrollPosition != null && scrollContainerRef.current) {
|
|
59
|
+
let scrollContainerRefCurrent = scrollContainerRef.current;
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
scrollContainerRefCurrent = parseInt(savedScrollPosition, 10);
|
|
62
|
+
}, 300);
|
|
63
|
+
}
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const scrollContainer = scrollContainerRef.current;
|
|
68
|
+
if (scrollContainer) {
|
|
69
|
+
scrollContainer.addEventListener('scroll', handleScroll);
|
|
70
|
+
}
|
|
71
|
+
return () => {
|
|
72
|
+
if (scrollContainer) {
|
|
73
|
+
scrollContainer.removeEventListener('scroll', handleScroll);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}, [scrollContainerRef.current]);
|
|
77
|
+
|
|
78
|
+
const handleObserver = entities => {
|
|
79
|
+
const target = entities[0];
|
|
80
|
+
if (!target.isIntersecting) return;
|
|
81
|
+
if (filteredListings.length > itemLimit) {
|
|
82
|
+
setItemLimit(prevLimit => prevLimit + 100);
|
|
83
|
+
} else if (observer.current) {
|
|
84
|
+
observer.current.disconnect();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if(sortSetting){
|
|
90
|
+
dynamicSort(filteredListings, sortSetting.field, sortSetting.type);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const handleScroll = () => {
|
|
94
|
+
if (scrollContainerRef.current) {
|
|
95
|
+
setScrollPosition(scrollContainerRef.current.scrollTop);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return { itemLimit, expandedId, sortSetting, scrollPosition, loader, scrollContainerRef, itemRefs, setSortSetting, setScrollPosition, dynamicSort, filteredListings };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export default useListLogic;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import useWindowSize from './useWindowSize';
|
|
3
|
+
|
|
4
|
+
export default function useRefScrollProgress({ inputRef }) {
|
|
5
|
+
const ref = inputRef;
|
|
6
|
+
const [start, setStart] = useState(null);
|
|
7
|
+
const [end, setEnd] = useState(null);
|
|
8
|
+
const size = useWindowSize();
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!ref.current) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const rect = ref.current.getBoundingClientRect();
|
|
16
|
+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
17
|
+
const offsetTop = rect.top + scrollTop;
|
|
18
|
+
|
|
19
|
+
setStart(offsetTop / document.body.clientHeight);
|
|
20
|
+
setEnd((offsetTop + rect.height) / document.body.clientHeight);
|
|
21
|
+
}, [ref, size]);
|
|
22
|
+
|
|
23
|
+
return { ref, start, end };
|
|
24
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const isBrowser = typeof window !== 'undefined';
|
|
4
|
+
|
|
5
|
+
function useScript(src) {
|
|
6
|
+
// Keep track of script status ("idle", "loading", "ready", "error")
|
|
7
|
+
const [status, setStatus] = useState(src ? "loading" : "idle");
|
|
8
|
+
useEffect(
|
|
9
|
+
() => {
|
|
10
|
+
// Allow falsy src value if waiting on other data needed for
|
|
11
|
+
// constructing the script URL passed to this hook.
|
|
12
|
+
if (!isBrowser || !src) {
|
|
13
|
+
setStatus("idle");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
// Fetch existing script element by src
|
|
17
|
+
// It may have been added by another intance of this hook
|
|
18
|
+
let script = document.querySelector(`script[src="${src}"]`);
|
|
19
|
+
if (!script) {
|
|
20
|
+
// Create script
|
|
21
|
+
script = document.createElement("script");
|
|
22
|
+
script.src = src;
|
|
23
|
+
script.async = true;
|
|
24
|
+
script.setAttribute("data-status", "loading");
|
|
25
|
+
// Add script to document body
|
|
26
|
+
document.body.appendChild(script);
|
|
27
|
+
// Store status in attribute on script
|
|
28
|
+
// This can be read by other instances of this hook
|
|
29
|
+
const setAttributeFromEvent = event => {
|
|
30
|
+
script.setAttribute(
|
|
31
|
+
"data-status",
|
|
32
|
+
event.type === "load" ? "ready" : "error"
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
script.addEventListener("load", setAttributeFromEvent);
|
|
36
|
+
script.addEventListener("error", setAttributeFromEvent);
|
|
37
|
+
} else {
|
|
38
|
+
// Grab existing script status from attribute and set to state.
|
|
39
|
+
setStatus(script.getAttribute("data-status"));
|
|
40
|
+
}
|
|
41
|
+
// Script event handler to update status in state
|
|
42
|
+
// Note: Even if the script already exists we still need to add
|
|
43
|
+
// event handlers to update the state for *this* hook instance.
|
|
44
|
+
const setStateFromEvent = event => {
|
|
45
|
+
setStatus(event.type === "load" ? "ready" : "error");
|
|
46
|
+
};
|
|
47
|
+
// Add event listeners
|
|
48
|
+
script.addEventListener("load", setStateFromEvent);
|
|
49
|
+
script.addEventListener("error", setStateFromEvent);
|
|
50
|
+
// Remove event listeners on cleanup
|
|
51
|
+
return () => {
|
|
52
|
+
if (script) {
|
|
53
|
+
script.removeEventListener("load", setStateFromEvent);
|
|
54
|
+
script.removeEventListener("error", setStateFromEvent);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
[src] // Only re-run effect if script src changes
|
|
59
|
+
);
|
|
60
|
+
return status;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default useScript;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export const useScrollDirection = () => {
|
|
4
|
+
const threshold = 1;
|
|
5
|
+
const [scrollDir, setScrollDir] = useState('up');
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
let previousScrollYPosition = window.scrollY;
|
|
9
|
+
|
|
10
|
+
const scrolledMoreThanThreshold = currentScrollYPosition =>
|
|
11
|
+
Math.abs(currentScrollYPosition - previousScrollYPosition) > threshold;
|
|
12
|
+
|
|
13
|
+
const isScrollingUp = currentScrollYPosition =>
|
|
14
|
+
currentScrollYPosition > previousScrollYPosition &&
|
|
15
|
+
!(previousScrollYPosition > 0 && currentScrollYPosition === 0) &&
|
|
16
|
+
!(currentScrollYPosition > 0 && previousScrollYPosition === 0);
|
|
17
|
+
|
|
18
|
+
const updateScrollDirection = () => {
|
|
19
|
+
const currentScrollYPosition = window.scrollY;
|
|
20
|
+
|
|
21
|
+
if (scrolledMoreThanThreshold(currentScrollYPosition)) {
|
|
22
|
+
const newScrollDirection = isScrollingUp(currentScrollYPosition)
|
|
23
|
+
? 'down'
|
|
24
|
+
: 'up';
|
|
25
|
+
setScrollDir(newScrollDirection);
|
|
26
|
+
previousScrollYPosition =
|
|
27
|
+
currentScrollYPosition > 0 ? currentScrollYPosition : 0;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const onScroll = () => window.requestAnimationFrame(updateScrollDirection);
|
|
32
|
+
|
|
33
|
+
window.addEventListener("scroll", onScroll);
|
|
34
|
+
|
|
35
|
+
return () => window.removeEventListener("scroll", onScroll);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return scrollDir;
|
|
39
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useState, useContext } from 'react';
|
|
2
|
+
import trackEvent from '~/hooks/useEventTracker';
|
|
3
|
+
|
|
4
|
+
import { SectionContext } from '~/components/modules/sections/sectionContext';
|
|
5
|
+
|
|
6
|
+
const isBrowser = typeof window !== 'undefined';
|
|
7
|
+
|
|
8
|
+
const ignore = ['intro'];
|
|
9
|
+
const RATIO = 0.33;
|
|
10
|
+
// let first = true;
|
|
11
|
+
|
|
12
|
+
const useSectionTracker = () => {
|
|
13
|
+
const { setCurrentSection } = useContext(SectionContext);
|
|
14
|
+
|
|
15
|
+
const [lastSection, setLastSection] = useState('');
|
|
16
|
+
const [sections, setSections] = useState([]);
|
|
17
|
+
let timeout = null;
|
|
18
|
+
|
|
19
|
+
// Allows a blank hash or ensures there is a # in the hash and replaces current state
|
|
20
|
+
const setHash = hash => {
|
|
21
|
+
if (hash !== ' ' && hash.indexOf('#') === -1) {
|
|
22
|
+
hash = `#${hash}`;
|
|
23
|
+
}
|
|
24
|
+
if (window.history.replaceState) {
|
|
25
|
+
window.history.replaceState(window.history.state, null, hash);
|
|
26
|
+
} else {
|
|
27
|
+
window.location.replace(hash);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Called when a section is intersecting
|
|
32
|
+
const sectionIsIntersecting = (id, ratio, threshold) => {
|
|
33
|
+
// Ignore sections we don't want to track
|
|
34
|
+
if (ignore.indexOf(id) !== -1) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const newThreshold = threshold;
|
|
39
|
+
let found = false;
|
|
40
|
+
|
|
41
|
+
// Update sections we've seen before
|
|
42
|
+
sections.forEach(section => {
|
|
43
|
+
if (section.id === id) {
|
|
44
|
+
if (newThreshold < RATIO) {
|
|
45
|
+
section.active = false;
|
|
46
|
+
} else {
|
|
47
|
+
section.active = true;
|
|
48
|
+
}
|
|
49
|
+
section.threshold = newThreshold;
|
|
50
|
+
found = true;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Otherwise, add the section to the list
|
|
55
|
+
if (!found) {
|
|
56
|
+
setSections(sections => {
|
|
57
|
+
sections.push({ id, threshold: newThreshold, active: newThreshold < RATIO ? false : true });
|
|
58
|
+
return sections;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let maxThreshold = 0;
|
|
63
|
+
let sectionId = '';
|
|
64
|
+
|
|
65
|
+
clearTimeout(timeout);
|
|
66
|
+
|
|
67
|
+
timeout = setTimeout(() => {
|
|
68
|
+
// Find the section with the largest threshold
|
|
69
|
+
sections.forEach(section => {
|
|
70
|
+
if (section.active && section.threshold > maxThreshold) {
|
|
71
|
+
maxThreshold = section.threshold;
|
|
72
|
+
sectionId = section.id;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Set the current section (hash, track event)
|
|
77
|
+
if (isBrowser && sectionId && sectionId !== lastSection) {
|
|
78
|
+
setHash(ignore.some(id => id === sectionId) ? ' ' : sectionId);
|
|
79
|
+
trackEvent('Engagement', 'View Section', sectionId);
|
|
80
|
+
setCurrentSection(sectionId);
|
|
81
|
+
// } else {
|
|
82
|
+
} else if (isBrowser && window.scrollY < 100) {
|
|
83
|
+
setCurrentSection(' ');
|
|
84
|
+
setHash(' ');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Remember this section for next time so we don't set it again if not necessary
|
|
88
|
+
setLastSection(sectionId);
|
|
89
|
+
}, 500);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return sectionIsIntersecting;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export default useSectionTracker;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import UAParser from 'ua-parser-js';
|
|
3
|
+
|
|
4
|
+
const isBrowser = () => typeof window !== 'undefined';
|
|
5
|
+
|
|
6
|
+
const useUserAgent = () => {
|
|
7
|
+
const [state, setState] = useState(null);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!isBrowser) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let didRun = true;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const uaParser = new UAParser.UAParser();
|
|
18
|
+
uaParser.setUA(window.navigator.userAgent);
|
|
19
|
+
const payload = {
|
|
20
|
+
os: uaParser.getOS(),
|
|
21
|
+
browser: uaParser.getBrowser(),
|
|
22
|
+
cpu: uaParser.getCPU(),
|
|
23
|
+
device: uaParser.getDevice(),
|
|
24
|
+
engine: uaParser.getEngine()
|
|
25
|
+
};
|
|
26
|
+
if (didRun) {
|
|
27
|
+
setState(payload);
|
|
28
|
+
}
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (didRun) {
|
|
31
|
+
setState(null);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return () => {
|
|
36
|
+
didRun = false;
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
return state;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default useUserAgent;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function useWindowSize() {
|
|
4
|
+
// Initialize state with undefined width/height so server and client renders match
|
|
5
|
+
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
|
|
6
|
+
const [windowSize, setWindowSize] = useState({
|
|
7
|
+
width: undefined,
|
|
8
|
+
height: undefined
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
// Handler to call on window resize
|
|
13
|
+
function handleResize() {
|
|
14
|
+
// Set window width/height to state
|
|
15
|
+
setWindowSize({
|
|
16
|
+
width: window.innerWidth,
|
|
17
|
+
height: window.innerHeight
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
// Add event listener
|
|
21
|
+
window.addEventListener("resize", () => {
|
|
22
|
+
window.requestAnimationFrame(handleResize);
|
|
23
|
+
});
|
|
24
|
+
// Remove event listener on cleanup
|
|
25
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
26
|
+
}, []); // Empty array ensures that effect is only run on mount
|
|
27
|
+
return windowSize;
|
|
28
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
@config "./tailwind/tailwind.config.js";
|
|
2
|
+
@tailwind base;
|
|
3
|
+
@tailwind components;
|
|
4
|
+
@tailwind utilities;
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/* @layer base {
|
|
8
|
+
html {
|
|
9
|
+
@apply text-400 text-uiText [scroll-behavior:smooth];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@layer components {
|
|
14
|
+
.track * {
|
|
15
|
+
@apply pointer-events-none;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.stretched-link::after {
|
|
19
|
+
@apply content-[''] absolute inset-0 z-[1] pointer-events-auto bg-transparent;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.fit-content{
|
|
24
|
+
height:fit-content;
|
|
25
|
+
} */
|