refactor: split map bottom sheet sections
This commit is contained in:
parent
11ac4c9eb2
commit
505d9ce0f2
@ -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>
|
||||
);
|
||||
|
||||
218
frontend/src/components/maps/LocationMapBottomSheetSections.jsx
Normal file
218
frontend/src/components/maps/LocationMapBottomSheetSections.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user