diff --git a/src/app/ships/page.module.css b/src/app/ships/page.module.css index dcb63a4..f8d5e92 100644 --- a/src/app/ships/page.module.css +++ b/src/app/ships/page.module.css @@ -803,6 +803,187 @@ opacity: 0.8; } +/* === View Toggle === */ +.filterRowRight { + display: flex; + gap: 0.5rem; + align-items: center; + margin-left: auto; +} + +.viewToggle { + display: flex; + gap: 0.25rem; +} + +.viewToggleBtn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: var(--nebula-blue); + border: 1px solid var(--hull-grey); + border-radius: 4px; + color: var(--chrome-silver); + cursor: pointer; + transition: all 0.2s ease; +} + +.viewToggleBtn:hover, +.viewToggleBtnActive { + border-color: var(--plasma-cyan); + color: var(--plasma-cyan); +} + +.viewToggleBtnActive { + background: rgba(0, 212, 255, 0.1); +} + +/* === Ship Table === */ +.tableWrap { + overflow-x: auto; + border: 1px solid var(--hull-grey); + border-radius: 8px; +} + +.shipTable { + width: 100%; + border-collapse: collapse; + font-family: var(--font-jetbrains), 'JetBrains Mono', monospace; + font-size: 0.78rem; + min-width: 1400px; +} + +.tableHeaderCell { + padding: 0.6rem 0.75rem; + text-align: left; + color: var(--chrome-silver); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.04em; + white-space: nowrap; + background: rgba(13, 19, 33, 0.9); + border-bottom: 1px solid var(--hull-grey); + cursor: pointer; + user-select: none; + transition: color 0.15s ease; +} + +.tableHeaderCell:hover { + color: var(--star-white); +} + +.sortActive { + color: var(--plasma-cyan); +} + +.sortIndicator { + opacity: 0.4; + font-size: 0.65rem; + margin-left: 0.25rem; +} + +.sortActive .sortIndicator { + opacity: 1; +} + +.tableRow { + border-bottom: 1px solid rgba(255, 255, 255, 0.04); + cursor: pointer; + transition: background 0.1s ease; +} + +.tableRow:hover { + background: rgba(0, 212, 255, 0.05); +} + +.tableRowExpanded { + background: rgba(0, 212, 255, 0.07); + border-bottom: none; +} + +.tableCell { + padding: 0.45rem 0.75rem; + color: var(--chrome-silver); + white-space: nowrap; +} + +.tableCellNum { + text-align: right; + color: var(--star-white); + font-weight: 500; +} + +.tableName { + color: var(--star-white); + font-weight: 600; + position: relative; + padding-left: 1rem; +} + +.tableEmpireAccent { + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 55%; + border-radius: 2px; +} + +.tableExpandRow { + border-bottom: 1px solid var(--hull-grey); +} + +.tableExpandCell { + padding: 0; +} + +.tableExpandContent { + display: flex; + gap: 1.5rem; + padding: 1.25rem 1.5rem; + background: rgba(13, 19, 33, 0.6); +} + +.tableExpandImageWrap { + position: relative; + flex-shrink: 0; + width: 280px; +} + +.tableExpandImage { + width: 100%; + height: auto; + display: block; + border-radius: 6px; + object-fit: cover; +} + +.tableExpandText { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.tableExpandDescription { + font-size: 0.9rem; + color: var(--star-white); + line-height: 1.6; + margin: 0; +} + +.tableExpandLore { + font-size: 0.82rem; + color: var(--chrome-silver); + line-height: 1.75; + opacity: 0.85; + margin: 0; +} + /* === Responsive === */ @media (max-width: 768px) { .main { diff --git a/src/app/ships/page.tsx b/src/app/ships/page.tsx index 3817e6b..379e373 100644 --- a/src/app/ships/page.tsx +++ b/src/app/ships/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useMemo, useRef, useCallback } from 'react' +import { useState, useEffect, useMemo, useRef, useCallback, Fragment } from 'react' import Image from 'next/image' import styles from './page.module.css' @@ -58,6 +58,27 @@ interface ShipsResponse { tiers: number[] } +const TABLE_COLS = [ + { key: 'name', label: 'Name', title: 'Name', numeric: false }, + { key: 'empire_name', label: 'Empire', title: 'Empire', numeric: false }, + { key: 'category', label: 'Category',title: 'Category', numeric: false }, + { key: 'class', label: 'Class', title: 'Class', numeric: false }, + { key: 'tier', label: 'T', title: 'Tier', numeric: true }, + { key: 'base_hull', label: 'Hull', title: 'Hull HP', numeric: true }, + { key: 'base_shield', label: 'Shield', title: 'Shield HP', numeric: true }, + { key: 'base_shield_recharge', label: 'ShRgn', title: 'Shield Recharge/tick', numeric: true }, + { key: 'base_armor', label: 'Armor', title: 'Armor', numeric: true }, + { key: 'base_speed', label: 'Speed', title: 'Speed (AU/tick)', numeric: true }, + { key: 'base_fuel', label: 'Fuel', title: 'Fuel Capacity', numeric: true }, + { key: 'cargo_capacity', label: 'Cargo', title: 'Cargo Capacity', numeric: true }, + { key: 'cpu_capacity', label: 'CPU', title: 'CPU Capacity', numeric: true }, + { key: 'power_capacity', label: 'Power', title: 'Power Capacity', numeric: true }, + { key: 'weapon_slots', label: 'Wpn', title: 'Weapon Slots', numeric: true }, + { key: 'defense_slots', label: 'Def', title: 'Defense Slots', numeric: true }, + { key: 'utility_slots', label: 'Util', title: 'Utility Slots', numeric: true }, + { key: 'price', label: 'Price', title: 'Base Price (cr)', numeric: true }, +] as const + const EMPIRE_COLORS: Record = { solarian: '#ffd700', voidborn: '#9b59b6', @@ -245,7 +266,13 @@ export default function ShipsPage() { const [loading, setLoading] = useState(true) const [error, setError] = useState(false) + const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid') + const [sortCol, setSortCol] = useState('tier') + const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc') + const [tableExpandedId, setTableExpandedId] = useState(null) + const [activeEmpire, setActiveEmpire] = useState('') + const [activeCategory, setActiveCategory] = useState('') const [activeClasses, setActiveClasses] = useState>(new Set()) const [activeTier, setActiveTier] = useState(0) const [search, setSearch] = useState('') @@ -276,6 +303,25 @@ export default function ShipsPage() { }) }, []) + const setView = useCallback((mode: 'grid' | 'table') => { + setViewMode(mode) + if (mode === 'table') { + setExpandedIds(new Set()) + setAllExpanded(false) + } else { + setTableExpandedId(null) + } + }, []) + + const handleSort = useCallback((col: keyof Ship) => { + if (sortCol === col) { + setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')) + } else { + setSortCol(col) + setSortDir('asc') + } + }, [sortCol]) + useEffect(() => { function onClickOutside(e: MouseEvent) { if (classDropdownRef.current && !classDropdownRef.current.contains(e.target as Node)) { @@ -321,11 +367,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 +404,21 @@ export default function ShipsPage() { ) } return result - }, [ships, activeEmpire, activeClasses, activeTier, search]) + }, [ships, activeEmpire, activeCategory, activeClasses, activeTier, search]) + + const tableShips = useMemo(() => { + return [...filteredShips].sort((a, b) => { + const av = a[sortCol] + const bv = b[sortCol] + let cmp = 0 + if (typeof av === 'number' && typeof bv === 'number') { + cmp = av - bv + } else { + cmp = String(av ?? '').localeCompare(String(bv ?? '')) + } + return sortDir === 'asc' ? cmp : -cmp + }) + }, [filteredShips, sortCol, sortDir]) const toggleExpand = (id: string) => { setAllExpanded(false) @@ -391,6 +466,25 @@ export default function ShipsPage() { {!loading && !error && ships.length > 0 && (
+
+ Category + + {categories.map((cat) => ( + + ))} +
+
Empire +
+ {viewMode === 'grid' && ( + + )} +
+ + +
+
@@ -451,7 +574,7 @@ export default function ShipsPage() { Clear all
- {classes.map((cls) => ( + {visibleClasses.map((cls) => (