Compare commits

..

2 Commits

Author SHA1 Message Date
Dmitry Popov
cee545cfd9 feat(Audit): updated audit page pagination 2025-03-12 22:32:08 +01:00
Dmitry Popov
9612cda72b feat(Audit): updated audit page pagination 2025-03-12 22:32:00 +01:00
455 changed files with 11353 additions and 49255 deletions

View File

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

View File

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

View File

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

View File

@@ -18,8 +18,49 @@ permissions:
contents: write
jobs:
deploy-test:
name: 🚀 Deploy to test env (fly.io)
runs-on: ubuntu-latest
if: ${{ github.base_ref == 'main' || (github.ref == 'refs/heads/main' && github.event_name == 'push') }}
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: 👀 Read app name
uses: SebRollen/toml-action@v1.0.0
id: app_name
with:
file: "fly.toml"
field: "app"
- name: 🚀 Deploy Test
run: flyctl deploy --remote-only --wait-timeout=300 --ha=false
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
manual-approval:
name: Manual Approval
runs-on: ubuntu-latest
needs: deploy-test
if: success()
permissions:
issues: write
steps:
- name: Await Manual Approval
uses: trstringer/manual-approval@v1
with:
secret: ${{ github.TOKEN }}
approvers: DmitryPopov
minimum-approvals: 1
issue-title: "Manual Approval Required for Release"
issue-body: "Please approve or deny the deployment."
build:
name: 🛠 Build
needs: manual-approval
runs-on: ubuntu-22.04
if: ${{ (github.ref == 'refs/heads/main') && github.event_name == 'push' }}
permissions:

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
.PHONY: deploy install cleanup start yarn migrate format test coverage versions standalone-tests
.PHONY: deploy install cleanup start yarn migrate format test coverage versions
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
SHELL := /bin/bash
@@ -35,11 +35,6 @@ 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

@@ -17,6 +17,5 @@ module.exports = {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
"linebreak-style": "off",
},
};

View File

@@ -7,5 +7,5 @@
"semi": true,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "auto"
"endOfLine": "lf"
}

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

@@ -1,14 +1,14 @@
import { PrimeReactProvider } from 'primereact/api';
import { ErrorBoundary } from 'react-error-boundary';
import { PrimeReactProvider } from 'primereact/api';
import { ReactFlowProvider } from 'reactflow';
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
import { ErrorInfo, useCallback, useEffect, useRef } from 'react';
import { ReactFlowProvider } from 'reactflow';
import { useMapperHandlers } from './useMapperHandlers';
import { MapRootContent } from '@/hooks/Mapper/components/mapRootContent/MapRootContent.tsx';
import { MapRootProvider } from '@/hooks/Mapper/mapRootProvider';
import './common-styles/main.scss';
import { MapRootProvider } from '@/hooks/Mapper/mapRootProvider';
import { MapRootContent } from '@/hooks/Mapper/components/mapRootContent/MapRootContent.tsx';
const ErrorFallback = () => {
return <div className="!z-100 absolute w-screen h-screen bg-transparent"></div>;
@@ -20,7 +20,7 @@ export default function MapRoot({ hooks }) {
const mapperHandlerRefs = useRef([providerRef]);
const { handleCommand, handleMapEvent } = useMapperHandlers(mapperHandlerRefs.current, hooksRef);
const { handleCommand, handleMapEvent, handleMapEvents } = useMapperHandlers(mapperHandlerRefs.current, hooksRef);
const logError = useCallback((error: Error, info: ErrorInfo) => {
if (!hooksRef.current) {
@@ -35,6 +35,7 @@ export default function MapRoot({ hooks }) {
}
hooksRef.current.handleEvent('map_event', handleMapEvent);
hooksRef.current.handleEvent('map_events', handleMapEvents);
}, []);
return (

View File

@@ -67,22 +67,16 @@
}
}
.p-datatable-thead {
th, th.p-sortable-column {
font-size: 12px;
font-weight: bold;
padding: 3px 4px;
}
.p-sortable-column {
font-size: 12px;
font-weight: bold;
padding: 3px 4px;
}
.p-selectable-row td {
padding: 4px 4px;
}
.p-datatable.p-datatable-sm .p-datatable-tbody > tr > td {
padding: 3px 4px;
}
.p-sortable-column > .p-column-header-content > span:last-child {
transform: scale(0.7);
@@ -99,11 +93,6 @@
.p-dropdown-item {
padding: 0.25rem 0.5rem;
font-size: 14px;
width: 100%;
.p-dropdown-item-label {
width: 100%;
}
}
.p-dropdown-item-group {
@@ -148,67 +137,3 @@
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;
}
.suppress-menu-behaviour {
pointer-events: none;
.p-menuitem-content {
pointer-events: initial;
background-color: initial !important;
}
.p-menuitem-content:hover {
background-color: initial !important;
}
}
.p-autocomplete .p-autocomplete-multiple-container:not(.p-disabled).p-focus {
box-shadow: 0 0 0 1px #335c7e;
border-color: #335c7e;
}
.p-inputtext:enabled:focus {
box-shadow: 0 0 0 1px #335c7e;
border-color: #335c7e;
}
.p-inputtext:enabled:hover {
border-color: #335c7e;
}

View File

@@ -1,8 +1,11 @@
/* Основной класс диалога */
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);
@@ -26,10 +29,12 @@ body .p-dialog {
}
}
/* Стиль видимого диалога */
.p-dialog-visible {
visibility: visible;
}
/* Анимации */
.p-dialog-enter {
opacity: 0;
}
@@ -48,27 +53,31 @@ body .p-dialog {
transition: opacity 0.3s;
}
/* Заголовок диалога */
.p-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: #f4f4f4;
height: 40px;
//border-bottom: 1px solid #ddd;
}
/* Содержимое диалога */
.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;
@@ -84,12 +93,3 @@ 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

@@ -1,45 +0,0 @@
.p-confirm-popup {
display: flex;
flex-direction: column;
gap: 6px;
@apply p-[12px];
&::before, &::after {
display: none;
}
.p-confirm-popup-content, .p-confirm-popup-footer {
@apply p-0 m-0;
}
.p-confirm-popup-content {
display: flex;
gap: 6px;
}
.p-confirm-popup-footer {
display: flex;
justify-content: flex-end;
gap: 4px;
}
.p-confirm-popup-icon {
font-size: 14px;
}
.p-confirm-popup-message {
@apply m-0;
font-size: 12px;
}
.p-confirm-popup-reject.p-button-sm,
.p-confirm-popup-accept.p-button-sm {
@apply px-1.5 py-1 m-0;
& > span {
font-size: 12px;
line-height: 12px;
}
}
}

View File

@@ -1,77 +0,0 @@
.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,6 +1,4 @@
@import "fix-dialog";
@import "fix-popup";
@import "fix-tabs";
//@import "fix-input";
//@import "theme";

View File

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

@@ -1,37 +1,17 @@
import { emitMapEvent } from '@/hooks/Mapper/events';
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { useCallback } from 'react';
import classes from './Characters.module.scss';
interface CharactersProps {
data: CharacterTypeRaw[];
}
import clsx from 'clsx';
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';
export const Characters = ({ data }: CharactersProps) => {
const Characters = ({ data }: { data: CharacterTypeRaw[] }) => {
const [parent] = useAutoAnimate();
const {
outCommand,
data: { mainCharacterEveId, followingCharacterEveId },
} = useMapRootState();
const handleSelect = useCallback(async (character: CharacterTypeRaw) => {
if (!character) {
return;
}
await outCommand({
type: OutCommand.startTracking,
data: { character_eve_id: character.eve_id },
});
const handleSelect = useCallback((character: CharacterTypeRaw) => {
emitMapEvent({
name: Commands.centerSystem,
data: character.location?.solar_system_id?.toString(),
data: character?.location?.solar_system_id?.toString(),
});
}, []);
@@ -41,71 +21,21 @@ export const Characters = ({ data }: CharactersProps) => {
className="flex flex-col items-center justify-center"
onClick={() => handleSelect(character)}
>
<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 hover:bg-stone-300/90',
{
['border-stone-800/90']: !character.online,
['border-lime-600/70']: character.online,
},
)}
title={character.tracking_paused ? `${character.name} - Tracking Paused (click to resume)` : character.name}
>
{character.tracking_paused && (
<>
<span
className={clsx(
'absolute flex flex-col p-[2px] top-[0px] left-[0px] w-[35px] h-[35px]',
'text-yellow-500 text-[9px] z-10 bg-gray-800/40',
'pi',
PrimeIcons.PAUSE,
)}
/>
</>
)}
{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,
},
)}
<div className="tooltip tooltip-bottom" title={character.name}>
<a
className={clsx('wd-characters-icons wd-bg-default', { ['character-online']: character.online })}
style={{ backgroundImage: `url(https://images.evetech.net/characters/${character.eve_id}/portrait)` }}
></div>
></a>
</div>
</li>
));
return (
<ul className="flex gap-1 characters" id="characters" ref={parent}>
<ul className="flex characters" id="characters" ref={parent}>
{items}
</ul>
);
};
// eslint-disable-next-line react/display-name
export default Characters;

View File

@@ -1,12 +1,11 @@
import React, { RefObject } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PingType, SolarSystemRawType } from '@/hooks/Mapper/types';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { useContextMenuSystemItems } from '@/hooks/Mapper/components/contexts/ContextMenuSystem/useContextMenuSystemItems.tsx';
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
export interface ContextMenuSystemProps {
hubs: string[];
userHubs: string[];
contextMenuRef: RefObject<ContextMenu>;
systemId: string | undefined;
systems: SolarSystemRawType[];
@@ -14,12 +13,10 @@ export interface ContextMenuSystemProps {
onLockToggle(): void;
onOpenSettings(): void;
onHubToggle(): void;
onUserHubToggle(): void;
onSystemTag(val?: string): void;
onSystemStatus(val: number): void;
onSystemLabels(val: string): void;
onCustomLabelDialog(): void;
onTogglePing(type: PingType, solar_system_id: string, hasPing: boolean): void;
onWaypointSet: WaypointSetContextHandler;
}
@@ -28,7 +25,7 @@ export const ContextMenuSystem: React.FC<ContextMenuSystemProps> = ({ contextMen
return (
<>
<ContextMenu className="min-w-[200px]" model={items} ref={contextMenuRef} breakpoint="767px" />
<ContextMenu model={items} ref={contextMenuRef} breakpoint="767px" />
</>
);
};

View File

@@ -1,4 +1,3 @@
export * from './useTagMenu';
export * from './useStatusMenu';
export * from './useLabelsMenu';
export * from './useUserRoute';

View File

@@ -1 +1 @@
export * from './useTagMenu.tsx';
export * from './useTagMenu.ts';

View File

@@ -0,0 +1,68 @@
import { MenuItem } from 'primereact/menuitem';
import { PrimeIcons } from 'primereact/api';
import { useCallback, useRef } from 'react';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { getSystemById } from '@/hooks/Mapper/helpers';
import clsx from 'clsx';
import { GRADIENT_MENU_ACTIVE_CLASSES } from '@/hooks/Mapper/constants.ts';
const AVAILABLE_LETTERS = ['A', 'B', 'C', 'D', 'E', 'F', 'X', 'Y', 'Z'];
const AVAILABLE_NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
export const useTagMenu = (
systems: SolarSystemRawType[],
systemId: string | undefined,
onSystemTag: (val?: string) => void,
): (() => MenuItem) => {
const ref = useRef({ onSystemTag, systems, systemId });
ref.current = { onSystemTag, systems, systemId };
return useCallback(() => {
const { onSystemTag, systemId, systems } = ref.current;
const system = systemId ? getSystemById(systems, systemId) : undefined;
const isSelectedLetters = AVAILABLE_LETTERS.includes(system?.tag ?? '');
const isSelectedNumbers = AVAILABLE_NUMBERS.includes(system?.tag ?? '');
const menuItem: MenuItem = {
label: 'Tag',
icon: PrimeIcons.HASHTAG,
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: isSelectedLetters || isSelectedNumbers }),
items: [
...(system?.tag !== '' && system?.tag !== null
? [
{
label: 'Clear',
icon: PrimeIcons.BAN,
command: () => onSystemTag(),
},
]
: []),
{
label: 'Letter',
icon: PrimeIcons.TAGS,
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: isSelectedLetters }),
items: AVAILABLE_LETTERS.map(x => ({
label: x,
icon: PrimeIcons.TAG,
command: () => onSystemTag(x),
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: system?.tag === x }),
})),
},
{
label: 'Digit',
icon: PrimeIcons.TAGS,
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: isSelectedNumbers }),
items: AVAILABLE_NUMBERS.map(x => ({
label: x,
icon: PrimeIcons.TAG,
command: () => onSystemTag(x),
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: system?.tag === x }),
})),
},
],
};
return menuItem;
}, []);
};

View File

@@ -1,95 +0,0 @@
import { MenuItem } from 'primereact/menuitem';
import { PrimeIcons } from 'primereact/api';
import { useCallback, useRef } from 'react';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { getSystemById } from '@/hooks/Mapper/helpers';
import clsx from 'clsx';
import { GRADIENT_MENU_ACTIVE_CLASSES } from '@/hooks/Mapper/constants.ts';
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
import { Button } from 'primereact/button';
const AVAILABLE_TAGS = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'X',
'Y',
'Z',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
];
export const useTagMenu = (
systems: SolarSystemRawType[],
systemId: string | undefined,
onSystemTag: (val?: string) => void,
): (() => MenuItem) => {
const ref = useRef({ onSystemTag, systems, systemId });
ref.current = { onSystemTag, systems, systemId };
return useCallback(() => {
const { onSystemTag, systemId, systems } = ref.current;
const system = systemId ? getSystemById(systems, systemId) : undefined;
const isSelectedTag = AVAILABLE_TAGS.includes(system?.tag ?? '');
const menuItem: MenuItem = {
label: 'Tag',
icon: PrimeIcons.HASHTAG,
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: isSelectedTag }),
items: [
{
label: 'Digit',
icon: PrimeIcons.TAGS,
className: '!h-[128px] suppress-menu-behaviour',
template: () => {
return (
<LayoutEventBlocker className="flex flex-col gap-1 w-[200px] h-full px-2">
<div className="grid grid-cols-[auto_auto_auto_auto_auto_auto] gap-1">
{AVAILABLE_TAGS.map(x => (
<Button
outlined={system?.tag !== x}
severity="warning"
key={x}
value={x}
size="small"
className="p-[3px] justify-center"
onClick={() => system?.tag !== x && onSystemTag(x)}
>
{x}
</Button>
))}
<Button
disabled={!isSelectedTag}
icon="pi pi-ban"
size="small"
className="!p-0 !w-[initial] justify-center"
outlined
severity="help"
onClick={() => onSystemTag()}
></Button>
</div>
</LayoutEventBlocker>
);
},
},
],
};
return menuItem;
}, []);
};

View File

@@ -1,42 +0,0 @@
import { MapUserAddIcon, MapUserDeleteIcon } from '@/hooks/Mapper/icons';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef } from 'react';
import { WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
interface UseUserRouteProps {
systemId: string | undefined;
userHubs: string[];
onUserHubToggle(): void;
}
export const useUserRoute = ({ userHubs, systemId, onUserHubToggle }: UseUserRouteProps) => {
const {
data: { isSubscriptionActive },
windowsSettings,
} = useMapRootState();
const ref = useRef({ userHubs, systemId, onUserHubToggle, isSubscriptionActive, windowsSettings });
ref.current = { userHubs, systemId, onUserHubToggle, isSubscriptionActive, windowsSettings };
return useCallback(() => {
const { userHubs, systemId, onUserHubToggle, isSubscriptionActive, windowsSettings } = ref.current;
const isVisibleUserRoutes = windowsSettings.visible.some(x => x === WidgetsIds.userRoutes);
if (!isSubscriptionActive || !isVisibleUserRoutes || !systemId) {
return [];
}
return [
{
label: !userHubs.includes(systemId) ? 'Add User Route' : 'Remove User Route',
icon: !userHubs.includes(systemId) ? (
<MapUserAddIcon className="mr-1 relative left-[-2px]" />
) : (
<MapUserDeleteIcon className="mr-1 relative left-[-2px]" />
),
command: onUserHubToggle,
},
];
}, [windowsSettings]);
};

View File

@@ -5,29 +5,22 @@ import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
// import { PingType } from '@/hooks/Mapper/types/ping.ts';
interface UseContextMenuSystemHandlersProps {
hubs: string[];
userHubs: string[];
systems: SolarSystemRawType[];
outCommand: OutCommandHandler;
}
export const useContextMenuSystemHandlers = ({
systems,
hubs,
userHubs,
outCommand,
}: UseContextMenuSystemHandlersProps) => {
export const useContextMenuSystemHandlers = ({ systems, hubs, outCommand }: UseContextMenuSystemHandlersProps) => {
const contextMenuRef = useRef<ContextMenu | null>(null);
const [system, setSystem] = useState<string>();
const { deleteSystems } = useDeleteSystems();
const ref = useRef({ hubs, userHubs, system, systems, outCommand, deleteSystems });
ref.current = { hubs, userHubs, system, systems, outCommand, deleteSystems };
const ref = useRef({ hubs, system, systems, outCommand, deleteSystems });
ref.current = { hubs, system, systems, outCommand, deleteSystems };
const open = useCallback((ev: any, systemId: string) => {
setSystem(systemId);
@@ -79,37 +72,6 @@ export const useContextMenuSystemHandlers = ({
setSystem(undefined);
}, []);
const onUserHubToggle = useCallback(() => {
const { userHubs, system, outCommand } = ref.current;
if (!system) {
return;
}
outCommand({
type: !userHubs.includes(system) ? OutCommand.addUserHub : OutCommand.deleteUserHub,
data: {
system_id: system,
},
});
setSystem(undefined);
}, []);
// const onTogglePingRally = useCallback(() => {
// const { userHubs, system, outCommand } = ref.current;
// if (!system) {
// return;
// }
//
// outCommand({
// type: OutCommand.openPing,
// data: {
// solar_system_id: system,
// type: PingType.Rally,
// },
// });
// setSystem(undefined);
// }, []);
const onSystemTag = useCallback((tag?: string) => {
const { system, outCommand } = ref.current;
if (!system) {
@@ -142,6 +104,7 @@ export const useContextMenuSystemHandlers = ({
setSystem(undefined);
}, []);
const onSystemStatus = useCallback((status: number) => {
const { system, outCommand } = ref.current;
if (!system) {
@@ -214,8 +177,6 @@ export const useContextMenuSystemHandlers = ({
onDeleteSystem,
onLockToggle,
onHubToggle,
onUserHubToggle,
// onTogglePingRally,
onSystemTag,
onSystemTemporaryName,
onSystemStatus,

View File

@@ -1,9 +1,4 @@
import {
useLabelsMenu,
useStatusMenu,
useTagMenu,
useUserRoute,
} from '@/hooks/Mapper/components/contexts/ContextMenuSystem/hooks';
import { useLabelsMenu, useStatusMenu, useTagMenu } from '@/hooks/Mapper/components/contexts/ContextMenuSystem/hooks';
import { useMemo } from 'react';
import { getSystemById } from '@/hooks/Mapper/helpers';
import classes from './ContextMenuSystem.module.scss';
@@ -14,20 +9,11 @@ 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';
import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons';
import { PingType } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import clsx from 'clsx';
import { MenuItem } from 'primereact/menuitem';
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
export const useContextMenuSystemItems = ({
onDeleteSystem,
onLockToggle,
onHubToggle,
onUserHubToggle,
onTogglePing,
onSystemTag,
onSystemStatus,
onSystemLabels,
@@ -36,7 +22,6 @@ export const useContextMenuSystemItems = ({
onWaypointSet,
systemId,
hubs,
userHubs,
systems,
}: Omit<ContextMenuSystemProps, 'contextMenuRef'>) => {
const getTags = useTagMenu(systems, systemId, onSystemTag);
@@ -44,32 +29,9 @@ export const useContextMenuSystemItems = ({
const getLabels = useLabelsMenu(systems, systemId, onSystemLabels, onCustomLabelDialog);
const getWaypointMenu = useWaypointMenu(onWaypointSet);
const canLockSystem = useMapCheckPermissions([UserPermission.LOCK_SYSTEM]);
const canManageSystem = useMapCheckPermissions([UserPermission.UPDATE_SYSTEM]);
const canDeleteSystem = useMapCheckPermissions([UserPermission.DELETE_SYSTEM]);
const getUserRoutes = useUserRoute({ userHubs, systemId, onUserHubToggle });
const {
data: { pings, isSubscriptionActive },
} = useMapRootState();
const ping = useMemo(() => (pings.length === 1 ? pings[0] : undefined), [pings]);
const isShowPingBtn = useMemo(() => {
if (!isSubscriptionActive) {
return false;
}
if (pings.length === 0) {
return true;
}
return pings[0].solar_system_id === systemId;
}, [isSubscriptionActive, pings, systemId]);
return useMemo((): MenuItem[] => {
return useMemo(() => {
const system = systemId ? getSystemById(systems, systemId) : undefined;
const systemStaticInfo = getSystemStaticInfo(systemId)!;
const hasPing = ping?.solar_system_id === systemId;
if (!system || !systemId) {
return [];
@@ -82,9 +44,9 @@ export const useContextMenuSystemItems = ({
return (
<FastSystemActions
systemId={systemId}
systemName={systemStaticInfo.solar_system_name}
regionName={systemStaticInfo.region_name}
isWH={isWormholeSpace(systemStaticInfo.system_class)}
systemName={system.system_static_info.solar_system_name}
regionName={system.system_static_info.region_name}
isWH={isWormholeSpace(system.system_static_info.system_class)}
showEdit
onOpenSettings={onOpenSettings}
/>
@@ -95,106 +57,52 @@ export const useContextMenuSystemItems = ({
getTags(),
getStatus(),
...getLabels(),
...getWaypointMenu(systemId, systemStaticInfo.system_class),
...getWaypointMenu(systemId, system.system_static_info.system_class),
{
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
icon: !hubs.includes(systemId) ? (
<MapAddIcon className="mr-1 relative left-[-2px]" />
) : (
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
),
label: !hubs.includes(systemId) ? 'Add in Routes' : 'Remove from Routes',
icon: PrimeIcons.MAP_MARKER,
command: onHubToggle,
},
...getUserRoutes(),
{ separator: true },
{
command: () => onTogglePing(PingType.Rally, systemId, hasPing),
disabled: !isShowPingBtn,
template: () => {
const iconClasses = clsx({
'pi text-cyan-400 hero-signal': !hasPing,
'pi text-red-400 hero-signal-slash': hasPing,
});
if (isShowPingBtn) {
return <WdMenuItem icon={iconClasses}>{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}</WdMenuItem>;
}
return (
<MenuItemWithInfo
infoTitle="Locked. Ping can be set only for one system."
infoClass="pi-lock text-stone-500 mr-[12px]"
>
<WdMenuItem disabled icon={iconClasses}>
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
</WdMenuItem>
</MenuItemWithInfo>
);
},
},
...(system.locked && canLockSystem
? [
{
label: 'Unlock',
icon: PrimeIcons.LOCK_OPEN,
command: onLockToggle,
},
]
: []),
...(!system.locked && canManageSystem
? [
{
label: 'Lock',
icon: PrimeIcons.LOCK,
command: onLockToggle,
},
]
: []),
...(canDeleteSystem && !system.locked
? [
...(system.locked
? canLockSystem
? [
{
label: 'Unlock',
icon: PrimeIcons.LOCK_OPEN,
command: onLockToggle,
},
]
: []
: [
...(canLockSystem
? [
{
label: 'Lock',
icon: PrimeIcons.LOCK,
command: onLockToggle,
},
]
: []),
{ separator: true },
{
label: 'Delete',
icon: PrimeIcons.TRASH,
command: onDeleteSystem,
disabled: hasPing,
template: () => {
if (!hasPing) {
return <WdMenuItem icon="text-red-400 pi pi-trash">Delete</WdMenuItem>;
}
return (
<MenuItemWithInfo
infoTitle="Locked. System can not be deleted until ping set."
infoClass="pi-lock text-stone-500 mr-[12px]"
>
<WdMenuItem disabled icon="text-red-400 pi pi-trash">
Delete
</WdMenuItem>
</MenuItemWithInfo>
);
},
},
]
: []),
]),
];
}, [
systemId,
canLockSystem,
systems,
systemId,
getTags,
getStatus,
getLabels,
getWaypointMenu,
getUserRoutes,
hubs,
onHubToggle,
canLockSystem,
onLockToggle,
canDeleteSystem,
onDeleteSystem,
onOpenSettings,
onTogglePing,
ping,
isShowPingBtn,
onLockToggle,
onDeleteSystem,
]);
};

View File

@@ -11,7 +11,6 @@ import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components
import { useJumpPlannerMenu } from '@/hooks/Mapper/components/contexts/hooks';
import { Route } from '@/hooks/Mapper/types/routes.ts';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons';
export interface ContextMenuSystemInfoProps {
systemStatics: Map<number, SolarSystemStaticInfoRaw>;
@@ -70,12 +69,8 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
...getJumpPlannerMenu(system, routes),
...getWaypointMenu(systemId, system.system_class),
{
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
icon: !hubs.includes(systemId) ? (
<MapAddIcon className="mr-1 relative left-[-2px]" />
) : (
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
),
label: !hubs.includes(systemId) ? 'Add in Routes' : 'Remove from Routes',
icon: PrimeIcons.MAP_MARKER,
command: onHubToggle,
},
...(!systemOnMap

View File

@@ -1,25 +1,25 @@
import * as React from 'react';
import { useCallback, useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { Commands, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
export const useContextMenuSystemInfoHandlers = () => {
const { outCommand } = useMapRootState();
const { hubs = [], toggleHubCommand } = useRouteProvider();
interface UseContextMenuSystemHandlersProps {
hubs: string[];
outCommand: OutCommandHandler;
}
export const useContextMenuSystemInfoHandlers = ({ hubs, outCommand }: UseContextMenuSystemHandlersProps) => {
const contextMenuRef = useRef<ContextMenu | null>(null);
const [system, setSystem] = useState<string>();
const routeRef = useRef<(SolarSystemStaticInfoRaw | undefined)[]>([]);
const ref = useRef({ hubs, system, outCommand, toggleHubCommand });
ref.current = { hubs, system, outCommand, toggleHubCommand };
const ref = useRef({ hubs, system, outCommand });
ref.current = { hubs, system, outCommand };
const open = useCallback(
(ev: React.SyntheticEvent, systemId: string, route: (SolarSystemStaticInfoRaw | undefined)[]) => {
@@ -33,12 +33,17 @@ export const useContextMenuSystemInfoHandlers = () => {
);
const onHubToggle = useCallback(() => {
const { system } = ref.current;
const { hubs, system, outCommand } = ref.current;
if (!system) {
return;
}
ref.current.toggleHubCommand(system);
outCommand({
type: !hubs.includes(system) ? OutCommand.addHub : OutCommand.deleteHub,
data: {
system_id: system,
},
});
setSystem(undefined);
}, []);
@@ -54,8 +59,6 @@ export const useContextMenuSystemInfoHandlers = () => {
system_id: solarSystemId,
},
});
// TODO add it to some queue
setTimeout(() => {
emitMapEvent({
name: Commands.centerSystem,

View File

@@ -1,6 +1,6 @@
import { useCallback, useRef } from 'react';
import { LayoutEventBlocker, TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { LayoutEventBlocker, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons.ts';
import classes from './FastSystemActions.module.scss';
import clsx from 'clsx';
@@ -59,21 +59,9 @@ export const FastSystemActions = ({
return (
<LayoutEventBlocker className={clsx('flex px-2 gap-2 justify-between items-center h-full')}>
<div className={clsx('flex gap-2 items-center h-full', classes.Links)}>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open zkillboard' }}
source={ZKB_ICON}
onClick={handleOpenZKB}
/>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open Anoikis' }}
source={ANOIK_ICON}
onClick={handleOpenAnoikis}
/>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open Dotlan' }}
source={DOTLAN_ICON}
onClick={handleOpenDotlan}
/>
<WdImgButton tooltip={{ content: 'Open zkillboard' }} source={ZKB_ICON} onClick={handleOpenZKB} />
<WdImgButton tooltip={{ content: 'Open Anoikis' }} source={ANOIK_ICON} onClick={handleOpenAnoikis} />
<WdImgButton tooltip={{ content: 'Open Dotlan' }} source={DOTLAN_ICON} onClick={handleOpenDotlan} />
</div>
<div className="flex gap-2 items-center pl-1">
@@ -81,14 +69,14 @@ export const FastSystemActions = ({
textSize={WdImageSize.off}
className={PrimeIcons.COPY}
onClick={copySystemNameToClipboard}
tooltip={{ position: TooltipPosition.top, content: 'Copy system name' }}
tooltip={{ content: 'Copy system name' }}
/>
{showEdit && (
<WdImgButton
textSize={WdImageSize.off}
className="pi pi-pen-to-square text-base"
onClick={onOpenSettings}
tooltip={{ position: TooltipPosition.top, content: 'Edit system name and description' }}
tooltip={{ content: 'Edit system name and description' }}
/>
)}
</div>

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 = getSystemStaticInfo(systemIdFrom);
const origin = getSystemById(systems, systemIdFrom)?.system_static_info;
if (!origin) {
return [];

View File

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

View File

@@ -28,12 +28,11 @@ import {
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import clsx from 'clsx';
import { useBackgroundVars } from './hooks/useBackgroundVars';
import type { PanelPosition } from '@reactflow/core';
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
@@ -96,8 +95,6 @@ interface MapCompProps {
isShowBackgroundPattern?: boolean;
isSoftBackground?: boolean;
theme?: string;
pings: PingData[];
minimapPlacement?: PanelPosition;
}
const MapComp = ({
@@ -115,8 +112,6 @@ const MapComp = ({
isSoftBackground,
theme,
onAddSystem,
pings,
minimapPlacement = 'bottom-right',
}: MapCompProps) => {
const { getNodes } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
@@ -211,9 +206,8 @@ const MapComp = ({
...x,
showKSpaceBG: showKSpaceBG,
isThickConnections: isThickConnections,
pings,
}));
}, [showKSpaceBG, isThickConnections, pings, update]);
}, [showKSpaceBG, isThickConnections, update]);
return (
<>
@@ -276,9 +270,7 @@ const MapComp = ({
// onlyRenderVisibleElements
selectionMode={SelectionMode.Partial}
>
{isShowMinimap && (
<MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} position={minimapPlacement} />
)}
{isShowMinimap && <MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} />}
{isShowBackgroundPattern && <Background variant={variant} gap={gap} size={size} color={color} />}
</ReactFlow>
{/* <button className="z-auto btn btn-primary absolute top-20 right-20" onClick={handleGetPassages}>

View File

@@ -38,10 +38,6 @@ const INITIAL_DATA: MapData = {
systemSignatures: {} as Record<string, SystemSignature[]>,
options: {} as Record<string, string | boolean>,
isSubscriptionActive: false,
mainCharacterEveId: null,
followingCharacterEveId: null,
userHubs: [],
pings: [],
};
export interface MapContextProps {
@@ -57,7 +53,7 @@ const MapContext = createContext<MapContextProps>({
outCommand: async () => void 0,
});
export const MapProvider = ({ children, onCommand }: MapProviderProps) => {
export const MapProvider: React.FC<MapProviderProps> = ({ children, onCommand }) => {
const { update, ref } = useContextStore<MapData>({ ...INITIAL_DATA });
return (

View File

@@ -2,6 +2,7 @@ import React, { RefObject, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { Edge } from '@reactflow/core/dist/esm/types/edges';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import classes from './ContextMenuConnection.module.scss';
@@ -13,7 +14,6 @@ import {
SHIP_SIZES_NAMES_SHORT,
SHIP_SIZES_SIZE,
} from '@/hooks/Mapper/components/map/constants.ts';
import { Edge } from 'reactflow';
export interface ContextMenuConnectionProps {
contextMenuRef: RefObject<ContextMenu>;

View File

@@ -1,8 +1,9 @@
import { Edge, EdgeMouseHandler } from 'reactflow';
import { EdgeMouseHandler } from 'reactflow';
import { useCallback, useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { useMapState } from '../../MapProvider.tsx';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { Edge } from '@reactflow/core/dist/esm/types/edges';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';

View File

@@ -1,15 +1,14 @@
import { useMemo } from 'react';
import React, { useMemo } from 'react';
import { SystemKillsContent } from '../../../mapInterface/widgets/SystemKills/SystemKillsContent/SystemKillsContent';
import { useKillsCounter } from '../../hooks/useKillsCounter';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
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;
@@ -19,14 +18,12 @@ type KillsBookmarkTooltipProps = {
} & WithChildren &
WithClassName;
export const KillsCounter = ({
killsCount,
systemId,
className,
children,
size = TooltipSize.xs,
}: KillsBookmarkTooltipProps) => {
const { isLoading, kills: detailedKills } = useKillsCounter({
export const KillsCounter = ({ killsCount, systemId, className, children, size = 'xs' }: KillsBookmarkTooltipProps) => {
const {
isLoading,
kills: detailedKills,
systemNameMap,
} = useKillsCounter({
realSystemId: systemId,
});
@@ -40,24 +37,28 @@ export const KillsCounter = ({
}
// Calculate height based on number of kills, but ensure a minimum height
const killsNeededHeight = limitedKills.length * KILLS_ROW_HEIGHT;
const killsNeededHeight = limitedKills.length * ITEM_HEIGHT;
// Add a small buffer (10px) to prevent scrollbar from appearing unnecessarily
const tooltipHeight = Math.max(MIN_TOOLTIP_HEIGHT, Math.min(killsNeededHeight + 10, 500));
return (
<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}
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}>
{children}
</WdTooltipWrapper>
);

View File

@@ -6,8 +6,8 @@ import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widget
import { useLocalCharactersItemTemplate } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters';
import { useLocalCharacterWidgetSettings } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalWidgetSettings';
import classes from './SolarSystemLocalCounter.module.scss';
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider';
import { useTheme } from '@/hooks/Mapper/hooks/useTheme.ts';
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider/types.ts';
interface LocalCounterProps {
localCounterCharacters: Array<CharItemProps>;

View File

@@ -1,23 +1,11 @@
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
$pastel-blue: #5a7d9a;
$pastel-pink: rgb(30, 161, 255);
$pastel-pink: #d291bc;
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020;
$neon-color-1: rgb(27, 132, 236);
$neon-color-3: rgba(27, 132, 236, 0.40);
@keyframes move-stripes {
from {
background-position: 0 0;
}
to {
background-position: 30px 0;
}
}
.RootCustomNode {
display: flex;
width: 130px;
@@ -44,7 +32,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
&.Amarria,
&.Gallente,
&.Caldaria {
&::after {
&::before {
content: '';
position: absolute;
top: 0;
@@ -60,7 +48,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
}
&.Mataria {
&::after {
&::before {
background-image: url('/images/mataria-180.png');
opacity: 0.6;
background-position-x: 1px;
@@ -69,7 +57,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
}
&.Caldaria {
&::after {
&::before {
background-image: url('/images/caldaria-180.png');
opacity: 0.6;
background-position-x: 1px;
@@ -78,7 +66,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
}
&.Amarria {
&::after {
&::before {
opacity: 0.45;
background-image: url('/images/amarr-180.png');
background-position-x: 0;
@@ -87,7 +75,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
}
&.Gallente {
&::after {
&::before {
opacity: 0.5;
background-image: url('/images/gallente-180.png');
background-position-x: 1px;
@@ -100,29 +88,6 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
box-shadow: 0 0 10px #9a1af1c2;
}
&.rally {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
border-color: $neon-color-1;
background: repeating-linear-gradient(
45deg,
$neon-color-3 0px,
$neon-color-3 8px,
transparent 8px,
transparent 21px
);
background-size: 30px 30px;
animation: move-stripes 3s linear infinite;
}
}
&.eve-system-status-home {
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
background-image: linear-gradient(45deg, var(--eve-solar-system-status-color-background), transparent);
@@ -390,15 +355,3 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
}
}
}
.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, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import { useLocalCounter, useSolarSystemNode, useNodeKillsCount } from '../../hooks';
import {
EFFECT_BACKGROUND_STYLES,
MARKER_BOOKMARK_BG_STYLES,
@@ -14,27 +14,25 @@ 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';
import { Tag } from 'primereact/tag';
// let render = 0;
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
// console.log('JOipP', `render ${nodeVars.id}`, render++);
return (
<>
{nodeVars.visible && (
<div className={classes.Bookmarks}>
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
</div>
)}
{nodeVars.isShattered && (
<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 className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
<span className={clsx('pi pi-chart-pie', classes.icon)} />
</div>
)}
@@ -42,7 +40,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
<KillsCounter
killsCount={localKillsCount}
systemId={nodeVars.solarSystemId}
size={TooltipSize.lg}
size="lg"
killsActivityType={nodeVars.killsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
>
@@ -53,12 +51,6 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
</KillsCounter>
)}
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
</div>
)}
{nodeVars.labelsInfo.map(x => (
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
{x.shortName}
@@ -71,11 +63,8 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
className={clsx(
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
nodeVars.status !== undefined && classes[STATUS_CLASSES[nodeVars.status]],
{
[classes.selected]: nodeVars.selected,
[classes.rally]: nodeVars.isRally,
},
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
{ [classes.selected]: nodeVars.selected },
)}
onMouseDownCapture={e => nodeVars.dbClick(e)}
>
@@ -93,11 +82,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
</div>
{nodeVars.tag != null && nodeVars.tag !== '' && (
<Tag
value={nodeVars.tag}
severity="warning"
className="py-0 px-[2px] text-[9px] [&_.p-tag-value]:leading-[1.3]"
></Tag>
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{nodeVars.tag}</div>
)}
<div

View File

@@ -14,26 +14,25 @@ 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';
// let render = 0;
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
// console.log('JOipP', `render ${nodeVars.id}`, render++);
return (
<>
{nodeVars.visible && (
<div className={classes.Bookmarks}>
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
</div>
)}
{nodeVars.isShattered && (
<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 className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
<span className={clsx('pi pi-chart-pie', classes.icon)} />
</div>
)}
@@ -41,7 +40,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
<KillsCounter
killsCount={localKillsCount}
systemId={nodeVars.solarSystemId}
size={TooltipSize.lg}
size="lg"
killsActivityType={nodeVars.killsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
>
@@ -52,12 +51,6 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
</KillsCounter>
)}
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
</div>
)}
{nodeVars.labelsInfo.map(x => (
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
{x.shortName}
@@ -71,10 +64,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
{
[classes.selected]: nodeVars.selected,
[classes.rally]: nodeVars.isRally,
},
{ [classes.selected]: nodeVars.selected },
)}
onMouseDownCapture={e => nodeVars.dbClick(e)}
>
@@ -119,13 +109,23 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
{nodeVars.customName && (
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
<div
className={clsx(
classes.CustomName,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
)}
>
{nodeVars.customName}
</div>
)}
{!nodeVars.isWormhole && !nodeVars.customName && (
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
<div
className={clsx(
classes.RegionName,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
)}
>
{nodeVars.regionName}
</div>
)}

View File

@@ -16,7 +16,7 @@ export enum SOLAR_SYSTEM_CLASS_IDS {
thera = 12,
c13 = 13,
sentinel = 14,
barbican = 15,
baribican = 15,
vidette = 16,
conflux = 17,
redoubt = 18,
@@ -82,7 +82,7 @@ export const SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS = {
thera: SOLAR_SYSTEM_CLASS_GROUPS.thera,
c13: SOLAR_SYSTEM_CLASS_GROUPS.c13,
sentinel: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
barbican: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
baribican: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
vidette: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
conflux: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
redoubt: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
@@ -217,7 +217,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 14,
effectPower: 2,
title: 'Class 14 (Sentinel Drifter)',
shortTitle: 'Sentinel MZ',
shortTitle: 'Sentinel',
},
{
id: 'barbican',
@@ -225,7 +225,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 15,
effectPower: 2,
title: 'Class 15 (Barbican Drifter)',
shortTitle: 'Liberated Barbican',
shortTitle: 'Barbican',
},
{
id: 'vidette',
@@ -233,7 +233,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 16,
effectPower: 2,
title: 'Class 16 (Vidette Drifter)',
shortTitle: 'Sanctified Vidette',
shortTitle: 'Vidette',
},
{
id: 'conflux',
@@ -241,7 +241,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 17,
effectPower: 2,
title: 'Class 17 (Conflux Drifter)',
shortTitle: 'Conflux Eyrie',
shortTitle: 'Conflux',
},
{
id: 'redoubt',
@@ -249,7 +249,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 18,
effectPower: 2,
title: 'Class 18 (Redoubt Drifter)',
shortTitle: 'Azdaja Redoubt',
shortTitle: 'Redoubt',
},
{
id: 'a1',

View File

@@ -10,7 +10,7 @@ export const isWormholeSpace = (wormholeClassID: number) => {
case SOLAR_SYSTEM_CLASS_IDS.c6:
case SOLAR_SYSTEM_CLASS_IDS.c13:
case SOLAR_SYSTEM_CLASS_IDS.thera:
case SOLAR_SYSTEM_CLASS_IDS.barbican:
case SOLAR_SYSTEM_CLASS_IDS.baribican:
case SOLAR_SYSTEM_CLASS_IDS.vidette:
case SOLAR_SYSTEM_CLASS_IDS.conflux:
case SOLAR_SYSTEM_CLASS_IDS.redoubt:

View File

@@ -1,4 +1,5 @@
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useCallback, useRef } from 'react';
import {
CommandCharacterAdded,
CommandCharacterRemoved,
@@ -6,7 +7,6 @@ import {
CommandCharacterUpdated,
CommandPresentCharacters,
} from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react';
export const useCommandsCharacters = () => {
const { update } = useMapState();

View File

@@ -2,13 +2,10 @@ 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 };
@@ -16,10 +13,7 @@ export const useMapAddSystems = () => {
const { rf } = ref.current;
const nodes = rf.getNodes();
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);
const prepared: Node[] = systems.filter(x => !nodes.some(y => x.id === y.id)).map(convertSystem2Node);
rf.addNodes(prepared);
}, []);
};

View File

@@ -1,6 +1,6 @@
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { CommandKillsUpdated, CommandMapUpdated } from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react';
import { CommandKillsUpdated, CommandMapUpdated } from '@/hooks/Mapper/types';
export const useMapCommands = () => {
const { update } = useMapState();
@@ -8,21 +8,13 @@ export const useMapCommands = () => {
const ref = useRef({ update });
ref.current = { update };
const mapUpdated = useCallback(({ hubs, system_signatures, kills }: CommandMapUpdated) => {
const mapUpdated = useCallback(({ hubs }: CommandMapUpdated) => {
const out: Partial<MapData> = {};
if (hubs) {
out.hubs = hubs;
}
if (system_signatures) {
out.systemSignatures = system_signatures;
}
if (kills) {
out.kills = kills.reduce((acc, x) => ({ ...acc, [x.solar_system_id]: x.kills }), {});
}
ref.current.update(out);
}, []);

View File

@@ -1,8 +1,8 @@
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useCallback, useRef } from 'react';
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
import { convertConnection2Edge, convertSystem2Node } from '../../helpers';
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
export const useMapInit = () => {
const rf = useReactFlow();
@@ -14,7 +14,6 @@ export const useMapInit = () => {
return useCallback(
({
systems,
system_signatures,
kills,
connections,
wormholes,
@@ -52,10 +51,6 @@ 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

@@ -115,18 +115,35 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
}, 500);
break;
case Commands.pingAdded:
case Commands.pingCancelled:
case Commands.routes:
// do nothing here
break;
case Commands.signaturesUpdated:
// do nothing here
break;
case Commands.linkSignatureToSystem:
// do nothing here
break;
case Commands.detailedKillsUpdated:
// do nothing here
break;
case Commands.characterActivityData:
break;
case Commands.trackingCharactersData:
break;
case Commands.updateActivity:
break;
case Commands.updateTracking:
break;
case Commands.userSettingsUpdated:
// do nothing
break;
default:

View File

@@ -13,26 +13,28 @@ interface MapEvent {
payload?: Kill[];
}
export function useNodeKillsCount(systemId: number | string, initialKillsCount: number | null): number | null {
export function useNodeKillsCount(
systemId: number | string,
initialKillsCount: number | null
): number | null {
const [killsCount, setKillsCount] = useState<number | null>(initialKillsCount);
useEffect(() => {
setKillsCount(initialKillsCount);
}, [initialKillsCount]);
const handleEvent = useCallback(
(event: MapEvent): boolean => {
if (event.name === Commands.killsUpdated && Array.isArray(event.payload)) {
const killForSystem = event.payload.find(kill => kill.solar_system_id.toString() === systemId.toString());
if (killForSystem && typeof killForSystem.kills === 'number') {
setKillsCount(killForSystem.kills);
}
return true;
const handleEvent = useCallback((event: MapEvent): boolean => {
if (event.name === Commands.killsUpdated && Array.isArray(event.payload)) {
const killForSystem = event.payload.find(
kill => kill.solar_system_id.toString() === systemId.toString()
);
if (killForSystem && typeof killForSystem.kills === 'number') {
setKillsCount(killForSystem.kills);
}
return false;
},
[systemId],
);
return true;
}
return false;
}, [systemId]);
useMapEventListener(handleEvent);

View File

@@ -9,11 +9,10 @@ import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
import { CharacterTypeRaw, OutCommand, PingType, SystemSignature } from '@/hooks/Mapper/types';
import { CharacterTypeRaw, OutCommand, SystemSignature } from '@/hooks/Mapper/types';
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';
@@ -21,45 +20,6 @@ function getActivityType(count: number): string {
return 'activityDanger';
}
export interface SolarSystemNodeVars {
id: string;
selected: boolean;
visible: boolean;
isWormhole: boolean;
classTitleColor: string | null;
killsCount: number | null;
killsActivityType: string | null;
hasUserCharacters: boolean;
showHandlers: boolean;
regionClass: string | null;
systemName: string;
customName?: string | null;
labelCustom: string | null;
isShattered: boolean;
tag?: string | null;
status?: number;
labelsInfo: LabelInfo[];
dbClick: (event: React.MouseEvent<HTMLDivElement>) => void;
sortedStatics: Array<string | number>;
effectName: string | null;
regionName: string | null;
solarSystemId: string;
solarSystemName: string | null;
locked: boolean;
hubs: string[];
name: string | null;
isConnecting: boolean;
hoverNodeId: string | null;
charactersInSystem: Array<CharacterTypeRaw>;
userCharacters: string[];
unsplashedLeft: Array<SystemSignature>;
unsplashedRight: Array<SystemSignature>;
isThickConnections: boolean;
isRally: boolean;
classTitle: string | null;
temporaryName?: string | null;
}
const SpaceToClass: Record<string, string> = {
[Spaces.Caldari]: 'Caldaria',
[Spaces.Matar]: 'Mataria',
@@ -80,10 +40,11 @@ export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
return { localCounterCharacters };
}
export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars => {
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars {
const { id, data, selected } = props;
const {
id: solar_system_id,
system_static_info,
system_signatures,
locked,
name,
tag,
@@ -93,26 +54,23 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
linked_sig_eve_id: linkedSigEveId = '',
} = data;
const {
storedSettings: { interfaceSettings },
data: { systemSignatures: mapSystemSignatures },
} = useMapRootState();
const systemStaticInfo = useMemo(() => {
return getSystemStaticInfo(solar_system_id)!;
}, [solar_system_id]);
const {
system_class,
security,
class_title,
solar_system_id,
statics,
effect_name,
region_name,
region_id,
is_shattered,
solar_system_name,
} = systemStaticInfo;
} = system_static_info;
const {
interfaceSettings,
data: { systemSignatures: mapSystemSignatures },
} = useMapRootState();
const { isShowUnsplashedSignatures } = interfaceSettings;
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
@@ -131,17 +89,19 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
visibleNodes,
showKSpaceBG,
isThickConnections,
pings,
},
outCommand,
} = useMapState();
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
const systemSigs = useMemo(() => mapSystemSignatures[solar_system_id] || [], [solar_system_id, mapSystemSignatures]);
const systemSigs = useMemo(
() => mapSystemSignatures[solar_system_id] || system_signatures,
[system_signatures, solar_system_id, mapSystemSignatures],
);
const charactersInSystem = useMemo(() => {
return characters.filter(c => c.location?.solar_system_id === parseInt(solar_system_id) && c.online);
return characters.filter(c => c.location?.solar_system_id === solar_system_id && c.online);
}, [characters, solar_system_id]);
const isWormhole = isWormholeSpace(system_class);
@@ -161,7 +121,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
isShowLinkedSigId,
});
const killsCount = useMemo(() => kills[parseInt(solar_system_id)] ?? null, [kills, solar_system_id]);
const killsCount = useMemo(() => kills[solar_system_id] ?? null, [kills, solar_system_id]);
const killsActivityType = killsCount ? getActivityType(killsCount) : null;
const hasUserCharacters = useMemo(
@@ -172,7 +132,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
const dbClick = useDoubleClick(() => {
outCommand({
type: OutCommand.openSettings,
data: { system_id: solar_system_id },
data: { system_id: solar_system_id.toString() },
});
});
@@ -184,21 +144,16 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
const { systemName, computedTemporaryName, customName } = useSystemName({
isTempSystemNameEnabled,
temporary_name,
solar_system_name: solar_system_name || '',
isShowLinkedSigIdTempName,
linkedSigPrefix,
name,
systemStaticInfo,
});
const { unsplashedLeft, unsplashedRight } = useUnsplashedSignatures(systemSigs, isShowUnsplashedSignatures);
const hubsAsStrings = useMemo(() => hubs.map(item => item.toString()), [hubs]);
const isRally = useMemo(
() => pings.find(x => x.solar_system_id === solar_system_id && x.type === PingType.Rally),
[pings, solar_system_id],
);
const nodeVars: SolarSystemNodeVars = {
id,
selected,
@@ -235,8 +190,45 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
temporaryName: computedTemporaryName,
regionName: region_name,
solarSystemName: solar_system_name,
isRally,
};
return nodeVars;
};
}
export interface SolarSystemNodeVars {
id: string;
selected: boolean;
visible: boolean;
isWormhole: boolean;
classTitleColor: string | null;
killsCount: number | null;
killsActivityType: string | null;
hasUserCharacters: boolean;
showHandlers: boolean;
regionClass: string | null;
systemName: string;
customName?: string | null;
labelCustom: string | null;
isShattered: boolean;
tag?: string | null;
status?: number;
labelsInfo: LabelInfo[];
dbClick: (event: React.MouseEvent<HTMLDivElement>) => void;
sortedStatics: Array<string | number>;
effectName: string | null;
regionName: string | null;
solarSystemId: string;
solarSystemName: string | null;
locked: boolean;
hubs: string[];
name: string | null;
isConnecting: boolean;
hoverNodeId: string | null;
charactersInSystem: Array<CharacterTypeRaw>;
userCharacters: string[];
unsplashedLeft: Array<SystemSignature>;
unsplashedRight: Array<SystemSignature>;
isThickConnections: boolean;
classTitle: string | null;
temporaryName?: string | null;
}

View File

@@ -1,34 +1,30 @@
// useSystemName.ts
import { useMemo } from 'react';
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
interface UseSystemNameParams {
isTempSystemNameEnabled: boolean;
temporary_name?: string | null;
solar_system_name: string;
isShowLinkedSigIdTempName: boolean;
linkedSigPrefix: string | null;
name?: string | null;
systemStaticInfo: SolarSystemStaticInfoRaw;
}
export const useSystemName = ({
export function useSystemName({
isTempSystemNameEnabled,
temporary_name,
solar_system_name,
isShowLinkedSigIdTempName,
linkedSigPrefix,
name,
systemStaticInfo,
}: UseSystemNameParams) => {
const { solar_system_name = '' } = systemStaticInfo;
}: UseSystemNameParams) {
const computedTemporaryName = useMemo(() => {
if (!isTempSystemNameEnabled) {
return '';
}
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
return temporary_name ? `${linkedSigPrefix}${temporary_name}` : `${linkedSigPrefix}${solar_system_name}`;
}
return temporary_name ?? '';
}, [isTempSystemNameEnabled, temporary_name, solar_system_name, isShowLinkedSigIdTempName, linkedSigPrefix]);
@@ -36,7 +32,6 @@ export const useSystemName = ({
if (isTempSystemNameEnabled && computedTemporaryName) {
return computedTemporaryName;
}
return solar_system_name;
}, [isTempSystemNameEnabled, computedTemporaryName, solar_system_name]);
@@ -44,13 +39,11 @@ export const useSystemName = ({
if (isTempSystemNameEnabled && computedTemporaryName && name) {
return name;
}
if (solar_system_name !== name && name) {
return name;
}
return null;
}, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]);
return { systemName, computedTemporaryName, customName };
};
}

View File

@@ -542,32 +542,48 @@
background-color: #d10600;
}
.react-flow {
color: var(--text-color);
.react-flow__minimap-node {
fill: #ffb03a;
}
&__pane {
cursor: auto;
}
.react-flow__minimap {
border: 1px solid #282828;
border-radius: 4px;
background-color: rgb(47 37 37) !important;
overflow: hidden;
}
&__minimap {
background-color: rgba(66, 66, 66, 1);
opacity: 0.7;
border: 1px solid #2f2f2f;
border-radius: 4px;
overflow: hidden;
}
.react-flow__minimap-mask {
stroke-width: 2px;
fill: rgba(0, 0, 0, 0.5);
mix-blend-mode: overlay;
}
&__minimap-mask {
fill: rgba(28, 28, 28, 0.75);
}
.react-flow__minimap-mask {
stroke-width: 2px;
fill: rgb(0 0 0 / 50%) !important;
mix-blend-mode: inherit;
opacity: 1;
stroke: #fff;
&__controls {
filter: brightness(1.5);
}
&__minimap-node {
fill: #ffb03a;
}
}
.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

@@ -1,52 +0,0 @@
import { MarkdownComment } from '@/hooks/Mapper/components/mapInterface/components/Comments/components';
import { useEffect, useRef, useState } from 'react';
import { CommentType } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export interface CommentsProps {}
// eslint-disable-next-line no-empty-pattern
export const Comments = ({}: CommentsProps) => {
const [commentsList, setCommentsList] = useState<CommentType[]>([]);
const {
data: { selectedSystems },
comments: { loadComments, comments, lastUpdateKey },
} = useMapRootState();
const [systemId] = selectedSystems;
const ref = useRef({ loadComments, systemId });
ref.current = { loadComments, systemId };
useEffect(() => {
const commentsBySystem = comments.get(systemId);
if (!commentsBySystem) {
return;
}
const els = [...commentsBySystem.comments].sort((a, b) => +new Date(b.updated_at) - +new Date(a.updated_at));
setCommentsList(els);
}, [systemId, lastUpdateKey, comments]);
useEffect(() => {
ref.current.loadComments(systemId);
}, [systemId]);
if (commentsList.length === 0) {
return (
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
Not comments found here
</div>
);
}
return (
<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} />
))}
</div>
);
};

View File

@@ -1,70 +0,0 @@
.MarkdownCommentRoot {
border-left-width: 3px;
@apply text-[12px] leading-[1.2] text-stone-300 break-words;
@apply bg-gradient-to-r from-stone-600/40 via-stone-600/10 to-stone-600/0;
.h1 {
@apply text-[12px] font-normal m-0 p-0 border-none break-words whitespace-normal;
}
.h2 {
@apply text-[12px] font-normal m-0 p-0 border-none break-words whitespace-normal;
}
.h3 {
@apply text-[12px] font-normal m-0 p-0 border-none break-words whitespace-normal;
}
.h4 {
@apply text-[12px] font-normal m-0 p-0 border-none break-words whitespace-normal;
}
.h5 {
@apply text-[12px] font-normal m-0 p-0 border-none break-words whitespace-normal;
}
.h6 {
@apply text-[12px] font-normal m-0 p-0 border-none break-words whitespace-normal;
}
p {
@apply m-0 p-0 break-words whitespace-normal;
}
ul, ol {
@apply m-0 p-0 list-none;
}
li {
@apply m-0 break-words whitespace-normal;
}
blockquote {
@apply border-l-4 border-cyan-400 p-2 m-0 font-normal text-stone-300 italic break-words whitespace-normal;
}
a {
@apply text-violet-400 cursor-pointer transition-colors duration-200 break-words whitespace-normal;
&:hover {
@apply underline;
}
}
b, strong {
@apply font-bold text-green-400 break-words whitespace-normal;
}
i, em {
@apply italic text-pink-400 break-words whitespace-normal;
}
del {
@apply line-through text-stone-500 break-words whitespace-normal;
}
hr {
@apply border-none h-[1px] bg-cyan-400 opacity-50 my-2;
}
}

View File

@@ -1,97 +0,0 @@
import classes from './MarkdownComment.module.scss';
import clsx from 'clsx';
import {
InfoDrawer,
MarkdownTextViewer,
TimeAgo,
TooltipPosition,
WdImgButton,
} from '@/hooks/Mapper/components/ui-kit';
import { useGetCacheCharacter } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useCallback, useRef, useState } from 'react';
import { WdTransition } from '@/hooks/Mapper/components/ui-kit/WdTransition/WdTransition.tsx';
import { PrimeIcons } from 'primereact/api';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
const TOOLTIP_PROPS = { content: 'Remove comment', position: TooltipPosition.top };
export interface MarkdownCommentProps {
text: string;
time: string;
characterEveId: string;
id: string;
}
export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownCommentProps) => {
const char = useGetCacheCharacter(characterEveId);
const [hovered, setHovered] = useState(false);
const cpRemoveBtnRef = useRef<HTMLElement>();
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
const { outCommand } = useMapRootState();
const ref = useRef({ outCommand, id });
ref.current = { outCommand, id };
const handleDelete = useCallback(async () => {
await ref.current.outCommand({
type: OutCommand.deleteSystemComment,
data: ref.current.id,
});
}, []);
const handleMouseEnter = useCallback(() => setHovered(true), []);
const handleMouseLeave = useCallback(() => setHovered(false), []);
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
return (
<>
<InfoDrawer
labelClassName="mb-[3px]"
className={clsx(classes.MarkdownCommentRoot, 'p-1 bg-stone-700/20 ')}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
title={
<div className="flex items-center justify-between">
<div>
<span className="text-stone-500">
by <span className="text-orange-300/70">{char?.data?.name ?? ''}</span>
</span>
</div>
<WdTransition active={hovered} timeout={100}>
<div className="text-stone-500 max-h-[12px]">
{!hovered && <TimeAgo timestamp={time} />}
{hovered && (
// @ts-ignore
<div ref={cpRemoveBtnRef}>
<WdImgButton
className={clsx(PrimeIcons.TRASH, 'hover:text-red-400')}
tooltip={TOOLTIP_PROPS}
onClick={handleShowCP}
/>
</div>
)}
</div>
</WdTransition>
</div>
}
>
<MarkdownTextViewer>{text}</MarkdownTextViewer>
</InfoDrawer>
<ConfirmPopup
target={cpRemoveBtnRef.current}
visible={cpRemoveVisible}
onHide={handleHideCP}
message="Are you sure you want to delete?"
icon="pi pi-exclamation-triangle"
accept={handleDelete}
/>
</>
);
};

View File

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

View File

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

View File

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

View File

@@ -1,26 +0,0 @@
export const markdown = `
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
---
## Paragraphs
This is a regular text paragraph.
Another paragraph, but with **bold** and *italic* text, as well as ~~strikethrough~~.
> This is a block quote.
> Second line of the quote.
## Links
[Link to Google](https://www.google.com)
## Horizontal Line
A block quote with ~strikethrough~ and a URL: https://reactjs.org.
---
`;

View File

@@ -1,73 +0,0 @@
import { TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { MarkdownEditor } from '@/hooks/Mapper/components/mapInterface/components/MarkdownEditor';
import { useHotkey } from '@/hooks/Mapper/hooks';
import { useCallback, useRef, useState } from 'react';
import { OutCommand } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export interface CommentsEditorProps {}
// eslint-disable-next-line no-empty-pattern
export const CommentsEditor = ({}: CommentsEditorProps) => {
const [textVal, setTextVal] = useState('');
const {
data: { selectedSystems },
outCommand,
} = useMapRootState();
const [systemId] = selectedSystems;
const ref = useRef({ outCommand, systemId, textVal });
ref.current = { outCommand, systemId, textVal };
const handleFinishEdit = useCallback(async () => {
if (ref.current.textVal === '') {
return;
}
await ref.current.outCommand({
type: OutCommand.addSystemComment,
data: {
solarSystemId: ref.current.systemId,
value: ref.current.textVal,
},
});
setTextVal('');
}, []);
const handleClick = async () => {
await handleFinishEdit();
};
useHotkey(true, ['Enter'], async () => {
await handleFinishEdit();
});
return (
<MarkdownEditor
value={textVal}
onChange={setTextVal}
overlayContent={
<div className="w-full h-full flex justify-end items-end pointer-events-none pb-[1px] pr-[8px]">
<WdImgButton
disabled={textVal.length === 0}
tooltip={{
position: TooltipPosition.bottom,
content: (
<span>
Also you may use <span className="text-cyan-400">Meta + Enter</span> hotkey.
</span>
),
}}
textSize={WdImageSize.large}
className={clsx(PrimeIcons.SEND, 'text-[14px]')}
onClick={handleClick}
/>
</div>
}
/>
);
};

View File

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

View File

@@ -1,40 +0,0 @@
.CERoot {
@apply border border-stone-400/30 rounded-[2px];
:global {
.cm-content {
@apply bg-stone-600/40;
}
.cm-scroller {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.5) transparent;
}
.cm-scroller::-webkit-scrollbar {
width: 10px;
}
.cm-scroller::-webkit-scrollbar-track {
background: transparent;
}
.cm-scroller::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.5);
border-radius: 5px;
border: 2px solid transparent;
background-clip: content-box;
}
.cm-scroller::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.7);
}
.cm-scroller::-webkit-scrollbar-button {
display: none;
height: 0;
width: 0;
}
}
}

View File

@@ -1,86 +0,0 @@
import { ReactNode, useCallback, useRef, useState } from 'react';
import CodeMirror, { ViewPlugin } from '@uiw/react-codemirror';
import { markdown } from '@codemirror/lang-markdown';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView, type ViewUpdate } from '@codemirror/view';
import classes from './MarkdownEditor.module.scss';
import clsx from 'clsx';
// TODO special plugin which force CodeMirror using capture for paste event
const stopEventPropagationPlugin = ViewPlugin.fromClass(
class {
constructor(view: EditorView) {
// @ts-ignore
this.view = view;
// @ts-ignore
this.pasteHandler = (event: Event) => {
event.stopPropagation();
};
// @ts-ignore
view.dom.addEventListener('paste', this.pasteHandler);
}
destroy() {
// @ts-ignore
this.view.dom.removeEventListener('paste', this.pasteHandler);
}
},
);
const CODE_MIRROR_EXTENSIONS = [
markdown(),
EditorView.lineWrapping,
EditorView.theme({
'&': { backgroundColor: 'transparent !important' },
'& .cm-gutterElement': { display: 'none' },
}),
stopEventPropagationPlugin,
];
export interface MarkdownEditorProps {
overlayContent?: ReactNode;
value: string;
onChange: (value: string) => void;
}
export const MarkdownEditor = ({ value, onChange, overlayContent }: MarkdownEditorProps) => {
const [hasShift, setHasShift] = useState(false);
const refData = useRef({ onChange });
refData.current = { onChange };
const handleOnChange = useCallback((value: string, viewUpdate: ViewUpdate) => {
// Rerender happens after change
setTimeout(() => {
const scrollDOM = viewUpdate.view.scrollDOM;
setHasShift(scrollDOM.scrollHeight > scrollDOM.clientHeight);
}, 0);
refData.current.onChange(value);
}, []);
return (
<div className={clsx(classes.MarkdownEditor, 'relative')}>
<CodeMirror
value={value}
height="70px"
extensions={CODE_MIRROR_EXTENSIONS}
className={classes.CERoot}
theme={oneDark}
onChange={handleOnChange}
placeholder="Start typing..."
/>
<div
className={clsx('absolute top-0 left-0 h-full pointer-events-none', {
'w-full': !hasShift,
'w-[calc(100%-10px)]': hasShift,
})}
>
{overlayContent}
</div>
</div>
);
};

View File

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

View File

@@ -1,49 +0,0 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo } from 'react';
import { RoutesList } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesList';
export const PingRoute = () => {
const {
data: { routes, pings, loadingPublicRoutes },
} = useMapRootState();
const route = useMemo(() => {
const [ping] = pings;
if (!ping) {
return null;
}
return routes?.routes.find(x => ping.solar_system_id === x.destination.toString()) ?? null;
}, [routes, pings]);
const preparedRoute = useMemo(() => {
if (!route) {
return null;
}
return {
...route,
mapped_systems:
route.systems?.map(solar_system_id =>
routes?.systems_static_data.find(
system_static_data => system_static_data.solar_system_id === solar_system_id,
),
) ?? [],
};
}, [route, routes?.systems_static_data]);
if (loadingPublicRoutes) {
return <span className="m-0 text-[12px]">Loading...</span>;
}
if (!preparedRoute || preparedRoute.origin === preparedRoute.destination) {
return null;
}
return (
<div className="m-0 flex gap-2 items-center text-[12px]">
{preparedRoute.has_connection && <div className="text-[12px]">{preparedRoute.systems?.length ?? 2}</div>}
<RoutesList data={preparedRoute} />
</div>
);
};

View File

@@ -1,284 +0,0 @@
import { Button } from 'primereact/button';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Toast } from 'primereact/toast';
import clsx from 'clsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { Commands, OutCommand, PingType } from '@/hooks/Mapper/types';
import {
CharacterCardById,
SystemView,
TimeAgo,
TooltipPosition,
WdImgButton,
WdImgButtonTooltip,
} from '@/hooks/Mapper/components/ui-kit';
import useRefState from 'react-usestateref';
import { PrimeIcons } from 'primereact/api';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { PingRoute } from '@/hooks/Mapper/components/mapInterface/components/PingsInterface/PingRoute.tsx';
import { PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
const PING_PLACEMENT_MAP = {
[PingsPlacement.rightTop]: 'top-right',
[PingsPlacement.leftTop]: 'top-left',
[PingsPlacement.rightBottom]: 'bottom-right',
[PingsPlacement.leftBottom]: 'bottom-left',
};
const PING_PLACEMENT_MAP_OFFSETS = {
[PingsPlacement.rightTop]: { default: '!top-[56px]', withLeftMenu: '!top-[56px] !right-[64px]' },
[PingsPlacement.rightBottom]: { default: '!bottom-[15px]', withLeftMenu: '!bottom-[15px] !right-[64px]' },
[PingsPlacement.leftTop]: { default: '!top-[56px] !left-[64px]', withLeftMenu: '!top-[56px] !left-[64px]' },
[PingsPlacement.leftBottom]: { default: '!left-[64px] !bottom-[15px]', withLeftMenu: '!bottom-[15px]' },
};
const CLOSE_TOOLTIP_PROPS: WdImgButtonTooltip = {
content: 'Hide',
position: TooltipPosition.top,
className: '!leading-[0]',
};
const NAVIGATE_TOOLTIP_PROPS: WdImgButtonTooltip = {
content: 'Navigate To',
position: TooltipPosition.top,
className: '!leading-[0]',
};
const DELETE_TOOLTIP_PROPS: WdImgButtonTooltip = {
content: 'Remove',
position: TooltipPosition.top,
className: '!leading-[0]',
};
// const TOOLTIP_WAYPOINT_PROPS: WdImgButtonTooltip = {
// content: 'Waypoint',
// position: TooltipPosition.bottom,
// className: '!leading-[0]',
// };
const TITLES = {
[PingType.Alert]: 'Alert',
[PingType.Rally]: 'Rally Point',
};
const ICONS = {
[PingType.Alert]: 'pi-bell',
[PingType.Rally]: 'pi-bell',
};
export interface PingsInterfaceProps {
hasLeftOffset?: boolean;
}
// TODO: right now can be one ping. But in future will be multiple pings then:
// 1. we will use this as container
// 2. we will create PingInstance (which will contains ping Button and Toast
// 3. ADD Context menu
export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
const toast = useRef<Toast>(null);
const [isShow, setIsShow, isShowRef] = useRefState(false);
const cpRemoveBtnRef = useRef<HTMLElement>();
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
const {
storedSettings: { interfaceSettings },
data: { pings, selectedSystems },
outCommand,
} = useMapRootState();
const selectedSystem = useMemo(() => {
if (selectedSystems.length !== 1) {
return null;
}
return selectedSystems[0];
}, [selectedSystems]);
const ping = useMemo(() => (pings.length === 1 ? pings[0] : null), [pings]);
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
const navigateTo = useCallback(() => {
if (!ping) {
return;
}
emitMapEvent({
name: Commands.centerSystem,
data: ping.solar_system_id?.toString(),
});
}, [ping]);
const removePing = useCallback(async () => {
if (!ping) {
return;
}
await outCommand({
type: OutCommand.cancelPing,
data: { type: ping.type, solar_system_id: ping.solar_system_id },
});
}, [outCommand, ping]);
useEffect(() => {
if (!ping) {
return;
}
const tid = setTimeout(() => {
toast.current?.replace({ severity: 'warn', detail: ping.message });
setIsShow(true);
}, 200);
return () => clearTimeout(tid);
}, [ping]);
const handleClickShow = useCallback(() => {
if (!ping) {
return;
}
if (!isShowRef.current) {
toast.current?.show({ severity: 'warn', detail: ping.message });
setIsShow(true);
return;
}
toast.current?.clear();
setIsShow(false);
}, [ping]);
const handleClickHide = useCallback(() => {
toast.current?.clear();
setIsShow(false);
}, []);
const { placement, offsets } = useMemo(() => {
const rawPlacement =
interfaceSettings.pingsPlacement == null ? PingsPlacement.rightTop : interfaceSettings.pingsPlacement;
return {
placement: PING_PLACEMENT_MAP[rawPlacement],
offsets: PING_PLACEMENT_MAP_OFFSETS[rawPlacement],
};
}, [interfaceSettings]);
if (!ping) {
return null;
}
const isShowSelectedSystem = selectedSystem != null && selectedSystem !== ping.solar_system_id;
return (
<>
<Toast
position={placement as never}
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
ref={toast}
content={({ message }) => (
<section
className={clsx(
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
)}
>
<div className="flex gap-3">
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
<div className="flex flex-col gap-1 w-full">
<div className="flex justify-between">
<div>
<div className="m-0 font-semibold text-base text-white">{TITLES[ping.type]}</div>
<div className="flex gap-1 items-center">
{isShowSelectedSystem && (
<>
<SystemView systemId={selectedSystem} />
<span className="pi pi-angle-double-right text-[10px] relative top-[1px] text-stone-400" />
</>
)}
<SystemView systemId={ping.solar_system_id} />
{isShowSelectedSystem && (
<WdImgButton
className={clsx(PrimeIcons.QUESTION_CIRCLE, 'ml-[2px] relative top-[-2px] !text-[10px]')}
tooltip={{
position: TooltipPosition.top,
content: (
<div className="flex flex-col gap-1">
The settings for the route are taken from the Routes settings and can be configured
through them.
</div>
),
}}
/>
)}
</div>
</div>
<div className="flex flex-col items-end">
<CharacterCardById className="" characterId={ping.character_eve_id} simpleMode />
<TimeAgo timestamp={ping.inserted_at.toString()} className="text-stone-400 text-[11px]" />
</div>
</div>
{selectedSystem != null && <PingRoute />}
<p className="m-0 text-[13px] text-stone-200 min-h-[20px] pr-[16px]">{message.detail}</p>
</div>
<WdImgButton
className={clsx(PrimeIcons.TIMES, 'hover:text-red-400 mt-[3px]')}
tooltip={CLOSE_TOOLTIP_PROPS}
onClick={handleClickHide}
/>
</div>
{/*Button bar*/}
<div className="flex justify-end items-center gap-2 h-0 relative top-[-8px]">
<WdImgButton
className={clsx('pi-compass', 'hover:text-red-400 mt-[3px]')}
tooltip={NAVIGATE_TOOLTIP_PROPS}
onClick={navigateTo}
/>
{/*@ts-ignore*/}
<div ref={cpRemoveBtnRef}>
<WdImgButton
className={clsx('pi-trash', 'text-red-400 hover:text-red-300')}
tooltip={DELETE_TOOLTIP_PROPS}
onClick={handleShowCP}
/>
</div>
{/* TODO ADD solar system menu*/}
{/*<WdImgButton*/}
{/* className={clsx('pi-map-marker', 'hover:text-red-400 mt-[3px]')}*/}
{/* tooltip={TOOLTIP_WAYPOINT_PROPS}*/}
{/* onClick={handleClickHide}*/}
{/*/>*/}
</div>
</section>
)}
></Toast>
<Button
icon="pi pi-bell"
severity="warning"
aria-label="Notification"
size="small"
className="w-[33px] h-[33px]"
outlined
onClick={handleClickShow}
disabled={isShow}
/>
<ConfirmPopup
target={cpRemoveBtnRef.current}
visible={cpRemoveVisible}
onHide={handleHideCP}
message="Are you sure you want to delete ping?"
icon="pi pi-exclamation-triangle text-orange-400"
accept={removePing}
/>
</>
);
};

View File

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

View File

@@ -1,23 +1,26 @@
import { useCallback, useRef, useMemo } from 'react';
import { Dialog } from 'primereact/dialog';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommandLinkSignatureToSystem } from '@/hooks/Mapper/types';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { SHOW_DESCRIPTION_COLUMN_SETTING } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures';
import {
Setting,
COSMIC_SIGNATURE,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureSettingsDialog';
import { SignatureGroup } from '@/hooks/Mapper/types';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
import { useSystemInfo } from '@/hooks/Mapper/components/hooks';
import {
SOLAR_SYSTEM_CLASS_IDS,
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
} from '@/hooks/Mapper/components/map/constants.ts';
import {
SETTINGS_KEYS,
SignatureSettingsType,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
const K162_SIGNATURE_TYPE = WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME['K162'].shortName;
@@ -26,11 +29,11 @@ interface SystemLinkSignatureDialogProps {
setVisible: (visible: boolean) => void;
}
export const LINK_SIGNTATURE_SETTINGS: SignatureSettingsType = {
[SETTINGS_KEYS.COSMIC_SIGNATURE]: true,
[SETTINGS_KEYS.WORMHOLE]: true,
[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN]: true,
};
const signatureSettings: Setting[] = [
{ key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true },
{ key: SignatureGroup.Wormhole, name: 'Wormhole', value: true },
{ key: SHOW_DESCRIPTION_COLUMN_SETTING, name: 'Show Description Column', value: true, isFilter: false },
];
// Extend the SignatureCustomInfo type to include k162Type
interface ExtendedSignatureCustomInfo {
@@ -49,9 +52,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
ref.current = { outCommand };
// Get system info for the target system
const { staticInfo: targetSystemInfo, dynamicInfo: targetSystemDynamicInfo } = useSystemInfo({
systemId: `${data.solar_system_target}`,
});
const { staticInfo: targetSystemInfo } = useSystemInfo({ systemId: `${data.solar_system_target}` });
// Get the system class group for the target system
const targetSystemClassGroup = useMemo(() => {
@@ -146,7 +147,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
}
const whShipSize = getWhSize(wormholes, signature.type);
if (whShipSize !== undefined && whShipSize !== null) {
if (whShipSize) {
await outCommand({
type: OutCommand.updateConnectionShipSizeType,
data: {
@@ -162,12 +163,6 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
[data, setVisible, wormholes],
);
useEffect(() => {
if (!targetSystemDynamicInfo) {
handleHide();
}
}, [targetSystemDynamicInfo]);
return (
<Dialog
header="Select signature to link"
@@ -180,7 +175,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
<SystemSignaturesContent
systemId={`${data.solar_system_source}`}
hideLinkedSignatures
settings={LINK_SIGNTATURE_SETTINGS}
settings={signatureSettings}
onSelect={handleSelect}
selectable={true}
filterSignature={filterSignature}

View File

@@ -1,101 +0,0 @@
import { InputTextarea } from 'primereact/inputtextarea';
import { Dialog } from 'primereact/dialog';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { PingType } from '@/hooks/Mapper/types/ping.ts';
import { SystemView } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
const PING_TITLES = {
[PingType.Rally]: 'RALLY',
[PingType.Alert]: 'ALERT',
};
interface SystemPingDialogProps {
systemId: string;
type: PingType;
visible: boolean;
setVisible: (visible: boolean) => void;
}
export const SystemPingDialog = ({ systemId, type, visible, setVisible }: SystemPingDialogProps) => {
const { outCommand } = useMapRootState();
const [message, setMessage] = useState('');
const inputRef = useRef<HTMLTextAreaElement>();
const ref = useRef({ message, outCommand, systemId, type });
ref.current = { message, outCommand, systemId, type };
const handleSave = useCallback(() => {
const { message, outCommand, systemId, type } = ref.current;
outCommand({
type: OutCommand.addPing,
data: {
solar_system_id: systemId,
type,
message,
},
});
setVisible(false);
}, [setVisible]);
const onShow = useCallback(() => {
inputRef.current?.focus();
}, []);
return (
<Dialog
header={
<div className="flex gap-1 text-[13px] items-center text-stone-300">
<div>Ping:{` `}</div>
<div
className={clsx({
['text-cyan-400']: type === PingType.Rally,
})}
>
{PING_TITLES[type]}
</div>
<div className="text-[11px]">in</div> <SystemView systemId={systemId} className="relative top-[1px]" />
</div>
}
visible={visible}
draggable={false}
style={{ width: '450px' }}
onShow={onShow}
onHide={() => {
if (!visible) {
return;
}
setVisible(false);
}}
>
<form onSubmit={handleSave}>
<div className="flex flex-col gap-3 px-2">
<div className="flex flex-col gap-1">
<label className="text-[11px]" htmlFor="username">
Message
</label>
<InputTextarea
// @ts-ignore
ref={inputRef}
autoResize
rows={3}
cols={30}
value={message}
onChange={e => setMessage(e.target.value)}
/>
</div>
<div className="flex gap-2 justify-end">
<Button onClick={handleSave} size="small" severity="danger" label="Ping!"></Button>
</div>
</div>
</form>
</Dialog>
);
};

View File

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

View File

@@ -10,7 +10,6 @@ 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;
@@ -27,7 +26,6 @@ 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('');
@@ -35,11 +33,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, systemStaticInfo });
ref.current = { name, description, label, temporaryName, outCommand, systemId, system, systemStaticInfo };
const ref = useRef({ name, description, temporaryName, label, outCommand, systemId, system });
ref.current = { name, description, label, temporaryName, outCommand, systemId, system };
const handleSave = useCallback(() => {
const { name, description, label, temporaryName, outCommand, systemId, system, systemStaticInfo } = ref.current;
const { name, description, label, temporaryName, outCommand, systemId, system } = ref.current;
const outLabel = new LabelsManager(system?.labels ?? '');
outLabel.updateCustomLabel(label);
@@ -64,7 +62,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
type: OutCommand.updateSystemName,
data: {
system_id: systemId,
value: name.trim() || systemStaticInfo?.solar_system_name,
value: name.trim() || system?.system_static_info.solar_system_name,
},
});
@@ -80,11 +78,11 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
}, [setVisible]);
const handleResetSystemName = useCallback(() => {
const { systemStaticInfo } = ref.current;
if (!systemStaticInfo) {
const { system } = ref.current;
if (!system) {
return;
}
setName(systemStaticInfo.solar_system_name);
setName(system.system_static_info.solar_system_name);
}, []);
const onShow = useCallback(() => {
@@ -132,7 +130,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
<label htmlFor="username">Custom name</label>
<IconField>
{name !== systemStaticInfo?.solar_system_name && (
{name !== system?.system_static_info.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,15 +2,14 @@ import React from 'react';
import classes from './Widget.module.scss';
import clsx from 'clsx';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
export type WidgetProps = {
export interface WidgetProps {
label: React.ReactNode | string;
windowId?: string;
contentClassName?: string;
} & WithChildren;
children?: React.ReactNode;
}
export const Widget = ({ label, children, windowId, contentClassName }: WidgetProps) => {
export const Widget = ({ label, children, windowId }: WidgetProps) => {
return (
<div
data-window-id={windowId}
@@ -35,7 +34,7 @@ export const Widget = ({ label, children, windowId, contentClassName }: WidgetPr
{label}
</div>
<div
className={clsx(classes.Content, 'overflow-auto', 'bg-opacity-5 custom-scrollbar', contentClassName)}
className={clsx(classes.Content, 'overflow-auto', 'bg-opacity-5 custom-scrollbar')}
style={{ flexGrow: 1 }}
onContextMenu={e => {
e.preventDefault();

View File

@@ -2,4 +2,3 @@ export * from './Widget';
export * from './SystemSettingsDialog';
export * from './SystemCustomLabelDialog';
export * from './SystemLinkSignatureDialog';
export * from './PingsInterface';

View File

@@ -1,16 +1,14 @@
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
import {
CommentsWidget,
LocalCharacters,
RoutesWidget,
SystemInfo,
SystemSignatures,
SystemStructures,
WRoutesPublic,
WRoutesUser,
WSystemKills,
SystemKills,
} from '@/hooks/Mapper/components/mapInterface/widgets';
export const CURRENT_WINDOWS_VERSION = 9;
export const CURRENT_WINDOWS_VERSION = 8;
export const WINDOWS_LOCAL_STORE_KEY = 'windows:settings:v2';
export enum WidgetsIds {
@@ -20,8 +18,6 @@ export enum WidgetsIds {
routes = 'routes',
structures = 'structures',
kills = 'kills',
comments = 'comments',
userRoutes = 'userRoutes',
}
export const STORED_VISIBLE_WIDGETS_DEFAULT = [
@@ -58,14 +54,7 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
position: { x: 10, y: 530 },
size: { width: 510, height: 200 },
zIndex: 0,
content: () => <WRoutesPublic />,
},
{
id: WidgetsIds.userRoutes,
position: { x: 10, y: 10 },
size: { width: 510, height: 200 },
zIndex: 0,
content: () => <WRoutesUser />,
content: () => <RoutesWidget />,
},
{
id: WidgetsIds.structures,
@@ -79,14 +68,7 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
position: { x: 270, y: 730 },
size: { width: 510, height: 200 },
zIndex: 0,
content: () => <WSystemKills />,
},
{
id: WidgetsIds.comments,
position: { x: 10, y: 10 },
size: { width: 250, height: 300 },
zIndex: 0,
content: () => <CommentsWidget />,
content: () => <SystemKills />,
},
];
@@ -112,10 +94,6 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
id: WidgetsIds.routes,
label: 'Routes',
},
{
id: WidgetsIds.userRoutes,
label: 'User Routes',
},
{
id: WidgetsIds.structures,
label: 'Structures',
@@ -124,8 +102,4 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
id: WidgetsIds.kills,
label: 'Kills',
},
{
id: WidgetsIds.comments,
label: 'Comments',
},
];

View File

@@ -1,10 +1,6 @@
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
export const sortCharacters = (a: CharacterTypeRaw & WithIsOwnCharacter, b: CharacterTypeRaw & WithIsOwnCharacter) => {
if (a.online === b.online) {
return a.name.localeCompare(b.name);
}
if (a.online !== b.online) {
return a.online && !b.online ? -1 : 1;
}

View File

@@ -1,83 +0,0 @@
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { Comments } from '@/hooks/Mapper/components/mapInterface/components/Comments';
import { InfoDrawer, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { useRef } from 'react';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
import { COMPACT_MAX_WIDTH } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import clsx from 'clsx';
import { CommentsEditor } from '@/hooks/Mapper/components/mapInterface/components/CommentsEditor';
import { PrimeIcons } from 'primereact/api';
export const CommentsWidgetContent = () => {
const {
data: { selectedSystems },
} = useMapRootState();
const isNotSelectedSystem = selectedSystems.length !== 1;
if (isNotSelectedSystem) {
return (
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
System is not selected
</div>
);
}
return (
<div className={clsx('h-full grid grid-rows-[1fr_auto] gap-1 px-[4px]')}>
<Comments />
<CommentsEditor />
</div>
);
};
export const CommentsWidget = () => {
const containerRef = useRef<HTMLDivElement>(null);
const isCompact = useMaxWidth(containerRef, COMPACT_MAX_WIDTH);
const {
data: { selectedSystems, isSubscriptionActive },
} = useMapRootState();
const [systemId] = selectedSystems;
const isNotSelectedSystem = selectedSystems.length !== 1;
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">
{!isCompact && (
<div className="flex whitespace-nowrap text-ellipsis overflow-hidden text-stone-400">
Comments {isNotSelectedSystem ? '' : 'in'}
</div>
)}
{!isNotSelectedSystem && <SystemView systemId={systemId} className="select-none text-center" hideRegion />}
</div>
<WdImgButton
className={PrimeIcons.QUESTION_CIRCLE}
tooltip={{
position: TooltipPosition.left,
content: (
<div className="flex flex-col gap-1">
<InfoDrawer title={<b className="text-slate-50">How to add/delete comment?</b>}>
It is possible to use markdown formating. <br />
Only users with tracking permission can add/delete comments. <br />
</InfoDrawer>
<InfoDrawer title={<b className="text-slate-50">Limitations</b>}>
Each comment length is limited to <b>500</b> characters. <br />
No more than <b>{isSubscriptionActive ? '500' : '30'}</b> comments are allowed per system*. <br />
<small>* based on active map subscription.</small>
</InfoDrawer>
</div>
),
}}
/>
</div>
}
>
<CommentsWidgetContent />
</Widget>
);
};

View File

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

View File

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

View File

@@ -1,31 +1,79 @@
import React, { createContext, forwardRef, useContext } from 'react';
import {
RoutesImperativeHandle,
RoutesProviderInnerProps,
RoutesWidgetProps,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import React, { createContext, useContext, useEffect } from 'react';
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
type MapProviderProps = {
export type RoutesType = {
path_type: 'shortest' | 'secure' | 'insecure';
include_mass_crit: boolean;
include_eol: boolean;
include_frig: boolean;
include_cruise: boolean;
include_thera: boolean;
avoid_wormholes: boolean;
avoid_pochven: boolean;
avoid_edencom: boolean;
avoid_triglavian: boolean;
avoid: number[];
};
interface MapProviderProps {
children: React.ReactNode;
} & RoutesWidgetProps;
}
const RoutesContext = createContext<RoutesProviderInnerProps>({
export const DEFAULT_SETTINGS: RoutesType = {
path_type: 'shortest',
include_mass_crit: true,
include_eol: true,
include_frig: true,
include_cruise: true,
include_thera: true,
avoid_wormholes: false,
avoid_pochven: false,
avoid_edencom: false,
avoid_triglavian: false,
avoid: [],
};
export interface MapContextProps {
update: ContextStoreDataUpdate<RoutesType>;
data: RoutesType;
}
const RoutesContext = createContext<MapContextProps>({
update: () => {},
// @ts-ignore
data: {},
data: { ...DEFAULT_SETTINGS },
});
// INFO: this component have imperative handler but now it not using.
export const RoutesProvider = forwardRef<RoutesImperativeHandle, MapProviderProps>(
({ children, ...props } /*, ref*/) => {
// useImperativeHandle(ref, () => ({}));
export const RoutesProvider: React.FC<MapProviderProps> = ({ children }) => {
const { update, ref } = useContextStore<RoutesType>(
{ ...DEFAULT_SETTINGS },
{
onAfterAUpdate: values => {
localStorage.setItem(SESSION_KEY.routes, JSON.stringify(values));
},
},
);
return <RoutesContext.Provider value={{ ...props /*, loading, setLoading*/ }}>{children}</RoutesContext.Provider>;
},
);
RoutesProvider.displayName = 'RoutesProvider';
useEffect(() => {
const items = localStorage.getItem(SESSION_KEY.routes);
if (items) {
update(JSON.parse(items));
}
}, [update]);
return (
<RoutesContext.Provider
value={{
update,
data: ref,
}}
>
{children}
</RoutesContext.Provider>
);
};
export const useRouteProvider = () => {
const context = useContext<RoutesProviderInnerProps>(RoutesContext);
const context = useContext<MapContextProps>(RoutesContext);
return context;
};

View File

@@ -2,16 +2,16 @@ import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import {
LayoutEventBlocker,
LoadingWrapper,
SystemView,
SystemViewStandalone,
TooltipPosition,
WdCheckbox,
WdImgButton,
} from '@/hooks/Mapper/components/ui-kit';
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { forwardRef, MouseEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { getSystemById } from '@/hooks/Mapper/helpers/getSystemById.ts';
import classes from './RoutesWidget.module.scss';
import { useLoadRoutes } from './hooks';
import { RoutesList } from './RoutesList';
import clsx from 'clsx';
import { Route } from '@/hooks/Mapper/types/routes.ts';
@@ -25,10 +25,7 @@ import {
AddSystemDialog,
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
import {
RoutesImperativeHandle,
RoutesWidgetProps,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { OutCommand } from '@/hooks/Mapper/types';
const sortByDist = (a: Route, b: Route) => {
const distA = a.has_connection ? a.systems?.length || 0 : Infinity;
@@ -39,31 +36,45 @@ const sortByDist = (a: Route, b: Route) => {
export const RoutesWidgetContent = () => {
const {
data: { selectedSystems, systems, isSubscriptionActive },
data: { selectedSystems, hubs = [], systems, routes },
outCommand,
} = useMapRootState();
const { hubs = [], routesList, isRestricted, loading } = useRouteProvider();
const [systemId] = selectedSystems;
const { systems: systemStatics, loadSystems } = useLoadSystemStatic({ systems: hubs ?? [] });
const { open, ...systemCtxProps } = useContextMenuSystemInfoHandlers();
const { loading } = useLoadRoutes();
const { systems: systemStatics, loadSystems, lastUpdateKey } = useLoadSystemStatic({ systems: hubs ?? [] });
const { open, ...systemCtxProps } = useContextMenuSystemInfoHandlers({
outCommand,
hubs,
});
const preparedHubs = useMemo(() => {
return hubs.map(x => {
const sys = getSystemById(systems, x.toString());
return { ...systemStatics.get(parseInt(x))!, ...(sys && { customName: sys.name ?? '' }) };
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hubs, systems, systemStatics, lastUpdateKey]);
const preparedRoutes: Route[] = useMemo(() => {
return (
routesList?.routes
routes?.routes
.sort(sortByDist)
// .filter(x => x.destination.toString() !== systemId)
.filter(x => x.destination.toString() !== systemId)
.map(route => ({
...route,
mapped_systems:
route.systems?.map(solar_system_id =>
routesList?.systems_static_data.find(
routes?.systems_static_data.find(
system_static_data => system_static_data.solar_system_id === solar_system_id,
),
) ?? [],
})) ?? []
);
}, [routesList?.routes, routesList?.systems_static_data, systemId]);
}, [routes?.routes, routes?.systems_static_data, systemId]);
const refData = useRef({ open, loadSystems, preparedRoutes });
refData.current = { open, loadSystems, preparedRoutes };
@@ -86,13 +97,9 @@ export const RoutesWidgetContent = () => {
[handleClick],
);
if (isRestricted && !isSubscriptionActive) {
if (loading) {
return (
<div className="w-full h-full flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
User Routes available with &#39;Active&#39; map subscription only (contact map administrators)
</span>
</div>
<div className="w-full h-full flex justify-center items-center select-none text-center">Loading routes...</div>
);
}
@@ -110,10 +117,12 @@ export const RoutesWidgetContent = () => {
return (
<>
<LoadingWrapper loading={loading}>
{systemId !== undefined && routes && (
<div className={clsx(classes.RoutesGrid, 'px-2 py-2')}>
{preparedRoutes.map(route => {
// TODO do not delete this console log
const sys = preparedHubs.find(x => x.solar_system_id === route.destination)!;
// TODO do not delte this console log
// eslint-disable-next-line no-console
// console.log('JOipP', `Check sys [${route.destination}]:`, sys);
@@ -123,19 +132,15 @@ export const RoutesWidgetContent = () => {
<WdImgButton
className={clsx(PrimeIcons.BARS, classes.RemoveBtn)}
onClick={e => handleClick(e, route.destination.toString())}
tooltip={{
content: 'Click here to open system menu',
position: TooltipPosition.top,
offset: 10,
}}
tooltip={{ content: 'Click here to open system menu', position: TooltipPosition.top, offset: 10 }}
/>
<SystemView
systemId={route.destination.toString()}
<SystemViewStandalone
key={route.destination}
className={clsx('select-none text-center cursor-context-menu')}
hideRegion
compact
showCustomName
{...sys}
/>
</div>
<div className="text-right pl-1">{route.has_connection ? route.systems?.length ?? 2 : ''}</div>
@@ -146,7 +151,7 @@ export const RoutesWidgetContent = () => {
);
})}
</div>
</LoadingWrapper>
)}
<ContextMenuSystemInfo
hubs={hubs}
@@ -160,13 +165,15 @@ export const RoutesWidgetContent = () => {
);
};
type RoutesWidgetCompProps = {
title: ReactNode | string;
};
export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
export const RoutesWidgetComp = () => {
const [routeSettingsVisible, setRouteSettingsVisible] = useState(false);
const { data, update, addHubCommand } = useRouteProvider();
const { data, update } = useRouteProvider();
const {
data: { hubs = [] },
outCommand,
} = useMapRootState();
const preparedHubs = useMemo(() => hubs.map(x => parseInt(x)), [hubs]);
const isSecure = data.path_type === 'secure';
const handleSecureChange = useCallback(() => {
@@ -183,15 +190,24 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
async item => addHubCommand(item.value.toString()),
[addHubCommand],
async item => {
if (preparedHubs.includes(item.value)) {
return;
}
await outCommand({
type: OutCommand.addHub,
data: { system_id: item.value },
});
},
[hubs, outCommand],
);
return (
<Widget
label={
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
<span className="select-none">{title}</span>
<span className="select-none">Routes</span>
<LayoutEventBlocker className="flex items-center gap-2">
<WdImgButton
className={PrimeIcons.PLUS_CIRCLE}
@@ -215,7 +231,6 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
className={PrimeIcons.SLIDERS_H}
onClick={() => setRouteSettingsVisible(true)}
tooltip={{
position: TooltipPosition.top,
content: 'Click here to open Routes settings',
}}
/>
@@ -236,13 +251,10 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
);
};
export const RoutesWidget = forwardRef<RoutesImperativeHandle, RoutesWidgetProps & RoutesWidgetCompProps>(
({ title, ...props }, ref) => {
return (
<RoutesProvider {...props} ref={ref}>
<RoutesWidgetComp title={title} />
</RoutesProvider>
);
},
);
RoutesWidget.displayName = 'RoutesWidget';
export const RoutesWidget = () => {
return (
<RoutesProvider>
<RoutesWidgetComp />
</RoutesProvider>
);
};

View File

@@ -1,8 +1,10 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { OutCommand } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import {
RoutesType,
useRouteProvider,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
@@ -14,25 +16,13 @@ function usePrevious<T>(value: T): T | undefined {
return ref.current;
}
type UseLoadRoutesProps = {
loadRoutesCommand: LoadRoutesCommand;
hubs: string[];
routesList: RoutesList | undefined;
data: RoutesType;
deps?: unknown[];
};
export const useLoadRoutes = ({
data: routesSettings,
loadRoutesCommand,
hubs,
routesList,
deps = [],
}: UseLoadRoutesProps) => {
export const useLoadRoutes = () => {
const [loading, setLoading] = useState(false);
const { data: routesSettings } = useRouteProvider();
const {
data: { selectedSystems, systems, connections },
outCommand,
data: { selectedSystems, hubs, systems, connections },
} = useMapRootState();
const prevSys = usePrevious(systems);
@@ -41,16 +31,17 @@ export const useLoadRoutes = ({
const loadRoutes = useCallback(
(systemId: string, routesSettings: RoutesType) => {
loadRoutesCommand(systemId, routesSettings);
setLoading(true);
outCommand({
type: OutCommand.getRoutes,
data: {
system_id: systemId,
routes_settings: routesSettings,
},
});
},
[loadRoutesCommand],
[outCommand],
);
useEffect(() => {
setLoading(false);
}, [routesList]);
useEffect(() => {
if (selectedSystems.length !== 1) {
return;
@@ -70,8 +61,7 @@ export const useLoadRoutes = ({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
.map(x => routesSettings[x]),
...deps,
]);
return { loading, loadRoutes, setLoading };
return { loading, loadRoutes };
};

View File

@@ -1,24 +0,0 @@
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
export type LoadRoutesCommand = (systemId: string, routesSettings: RoutesType) => Promise<void>;
export type AddHubCommand = (systemId: string) => Promise<void>;
export type ToggleHubCommand = (systemId: string) => Promise<void>;
export type RoutesWidgetProps = {
data: RoutesType;
update: (d: RoutesType) => void;
hubs: string[];
routesList: RoutesList | undefined;
loading: boolean;
addHubCommand: AddHubCommand;
toggleHubCommand: ToggleHubCommand;
isRestricted?: boolean;
};
export type RoutesProviderInnerProps = RoutesWidgetProps;
export type RoutesImperativeHandle = {
stopLoading: () => void;
};

View File

@@ -1,24 +1,24 @@
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { LayoutEventBlocker, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { LayoutEventBlocker, SystemView, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { SystemInfoContent } from './SystemInfoContent';
import { PrimeIcons } from 'primereact/api';
import { useCallback, useState } from 'react';
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 },
data: { selectedSystems, systems },
} = useMapRootState();
const [systemId] = selectedSystems;
const systemStaticInfo = getSystemStaticInfo(systemId)!;
const { solar_system_name: solarSystemName } = systemStaticInfo || {};
const sys = getSystemById(systems, systemId)!;
const { solar_system_name: solarSystemName } = sys?.system_static_info || {};
const isNotSelectedSystem = selectedSystems.length !== 1;
@@ -42,7 +42,7 @@ export const SystemInfo = () => {
<WdImgButton
className="pi pi-pen-to-square"
onClick={() => setVisible(true)}
tooltip={{ position: TooltipPosition.top, content: 'Edit system name and description' }}
tooltip={{ content: 'Edit system name and description' }}
/>
</LayoutEventBlocker>
</div>

View File

@@ -3,7 +3,6 @@ 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;
@@ -15,9 +14,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 } = systemStaticInfo || {};
const { system_class, region_name, constellation_name, statics, effect_name, effect_power } =
sys.system_static_info || {};
const isWH = isWormholeSpace(system_class);
const sortedStatics = useMemo(() => sortWHClasses(wormholesData, statics), [wormholesData, statics]);

View File

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

@@ -0,0 +1,37 @@
// Custom scrollbar styling is now handled by the global custom-scrollbar class
.scrollerContent {
overflow-x: hidden;
overflow-y: auto;
padding-right: 8px;
padding-left: 8px;
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,26 +1,26 @@
import { useMemo } from 'react';
import React, { useMemo } from 'react';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { VirtualScroller } from 'primereact/virtualscroller';
import { useSystemKillsItemTemplate } from '../hooks/useSystemKillsItemTemplate';
import clsx from 'clsx';
import { WithClassName } from '@/hooks/Mapper/types/common.ts';
import classes from './SystemKillsContent.module.scss';
export const KILLS_ROW_HEIGHT = 40;
export const ITEM_HEIGHT = 35;
export type SystemKillsContentProps = {
export interface SystemKillsContentProps {
kills: DetailedKill[];
systemNameMap: Record<string, string>;
onlyOneSystem?: boolean;
timeRange?: number;
limit?: number;
} & WithClassName;
}
export const SystemKillsList = ({
export const SystemKillsContent: React.FC<SystemKillsContentProps> = ({
kills,
systemNameMap,
onlyOneSystem = false,
timeRange = 4,
limit,
className,
}: SystemKillsContentProps) => {
}) => {
const processedKills = useMemo(() => {
if (!kills || kills.length === 0) return [];
@@ -47,17 +47,30 @@ export const SystemKillsList = ({
return filteredKills;
}, [kills, timeRange, limit]);
const itemTemplate = useSystemKillsItemTemplate(onlyOneSystem);
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
};
return (
<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,
)}
/>
<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>
);
};
export default SystemKillsContent;

View File

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

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import React from 'react';
import clsx from 'clsx';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import {
@@ -14,15 +14,14 @@ 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 type CompactKillRowProps = {
export interface CompactKillRowProps {
killDetails: DetailedKill;
systemName: string;
onlyOneSystem: boolean;
} & WithClassName;
}
export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, className }: CompactKillRowProps) => {
export const KillRowDetail: React.FC<CompactKillRowProps> = ({ killDetails, systemName, onlyOneSystem }) => {
const {
killmail_id = 0,
// Victim data
@@ -81,24 +80,13 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
'Victim',
);
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,
],
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } = getAttackerPrimaryImageAndTooltip(
attackerIsNpc,
attackerAllianceLogoUrl,
attackerCorpLogoUrl,
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.
@@ -112,8 +100,6 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
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 */}
@@ -156,14 +142,12 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
{victim_char_name}
<span className="text-stone-400"> / {victimAffiliationTicker}</span>
</div>
<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>
<div className="truncate text-stone-300">
{victim_ship_name}
{killValueFormatted && (
<>
<span className="text-stone-400">/</span>
<span className="text-green-400">{killValueFormatted}</span>
<span className="ml-1 text-stone-400">/</span>
<span className="ml-1 text-green-400">{killValueFormatted}</span>
</>
)}
</div>

View File

@@ -0,0 +1,15 @@
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,9 +1,10 @@
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
import debounce from 'lodash.debounce';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { Commands, 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;
@@ -28,6 +29,10 @@ 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;
@@ -36,6 +41,32 @@ 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,9 +4,10 @@ import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { KillItemTemplate } from '../components/KillItemTemplate';
export function useSystemKillsItemTemplate(onlyOneSystem: boolean) {
export function useSystemKillsItemTemplate(systemNameMap: Record<string, string>, onlyOneSystem: boolean) {
return useCallback(
(kill: DetailedKill, options: VirtualScrollerTemplateOptions) => KillItemTemplate(onlyOneSystem, kill, options),
[onlyOneSystem],
(kill: DetailedKill, options: VirtualScrollerTemplateOptions) =>
KillItemTemplate(systemNameMap, onlyOneSystem, kill, options),
[systemNameMap, onlyOneSystem],
);
}

View File

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

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