diff --git a/CLAUDE.md b/CLAUDE.md index 2f1adf13..f56ea6d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,9 +10,9 @@ Web client for **TLE Community** (The Lacuna Expanse), a community-run continuat - `npm start` — start Vite dev server against a real backend (needs `LACUNA_SERVER_URL` etc. set, see `.env.local`) - `npm run start:stubbed` — run the local stubbed server (`stubs/server.ts`, port 3005) and Vite (`--mode stubbed`, loading `.env.stubbed`) together via `concurrently`. The stub server runs under `tsx watch`. `npm run serve:stubs` runs just the stub server. -- `npm run lint` — Oxlint over `app/**/*.{ts,tsx}` -- `npm run format` — Prettier, writes in place -- `npm run types` — `tsc --noEmit` +- `npm run lint:check` — Oxlint (config in `oxlint.config.ts`); `npm run lint:fix` applies autofixes +- `npm run format:check` — Prettier check; `npm run format:fix` writes in place +- `npm run types:check` — `tsc --noEmit` - `npm test` — Jest; `npm run test:watch` for watch mode, `npm run test:coverage` for coverage - Single file: `npx jest app/path/to/file.test.ts` - Single test: `npx jest -t "test name"` diff --git a/app/components/building/storageTab.test.tsx b/app/components/building/storageTab.test.tsx new file mode 100644 index 00000000..4b6370f7 --- /dev/null +++ b/app/components/building/storageTab.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { Building, BuildingsViewResponse } from 'app/interfaces'; +import StorageTab, { hasStorage } from 'app/components/building/storageTab'; + +jest.mock('app/lacuna'); + +const capacities = { + food_capacity: 0, + ore_capacity: 0, + water_capacity: 0, + energy_capacity: 0, + waste_capacity: 0, +}; + +const makeBuilding = (now: Partial, upgrade: Partial = now) => + ({ + id: 7, + url: '/foodreserve', + ...capacities, + ...now, + upgrade: { production: { ...capacities, ...upgrade } }, + }) as Building; + +const renderTab = (building: Building) => + render(); + +test('shows current and upgrade capacity for the resources the building stores', () => { + renderTab(makeBuilding({ food_capacity: 1000 }, { food_capacity: 1500 })); + + expect(screen.getByTitle('1,000')).toBeInTheDocument(); + expect(screen.getByTitle('1,500')).toBeInTheDocument(); +}); + +test('leaves out resources the building stores none of at either level', () => { + const { container } = renderTab(makeBuilding({ food_capacity: 1000 }, { food_capacity: 1500 })); + + // One line per column, both food. + expect(container.querySelectorAll('span[title]')).toHaveLength(2); +}); + +test('a resource only the upgrade stores is still listed', () => { + renderTab(makeBuilding({}, { water_capacity: 250 })); + + expect(screen.getByTitle('0')).toBeInTheDocument(); + expect(screen.getByTitle('250')).toBeInTheDocument(); +}); + +test('hasStorage is false for a building with no capacity at all', () => { + expect(hasStorage(makeBuilding({}))).toBe(false); +}); + +test('hasStorage is true when only the upgrade adds capacity', () => { + expect(hasStorage(makeBuilding({}, { ore_capacity: 10 }))).toBe(true); +}); diff --git a/app/components/building/storageTab.tsx b/app/components/building/storageTab.tsx new file mode 100644 index 00000000..84461dec --- /dev/null +++ b/app/components/building/storageTab.tsx @@ -0,0 +1,76 @@ +import React from 'react'; + +import ResourceLine from 'app/components/building/resourceLine'; + +import { Building, BuildingsViewResponse } from 'app/interfaces'; + +import * as util from 'app/util'; + +type Props = { + view: BuildingsViewResponse; +}; + +const RESOURCES = ['food', 'ore', 'water', 'energy', 'waste'] as const; + +type Resource = (typeof RESOURCES)[number]; + +const current = (building: Building, resource: Resource) => building[`${resource}_capacity`]; + +const upgraded = (building: Building, resource: Resource) => + building.upgrade.production[`${resource}_capacity`]; + +// Only the resources this building stores at either level, so a food reserve lists food alone. +const storedResources = (building: Building) => + RESOURCES.filter( + (resource) => current(building, resource) > 0 || upgraded(building, resource) > 0 + ); + +export const hasStorage = (building: Building) => storedResources(building).length > 0; + +// +// How much each resource the building stores now, beside how much it would store after upgrading. +// Shown on any building with capacity, which covers the dedicated storage buildings as well as the +// ones that store as a side effect (planetary command, distribution center, ...). +// +const StorageTab: React.FunctionComponent = ({ view: { building } }) => { + const resources = storedResources(building); + + return ( +
+
+
+
Current Storage
+
+
+
Upgrade Storage
+
+
+ +
+
+ {resources.map((resource) => ( + + ))} +
+ +
+ {resources.map((resource) => ( + + ))} +
+
+
+ ); +}; + +export default StorageTab; diff --git a/app/hocs/withBuildingTabs.tsx b/app/hocs/withBuildingTabs.tsx index bb6554df..50e1e98d 100644 --- a/app/hocs/withBuildingTabs.tsx +++ b/app/hocs/withBuildingTabs.tsx @@ -10,6 +10,7 @@ import { Tab } from 'app/interfaces/tabber'; import BuildingInformation from 'app/components/building/information'; import ProductionTab from 'app/components/building/productionTab'; import RepairTab from 'app/components/building/repairTab'; +import StorageTab, { hasStorage } from 'app/components/building/storageTab'; function withBuildingTabs( customBuilding: CustomBuildingComponent @@ -22,6 +23,11 @@ function withBuildingTabs( title: 'Production', component: () => , }, + { + title: 'Storage', + shouldRender: hasStorage(this.props.view.building), + component: () => , + }, { title: 'Repair', shouldRender: this.props.view.building.efficiency !== 100,