From 03814b8b4c3e0ab4720056afb4f43632b4016462 Mon Sep 17 00:00:00 2001 From: Nico Date: Thu, 4 Jun 2026 08:29:15 -0700 Subject: [PATCH] refactor: extract map draft actions --- .../src/hooks/useLocationMapDraftActions.js | 115 ++++++++++++++++ frontend/src/pages/LocationMapManager.jsx | 124 +++--------------- 2 files changed, 136 insertions(+), 103 deletions(-) create mode 100644 frontend/src/hooks/useLocationMapDraftActions.js diff --git a/frontend/src/hooks/useLocationMapDraftActions.js b/frontend/src/hooks/useLocationMapDraftActions.js new file mode 100644 index 0000000..1ce69df --- /dev/null +++ b/frontend/src/hooks/useLocationMapDraftActions.js @@ -0,0 +1,115 @@ +import { useCallback } from "react"; +import { + createBlankLocationMap, + createLocationMapFromZones, + publishLocationMapDraft, + saveLocationMapDraft, +} from "../api/locationMaps"; +import getApiErrorMessage from "../lib/getApiErrorMessage"; +import { prepareObjectsForSave } from "../lib/locationMapUtils"; + +export default function useLocationMapDraftActions({ + activeHouseholdId, + beginSaving, + endSaving, + hasUnsavedChanges, + locationId, + mapDraft, + objects, + setEditorTool, + setMapState, + setMode, + setPreviewDraft, + syncMapDraftFromState, + toast, +}) { + const applyDraftResponse = useCallback((response, nextMode = "edit", nextPreviewDraft = true) => { + setMapState(response.data); + setMode(nextMode); + setPreviewDraft(nextPreviewDraft); + syncMapDraftFromState(response.data, nextMode, nextPreviewDraft); + }, [setMapState, setMode, setPreviewDraft, syncMapDraftFromState]); + + const handleCreateBlank = useCallback(async () => { + if (!activeHouseholdId || !locationId) return; + beginSaving("create-blank"); + try { + const response = await createBlankLocationMap(activeHouseholdId, locationId, mapDraft); + applyDraftResponse(response); + setEditorTool("edit"); + toast.success("Created map", "Blank draft map created"); + } catch (error) { + toast.error("Create map failed", getApiErrorMessage(error, "Failed to create map")); + } finally { + endSaving(); + } + }, [activeHouseholdId, applyDraftResponse, beginSaving, endSaving, locationId, mapDraft, setEditorTool, toast]); + + const handleCreateFromZones = useCallback(async () => { + if (!activeHouseholdId || !locationId) return; + beginSaving("create-zones"); + try { + const response = await createLocationMapFromZones(activeHouseholdId, locationId, mapDraft); + applyDraftResponse(response); + setEditorTool("edit"); + toast.success("Created starter map", "Zones were added as editable rectangles"); + } catch (error) { + toast.error("Create map failed", getApiErrorMessage(error, "Failed to create map from zones")); + } finally { + endSaving(); + } + }, [activeHouseholdId, applyDraftResponse, beginSaving, endSaving, locationId, mapDraft, setEditorTool, toast]); + + const handleSaveDraft = useCallback(async () => { + if (!activeHouseholdId || !locationId) return; + beginSaving("save"); + try { + const response = await saveLocationMapDraft(activeHouseholdId, locationId, { + map: mapDraft, + objects: prepareObjectsForSave(objects), + }); + applyDraftResponse(response); + toast.success("Saved draft", "Map draft saved"); + } catch (error) { + toast.error("Save draft failed", getApiErrorMessage(error, "Failed to save map draft")); + } finally { + endSaving(); + } + }, [activeHouseholdId, applyDraftResponse, beginSaving, endSaving, locationId, mapDraft, objects, toast]); + + const handlePublish = useCallback(async () => { + if (!activeHouseholdId || !locationId) return; + beginSaving("publish"); + let savedPendingDraft = false; + try { + if (hasUnsavedChanges) { + const saveResponse = await saveLocationMapDraft(activeHouseholdId, locationId, { + map: mapDraft, + objects: prepareObjectsForSave(objects), + }); + savedPendingDraft = true; + applyDraftResponse(saveResponse); + } + + const response = await publishLocationMapDraft(activeHouseholdId, locationId); + applyDraftResponse(response, "view", false); + setEditorTool("pan"); + toast.success( + "Published map", + hasUnsavedChanges + ? "Latest edits were saved and published" + : "Map is now visible to household members" + ); + } catch (error) { + const message = getApiErrorMessage(error, "Failed to publish map"); + toast.error("Publish failed", savedPendingDraft ? `${message}. Draft changes were saved.` : message); + } finally { + endSaving(); + } + }, [ + activeHouseholdId, applyDraftResponse, beginSaving, endSaving, hasUnsavedChanges, + locationId, mapDraft, objects, setEditorTool, toast, + ]); + + return { handleCreateBlank, handleCreateFromZones, handlePublish, handleSaveDraft }; +} diff --git a/frontend/src/pages/LocationMapManager.jsx b/frontend/src/pages/LocationMapManager.jsx index e6dcbb2..8c972c1 100644 --- a/frontend/src/pages/LocationMapManager.jsx +++ b/frontend/src/pages/LocationMapManager.jsx @@ -1,11 +1,5 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { useLocation, useNavigate, useParams } from "react-router-dom"; -import { - createBlankLocationMap, - createLocationMapFromZones, - publishLocationMapDraft, - saveLocationMapDraft, -} from "../api/locationMaps"; import { AuthContext } from "../context/AuthContext"; import { HouseholdContext } from "../context/HouseholdContext"; import { StoreContext } from "../context/StoreContext"; @@ -16,9 +10,9 @@ import LocationMapToolbar from "../components/maps/LocationMapToolbar"; import LocationMapTopbar from "../components/maps/LocationMapTopbar"; import ConfirmSlideModal from "../components/modals/ConfirmSlideModal"; import useActionToast from "../hooks/useActionToast"; +import useLocationMapDraftActions from "../hooks/useLocationMapDraftActions"; import useLocationMapDraftState from "../hooks/useLocationMapDraftState"; import useUnsavedMapLeaveGuard from "../hooks/useUnsavedMapLeaveGuard"; -import getApiErrorMessage from "../lib/getApiErrorMessage"; import { DEFAULT_MAP_SIZE, clampMapZoom, @@ -31,7 +25,6 @@ import { itemsForZone, mapForMode, objectZoneId, - prepareObjectsForSave, unmappedItems, } from "../lib/locationMapUtils"; import "../styles/pages/LocationMapManager.css"; @@ -188,101 +181,26 @@ export default function LocationMapManager() { }); }; - const handleCreateBlank = async () => { - if (!activeHousehold?.id || !locationId) return; - beginSaving("create-blank"); - try { - const response = await createBlankLocationMap(activeHousehold.id, locationId, mapDraft); - setMapState(response.data); - setMode("edit"); - setEditorTool("edit"); - setPreviewDraft(true); - syncMapDraftFromState(response.data, "edit", true); - toast.success("Created map", "Blank draft map created"); - } catch (error) { - toast.error("Create map failed", getApiErrorMessage(error, "Failed to create map")); - } finally { - endSaving(); - } - }; - - const handleCreateFromZones = async () => { - if (!activeHousehold?.id || !locationId) return; - beginSaving("create-zones"); - try { - const response = await createLocationMapFromZones(activeHousehold.id, locationId, mapDraft); - setMapState(response.data); - setMode("edit"); - setEditorTool("edit"); - setPreviewDraft(true); - syncMapDraftFromState(response.data, "edit", true); - toast.success("Created starter map", "Zones were added as editable rectangles"); - } catch (error) { - toast.error("Create map failed", getApiErrorMessage(error, "Failed to create map from zones")); - } finally { - endSaving(); - } - }; - - const handleSaveDraft = async () => { - if (!activeHousehold?.id || !locationId) return; - beginSaving("save"); - try { - const response = await saveLocationMapDraft(activeHousehold.id, locationId, { - map: mapDraft, - objects: prepareObjectsForSave(objects), - }); - setMapState(response.data); - setMode("edit"); - setPreviewDraft(true); - syncMapDraftFromState(response.data, "edit", true); - toast.success("Saved draft", "Map draft saved"); - } catch (error) { - toast.error("Save draft failed", getApiErrorMessage(error, "Failed to save map draft")); - } finally { - endSaving(); - } - }; - - const handlePublish = async () => { - if (!activeHousehold?.id || !locationId) return; - beginSaving("publish"); - let savedPendingDraft = false; - try { - if (hasUnsavedChanges) { - const saveResponse = await saveLocationMapDraft(activeHousehold.id, locationId, { - map: mapDraft, - objects: prepareObjectsForSave(objects), - }); - savedPendingDraft = true; - setMapState(saveResponse.data); - setMode("edit"); - setPreviewDraft(true); - syncMapDraftFromState(saveResponse.data, "edit", true); - } - - const response = await publishLocationMapDraft(activeHousehold.id, locationId); - setMapState(response.data); - setMode("view"); - setEditorTool("pan"); - setPreviewDraft(false); - syncMapDraftFromState(response.data, "view", false); - toast.success( - "Published map", - hasUnsavedChanges - ? "Latest edits were saved and published" - : "Map is now visible to household members" - ); - } catch (error) { - const message = getApiErrorMessage(error, "Failed to publish map"); - toast.error( - "Publish failed", - savedPendingDraft ? `${message}. Draft changes were saved.` : message - ); - } finally { - endSaving(); - } - }; + const { + handleCreateBlank, + handleCreateFromZones, + handlePublish, + handleSaveDraft, + } = useLocationMapDraftActions({ + activeHouseholdId: activeHousehold?.id, + beginSaving, + endSaving, + hasUnsavedChanges, + locationId, + mapDraft, + objects, + setEditorTool, + setMapState, + setMode, + setPreviewDraft, + syncMapDraftFromState, + toast, + }); const handleAddObject = () => { if (!canManage) return;