refactor: extract map view controls
This commit is contained in:
parent
bc7b8e41f8
commit
604025588e
28
frontend/src/components/maps/LocationMapStateViews.jsx
Normal file
28
frontend/src/components/maps/LocationMapStateViews.jsx
Normal file
@ -0,0 +1,28 @@
|
||||
import LocationMapTopbar from "./LocationMapTopbar";
|
||||
|
||||
export function LocationMapMessageState({ message }) {
|
||||
return (
|
||||
<div className="location-map-page">
|
||||
<p className="location-map-loading">{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LocationMapLoadErrorState({ location, loadError, onBack, onRetry }) {
|
||||
return (
|
||||
<div className="location-map-page">
|
||||
<LocationMapTopbar location={location} status="Load Error" onBack={onBack} />
|
||||
<main className="location-map-workspace">
|
||||
<section className="location-map-setup location-map-load-error" role="alert" aria-live="polite">
|
||||
<h2>Map Unavailable</h2>
|
||||
<p>{loadError}</p>
|
||||
<div className="location-map-setup-actions">
|
||||
<button type="button" className="btn-primary" onClick={onRetry}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
138
frontend/src/hooks/useLocationMapViewControls.js
Normal file
138
frontend/src/hooks/useLocationMapViewControls.js
Normal file
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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 (
|
||||
<div className="location-map-page">
|
||||
<p className="location-map-loading">Loading map...</p>
|
||||
</div>
|
||||
);
|
||||
return <LocationMapMessageState message="Loading map..." />;
|
||||
}
|
||||
|
||||
if (!activeHousehold) {
|
||||
return (
|
||||
<div className="location-map-page">
|
||||
<p className="location-map-loading">Select a household to manage maps.</p>
|
||||
</div>
|
||||
);
|
||||
return <LocationMapMessageState message="Select a household to manage maps." />;
|
||||
}
|
||||
|
||||
if (loadError) {
|
||||
return (
|
||||
<div className="location-map-page">
|
||||
<LocationMapTopbar location={location} status="Load Error" onBack={requestBackNavigation} />
|
||||
<main className="location-map-workspace">
|
||||
<section className="location-map-setup location-map-load-error" role="alert" aria-live="polite">
|
||||
<h2>Map Unavailable</h2>
|
||||
<p>{loadError}</p>
|
||||
<div className="location-map-setup-actions">
|
||||
<button type="button" className="btn-primary" onClick={loadMap}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<LocationMapLoadErrorState
|
||||
location={location}
|
||||
loadError={loadError}
|
||||
onBack={requestBackNavigation}
|
||||
onRetry={loadMap}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user