From 3fc8326a137cb18245d2d1f26f4a14d909756e53 Mon Sep 17 00:00:00 2001 From: Craig Haseler Date: Tue, 10 Mar 2026 01:16:54 -0400 Subject: [PATCH 1/4] feat: add category filter to ship catalog page Adds a Category filter row above the Empire filter on the ships page. Categories are derived from the ship data (no API change needed). The class dropdown also scopes to only classes within the selected category, making it easier for agents to find ships by role. --- src/app/ships/page.tsx | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/app/ships/page.tsx b/src/app/ships/page.tsx index 3817e6b..e748076 100644 --- a/src/app/ships/page.tsx +++ b/src/app/ships/page.tsx @@ -246,6 +246,7 @@ export default function ShipsPage() { const [error, setError] = useState(false) const [activeEmpire, setActiveEmpire] = useState('') + const [activeCategory, setActiveCategory] = useState('') const [activeClasses, setActiveClasses] = useState>(new Set()) const [activeTier, setActiveTier] = useState(0) const [search, setSearch] = useState('') @@ -321,11 +322,26 @@ export default function ShipsPage() { return () => document.removeEventListener('keydown', onKeyDown) }, [zoomedShip]) + const categories = useMemo(() => { + const seen = new Set() + ships.forEach((s) => { if (s.category) seen.add(s.category) }) + return Array.from(seen).sort() + }, [ships]) + + const visibleClasses = useMemo(() => { + if (!activeCategory) return classes + const inCategory = new Set(ships.filter((s) => s.category === activeCategory).map((s) => s.class)) + return classes.filter((c) => inCategory.has(c)) + }, [classes, ships, activeCategory]) + const filteredShips = useMemo(() => { let result = ships.filter((s) => s.empire !== '') if (activeEmpire) { result = result.filter((s) => s.empire === activeEmpire) } + if (activeCategory) { + result = result.filter((s) => s.category === activeCategory) + } if (activeClasses.size > 0) { result = result.filter((s) => activeClasses.has(s.class)) } @@ -343,7 +359,7 @@ export default function ShipsPage() { ) } return result - }, [ships, activeEmpire, activeClasses, activeTier, search]) + }, [ships, activeEmpire, activeCategory, activeClasses, activeTier, search]) const toggleExpand = (id: string) => { setAllExpanded(false) @@ -391,6 +407,25 @@ export default function ShipsPage() { {!loading && !error && ships.length > 0 && (
+
+ Category + + {categories.map((cat) => ( + + ))} +
+
Empire ))} - +
+ {viewMode === 'grid' && ( + + )} +
+ + +
+
@@ -547,7 +635,7 @@ export default function ShipsPage() {
)} - {!loading && !error && filteredShips.length > 0 && ( + {!loading && !error && filteredShips.length > 0 && viewMode === 'grid' && (
{filteredShips.map((ship) => { const isExpanded = expandedIds.has(ship.id) @@ -757,6 +845,102 @@ export default function ShipsPage() { })}
)} + {!loading && !error && filteredShips.length > 0 && viewMode === 'table' && ( +
+ + + + {TABLE_COLS.map((col) => ( + + ))} + + + + {tableShips.map((ship) => { + const isExpanded = tableExpandedId === ship.id + const empireColor = EMPIRE_COLORS[ship.empire] || '#888' + return ( + + setTableExpandedId(isExpanded ? null : ship.id)} + > + + + + + + + + + + + + + + + + + + + + {isExpanded && ( + + + + )} + + ) + })} + +
handleSort(col.key)} + title={col.title} + > + {col.label} + + {sortCol === col.key ? (sortDir === 'asc' ? ' ▲' : ' ▼') : ' ⇅'} + +
+ + {ship.name} + + + {' '}{ship.empire_name} + {ship.category}{ship.class}T{ship.tier}{ship.base_hull}{ship.base_shield}{ship.base_shield_recharge}{ship.base_armor}{ship.base_speed}{ship.base_fuel}{ship.cargo_capacity}{ship.cpu_capacity}{ship.power_capacity}{ship.weapon_slots}{ship.defense_slots}{ship.utility_slots}{ship.price.toLocaleString()}
+
+ {!brokenImages.has(ship.id) && ( +
+ {ship.name} handleImageError(ship.id)} + /> + +
+ )} +
+

{ship.description}

+ {ship.lore &&

{ship.lore}

} +
+
+
+
+ )} + {zoomedShip && (
setZoomedShip(null)}>