diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 40fa329..e8b3d09 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,4 @@ +export * from './use-context-fallback.hook'; +export * from './use-geolocation.hook'; +export * from './use-mounted.hook'; export * from './use-timeout.hook'; diff --git a/src/hooks/use-context-fallback.hook.ts b/src/hooks/use-context-fallback.hook.ts new file mode 100644 index 0000000..9fdaf24 --- /dev/null +++ b/src/hooks/use-context-fallback.hook.ts @@ -0,0 +1,15 @@ +import { Context, useContext } from 'react'; + +export const useContextFallback = (value: Context): T => { + const context = useContext(value); + + if (context === undefined) { + throw new Error( + `Components that require this context must be children of ${ + value.displayName ?? 'the appropriate provider' + }.` + ); + } + + return context; +}; diff --git a/src/hooks/use-geolocation.hook.ts b/src/hooks/use-geolocation.hook.ts new file mode 100644 index 0000000..3373852 --- /dev/null +++ b/src/hooks/use-geolocation.hook.ts @@ -0,0 +1,23 @@ +import { useEffect, useMemo, useState } from 'react'; + +import { Coordinates } from '../models'; + +export const useGeoLocation = (): Coordinates | null => { + const [geoLocation, setGeoLocation] = useState(null); + + useEffect(() => { + navigator.geolocation.getCurrentPosition( + ({ coords }) => setGeoLocation(coords), + () => setGeoLocation(null), + { + enableHighAccuracy: false, + timeout: 60000, + } + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const value = useMemo(() => geoLocation, [geoLocation]); + + return value; +}; diff --git a/src/hooks/use-mounted.hook.ts b/src/hooks/use-mounted.hook.ts new file mode 100644 index 0000000..a374874 --- /dev/null +++ b/src/hooks/use-mounted.hook.ts @@ -0,0 +1,16 @@ +import { useCallback, useEffect, useRef } from 'react'; + +export const useMounted = () => { + const mountedRef = useRef(false); + const isMounted = useCallback(() => mountedRef.current, []); + + useEffect(() => { + mountedRef.current = true; + + return () => { + mountedRef.current = false; + }; + }, []); + + return isMounted; +}; diff --git a/src/index.tsx b/src/index.tsx index 9b4d589..6d1ef76 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,3 +3,4 @@ import './style.css'; export * from './components'; export * from './hooks'; export * from './models'; +export * from './utils'; diff --git a/src/utils/distance.util.ts b/src/utils/distance.util.ts new file mode 100644 index 0000000..af055d9 --- /dev/null +++ b/src/utils/distance.util.ts @@ -0,0 +1,35 @@ +import { Coordinates } from '../models'; + +/** + * Distance calculation based on the Haversine formula + * Credit: https://stackoverflow.com/a/21623206/12130423 + * @param coordsA - The coords of the users location (geoCoords or defaultCoords) + * @param coordsB - The coords of the city + * + * @returns Distance in km (number) to two decimal places + */ +export const getDistanceBetweenCoords = ( + coordsA: Coordinates, + coordsB: Coordinates +): number => { + const R = 6371.071; // Radius of the Earth in miles + const coordsALatRad = coordsA.latitude * (Math.PI / 180); // Convert degrees to radians + const coordsBLatRad = coordsB.latitude * (Math.PI / 180); // Convert degrees to radians + const latDiff = coordsBLatRad - coordsALatRad; // Radian difference (latitudes) + const lonDiff = (coordsB.longitude - coordsA.longitude) * (Math.PI / 180); // Radian difference (longitudes) + + const distance = + 2 * + R * + Math.asin( + Math.sqrt( + Math.sin(latDiff / 2) * Math.sin(latDiff / 2) + + Math.cos(coordsALatRad) * + Math.cos(coordsBLatRad) * + Math.sin(lonDiff / 2) * + Math.sin(lonDiff / 2) + ) + ); + + return Number(parseFloat(String(distance)).toFixed(2)); +}; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..410926c --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export * from './distance.util';