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:
parent
ae24cad458
commit
edbf7086a6
6 changed files with 133 additions and 4948 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue