19 Commits

Author SHA1 Message Date
calli
de49595f55 ui tweaks. add FAQ section. move filters to submenu 2025-04-25 09:38:10 +03:00
calli
a1f682e9fc Align tables a bit better and add setting to show icons instead of product texts 2025-04-25 09:01:51 +03:00
calli
67acea9be4 increase automatic refresh to 5 minutes 2025-04-23 18:59:34 +03:00
calli
98b450fcc7 rearrange character layout and align tables to character image 2025-04-23 18:52:05 +03:00
calli
eb15696241 make account card clickable for easier collapse action 2025-04-23 17:59:50 +03:00
calli
741b2480b9 add account level statistics 2025-04-23 17:41:09 +03:00
calli
b185e5d044 hide extraction simulation for planets without extractors 2025-04-23 15:23:57 +03:00
calli
f0d4708b43 show extraction tooltip only if we have extractors 2025-04-23 15:23:32 +03:00
calli
0f33a7ff0c Move activity level local storage read to useState 2025-04-23 15:18:32 +03:00
calli
bbdcece163 Show imports per planet 2025-04-23 15:15:26 +03:00
calli
2a1e74ca79 Remove sentry tunnel routing 2025-04-23 15:14:59 +03:00
calli
0e141321b6 add realized production to totals and settings for comparing it to estimates 2025-04-23 12:31:43 +03:00
calli
d433f5420e Update luxon dependecy 2025-04-23 12:31:19 +03:00
calli
fa5f79068f Refactor compact view planet card to be more compact and show extraction estimates 2025-04-23 11:35:22 +03:00
calli
c07dba3afc Fix extraction amounts to use correct extractors 2025-04-23 11:33:08 +03:00
calli
a871bac35f Move extraction details expand to row click 2025-04-23 11:32:44 +03:00
calli
eca35f51ac Use nextjs Image instead of html img 2025-04-23 11:32:19 +03:00
calli
9e9ab359ec Add a cell to table header to even it out 2025-04-23 11:31:51 +03:00
calli
1fc0efd3da Add mass collapse and expand 2025-04-23 11:30:59 +03:00
22 changed files with 885 additions and 366 deletions

View File

@@ -43,11 +43,6 @@ module.exports = withSentryConfig(
// Transpiles SDK to be compatible with IE11 (increases bundle size) // Transpiles SDK to be compatible with IE11 (increases bundle size)
transpileClientSDK: true, transpileClientSDK: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// Hides source maps from generated client bundles // Hides source maps from generated client bundles
hideSourceMaps: true, hideSourceMaps: true,

32
package-lock.json generated
View File

@@ -22,7 +22,7 @@
"chart.js": "^4.4.7", "chart.js": "^4.4.7",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"eslint": "8.42.0", "eslint": "8.42.0",
"luxon": "^3.3.0", "luxon": "^3.6.1",
"next": "^14.2.23", "next": "^14.2.23",
"next-plausible": "^3.12.0", "next-plausible": "^3.12.0",
"react": "^18.3.1", "react": "^18.3.1",
@@ -37,7 +37,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/luxon": "^3.3.0", "@types/luxon": "^3.6.1",
"@types/react-color": "^3.0.7", "@types/react-color": "^3.0.7",
"@types/three": "^0.152.1", "@types/three": "^0.152.1",
"eslint-config-next": "^14.2.23", "eslint-config-next": "^14.2.23",
@@ -2516,10 +2516,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/luxon": { "node_modules/@types/luxon": {
"version": "3.3.0", "version": "3.6.1",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.1.tgz",
"integrity": "sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg==", "integrity": "sha512-GEo4k1yY5PBaPLqEbk+vp2LhVCKPa/TQlTjuCf3exvx6fjB+jVuDa/Zopi8eznEkJf8yeZRzSK/kzG14g5aXPQ==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/@types/mysql": { "node_modules/@types/mysql": {
"version": "2.15.26", "version": "2.15.26",
@@ -6459,9 +6460,10 @@
} }
}, },
"node_modules/luxon": { "node_modules/luxon": {
"version": "3.3.0", "version": "3.6.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
"integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==", "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
@@ -10963,9 +10965,9 @@
"dev": true "dev": true
}, },
"@types/luxon": { "@types/luxon": {
"version": "3.3.0", "version": "3.6.1",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.1.tgz",
"integrity": "sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg==", "integrity": "sha512-GEo4k1yY5PBaPLqEbk+vp2LhVCKPa/TQlTjuCf3exvx6fjB+jVuDa/Zopi8eznEkJf8yeZRzSK/kzG14g5aXPQ==",
"dev": true "dev": true
}, },
"@types/mysql": { "@types/mysql": {
@@ -13688,9 +13690,9 @@
} }
}, },
"luxon": { "luxon": {
"version": "3.3.0", "version": "3.6.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
"integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==" "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="
}, },
"magic-string": { "magic-string": {
"version": "0.30.17", "version": "0.30.17",

View File

@@ -24,7 +24,7 @@
"chart.js": "^4.4.7", "chart.js": "^4.4.7",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"eslint": "8.42.0", "eslint": "8.42.0",
"luxon": "^3.3.0", "luxon": "^3.6.1",
"next": "^14.2.23", "next": "^14.2.23",
"next-plausible": "^3.12.0", "next-plausible": "^3.12.0",
"react": "^18.3.1", "react": "^18.3.1",
@@ -39,7 +39,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/luxon": "^3.3.0", "@types/luxon": "^3.6.1",
"@types/react-color": "^3.0.7", "@types/react-color": "^3.0.7",
"@types/three": "^0.152.1", "@types/three": "^0.152.1",
"eslint-config-next": "^14.2.23", "eslint-config-next": "^14.2.23",

View File

@@ -1,9 +1,9 @@
import { AccessToken } from "@/types"; import { AccessToken } from "@/types";
import { Box, Stack, Typography, useTheme, Paper, IconButton } from "@mui/material"; import { Box, Stack, Typography, useTheme, Paper, IconButton, Divider } from "@mui/material";
import { CharacterRow } from "../Characters/CharacterRow"; import { CharacterRow } from "../Characters/CharacterRow";
import { PlanetaryInteractionRow } from "../PlanetaryInteraction/PlanetaryInteractionRow"; import { PlanetaryInteractionRow } from "../PlanetaryInteraction/PlanetaryInteractionRow";
import { SessionContext } from "@/app/context/Context"; import { SessionContext } from "@/app/context/Context";
import { useContext, useState } from "react"; import { useContext, useState, useEffect } from "react";
import { PlanRow } from "./PlanRow"; import { PlanRow } from "./PlanRow";
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
@@ -14,17 +14,36 @@ import { STORAGE_IDS } from "@/const";
interface AccountTotals { interface AccountTotals {
monthlyEstimate: number; monthlyEstimate: number;
storageValue: number; storageValue: number;
planetCount: number;
characterCount: number;
runningExtractors: number;
totalExtractors: number;
} }
const calculateAccountTotals = (characters: AccessToken[], piPrices: EvePraisalResult | undefined): AccountTotals => { const calculateAccountTotals = (characters: AccessToken[], piPrices: EvePraisalResult | undefined): AccountTotals => {
let totalMonthlyEstimate = 0; let totalMonthlyEstimate = 0;
let totalStorageValue = 0; let totalStorageValue = 0;
let totalPlanetCount = 0;
let totalCharacterCount = characters.length;
let runningExtractors = 0;
let totalExtractors = 0;
characters.forEach((character) => { characters.forEach((character) => {
totalPlanetCount += character.planets.length;
character.planets.forEach((planet) => { character.planets.forEach((planet) => {
const { localExports } = planetCalculations(planet); const { localExports, extractors } = planetCalculations(planet);
const planetConfig = character.planetConfig.find(p => p.planetId === planet.planet_id); const planetConfig = character.planetConfig.find(p => p.planetId === planet.planet_id);
// Count running and total extractors
if (!planetConfig?.excludeFromTotals) {
extractors.forEach(extractor => {
totalExtractors++;
if (extractor.expiry_time && new Date(extractor.expiry_time) > new Date()) {
runningExtractors++;
}
});
}
// Calculate monthly estimate // Calculate monthly estimate
if (!planetConfig?.excludeFromTotals) { if (!planetConfig?.excludeFromTotals) {
localExports.forEach((exportItem) => { localExports.forEach((exportItem) => {
@@ -56,15 +75,24 @@ const calculateAccountTotals = (characters: AccessToken[], piPrices: EvePraisalR
return { return {
monthlyEstimate: totalMonthlyEstimate, monthlyEstimate: totalMonthlyEstimate,
storageValue: totalStorageValue storageValue: totalStorageValue,
planetCount: totalPlanetCount,
characterCount: totalCharacterCount,
runningExtractors,
totalExtractors
}; };
}; };
export const AccountCard = ({ characters }: { characters: AccessToken[] }) => { export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { characters: AccessToken[], isCollapsed?: boolean }) => {
const theme = useTheme(); const theme = useTheme();
const [isCollapsed, setIsCollapsed] = useState(false); const [localIsCollapsed, setLocalIsCollapsed] = useState(false);
const { planMode, piPrices } = useContext(SessionContext); const { planMode, piPrices } = useContext(SessionContext);
const { monthlyEstimate, storageValue } = calculateAccountTotals(characters, piPrices); const { monthlyEstimate, storageValue, planetCount, characterCount, runningExtractors, totalExtractors } = calculateAccountTotals(characters, piPrices);
// Update local collapse state when prop changes
useEffect(() => {
setLocalIsCollapsed(propIsCollapsed ?? false);
}, [propIsCollapsed]);
return ( return (
<Paper <Paper
@@ -97,7 +125,12 @@ export const AccountCard = ({ characters }: { characters: AccessToken[] }) => {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
cursor: 'pointer',
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}} }}
onClick={() => setLocalIsCollapsed(!localIsCollapsed)}
> >
<Box> <Box>
<Typography <Typography
@@ -111,39 +144,74 @@ export const AccountCard = ({ characters }: { characters: AccessToken[] }) => {
? `Account: ${characters[0].account}` ? `Account: ${characters[0].account}`
: "No account name"} : "No account name"}
</Typography> </Typography>
<Typography <Box sx={{
sx={{ display: 'flex',
fontSize: "0.8rem", gap: 2,
color: theme.palette.text.secondary, flexWrap: 'wrap',
}} mt: 1,
> alignItems: 'center'
Monthly Estimate: {monthlyEstimate >= 1000 }}>
? `${(monthlyEstimate / 1000).toFixed(2)} B` <Typography
: `${monthlyEstimate.toFixed(2)} M`} ISK sx={{
</Typography> fontSize: "0.8rem",
<Typography color: theme.palette.text.secondary,
sx={{ }}
fontSize: "0.8rem", >
color: theme.palette.text.secondary, Monthly: {monthlyEstimate >= 1000
}} ? `${(monthlyEstimate / 1000).toFixed(2)} B`
> : `${monthlyEstimate.toFixed(2)} M`} ISK
Storage Value: {storageValue >= 1000 </Typography>
? `${(storageValue / 1000).toFixed(2)} B` <Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
: `${storageValue.toFixed(2)} M`} ISK <Typography
</Typography> sx={{
fontSize: "0.8rem",
color: theme.palette.text.secondary,
}}
>
Storage: {storageValue >= 1000
? `${(storageValue / 1000).toFixed(2)} B`
: `${storageValue.toFixed(2)} M`} ISK
</Typography>
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
<Typography
sx={{
fontSize: "0.8rem",
color: theme.palette.text.secondary,
}}
>
Planets: {planetCount}
</Typography>
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
<Typography
sx={{
fontSize: "0.8rem",
color: theme.palette.text.secondary,
}}
>
Characters: {characterCount}
</Typography>
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
<Typography
sx={{
fontSize: "0.8rem",
color: runningExtractors < totalExtractors ? theme.palette.error.main : theme.palette.text.secondary,
}}
>
Extractors: {runningExtractors}/{totalExtractors}
</Typography>
</Box>
</Box> </Box>
<IconButton <IconButton
size="small" size="small"
onClick={() => setIsCollapsed(!isCollapsed)}
sx={{ sx={{
transform: isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)', transform: localIsCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease-in-out' transition: 'transform 0.2s ease-in-out'
}} }}
> >
{isCollapsed ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />} {localIsCollapsed ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton> </IconButton>
</Box> </Box>
{!isCollapsed && characters.map((c) => ( {!localIsCollapsed && characters.map((c) => (
<Stack <Stack
key={c.character.characterId} key={c.character.characterId}
direction="row" direction="row"

View File

@@ -1,21 +0,0 @@
import { SessionContext } from "@/app/context/Context";
import { Button, Tooltip } from "@mui/material";
import { useContext } from "react";
export const AlertModeButton = () => {
const { alertMode, toggleAlertMode } = useContext(SessionContext);
return (
<Tooltip title="Toggle alert mode to show only accounts and planets that need action.">
<Button
style={{
backgroundColor: alertMode
? "rgba(144, 202, 249, 0.08)"
: "inherit",
}}
onClick={toggleAlertMode}
>
Alert mode
</Button>
</Tooltip>
);
};

View File

@@ -12,20 +12,25 @@ import * as React from "react";
import { DowloadButton } from "../Backup/DowloadButton"; import { DowloadButton } from "../Backup/DowloadButton";
import { UploadButton } from "../Backup/UploadButton"; import { UploadButton } from "../Backup/UploadButton";
import { CCPButton } from "../CCP/CCPButton"; import { CCPButton } from "../CCP/CCPButton";
import { CompactModeButton } from "../CompactModeButton/CompactModeButton";
import { DiscordButton } from "../Discord/DiscordButton"; import { DiscordButton } from "../Discord/DiscordButton";
import { GitHubButton } from "../Github/GitHubButton"; import { GitHubButton } from "../Github/GitHubButton";
import { LoginButton } from "../Login/LoginButton"; import { LoginButton } from "../Login/LoginButton";
import { PlanModeButton } from "../PlanModeButton/PlanModeButton";
import { SettingsButton } from "../Settings/SettingsButtons"; import { SettingsButton } from "../Settings/SettingsButtons";
import { AlertModeButton } from "../AlertModeButton/AlertModeButton"; import {
import { SupportButton } from "../SupportButton/SupportButton"; Button,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
} from "@mui/material";
import { useState } from "react";
function ResponsiveAppBar() { function ResponsiveAppBar() {
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>( const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(
null, null,
); );
const [faqOpen, setFaqOpen] = useState(false);
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => { const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElNav(event.currentTarget); setAnchorElNav(event.currentTarget);
@@ -102,23 +107,26 @@ function ResponsiveAppBar() {
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<GitHubButton /> <GitHubButton />
</MenuItem> </MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<CCPButton />
</MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<SupportButton />
</MenuItem>
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<SettingsButton /> <SettingsButton />
</MenuItem> </MenuItem>
<MenuItem onClick={handleCloseNavMenu}> <MenuItem
<CompactModeButton /> onClick={() => {
handleCloseNavMenu();
setFaqOpen(true);
}}
>
<Button
href=""
style={{ width: "100%" }}
sx={{ color: "white", display: "flex", justifyContent: "flex-start" }}
>
FAQ
</Button>
</MenuItem> </MenuItem>
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<PlanModeButton /> <CCPButton />
</MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<AlertModeButton />
</MenuItem> </MenuItem>
</Menu> </Menu>
</Box> </Box>
@@ -146,7 +154,7 @@ function ResponsiveAppBar() {
flexGrow: 1, flexGrow: 1,
display: { xs: "none", md: "flex" }, display: { xs: "none", md: "flex" },
alignItems: "center", alignItems: "center",
gap: "0.2rem" gap: "0.2rem",
}} }}
> >
<LoginButton /> <LoginButton />
@@ -154,15 +162,132 @@ function ResponsiveAppBar() {
<UploadButton /> <UploadButton />
<DiscordButton /> <DiscordButton />
<GitHubButton /> <GitHubButton />
<CCPButton />
<SupportButton />
<SettingsButton /> <SettingsButton />
<CompactModeButton /> <Button onClick={() => setFaqOpen(true)} color="inherit">
<PlanModeButton /> FAQ
<AlertModeButton /> </Button>
<CCPButton />
</Box> </Box>
</Toolbar> </Toolbar>
</Container> </Container>
<Dialog
open={faqOpen}
onClose={() => setFaqOpen(false)}
aria-labelledby="faq-dialog-title"
>
<DialogTitle id="faq-dialog-title">
Frequently Asked Questions
</DialogTitle>
<DialogContent>
<DialogContentText>
<strong>EVE Online Partner Code</strong>
<br />
Consider using my partner code for CCP related purchases to support
this project:
<Button
href=""
style={{ width: "100%" }}
sx={{ color: "white", display: "block" }}
onClick={() => {
navigator.clipboard.writeText("CALLIEVE");
}}
>
CALLIEVE
</Button>{" "}
Click button above to copy to clipboard.
<br />
<br />
<strong>What is this application?</strong>
<br />
This EVE Online Planetary Interaction tool that helps you track and
manage your colonies and production chains. Main usecase is to see
if your extractors are running.
<br />
<br />
<strong>How do I add characters?</strong>
<br />
You can add characters by clicking the &quot;Add Character&quot;
button in the app bar and following the authentication process.
<br />
<br />
<strong>Why don&apos;t my storage numbers add up?</strong>
<br />
EVE Online API (ESI) provides planetary interaction endpoints. These
are updated when you submit changes to planet. For example after an
extractor restart.
<br />
<br />
<strong>What does exclude mean?</strong>
<br />
Exclude means that this planet will not be counted in totals. This
is useful if you have longer production chains where exports of a
planet are consumed by another planet.
<br />
<br />
<strong>How do see my planets production chains?</strong>
<br />
Click on the planet row to open the extraction simulation.
<br />
<br />
<strong>What is Compact Mode?</strong>
<br />
Compact Mode reduces the size of character cards to show more
information at once. You can toggle it in the settings.
<br />
<br />
<strong>What is Alert Mode?</strong>
<br />
Alert mode shows only the planets that have extractors offline and
need attention.
<br />
<br />
<strong>What does off-blanace mean?</strong>
<br />
Off-blanace alert shows up for planets that have two extractors that
are extracting different amount of material. Treshold can be set in
settings. Generally you want to keep this below 1000.
<br />
<br />
<strong>How do I reorder accounts?</strong>
<br />
You can drag and drop account cards to reorder them. The order will
be saved automatically.
<br />
<br />
<strong>How do I add a character to account groups?</strong>
<br />
You can add a character to an account group by clicking the cog on
the character card and typing in the account/group name.
<br />
<br />
<strong>I like icons! Why so much text?</strong>
<br />
Toggle this option in settings.
<br />
<br />
<strong>I like colors! Why your alert colors are so ugly?</strong>
<br />
Change the color scheme in settings.
<br />
<br />
<strong>What does the 3D view do?</strong>
<br />
Nothing. Just made it for fun
<br />
<br />
<strong>Why are planet settings empty?</strong>
<br />
Its a work in progress feature. Plan is to be able to configuer
planet and planet exports to belong to production chains.
<br />
<br />
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setFaqOpen(false)}>Close</Button>
</DialogActions>
</Dialog>
</AppBar> </AppBar>
); );
} }

View File

@@ -7,16 +7,18 @@ import { styled, useTheme } from "@mui/material/styles";
import React from "react"; import React from "react";
import { CharacterDialog } from "./CharacterDialog"; import { CharacterDialog } from "./CharacterDialog";
import { AccessToken } from "@/types"; import { AccessToken } from "@/types";
import { Box, Button, Tooltip } from "@mui/material"; import { Box, Tooltip, IconButton, Typography } from "@mui/material";
import SettingsIcon from "@mui/icons-material/Settings";
import { EVE_IMAGE_URL } from "@/const"; import { EVE_IMAGE_URL } from "@/const";
import { CharacterContext } from "@/app/context/Context"; import { CharacterContext } from "@/app/context/Context";
const StackItem = styled(Stack)(({ theme }) => ({ const StackItem = styled(Stack)(({ theme }) => ({
...theme.typography.body2, ...theme.typography.body2,
padding: theme.custom.compactMode ? theme.spacing(1) : theme.spacing(2), padding: theme.custom.compactMode ? theme.spacing(1) : theme.spacing(2),
display: "flex",
textAlign: "left", textAlign: "left",
justifyContent: "center", justifyContent: "flex-start",
alignItems: "center", alignItems: "flex-start",
})); }));
export const CharacterRow = ({ character }: { character: AccessToken }) => { export const CharacterRow = ({ character }: { character: AccessToken }) => {
@@ -29,8 +31,6 @@ export const CharacterRow = ({ character }: { character: AccessToken }) => {
return ( return (
<StackItem <StackItem
key={character.character.characterId} key={character.character.characterId}
alignItems="flex-start"
justifyContent="flex-start"
> >
<CharacterDialog <CharacterDialog
character={selectedCharacter} character={selectedCharacter}
@@ -38,13 +38,49 @@ export const CharacterRow = ({ character }: { character: AccessToken }) => {
updateCharacter={updateCharacter} updateCharacter={updateCharacter}
closeDialog={() => setSelectedCharacter(undefined)} closeDialog={() => setSelectedCharacter(undefined)}
/> />
<Typography
sx={{
whiteSpace: "nowrap",
fontSize: theme.custom.smallText,
textAlign: "left",
lineHeight: 1.2,
marginBottom: "0.4rem",
marginLeft: "0.2rem",
overflow: "visible",
textOverflow: "clip",
width: "1rem"
}}
>
{character.character.name}
</Typography>
<Tooltip title={character.comment}> <Tooltip title={character.comment}>
<Box <Box
display="flex" display="flex"
flexDirection="column" flexDirection="column"
maxWidth={120} maxWidth={120}
onClick={() => setSelectedCharacter(character)} onClick={() => setSelectedCharacter(character)}
position="relative"
sx={{ cursor: "pointer" }}
> >
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
setSelectedCharacter(character);
}}
sx={{
p: 0,
position: "absolute",
top: 4,
left: 4,
backgroundColor: "rgba(0, 0, 0, 0.5)",
"&:hover": {
backgroundColor: "rgba(0, 0, 0, 0.7)",
},
}}
>
<SettingsIcon fontSize="small" sx={{ color: "white" }} />
</IconButton>
<Image <Image
unoptimized unoptimized
src={`${EVE_IMAGE_URL}/characters/${character.character.characterId}/portrait?size=128`} src={`${EVE_IMAGE_URL}/characters/${character.character.characterId}/portrait?size=128`}
@@ -53,16 +89,6 @@ export const CharacterRow = ({ character }: { character: AccessToken }) => {
height={theme.custom.cardImageSize} height={theme.custom.cardImageSize}
style={{ marginBottom: "0.2rem", borderRadius: 8 }} style={{ marginBottom: "0.2rem", borderRadius: 8 }}
/> />
<Button
style={{
padding: 6,
fontSize: theme.custom.smallText,
lineHeight: 1,
}}
variant="outlined"
>
{character.character.name}
</Button>
</Box> </Box>
</Tooltip> </Tooltip>
</StackItem> </StackItem>

View File

@@ -1,21 +0,0 @@
import { SessionContext } from "@/app/context/Context";
import { Button, Tooltip } from "@mui/material";
import { useContext } from "react";
export const CompactModeButton = () => {
const { compactMode, toggleCompactMode } = useContext(SessionContext);
return (
<Tooltip title="Toggle compact layout for widescreen">
<Button
style={{
backgroundColor: compactMode
? "rgba(144, 202, 249, 0.08)"
: "inherit",
}}
onClick={toggleCompactMode}
>
Compact mode
</Button>
</Tooltip>
);
};

View File

@@ -5,13 +5,22 @@ import {
Grid, Grid,
ThemeProvider, ThemeProvider,
createTheme, createTheme,
Button,
Tooltip,
} from "@mui/material"; } from "@mui/material";
import { AccountCard } from "./Account/AccountCard"; import { AccountCard } from "./Account/AccountCard";
import { AccessToken } from "@/types"; import { AccessToken } from "@/types";
import { CharacterContext, SessionContext } from "../context/Context"; import { CharacterContext, SessionContext } from "../context/Context";
import ResponsiveAppBar from "./AppBar/AppBar"; import ResponsiveAppBar from "./AppBar/AppBar";
import { Summary } from "./Summary/Summary"; import { Summary } from "./Summary/Summary";
import { DragDropContext, Droppable, Draggable, DropResult } from "@hello-pangea/dnd"; import {
DragDropContext,
Droppable,
Draggable,
DropResult,
} from "@hello-pangea/dnd";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
interface Grouped { interface Grouped {
[key: string]: AccessToken[]; [key: string]: AccessToken[];
@@ -40,7 +49,9 @@ declare module "@mui/material/styles" {
export const MainGrid = () => { export const MainGrid = () => {
const { characters, updateCharacter } = useContext(CharacterContext); const { characters, updateCharacter } = useContext(CharacterContext);
const { compactMode, toggleCompactMode, alertMode, toggleAlertMode, planMode, togglePlanMode } = useContext(SessionContext);
const [accountOrder, setAccountOrder] = useState<string[]>([]); const [accountOrder, setAccountOrder] = useState<string[]>([]);
const [allCollapsed, setAllCollapsed] = useState(false);
// Initialize account order when characters change // Initialize account order when characters change
useEffect(() => { useEffect(() => {
@@ -50,15 +61,19 @@ export const MainGrid = () => {
group[account ?? ""] = group[account ?? ""] ?? []; group[account ?? ""] = group[account ?? ""] ?? [];
group[account ?? ""].push(character); group[account ?? ""].push(character);
return group; return group;
}, {}) }, {}),
); );
const savedOrder = localStorage.getItem('accountOrder'); const savedOrder = localStorage.getItem("accountOrder");
if (savedOrder) { if (savedOrder) {
try { try {
const parsedOrder = JSON.parse(savedOrder); const parsedOrder = JSON.parse(savedOrder);
const validOrder = parsedOrder.filter((account: string) => currentAccounts.includes(account)); const validOrder = parsedOrder.filter((account: string) =>
const newAccounts = currentAccounts.filter(account => !validOrder.includes(account)); currentAccounts.includes(account),
);
const newAccounts = currentAccounts.filter(
(account) => !validOrder.includes(account),
);
setAccountOrder([...validOrder, ...newAccounts]); setAccountOrder([...validOrder, ...newAccounts]);
} catch (e) { } catch (e) {
setAccountOrder(currentAccounts); setAccountOrder(currentAccounts);
@@ -70,7 +85,7 @@ export const MainGrid = () => {
useEffect(() => { useEffect(() => {
if (accountOrder.length > 0) { if (accountOrder.length > 0) {
localStorage.setItem('accountOrder', JSON.stringify(accountOrder)); localStorage.setItem("accountOrder", JSON.stringify(accountOrder));
} }
}, [accountOrder]); }, [accountOrder]);
@@ -81,7 +96,6 @@ export const MainGrid = () => {
return group; return group;
}, {}); }, {});
const { compactMode } = useContext(SessionContext);
const [darkTheme, setDarkTheme] = useState( const [darkTheme, setDarkTheme] = useState(
createTheme({ createTheme({
palette: { palette: {
@@ -134,13 +148,70 @@ export const MainGrid = () => {
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<ResponsiveAppBar /> <ResponsiveAppBar />
{compactMode ? <></> : <Summary characters={characters} />} {compactMode ? <></> : <Summary characters={characters} />}
<Box
sx={{
display: "flex",
justifyContent: "flex-start",
padding: 1,
gap: 1,
}}
>
<Button
startIcon={
allCollapsed ? <KeyboardArrowDownIcon /> : <KeyboardArrowUpIcon />
}
onClick={() => setAllCollapsed(!allCollapsed)}
size="small"
>
{allCollapsed ? "Expand All" : "Collapse All"}
</Button>
<Tooltip title="Toggle compact layout for widescreen">
<Button
size="small"
style={{
backgroundColor: compactMode
? "rgba(144, 202, 249, 0.08)"
: "inherit",
}}
onClick={toggleCompactMode}
>
Compact mode
</Button>
</Tooltip>
<Tooltip title="Toggle alert mode to show only accounts and planets that need action.">
<Button
size="small"
style={{
backgroundColor: alertMode
? "rgba(144, 202, 249, 0.08)"
: "inherit",
}}
onClick={toggleAlertMode}
>
Alert mode
</Button>
</Tooltip>
<Tooltip title="Toggle plan mode that show layout for widescreen">
<Button
size="small"
style={{
backgroundColor: planMode
? "rgba(144, 202, 249, 0.08)"
: "inherit",
}}
onClick={togglePlanMode}
>
Plan mode
</Button>
</Tooltip>
</Box>
<DragDropContextComponent onDragEnd={handleDragEnd}> <DragDropContextComponent onDragEnd={handleDragEnd}>
<DroppableComponent droppableId="accounts"> <DroppableComponent droppableId="accounts">
{(provided: any) => ( {(provided: any) => (
<Grid <Grid
container container
spacing={1} spacing={1}
sx={{ padding: 1 }} sx={{ padding: 1, width: "100%" }}
{...provided.droppableProps} {...provided.droppableProps}
ref={provided.innerRef} ref={provided.innerRef}
> >
@@ -159,14 +230,18 @@ export const MainGrid = () => {
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
sx={{ sx={{
'& > *': { "& > *": {
width: '100%', width: "100%",
}, },
}} }}
> >
{groupByAccount[account] && groupByAccount[account].length > 0 && ( {groupByAccount[account] &&
<AccountCard characters={groupByAccount[account]} /> groupByAccount[account].length > 0 && (
)} <AccountCard
characters={groupByAccount[account]}
isCollapsed={allCollapsed}
/>
)}
</Grid> </Grid>
)} )}
</DraggableComponent> </DraggableComponent>

View File

@@ -1,15 +0,0 @@
import { SessionContext } from "@/app/context/Context";
import { Button, Tooltip } from "@mui/material";
import { useContext } from "react";
export const PlanModeButton = () => {
const { planMode, togglePlanMode } = useContext(SessionContext);
return (
<Tooltip title="Toggle plan mode that show layout for widescreen">
<Button onClick={togglePlanMode} style={{backgroundColor: planMode ? 'rgba(144, 202, 249, 0.08)' : 'inherit'}}>
Plan mode
</Button>
</Tooltip>
);
};

View File

@@ -170,7 +170,7 @@ export const ExtractionSimulationDisplay: React.FC<ExtractionSimulationDisplayPr
return ( return (
<Box> <Box>
<Paper sx={{ p: 2 }}> {extractors.length > 0 ? <Paper sx={{ p: 2 }}>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Extraction Simulation Extraction Simulation
</Typography> </Typography>
@@ -210,7 +210,7 @@ export const ExtractionSimulationDisplay: React.FC<ExtractionSimulationDisplayPr
<div style={{ height: '300px' }}> <div style={{ height: '300px' }}>
<Line data={chartData} options={chartOptions} /> <Line data={chartData} options={chartOptions} />
</div> </div>
</Paper> </Paper> : null}
<ProductionChainVisualization <ProductionChainVisualization
extractedTypeIds={extractedTypeIds} extractedTypeIds={extractedTypeIds}

View File

@@ -133,9 +133,9 @@ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipPr
</Box> </Box>
<Box sx={{ flex: 1, minWidth: 0 }}> <Box sx={{ flex: 1, minWidth: 0 }}>
<Stack spacing={1}> <Stack spacing={1}>
{extractorPrograms.map(({ typeId, cycleTime, cycles, installTime, expiryTime }) => { {extractorPrograms.map(({ typeId, cycleTime, cycles, installTime, expiryTime }, idx) => {
const prediction = getProgramOutputPrediction( const prediction = getProgramOutputPrediction(
extractors.find(e => e.typeId === typeId)?.baseValue || 0, extractors[idx].baseValue,
CYCLE_TIME, CYCLE_TIME,
cycles cycles
); );
@@ -159,6 +159,9 @@ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipPr
<Typography variant="body2"> <Typography variant="body2">
Average per Cycle: {(totalOutput / cycles).toFixed(1)} units Average per Cycle: {(totalOutput / cycles).toFixed(1)} units
</Typography> </Typography>
<Typography variant="body2">
Average per hour: {(totalOutput / cycles * 2).toFixed(1)} units
</Typography>
<Typography variant="body2"> <Typography variant="body2">
Expires in: <Countdown overtime={true} date={DateTime.fromISO(expiryTime).toMillis()} /> Expires in: <Countdown overtime={true} date={DateTime.fromISO(expiryTime).toMillis()} />
</Typography> </Typography>

View File

@@ -1,31 +1,21 @@
import { Stack, Typography, styled, useTheme } from "@mui/material"; import { Stack, Typography, styled, useTheme, Tooltip } from "@mui/material";
import Image from "next/image"; import Image from "next/image";
import { import {
AccessToken, AccessToken,
Planet,
PlanetInfo,
PlanetInfoUniverse,
PlanetWithInfo, PlanetWithInfo,
} from "@/types"; } from "@/types";
import React, { forwardRef, useContext, useEffect, useState } from "react"; import React, { useContext } from "react";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { EXTRACTOR_TYPE_IDS } from "@/const"; import { EXTRACTOR_TYPE_IDS } from "@/const";
import Countdown from "react-countdown"; import Countdown from "react-countdown";
import PinsCanvas3D from "./PinsCanvas3D"; import { getProgramOutputPrediction } from "./ExtractionSimulation";
import Slide from "@mui/material/Slide";
import { TransitionProps } from "@mui/material/transitions";
import Dialog from "@mui/material/Dialog";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import Button from "@mui/material/Button";
import { import {
alertModeVisibility, alertModeVisibility,
extractorsHaveExpired, extractorsHaveExpired,
timeColor, timeColor,
} from "./timeColors"; } from "./timeColors";
import { ColorContext, SessionContext } from "@/app/context/Context"; import { ColorContext, SessionContext } from "@/app/context/Context";
import { ExtractionSimulationTooltip } from "./ExtractionSimulationTooltip";
const StackItem = styled(Stack)(({ theme }) => ({ const StackItem = styled(Stack)(({ theme }) => ({
...theme.typography.body2, ...theme.typography.body2,
@@ -36,15 +26,6 @@ const StackItem = styled(Stack)(({ theme }) => ({
alignItems: "center", alignItems: "center",
})); }));
const Transition = forwardRef(function Transition(
props: TransitionProps & {
children: React.ReactElement;
},
ref: React.Ref<unknown>,
) {
return <Slide direction="up" ref={ref} {...props} />;
});
export const PlanetCard = ({ export const PlanetCard = ({
character, character,
planet, planet,
@@ -57,18 +38,8 @@ export const PlanetCard = ({
const planetInfo = planet.info; const planetInfo = planet.info;
const planetInfoUniverse = planet.infoUniverse; const planetInfoUniverse = planet.infoUniverse;
const [planetRenderOpen, setPlanetRenderOpen] = useState(false);
const theme = useTheme(); const theme = useTheme();
const handle3DrenderOpen = () => {
setPlanetRenderOpen(true);
};
const handle3DrenderClose = () => {
setPlanetRenderOpen(false);
};
const extractorsExpiryTime = const extractorsExpiryTime =
(planetInfo && (planetInfo &&
planetInfo.pins planetInfo.pins
@@ -79,7 +50,77 @@ export const PlanetCard = ({
const { colors } = useContext(ColorContext); const { colors } = useContext(ColorContext);
const expired = extractorsHaveExpired(extractorsExpiryTime); const expired = extractorsHaveExpired(extractorsExpiryTime);
const CYCLE_TIME = 30 * 60; // 30 minutes in seconds
const extractors = planetInfo?.pins
.filter((p) => EXTRACTOR_TYPE_IDS.some((e) => e === p.type_id))
.map((p) => ({
typeId: p.type_id,
baseValue: p.extractor_details?.qty_per_cycle || 0,
cycleTime: p.extractor_details?.cycle_time || 3600,
installTime: p.install_time || "",
expiryTime: p.expiry_time || "",
installedSchematicId: p.extractor_details?.product_type_id || undefined
})) || [];
// Calculate program duration and cycles for each extractor
const extractorPrograms = extractors.map(extractor => {
const installDate = new Date(extractor.installTime);
const expiryDate = new Date(extractor.expiryTime);
const programDuration = (expiryDate.getTime() - installDate.getTime()) / 1000; // Convert to seconds
return {
...extractor,
programDuration,
cycles: Math.floor(programDuration / CYCLE_TIME)
};
});
// Get output predictions for each extractor
const extractorOutputs = extractorPrograms.map(extractor => ({
typeId: extractor.typeId,
cycleTime: CYCLE_TIME,
cycles: extractor.cycles,
prediction: getProgramOutputPrediction(
extractor.baseValue,
CYCLE_TIME,
extractor.cycles
)
}));
// Calculate average per hour for each extractor
const extractorAverages = extractorOutputs.map(extractor => {
const totalOutput = extractor.prediction.reduce((sum, val) => sum + val, 0);
const programDuration = extractor.cycles * CYCLE_TIME;
const averagePerHour = (totalOutput / programDuration) * 3600;
return {
typeId: extractor.typeId,
averagePerHour
};
});
return ( return (
<Tooltip
title={
extractors.length > 0 ? (
<ExtractionSimulationTooltip
extractors={extractors}
/>
) : null
}
componentsProps={{
tooltip: {
sx: {
bgcolor: 'background.paper',
'& .MuiTooltip-arrow': {
color: 'background.paper',
},
maxWidth: 'none',
width: 'fit-content'
}
}
}}
>
<StackItem <StackItem
alignItems="flex-start" alignItems="flex-start"
height="100%" height="100%"
@@ -87,15 +128,32 @@ export const PlanetCard = ({
minHeight={theme.custom.cardMinHeight} minHeight={theme.custom.cardMinHeight}
visibility={alertModeVisibility(alertMode, expired)} visibility={alertModeVisibility(alertMode, expired)}
> >
<Image
unoptimized <div style={{ position: 'relative' }}>
src={`/${planet.planet_type}.png`} <Image
alt="" unoptimized
width={theme.custom.cardImageSize} src={`/${planet.planet_type}.png`}
height={theme.custom.cardImageSize} alt=""
style={{ borderRadius: 8, marginRight: 4 }} width={theme.custom.cardImageSize}
onClick={handle3DrenderOpen} height={theme.custom.cardImageSize}
/> style={{
borderRadius: 8,
marginRight: 4,
position: 'relative',
zIndex: 0
}}
/>
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: theme.custom.cardImageSize,
height: theme.custom.cardImageSize,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderRadius: 8,
}} />
</div>
{expired && ( {expired && (
<Image <Image
width={32} width={32}
@@ -109,55 +167,41 @@ export const PlanetCard = ({
<Typography fontSize={theme.custom.smallText}> <Typography fontSize={theme.custom.smallText}>
{planetInfoUniverse?.name} {planetInfoUniverse?.name}
</Typography> </Typography>
<Typography fontSize={theme.custom.smallText}> {extractorsExpiryTime.map((e, idx) => {
L{planet.upgrade_level} const extractor = extractors[idx];
</Typography> const average = extractorAverages[idx];
return (
<div key={`${e}-${idx}-${character.character.characterId}`}>
<Typography
color={timeColor(e, colors)}
fontSize={theme.custom.smallText}
>
{!expired && e && <Countdown
overtime={true}
date={DateTime.fromISO(e).toMillis()}
/>
}
</Typography>
{!expired && extractor && average && (
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Image
unoptimized
src={`https://images.evetech.net/types/${extractor.installedSchematicId}/icon?size=32`}
alt=""
width={16}
height={16}
style={{ borderRadius: 4 }}
/>
<Typography fontSize={theme.custom.smallText}>
{average.averagePerHour.toFixed(1)}/h
</Typography>
</div>
)}
</div>
);
})}
</div> </div>
{extractorsExpiryTime.map((e, idx) => {
return (
<Typography
key={`${e}-${idx}-${character.character.characterId}`}
color={timeColor(e, colors)}
fontSize={theme.custom.smallText}
>
{e ? (
<Countdown
overtime={true}
date={DateTime.fromISO(e).toMillis()}
/>
) : (
"STOPPED"
)}
</Typography>
);
})}
<Dialog
fullScreen
open={planetRenderOpen}
onClose={handle3DrenderClose}
TransitionComponent={Transition}
>
<AppBar sx={{ position: "relative" }}>
<Toolbar>
<IconButton
edge="start"
color="inherit"
onClick={handle3DrenderClose}
aria-label="close"
>
<CloseIcon />
</IconButton>
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
{planetInfoUniverse?.name}
</Typography>
<Button autoFocus color="inherit" onClick={handle3DrenderClose}>
Close
</Button>
</Toolbar>
</AppBar>
<PinsCanvas3D planetInfo={planetInfo} />
</Dialog>
</StackItem> </StackItem>
</Tooltip>
); );
}; };

View File

@@ -1,5 +1,5 @@
import { ColorContext, SessionContext } from "@/app/context/Context"; import { ColorContext, SessionContext } from "@/app/context/Context";
import { PI_TYPES_MAP, STORAGE_IDS, STORAGE_CAPACITIES, PI_PRODUCT_VOLUMES } from "@/const"; import { PI_TYPES_MAP, STORAGE_IDS, STORAGE_CAPACITIES, PI_PRODUCT_VOLUMES, EVE_IMAGE_URL } from "@/const";
import { planetCalculations } from "@/planets"; import { planetCalculations } from "@/planets";
import { AccessToken, PlanetWithInfo } from "@/types"; import { AccessToken, PlanetWithInfo } from "@/types";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
@@ -23,8 +23,6 @@ import { ExtractionSimulationDisplay } from './ExtractionSimulationDisplay';
import { ExtractionSimulationTooltip } from './ExtractionSimulationTooltip'; import { ExtractionSimulationTooltip } from './ExtractionSimulationTooltip';
import { ProductionNode } from './ExtractionSimulation'; import { ProductionNode } from './ExtractionSimulation';
import { Collapse, Box, Stack } from "@mui/material"; import { Collapse, Box, Stack } from "@mui/material";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
const Transition = forwardRef(function Transition( const Transition = forwardRef(function Transition(
props: TransitionProps & { props: TransitionProps & {
@@ -43,6 +41,7 @@ export const PlanetTableRow = ({
character: AccessToken; character: AccessToken;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { showProductIcons } = useContext(SessionContext);
const [planetRenderOpen, setPlanetRenderOpen] = useState(false); const [planetRenderOpen, setPlanetRenderOpen] = useState(false);
const [planetConfigOpen, setPlanetConfigOpen] = useState(false); const [planetConfigOpen, setPlanetConfigOpen] = useState(false);
@@ -155,13 +154,56 @@ export const PlanetTableRow = ({
}); });
}; };
const renderProductDisplay = (typeId: number, amount?: number) => {
if (showProductIcons) {
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}>
<Image
src={`${EVE_IMAGE_URL}/types/${typeId}/icon?size=32`}
alt={PI_TYPES_MAP[typeId].name}
width={32}
height={32}
style={{ marginRight: "5px" }}
/>
{amount !== undefined && (
<Typography fontSize={theme.custom.smallText} style={{ marginLeft: "5px", flexShrink: 0 }}>
{amount}
</Typography>
)}
</div>
);
}
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}>
<Typography fontSize={theme.custom.smallText}>
{PI_TYPES_MAP[typeId].name}
</Typography>
{amount !== undefined && (
<Typography fontSize={theme.custom.smallText} style={{ marginLeft: "5px", flexShrink: 0 }}>
{amount}
</Typography>
)}
</div>
);
};
return ( return (
<> <>
<TableRow <TableRow
style={{ visibility: alertModeVisibility(alertMode, expired) }} style={{ visibility: alertModeVisibility(alertMode, expired) }}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }} sx={{
"&:last-child td, &:last-child th": { border: 0 },
cursor: 'pointer',
'&:hover': {
backgroundColor: 'action.hover'
}
}}
onClick={(e) => {
if (!(e.target as HTMLElement).closest('.clickable-cell')) return;
setSimulationOpen(!simulationOpen);
}}
> >
<TableCell component="th" scope="row"> <TableCell component="th" scope="row" className="clickable-cell">
<Tooltip <Tooltip
title={`${ title={`${
planet.planet_type.charAt(0).toUpperCase() + planet.planet_type.charAt(0).toUpperCase() +
@@ -179,17 +221,19 @@ export const PlanetTableRow = ({
<Tooltip <Tooltip
placement="right" placement="right"
title={ title={
<ExtractionSimulationTooltip extractors.length > 0 ? (
extractors={extractors <ExtractionSimulationTooltip
.filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle) extractors={extractors
.map(e => ({ .filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle)
typeId: e.extractor_details!.product_type_id!, .map(e => ({
baseValue: e.extractor_details!.qty_per_cycle!, typeId: e.extractor_details!.product_type_id!,
cycleTime: e.extractor_details!.cycle_time || 3600, baseValue: e.extractor_details!.qty_per_cycle!,
installTime: e.install_time ?? "", cycleTime: e.extractor_details!.cycle_time || 3600,
expiryTime: e.expiry_time ?? "" installTime: e.install_time ?? "",
}))} expiryTime: e.expiry_time ?? ""
/> }))}
/>
) : null
} }
componentsProps={{ componentsProps={{
tooltip: { tooltip: {
@@ -225,14 +269,15 @@ export const PlanetTableRow = ({
</div> </div>
</Tooltip> </Tooltip>
</TableCell> </TableCell>
<TableCell>{planet.upgrade_level}</TableCell> <TableCell className="clickable-cell">{planet.upgrade_level}</TableCell>
<TableCell> <TableCell className="clickable-cell">
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{extractors.length === 0 &&<Typography fontSize={theme.custom.smallText}>No extractors</Typography>}
{extractors.map((e, idx) => { {extractors.map((e, idx) => {
return ( return (
<div <div
key={`${e}-${idx}-${character.character.characterId}`} key={`${e}-${idx}-${character.character.characterId}`}
style={{ display: "flex" }} style={{ display: "flex", alignItems: "center" }}
> >
<Typography <Typography
color={timeColor(e.expiry_time, colors)} color={timeColor(e.expiry_time, colors)}
@@ -248,52 +293,47 @@ export const PlanetTableRow = ({
"STOPPED" "STOPPED"
)} )}
</Typography> </Typography>
<Typography fontSize={theme.custom.smallText}> {renderProductDisplay(e.extractor_details?.product_type_id ?? 0)}
{
PI_TYPES_MAP[e.extractor_details?.product_type_id ?? 0]
?.name
}
</Typography>
</div> </div>
); );
})} })}
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell className="clickable-cell">
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{Array.from(localProduction).map((schematic, idx) => { {Array.from(localProduction).map((schematic, idx) => {
return ( return (
<Typography <div
key={`prod-${character.character.characterId}-${planet.planet_id}-${idx}`} key={`prod-${character.character.characterId}-${planet.planet_id}-${idx}`}
fontSize={theme.custom.smallText} style={{ display: "flex", alignItems: "center" }}
> >
{schematic[1].name} {renderProductDisplay(schematic[1].outputs[0].type_id)}
</Typography> </div>
); );
})} })}
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell className="clickable-cell">
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{localImports.map((i) => ( {localImports.map((i) => (
<Typography <div
key={`import-${character.character.characterId}-${planet.planet_id}-${i.type_id}`} key={`import-${character.character.characterId}-${planet.planet_id}-${i.type_id}`}
fontSize={theme.custom.smallText} style={{ display: "flex", alignItems: "center" }}
> >
{PI_TYPES_MAP[i.type_id].name} ({i.quantity}/h) {renderProductDisplay(i.type_id, i.quantity * i.factoryCount)}
</Typography> </div>
))} ))}
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell className="clickable-cell">
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{localExports.map((exports) => ( {localExports.map((exports) => (
<Typography <div
key={`export-${character.character.characterId}-${planet.planet_id}-${exports.typeId}`} key={`export-${character.character.characterId}-${planet.planet_id}-${exports.typeId}`}
fontSize={theme.custom.smallText} style={{ display: "flex", alignItems: "center" }}
> >
{PI_TYPES_MAP[exports.typeId].name} {renderProductDisplay(exports.typeId, exports.amount)}
</Typography> </div>
))} ))}
</div> </div>
</TableCell> </TableCell>
@@ -314,7 +354,7 @@ export const PlanetTableRow = ({
))} ))}
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell className="clickable-cell">
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{localExports.map((exports) => ( {localExports.map((exports) => (
<Typography <Typography
@@ -326,7 +366,7 @@ export const PlanetTableRow = ({
))} ))}
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell className="clickable-cell">
<div <div
style={{ style={{
display: "flex", display: "flex",
@@ -359,8 +399,9 @@ export const PlanetTableRow = ({
})} })}
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell className="clickable-cell">
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{storageFacilities.length === 0 &&<Typography fontSize={theme.custom.smallText}>No storage</Typography>}
{storageFacilities {storageFacilities
.sort((a, b) => { .sort((a, b) => {
const isALaunchpad = a.type_id === 2256 || a.type_id === 2542 || a.type_id === 2543 || a.type_id === 2544 || a.type_id === 2552 || a.type_id === 2555 || a.type_id === 2556 || a.type_id === 2557; const isALaunchpad = a.type_id === 2256 || a.type_id === 2542 || a.type_id === 2543 || a.type_id === 2544 || a.type_id === 2552 || a.type_id === 2555 || a.type_id === 2556 || a.type_id === 2557;
@@ -401,7 +442,7 @@ export const PlanetTableRow = ({
})} })}
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell className="menu-cell">
<IconButton <IconButton
aria-label="more" aria-label="more"
aria-controls="planet-menu" aria-controls="planet-menu"
@@ -423,14 +464,6 @@ export const PlanetTableRow = ({
}}> }}>
Configure Planet Configure Planet
</MenuItem> </MenuItem>
{extractors.length > 0 && (
<MenuItem onClick={() => {
setSimulationOpen(!simulationOpen);
handleMenuClose();
}}>
{simulationOpen ? 'Hide Extraction Simulation' : 'Show Extraction Simulation'}
</MenuItem>
)}
<MenuItem onClick={() => { <MenuItem onClick={() => {
handle3DrenderOpen(); handle3DrenderOpen();
handleMenuClose(); handleMenuClose();
@@ -441,7 +474,7 @@ export const PlanetTableRow = ({
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell colSpan={6} style={{ paddingBottom: 0, paddingTop: 0 }}> <TableCell colSpan={6} style={{ paddingBottom: 0, paddingTop: 0, border: 'none' }}>
<Collapse in={simulationOpen} timeout="auto" unmountOnExit> <Collapse in={simulationOpen} timeout="auto" unmountOnExit>
<Box sx={{ my: 2 }}> <Box sx={{ my: 2 }}>
<ExtractionSimulationDisplay <ExtractionSimulationDisplay

View File

@@ -1,5 +1,5 @@
import { AccessToken } from "@/types"; import { AccessToken } from "@/types";
import { Stack, Tooltip, Typography, styled, useTheme } from "@mui/material"; import { Icon, IconButton, Stack, Tooltip, Typography, styled, useTheme } from "@mui/material";
import { PlanetCard } from "./PlanetCard"; import { PlanetCard } from "./PlanetCard";
import { NoPlanetCard } from "./NoPlanetCard"; import { NoPlanetCard } from "./NoPlanetCard";
import Table from "@mui/material/Table"; import Table from "@mui/material/Table";
@@ -10,7 +10,7 @@ import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow"; import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import { PlanetTableRow } from "./PlanetTableRow"; import { PlanetTableRow } from "./PlanetTableRow";
import Image from "next/image"; import { Settings } from "@mui/icons-material";
const StackItem = styled(Stack)(({ theme }) => ({ const StackItem = styled(Stack)(({ theme }) => ({
...theme.typography.body2, ...theme.typography.body2,
@@ -36,8 +36,8 @@ const PlanetaryIteractionTable = ({
return ( return (
<StackItem width="100%"> <StackItem width="100%">
<TableContainer component={Paper}> <TableContainer component={Paper} sx={{ width: '100%' }}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table" sx={{ width: '100%' }}>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell width="8%"> <TableCell width="8%">
@@ -84,8 +84,8 @@ const PlanetaryIteractionTable = ({
</Tooltip> </Tooltip>
</TableCell> </TableCell>
<TableCell width="2%"> <TableCell width="2%">
<Tooltip title="How many units per hour factories are producing"> <Tooltip title="How many units per hour factories are producing on this planet">
<Typography fontSize={theme.custom.smallText}>u/h</Typography> <Typography fontSize={theme.custom.smallText}>uph</Typography>
</Tooltip> </Tooltip>
</TableCell> </TableCell>
<TableCell width="4%" align="right"> <TableCell width="4%" align="right">
@@ -98,10 +98,17 @@ const PlanetaryIteractionTable = ({
<TableCell width="10%"> <TableCell width="10%">
<Tooltip title="Storage facility fill rate"> <Tooltip title="Storage facility fill rate">
<Typography fontSize={theme.custom.smallText}> <Typography fontSize={theme.custom.smallText}>
Storage Fill rate Storage%
</Typography> </Typography>
</Tooltip> </Tooltip>
</TableCell> </TableCell>
<TableCell>
<Tooltip title="Planet settings">
<IconButton aria-label="settings">
<Settings fontSize="small" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
@@ -152,8 +159,8 @@ export const PlanetaryInteractionRow = ({
const theme = useTheme(); const theme = useTheme();
return theme.custom.compactMode ? ( return theme.custom.compactMode ? (
<PlanetaryInteractionIconsRow character={character} /> <div style={{ marginTop: "1.2rem" }}><PlanetaryInteractionIconsRow character={character} /></div>
) : ( ) : (
<PlanetaryIteractionTable character={character} /> <div style={{ marginTop: "1.4rem" }}><PlanetaryIteractionTable character={character} /></div>
); );
}; };

View File

@@ -1,9 +1,10 @@
import React from 'react'; import React from 'react';
import { Box, Paper, Typography, Grid, Stack, Divider, Tooltip } from '@mui/material'; import { Box, Paper, Typography, Grid, Stack } from '@mui/material';
import { EVE_IMAGE_URL } from '@/const'; import { EVE_IMAGE_URL } from '@/const';
import { PI_TYPES_MAP } from '@/const'; import { PI_TYPES_MAP } from '@/const';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import Countdown from 'react-countdown'; import Countdown from 'react-countdown';
import Image from 'next/image';
interface Factory { interface Factory {
schematic_id: number; schematic_id: number;
@@ -291,7 +292,7 @@ export const ProductionChainVisualization: React.FC<ProductionChainVisualization
}} }}
> >
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<img <Image
src={`${EVE_IMAGE_URL}/types/${typeId}/icon`} src={`${EVE_IMAGE_URL}/types/${typeId}/icon`}
alt={type?.name ?? `Type ${typeId}`} alt={type?.name ?? `Type ${typeId}`}
width={48} width={48}

View File

@@ -13,13 +13,15 @@ import {
Typography, Typography,
TextField, TextField,
Box, Box,
FormControlLabel,
Checkbox,
} from "@mui/material"; } from "@mui/material";
import { ColorChangeHandler, ColorResult, CompactPicker } from "react-color"; import { ColorChangeHandler, ColorResult, CompactPicker } from "react-color";
import React, { useState, useContext } from "react"; import React, { useState, useContext } from "react";
export const SettingsButton = () => { export const SettingsButton = () => {
const { colors, setColors } = useContext(ColorContext); const { colors, setColors } = useContext(ColorContext);
const { balanceThreshold, setBalanceThreshold } = useContext(SessionContext); const { balanceThreshold, setBalanceThreshold, showProductIcons, setShowProductIcons } = useContext(SessionContext);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleClickOpen = () => { const handleClickOpen = () => {
@@ -45,6 +47,10 @@ export const SettingsButton = () => {
} }
}; };
const handleShowProductIconsChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setShowProductIcons(event.target.checked);
};
return ( return (
<Tooltip title="Toggle settings dialog"> <Tooltip title="Toggle settings dialog">
<> <>
@@ -57,13 +63,24 @@ export const SettingsButton = () => {
aria-labelledby="alert-dialog-title" aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"
> >
<DialogTitle id="alert-dialog-title"> <DialogTitle id="alert-dialog-title">
{"Settings"} {"Settings"}
</DialogTitle> </DialogTitle>
<DialogContent style={{ paddingTop: "1rem" }}> <DialogContent style={{ paddingTop: "1rem" }}>
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<Typography variant="subtitle1">Display Settings</Typography>
<FormControlLabel
control={
<Checkbox
checked={showProductIcons}
onChange={handleShowProductIconsChange}
/>
}
label="Show product icons instead of names"
/>
</Box>
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle1">Balance Threshold</Typography> <Typography variant="subtitle1">Balance Threshold</Typography>
<TextField <TextField
type="number" type="number"
@@ -87,7 +104,6 @@ export const SettingsButton = () => {
</div> </div>
); );
})} })}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose}>Close</Button> <Button onClick={handleClose}>Close</Button>

View File

@@ -1,5 +1,5 @@
import { SessionContext } from "@/app/context/Context"; import { SessionContext } from "@/app/context/Context";
import { PI_TYPES_MAP } from "@/const"; import { PI_TYPES_MAP, STORAGE_IDS } from "@/const";
import { planetCalculations } from "@/planets"; import { planetCalculations } from "@/planets";
import { AccessToken } from "@/types"; import { AccessToken } from "@/types";
import { import {
@@ -19,10 +19,12 @@ import {
AccordionSummary, AccordionSummary,
AccordionDetails, AccordionDetails,
TableSortLabel, TableSortLabel,
Box,
TextField,
} from "@mui/material"; } from "@mui/material";
import { useContext, useState } from "react"; import { useContext, useState, useEffect } from "react";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { argv0 } from "process"; import { DateTime } from "luxon";
const StackItem = styled(Stack)(({ theme }) => ({ const StackItem = styled(Stack)(({ theme }) => ({
...theme.typography.body2, ...theme.typography.body2,
@@ -41,13 +43,39 @@ const displayValue = (valueInMillions: number): string =>
? `${(valueInMillions / 1000).toFixed(2)} B` ? `${(valueInMillions / 1000).toFixed(2)} B`
: `${valueInMillions.toFixed(2)} M`; : `${valueInMillions.toFixed(2)} M`;
type SortBy = "name" | "perHour" | "price"; type SortBy = "name" | "perHour" | "storage" | "price" | "storagePrice" | "progress";
type SortDirection = "asc" | "desc"; type SortDirection = "asc" | "desc";
export const Summary = ({ characters }: { characters: AccessToken[] }) => { export const Summary = ({ characters }: { characters: AccessToken[] }) => {
const { piPrices } = useContext(SessionContext); const { piPrices } = useContext(SessionContext);
const [sortDirection, setSortDirection] = useState<SortDirection>("asc"); const [sortDirection, setSortDirection] = useState<SortDirection>("asc");
const [sortBy, setSortBy] = useState<SortBy>("name"); const [sortBy, setSortBy] = useState<SortBy>("name");
const [startDate, setStartDate] = useState<string>(DateTime.now().startOf('day').toISO());
const [activityPercentage, setActivityPercentage] = useState<number>(100);
// Load saved values from localStorage on mount
useEffect(() => {
const savedDate = localStorage.getItem('productionStartDate');
if (savedDate) {
setStartDate(savedDate);
}
const savedActivity = localStorage.getItem('activityPercentage');
if (savedActivity) {
setActivityPercentage(parseFloat(savedActivity));
}
}, []);
// Save values to localStorage when they change
useEffect(() => {
localStorage.setItem('productionStartDate', startDate);
}, [startDate]);
useEffect(() => {
localStorage.setItem('activityPercentage', activityPercentage.toString());
}, [activityPercentage]);
// Calculate exports and storage amounts
const exports = characters.flatMap((char) => { const exports = characters.flatMap((char) => {
return char.planets return char.planets
.filter( .filter(
@@ -62,6 +90,28 @@ export const Summary = ({ characters }: { characters: AccessToken[] }) => {
}); });
}); });
// Calculate storage amounts
const storageAmounts = characters.reduce<Grouped>((totals, character) => {
character.planets
.filter(
(p) =>
!character.planetConfig.some(
(c) => c.planetId == p.planet_id && c.excludeFromTotals,
),
)
.forEach((planet) => {
planet.info.pins
.filter(pin => STORAGE_IDS().some(storage => storage.type_id === pin.type_id))
.forEach(storage => {
storage.contents?.forEach(content => {
const current = totals[content.type_id] || 0;
totals[content.type_id] = current + content.amount;
});
});
});
return totals;
}, {});
const groupedByMaterial = exports.reduce<Grouped>((totals, material) => { const groupedByMaterial = exports.reduce<Grouped>((totals, material) => {
const { typeId, amount } = material; const { typeId, amount } = material;
const newTotal = isNaN(totals[typeId]) ? amount : totals[typeId] + amount; const newTotal = isNaN(totals[typeId]) ? amount : totals[typeId] + amount;
@@ -69,27 +119,44 @@ export const Summary = ({ characters }: { characters: AccessToken[] }) => {
return totals; return totals;
}, {}); }, {});
const startDateTime = DateTime.fromISO(startDate);
const hoursSinceStart = DateTime.now().diff(startDateTime, 'hours').hours;
const withProductNameAndPrice = Object.keys(groupedByMaterial).map( const withProductNameAndPrice = Object.keys(groupedByMaterial).map(
(typeIdString) => { (typeIdString) => {
const typeId = parseInt(typeIdString); const typeId = parseInt(typeIdString);
const amount = groupedByMaterial[typeId]; const amount = groupedByMaterial[typeId];
const storageAmount = storageAmounts[typeId] || 0;
const adjustedAmount = amount * (activityPercentage / 100);
const valueInMillions = const valueInMillions =
(((piPrices?.appraisal.items.find((a) => a.typeID === typeId)?.prices (((piPrices?.appraisal.items.find((a) => a.typeID === typeId)?.prices
.sell.min ?? 0) * .sell.min ?? 0) *
amount) / adjustedAmount) /
1000000) * 1000000) *
24 * 24 *
30; 30;
const storageValueInMillions =
((piPrices?.appraisal.items.find((a) => a.typeID === typeId)?.prices
.sell.min ?? 0) *
storageAmount) /
1000000;
// Calculate expected production and progress
const expectedProduction = adjustedAmount * hoursSinceStart;
const progress = hoursSinceStart > 0 ? (storageAmount / expectedProduction) * 100 : 0;
return { return {
typeId, typeId,
amount, amount: adjustedAmount,
storageAmount,
materialName: PI_TYPES_MAP[typeId].name, materialName: PI_TYPES_MAP[typeId].name,
price: valueInMillions, price: valueInMillions,
storageValue: storageValueInMillions,
progress,
}; };
}, },
); );
const theme = useTheme();
return ( return (
<StackItem width="100%"> <StackItem width="100%">
@@ -98,14 +165,48 @@ export const Summary = ({ characters }: { characters: AccessToken[] }) => {
<h2>Totals</h2> <h2>Totals</h2>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<TableContainer component={Paper}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Table size="small" aria-label="a dense table"> <TextField
label="Production Start Date"
type="date"
value={DateTime.fromISO(startDate).toFormat('yyyy-MM-dd')}
onChange={(e) => {
const newDate = DateTime.fromFormat(e.target.value, 'yyyy-MM-dd').startOf('day').toISO();
if (newDate) setStartDate(newDate);
}}
InputLabelProps={{
shrink: true,
}}
size="small"
/>
<TextField
label="Activity %"
type="number"
value={activityPercentage}
onChange={(e) => {
const value = Math.min(100, Math.max(0, parseFloat(e.target.value) || 0));
setActivityPercentage(value);
}}
InputLabelProps={{
shrink: true,
}}
inputProps={{
min: 0,
max: 100,
step: 1,
}}
size="small"
sx={{ width: '100px' }}
/>
</Box>
<TableContainer component={Paper} sx={{ width: '100%' }}>
<Table size="small" aria-label="a dense table" sx={{ width: '100%' }}>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell width="40%"> <TableCell width="20%">
<Tooltip title="What exports factories are producing"> <Tooltip title="What exports factories are producing">
<TableSortLabel <TableSortLabel
active={true} active={sortBy === "name"}
direction={sortDirection} direction={sortDirection}
onClick={() => { onClick={() => {
setSortDirection( setSortDirection(
@@ -118,10 +219,10 @@ export const Summary = ({ characters }: { characters: AccessToken[] }) => {
</TableSortLabel> </TableSortLabel>
</Tooltip> </Tooltip>
</TableCell> </TableCell>
<TableCell width="10%"> <TableCell width="10%" align="right">
<Tooltip title="How many units per hour factories are producing"> <Tooltip title={`Adjusted production rate (${activityPercentage}% activity)`}>
<TableSortLabel <TableSortLabel
active={true} active={sortBy === "perHour"}
direction={sortDirection} direction={sortDirection}
onClick={() => { onClick={() => {
setSortDirection( setSortDirection(
@@ -135,9 +236,41 @@ export const Summary = ({ characters }: { characters: AccessToken[] }) => {
</Tooltip> </Tooltip>
</TableCell> </TableCell>
<TableCell width="10%" align="right"> <TableCell width="10%" align="right">
<Tooltip title="How many million ISK per month this planet is exporting (Jita sell min)"> <Tooltip title="Amount currently in storage">
<TableSortLabel <TableSortLabel
active={true} active={sortBy === "storage"}
direction={sortDirection}
onClick={() => {
setSortDirection(
sortDirection === "asc" ? "desc" : "asc",
);
setSortBy("storage");
}}
>
Units in storage
</TableSortLabel>
</Tooltip>
</TableCell>
<TableCell width="15%" align="right">
<Tooltip title="Current ISK value of storage">
<TableSortLabel
active={sortBy === "storagePrice"}
direction={sortDirection}
onClick={() => {
setSortDirection(
sortDirection === "asc" ? "desc" : "asc",
);
setSortBy("storagePrice");
}}
>
Produced ISK value
</TableSortLabel>
</Tooltip>
</TableCell>
<TableCell width="15%" align="right">
<Tooltip title="Monthly ISK value from production">
<TableSortLabel
active={sortBy === "price"}
direction={sortDirection} direction={sortDirection}
onClick={() => { onClick={() => {
setSortDirection( setSortDirection(
@@ -146,7 +279,23 @@ export const Summary = ({ characters }: { characters: AccessToken[] }) => {
setSortBy("price"); setSortBy("price");
}} }}
> >
ISK/M Maximum possible ISK/M
</TableSortLabel>
</Tooltip>
</TableCell>
<TableCell width="15%" align="right">
<Tooltip title="Progress towards monthly production target">
<TableSortLabel
active={sortBy === "progress"}
direction={sortDirection}
onClick={() => {
setSortDirection(
sortDirection === "asc" ? "desc" : "asc",
);
setSortBy("progress");
}}
>
Progress from start date
</TableSortLabel> </TableSortLabel>
</Tooltip> </Tooltip>
</TableCell> </TableCell>
@@ -167,12 +316,30 @@ export const Summary = ({ characters }: { characters: AccessToken[] }) => {
if (sortDirection === "desc") if (sortDirection === "desc")
return a.amount > b.amount ? -1 : 1; return a.amount > b.amount ? -1 : 1;
} }
if (sortBy === "storage") {
if (sortDirection === "asc")
return a.storageAmount > b.storageAmount ? 1 : -1;
if (sortDirection === "desc")
return a.storageAmount > b.storageAmount ? -1 : 1;
}
if (sortBy === "price") { if (sortBy === "price") {
if (sortDirection === "asc") if (sortDirection === "asc")
return a.price > b.price ? 1 : -1; return a.price > b.price ? 1 : -1;
if (sortDirection === "desc") if (sortDirection === "desc")
return a.price > b.price ? -1 : 1; return a.price > b.price ? -1 : 1;
} }
if (sortBy === "storagePrice") {
if (sortDirection === "asc")
return a.storageValue > b.storageValue ? 1 : -1;
if (sortDirection === "desc")
return a.storageValue > b.storageValue ? -1 : 1;
}
if (sortBy === "progress") {
if (sortDirection === "asc")
return a.progress > b.progress ? 1 : -1;
if (sortDirection === "desc")
return a.progress > b.progress ? -1 : 1;
}
return 0; return 0;
}) })
.map((product) => ( .map((product) => (
@@ -180,7 +347,10 @@ export const Summary = ({ characters }: { characters: AccessToken[] }) => {
key={product.materialName} key={product.materialName}
material={product.materialName} material={product.materialName}
amount={product.amount} amount={product.amount}
storageAmount={product.storageAmount}
price={product.price} price={product.price}
storageValue={product.storageValue}
progress={product.progress}
/> />
))} ))}
<SummaryRow <SummaryRow
@@ -189,6 +359,10 @@ export const Summary = ({ characters }: { characters: AccessToken[] }) => {
(amount, p) => amount + p.price, (amount, p) => amount + p.price,
0, 0,
)} )}
storageValue={withProductNameAndPrice.reduce(
(amount, p) => amount + p.storageValue,
0,
)}
/> />
</TableBody> </TableBody>
</Table> </Table>
@@ -202,17 +376,32 @@ export const Summary = ({ characters }: { characters: AccessToken[] }) => {
const SummaryRow = ({ const SummaryRow = ({
material, material,
amount, amount,
storageAmount,
price, price,
storageValue,
progress,
}: { }: {
material: string; material: string;
amount?: number; amount?: number;
storageAmount?: number;
price: number; price: number;
storageValue?: number;
progress?: number;
}) => ( }) => (
<TableRow> <TableRow>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{material} {material}
</TableCell> </TableCell>
<TableCell>{amount}</TableCell> <TableCell align="right">{amount?.toFixed(1)}</TableCell>
<TableCell align="right">{storageAmount?.toFixed(1)}</TableCell>
<TableCell align="right">{storageValue !== undefined && displayValue(storageValue)}</TableCell>
<TableCell align="right">{displayValue(price)}</TableCell> <TableCell align="right">{displayValue(price)}</TableCell>
<TableCell align="right">
{progress !== undefined && (
<Typography color={progress >= 100 ? 'success.main' : progress >= 75 ? 'warning.main' : 'error.main'}>
{progress.toFixed(1)}%
</Typography>
)}
</TableCell>
</TableRow> </TableRow>
); );

View File

@@ -1,23 +0,0 @@
import { Box, Button, Tooltip } from "@mui/material";
export const SupportButton = () => {
return (
<Box>
<Tooltip
title={`
Consider using code 'CALLIEVE' on EVE store checkout to support the project! Click to copy to clipboard ;)
`}
>
<Button
href=""
style={{ width: "100%" }}
sx={{ color: "white", display: "block" }}
onClick={() => {
navigator.clipboard.writeText("CALLIEVE");
}}
>
CALLIEVE
</Button>
</Tooltip>
</Box>
);
};

View File

@@ -37,6 +37,8 @@ export const SessionContext = createContext<{
}) => PlanetConfig; }) => PlanetConfig;
balanceThreshold: number; balanceThreshold: number;
setBalanceThreshold: Dispatch<SetStateAction<number>>; setBalanceThreshold: Dispatch<SetStateAction<number>>;
showProductIcons: boolean;
setShowProductIcons: (show: boolean) => void;
}>({ }>({
sessionReady: false, sessionReady: false,
refreshSession: () => {}, refreshSession: () => {},
@@ -62,7 +64,10 @@ export const SessionContext = createContext<{
}, },
balanceThreshold: 1000, balanceThreshold: 1000,
setBalanceThreshold: () => {}, setBalanceThreshold: () => {},
showProductIcons: false,
setShowProductIcons: () => {},
}); });
export type ColorSelectionType = { export type ColorSelectionType = {
defaultColor: string; defaultColor: string;
expiredColor: string; expiredColor: string;
@@ -84,6 +89,7 @@ export const defaultColors = {
dayColor: "#2F695A", dayColor: "#2F695A",
twoDaysColor: "#2F695A", twoDaysColor: "#2F695A",
}; };
export const ColorContext = createContext<{ export const ColorContext = createContext<{
colors: ColorSelectionType; colors: ColorSelectionType;
setColors: (colors: ColorSelectionType) => void; setColors: (colors: ColorSelectionType) => void;

View File

@@ -30,6 +30,7 @@ const Home = () => {
undefined, undefined,
); );
const [balanceThreshold, setBalanceThreshold] = useState(1000); const [balanceThreshold, setBalanceThreshold] = useState(1000);
const [showProductIcons, setShowProductIcons] = useState(false);
const [colors, setColors] = useState<ColorSelectionType>(defaultColors); const [colors, setColors] = useState<ColorSelectionType>(defaultColors);
const [alertMode, setAlertMode] = useState(false); const [alertMode, setAlertMode] = useState(false);
@@ -248,7 +249,7 @@ const Home = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
const ESI_CACHE_TIME_MS = 600000; const ESI_CACHE_TIME_MS = 3000000;
const interval = setInterval(() => { const interval = setInterval(() => {
const characters = initializeCharacters(); const characters = initializeCharacters();
refreshSession(characters) refreshSession(characters)
@@ -278,6 +279,8 @@ const Home = () => {
readPlanetConfig, readPlanetConfig,
balanceThreshold, balanceThreshold,
setBalanceThreshold, setBalanceThreshold,
showProductIcons,
setShowProductIcons,
}} }}
> >
<CharacterContext.Provider <CharacterContext.Provider

View File

@@ -94,7 +94,13 @@ export const planetCalculations = (planet: PlanetWithInfo) => {
![...locallyProduced, ...locallyExcavated].some( ![...locallyProduced, ...locallyExcavated].some(
(lp) => lp === p.type_id, (lp) => lp === p.type_id,
), ),
); ).map((p) => ({
...p,
factoryCount: planetInfo.pins
.filter((f) => f.schematic_id === p.schematic_id)
.length,
}));
const localExports = locallyProduced const localExports = locallyProduced
.filter((p) => !locallyConsumed.some((lp) => lp === p)) .filter((p) => !locallyConsumed.some((lp) => lp === p))