fix(frontend): harden inventory search — q= validation, error boundary, stable selection, URL throttle

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-15 06:41:54 +02:00
parent ae24cad458
commit edbf7086a6
6 changed files with 133 additions and 4948 deletions

View file

@ -1,4 +1,4 @@
import { useState } from 'react';
import { Component, useEffect, useState, type ErrorInfo, type ReactNode } from 'react';
import { useInventorySearch } from './useInventorySearch';
import { FilterSidebar } from './FilterSidebar';
import { ActiveChips } from './ActiveChips';
@ -6,10 +6,54 @@ import { ResultsTable } from './ResultsTable';
import { DetailPanel } from './DetailPanel';
import type { InvItem } from './types';
export function InventorySearchPage() {
/** Composite key used to re-match a selected item across refetches (objects are recreated each fetch). */
function itemKey(i: InvItem): string {
return `${i.name} ${i.character_name} ${i.last_updated ?? ''}`;
}
class InventorySearchErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
constructor(props: { children: ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(): { hasError: boolean } {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo): void {
console.error('Inventory search crashed:', error, info);
}
render(): ReactNode {
if (this.state.hasError) {
return (
<div className="inv-page">
<div className="inv-error" style={{ margin: 'auto', textAlign: 'center', padding: 24 }}>
<p style={{ marginBottom: 12 }}>Something went wrong loading the inventory search page.</p>
<button className="inv-btn" onClick={() => { window.location.href = '/?view=inventory'; }}>
Reset filters
</button>
</div>
</div>
);
}
return this.props.children;
}
}
function InventorySearchPageInner() {
const search = useInventorySearch();
const [selected, setSelected] = useState<InvItem | null>(null);
// Selection is by object identity; every refetch produces new item objects, so re-match the
// previously selected item by composite key in the new result set (or clear it if it's gone).
useEffect(() => {
if (!selected) return;
const items = search.result?.items ?? [];
const key = itemKey(selected);
const match = items.find(i => itemKey(i) === key);
if (match && match !== selected) setSelected(match);
else if (!match) setSelected(null);
}, [search.result]);
return (
<div className="inv-page">
<div className="inv-topbar">
@ -38,3 +82,11 @@ export function InventorySearchPage() {
</div>
);
}
export function InventorySearchPage() {
return (
<InventorySearchErrorBoundary>
<InventorySearchPageInner />
</InventorySearchErrorBoundary>
);
}