feat(frontend): multi-select weapon-type-by-skill filter + auto OD column
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
b4bee24773
commit
d6fa0fa9b0
7 changed files with 37 additions and 18 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import type { InventorySearch } from './useInventorySearch';
|
||||
import { RATING_DEFS } from './constants';
|
||||
import { RATING_DEFS, WEAPON_TYPES } from './constants';
|
||||
|
||||
interface Chip { label: string; gold?: boolean; remove: () => void; }
|
||||
|
||||
|
|
@ -12,9 +12,13 @@ export function ActiveChips({ search }: { search: InventorySearch }) {
|
|||
remove: () => update({ characters: 'all' }),
|
||||
});
|
||||
if (f.itemType !== 'all') chips.push({
|
||||
label: f.itemType[0].toUpperCase() + f.itemType.slice(1) + (f.weaponType ? `: ${f.weaponType}` : ''),
|
||||
remove: () => update({ itemType: 'all', weaponType: '' }),
|
||||
label: f.itemType[0].toUpperCase() + f.itemType.slice(1),
|
||||
remove: () => update({ itemType: 'all', weaponTypes: [] }),
|
||||
});
|
||||
for (const wt of f.weaponTypes) {
|
||||
const label = WEAPON_TYPES.find(w => w.value === wt)?.label ?? wt;
|
||||
chips.push({ label, remove: () => update({ weaponTypes: f.weaponTypes.filter(x => x !== wt) }) });
|
||||
}
|
||||
for (const s of f.slots) chips.push({ label: `Slot: ${s}`, remove: () => update({ slots: f.slots.filter(x => x !== s) }) });
|
||||
for (const c of f.cantrips) chips.push({
|
||||
label: c.replace(/^Legendary /, 'Leg. '), gold: true,
|
||||
|
|
|
|||
|
|
@ -66,14 +66,22 @@ export function FilterSidebar({ search }: { search: InventorySearch }) {
|
|||
{types.map(([v, label]) => (
|
||||
<label className="inv-fitem" key={v}>
|
||||
<input type="radio" name="inv-t" checked={f.itemType === v}
|
||||
onChange={() => update({ itemType: v, weaponType: '' })} /> {label}
|
||||
onChange={() => update({ itemType: v, weaponTypes: [] })} /> {label}
|
||||
</label>
|
||||
))}
|
||||
{f.itemType === 'weapon' && (
|
||||
<select className="inv-minisearch" value={f.weaponType}
|
||||
onChange={e => update({ weaponType: e.target.value })}>
|
||||
{WEAPON_TYPES.map(w => <option key={w.value} value={w.value}>{w.label}</option>)}
|
||||
</select>
|
||||
<div className="inv-subgroup">
|
||||
{WEAPON_TYPES.map(w => (
|
||||
<label className="inv-fitem" key={w.value}>
|
||||
<input type="checkbox" checked={f.weaponTypes.includes(w.value)}
|
||||
onChange={() => update({
|
||||
weaponTypes: f.weaponTypes.includes(w.value)
|
||||
? f.weaponTypes.filter(x => x !== w.value)
|
||||
: [...f.weaponTypes, w.value],
|
||||
})} /> {w.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Group>
|
||||
<Group title="Slots" defaultOpen badge={f.slots.length || undefined}>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,11 @@ export function ResultsTable({ search, selected, onSelect }: {
|
|||
const [visible, setVisible] = useState<Set<string>>(loadVisible);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const cols = useMemo(() => COLUMNS.filter(c => visible.has(c.key)), [visible]);
|
||||
const cols = useMemo(() => {
|
||||
const eff = new Set(visible);
|
||||
if (f.itemType === 'weapon') eff.add('od_rating');
|
||||
return COLUMNS.filter(c => eff.has(c.key));
|
||||
}, [visible, f.itemType]);
|
||||
const items = result?.items ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -44,11 +44,13 @@ export const ARMOR_SLOTS = ['Head', 'Chest', 'Abdomen', 'Upper Arms', 'Lower Arm
|
|||
'Hands', 'Upper Legs', 'Lower Legs', 'Feet', 'Shield'];
|
||||
|
||||
export const WEAPON_TYPES: Array<{ value: string; label: string }> = [
|
||||
{ value: '', label: 'All weapons' }, { value: 'heavy', label: 'Heavy' },
|
||||
{ value: 'light', label: 'Light' }, { value: 'finesse', label: 'Finesse' },
|
||||
{ value: 'two_handed', label: 'Two-handed' }, { value: 'bow', label: 'Bow' },
|
||||
{ value: 'crossbow', label: 'Crossbow' }, { value: 'thrown', label: 'Thrown' },
|
||||
{ value: 'caster', label: 'Wand/Staff/Orb' },
|
||||
{ value: 'heavy', label: 'Heavy' },
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'finesse', label: 'Finesse' },
|
||||
{ value: 'two_handed', label: 'Two-Handed' },
|
||||
{ value: 'missile', label: 'Missile' },
|
||||
{ value: 'war', label: 'War Magic' },
|
||||
{ value: 'void', label: 'Void Magic' },
|
||||
];
|
||||
|
||||
export interface RatingDef { param: string; label: string; common?: boolean; }
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export interface SearchFilters {
|
|||
/** 'all' → include_all_characters=true; otherwise explicit list. */
|
||||
characters: 'all' | string[];
|
||||
itemType: ItemType;
|
||||
weaponType: string; // '' = all weapon types
|
||||
weaponTypes: string[]; // [] = all weapon types
|
||||
slots: string[];
|
||||
cantrips: string[]; // full "Legendary X" value strings
|
||||
spellContains: string;
|
||||
|
|
@ -61,7 +61,7 @@ export interface SearchFilters {
|
|||
}
|
||||
|
||||
export const DEFAULT_FILTERS: SearchFilters = {
|
||||
text: '', characters: 'all', itemType: 'all', weaponType: '', slots: [],
|
||||
text: '', characters: 'all', itemType: 'all', weaponTypes: [], slots: [],
|
||||
cantrips: [], spellContains: '', ratings: {}, itemSet: '',
|
||||
equipStatus: '', bonded: false, attuned: false, rare: false,
|
||||
maxLevel: '', minValue: '', minWorkmanship: '', maxBurden: '',
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ function validateFilterValue(key: keyof SearchFilters, value: unknown): unknown
|
|||
return undefined;
|
||||
case 'slots':
|
||||
case 'cantrips':
|
||||
case 'weaponTypes':
|
||||
return isStringArray(value) ? value : undefined;
|
||||
case 'itemType':
|
||||
return typeof value === 'string' && ITEM_TYPES.has(value) ? value : undefined;
|
||||
|
|
@ -48,7 +49,6 @@ function validateFilterValue(key: keyof SearchFilters, value: unknown): unknown
|
|||
return out;
|
||||
}
|
||||
case 'text':
|
||||
case 'weaponType':
|
||||
case 'spellContains':
|
||||
case 'itemSet':
|
||||
case 'sortBy':
|
||||
|
|
@ -104,7 +104,7 @@ export function buildParams(f: SearchFilters): URLSearchParams {
|
|||
case 'pants': p.set('pants_only', 'true'); break;
|
||||
case 'weapon':
|
||||
p.set('weapon_only', 'true');
|
||||
if (f.weaponType) p.set('weapon_type', f.weaponType);
|
||||
if (f.weaponTypes.length) p.set('weapon_types', f.weaponTypes.join(','));
|
||||
break;
|
||||
}
|
||||
if (f.slots.length) p.set('slot_names', f.slots.join(','));
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
.inv-fitem { display: flex; align-items: center; gap: 6px; padding: 2px 0; font-size: 12px; color: #bbb; cursor: pointer; }
|
||||
.inv-fitem:hover { color: #fff; }
|
||||
.inv-fitem input { accent-color: #88f; }
|
||||
.inv-subgroup { margin: 2px 0 2px 10px; }
|
||||
.inv-subhead { color: #666; font-size: 9px; text-transform: uppercase; letter-spacing: 1px; margin: 6px 0 2px; }
|
||||
.inv-linky { color: #88f; font-size: 10px; cursor: pointer; }
|
||||
.inv-linky:hover { text-decoration: underline; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue