307 lines
8.8 KiB
JavaScript
307 lines
8.8 KiB
JavaScript
import { useEffect, useState } from "react";
|
|
import { getMapObjectDisplayLabel, 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 MAP_ITEM_PREVIEW_LIMIT = 8;
|
|
|
|
function MapItemActionRow({ item, onSelectItem }) {
|
|
const isBought = Boolean(item.bought);
|
|
|
|
return (
|
|
<li className={`location-map-item-row ${isBought ? "is-bought" : ""}`}>
|
|
<button
|
|
type="button"
|
|
className="location-map-item-button"
|
|
onClick={() => {
|
|
if (!isBought) onSelectItem?.(item);
|
|
}}
|
|
disabled={isBought}
|
|
aria-label={isBought ? `${item.item_name} bought` : `Mark ${item.item_name} bought`}
|
|
>
|
|
<span>{item.item_name}</span>
|
|
<small>{isBought ? "Bought" : `x${item.quantity}`}</small>
|
|
</button>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
function MapItemActionList({ items, onSelectItem, itemGroupLabel }) {
|
|
const [showAllItems, setShowAllItems] = useState(false);
|
|
const itemListKey = items.map((item) => item.id).join(":");
|
|
|
|
useEffect(() => {
|
|
setShowAllItems(false);
|
|
}, [itemListKey]);
|
|
|
|
const visibleItems = showAllItems ? items : items.slice(0, MAP_ITEM_PREVIEW_LIMIT);
|
|
const overflowItemCount = items.length - visibleItems.length;
|
|
const hasOverflow = items.length > MAP_ITEM_PREVIEW_LIMIT;
|
|
|
|
return (
|
|
<ul>
|
|
{visibleItems.map((item) => (
|
|
<MapItemActionRow key={item.id} item={item} onSelectItem={onSelectItem} />
|
|
))}
|
|
{hasOverflow ? (
|
|
<li className="location-map-list-more">
|
|
<button
|
|
type="button"
|
|
className="location-map-list-more-button"
|
|
onClick={() => setShowAllItems((current) => !current)}
|
|
aria-label={
|
|
showAllItems
|
|
? `Show fewer ${itemGroupLabel}`
|
|
: `Show ${overflowItemCount} more ${itemGroupLabel}`
|
|
}
|
|
>
|
|
{showAllItems ? "Show fewer" : `+${overflowItemCount} more`}
|
|
</button>
|
|
</li>
|
|
) : null}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
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] }))
|
|
}
|
|
>
|
|
<span
|
|
className={`location-map-layer-state ${filters[key] ? "is-on" : ""}`}
|
|
aria-hidden="true"
|
|
/>
|
|
<span className="location-map-layer-label">{label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function LocationMapOverview({ ariaLabel, rows }) {
|
|
return (
|
|
<div className="location-map-overview" role="group" 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>
|
|
);
|
|
}
|
|
|
|
function InlineStateAction({ label, value, actionLabel, actionText = "Show", onAction }) {
|
|
return (
|
|
<div className="location-map-inline-state">
|
|
<span>{label}</span>
|
|
<strong>{value}</strong>
|
|
{onAction ? (
|
|
<button type="button" onClick={onAction} aria-label={actionLabel}>
|
|
{actionText}
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SelectedMapObjectForm({
|
|
selectedObject,
|
|
zones = [],
|
|
saving,
|
|
onObjectField,
|
|
onZoneLinkChange,
|
|
onDuplicateObject,
|
|
onDeleteObject,
|
|
}) {
|
|
const selectedObjectLabel = getMapObjectDisplayLabel(selectedObject);
|
|
|
|
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)}
|
|
>
|
|
<span
|
|
className={`location-map-object-flag-state ${selectedObject.visible !== false ? "is-on" : ""}`}
|
|
aria-hidden="true"
|
|
/>
|
|
<span className="location-map-object-flag-label">Visible</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={selectedObject.locked ? "active" : ""}
|
|
aria-pressed={Boolean(selectedObject.locked)}
|
|
disabled={saving}
|
|
onClick={() => onObjectField("locked", !selectedObject.locked)}
|
|
>
|
|
<span
|
|
className={`location-map-object-flag-state ${selectedObject.locked ? "is-on" : ""}`}
|
|
aria-hidden="true"
|
|
/>
|
|
<span className="location-map-object-flag-label">Locked</span>
|
|
</button>
|
|
</div>
|
|
<div className="location-map-editor-actions">
|
|
<button
|
|
type="button"
|
|
onClick={onDuplicateObject}
|
|
disabled={saving}
|
|
aria-label={`Duplicate area ${selectedObjectLabel}`}
|
|
>
|
|
Duplicate
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="danger"
|
|
onClick={onDeleteObject}
|
|
disabled={saving}
|
|
aria-label={`Delete area ${selectedObjectLabel}`}
|
|
>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SelectedZoneItemsPanel({
|
|
selectedZoneItems,
|
|
hasHiddenSelectedItems,
|
|
hiddenSelectedItemCount,
|
|
onShowSelectedZoneItems,
|
|
onSelectZoneItem,
|
|
}) {
|
|
if (selectedZoneItems.length === 0) {
|
|
if (!hasHiddenSelectedItems) return null;
|
|
|
|
return (
|
|
<div className="location-map-zone-items">
|
|
<div className="location-map-zone-empty">
|
|
<InlineStateAction
|
|
label="Hidden"
|
|
value={hiddenSelectedItemCount}
|
|
actionLabel="Show Zone Items"
|
|
onAction={onShowSelectedZoneItems}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="location-map-zone-items">
|
|
<MapItemActionList
|
|
items={selectedZoneItems}
|
|
onSelectItem={onSelectZoneItem}
|
|
itemGroupLabel="zone items"
|
|
/>
|
|
{hasHiddenSelectedItems ? (
|
|
<div className="location-map-zone-hidden-note">
|
|
<InlineStateAction
|
|
label="Hidden"
|
|
value={hiddenSelectedItemCount}
|
|
actionLabel="Show All Zone Items"
|
|
actionText="Show All"
|
|
onAction={onShowSelectedZoneItems}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function UnmappedItemsPanel({
|
|
visibleUnmappedItems,
|
|
hiddenUnmappedItemCount,
|
|
hasHiddenUnmappedItems,
|
|
onShowUnmappedItems,
|
|
onSelectUnmappedItem,
|
|
}) {
|
|
if (visibleUnmappedItems.length === 0 && hasHiddenUnmappedItems) {
|
|
return (
|
|
<div className="location-map-unmapped-list" aria-label="Unmapped items">
|
|
<div className="location-map-zone-empty">
|
|
<InlineStateAction
|
|
label="Hidden"
|
|
value={hiddenUnmappedItemCount}
|
|
actionLabel="Show Unmapped Items"
|
|
onAction={onShowUnmappedItems}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (visibleUnmappedItems.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="location-map-unmapped-list" aria-label="Unmapped items">
|
|
<MapItemActionList
|
|
items={visibleUnmappedItems}
|
|
onSelectItem={onSelectUnmappedItem}
|
|
itemGroupLabel="unmapped items"
|
|
/>
|
|
{hiddenUnmappedItemCount > 0 ? (
|
|
<div className="location-map-zone-hidden-note">
|
|
<InlineStateAction
|
|
label="Hidden"
|
|
value={hiddenUnmappedItemCount}
|
|
actionLabel="Show All Unmapped Items"
|
|
actionText="Show All"
|
|
onAction={onShowUnmappedItems}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|