Compare commits

...

57 Commits

Author SHA1 Message Date
achichenkov
ef26d7129a fix(Map): Fix icons of main, follow and shattered 2025-04-13 17:08:15 +03:00
Dmitry Popov
602a61b08d chore: release version v1.59.4
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-04-12 02:14:17 +02:00
Aleksei Chichenkov
d8222d83f0 Refactoring and fixing problems (#317)
* fix(Map): fix design of kills widget, fix design of signatures widget - refactor a lot of code, fixed problem with tooltip blinking

* fix(Map): refactor Tracking dialog, refactor Activity tracker, refactor codebase and some styles

* fix(Core): don't count character passage on manual add connection

* refactor(Core): improved characters tracking API

* fix(Core): fixed link signature to system on 'leads to' set

* fix(Map): Refactor map settings and prepared it to easier using

* fix(Map): Add support new command for following update

* fix(Map): Add support new command for main update

* refactor(Core): Reduce map init data by using cached system static data

* refactor(Core): Reduce map init data by extract signatures loading to a separate event

* fix(Core): adjusted IP rate limits

* fix(Map): Update design of tracking characters. Added icons for following and main. Added ability to see that character on the station or structure

---------

Co-authored-by: achichenkov <aleksei.chichenkov@telleqt.ai>
Co-authored-by: Dmitry Popov <dmitriypopovsamara@gmail.com>
2025-04-11 23:17:53 +04:00
CI
7da5512d45 chore: release version v1.59.4
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-04-10 22:07:50 +00:00
Dmitry Popov
8bf9ae7824 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-04-10 23:58:55 +02:00
Dmitry Popov
f57777e417 chore: release version v1.59.2 2025-04-10 23:58:52 +02:00
CI
b3cc3d857a chore: release version v1.59.3 2025-04-10 21:50:32 +00:00
Dmitry Popov
bf442d9e70 chore: release version v1.59.2 2025-04-10 23:42:06 +02:00
windstep
1a556d05ba fixed error in different localization (#312) 2025-04-11 01:41:00 +04:00
CI
dab301e6d3 chore: release version v1.59.2 2025-04-10 21:40:39 +00:00
Dmitry Popov
8ab4b4c788 fix (Core): fixed connection validation 2025-04-10 23:25:33 +02:00
CI
8a5f96a847 chore: release version v1.59.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-03-26 21:50:35 +00:00
guarzo
149fa57075 fix (doc): improve bot setup instructions (#309) 2025-03-27 01:41:21 +04:00
CI
affe184ccd chore: release version v1.59.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-03-23 10:26:30 +00:00
Dmitry Popov
1e5e73c4ae feat(Core): added handling cases when wrong connections created 2025-03-23 11:06:20 +01:00
Tyson GG
c76316da03 feat (api) add map connections endpoint (#301)
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-03-23 00:43:32 +04:00
CI
de6205f860 chore: release version v1.58.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-03-22 08:38:37 +00:00
Dmitry Popov
f994255091 feat(Core): Show online state on map characters page 2025-03-22 09:29:13 +01:00
Tyson GG
6d4981a3db fix (routes) fix query parameter formatting when calling esi routes endpoint (#302) 2025-03-22 11:53:12 +04:00
guarzo
06fef2296f feat (api): update character activity and api to allow date range (#299)
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
* feat (api): update character activity and api to allow date range
2025-03-21 21:05:48 +04:00
CI
999a702291 chore: release version v1.57.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-03-20 17:51:07 +00:00
Dmitry Popov
020b9bb2c2 chore: added user-agent & ensured cache handled correctly on each request 2025-03-20 18:39:40 +01:00
CI
7713caab51 chore: release version v1.57.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-03-19 15:33:39 +00:00
guarzo
97a777d729 feat (doc): update bot news (#294) 2025-03-19 19:17:25 +04:00
CI
8241d1f08c chore: release version v1.56.6 2025-03-19 14:45:22 +00:00
Dmitry Popov
2ac85bbfff chore: release version v1.56.5 2025-03-19 15:08:51 +01:00
CI
3f68ae2235 chore: release version v1.56.5
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 / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-19 13:07:55 +00:00
Dmitry Popov
0f7b6f75df Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-03-19 13:57:58 +01:00
Dmitry Popov
b048e8f5ca chore: added fallback chipher options 2025-03-19 13:55:39 +01:00
CI
9783dc45ff chore: release version v1.56.4 2025-03-19 11:36:46 +00:00
Dmitry Popov
badbefbade Revert "fix: cloak key error behavior (#288)" (#290)
This reverts commit 9b5ea2f84b.
2025-03-19 15:30:07 +04:00
CI
b6a265cfad chore: release version v1.56.3 2025-03-19 07:26:24 +00:00
guarzo
9b5ea2f84b fix: cloak key error behavior (#288) 2025-03-19 11:13:54 +04:00
guarzo
d8acfa5c05 refactor: standalone unit tests (#278)
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-03-18 21:37:52 +04:00
CI
2a5b6924eb chore: release version v1.56.2 2025-03-18 16:47:40 +00:00
Dmitry Popov
3b9aee1eb9 fix: show signature tooltip on top 2025-03-18 17:33:18 +01:00
CI
83801c9063 chore: release version v1.56.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-03-18 09:34:32 +00:00
guarzo
0f34350c58 fix: update activity api (#284) 2025-03-18 11:51:29 +04:00
guarzo
1c4c0f0715 fix: qol updates for dev (#283) 2025-03-18 11:50:33 +04:00
Dmitry Popov
3825fc831a refactor: removal of legacy event
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-03-17 22:51:29 +01:00
guarzo
654670cbc8 refactor: removal of legacy event (#277)
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-03-18 00:11:32 +04:00
CI
947570072c chore: release version v1.56.0 2025-03-17 18:25:38 +00:00
Dmitry Popov
01b6b45380 fix: character activity hide error 2025-03-17 18:54:22 +01:00
guarzo
b9dc1f8357 fix: character added to map on follow (#272) 2025-03-17 19:42:42 +04:00
guarzo
b4bd810c9d refactor: updates to track and follow (#270) 2025-03-17 17:36:08 +04:00
guarzo
490b037920 refactor: simplify track and follow (#265)
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-03-17 16:08:45 +04:00
guarzo
cdff5458bc feat: add static wh info (#262)
* feat: add static wh info
2025-03-17 16:03:57 +04:00
guarzo
09314a09e9 feat [doc]: new bot release (#234) 2025-03-17 11:43:27 +04:00
guarzo
49ea8edb27 feat (api): add character activity api (#263)
* feat (api): add character activity api
2025-03-17 11:36:45 +04:00
CI
86e5ff2fff chore: release version v1.55.2
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-03-16 20:16:11 +00:00
Dmitry Popov
1ade0ae6b9 Merge branch 'main' of github.com:wanderer-industries/wanderer
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-03-16 20:50:02 +01:00
Dmitry Popov
cadfb59b8d fix(Core): fixed lazy delete reset state 2025-03-16 20:49:58 +01:00
CI
5c2013f19c chore: release version v1.55.1 2025-03-16 12:48:21 +00:00
Dmitry Popov
8db46113f4 fix(Core): fixed lazy delete timeouts 2025-03-16 13:32:39 +01:00
Dmitry Popov
3028a0b1c0 Merge branch 'main' of github.com:wanderer-industries/wanderer
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-03-15 23:45:01 +01:00
Dmitry Popov
e9b4e39061 fix(Core): fixed lazy delete settings 2025-03-15 23:44:57 +01:00
guarzo
9c6715e4e5 fix: keep character api off by default (#258)
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-03-15 22:38:50 +04:00
186 changed files with 25366 additions and 4167 deletions

View File

@@ -1,6 +1,27 @@
FROM elixir:1.17-otp-27
RUN apt install -yq curl gnupg
# Install OS packages and Node.js (via nodesource),
# plus inotify-tools and yarn
RUN apt-get update && apt-get install -y --no-install-recommends \
sudo \
curl \
make \
git \
bash \
build-essential \
ca-certificates \
jq \
vim \
net-tools \
procps \
# Optionally add any other tools you need, e.g. vim, wget...
&& curl -sL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y --no-install-recommends nodejs inotify-tools \
&& npm install -g yarn \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN apt --fix-broken install
RUN mix local.hex --force

View File

@@ -1,20 +1,30 @@
{
"name": "wanderer-dev",
"dockerComposeFile": ["./docker-compose.yml"],
"extensions": [
"jakebecker.elixir-ls",
"JakeBecker.elixir-ls",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
],
"customizations": {
"vscode": {
"extensions": [
"jakebecker.elixir-ls",
"JakeBecker.elixir-ls",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
],
"settings": {
"editor.formatOnSave": true,
"search.exclude": {
"**/doc": true
},
"elixirLS.dialyzerEnabled": false
}
}
},
"service": "wanderer",
"workspaceFolder": "/app",
"shutdownAction": "stopCompose",
"settings": {
"editor.formatOnSave": true,
"search.exclude": {
"**/doc": true
},
"elixirLS.dialyzerEnabled": false
}
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"networkArgs": ["--add-host=host.docker.internal:host-gateway"]
}
},
"forwardPorts": [4444]
}

View File

@@ -14,15 +14,15 @@ services:
wanderer:
environment:
PORT: 8000
PORT: 4444
DB_HOST: db
WEB_APP_URL: "http://localhost:8000"
WEB_APP_URL: "http://localhost:4444"
ERL_AFLAGS: "-kernel shell_history enabled"
build:
context: .
dockerfile: Dockerfile
ports:
- 8000:8000
- 4444:4444
volumes:
- ..:/app:delegated
- ~/.gitconfig:/root/.gitconfig

View File

@@ -2,6 +2,157 @@
<!-- changelog -->
## [v1.59.4](https://github.com/wanderer-industries/wanderer/compare/v1.59.3...v1.59.4) (2025-04-10)
## [v1.59.3](https://github.com/wanderer-industries/wanderer/compare/v1.59.2...v1.59.3) (2025-04-10)
## [v1.59.2](https://github.com/wanderer-industries/wanderer/compare/v1.59.1...v1.59.2) (2025-04-10)
### Bug Fixes:
* Core: fixed connection validation
## [v1.59.1](https://github.com/wanderer-industries/wanderer/compare/v1.59.0...v1.59.1) (2025-03-26)
### Bug Fixes:
* doc: improve bot setup instructions (#309)
## [v1.59.0](https://github.com/wanderer-industries/wanderer/compare/v1.58.0...v1.59.0) (2025-03-23)
### Features:
* Core: added handling cases when wrong connections created
## [v1.58.0](https://github.com/wanderer-industries/wanderer/compare/v1.57.1...v1.58.0) (2025-03-22)
### Features:
* Core: Show online state on map characters page
* api: update character activity and api to allow date range (#299)
* api: update character activity and api to allow date range
## [v1.57.1](https://github.com/wanderer-industries/wanderer/compare/v1.57.0...v1.57.1) (2025-03-20)
## [v1.57.0](https://github.com/wanderer-industries/wanderer/compare/v1.56.6...v1.57.0) (2025-03-19)
### Features:
* doc: update bot news (#294)
## [v1.56.6](https://github.com/wanderer-industries/wanderer/compare/v1.56.5...v1.56.6) (2025-03-19)
## [v1.56.5](https://github.com/wanderer-industries/wanderer/compare/v1.56.4...v1.56.5) (2025-03-19)
## [v1.56.4](https://github.com/wanderer-industries/wanderer/compare/v1.56.3...v1.56.4) (2025-03-19)
## [v1.56.3](https://github.com/wanderer-industries/wanderer/compare/v1.56.2...v1.56.3) (2025-03-19)
### Bug Fixes:
* cloak key error behavior (#288)
## [v1.56.2](https://github.com/wanderer-industries/wanderer/compare/v1.56.1...v1.56.2) (2025-03-18)
### Bug Fixes:
* show signature tooltip on top
## [v1.56.1](https://github.com/wanderer-industries/wanderer/compare/v1.56.0...v1.56.1) (2025-03-18)
### Bug Fixes:
* update activity api (#284)
* qol updates for dev (#283)
## [v1.56.0](https://github.com/wanderer-industries/wanderer/compare/v1.55.2...v1.56.0) (2025-03-17)
### Features:
* add static wh info (#262)
* add static wh info
* api: add character activity api (#263)
* api: add character activity api
### Bug Fixes:
* character activity hide error
* character added to map on follow (#272)
## [v1.55.2](https://github.com/wanderer-industries/wanderer/compare/v1.55.1...v1.55.2) (2025-03-16)
### Bug Fixes:
* Core: fixed lazy delete reset state
## [v1.55.1](https://github.com/wanderer-industries/wanderer/compare/v1.55.0...v1.55.1) (2025-03-16)
### Bug Fixes:
* Core: fixed lazy delete timeouts
* Core: fixed lazy delete settings
* keep character api off by default (#258)
## [v1.55.0](https://github.com/wanderer-industries/wanderer/compare/v1.54.1...v1.55.0) (2025-03-15)

View File

@@ -1,4 +1,4 @@
.PHONY: deploy install cleanup start yarn migrate format test coverage versions
.PHONY: deploy install cleanup start yarn migrate format test coverage versions standalone-tests
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
SHELL := /bin/bash
@@ -35,6 +35,11 @@ test t:
coverage cover co:
mix test --cover
unit-tests ut:
@echo "Running unit tests..."
@find test/unit -name "*.exs" -exec elixir {} \;
@echo "All unit tests completed."
versions v:
@echo "Tool Versions"
@cat .tool-versions

View File

@@ -112,19 +112,19 @@ body > div:first-of-type {
}
.wd-characters-icons {
display: flex;
transition:
border-color 250ms,
opacity 250ms;
width: 35px;
height: 35px;
border-radius: 50%;
border-width: 2px;
border-style: solid;
border-color: #5a5a5a;
background-color: rgba(0, 0, 0, 0);
cursor: pointer;
opacity: 0.6;
/*display: flex;*/
/*transition:*/
/* border-color 250ms,*/
/* opacity 250ms;*/
/*width: 35px;*/
/*height: 35px;*/
/*border-radius: 50%;*/
/*border-width: 2px;*/
/*border-style: solid;*/
/*border-color: #5a5a5a;*/
/*background-color: rgba(0, 0, 0, 0);*/
/*cursor: pointer;*/
/*opacity: 0.6;*/
}
.wd-bg-default {

View File

@@ -143,3 +143,40 @@
background: #966d3d;
}
}
.p-datatable-wrapper {
height: 100%;
& {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.5) transparent;
}
&::-webkit-scrollbar {
width: 10px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.5);
border-radius: 5px;
border: 2px solid transparent;
background-clip: content-box;
}
&::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.7);
}
&::-webkit-scrollbar-button {
display: none;
height: 0;
width: 0;
}
}
.p-datatable .p-datatable-tbody > tr.p-highlight {
background: initial;
}

View File

@@ -1,11 +1,8 @@
/* Основной класс диалога */
body .p-dialog {
display: flex;
flex-direction: column;
//position: absolute;
top: 0;
left: 0;
//visibility: hidden;
overflow: hidden;
border-radius: 2px;
box-shadow: 0 2px 10px 0 rgba(0,0,0,0.2);
@@ -29,12 +26,10 @@ body .p-dialog {
}
}
/* Стиль видимого диалога */
.p-dialog-visible {
visibility: visible;
}
/* Анимации */
.p-dialog-enter {
opacity: 0;
}
@@ -53,31 +48,27 @@ body .p-dialog {
transition: opacity 0.3s;
}
/* Заголовок диалога */
.p-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: #f4f4f4;
//border-bottom: 1px solid #ddd;
height: 40px;
}
/* Содержимое диалога */
.p-dialog-content {
padding: 0.5rem;
overflow-y: auto;
flex: 1;
}
/* Подвал диалога */
.p-dialog-footer {
padding: 1rem;
border-top: 1px solid #ddd;
background: #f4f4f4;
}
/* Кнопка закрытия диалога */
.p-dialog-header-close {
display: flex;
align-items: center;
@@ -93,3 +84,12 @@ body .p-dialog {
.p-dialog-header-close .pi {
font-size: 1.25rem;
}
.p-dialog {
.p-dialog-title {
font-size: 1rem !important;
}
.p-dialog-header-icons {
align-self: initial !important;
}
}

View File

@@ -0,0 +1,77 @@
.vertical-tabs-container {
display: flex;
width: 100%;
min-height: 300px;
.p-tabview {
width: 100%;
display: flex;
align-items: flex-start;
}
.p-tabview-panels {
padding: 6px 1rem;
flex-grow: 1;
height: 100%;
}
.p-tabview-nav-container {
border-right: none;
height: 100%;
}
.p-tabview-nav {
flex-direction: column;
width: 150px;
min-height: 100%;
border: none;
li {
width: 100%;
border-right: 4px solid var(--surface-hover);
background-color: var(--surface-card);
transition: background-color 200ms, border-right-color 200ms;
&:hover {
background-color: var(--surface-hover);
border-right: 4px solid var(--surface-100);
}
.p-tabview-nav-link {
transition: color 200ms;
justify-content: flex-end;
padding: 10px;
//background-color: var(--surface-card);
background-color: initial;
border: none;
color: var(--gray-400);
border-radius: initial;
font-weight: 400;
margin: 0;
}
&.p-tabview-selected {
background-color: var(--surface-50);
border-right: 4px solid var(--primary-color);
.p-tabview-nav-link {
font-weight: 600;
color: var(--primary-color);
}
&:hover {
//background-color: var(--surface-hover);
border-right: 4px solid var(--primary-color);
}
}
}
}
.p-tabview-panel {
flex-grow: 1;
}
}

View File

@@ -1,5 +1,6 @@
@import "fix-dialog";
@import "fix-popup";
@import "fix-tabs";
//@import "fix-input";
//@import "theme";

View File

@@ -0,0 +1,18 @@
.Docked {
content: " ";
display: inline-block;
width: 11px;
height: 11px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
position: absolute;
z-index: 1;
overflow: hidden;
border-radius: 1px;
background-image: url(/images/citadelLarge.png);
left: 2px;
top: 22px;
transform: rotateZ(0deg);
}

View File

@@ -4,10 +4,22 @@ import { useAutoAnimate } from '@formkit/auto-animate/react';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import classes from './Characters.module.scss';
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
import { PrimeIcons } from 'primereact/api';
const Characters = ({ data }: { data: CharacterTypeRaw[] }) => {
interface CharactersProps {
data: CharacterTypeRaw[];
}
export const Characters = ({ data }: CharactersProps) => {
const [parent] = useAutoAnimate();
const {
data: { mainCharacterEveId, followingCharacterEveId },
} = useMapRootState();
const handleSelect = useCallback((character: CharacterTypeRaw) => {
emitMapEvent({
name: Commands.centerSystem,
@@ -21,21 +33,58 @@ const Characters = ({ data }: { data: CharacterTypeRaw[] }) => {
className="flex flex-col items-center justify-center"
onClick={() => handleSelect(character)}
>
<div className="tooltip tooltip-bottom" title={character.name}>
<a
className={clsx('wd-characters-icons wd-bg-default', { ['character-online']: character.online })}
<div
className={clsx(
'overflow-hidden relative',
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
'transition-colors duration-250',
{
['border-stone-800/90']: !character.online,
['border-lime-600/70']: character.online,
},
)}
title={character.name}
>
{mainCharacterEveId === character.eve_id && (
<span
className={clsx(
'absolute top-[2px] left-[22px] w-[9px] h-[9px]',
'text-yellow-500 text-[9px] rounded-[1px] z-10',
'pi',
PrimeIcons.STAR_FILL,
)}
/>
)}
{followingCharacterEveId === character.eve_id && (
<span
className={clsx(
'absolute top-[23px] left-[22px] w-[10px] h-[10px]',
'text-sky-300 text-[10px] rounded-[1px] z-10',
'pi pi-angle-double-right',
)}
/>
)}
{isDocked(character.location) && <div className={classes.Docked} />}
<div
className={clsx(
'flex w-full h-full bg-transparent cursor-pointer',
'bg-center bg-no-repeat bg-[length:100%]',
'transition-opacity',
'shadow-[inset_0_1px_6px_1px_#000000]',
{
['opacity-60']: !character.online,
['opacity-100']: character.online,
},
)}
style={{ backgroundImage: `url(https://images.evetech.net/characters/${character.eve_id}/portrait)` }}
></a>
></div>
</div>
</li>
));
return (
<ul className="flex characters" id="characters" ref={parent}>
<ul className="flex gap-1 characters" id="characters" ref={parent}>
{items}
</ul>
);
};
// eslint-disable-next-line react/display-name
export default Characters;

View File

@@ -9,6 +9,7 @@ import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components
import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
export const useContextMenuSystemItems = ({
onDeleteSystem,
@@ -32,6 +33,7 @@ export const useContextMenuSystemItems = ({
return useMemo(() => {
const system = systemId ? getSystemById(systems, systemId) : undefined;
const systemStaticInfo = getSystemStaticInfo(systemId)!;
if (!system || !systemId) {
return [];
@@ -44,9 +46,9 @@ export const useContextMenuSystemItems = ({
return (
<FastSystemActions
systemId={systemId}
systemName={system.system_static_info.solar_system_name}
regionName={system.system_static_info.region_name}
isWH={isWormholeSpace(system.system_static_info.system_class)}
systemName={systemStaticInfo.solar_system_name}
regionName={systemStaticInfo.region_name}
isWH={isWormholeSpace(systemStaticInfo.system_class)}
showEdit
onOpenSettings={onOpenSettings}
/>
@@ -57,7 +59,7 @@ export const useContextMenuSystemItems = ({
getTags(),
getStatus(),
...getLabels(),
...getWaypointMenu(systemId, system.system_static_info.system_class),
...getWaypointMenu(systemId, systemStaticInfo.system_class),
{
label: !hubs.includes(systemId) ? 'Add in Routes' : 'Remove from Routes',
icon: PrimeIcons.MAP_MARKER,

View File

@@ -4,8 +4,8 @@ import { useCallback } from 'react';
import { isPossibleSpace } from '@/hooks/Mapper/components/map/helpers/isKnownSpace.ts';
import { Route } from '@/hooks/Mapper/types/routes.ts';
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { SOLAR_SYSTEM_CLASS_IDS } from '@/hooks/Mapper/components/map/constants.ts';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
const imperialSpace = [SOLAR_SYSTEM_CLASS_IDS.hs, SOLAR_SYSTEM_CLASS_IDS.ls, SOLAR_SYSTEM_CLASS_IDS.ns];
const criminalSpace = [SOLAR_SYSTEM_CLASS_IDS.ls, SOLAR_SYSTEM_CLASS_IDS.ns];
@@ -47,7 +47,7 @@ export const useJumpPlannerMenu = (
return [];
}
const origin = getSystemById(systems, systemIdFrom)?.system_static_info;
const origin = getSystemStaticInfo(systemIdFrom);
if (!origin) {
return [];

View File

@@ -1,7 +1,7 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo } from 'react';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { getSystemStaticInfo } from '../../mapRootProvider/hooks/useLoadSystemStatic';
interface UseSystemInfoProps {
systemId: string;
@@ -12,10 +12,8 @@ export const useSystemInfo = ({ systemId }: UseSystemInfoProps) => {
data: { systems, connections },
} = useMapRootState();
const { systems: systemStatics } = useLoadSystemStatic({ systems: [systemId] });
return useMemo(() => {
const staticInfo = systemStatics.get(parseInt(systemId));
const staticInfo = getSystemStaticInfo(parseInt(systemId));
const dynamicInfo = getSystemById(systems, systemId);
if (!staticInfo || !dynamicInfo) {
@@ -29,5 +27,5 @@ export const useSystemInfo = ({ systemId }: UseSystemInfoProps) => {
.filter(x => x !== systemId);
return { dynamicInfo, staticInfo, leadsTo };
}, [systemStatics, systemId, systems, connections]);
}, [systemId, systems, connections]);
};

View File

@@ -1,14 +1,15 @@
import React, { useMemo } from 'react';
import { SystemKillsContent } from '../../../mapInterface/widgets/SystemKills/SystemKillsContent/SystemKillsContent';
import { useMemo } from 'react';
import { useKillsCounter } from '../../hooks/useKillsCounter';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
import {
KILLS_ROW_HEIGHT,
SystemKillsList,
} from '@/hooks/Mapper/components/mapInterface/widgets/WSystemKills/SystemKillsList';
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
const ITEM_HEIGHT = 35;
const MIN_TOOLTIP_HEIGHT = 40;
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
type KillsBookmarkTooltipProps = {
killsCount: number;
killsActivityType: string | null;
@@ -18,7 +19,13 @@ type KillsBookmarkTooltipProps = {
} & WithChildren &
WithClassName;
export const KillsCounter = ({ killsCount, systemId, className, children, size = 'xs' }: KillsBookmarkTooltipProps) => {
export const KillsCounter = ({
killsCount,
systemId,
className,
children,
size = TooltipSize.xs,
}: KillsBookmarkTooltipProps) => {
const {
isLoading,
kills: detailedKills,
@@ -37,28 +44,24 @@ export const KillsCounter = ({ killsCount, systemId, className, children, size =
}
// Calculate height based on number of kills, but ensure a minimum height
const killsNeededHeight = limitedKills.length * ITEM_HEIGHT;
const killsNeededHeight = limitedKills.length * KILLS_ROW_HEIGHT;
// Add a small buffer (10px) to prevent scrollbar from appearing unnecessarily
const tooltipHeight = Math.max(MIN_TOOLTIP_HEIGHT, Math.min(killsNeededHeight + 10, 500));
const tooltipContent = (
<div
style={{
width: '400px',
height: `${tooltipHeight}px`,
display: 'flex',
flexDirection: 'column',
}}
className="overflow-hidden"
>
<div className="flex-1 h-full">
<SystemKillsContent kills={limitedKills} systemNameMap={systemNameMap} onlyOneSystem />
</div>
</div>
);
return (
<WdTooltipWrapper content={tooltipContent} className={className} size={size} interactive={true}>
<WdTooltipWrapper
content={
<div className="overflow-hidden flex w-[450px] flex-col" style={{ height: `${tooltipHeight}px` }}>
<div className="flex-1 h-full">
<SystemKillsList kills={limitedKills} onlyOneSystem />
</div>
</div>
}
className={className}
tooltipClassName="!px-0"
size={size}
interactive={true}
>
{children}
</WdTooltipWrapper>
);

View File

@@ -355,3 +355,15 @@ $tooltip-bg: #202020;
}
}
}
.ShatteredIcon {
position: relative;
//top: -1px;
left: -1px;
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
background-image: url(/images/chart-network-svgrepo-com.svg)
}

View File

@@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeDefault.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useLocalCounter, useSolarSystemNode, useNodeKillsCount } from '../../hooks';
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import {
EFFECT_BACKGROUND_STYLES,
MARKER_BOOKMARK_BG_STYLES,
@@ -14,6 +14,8 @@ import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/Worm
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
@@ -31,8 +33,10 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
)}
{nodeVars.isShattered && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
<span className={clsx('pi pi-chart-pie', classes.icon)} />
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered, '!pr-[2px]')}>
<WdTooltipWrapper content="Shattered" position={TooltipPosition.top}>
<span className={clsx('block w-[10px] h-[10px]', classes.ShatteredIcon)} />
</WdTooltipWrapper>
</div>
)}
@@ -40,7 +44,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
<KillsCounter
killsCount={localKillsCount}
systemId={nodeVars.solarSystemId}
size="lg"
size={TooltipSize.lg}
killsActivityType={nodeVars.killsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
>

View File

@@ -14,6 +14,8 @@ import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/Worm
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
@@ -31,8 +33,10 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
)}
{nodeVars.isShattered && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
<span className={clsx('pi pi-chart-pie', classes.icon)} />
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered, '!pr-[2px]')}>
<WdTooltipWrapper content="Shattered" position={TooltipPosition.top}>
<span className={clsx('block w-[10px] h-[10px]', classes.ShatteredIcon)} />
</WdTooltipWrapper>
</div>
)}
@@ -40,7 +44,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
<KillsCounter
killsCount={localKillsCount}
systemId={nodeVars.solarSystemId}
size="lg"
size={TooltipSize.lg}
killsActivityType={nodeVars.killsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
>

View File

@@ -2,10 +2,13 @@ import { Node, useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandAddSystems } from '@/hooks/Mapper/types/mapHandlers.ts';
import { convertSystem2Node } from '../../helpers';
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
export const useMapAddSystems = () => {
const rf = useReactFlow();
const { addSystemStatic } = useLoadSystemStatic({ systems: [] });
const ref = useRef({ rf });
ref.current = { rf };
@@ -13,7 +16,10 @@ export const useMapAddSystems = () => {
const { rf } = ref.current;
const nodes = rf.getNodes();
const prepared: Node[] = systems.filter(x => !nodes.some(y => x.id === y.id)).map(convertSystem2Node);
const newSystems = systems.filter(x => !nodes.some(y => x.id === y.id));
newSystems.forEach(x => addSystemStatic(x.system_static_info));
const prepared: Node[] = newSystems.map(convertSystem2Node);
rf.addNodes(prepared);
}, []);
};

View File

@@ -14,6 +14,7 @@ export const useMapInit = () => {
return useCallback(
({
systems,
system_signatures,
kills,
connections,
wormholes,
@@ -51,6 +52,10 @@ export const useMapInit = () => {
updateData.systems = systems;
}
if (system_signatures) {
updateData.systemSignatures = system_signatures;
}
if (kills) {
updateData.kills = kills.reduce((acc, x) => ({ ...acc, [x.solar_system_id]: x.kills }), {});
}

View File

@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { useSystemKills } from '../../mapInterface/widgets/SystemKills/hooks/useSystemKills';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useSystemKills } from '@/hooks/Mapper/components/mapInterface/widgets/WSystemKills/hooks/useSystemKills.ts';
interface UseKillsCounterProps {
realSystemId: string;

View File

@@ -13,6 +13,7 @@ import { CharacterTypeRaw, OutCommand, SystemSignature } from '@/hooks/Mapper/ty
import { useUnsplashedSignatures } from './useUnsplashedSignatures';
import { useSystemName } from './useSystemName';
import { LabelInfo, useLabelsInfo } from './useLabelsInfo';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
function getActivityType(count: number): string {
if (count <= 5) return 'activityNormal';
@@ -43,8 +44,7 @@ export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars {
const { id, data, selected } = props;
const {
system_static_info,
system_signatures,
id: solar_system_id,
locked,
name,
tag,
@@ -54,23 +54,24 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
linked_sig_eve_id: linkedSigEveId = '',
} = data;
const {
interfaceSettings,
data: { systemSignatures: mapSystemSignatures },
} = useMapRootState();
const {
system_class,
security,
class_title,
solar_system_id,
statics,
effect_name,
region_name,
region_id,
is_shattered,
solar_system_name,
} = system_static_info;
const {
interfaceSettings,
data: { systemSignatures: mapSystemSignatures },
} = useMapRootState();
} = useMemo(() => {
return getSystemStaticInfo(parseInt(solar_system_id))!;
}, [solar_system_id]);
const { isShowUnsplashedSignatures } = interfaceSettings;
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
@@ -95,13 +96,10 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
const systemSigs = useMemo(
() => mapSystemSignatures[solar_system_id] || system_signatures,
[system_signatures, solar_system_id, mapSystemSignatures],
);
const systemSigs = useMemo(() => mapSystemSignatures[solar_system_id] || [], [solar_system_id, mapSystemSignatures]);
const charactersInSystem = useMemo(() => {
return characters.filter(c => c.location?.solar_system_id === solar_system_id && c.online);
return characters.filter(c => c.location?.solar_system_id === parseInt(solar_system_id) && c.online);
}, [characters, solar_system_id]);
const isWormhole = isWormholeSpace(system_class);
@@ -121,7 +119,7 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
isShowLinkedSigId,
});
const killsCount = useMemo(() => kills[solar_system_id] ?? null, [kills, solar_system_id]);
const killsCount = useMemo(() => kills[parseInt(solar_system_id)] ?? null, [kills, solar_system_id]);
const killsActivityType = killsCount ? getActivityType(killsCount) : null;
const hasUserCharacters = useMemo(

View File

@@ -542,48 +542,32 @@
background-color: #d10600;
}
.react-flow {
color: var(--text-color);
&__pane {
cursor: auto;
}
.react-flow__minimap-node {
fill: #ffb03a;
}
&__minimap {
background-color: rgba(66, 66, 66, 1);
opacity: 0.7;
border: 1px solid #2f2f2f;
border-radius: 4px;
overflow: hidden;
}
.react-flow__minimap {
border: 1px solid #282828;
border-radius: 4px;
background-color: rgb(47 37 37) !important;
overflow: hidden;
}
&__minimap-mask {
fill: rgba(28, 28, 28, 0.75);
}
.react-flow__minimap-mask {
stroke-width: 2px;
fill: rgba(0, 0, 0, 0.5);
mix-blend-mode: overlay;
}
&__controls {
filter: brightness(1.5);
}
&__minimap-node {
fill: #ffb03a;
}
.react-flow__minimap-mask {
stroke-width: 2px;
fill: rgb(0 0 0 / 50%) !important;
mix-blend-mode: inherit;
opacity: 1;
stroke: #fff;
}
.context-menu-active {
background-color: rgba(131, 131, 131, 0.33);
}
.p-dialog {
.p-dialog-header {
height: 40px;
padding: 1rem;
padding-right: 10px !important;
}
.p-dialog-title {
font-size: 1rem !important;
}
.p-dialog-header-icons {
align-self: initial !important;
}
}

View File

@@ -43,7 +43,7 @@ export const Comments = ({}: CommentsProps) => {
}
return (
<div className="flex flex-col gap-1 mt-1 whitespace-nowrap overflow-auto text-ellipsis custom-scrollbar">
<div className="flex flex-col gap-1 whitespace-nowrap overflow-auto text-ellipsis custom-scrollbar">
{commentsList.map(({ id, text, updated_at, characterEveId }) => (
<MarkdownComment key={id} text={text} time={updated_at} characterEveId={characterEveId} id={id} />
))}

View File

@@ -16,7 +16,6 @@ const stopEventPropagationPlugin = ViewPlugin.fromClass(
// @ts-ignore
this.pasteHandler = (event: Event) => {
console.log('Paste done in editor, stopping global listeners.');
event.stopPropagation();
};

View File

@@ -10,6 +10,7 @@ import { OutCommand } from '@/hooks/Mapper/types';
import { IconField } from 'primereact/iconfield';
import { TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
interface SystemSettingsDialog {
systemId: string;
@@ -26,6 +27,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
const system = getSystemById(systems, systemId);
const systemStaticInfo = getSystemStaticInfo(systemId);
const [name, setName] = useState('');
const [label, setLabel] = useState('');
@@ -33,11 +35,11 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
const [description, setDescription] = useState('');
const inputRef = useRef<HTMLInputElement>();
const ref = useRef({ name, description, temporaryName, label, outCommand, systemId, system });
ref.current = { name, description, label, temporaryName, outCommand, systemId, system };
const ref = useRef({ name, description, temporaryName, label, outCommand, systemId, system, systemStaticInfo });
ref.current = { name, description, label, temporaryName, outCommand, systemId, system, systemStaticInfo };
const handleSave = useCallback(() => {
const { name, description, label, temporaryName, outCommand, systemId, system } = ref.current;
const { name, description, label, temporaryName, outCommand, systemId, system, systemStaticInfo } = ref.current;
const outLabel = new LabelsManager(system?.labels ?? '');
outLabel.updateCustomLabel(label);
@@ -62,7 +64,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
type: OutCommand.updateSystemName,
data: {
system_id: systemId,
value: name.trim() || system?.system_static_info.solar_system_name,
value: name.trim() || systemStaticInfo?.solar_system_name,
},
});
@@ -78,11 +80,11 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
}, [setVisible]);
const handleResetSystemName = useCallback(() => {
const { system } = ref.current;
if (!system) {
const { systemStaticInfo } = ref.current;
if (!systemStaticInfo) {
return;
}
setName(system.system_static_info.solar_system_name);
setName(systemStaticInfo.solar_system_name);
}, []);
const onShow = useCallback(() => {
@@ -130,7 +132,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
<label htmlFor="username">Custom name</label>
<IconField>
{name !== system?.system_static_info.solar_system_name && (
{name !== systemStaticInfo?.solar_system_name && (
<WdImgButton
className="pi pi-undo"
textSize={WdImageSize.large}

View File

@@ -1,5 +1,5 @@
.root {
padding-bottom: 5px;
}
.Header {

View File

@@ -2,14 +2,15 @@ import React from 'react';
import classes from './Widget.module.scss';
import clsx from 'clsx';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
export interface WidgetProps {
export type WidgetProps = {
label: React.ReactNode | string;
windowId?: string;
children?: React.ReactNode;
}
contentClassName?: string;
} & WithChildren;
export const Widget = ({ label, children, windowId }: WidgetProps) => {
export const Widget = ({ label, children, windowId, contentClassName }: WidgetProps) => {
return (
<div
data-window-id={windowId}
@@ -34,7 +35,7 @@ export const Widget = ({ label, children, windowId }: WidgetProps) => {
{label}
</div>
<div
className={clsx(classes.Content, 'overflow-auto', 'bg-opacity-5 custom-scrollbar')}
className={clsx(classes.Content, 'overflow-auto', 'bg-opacity-5 custom-scrollbar', contentClassName)}
style={{ flexGrow: 1 }}
onContextMenu={e => {
e.preventDefault();

View File

@@ -5,7 +5,7 @@ import {
SystemInfo,
SystemSignatures,
SystemStructures,
SystemKills,
WSystemKills,
} from '@/hooks/Mapper/components/mapInterface/widgets';
import { CommentsWidget } from '@/hooks/Mapper/components/mapInterface/widgets/CommentsWidget';
@@ -70,7 +70,7 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
position: { x: 270, y: 730 },
size: { width: 510, height: 200 },
zIndex: 0,
content: () => <SystemKills />,
content: () => <WSystemKills />,
},
{
id: WidgetsIds.comments,

View File

@@ -44,6 +44,7 @@ export const CommentsWidget = () => {
return (
<Widget
contentClassName="my-1"
label={
<div ref={containerRef} className="flex justify-between items-center gap-1 text-xs w-full">
<div className="flex items-center gap-1">

View File

@@ -5,20 +5,20 @@ import { SystemInfoContent } from './SystemInfoContent';
import { PrimeIcons } from 'primereact/api';
import { useState, useCallback } from 'react';
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
export const SystemInfo = () => {
const [visible, setVisible] = useState(false);
const {
data: { selectedSystems, systems },
data: { selectedSystems },
} = useMapRootState();
const [systemId] = selectedSystems;
const sys = getSystemById(systems, systemId)!;
const { solar_system_name: solarSystemName } = sys?.system_static_info || {};
const systemStaticInfo = getSystemStaticInfo(systemId)!;
const { solar_system_name: solarSystemName } = systemStaticInfo || {};
const isNotSelectedSystem = selectedSystems.length !== 1;

View File

@@ -3,6 +3,7 @@ import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormhol
import { useMemo } from 'react';
import { getSystemById, sortWHClasses } from '@/hooks/Mapper/helpers';
import { InfoDrawer, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
interface SystemInfoContentProps {
systemId: string;
@@ -14,9 +15,9 @@ export const SystemInfoContent = ({ systemId }: SystemInfoContentProps) => {
} = useMapRootState();
const sys = getSystemById(systems, systemId)! || {};
const systemStaticInfo = getSystemStaticInfo(systemId)!;
const { description } = sys;
const { system_class, region_name, constellation_name, statics, effect_name, effect_power } =
sys.system_static_info || {};
const { system_class, region_name, constellation_name, statics, effect_name, effect_power } = systemStaticInfo || {};
const isWH = isWormholeSpace(system_class);
const sortedStatics = useMemo(() => sortWHClasses(wormholesData, statics), [wormholesData, statics]);

View File

@@ -1,106 +0,0 @@
import React, { useMemo, useState } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemKillsContent } from './SystemKillsContent/SystemKillsContent';
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 = React.memo(() => {
const {
data: { selectedSystems, systems, isSubscriptionActive },
outCommand,
} = useMapRootState();
const [systemId] = selectedSystems || [];
const [settingsDialogVisible, setSettingsDialogVisible] = useState(false);
const systemNameMap = useMemo(() => {
const map: Record<string, string> = {};
systems.forEach(sys => {
map[sys.id] = sys.temporary_name || sys.name || '???';
});
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;
const { kills, isLoading, error } = useSystemKills({
systemId,
outCommand,
showAllVisible: visible,
sinceHours: settings.timeRange,
});
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">
<Widget label={<KillsHeader systemId={systemId} onOpenSettings={() => setSettingsDialogVisible(true)} />}>
{!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 ? (
<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 &quot;Show all systems&quot;)
</span>
</div>
) : 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>
) : 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>
) : !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>
) : (
<SystemKillsContent
kills={filteredKills}
systemNameMap={systemNameMap}
onlyOneSystem={!visible}
timeRange={settings.timeRange}
/>
)}
</Widget>
{settingsDialogVisible && <KillsSettingsDialog visible setVisible={setSettingsDialogVisible} />}
</div>
);
});
SystemKills.displayName = 'SystemKills';

View File

@@ -1,35 +0,0 @@
// Custom scrollbar styling is now handled by the global custom-scrollbar class
.scrollerContent {
overflow-x: hidden;
overflow-y: auto;
height: 100% !important;
}
// VirtualScroller specific styles that can't be handled with Tailwind
.VirtualScroller {
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
height: 100% !important;
// Target this specific VirtualScroller instance
&:global(.p-virtualscroller) {
height: 100% !important;
:global(.p-virtualscroller-content) {
height: 100% !important;
}
}
}
// Fix for PrimeReact VirtualScroller - these need to be global
:global {
.p-virtualscroller {
display: flex;
flex-direction: column;
}
.p-virtualscroller-content {
flex: 1;
}
}

View File

@@ -1,20 +0,0 @@
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

@@ -1,15 +0,0 @@
import React from 'react';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { KillRowDetail } from './KillRowDetail.tsx';
export interface KillRowProps {
killDetails: DetailedKill;
systemName: string;
onlyOneSystem?: boolean;
}
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 +0,0 @@
export * from './SystemKills';

View File

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

View File

@@ -2,7 +2,6 @@ import { Dialog } from 'primereact/dialog';
import { useCallback, useState } from 'react';
import { Button } from 'primereact/button';
import { TabPanel, TabView } from 'primereact/tabview';
import styles from './SystemSignatureSettingsDialog.module.scss';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { Dropdown } from 'primereact/dropdown';
import {
@@ -72,26 +71,24 @@ export const SystemSignatureSettingsDialog = ({
<Dialog header="System Signatures Settings" visible={true} onHide={onCancel} className="w-full max-w-lg h-[500px]">
<div className="flex flex-col gap-3 justify-between h-full">
<div className="flex flex-col gap-2">
<div className={styles.verticalTabsContainer}>
<TabView
activeIndex={activeIndex}
onTabChange={e => setActiveIndex(e.index)}
className={styles.verticalTabView}
>
<TabPanel header="Filters" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">
{SIGNATURE_SETTINGS.filterFlags.map(renderSetting)}
</div>
</TabPanel>
<TabPanel header="User Interface" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">
{SIGNATURE_SETTINGS.uiFlags.map(renderSetting)}
<div className="my-2 border-t border-stone-700/50"></div>
{SIGNATURE_SETTINGS.uiOther.map(renderSetting)}
</div>
</TabPanel>
</TabView>
</div>
<TabView
activeIndex={activeIndex}
onTabChange={e => setActiveIndex(e.index)}
className="vertical-tabs-container"
>
<TabPanel header="Filters">
<div className="w-full h-full flex flex-col gap-1">
{SIGNATURE_SETTINGS.filterFlags.map(renderSetting)}
</div>
</TabPanel>
<TabPanel header="User Interface">
<div className="w-full h-full flex flex-col gap-1">
{SIGNATURE_SETTINGS.uiFlags.map(renderSetting)}
<div className="my-2 border-t border-stone-700/50"></div>
{SIGNATURE_SETTINGS.uiOther.map(renderSetting)}
</div>
</TabPanel>
</TabView>
</div>
<div className="flex gap-2 justify-end">

View File

@@ -1,8 +1,8 @@
import { useCallback, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemSignaturesContent } from './SystemSignaturesContent';
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
import { SystemSignature } from '@/hooks/Mapper/types';
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useHotkey } from '@/hooks/Mapper/hooks';
import { SystemSignaturesHeader } from './SystemSignatureHeader';
@@ -10,15 +10,19 @@ import useLocalStorageState from 'use-local-storage-state';
import {
SETTINGS_KEYS,
SETTINGS_VALUES,
SIGNATURE_DELETION_TIMEOUTS,
SIGNATURE_SETTING_STORE_KEY,
SIGNATURE_WINDOW_ID,
SIGNATURES_DELETION_TIMING,
SignatureSettingsType,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { calculateTimeRemaining } from './helpers';
export const SystemSignatures = () => {
const [visible, setVisible] = useState(false);
const [sigCount, setSigCount] = useState<number>(0);
const [pendingSigs, setPendingSigs] = useState<SystemSignature[]>([]);
const [pendingTimeRemaining, setPendingTimeRemaining] = useState<number | undefined>();
const undoPendingFnRef = useRef<() => void>(() => {});
const {
@@ -51,27 +55,45 @@ export const SystemSignatures = () => {
event.stopPropagation();
undoPendingFnRef.current();
setPendingSigs([]);
setPendingTimeRemaining(undefined);
}
});
const handleUndoClick = useCallback(() => {
undoPendingFnRef.current();
setPendingSigs([]);
setPendingTimeRemaining(undefined);
}, []);
const handleSettingsButtonClick = useCallback(() => {
setVisible(true);
}, []);
const handlePendingChange = useCallback((newPending: SystemSignature[], newUndo: () => void) => {
setPendingSigs(prev => {
if (newPending.length === prev.length && newPending.every(np => prev.some(pp => pp.eve_id === np.eve_id))) {
return prev;
}
return newPending;
});
undoPendingFnRef.current = newUndo;
}, []);
const handlePendingChange = useCallback(
(pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>, newUndo: () => void) => {
setPendingSigs(() => {
return Object.values(pending.current).filter(sig => sig.pendingDeletion);
});
undoPendingFnRef.current = newUndo;
},
[],
);
// Calculate the minimum time remaining for any pending signature
useEffect(() => {
if (pendingSigs.length === 0) {
setPendingTimeRemaining(undefined);
return;
}
const calculate = () => {
setPendingTimeRemaining(() => calculateTimeRemaining(pendingSigs));
};
calculate();
const interval = setInterval(calculate, 1000);
return () => clearInterval(interval);
}, [pendingSigs]);
return (
<Widget
@@ -79,8 +101,8 @@ export const SystemSignatures = () => {
<SystemSignaturesHeader
sigCount={sigCount}
lazyDeleteValue={currentSettings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
// lazyDeleteValue={lazyDeleteValue}
pendingCount={pendingSigs.length}
pendingTimeRemaining={pendingTimeRemaining}
onLazyDeleteChange={handleLazyDeleteChange}
onUndoClick={handleUndoClick}
onSettingsClick={handleSettingsButtonClick}
@@ -96,6 +118,12 @@ export const SystemSignatures = () => {
<SystemSignaturesContent
systemId={systemId}
settings={currentSettings}
deletionTiming={
SIGNATURE_DELETION_TIMEOUTS[
(currentSettings[SETTINGS_KEYS.DELETION_TIMING] as keyof typeof SIGNATURE_DELETION_TIMEOUTS) ||
SIGNATURES_DELETION_TIMING.DEFAULT
] as number
}
onLazyDeleteChange={handleLazyDeleteChange}
onCountChange={handleSigCountChange}
onPendingChange={handlePendingChange}

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { PrimeIcons } from 'primereact/api';
import { Column } from 'primereact/column';
import {
DataTable,
DataTableRowClickEvent,
@@ -6,13 +7,9 @@ import {
DataTableStateEvent,
SortOrder,
} from 'primereact/datatable';
import { Column } from 'primereact/column';
import { PrimeIcons } from 'primereact/api';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import useLocalStorageState from 'use-local-storage-state';
import { ExtendedSystemSignature, SignatureGroup, SignatureKind, 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 {
COMPACT_MAX_WIDTH,
@@ -24,6 +21,9 @@ import {
SIGNATURE_WINDOW_ID,
SignatureSettingsType,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
import { TooltipPosition, WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { ExtendedSystemSignature, SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types';
import {
renderAddedTimeLeft,
@@ -32,10 +32,10 @@ import {
renderInfoColumn,
renderUpdatedTimeLeft,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
import { getSignatureRowClass } from '../helpers/rowStyles';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { getSignatureRowClass } from '../helpers/rowStyles';
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
@@ -57,7 +57,10 @@ interface SystemSignaturesContentProps {
onSelect?: (signature: SystemSignature) => void;
onLazyDeleteChange?: (value: boolean) => void;
onCountChange?: (count: number) => void;
onPendingChange?: (pending: ExtendedSystemSignature[], undo: () => void) => void;
onPendingChange?: (
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
undo: () => void,
) => void;
deletionTiming?: number;
filterSignature?: (signature: SystemSignature) => boolean;
}
@@ -152,7 +155,7 @@ export const SystemSignaturesContent = ({
(e: { value: SystemSignature[] }) => {
selectable ? onSelect?.(e.value[0]) : setSelectedSignatures(e.value as ExtendedSystemSignature[]);
},
[selectable, onSelect],
[selectable],
);
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
@@ -345,6 +348,7 @@ export const SystemSignaturesContent = ({
<WdTooltip
className="bg-stone-900/95 text-slate-50"
ref={tooltipRef}
position={TooltipPosition.top}
content={
hoveredSignature ? (
<SignatureView signature={hoveredSignature} showCharacterPortrait={showCharacterPortrait} />

View File

@@ -132,6 +132,8 @@ export enum SIGNATURES_DELETION_TIMING {
EXTENDED,
}
export type SignatureDeletionTimingType = { [key in SIGNATURES_DELETION_TIMING]?: unknown };
export const SIGNATURE_SETTINGS = {
filterFlags: [
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.COSMIC_ANOMALY, name: 'Show Anomalies' },
@@ -180,7 +182,7 @@ export const SETTINGS_VALUES: SignatureSettingsType = {
[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN]: true,
[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN]: true,
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: true,
[SETTINGS_KEYS.KEEP_LAZY_DELETE]: true,
[SETTINGS_KEYS.KEEP_LAZY_DELETE]: false,
[SETTINGS_KEYS.DELETION_TIMING]: SIGNATURES_DELETION_TIMING.DEFAULT,
[SETTINGS_KEYS.COLOR_BY_TYPE]: true,
[SETTINGS_KEYS.SHOW_CHARACTER_PORTRAIT]: true,
@@ -200,3 +202,9 @@ export const SETTINGS_VALUES: SignatureSettingsType = {
[SETTINGS_KEYS.GAS_SITE]: true,
[SETTINGS_KEYS.COMBAT_SITE]: true,
};
export const SIGNATURE_DELETION_TIMEOUTS: SignatureDeletionTimingType = {
[SIGNATURES_DELETION_TIMING.DEFAULT]: 10_000,
[SIGNATURES_DELETION_TIMING.IMMEDIATE]: 0,
[SIGNATURES_DELETION_TIMING.EXTENDED]: 30_000,
};

View File

@@ -1,4 +1,4 @@
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
import { FINAL_DURATION_MS } from '../constants';
export function prepareUpdatePayload(
@@ -49,40 +49,56 @@ export function schedulePendingAdditionForSig(
);
}
export function mergeLocalPendingAdditions(
export function mergeLocalPending(
pendingMapRef: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
serverSigs: ExtendedSystemSignature[],
localSigs: ExtendedSystemSignature[],
): ExtendedSystemSignature[] {
const now = Date.now();
const pendingAdditions = localSigs.filter(sig => sig.pendingAddition && sig.pendingUntil && sig.pendingUntil > now);
const pendingDeletions = Object.values(pendingMapRef.current).filter(
sig => sig.pendingDeletion && 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)) {
pendingDeletions.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>,
export function scheduleLazyTimers(
signatures: ExtendedSystemSignature[],
pendingMapRef: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
finalizeFn: (sig: ExtendedSystemSignature) => Promise<void>,
finalDuration = FINAL_DURATION_MS,
) {
const now = Date.now();
toRemove.forEach(sig => {
signatures.forEach(sig => {
const finalTimeoutId = window.setTimeout(async () => {
await finalizeRemoval(sig);
await finalizeFn(sig);
}, finalDuration);
setPendingMap(prev => ({
...prev,
pendingMapRef.current = {
...pendingMapRef.current,
[sig.eve_id]: {
finalUntil: now + finalDuration,
...sig,
finalTimeoutId,
},
}));
};
});
}
export const calculateTimeRemaining = (pendingSigs: SystemSignature[]) => {
const now = Date.now();
let minTime: number | undefined = undefined;
pendingSigs.forEach(sig => {
const extendedSig = sig as unknown as { pendingUntil?: number };
if (extendedSig.pendingUntil && (minTime === undefined || extendedSig.pendingUntil - now < minTime)) {
minTime = extendedSig.pendingUntil - now;
}
});
return minTime && minTime > 0 ? minTime : undefined;
};

View File

@@ -6,7 +6,10 @@ export interface UseSystemSignaturesDataProps {
settings: SignatureSettingsType;
hideLinkedSignatures?: boolean;
onCountChange?: (count: number) => void;
onPendingChange?: (pending: ExtendedSystemSignature[], undo: () => void) => void;
onPendingChange?: (
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
undo: () => void,
) => void;
onLazyDeleteChange?: (value: boolean) => void;
deletionTiming?: number;
}
@@ -15,16 +18,21 @@ export interface UseFetchingParams {
systemId: string;
signaturesRef: React.MutableRefObject<ExtendedSystemSignature[]>;
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>;
localPendingDeletions: ExtendedSystemSignature[];
pendingDeletionMapRef: React.MutableRefObject<Record<string, ExtendedSystemSignature>>;
}
export interface UsePendingDeletionParams {
systemId: string;
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>;
deletionTiming?: number;
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>;
onPendingChange?: (
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
undo: () => void,
) => void;
}
export interface UsePendingAdditionParams {
systemId: string;
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>;
deletionTiming?: number;
}

View File

@@ -1,70 +0,0 @@
import { useCallback, useRef, useState } from 'react';
import { UsePendingAdditionParams } from './types';
import { FINAL_DURATION_MS } from '../constants';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { schedulePendingAdditionForSig } from '../helpers/contentHelpers';
export const usePendingAdditions = ({ setSignatures, deletionTiming }: UsePendingAdditionParams) => {
const [pendingUndoAdditions, setPendingUndoAdditions] = useState<ExtendedSystemSignature[]>([]);
const pendingAdditionMapRef = useRef<Record<string, { finalUntil: number; finalTimeoutId: number }>>({});
// Use the provided deletion timing or fall back to the default
const finalDuration = deletionTiming !== undefined ? deletionTiming : FINAL_DURATION_MS;
const processAddedSignatures = useCallback(
(added: ExtendedSystemSignature[]) => {
if (!added.length) return;
// If duration is 0, don't show pending state
if (finalDuration === 0) {
setSignatures(prev => [
...prev,
...added.map(sig => ({
...sig,
pendingAddition: false,
})),
]);
return;
}
const now = Date.now();
setSignatures(prev => [
...prev,
...added.map(sig => ({
...sig,
pendingAddition: true,
pendingUntil: now + finalDuration,
})),
]);
added.forEach(sig => {
schedulePendingAdditionForSig(
sig,
finalDuration,
setSignatures,
pendingAdditionMapRef,
setPendingUndoAdditions,
);
});
},
[setSignatures, finalDuration],
);
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

@@ -1,17 +1,19 @@
import { useState, useCallback } from 'react';
import { useCallback, useRef, useEffect } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { prepareUpdatePayload, scheduleLazyDeletionTimers } from '../helpers';
import { prepareUpdatePayload, scheduleLazyTimers } from '../helpers';
import { UsePendingDeletionParams } from './types';
import { FINAL_DURATION_MS } from '../constants';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
export function usePendingDeletions({ systemId, setSignatures, deletionTiming }: UsePendingDeletionParams) {
export function usePendingDeletions({
systemId,
setSignatures,
deletionTiming,
onPendingChange,
}: UsePendingDeletionParams) {
const { outCommand } = useMapRootState();
const [localPendingDeletions, setLocalPendingDeletions] = useState<ExtendedSystemSignature[]>([]);
const [pendingDeletionMap, setPendingDeletionMap] = useState<
Record<string, { finalUntil: number; finalTimeoutId: number }>
>({});
const pendingDeletionMapRef = useRef<Record<string, ExtendedSystemSignature>>({});
// Use the provided deletion timing or fall back to the default
const finalDuration = deletionTiming !== undefined ? deletionTiming : FINAL_DURATION_MS;
@@ -37,15 +39,17 @@ export function usePendingDeletions({ systemId, setSignatures, deletionTiming }:
const processedRemoved = removed.map(r => ({
...r,
pendingDeletion: true,
pendingAddition: false,
pendingUntil: now + finalDuration,
}));
setLocalPendingDeletions(prev => [...prev, ...processedRemoved]);
pendingDeletionMapRef.current = {
...pendingDeletionMapRef.current,
...processedRemoved.reduce((acc: any, sig) => {
acc[sig.eve_id] = sig;
return acc;
}, {}),
};
outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, []),
});
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
setSignatures(prev =>
prev.map(sig => {
@@ -56,37 +60,35 @@ export function usePendingDeletions({ systemId, setSignatures, deletionTiming }:
}),
);
scheduleLazyDeletionTimers(
scheduleLazyTimers(
processedRemoved,
setPendingDeletionMap,
pendingDeletionMapRef,
async sig => {
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, [], [], [sig]),
});
setLocalPendingDeletions(prev => prev.filter(x => x.eve_id !== sig.eve_id));
delete pendingDeletionMapRef.current[sig.eve_id];
setSignatures(prev => prev.filter(x => x.eve_id !== sig.eve_id));
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
},
finalDuration,
);
},
[systemId, outCommand, setSignatures, finalDuration],
[systemId, outCommand, finalDuration],
);
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]);
Object.values(pendingDeletionMapRef.current).forEach(({ finalTimeoutId }) => {
clearTimeout(finalTimeoutId);
});
pendingDeletionMapRef.current = {};
setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x)));
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
}, []);
return {
localPendingDeletions,
setLocalPendingDeletions,
pendingDeletionMap,
setPendingDeletionMap,
pendingDeletionMapRef,
processRemovedSignatures,
clearPendingDeletions,
};

View File

@@ -1,16 +1,15 @@
import { useCallback } from 'react';
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { prepareUpdatePayload, getActualSigs, mergeLocalPendingAdditions } from '../helpers';
import { prepareUpdatePayload, getActualSigs, mergeLocalPending } from '../helpers';
import { UseFetchingParams } from './types';
import { FINAL_DURATION_MS } from '../constants';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const useSignatureFetching = ({
systemId,
signaturesRef,
setSignatures,
localPendingDeletions,
pendingDeletionMapRef,
}: UseFetchingParams) => {
const {
data: { characters },
@@ -22,9 +21,6 @@ export const useSignatureFetching = ({
setSignatures([]);
return;
}
if (localPendingDeletions.length) {
return;
}
const resp = await outCommand({
type: OutCommand.getSignatures,
data: { system_id: systemId },
@@ -36,8 +32,8 @@ export const useSignatureFetching = ({
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]);
setSignatures(() => mergeLocalPending(pendingDeletionMapRef, extended));
}, [characters, systemId, outCommand]);
const handleUpdateSignatures = useCallback(
async (newList: ExtendedSystemSignature[], updateOnly: boolean, skipUpdateUntouched?: boolean) => {
@@ -48,24 +44,12 @@ export const useSignatureFetching = ({
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],
[systemId, outCommand, signaturesRef],
);
return {

View File

@@ -1,13 +1,12 @@
import { useCallback, useEffect, useState } from 'react';
import useRefState from 'react-usestateref';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands, ExtendedSystemSignature, SignatureKind, SystemSignature } from '@/hooks/Mapper/types';
import { Commands, ExtendedSystemSignature, SignatureKind } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { parseSignatures } from '@/hooks/Mapper/helpers';
import { getActualSigs, mergeLocalPendingAdditions } from '../helpers';
import { getActualSigs } from '../helpers';
import { useSignatureFetching } from './useSignatureFetching';
import { usePendingAdditions } from './usePendingAdditions';
import { usePendingDeletions } from './usePendingDeletions';
import { UseSystemSignaturesDataProps } from './types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
@@ -25,24 +24,18 @@ export const useSystemSignaturesData = ({
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
const { localPendingDeletions, setLocalPendingDeletions, processRemovedSignatures, clearPendingDeletions } =
usePendingDeletions({
systemId,
setSignatures,
deletionTiming,
});
const { pendingUndoAdditions, setPendingUndoAdditions, processAddedSignatures, clearPendingAdditions } =
usePendingAdditions({
setSignatures,
deletionTiming,
});
const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({
systemId,
setSignatures,
deletionTiming,
onPendingChange,
});
const { handleGetSignatures, handleUpdateSignatures } = useSignatureFetching({
systemId,
signaturesRef,
setSignatures,
localPendingDeletions,
pendingDeletionMapRef,
});
const handlePaste = useCallback(
@@ -56,18 +49,16 @@ export const useSystemSignaturesData = ({
const currentNonPending = lazyDeleteValue
? signaturesRef.current.filter(sig => !sig.pendingDeletion)
: signaturesRef.current.filter(sig => !sig.pendingDeletion && !sig.pendingAddition);
: signaturesRef.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({
}
if (updated.length !== 0 || added.length !== 0) {
await outCommand({
type: OutCommand.updateSignatures,
data: {
system_id: systemId,
@@ -76,33 +67,14 @@ export const useSystemSignaturesData = ({
removed: [],
},
});
if (resp) {
const finalSigs = (resp.signatures ?? []) as SystemSignature[];
setSignatures(prev =>
mergeLocalPendingAdditions(
finalSigs.map(x => ({ ...x })),
prev,
),
);
}
}
const keepLazy = settings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean;
const keepLazy = settings[SETTINGS_KEYS.KEEP_LAZY_DELETE] as boolean;
if (lazyDeleteValue && !keepLazy) {
onLazyDeleteChange?.(false);
}
},
[
settings,
signaturesRef,
processAddedSignatures,
processRemovedSignatures,
outCommand,
systemId,
setSignatures,
onLazyDeleteChange,
],
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange],
);
const handleDeleteSelected = useCallback(async () => {
@@ -112,7 +84,7 @@ export const useSystemSignaturesData = ({
await handleUpdateSignatures(finalList, false, true);
setSelectedSignatures([]);
}, [selectedSignatures, signatures, handleUpdateSignatures]);
}, [selectedSignatures, signatures]);
const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures);
@@ -120,42 +92,7 @@ export const useSystemSignaturesData = ({
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]);
}, [clearPendingDeletions]);
useMapEventListener(event => {
if (event.name === Commands.signaturesUpdated && String(event.data) === String(systemId)) {
@@ -167,14 +104,15 @@ export const useSystemSignaturesData = ({
useEffect(() => {
if (!systemId) {
setSignatures([]);
undoPending();
return;
}
handleGetSignatures();
}, [systemId, handleGetSignatures, setSignatures]);
}, [systemId]);
useEffect(() => {
onCountChange?.(signatures.length);
}, [signatures, onCountChange]);
}, [signatures]);
return {
signatures,

View File

@@ -32,7 +32,12 @@ export function parseFormatOneLine(line: string): StructureItem | null {
return null;
}
if (rawTypeName != STRUCTURE_TYPE_MAP[rawTypeId]) {
// in some localizations (like russian) there is an option called "mark names with *"
// The example output will be "35826 Itamo - Research & Production Azbel* 609 м"
// so, let's fix this
const localizationFixedName = rawTypeName.replace("*", "");
if (localizationFixedName != STRUCTURE_TYPE_MAP[rawTypeId]) {
return null;
}

View File

@@ -1,26 +1,26 @@
import React, { useMemo } from 'react';
import { useMemo } from 'react';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { VirtualScroller } from 'primereact/virtualscroller';
import { useSystemKillsItemTemplate } from '../hooks/useSystemKillsItemTemplate';
import classes from './SystemKillsContent.module.scss';
import clsx from 'clsx';
import { WithClassName } from '@/hooks/Mapper/types/common.ts';
export const ITEM_HEIGHT = 35;
export const KILLS_ROW_HEIGHT = 40;
export interface SystemKillsContentProps {
export type SystemKillsContentProps = {
kills: DetailedKill[];
systemNameMap: Record<string, string>;
onlyOneSystem?: boolean;
timeRange?: number;
limit?: number;
}
} & WithClassName;
export const SystemKillsContent: React.FC<SystemKillsContentProps> = ({
export const SystemKillsList = ({
kills,
systemNameMap,
onlyOneSystem = false,
timeRange = 4,
limit,
}) => {
className,
}: SystemKillsContentProps) => {
const processedKills = useMemo(() => {
if (!kills || kills.length === 0) return [];
@@ -47,30 +47,17 @@ export const SystemKillsContent: React.FC<SystemKillsContentProps> = ({
return filteredKills;
}, [kills, timeRange, limit]);
const itemTemplate = useSystemKillsItemTemplate(systemNameMap, onlyOneSystem);
// Define style for the VirtualScroller
const virtualScrollerStyle: React.CSSProperties = {
boxSizing: 'border-box',
height: '100%', // Use 100% height to fill the container
};
const itemTemplate = useSystemKillsItemTemplate(onlyOneSystem);
return (
<div className="h-full w-full flex flex-col overflow-hidden" data-testid="system-kills-content">
<VirtualScroller
items={processedKills}
itemSize={ITEM_HEIGHT}
itemTemplate={itemTemplate}
className={`w-full h-full flex-1 select-none ${classes.VirtualScroller}`}
style={virtualScrollerStyle}
pt={{
content: {
className: `custom-scrollbar ${classes.scrollerContent}`,
},
}}
/>
</div>
<VirtualScroller
items={processedKills}
itemSize={KILLS_ROW_HEIGHT}
itemTemplate={itemTemplate}
className={clsx(
'w-full flex-1 select-none !h-full overflow-x-hidden overflow-y-auto custom-scrollbar',
className,
)}
/>
);
};
export default SystemKillsContent;

View File

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

View File

@@ -0,0 +1,109 @@
import { useCallback, useMemo, useState } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemKillsList } from './SystemKillsList';
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 { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
const SystemKillsContent = () => {
const {
data: { selectedSystems, isSubscriptionActive },
outCommand,
} = useMapRootState();
const [systemId] = selectedSystems || [];
const systemStaticInfo = getSystemStaticInfo(systemId)!;
const [settings] = useKillsWidgetSettings();
const visible = settings.showAll;
const { kills, isLoading, error } = useSystemKills({
systemId,
outCommand,
showAllVisible: visible,
sinceHours: settings.timeRange,
});
const isNothingSelected = !systemId && !visible;
const showLoading = isLoading && kills.length === 0;
const filteredKills = useMemo(() => {
if (!settings.whOnly || !visible) return kills;
return kills.filter(kill => {
if (!systemStaticInfo) {
console.warn(`System with id ${kill.solar_system_id} not found.`);
return false;
}
return isWormholeSpace(systemStaticInfo.system_class);
});
}, [kills, settings.whOnly, systemStaticInfo, visible]);
if (!isSubscriptionActive) {
return (
<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>
);
}
if (isNothingSelected) {
return (
<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 &quot;Show all systems&quot;)
</span>
</div>
);
}
if (showLoading) {
return (
<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>
);
}
if (error) {
return (
<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>
);
}
if (!filteredKills || filteredKills.length === 0) {
return (
<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>
);
}
return <SystemKillsList kills={filteredKills} onlyOneSystem={!visible} timeRange={settings.timeRange} />;
};
export const WSystemKills = () => {
const [settingsDialogVisible, setSettingsDialogVisible] = useState(false);
const {
data: { selectedSystems },
} = useMapRootState();
const [systemId] = selectedSystems || [];
const handleOpenSettings = useCallback(() => setSettingsDialogVisible(true), []);
return (
<Widget label={<KillsHeader systemId={systemId} onOpenSettings={handleOpenSettings} />}>
<SystemKillsContent />
{settingsDialogVisible && <KillsSettingsDialog visible setVisible={setSettingsDialogVisible} />}
</Widget>
);
};

View File

@@ -0,0 +1,24 @@
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import { KillRowDetail } from '@/hooks/Mapper/components/mapInterface/widgets/WSystemKills/components/KillRowDetail.tsx';
import clsx from 'clsx';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
export const KillItemTemplate = (
onlyOneSystem: boolean,
kill: DetailedKill,
options: VirtualScrollerTemplateOptions,
) => {
const systemName = getSystemStaticInfo(kill.solar_system_id)?.solar_system_name || `System ${kill.solar_system_id}`;
return (
<div style={{ height: `${options.props.itemSize}px` }}>
<KillRowDetail
killDetails={kill}
systemName={systemName}
onlyOneSystem={onlyOneSystem}
className={clsx(options.odd && 'bg-stone-800/50')}
/>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import React from 'react';
import { useMemo } from 'react';
import clsx from 'clsx';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import {
@@ -14,14 +14,15 @@ import {
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import classes from './KillRowDetail.module.scss';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { WithClassName } from '@/hooks/Mapper/types/common.ts';
export interface CompactKillRowProps {
export type CompactKillRowProps = {
killDetails: DetailedKill;
systemName: string;
onlyOneSystem: boolean;
}
} & WithClassName;
export const KillRowDetail: React.FC<CompactKillRowProps> = ({ killDetails, systemName, onlyOneSystem }) => {
export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, className }: CompactKillRowProps) => {
const {
killmail_id = 0,
// Victim data
@@ -80,13 +81,24 @@ export const KillRowDetail: React.FC<CompactKillRowProps> = ({ killDetails, syst
'Victim',
);
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } = getAttackerPrimaryImageAndTooltip(
attackerIsNpc,
attackerAllianceLogoUrl,
attackerCorpLogoUrl,
final_blow_alliance_name,
final_blow_corp_name,
final_blow_ship_type_id,
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } = useMemo(
() =>
getAttackerPrimaryImageAndTooltip(
attackerIsNpc,
attackerAllianceLogoUrl,
attackerCorpLogoUrl,
final_blow_alliance_name,
final_blow_corp_name,
final_blow_ship_type_id,
),
[
attackerAllianceLogoUrl,
attackerCorpLogoUrl,
attackerIsNpc,
final_blow_alliance_name,
final_blow_corp_name,
final_blow_ship_type_id,
],
);
// Define attackerTicker to use the alliance ticker if available, otherwise the corp ticker.
@@ -100,6 +112,8 @@ export const KillRowDetail: React.FC<CompactKillRowProps> = ({ killDetails, syst
className={clsx(
'h-10 flex items-center border-b border-stone-800',
'text-xs whitespace-nowrap overflow-hidden leading-none',
'px-1',
className,
)}
>
{/* Victim Section */}
@@ -142,12 +156,14 @@ export const KillRowDetail: React.FC<CompactKillRowProps> = ({ killDetails, syst
{victim_char_name}
<span className="text-stone-400"> / {victimAffiliationTicker}</span>
</div>
<div className="truncate text-stone-300">
{victim_ship_name}
<div className="truncate text-stone-300 flex items-center gap-1">
<span className="text-stone-400 overflow-hidden text-ellipsis whitespace-nowrap max-w-[140px]">
{victim_ship_name}
</span>
{killValueFormatted && (
<>
<span className="ml-1 text-stone-400">/</span>
<span className="ml-1 text-green-400">{killValueFormatted}</span>
<span className="text-stone-400">/</span>
<span className="text-green-400">{killValueFormatted}</span>
</>
)}
</div>

View File

@@ -1,10 +1,9 @@
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
import debounce from 'lodash.debounce';
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useKillsWidgetSettings } from './useKillsWidgetSettings';
import { useMapEventListener, MapEvent } from '@/hooks/Mapper/events';
interface UseSystemKillsProps {
systemId?: string;
@@ -29,10 +28,6 @@ function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceH
return Object.values(byId);
}
interface DetailedKillsEvent extends MapEvent<Commands> {
payload: Record<string, DetailedKill[]>;
}
export function useSystemKills({ systemId, outCommand, showAllVisible = false, sinceHours = 24 }: UseSystemKillsProps) {
const { data, update } = useMapRootState();
const { detailedKills = {}, systems = [] } = data;
@@ -41,32 +36,6 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
const effectiveSinceHours = sinceHours;
const updateDetailedKills = useCallback(
(newKillsMap: Record<string, DetailedKill[]>) => {
update(prev => {
const oldKills = prev.detailedKills ?? {};
const updated = { ...oldKills };
for (const [sid, killsArr] of Object.entries(newKillsMap)) {
updated[sid] = killsArr;
}
return { ...prev, detailedKills: updated };
}, true);
},
[update],
);
useMapEventListener((event: MapEvent<Commands>) => {
if (event.name === Commands.detailedKillsUpdated) {
const detailedEvent = event as DetailedKillsEvent;
if (systemId && !Object.keys(detailedEvent.payload).includes(systemId.toString())) {
return false;
}
updateDetailedKills(detailedEvent.payload);
return true;
}
return false;
});
const effectiveSystemIds = useMemo(() => {
if (showAllVisible) {
return systems.map(s => s.id).filter(id => !excludedSystems.includes(Number(id)));

View File

@@ -4,10 +4,9 @@ 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) {
export function useSystemKillsItemTemplate(onlyOneSystem: boolean) {
return useCallback(
(kill: DetailedKill, options: VirtualScrollerTemplateOptions) =>
KillItemTemplate(systemNameMap, onlyOneSystem, kill, options),
[systemNameMap, onlyOneSystem],
(kill: DetailedKill, options: VirtualScrollerTemplateOptions) => KillItemTemplate(onlyOneSystem, kill, options),
[onlyOneSystem],
);
}

View File

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

View File

@@ -3,4 +3,4 @@ export * from './SystemInfo';
export * from './RoutesWidget';
export * from './SystemSignatures';
export * from './SystemStructures';
export * from './SystemKills';
export * from './WSystemKills';

View File

@@ -9,9 +9,10 @@ import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/compone
import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu';
import { MapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings';
import { CharacterActivity } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity';
import { TrackAndFollow } from '@/hooks/Mapper/components/mapRootContent/components/TrackAndFollow/TrackAndFollow';
import { useCharacterActivityHandlers } from './hooks/useCharacterActivityHandlers';
import { useTrackAndFollowHandlers } from './hooks/useTrackAndFollowHandlers';
import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types';
export interface MapRootContentProps {}
@@ -19,18 +20,28 @@ export interface MapRootContentProps {}
export const MapRootContent = ({}: MapRootContentProps) => {
const { interfaceSettings, data } = useMapRootState();
const { isShowMenu } = interfaceSettings;
const { showCharacterActivity, showTrackAndFollow } = data;
const { showCharacterActivity } = data;
const { handleHideCharacterActivity } = useCharacterActivityHandlers();
const { handleHideTracking } = useTrackAndFollowHandlers();
const themeClass = `${interfaceSettings.theme ?? 'default'}-theme`;
const [showOnTheMap, setShowOnTheMap] = useState(false);
const [showMapSettings, setShowMapSettings] = useState(false);
const [showTrackingDialog, setShowTrackingDialog] = useState(false);
/* Important Notice - this solution needs for use one instance of MapInterface */
const mapInterface = <MapInterface />;
const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []);
const handleShowMapSettings = useCallback(() => setShowMapSettings(true), []);
const handleShowTrackingDialog = useCallback(() => setShowTrackingDialog(true), []);
useMapEventListener(event => {
if (event.name === Commands.showTracking) {
setShowTrackingDialog(true);
return true;
}
});
useSkipContextMenu();
@@ -44,13 +55,21 @@ export const MapRootContent = ({}: MapRootContentProps) => {
{mapInterface}
</div>
<div className="absolute top-0 right-0 w-14 h-[calc(100%+3.5rem)] pointer-events-auto">
<RightBar onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} />
<RightBar
onShowOnTheMap={handleShowOnTheMap}
onShowMapSettings={handleShowMapSettings}
onShowTrackingDialog={handleShowTrackingDialog}
/>
</div>
</div>
) : (
<div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none">
<Topbar>
<MapContextMenu onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} />
<MapContextMenu
onShowOnTheMap={handleShowOnTheMap}
onShowMapSettings={handleShowMapSettings}
onShowTrackingDialog={handleShowTrackingDialog}
/>
</Topbar>
{mapInterface}
</div>
@@ -60,7 +79,9 @@ export const MapRootContent = ({}: MapRootContentProps) => {
{showCharacterActivity && (
<CharacterActivity visible={showCharacterActivity} onHide={handleHideCharacterActivity} />
)}
{showTrackAndFollow && <TrackAndFollow visible={showTrackAndFollow} onHide={handleHideTracking} />}
{showTrackingDialog && (
<TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} />
)}
</Layout>
</div>
);

View File

@@ -1,129 +1,17 @@
import { useEffect, useMemo, useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { ProgressSpinner } from 'primereact/progressspinner';
import { CharacterCard } from '../../../ui-kit';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
export interface ActivitySummary {
character: CharacterTypeRaw;
passages: number;
connections: number;
signatures: number;
}
import { CharacterActivityContent } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivityContent.tsx';
interface CharacterActivityProps {
visible: boolean;
onHide: () => void;
}
const getRowClassName = () => ['text-xs', 'leading-tight'];
const renderCharacterTemplate = (rowData: ActivitySummary) => {
return <CharacterCard compact isOwn {...rowData.character} />;
};
const renderValueTemplate = (rowData: ActivitySummary, field: keyof ActivitySummary) => {
return <div className="tabular-nums">{rowData[field] as number}</div>;
};
export const CharacterActivity = ({ visible, onHide }: CharacterActivityProps) => {
const { data } = useMapRootState();
const { characterActivityData } = data;
const [localActivity, setLocalActivity] = useState<ActivitySummary[]>([]);
const [loading, setLoading] = useState(true);
const activity = useMemo(() => {
return characterActivityData?.activity || [];
}, [characterActivityData]);
useEffect(() => {
setLocalActivity(activity);
setLoading(characterActivityData?.loading !== false);
}, [activity, characterActivityData]);
const renderContent = () => {
if (loading) {
return (
<div className="flex flex-col items-center justify-center h-[400px] w-full">
<ProgressSpinner className="w-[50px] h-[50px]" strokeWidth="4" />
<div className="mt-4 text-text-color-secondary text-sm">Loading character activity data...</div>
</div>
);
}
if (localActivity.length === 0) {
return (
<div className="p-8 text-center text-text-color-secondary italic">No character activity data available</div>
);
}
return (
<DataTable
value={localActivity}
scrollable
scrollHeight="400px"
resizableColumns
columnResizeMode="fit"
className="w-full"
tableClassName="w-full border-0"
emptyMessage="No character activity data available"
sortField="passages"
sortOrder={-1}
size="small"
rowClassName={getRowClassName}
rowHover
>
<Column
field="character_name"
header="Character"
body={renderCharacterTemplate}
sortable
// headerStyle={{ minWidth: '75px', height: 'auto', overflow: 'visible' }}
// bodyStyle={{ minWidth: '75px' }}
// className={classes.characterColumn}
// headerClassName={classes.columnHeader}
/>
<Column
field="passages"
header="Passages"
body={rowData => renderValueTemplate(rowData, 'passages')}
sortable
// headerStyle={{ width: '120px', textAlign: 'center', height: 'auto', overflow: 'visible' }}
// bodyStyle={{ width: '120px', textAlign: 'center' }}
// className={classes.numericColumn}
// headerClassName={classes.columnHeader}
/>
<Column
field="connections"
header="Connections"
body={rowData => renderValueTemplate(rowData, 'connections')}
sortable
// headerStyle={{ width: '120px', textAlign: 'center', height: 'auto', overflow: 'visible' }}
// bodyStyle={{ width: '120px', textAlign: 'center' }}
// className={classes.numericColumn}
// headerClassName={classes.columnHeader}
/>
<Column
field="signatures"
header="Signatures"
body={rowData => renderValueTemplate(rowData, 'signatures')}
sortable
// headerStyle={{ width: '120px', textAlign: 'center', height: 'auto', overflow: 'visible' }}
// bodyStyle={{ width: '120px', textAlign: 'center' }}
// className={classes.numericColumn}
// headerClassName={classes.columnHeader}
/>
</DataTable>
);
};
return (
<Dialog header="Character Activity" visible={visible} className="max-w-[600px]" onHide={onHide} dismissableMask>
<div className="w-full h-[400px] flex flex-col overflow-hidden p-0 m-0">{renderContent()}</div>
<Dialog header="Character Activity" visible={visible} className="w-[550px]" onHide={onHide} dismissableMask>
<div className="w-full h-[500px] flex flex-col overflow-hidden p-0 m-0">
<CharacterActivityContent />
</div>
</Dialog>
);
};

View File

@@ -0,0 +1,71 @@
import { ProgressSpinner } from 'primereact/progressspinner';
import { DataTable } from 'primereact/datatable';
import {
getRowClassName,
renderCharacterTemplate,
renderValueTemplate,
} from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/helpers.tsx';
import { Column } from 'primereact/column';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo } from 'react';
export const CharacterActivityContent = () => {
const {
data: { characterActivityData },
} = useMapRootState();
const activity = useMemo(() => characterActivityData?.activity || [], [characterActivityData]);
const loading = useMemo(() => characterActivityData?.loading !== false, [characterActivityData]);
if (loading) {
return (
<div className="flex flex-col items-center justify-center h-full w-full">
<ProgressSpinner className="w-[50px] h-[50px]" strokeWidth="4" />
<div className="mt-4 text-text-color-secondary text-sm">Loading character activity data...</div>
</div>
);
}
if (activity.length === 0) {
return <div className="p-8 text-center text-text-color-secondary italic">No character activity data available</div>;
}
return (
<DataTable
value={activity}
scrollable
className="w-full"
tableClassName="w-full border-0"
emptyMessage="No character activity data available"
sortField="passages"
sortOrder={-1}
size="small"
rowClassName={getRowClassName}
rowHover
>
<Column field="character_name" header="Character" body={renderCharacterTemplate} sortable className="!py-[6px]" />
<Column
field="passages"
header="Passages"
headerClassName="[&_.p-column-header-content]:justify-center"
body={rowData => renderValueTemplate(rowData, 'passages')}
sortable
/>
<Column
field="connections"
header="Connections"
headerClassName="[&_.p-column-header-content]:justify-center"
body={rowData => renderValueTemplate(rowData, 'connections')}
sortable
/>
<Column
field="signatures"
header="Signatures"
headerClassName="[&_.p-column-header-content]:justify-center"
body={rowData => renderValueTemplate(rowData, 'signatures')}
sortable
/>
</DataTable>
);
};

View File

@@ -0,0 +1,12 @@
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
import { ActivitySummary } from '@/hooks/Mapper/types';
export const getRowClassName = () => ['text-xs', 'leading-tight'];
export const renderCharacterTemplate = (rowData: ActivitySummary) => {
return <CharacterCard compact isOwn {...rowData.character} />;
};
export const renderValueTemplate = (rowData: ActivitySummary, field: keyof ActivitySummary) => {
return <div className="tabular-nums w-full flex justify-center">{rowData[field] as number}</div>;
};

View File

@@ -11,22 +11,16 @@ import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
export interface MapContextMenuProps {
onShowOnTheMap?: () => void;
onShowMapSettings?: () => void;
onShowTrackingDialog?: () => void;
}
export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContextMenuProps) => {
export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings, onShowTrackingDialog }: MapContextMenuProps) => {
const { outCommand, setInterfaceSettings } = useMapRootState();
const canTrackCharacters = useMapCheckPermissions([UserPermission.TRACK_CHARACTER]);
const menuRight = useRef<Menu>(null);
const handleAddCharacter = useCallback(() => {
outCommand({
type: OutCommand.showTracking,
data: {},
});
}, [outCommand]);
const handleShowActivity = useCallback(() => {
outCommand({
type: OutCommand.showActivity,
@@ -40,8 +34,8 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContext
{
label: 'Tracking',
icon: 'pi pi-user-plus',
command: handleAddCharacter,
visible: true,
command: onShowTrackingDialog,
visible: canTrackCharacters,
},
{
label: 'Character Activity',
@@ -76,7 +70,7 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContext
).filter(item => item.visible);
}, [
canTrackCharacters,
handleAddCharacter,
onShowTrackingDialog,
handleShowActivity,
onShowMapSettings,
onShowOnTheMap,

View File

@@ -1,84 +1,3 @@
.verticalTabsContainer {
display: flex;
width: 100%;
min-height: 300px;
:global {
.p-tabview {
width: 100%;
display: flex;
align-items: flex-start;
}
.p-tabview-panels {
padding: 6px 1rem !important;
flex-grow: 1;
height: 100%;
}
.p-tabview-nav-container {
border-right: none;
height: 100%;
}
.p-tabview-nav {
flex-direction: column;
width: 150px;
min-height: 100%;
border: none;
li {
width: 100%;
border-right: 4px solid var(--surface-hover);
background-color: var(--surface-card);
transition: background-color 200ms, border-right-color 200ms;
&:hover {
background-color: var(--surface-hover);
border-right: 4px solid var(--surface-100);
}
.p-tabview-nav-link {
transition: color 200ms;
justify-content: flex-end;
padding: 10px;
//background-color: var(--surface-card);
background-color: initial;
border: none;
color: var(--gray-400);
border-radius: initial;
font-weight: 400;
margin: 0;
}
&.p-tabview-selected {
background-color: var(--surface-50);
border-right: 4px solid var(--primary-color);
.p-tabview-nav-link {
font-weight: 600;
color: var(--primary-color);
}
&:hover {
//background-color: var(--surface-hover);
border-right: 4px solid var(--primary-color);
}
}
}
}
.p-tabview-panel {
flex-grow: 1;
}
}
}
.CheckboxContainer {
display: grid;
grid-template-columns: auto 1fr auto;

View File

@@ -1,210 +1,56 @@
import styles from './MapSettings.module.scss';
import { Dialog } from 'primereact/dialog';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { TabPanel, TabView } from 'primereact/tabview';
import { PrettySwitchbox } from './components';
import {
InterfaceStoredSettingsProps,
useMapRootState,
InterfaceStoredSettings,
AvailableThemes
} from '@/hooks/Mapper/mapRootProvider';
import { useMapRootState } 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';
export enum UserSettingsRemoteProps {
link_signature_on_splash = 'link_signature_on_splash',
select_on_spash = 'select_on_spash',
delete_connection_with_sigs = 'delete_connection_with_sigs',
}
export const DEFAULT_REMOTE_SETTINGS = {
[UserSettingsRemoteProps.link_signature_on_splash]: false,
[UserSettingsRemoteProps.select_on_spash]: false,
[UserSettingsRemoteProps.delete_connection_with_sigs]: false,
};
export const UserSettingsRemoteList = [
UserSettingsRemoteProps.link_signature_on_splash,
UserSettingsRemoteProps.select_on_spash,
UserSettingsRemoteProps.delete_connection_with_sigs,
];
export type UserSettingsRemote = {
link_signature_on_splash: boolean;
select_on_spash: boolean;
delete_connection_with_sigs: boolean;
};
export type UserSettings = UserSettingsRemote & InterfaceStoredSettings;
import {
CONNECTIONS_CHECKBOXES_PROPS,
SIGNATURES_CHECKBOXES_PROPS,
SYSTEMS_CHECKBOXES_PROPS,
THEME_SETTING,
UI_CHECKBOXES_PROPS,
} from './constants.ts';
import {
MapSettingsProvider,
useMapSettings,
} from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx';
import { WidgetsSettings } from './components/WidgetsSettings';
import { CommonSettings } from './components/CommonSettings';
import { SettingsListItem } from './types.ts';
export interface MapSettingsProps {
visible: boolean;
onHide: () => void;
}
type SettingsListItem = {
prop: keyof UserSettings;
label: string;
type: 'checkbox' | 'dropdown';
options?: { label: string; value: string }[];
};
const COMMON_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: InterfaceStoredSettingsProps.isShowMinimap,
label: 'Show Minimap',
type: 'checkbox',
},
];
const SYSTEMS_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: InterfaceStoredSettingsProps.isShowKSpace,
label: 'Highlight Low/High-security systems',
type: 'checkbox',
},
{
prop: UserSettingsRemoteProps.select_on_spash,
label: 'Auto-select splashed',
type: 'checkbox',
},
];
const SIGNATURES_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: UserSettingsRemoteProps.link_signature_on_splash,
label: 'Link signature on splash',
type: 'checkbox',
},
{
prop: InterfaceStoredSettingsProps.isShowUnsplashedSignatures,
label: 'Show unsplashed signatures',
type: 'checkbox',
},
];
const CONNECTIONS_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: UserSettingsRemoteProps.delete_connection_with_sigs,
label: 'Delete connections to linked signatures',
type: 'checkbox',
},
{
prop: InterfaceStoredSettingsProps.isThickConnections,
label: 'Thicker connections',
type: 'checkbox',
},
];
const UI_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: InterfaceStoredSettingsProps.isShowMenu,
label: 'Enable compact map menu bar',
type: 'checkbox',
},
{
prop: InterfaceStoredSettingsProps.isShowBackgroundPattern,
label: 'Show background pattern',
type: 'checkbox',
},
{
prop: InterfaceStoredSettingsProps.isSoftBackground,
label: 'Enable soft background',
type: 'checkbox',
},
];
const THEME_OPTIONS = [
{ label: 'Default', value: AvailableThemes.default },
{ label: 'Pathfinder', value: AvailableThemes.pathfinder },
];
const THEME_SETTING: SettingsListItem = {
prop: 'theme',
label: 'Theme',
type: 'dropdown',
options: THEME_OPTIONS,
};
export const MapSettings = ({ visible, onHide }: MapSettingsProps) => {
export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
const [activeIndex, setActiveIndex] = useState(0);
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState();
const [userRemoteSettings, setUserRemoteSettings] = useState<UserSettingsRemote>({
...DEFAULT_REMOTE_SETTINGS,
});
const { outCommand } = useMapRootState();
const mergedSettings = useMemo(() => {
return {
...userRemoteSettings,
...interfaceSettings,
};
}, [userRemoteSettings, interfaceSettings]);
const { renderSettingItem, setUserRemoteSettings } = useMapSettings();
const handleShow = async () => {
const { user_settings } = await outCommand({
const refVars = useRef({ outCommand, onHide, visible });
refVars.current = { outCommand, onHide, visible };
const handleShow = useCallback(async () => {
const { user_settings } = await refVars.current.outCommand({
type: OutCommand.getUserSettings,
data: null,
});
setUserRemoteSettings({
...user_settings,
});
};
}, [setUserRemoteSettings]);
const handleSettingChange = useCallback(
async (prop: keyof UserSettings, value: boolean | string) => {
if (UserSettingsRemoteList.includes(prop as any)) {
const newRemoteSettings = {
...userRemoteSettings,
[prop]: value,
};
await outCommand({
type: OutCommand.updateUserSettings,
data: newRemoteSettings,
});
setUserRemoteSettings(newRemoteSettings);
} else {
setInterfaceSettings({
...interfaceSettings,
[prop]: value,
});
}
},
[userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings],
);
const renderSettingItem = (item: SettingsListItem) => {
const currentValue = mergedSettings[item.prop];
if (item.type === 'checkbox') {
return (
<PrettySwitchbox
key={item.prop}
label={item.label}
checked={!!currentValue}
setChecked={checked => handleSettingChange(item.prop, checked)}
/>
);
const handleHide = useCallback(() => {
if (!refVars.current.visible) {
return;
}
if (item.type === 'dropdown' && item.options) {
return (
<div key={item.prop} className="flex items-center gap-2 mt-2">
<label className="text-sm">{item.label}:</label>
<Dropdown
className="text-sm"
value={currentValue}
options={item.options}
onChange={e => handleSettingChange(item.prop, e.value)}
placeholder="Select a theme"
/>
</div>
);
}
return null;
};
setActiveIndex(0);
refVars.current.onHide();
}, []);
const renderSettingsList = (list: SettingsListItem[]) => {
return list.map(renderSettingItem);
@@ -217,47 +63,53 @@ export const MapSettings = ({ visible, onHide }: MapSettingsProps) => {
draggable={false}
style={{ width: '550px' }}
onShow={handleShow}
onHide={() => {
if (!visible) return;
setActiveIndex(0);
onHide();
}}
onHide={handleHide}
>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<div className={styles.verticalTabsContainer}>
<TabView activeIndex={activeIndex} onTabChange={e => setActiveIndex(e.index)}>
<TabPanel header="Common" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">{renderSettingsList(COMMON_CHECKBOXES_PROPS)}</div>
</TabPanel>
<TabView
activeIndex={activeIndex}
className="vertical-tabs-container"
onTabChange={e => setActiveIndex(e.index)}
>
<TabPanel header="Common" headerClassName={styles.verticalTabHeader}>
<CommonSettings />
</TabPanel>
<TabPanel header="Systems" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">{renderSettingsList(SYSTEMS_CHECKBOXES_PROPS)}</div>
</TabPanel>
<TabPanel header="Systems" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">{renderSettingsList(SYSTEMS_CHECKBOXES_PROPS)}</div>
</TabPanel>
<TabPanel header="Connections" headerClassName={styles.verticalTabHeader}>
{renderSettingsList(CONNECTIONS_CHECKBOXES_PROPS)}
</TabPanel>
<TabPanel header="Connections" headerClassName={styles.verticalTabHeader}>
{renderSettingsList(CONNECTIONS_CHECKBOXES_PROPS)}
</TabPanel>
<TabPanel header="Signatures" headerClassName={styles.verticalTabHeader}>
{renderSettingsList(SIGNATURES_CHECKBOXES_PROPS)}
</TabPanel>
<TabPanel header="Signatures" headerClassName={styles.verticalTabHeader}>
{renderSettingsList(SIGNATURES_CHECKBOXES_PROPS)}
</TabPanel>
<TabPanel header="User Interface" headerClassName={styles.verticalTabHeader}>
{renderSettingsList(UI_CHECKBOXES_PROPS)}
</TabPanel>
<TabPanel header="User Interface" headerClassName={styles.verticalTabHeader}>
{renderSettingsList(UI_CHECKBOXES_PROPS)}
</TabPanel>
<TabPanel header="Widgets" className="h-full" headerClassName={styles.verticalTabHeader}>
<WidgetsSettings />
</TabPanel>
<TabPanel header="Widgets" className="h-full" headerClassName={styles.verticalTabHeader}>
<WidgetsSettings />
</TabPanel>
<TabPanel header="Theme" headerClassName={styles.verticalTabHeader}>
{renderSettingItem(THEME_SETTING)}
</TabPanel>
</TabView>
</div>
<TabPanel header="Theme" headerClassName={styles.verticalTabHeader}>
{renderSettingItem(THEME_SETTING)}
</TabPanel>
</TabView>
</div>
</div>
</Dialog>
);
};
export const MapSettings = (props: MapSettingsProps) => {
return (
<MapSettingsProvider>
<MapSettingsComp {...props} />
</MapSettingsProvider>
);
};

View File

@@ -0,0 +1,119 @@
import {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useCallback,
useContext,
useMemo,
useRef,
useState,
} from 'react';
import {
SettingsListItem,
UserSettings,
UserSettingsRemote,
} from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/types.ts';
import {
DEFAULT_REMOTE_SETTINGS,
UserSettingsRemoteList,
} from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/constants.ts';
import { OutCommand } from '@/hooks/Mapper/types';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { Dropdown } from 'primereact/dropdown';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
type MapSettingsContextType = {
renderSettingItem: (item: SettingsListItem) => ReactNode;
setUserRemoteSettings: Dispatch<SetStateAction<UserSettingsRemote>>;
};
const MapSettingsContext = createContext<MapSettingsContextType | undefined>(undefined);
export const MapSettingsProvider = ({ children }: { children: ReactNode }) => {
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState();
const [userRemoteSettings, setUserRemoteSettings] = useState<UserSettingsRemote>({
...DEFAULT_REMOTE_SETTINGS,
});
const mergedSettings = useMemo(() => {
return {
...userRemoteSettings,
...interfaceSettings,
};
}, [userRemoteSettings, interfaceSettings]);
const refVars = useRef({ mergedSettings, userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings });
refVars.current = { mergedSettings, userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings };
const handleSettingChange = useCallback(async (prop: keyof UserSettings, value: boolean | string) => {
const { userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings } = refVars.current;
if (UserSettingsRemoteList.includes(prop as any)) {
const newRemoteSettings = {
...userRemoteSettings,
[prop]: value,
};
await outCommand({
type: OutCommand.updateUserSettings,
data: newRemoteSettings,
});
setUserRemoteSettings(newRemoteSettings);
} else {
setInterfaceSettings({
...interfaceSettings,
[prop]: value,
});
}
}, []);
const renderSettingItem = useCallback(
(item: SettingsListItem) => {
const currentValue = refVars.current.mergedSettings[item.prop];
if (item.type === 'checkbox') {
return (
<PrettySwitchbox
key={item.prop}
label={item.label}
checked={!!currentValue}
setChecked={checked => handleSettingChange(item.prop, checked)}
/>
);
}
if (item.type === 'dropdown' && item.options) {
return (
<div key={item.prop} className="flex items-center gap-2 mt-2">
<label className="text-sm">{item.label}:</label>
<Dropdown
className="text-sm"
value={currentValue}
options={item.options}
onChange={e => handleSettingChange(item.prop, e.value)}
placeholder="Select a theme"
/>
</div>
);
}
return null;
},
[handleSettingChange],
);
return (
<MapSettingsContext.Provider value={{ renderSettingItem, setUserRemoteSettings }}>
{children}
</MapSettingsContext.Provider>
);
};
export const useMapSettings = () => {
const context = useContext(MapSettingsContext);
if (!context) {
throw new Error('useMapSettings must be used within a MapSettingsProvider');
}
return context;
};

View File

@@ -0,0 +1,13 @@
import { COMMON_CHECKBOXES_PROPS } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/constants.ts';
import { useMapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx';
import { SettingsListItem } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/types.ts';
export const CommonSettings = () => {
const { renderSettingItem } = useMapSettings();
const renderSettingsList = (list: SettingsListItem[]) => {
return list.map(renderSettingItem);
};
return <div className="w-full h-full flex flex-col gap-1">{renderSettingsList(COMMON_CHECKBOXES_PROPS)}</div>;
};

View File

@@ -1,4 +1,4 @@
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components/index.ts';
import { WIDGETS_CHECKBOXES_PROPS, WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback } from 'react';

View File

@@ -0,0 +1,91 @@
import { SettingsListItem, UserSettingsRemoteProps } from './types.ts';
import { AvailableThemes, InterfaceStoredSettingsProps } from '@/hooks/Mapper/mapRootProvider';
export const DEFAULT_REMOTE_SETTINGS = {
[UserSettingsRemoteProps.link_signature_on_splash]: false,
[UserSettingsRemoteProps.select_on_spash]: false,
[UserSettingsRemoteProps.delete_connection_with_sigs]: false,
};
export const UserSettingsRemoteList = [
UserSettingsRemoteProps.link_signature_on_splash,
UserSettingsRemoteProps.select_on_spash,
UserSettingsRemoteProps.delete_connection_with_sigs,
];
export const COMMON_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: InterfaceStoredSettingsProps.isShowMinimap,
label: 'Show Minimap',
type: 'checkbox',
},
];
export const SYSTEMS_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: InterfaceStoredSettingsProps.isShowKSpace,
label: 'Highlight Low/High-security systems',
type: 'checkbox',
},
{
prop: UserSettingsRemoteProps.select_on_spash,
label: 'Auto-select splashed',
type: 'checkbox',
},
];
export const SIGNATURES_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: UserSettingsRemoteProps.link_signature_on_splash,
label: 'Link signature on splash',
type: 'checkbox',
},
{
prop: InterfaceStoredSettingsProps.isShowUnsplashedSignatures,
label: 'Show unsplashed signatures',
type: 'checkbox',
},
];
export const CONNECTIONS_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: UserSettingsRemoteProps.delete_connection_with_sigs,
label: 'Delete connections to linked signatures',
type: 'checkbox',
},
{
prop: InterfaceStoredSettingsProps.isThickConnections,
label: 'Thicker connections',
type: 'checkbox',
},
];
export const UI_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: InterfaceStoredSettingsProps.isShowMenu,
label: 'Enable compact map menu bar',
type: 'checkbox',
},
{
prop: InterfaceStoredSettingsProps.isShowBackgroundPattern,
label: 'Show background pattern',
type: 'checkbox',
},
{
prop: InterfaceStoredSettingsProps.isSoftBackground,
label: 'Enable soft background',
type: 'checkbox',
},
];
export const THEME_OPTIONS = [
{ label: 'Default', value: AvailableThemes.default },
{ label: 'Pathfinder', value: AvailableThemes.pathfinder },
];
export const THEME_SETTING: SettingsListItem = {
prop: 'theme',
label: 'Theme',
type: 'dropdown',
options: THEME_OPTIONS,
};

View File

@@ -0,0 +1,22 @@
import { InterfaceStoredSettings } from '@/hooks/Mapper/mapRootProvider';
export enum UserSettingsRemoteProps {
link_signature_on_splash = 'link_signature_on_splash',
select_on_spash = 'select_on_spash',
delete_connection_with_sigs = 'delete_connection_with_sigs',
}
export type UserSettingsRemote = {
link_signature_on_splash: boolean;
select_on_spash: boolean;
delete_connection_with_sigs: boolean;
};
export type UserSettings = UserSettingsRemote & InterfaceStoredSettings;
export type SettingsListItem = {
prop: keyof UserSettings;
label: string;
type: 'checkbox' | 'dropdown';
options?: { label: string; value: string }[];
};

View File

@@ -1,7 +1,6 @@
import classes from './RightBar.module.scss';
import clsx from 'clsx';
import { useCallback } from 'react';
import { OutCommand } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
@@ -12,23 +11,16 @@ import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
interface RightBarProps {
onShowOnTheMap?: () => void;
onShowMapSettings?: () => void;
onShowTrackingDialog?: () => void;
}
export const RightBar = ({ onShowOnTheMap, onShowMapSettings }: RightBarProps) => {
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState();
export const RightBar = ({ onShowOnTheMap, onShowMapSettings, onShowTrackingDialog }: RightBarProps) => {
const { interfaceSettings, setInterfaceSettings } = useMapRootState();
const canTrackCharacters = useMapCheckPermissions([UserPermission.TRACK_CHARACTER]);
const isShowMinimap = interfaceSettings.isShowMinimap === undefined ? true : interfaceSettings.isShowMinimap;
const handleShowTracking = useCallback(() => {
// Use the OutCommand pattern for showing the tracking dialog
outCommand({
type: OutCommand.showTracking,
data: {},
});
}, [outCommand]);
const toggleMinimap = useCallback(() => {
setInterfaceSettings(x => ({
...x,
@@ -60,19 +52,19 @@ export const RightBar = ({ onShowOnTheMap, onShowMapSettings }: RightBarProps) =
)}
>
<div className="flex flex-col gap-2 items-center mt-1">
<WdTooltipWrapper content="Tracking status" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={handleShowTracking}
id="show-tracking-button"
>
<i className="pi pi-user-plus"></i>
</button>
</WdTooltipWrapper>
{canTrackCharacters && (
<>
<WdTooltipWrapper content="Tracking status" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={onShowTrackingDialog}
id="show-tracking-button"
>
<i className="pi pi-user-plus"></i>
</button>
</WdTooltipWrapper>
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"

View File

@@ -1,3 +0,0 @@
.trackFollowHeader {
background-color: #1e1e1e;
}

View File

@@ -1,115 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { VirtualScroller } from 'primereact/virtualscroller';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { TrackingCharacterWrapper } from './TrackingCharacterWrapper';
import { TrackingCharacter } from './types';
interface TrackAndFollowProps {
visible: boolean;
onHide: () => void;
}
export const TrackAndFollow = ({ visible, onHide }: TrackAndFollowProps) => {
const [trackedCharacters, setTrackedCharacters] = useState<string[]>([]);
const [followedCharacter, setFollowedCharacter] = useState<string | null>(null);
const { outCommand, data } = useMapRootState();
const { trackingCharactersData } = data;
const characters = useMemo(() => trackingCharactersData || [], [trackingCharactersData]);
useEffect(() => {
if (trackingCharactersData) {
const newTrackedCharacters = trackingCharactersData.filter(tc => tc.tracked).map(tc => tc.character.eve_id);
setTrackedCharacters(newTrackedCharacters);
const followedChar = trackingCharactersData.find(tc => tc.followed);
if (followedChar?.character?.eve_id !== followedCharacter) {
setFollowedCharacter(followedChar?.character?.eve_id || null);
}
}
}, [followedCharacter, trackingCharactersData]);
const handleTrackToggle = (characterId: string) => {
const isCurrentlyTracked = trackedCharacters.includes(characterId);
if (isCurrentlyTracked) {
setTrackedCharacters(prev => prev.filter(id => id !== characterId));
} else {
setTrackedCharacters(prev => [...prev, characterId]);
}
outCommand({
type: OutCommand.toggleTrack,
data: { 'character-id': characterId },
});
};
const handleFollowToggle = async (characterEveId: string) => {
const isCurrentlyFollowed = followedCharacter === characterEveId;
const isCurrentlyTracked = trackedCharacters.includes(characterEveId);
// If not followed and not tracked, we need to track it first
if (!isCurrentlyFollowed && !isCurrentlyTracked) {
setTrackedCharacters(prev => [...prev, characterEveId]);
// Send track command first
await outCommand({
type: OutCommand.toggleTrack,
data: { 'character-id': characterEveId },
});
// Then send follow command after a short delay
setTimeout(() => {
outCommand({
type: OutCommand.toggleFollow,
data: { 'character-id': characterEveId },
});
}, 100);
} else {
// Otherwise just toggle follow
await outCommand({
type: OutCommand.toggleFollow,
data: { 'character-id': characterEveId },
});
}
};
const rowTemplate = (tc: TrackingCharacter) => {
return (
<TrackingCharacterWrapper
key={tc.character.eve_id}
character={tc.character}
isTracked={trackedCharacters.includes(tc.character.eve_id)}
isFollowed={followedCharacter === tc.character.eve_id}
onTrackToggle={() => handleTrackToggle(tc.character.eve_id)}
onFollowToggle={() => handleFollowToggle(tc.character.eve_id)}
/>
);
};
return (
<Dialog
header={
<div className="dialog-header">
<span>Track & Follow</span>
</div>
}
visible={visible}
onHide={onHide}
className="w-[500px] text-text-color"
contentClassName="!p-0"
>
<div className="w-full overflow-hidden">
<div className="grid grid-cols-[80px_80px_1fr] p-1 font-normal text-sm text-center border-b border-[#383838]">
<div>Track</div>
<div>Follow</div>
<div className="text-center">Character</div>
</div>
<VirtualScroller items={characters} itemSize={48} itemTemplate={rowTemplate} className="h-72 w-full" />
</div>
</Dialog>
);
};

View File

@@ -1,10 +0,0 @@
.characterRow {
border-color: var(--surface-border);
border-width: 0 0 1px 0;
border-style: solid;
opacity: 0.5;
&:last-child {
border-bottom: none;
}
}

View File

@@ -1,47 +0,0 @@
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit/WdCheckbox/WdCheckbox';
import WdRadioButton from '@/hooks/Mapper/components/ui-kit/WdRadioButton';
import { CharacterCard, TooltipPosition, WdTooltipWrapper } from '../../../ui-kit';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
interface TrackingCharacterWrapperProps {
character: CharacterTypeRaw;
isTracked: boolean;
isFollowed: boolean;
onTrackToggle: () => void;
onFollowToggle: () => void;
}
export const TrackingCharacterWrapper = ({
character,
isTracked,
isFollowed,
onTrackToggle,
onFollowToggle,
}: TrackingCharacterWrapperProps) => {
const trackCheckboxId = `track-${character.eve_id}`;
const followRadioId = `follow-${character.eve_id}`;
return (
<div className="grid grid-cols-[80px_80px_1fr] items-center min-h-8 hover:bg-neutral-800 border-b border-[#383838]">
<div className="flex justify-center items-center p-0.5 text-center">
<WdTooltipWrapper content="Track this character on the map" position={TooltipPosition.top}>
<div className="flex justify-center items-center w-full">
<WdCheckbox id={trackCheckboxId} label="" value={isTracked} onChange={() => onTrackToggle()} />
</div>
</WdTooltipWrapper>
</div>
<div className="flex justify-center items-center p-0.5 text-center">
<WdTooltipWrapper content="Follow this character's movements on the map" position={TooltipPosition.top}>
<div className="flex justify-center items-center w-full">
<div onClick={onFollowToggle} className="cursor-pointer">
<WdRadioButton id={followRadioId} name="followed_character" checked={isFollowed} onChange={() => {}} />
</div>
</div>
</WdTooltipWrapper>
</div>
<div className="flex items-center justify-center">
<CharacterCard showShipName={false} showSystem={false} isOwn {...character} />
</div>
</div>
);
};

View File

@@ -1,9 +0,0 @@
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
/**
* Interface for a character that can be tracked and followed
*/
export interface TrackingCharacter {
character: CharacterTypeRaw;
tracked: boolean;
followed: boolean;
}

View File

@@ -0,0 +1,52 @@
import { Column } from 'primereact/column';
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
import { DataTable } from 'primereact/datatable';
import { useCallback, useEffect, useRef, useState } from 'react';
import { TrackingCharacter } from '@/hooks/Mapper/types';
import { useTracking } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog/TrackingProvider.tsx';
export const TrackingCharactersList = () => {
const [selected, setSelected] = useState<TrackingCharacter[]>([]);
const { trackingCharacters, updateTracking } = useTracking();
const refVars = useRef({ trackingCharacters });
refVars.current = { trackingCharacters };
useEffect(() => {
setSelected(trackingCharacters.filter(x => x.tracked));
}, [trackingCharacters]);
const handleChangeSelect = useCallback(
(selected: TrackingCharacter[]) => updateTracking(selected.map(x => x.character.eve_id)),
[updateTracking],
);
return (
<DataTable
value={trackingCharacters}
size="small"
selectionMode={null}
selection={selected}
onSelectionChange={e => handleChangeSelect(e.value)}
virtualScrollerOptions={{ itemSize: 40 }}
className="relative w-full select-none min-h-0 h-full"
resizableColumns={false}
rowHover
selectAll
>
<Column
selectionMode="multiple"
headerClassName="h-[40px] !pl-4"
className="w-12 max-w-12 !pl-4 [&_div]:mt-[-2px] "
/>
<Column
field="eve_id"
header="Character with tracking access"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
headerClassName="[&_div]:ml-2"
body={row => {
return <CharacterCard showShipName={false} showSystem={false} isOwn {...row.character} />;
}}
/>
</DataTable>
);
};

View File

@@ -0,0 +1,66 @@
import { useEffect, useRef, useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { TabPanel, TabView } from 'primereact/tabview';
import { TrackingSettings } from './TrackingSettings.tsx';
import { TrackingCharactersList } from './TrackingCharactersList.tsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { TrackingProvider, useTracking } from './TrackingProvider.tsx';
interface TrackingDialogProps {
visible: boolean;
onHide: () => void;
}
const TrackingDialogComp = ({ visible, onHide }: TrackingDialogProps) => {
const [activeIndex, setActiveIndex] = useState(0);
const { outCommand } = useMapRootState();
const { loadTracking } = useTracking();
const refVars = useRef({ outCommand });
refVars.current = { outCommand };
useEffect(() => {
if (!visible) {
return;
}
loadTracking();
}, [loadTracking, visible]);
return (
<Dialog
header={
<div className="dialog-header">
<span className="pointer-events-none">Track & Follow</span>
</div>
}
draggable={false}
resizable={false}
visible={visible}
onHide={onHide}
className="w-[640px] h-[400px] text-text-color min-h-0"
>
<TabView
className="vertical-tabs-container h-full [&_.p-tabview-panels]:!pr-0"
activeIndex={activeIndex}
onTabChange={e => setActiveIndex(e.index)}
renderActiveOnly={false}
>
<TabPanel header="Tracking" contentClassName="h-full">
<TrackingCharactersList />
</TabPanel>
<TabPanel header="Follow & Settings">
<TrackingSettings />
</TabPanel>
</TabView>
</Dialog>
);
};
export const TrackingDialog = (props: TrackingDialogProps) => {
return (
<TrackingProvider>
<TrackingDialogComp {...props} />
</TrackingProvider>
);
};

View File

@@ -0,0 +1,149 @@
import { createContext, useCallback, useContext, useRef, useState } from 'react';
import { OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { IncomingEvent, WithChildren } from '@/hooks/Mapper/types/common.ts';
import { CommandInCharactersTrackingInfo } from '@/hooks/Mapper/types/commandsIn.ts';
type DiffTrackingInfo = { characterId: string; tracked: boolean };
type TrackingContextType = {
loadTracking: () => void;
updateTracking: (selected: string[]) => void;
updateFollowing: (characterId: string | null) => void;
updateMain: (characterId: string) => void;
trackingCharacters: TrackingCharacter[];
following: string | null;
main: string | null;
loading: boolean;
};
const TrackingContext = createContext<TrackingContextType | undefined>(undefined);
export const TrackingProvider = ({ children }: WithChildren) => {
const [trackingCharacters, setTrackingCharacters] = useState<TrackingCharacter[]>([]);
const [following, setFollowing] = useState<string | null>(null);
const [main, setMain] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const { outCommand } = useMapRootState();
const refVars = useRef({ outCommand, trackingCharacters, following });
refVars.current = { outCommand, trackingCharacters, following };
const loadTracking = useCallback(async () => {
setLoading(true);
try {
const res: IncomingEvent<CommandInCharactersTrackingInfo> = await refVars.current.outCommand({
type: OutCommand.getCharactersTrackingInfo,
data: {},
});
setTrackingCharacters(res.data.characters);
setFollowing(res.data.following);
setMain(res.data.main);
} catch (err) {
console.error('TrackingProviderError', err);
}
setLoading(false);
}, []);
const changeTrackingCommand = useCallback(
async (characterId: string, track: boolean) => {
try {
await outCommand({
type: OutCommand.updateCharacterTracking,
data: { character_eve_id: characterId, track },
});
} catch (error) {
console.error('Error toggling track:', error);
}
},
[outCommand],
);
const updateFollowing = useCallback(
async (characterId: string | null) => {
try {
await outCommand({
type: OutCommand.updateFollowingCharacter,
data: { character_eve_id: characterId },
});
setFollowing(characterId);
} catch (error) {
console.error('Error toggling follow:', error);
}
},
[outCommand],
);
const updateTracking = useCallback(
async (selected: string[]) => {
const { following, trackingCharacters } = refVars.current;
const diffToUpdate: DiffTrackingInfo[] = [];
const newVal = trackingCharacters.map(x => {
const next = selected.includes(x.character.eve_id);
if (next !== x.tracked) {
diffToUpdate.push({ characterId: x.character.eve_id, tracked: next });
}
return {
tracked: selected.includes(x.character.eve_id),
character: x.character,
};
});
await Promise.all(diffToUpdate.map(x => changeTrackingCommand(x.characterId, x.tracked)));
if (newVal.some(x => following != null && x.character.eve_id === following && !x.tracked)) {
await updateFollowing(null);
setFollowing(null);
}
setTrackingCharacters(newVal);
},
[changeTrackingCommand, updateFollowing],
);
const updateMain = useCallback(
async (characterId: string) => {
try {
await outCommand({
type: OutCommand.updateMainCharacter,
data: { character_eve_id: characterId },
});
setMain(characterId);
} catch (error) {
console.error('Error toggling main:', error);
}
},
[outCommand],
);
return (
<TrackingContext.Provider
value={{
loadTracking,
trackingCharacters,
following,
main,
loading,
updateTracking,
updateFollowing,
updateMain,
}}
>
{children}
</TrackingContext.Provider>
);
};
export const useTracking = () => {
const context = useContext(TrackingContext);
if (!context) {
throw new Error('useTracking must be used within a TrackingProvider');
}
return context;
};

View File

@@ -0,0 +1,79 @@
import { Dropdown } from 'primereact/dropdown';
import { useCallback, useMemo } from 'react';
import { TrackingCharacter } from '@/hooks/Mapper/types';
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
import { useTracking } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog/TrackingProvider.tsx';
const renderValCharacterTemplate = (row: TrackingCharacter | undefined) => {
if (!row) {
return <div className="h-[26px] flex items-center">Character is not selected</div>;
}
return (
<div className="py-1">
<CharacterCard compact showShipName={false} showSystem={false} isOwn {...row.character} />
</div>
);
};
const renderCharacterTemplate = (row: TrackingCharacter | undefined) => {
if (!row) {
return <div className="h-[33px] flex items-center">Character is not selected</div>;
}
return <CharacterCard showShipName={false} showSystem={false} isOwn {...row.character} />;
};
export const TrackingSettings = () => {
const { trackingCharacters, following, main, updateFollowing, updateMain } = useTracking();
const followingChar = useMemo(
() => trackingCharacters.filter(x => x.tracked).find(x => x.character.eve_id === following),
[following, trackingCharacters],
);
const availableForFollowingCharacters = useMemo(
() => trackingCharacters.filter(x => x.tracked),
[trackingCharacters],
);
const mainChar = useMemo(() => trackingCharacters.find(x => x.character.eve_id === main), [main, trackingCharacters]);
const handleSelectFollowing = useCallback(
(e: TrackingCharacter | null) => updateFollowing(e == null ? null : e.character.eve_id),
[updateFollowing],
);
const handleSelectMain = useCallback((e: TrackingCharacter) => updateMain(e.character.eve_id), [updateMain]);
return (
<div className="w-full h-full flex flex-col gap-1">
<div className="flex items-center justify-between gap-2 mx-2">
<label className="text-stone-400 text-[13px] select-none">Main character</label>
<Dropdown
options={trackingCharacters}
value={mainChar}
onChange={e => handleSelectMain(e.value)}
className="w-[230px]"
itemTemplate={renderCharacterTemplate}
valueTemplate={renderValCharacterTemplate}
/>
</div>
<div className="flex items-center justify-between gap-2 mx-2">
<label className="text-stone-400 text-[13px] select-none">Following character</label>
<Dropdown
disabled={availableForFollowingCharacters.length === 0}
options={availableForFollowingCharacters}
value={followingChar}
onChange={e => handleSelectFollowing(e.value)}
className="w-[230px]"
itemTemplate={renderCharacterTemplate}
valueTemplate={renderValCharacterTemplate}
showClear
placeholder="Character is not selected"
/>
</div>
</div>
);
};

View File

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

View File

@@ -18,13 +18,7 @@ export const useCharacterActivityHandlers = () => {
...state,
showCharacterActivity: false,
}));
// Send the command to the server
outCommand({
type: OutCommand.hideActivity,
data: {},
});
}, [outCommand, update]);
}, [update]);
/**
* Handle showing the character activity dialog
@@ -56,7 +50,7 @@ export const useCharacterActivityHandlers = () => {
// Update local state with the activity data
update(state => ({
...state,
characterActivityData: activityData.activity,
characterActivityData: activityData,
showCharacterActivity: true,
}));
},

View File

@@ -1,122 +0,0 @@
import { useCallback } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand, CommandData, Commands } from '@/hooks/Mapper/types/mapHandlers';
import type { TrackingCharacter } from '@/hooks/Mapper/components/mapRootContent/components/TrackAndFollow/types';
/**
* Hook for track and follow related handlers
*/
export const useTrackAndFollowHandlers = () => {
const { outCommand, update } = useMapRootState();
/**
* Handle hiding the track and follow dialog
*/
const handleHideTracking = useCallback(() => {
// Send the command to the server first
outCommand({
type: OutCommand.hideTracking,
data: {},
});
// Then update local state to hide the dialog
update(state => ({
...state,
showTrackAndFollow: false,
}));
}, [outCommand, update]);
/**
* Handle showing the track and follow dialog
*/
const handleShowTracking = useCallback(() => {
// Update local state to show the dialog
update(state => ({
...state,
showTrackAndFollow: true,
}));
// Send the command to the server
outCommand({
type: OutCommand.showTracking,
data: {},
});
}, [outCommand, update]);
/**
* Handle updating tracking data
*/
const handleUpdateTracking = useCallback(
(trackingData: { characters: TrackingCharacter[] }) => {
if (!trackingData || !trackingData.characters) {
console.error('Invalid tracking data received:', trackingData);
return;
}
// Update local state with the tracking data
update(state => ({
...state,
trackingCharactersData: trackingData.characters,
showTrackAndFollow: true,
}));
},
[update],
);
/**
* Handle toggling character tracking
*/
const handleToggleTrack = useCallback(
(characterId: string) => {
if (!characterId) return;
// Send the toggle track command to the server
outCommand({
type: OutCommand.toggleTrack,
data: { 'character-id': characterId },
});
// Note: The local state is now updated in the TrackAndFollow component
// for immediate UI feedback, while we wait for the server response
},
[outCommand],
);
/**
* Handle toggling character following
*/
const handleToggleFollow = useCallback(
(characterId: string) => {
if (!characterId) return;
// Send the toggle follow command to the server
outCommand({
type: OutCommand.toggleFollow,
data: { 'character-id': characterId },
});
// Note: The local state is now updated in the TrackAndFollow component
// for immediate UI feedback, while we wait for the server response
},
[outCommand],
);
/**
* Handle user settings updates
*/
const handleUserSettingsUpdated = useCallback((settingsData: CommandData[Commands.userSettingsUpdated]) => {
if (!settingsData || !settingsData.settings) {
console.error('Invalid settings data received:', settingsData);
}
}, []);
return {
handleHideTracking,
handleShowTracking,
handleUpdateTracking,
handleToggleTrack,
handleToggleFollow,
handleUserSettingsUpdated,
};
};

View File

@@ -1,5 +1,5 @@
import { Map, MAP_ROOT_ID } from '@/hooks/Mapper/components/map/Map.tsx';
import { useCallback, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OnMapAddSystemCallback, OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
@@ -34,13 +34,14 @@ export const MapWrapper = () => {
const {
update,
outCommand,
data: { selectedConnections, selectedSystems, hubs, systems, linkSignatureToSystem },
data: { selectedConnections, selectedSystems, hubs, systems, linkSignatureToSystem, systemSignatures },
interfaceSettings: {
isShowMenu,
isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap,
isShowKSpace,
isThickConnections,
isShowBackgroundPattern,
isShowUnsplashedSignatures,
isSoftBackground,
theme,
},
@@ -58,8 +59,15 @@ export const MapWrapper = () => {
const [openAddSystem, setOpenAddSystem] = useState<XYPosition | null>(null);
const [selectedConnection, setSelectedConnection] = useState<SolarSystemConnection | null>(null);
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems });
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems };
const ref = useRef({
selectedConnections,
selectedSystems,
systemContextProps,
systems,
systemSignatures,
deleteSystems,
});
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, systemSignatures, deleteSystems };
useMapEventListener(event => {
runCommand(event);
@@ -128,7 +136,7 @@ export const MapWrapper = () => {
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
async item => {
if (ref.current.systems.some(x => x.system_static_info.solar_system_id === item.value)) {
if (ref.current.systems.some(x => parseInt(x.id) === item.value)) {
emitMapEvent({
name: Commands.centerSystem,
data: item.value.toString(),
@@ -156,6 +164,15 @@ export const MapWrapper = () => {
handleDeleteSelected();
});
useEffect(() => {
const { systemSignatures, systems } = ref.current;
if (!isShowUnsplashedSignatures || Object.keys(systemSignatures).length !== 0 || systems?.length === 0) {
return;
}
outCommand({ type: OutCommand.loadSignatures, data: {} });
}, [isShowUnsplashedSignatures, systems]);
return (
<>
<Map

View File

@@ -1,4 +1,4 @@
import Characters from '../characters/Characters';
import { Characters } from '../characters/Characters';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo } from 'react';
import clsx from 'clsx';

View File

@@ -0,0 +1,18 @@
.Docked {
content: " ";
display: inline-block;
width: 11px;
height: 11px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
position: absolute;
z-index: 1;
overflow: hidden;
border-radius: 1px;
background-image: url(/images/citadelLarge.png);
left: 10px;
top: 10px;
transform: rotateZ(0deg);
}

View File

@@ -5,6 +5,8 @@ import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
import { Commands } from '@/hooks/Mapper/types/mapHandlers';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { CharacterPortrait, CharacterPortraitSize } from '@/hooks/Mapper/components/ui-kit';
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
import classes from './CharacterCard.module.scss';
type CharacterCardProps = {
compact?: boolean;
@@ -45,8 +47,9 @@ export const CharacterCard = ({
if (compact) {
return (
<div className={clsx('w-full text-xs box-border')} onClick={handleSelect}>
<div className="w-full flex items-center gap-1">
<div className="w-full flex items-center gap-1 relative">
<CharacterPortrait characterEveId={char.eve_id} size={CharacterPortraitSize.w18} />
{isDocked(char.location) && <span className={classes.Docked} />}
<div className="flex flex-grow overflow-hidden text-left">
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
<span className={clsx(isOwn ? 'text-orange-400' : 'text-gray-200')}>{char.name}</span>{' '}

View File

@@ -39,259 +39,262 @@ interface TriggerInfo {
const LEAVE_DELAY = 100;
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);
export const WdTooltip = forwardRef(
(
{
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);
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
const [triggerInfo, setTriggerInfo] = useState<TriggerInfo | null>(null);
const [triggerInfo, setTriggerInfo] = useState<TriggerInfo | null>(null);
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | 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 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;
const tooltipWidth = tooltipRef.current.offsetWidth;
const tooltipHeight = tooltipRef.current.offsetHeight;
let newLeft = x;
let newTop = y;
let newLeft = x;
let newTop = y;
if (newLeft < 0) {
newLeft = 10;
}
if (newLeft < 0) {
newLeft = 10;
}
if (newTop < 0) {
newTop = 10;
}
if (newTop < 0) {
newTop = 10;
}
const rightEdge = newLeft + tooltipWidth + 10;
if (rightEdge > window.innerWidth) {
newLeft = window.innerWidth - tooltipWidth - 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;
}
const bottomEdge = newTop + tooltipHeight + 10;
if (bottomEdge > window.innerHeight) {
newTop = window.innerHeight - tooltipHeight - 10;
}
return { left: newLeft, top: newTop };
}, []);
return { left: newLeft, top: newTop };
}, []);
const scheduleHide = useCallback(() => {
if (!interactive) {
setVisible(false);
setPos(null);
return;
}
if (!hideTimeoutRef.current) {
hideTimeoutRef.current = setTimeout(() => {
const scheduleHide = useCallback(() => {
if (!interactive) {
setVisible(false);
setPos(null);
}, LEAVE_DELAY);
}
}, [interactive]);
useImperativeHandle(ref, () => ({
show: (e?: React.MouseEvent) => {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
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();
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,
}));
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) {
hideTimeoutRef.current = setTimeout(() => {
setVisible(false);
setPos(null);
}, LEAVE_DELAY);
}
}, [interactive]);
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.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
if (x <= 0) {
x = rect.left + rect.width + offset;
}
break;
case TooltipPosition.right:
x = rect.left + rect.width + offset;
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;
break;
case TooltipPosition.bottom:
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
y = rect.bottom + offset;
break;
}
setPos(calcTooltipPosition({ x, y }));
}
};
const debounced = debounce(handleMouseMove, 15);
document.addEventListener('mousemove', debounced);
return () => {
document.removeEventListener('mousemove', debounced);
debounced.cancel();
};
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition, scheduleHide]);
useEffect(() => {
return () => {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
}
};
}, []);
if (!visible) return null;
return createPortal(
<div
ref={tooltipRef}
className={clsx(
classes.tooltip,
interactive ? 'pointer-events-auto' : 'pointer-events-none',
'absolute px-2 py-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) {
useImperativeHandle(ref, () => ({
show: (e?: React.MouseEvent) => {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
setIsMouseInsideTooltip(true);
}}
onMouseLeave={() => {
setIsMouseInsideTooltip(false);
if (interactive) {
scheduleHide();
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();
setTriggerInfo({ clientX: e.clientX, clientY: e.clientY, rect });
}
}
}}
{...restProps}
>
{typeof content === 'function' ? content() : content}
</div>,
document.body,
);
});
setVisible(true);
},
hide: () => {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
}
setVisible(false);
setPos(null);
},
getIsMouseInside: () => isMouseInsideTooltip,
}));
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.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
if (x <= 0) {
x = rect.left + rect.width + offset;
}
break;
case TooltipPosition.right:
x = rect.left + rect.width + offset;
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;
break;
case TooltipPosition.bottom:
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
y = rect.bottom + offset;
break;
}
setPos(calcTooltipPosition({ x, y }));
}
};
const debounced = debounce(handleMouseMove, 15);
document.addEventListener('mousemove', debounced);
return () => {
document.removeEventListener('mousemove', debounced);
debounced.cancel();
};
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition, scheduleHide]);
useEffect(() => {
return () => {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
}
};
}, []);
if (!visible) {
return null;
}
return createPortal(
<div
ref={tooltipRef}
className={clsx(
classes.tooltip,
interactive ? 'pointer-events-auto' : 'pointer-events-none',
'absolute px-1 py-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={() => {
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

@@ -2,18 +2,21 @@ 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';
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
import { sizeClass, TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
export type WdTooltipWrapperProps = {
content?: (() => ReactNode) | ReactNode;
size?: TooltipSize;
interactive?: boolean;
tooltipClassName?: string;
} & Omit<HTMLProps<HTMLDivElement>, 'content' | 'size'> &
Omit<TooltipProps, 'content'>;
export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperProps>(
({ className, children, content, offset, position, targetSelector, interactive, size, ...props }, forwardedRef) => {
(
{ className, children, content, offset, position, targetSelector, interactive, size, tooltipClassName, ...props },
forwardedRef,
) => {
const suffix = useMemo(() => Math.random().toString(36).slice(2, 7), []);
const autoClass = `wdTooltipAutoTrigger-${suffix}`;
const finalTargetSelector = targetSelector || `.${autoClass}`;
@@ -29,7 +32,7 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
content={content}
interactive={interactive}
targetSelector={finalTargetSelector}
className={size ? sizeClass(size) : undefined}
className={clsx(size && sizeClass(size), tooltipClassName)}
/>
</div>
);
@@ -37,18 +40,3 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
);
WdTooltipWrapper.displayName = 'WdTooltipWrapper';
function sizeClass(size: TooltipSize) {
switch (size) {
case 'xs':
return classes.wdTooltipSizeXs;
case 'sm':
return classes.wdTooltipSizeSm;
case 'md':
return classes.wdTooltipSizeMd;
case 'lg':
return classes.wdTooltipSizeLg;
default:
return undefined;
}
}

View File

@@ -0,0 +1,23 @@
import classes from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/WdTooltipWrapper.module.scss';
export enum TooltipSize {
xs = 'xs',
sm = 'sm',
md = 'md',
lg = 'lg',
}
export const sizeClass = (size: TooltipSize) => {
switch (size) {
case TooltipSize.xs:
return classes.wdTooltipSizeXs;
case TooltipSize.sm:
return classes.wdTooltipSizeSm;
case TooltipSize.md:
return classes.wdTooltipSizeMd;
case TooltipSize.lg:
return classes.wdTooltipSizeLg;
default:
return undefined;
}
};

View File

@@ -0,0 +1,9 @@
import { LocationRaw } from '@/hooks/Mapper/types';
export const isDocked = (location: LocationRaw | null) => {
if (!location) {
return false;
}
return location.station_id != null || location.structure_id != null;
};

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