diff --git a/src/app/components/MainGrid.tsx b/src/app/components/MainGrid.tsx index 9d43d9e..477c696 100644 --- a/src/app/components/MainGrid.tsx +++ b/src/app/components/MainGrid.tsx @@ -10,6 +10,7 @@ import { AccountCard } from "./Account/AccountCard"; import { AccessToken } from "@/types"; import { CharacterContext, SessionContext } from "../context/Context"; import ResponsiveAppBar from "./AppBar/AppBar"; +import { Summary } from "./Summary/Summary"; interface Grouped { [key: string]: AccessToken[]; @@ -58,7 +59,7 @@ export const MainGrid = () => { cardMinHeight: compactMode ? 100 : 170, stoppedPosition: compactMode ? 32 : 48, }, - }) + }), ); useEffect(() => { @@ -74,7 +75,7 @@ export const MainGrid = () => { cardMinHeight: compactMode ? 100 : 170, stoppedPosition: compactMode ? 32 : 48, }, - }) + }), ); }, [compactMode]); @@ -83,6 +84,7 @@ export const MainGrid = () => { + {Object.values(groupByAccount).map((g, id) => ( ({ ...theme.typography.body2, @@ -37,21 +46,16 @@ const Transition = forwardRef(function Transition( }); export const PlanetCard = ({ - planet, character, + planet, }: { - planet: Planet; character: AccessToken; + planet: PlanetWithInfo; }) => { const { alertMode } = useContext(SessionContext); - const [planetInfo, setPlanetInfo] = useState( - undefined, - ); - - const [planetInfoUniverse, setPlanetInfoUniverse] = useState< - PlanetInfoUniverse | undefined - >(undefined); + const planetInfo = planet.info; + const planetInfoUniverse = planet.infoUniverse; const [planetRenderOpen, setPlanetRenderOpen] = useState(false); @@ -71,40 +75,10 @@ export const PlanetCard = ({ .filter((p) => EXTRACTOR_TYPE_IDS.some((e) => e === p.type_id)) .map((p) => p.expiry_time)) ?? []; - const getPlanet = async ( - character: AccessToken, - planet: Planet, - ): Promise => { - const api = new Api(); - const planetInfo = ( - await api.v3.getCharactersCharacterIdPlanetsPlanetId( - character.character.characterId, - planet.planet_id, - { - token: character.access_token, - }, - ) - ).data; - return planetInfo; - }; - - const getPlanetUniverse = async ( - planet: Planet, - ): Promise => { - const api = new Api(); - const planetInfo = ( - await api.v1.getUniversePlanetsPlanetId(planet.planet_id) - ).data; - return planetInfo; - }; const { colors } = useContext(ColorContext); - const expired = extractorsHaveExpired(extractorsExpiryTime) + const expired = extractorsHaveExpired(extractorsExpiryTime); - useEffect(() => { - getPlanet(character, planet).then(setPlanetInfo); - getPlanetUniverse(planet).then(setPlanetInfoUniverse); - }, [planet, character]); return ( {expired && ( { const theme = useTheme(); @@ -53,131 +48,16 @@ export const PlanetTableRow = ({ }; const { piPrices, alertMode } = useContext(SessionContext); - const [planetInfo, setPlanetInfo] = useState( - undefined, - ); - - const [planetInfoUniverse, setPlanetInfoUniverse] = useState< - PlanetInfoUniverse | undefined - >(undefined); - - const [extractors, setExtractors] = useState([]); - const [production, setProduction] = useState( - new Map(), - ); - - const [imports, setImports] = useState< - (typeof PI_SCHEMATICS)[number]["inputs"] - >([]); - - const [exports, setExports] = useState<{ typeId: number; amount: number }[]>( - [], - ); - - type SchematicId = number; - - const getPlanet = async ( - character: AccessToken, - planet: Planet, - ): Promise => { - const api = new Api(); - const planetRes = await api.v3.getCharactersCharacterIdPlanetsPlanetId( - character.character.characterId, - planet.planet_id, - { - token: character.access_token, - }, - ); - - const planetInfo = planetRes.data; - - setExtractors( - planetInfo.pins.filter((p) => - EXTRACTOR_TYPE_IDS.some((e) => e === p.type_id), - ), - ); - - const localProduction = planetInfo.pins - .filter((p) => FACTORY_IDS().some((e) => e.type_id === p.type_id)) - .reduce((acc, f) => { - if (f.schematic_id) { - const schematic = PI_SCHEMATICS.find( - (s) => s.schematic_id == f.schematic_id, - ); - if (schematic) acc.set(f.schematic_id, schematic); - } - return acc; - }, new Map()); - - setProduction(localProduction); - - const locallyProduced = Array.from(localProduction) - .flatMap((p) => p[1].outputs) - .map((p) => p.type_id); - - const locallyConsumed = Array.from(localProduction) - .flatMap((p) => p[1].inputs) - .map((p) => p.type_id); - - const locallyExcavated = planetInfo.pins - .filter((p) => EXTRACTOR_TYPE_IDS.some((e) => e === p.type_id)) - .map((e) => e.extractor_details?.product_type_id ?? 0); - - const localImports = Array.from(localProduction) - .flatMap((p) => p[1].inputs) - .filter( - (p) => - ![...locallyProduced, ...locallyExcavated].some( - (lp) => lp === p.type_id, - ), - ); - - const localExports = locallyProduced - .filter((p) => !locallyConsumed.some((lp) => lp === p)) - .map((typeId) => { - const outputs = PI_SCHEMATICS.flatMap((s) => s.outputs).find( - (s) => s.type_id === typeId, - ); - if (!outputs) return { typeId, amount: 0 }; - const cycleTime = - PI_SCHEMATICS.find((s) => s.schematic_id === outputs.schematic_id) - ?.cycle_time ?? 3600; - const factoriesProducing = planetInfo.pins - .filter((p) => FACTORY_IDS().some((e) => e.type_id === p.type_id)) - .filter((f) => f.schematic_id === outputs?.schematic_id); - const amount = outputs.quantity - ? factoriesProducing.length * outputs.quantity * (3600 / cycleTime) - : 0; - return { - typeId, - amount, - }; - }); - - setImports(localImports); - setExports(localExports); - return planetInfo; - }; - - const getPlanetUniverse = async ( - planet: Planet, - ): Promise => { - const api = new Api(); - const planetInfo = ( - await api.v1.getUniversePlanetsPlanetId(planet.planet_id) - ).data; - return planetInfo; - }; - - useEffect(() => { - getPlanet(character, planet).then(setPlanetInfo); - getPlanetUniverse(planet).then(setPlanetInfoUniverse); - }, [planet, character]); - - const expired = extractorsHaveExpired(extractors.map(e => e.expiry_time)) + const planetInfo = planet.info; + const planetInfoUniverse = planet.infoUniverse; + const { expired, extractors, localProduction, localImports, localExports } = + planetCalculations(planet); const { colors } = useContext(ColorContext); return ( - +
- {Array.from(production).map((schematic, idx) => { + {Array.from(localProduction).map((schematic, idx) => { return (
- {imports.map((i) => ( + {localImports.map((i) => (
- {exports.map((exports) => ( + {localExports.map((exports) => (
- {exports.map((exports) => ( + {localExports.map((exports) => ( - {exports.map((e) => { + {localExports.map((e) => { const valueInMillions = (((piPrices?.appraisal.items.find((a) => a.typeID === e.typeId) ?.prices.sell.min ?? 0) * diff --git a/src/app/components/PlanetaryInteraction/PlanetaryInteractionRow.tsx b/src/app/components/PlanetaryInteraction/PlanetaryInteractionRow.tsx index 24c60fe..e9c1d99 100644 --- a/src/app/components/PlanetaryInteraction/PlanetaryInteractionRow.tsx +++ b/src/app/components/PlanetaryInteraction/PlanetaryInteractionRow.tsx @@ -1,7 +1,5 @@ -import { Api } from "@/esi-api"; -import { AccessToken, Planet } from "@/types"; +import { AccessToken } from "@/types"; import { Stack, Tooltip, Typography, styled, useTheme } from "@mui/material"; -import { useEffect, useState } from "react"; import { PlanetCard } from "./PlanetCard"; import { NoPlanetCard } from "./NoPlanetCard"; import Table from "@mui/material/Table"; @@ -21,25 +19,10 @@ const StackItem = styled(Stack)(({ theme }) => ({ alignItems: "center", })); -const getPlanets = async (character: AccessToken): Promise => { - const api = new Api(); - const planets = ( - await api.v1.getCharactersCharacterIdPlanets( - character.character.characterId, - { - token: character.access_token, - } - ) - ).data; - return planets; -}; - const PlanetaryIteractionTable = ({ character, - planets, }: { character: AccessToken; - planets: Planet[]; }) => { const theme = useTheme(); @@ -102,7 +85,7 @@ const PlanetaryIteractionTable = ({ - {planets.map((planet) => ( + {character.planets.map((planet) => ( { return ( - {planets.map((planet) => ( + {character.planets.map((planet) => ( ))} - {Array.from(Array(6 - planets.length).keys()).map((i, id) => ( + {Array.from(Array(6 - character.planets.length).keys()).map((i, id) => ( @@ -148,15 +129,11 @@ export const PlanetaryInteractionRow = ({ }: { character: AccessToken; }) => { - const [planets, setPlanets] = useState([]); const theme = useTheme(); - useEffect(() => { - getPlanets(character).then(setPlanets).catch(console.log); - }, [character]); return theme.custom.compactMode ? ( - + ) : ( - + ); }; diff --git a/src/app/components/Summary/Summary.tsx b/src/app/components/Summary/Summary.tsx new file mode 100644 index 0000000..962d8a2 --- /dev/null +++ b/src/app/components/Summary/Summary.tsx @@ -0,0 +1,119 @@ +import { PI_TYPES_MAP } from "@/const"; +import { planetCalculations } from "@/planets"; +import { AccessToken } from "@/types"; +import { + TableContainer, + Paper, + Table, + TableHead, + TableRow, + TableCell, + Typography, + Tooltip, + TableBody, + useTheme, + Stack, + styled, +} from "@mui/material"; + +const StackItem = styled(Stack)(({ theme }) => ({ + ...theme.typography.body2, + padding: theme.custom.compactMode ? theme.spacing(1) : theme.spacing(2), + textAlign: "left", + justifyContent: "center", + alignItems: "center", +})); + +interface Grouped { + [key: number]: number; +} + +export const Summary = ({ characters }: { characters: AccessToken[] }) => { + const exports = characters.flatMap((char) => { + return char.planets.flatMap((planet) => { + const { localExports } = planetCalculations(planet); + return localExports; + }); + }); + + const groupedByMaterial = exports.reduce((totals, material) => { + const { typeId, amount } = material; + const newTotal = isNaN(totals[typeId]) ? amount : totals[typeId] + amount; + totals[typeId] = newTotal; + return totals; + }, {}); + + const withProductNameAndPrice = Object.keys(groupedByMaterial).map( + (typeIdString) => { + const typeId = parseInt(typeIdString); + return { + typeId, + amount: groupedByMaterial[typeId], + materialName: PI_TYPES_MAP[typeId].name, + price: 0, + }; + }, + ); + const theme = useTheme(); + + return ( + +

Totals

+ + + + + + + + Exports + + + + + + u/h + + + + + + ISK/M + + + + + + + {withProductNameAndPrice.map((product) => ( + + ))} + +
+
+
+ ); +}; + +const SummaryRow = ({ + material, + amount, + price, +}: { + material: string; + amount: number; + price: number; +}) => ( + + + {material} + + {amount} + {price} + +); diff --git a/src/app/page.tsx b/src/app/page.tsx index 6087f33..a3895ae 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,7 +4,7 @@ import "@fontsource/roboto/400.css"; import "@fontsource/roboto/500.css"; import "@fontsource/roboto/700.css"; import { memo, useCallback, useEffect, useState } from "react"; -import { AccessToken, CharacterUpdate, Env } from "../types"; +import { AccessToken, CharacterUpdate, Env, PlanetWithInfo } from "../types"; import { MainGrid } from "./components/MainGrid"; import { refreshToken } from "@/esi-sso"; import { @@ -16,6 +16,7 @@ import { } from "./context/Context"; import { useSearchParams } from "next/navigation"; import { EvePraisalResult, fetchAllPrices } from "@/eve-praisal"; +import { getPlanet, getPlanetUniverse, getPlanets } from "@/planets"; const Home = () => { const [characters, setCharacters] = useState([]); @@ -31,8 +32,6 @@ const Home = () => { const searchParams = useSearchParams(); const code = searchParams && searchParams.get("code"); - // Memoize chracter state manipulations - const deleteCharacter = (character: AccessToken) => { const charactersToSave = characters.filter( (c) => character.character.characterId !== c.character.characterId, @@ -83,6 +82,26 @@ const Home = () => { return []; }, []); + const initializeCharacterPlanets = ( + characters: AccessToken[], + ): Promise => + Promise.all( + characters.map(async (c) => { + const planets = await getPlanets(c); + const planetsWithInfo: PlanetWithInfo[] = await Promise.all( + planets.map(async (p) => ({ + ...p, + info: await getPlanet(c, p), + infoUniverse: await getPlanetUniverse(p), + })), + ); + return { + ...c, + planets: planetsWithInfo, + }; + }), + ); + const saveCharacters = (characters: AccessToken[]): AccessToken[] => { localStorage.setItem("characters", JSON.stringify(characters)); return characters; @@ -148,6 +167,7 @@ const Home = () => { .then(refreshSession) .then(handleCallback) .then(saveCharacters) + .then(initializeCharacterPlanets) .then(setCharacters) .then(() => setSessionReady(true)); }, []); diff --git a/src/planets.ts b/src/planets.ts new file mode 100644 index 0000000..df774e4 --- /dev/null +++ b/src/planets.ts @@ -0,0 +1,122 @@ +import { + AccessToken, + Planet, + PlanetInfo, + PlanetInfoUniverse, + PlanetWithInfo, +} from "@/types"; +import { Api } from "@/esi-api"; +import { EXTRACTOR_TYPE_IDS, FACTORY_IDS, PI_SCHEMATICS } from "@/const"; +import { extractorsHaveExpired } from "./app/components/PlanetaryInteraction/timeColors"; + +export const getPlanets = async (character: AccessToken): Promise => { + const api = new Api(); + const planets = ( + await api.v1.getCharactersCharacterIdPlanets( + character.character.characterId, + { + token: character.access_token, + }, + ) + ).data; + return planets; +}; + +export const getPlanet = async ( + character: AccessToken, + planet: Planet, +): Promise => { + const api = new Api(); + const planetInfo = ( + await api.v3.getCharactersCharacterIdPlanetsPlanetId( + character.character.characterId, + planet.planet_id, + { + token: character.access_token, + }, + ) + ).data; + return planetInfo; +}; + +export const getPlanetUniverse = async ( + planet: Planet, +): Promise => { + const api = new Api(); + const planetInfo = (await api.v1.getUniversePlanetsPlanetId(planet.planet_id)) + .data; + return planetInfo; +}; + +export const planetCalculations = (planet: PlanetWithInfo) => { + const planetInfo = planet.info; + type SchematicId = number; + const extractors: PlanetInfo["pins"] = planetInfo.pins.filter((p) => + EXTRACTOR_TYPE_IDS.some((e) => e === p.type_id), + ); + const localProduction = planetInfo.pins + .filter((p) => FACTORY_IDS().some((e) => e.type_id === p.type_id)) + .reduce((acc, f) => { + if (f.schematic_id) { + const schematic = PI_SCHEMATICS.find( + (s) => s.schematic_id == f.schematic_id, + ); + if (schematic) acc.set(f.schematic_id, schematic); + } + return acc; + }, new Map()); + + const locallyProduced = Array.from(localProduction) + .flatMap((p) => p[1].outputs) + .map((p) => p.type_id); + + const locallyConsumed = Array.from(localProduction) + .flatMap((p) => p[1].inputs) + .map((p) => p.type_id); + + const locallyExcavated = planetInfo.pins + .filter((p) => EXTRACTOR_TYPE_IDS.some((e) => e === p.type_id)) + .map((e) => e.extractor_details?.product_type_id ?? 0); + + const localImports = Array.from(localProduction) + .flatMap((p) => p[1].inputs) + .filter( + (p) => + ![...locallyProduced, ...locallyExcavated].some( + (lp) => lp === p.type_id, + ), + ); + + const localExports = locallyProduced + .filter((p) => !locallyConsumed.some((lp) => lp === p)) + .map((typeId) => { + const outputs = PI_SCHEMATICS.flatMap((s) => s.outputs).find( + (s) => s.type_id === typeId, + ); + if (!outputs) return { typeId, amount: 0 }; + const cycleTime = + PI_SCHEMATICS.find((s) => s.schematic_id === outputs.schematic_id) + ?.cycle_time ?? 3600; + const factoriesProducing = planetInfo.pins + .filter((p) => FACTORY_IDS().some((e) => e.type_id === p.type_id)) + .filter((f) => f.schematic_id === outputs?.schematic_id); + const amount = outputs.quantity + ? factoriesProducing.length * outputs.quantity * (3600 / cycleTime) + : 0; + return { + typeId, + amount, + }; + }); + + const expired = extractorsHaveExpired(extractors.map((e) => e.expiry_time)); + return { + expired, + localExports, + localImports, + locallyConsumed, + locallyProduced, + localProduction, + extractors, + }; +}; diff --git a/src/types.ts b/src/types.ts index 083e7f7..f359f6f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,6 +10,7 @@ export interface AccessToken { needsLogin: boolean; comment: string; system: string; + planets: PlanetWithInfo[]; } export interface Character { @@ -25,6 +26,7 @@ export interface CharacterPlanets { name: string; characterId: number; account?: string; + system?: string; planets: PlanetWithInfo[]; }