5 Commits
push ... v1.0.2

10 changed files with 590 additions and 101 deletions

54
.github/workflows/container.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: Build and Push Container
on:
push:
tags:
- 'v*' # This will match tags like v1.0.0, v2.1.0, etc.
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max

View File

@@ -2,7 +2,7 @@
version: "2.1" version: "2.1"
services: services:
eve-pi: eve-pi:
build: . image: ghcr.io/calli-eve/eve-pi:latest
container_name: eve-pi container_name: eve-pi
environment: environment:
- EVE_SSO_CLIENT_ID=${EVE_SSO_CLIENT_ID} - EVE_SSO_CLIENT_ID=${EVE_SSO_CLIENT_ID}

191
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@fontsource/roboto": "^5.0.3", "@fontsource/roboto": "^5.0.3",
"@hello-pangea/dnd": "^18.0.1",
"@mui/icons-material": "^5.11.16", "@mui/icons-material": "^5.11.16",
"@mui/material": "^5.13.5", "@mui/material": "^5.13.5",
"@sentry/nextjs": "^8.2.1", "@sentry/nextjs": "^8.2.1",
@@ -279,11 +280,12 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.22.5", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.13.11" "regenerator-runtime": "^0.14.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -534,6 +536,57 @@
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.3.tgz", "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.3.tgz",
"integrity": "sha512-jbZDFwEFARDlo8TqG7th/xjhuq87GYfFpFb+uxuy+0Ng1bhRVgBRWlLj8+WIKhCTOr+h4QXbjpybLWFLUirOwQ==" "integrity": "sha512-jbZDFwEFARDlo8TqG7th/xjhuq87GYfFpFb+uxuy+0Ng1bhRVgBRWlLj8+WIKhCTOr+h4QXbjpybLWFLUirOwQ=="
}, },
"node_modules/@hello-pangea/dnd": {
"version": "18.0.1",
"resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz",
"integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.26.7",
"css-box-model": "^1.2.1",
"raf-schd": "^4.0.3",
"react-redux": "^9.2.0",
"redux": "^5.0.1"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/@hello-pangea/dnd/node_modules/@types/react": {
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@hello-pangea/dnd/node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
@@ -2604,6 +2657,12 @@
"lil-gui": "~0.17.0" "lil-gui": "~0.17.0"
} }
}, },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@types/webxr": { "node_modules/@types/webxr": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz",
@@ -3711,6 +3770,15 @@
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/css-box-model": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
"license": "MIT",
"dependencies": {
"tiny-invariant": "^1.0.6"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
@@ -7323,6 +7391,12 @@
} }
] ]
}, },
"node_modules/raf-schd": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
"license": "MIT"
},
"node_modules/randombytes": { "node_modules/randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -7472,6 +7546,12 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -7496,9 +7576,10 @@
} }
}, },
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.13.11", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
}, },
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.4", "version": "1.5.4",
@@ -8668,6 +8749,12 @@
"resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz", "resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz",
"integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug==" "integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug=="
}, },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinycolor2": { "node_modules/tinycolor2": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
@@ -8935,6 +9022,15 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -9483,11 +9579,11 @@
} }
}, },
"@babel/runtime": { "@babel/runtime": {
"version": "7.22.5", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"requires": { "requires": {
"regenerator-runtime": "^0.13.11" "regenerator-runtime": "^0.14.0"
} }
}, },
"@babel/template": { "@babel/template": {
@@ -9683,6 +9779,39 @@
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.3.tgz", "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.3.tgz",
"integrity": "sha512-jbZDFwEFARDlo8TqG7th/xjhuq87GYfFpFb+uxuy+0Ng1bhRVgBRWlLj8+WIKhCTOr+h4QXbjpybLWFLUirOwQ==" "integrity": "sha512-jbZDFwEFARDlo8TqG7th/xjhuq87GYfFpFb+uxuy+0Ng1bhRVgBRWlLj8+WIKhCTOr+h4QXbjpybLWFLUirOwQ=="
}, },
"@hello-pangea/dnd": {
"version": "18.0.1",
"resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz",
"integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==",
"requires": {
"@babel/runtime": "^7.26.7",
"css-box-model": "^1.2.1",
"raf-schd": "^4.0.3",
"react-redux": "^9.2.0",
"redux": "^5.0.1"
},
"dependencies": {
"@types/react": {
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
"optional": true,
"peer": true,
"requires": {
"csstype": "^3.0.2"
}
},
"react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"requires": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
}
}
}
},
"@humanwhocodes/config-array": { "@humanwhocodes/config-array": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
@@ -10970,6 +11099,11 @@
"lil-gui": "~0.17.0" "lil-gui": "~0.17.0"
} }
}, },
"@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
},
"@types/webxr": { "@types/webxr": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz",
@@ -11740,6 +11874,14 @@
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
}, },
"css-box-model": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
"requires": {
"tiny-invariant": "^1.0.6"
}
},
"csstype": { "csstype": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
@@ -14130,6 +14272,11 @@
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
}, },
"raf-schd": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
},
"randombytes": { "randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -14244,6 +14391,11 @@
"picomatch": "^2.2.1" "picomatch": "^2.2.1"
} }
}, },
"redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
},
"reflect.getprototypeof": { "reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -14261,9 +14413,9 @@
} }
}, },
"regenerator-runtime": { "regenerator-runtime": {
"version": "0.13.11", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
}, },
"regexp.prototype.flags": { "regexp.prototype.flags": {
"version": "1.5.4", "version": "1.5.4",
@@ -15028,6 +15180,11 @@
"resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz", "resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz",
"integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug==" "integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug=="
}, },
"tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
},
"tinycolor2": { "tinycolor2": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
@@ -15201,6 +15358,12 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"requires": {}
},
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@@ -13,6 +13,7 @@
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@fontsource/roboto": "^5.0.3", "@fontsource/roboto": "^5.0.3",
"@hello-pangea/dnd": "^18.0.1",
"@mui/icons-material": "^5.11.16", "@mui/icons-material": "^5.11.16",
"@mui/material": "^5.13.5", "@mui/material": "^5.13.5",
"@sentry/nextjs": "^8.2.1", "@sentry/nextjs": "^8.2.1",

View File

@@ -1,40 +1,163 @@
import { AccessToken } from "@/types"; import { AccessToken } from "@/types";
import { Box, Stack, Typography, useTheme } from "@mui/material"; import { Box, Stack, Typography, useTheme, Paper, IconButton } 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 } from "react"; import { useContext, useState } from "react";
import { PlanRow } from "./PlanRow"; import { PlanRow } from "./PlanRow";
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import { planetCalculations } from "@/planets";
import { EvePraisalResult } from "@/eve-praisal";
import { STORAGE_IDS } from "@/const";
interface AccountTotals {
monthlyEstimate: number;
storageValue: number;
}
const calculateAccountTotals = (characters: AccessToken[], piPrices: EvePraisalResult | undefined): AccountTotals => {
let totalMonthlyEstimate = 0;
let totalStorageValue = 0;
characters.forEach((character) => {
character.planets.forEach((planet) => {
const { localExports } = planetCalculations(planet);
const planetConfig = character.planetConfig.find(p => p.planetId === planet.planet_id);
// Calculate monthly estimate
if (!planetConfig?.excludeFromTotals) {
localExports.forEach((exportItem) => {
const valueInMillions = (((piPrices?.appraisal.items.find(
(a) => a.typeID === exportItem.typeId,
)?.prices.sell.min ?? 0) *
exportItem.amount) /
1000000) *
24 *
30;
totalMonthlyEstimate += valueInMillions;
});
}
if (!planetConfig?.excludeFromTotals) {
planet.info.pins
.filter(pin => STORAGE_IDS().some(storage => storage.type_id === pin.type_id))
.forEach(storage => {
storage.contents?.forEach(content => {
const valueInMillions = (piPrices?.appraisal.items.find(
(a) => a.typeID === content.type_id,
)?.prices.sell.min ?? 0) * content.amount / 1000000;
totalStorageValue += valueInMillions;
});
});
}
});
});
return {
monthlyEstimate: totalMonthlyEstimate,
storageValue: totalStorageValue
};
};
export const AccountCard = ({ characters }: { characters: AccessToken[] }) => { export const AccountCard = ({ characters }: { characters: AccessToken[] }) => {
const theme = useTheme(); const theme = useTheme();
const [isCollapsed, setIsCollapsed] = useState(false);
const { planMode, piPrices } = useContext(SessionContext);
const { monthlyEstimate, storageValue } = calculateAccountTotals(characters, piPrices);
const { planMode } = useContext(SessionContext);
return ( return (
<Box <Paper
elevation={2}
sx={{ sx={{
padding: 1, padding: theme.custom.compactMode ? theme.spacing(1) : theme.spacing(2),
borderBottom: theme.custom.compactMode ? "" : "solid 1px gray", margin: theme.spacing(1),
display: 'flex',
alignItems: 'flex-start',
gap: 1,
backgroundColor: theme.palette.background.paper,
transition: 'all 0.2s ease-in-out',
cursor: 'grab',
'&:hover': {
boxShadow: theme.shadows[4],
},
'&:active': {
boxShadow: theme.shadows[8],
cursor: 'grabbing',
},
}} }}
> >
<Typography style={{ fontSize: "0.8rem" }} paddingLeft={2}> <Box sx={{ flex: 1 }}>
{characters[0].account !== "-" <Box
? `Account: ${characters[0].account}` sx={{
: "No account name"} backgroundColor: theme.palette.background.default,
</Typography> borderRadius: 1,
{characters.map((c) => ( padding: theme.spacing(1),
<Stack marginBottom: theme.spacing(2),
key={c.character.characterId} display: 'flex',
direction="row" alignItems: 'center',
alignItems="flex-start" justifyContent: 'space-between',
}}
> >
<CharacterRow character={c} /> <Box>
{planMode ? ( <Typography
<PlanRow character={c} /> sx={{
) : ( fontSize: "0.9rem",
<PlanetaryInteractionRow character={c} /> fontWeight: 500,
)} color: theme.palette.text.primary,
</Stack> }}
))} >
</Box> {characters.length > 0 && characters[0].account !== "-"
? `Account: ${characters[0].account}`
: "No account name"}
</Typography>
<Typography
sx={{
fontSize: "0.8rem",
color: theme.palette.text.secondary,
}}
>
Monthly Estimate: {monthlyEstimate >= 1000
? `${(monthlyEstimate / 1000).toFixed(2)} B`
: `${monthlyEstimate.toFixed(2)} M`} ISK
</Typography>
<Typography
sx={{
fontSize: "0.8rem",
color: theme.palette.text.secondary,
}}
>
Storage Value: {storageValue >= 1000
? `${(storageValue / 1000).toFixed(2)} B`
: `${storageValue.toFixed(2)} M`} ISK
</Typography>
</Box>
<IconButton
size="small"
onClick={() => setIsCollapsed(!isCollapsed)}
sx={{
transform: isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease-in-out'
}}
>
{isCollapsed ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</Box>
{!isCollapsed && characters.map((c) => (
<Stack
key={c.character.characterId}
direction="row"
alignItems="flex-start"
>
<CharacterRow character={c} />
{planMode ? (
<PlanRow character={c} />
) : (
<PlanetaryInteractionRow character={c} />
)}
</Stack>
))}
</Box>
</Paper>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { Button, Dialog, DialogActions, DialogTitle } from "@mui/material"; import { Button, Dialog, DialogActions, DialogTitle, Box } from "@mui/material";
import { AccessToken, CharacterUpdate } from "../../../types"; import { AccessToken, CharacterUpdate } from "../../../types";
import { useEffect, useState, KeyboardEvent } from "react"; import { useEffect, useState, KeyboardEvent } from "react";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
@@ -34,7 +34,9 @@ export const CharacterDialog = ({
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => { const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === "Enter") { if (event.key === "Enter") {
closeDialog(); closeDialog();
character && updateCharacter(character, { account, comment }); if (character) {
updateCharacter(character, { account, comment });
}
} }
}; };
@@ -44,16 +46,27 @@ export const CharacterDialog = ({
onClose={closeDialog} onClose={closeDialog}
fullWidth={true} fullWidth={true}
> >
<DialogTitle>{character && character.character.name}</DialogTitle> <DialogTitle>{character?.character?.name}</DialogTitle>
<TextField <Box sx={{ display: 'flex', alignItems: 'center', margin: 1 }}>
id="outlined-basic" <TextField
label="Account name" id="outlined-basic"
variant="outlined" label="Account name"
value={account ?? ""} variant="outlined"
sx={{ margin: 1 }} value={account ?? ""}
onChange={(event) => setAccount(event.target.value)} sx={{ flex: 1 }}
onKeyDown={handleKeyDown} onChange={(event) => setAccount(event.target.value)}
/> onKeyDown={handleKeyDown}
/>
<Button
onClick={() => {
setAccount("-");
}}
variant="outlined"
sx={{ ml: 1 }}
>
Clear account
</Button>
</Box>
<TextField <TextField
id="outlined-basic" id="outlined-basic"
label="System" label="System"
@@ -77,6 +90,7 @@ export const CharacterDialog = ({
<DialogActions> <DialogActions>
<Button <Button
onClick={() => { onClick={() => {
console.log("Saving character", character, { account, comment, system });
character && character &&
updateCharacter(character, { account, comment, system }); updateCharacter(character, { account, comment, system });
closeDialog(); closeDialog();

View File

@@ -11,6 +11,7 @@ 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";
interface Grouped { interface Grouped {
[key: string]: AccessToken[]; [key: string]: AccessToken[];
@@ -38,7 +39,41 @@ declare module "@mui/material/styles" {
} }
export const MainGrid = () => { export const MainGrid = () => {
const { characters } = useContext(CharacterContext); const { characters, updateCharacter } = useContext(CharacterContext);
const [accountOrder, setAccountOrder] = useState<string[]>([]);
// Initialize account order when characters change
useEffect(() => {
const currentAccounts = Object.keys(
characters.reduce<Grouped>((group, character) => {
const { account } = character;
group[account ?? ""] = group[account ?? ""] ?? [];
group[account ?? ""].push(character);
return group;
}, {})
);
const savedOrder = localStorage.getItem('accountOrder');
if (savedOrder) {
try {
const parsedOrder = JSON.parse(savedOrder);
const validOrder = parsedOrder.filter((account: string) => currentAccounts.includes(account));
const newAccounts = currentAccounts.filter(account => !validOrder.includes(account));
setAccountOrder([...validOrder, ...newAccounts]);
} catch (e) {
setAccountOrder(currentAccounts);
}
} else {
setAccountOrder(currentAccounts);
}
}, [characters]);
useEffect(() => {
if (accountOrder.length > 0) {
localStorage.setItem('accountOrder', JSON.stringify(accountOrder));
}
}, [accountOrder]);
const groupByAccount = characters.reduce<Grouped>((group, character) => { const groupByAccount = characters.reduce<Grouped>((group, character) => {
const { account } = character; const { account } = character;
group[account ?? ""] = group[account ?? ""] ?? []; group[account ?? ""] = group[account ?? ""] ?? [];
@@ -79,24 +114,68 @@ export const MainGrid = () => {
); );
}, [compactMode]); }, [compactMode]);
const handleDragEnd = (result: DropResult) => {
if (!result.destination) return;
const items = Array.from(accountOrder);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
setAccountOrder(items);
};
const DragDropContextComponent = DragDropContext as any;
const DroppableComponent = Droppable as any;
const DraggableComponent = Draggable as any;
return ( return (
<ThemeProvider theme={darkTheme}> <ThemeProvider theme={darkTheme}>
<CssBaseline /> <CssBaseline />
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<ResponsiveAppBar /> <ResponsiveAppBar />
{compactMode ? <></> : <Summary characters={characters} />} {compactMode ? <></> : <Summary characters={characters} />}
<Grid container spacing={1}> <DragDropContextComponent onDragEnd={handleDragEnd}>
{Object.values(groupByAccount).map((g, id) => ( <DroppableComponent droppableId="accounts">
<Grid {(provided: any) => (
item <Grid
xs={12} container
sm={compactMode ? 6 : 12} spacing={1}
key={`account-${id}-${g[0].account}`} sx={{ padding: 1 }}
> {...provided.droppableProps}
<AccountCard characters={g} /> ref={provided.innerRef}
</Grid> >
))} {accountOrder.map((account, index) => (
</Grid> <DraggableComponent
key={account}
draggableId={account}
index={index}
>
{(provided: any) => (
<Grid
item
xs={12}
sm={compactMode ? 6 : 12}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
sx={{
'& > *': {
width: '100%',
},
}}
>
{groupByAccount[account] && groupByAccount[account].length > 0 && (
<AccountCard characters={groupByAccount[account]} />
)}
</Grid>
)}
</DraggableComponent>
))}
{provided.placeholder}
</Grid>
)}
</DroppableComponent>
</DragDropContextComponent>
</Box> </Box>
</ThemeProvider> </ThemeProvider>
); );

View File

@@ -97,15 +97,6 @@ export const PlanetTableRow = ({
cycleTime: schematic.cycle_time cycleTime: schematic.cycle_time
})); }));
// Convert Map to Array for schematic IDs
const installedSchematicIds = Array.from(localProduction.values()).map(p => p.schematic_id);
// Get extractor head types safely
const extractedTypeIds = extractors
.map(e => e.extractor_details?.product_type_id)
.filter((id): id is number => id !== undefined);
// Get storage facilities
const storageFacilities = planetInfo.pins.filter(pin => const storageFacilities = planetInfo.pins.filter(pin =>
STORAGE_IDS().some(storage => storage.type_id === pin.type_id) STORAGE_IDS().some(storage => storage.type_id === pin.type_id)
); );
@@ -116,20 +107,26 @@ export const PlanetTableRow = ({
const storageType = PI_TYPES_MAP[pin.type_id].name; const storageType = PI_TYPES_MAP[pin.type_id].name;
const storageCapacity = STORAGE_CAPACITIES[pin.type_id] || 0; const storageCapacity = STORAGE_CAPACITIES[pin.type_id] || 0;
// Calculate total volume of stored products for this specific pin
const totalVolume = (pin.contents || []) const totalVolume = (pin.contents || [])
.reduce((sum: number, item: any) => { .reduce((sum: number, item: any) => {
const volume = PI_PRODUCT_VOLUMES[item.type_id] || 0; const volume = PI_PRODUCT_VOLUMES[item.type_id] || 0;
return sum + (item.amount * volume); return sum + (item.amount * volume);
}, 0); }, 0);
const totalValue = (pin.contents || [])
.reduce((sum: number, item: any) => {
const price = piPrices?.appraisal.items.find((a) => a.typeID === item.type_id)?.prices.sell.min ?? 0;
return sum + (item.amount * price);
}, 0);
const fillRate = storageCapacity > 0 ? (totalVolume / storageCapacity) * 100 : 0; const fillRate = storageCapacity > 0 ? (totalVolume / storageCapacity) * 100 : 0;
return { return {
type: storageType, type: storageType,
capacity: storageCapacity, capacity: storageCapacity,
used: totalVolume, used: totalVolume,
fillRate: fillRate fillRate: fillRate,
value: totalValue
}; };
}; };
@@ -319,6 +316,11 @@ export const PlanetTableRow = ({
<Typography fontSize={theme.custom.smallText} style={{ color }}> <Typography fontSize={theme.custom.smallText} style={{ color }}>
{fillRate.toFixed(1)}% {fillRate.toFixed(1)}%
</Typography> </Typography>
{storageInfo.value > 0 && (
<Typography fontSize={theme.custom.smallText} style={{ marginLeft: "5px" }}>
({Math.round(storageInfo.value / 1000000)}M)
</Typography>
)}
</div> </div>
); );
})} })}

View File

@@ -51,7 +51,7 @@ export const getPlanetUniverse = async (
export const planetCalculations = (planet: PlanetWithInfo) => { export const planetCalculations = (planet: PlanetWithInfo) => {
const planetInfo = planet.info; const planetInfo = planet.info;
type SchematicId = number; type SchematicId = number;
const extractors: PlanetInfo["pins"] = planetInfo.pins.filter((p) => const extractors = planetInfo.pins.filter((p) =>
EXTRACTOR_TYPE_IDS.some((e) => e === p.type_id), EXTRACTOR_TYPE_IDS.some((e) => e === p.type_id),
); );
const localProduction = planetInfo.pins const localProduction = planetInfo.pins

View File

@@ -1,5 +1,4 @@
import { PlanetConfig } from "./app/components/PlanetConfig/PlanetConfigDialog"; import { PlanetConfig } from "./app/components/PlanetConfig/PlanetConfigDialog";
import { Api } from "./esi-api";
export interface AccessToken { export interface AccessToken {
access_token: string; access_token: string;
@@ -20,10 +19,50 @@ export interface Character {
characterId: number; characterId: number;
} }
export interface Planet {
planet_id: number;
solar_system_id: number;
planet_type: "temperate" | "barren" | "oceanic" | "ice" | "gas" | "lava" | "storm" | "plasma";
last_update: string;
num_pins: number;
owner_id: number;
upgrade_level: number;
}
export interface PlanetInfo {
links: Array<{
destination_pin_id: number;
link_level: number;
source_pin_id: number;
}>;
pins: Pin[];
routes: Array<{
content_type_id: number;
destination_pin_id: number;
quantity: number;
route_id: number;
source_pin_id: number;
waypoints?: number[];
}>;
}
export interface PlanetInfoUniverse {
name: string;
planet_id: number;
system_id: number;
type_id: number;
position: {
x: number;
y: number;
z: number;
};
}
export interface PlanetWithInfo extends Planet { export interface PlanetWithInfo extends Planet {
info: PlanetInfo; info: PlanetInfo;
infoUniverse: PlanetInfoUniverse; infoUniverse: PlanetInfoUniverse;
} }
export interface CharacterPlanets { export interface CharacterPlanets {
name: string; name: string;
characterId: number; characterId: number;
@@ -38,31 +77,45 @@ export interface CharacterUpdate {
system?: string; system?: string;
} }
export type Planet = EsiType<"v1", "getCharactersCharacterIdPlanets">[number];
export type PlanetInfoUniverse = EsiType<"v1", "getUniversePlanetsPlanetId">;
export type PlanetInfo = EsiType<
"v3",
"getCharactersCharacterIdPlanetsPlanetId"
>;
export interface Env { export interface Env {
EVE_SSO_CALLBACK_URL: string; EVE_SSO_CALLBACK_URL: string;
EVE_SSO_CLIENT_ID: string; EVE_SSO_CLIENT_ID: string;
} }
type EsiApiVersionType = keyof InstanceType<typeof Api<unknown>>; export interface EvePraisalResult {
type EsiApiPathType<V extends EsiApiVersionType> = keyof InstanceType< appraisal: {
typeof Api<unknown> items: Array<{
>[V]; typeID: number;
type EsiApiResponseType< prices: {
V extends EsiApiVersionType, sell: {
T extends EsiApiPathType<V>, min: number;
> = Awaited<ReturnType<InstanceType<typeof Api<unknown>>[V][T]>>; };
export type EsiType< };
V extends EsiApiVersionType, }>;
T extends EsiApiPathType<V>, };
> = EsiApiResponseType<V, T> extends { data: any } }
? EsiApiResponseType<V, T>["data"]
: never; export interface Pin {
pin_id: number;
type_id: number;
schematic_id?: number;
expiry_time?: string;
install_time?: string;
latitude: number;
longitude: number;
extractor_details?: {
cycle_time?: number;
head_radius?: number;
heads: Array<{
head_id: number;
latitude: number;
longitude: number;
}>;
product_type_id?: number;
qty_per_cycle?: number;
};
contents?: Array<{
type_id: number;
amount: number;
}>;
}