refactor: split map bottom sheet sections

This commit is contained in:
Nico 2026-06-04 07:17:26 -07:00
parent 11ac4c9eb2
commit 505d9ce0f2
2 changed files with 267 additions and 189 deletions

View File

@ -2,22 +2,17 @@ import { useMemo } from "react";
import {
DEFAULT_MAP_FILTERS,
getMapObjectDisplayLabel,
objectZoneId,
} from "../../lib/locationMapUtils";
import {
LocationMapLayerPanel,
LocationMapOverview,
MAP_DISPLAY_CONTROLS,
SelectedMapObjectForm,
SelectedZoneItemsPanel,
UnmappedItemsPanel,
} from "./LocationMapBottomSheetSections";
function objectZoneId(object) {
return object?.zone_id ? String(object.zone_id) : "";
}
const DISPLAY_CONTROLS = [
["showZones", "Zones"],
["showLabels", "Labels"],
["showCounts", "Counts"],
["showPins", "Pins"],
["showMyItems", "Mine"],
["showOtherItems", "Others"],
["showCompleted", "Bought"],
["showUnmapped", "Unmapped"],
];
const EMPTY_ITEMS = [];
export default function LocationMapBottomSheet({
@ -87,7 +82,7 @@ export default function LocationMapBottomSheet({
const showEditorControls = mode === "edit" && canManage;
const showSelectedObjectForm = Boolean(mode === "edit" && editorTool === "edit" && selectedObject);
const changedLayerCount = useMemo(
() => DISPLAY_CONTROLS.filter(([key]) => filters[key] !== DEFAULT_MAP_FILTERS[key]).length,
() => MAP_DISPLAY_CONTROLS.filter(([key]) => filters[key] !== DEFAULT_MAP_FILTERS[key]).length,
[filters]
);
const totalAreaCount = mapObjects.length;
@ -102,11 +97,6 @@ export default function LocationMapBottomSheet({
[mapObjects]
);
const showDraftOverview = mode === "edit" && editorTool === "edit" && !selectedObject;
const unmappedItemPreview = useMemo(
() => visibleUnmappedItems.slice(0, 8),
[visibleUnmappedItems]
);
const overflowUnmappedItemCount = visibleUnmappedItems.length - unmappedItemPreview.length;
const hiddenUnmappedItemCount = Math.max(0, unmappedItemCount - visibleUnmappedItems.length);
const hasHiddenUnmappedItems =
filters.showUnmapped && unmappedItemCount > 0 && visibleUnmappedItems.length === 0;
@ -211,23 +201,7 @@ export default function LocationMapBottomSheet({
</div>
{layersOpen ? (
<div className="location-map-display-panel">
<div className="location-map-display-controls">
{DISPLAY_CONTROLS.map(([key, label]) => (
<button
key={key}
type="button"
className={filters[key] ? "active" : ""}
aria-pressed={filters[key]}
onClick={() =>
setFilters((current) => ({ ...current, [key]: !current[key] }))
}
>
{label}
</button>
))}
</div>
</div>
<LocationMapLayerPanel filters={filters} setFilters={setFilters} />
) : null}
{showSelectedObjectForm && primaryDraftActions ? (
@ -249,128 +223,46 @@ export default function LocationMapBottomSheet({
) : null}
{showMapOverview ? (
<div className="location-map-overview" aria-label="Map summary">
<div className="location-map-overview-row">
<span>Areas</span>
<strong>{filters.showZones ? `${visibleAreaCount} shown` : "Hidden"}</strong>
</div>
<div className="location-map-overview-row">
<span>Mapped items</span>
<strong>{mappedItemSummary}</strong>
</div>
<div className="location-map-overview-row">
<span>Unmapped</span>
<strong>{unmappedItemSummary}</strong>
</div>
</div>
<LocationMapOverview
ariaLabel="Map summary"
rows={[
{ label: "Areas", value: filters.showZones ? `${visibleAreaCount} shown` : "Hidden" },
{ label: "Mapped items", value: mappedItemSummary },
{ label: "Unmapped", value: unmappedItemSummary },
]}
/>
) : null}
{showDraftOverview ? (
<div className="location-map-overview" aria-label="Draft map summary">
<div className="location-map-overview-row">
<span>Areas</span>
<strong>{totalAreaCount}</strong>
</div>
<div className="location-map-overview-row">
<span>Hidden</span>
<strong>{hiddenAreaCount}</strong>
</div>
<div className="location-map-overview-row">
<span>Unlinked</span>
<strong>{unlinkedAreaCount}</strong>
</div>
</div>
<LocationMapOverview
ariaLabel="Draft map summary"
rows={[
{ label: "Areas", value: totalAreaCount },
{ label: "Hidden", value: hiddenAreaCount },
{ label: "Unlinked", value: unlinkedAreaCount },
]}
/>
) : null}
{mode === "edit" && editorTool === "edit" && selectedObject ? (
<div className="location-map-object-form">
<label className="location-map-field-row">
<span>Label</span>
<input
value={selectedObject.label || ""}
disabled={saving}
onChange={(event) => onObjectField("label", event.target.value)}
/>
</label>
<label className="location-map-field-row">
<span>Zone</span>
<select
aria-label="Linked zone"
value={objectZoneId(selectedObject)}
disabled={saving}
onChange={(event) => onZoneLinkChange(event.target.value)}
>
<option value="">No linked zone</option>
{(mapState?.zones || []).map((zone) => (
<option key={zone.id} value={zone.id}>{zone.name}</option>
))}
</select>
</label>
<div className="location-map-object-flags">
<button
type="button"
className={selectedObject.visible !== false ? "active" : ""}
aria-pressed={selectedObject.visible !== false}
disabled={saving}
onClick={() => onObjectField("visible", selectedObject.visible === false)}
>
Visible
</button>
<button
type="button"
className={selectedObject.locked ? "active" : ""}
aria-pressed={Boolean(selectedObject.locked)}
disabled={saving}
onClick={() => onObjectField("locked", !selectedObject.locked)}
>
Locked
</button>
</div>
<div className="location-map-editor-actions">
<button type="button" onClick={onDuplicateObject} disabled={saving}>Duplicate</button>
<button type="button" className="danger" onClick={onDeleteObject} disabled={saving}>Delete</button>
</div>
</div>
<SelectedMapObjectForm
selectedObject={selectedObject}
zones={mapState?.zones || EMPTY_ITEMS}
saving={saving}
onObjectField={onObjectField}
onZoneLinkChange={onZoneLinkChange}
onDuplicateObject={onDuplicateObject}
onDeleteObject={onDeleteObject}
/>
) : mode === "edit" ? (
null
) : selectedObject ? (
<div className="location-map-zone-items">
{selectedZoneItems.length === 0 ? (
<div className="location-map-zone-empty">
<p className="location-map-muted">
{hasHiddenSelectedItems
? "Items are assigned here, but hidden by layer filters."
: "No items assigned to this zone yet."}
</p>
{hasHiddenSelectedItems ? (
<button type="button" onClick={showSelectedZoneItems}>
Show Zone Items
</button>
) : null}
</div>
) : (
<>
<ul>
{selectedZoneItems.map((item) => (
<li key={item.id}>
<span>{item.item_name}</span>
<small>x{item.quantity}</small>
</li>
))}
</ul>
{hasHiddenSelectedItems ? (
<div className="location-map-zone-hidden-note">
<p className="location-map-muted">
{hiddenSelectedItemCount} more hidden by layer filters.
</p>
<button type="button" onClick={showSelectedZoneItems}>
Show All Zone Items
</button>
</div>
) : null}
</>
)}
</div>
<SelectedZoneItemsPanel
selectedZoneItems={selectedZoneItems}
hasHiddenSelectedItems={hasHiddenSelectedItems}
hiddenSelectedItemCount={hiddenSelectedItemCount}
onShowSelectedZoneItems={showSelectedZoneItems}
/>
) : null}
{showSelectedObjectForm && secondaryDraftActions ? (
@ -383,45 +275,13 @@ export default function LocationMapBottomSheet({
</div>
) : null}
{filters.showUnmapped && visibleUnmappedItems.length > 0 ? (
<div className="location-map-unmapped-list">
<strong>Unmapped Items</strong>
<ul>
{unmappedItemPreview.map((item) => (
<li key={item.id}>{item.item_name}</li>
))}
{overflowUnmappedItemCount > 0 ? (
<li
className="location-map-unmapped-more"
aria-label={`${overflowUnmappedItemCount} more unmapped items`}
>
+{overflowUnmappedItemCount} more
</li>
) : null}
</ul>
{hiddenUnmappedItemCount > 0 ? (
<div className="location-map-zone-hidden-note">
<p className="location-map-muted">
{hiddenUnmappedItemCount} more hidden by layer filters.
</p>
<button type="button" onClick={showUnmappedItems}>
Show All Unmapped Items
</button>
</div>
) : null}
</div>
) : hasHiddenUnmappedItems ? (
<div className="location-map-unmapped-list">
<strong>Unmapped Items</strong>
<div className="location-map-zone-empty">
<p className="location-map-muted">
Unmapped items are hidden by layer filters.
</p>
<button type="button" onClick={showUnmappedItems}>
Show Unmapped Items
</button>
</div>
</div>
{filters.showUnmapped ? (
<UnmappedItemsPanel
visibleUnmappedItems={visibleUnmappedItems}
hiddenUnmappedItemCount={hiddenUnmappedItemCount}
hasHiddenUnmappedItems={hasHiddenUnmappedItems}
onShowUnmappedItems={showUnmappedItems}
/>
) : null}
</aside>
);

View File

@ -0,0 +1,218 @@
import { objectZoneId } from "../../lib/locationMapUtils";
export const MAP_DISPLAY_CONTROLS = [
["showZones", "Zones"],
["showLabels", "Labels"],
["showCounts", "Counts"],
["showPins", "Pins"],
["showMyItems", "Mine"],
["showOtherItems", "Others"],
["showCompleted", "Bought"],
["showUnmapped", "Unmapped"],
];
const UNMAPPED_PREVIEW_LIMIT = 8;
export function LocationMapLayerPanel({ filters, setFilters }) {
return (
<div className="location-map-display-panel">
<div className="location-map-display-controls">
{MAP_DISPLAY_CONTROLS.map(([key, label]) => (
<button
key={key}
type="button"
className={filters[key] ? "active" : ""}
aria-pressed={filters[key]}
onClick={() =>
setFilters((current) => ({ ...current, [key]: !current[key] }))
}
>
{label}
</button>
))}
</div>
</div>
);
}
export function LocationMapOverview({ ariaLabel, rows }) {
return (
<div className="location-map-overview" aria-label={ariaLabel}>
{rows.map((row) => (
<div className="location-map-overview-row" key={row.label}>
<span>{row.label}</span>
<strong>{row.value}</strong>
</div>
))}
</div>
);
}
export function SelectedMapObjectForm({
selectedObject,
zones = [],
saving,
onObjectField,
onZoneLinkChange,
onDuplicateObject,
onDeleteObject,
}) {
return (
<div className="location-map-object-form">
<label className="location-map-field-row">
<span>Label</span>
<input
value={selectedObject.label || ""}
disabled={saving}
onChange={(event) => onObjectField("label", event.target.value)}
/>
</label>
<label className="location-map-field-row">
<span>Zone</span>
<select
aria-label="Linked zone"
value={objectZoneId(selectedObject)}
disabled={saving}
onChange={(event) => onZoneLinkChange(event.target.value)}
>
<option value="">No linked zone</option>
{zones.map((zone) => (
<option key={zone.id} value={zone.id}>{zone.name}</option>
))}
</select>
</label>
<div className="location-map-object-flags">
<button
type="button"
className={selectedObject.visible !== false ? "active" : ""}
aria-pressed={selectedObject.visible !== false}
disabled={saving}
onClick={() => onObjectField("visible", selectedObject.visible === false)}
>
Visible
</button>
<button
type="button"
className={selectedObject.locked ? "active" : ""}
aria-pressed={Boolean(selectedObject.locked)}
disabled={saving}
onClick={() => onObjectField("locked", !selectedObject.locked)}
>
Locked
</button>
</div>
<div className="location-map-editor-actions">
<button type="button" onClick={onDuplicateObject} disabled={saving}>Duplicate</button>
<button type="button" className="danger" onClick={onDeleteObject} disabled={saving}>Delete</button>
</div>
</div>
);
}
export function SelectedZoneItemsPanel({
selectedZoneItems,
hasHiddenSelectedItems,
hiddenSelectedItemCount,
onShowSelectedZoneItems,
}) {
if (selectedZoneItems.length === 0) {
return (
<div className="location-map-zone-items">
<div className="location-map-zone-empty">
<p className="location-map-muted">
{hasHiddenSelectedItems
? "Items are assigned here, but hidden by layer filters."
: "No items assigned to this zone yet."}
</p>
{hasHiddenSelectedItems ? (
<button type="button" onClick={onShowSelectedZoneItems}>
Show Zone Items
</button>
) : null}
</div>
</div>
);
}
return (
<div className="location-map-zone-items">
<ul>
{selectedZoneItems.map((item) => (
<li key={item.id}>
<span>{item.item_name}</span>
<small>x{item.quantity}</small>
</li>
))}
</ul>
{hasHiddenSelectedItems ? (
<div className="location-map-zone-hidden-note">
<p className="location-map-muted">
{hiddenSelectedItemCount} more hidden by layer filters.
</p>
<button type="button" onClick={onShowSelectedZoneItems}>
Show All Zone Items
</button>
</div>
) : null}
</div>
);
}
export function UnmappedItemsPanel({
visibleUnmappedItems,
hiddenUnmappedItemCount,
hasHiddenUnmappedItems,
onShowUnmappedItems,
}) {
if (visibleUnmappedItems.length === 0 && hasHiddenUnmappedItems) {
return (
<div className="location-map-unmapped-list">
<strong>Unmapped Items</strong>
<div className="location-map-zone-empty">
<p className="location-map-muted">
Unmapped items are hidden by layer filters.
</p>
<button type="button" onClick={onShowUnmappedItems}>
Show Unmapped Items
</button>
</div>
</div>
);
}
if (visibleUnmappedItems.length === 0) {
return null;
}
const unmappedItemPreview = visibleUnmappedItems.slice(0, UNMAPPED_PREVIEW_LIMIT);
const overflowUnmappedItemCount = visibleUnmappedItems.length - unmappedItemPreview.length;
return (
<div className="location-map-unmapped-list">
<strong>Unmapped Items</strong>
<ul>
{unmappedItemPreview.map((item) => (
<li key={item.id}>{item.item_name}</li>
))}
{overflowUnmappedItemCount > 0 ? (
<li
className="location-map-unmapped-more"
aria-label={`${overflowUnmappedItemCount} more unmapped items`}
>
+{overflowUnmappedItemCount} more
</li>
) : null}
</ul>
{hiddenUnmappedItemCount > 0 ? (
<div className="location-map-zone-hidden-note">
<p className="location-map-muted">
{hiddenUnmappedItemCount} more hidden by layer filters.
</p>
<button type="button" onClick={onShowUnmappedItems}>
Show All Unmapped Items
</button>
</div>
) : null}
</div>
);
}