Compare commits

..

36 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
157 changed files with 6120 additions and 3777 deletions

View File

@@ -2,6 +2,103 @@
<!-- 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)

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,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);
@@ -348,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

@@ -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,85 +0,0 @@
import { useCallback, useMemo } from 'react';
import { Dialog } from 'primereact/dialog';
import { VirtualScroller } from 'primereact/virtualscroller';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { TrackingCharacter } from './types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { TrackingCharacterWrapper } from './TrackingCharacterWrapper';
interface TrackAndFollowProps {
visible: boolean;
onHide: () => void;
}
export const TrackAndFollow = ({ visible, onHide }: TrackAndFollowProps) => {
const { outCommand, data } = useMapRootState();
const { trackingCharactersData } = data;
const characters = useMemo(() => trackingCharactersData || [], [trackingCharactersData]);
const handleTrackToggle = useCallback(
async (characterId: string) => {
try {
await outCommand({
type: OutCommand.toggleTrack,
data: { character_id: characterId },
});
} catch (error) {
console.error('Error toggling track:', error);
}
},
[outCommand],
);
const handleFollowToggle = useCallback(
async (characterId: string) => {
try {
await outCommand({
type: OutCommand.toggleFollow,
data: { character_id: characterId },
});
} catch (error) {
console.error('Error toggling follow:', error);
}
},
[outCommand],
);
const rowTemplate = (tc: TrackingCharacter) => {
const characterEveId = tc.character.eve_id;
return (
<TrackingCharacterWrapper
key={characterEveId}
character={tc.character}
isTracked={tc.tracked}
isFollowed={tc.followed}
onTrackToggle={() => handleTrackToggle(characterEveId)}
onFollowToggle={() => handleFollowToggle(characterEveId)}
/>
);
};
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,49 +0,0 @@
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit/WdCheckbox/WdCheckbox';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip/WdTooltip';
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
import WdRadioButton from '@/hooks/Mapper/components/ui-kit/WdRadioButton';
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

@@ -1,115 +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(() => {
// Then update local state to hide the dialog
update(state => ({
...state,
showTrackAndFollow: false,
}));
}, [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;
};

View File

@@ -1,10 +1,12 @@
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
import { createContext, Dispatch, ForwardedRef, forwardRef, SetStateAction, useContext, useEffect } from 'react';
import {
ActivitySummary,
CommandLinkSignatureToSystem,
MapUnionTypes,
OutCommandHandler,
SolarSystemConnection,
TrackingCharacter,
UseCharactersCacheData,
UseCommentsData,
} from '@/hooks/Mapper/types';
@@ -18,8 +20,6 @@ import {
} from '@/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts';
import { WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
import { DetailedKill } from '../types/kills';
import { ActivitySummary } from '../components/mapRootContent/components/CharacterActivity';
import { TrackingCharacter } from '../components/mapRootContent/components/TrackAndFollow/types';
export type MapRootData = MapUnionTypes & {
selectedSystems: string[];
@@ -31,7 +31,6 @@ export type MapRootData = MapUnionTypes & {
activity: ActivitySummary[];
loading?: boolean;
};
showTrackAndFollow: boolean;
trackingCharactersData: TrackingCharacter[];
};
@@ -45,7 +44,6 @@ const INITIAL_DATA: MapRootData = {
activity: [],
loading: false,
},
showTrackAndFollow: false,
trackingCharactersData: [],
userCharacters: [],
presentCharacters: [],
@@ -62,6 +60,8 @@ const INITIAL_DATA: MapRootData = {
options: {},
isSubscriptionActive: false,
linkSignatureToSystem: null,
mainCharacterEveId: null,
followingCharacterEveId: null,
};
export enum AvailableThemes {
@@ -166,6 +166,8 @@ export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProvide
},
);
const { windowsSettings, toggleWidgetVisibility, updateWidgetSettings, resetWidgets } = useStoreWidgets();
const comments = useComments({ outCommand });
const charactersCache = useCharactersCache({ outCommand });
useEffect(() => {
let foundNew = false;
@@ -183,11 +185,9 @@ export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProvide
if (foundNew) {
setInterfaceSettings(newVals);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const comments = useComments({ outCommand });
const charactersCache = useCharactersCache({ outCommand });
return (
<MapRootContext.Provider
value={{

View File

@@ -9,20 +9,25 @@ export const useMapInit = () => {
const { addSystemStatic } = useLoadSystemStatic({ systems: [] });
return useCallback(
({
systems,
connections,
effects,
wormholes,
system_static_infos,
characters,
user_characters,
present_characters,
hubs,
user_permissions,
options,
is_subscription_active,
}: CommandInit) => {
(props: CommandInit) => {
const {
systems,
system_signatures,
connections,
effects,
wormholes,
system_static_infos,
characters,
user_characters,
present_characters,
hubs,
user_permissions,
options,
is_subscription_active,
main_character_eve_id,
following_character_eve_id,
} = props;
const updateData: Partial<MapRootData> = {};
if (wormholes) {
@@ -50,6 +55,10 @@ export const useMapInit = () => {
updateData.systems = systems;
}
if (system_signatures) {
updateData.systemSignatures = system_signatures;
}
if (connections) {
updateData.connections = connections;
}
@@ -66,7 +75,9 @@ export const useMapInit = () => {
updateData.options = options;
}
updateData.isSubscriptionActive = is_subscription_active;
if (is_subscription_active) {
updateData.isSubscriptionActive = is_subscription_active;
}
if (system_static_infos) {
system_static_infos.forEach(static_info => {
@@ -74,6 +85,14 @@ export const useMapInit = () => {
});
}
if (main_character_eve_id) {
updateData.mainCharacterEveId = main_character_eve_id;
}
if ('following_character_eve_id' in props) {
updateData.followingCharacterEveId = following_character_eve_id;
}
update(updateData);
},
[update, addSystemStatic],

View File

@@ -9,6 +9,13 @@ type SystemStaticResult = {
// TODO maybe later we can store in Static data in provider
const cache = new Map<number, SolarSystemStaticInfoRaw>();
export const getSystemStaticInfo = (solarSystemId: number | string | undefined) => {
if (!solarSystemId) {
return;
}
return cache.get(typeof solarSystemId == 'number' ? solarSystemId : parseInt(solarSystemId));
};
export const loadSystemStaticInfo = async (outCommand: OutCommandHandler, systems: number[]) => {
const result = await outCommand({
type: OutCommand.getSystemStaticInfos,

View File

@@ -57,112 +57,116 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
const { addComment, removeComment } = useCommandComments();
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
useImperativeHandle(ref, () => {
return {
command(type, data) {
switch (type) {
case Commands.init: // USED
mapInit(data as CommandInit);
break;
case Commands.addSystems: // USED
addSystems(data as CommandAddSystems);
break;
case Commands.updateSystems: // USED
updateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems: // USED
removeSystems(data as CommandRemoveSystems);
break;
case Commands.addConnections: // USED
addConnections(data as CommandAddConnections);
break;
case Commands.removeConnections: // USED
removeConnections(data as CommandRemoveConnections);
break;
case Commands.updateConnection: // USED
updateConnection(data as CommandUpdateConnection);
break;
case Commands.charactersUpdated: // USED
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded: // USED
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved: // USED
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated: // USED
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters: // USED
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.mapUpdated: // USED
mapUpdated(data as CommandMapUpdated);
break;
case Commands.routes:
mapRoutes(data as CommandRoutes);
break;
useImperativeHandle(
ref,
() => {
return {
command(type, data) {
switch (type) {
case Commands.init: // USED
mapInit(data as CommandInit);
break;
case Commands.addSystems: // USED
addSystems(data as CommandAddSystems);
break;
case Commands.updateSystems: // USED
updateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems: // USED
removeSystems(data as CommandRemoveSystems);
break;
case Commands.addConnections: // USED
addConnections(data as CommandAddConnections);
break;
case Commands.removeConnections: // USED
removeConnections(data as CommandRemoveConnections);
break;
case Commands.updateConnection: // USED
updateConnection(data as CommandUpdateConnection);
break;
case Commands.charactersUpdated: // USED
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded: // USED
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved: // USED
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated: // USED
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters: // USED
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.mapUpdated: // USED
mapUpdated(data as CommandMapUpdated);
break;
case Commands.routes:
mapRoutes(data as CommandRoutes);
break;
case Commands.signaturesUpdated: // USED
updateSystemSignatures(data as CommandSignaturesUpdated);
break;
case Commands.signaturesUpdated: // USED
updateSystemSignatures(data as CommandSignaturesUpdated);
break;
case Commands.linkSignatureToSystem: // USED
setTimeout(() => {
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
}, 200);
break;
case Commands.linkSignatureToSystem: // USED
setTimeout(() => {
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
}, 200);
break;
case Commands.centerSystem: // USED
// do nothing here
break;
case Commands.centerSystem: // USED
// do nothing here
break;
case Commands.selectSystem: // USED
// do nothing here
break;
case Commands.selectSystem: // USED
// do nothing here
break;
case Commands.killsUpdated:
// do nothing here
break;
case Commands.killsUpdated:
// do nothing here
break;
case Commands.detailedKillsUpdated:
updateDetailedKills(data as Record<string, DetailedKill[]>);
break;
case Commands.detailedKillsUpdated:
updateDetailedKills(data as Record<string, DetailedKill[]>);
break;
case Commands.characterActivityData:
characterActivityData(data as CommandCharacterActivityData);
break;
case Commands.characterActivityData:
characterActivityData(data as CommandCharacterActivityData);
break;
case Commands.trackingCharactersData:
trackingCharactersData(data as CommandTrackingCharactersData);
break;
case Commands.trackingCharactersData:
trackingCharactersData(data as CommandTrackingCharactersData);
break;
case Commands.updateActivity:
break;
case Commands.updateActivity:
break;
case Commands.updateTracking:
break;
case Commands.updateTracking:
break;
case Commands.userSettingsUpdated:
userSettingsUpdated(data as CommandUserSettingsUpdated);
break;
case Commands.userSettingsUpdated:
userSettingsUpdated(data as CommandUserSettingsUpdated);
break;
case Commands.systemCommentAdded:
addComment(data as CommandCommentAdd);
break;
case Commands.systemCommentAdded:
addComment(data as CommandCommentAdd);
break;
case Commands.systemCommentRemoved:
removeComment(data as CommandCommentRemoved);
break;
case Commands.systemCommentRemoved:
removeComment(data as CommandCommentRemoved);
break;
default:
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
break;
}
default:
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
break;
}
emitMapEvent({ name: type, data });
},
};
}, []);
emitMapEvent({ name: type, data });
},
};
},
[],
);
};

View File

@@ -17,6 +17,7 @@ export type ShipTypeRaw = {
export type LocationRaw = {
solar_system_id: number | null;
structure_id: number | null;
station_id: number | null;
};
export type CharacterTypeRaw = {
@@ -34,6 +35,11 @@ export type CharacterTypeRaw = {
corporation_ticker: string;
};
export interface TrackingCharacter {
character: CharacterTypeRaw;
tracked: boolean;
}
export type WithIsOwnCharacter = {
isOwn: boolean;
};
@@ -56,3 +62,10 @@ export interface UseCharactersCacheData {
characters: Map<string, CharacterCache>;
lastUpdateKey: number;
}
export interface ActivitySummary {
character: CharacterTypeRaw;
passages: number;
connections: number;
signatures: number;
}

View File

@@ -0,0 +1,7 @@
import { TrackingCharacter } from './character.ts';
export type CommandInCharactersTrackingInfo = {
characters: TrackingCharacter[];
following: string | null;
main: string | null;
};

View File

@@ -9,3 +9,5 @@ export interface WithClassName {
}
export type WithHTMLProps = React.HTMLAttributes<HTMLDivElement>;
export type IncomingEvent<T> = { data: T };

View File

@@ -1,12 +1,10 @@
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types/system.ts';
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts';
import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Mapper/types/character.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
import { ActivitySummary } from '../components/mapRootContent/components/CharacterActivity';
import { TrackingCharacter } from '../components/mapRootContent/components/TrackAndFollow/types';
import { CommentType, UserPermissions } from '@/hooks/Mapper/types';
import { CommentType, SystemSignature, UserPermissions } from '@/hooks/Mapper/types';
export enum Commands {
init = 'init',
@@ -37,6 +35,7 @@ export enum Commands {
updateActivity = 'update_activity',
updateTracking = 'update_tracking',
userSettingsUpdated = 'user_settings_updated',
showTracking = 'show_tracking',
}
export type Command =
@@ -62,14 +61,17 @@ export type Command =
| Commands.signaturesUpdated
| Commands.systemCommentAdded
| Commands.systemCommentRemoved
| Commands.systemCommentsUpdated
| Commands.characterActivityData
| Commands.trackingCharactersData
| Commands.userSettingsUpdated
| Commands.updateActivity
| Commands.updateTracking;
| Commands.updateTracking
| Commands.showTracking;
export type CommandInit = {
systems: SolarSystemRawType[];
system_signatures: Record<string, SystemSignature[]>;
kills: Kill[];
system_static_infos: SolarSystemStaticInfoRaw[];
connections: SolarSystemConnection[];
@@ -84,6 +86,8 @@ export type CommandInit = {
options: Record<string, string | boolean>;
reset?: boolean;
is_subscription_active?: boolean;
main_character_eve_id?: string | null;
following_character_eve_id?: string | null;
};
export type CommandAddSystems = SolarSystemRawType[];
@@ -123,14 +127,7 @@ export type CommandUserSettingsUpdated = {
settings: UserSettings;
};
export type CommandShowActivity = null;
export type CommandHideActivity = null;
export type CommandShowTracking = null;
export type CommandHideTracking = null;
export type CommandUiLoaded = { version: string | null };
export type CommandLogMapError = { error: string; componentStack: string };
export type CommandMapEvent = { type: Command; data: unknown };
export type CommandMapEvents = Array<{ type: Command; data: unknown }>;
export type CommandUpdateActivity = {
characterId: number;
systemId: number;
@@ -186,6 +183,8 @@ export interface CommandData {
[Commands.updateTracking]: CommandUpdateTracking;
[Commands.systemCommentAdded]: CommandCommentAdd;
[Commands.systemCommentRemoved]: CommandCommentRemoved;
[Commands.systemCommentsUpdated]: unknown;
[Commands.showTracking]: CommandShowTracking;
}
export interface MapHandlers {
@@ -201,6 +200,7 @@ export enum OutCommand {
getSignatures = 'get_signatures',
getSystemStaticInfos = 'get_system_static_infos',
getConnectionInfo = 'get_connection_info',
loadSignatures = 'load_signatures',
updateConnectionTimeStatus = 'update_connection_time_status',
updateConnectionType = 'update_connection_type',
updateConnectionMassStatus = 'update_connection_mass_status',
@@ -234,9 +234,13 @@ export enum OutCommand {
addSystemComment = 'addSystemComment',
deleteSystemComment = 'deleteSystemComment',
getSystemComments = 'getSystemComments',
toggleTrack = 'toggle_track',
// toggleTrack = 'toggle_track',
toggleFollow = 'toggle_follow',
getCharacterInfo = 'getCharacterInfo',
getCharactersTrackingInfo = 'getCharactersTrackingInfo',
updateCharacterTracking = 'updateCharacterTracking',
updateFollowingCharacter = 'updateFollowingCharacter',
updateMainCharacter = 'updateMainCharacter',
// Only UI commands
openSettings = 'open_settings',

View File

@@ -23,4 +23,7 @@ export type MapUnionTypes = {
userPermissions: Partial<UserPermissions>;
options: Record<string, string | boolean>;
isSubscriptionActive: boolean;
mainCharacterEveId: string | null;
followingCharacterEveId: string | null;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

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