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 (
+
+
+
+
+
+
+
+
+ );
+};