diff --git a/frontend/src/components/maps/LocationMapStateViews.jsx b/frontend/src/components/maps/LocationMapStateViews.jsx
new file mode 100644
index 0000000..26dd0de
--- /dev/null
+++ b/frontend/src/components/maps/LocationMapStateViews.jsx
@@ -0,0 +1,28 @@
+import LocationMapTopbar from "./LocationMapTopbar";
+
+export function LocationMapMessageState({ message }) {
+ return (
+
+ );
+}
+
+export function LocationMapLoadErrorState({ location, loadError, onBack, onRetry }) {
+ return (
+
+
+
+
+ Map Unavailable
+ {loadError}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/hooks/useLocationMapViewControls.js b/frontend/src/hooks/useLocationMapViewControls.js
new file mode 100644
index 0000000..47c9b08
--- /dev/null
+++ b/frontend/src/hooks/useLocationMapViewControls.js
@@ -0,0 +1,138 @@
+import { useCallback, useEffect } from "react";
+import {
+ DEFAULT_MAP_SIZE,
+ clampMapZoom,
+ getObjectKey,
+} from "../lib/locationMapUtils";
+
+export default function useLocationMapViewControls({
+ editorTool,
+ filters,
+ hasUnsavedChanges,
+ mapSize,
+ mapState,
+ mode,
+ objects,
+ previewDraft,
+ selectedObjectKey,
+ setEditorTool,
+ setFuture,
+ setHasUnsavedChanges,
+ setHistory,
+ setMode,
+ setObjects,
+ setPreviewDraft,
+ setSelectedObjectKey,
+ setZoom,
+ svgRef,
+ syncMapDraftFromState,
+}) {
+ useEffect(() => {
+ if (mode !== "edit") {
+ setEditorTool("pan");
+ return;
+ }
+
+ if (editorTool === "pan") {
+ setSelectedObjectKey(null);
+ }
+ }, [editorTool, mode, setEditorTool, setSelectedObjectKey]);
+
+ useEffect(() => {
+ if (!selectedObjectKey) return;
+
+ const selectedMapObject = objects.find((object) => getObjectKey(object) === selectedObjectKey);
+ if (!selectedMapObject || !filters.showZones || selectedMapObject.visible === false) {
+ setSelectedObjectKey(null);
+ }
+ }, [filters.showZones, objects, selectedObjectKey, setSelectedObjectKey]);
+
+ const handleUndo = useCallback(() => {
+ setHistory((previous) => {
+ if (previous.length === 0) return previous;
+ const priorObjects = previous[previous.length - 1];
+ setFuture((nextFuture) => [objects.map((object) => ({ ...object })), ...nextFuture.slice(0, 19)]);
+ setObjects(priorObjects);
+ setHasUnsavedChanges(true);
+ return previous.slice(0, -1);
+ });
+ }, [objects, setFuture, setHasUnsavedChanges, setHistory, setObjects]);
+
+ const handleRedo = useCallback(() => {
+ setFuture((previous) => {
+ if (previous.length === 0) return previous;
+ const nextObjects = previous[0];
+ setHistory((nextHistory) => [...nextHistory.slice(-19), objects.map((object) => ({ ...object }))]);
+ setObjects(nextObjects);
+ setHasUnsavedChanges(true);
+ return previous.slice(1);
+ });
+ }, [objects, setFuture, setHasUnsavedChanges, setHistory, setObjects]);
+
+ const handlePreviewDraft = useCallback(() => {
+ setMode("view");
+ setEditorTool("pan");
+ setPreviewDraft(true);
+ setSelectedObjectKey(null);
+ }, [setEditorTool, setMode, setPreviewDraft, setSelectedObjectKey]);
+
+ const handleViewMode = useCallback(() => {
+ setMode("view");
+ setEditorTool("pan");
+ if (hasUnsavedChanges) {
+ setPreviewDraft(true);
+ setSelectedObjectKey(null);
+ return;
+ }
+ setPreviewDraft(false);
+ if (mapState) {
+ syncMapDraftFromState(mapState, "view", false);
+ }
+ }, [
+ hasUnsavedChanges,
+ mapState,
+ setEditorTool,
+ setMode,
+ setPreviewDraft,
+ setSelectedObjectKey,
+ syncMapDraftFromState,
+ ]);
+
+ const handleEditMode = useCallback(() => {
+ setMode("edit");
+ setEditorTool("edit");
+ setPreviewDraft(true);
+ if (!previewDraft && mapState && !hasUnsavedChanges) {
+ syncMapDraftFromState(mapState, "edit", true);
+ }
+ }, [
+ hasUnsavedChanges,
+ mapState,
+ previewDraft,
+ setEditorTool,
+ setMode,
+ setPreviewDraft,
+ syncMapDraftFromState,
+ ]);
+
+ const handleFitMap = useCallback(() => {
+ const scrollElement = svgRef.current?.closest(".location-map-scroll");
+ const fallbackWidth = typeof window === "undefined" ? DEFAULT_MAP_SIZE.width : window.innerWidth;
+ const fallbackHeight = typeof window === "undefined" ? DEFAULT_MAP_SIZE.height : window.innerHeight;
+ const availableWidth = Math.max(280, (scrollElement?.clientWidth || fallbackWidth) - 24);
+ const availableHeight = Math.max(220, (scrollElement?.clientHeight || fallbackHeight) - 24);
+ const nextZoom = clampMapZoom(
+ Math.min(availableWidth / mapSize.width, availableHeight / mapSize.height)
+ );
+ setZoom(Number(nextZoom.toFixed(2)));
+ }, [mapSize.height, mapSize.width, setZoom, svgRef]);
+
+ return {
+ handleEditMode,
+ handleFitMap,
+ handlePreviewDraft,
+ handleRedo,
+ handleUndo,
+ handleViewMode,
+ };
+}
diff --git a/frontend/src/pages/LocationMapManager.jsx b/frontend/src/pages/LocationMapManager.jsx
index 81771aa..c020e1b 100644
--- a/frontend/src/pages/LocationMapManager.jsx
+++ b/frontend/src/pages/LocationMapManager.jsx
@@ -6,6 +6,10 @@ import { StoreContext } from "../context/StoreContext";
import LocationMapBottomSheet from "../components/maps/LocationMapBottomSheet";
import LocationMapCanvas from "../components/maps/LocationMapCanvas";
import LocationMapSetupPanel from "../components/maps/LocationMapSetupPanel";
+import {
+ LocationMapLoadErrorState,
+ LocationMapMessageState,
+} from "../components/maps/LocationMapStateViews";
import LocationMapToolbar from "../components/maps/LocationMapToolbar";
import LocationMapTopbar from "../components/maps/LocationMapTopbar";
import ConfirmSlideModal from "../components/modals/ConfirmSlideModal";
@@ -13,10 +17,10 @@ import useActionToast from "../hooks/useActionToast";
import useLocationMapDraftActions from "../hooks/useLocationMapDraftActions";
import useLocationMapDraftState from "../hooks/useLocationMapDraftState";
import useLocationMapObjectActions from "../hooks/useLocationMapObjectActions";
+import useLocationMapViewControls from "../hooks/useLocationMapViewControls";
import useUnsavedMapLeaveGuard from "../hooks/useUnsavedMapLeaveGuard";
import {
DEFAULT_MAP_SIZE,
- clampMapZoom,
getMapObjectDisplayLabel,
getMapStatus,
getObjectKey,
@@ -145,26 +149,6 @@ export default function LocationMapManager() {
}
}, [activeStore?.id, locationId, setActiveStore, stores]);
- useEffect(() => {
- if (mode !== "edit") {
- setEditorTool("pan");
- return;
- }
-
- if (editorTool === "pan") {
- setSelectedObjectKey(null);
- }
- }, [editorTool, mode]);
-
- useEffect(() => {
- if (!selectedObjectKey) return;
-
- const selectedMapObject = objects.find((object) => getObjectKey(object) === selectedObjectKey);
- if (!selectedMapObject || !filters.showZones || selectedMapObject.visible === false) {
- setSelectedObjectKey(null);
- }
- }, [filters.showZones, objects, selectedObjectKey]);
-
const {
handleCreateBlank,
handleCreateFromZones,
@@ -215,69 +199,35 @@ export default function LocationMapManager() {
toast,
});
- const handleUndo = () => {
- setHistory((previous) => {
- if (previous.length === 0) return previous;
- const priorObjects = previous[previous.length - 1];
- setFuture((nextFuture) => [objects.map((object) => ({ ...object })), ...nextFuture.slice(0, 19)]);
- setObjects(priorObjects);
- setHasUnsavedChanges(true);
- return previous.slice(0, -1);
- });
- };
-
- const handleRedo = () => {
- setFuture((previous) => {
- if (previous.length === 0) return previous;
- const nextObjects = previous[0];
- setHistory((nextHistory) => [...nextHistory.slice(-19), objects.map((object) => ({ ...object }))]);
- setObjects(nextObjects);
- setHasUnsavedChanges(true);
- return previous.slice(1);
- });
- };
-
- const handlePreviewDraft = () => {
- setMode("view");
- setEditorTool("pan");
- setPreviewDraft(true);
- setSelectedObjectKey(null);
- };
-
- const handleViewMode = () => {
- setMode("view");
- setEditorTool("pan");
- if (hasUnsavedChanges) {
- setPreviewDraft(true);
- setSelectedObjectKey(null);
- return;
- }
- setPreviewDraft(false);
- if (mapState) {
- syncMapDraftFromState(mapState, "view", false);
- }
- };
-
- const handleEditMode = () => {
- setMode("edit");
- setEditorTool("edit");
- setPreviewDraft(true);
- if (!previewDraft && mapState && !hasUnsavedChanges) {
- syncMapDraftFromState(mapState, "edit", true);
- }
- };
-
- const handleFitMap = () => {
- const scrollElement = svgRef.current?.closest(".location-map-scroll");
- const fallbackWidth = typeof window === "undefined" ? DEFAULT_MAP_SIZE.width : window.innerWidth;
- const fallbackHeight = typeof window === "undefined" ? DEFAULT_MAP_SIZE.height : window.innerHeight;
- const availableWidth = Math.max(280, (scrollElement?.clientWidth || fallbackWidth) - 24);
- const availableHeight = Math.max(220, (scrollElement?.clientHeight || fallbackHeight) - 24);
- const nextZoom = clampMapZoom(
- Math.min(availableWidth / mapSize.width, availableHeight / mapSize.height)
- );
- setZoom(Number(nextZoom.toFixed(2)));
- };
+ const {
+ handleEditMode,
+ handleFitMap,
+ handlePreviewDraft,
+ handleRedo,
+ handleUndo,
+ handleViewMode,
+ } = useLocationMapViewControls({
+ editorTool,
+ filters,
+ hasUnsavedChanges,
+ mapSize,
+ mapState,
+ mode,
+ objects,
+ previewDraft,
+ selectedObjectKey,
+ setEditorTool,
+ setFuture,
+ setHasUnsavedChanges,
+ setHistory,
+ setMode,
+ setObjects,
+ setPreviewDraft,
+ setSelectedObjectKey,
+ setZoom,
+ svgRef,
+ syncMapDraftFromState,
+ });
const performBackNavigation = useCallback(() => {
if (typeof returnPath === "string" && returnPath.startsWith("/")) {
@@ -299,37 +249,21 @@ export default function LocationMapManager() {
});
if (!hasLoaded || householdLoading || loading) {
- return (
-
- );
+ return ;
}
if (!activeHousehold) {
- return (
-
-
Select a household to manage maps.
-
- );
+ return ;
}
if (loadError) {
return (
-
-
-
-
- Map Unavailable
- {loadError}
-
-
-
-
-
-
+
);
}