From 604025588ecd1c67864a230454b98b3e327af15a Mon Sep 17 00:00:00 2001 From: Nico Date: Thu, 4 Jun 2026 09:13:39 -0700 Subject: [PATCH] refactor: extract map view controls --- .../components/maps/LocationMapStateViews.jsx | 28 ++++ .../src/hooks/useLocationMapViewControls.js | 138 ++++++++++++++++ frontend/src/pages/LocationMapManager.jsx | 150 +++++------------- 3 files changed, 208 insertions(+), 108 deletions(-) create mode 100644 frontend/src/components/maps/LocationMapStateViews.jsx create mode 100644 frontend/src/hooks/useLocationMapViewControls.js 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 ( +
+

{message}

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

Loading map...

-
- ); + return ; } if (!activeHousehold) { - return ( -
-

Select a household to manage maps.

-
- ); + return ; } if (loadError) { return ( -
- -
-
-

Map Unavailable

-

{loadError}

-
- -
-
-
-
+ ); }