Compare commits

...

69 Commits

Author SHA1 Message Date
CI
ae7f4edf4a chore: release version v1.52.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-19 20:24:25 +00:00
Dmitry Popov
52eab28f27 feat(Map): Added map characters view 2025-02-19 21:12:51 +01:00
Dmitry Popov
6098d32bce chore: release version v1.51.3 2025-02-19 17:48:13 +01:00
CI
1839834771 chore: release version v1.51.3
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-19 09:11:12 +00:00
guarzo
7cdfb87853 fix issue with deselection and linked sig splash filters (#187) 2025-02-19 12:23:22 +04:00
guarzo
3d54783a3e fix: pending deletion working again (#185)
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-19 02:24:15 +04:00
CI
f965461820 chore: release version v1.51.2 2025-02-18 17:36:51 +00:00
Dmitry Popov
6d67f87d4b Sigs character name (#182)
* feat(Signatures): Added character names
2025-02-18 21:12:46 +04:00
CI
60697a50c2 chore: release version v1.51.1
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-18 07:56:02 +00:00
guarzo
778d23da06 updates to acl api (#179) 2025-02-18 11:24:14 +04:00
CI
0ee9a15d5d chore: release version v1.51.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-17 22:44:07 +00:00
guarzo
24bb902bb9 feat: add undo deletion for signatures (#155)
* feat: add undo for signature deletion and addition
2025-02-18 02:29:40 +04:00
CI
32fe6395a1 chore: release version v1.50.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-17 00:09:15 +00:00
guarzo
5f506bf4b2 feat: allow addition of characters to acl without preregistration (#176) 2025-02-17 03:52:47 +04:00
CI
0127ebfe46 chore: release version v1.49.0
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-02-15 08:37:43 +00:00
guarzo
8c5366fd9b feat: add api for acl management (#171) 2025-02-15 12:16:42 +04:00
CI
dbcad892a9 chore: release version v1.48.1
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-02-13 17:40:15 +00:00
Dmitry Popov
6da3096db1 chore: release version v1.48.0 2025-02-13 18:04:28 +01:00
CI
cd8efcd6e3 chore: release version v1.48.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-12 22:46:57 +00:00
Dmitry Popov
b52471ae5e chore: release version v1.47.6 2025-02-12 23:37:54 +01:00
Dmitry Popov
438fecb61f chore: release version v1.47.6 2025-02-12 23:07:45 +01:00
guarzo
70b589a359 System Kills cleanup (#166)
* fix: styling and count of kills tooltip
2025-02-13 01:23:36 +04:00
guarzo
cf7069b3b2 feat: autosize local character tooltip and increase hover target (#165) 2025-02-13 01:11:40 +04:00
CI
b2198e469e chore: release version v1.47.6
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-12 09:47:56 +00:00
Dmitry Popov
8ab337e8e7 chore: release version v1.47.5 2025-02-12 10:17:35 +01:00
CI
51878ab503 chore: release version v1.47.5 2025-02-12 07:55:37 +00:00
guarzo
401dfad298 fix: sync kills count bookmark and the kills widget (#160)
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64/v8) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
* fix: lazy load kills widget
2025-02-12 03:15:16 +04:00
CI
18cff7d312 chore: release version v1.47.4
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64/v8) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-11 11:10:34 +00:00
Dmitry Popov
7896de00d6 chore: release version v1.47.3 2025-02-11 12:02:42 +01:00
CI
3b079505c3 chore: release version v1.47.3 2025-02-11 10:20:28 +00:00
Dmitry Popov
5b972b03e5 Revert "fix: lazy load kills widget (#157)" (#158)
This reverts commit b29e57b3a4.
2025-02-11 14:20:01 +04:00
CI
79b284c46d chore: release version v1.47.2 2025-02-11 08:31:23 +00:00
guarzo
b29e57b3a4 fix: lazy load kills widget (#157)
* fix: lazy load kills widget

* fix: updates for eslint and pr feedback
2025-02-11 12:28:24 +04:00
CI
c6f4baeee3 chore: release version v1.47.1
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64/v8) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-02-09 16:27:54 +00:00
Dmitry Popov
6d341be072 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-02-09 17:27:27 +01:00
Dmitry Popov
2437ec9c84 fix(Connections): Fixed connections auto-refresh after update 2025-02-09 17:27:22 +01:00
CI
7e692b5805 chore: release version v1.47.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64/v8) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-09 10:13:17 +00:00
Dmitry Popov
01b7370ecd feat(Map): Added check for active map subscription to using Map APIs 2025-02-09 11:12:43 +01:00
CI
20ad8b07d7 chore: release version v1.46.1 2025-02-09 09:36:55 +00:00
Aleksei Chichenkov
cab1880fb0 fix(Map): Fixed a lot of design and architect issues after last milli… (#154)
* fix(Map): Fixed a lot of design and architect issues after last million PRs

* fix(Map): removed unnecessary hooks styles

---------

Co-authored-by: achichenkov <aleksei.chichenkov@telleqt.ai>
2025-02-09 13:36:25 +04:00
CI
78eefcd6a7 chore: release version v1.46.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64/v8) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-08 11:03:09 +00:00
Dmitry Popov
eec78d38a8 feat: Added WANDERER_RESTRICT_MAPS_CREATION env support
Restrict maps creation for any registered users, allow for server admins
only.
2025-02-08 12:02:38 +01:00
CI
73f8b1f06b chore: release version v1.45.5
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64/v8) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-07 19:16:01 +00:00
guarzo
f96cb01860 fix: restore styling for local characters list (#152) 2025-02-07 23:15:20 +04:00
CI
6800be1bb6 chore: release version v1.45.4 2025-02-07 19:15:01 +00:00
guarzo
143f0a5b3a fix: remove snap to grid customization (#153) 2025-02-07 23:14:29 +04:00
CI
b6495504f8 chore: release version v1.45.3
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64/v8) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-02-05 17:22:36 +00:00
guarzo
2f07ec1b74 fix: color and formatting fixes for local character (#150) 2025-02-05 21:22:10 +04:00
CI
7073a0e8e6 chore: release version v1.45.2
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64/v8) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-05 15:55:30 +00:00
guarzo
bb0d91a3c7 fix: fix route list hover and on the map character list (#149)
* fix: correct formatting for on the map character list

* fix: fix hover for route list
2025-02-05 19:55:05 +04:00
CI
1cb12b97ba chore: release version v1.45.1 2025-02-05 14:59:57 +00:00
guarzo
860d20dc66 fix: kill count subscript position on firefox, and remove kill filter for single system (#148) 2025-02-05 18:59:30 +04:00
CI
a850071965 chore: release version v1.45.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64/v8) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-05 07:02:17 +00:00
guarzo
fc41573e70 feat: allow filtering of k-space kills (#147) 2025-02-05 07:01:46 +00:00
CI
97f1808fb5 chore: release version v1.44.9
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64/v8) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-04 21:00:56 +00:00
guarzo
d31046eebb fix: improve local character header shrink behavior (#146) 2025-02-04 21:00:30 +00:00
CI
a70fa50eab chore: release version v1.44.8 2025-02-04 19:32:13 +00:00
Dmitry Popov
9a082c26f5 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-02-04 20:31:26 +01:00
Dmitry Popov
6af2dc1ed5 fix(Core): include external libraries in build 2025-02-04 20:31:21 +01:00
CI
5fd1509d44 chore: release version v1.44.7 2025-02-04 19:21:18 +00:00
Dmitry Popov
2448c0531b fix(Core): include external libraries in build 2025-02-04 20:20:34 +01:00
CI
b685ea1013 chore: release version v1.44.6 2025-02-04 17:19:49 +00:00
guarzo
55465688c8 Add hover tooltips for local counter and kills bookmark (#130)
* feat: add local pilots and kills display on hover
2025-02-04 21:19:13 +04:00
CI
ac3c7e0c44 chore: release version v1.44.5 2025-02-04 17:12:10 +00:00
guarzo
2d6ab5646c fix: include category param in search cache key (#144) 2025-02-04 21:11:41 +04:00
CI
67b373ac29 chore: release version v1.44.4
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-02-02 21:45:53 +00:00
Dmitry Popov
678169e6fa chore: release version v1.44.0 2025-02-02 22:44:55 +01:00
Dmitry Popov
7ee3c8db82 Merge branch 'main' into zkill_subs 2025-02-02 22:25:00 +01:00
Dmitry Popov
304f4b01ab chore: release version v1.44.0 2025-02-02 22:24:47 +01:00
155 changed files with 5734 additions and 2297 deletions

View File

@@ -1,7 +1,12 @@
{
"name": "wanderer-dev",
"dockerComposeFile": ["./docker-compose.yml"],
"extensions": ["jakebecker.elixir-ls"],
"extensions": [
"jakebecker.elixir-ls",
"JakeBecker.elixir-ls",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
],
"service": "wanderer",
"workspaceFolder": "/app",
"shutdownAction": "stopCompose",

View File

@@ -7,4 +7,5 @@ export EVE_CLIENT_WITH_WALLET_SECRET="<EVE_CLIENT_WITH_WALLET_SECRET>"
export GIT_SHA="1111"
export WANDERER_INVITES="false"
export WANDERER_PUBLIC_API_DISABLED="false"
export WANDERER_CHARACTER_API_DISABLED="false"
export WANDERER_ZKILL_PRELOAD_DISABLED="false"

View File

@@ -1,8 +1,6 @@
name: Build
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
push:
branches:
- main
@@ -41,8 +39,28 @@ jobs:
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
manual-approval:
name: Manual Approval
runs-on: ubuntu-latest
needs: deploy-test
if: success()
permissions:
issues: write
steps:
- name: Await Manual Approval
uses: trstringer/manual-approval@v1
with:
secret: ${{ github.TOKEN }}
approvers: DmitryPopov
minimum-approvals: 1
issue-title: "Manual Approval Required for Release"
issue-body: "Please approve or deny the deployment."
build:
name: 🛠 Build
needs: manual-approval
runs-on: ubuntu-22.04
if: ${{ (github.ref == 'refs/heads/main') && github.event_name == 'push' }}
permissions:

View File

@@ -2,6 +2,260 @@
<!-- changelog -->
## [v1.52.0](https://github.com/wanderer-industries/wanderer/compare/v1.51.3...v1.52.0) (2025-02-19)
### Features:
* Map: Added map characters view
## [v1.51.3](https://github.com/wanderer-industries/wanderer/compare/v1.51.2...v1.51.3) (2025-02-19)
### Bug Fixes:
* pending deletion working again (#185)
## [v1.51.2](https://github.com/wanderer-industries/wanderer/compare/v1.51.1...v1.51.2) (2025-02-18)
## [v1.51.1](https://github.com/wanderer-industries/wanderer/compare/v1.51.0...v1.51.1) (2025-02-18)
## [v1.51.0](https://github.com/wanderer-industries/wanderer/compare/v1.50.0...v1.51.0) (2025-02-17)
### Features:
* add undo deletion for signatures (#155)
* add undo for signature deletion and addition
## [v1.50.0](https://github.com/wanderer-industries/wanderer/compare/v1.49.0...v1.50.0) (2025-02-17)
### Features:
* allow addition of characters to acl without preregistration (#176)
## [v1.49.0](https://github.com/wanderer-industries/wanderer/compare/v1.48.1...v1.49.0) (2025-02-15)
### Features:
* add api for acl management (#171)
## [v1.48.1](https://github.com/wanderer-industries/wanderer/compare/v1.48.0...v1.48.1) (2025-02-13)
## [v1.48.0](https://github.com/wanderer-industries/wanderer/compare/v1.47.6...v1.48.0) (2025-02-12)
### Features:
* autosize local character tooltip and increase hover target (#165)
## [v1.47.6](https://github.com/wanderer-industries/wanderer/compare/v1.47.5...v1.47.6) (2025-02-12)
## [v1.47.5](https://github.com/wanderer-industries/wanderer/compare/v1.47.4...v1.47.5) (2025-02-12)
### Bug Fixes:
* sync kills count bookmark and the kills widget (#160)
* lazy load kills widget
## [v1.47.4](https://github.com/wanderer-industries/wanderer/compare/v1.47.3...v1.47.4) (2025-02-11)
## [v1.47.3](https://github.com/wanderer-industries/wanderer/compare/v1.47.2...v1.47.3) (2025-02-11)
## [v1.47.2](https://github.com/wanderer-industries/wanderer/compare/v1.47.1...v1.47.2) (2025-02-11)
### Bug Fixes:
* lazy load kills widget (#157)
* lazy load kills widget
* updates for eslint and pr feedback
## [v1.47.1](https://github.com/wanderer-industries/wanderer/compare/v1.47.0...v1.47.1) (2025-02-09)
### Bug Fixes:
* Connections: Fixed connections auto-refresh after update
## [v1.47.0](https://github.com/wanderer-industries/wanderer/compare/v1.46.1...v1.47.0) (2025-02-09)
### Features:
* Map: Added check for active map subscription to using Map APIs
## [v1.46.1](https://github.com/wanderer-industries/wanderer/compare/v1.46.0...v1.46.1) (2025-02-09)
### Bug Fixes:
* Map: Fixed a lot of design and architect issues after last milli… (#154)
* Map: Fixed a lot of design and architect issues after last million PRs
* Map: removed unnecessary hooks styles
## [v1.46.0](https://github.com/wanderer-industries/wanderer/compare/v1.45.5...v1.46.0) (2025-02-08)
### Features:
* Added WANDERER_RESTRICT_MAPS_CREATION env support
## [v1.45.5](https://github.com/wanderer-industries/wanderer/compare/v1.45.4...v1.45.5) (2025-02-07)
### Bug Fixes:
* restore styling for local characters list (#152)
## [v1.45.4](https://github.com/wanderer-industries/wanderer/compare/v1.45.3...v1.45.4) (2025-02-07)
### Bug Fixes:
* remove snap to grid customization (#153)
## [v1.45.3](https://github.com/wanderer-industries/wanderer/compare/v1.45.2...v1.45.3) (2025-02-05)
### Bug Fixes:
* color and formatting fixes for local character (#150)
## [v1.45.2](https://github.com/wanderer-industries/wanderer/compare/v1.45.1...v1.45.2) (2025-02-05)
### Bug Fixes:
* fix route list hover and on the map character list (#149)
* correct formatting for on the map character list
* fix hover for route list
## [v1.45.1](https://github.com/wanderer-industries/wanderer/compare/v1.45.0...v1.45.1) (2025-02-05)
### Bug Fixes:
* kill count subscript position on firefox, and remove kill filter for single system (#148)
## [v1.45.0](https://github.com/wanderer-industries/wanderer/compare/v1.44.9...v1.45.0) (2025-02-05)
### Features:
* allow filtering of k-space kills (#147)
## [v1.44.9](https://github.com/wanderer-industries/wanderer/compare/v1.44.8...v1.44.9) (2025-02-04)
### Bug Fixes:
* improve local character header shrink behavior (#146)
## [v1.44.8](https://github.com/wanderer-industries/wanderer/compare/v1.44.7...v1.44.8) (2025-02-04)
### Bug Fixes:
* Core: include external libraries in build
## [v1.44.7](https://github.com/wanderer-industries/wanderer/compare/v1.44.6...v1.44.7) (2025-02-04)
### Bug Fixes:
* Core: include external libraries in build
## [v1.44.6](https://github.com/wanderer-industries/wanderer/compare/v1.44.5...v1.44.6) (2025-02-04)
## [v1.44.5](https://github.com/wanderer-industries/wanderer/compare/v1.44.4...v1.44.5) (2025-02-04)
### Bug Fixes:
* include category param in search cache key (#144)
## [v1.44.4](https://github.com/wanderer-industries/wanderer/compare/v1.44.3...v1.44.4) (2025-02-02)
## [v1.44.3](https://github.com/wanderer-industries/wanderer/compare/v1.44.2...v1.44.3) (2025-02-02)

View File

@@ -58,6 +58,7 @@ Now you can visit [`localhost:8000`](http://localhost:8000) from your browser.
- `root@0d0a785313b6:/app# apt update`
- `root@0d0a785313b6:/app# curl -sL https://deb.nodesource.com/setup_18.x | bash -`
- `root@0d0a785313b6:/app# apt-get install nodejs inotify-tools -y`
- `root@0d0a785313b6:/app# npm install -g yarn`
- `root@0d0a785313b6:/app# mix setup`
- See how to run server in #Run section

View File

@@ -1,6 +1,3 @@
// import '@fontsource-variable/inter'
// import '@fontsource-variable/jetbrains-mono'
// import './lib/tailwind/index.css';
import './css/app.css';
import './lib/phoenix';

View File

@@ -936,3 +936,66 @@ body > div:first-of-type {
width: 16px;
height: 16px;
}
.verticalTabsContainer {
display: flex;
width: 100%;
min-height: 300px;
}
.verticalTabsContainer .p-tabview {
width: 100%;
display: flex;
align-items: flex-start;
}
.verticalTabsContainer .p-tabview-panels {
padding: 6px 1rem !important;
flex-grow: 1;
height: 100%;
}
.verticalTabsContainer .p-tabview-nav-container {
border-right: none;
height: 100%;
}
.verticalTabsContainer .p-tabview-nav {
flex-direction: column;
width: 150px;
min-height: 100%;
border: none;
}
.verticalTabsContainer .p-tabview-nav li {
width: 100%;
border-right: 4px solid var(--surface-hover);
background-color: var(--surface-card);
transition:
background-color 200ms,
border-right-color 200ms;
}
.verticalTabsContainer .p-tabview-nav li:hover {
background-color: var(--surface-hover);
border-right: 4px solid var(--surface-100);
}
.verticalTabsContainer .p-tabview-nav li .p-tabview-nav-link {
transition: color 200ms;
justify-content: flex-end;
padding: 10px;
background-color: initial;
border: none;
color: var(--gray-400);
border-radius: initial;
font-weight: 400;
margin: 0;
}
.verticalTabsContainer .p-tabview-nav li.p-tabview-selected {
background-color: var(--surface-50);
border-right: 4px solid var(--primary-color);
}
.verticalTabsContainer .p-tabview-nav li.p-tabview-selected .p-tabview-nav-link {
font-weight: 600;
color: var(--primary-color);
}
.verticalTabsContainer .p-tabview-nav li.p-tabview-selected:hover {
border-right: 4px solid var(--primary-color);
}
.verticalTabsContainer .p-tabview-panel {
flex-grow: 1;
}

View File

@@ -1,3 +0,0 @@
.active {
background-color: rgba(98, 98, 98, 0.33);
}

View File

@@ -1,3 +0,0 @@
.active {
background-color: rgba(98, 98, 98, 0.33);
}

View File

@@ -1,3 +0,0 @@
.active {
background-color: rgba(98, 98, 98, 0.33);
}

View File

@@ -1,3 +0,0 @@
.active {
background-color: rgba(98, 98, 98, 0.33);
}

View File

@@ -1,2 +1,3 @@
export * from './useSystemInfo';
export * from './useGetOwnOnlineCharacters';
export * from './useElementWidth';

View File

@@ -0,0 +1,43 @@
import { useState, useLayoutEffect, RefObject } from 'react';
/**
* useElementWidth
*
* A custom hook that accepts a ref to an HTML element and returns its current width.
* It uses a ResizeObserver and window resize listener to update the width when necessary.
*
* @param ref - A RefObject pointing to an HTML element.
* @returns The current width of the element.
*/
export function useElementWidth<T extends HTMLElement>(ref: RefObject<T>): number {
const [width, setWidth] = useState<number>(0);
useLayoutEffect(() => {
const updateWidth = () => {
if (ref.current) {
const newWidth = ref.current.getBoundingClientRect().width;
if (newWidth > 0) {
setWidth(newWidth);
}
}
};
updateWidth(); // Initial measurement
const observer = new ResizeObserver(() => {
const id = setTimeout(updateWidth, 100);
return () => clearTimeout(id);
});
if (ref.current) {
observer.observe(ref.current);
}
window.addEventListener("resize", updateWidth);
return () => {
observer.disconnect();
window.removeEventListener("resize", updateWidth);
};
}, [ref]);
return width;
}

View File

@@ -84,7 +84,6 @@ interface MapCompProps {
onCommand: OutCommandHandler;
onSelectionChange: OnMapSelectionChange;
onManualDelete(systems: string[]): void;
canRemoveConnection?(connectionId: string): boolean;
onConnectionInfoClick?(e: SolarSystemConnection): void;
onAddSystem?: OnMapAddSystemCallback;
onSelectionContextMenu?: NodeSelectionMouseHandler;
@@ -114,9 +113,8 @@ const MapComp = ({
isSoftBackground,
theme,
onAddSystem,
canRemoveConnection,
}: MapCompProps) => {
const { getEdge, getNode, getNodes } = useReactFlow();
const { getNode, getNodes } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
@@ -128,7 +126,6 @@ const MapComp = ({
const { variant, gap, size, color } = useBackgroundVars(theme);
const { isPanAndDrag, nodeComponent, connectionMode } = getBehaviorForTheme(theme || 'default');
// You can create nodeTypes dynamically based on the node component
const nodeTypes = useMemo(() => {
return {
custom: nodeComponent,
@@ -225,40 +222,6 @@ const MapComp = ({
[getNode, getNodes, onManualDelete, onNodesChange],
);
const handleEdgesChange = useCallback(
(changes: EdgeChange[]) => {
const nextChanges = changes.reduce((acc, change) => {
if (change.type !== 'remove') {
return [...acc, change];
}
if (canRemoveConnection?.(change.id)) {
return [...acc, change];
}
const edge = getEdge(change.id);
if (!edge) {
return [...acc, change];
}
const sourceNode = getNode(edge.source);
const targetNode = getNode(edge.target);
if (!sourceNode || !targetNode) {
return [...acc, change];
}
if (sourceNode.data.locked || targetNode.data.locked) {
return acc;
}
return [...acc, change];
}, [] as EdgeChange[]);
onEdgesChange(nextChanges);
},
[getEdge, getNode, onEdgesChange],
);
useEffect(() => {
update(x => ({
...x,
@@ -274,7 +237,7 @@ const MapComp = ({
nodes={nodes}
edges={edges}
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
// TODO we need save into session all of this
// and on any action do either

View File

@@ -37,6 +37,7 @@ const INITIAL_DATA: MapData = {
userPermissions: {},
systemSignatures: {} as Record<string, SystemSignature[]>,
options: {} as Record<string, string | boolean>,
isSubscriptionActive: false,
};
export interface MapContextProps {

View File

@@ -0,0 +1,48 @@
.KillsBookmark {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 8px;
font-weight: 700;
border: 0;
border-radius: 5px 5px 0 0;
padding: 4px 3px;
}
.KillsBookmarkWithIcon {
display: flex;
align-items: center;
justify-content: center;
margin-top: -2px;
text-shadow: 0 0 3px #000;
padding-right: 2px;
height: 8px;
font-size: 8px;
line-height: 12px;
font-weight: 700;
text-size-adjust: 100%;
.pi {
font-size: 9px;
}
.text {
font-size: 9px;
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
}
.TooltipContainer {
background-color: #1a1a1a;
color: #fff;
padding: 3px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border-radius: 2px;
pointer-events: auto;
max-width: 500px;
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
}

View File

@@ -0,0 +1,39 @@
import { SystemKillsContent } from '../../../mapInterface/widgets/SystemKills/SystemKillsContent/SystemKillsContent';
import { useKillsCounter } from '../../hooks/useKillsCounter';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
type KillsBookmarkTooltipProps = {
killsCount: number;
killsActivityType: string | null;
systemId: string;
className?: string;
size?: TooltipSize;
} & WithChildren &
WithClassName;
export const KillsCounter = ({ killsCount, systemId, className, children, size = 'xs' }: KillsBookmarkTooltipProps) => {
const { isLoading, kills: detailedKills, systemNameMap } = useKillsCounter({ realSystemId: systemId });
if (!killsCount || detailedKills.length === 0 || !systemId || isLoading) return null;
const tooltipContent = (
<div style={{ width: '100%', minWidth: '300px', overflow: 'hidden' }}>
<SystemKillsContent
kills={detailedKills}
systemNameMap={systemNameMap}
onlyOneSystem={true}
autoSize={true}
limit={killsCount}
/>
</div>
);
return (
<WdTooltipWrapper content={tooltipContent} className={className} size={size} interactive={true}>
{children}
</WdTooltipWrapper>
);
};

View File

@@ -0,0 +1,54 @@
.TooltipActive {
pointer-events: auto !important;
position: relative;
z-index: 3;
}
.hoverTarget {
padding: 0.5rem;
margin: -0.5rem;
display: inline-block;
}
.localCounter {
mix-blend-mode: screen;
display: flex;
align-items: center;
gap: 1px;
position: relative;
top: 1px;
color: var(--rf-node-local-counter);
&.hasUserCharacters {
color: var(--rf-has-user-characters);
}
& > i {
font-size: 9px;
position: relative;
}
& > span {
font-size: 9px;
line-height: 9px;
font-weight: var(--rf-local-counter-font-weight, 500);
@-moz-document url-prefix() {
position: relative;
top: -1px;
}
}
}
.Pathfinder {
.localCounter {
@-moz-document url-prefix() {
top: 0;
}
& > span {
position: relative;
top: -1px;
}
}
}

View File

@@ -0,0 +1,61 @@
import { useMemo } from 'react';
import clsx from 'clsx';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widgets/LocalCharacters/components';
import { useLocalCharactersItemTemplate } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters';
import { useLocalCharacterWidgetSettings } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalWidgetSettings';
import classes from './SolarSystemLocalCounter.module.scss';
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider';
import { useTheme } from '@/hooks/Mapper/hooks/useTheme.ts';
interface LocalCounterProps {
localCounterCharacters: Array<CharItemProps>;
hasUserCharacters: boolean;
showIcon?: boolean;
}
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
const [settings] = useLocalCharacterWidgetSettings();
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
const theme = useTheme();
const pilotTooltipContent = useMemo(() => {
return (
<div
style={{
width: '100%',
minWidth: '300px',
overflow: 'hidden',
}}
>
<LocalCharactersList items={localCounterCharacters} itemTemplate={itemTemplate} itemSize={26} autoSize={true} />
</div>
);
}, [localCounterCharacters, itemTemplate]);
if (localCounterCharacters.length === 0) {
return null;
}
return (
<div
className={clsx(classes.TooltipActive, {
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
})}
>
<WdTooltipWrapper content={pilotTooltipContent} position={TooltipPosition.right} offset={0} interactive={true}>
<div className={clsx(classes.hoverTarget)}>
<div
className={clsx(classes.localCounter, {
[classes.hasUserCharacters]: hasUserCharacters,
})}
>
{showIcon && <i className="pi pi-users" />}
<span>{localCounterCharacters.length}</span>
</div>
</div>
</WdTooltipWrapper>
</div>
);
};

View File

@@ -2,34 +2,30 @@
$pastel-blue: #5a7d9a;
$pastel-pink: #d291bc;
$pastel-green: #88b04b;
$pastel-yellow: #ffdd59;
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020;
$node-bg-color: #202020;
$node-soft-bg-color: #202020;
$text-color: #ffffff;
$tag-color: #38BDF8;
$region-name: #D6D3D1;
$custom-name: #93C5FD;
.RootCustomNode {
display: flex;
width: 130px;
height: 34px;
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
flex-direction: column;
padding: 2px 6px;
font-size: 10px;
background-color: $tooltip-bg;
background-color: var(--rf-node-bg-color, #202020) !important;
color: var(--rf-text-color, #ffffff);
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
border: 1px solid darken($pastel-blue, 10%);
border-radius: 5px;
position: relative;
z-index: 1;
z-index: 3;
overflow: hidden;
&.Mataria,
@@ -92,21 +88,9 @@ $custom-name: #93C5FD;
box-shadow: 0 0 10px #9a1af1c2;
}
&.tooltip {
background-color: $tooltip-bg;
color: $text-color;
padding: 5px 10px;
border-radius: 3px;
border: 1px solid $pastel-pink;
}
&.eve-system-status-home {
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-home),
transparent
);
background-image: linear-gradient(45deg, var(--eve-solar-system-status-color-background), transparent);
&.selected {
border-color: var(--eve-solar-system-status-color-home);
}
@@ -114,11 +98,7 @@ $custom-name: #93C5FD;
&.eve-system-status-friendly {
border: 1px solid var(--eve-solar-system-status-color-friendly-dark20);
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-friendly-dark30),
transparent
);
background-image: linear-gradient(275deg, var(--eve-solar-system-status-friendly-dark30), transparent);
&.selected {
border-color: var(--eve-solar-system-status-color-friendly-dark5);
}
@@ -133,27 +113,15 @@ $custom-name: #93C5FD;
}
&.eve-system-status-warning {
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-warning),
transparent
);
background-image: linear-gradient(275deg, var(--eve-solar-system-status-warning), transparent);
}
&.eve-system-status-dangerous {
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-dangerous),
transparent
);
background-image: linear-gradient(275deg, var(--eve-solar-system-status-dangerous), transparent);
}
&.eve-system-status-target {
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-target),
transparent
);
background-image: linear-gradient(275deg, var(--eve-solar-system-status-target), transparent);
}
}
@@ -178,8 +146,6 @@ $custom-name: #93C5FD;
padding-left: 3px;
padding-right: 3px;
//background-color: #833ca4;
&:not(:first-child) {
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
}
@@ -266,26 +232,17 @@ $custom-name: #93C5FD;
.TagTitle {
font-size: 11px;
font-weight: medium;
font-weight: 500;
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
color: var(--rf-tag-color, #38BDF8);
color: var(--rf-tag-color, #38bdf8);
}
/* Firefox kostyl */
@-moz-document url-prefix() {
.classSystemName {
font-family: inherit !important;
font-weight: bold;
}
}
.classSystemName {
//font-weight: bold;
}
.solarSystemName {
}
}
.BottomRow {
@@ -294,22 +251,23 @@ $custom-name: #93C5FD;
align-items: center;
height: 19px;
.localCounter {
display: flex;
//align-items: center;
gap: 2px;
& > i {
position: relative;
top: 1px;
.hasLocalCounter {
margin-right: 2px;
&.countAbove9 {
margin-right: 1.5rem;
}
}
& > span {
font-size: 9px;
line-height: 9px;
font-weight: 500;
//margin-top: 1px;
}
.lockIcon {
font-size: 0.45rem;
font-weight: bold;
position: relative;
}
.mapMarker {
font-size: 0.45rem;
font-weight: bold;
position: relative;
}
}
@@ -340,7 +298,8 @@ $custom-name: #93C5FD;
.Handlers {
position: absolute;
z-index: 2;
z-index: 4;
pointer-events: none;
top: 0;
left: 0;
width: 100%;
@@ -353,6 +312,7 @@ $custom-name: #93C5FD;
border: 1px solid $pastel-blue;
width: 5px;
height: 5px;
pointer-events: auto;
&.selected {
border-color: $pastel-pink;

View File

@@ -1,20 +1,24 @@
import { memo } from 'react';
import { MapSolarSystemType } from '../../map.types';
import { Handle, Position, NodeProps } from 'reactflow';
import { Handle, NodeProps, Position } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeDefault.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
import { useLocalCounter, useSolarSystemNode, useNodeKillsCount } from '../../hooks/useSolarSystemLogic';
import {
EFFECT_BACKGROUND_STYLES,
MARKER_BOOKMARK_BG_STYLES,
STATUS_CLASSES,
EFFECT_BACKGROUND_STYLES,
} from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
return (
<>
@@ -22,7 +26,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
<div className={classes.Bookmarks}>
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
</div>
)}
@@ -32,13 +36,19 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
</div>
)}
{nodeVars.killsCount && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}>
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && (
<KillsCounter
killsCount={localKillsCount}
systemId={nodeVars.solarSystemId}
size="lg"
killsActivityType={nodeVars.killsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
>
<div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div>
</div>
</KillsCounter>
)}
{nodeVars.labelsInfo.map(x => (
@@ -53,11 +63,10 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
className={clsx(
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
classes[STATUS_CLASSES[nodeVars.status]],
{
[classes.selected]: nodeVars.selected,
},
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
{ [classes.selected]: nodeVars.selected },
)}
onMouseDownCapture={e => nodeVars.dbClick(e)}
>
{nodeVars.visible && (
<>
@@ -88,7 +97,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
{nodeVars.isWormhole && (
<div className={classes.statics}>
{nodeVars.sortedStatics.map(whClass => (
<WormholeClassComp key={whClass} id={whClass} />
<WormholeClassComp key={String(whClass)} id={String(whClass)} />
))}
</div>
)}
@@ -113,27 +122,18 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
{nodeVars.isWormhole && !nodeVars.customName && <div />}
<div className="flex items-center justify-end">
<div className="flex gap-1 items-center">
{nodeVars.locked && (
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
)}
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
)}
{nodeVars.charactersInSystem.length > 0 && (
<div
className={clsx(classes.localCounter, {
['text-amber-300']: nodeVars.hasUserCharacters,
})}
>
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
</div>
<div className="flex items-center gap-1 justify-end">
<div className={clsx('flex items-center gap-1')}>
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
)}
</div>
<LocalCounter
hasUserCharacters={nodeVars.hasUserCharacters}
localCounterCharacters={localCounterCharacters}
/>
</div>
</div>
</>
@@ -145,7 +145,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
{nodeVars.unsplashedLeft.length > 0 && (
<div className={classes.Unsplashed}>
{nodeVars.unsplashedLeft.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
<UnsplashedSignature key={sig.eve_id} signature={sig} />
))}
</div>
)}
@@ -153,14 +153,14 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
{nodeVars.unsplashedRight.length > 0 && (
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
{nodeVars.unsplashedRight.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
<UnsplashedSignature key={sig.eve_id} signature={sig} />
))}
</div>
)}
</>
)}
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
<div className={classes.Handlers}>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleTop, {

View File

@@ -1,91 +1,20 @@
@import './SolarSystemNodeDefault.module.scss';
/* ---------------------------
Only override what's different
--------------------------- */
/* ---------------------------------------------
Only override what's different from the base
Currently none required
---------------------------------------------- */
/* 1) .RootCustomNode:
- new background-color using CSS var
- plus color, font-family, and font-weight */
.RootCustomNode {
background-color: var(--rf-node-bg-color, #202020) !important;
color: var(--rf-text-color, #ffffff);
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
/* 2) .Bookmarks:
- add var-based font family/weight
*/
.Bookmarks {
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
/* 3) .HeadRow, .classTitle, .classSystemName:
- add new references to var-based font family/weight
*/
.HeadRow {
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
.classTitle {
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
@-moz-document url-prefix() {
.classSystemName {
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
}
.classSystemName {
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
}
/* 4) .BottomRow:
- introduces .tagTitle, .regionName, .customName, .localCounter
referencing new CSS variables */
.BottomRow {
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
.tagTitle {
font-size: 11px;
font-weight: medium;
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
color: var(--rf-tag-color, #38BDF8);
}
.regionName {
color: var(--rf-region-name, #D6D3D1);
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
.customName {
color: var(--rf-custom-name, #93C5FD);
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
.localCounter {
display: flex;
color: var(--rf-has-user-characters, #fbbf24);
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
gap: 2px;
.hasUserCharacters {
color: var(--rf-has-user-characters, #fbbf24);
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
&.eve-system-status-home {
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-home),
transparent
);
&.selected {
border-color: var(--eve-solar-system-status-color-home);
}
}
}

View File

@@ -1,20 +1,24 @@
import { memo } from 'react';
import { MapSolarSystemType } from '../../map.types';
import { Handle, Position, NodeProps } from 'reactflow';
import { Handle, NodeProps, Position } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeTheme.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks/useSolarSystemLogic';
import {
EFFECT_BACKGROUND_STYLES,
MARKER_BOOKMARK_BG_STYLES,
STATUS_CLASSES,
EFFECT_BACKGROUND_STYLES,
} from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
return (
<>
@@ -22,7 +26,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
<div className={classes.Bookmarks}>
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
</div>
)}
@@ -32,13 +36,19 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
</div>
)}
{nodeVars.killsCount && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}>
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && (
<KillsCounter
killsCount={localKillsCount}
systemId={nodeVars.solarSystemId}
size="lg"
killsActivityType={nodeVars.killsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
>
<div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div>
</div>
</KillsCounter>
)}
{nodeVars.labelsInfo.map(x => (
@@ -53,11 +63,10 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
className={clsx(
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
classes[STATUS_CLASSES[nodeVars.status]],
{
[classes.selected]: nodeVars.selected,
},
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
{ [classes.selected]: nodeVars.selected },
)}
onMouseDownCapture={e => nodeVars.dbClick(e)}
>
{nodeVars.visible && (
<>
@@ -88,7 +97,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
{nodeVars.isWormhole && (
<div className={classes.statics}>
{nodeVars.sortedStatics.map(whClass => (
<WormholeClassComp key={whClass} id={whClass} />
<WormholeClassComp key={String(whClass)} id={String(whClass)} />
))}
</div>
)}
@@ -123,27 +132,18 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
{nodeVars.isWormhole && !nodeVars.customName && <div />}
<div className="flex items-center justify-end">
<div className="flex gap-1 items-center">
{nodeVars.locked && (
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
)}
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
)}
{nodeVars.charactersInSystem.length > 0 && (
<div
className={clsx(classes.localCounter, {
[classes.hasUserCharacters]: nodeVars.hasUserCharacters,
})}
>
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
</div>
<div className="flex items-center gap-1 justify-end">
<div className={clsx('flex items-center gap-1')}>
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
)}
</div>
<LocalCounter
hasUserCharacters={nodeVars.hasUserCharacters}
localCounterCharacters={localCounterCharacters}
/>
</div>
</div>
</>
@@ -155,7 +155,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
{nodeVars.unsplashedLeft.length > 0 && (
<div className={classes.Unsplashed}>
{nodeVars.unsplashedLeft.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
<UnsplashedSignature key={sig.eve_id} signature={sig} />
))}
</div>
)}
@@ -163,14 +163,14 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
{nodeVars.unsplashedRight.length > 0 && (
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
{nodeVars.unsplashedRight.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
<UnsplashedSignature key={sig.eve_id} signature={sig} />
))}
</div>
)}
</>
)}
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
<div className={classes.Handlers}>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleTop, {

View File

@@ -10,5 +10,6 @@ export const convertSystem2Node = (sys: SolarSystemRawType): Node => {
position: sys.position,
data: sys,
draggable: !sys.locked,
deletable: !sys.locked,
};
};

View File

@@ -6,6 +6,7 @@ export function useBackgroundVars(themeName?: string) {
const [gap, setGap] = useState<number>(16);
const [size, setSize] = useState<number>(1);
const [color, setColor] = useState('#81818b');
const [snapSize, setSnapSize] = useState<number>(25);
useEffect(() => {
// match any element whose entire `class` attribute ends with "-theme"
@@ -29,16 +30,19 @@ export function useBackgroundVars(themeName?: string) {
const cssVarGap = style.getPropertyValue('--rf-bg-gap');
const cssVarSize = style.getPropertyValue('--rf-bg-size');
const cssVarSnapSize = style.getPropertyValue('--rf-snap-size');
const cssColor = style.getPropertyValue('--rf-bg-pattern-color');
const gapNum = parseInt(cssVarGap, 10) || 16;
const sizeNum = parseInt(cssVarSize, 10) || 1;
const snapSize = parseInt(cssVarSnapSize, 10) || 25; //react-flow default
setVariant(finalVariant);
setGap(gapNum);
setSize(sizeNum);
setColor(cssColor);
setSnapSize(snapSize);
}, [themeName]);
return { variant, gap, size, color };
return { variant, gap, size, color, snapSize };
}

View File

@@ -0,0 +1,44 @@
import { useMemo } from 'react';
import { useSystemKills } from '../../mapInterface/widgets/SystemKills/hooks/useSystemKills';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
interface UseKillsCounterProps {
realSystemId: string;
}
export function useKillsCounter({ realSystemId }: UseKillsCounterProps) {
const { data: mapData, outCommand } = useMapRootState();
const { systems } = mapData;
const systemNameMap = useMemo(() => {
const m: Record<string, string> = {};
systems.forEach(sys => {
m[sys.id] = sys.temporary_name || sys.name || '???';
});
return m;
}, [systems]);
const { kills: allKills, isLoading } = useSystemKills({
systemId: realSystemId,
outCommand,
showAllVisible: false,
});
const filteredKills = useMemo(() => {
if (!allKills || allKills.length === 0) return [];
return [...allKills]
.sort((a, b) => {
const aTime = a.kill_time ? new Date(a.kill_time).getTime() : 0;
const bTime = b.kill_time ? new Date(b.kill_time).getTime() : 0;
return bTime - aTime;
})
.slice(0, 10);
}, [allKills]);
return {
isLoading,
kills: filteredKills,
systemNameMap,
};
}

View File

@@ -127,6 +127,10 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
// do nothing here
break;
case Commands.detailedKillsUpdated:
// do nothing here
break;
default:
console.warn(`Map handlers: Unknown command: ${type}`, data);
break;

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { MapSolarSystemType } from '../map.types';
import { NodeProps } from 'reactflow';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
@@ -10,10 +10,18 @@ import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormhol
import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager';
import { CharacterTypeRaw, OutCommand } from '@/hooks/Mapper/types';
import { CharacterTypeRaw, Commands, OutCommand, SystemSignature } from '@/hooks/Mapper/types';
import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants';
import { useMapEventListener } from '@/hooks/Mapper/events';
function getActivityType(count: number) {
export type LabelInfo = {
id: string;
shortName: string;
};
export type UnsplashedSignatureType = SystemSignature & { sig_id: string };
function getActivityType(count: number): string {
if (count <= 5) return 'activityNormal';
if (count <= 30) return 'activityWarn';
return 'activityDanger';
@@ -26,12 +34,25 @@ const SpaceToClass: Record<string, string> = {
[Spaces.Gallente]: 'Gallente',
};
function sortedLabels(labels: string[]) {
function sortedLabels(labels: string[]): LabelInfo[] {
if (!labels) return [];
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x]);
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x] as LabelInfo);
}
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
const localCounterCharacters = useMemo(() => {
return nodeVars.charactersInSystem
.map(char => ({
...char,
compact: true,
isOwn: nodeVars.userCharacters.includes(char.eve_id),
}))
.sort((a, b) => a.name.localeCompare(b.name));
}, [nodeVars.charactersInSystem, nodeVars.userCharacters]);
return { localCounterCharacters };
}
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars {
const { id, data, selected } = props;
const {
system_static_info,
@@ -71,7 +92,6 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
const {
data: {
characters,
presentCharacters,
wormholesData,
hubs,
kills,
@@ -87,15 +107,14 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
const systemSignatures = useMemo(
const systemSigs = useMemo(
() => mapSystemSignatures[solar_system_id] || system_signatures,
[system_signatures, solar_system_id, mapSystemSignatures],
);
const charactersInSystem = useMemo(() => {
return characters.filter(c => c.location?.solar_system_id === solar_system_id).filter(c => c.online);
// eslint-disable-next-line
}, [characters, presentCharacters, solar_system_id]);
return characters.filter(c => c.location?.solar_system_id === solar_system_id && c.online);
}, [characters, solar_system_id]);
const isWormhole = isWormholeSpace(system_class);
@@ -136,52 +155,64 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
const temporaryName = useMemo(() => {
const computedTemporaryName = useMemo(() => {
if (!isTempSystemNameEnabled) {
return '';
}
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
return temporary_name ? `${linkedSigPrefix}${temporary_name}` : `${linkedSigPrefix}${solar_system_name}`;
}
return temporary_name;
}, [isShowLinkedSigIdTempName, isTempSystemNameEnabled, linkedSigPrefix, solar_system_name, temporary_name]);
const systemName = useMemo(() => {
if (isTempSystemNameEnabled && temporaryName) {
return temporaryName;
if (isTempSystemNameEnabled && computedTemporaryName) {
return computedTemporaryName;
}
return solar_system_name;
}, [isTempSystemNameEnabled, solar_system_name, temporaryName]);
}, [isTempSystemNameEnabled, solar_system_name, computedTemporaryName]);
const customName = (isTempSystemNameEnabled && temporaryName && name) || (solar_system_name !== name && name) || null;
const customName = useMemo(() => {
if (isTempSystemNameEnabled && computedTemporaryName && name) {
return name;
}
if (solar_system_name !== name && name) {
return name;
}
return null;
}, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]);
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
if (!isShowUnsplashedSignatures) {
return [[], []];
}
return prepareUnsplashedChunks(
systemSignatures
systemSigs
.filter(s => s.group === 'Wormhole' && !s.linked_system)
.map(s => ({
eve_id: s.eve_id,
type: s.type,
custom_info: s.custom_info,
})),
kind: s.kind,
name: s.name,
group: s.group,
})) as UnsplashedSignatureType[],
);
}, [isShowUnsplashedSignatures, systemSignatures]);
}, [isShowUnsplashedSignatures, systemSigs]);
const nodeVars = {
// Ensure hubs are always strings.
const hubsAsStrings = useMemo(() => hubs.map(item => item.toString()), [hubs]);
const nodeVars: SolarSystemNodeVars = {
id,
selected,
visible,
isWormhole,
classTitleColor,
killsCount,
killsActivityType,
hasUserCharacters,
userCharacters,
showHandlers,
regionClass,
systemName,
@@ -195,10 +226,10 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
sortedStatics,
effectName: effect_name,
regionName: region_name,
solarSystemId: solar_system_id,
solarSystemId: solar_system_id.toString(),
solarSystemName: solar_system_name,
locked,
hubs,
hubs: hubsAsStrings,
name: name,
isConnecting,
hoverNodeId,
@@ -207,7 +238,7 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
unsplashedRight,
isThickConnections,
classTitle: class_title,
temporaryName: temporary_name,
temporaryName: computedTemporaryName,
};
return nodeVars;
@@ -230,25 +261,45 @@ export interface SolarSystemNodeVars {
isShattered: boolean;
tag?: string | null;
status?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
labelsInfo: Array<any>;
dbClick: (event?: void) => void;
labelsInfo: LabelInfo[];
dbClick: (event: React.MouseEvent<HTMLDivElement>) => void;
sortedStatics: Array<string | number>;
effectName: string | null;
regionName: string | null;
solarSystemId: number;
solarSystemId: string;
solarSystemName: string | null;
locked: boolean;
hubs: string[] | number[];
hubs: string[];
name: string | null;
isConnecting: boolean;
hoverNodeId: string | null;
charactersInSystem: Array<CharacterTypeRaw>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
unsplashedLeft: Array<any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
unsplashedRight: Array<any>;
userCharacters: string[];
unsplashedLeft: Array<SystemSignature>;
unsplashedRight: Array<SystemSignature>;
isThickConnections: boolean;
classTitle: string | null;
temporaryName?: string | null;
}
export function useNodeKillsCount(systemId: number | string, initialKillsCount: number | null): number | null {
const [killsCount, setKillsCount] = useState<number | null>(initialKillsCount);
useEffect(() => {
setKillsCount(initialKillsCount);
}, [initialKillsCount]);
useMapEventListener(event => {
if (event.name === Commands.killsUpdated && event.data?.toString() === systemId.toString()) {
//@ts-ignore
if (event.payload && typeof event.payload.kills === 'number') {
// @ts-ignore
setKillsCount(event.payload.kills);
}
return true;
}
return false;
});
return killsCount;
}

View File

@@ -11,7 +11,8 @@
--rf-tag-color: #38BDF8;
--rf-region-name: #D6D3D1;
--rf-custom-name: #93C5FD;
--rf-node-font-family: 'Shentox', 'Rogan', sans-serif !important;
--rf-node-font-weight: 500;
--rf-bg-variant: "dots";
--rf-bg-gap: 15;
@@ -28,4 +29,8 @@
--tooltip-bg: #202020;
--window-corner: #72716f;
--rf-local-counter-font-weight: 500;
--rf-node-local-counter: inherit;
--rf-has-user-characters: #ffc75d;
}

View File

@@ -1,19 +1,19 @@
$friendlyBase: #3bbd39;
$friendlyAlpha: #3bbd3952;
$friendlyBase: #3bbd39;
$friendlyAlpha: #3bbd3952;
$friendlyDark20: darken($friendlyBase, 20%);
$friendlyDark30: darken($friendlyBase, 30%);
$friendlyDark5: darken($friendlyBase, 5%);
$lookingForBase: #43c2fd;
$lookingForBase: #43c2fd;
$lookingForAlpha: rgba(67, 176, 253, 0.48);
$lookingForDark15: darken($lookingForBase, 15%);
$homeBase: rgb(197, 253, 67);
$homeAlpha: rgba(197, 253, 67, 0.32);
$homeBase: rgb(179, 253, 67);
$homeAlpha: rgba(186, 248, 48, 0.32);
$homeBackground: #a0fa5636;
$homeDark30: darken($homeBase, 30%);
:root {
--pastel-blue: #5a7d9a;
--pastel-pink: #d291bc;
@@ -98,6 +98,7 @@ $homeDark30: darken($homeBase, 30%);
--eve-solar-system-status-color-unknown: transparent;
--eve-solar-system-status-home: #{$homeAlpha};
--eve-solar-system-status-color-home: #{$homeBase};
--eve-solar-system-status-color-background: #{$homeBackground};
--eve-solar-system-status-color-home-dark30: #{$homeDark30};
--eve-solar-system-status-friendly: #{$friendlyAlpha};
--eve-solar-system-status-color-friendly: #{$friendlyBase};

View File

@@ -2,24 +2,31 @@
@import './eve-common';
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
$homeBase: rgb(197, 253, 67);
$homeAlpha: rgba(197, 253, 67, 0.32);
$homeDark30: darken($homeBase, 30%);
.pathfinder-theme {
/* -- Override values from the default theme -- */
--rf-bg-color: #000000;
--rf-soft-bg-color: #282828;
--rf-node-bg-color: #202020;
--rf-node-soft-bg-color: #313335;
--rf-node-font-weight: bold;
--rf-text-color: #adadad;
--rf-region-name: var(--rf-text-color);
--rf-custom-name: var(--rf-text-color);
--tooltip-bg: #202020;
--rf-bg-variant: "lines";
--rf-bg-gap: 32;
--rf-bg-size: 1;
--rf-bg-gap: 34;
--rf-snap-size: 17;
--rf-bg-pattern-color: #313131;
--rf-local-counter-font-weight: 700;
/* Additional node-specific overrides */
--rf-node-line-height: normal;
--rf-node-font-family: 'Oxygen', sans-serif;
--rf-tag-color: #fbbf24;
/* -- theme-specific variables -- */
--eve-effect-pulsar: #428bca;
--eve-effect-magnetar: #e06fdf;
--eve-effect-wolfRayet: #e28a0d;
@@ -39,13 +46,9 @@
--eve-wh-type-color-c13: #7986cb;
--eve-wh-type-color-drifter: #44aa82;
--rf-node-font-weight: bold;
--rf-node-line-height: normal;
--rf-node-font-family: 'Oxygen', sans-serif;
--rf-node-text-color: var(--pf-text-color);
--rf-node-local-counter: #5cb85c;
--rf-has-user-characters: #ffc75d;
--rf-tag-color: #fbbf24;
--rf-has-user-characters: #5cb85c;
--window-corner: #72716f;
--eve-solar-system-status-home: #{$homeAlpha};
--eve-solar-system-status-color-home: #{$homeBase};
}

View File

@@ -1,5 +1,3 @@
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
import { useMemo } from 'react';
import { WindowManager } from '@/hooks/Mapper/components/ui-kit/WindowManager';
import { DEFAULT_WIDGETS } from '@/hooks/Mapper/components/mapInterface/constants.tsx';

View File

@@ -103,13 +103,3 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
label: 'Kills',
},
];
export function getWidgetsCheckboxesProps(detailedKillsDisabled: boolean): WidgetsCheckboxesType {
return filterOutKills(WIDGETS_CHECKBOXES_PROPS, detailedKillsDisabled);
}
function filterOutKills<T extends { id: WidgetsIds }>(items: T[], shouldFilter: boolean) {
if (!shouldFilter) return items;
return items.filter((w) => w.id !== WidgetsIds.kills);
}

View File

@@ -1,13 +1,3 @@
.VirtualScroller {
height: 100% !important;
}
.CharacterRow {
//border-left-width: 1px;
&.CardBorderLeftIsOwn {
border-left-color: rgb(251 146 60 / 1)
}
}

View File

@@ -1,130 +1,71 @@
import { useCallback, useMemo, useRef } from 'react';
import { useMemo } from 'react';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import clsx from 'clsx';
import classes from './LocalCharacters.module.scss';
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
import { CharacterCard, LayoutEventBlocker, WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters.ts';
import useLocalStorageState from 'use-local-storage-state';
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters';
import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
type CharItemProps = {
compact: boolean;
} & CharacterTypeRaw &
WithIsOwnCharacter;
const useItemTemplate = () => {
const {
data: { presentCharacters },
} = useMapRootState();
return useCallback(
(char: CharItemProps, options: VirtualScrollerTemplateOptions) => {
return (
<div
className={clsx(classes.CharacterRow, 'w-full box-border', {
'surface-hover': options.odd,
['border-b border-gray-600 border-opacity-20']: !options.last,
['bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10']: char.online,
})}
style={{ height: options.props.itemSize + 'px' }}
>
<CharacterCard showShipName {...char} />
</div>
);
},
// eslint-disable-next-line
[presentCharacters],
);
};
type WindowLocalSettingsType = {
compact: boolean;
showOffline: boolean;
version: number;
};
const STORED_DEFAULT_VALUES: WindowLocalSettingsType = {
compact: true,
showOffline: false,
version: 0,
};
import { UserPermission } from '@/hooks/Mapper/types/permissions';
import { LocalCharactersList } from './components/LocalCharactersList';
import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters';
import { useLocalCharacterWidgetSettings } from './hooks/useLocalWidgetSettings';
import { LocalCharactersHeader } from './components/LocalCharactersHeader';
import classes from './LocalCharacters.module.scss';
import clsx from 'clsx';
export const LocalCharacters = () => {
const {
data: { characters, userCharacters, selectedSystems, presentCharacters },
data: { characters, userCharacters, selectedSystems },
} = useMapRootState();
const [settings, setSettings] = useLocalStorageState<WindowLocalSettingsType>('window:local:settings', {
defaultValue: STORED_DEFAULT_VALUES,
});
const [settings, setSettings] = useLocalCharacterWidgetSettings();
const [systemId] = selectedSystems;
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
const showOffline = useMemo(
() => !restrictOfflineShowing || isAdminOrManager,
[isAdminOrManager, restrictOfflineShowing],
);
const itemTemplate = useItemTemplate();
const sorted = useMemo(() => {
const sorted = characters
const filtered = characters
.filter(x => x.location?.solar_system_id?.toString() === systemId)
.map(x => ({ ...x, isOwn: userCharacters.includes(x.eve_id), compact: settings.compact }))
.map(x => ({
...x,
isOwn: userCharacters.includes(x.eve_id),
compact: settings.compact,
showShipName: settings.showShipName,
}))
.sort(sortCharacters);
if (!showOffline || !settings.showOffline) {
return sorted.filter(c => c.online);
return filtered.filter(c => c.online);
}
return sorted;
// eslint-disable-next-line
}, [showOffline, characters, settings.showOffline, settings.compact, systemId, userCharacters, presentCharacters]);
return filtered;
}, [
characters,
systemId,
userCharacters,
settings.compact,
settings.showOffline,
settings.showShipName,
showOffline,
]);
const isNobodyHere = sorted.length === 0;
const isNotSelectedSystem = selectedSystems.length !== 1;
const showList = sorted.length > 0 && selectedSystems.length === 1;
const ref = useRef<HTMLDivElement>(null);
const compact = useMaxWidth(ref, 145);
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
return (
<Widget
label={
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
<span className="select-none">Local{showList ? ` [${sorted.length}]` : ''}</span>
<LayoutEventBlocker className="flex items-center gap-2">
{showOffline && (
<WdTooltipWrapper content="Show offline characters in system">
<WdCheckbox
size="xs"
labelSide="left"
label={compact ? '' : 'Show offline'}
value={settings.showOffline}
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
onChange={() => setSettings(() => ({ ...settings, showOffline: !settings.showOffline }))}
/>
</WdTooltipWrapper>
)}
<span
className={clsx('w-4 h-4 cursor-pointer', {
['hero-bars-2']: settings.compact,
['hero-bars-3']: !settings.compact,
})}
onClick={() => setSettings(() => ({ ...settings, compact: !settings.compact }))}
></span>
</LayoutEventBlocker>
</div>
<LocalCharactersHeader
sortedCount={sorted.length}
showList={showList}
showOffline={showOffline}
settings={settings}
setSettings={setSettings}
/>
}
>
{isNotSelectedSystem && (
@@ -132,23 +73,20 @@ export const LocalCharacters = () => {
System is not selected
</div>
)}
{isNobodyHere && !isNotSelectedSystem && (
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
Nobody here
</div>
)}
{showList && (
<VirtualScroller
<LocalCharactersList
items={sorted}
itemSize={settings.compact ? 26 : 41}
itemTemplate={itemTemplate}
className={clsx(
classes.VirtualScroller,
containerClassName={clsx(
'w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none',
classes.VirtualScroller,
)}
autoSize={false}
/>
)}
</Widget>

View File

@@ -0,0 +1,76 @@
import React, { useRef } from 'react';
import clsx from 'clsx';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { LayoutEventBlocker, TooltipPosition, WdCheckbox, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
interface LocalCharactersHeaderProps {
sortedCount: number;
showList: boolean;
showOffline: boolean;
settings: {
compact: boolean;
showOffline: boolean;
showShipName: boolean;
};
setSettings: (fn: (prev: any) => any) => void;
}
export const LocalCharactersHeader: React.FC<LocalCharactersHeaderProps> = ({
sortedCount,
showList,
showOffline,
settings,
setSettings,
}) => {
const headerRef = useRef<HTMLDivElement>(null);
const compactOffline = useMaxWidth(headerRef, 145);
const compactShipName = useMaxWidth(headerRef, 195);
return (
<div className="flex w-full items-center text-xs justify-between" ref={headerRef}>
<div className="flex-shrink-0 select-none mr-2">Local{showList ? ` [${sortedCount}]` : ''}</div>
<LayoutEventBlocker className="flex items-center gap-2 justify-end">
<div className="flex items-center gap-2">
{showOffline && (
<WdTooltipWrapper content="Show offline characters in system" position={TooltipPosition.top}>
<WdCheckbox
size="xs"
labelSide="left"
label={compactOffline ? '' : 'Offline'}
value={settings.showOffline}
onChange={() => setSettings((prev: any) => ({ ...prev, showOffline: !prev.showOffline }))}
classNameLabel={clsx('whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300', {
truncate: compactOffline,
})}
/>
</WdTooltipWrapper>
)}
{settings.compact && (
<WdTooltipWrapper content="Show ship name in compact rows" position={TooltipPosition.top}>
<WdCheckbox
size="xs"
labelSide="left"
label={compactShipName ? '' : 'Ship name'}
value={settings.showShipName}
onChange={() => setSettings((prev: any) => ({ ...prev, showShipName: !prev.showShipName }))}
classNameLabel={clsx('whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300', {
truncate: compactShipName,
})}
/>
</WdTooltipWrapper>
)}
</div>
<WdTooltipWrapper content="Enable compact mode" position={TooltipPosition.top}>
<span
className={clsx('w-4 h-4 min-w-[1rem] cursor-pointer', {
'hero-bars-2': settings.compact,
'hero-bars-3': !settings.compact,
})}
onClick={() => setSettings((prev: any) => ({ ...prev, compact: !prev.compact }))}
/>
</WdTooltipWrapper>
</LayoutEventBlocker>
</div>
);
};

View File

@@ -0,0 +1,6 @@
.CharacterRow {
&.CardBorderLeftIsOwn {
border-left-color: rgb(251 146 60 / 1)
}
}

View File

@@ -0,0 +1,27 @@
import classes from './LocalCharactersItemTemplate.module.scss';
import clsx from 'clsx';
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
import { CharItemProps } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/components';
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
export type LocalCharactersItemTemplateProps = { showShipName: boolean } & CharItemProps &
VirtualScrollerTemplateOptions;
export const LocalCharactersItemTemplate = ({ showShipName, ...options }: LocalCharactersItemTemplateProps) => {
return (
<div
className={clsx(
classes.CharacterRow,
'box-border flex items-center w-full whitespace-nowrap overflow-hidden text-ellipsis min-w-[0px]',
{
'surface-hover': options.odd,
'border-b border-gray-600 border-opacity-20': !options.last,
'bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10': options.online,
},
)}
style={{ height: `${options.props.itemSize}px` }}
>
<CharacterCard showShipName={showShipName} {...options} />
</div>
);
};

View File

@@ -0,0 +1 @@
export * from './LocalCharactersItemTemplate.tsx';

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import clsx from 'clsx';
import { CharItemProps } from './types';
export type LocalCharactersListProps = {
items: Array<CharItemProps>;
itemSize: number;
itemTemplate: (char: CharItemProps, options: VirtualScrollerTemplateOptions) => React.ReactNode;
containerClassName?: string;
style?: React.CSSProperties;
autoSize?: boolean;
};
export const LocalCharactersList = ({
items,
itemSize,
itemTemplate,
containerClassName,
style = {},
autoSize = false,
}: LocalCharactersListProps) => {
const computedHeight = autoSize ? `${Math.max(items.length, 1) * itemSize}px` : style.height || '100%';
const localStyle: React.CSSProperties = {
...style,
height: computedHeight,
width: '100%',
boxSizing: 'border-box',
overflowX: 'hidden',
};
return (
<VirtualScroller
items={items}
itemSize={itemSize}
orientation="vertical"
className={clsx('w-full h-full', containerClassName)}
itemTemplate={itemTemplate}
autoSize={autoSize}
style={localStyle}
/>
);
};

View File

@@ -0,0 +1,3 @@
export * from './LocalCharactersItemTemplate';
export * from './LocalCharactersList';
export * from './types';

View File

@@ -0,0 +1,6 @@
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
export type CharItemProps = {
compact: boolean;
} & CharacterTypeRaw &
WithIsOwnCharacter;

View File

@@ -0,0 +1,12 @@
import { useCallback } from 'react';
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import { CharItemProps, LocalCharactersItemTemplate } from '../components';
export function useLocalCharactersItemTemplate(showShipName: boolean) {
return useCallback(
(char: CharItemProps, options: VirtualScrollerTemplateOptions) => (
<LocalCharactersItemTemplate {...char} {...options} showShipName={showShipName} />
),
[showShipName],
);
}

View File

@@ -0,0 +1,21 @@
import useLocalStorageState from 'use-local-storage-state';
export interface LocalCharacterWidgetSettings {
compact: boolean;
showOffline: boolean;
version: number;
showShipName: boolean;
}
export const LOCAL_CHARACTER_WIDGET_DEFAULT: LocalCharacterWidgetSettings = {
compact: true,
showOffline: false,
version: 0,
showShipName: false,
};
export function useLocalCharacterWidgetSettings() {
return useLocalStorageState<LocalCharacterWidgetSettings>('kills:widget:settings', {
defaultValue: LOCAL_CHARACTER_WIDGET_DEFAULT,
});
}

View File

@@ -1,7 +1,7 @@
import classes from './RoutesList.module.scss';
import { Route, SystemStaticInfoShort } from '@/hooks/Mapper/types/routes.ts';
import clsx from 'clsx';
import { SystemViewStandalone, WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
import { SystemViewStandalone, TooltipPosition, WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
import { getBackgroundClass, getShapeClass } from '@/hooks/Mapper/components/map/helpers';
import { MouseEvent, useCallback, useRef, useState } from 'react';
import { Commands } from '@/hooks/Mapper/types';
@@ -46,9 +46,11 @@ export const RouteSystem = ({
<>
<WdTooltip
ref={tooltipRef}
position={TooltipPosition.top}
// targetSelector={`.tooltip-route-sys_${destination}_${solar_system_id}`}
content={() => (
<SystemViewStandalone
className="mx-[4px]"
security={security}
system_class={system_class}
class_title={class_title}
@@ -63,8 +65,8 @@ export const RouteSystem = ({
tooltipRef.current?.show(e);
onMouseEnter?.(solar_system_id);
}}
onMouseLeave={e => {
tooltipRef.current?.hide(e);
onMouseLeave={() => {
tooltipRef.current?.hide();
onMouseLeave?.();
}}
onContextMenu={handleContext}

View File

@@ -184,7 +184,7 @@ export const RoutesWidgetComp = () => {
}, [data, update]);
const ref = useRef<HTMLDivElement>(null);
const compact = useMaxWidth(ref, 155);
const compact = useMaxWidth(ref, 170);
const [openAddSystem, setOpenAddSystem] = useState<boolean>(false);
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
@@ -217,14 +217,14 @@ export const RoutesWidgetComp = () => {
}}
/>
<WdTooltipWrapper content="Show shortest route">
<WdTooltipWrapper content="Show shortest route" position={TooltipPosition.top}>
<WdCheckbox
size="xs"
labelSide="left"
label={compact ? '' : 'Show shortest'}
value={!isSecure}
onChange={handleSecureChange}
classNameLabel={clsx('text-red-400')}
classNameLabel="text-red-400 whitespace-nowrap"
/>
</WdTooltipWrapper>
<WdImgButton

View File

@@ -6,15 +6,16 @@ import { KillsHeader } from './components/SystemKillsHeader';
import { useKillsWidgetSettings } from './hooks/useKillsWidgetSettings';
import { useSystemKills } from './hooks/useSystemKills';
import { KillsSettingsDialog } from './components/SystemKillsSettingsDialog';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
export const SystemKills: React.FC = () => {
export const SystemKills: React.FC = React.memo(() => {
const {
data: { selectedSystems, systems },
data: { selectedSystems, systems, isSubscriptionActive },
outCommand,
} = useMapRootState();
const [systemId] = selectedSystems || [];
const [settingsDialogVisible, setSettingsDialogVisible] = useState(false);
const systemNameMap = useMemo(() => {
@@ -25,6 +26,16 @@ export const SystemKills: React.FC = () => {
return map;
}, [systems]);
const systemBySolarSystemId = useMemo(() => {
const map: Record<number, SolarSystemRawType> = {};
systems.forEach(sys => {
if (sys.system_static_info?.solar_system_id != null) {
map[sys.system_static_info.solar_system_id] = sys;
}
});
return map;
}, [systems]);
const [settings] = useKillsWidgetSettings();
const visible = settings.showAll;
@@ -37,49 +48,62 @@ export const SystemKills: React.FC = () => {
const isNothingSelected = !systemId && !visible;
const showLoading = isLoading && kills.length === 0;
const filteredKills = useMemo(() => {
if (!settings.whOnly || !visible) return kills;
return kills.filter(kill => {
const system = systemBySolarSystemId[kill.solar_system_id];
if (!system) {
console.warn(`System with id ${kill.solar_system_id} not found.`);
return false;
}
return isWormholeSpace(system.system_static_info.system_class);
});
}, [kills, settings.whOnly, systemBySolarSystemId, visible]);
return (
<div className="h-full flex flex-col min-h-0">
<div className="flex flex-col flex-1 min-h-0">
<Widget label={<KillsHeader systemId={systemId} onOpenSettings={() => setSettingsDialogVisible(true)} />}>
{isNothingSelected && (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
No system selected (or toggle Show all systems)
{!isSubscriptionActive ? (
<div className="w-full h-full flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
Kills available with &#39;Active&#39; map subscription only (contact map administrators)
</span>
</div>
)}
{!isNothingSelected && showLoading && (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
Loading Kills...
) : isNothingSelected ? (
<div className="w-full h-full flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
No system selected (or toggle Show all systems)
</span>
</div>
)}
{!isNothingSelected && !showLoading && error && (
<div className="w-full h-full flex justify-center items-center select-none text-center text-red-400 text-sm">
{error}
) : showLoading ? (
<div className="w-full h-full flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">Loading Kills...</span>
</div>
)}
{!isNothingSelected && !showLoading && !error && (!kills || kills.length === 0) && (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
No kills found
) : error ? (
<div className="w-full h-full flex items-center justify-center">
<span className="select-none text-center text-red-400 text-sm">{error}</span>
</div>
)}
{!isNothingSelected && !showLoading && !error && (
<div className="flex-1 flex flex-col overflow-y-auto">
) : !filteredKills || filteredKills.length === 0 ? (
<div className="w-full h-full flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">No kills found</span>
</div>
) : (
<div className="w-full h-full" style={{ height: '100%' }}>
<SystemKillsContent
key={settings.compact ? 'compact' : 'normal'}
kills={kills}
kills={filteredKills}
systemNameMap={systemNameMap}
compact={settings.compact}
onlyOneSystem={!visible}
timeRange={settings.timeRange}
/>
</div>
)}
</Widget>
</div>
<KillsSettingsDialog visible={settingsDialogVisible} setVisible={setSettingsDialogVisible} />
{settingsDialogVisible && <KillsSettingsDialog visible setVisible={setSettingsDialogVisible} />}
</div>
);
};
});
SystemKills.displayName = 'SystemKills';

View File

@@ -1,16 +1,14 @@
.TableRowCompact {
height: 8px;
max-height: 8px;
font-size: 12px !important;
line-height: 8px;
.wrapper {
overflow-x: hidden;
box-sizing: border-box;
}
.Table {
font-size: 12px;
border-collapse: collapse;
.scrollerContent {
width: 100%;
box-sizing: border-box;
overflow-x: hidden;
}
.Tooltip {
white-space: pre-line;
line-height: 1.2rem;
.VirtualScroller {
height: 100% !important;
}

View File

@@ -1,50 +1,91 @@
import React, { useMemo } from 'react';
import React, { useMemo, useRef, useEffect, useState } from 'react';
import clsx from 'clsx';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { KillRow } from '../components/SystemKillsRow';
import { VirtualScroller } from 'primereact/virtualscroller';
import { useSystemKillsItemTemplate } from '../hooks/useSystemKillsItemTemplate';
import classes from './SystemKillsContent.module.scss';
interface SystemKillsContentProps {
export const ITEM_HEIGHT = 35;
export const CONTENT_MARGINS = 5;
export interface SystemKillsContentProps {
kills: DetailedKill[];
systemNameMap: Record<string, string>;
compact?: boolean;
onlyOneSystem?: boolean;
autoSize?: boolean;
timeRange?: number;
limit?: number;
}
export const SystemKillsContent: React.FC<SystemKillsContentProps> = ({
kills,
systemNameMap,
compact = false,
onlyOneSystem = false,
autoSize = false,
timeRange = 4,
limit,
}) => {
const sortedKills = useMemo(() => {
return [...kills].sort((a, b) => {
const timeA = a.kill_time ? new Date(a.kill_time).getTime() : 0;
const timeB = b.kill_time ? new Date(b.kill_time).getTime() : 0;
return timeB - timeA;
});
}, [kills]);
const processedKills = useMemo(() => {
const sortedKills = kills
.filter(k => k.kill_time)
.sort((a, b) => new Date(b.kill_time!).getTime() - new Date(a.kill_time!).getTime());
if (limit !== undefined) {
return sortedKills.slice(0, limit);
} else {
const now = Date.now();
const cutoff = now - timeRange * 60 * 60 * 1000;
return sortedKills.filter(k => new Date(k.kill_time!).getTime() >= cutoff);
}
}, [kills, timeRange, limit]);
const computedHeight = autoSize ? Math.max(processedKills.length, 1) * ITEM_HEIGHT + CONTENT_MARGINS : undefined;
const containerRef = useRef<HTMLDivElement>(null);
const scrollerRef = useRef<VirtualScroller | null>(null);
const [containerHeight, setContainerHeight] = useState<number>(0);
useEffect(() => {
if (!autoSize && containerRef.current) {
const measure = () => {
const newHeight = containerRef.current?.clientHeight || 0;
setContainerHeight(newHeight);
};
measure();
const observer = new ResizeObserver(measure);
observer.observe(containerRef.current);
window.addEventListener('resize', measure);
return () => {
observer.disconnect();
window.removeEventListener('resize', measure);
};
}
}, [autoSize]);
const itemTemplate = useSystemKillsItemTemplate(systemNameMap, onlyOneSystem);
const scrollerHeight = autoSize ? `${computedHeight}px` : containerHeight ? `${containerHeight}px` : '100%';
return (
<div
className={clsx(
'flex flex-col w-full text-stone-200 text-xs transition-all duration-300',
compact ? 'p-1' : 'p-1',
)}
>
{sortedKills.map(kill => {
const systemIdStr = String(kill.solar_system_id);
const systemName = systemNameMap[systemIdStr] || `System ${systemIdStr}`;
return (
<KillRow
key={kill.killmail_id}
killDetails={kill}
systemName={systemName}
isCompact={compact}
onlyOneSystem={onlyOneSystem}
/>
);
})}
<div ref={autoSize ? undefined : containerRef} className={clsx('w-full h-full', classes.wrapper)}>
<VirtualScroller
ref={autoSize ? undefined : scrollerRef}
items={processedKills}
itemSize={ITEM_HEIGHT}
itemTemplate={itemTemplate}
autoSize={autoSize}
scrollWidth="100%"
style={{ height: scrollerHeight }}
className={clsx('w-full h-full custom-scrollbar select-none overflow-x-hidden overflow-y-auto', {
[classes.VirtualScroller]: !autoSize,
})}
pt={{
content: {
className: classes.scrollerContent,
},
}}
/>
</div>
);
};

View File

@@ -1,52 +0,0 @@
import React from 'react';
import clsx from 'clsx';
import { zkillLink } from '../helpers';
import classes from './SystemKillRow.module.scss';
interface AttackerRowSubInfoProps {
finalBlowCharId: number | null | undefined;
finalBlowCharName?: string;
attackerPortraitUrl: string | null;
finalBlowCorpId: number | null | undefined;
finalBlowCorpName?: string;
attackerCorpLogoUrl: string | null;
finalBlowAllianceId: number | null | undefined;
finalBlowAllianceName?: string;
attackerAllianceLogoUrl: string | null;
containerHeight?: number;
}
export const AttackerRowSubInfo: React.FC<AttackerRowSubInfoProps> = ({
finalBlowCharId = 0,
finalBlowCharName,
attackerPortraitUrl,
containerHeight = 8,
}) => {
if (!attackerPortraitUrl || finalBlowCharId === null || finalBlowCharId <= 0) {
return null;
}
const containerClass = `h-${containerHeight}`;
return (
<div className={clsx('flex items-start gap-1', containerClass)}>
<div className="relative shrink-0 w-auto h-full overflow-hidden">
<a
href={zkillLink('character', finalBlowCharId)}
target="_blank"
rel="noopener noreferrer"
className="block h-full"
>
<img
src={attackerPortraitUrl}
alt={finalBlowCharName || 'AttackerPortrait'}
className={clsx(classes.killRowImage, 'h-full w-auto object-contain')}
/>
</a>
</div>
</div>
);
};

View File

@@ -1,197 +0,0 @@
// FullKillRow.tsx
import React from 'react';
import clsx from 'clsx';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import {
formatISK,
formatTimeMixed,
zkillLink,
getAttackerSubscript,
buildVictimImageUrls,
buildAttackerImageUrls,
getPrimaryLogoAndTooltip,
getAttackerPrimaryImageAndTooltip,
} from '../helpers';
import { VictimRowSubInfo } from './VictimRowSubInfo';
import { WdTooltipWrapper } from '../../../../ui-kit/WdTooltipWrapper';
import classes from './SystemKillRow.module.scss';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
export interface FullKillRowProps {
killDetails: DetailedKill;
systemName: string;
onlyOneSystem: boolean;
}
export const FullKillRow: React.FC<FullKillRowProps> = ({ killDetails, systemName, onlyOneSystem }) => {
const {
killmail_id = 0,
victim_char_name = '',
victim_alliance_ticker = '',
victim_corp_ticker = '',
victim_ship_name = '',
victim_char_id = 0,
victim_corp_id = 0,
victim_alliance_id = 0,
victim_ship_type_id = 0,
victim_corp_name = '',
victim_alliance_name = '',
final_blow_char_id = 0,
final_blow_char_name = '',
final_blow_alliance_ticker = '',
final_blow_corp_ticker = '',
final_blow_corp_name = '',
final_blow_alliance_name = '',
final_blow_corp_id = 0,
final_blow_alliance_id = 0,
final_blow_ship_name = '',
final_blow_ship_type_id = 0,
total_value = 0,
kill_time = '',
} = killDetails || {};
const attackerIsNpc = final_blow_char_id === 0;
const victimAffiliation = victim_alliance_ticker || victim_corp_ticker;
const attackerAffiliation = attackerIsNpc ? '' : final_blow_alliance_ticker || final_blow_corp_ticker || '';
const killValueFormatted = total_value !== null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
const { victimPortraitUrl, victimCorpLogoUrl, victimAllianceLogoUrl } = buildVictimImageUrls({
victim_char_id,
victim_ship_type_id,
victim_corp_id,
victim_alliance_id,
});
const { attackerPortraitUrl, attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
final_blow_char_id,
final_blow_corp_id,
final_blow_alliance_id,
});
const { url: victimPrimaryImageUrl, tooltip: victimPrimaryTooltip } = getPrimaryLogoAndTooltip(
victimAllianceLogoUrl,
victimCorpLogoUrl,
victim_alliance_name,
victim_corp_name,
'Victim',
);
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } = getAttackerPrimaryImageAndTooltip(
attackerIsNpc,
attackerAllianceLogoUrl,
attackerCorpLogoUrl,
final_blow_alliance_name,
final_blow_corp_name,
final_blow_ship_type_id || 0,
);
const attackerSubscript = getAttackerSubscript(killDetails);
return (
<div className={clsx(classes.killRowContainer, 'h-18 w-full justify-between items-start text-sm py-[4px]')}>
{/* ---------------- Victim Side ---------------- */}
<div className="flex items-start gap-1 min-w-0 h-full">
{/* Victim top-level logo (corp or alliance), with tooltip */}
{victimPrimaryImageUrl && (
<WdTooltipWrapper content={victimPrimaryTooltip} position={TooltipPosition.top}>
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
<a
href={zkillLink('kill', killmail_id)}
target="_blank"
rel="noopener noreferrer"
className="block w-full h-full"
>
<img
src={victimPrimaryImageUrl}
alt="VictimPrimaryLogo"
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
</a>
</div>
</WdTooltipWrapper>
)}
<VictimRowSubInfo
victimCharName={victim_char_name}
victimCharacterId={victim_char_id}
victimPortraitUrl={victimPortraitUrl}
/>
<div className="flex flex-col text-stone-200 leading-4 min-w-0 overflow-hidden">
<div className="truncate">
<span className="font-semibold">{victim_char_name}</span>
{victimAffiliation && <span className="ml-1 text-stone-400">/ {victimAffiliation}</span>}
</div>
<div className="truncate text-stone-300">
{victim_ship_name}
{killValueFormatted && (
<>
<span className="ml-1 text-stone-400">/</span>
<span className="ml-1 text-green-400">{killValueFormatted}</span>
</>
)}
</div>
<div className="truncate text-stone-400">{!onlyOneSystem && systemName && <span>{systemName}</span>}</div>
</div>
</div>
<div className="flex items-start gap-1 min-w-0 h-full">
<div className="flex flex-col items-end leading-4 min-w-0 overflow-hidden text-right">
{!attackerIsNpc && (
<div className="truncate font-semibold">
{final_blow_char_name}
{attackerAffiliation && <span className="ml-1 text-stone-400">/ {attackerAffiliation}</span>}
</div>
)}
{!attackerIsNpc && final_blow_ship_name && (
<div className="truncate text-stone-300">{final_blow_ship_name}</div>
)}
<div className="truncate text-red-400">{killTimeAgo}</div>
</div>
{!attackerIsNpc && attackerPortraitUrl && final_blow_char_id !== null && final_blow_char_id > 0 && (
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
<a
href={zkillLink('character', final_blow_char_id)}
target="_blank"
rel="noopener noreferrer"
className="block w-full h-full"
>
<img
src={attackerPortraitUrl}
alt="AttackerPortrait"
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
</a>
</div>
)}
{attackerPrimaryImageUrl && (
<WdTooltipWrapper content={attackerPrimaryTooltip} position={TooltipPosition.top}>
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
<a
href={zkillLink('kill', killmail_id)}
target="_blank"
rel="noopener noreferrer"
className="block w-full h-full"
>
<img
src={attackerPrimaryImageUrl}
alt={attackerIsNpc ? 'NpcShip' : 'AttackerPrimaryLogo'}
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
{attackerSubscript && (
<span className={clsx(attackerSubscript.cssClass, classes.attackerCountLabel)}>
{attackerSubscript.label}
</span>
)}
</a>
</div>
</WdTooltipWrapper>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,20 @@
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import { KillRow } from './SystemKillsRow';
import clsx from 'clsx';
export function KillItemTemplate(
systemNameMap: Record<string, string>,
onlyOneSystem: boolean,
kill: DetailedKill,
options: VirtualScrollerTemplateOptions,
) {
const systemIdStr = String(kill.solar_system_id);
const systemName = systemNameMap[systemIdStr] || `System ${systemIdStr}`;
return (
<div style={{ height: `${options.props.itemSize}px` }} className={clsx({ 'bg-gray-900': options.odd })}>
<KillRow killDetails={kill} systemName={systemName} onlyOneSystem={onlyOneSystem} />
</div>
);
}

View File

@@ -11,8 +11,8 @@ import {
getPrimaryLogoAndTooltip,
getAttackerPrimaryImageAndTooltip,
} from '../helpers';
import { WdTooltipWrapper } from '../../../../ui-kit/WdTooltipWrapper';
import classes from './SystemKillRow.module.scss';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import classes from './KillRowDetail.module.scss';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
export interface CompactKillRowProps {
@@ -21,10 +21,10 @@ export interface CompactKillRowProps {
onlyOneSystem: boolean;
}
export const CompactKillRow: React.FC<CompactKillRowProps> = ({ killDetails, systemName, onlyOneSystem }) => {
export const KillRowDetail: React.FC<CompactKillRowProps> = ({ killDetails, systemName, onlyOneSystem }) => {
const {
killmail_id = 0,
// Victim data
victim_char_name = 'Unknown Pilot',
victim_alliance_ticker = '',
victim_corp_ticker = '',
@@ -35,7 +35,7 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({ killDetails, sys
victim_corp_id = 0,
victim_alliance_id = 0,
victim_ship_type_id = 0,
// Attacker data
final_blow_char_id = 0,
final_blow_char_name = '',
final_blow_alliance_ticker = '',
@@ -45,25 +45,27 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({ killDetails, sys
final_blow_corp_id = 0,
final_blow_corp_name = '',
final_blow_ship_type_id = 0,
kill_time = '',
total_value = 0,
} = killDetails || {};
const attackerIsNpc = final_blow_char_id === 0;
// Define victim affiliation ticker.
const victimAffiliationTicker = victim_alliance_ticker || victim_corp_ticker || 'No Ticker';
const killValueFormatted = total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
const attackerName = attackerIsNpc ? '' : final_blow_char_name;
const attackerTicker = attackerIsNpc ? '' : final_blow_alliance_ticker || final_blow_corp_ticker || '';
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
const attackerSubscript = getAttackerSubscript(killDetails);
const { victimCorpLogoUrl, victimAllianceLogoUrl } = buildVictimImageUrls({
const { victimCorpLogoUrl, victimAllianceLogoUrl, victimShipUrl } = buildVictimImageUrls({
victim_char_id,
victim_ship_type_id,
victim_corp_id,
victim_alliance_id,
});
const { attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
final_blow_char_id,
final_blow_corp_id,
@@ -84,9 +86,15 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({ killDetails, sys
attackerCorpLogoUrl,
final_blow_alliance_name,
final_blow_corp_name,
final_blow_ship_type_id || 0,
final_blow_ship_type_id,
);
// Define attackerTicker to use the alliance ticker if available, otherwise the corp ticker.
const attackerTicker = attackerIsNpc ? '' : final_blow_alliance_ticker || final_blow_corp_ticker || '';
// For the attacker image link: if the attacker is not an NPC, link to the character page; otherwise, link to the kill page.
const attackerLink = attackerIsNpc ? zkillLink('kill', killmail_id) : zkillLink('character', final_blow_char_id);
return (
<div
className={clsx(
@@ -94,24 +102,42 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({ killDetails, sys
'text-xs whitespace-nowrap overflow-hidden leading-none',
)}
>
{victimPrimaryLogoUrl && (
<WdTooltipWrapper content={victimPrimaryTooltip} position={TooltipPosition.top}>
<a
href={zkillLink('kill', killmail_id)}
target="_blank"
rel="noopener noreferrer"
className="relative shrink-0 w-8 h-8 overflow-hidden"
>
<img
src={victimPrimaryLogoUrl}
alt="VictimPrimaryLogo"
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
</a>
</WdTooltipWrapper>
)}
<div className="flex flex-col ml-2 min-w-0 overflow-hidden leading-[1rem]">
{/* Victim Section */}
<div className="flex items-center gap-1">
{victimShipUrl && (
<div className="relative shrink-0 w-8 h-8 overflow-hidden">
<a
href={zkillLink('kill', killmail_id)}
target="_blank"
rel="noopener noreferrer"
className="block w-full h-full"
>
<img
src={victimShipUrl}
alt="Victim Ship"
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
</a>
</div>
)}
{victimPrimaryLogoUrl && (
<WdTooltipWrapper content={victimPrimaryTooltip} position={TooltipPosition.top}>
<a
href={zkillLink('kill', killmail_id)}
target="_blank"
rel="noopener noreferrer"
className="relative block shrink-0 w-8 h-8 overflow-hidden"
>
<img
src={victimPrimaryLogoUrl}
alt="Victim Primary Logo"
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
</a>
</WdTooltipWrapper>
)}
</div>
<div className="flex flex-col ml-2 flex-1 min-w-0 overflow-hidden leading-[1rem]">
<div className="truncate text-stone-200">
{victim_char_name}
<span className="text-stone-400"> / {victimAffiliationTicker}</span>
@@ -126,13 +152,12 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({ killDetails, sys
)}
</div>
</div>
<div className="flex items-center ml-auto gap-2">
<div className="flex flex-col items-end min-w-0 overflow-hidden text-right leading-[1rem]">
{!attackerIsNpc && (attackerName || attackerTicker) && (
<div className="flex flex-col items-end flex-1 min-w-0 overflow-hidden text-right leading-[1rem]">
{!attackerIsNpc && (final_blow_char_name || attackerTicker) && (
<div className="truncate text-stone-200">
{attackerName}
{attackerTicker && <span className="ml-1 text-stone-400">/ {attackerTicker}</span>}
{final_blow_char_name}
{!attackerIsNpc && attackerTicker && <span className="ml-1 text-stone-400">/ {attackerTicker}</span>}
</div>
)}
<div className="truncate text-stone-400">
@@ -145,18 +170,17 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({ killDetails, sys
)}
</div>
</div>
{attackerPrimaryImageUrl && (
<WdTooltipWrapper content={attackerPrimaryTooltip} position={TooltipPosition.top}>
<a
href={zkillLink('kill', killmail_id)}
href={attackerLink}
target="_blank"
rel="noopener noreferrer"
className="relative shrink-0 w-8 h-8 overflow-hidden"
className="relative block shrink-0 w-8 h-8 overflow-hidden"
>
<img
src={attackerPrimaryImageUrl}
alt={attackerIsNpc ? 'NpcShip' : 'AttackerPrimaryLogo'}
alt={attackerIsNpc ? 'NPC Ship' : 'Attacker Primary Logo'}
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
{attackerSubscript && (
@@ -166,7 +190,6 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({ killDetails, sys
attackerSubscript.cssClass,
'text-[0.6rem] leading-none px-[2px]',
)}
style={{ bottom: 0, right: 0 }}
>
{attackerSubscript.label}
</span>

View File

@@ -1,20 +1,22 @@
import React from 'react';
import React, { useRef } from 'react';
import {
LayoutEventBlocker,
SystemView,
TooltipPosition,
WdCheckbox,
WdImgButton,
TooltipPosition,
SystemView,
WdTooltipWrapper,
} from '@/hooks/Mapper/components/ui-kit';
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
import { PrimeIcons } from 'primereact/api';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
interface KillsWidgetHeaderProps {
interface KillsHeaderProps {
systemId?: string;
onOpenSettings: () => void;
}
export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpenSettings }) => {
export const KillsHeader: React.FC<KillsHeaderProps> = ({ systemId, onOpenSettings }) => {
const [settings, setSettings] = useKillsWidgetSettings();
const { showAll } = settings;
@@ -22,8 +24,11 @@ export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpen
setSettings(prev => ({ ...prev, showAll: !prev.showAll }));
};
const headerRef = useRef<HTMLDivElement>(null);
const compact = useMaxWidth(headerRef, 150);
return (
<div className="flex justify-between items-center text-xs w-full">
<div className="flex w-full items-center justify-between text-xs" ref={headerRef}>
<div className="flex items-center gap-1">
<div className="text-stone-400">
Kills
@@ -32,24 +37,28 @@ export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpen
{systemId && !showAll && <SystemView systemId={systemId} className="select-none text-center" hideRegion />}
</div>
<LayoutEventBlocker className="flex gap-2 items-center">
<WdCheckbox
size="xs"
labelSide="left"
label="Show all systems"
value={showAll}
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
onChange={onToggleShowAllVisible}
/>
<LayoutEventBlocker className="flex items-center gap-2 justify-end">
<div className="flex items-center gap-2">
<WdTooltipWrapper content="Show all systems" position={TooltipPosition.top}>
<WdCheckbox
size="xs"
labelSide="left"
label={compact ? 'All' : 'Show all systems'}
value={showAll}
onChange={onToggleShowAllVisible}
classNameLabel="whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300"
/>
</WdTooltipWrapper>
<WdImgButton
className={PrimeIcons.SLIDERS_H}
onClick={onOpenSettings}
tooltip={{
content: 'Open Kills Settings',
position: TooltipPosition.left,
}}
/>
<WdImgButton
className={PrimeIcons.SLIDERS_H}
onClick={onOpenSettings}
tooltip={{
content: 'Open Kills Settings',
position: TooltipPosition.top,
}}
/>
</div>
</LayoutEventBlocker>
</div>
);

View File

@@ -1,24 +1,15 @@
import React from 'react';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { CompactKillRow } from './CompactKillRow';
import { FullKillRow } from './FullKillRow';
import { KillRowDetail } from './KillRowDetail.tsx';
export interface KillRowProps {
killDetails: DetailedKill;
systemName: string;
isCompact?: boolean;
onlyOneSystem?: boolean;
}
export const KillRow: React.FC<KillRowProps> = ({
killDetails,
systemName,
isCompact = false,
onlyOneSystem = false,
}) => {
if (isCompact) {
return <CompactKillRow killDetails={killDetails} systemName={systemName} onlyOneSystem={onlyOneSystem} />;
}
return <FullKillRow killDetails={killDetails} systemName={systemName} onlyOneSystem={onlyOneSystem} />;
const KillRowComponent: React.FC<KillRowProps> = ({ killDetails, systemName, onlyOneSystem = false }) => {
return <KillRowDetail killDetails={killDetails} systemName={systemName} onlyOneSystem={onlyOneSystem} />;
};
export const KillRow = React.memo(KillRowComponent);

View File

@@ -1,13 +1,14 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import { WdImgButton, SystemView, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { PrimeIcons } from 'primereact/api';
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
import {
AddSystemDialog,
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
import { SystemView, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
interface KillsSettingsDialogProps {
visible: boolean;
@@ -17,30 +18,39 @@ interface KillsSettingsDialogProps {
export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visible, setVisible }) => {
const [globalSettings, setGlobalSettings] = useKillsWidgetSettings();
const localRef = useRef({
compact: globalSettings.compact,
showAll: globalSettings.showAll,
whOnly: globalSettings.whOnly,
excludedSystems: globalSettings.excludedSystems || [],
timeRange: globalSettings.timeRange,
});
const [, forceRender] = useState(0);
const [addSystemDialogVisible, setAddSystemDialogVisible] = useState(false);
useEffect(() => {
if (visible) {
localRef.current = {
compact: globalSettings.compact,
showAll: globalSettings.showAll,
whOnly: globalSettings.whOnly,
excludedSystems: globalSettings.excludedSystems || [],
timeRange: globalSettings.timeRange,
};
forceRender(n => n + 1);
}
}, [visible, globalSettings]);
const handleCompactChange = useCallback((checked: boolean) => {
const handleWHChange = useCallback((checked: boolean) => {
localRef.current = {
...localRef.current,
compact: checked,
whOnly: checked,
};
forceRender(n => n + 1);
}, []);
const handleTimeRangeChange = useCallback((newTimeRange: number) => {
localRef.current = {
...localRef.current,
timeRange: newTimeRange,
};
forceRender(n => n + 1);
}, []);
@@ -78,6 +88,7 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
const localData = localRef.current;
const excluded = localData.excludedSystems || [];
const timeRangeOptions = [4, 12, 24];
return (
<Dialog header="Kills Settings" visible={visible} style={{ width: '440px' }} draggable={false} onHide={handleHide}>
@@ -85,15 +96,34 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
<div className="flex items-center gap-2">
<input
type="checkbox"
id="kills-compact-mode"
checked={localData.compact}
onChange={e => handleCompactChange(e.target.checked)}
id="kills-wormhole-only-mode"
checked={localData.whOnly}
onChange={e => handleWHChange(e.target.checked)}
/>
<label htmlFor="kills-compact-mode" className="cursor-pointer">
Use compact mode
<label htmlFor="kills-wormhole-only-mode" className="cursor-pointer">
Only show wormhole kills
</label>
</div>
<div className="flex flex-col gap-1">
<span className="text-sm">Time Range:</span>
<div className="flex flex-wrap gap-2">
{timeRangeOptions.map(option => (
<label key={option} className="cursor-pointer flex items-center gap-1">
<input
type="radio"
name="timeRange"
value={option}
checked={localData.timeRange === option}
onChange={() => handleTimeRangeChange(option)}
/>
<span className="text-sm">{option} Hours</span>
</label>
))}
</div>
</div>
{/* Excluded Systems */}
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<label className="text-sm text-stone-400">Excluded Systems</label>
@@ -106,8 +136,7 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
{excluded.length === 0 && <div className="text-stone-500 text-xs italic">No systems excluded.</div>}
{excluded.map(sysId => (
<div key={sysId} className="flex items-center justify-between border-b border-stone-600 py-1 px-1 text-xs">
<SystemView systemId={sysId.toString()} hideRegion compact />
<SystemView systemId={sysId.toString()} hideRegion />
<WdImgButton
className={PrimeIcons.TRASH}
onClick={() => handleRemoveSystem(sysId)}
@@ -117,13 +146,11 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
))}
</div>
{/* Apply + Close button row */}
<div className="flex gap-2 justify-end mt-4">
<Button onClick={handleApply} label="Apply" outlined size="small" />
</div>
</div>
{/* AddSystemDialog for picking new systems to exclude */}
<AddSystemDialog
title="Add system to kills exclude list"
visible={addSystemDialogVisible}

View File

@@ -1,40 +0,0 @@
// VictimSubRowInfo.tsx
import React from 'react';
import clsx from 'clsx';
import { zkillLink } from '../helpers';
import classes from './SystemKillRow.module.scss';
interface VictimRowSubInfoProps {
victimCharacterId: number | null;
victimPortraitUrl: string | null;
victimCharName?: string;
}
export const VictimRowSubInfo: React.FC<VictimRowSubInfoProps> = ({
victimCharacterId = 0,
victimPortraitUrl,
victimCharName,
}) => {
if (!victimPortraitUrl || victimCharacterId === null || victimCharacterId <= 0) {
return null;
}
return (
<div className="flex items-start gap-1 h-14">
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
<a
href={zkillLink('character', victimCharacterId)}
target="_blank"
rel="noopener noreferrer"
className="block w-full h-full"
>
<img
src={victimPortraitUrl}
alt={victimCharName || 'Victim Portrait'}
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
</a>
</div>
</div>
);
};

View File

@@ -96,7 +96,7 @@ export function getAttackerPrimaryImageAndTooltip(
corpUrl: string | null,
allianceName: string,
corpName: string,
finalBlowShipTypeId: number,
finalBlowShipTypeId: number | null,
npcFallback: string = 'NPC Attacker',
) {
if (isNpc) {

View File

@@ -2,17 +2,19 @@ import { useMemo, useCallback } from 'react';
import useLocalStorageState from 'use-local-storage-state';
export interface KillsWidgetSettings {
compact: boolean;
showAll: boolean;
whOnly: boolean;
excludedSystems: number[];
version: number;
timeRange: number;
}
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
compact: false,
showAll: false,
whOnly: true,
excludedSystems: [],
version: 0,
version: 2,
timeRange: 1,
};
function mergeWithDefaults(settings?: Partial<KillsWidgetSettings>): KillsWidgetSettings {

View File

@@ -38,9 +38,14 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
const [settings] = useKillsWidgetSettings();
const excludedSystems = settings.excludedSystems;
const visibleSystemIds = useMemo(() => {
return systems.map(s => s.id).filter(id => !excludedSystems.includes(Number(id)));
}, [systems, excludedSystems]);
// When showing all visible kills, filter out excluded systems;
// when showAllVisible is false, ignore the exclusion filter.
const effectiveSystemIds = useMemo(() => {
if (showAllVisible) {
return systems.map(s => s.id).filter(id => !excludedSystems.includes(Number(id)));
}
return systems.map(s => s.id);
}, [systems, excludedSystems, showAllVisible]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -80,7 +85,7 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
if (showAllVisible || forceFallback) {
eventType = OutCommand.getSystemsKills;
requestData = {
system_ids: visibleSystemIds,
system_ids: effectiveSystemIds,
since_hours: sinceHours,
};
} else if (systemId) {
@@ -101,13 +106,13 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
});
// Single system => `resp.kills`
if (resp.kills) {
if (resp?.kills) {
const arr = resp.kills as DetailedKill[];
const sid = systemId ?? 'unknown';
mergeKillsIntoGlobal({ [sid]: arr });
}
// multiple => `resp.systems_kills`
else if (resp.systems_kills) {
// multiple systems => `resp.systems_kills`
else if (resp?.systems_kills) {
mergeKillsIntoGlobal(resp.systems_kills as Record<string, DetailedKill[]>);
} else {
console.warn('[useSystemKills] Unexpected kills response =>', resp);
@@ -119,7 +124,7 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
setIsLoading(false);
}
},
[showAllVisible, systemId, outCommand, visibleSystemIds, sinceHours, mergeKillsIntoGlobal],
[showAllVisible, systemId, outCommand, effectiveSystemIds, sinceHours, mergeKillsIntoGlobal],
);
const debouncedFetchKills = useMemo(
@@ -133,15 +138,15 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
const finalKills = useMemo(() => {
if (showAllVisible) {
return visibleSystemIds.flatMap(sid => detailedKills[sid] ?? []);
return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []);
} else if (systemId) {
return detailedKills[systemId] ?? [];
} else if (didFallbackFetch.current) {
// if we already did a fallback, we may have data for multiple systems
return visibleSystemIds.flatMap(sid => detailedKills[sid] ?? []);
return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []);
}
return [];
}, [showAllVisible, systemId, didFallbackFetch, visibleSystemIds, detailedKills]);
}, [showAllVisible, systemId, effectiveSystemIds, detailedKills]);
const effectiveIsLoading = isLoading && finalKills.length === 0;
@@ -150,19 +155,19 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
didFallbackFetch.current = true;
// Cancel any queued debounced calls, then do the fallback.
debouncedFetchKills.cancel();
fetchKills(true); // forceFallback => fetch as though showAll
fetchKills(true); // forceFallback => fetch as though showAllVisible is true
}
}, [systemId, showAllVisible, debouncedFetchKills, fetchKills, didFallbackFetch]);
}, [systemId, showAllVisible, debouncedFetchKills, fetchKills]);
useEffect(() => {
if (visibleSystemIds.length === 0) return;
if (effectiveSystemIds.length === 0) return;
if (showAllVisible || systemId) {
debouncedFetchKills();
// Clean up the debounce on unmount or changes
return () => debouncedFetchKills.cancel();
}
}, [showAllVisible, systemId, visibleSystemIds, debouncedFetchKills]);
}, [showAllVisible, systemId, effectiveSystemIds, debouncedFetchKills]);
const refetch = useCallback(() => {
debouncedFetchKills.cancel();

View File

@@ -0,0 +1,13 @@
// useSystemKillsItemTemplate.tsx
import { useCallback } from 'react';
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { KillItemTemplate } from '../components/KillItemTemplate';
export function useSystemKillsItemTemplate(systemNameMap: Record<string, string>, onlyOneSystem: boolean) {
return useCallback(
(kill: DetailedKill, options: VirtualScrollerTemplateOptions) =>
KillItemTemplate(systemNameMap, onlyOneSystem, kill, options),
[systemNameMap, onlyOneSystem],
);
}

View File

@@ -1,3 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import {
InfoDrawer,
@@ -19,28 +20,29 @@ import {
STRUCTURE,
SystemSignatureSettingsDialog,
} from './SystemSignatureSettingsDialog';
import { SignatureGroup } from '@/hooks/Mapper/types';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { PrimeIcons } from 'primereact/api';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CheckboxChangeEvent } from 'primereact/checkbox';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { useHotkey } from '@/hooks/Mapper/hooks';
import { COMPACT_MAX_WIDTH } from './constants';
const SIGNATURE_SETTINGS_KEY = 'wanderer_system_signature_settings_v5_2';
const SIGNATURE_SETTINGS_KEY = 'wanderer_system_signature_settings_v5_5';
export const SHOW_DESCRIPTION_COLUMN_SETTING = 'show_description_column_setting';
export const SHOW_UPDATED_COLUMN_SETTING = 'SHOW_UPDATED_COLUMN_SETTING';
export const SHOW_CHARACTER_COLUMN_SETTING = 'SHOW_CHARACTER_COLUMN_SETTING';
export const LAZY_DELETE_SIGNATURES_SETTING = 'LAZY_DELETE_SIGNATURES_SETTING';
export const KEEP_LAZY_DELETE_SETTING = 'KEEP_LAZY_DELETE_ENABLED_SETTING';
const settings: Setting[] = [
const SETTINGS: Setting[] = [
{ key: SHOW_UPDATED_COLUMN_SETTING, name: 'Show Updated Column', value: false, isFilter: false },
{ key: SHOW_DESCRIPTION_COLUMN_SETTING, name: 'Show Description Column', value: false, isFilter: false },
{ key: SHOW_CHARACTER_COLUMN_SETTING, name: 'Show Character Column', value: false, isFilter: false },
{ key: LAZY_DELETE_SIGNATURES_SETTING, name: 'Lazy Delete Signatures', value: false, isFilter: false },
{ key: KEEP_LAZY_DELETE_SETTING, name: 'Keep "Lazy Delete" Enabled', value: false, isFilter: false },
{ key: COSMIC_ANOMALY, name: 'Show Anomalies', value: true, isFilter: true },
{ key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true, isFilter: true },
{ key: DEPLOYABLE, name: 'Show Deployables', value: true, isFilter: true },
@@ -56,121 +58,160 @@ const settings: Setting[] = [
{ key: SignatureGroup.CombatSite, name: 'Show Combat Sites', value: true, isFilter: true },
];
const defaultSettings = () => {
return [...settings];
};
const getDefaultSettings = (): Setting[] => [...SETTINGS];
export const SystemSignatures = () => {
export const SystemSignatures: React.FC = () => {
const {
data: { selectedSystems },
} = useMapRootState();
const [visible, setVisible] = useState(false);
const [settings, setSettings] = useState<Setting[]>(defaultSettings);
const [currentSettings, setCurrentSettings] = useState<Setting[]>(() => {
const stored = localStorage.getItem(SIGNATURE_SETTINGS_KEY);
if (stored) {
try {
return JSON.parse(stored) as Setting[];
} catch (error) {
console.error('Error parsing stored settings', error);
}
}
return getDefaultSettings();
});
useEffect(() => {
localStorage.setItem(SIGNATURE_SETTINGS_KEY, JSON.stringify(currentSettings));
}, [currentSettings]);
const [sigCount, setSigCount] = useState<number>(0);
const [pendingSigs, setPendingSigs] = useState<SystemSignature[]>([]);
const [undoPending, setUndoPending] = useState<() => void>(() => () => {});
const handleSigCountChange = useCallback((count: number) => {
setSigCount(count);
}, []);
const [systemId] = selectedSystems;
const isNotSelectedSystem = selectedSystems.length !== 1;
const lazyDeleteValue = useMemo(() => {
return settings.find(setting => setting.key === LAZY_DELETE_SIGNATURES_SETTING)!.value;
}, [settings]);
const lazyDeleteValue = useMemo(
() => currentSettings.find(setting => setting.key === LAZY_DELETE_SIGNATURES_SETTING)?.value || false,
[currentSettings],
);
const handleSettingsChange = useCallback((settings: Setting[]) => {
setSettings(settings);
localStorage.setItem(SIGNATURE_SETTINGS_KEY, JSON.stringify(settings));
const handleSettingsChange = useCallback((newSettings: Setting[]) => {
setCurrentSettings(newSettings);
setVisible(false);
}, []);
const handleLazyDeleteChange = useCallback((value: boolean) => {
setSettings(settings => {
const lazyDelete = settings.find(setting => setting.key === LAZY_DELETE_SIGNATURES_SETTING)!;
lazyDelete.value = value;
localStorage.setItem(SIGNATURE_SETTINGS_KEY, JSON.stringify(settings));
return [...settings];
});
setCurrentSettings(prevSettings =>
prevSettings.map(setting => (setting.key === LAZY_DELETE_SIGNATURES_SETTING ? { ...setting, value } : setting)),
);
}, []);
useEffect(() => {
const restoredSettings = localStorage.getItem(SIGNATURE_SETTINGS_KEY);
const containerRef = useRef<HTMLDivElement>(null);
const isCompact = useMaxWidth(containerRef, COMPACT_MAX_WIDTH);
if (restoredSettings) {
setSettings(JSON.parse(restoredSettings));
useHotkey(true, ['z'], (event: KeyboardEvent) => {
if (pendingSigs.length > 0) {
event.preventDefault();
event.stopPropagation();
undoPending();
setPendingSigs([]);
}
});
const handleUndoClick = useCallback(() => {
undoPending();
setPendingSigs([]);
}, [undoPending]);
const handleSettingsButtonClick = useCallback(() => {
setVisible(true);
}, []);
const ref = useRef<HTMLDivElement>(null);
const compact = useMaxWidth(ref, 260);
const renderLabel = () => (
<div className="flex justify-between items-center text-xs w-full h-full" ref={containerRef}>
<div className="flex justify-between items-center gap-1">
{!isCompact && (
<div className="flex whitespace-nowrap text-ellipsis overflow-hidden text-stone-400">
{sigCount ? `[${sigCount}] ` : ''}Signatures {isNotSelectedSystem ? '' : 'in'}
</div>
)}
{!isNotSelectedSystem && <SystemView systemId={systemId} className="select-none text-center" hideRegion />}
</div>
<LayoutEventBlocker className="flex gap-2.5">
<WdTooltipWrapper content="Enable Lazy delete">
<WdCheckbox
size="xs"
labelSide="left"
label={isCompact ? '' : 'Lazy delete'}
value={lazyDeleteValue}
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300 whitespace-nowrap text-ellipsis overflow-hidden"
onChange={(event: CheckboxChangeEvent) => handleLazyDeleteChange(!!event.checked)}
/>
</WdTooltipWrapper>
{pendingSigs.length > 0 && (
<WdImgButton
className={PrimeIcons.UNDO}
style={{ color: 'red' }}
tooltip={{ content: `Undo pending changes (${pendingSigs.length})` }}
onClick={handleUndoClick}
/>
)}
<WdImgButton
className={PrimeIcons.QUESTION_CIRCLE}
tooltip={{
position: TooltipPosition.left,
content: (
<div className="flex flex-col gap-1">
<InfoDrawer title={<b className="text-slate-50">How to add/update signature?</b>}>
In game you need to select one or more signatures <br /> in the list in{' '}
<b className="text-sky-500">Probe scanner</b>. <br /> Use next hotkeys:
<br />
<b className="text-sky-500">Shift + LMB</b> or <b className="text-sky-500">Ctrl + LMB</b>
<br /> or <b className="text-sky-500">Ctrl + A</b> for select all
<br /> and then use <b className="text-sky-500">Ctrl + C</b>, after you need to go <br />
here, select Solar system and paste it with <b className="text-sky-500">Ctrl + V</b>
</InfoDrawer>
<InfoDrawer title={<b className="text-slate-50">How to select?</b>}>
For selecting any signature, click on it <br /> with hotkeys{' '}
<b className="text-sky-500">Shift + LMB</b> or <b className="text-sky-500">Ctrl + LMB</b>
</InfoDrawer>
<InfoDrawer title={<b className="text-slate-50">How to delete?</b>}>
To delete any signature, first select it <br /> and then press <b className="text-sky-500">Del</b>
</InfoDrawer>
</div>
) as React.ReactNode,
}}
/>
<WdImgButton className={PrimeIcons.SLIDERS_H} onClick={handleSettingsButtonClick} />
</LayoutEventBlocker>
</div>
);
return (
<Widget
label={
<div className="flex justify-between items-center text-xs w-full h-full" ref={ref}>
<div className="flex justify-between items-center gap-1">
{!compact && (
<div className="flex whitespace-nowrap text-ellipsis overflow-hidden text-stone-400">
Signatures {isNotSelectedSystem ? '' : 'in'}
</div>
)}
{!isNotSelectedSystem && <SystemView systemId={systemId} className="select-none text-center" hideRegion />}
</div>
<LayoutEventBlocker className="flex gap-2.5">
<WdTooltipWrapper content="Enable Lazy delete">
<WdCheckbox
size="xs"
labelSide="left"
label={compact ? '' : 'Lazy delete'}
value={lazyDeleteValue}
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300 whitespace-nowrap text-ellipsis overflow-hidden"
onChange={(event: CheckboxChangeEvent) => handleLazyDeleteChange(!!event.checked)}
/>
</WdTooltipWrapper>
<WdImgButton
className={PrimeIcons.QUESTION_CIRCLE}
tooltip={{
position: TooltipPosition.left,
// @ts-ignore
content: (
<div className="flex flex-col gap-1">
<InfoDrawer title={<b className="text-slate-50">How to add/update signature?</b>}>
In game you need select one or more signatures <br /> in list in{' '}
<b className="text-sky-500">Probe scanner</b>. <br /> Use next hotkeys:
<br />
<b className="text-sky-500">Shift + LMB</b> or <b className="text-sky-500">Ctrl + LMB</b>
<br /> or <b className="text-sky-500">Ctrl + A</b> for select all
<br />
and then use <b className="text-sky-500">Ctrl + C</b>, after you need to go <br />
here select Solar system and paste it with <b className="text-sky-500">Ctrl + V</b>
</InfoDrawer>
<InfoDrawer title={<b className="text-slate-50">How to select?</b>}>
For select any signature need click on that, <br /> with hotkeys{' '}
<b className="text-sky-500">Shift + LMB</b> or <b className="text-sky-500">Ctrl + LMB</b>
</InfoDrawer>
<InfoDrawer title={<b className="text-slate-50">How to delete?</b>}>
For delete any signature first of all you need select before
<br /> and then use <b className="text-sky-500">Del</b>
</InfoDrawer>
</div>
) as React.ReactNode,
}}
/>
<WdImgButton className={PrimeIcons.SLIDERS_H} onClick={() => setVisible(true)} />
</LayoutEventBlocker>
</div>
}
>
<Widget label={renderLabel()}>
{isNotSelectedSystem ? (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
System is not selected
</div>
) : (
<SystemSignaturesContent systemId={systemId} settings={settings} onLazyDeleteChange={handleLazyDeleteChange} />
<SystemSignaturesContent
systemId={systemId}
settings={currentSettings}
onLazyDeleteChange={handleLazyDeleteChange}
onCountChange={handleSigCountChange}
onPendingChange={(pending, undo) => {
setPendingSigs(pending);
setUndoPending(() => undo);
}}
/>
)}
{visible && (
<SystemSignatureSettingsDialog
settings={settings}
settings={currentSettings}
onCancel={() => setVisible(false)}
onSave={handleSettingsChange}
/>
@@ -178,3 +219,5 @@ export const SystemSignatures = () => {
</Widget>
);
};
export default SystemSignatures;

View File

@@ -1,10 +0,0 @@
.TableRowCompact {
height: 8px;
max-height: 8px;
font-size: 12px !important;
line-height: 8px;
}
.Table {
}

View File

@@ -1,29 +1,27 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { parseSignatures } from '@/hooks/Mapper/helpers';
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
import {
getGroupIdByRawGroup,
GROUPS_LIST,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { DataTable, DataTableRowClickEvent, DataTableRowMouseEvent, SortOrder } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import useRefState from 'react-usestateref';
import { Setting } from '../SystemSignatureSettingsDialog';
import { useHotkey } from '@/hooks/Mapper/hooks';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
import { useClipboard } from '@/hooks/Mapper/hooks/useClipboard';
import { PrimeIcons } from 'primereact/api';
import useLocalStorageState from 'use-local-storage-state';
import classes from './SystemSignaturesContent.module.scss';
import clsx from 'clsx';
import { SystemSignature } from '@/hooks/Mapper/types';
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
import { WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { SignatureView } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView';
import {
getActualSigs,
getRowColorByTimeLeft,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/helpers';
COMPACT_MAX_WIDTH,
GROUPS_LIST,
MEDIUM_MAX_WIDTH,
OTHER_COLUMNS_WIDTH,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
import {
KEEP_LAZY_DELETE_SETTING,
LAZY_DELETE_SIGNATURES_SETTING,
SHOW_DESCRIPTION_COLUMN_SETTING,
SHOW_UPDATED_COLUMN_SETTING,
SHOW_CHARACTER_COLUMN_SETTING,
} from '../SystemSignatures';
import { COSMIC_SIGNATURE } from '../SystemSignatureSettingsDialog';
import {
renderAddedTimeLeft,
renderDescription,
@@ -31,18 +29,12 @@ import {
renderInfoColumn,
renderUpdatedTimeLeft,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
import useLocalStorageState from 'use-local-storage-state';
import { PrimeIcons } from 'primereact/api';
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { COSMIC_SIGNATURE } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureSettingsDialog';
import {
SHOW_DESCRIPTION_COLUMN_SETTING,
SHOW_UPDATED_COLUMN_SETTING,
LAZY_DELETE_SIGNATURES_SETTING,
KEEP_LAZY_DELETE_SETTING,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures';
import { ExtendedSystemSignature } from '../helpers/contentHelpers';
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
import { getSignatureRowClass } from '../helpers/rowStyles';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
type SystemSignaturesSortSettings = {
sortField: string;
sortOrder: SortOrder;
@@ -55,391 +47,292 @@ const SORT_DEFAULT_VALUES: SystemSignaturesSortSettings = {
interface SystemSignaturesContentProps {
systemId: string;
settings: Setting[];
settings: { key: string; value: boolean }[];
hideLinkedSignatures?: boolean;
selectable?: boolean;
onSelect?: (signature: SystemSignature) => void;
onLazyDeleteChange?: (value: boolean) => void;
onCountChange?: (count: number) => void;
onPendingChange?: (pending: ExtendedSystemSignature[], undo: () => void) => void;
}
export const SystemSignaturesContent = ({
const headerInlineStyle = { padding: '2px', fontSize: '12px', lineHeight: '1.333' };
export function SystemSignaturesContent({
systemId,
settings,
hideLinkedSignatures,
selectable,
onSelect,
onLazyDeleteChange,
}: SystemSignaturesContentProps) => {
const { outCommand } = useMapRootState();
const [signatures, setSignatures, signaturesRef] = useRefState<SystemSignature[]>([]);
const [selectedSignatures, setSelectedSignatures] = useState<SystemSignature[]>([]);
const [nameColumnWidth, setNameColumnWidth] = useState('auto');
const [selectedSignature, setSelectedSignature] = useState<SystemSignature | null>(null);
const [hoveredSig, setHoveredSig] = useState<SystemSignature | null>(null);
const [sortSettings, setSortSettings] = useLocalStorageState<SystemSignaturesSortSettings>('window:signatures:sort', {
defaultValue: SORT_DEFAULT_VALUES,
});
const tableRef = useRef<HTMLDivElement>(null);
const compact = useMaxWidth(tableRef, 260);
const medium = useMaxWidth(tableRef, 380);
const refData = useRef({ selectable });
refData.current = { selectable };
const tooltipRef = useRef<WdTooltipHandlers>(null);
const { clipboardContent, setClipboardContent } = useClipboard();
const lazyDeleteValue = useMemo(() => {
return settings.find(setting => setting.key === LAZY_DELETE_SIGNATURES_SETTING)?.value ?? false;
}, [settings]);
const keepLazyDeleteValue = useMemo(() => {
return settings.find(setting => setting.key === KEEP_LAZY_DELETE_SETTING)?.value ?? false;
}, [settings]);
const handleResize = useCallback(() => {
if (tableRef.current) {
const tableWidth = tableRef.current.offsetWidth;
const otherColumnsWidth = 276;
const availableWidth = tableWidth - otherColumnsWidth;
setNameColumnWidth(`${availableWidth}px`);
}
}, []);
const groupSettings = useMemo(() => settings.filter(s => (GROUPS_LIST as string[]).includes(s.key)), [settings]);
const showDescriptionColumn = useMemo(
() => settings.find(s => s.key === SHOW_DESCRIPTION_COLUMN_SETTING)?.value,
[settings],
);
const showUpdatedColumn = useMemo(() => settings.find(s => s.key === SHOW_UPDATED_COLUMN_SETTING)?.value, [settings]);
const filteredSignatures = useMemo(() => {
return signatures
.filter(x => {
if (hideLinkedSignatures && !!x.linked_system) {
return false;
}
const isCosmicSignature = x.kind === COSMIC_SIGNATURE;
const preparedGroup = getGroupIdByRawGroup(x.group);
if (isCosmicSignature) {
const showCosmicSignatures = settings.find(y => y.key === COSMIC_SIGNATURE)?.value;
if (showCosmicSignatures) {
return !x.group || groupSettings.find(y => y.key === preparedGroup)?.value;
} else {
return !!x.group && groupSettings.find(y => y.key === preparedGroup)?.value;
}
}
return settings.find(y => y.key === x.kind)?.value;
})
.sort((a, b) => {
return new Date(b.updated_at || 0).getTime() - new Date(a.updated_at || 0).getTime();
});
}, [signatures, settings, groupSettings, hideLinkedSignatures]);
const handleGetSignatures = useCallback(async () => {
const { signatures } = await outCommand({
type: OutCommand.getSignatures,
data: { system_id: systemId },
onCountChange,
onPendingChange,
}: SystemSignaturesContentProps) {
const { signatures, selectedSignatures, setSelectedSignatures, handleDeleteSelected, handleSelectAll, handlePaste } =
useSystemSignaturesData({
systemId,
settings,
onCountChange,
onPendingChange,
onLazyDeleteChange,
});
setSignatures(signatures);
}, [outCommand, systemId]);
const handleUpdateSignatures = useCallback(
async (newSignatures: SystemSignature[], updateOnly: boolean, skipUpdateUntouched?: boolean) => {
const { added, updated, removed } = getActualSigs(
signaturesRef.current,
newSignatures,
updateOnly,
skipUpdateUntouched,
);
const { signatures: updatedSignatures } = await outCommand({
type: OutCommand.updateSignatures,
data: {
system_id: systemId,
added,
updated,
removed,
},
});
setSignatures(() => updatedSignatures);
setSelectedSignatures([]);
},
[outCommand, systemId],
const [sortSettings, setSortSettings] = useLocalStorageState<{ sortField: string; sortOrder: SortOrder }>(
'window:signatures:sort',
{ defaultValue: SORT_DEFAULT_VALUES },
);
const handleDeleteSelected = useCallback(
async (e: KeyboardEvent) => {
if (selectable) {
return;
}
if (selectedSignatures.length === 0) {
return;
}
const tableRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<WdTooltipHandlers>(null);
const [hoveredSignature, setHoveredSignature] = useState<SystemSignature | null>(null);
e.preventDefault();
e.stopPropagation();
const isCompact = useMaxWidth(tableRef, COMPACT_MAX_WIDTH);
const isMedium = useMaxWidth(tableRef, MEDIUM_MAX_WIDTH);
const selectedSignaturesEveIds = selectedSignatures.map(x => x.eve_id);
await handleUpdateSignatures(
signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id)),
false,
true,
);
},
[handleUpdateSignatures, selectable, signatures, selectedSignatures],
);
const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures);
}, [signatures]);
const handleSelectSignatures = useCallback(
// TODO still will be good to define types if we use typescript
// @ts-ignore
e => {
if (selectable) {
onSelect?.(e.value);
} else {
setSelectedSignatures(e.value);
}
},
[onSelect, selectable],
);
const handlePaste = async (clipboardContent: string) => {
const newSignatures = parseSignatures(
clipboardContent,
settings.map(x => x.key),
);
handleUpdateSignatures(newSignatures, !lazyDeleteValue);
if (lazyDeleteValue && !keepLazyDeleteValue) {
onLazyDeleteChange?.(false);
}
};
const handleEnterRow = useCallback(
(e: DataTableRowMouseEvent) => {
setHoveredSig(filteredSignatures[e.index]);
tooltipRef.current?.show(e.originalEvent);
},
[filteredSignatures],
);
const handleLeaveRow = useCallback((e: DataTableRowMouseEvent) => {
tooltipRef.current?.hide(e.originalEvent);
setHoveredSig(null);
}, []);
const lazyDeleteEnabled = settings.find(s => s.key === LAZY_DELETE_SIGNATURES_SETTING)?.value ?? false;
const keepLazyDeleteEnabled = settings.find(s => s.key === KEEP_LAZY_DELETE_SETTING)?.value ?? false;
const { clipboardContent, setClipboardContent } = useClipboard();
useEffect(() => {
if (refData.current.selectable) {
return;
}
if (!clipboardContent?.text) {
return;
}
if (selectable) return;
if (!clipboardContent?.text) return;
handlePaste(clipboardContent.text);
if (lazyDeleteEnabled && !keepLazyDeleteEnabled) {
onLazyDeleteChange?.(false);
}
setClipboardContent(null);
}, [clipboardContent, selectable, lazyDeleteValue, keepLazyDeleteValue]);
}, [
selectable,
clipboardContent,
handlePaste,
setClipboardContent,
lazyDeleteEnabled,
keepLazyDeleteEnabled,
onLazyDeleteChange,
]);
useHotkey(true, ['a'], handleSelectAll);
useHotkey(false, ['Backspace', 'Delete'], handleDeleteSelected);
useEffect(() => {
if (!systemId) {
setSignatures([]);
return;
}
handleGetSignatures();
}, [systemId]);
useMapEventListener(event => {
switch (event.name) {
case Commands.signaturesUpdated:
if (event.data?.toString() !== systemId.toString()) {
return;
}
handleGetSignatures();
return true;
}
useHotkey(false, ['Backspace', 'Delete'], (event: KeyboardEvent) => {
event.preventDefault();
event.stopPropagation();
handleDeleteSelected();
});
useEffect(() => {
const observer = new ResizeObserver(handleResize);
if (tableRef.current) {
observer.observe(tableRef.current);
}
handleResize(); // Call on mount to set initial width
return () => {
if (tableRef.current) {
observer.unobserve(tableRef.current);
}
};
const [nameColumnWidth, setNameColumnWidth] = useState('auto');
const handleResize = useCallback(() => {
if (!tableRef.current) return;
const tableWidth = tableRef.current.offsetWidth;
const otherColumnsWidth = OTHER_COLUMNS_WIDTH;
setNameColumnWidth(`${tableWidth - otherColumnsWidth}px`);
}, []);
useEffect(() => {
if (!tableRef.current) return;
const observer = new ResizeObserver(handleResize);
observer.observe(tableRef.current);
handleResize();
return () => {
observer.disconnect();
};
}, [handleResize]);
const renderToolbar = (/*row: SystemSignature*/) => {
return (
<div className="flex justify-end items-center gap-2 mr-[4px]">
<WdTooltipWrapper content="To Edit Signature do double click">
<span className={clsx(PrimeIcons.PENCIL, 'text-[10px]')}></span>
</WdTooltipWrapper>
</div>
);
};
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
const [showSignatureSettings, setShowSignatureSettings] = useState(false);
const handleRowClick = (e: DataTableRowClickEvent) => {
setSelectedSignature(e.data as SystemSignature);
setSelectedSignatureForDialog(e.data as SystemSignature);
setShowSignatureSettings(true);
};
return (
<>
<div ref={tableRef} className={'h-full '}>
{filteredSignatures.length === 0 ? (
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
No signatures
</div>
) : (
<>
{/* @ts-ignore */}
<DataTable
className={classes.Table}
value={filteredSignatures}
size="small"
selectionMode={selectable ? 'single' : 'multiple'}
selection={selectedSignatures}
metaKeySelection
onSelectionChange={handleSelectSignatures}
dataKey="eve_id"
tableClassName="w-full select-none"
resizableColumns={false}
onRowDoubleClick={handleRowClick}
rowHover
selectAll
sortField={sortSettings.sortField}
sortOrder={sortSettings.sortOrder}
onSort={event => setSortSettings(() => ({ sortField: event.sortField, sortOrder: event.sortOrder }))}
onRowMouseEnter={compact || medium ? handleEnterRow : undefined}
onRowMouseLeave={compact || medium ? handleLeaveRow : undefined}
rowClassName={row => {
if (selectedSignatures.some(x => x.eve_id === row.eve_id)) {
return clsx(classes.TableRowCompact, 'bg-amber-500/50 hover:bg-amber-500/70 transition duration-200');
}
const dateClass = getRowColorByTimeLeft(row.inserted_at ? new Date(row.inserted_at) : undefined);
if (!dateClass) {
return clsx(classes.TableRowCompact, 'hover:bg-purple-400/20 transition duration-200');
}
return clsx(classes.TableRowCompact, dateClass);
}}
>
<Column
bodyClassName="p-0 px-1"
field="group"
body={x => renderIcon(x)}
style={{ maxWidth: 26, minWidth: 26, width: 26, height: 25 }}
></Column>
<Column
field="eve_id"
header="Id"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 72, minWidth: 72, width: 72 }}
sortable
></Column>
<Column
field="group"
header="Group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
hidden={compact}
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
sortable
></Column>
<Column
field="info"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
body={renderInfoColumn}
style={{ maxWidth: nameColumnWidth }}
hidden={compact || medium}
></Column>
{showDescriptionColumn && (
<Column
field="description"
header="Description"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
body={renderDescription}
hidden={compact}
sortable
></Column>
)}
<Column
field="inserted_at"
header="Added"
dataType="date"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
body={renderAddedTimeLeft}
sortable
></Column>
{showUpdatedColumn && (
<Column
field="updated_at"
header="Updated"
dataType="date"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
body={renderUpdatedTimeLeft}
sortable
></Column>
)}
{!selectable && (
<Column
bodyClassName="p-0 pl-1 pr-2"
field="group"
body={renderToolbar}
// headerClassName={headerClasses}
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
></Column>
)}
</DataTable>
</>
)}
<WdTooltip
className="bg-stone-900/95 text-slate-50"
ref={tooltipRef}
content={hoveredSig ? <SignatureView {...hoveredSig} /> : null}
/>
{showSignatureSettings && (
<SignatureSettings
systemId={systemId}
show
onHide={() => setShowSignatureSettings(false)}
signatureData={selectedSignature}
/>
)}
</div>
</>
const handleSelectSignatures = useCallback(
(e: { value: SystemSignature[] }) => {
if (selectable) {
onSelect?.(e.value[0]);
} else {
setSelectedSignatures(e.value as ExtendedSystemSignature[]);
}
},
[selectable, onSelect, setSelectedSignatures],
);
};
const showDescriptionColumn = settings.find(s => s.key === SHOW_DESCRIPTION_COLUMN_SETTING)?.value;
const showUpdatedColumn = settings.find(s => s.key === SHOW_UPDATED_COLUMN_SETTING)?.value;
const showCharacterColumn = settings.find(s => s.key === SHOW_CHARACTER_COLUMN_SETTING)?.value;
const enabledGroups = settings
.filter(s => GROUPS_LIST.includes(s.key as SignatureGroup) && s.value === true)
.map(s => s.key);
const filteredSignatures = useMemo<ExtendedSystemSignature[]>(() => {
return signatures.filter(sig => {
if (hideLinkedSignatures && sig.linked_system) {
return false;
}
if (sig.kind === COSMIC_SIGNATURE) {
const showCosmic = settings.find(y => y.key === COSMIC_SIGNATURE)?.value;
if (!showCosmic) return false;
if (sig.group) {
return enabledGroups.includes(sig.group);
}
return true;
} else {
return settings.find(y => y.key === sig.kind)?.value;
}
});
}, [signatures, hideLinkedSignatures, settings, enabledGroups]);
return (
<div ref={tableRef} className="h-full">
{filteredSignatures.length === 0 ? (
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
No signatures
</div>
) : (
<DataTable
value={filteredSignatures}
size="small"
selectionMode="multiple"
selection={selectedSignatures}
metaKeySelection
onSelectionChange={handleSelectSignatures}
dataKey="eve_id"
className="w-full select-none"
resizableColumns={false}
rowHover
selectAll
onRowDoubleClick={handleRowClick}
sortField={sortSettings.sortField}
sortOrder={sortSettings.sortOrder}
onSort={e => setSortSettings({ sortField: e.sortField, sortOrder: e.sortOrder })}
onRowMouseEnter={
isCompact || isMedium
? (e: DataTableRowMouseEvent) => {
setHoveredSignature(filteredSignatures[e.index]);
tooltipRef.current?.show(e.originalEvent);
}
: undefined
}
onRowMouseLeave={
isCompact || isMedium
? () => {
setHoveredSignature(null);
tooltipRef.current?.hide();
}
: undefined
}
rowClassName={rowData => getSignatureRowClass(rowData as ExtendedSystemSignature, selectedSignatures)}
>
<Column
field="icon"
header=""
headerStyle={headerInlineStyle}
body={sig => renderIcon(sig)}
bodyClassName="p-0 px-1"
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
/>
<Column
field="eve_id"
header="Id"
headerStyle={headerInlineStyle}
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 72, minWidth: 72, width: 72 }}
sortable
/>
<Column
field="group"
header="Group"
headerStyle={headerInlineStyle}
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
body={sig => sig.group ?? ''}
hidden={isCompact}
sortable
/>
<Column
field="info"
header="Info"
headerStyle={headerInlineStyle}
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: nameColumnWidth }}
hidden={isCompact || isMedium}
body={renderInfoColumn}
/>
{showDescriptionColumn && (
<Column
field="description"
header="Description"
headerStyle={headerInlineStyle}
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
hidden={isCompact}
body={renderDescription}
sortable
/>
)}
<Column
field="inserted_at"
header="Added"
headerStyle={headerInlineStyle}
dataType="date"
body={renderAddedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
sortable
/>
{showUpdatedColumn && (
<Column
field="updated_at"
header="Updated"
headerStyle={headerInlineStyle}
dataType="date"
body={renderUpdatedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
sortable
/>
)}
{showCharacterColumn && (
<Column
field="character_name"
header="Character"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
sortable
></Column>
)}
{!selectable && (
<Column
header=""
headerStyle={headerInlineStyle}
body={() => (
<div className="flex justify-end items-center gap-2 mr-[4px]">
<WdTooltipWrapper content="Double-click a row to edit signature">
<span className={PrimeIcons.PENCIL + ' text-[10px]'} />
</WdTooltipWrapper>
</div>
)}
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
bodyClassName="p-0 pl-1 pr-2"
/>
)}
</DataTable>
)}
<WdTooltip
className="bg-stone-900/95 text-slate-50"
ref={tooltipRef}
content={hoveredSignature ? <SignatureView {...hoveredSignature} /> : null}
/>
{showSignatureSettings && (
<SignatureSettings
systemId={systemId}
show
onHide={() => setShowSignatureSettings(false)}
signatureData={selectedSignatureForDialog || undefined}
/>
)}
</div>
);
}

View File

@@ -10,6 +10,13 @@ import {
export const TIME_ONE_MINUTE = 1000 * 60;
export const TIME_TEN_MINUTES = 1000 * 60 * 10;
export const TIME_ONE_DAY = 24 * 60 * 60 * 1000;
export const TIME_ONE_WEEK = 7 * TIME_ONE_DAY;
export const FINAL_DURATION_MS = 10000;
export const COMPACT_MAX_WIDTH = 260;
export const MEDIUM_MAX_WIDTH = 380;
export const OTHER_COLUMNS_WIDTH = 276;
export const GROUPS_LIST = [
SignatureGroup.GasSite,

View File

@@ -0,0 +1,94 @@
import { SystemSignature } from '@/hooks/Mapper/types';
import { FINAL_DURATION_MS } from '../constants';
export interface ExtendedSystemSignature extends SystemSignature {
pendingDeletion?: boolean;
pendingAddition?: boolean;
pendingUntil?: number;
}
export function prepareUpdatePayload(
systemId: string,
added: ExtendedSystemSignature[],
updated: ExtendedSystemSignature[],
removed: ExtendedSystemSignature[],
) {
return {
system_id: systemId,
added: added.map(s => ({ ...s })),
updated: updated.map(s => ({ ...s })),
removed: removed.map(s => ({ ...s })),
};
}
export function schedulePendingAdditionForSig(
sig: ExtendedSystemSignature,
finalDuration: number,
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>,
pendingAdditionMapRef: React.MutableRefObject<Record<string, { finalUntil: number; finalTimeoutId: number }>>,
setPendingUndoAdditions: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>,
) {
setPendingUndoAdditions(prev => [...prev, sig]);
const now = Date.now();
const finalTimeoutId = window.setTimeout(() => {
setSignatures(prev =>
prev.map(x => (x.eve_id === sig.eve_id ? { ...x, pendingAddition: false, pendingUntil: undefined } : x)),
);
const clone = { ...pendingAdditionMapRef.current };
delete clone[sig.eve_id];
pendingAdditionMapRef.current = clone;
setPendingUndoAdditions(prev => prev.filter(x => x.eve_id !== sig.eve_id));
}, finalDuration);
pendingAdditionMapRef.current = {
...pendingAdditionMapRef.current,
[sig.eve_id]: {
finalUntil: now + finalDuration,
finalTimeoutId,
},
};
setSignatures(prev =>
prev.map(x => (x.eve_id === sig.eve_id ? { ...x, pendingAddition: true, pendingUntil: now + finalDuration } : x)),
);
}
export function mergeLocalPendingAdditions(
serverSigs: ExtendedSystemSignature[],
localSigs: ExtendedSystemSignature[],
): ExtendedSystemSignature[] {
const now = Date.now();
const pendingAdditions = localSigs.filter(sig => sig.pendingAddition && sig.pendingUntil && sig.pendingUntil > now);
const mergedMap = new Map<string, ExtendedSystemSignature>();
serverSigs.forEach(sig => mergedMap.set(sig.eve_id, sig));
pendingAdditions.forEach(sig => {
if (!mergedMap.has(sig.eve_id)) {
mergedMap.set(sig.eve_id, sig);
}
});
return Array.from(mergedMap.values());
}
export function scheduleLazyDeletionTimers(
toRemove: ExtendedSystemSignature[],
setPendingMap: React.Dispatch<React.SetStateAction<Record<string, { finalUntil: number; finalTimeoutId: number }>>>,
finalizeRemoval: (sig: ExtendedSystemSignature) => Promise<void>,
finalDuration = FINAL_DURATION_MS,
) {
const now = Date.now();
toRemove.forEach(sig => {
const finalTimeoutId = window.setTimeout(async () => {
await finalizeRemoval(sig);
}, finalDuration);
setPendingMap(prev => ({
...prev,
[sig.eve_id]: {
finalUntil: now + finalDuration,
finalTimeoutId,
},
}));
});
}

View File

@@ -1,25 +1,67 @@
import { SystemSignature } from '@/hooks/Mapper/types';
import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { getState } from './getState.ts';
import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
import { getState } from './getState';
/**
* Compare two lists of signatures and return which are added, updated, or removed.
*
* @param oldSignatures existing signatures (in memory or from server)
* @param newSignatures newly parsed or incoming signatures from user input
* @param updateOnly if true, do NOT remove old signatures not found in newSignatures
* @param skipUpdateUntouched if true, do NOT push unmodified signatures into the `updated` array
*/
export const getActualSigs = (
oldSignatures: SystemSignature[],
newSignatures: SystemSignature[],
updateOnly: boolean,
updateOnly?: boolean,
skipUpdateUntouched?: boolean,
): { added: SystemSignature[]; updated: SystemSignature[]; removed: SystemSignature[] } => {
const updated: SystemSignature[] = [];
const removed: SystemSignature[] = [];
const added: SystemSignature[] = [];
oldSignatures.forEach(oldSig => {
// if old sigs is not contains in newSigs we need mark it as removed
// otherwise we check
const newSig = newSignatures.find(s => s.eve_id === oldSig.eve_id);
if (newSig) {
// we take new sig and now we need check that sig has been updated
const isNeedUpgrade = getState(GROUPS_LIST, newSig) > getState(GROUPS_LIST, oldSig);
if (isNeedUpgrade) {
updated.push({ ...oldSig, group: newSig.group, name: newSig.name });
const needUpgrade = getState(GROUPS_LIST, newSig) > getState(GROUPS_LIST, oldSig);
const mergedSig = { ...oldSig };
let changed = false;
if (needUpgrade) {
mergedSig.group = newSig.group;
mergedSig.name = newSig.name;
changed = true;
}
if (newSig.description && newSig.description !== oldSig.description) {
mergedSig.description = newSig.description;
changed = true;
}
try {
const oldInfo = JSON.parse(oldSig.custom_info || '{}');
const newInfo = JSON.parse(newSig.custom_info || '{}');
let infoChanged = false;
for (const key in newInfo) {
if (oldInfo[key] !== newInfo[key]) {
oldInfo[key] = newInfo[key];
infoChanged = true;
}
}
if (infoChanged) {
mergedSig.custom_info = JSON.stringify(oldInfo);
changed = true;
}
} catch (e) {
console.error(`getActualSigs: Error merging custom_info for ${oldSig.eve_id}`, e);
}
if (newSig.updated_at !== oldSig.updated_at) {
mergedSig.updated_at = newSig.updated_at;
changed = true;
}
if (changed) {
updated.push(mergedSig);
} else if (!skipUpdateUntouched) {
updated.push({ ...oldSig });
}
@@ -30,8 +72,12 @@ export const getActualSigs = (
}
});
const oldSignaturesIds = oldSignatures.map(x => x.eve_id);
const added = newSignatures.filter(s => !oldSignaturesIds.includes(s.eve_id));
const oldIds = new Set(oldSignatures.map(x => x.eve_id));
newSignatures.forEach(s => {
if (!oldIds.has(s.eve_id)) {
added.push(s);
}
});
return { added, updated, removed };
};

View File

@@ -3,9 +3,9 @@ import {
TIME_TEN_MINUTES,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
export const getRowColorByTimeLeft = (date: Date | undefined) => {
export const getRowBackgroundColor = (date: Date | undefined): string => {
if (!date) {
return null;
return '';
}
const currentDate = new Date();
@@ -18,4 +18,6 @@ export const getRowColorByTimeLeft = (date: Date | undefined) => {
if (diff < TIME_TEN_MINUTES) {
return 'bg-lime-700/40 transition hover:bg-lime-700/50';
}
return '';
};

View File

@@ -1,11 +1,8 @@
import { SystemSignature } from '@/hooks/Mapper/types';
// also we need detect changes, we need understand that sigs has states
// first state when kind is Cosmic Signature or Cosmic Anomaly and group is empty
// and we should detect it for ungrade sigs
export const getState = (_: string[], newSig: SystemSignature) => {
let state = -1;
if (!newSig.group || newSig.group === '') {
if (!newSig.group) {
state = 0;
} else if (!newSig.name || newSig.name === '') {
state = 1;

View File

@@ -1,3 +1,5 @@
export * from './getState';
export * from './getRowColorByTimeLeft';
export * from './getRowBackgroundColor';
export * from './getActualSigs';
export * from './contentHelpers';
export * from './rowStyles';

View File

@@ -0,0 +1,34 @@
.pendingDeletion {
background-color: #f87171;
transition: background-color 0.2s ease;
}
.pendingDeletion td {
background-color: #f87171;
transition: background-color 0.2s ease;
}
.pendingDeletion {
background-color: #f87171;
transition: background-color 0.2s ease;
}
.Table thead tr {
font-size: 12px !important;
line-height: 1.333;
}
.TableRowCompact {
font-size: 12px !important;
line-height: 1.333;
}
.Table td {
padding: 2px;
height: 25px;
border: 1px solid #383838;
box-sizing: border-box;
}

View File

@@ -0,0 +1,20 @@
import clsx from 'clsx';
import { ExtendedSystemSignature } from './contentHelpers';
import { getRowBackgroundColor } from './getRowBackgroundColor';
import classes from './rowStyles.module.scss';
export function getSignatureRowClass(
row: ExtendedSystemSignature,
selectedSignatures: ExtendedSystemSignature[],
): string {
const isSelected = selectedSignatures.some(s => s.eve_id === row.eve_id);
return clsx(
classes.TableRowCompact,
'p-selectable-row',
isSelected && 'bg-amber-500/50 hover:bg-amber-500/70 transition duration-200',
!isSelected && row.pendingDeletion && classes.pendingDeletion,
!isSelected && getRowBackgroundColor(row.inserted_at ? new Date(row.inserted_at) : undefined),
'hover:bg-purple-400/20 transition duration-200',
);
}

View File

@@ -0,0 +1,26 @@
import { ExtendedSystemSignature } from '../helpers/contentHelpers';
export interface UseSystemSignaturesDataProps {
systemId: string;
settings: { key: string; value: boolean }[];
hideLinkedSignatures?: boolean;
onCountChange?: (count: number) => void;
onPendingChange?: (pending: ExtendedSystemSignature[], undo: () => void) => void;
onLazyDeleteChange?: (value: boolean) => void;
}
export interface UseFetchingParams {
systemId: string;
signaturesRef: React.MutableRefObject<ExtendedSystemSignature[]>;
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>;
localPendingDeletions: ExtendedSystemSignature[];
}
export interface UsePendingDeletionParams {
systemId: string;
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>;
}
export interface UsePendingAdditionParams {
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>;
}

View File

@@ -0,0 +1,53 @@
import { useState, useCallback, useRef } from 'react';
import { ExtendedSystemSignature, schedulePendingAdditionForSig } from '../helpers/contentHelpers';
import { UsePendingAdditionParams } from './types';
import { FINAL_DURATION_MS } from '../constants';
export function usePendingAdditions({ setSignatures }: UsePendingAdditionParams) {
const [pendingUndoAdditions, setPendingUndoAdditions] = useState<ExtendedSystemSignature[]>([]);
const pendingAdditionMapRef = useRef<Record<string, { finalUntil: number; finalTimeoutId: number }>>({});
const processAddedSignatures = useCallback(
(added: ExtendedSystemSignature[]) => {
if (!added.length) return;
const now = Date.now();
setSignatures(prev => [
...prev,
...added.map(sig => ({
...sig,
pendingAddition: true,
pendingUntil: now + FINAL_DURATION_MS,
})),
]);
added.forEach(sig => {
schedulePendingAdditionForSig(
sig,
FINAL_DURATION_MS,
setSignatures,
pendingAdditionMapRef,
setPendingUndoAdditions,
);
});
},
[setSignatures],
);
const clearPendingAdditions = useCallback(() => {
Object.values(pendingAdditionMapRef.current).forEach(({ finalTimeoutId }) => {
clearTimeout(finalTimeoutId);
});
pendingAdditionMapRef.current = {};
setSignatures(prev =>
prev.map(x => (x.pendingAddition ? { ...x, pendingAddition: false, pendingUntil: undefined } : x)),
);
setPendingUndoAdditions([]);
}, [setSignatures]);
return {
pendingUndoAdditions,
setPendingUndoAdditions,
pendingAdditionMapRef,
processAddedSignatures,
clearPendingAdditions,
};
}

View File

@@ -0,0 +1,79 @@
import { useState, useCallback } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { ExtendedSystemSignature, prepareUpdatePayload, scheduleLazyDeletionTimers } from '../helpers';
import { UsePendingDeletionParams } from './types';
import { FINAL_DURATION_MS } from '../constants';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export function usePendingDeletions({ systemId, setSignatures }: UsePendingDeletionParams) {
const { outCommand } = useMapRootState();
const [localPendingDeletions, setLocalPendingDeletions] = useState<ExtendedSystemSignature[]>([]);
const [pendingDeletionMap, setPendingDeletionMap] = useState<
Record<string, { finalUntil: number; finalTimeoutId: number }>
>({});
const processRemovedSignatures = useCallback(
async (
removed: ExtendedSystemSignature[],
added: ExtendedSystemSignature[],
updated: ExtendedSystemSignature[],
) => {
if (!removed.length) return;
const now = Date.now();
const processedRemoved = removed.map(r => ({
...r,
pendingDeletion: true,
pendingAddition: false,
pendingUntil: now + FINAL_DURATION_MS,
}));
setLocalPendingDeletions(prev => [...prev, ...processedRemoved]);
outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, []),
});
setSignatures(prev =>
prev.map(sig => {
if (processedRemoved.find(r => r.eve_id === sig.eve_id)) {
return { ...sig, pendingDeletion: true, pendingUntil: now + FINAL_DURATION_MS };
}
return sig;
}),
);
scheduleLazyDeletionTimers(
processedRemoved,
setPendingDeletionMap,
async sig => {
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, [], [], [sig]),
});
setLocalPendingDeletions(prev => prev.filter(x => x.eve_id !== sig.eve_id));
setSignatures(prev => prev.filter(x => x.eve_id !== sig.eve_id));
},
FINAL_DURATION_MS,
);
},
[systemId, outCommand, setSignatures],
);
const clearPendingDeletions = useCallback(() => {
Object.values(pendingDeletionMap).forEach(({ finalTimeoutId }) => clearTimeout(finalTimeoutId));
setPendingDeletionMap({});
setSignatures(prev =>
prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false, pendingUntil: undefined } : x)),
);
setLocalPendingDeletions([]);
}, [pendingDeletionMap, setSignatures]);
return {
localPendingDeletions,
setLocalPendingDeletions,
pendingDeletionMap,
setPendingDeletionMap,
processRemovedSignatures,
clearPendingDeletions,
};
}

View File

@@ -0,0 +1,75 @@
import { useCallback } from 'react';
import { SystemSignature } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { ExtendedSystemSignature, prepareUpdatePayload, getActualSigs, mergeLocalPendingAdditions } from '../helpers';
import { UseFetchingParams } from './types';
import { FINAL_DURATION_MS } from '../constants';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export function useSignatureFetching({
systemId,
signaturesRef,
setSignatures,
localPendingDeletions,
}: UseFetchingParams) {
const {
data: { characters },
outCommand,
} = useMapRootState();
const handleGetSignatures = useCallback(async () => {
if (!systemId) {
setSignatures([]);
return;
}
if (localPendingDeletions.length) {
return;
}
const resp = await outCommand({
type: OutCommand.getSignatures,
data: { system_id: systemId },
});
const serverSigs = (resp.signatures ?? []) as SystemSignature[];
const extended = serverSigs.map(s => ({
...s,
character_name: characters.find(c => c.eve_id === s.character_eve_id)?.name,
})) as ExtendedSystemSignature[];
setSignatures(prev => mergeLocalPendingAdditions(extended, prev));
}, [characters, systemId, localPendingDeletions, outCommand, setSignatures]);
const handleUpdateSignatures = useCallback(
async (newList: ExtendedSystemSignature[], updateOnly: boolean, skipUpdateUntouched?: boolean) => {
const { added, updated, removed } = getActualSigs(
signaturesRef.current,
newList,
updateOnly,
skipUpdateUntouched,
);
if (added.length > 0) {
const now = Date.now();
setSignatures(prev => [
...prev,
...added.map(a => ({
...a,
pendingAddition: true,
pendingUntil: now + FINAL_DURATION_MS,
})),
]);
}
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
},
[systemId, outCommand, signaturesRef, setSignatures],
);
return {
handleGetSignatures,
handleUpdateSignatures,
};
}

View File

@@ -0,0 +1,204 @@
import { useCallback, useEffect, useState } from 'react';
import useRefState from 'react-usestateref';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands, SystemSignature } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { parseSignatures } from '@/hooks/Mapper/helpers';
import {
KEEP_LAZY_DELETE_SETTING,
LAZY_DELETE_SIGNATURES_SETTING,
} from '@/hooks/Mapper/components/mapInterface/widgets';
import { ExtendedSystemSignature, getActualSigs, mergeLocalPendingAdditions } from '../helpers';
import { useSignatureFetching } from './useSignatureFetching';
import { usePendingAdditions } from './usePendingAdditions';
import { usePendingDeletions } from './usePendingDeletions';
import { UseSystemSignaturesDataProps } from './types';
import { TIME_ONE_DAY, TIME_ONE_WEEK } from '../constants';
import { SignatureGroup } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export function useSystemSignaturesData({
systemId,
settings,
onCountChange,
onPendingChange,
onLazyDeleteChange,
}: UseSystemSignaturesDataProps) {
const { outCommand } = useMapRootState();
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
const { localPendingDeletions, setLocalPendingDeletions, processRemovedSignatures, clearPendingDeletions } =
usePendingDeletions({
systemId,
setSignatures,
});
const { pendingUndoAdditions, setPendingUndoAdditions, processAddedSignatures, clearPendingAdditions } =
usePendingAdditions({
setSignatures,
});
const { handleGetSignatures, handleUpdateSignatures } = useSignatureFetching({
systemId,
signaturesRef,
setSignatures,
localPendingDeletions,
});
const handlePaste = useCallback(
async (clipboardString: string) => {
const lazyDeleteValue = settings.find(s => s.key === LAZY_DELETE_SIGNATURES_SETTING)?.value ?? false;
const incomingSignatures = parseSignatures(
clipboardString,
settings.map(s => s.key),
) as ExtendedSystemSignature[];
const current = signaturesRef.current;
const currentNonPending = lazyDeleteValue
? current.filter(sig => !sig.pendingDeletion)
: current.filter(sig => !sig.pendingDeletion && !sig.pendingAddition);
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, true);
if (added.length > 0) {
processAddedSignatures(added);
}
if (removed.length > 0) {
await processRemovedSignatures(removed, added, updated);
} else {
const resp = await outCommand({
type: OutCommand.updateSignatures,
data: {
system_id: systemId,
added,
updated,
removed: [],
},
});
if (resp) {
const finalSigs = (resp.signatures ?? []) as SystemSignature[];
setSignatures(prev =>
mergeLocalPendingAdditions(
finalSigs.map(x => ({ ...x })),
prev,
),
);
}
}
const keepLazy = settings.find(s => s.key === KEEP_LAZY_DELETE_SETTING)?.value ?? false;
if (lazyDeleteValue && !keepLazy) {
setTimeout(() => {
onLazyDeleteChange?.(false);
}, 0);
}
},
[
settings,
signaturesRef,
processAddedSignatures,
processRemovedSignatures,
outCommand,
systemId,
setSignatures,
onLazyDeleteChange,
],
);
const handleDeleteSelected = useCallback(async () => {
if (!selectedSignatures.length) return;
const selectedIds = selectedSignatures.map(s => s.eve_id);
const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id));
await handleUpdateSignatures(finalList, false, true);
setSelectedSignatures([]);
}, [selectedSignatures, signatures, handleUpdateSignatures]);
const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures);
}, [signatures]);
const undoPending = useCallback(() => {
clearPendingDeletions();
clearPendingAdditions();
setSignatures(prev =>
prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false, pendingUntil: undefined } : x)),
);
if (pendingUndoAdditions.length) {
pendingUndoAdditions.forEach(async sig => {
await outCommand({
type: OutCommand.updateSignatures,
data: {
system_id: systemId,
added: [],
updated: [],
removed: [sig],
},
});
});
setSignatures(prev => prev.filter(x => !pendingUndoAdditions.some(u => u.eve_id === x.eve_id)));
setPendingUndoAdditions([]);
}
setLocalPendingDeletions([]);
}, [
clearPendingDeletions,
clearPendingAdditions,
pendingUndoAdditions,
setPendingUndoAdditions,
setLocalPendingDeletions,
setSignatures,
outCommand,
systemId,
]);
useEffect(() => {
const combined = [...localPendingDeletions, ...pendingUndoAdditions];
onPendingChange?.(combined, undoPending);
}, [localPendingDeletions, pendingUndoAdditions, onPendingChange, undoPending]);
useEffect(() => {
if (!systemId) return;
const now = Date.now();
const oldOnes = signaturesRef.current.filter(sig => {
if (!sig.inserted_at) return false;
const inserted = new Date(sig.inserted_at).getTime();
const threshold = sig.group === SignatureGroup.Wormhole ? TIME_ONE_DAY : TIME_ONE_WEEK;
return now - inserted > threshold;
});
if (oldOnes.length) {
const remain = signaturesRef.current.filter(x => !oldOnes.includes(x));
handleUpdateSignatures(remain, false, true);
}
}, [systemId, handleUpdateSignatures, signaturesRef]);
useMapEventListener(event => {
if (event.name === Commands.signaturesUpdated && String(event.data) === String(systemId)) {
handleGetSignatures();
return true;
}
});
useEffect(() => {
if (!systemId) {
setSignatures([]);
return;
}
handleGetSignatures();
}, [systemId, handleGetSignatures, setSignatures]);
useEffect(() => {
onCountChange?.(signatures.length);
}, [signatures, onCountChange]);
return {
signatures,
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,
handleSelectAll,
handlePaste,
};
}

View File

@@ -24,6 +24,12 @@ export const renderInfoColumn = (row: SystemSignature) => {
</WdTooltipWrapper>
)}
{customInfo.isCrit && (
<WdTooltipWrapper offset={5} position={TooltipPosition.top} content="Signature marked as Crit">
<div className="pi pi-clock text-fuchsia-400 text-[11px] mr-[2px]"></div>
</WdTooltipWrapper>
)}
{row.type && (
<WHClassView
className="text-[11px]"

View File

@@ -3,7 +3,12 @@ import { Dialog } from 'primereact/dialog';
import { useCallback, useMemo, useState } from 'react';
import { TabPanel, TabView } from 'primereact/tabview';
import { PrettySwitchbox } from './components';
import { InterfaceStoredSettingsProps, useMapRootState, InterfaceStoredSettings } from '@/hooks/Mapper/mapRootProvider';
import {
InterfaceStoredSettingsProps,
useMapRootState,
InterfaceStoredSettings,
AvailableThemes
} from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { Dropdown } from 'primereact/dropdown';
import { WidgetsSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components/WidgetsSettings/WidgetsSettings.tsx';
@@ -112,8 +117,8 @@ const UI_CHECKBOXES_PROPS: SettingsListItem[] = [
];
const THEME_OPTIONS = [
{ label: 'Default', value: 'default' },
{ label: 'Pathfinder', value: 'pathfinder' },
{ label: 'Default', value: AvailableThemes.default },
{ label: 'Pathfinder', value: AvailableThemes.pathfinder },
];
const THEME_SETTING: SettingsListItem = {

View File

@@ -1,5 +1,5 @@
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { getWidgetsCheckboxesProps, WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
import { WIDGETS_CHECKBOXES_PROPS, WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback } from 'react';
@@ -9,20 +9,17 @@ export interface WidgetsSettingsProps {}
// eslint-disable-next-line no-empty-pattern
export const WidgetsSettings = ({}: WidgetsSettingsProps) => {
const { windowsSettings, toggleWidgetVisibility, resetWidgets, data } = useMapRootState();
const { windowsSettings, toggleWidgetVisibility, resetWidgets } = useMapRootState();
const handleWidgetSettingsChange = useCallback(
(widget: WidgetsIds) => toggleWidgetVisibility(widget),
[toggleWidgetVisibility],
);
const detailedKillsDisabled = data.options?.detailedKillsDisabled === true;
const widgetProps = getWidgetsCheckboxesProps(detailedKillsDisabled);
return (
<div className="flex flex-col h-full gap-2">
<div>
{widgetProps.map(widget => (
{WIDGETS_CHECKBOXES_PROPS.map(widget => (
<PrettySwitchbox
key={widget.id}
label={widget.label}

View File

@@ -17,7 +17,7 @@ export interface MapSettingsProps {
systemId: string;
show: boolean;
onHide: () => void;
signatureData: SystemSignature | null;
signatureData: SystemSignature | undefined;
}
export const SignatureSettings = ({ systemId, show, onHide, signatureData }: MapSettingsProps) => {

View File

@@ -33,7 +33,7 @@ export const MapWrapper = () => {
const {
update,
outCommand,
data: { selectedConnections, selectedSystems, hubs, systems, connections, linkSignatureToSystem },
data: { selectedConnections, selectedSystems, hubs, systems, linkSignatureToSystem },
interfaceSettings: {
isShowMenu,
isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap,
@@ -56,8 +56,8 @@ export const MapWrapper = () => {
const [openAddSystem, setOpenAddSystem] = useState<XYPosition | null>(null);
const [selectedConnection, setSelectedConnection] = useState<SolarSystemConnection | null>(null);
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, connections, deleteSystems });
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, connections, deleteSystems };
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems });
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems };
useMapEventListener(event => {
runCommand(event);
@@ -125,11 +125,6 @@ export const MapWrapper = () => {
setOpenAddSystem(coordinates);
}, []);
const canRemoveConnection = useCallback((connectionId: string) => {
const { connections } = ref.current;
return !connections.some(x => x.id === connectionId);
}, []);
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
async item => {
if (ref.current.systems.some(x => x.system_static_info.solar_system_id === item.value)) {
@@ -166,7 +161,6 @@ export const MapWrapper = () => {
isSoftBackground={isSoftBackground}
theme={theme}
onAddSystem={onAddSystem}
canRemoveConnection={canRemoveConnection}
/>
{openSettings != null && (

View File

@@ -28,6 +28,7 @@
.CharIcon {
border-radius: 0 !important;
border: 1px solid #2b2b2b;
}
.CharRow {

View File

@@ -8,8 +8,8 @@ import { emitMapEvent } from '@/hooks/Mapper/events';
type CharacterCardProps = {
compact?: boolean;
showShipName?: boolean;
showSystem?: boolean;
showShipName?: boolean;
useSystemsCache?: boolean;
} & CharacterTypeRaw &
WithIsOwnCharacter;
@@ -18,12 +18,16 @@ const SHIP_NAME_RX = /u'|'/g;
export const getShipName = (name: string) => {
return name
.replace(SHIP_NAME_RX, '')
.replace(/\\u([\dA-Fa-f]{4})/g, (_, grp) => String.fromCharCode(parseInt(grp, 16)))
.replace(/\\x([\dA-Fa-f]{2})/g, (_, grp) => String.fromCharCode(parseInt(grp, 16)));
.replace(/\\u([\dA-Fa-f]{4})/g, (_, grp) =>
String.fromCharCode(parseInt(grp, 16))
)
.replace(/\\x([\dA-Fa-f]{2})/g, (_, grp) =>
String.fromCharCode(parseInt(grp, 16))
);
};
export const CharacterCard = ({
compact,
compact = false,
isOwn,
showSystem,
showShipName,
@@ -37,59 +41,105 @@ export const CharacterCard = ({
});
}, [char]);
return (
<div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')} onClick={handleSelect}>
<div className="flex px-2 py-1 gap-1">
{!compact && (
const shipNameText = char.ship?.ship_name ? getShipName(char.ship.ship_name) : '';
const tickerText = char.alliance_id ? char.alliance_ticker : char.corporation_ticker;
const shipType = char.ship?.ship_type_info?.name;
const locationShown = showSystem && char.location?.solar_system_id;
if (compact) {
return (
<div
className={clsx(classes.CharacterCard, 'w-full text-xs box-border')}
onClick={handleSelect}
>
<div className="w-full px-2 py-1 flex items-center gap-2" style={{ minWidth: 0 }}>
<img
src={`https://images.evetech.net/characters/${char.eve_id}/portrait`}
alt={`${char.name} portrait`}
style={{
width: '18px',
height: '18px',
borderRadius: 0,
flexShrink: 0,
border: '1px solid #2b2b2b',
}}
/>
<div className="flex flex-grow overflow-hidden text-left" style={{ minWidth: 0 }}>
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
<span className={clsx(isOwn ? 'text-orange-400' : 'text-gray-200')}>
{char.name}
</span>{" "}
<span className="text-gray-400">
{(!locationShown && showShipName && shipNameText)
? `- ${shipNameText}`
: `[${tickerText}]`}
</span>
</div>
</div>
{shipType && (
<div
className="text-gray-300 overflow-hidden text-ellipsis whitespace-nowrap flex-shrink-0"
style={{ maxWidth: '120px' }}
title={shipType}
>
{shipType}
</div>
)}
</div>
</div>
);
} else {
return (
<div
className={clsx(classes.CharacterCard, 'w-full text-xs box-border')}
onClick={handleSelect}
>
<div className="w-full px-2 py-1 flex items-center gap-2" style={{ minWidth: 0 }}>
<span
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
style={{
backgroundImage: `url(https://images.evetech.net/characters/${char.eve_id}/portrait)`,
minWidth: '33px',
minHeight: '33px',
width: '33px',
height: '33px',
}}
/>
)}
<div className="flex flex-col flex-grow">
<div
className={clsx(classes.CharRow, 'w-full', {
[classes.TwoColumns]: !char.ship,
[classes.ThreeColumns]: char.ship,
})}
>
<span
className={clsx(classes.CharName, 'text-ellipsis overflow-hidden whitespace-nowrap', {
[classes.CardBorderLeftIsOwn]: isOwn,
})}
title={char.name}
>
{char.name}
</span>
{char.alliance_id && <span className="text-gray-400">[{char.alliance_ticker}]</span>}
{!char.alliance_id && <span className="text-gray-400">[{char.corporation_ticker}]</span>}
{char.ship?.ship_type_info && (
<div
className="flex-grow text-ellipsis overflow-hidden whitespace-nowrap"
title={char.ship.ship_type_info.name}
>
{char.ship.ship_type_info.name}
<div className="flex flex-col flex-grow overflow-hidden" style={{ minWidth: 0 }}>
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
<span className={clsx(isOwn ? 'text-orange-400' : 'text-gray-200')}>
{char.name}
</span>{" "}
<span className="text-gray-400">[{tickerText}]</span>
</div>
{locationShown ? (
<div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap">
<SystemView
systemId={char?.location?.solar_system_id?.toString() || ''}
useSystemsCache={useSystemsCache}
/>
</div>
) : (
shipNameText && (
<div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap">
{shipNameText}
</div>
)
)}
</div>
{showShipName && !compact && char.ship?.ship_name && (
<div className="grid w-full">
<span className="text-ellipsis overflow-hidden whitespace-nowrap">
{getShipName(char.ship.ship_name)}
</span>
{shipType && (
<div className="flex-shrink-0 self-start">
<div
className="text-gray-300 overflow-hidden text-ellipsis whitespace-nowrap"
style={{ maxWidth: '200px' }}
title={shipType}
>
{shipType}
</div>
</div>
)}
{showSystem && !compact && char.location?.solar_system_id && (
<SystemView systemId={char.location.solar_system_id.toString()} useSystemsCache={useSystemsCache} />
)}
</div>
</div>
</div>
);
);
}
};

View File

@@ -2,12 +2,12 @@ import classes from './WdCheckbox.module.scss';
import { Checkbox, CheckboxChangeEvent } from 'primereact/checkbox';
import { WithClassName } from '@/hooks/Mapper/types/common';
import clsx from 'clsx';
import { useMemo } from 'react';
import React, { useMemo } from 'react';
let counter = 0;
export interface WdCheckboxProps {
label: string;
label: React.ReactNode | string;
classNameLabel?: string;
value: boolean;
labelSide?: 'left' | 'right';

View File

@@ -0,0 +1,67 @@
import React from 'react';
import clsx from 'clsx';
import { WdCheckbox, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
/**
* Display modes for the responsive checkbox.
*
* - "full": show the full label (e.g. "Show offline" or "Show ship name")
* - "abbr": show the abbreviated label (e.g. "Offline" or "Ship name")
* - "checkbox": show only the checkbox (no text)
* - "hide": do not render the checkbox at all
*/
export type WdDisplayMode = 'full' | 'abbr' | 'checkbox' | 'hide';
export interface WdResponsiveCheckboxProps {
tooltipContent: string;
size: 'xs' | 'normal' | 'm';
labelFull: string;
labelAbbreviated: string;
value: boolean;
onChange: () => void;
classNameLabel?: string;
containerClassName?: string;
labelSide?: 'left' | 'right';
displayMode: WdDisplayMode;
}
export const WdResponsiveCheckbox: React.FC<WdResponsiveCheckboxProps> = ({
tooltipContent,
size,
labelFull,
labelAbbreviated,
value,
onChange,
classNameLabel,
containerClassName,
labelSide = 'left',
displayMode,
}) => {
if (displayMode === 'hide') {
return null;
}
const label =
displayMode === 'full'
? labelFull
: displayMode === 'abbr'
? labelAbbreviated
: displayMode === 'checkbox'
? ''
: labelFull;
const checkbox = (
<div className={clsx('min-w-0', containerClassName)}>
<WdCheckbox
size={size}
labelSide={labelSide}
label={label}
value={value}
classNameLabel={classNameLabel}
onChange={onChange}
/>
</div>
);
return tooltipContent ? <WdTooltipWrapper content={tooltipContent}>{checkbox}</WdTooltipWrapper> : checkbox;
};

View File

@@ -0,0 +1 @@
export * from './WdResponsiveCheckbox';

View File

@@ -12,7 +12,7 @@ export enum TooltipPosition {
bottom = 'bottom',
}
export interface TooltipProps {
export interface TooltipProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'content'> {
position?: TooltipPosition;
offset?: number;
content: (() => React.ReactNode) | React.ReactNode;
@@ -27,183 +27,270 @@ export interface OffsetPosition {
export interface WdTooltipHandlers {
show: (e?: React.MouseEvent) => void;
hide: (e?: React.MouseEvent) => void;
hide: () => void;
getIsMouseInside: () => boolean;
}
export const WdTooltip = forwardRef(
(props: TooltipProps & { className?: string }, ref: ForwardedRef<WdTooltipHandlers>) => {
const {
content,
targetSelector,
position: tPosition = TooltipPosition.default,
className,
offset = 5,
interactive = false,
} = props;
interface TriggerInfo {
clientX: number;
clientY: number;
rect: DOMRect;
}
const [visible, setVisible] = useState(false);
const [pos, setPos] = useState<OffsetPosition | null>(null);
const [ev, setEv] = useState<React.MouseEvent>();
const tooltipRef = useRef<HTMLDivElement>(null);
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
const LEAVE_DELAY = 100;
const calcTooltipPosition = useCallback(({ x, y }: { x: number; y: number }) => {
if (!tooltipRef.current) return { left: x, top: y };
const tooltipWidth = tooltipRef.current.offsetWidth;
const tooltipHeight = tooltipRef.current.offsetHeight;
let newLeft = x;
let newTop = y;
export const WdTooltip = forwardRef(function WdTooltip(
{
content,
targetSelector,
position: tPosition = TooltipPosition.default,
offset = 5,
interactive = false,
className,
...restProps
}: TooltipProps,
ref: ForwardedRef<WdTooltipHandlers>,
) {
// Always initialize position so we never have a null value.
const [visible, setVisible] = useState(false);
const [pos, setPos] = useState<OffsetPosition | null>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
if (newLeft < 0) newLeft = 10;
if (newTop < 0) newTop = 10;
if (newLeft + tooltipWidth + 10 > window.innerWidth) {
newLeft = window.innerWidth - tooltipWidth - 10;
}
if (newTop + tooltipHeight + 10 > window.innerHeight) {
newTop = window.innerHeight - tooltipHeight - 10;
}
return { left: newLeft, top: newTop };
}, []);
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
useImperativeHandle(ref, () => ({
show: (mouseEvt?: React.MouseEvent) => {
if (mouseEvt) setEv(mouseEvt);
setPos(null);
setVisible(true);
},
hide: () => {
const [triggerInfo, setTriggerInfo] = useState<TriggerInfo | null>(null);
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const calcTooltipPosition = useCallback(({ x, y }: { x: number; y: number }) => {
if (!tooltipRef.current) return { left: x, top: y };
const tooltipWidth = tooltipRef.current.offsetWidth;
const tooltipHeight = tooltipRef.current.offsetHeight;
let newLeft = x;
let newTop = y;
if (newLeft < 0) {
newLeft = 10;
}
if (newTop < 0) {
newTop = 10;
}
const rightEdge = newLeft + tooltipWidth + 10;
if (rightEdge > window.innerWidth) {
newLeft = window.innerWidth - tooltipWidth - 10;
}
const bottomEdge = newTop + tooltipHeight + 10;
if (bottomEdge > window.innerHeight) {
newTop = window.innerHeight - tooltipHeight - 10;
}
return { left: newLeft, top: newTop };
}, []);
const scheduleHide = useCallback(() => {
if (!interactive) {
setVisible(false);
setPos(null);
return;
}
if (!hideTimeoutRef.current) {
hideTimeoutRef.current = setTimeout(() => {
setVisible(false);
},
getIsMouseInside: () => isMouseInsideTooltip,
}));
setPos(null);
}, LEAVE_DELAY);
}
}, [interactive]);
useEffect(() => {
if (!tooltipRef.current || !ev) return;
const tooltipEl = tooltipRef.current;
const { clientX, clientY, target } = ev;
const targetBounds = (target as HTMLElement).getBoundingClientRect();
let offsetX = clientX;
let offsetY = clientY;
if (tPosition === TooltipPosition.left) {
const tooltipBounds = tooltipEl.getBoundingClientRect();
offsetX = targetBounds.left - tooltipBounds.width - offset;
offsetY = targetBounds.y + targetBounds.height / 2 - tooltipBounds.height / 2;
if (offsetX <= 0) {
offsetX = targetBounds.left + targetBounds.width + offset;
}
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
return;
useImperativeHandle(ref, () => ({
show: (e?: React.MouseEvent) => {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
if (tPosition === TooltipPosition.right) {
offsetX = targetBounds.left + targetBounds.width + offset;
offsetY = targetBounds.y + targetBounds.height / 2 - tooltipEl.offsetHeight / 2;
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
return;
}
if (tPosition === TooltipPosition.top) {
offsetY = targetBounds.top - tooltipEl.offsetHeight - offset;
offsetX = targetBounds.x + targetBounds.width / 2 - tooltipEl.offsetWidth / 2;
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
return;
}
if (tPosition === TooltipPosition.bottom) {
offsetY = targetBounds.bottom + offset;
offsetX = targetBounds.x + targetBounds.width / 2 - tooltipEl.offsetWidth / 2;
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
return;
}
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
}, [calcTooltipPosition, ev, tPosition, offset]);
useEffect(() => {
if (!targetSelector) return;
function handleMouseMove(nativeEvt: globalThis.MouseEvent) {
const targetEl = nativeEvt.target as HTMLElement | null;
if (!targetEl) {
setVisible(false);
return;
}
const triggerEl = targetEl.closest(targetSelector!);
const isInsideTooltip = interactive && tooltipRef.current?.contains(targetEl);
if (!triggerEl && !isInsideTooltip) {
setVisible(false);
return;
}
setVisible(true);
if (triggerEl && tooltipRef.current) {
if (e) {
// Use e.currentTarget (or fallback to e.target) to determine the trigger element.
const triggerEl = (e.currentTarget as HTMLElement) || (e.target as HTMLElement);
if (triggerEl) {
const rect = triggerEl.getBoundingClientRect();
const tooltipEl = tooltipRef.current;
let x = nativeEvt.clientX;
let y = nativeEvt.clientY;
setTriggerInfo({ clientX: e.clientX, clientY: e.clientY, rect });
setPos(calcTooltipPosition({ x: e.clientX, y: e.clientY }));
}
}
setVisible(true);
},
hide: () => {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
}
setVisible(false);
setPos(null);
},
getIsMouseInside: () => isMouseInsideTooltip,
}));
if (tPosition === TooltipPosition.left) {
useEffect(() => {
if (!tooltipRef.current || !triggerInfo) return;
const tooltipEl = tooltipRef.current;
const { rect } = triggerInfo;
let x = triggerInfo.clientX;
let y = triggerInfo.clientY;
if (tPosition === TooltipPosition.left) {
const tooltipBounds = tooltipEl.getBoundingClientRect();
x = rect.left - tooltipBounds.width - offset;
y = rect.top + rect.height / 2 - tooltipBounds.height / 2;
if (x <= 0) {
x = rect.left + rect.width + offset;
}
setPos(calcTooltipPosition({ x, y }));
return;
}
if (tPosition === TooltipPosition.right) {
x = rect.left + rect.width + offset;
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
setPos(calcTooltipPosition({ x, y }));
return;
}
if (tPosition === TooltipPosition.top) {
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
y = rect.top - tooltipEl.offsetHeight - offset;
setPos(calcTooltipPosition({ x, y }));
return;
}
if (tPosition === TooltipPosition.bottom) {
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
y = rect.bottom + offset;
setPos(calcTooltipPosition({ x, y }));
return;
}
// Default case: use stored coordinates.
setPos(calcTooltipPosition({ x, y }));
}, [calcTooltipPosition, triggerInfo, tPosition, offset]);
useEffect(() => {
if (!targetSelector) return;
const handleMouseMove = (evt: MouseEvent) => {
const targetEl = evt.target as HTMLElement | null;
if (!targetEl) {
scheduleHide();
return;
}
const triggerEl = targetEl.closest(targetSelector);
const insideTooltip = interactive && tooltipRef.current?.contains(targetEl);
if (!triggerEl && !insideTooltip) {
scheduleHide();
return;
}
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
setVisible(true);
if (triggerEl && tooltipRef.current) {
const rect = triggerEl.getBoundingClientRect();
const tooltipEl = tooltipRef.current;
let x = evt.clientX;
let y = evt.clientY;
switch (tPosition) {
case TooltipPosition.left:
x = rect.left - tooltipEl.offsetWidth - offset;
y = rect.y + rect.height / 2 - tooltipEl.offsetHeight / 2;
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
if (x <= 0) {
x = rect.left + rect.width + offset;
}
} else if (tPosition === TooltipPosition.right) {
break;
case TooltipPosition.right:
x = rect.left + rect.width + offset;
y = rect.y + rect.height / 2 - tooltipEl.offsetHeight / 2;
} else if (tPosition === TooltipPosition.top) {
x = rect.x + rect.width / 2 - tooltipEl.offsetWidth / 2;
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
break;
case TooltipPosition.top:
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
y = rect.top - tooltipEl.offsetHeight - offset;
} else if (tPosition === TooltipPosition.bottom) {
x = rect.x + rect.width / 2 - tooltipEl.offsetWidth / 2;
break;
case TooltipPosition.bottom:
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
y = rect.bottom + offset;
}
setPos(calcTooltipPosition({ x, y }));
break;
}
setPos(calcTooltipPosition({ x, y }));
}
};
const debounced = debounce(handleMouseMove, 10);
const debounced = debounce(handleMouseMove, 15);
const listener: EventListener = evt => {
debounced(evt as globalThis.MouseEvent);
};
document.addEventListener('mousemove', debounced);
return () => {
document.removeEventListener('mousemove', debounced);
debounced.cancel();
};
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition, scheduleHide]);
document.addEventListener('mousemove', listener);
return () => {
document.removeEventListener('mousemove', listener);
};
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition]);
useEffect(() => {
return () => {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
}
};
}, []);
return createPortal(
visible && (
<div
ref={tooltipRef}
className={clsx(
classes.tooltip,
interactive ? 'pointer-events-auto' : 'pointer-events-none',
'absolute p-1 border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
pos === null ? 'invisible' : '',
className,
)}
style={{
top: pos?.top ?? 0,
left: pos?.left ?? 0,
zIndex: 10000,
}}
onMouseEnter={() => interactive && setIsMouseInsideTooltip(true)}
onMouseLeave={() => interactive && setIsMouseInsideTooltip(false)}
>
{typeof content === 'function' ? content() : content}
</div>
),
document.body,
);
},
);
if (!visible) return null;
return createPortal(
<div
ref={tooltipRef}
className={clsx(
classes.tooltip,
interactive ? 'pointer-events-auto' : 'pointer-events-none',
'absolute p-1 border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
className,
pos === null ? 'invisible' : '',
)}
style={{
top: pos?.top ?? 0,
left: pos?.left ?? 0,
zIndex: 10000,
}}
onMouseEnter={() => {
if (interactive && hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
setIsMouseInsideTooltip(true);
}}
onMouseLeave={() => {
setIsMouseInsideTooltip(false);
if (interactive) {
scheduleHide();
}
}}
{...restProps}
>
{typeof content === 'function' ? content() : content}
</div>,
document.body,
);
});
WdTooltip.displayName = 'WdTooltip';

View File

@@ -1,3 +1,25 @@
/* WdTooltipWrapper.module.scss */
.WdTooltipWrapperRoot {
display: inline-block;
}
.wdTooltipSizeXs {
font-size: 0.7rem;
max-width: 150px;
}
.wdTooltipSizeSm {
font-size: 0.8rem;
max-width: 200px;
}
.wdTooltipSizeMd {
font-size: 0.9rem;
max-width: 250px;
}
.wdTooltipSizeLg {
font-size: 1rem !important;
min-width: 350px;
}

View File

@@ -1,4 +1,4 @@
import { forwardRef, HTMLProps, ReactNode } from 'react';
import { forwardRef, HTMLProps, ReactNode, useMemo } from 'react';
import clsx from 'clsx';
import { WdTooltip, WdTooltipHandlers, TooltipProps } from '@/hooks/Mapper/components/ui-kit';
import classes from './WdTooltipWrapper.module.scss';
@@ -13,11 +13,8 @@ export type WdTooltipWrapperProps = {
Omit<TooltipProps, 'content'>;
export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperProps>(
(
{ className, children, content, offset, position, targetSelector, interactive = false, size, ...props },
forwardedRef,
) => {
const suffix = Math.random().toString(36).slice(2, 7);
({ className, children, content, offset, position, targetSelector, interactive, size, ...props }, forwardedRef) => {
const suffix = useMemo(() => Math.random().toString(36).slice(2, 7), []);
const autoClass = `wdTooltipAutoTrigger-${suffix}`;
const finalTargetSelector = targetSelector || `.${autoClass}`;

View File

@@ -11,3 +11,5 @@ export * from './WdImgButton';
export * from './WdTooltip';
export * from './WdCheckbox';
export * from './TimeAgo';
export * from './WdTooltipWrapper';
export * from './WdResponsiveCheckBox';

View File

@@ -2,12 +2,13 @@ import { WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constan
import { WormholeDataRaw } from '@/hooks/Mapper/types';
export const sortWHClasses = (wormholesData: Record<string, WormholeDataRaw>, statics: string[]) => {
if (!statics) {
if (!statics || !wormholesData) {
return [];
}
return statics
.map(x => wormholesData[x])
.filter(x => !!x)
.map(x => ({ name: x.name, ...WORMHOLES_ADDITIONAL_INFO[x.dest] }))
.sort((a, b) => a.wormholeClassID - b.wormholeClassID)
.map(x => x.name);

View File

@@ -0,0 +1,7 @@
import { AvailableThemes, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const useTheme = (): AvailableThemes => {
const { interfaceSettings } = useMapRootState();
return interfaceSettings.theme;
};

View File

@@ -42,9 +42,15 @@ const INITIAL_DATA: MapRootData = {
selectedConnections: [],
userPermissions: {},
options: {},
isSubscriptionActive: false,
linkSignatureToSystem: null,
};
export enum AvailableThemes {
default = 'default',
pathfinder = 'pathfinder',
}
export enum InterfaceStoredSettingsProps {
isShowMenu = 'isShowMenu',
isShowMinimap = 'isShowMinimap',
@@ -64,7 +70,7 @@ export type InterfaceStoredSettings = {
isShowUnsplashedSignatures: boolean;
isShowBackgroundPattern: boolean;
isSoftBackground: boolean;
theme: string;
theme: AvailableThemes;
};
export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
@@ -75,7 +81,7 @@ export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
isShowUnsplashedSignatures: false,
isShowBackgroundPattern: true,
isSoftBackground: false,
theme: 'default',
theme: AvailableThemes.default,
};
export interface MapRootContextProps {

View File

@@ -21,6 +21,7 @@ export const useMapInit = () => {
hubs,
user_permissions,
options,
is_subscription_active,
}: CommandInit) => {
const updateData: Partial<MapRootData> = {};
@@ -65,6 +66,8 @@ export const useMapInit = () => {
updateData.options = options;
}
updateData.isSubscriptionActive = is_subscription_active;
if (system_static_infos) {
system_static_infos.forEach(static_info => {
addSystemStatic(static_info);

View File

@@ -21,7 +21,6 @@ export interface DetailedKill {
victim_ship_type_id?: number | null;
victim_ship_name?: string;
final_blow_char_id?: number | null;
final_blow_char_name?: string;
final_blow_corp_id?: number | null;

View File

@@ -66,6 +66,7 @@ export type CommandInit = {
routes: RoutesList;
options: Record<string, string | boolean>;
reset?: boolean;
is_subscription_active?: boolean;
};
export type CommandAddSystems = SolarSystemRawType[];
export type CommandUpdateSystems = SolarSystemRawType[];

View File

@@ -22,4 +22,5 @@ export type MapUnionTypes = {
connections: SolarSystemConnection[];
userPermissions: Partial<UserPermissions>;
options: Record<string, string | boolean>;
isSubscriptionActive: boolean;
};

Some files were not shown because too many files have changed in this diff Show More