diff --git a/README.md b/README.md index 6153575..a34e29b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ # wri The spatial mapping component to the Watershed Restoration Initiative + +## Development + +### Setup + +1. Create `.env` file in the root directory with the following content: + +```txt +VITE_DISCOVER=YOUR_DISCOVER_API_KEY +``` diff --git a/package-lock.json b/package-lock.json index 9dbfe3b..4966219 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@arcgis/core": "^4.31.6", "@ugrc/layer-selector": "^6.2.10", "@ugrc/utah-design-system": "^1.16.1", + "clsx": "^2.1.1", "firebase": "^11.0.2", "immer": "^10.1.1", "ky": "^1.7.2", diff --git a/package.json b/package.json index ecac920..c879225 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@arcgis/core": "^4.31.6", "@ugrc/layer-selector": "^6.2.10", "@ugrc/utah-design-system": "^1.16.1", + "clsx": "^2.1.1", "firebase": "^11.0.2", "immer": "^10.1.1", "ky": "^1.7.2", diff --git a/src/components/MapContainer.tsx b/src/components/MapContainer.tsx index c30baa2..c37217d 100644 --- a/src/components/MapContainer.tsx +++ b/src/components/MapContainer.tsx @@ -26,6 +26,7 @@ import { import { useMap } from './hooks'; import '@ugrc/layer-selector/src/LayerSelector.css'; +import { NavigationHistory } from './NavigationHistory'; type LayerFactory = { Factory: new () => __esri.Layer; @@ -125,6 +126,7 @@ export const MapContainer = () => { return ( <> +
{selectorOptions?.view && } diff --git a/src/components/NavigationHistory.tsx b/src/components/NavigationHistory.tsx new file mode 100644 index 0000000..4ab661c --- /dev/null +++ b/src/components/NavigationHistory.tsx @@ -0,0 +1,129 @@ +import { watch } from '@arcgis/core/core/reactiveUtils'; +import { Button } from '@ugrc/utah-design-system'; +import { useViewUiPosition } from '@ugrc/utilities/hooks'; +import clsx from 'clsx'; +import { WritableDraft } from 'immer'; +import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; +import { useEffect, useRef } from 'react'; +import { useImmerReducer } from 'use-immer'; + +type State = { + history: WritableDraft<__esri.Extent>[]; + index: number; +}; + +type Action = + | { + type: 'back' | 'forward'; + } + | { + type: 'history'; + payload: __esri.Extent; + }; + +const initialState: State = { + history: [], + index: 0, +}; + +function reducer(draft: State, action: Action) { + switch (action.type) { + case 'back': + draft.index = draft.index - 1; + + break; + case 'forward': + draft.index = draft.index + 1; + + break; + case 'history': + draft.history.splice(draft.index + 1, Infinity, action.payload); + draft.index = draft.history.length - 1; + + break; + } +} + +export const NavigationHistory = ({ + view, + position, +}: { + view: __esri.MapView; + position?: __esri.UIAddComponent['position']; +}) => { + const uiPosition = useViewUiPosition(view, position ?? 'top-left'); + const [state, dispatch] = useImmerReducer(reducer, initialState); + const isButtonExtentChange = useRef(false); + + useEffect(() => { + if (!view?.extent) return; + + const handle = watch( + () => [view.stationary, view.extent], + ([stationary]) => { + if (!stationary) return; + + // prevent infinite loop + if (isButtonExtentChange.current) { + isButtonExtentChange.current = false; + + return; + } + + dispatch({ + type: 'history', + payload: view.extent, + }); + }, + ); + + return () => { + handle.remove(); + }; + }, [dispatch, view]); + + useEffect(() => { + if (view && state.history[state.index]) { + isButtonExtentChange.current = true; // prevent infinite loop + view.goTo(state.history[state.index]); + } + }, [state, view]); + + const backwardIsDisabled = state.index === 0; + const forwardIsDisabled = state.index >= state.history.length - 1; + const iconClasses = + 'size-5 stroke-[1.5] transition-colors duration-150 ease-in-out will-change-transform group-enabled/button:[#6e6e6e] group-enabled/button:group-hover/button:text-[#151515] group-disabled/button:[#cfcfcf] group-disabled/button:opacity-50'; + const buttonContainerClasses = + 'group/icon flex size-[32px] items-center justify-center bg-white shadow-[0_1px_2px_#0000004d]'; + const buttonClasses = + 'group/button size-full stroke-[4] p-0 transition-colors duration-150 ease-in-out will-change-transform focus:min-h-0 focus:outline-offset-[-2px] group/icon-hover:bg-[#f3f3f3]'; + + return ( +
+
+ +
+
+ +
+
+ ); +};