mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-11-03 07:57:05 +00:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e692b5805 | ||
|
|
01b7370ecd | ||
|
|
20ad8b07d7 | ||
|
|
cab1880fb0 | ||
|
|
78eefcd6a7 | ||
|
|
eec78d38a8 | ||
|
|
73f8b1f06b | ||
|
|
f96cb01860 | ||
|
|
6800be1bb6 | ||
|
|
143f0a5b3a | ||
|
|
b6495504f8 | ||
|
|
2f07ec1b74 | ||
|
|
7073a0e8e6 | ||
|
|
bb0d91a3c7 | ||
|
|
1cb12b97ba | ||
|
|
860d20dc66 | ||
|
|
a850071965 | ||
|
|
fc41573e70 | ||
|
|
97f1808fb5 | ||
|
|
d31046eebb | ||
|
|
a70fa50eab | ||
|
|
9a082c26f5 | ||
|
|
6af2dc1ed5 | ||
|
|
5fd1509d44 | ||
|
|
2448c0531b | ||
|
|
b685ea1013 | ||
|
|
55465688c8 | ||
|
|
ac3c7e0c44 | ||
|
|
2d6ab5646c | ||
|
|
67b373ac29 | ||
|
|
678169e6fa | ||
|
|
7ee3c8db82 | ||
|
|
304f4b01ab | ||
|
|
4af12c21b2 | ||
|
|
497da1e5f7 | ||
|
|
5bd968acae | ||
|
|
f74c20142c | ||
|
|
d4c40d7542 | ||
|
|
04f3fec0c0 | ||
|
|
cd0b4b0fc9 | ||
|
|
e7b115e6e6 | ||
|
|
dff8fc6396 | ||
|
|
afdaeb3d34 | ||
|
|
ac6053361e | ||
|
|
eb3e1ba3aa | ||
|
|
8468a9b5de | ||
|
|
5eafe59dcb | ||
|
|
b38bcaa8cf | ||
|
|
8a238a447d | ||
|
|
3731219216 | ||
|
|
73d5fd5f67 | ||
|
|
e8e4aed6d5 | ||
|
|
63571a462f | ||
|
|
606add4142 | ||
|
|
dac480b059 | ||
|
|
5f67cb1dd7 | ||
|
|
5886fff753 | ||
|
|
da2e12bdd1 | ||
|
|
05c3d20e56 | ||
|
|
4633d26517 | ||
|
|
30b0556d47 | ||
|
|
e094378dc5 | ||
|
|
0c48189503 | ||
|
|
a5c346627a | ||
|
|
4e526040bf | ||
|
|
869c25cd60 | ||
|
|
6aac698cd8 | ||
|
|
230016b90f | ||
|
|
4b1aef8dd9 | ||
|
|
d34509d7a0 | ||
|
|
fca98ec232 | ||
|
|
e2814e95bd | ||
|
|
68a3f84704 | ||
|
|
4bc76feefc | ||
|
|
da39a55fd0 | ||
|
|
ee3cf04cd4 | ||
|
|
d79e7fe2ff | ||
|
|
8de9fdef32 | ||
|
|
f51deeec2d | ||
|
|
a971c69a96 |
@@ -7,3 +7,4 @@ export EVE_CLIENT_WITH_WALLET_SECRET="<EVE_CLIENT_WITH_WALLET_SECRET>"
|
||||
export GIT_SHA="1111"
|
||||
export WANDERER_INVITES="false"
|
||||
export WANDERER_PUBLIC_API_DISABLED="false"
|
||||
export WANDERER_ZKILL_PRELOAD_DISABLED="false"
|
||||
|
||||
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -140,6 +140,7 @@ jobs:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm64/v8
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
|
||||
302
CHANGELOG.md
302
CHANGELOG.md
@@ -2,6 +2,308 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.47.0](https://github.com/wanderer-industries/wanderer/compare/v1.46.1...v1.47.0) (2025-02-09)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Map: Added check for active map subscription to using Map APIs
|
||||
|
||||
## [v1.46.1](https://github.com/wanderer-industries/wanderer/compare/v1.46.0...v1.46.1) (2025-02-09)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed a lot of design and architect issues after last milli⦠(#154)
|
||||
|
||||
* Map: Fixed a lot of design and architect issues after last million PRs
|
||||
|
||||
* Map: removed unnecessary hooks styles
|
||||
|
||||
## [v1.46.0](https://github.com/wanderer-industries/wanderer/compare/v1.45.5...v1.46.0) (2025-02-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Added WANDERER_RESTRICT_MAPS_CREATION env support
|
||||
|
||||
## [v1.45.5](https://github.com/wanderer-industries/wanderer/compare/v1.45.4...v1.45.5) (2025-02-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* restore styling for local characters list (#152)
|
||||
|
||||
## [v1.45.4](https://github.com/wanderer-industries/wanderer/compare/v1.45.3...v1.45.4) (2025-02-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* remove snap to grid customization (#153)
|
||||
|
||||
## [v1.45.3](https://github.com/wanderer-industries/wanderer/compare/v1.45.2...v1.45.3) (2025-02-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* color and formatting fixes for local character (#150)
|
||||
|
||||
## [v1.45.2](https://github.com/wanderer-industries/wanderer/compare/v1.45.1...v1.45.2) (2025-02-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* fix route list hover and on the map character list (#149)
|
||||
|
||||
* correct formatting for on the map character list
|
||||
|
||||
* fix hover for route list
|
||||
|
||||
## [v1.45.1](https://github.com/wanderer-industries/wanderer/compare/v1.45.0...v1.45.1) (2025-02-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* kill count subscript position on firefox, and remove kill filter for single system (#148)
|
||||
|
||||
## [v1.45.0](https://github.com/wanderer-industries/wanderer/compare/v1.44.9...v1.45.0) (2025-02-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* allow filtering of k-space kills (#147)
|
||||
|
||||
## [v1.44.9](https://github.com/wanderer-industries/wanderer/compare/v1.44.8...v1.44.9) (2025-02-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* improve local character header shrink behavior (#146)
|
||||
|
||||
## [v1.44.8](https://github.com/wanderer-industries/wanderer/compare/v1.44.7...v1.44.8) (2025-02-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: include external libraries in build
|
||||
|
||||
## [v1.44.7](https://github.com/wanderer-industries/wanderer/compare/v1.44.6...v1.44.7) (2025-02-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: include external libraries in build
|
||||
|
||||
## [v1.44.6](https://github.com/wanderer-industries/wanderer/compare/v1.44.5...v1.44.6) (2025-02-04)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.44.5](https://github.com/wanderer-industries/wanderer/compare/v1.44.4...v1.44.5) (2025-02-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* include category param in search cache key (#144)
|
||||
|
||||
## [v1.44.4](https://github.com/wanderer-industries/wanderer/compare/v1.44.3...v1.44.4) (2025-02-02)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.44.3](https://github.com/wanderer-industries/wanderer/compare/v1.44.2...v1.44.3) (2025-02-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* restored kills lightning bolt functionality (#143)
|
||||
|
||||
## [v1.44.2](https://github.com/wanderer-industries/wanderer/compare/v1.44.1...v1.44.2) (2025-02-02)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.44.1](https://github.com/wanderer-industries/wanderer/compare/v1.44.0...v1.44.1) (2025-02-01)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed problem with windows. (#140)
|
||||
|
||||
* Map: Fixed problem with windows.
|
||||
|
||||
* Core: Added min heigth for body
|
||||
|
||||
## [v1.44.0](https://github.com/wanderer-industries/wanderer/compare/v1.43.9...v1.44.0) (2025-02-01)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* add news post for zkill widget
|
||||
|
||||
* add zkill widget
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* design feedback patch
|
||||
|
||||
* removed unneeded event handler
|
||||
|
||||
## [v1.43.9](https://github.com/wanderer-industries/wanderer/compare/v1.43.8...v1.43.9) (2025-01-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Add discord link to 'Like' icon on main interface
|
||||
|
||||
## [v1.43.8](https://github.com/wanderer-industries/wanderer/compare/v1.43.7...v1.43.8) (2025-01-26)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Update shuttered constellations (required EVE DB data update on server).
|
||||
|
||||
## [v1.43.7](https://github.com/wanderer-industries/wanderer/compare/v1.43.6...v1.43.7) (2025-01-26)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.43.6](https://github.com/wanderer-industries/wanderer/compare/v1.43.5...v1.43.6) (2025-01-22)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Widgets: Fix widgets not visible on map
|
||||
|
||||
## [v1.43.5](https://github.com/wanderer-industries/wanderer/compare/v1.43.4...v1.43.5) (2025-01-22)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Audit: Fix signature added/removed system name
|
||||
|
||||
## [v1.43.4](https://github.com/wanderer-industries/wanderer/compare/v1.43.3...v1.43.4) (2025-01-21)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* improve structure widget styling (#127)
|
||||
|
||||
## [v1.43.3](https://github.com/wanderer-industries/wanderer/compare/v1.43.2...v1.43.3) (2025-01-21)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.43.2](https://github.com/wanderer-industries/wanderer/compare/v1.43.1...v1.43.2) (2025-01-21)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* prevent constraint error for follow/toggle (#132)
|
||||
|
||||
## [v1.43.1](https://github.com/wanderer-industries/wanderer/compare/v1.43.0...v1.43.1) (2025-01-20)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.43.0](https://github.com/wanderer-industries/wanderer/compare/v1.42.5...v1.43.0) (2025-01-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* add news post for structures widget (#131)
|
||||
|
||||
## [v1.42.5](https://github.com/wanderer-industries/wanderer/compare/v1.42.4...v1.42.5) (2025-01-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix link signatures on splash. Fix deleting connection on locked system remove.
|
||||
|
||||
## [v1.42.4](https://github.com/wanderer-industries/wanderer/compare/v1.42.3...v1.42.4) (2025-01-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Fix system statics list (required EVE DB data update). Add system name to signature added/removed audit log
|
||||
|
||||
## [v1.42.3](https://github.com/wanderer-industries/wanderer/compare/v1.42.2...v1.42.3) (2025-01-17)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* change structure tooltip to avoid paste confusion (#125)
|
||||
|
||||
* change structure tooltip to avoid paste confusion
|
||||
|
||||
* clarify use of evetime and use primereact calendar
|
||||
|
||||
## [v1.42.2](https://github.com/wanderer-industries/wanderer/compare/v1.42.1...v1.42.2) (2025-01-16)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.42.1](https://github.com/wanderer-industries/wanderer/compare/v1.42.0...v1.42.1) (2025-01-16)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Remove linked sig ID if system containing signature removed from map
|
||||
|
||||
## [v1.42.0](https://github.com/wanderer-industries/wanderer/compare/v1.41.0...v1.42.0) (2025-01-16)
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ Now you can visit [`localhost:8000`](http://localhost:8000) from your browser.
|
||||
- `root@0d0a785313b6:/app# apt update`
|
||||
- `root@0d0a785313b6:/app# curl -sL https://deb.nodesource.com/setup_18.x | bash -`
|
||||
- `root@0d0a785313b6:/app# apt-get install nodejs inotify-tools -y`
|
||||
- `root@0d0a785313b6:/app# npm install -g yarn`
|
||||
- `root@0d0a785313b6:/app# mix setup`
|
||||
|
||||
- See how to run server in #Run section
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// import '@fontsource-variable/inter'
|
||||
// import '@fontsource-variable/jetbrains-mono'
|
||||
// import './lib/tailwind/index.css';
|
||||
import './css/app.css';
|
||||
|
||||
import './lib/phoenix';
|
||||
|
||||
@@ -25,6 +25,10 @@ body {
|
||||
width: 400px; /* As IE6 ignores !important it will set width as 400px; */
|
||||
}
|
||||
|
||||
body > div:first-of-type {
|
||||
min-height: 500px !important;
|
||||
}
|
||||
|
||||
.lending-normal {
|
||||
font-family: 'Shentox', 'Rogan', sans-serif !important;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -112,3 +112,28 @@
|
||||
.p-autocomplete .p-autocomplete-multiple-container .p-autocomplete-token {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Fixed sizes of Input switch */
|
||||
.p-inputswitch {
|
||||
width: 2.0rem;
|
||||
height: 1.15rem;
|
||||
|
||||
.p-inputswitch-slider:before {
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
left: 0.14rem;
|
||||
margin-top: -0.385rem;
|
||||
}
|
||||
|
||||
&.p-highlight .p-inputswitch-slider:before {
|
||||
transform: translateX(0.8rem);
|
||||
}
|
||||
|
||||
&:not(.p-disabled):has(.p-inputswitch-input:hover) .p-inputswitch-slider {
|
||||
background: rgb(255 255 255 / 21%);
|
||||
}
|
||||
|
||||
&.p-highlight .p-inputswitch-slider {
|
||||
background: #966d3d;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './useSystemInfo';
|
||||
export * from './useGetOwnOnlineCharacters';
|
||||
export * from './useElementWidth';
|
||||
|
||||
43
assets/js/hooks/Mapper/components/hooks/useElementWidth.ts
Normal file
43
assets/js/hooks/Mapper/components/hooks/useElementWidth.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useState, useLayoutEffect, RefObject } from 'react';
|
||||
|
||||
/**
|
||||
* useElementWidth
|
||||
*
|
||||
* A custom hook that accepts a ref to an HTML element and returns its current width.
|
||||
* It uses a ResizeObserver and window resize listener to update the width when necessary.
|
||||
*
|
||||
* @param ref - A RefObject pointing to an HTML element.
|
||||
* @returns The current width of the element.
|
||||
*/
|
||||
export function useElementWidth<T extends HTMLElement>(ref: RefObject<T>): number {
|
||||
const [width, setWidth] = useState<number>(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const updateWidth = () => {
|
||||
if (ref.current) {
|
||||
const newWidth = ref.current.getBoundingClientRect().width;
|
||||
if (newWidth > 0) {
|
||||
setWidth(newWidth);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateWidth(); // Initial measurement
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
const id = setTimeout(updateWidth, 100);
|
||||
return () => clearTimeout(id);
|
||||
});
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
window.addEventListener("resize", updateWidth);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("resize", updateWidth);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
return width;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo }
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Edge,
|
||||
EdgeChange,
|
||||
MiniMap,
|
||||
Node,
|
||||
NodeChange,
|
||||
@@ -83,6 +84,7 @@ interface MapCompProps {
|
||||
onCommand: OutCommandHandler;
|
||||
onSelectionChange: OnMapSelectionChange;
|
||||
onManualDelete(systems: string[]): void;
|
||||
canRemoveConnection?(connectionId: string): boolean;
|
||||
onConnectionInfoClick?(e: SolarSystemConnection): void;
|
||||
onAddSystem?: OnMapAddSystemCallback;
|
||||
onSelectionContextMenu?: NodeSelectionMouseHandler;
|
||||
@@ -112,8 +114,9 @@ const MapComp = ({
|
||||
isSoftBackground,
|
||||
theme,
|
||||
onAddSystem,
|
||||
canRemoveConnection,
|
||||
}: MapCompProps) => {
|
||||
const { getNode, getNodes } = useReactFlow();
|
||||
const { getEdge, getNode, getNodes } = useReactFlow();
|
||||
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
|
||||
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
|
||||
|
||||
@@ -125,7 +128,6 @@ const MapComp = ({
|
||||
const { variant, gap, size, color } = useBackgroundVars(theme);
|
||||
const { isPanAndDrag, nodeComponent, connectionMode } = getBehaviorForTheme(theme || 'default');
|
||||
|
||||
// You can create nodeTypes dynamically based on the node component
|
||||
const nodeTypes = useMemo(() => {
|
||||
return {
|
||||
custom: nodeComponent,
|
||||
@@ -222,6 +224,40 @@ const MapComp = ({
|
||||
[getNode, getNodes, onManualDelete, onNodesChange],
|
||||
);
|
||||
|
||||
const handleEdgesChange = useCallback(
|
||||
(changes: EdgeChange[]) => {
|
||||
const nextChanges = changes.reduce((acc, change) => {
|
||||
if (change.type !== 'remove') {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
if (canRemoveConnection?.(change.id)) {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
const edge = getEdge(change.id);
|
||||
if (!edge) {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
const sourceNode = getNode(edge.source);
|
||||
const targetNode = getNode(edge.target);
|
||||
if (!sourceNode || !targetNode) {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
if (sourceNode.data.locked || targetNode.data.locked) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [...acc, change];
|
||||
}, [] as EdgeChange[]);
|
||||
|
||||
onEdgesChange(nextChanges);
|
||||
},
|
||||
[canRemoveConnection, getEdge, getNode, onEdgesChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
update(x => ({
|
||||
...x,
|
||||
@@ -237,7 +273,7 @@ const MapComp = ({
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
onConnect={onConnect}
|
||||
// TODO we need save into session all of this
|
||||
// and on any action do either
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { MapUnionTypes } from '@/hooks/Mapper/types';
|
||||
import { MapUnionTypes, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
|
||||
|
||||
export type MapData = MapUnionTypes & {
|
||||
@@ -30,10 +30,14 @@ const INITIAL_DATA: MapData = {
|
||||
isConnecting: false,
|
||||
connections: [],
|
||||
hoverNodeId: null,
|
||||
linkedSigEveId: '',
|
||||
visibleNodes: new Set(),
|
||||
showKSpaceBG: false,
|
||||
isThickConnections: false,
|
||||
userPermissions: {},
|
||||
systemSignatures: {} as Record<string, SystemSignature[]>,
|
||||
options: {} as Record<string, string | boolean>,
|
||||
is_subscription_active: false,
|
||||
};
|
||||
|
||||
export interface MapContextProps {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
.KillsBookmark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
border: 0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
padding: 4px 3px;
|
||||
|
||||
}
|
||||
|
||||
.KillsBookmarkWithIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: -2px;
|
||||
text-shadow: 0 0 3px #000;
|
||||
padding-right: 2px;
|
||||
height: 8px;
|
||||
font-size: 8px;
|
||||
line-height: 12px;
|
||||
font-weight: 700;
|
||||
text-size-adjust: 100%;
|
||||
|
||||
.pi {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 9px;
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.TooltipContainer {
|
||||
background-color: #1a1a1a;
|
||||
color: #fff;
|
||||
padding: 3px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 2px;
|
||||
pointer-events: auto;
|
||||
max-width: 500px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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.ts';
|
||||
|
||||
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
|
||||
|
||||
type KillsBookmarkTooltipProps = {
|
||||
killsCount: number;
|
||||
killsActivityType: string | null;
|
||||
systemId: string;
|
||||
className?: string;
|
||||
size?: TooltipSize;
|
||||
} & WithChildren &
|
||||
WithClassName;
|
||||
|
||||
export const KillsCounter = ({ killsCount, systemId, className, children, size = 'xs' }: KillsBookmarkTooltipProps) => {
|
||||
const { isLoading, kills: detailedKills, systemNameMap } = useKillsCounter({ realSystemId: systemId });
|
||||
|
||||
if (!killsCount || detailedKills.length === 0 || !systemId || isLoading) return null;
|
||||
|
||||
const tooltipContent = (
|
||||
<SystemKillsContent kills={detailedKills} systemNameMap={systemNameMap} compact={true} onlyOneSystem={true} />
|
||||
);
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<WdTooltipWrapper content={tooltipContent} className={className} size={size} interactive={true}>
|
||||
{children}
|
||||
</WdTooltipWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
.TooltipActive {
|
||||
pointer-events: auto !important;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.localCounter {
|
||||
mix-blend-mode: screen;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
color: var(--rf-node-local-counter);
|
||||
|
||||
&.hasUserCharacters {
|
||||
color: var(--rf-has-user-characters);
|
||||
}
|
||||
|
||||
& > i {
|
||||
font-size: 9px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-weight: var(--rf-local-counter-font-weight, 500);
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Pathfinder {
|
||||
.localCounter {
|
||||
@-moz-document url-prefix() {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
& > span {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
|
||||
import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widgets/LocalCharacters/components';
|
||||
import { useLocalCharactersItemTemplate } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters';
|
||||
import { useLocalCharacterWidgetSettings } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalWidgetSettings';
|
||||
import classes from './SolarSystemLocalCounter.module.scss';
|
||||
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useTheme } from '@/hooks/Mapper/hooks/useTheme.ts';
|
||||
|
||||
interface LocalCounterProps {
|
||||
localCounterCharacters: Array<CharItemProps>;
|
||||
hasUserCharacters: boolean;
|
||||
showIcon?: boolean;
|
||||
}
|
||||
|
||||
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
|
||||
const [settings] = useLocalCharacterWidgetSettings();
|
||||
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
|
||||
const theme = useTheme();
|
||||
|
||||
const pilotTooltipContent = useMemo(() => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '300px',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
height: '300px',
|
||||
}}
|
||||
>
|
||||
<LocalCharactersList items={localCounterCharacters} itemTemplate={itemTemplate} itemSize={26} />
|
||||
</div>
|
||||
);
|
||||
}, [localCounterCharacters, itemTemplate]);
|
||||
|
||||
if (localCounterCharacters.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.TooltipActive, {
|
||||
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
|
||||
})}
|
||||
>
|
||||
<WdTooltipWrapper
|
||||
// @ts-ignore
|
||||
content={pilotTooltipContent}
|
||||
position={TooltipPosition.right}
|
||||
offset={0}
|
||||
>
|
||||
<div
|
||||
className={clsx(classes.localCounter, {
|
||||
[classes.hasUserCharacters]: hasUserCharacters,
|
||||
})}
|
||||
>
|
||||
{showIcon && <i className="pi pi-users" />}
|
||||
<span>{localCounterCharacters.length}</span>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,34 +2,30 @@
|
||||
|
||||
$pastel-blue: #5a7d9a;
|
||||
$pastel-pink: #d291bc;
|
||||
$pastel-green: #88b04b;
|
||||
$pastel-yellow: #ffdd59;
|
||||
$dark-bg: #2d2d2d;
|
||||
$text-color: #ffffff;
|
||||
$tooltip-bg: #202020;
|
||||
|
||||
$node-bg-color: #202020;
|
||||
$node-soft-bg-color: #202020;
|
||||
$text-color: #ffffff;
|
||||
$tag-color: #38BDF8;
|
||||
$region-name: #D6D3D1;
|
||||
$custom-name: #93C5FD;
|
||||
|
||||
.RootCustomNode {
|
||||
display: flex;
|
||||
width: 130px;
|
||||
height: 34px;
|
||||
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
|
||||
flex-direction: column;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
|
||||
background-color: $tooltip-bg;
|
||||
background-color: var(--rf-node-bg-color, #202020) !important;
|
||||
color: var(--rf-text-color, #ffffff);
|
||||
|
||||
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
|
||||
border: 1px solid darken($pastel-blue, 10%);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
|
||||
&.Mataria,
|
||||
@@ -92,19 +88,11 @@ $custom-name: #93C5FD;
|
||||
box-shadow: 0 0 10px #9a1af1c2;
|
||||
}
|
||||
|
||||
&.tooltip {
|
||||
background-color: $tooltip-bg;
|
||||
color: $text-color;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $pastel-pink;
|
||||
}
|
||||
|
||||
&.eve-system-status-home {
|
||||
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-home),
|
||||
45deg,
|
||||
var(--eve-solar-system-status-color-background),
|
||||
transparent
|
||||
);
|
||||
&.selected {
|
||||
@@ -178,8 +166,6 @@ $custom-name: #93C5FD;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
|
||||
//background-color: #833ca4;
|
||||
|
||||
&:not(:first-child) {
|
||||
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@@ -266,26 +252,18 @@ $custom-name: #93C5FD;
|
||||
|
||||
.TagTitle {
|
||||
font-size: 11px;
|
||||
font-weight: medium;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
|
||||
|
||||
color: var(--rf-tag-color, #38BDF8);
|
||||
}
|
||||
|
||||
/* Firefox kostyl */
|
||||
@-moz-document url-prefix() {
|
||||
.classSystemName {
|
||||
font-family: inherit !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.classSystemName {
|
||||
//font-weight: bold;
|
||||
}
|
||||
|
||||
.solarSystemName {
|
||||
}
|
||||
}
|
||||
|
||||
.BottomRow {
|
||||
@@ -294,22 +272,23 @@ $custom-name: #93C5FD;
|
||||
align-items: center;
|
||||
height: 19px;
|
||||
|
||||
.localCounter {
|
||||
display: flex;
|
||||
//align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
& > i {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
.hasLocalCounter {
|
||||
margin-right: 2px;
|
||||
&.countAbove9 {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-weight: 500;
|
||||
//margin-top: 1px;
|
||||
}
|
||||
.lockIcon {
|
||||
font-size: 0.45rem;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mapMarker {
|
||||
font-size: 0.45rem;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +319,8 @@ $custom-name: #93C5FD;
|
||||
|
||||
.Handlers {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
@@ -353,6 +333,7 @@ $custom-name: #93C5FD;
|
||||
border: 1px solid $pastel-blue;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
pointer-events: auto;
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
@@ -395,3 +376,4 @@ $custom-name: #93C5FD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { memo } from 'react';
|
||||
import { MapSolarSystemType } from '../../map.types';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
import classes from './SolarSystemNodeDefault.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
|
||||
import { useLocalCounter, useSolarSystemNode } from '../../hooks/useSolarSystemLogic';
|
||||
import {
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
STATUS_CLASSES,
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
} from '@/hooks/Mapper/components/map/constants';
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { LocalCounter } from './SolarSystemLocalCounter';
|
||||
import { KillsCounter } from './SolarSystemKillsCounter';
|
||||
|
||||
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
const { localCounterCharacters } = useLocalCounter(nodeVars);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -22,7 +25,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
<div className={classes.Bookmarks}>
|
||||
{nodeVars.labelCustom !== '' && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -32,13 +35,19 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.killsCount && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}>
|
||||
{nodeVars.killsCount && nodeVars.killsCount > 0 && nodeVars.solarSystemId && (
|
||||
<KillsCounter
|
||||
killsCount={nodeVars.killsCount}
|
||||
systemId={nodeVars.solarSystemId}
|
||||
size="lg"
|
||||
killsActivityType={nodeVars.killsActivityType}
|
||||
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
|
||||
>
|
||||
<div className={clsx(classes.BookmarkWithIcon)}>
|
||||
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
||||
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</KillsCounter>
|
||||
)}
|
||||
|
||||
{nodeVars.labelsInfo.map(x => (
|
||||
@@ -53,11 +62,10 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
className={clsx(
|
||||
classes.RootCustomNode,
|
||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||
classes[STATUS_CLASSES[nodeVars.status]],
|
||||
{
|
||||
[classes.selected]: nodeVars.selected,
|
||||
},
|
||||
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
|
||||
{ [classes.selected]: nodeVars.selected },
|
||||
)}
|
||||
onMouseDownCapture={e => nodeVars.dbClick(e)}
|
||||
>
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
@@ -88,7 +96,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
{nodeVars.isWormhole && (
|
||||
<div className={classes.statics}>
|
||||
{nodeVars.sortedStatics.map(whClass => (
|
||||
<WormholeClassComp key={whClass} id={whClass} />
|
||||
<WormholeClassComp key={String(whClass)} id={String(whClass)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -113,27 +121,18 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex gap-1 items-center">
|
||||
{nodeVars.locked && (
|
||||
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
|
||||
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{nodeVars.charactersInSystem.length > 0 && (
|
||||
<div
|
||||
className={clsx(classes.localCounter, {
|
||||
['text-amber-300']: nodeVars.hasUserCharacters,
|
||||
})}
|
||||
>
|
||||
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
|
||||
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<div className={clsx('flex items-center gap-1')}>
|
||||
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
|
||||
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LocalCounter
|
||||
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||
localCounterCharacters={localCounterCharacters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -145,7 +144,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -153,14 +152,14 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
|
||||
<div className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
|
||||
@@ -1,91 +1,20 @@
|
||||
@import './SolarSystemNodeDefault.module.scss';
|
||||
|
||||
/* ---------------------------
|
||||
Only override what's different
|
||||
--------------------------- */
|
||||
/* ---------------------------------------------
|
||||
Only override what's different from the base
|
||||
Currently none required
|
||||
---------------------------------------------- */
|
||||
|
||||
/* 1) .RootCustomNode:
|
||||
- new background-color using CSS var
|
||||
- plus color, font-family, and font-weight */
|
||||
.RootCustomNode {
|
||||
background-color: var(--rf-node-bg-color, #202020) !important;
|
||||
color: var(--rf-text-color, #ffffff);
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
|
||||
/* 2) .Bookmarks:
|
||||
- add var-based font family/weight
|
||||
*/
|
||||
.Bookmarks {
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
|
||||
/* 3) .HeadRow, .classTitle, .classSystemName:
|
||||
- add new references to var-based font family/weight
|
||||
*/
|
||||
.HeadRow {
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
|
||||
.classTitle {
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
.classSystemName {
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.classSystemName {
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 4) .BottomRow:
|
||||
- introduces .tagTitle, .regionName, .customName, .localCounter
|
||||
referencing new CSS variables */
|
||||
.BottomRow {
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
|
||||
.tagTitle {
|
||||
font-size: 11px;
|
||||
font-weight: medium;
|
||||
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
color: var(--rf-tag-color, #38BDF8);
|
||||
}
|
||||
|
||||
.regionName {
|
||||
color: var(--rf-region-name, #D6D3D1);
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
|
||||
.customName {
|
||||
color: var(--rf-custom-name, #93C5FD);
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
|
||||
.localCounter {
|
||||
display: flex;
|
||||
color: var(--rf-has-user-characters, #fbbf24);
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
gap: 2px;
|
||||
|
||||
.hasUserCharacters {
|
||||
color: var(--rf-has-user-characters, #fbbf24);
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
&.eve-system-status-home {
|
||||
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-home),
|
||||
transparent
|
||||
);
|
||||
&.selected {
|
||||
border-color: var(--eve-solar-system-status-color-home);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { memo } from 'react';
|
||||
import { MapSolarSystemType } from '../../map.types';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
import classes from './SolarSystemNodeTheme.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
|
||||
import { useLocalCounter, useSolarSystemNode } from '../../hooks/useSolarSystemLogic';
|
||||
import {
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
STATUS_CLASSES,
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
} from '@/hooks/Mapper/components/map/constants';
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { LocalCounter } from './SolarSystemLocalCounter';
|
||||
import { KillsCounter } from './SolarSystemKillsCounter';
|
||||
|
||||
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
const { localCounterCharacters } = useLocalCounter(nodeVars);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -22,7 +25,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
<div className={classes.Bookmarks}>
|
||||
{nodeVars.labelCustom !== '' && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -32,13 +35,19 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.killsCount && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}>
|
||||
{nodeVars.killsCount && nodeVars.killsCount > 0 && nodeVars.solarSystemId && (
|
||||
<KillsCounter
|
||||
killsCount={nodeVars.killsCount}
|
||||
systemId={nodeVars.solarSystemId}
|
||||
size="lg"
|
||||
killsActivityType={nodeVars.killsActivityType}
|
||||
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
|
||||
>
|
||||
<div className={clsx(classes.BookmarkWithIcon)}>
|
||||
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
||||
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</KillsCounter>
|
||||
)}
|
||||
|
||||
{nodeVars.labelsInfo.map(x => (
|
||||
@@ -53,11 +62,10 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
className={clsx(
|
||||
classes.RootCustomNode,
|
||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||
classes[STATUS_CLASSES[nodeVars.status]],
|
||||
{
|
||||
[classes.selected]: nodeVars.selected,
|
||||
},
|
||||
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
|
||||
{ [classes.selected]: nodeVars.selected },
|
||||
)}
|
||||
onMouseDownCapture={e => nodeVars.dbClick(e)}
|
||||
>
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
@@ -88,7 +96,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
{nodeVars.isWormhole && (
|
||||
<div className={classes.statics}>
|
||||
{nodeVars.sortedStatics.map(whClass => (
|
||||
<WormholeClassComp key={whClass} id={whClass} />
|
||||
<WormholeClassComp key={String(whClass)} id={String(whClass)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -123,27 +131,18 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex gap-1 items-center">
|
||||
{nodeVars.locked && (
|
||||
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
|
||||
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{nodeVars.charactersInSystem.length > 0 && (
|
||||
<div
|
||||
className={clsx(classes.localCounter, {
|
||||
[classes.hasUserCharacters]: nodeVars.hasUserCharacters,
|
||||
})}
|
||||
>
|
||||
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
|
||||
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<div className={clsx('flex items-center gap-1')}>
|
||||
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
|
||||
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LocalCounter
|
||||
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||
localCounterCharacters={localCounterCharacters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -155,7 +154,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -163,14 +162,14 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
|
||||
<div className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
|
||||
@@ -15,4 +15,8 @@
|
||||
font-weight: bolder;
|
||||
display: block;
|
||||
}
|
||||
|
||||
& > .Eol {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper
|
||||
import { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { renderInfoColumn } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
|
||||
import { k162Types } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
|
||||
|
||||
interface UnsplashedSignatureProps {
|
||||
signature: SystemSignature;
|
||||
@@ -22,17 +22,22 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
|
||||
const whData = useMemo(() => wormholesData[signature.type], [signature.type, wormholesData]);
|
||||
const whClass = useMemo(() => (whData ? WORMHOLES_ADDITIONAL_INFO[whData.dest] : null), [whData]);
|
||||
|
||||
const k162TypeOption = useMemo(() => {
|
||||
if (!signature.custom_info) {
|
||||
return null;
|
||||
}
|
||||
const customInfo = JSON.parse(signature.custom_info);
|
||||
if (!customInfo.k162Type) {
|
||||
return null;
|
||||
}
|
||||
return k162Types.find(x => x.value === customInfo.k162Type);
|
||||
const customInfo = useMemo(() => {
|
||||
return parseSignatureCustomInfo(signature.custom_info);
|
||||
}, [signature]);
|
||||
|
||||
const k162TypeOption = useMemo(() => {
|
||||
if (!customInfo?.k162Type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return K162_TYPES_MAP[customInfo.k162Type];
|
||||
}, [customInfo]);
|
||||
|
||||
const isEOL = useMemo(() => {
|
||||
return customInfo?.isEOL;
|
||||
}, [customInfo]);
|
||||
|
||||
const whClassStyle = useMemo(() => {
|
||||
if (signature.type === 'K162' && k162TypeOption) {
|
||||
const k162Data = wormholesData[k162TypeOption.whClassName];
|
||||
@@ -45,19 +50,19 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
|
||||
return (
|
||||
<WdTooltipWrapper
|
||||
className={clsx(classes.Signature)}
|
||||
// @ts-ignore
|
||||
content={
|
||||
(
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title={<b className="text-slate-50">{signature.eve_id}</b>}>
|
||||
{renderInfoColumn(signature)}
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
) as React.ReactNode
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title={<b className="text-slate-50">{signature.eve_id}</b>}>
|
||||
{renderInfoColumn(signature)}
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={clsx(classes.Box, whClassStyle)}>
|
||||
<svg width="13" height="4" viewBox="0 0 13 4" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="13" height="4" rx="2" className={whClassStyle} fill="currentColor" />
|
||||
<svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="1" width="13" height="4" rx="2" className={whClassStyle} fill="currentColor" />
|
||||
{isEOL && <rect x="4" width="5" height="6" rx="1" className={clsx(classes.Eol)} fill="#a153ac" />}
|
||||
</svg>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
@@ -6,6 +6,7 @@ export function useBackgroundVars(themeName?: string) {
|
||||
const [gap, setGap] = useState<number>(16);
|
||||
const [size, setSize] = useState<number>(1);
|
||||
const [color, setColor] = useState('#81818b');
|
||||
const [snapSize, setSnapSize] = useState<number>(25);
|
||||
|
||||
useEffect(() => {
|
||||
// match any element whose entire `class` attribute ends with "-theme"
|
||||
@@ -29,16 +30,19 @@ export function useBackgroundVars(themeName?: string) {
|
||||
|
||||
const cssVarGap = style.getPropertyValue('--rf-bg-gap');
|
||||
const cssVarSize = style.getPropertyValue('--rf-bg-size');
|
||||
const cssVarSnapSize = style.getPropertyValue('--rf-snap-size');
|
||||
const cssColor = style.getPropertyValue('--rf-bg-pattern-color');
|
||||
|
||||
const gapNum = parseInt(cssVarGap, 10) || 16;
|
||||
const sizeNum = parseInt(cssVarSize, 10) || 1;
|
||||
const snapSize = parseInt(cssVarSnapSize, 10) || 25; //react-flow default
|
||||
|
||||
setVariant(finalVariant);
|
||||
setGap(gapNum);
|
||||
setSize(sizeNum);
|
||||
setColor(cssColor);
|
||||
setSnapSize(snapSize);
|
||||
}, [themeName]);
|
||||
|
||||
return { variant, gap, size, color };
|
||||
return { variant, gap, size, color, snapSize };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSystemKills } from '../../mapInterface/widgets/SystemKills/hooks/useSystemKills';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
interface UseKillsCounterProps {
|
||||
realSystemId: string;
|
||||
}
|
||||
|
||||
export function useKillsCounter({ realSystemId }: UseKillsCounterProps) {
|
||||
const { data: mapData, outCommand } = useMapRootState();
|
||||
const { systems } = mapData;
|
||||
|
||||
const systemNameMap = useMemo(() => {
|
||||
const m: Record<string, string> = {};
|
||||
systems.forEach(sys => {
|
||||
m[sys.id] = sys.temporary_name || sys.name || '???';
|
||||
});
|
||||
return m;
|
||||
}, [systems]);
|
||||
|
||||
const { kills: allKills, isLoading } = useSystemKills({
|
||||
systemId: realSystemId,
|
||||
outCommand,
|
||||
showAllVisible: false,
|
||||
});
|
||||
|
||||
const filteredKills = useMemo(() => {
|
||||
if (!allKills || allKills.length === 0) return [];
|
||||
|
||||
return [...allKills]
|
||||
.sort((a, b) => {
|
||||
const aTime = a.kill_time ? new Date(a.kill_time).getTime() : 0;
|
||||
const bTime = b.kill_time ? new Date(b.kill_time).getTime() : 0;
|
||||
return bTime - aTime;
|
||||
})
|
||||
.slice(0, 10);
|
||||
}, [allKills]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
kills: filteredKills,
|
||||
systemNameMap,
|
||||
};
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
setTimeout(() => addConnections(data as CommandAddConnections), 100);
|
||||
break;
|
||||
case Commands.removeConnections:
|
||||
removeConnections(data as CommandRemoveConnections);
|
||||
setTimeout(() => removeConnections(data as CommandRemoveConnections), 100);
|
||||
break;
|
||||
case Commands.charactersUpdated:
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
@@ -127,6 +127,10 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.detailedKillsUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Map handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
|
||||
@@ -10,10 +10,17 @@ import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormhol
|
||||
import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager';
|
||||
import { CharacterTypeRaw, OutCommand } from '@/hooks/Mapper/types';
|
||||
import { CharacterTypeRaw, OutCommand, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants';
|
||||
|
||||
function getActivityType(count: number) {
|
||||
export type LabelInfo = {
|
||||
id: string;
|
||||
shortName: string;
|
||||
};
|
||||
|
||||
export type UnsplashedSignatureType = SystemSignature & { sig_id: string };
|
||||
|
||||
function getActivityType(count: number): string {
|
||||
if (count <= 5) return 'activityNormal';
|
||||
if (count <= 30) return 'activityWarn';
|
||||
return 'activityDanger';
|
||||
@@ -26,12 +33,25 @@ const SpaceToClass: Record<string, string> = {
|
||||
[Spaces.Gallente]: 'Gallente',
|
||||
};
|
||||
|
||||
function sortedLabels(labels: string[]) {
|
||||
function sortedLabels(labels: string[]): LabelInfo[] {
|
||||
if (!labels) return [];
|
||||
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x]);
|
||||
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x] as LabelInfo);
|
||||
}
|
||||
|
||||
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
|
||||
const localCounterCharacters = useMemo(() => {
|
||||
return nodeVars.charactersInSystem
|
||||
.map(char => ({
|
||||
...char,
|
||||
compact: true,
|
||||
isOwn: nodeVars.userCharacters.includes(char.eve_id),
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [nodeVars.charactersInSystem, nodeVars.userCharacters]);
|
||||
return { localCounterCharacters };
|
||||
}
|
||||
|
||||
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars {
|
||||
const { id, data, selected } = props;
|
||||
const {
|
||||
system_static_info,
|
||||
@@ -71,7 +91,6 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
const {
|
||||
data: {
|
||||
characters,
|
||||
presentCharacters,
|
||||
wormholesData,
|
||||
hubs,
|
||||
kills,
|
||||
@@ -87,15 +106,14 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
|
||||
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
|
||||
|
||||
const systemSignatures = useMemo(
|
||||
const systemSigs = useMemo(
|
||||
() => mapSystemSignatures[solar_system_id] || system_signatures,
|
||||
[system_signatures, solar_system_id, mapSystemSignatures],
|
||||
);
|
||||
|
||||
const charactersInSystem = useMemo(() => {
|
||||
return characters.filter(c => c.location?.solar_system_id === solar_system_id).filter(c => c.online);
|
||||
// eslint-disable-next-line
|
||||
}, [characters, presentCharacters, solar_system_id]);
|
||||
return characters.filter(c => c.location?.solar_system_id === solar_system_id && c.online);
|
||||
}, [characters, solar_system_id]);
|
||||
|
||||
const isWormhole = isWormholeSpace(system_class);
|
||||
|
||||
@@ -136,52 +154,65 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
|
||||
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
|
||||
|
||||
const temporaryName = useMemo(() => {
|
||||
const computedTemporaryName = useMemo(() => {
|
||||
if (!isTempSystemNameEnabled) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
|
||||
return temporary_name ? `${linkedSigPrefix}・${temporary_name}` : `${linkedSigPrefix}・${solar_system_name}`;
|
||||
}
|
||||
|
||||
return temporary_name;
|
||||
}, [isShowLinkedSigIdTempName, isTempSystemNameEnabled, linkedSigPrefix, solar_system_name, temporary_name]);
|
||||
|
||||
const systemName = useMemo(() => {
|
||||
if (isTempSystemNameEnabled && temporaryName) {
|
||||
return temporaryName;
|
||||
if (isTempSystemNameEnabled && computedTemporaryName) {
|
||||
return computedTemporaryName;
|
||||
}
|
||||
return solar_system_name;
|
||||
}, [isTempSystemNameEnabled, solar_system_name, temporaryName]);
|
||||
}, [isTempSystemNameEnabled, solar_system_name, computedTemporaryName]);
|
||||
|
||||
const customName = (isTempSystemNameEnabled && temporaryName && name) || (solar_system_name !== name && name) || null;
|
||||
const customName = useMemo(() => {
|
||||
if (isTempSystemNameEnabled && computedTemporaryName && name) {
|
||||
return name;
|
||||
}
|
||||
if (solar_system_name !== name && name) {
|
||||
return name;
|
||||
}
|
||||
return null;
|
||||
}, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]);
|
||||
|
||||
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
|
||||
if (!isShowUnsplashedSignatures) {
|
||||
return [[], []];
|
||||
}
|
||||
return prepareUnsplashedChunks(
|
||||
systemSignatures
|
||||
systemSigs
|
||||
.filter(s => s.group === 'Wormhole' && !s.linked_system)
|
||||
.map(s => ({
|
||||
eve_id: s.eve_id,
|
||||
type: s.type,
|
||||
custom_info: s.custom_info,
|
||||
})),
|
||||
kind: s.kind,
|
||||
name: s.name,
|
||||
group: s.group,
|
||||
sig_id: s.eve_id, // Add a unique key property
|
||||
})) as UnsplashedSignatureType[],
|
||||
);
|
||||
}, [isShowUnsplashedSignatures, systemSignatures]);
|
||||
}, [isShowUnsplashedSignatures, systemSigs]);
|
||||
|
||||
const nodeVars = {
|
||||
// Ensure hubs are always strings.
|
||||
const hubsAsStrings = useMemo(() => hubs.map(item => item.toString()), [hubs]);
|
||||
|
||||
const nodeVars: SolarSystemNodeVars = {
|
||||
id,
|
||||
selected,
|
||||
|
||||
visible,
|
||||
isWormhole,
|
||||
classTitleColor,
|
||||
killsCount,
|
||||
killsActivityType,
|
||||
hasUserCharacters,
|
||||
userCharacters,
|
||||
showHandlers,
|
||||
regionClass,
|
||||
systemName,
|
||||
@@ -195,10 +226,10 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
sortedStatics,
|
||||
effectName: effect_name,
|
||||
regionName: region_name,
|
||||
solarSystemId: solar_system_id,
|
||||
solarSystemId: solar_system_id.toString(),
|
||||
solarSystemName: solar_system_name,
|
||||
locked,
|
||||
hubs,
|
||||
hubs: hubsAsStrings,
|
||||
name: name,
|
||||
isConnecting,
|
||||
hoverNodeId,
|
||||
@@ -207,7 +238,7 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
unsplashedRight,
|
||||
isThickConnections,
|
||||
classTitle: class_title,
|
||||
temporaryName: temporary_name,
|
||||
temporaryName: computedTemporaryName,
|
||||
};
|
||||
|
||||
return nodeVars;
|
||||
@@ -230,24 +261,22 @@ export interface SolarSystemNodeVars {
|
||||
isShattered: boolean;
|
||||
tag?: string | null;
|
||||
status?: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
labelsInfo: Array<any>;
|
||||
dbClick: (event?: void) => void;
|
||||
labelsInfo: LabelInfo[];
|
||||
dbClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
sortedStatics: Array<string | number>;
|
||||
effectName: string | null;
|
||||
regionName: string | null;
|
||||
solarSystemId: number;
|
||||
solarSystemId: string;
|
||||
solarSystemName: string | null;
|
||||
locked: boolean;
|
||||
hubs: string[] | number[];
|
||||
hubs: string[];
|
||||
name: string | null;
|
||||
isConnecting: boolean;
|
||||
hoverNodeId: string | null;
|
||||
charactersInSystem: Array<CharacterTypeRaw>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
unsplashedLeft: Array<any>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
unsplashedRight: Array<any>;
|
||||
userCharacters: string[];
|
||||
unsplashedLeft: Array<SystemSignature>;
|
||||
unsplashedRight: Array<SystemSignature>;
|
||||
isThickConnections: boolean;
|
||||
classTitle: string | null;
|
||||
temporaryName?: string | null;
|
||||
@@ -11,7 +11,8 @@
|
||||
--rf-tag-color: #38BDF8;
|
||||
--rf-region-name: #D6D3D1;
|
||||
--rf-custom-name: #93C5FD;
|
||||
|
||||
--rf-node-font-family: 'Shentox', 'Rogan', sans-serif !important;
|
||||
--rf-node-font-weight: 500;
|
||||
|
||||
--rf-bg-variant: "dots";
|
||||
--rf-bg-gap: 15;
|
||||
@@ -28,4 +29,8 @@
|
||||
--tooltip-bg: #202020;
|
||||
|
||||
--window-corner: #72716f;
|
||||
|
||||
--rf-local-counter-font-weight: 500;
|
||||
--rf-node-local-counter: inherit;
|
||||
--rf-has-user-characters: #ffc75d;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
|
||||
$friendlyBase: #3bbd39;
|
||||
$friendlyAlpha: #3bbd3952;
|
||||
$friendlyBase: #3bbd39;
|
||||
$friendlyAlpha: #3bbd3952;
|
||||
$friendlyDark20: darken($friendlyBase, 20%);
|
||||
$friendlyDark30: darken($friendlyBase, 30%);
|
||||
$friendlyDark5: darken($friendlyBase, 5%);
|
||||
|
||||
$lookingForBase: #43c2fd;
|
||||
$lookingForBase: #43c2fd;
|
||||
$lookingForAlpha: rgba(67, 176, 253, 0.48);
|
||||
$lookingForDark15: darken($lookingForBase, 15%);
|
||||
|
||||
$homeBase: rgb(197, 253, 67);
|
||||
$homeAlpha: rgba(197, 253, 67, 0.32);
|
||||
$homeBase: rgb(179, 253, 67);
|
||||
$homeAlpha: rgba(186, 248, 48, 0.32);
|
||||
$homeBackground: #a0fa5636;
|
||||
$homeDark30: darken($homeBase, 30%);
|
||||
|
||||
|
||||
:root {
|
||||
--pastel-blue: #5a7d9a;
|
||||
--pastel-pink: #d291bc;
|
||||
@@ -98,6 +98,7 @@ $homeDark30: darken($homeBase, 30%);
|
||||
--eve-solar-system-status-color-unknown: transparent;
|
||||
--eve-solar-system-status-home: #{$homeAlpha};
|
||||
--eve-solar-system-status-color-home: #{$homeBase};
|
||||
--eve-solar-system-status-color-background: #{$homeBackground};
|
||||
--eve-solar-system-status-color-home-dark30: #{$homeDark30};
|
||||
--eve-solar-system-status-friendly: #{$friendlyAlpha};
|
||||
--eve-solar-system-status-color-friendly: #{$friendlyBase};
|
||||
|
||||
@@ -2,24 +2,31 @@
|
||||
@import './eve-common';
|
||||
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
|
||||
|
||||
$homeBase: rgb(197, 253, 67);
|
||||
$homeAlpha: rgba(197, 253, 67, 0.32);
|
||||
$homeDark30: darken($homeBase, 30%);
|
||||
|
||||
.pathfinder-theme {
|
||||
/* -- Override values from the default theme -- */
|
||||
--rf-bg-color: #000000;
|
||||
--rf-soft-bg-color: #282828;
|
||||
|
||||
--rf-node-bg-color: #202020;
|
||||
--rf-node-soft-bg-color: #313335;
|
||||
--rf-node-font-weight: bold;
|
||||
--rf-text-color: #adadad;
|
||||
--rf-region-name: var(--rf-text-color);
|
||||
--rf-custom-name: var(--rf-text-color);
|
||||
|
||||
--tooltip-bg: #202020;
|
||||
|
||||
--rf-bg-variant: "lines";
|
||||
--rf-bg-gap: 32;
|
||||
--rf-bg-size: 1;
|
||||
--rf-bg-gap: 34;
|
||||
--rf-snap-size: 17;
|
||||
--rf-bg-pattern-color: #313131;
|
||||
--rf-local-counter-font-weight: 700;
|
||||
|
||||
/* Additional node-specific overrides */
|
||||
--rf-node-line-height: normal;
|
||||
--rf-node-font-family: 'Oxygen', sans-serif;
|
||||
--rf-tag-color: #fbbf24;
|
||||
|
||||
/* -- theme-specific variables -- */
|
||||
--eve-effect-pulsar: #428bca;
|
||||
--eve-effect-magnetar: #e06fdf;
|
||||
--eve-effect-wolfRayet: #e28a0d;
|
||||
@@ -39,13 +46,9 @@
|
||||
--eve-wh-type-color-c13: #7986cb;
|
||||
--eve-wh-type-color-drifter: #44aa82;
|
||||
|
||||
--rf-node-font-weight: bold;
|
||||
--rf-node-line-height: normal;
|
||||
--rf-node-font-family: 'Oxygen', sans-serif;
|
||||
--rf-node-text-color: var(--pf-text-color);
|
||||
--rf-node-local-counter: #5cb85c;
|
||||
--rf-has-user-characters: #ffc75d;
|
||||
|
||||
--rf-tag-color: #fbbf24;
|
||||
--rf-has-user-characters: #5cb85c;
|
||||
|
||||
--window-corner: #72716f;
|
||||
--eve-solar-system-status-home: #{$homeAlpha};
|
||||
--eve-solar-system-status-color-home: #{$homeBase};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { useMemo } from 'react';
|
||||
import { WindowManager } from '@/hooks/Mapper/components/ui-kit/WindowManager';
|
||||
import { DEFAULT_WIDGETS } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
@@ -21,5 +19,12 @@ export const MapInterface = () => {
|
||||
.filter(x => windowsSettings.visible.some(j => x.id === j));
|
||||
}, [windowsSettings]);
|
||||
|
||||
return <WindowManager windows={items} dragSelector=".react-grid-dragHandleExample" onChange={updateWidgetSettings} />;
|
||||
return (
|
||||
<WindowManager
|
||||
windows={items}
|
||||
viewPort={windowsSettings.viewPort}
|
||||
dragSelector=".react-grid-dragHandleExample"
|
||||
onChange={updateWidgetSettings}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
SystemInfo,
|
||||
SystemSignatures,
|
||||
SystemStructures,
|
||||
SystemKills,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
|
||||
export const CURRENT_WINDOWS_VERSION = 8;
|
||||
@@ -16,6 +17,7 @@ export enum WidgetsIds {
|
||||
local = 'local',
|
||||
routes = 'routes',
|
||||
structures = 'structures',
|
||||
kills = 'kills',
|
||||
}
|
||||
|
||||
export const STORED_VISIBLE_WIDGETS_DEFAULT = [
|
||||
@@ -61,6 +63,13 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
|
||||
zIndex: 0,
|
||||
content: () => <SystemStructures />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.kills,
|
||||
position: { x: 270, y: 730 },
|
||||
size: { width: 510, height: 200 },
|
||||
zIndex: 0,
|
||||
content: () => <SystemKills />,
|
||||
},
|
||||
];
|
||||
|
||||
type WidgetsCheckboxesType = {
|
||||
@@ -89,4 +98,8 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
|
||||
id: WidgetsIds.structures,
|
||||
label: 'Structures',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.kills,
|
||||
label: 'Kills',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
.VirtualScroller {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.CharacterRow {
|
||||
//border-left-width: 1px;
|
||||
|
||||
&.CardBorderLeftIsOwn {
|
||||
border-left-color: rgb(251 146 60 / 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,130 +1,71 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import clsx from 'clsx';
|
||||
import classes from './LocalCharacters.module.scss';
|
||||
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
|
||||
import { CharacterCard, LayoutEventBlocker, WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters.ts';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters';
|
||||
import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
|
||||
type CharItemProps = {
|
||||
compact: boolean;
|
||||
} & CharacterTypeRaw &
|
||||
WithIsOwnCharacter;
|
||||
|
||||
const useItemTemplate = () => {
|
||||
const {
|
||||
data: { presentCharacters },
|
||||
} = useMapRootState();
|
||||
|
||||
return useCallback(
|
||||
(char: CharItemProps, options: VirtualScrollerTemplateOptions) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.CharacterRow, 'w-full box-border', {
|
||||
'surface-hover': options.odd,
|
||||
['border-b border-gray-600 border-opacity-20']: !options.last,
|
||||
['bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10']: char.online,
|
||||
})}
|
||||
style={{ height: options.props.itemSize + 'px' }}
|
||||
>
|
||||
<CharacterCard showShipName {...char} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
[presentCharacters],
|
||||
);
|
||||
};
|
||||
|
||||
type WindowLocalSettingsType = {
|
||||
compact: boolean;
|
||||
showOffline: boolean;
|
||||
version: number;
|
||||
};
|
||||
|
||||
const STORED_DEFAULT_VALUES: WindowLocalSettingsType = {
|
||||
compact: true,
|
||||
showOffline: false,
|
||||
version: 0,
|
||||
};
|
||||
import { UserPermission } from '@/hooks/Mapper/types/permissions';
|
||||
import { LocalCharactersList } from './components/LocalCharactersList';
|
||||
import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters';
|
||||
import { useLocalCharacterWidgetSettings } from './hooks/useLocalWidgetSettings';
|
||||
import { LocalCharactersHeader } from './components/LocalCharactersHeader';
|
||||
import classes from './LocalCharacters.module.scss';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const LocalCharacters = () => {
|
||||
const {
|
||||
data: { characters, userCharacters, selectedSystems, presentCharacters },
|
||||
data: { characters, userCharacters, selectedSystems },
|
||||
} = useMapRootState();
|
||||
|
||||
const [settings, setSettings] = useLocalStorageState<WindowLocalSettingsType>('window:local:settings', {
|
||||
defaultValue: STORED_DEFAULT_VALUES,
|
||||
});
|
||||
|
||||
const [settings, setSettings] = useLocalCharacterWidgetSettings();
|
||||
const [systemId] = selectedSystems;
|
||||
|
||||
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
|
||||
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
|
||||
|
||||
const showOffline = useMemo(
|
||||
() => !restrictOfflineShowing || isAdminOrManager,
|
||||
[isAdminOrManager, restrictOfflineShowing],
|
||||
);
|
||||
|
||||
const itemTemplate = useItemTemplate();
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const sorted = characters
|
||||
const filtered = characters
|
||||
.filter(x => x.location?.solar_system_id?.toString() === systemId)
|
||||
.map(x => ({ ...x, isOwn: userCharacters.includes(x.eve_id), compact: settings.compact }))
|
||||
.map(x => ({
|
||||
...x,
|
||||
isOwn: userCharacters.includes(x.eve_id),
|
||||
compact: settings.compact,
|
||||
showShipName: settings.showShipName,
|
||||
}))
|
||||
.sort(sortCharacters);
|
||||
|
||||
if (!showOffline || !settings.showOffline) {
|
||||
return sorted.filter(c => c.online);
|
||||
return filtered.filter(c => c.online);
|
||||
}
|
||||
|
||||
return sorted;
|
||||
// eslint-disable-next-line
|
||||
}, [showOffline, characters, settings.showOffline, settings.compact, systemId, userCharacters, presentCharacters]);
|
||||
return filtered;
|
||||
}, [
|
||||
characters,
|
||||
systemId,
|
||||
userCharacters,
|
||||
settings.compact,
|
||||
settings.showOffline,
|
||||
settings.showShipName,
|
||||
showOffline,
|
||||
]);
|
||||
|
||||
const isNobodyHere = sorted.length === 0;
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
const showList = sorted.length > 0 && selectedSystems.length === 1;
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const compact = useMaxWidth(ref, 145);
|
||||
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
|
||||
|
||||
return (
|
||||
<Widget
|
||||
label={
|
||||
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
|
||||
<span className="select-none">Local{showList ? ` [${sorted.length}]` : ''}</span>
|
||||
<LayoutEventBlocker className="flex items-center gap-2">
|
||||
{showOffline && (
|
||||
<WdTooltipWrapper content="Show offline characters in system">
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compact ? '' : 'Show offline'}
|
||||
value={settings.showOffline}
|
||||
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
|
||||
onChange={() => setSettings(() => ({ ...settings, showOffline: !settings.showOffline }))}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={clsx('w-4 h-4 cursor-pointer', {
|
||||
['hero-bars-2']: settings.compact,
|
||||
['hero-bars-3']: !settings.compact,
|
||||
})}
|
||||
onClick={() => setSettings(() => ({ ...settings, compact: !settings.compact }))}
|
||||
></span>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
<LocalCharactersHeader
|
||||
sortedCount={sorted.length}
|
||||
showList={showList}
|
||||
showOffline={showOffline}
|
||||
settings={settings}
|
||||
setSettings={setSettings}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isNotSelectedSystem && (
|
||||
@@ -132,23 +73,20 @@ export const LocalCharacters = () => {
|
||||
System is not selected
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isNobodyHere && !isNotSelectedSystem && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
|
||||
Nobody here
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showList && (
|
||||
<VirtualScroller
|
||||
<LocalCharactersList
|
||||
items={sorted}
|
||||
itemSize={settings.compact ? 26 : 41}
|
||||
itemTemplate={itemTemplate}
|
||||
className={clsx(
|
||||
classes.VirtualScroller,
|
||||
containerClassName={clsx(
|
||||
'w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none',
|
||||
classes.VirtualScroller,
|
||||
)}
|
||||
autoSize={false}
|
||||
/>
|
||||
)}
|
||||
</Widget>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import { LayoutEventBlocker, TooltipPosition, WdCheckbox, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
interface LocalCharactersHeaderProps {
|
||||
sortedCount: number;
|
||||
showList: boolean;
|
||||
showOffline: boolean;
|
||||
settings: {
|
||||
compact: boolean;
|
||||
showOffline: boolean;
|
||||
showShipName: boolean;
|
||||
};
|
||||
setSettings: (fn: (prev: any) => any) => void;
|
||||
}
|
||||
|
||||
export const LocalCharactersHeader: React.FC<LocalCharactersHeaderProps> = ({
|
||||
sortedCount,
|
||||
showList,
|
||||
showOffline,
|
||||
settings,
|
||||
setSettings,
|
||||
}) => {
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const compactOffline = useMaxWidth(headerRef, 145);
|
||||
const compactShipName = useMaxWidth(headerRef, 195);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center text-xs justify-between" ref={headerRef}>
|
||||
<div className="flex-shrink-0 select-none mr-2">Local{showList ? ` [${sortedCount}]` : ''}</div>
|
||||
<LayoutEventBlocker className="flex items-center gap-2 justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
{showOffline && (
|
||||
<WdTooltipWrapper content="Show offline characters in system" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compactOffline ? '' : 'Offline'}
|
||||
value={settings.showOffline}
|
||||
onChange={() => setSettings((prev: any) => ({ ...prev, showOffline: !prev.showOffline }))}
|
||||
classNameLabel={clsx('whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300', {
|
||||
truncate: compactOffline,
|
||||
})}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{settings.compact && (
|
||||
<WdTooltipWrapper content="Show ship name in compact rows" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compactShipName ? '' : 'Ship name'}
|
||||
value={settings.showShipName}
|
||||
onChange={() => setSettings((prev: any) => ({ ...prev, showShipName: !prev.showShipName }))}
|
||||
classNameLabel={clsx('whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300', {
|
||||
truncate: compactShipName,
|
||||
})}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
<WdTooltipWrapper content="Enable compact mode" position={TooltipPosition.top}>
|
||||
<span
|
||||
className={clsx('w-4 h-4 min-w-[1rem] cursor-pointer', {
|
||||
'hero-bars-2': settings.compact,
|
||||
'hero-bars-3': !settings.compact,
|
||||
})}
|
||||
onClick={() => setSettings((prev: any) => ({ ...prev, compact: !prev.compact }))}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.CharacterRow {
|
||||
&.CardBorderLeftIsOwn {
|
||||
border-left-color: rgb(251 146 60 / 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import classes from './LocalCharactersItemTemplate.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { CharItemProps } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/components';
|
||||
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
|
||||
export type LocalCharactersItemTemplateProps = { showShipName: boolean } & CharItemProps &
|
||||
VirtualScrollerTemplateOptions;
|
||||
|
||||
export const LocalCharactersItemTemplate = ({ showShipName, ...options }: LocalCharactersItemTemplateProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
classes.CharacterRow,
|
||||
'box-border flex items-center w-full whitespace-nowrap overflow-hidden text-ellipsis min-w-[0px]',
|
||||
{
|
||||
'surface-hover': options.odd,
|
||||
'border-b border-gray-600 border-opacity-20': !options.last,
|
||||
'bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10': options.online,
|
||||
},
|
||||
)}
|
||||
style={{ height: `${options.props.itemSize}px` }}
|
||||
>
|
||||
<CharacterCard showShipName={showShipName} {...options} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './LocalCharactersItemTemplate.tsx';
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import clsx from 'clsx';
|
||||
import { CharItemProps } from './types';
|
||||
|
||||
type LocalCharactersListProps = {
|
||||
items: Array<CharItemProps>;
|
||||
itemSize: number;
|
||||
itemTemplate: (char: CharItemProps, options: VirtualScrollerTemplateOptions) => React.ReactNode;
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
export const LocalCharactersList = ({
|
||||
items,
|
||||
itemSize,
|
||||
itemTemplate,
|
||||
containerClassName,
|
||||
}: LocalCharactersListProps) => {
|
||||
return (
|
||||
<VirtualScroller
|
||||
items={items}
|
||||
itemSize={itemSize}
|
||||
orientation="vertical"
|
||||
className={clsx('w-full h-full', containerClassName)}
|
||||
itemTemplate={itemTemplate}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './LocalCharactersItemTemplate';
|
||||
export * from './LocalCharactersList';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,6 @@
|
||||
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
|
||||
|
||||
export type CharItemProps = {
|
||||
compact: boolean;
|
||||
} & CharacterTypeRaw &
|
||||
WithIsOwnCharacter;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import { CharItemProps, LocalCharactersItemTemplate } from '../components';
|
||||
|
||||
export function useLocalCharactersItemTemplate(showShipName: boolean) {
|
||||
return useCallback(
|
||||
(char: CharItemProps, options: VirtualScrollerTemplateOptions) => (
|
||||
<LocalCharactersItemTemplate {...char} {...options} showShipName={showShipName} />
|
||||
),
|
||||
[showShipName],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
|
||||
export interface LocalCharacterWidgetSettings {
|
||||
compact: boolean;
|
||||
showOffline: boolean;
|
||||
version: number;
|
||||
showShipName: boolean;
|
||||
}
|
||||
|
||||
export const LOCAL_CHARACTER_WIDGET_DEFAULT: LocalCharacterWidgetSettings = {
|
||||
compact: true,
|
||||
showOffline: false,
|
||||
version: 0,
|
||||
showShipName: false,
|
||||
};
|
||||
|
||||
export function useLocalCharacterWidgetSettings() {
|
||||
return useLocalStorageState<LocalCharacterWidgetSettings>('kills:widget:settings', {
|
||||
defaultValue: LOCAL_CHARACTER_WIDGET_DEFAULT,
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import classes from './RoutesList.module.scss';
|
||||
import { Route, SystemStaticInfoShort } from '@/hooks/Mapper/types/routes.ts';
|
||||
import clsx from 'clsx';
|
||||
import { SystemViewStandalone, WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SystemViewStandalone, TooltipPosition, WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { getBackgroundClass, getShapeClass } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { MouseEvent, useCallback, useRef, useState } from 'react';
|
||||
import { Commands } from '@/hooks/Mapper/types';
|
||||
@@ -46,9 +46,11 @@ export const RouteSystem = ({
|
||||
<>
|
||||
<WdTooltip
|
||||
ref={tooltipRef}
|
||||
position={TooltipPosition.top}
|
||||
// targetSelector={`.tooltip-route-sys_${destination}_${solar_system_id}`}
|
||||
content={() => (
|
||||
<SystemViewStandalone
|
||||
className="mx-[4px]"
|
||||
security={security}
|
||||
system_class={system_class}
|
||||
class_title={class_title}
|
||||
@@ -63,8 +65,8 @@ export const RouteSystem = ({
|
||||
tooltipRef.current?.show(e);
|
||||
onMouseEnter?.(solar_system_id);
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
tooltipRef.current?.hide(e);
|
||||
onMouseLeave={() => {
|
||||
tooltipRef.current?.hide();
|
||||
onMouseLeave?.();
|
||||
}}
|
||||
onContextMenu={handleContext}
|
||||
|
||||
@@ -184,7 +184,7 @@ export const RoutesWidgetComp = () => {
|
||||
}, [data, update]);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const compact = useMaxWidth(ref, 155);
|
||||
const compact = useMaxWidth(ref, 170);
|
||||
const [openAddSystem, setOpenAddSystem] = useState<boolean>(false);
|
||||
|
||||
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
|
||||
@@ -217,14 +217,14 @@ export const RoutesWidgetComp = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<WdTooltipWrapper content="Show shortest route">
|
||||
<WdTooltipWrapper content="Show shortest route" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compact ? '' : 'Show shortest'}
|
||||
value={!isSecure}
|
||||
onChange={handleSecureChange}
|
||||
classNameLabel={clsx('text-red-400')}
|
||||
classNameLabel="text-red-400 whitespace-nowrap"
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
<WdImgButton
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
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';
|
||||
|
||||
export const SystemKills: React.FC = () => {
|
||||
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 [settings] = useKillsWidgetSettings();
|
||||
const visible = settings.showAll;
|
||||
|
||||
const { kills, isLoading, error } = useSystemKills({
|
||||
systemId,
|
||||
outCommand,
|
||||
showAllVisible: visible,
|
||||
});
|
||||
|
||||
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 = systems.find(
|
||||
sys => sys.system_static_info.solar_system_id === 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, systems]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col min-h-0">
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
<Widget
|
||||
label={
|
||||
<KillsHeader
|
||||
systemId={systemId}
|
||||
onOpenSettings={() => setSettingsDialogVisible(true)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="relative h-full">
|
||||
{!isSubscriptionActive ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
Kills available with 'Active' map subscription only (contact map administrators)
|
||||
</span>
|
||||
</div>
|
||||
) : isNothingSelected ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
No system selected (or toggle “Show all systems”)
|
||||
</span>
|
||||
</div>
|
||||
) : showLoading ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
Loading Kills...
|
||||
</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="absolute inset-0 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="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
No kills found
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<SystemKillsContent
|
||||
key={settings.compact ? 'compact' : 'normal'}
|
||||
kills={filteredKills}
|
||||
systemNameMap={systemNameMap}
|
||||
compact={settings.compact}
|
||||
onlyOneSystem={!visible}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Widget>
|
||||
</div>
|
||||
|
||||
<KillsSettingsDialog
|
||||
visible={settingsDialogVisible}
|
||||
setVisible={setSettingsDialogVisible}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
.TableRowCompact {
|
||||
height: 8px;
|
||||
max-height: 8px;
|
||||
font-size: 12px !important;
|
||||
line-height: 8px;
|
||||
}
|
||||
|
||||
.Table {
|
||||
font-size: 12px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.Tooltip {
|
||||
white-space: pre-line;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { KillRow } from '../components/SystemKillsRow';
|
||||
|
||||
interface SystemKillsContentProps {
|
||||
kills: DetailedKill[];
|
||||
systemNameMap: Record<string, string>;
|
||||
compact?: boolean;
|
||||
onlyOneSystem?: boolean;
|
||||
}
|
||||
|
||||
export const SystemKillsContent: React.FC<SystemKillsContentProps> = ({
|
||||
kills,
|
||||
systemNameMap,
|
||||
compact = false,
|
||||
onlyOneSystem = false,
|
||||
}) => {
|
||||
const sortedKills = useMemo(() => {
|
||||
return [...kills].sort((a, b) => {
|
||||
const timeA = a.kill_time ? new Date(a.kill_time).getTime() : 0;
|
||||
const timeB = b.kill_time ? new Date(b.kill_time).getTime() : 0;
|
||||
return timeB - timeA;
|
||||
});
|
||||
}, [kills]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-col w-full text-stone-200 text-xs transition-all duration-300',
|
||||
compact ? 'p-1' : 'p-1'
|
||||
)}
|
||||
>
|
||||
{sortedKills.map(kill => {
|
||||
const systemIdStr = String(kill.solar_system_id);
|
||||
const systemName = systemNameMap[systemIdStr] || `System ${systemIdStr}`;
|
||||
|
||||
return (
|
||||
<KillRow
|
||||
key={kill.killmail_id}
|
||||
killDetails={kill}
|
||||
systemName={systemName}
|
||||
isCompact={compact}
|
||||
onlyOneSystem={onlyOneSystem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,235 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import {
|
||||
formatISK,
|
||||
formatTimeMixed,
|
||||
zkillLink,
|
||||
getAttackerSubscript,
|
||||
buildVictimImageUrls,
|
||||
buildAttackerImageUrls,
|
||||
getPrimaryLogoAndTooltip,
|
||||
getAttackerPrimaryImageAndTooltip,
|
||||
} from '../helpers';
|
||||
import { WdTooltipWrapper } from '../../../../ui-kit/WdTooltipWrapper';
|
||||
import classes from './SystemKillRow.module.scss';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
export interface CompactKillRowProps {
|
||||
killDetails: DetailedKill;
|
||||
systemName: string;
|
||||
onlyOneSystem: boolean;
|
||||
}
|
||||
|
||||
export const CompactKillRow: React.FC<CompactKillRowProps> = ({
|
||||
killDetails,
|
||||
systemName,
|
||||
onlyOneSystem,
|
||||
}) => {
|
||||
const {
|
||||
killmail_id = 0,
|
||||
|
||||
// Victim
|
||||
victim_char_name = 'Unknown Pilot',
|
||||
victim_alliance_ticker = '',
|
||||
victim_corp_ticker = '',
|
||||
victim_ship_name = 'Unknown Ship',
|
||||
victim_corp_name = '',
|
||||
victim_alliance_name = '',
|
||||
victim_char_id = 0,
|
||||
victim_corp_id = 0,
|
||||
victim_alliance_id = 0,
|
||||
victim_ship_type_id = 0,
|
||||
|
||||
// Attacker
|
||||
final_blow_char_id = 0,
|
||||
final_blow_char_name = '',
|
||||
final_blow_alliance_ticker = '',
|
||||
final_blow_alliance_name = '',
|
||||
final_blow_alliance_id = 0,
|
||||
final_blow_corp_ticker = '',
|
||||
final_blow_corp_id = 0,
|
||||
final_blow_corp_name = '',
|
||||
final_blow_ship_type_id = 0,
|
||||
|
||||
kill_time = '',
|
||||
total_value = 0,
|
||||
} = killDetails || {};
|
||||
|
||||
const attackerIsNpc = final_blow_char_id === 0;
|
||||
|
||||
// Tickers & strings
|
||||
const victimAffiliationTicker =
|
||||
victim_alliance_ticker || victim_corp_ticker || 'No Ticker';
|
||||
const killValueFormatted =
|
||||
total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
|
||||
const attackerName = attackerIsNpc ? '' : final_blow_char_name;
|
||||
const attackerTicker = attackerIsNpc
|
||||
? ''
|
||||
: final_blow_alliance_ticker || final_blow_corp_ticker || '';
|
||||
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
|
||||
const attackerSubscript = getAttackerSubscript(killDetails);
|
||||
|
||||
// Victim images, including the ship
|
||||
const {
|
||||
victimCorpLogoUrl,
|
||||
victimAllianceLogoUrl,
|
||||
victimShipUrl,
|
||||
} = buildVictimImageUrls({
|
||||
victim_char_id,
|
||||
victim_ship_type_id,
|
||||
victim_corp_id,
|
||||
victim_alliance_id,
|
||||
});
|
||||
|
||||
// Attacker corp/alliance
|
||||
const { attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
|
||||
final_blow_char_id,
|
||||
final_blow_corp_id,
|
||||
final_blow_alliance_id,
|
||||
});
|
||||
|
||||
// Victim corp/alliance logo
|
||||
const { url: victimPrimaryLogoUrl, tooltip: victimPrimaryTooltip } =
|
||||
getPrimaryLogoAndTooltip(
|
||||
victimAllianceLogoUrl,
|
||||
victimCorpLogoUrl,
|
||||
victim_alliance_name,
|
||||
victim_corp_name,
|
||||
'Victim'
|
||||
);
|
||||
|
||||
// Attacker corp/alliance or NPC ship
|
||||
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } =
|
||||
getAttackerPrimaryImageAndTooltip(
|
||||
attackerIsNpc,
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'h-10 flex items-center border-b border-stone-800',
|
||||
'text-xs whitespace-nowrap overflow-hidden leading-none'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
{victimShipUrl && (
|
||||
<div className="relative shrink-0 w-8 h-8 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimShipUrl}
|
||||
alt="VictimShip"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{victimPrimaryLogoUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={victimPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative block shrink-0 w-8 h-8 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={victimPrimaryLogoUrl}
|
||||
alt="VictimPrimaryLogo"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col ml-2 min-w-0 overflow-hidden leading-[1rem]">
|
||||
<div className="truncate text-stone-200">
|
||||
{victim_char_name}
|
||||
<span className="text-stone-400"> / {victimAffiliationTicker}</span>
|
||||
</div>
|
||||
<div className="truncate text-stone-300">
|
||||
{victim_ship_name}
|
||||
{killValueFormatted && (
|
||||
<>
|
||||
<span className="ml-1 text-stone-400">/</span>
|
||||
<span className="ml-1 text-green-400">{killValueFormatted}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center ml-auto gap-2">
|
||||
<div className="flex flex-col items-end min-w-0 overflow-hidden text-right leading-[1rem]">
|
||||
{!attackerIsNpc && (attackerName || attackerTicker) && (
|
||||
<div className="truncate text-stone-200">
|
||||
{attackerName}
|
||||
{attackerTicker && (
|
||||
<span className="ml-1 text-stone-400">/ {attackerTicker}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="truncate text-stone-400">
|
||||
{!onlyOneSystem && systemName ? (
|
||||
<>
|
||||
{systemName} /{' '}
|
||||
<span className="ml-1 text-red-400">{killTimeAgo}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-red-400">{killTimeAgo}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{attackerPrimaryImageUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={attackerPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative block shrink-0 w-8 h-8 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={attackerPrimaryImageUrl}
|
||||
alt={attackerIsNpc ? 'NpcShip' : 'AttackerPrimaryLogo'}
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
{attackerSubscript && (
|
||||
<span
|
||||
className={clsx(
|
||||
classes.attackerCountLabel,
|
||||
attackerSubscript.cssClass,
|
||||
'text-[0.6rem] leading-none px-[2px]'
|
||||
)}
|
||||
>
|
||||
{attackerSubscript.label}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,268 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import {
|
||||
formatISK,
|
||||
formatTimeMixed,
|
||||
zkillLink,
|
||||
getAttackerSubscript,
|
||||
buildVictimImageUrls,
|
||||
buildAttackerImageUrls,
|
||||
getPrimaryLogoAndTooltip,
|
||||
getAttackerPrimaryImageAndTooltip,
|
||||
} from '../helpers';
|
||||
import { VictimRowSubInfo } from './VictimRowSubInfo';
|
||||
import { WdTooltipWrapper } from '../../../../ui-kit/WdTooltipWrapper';
|
||||
import classes from './SystemKillRow.module.scss';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
export interface FullKillRowProps {
|
||||
killDetails: DetailedKill;
|
||||
systemName: string;
|
||||
onlyOneSystem: boolean;
|
||||
}
|
||||
|
||||
export const FullKillRow: React.FC<FullKillRowProps> = ({
|
||||
killDetails,
|
||||
systemName,
|
||||
onlyOneSystem,
|
||||
}) => {
|
||||
const {
|
||||
killmail_id = 0,
|
||||
// Victim data
|
||||
victim_char_name = '',
|
||||
victim_alliance_ticker = '',
|
||||
victim_corp_ticker = '',
|
||||
victim_ship_name = '',
|
||||
victim_char_id = 0,
|
||||
victim_corp_id = 0,
|
||||
victim_alliance_id = 0,
|
||||
victim_ship_type_id = 0,
|
||||
victim_corp_name = '',
|
||||
victim_alliance_name = '',
|
||||
// Attacker data
|
||||
final_blow_char_id = 0,
|
||||
final_blow_char_name = '',
|
||||
final_blow_alliance_ticker = '',
|
||||
final_blow_corp_ticker = '',
|
||||
final_blow_corp_name = '',
|
||||
final_blow_alliance_name = '',
|
||||
final_blow_corp_id = 0,
|
||||
final_blow_alliance_id = 0,
|
||||
final_blow_ship_name = '',
|
||||
final_blow_ship_type_id = 0,
|
||||
total_value = 0,
|
||||
kill_time = '',
|
||||
} = killDetails || {};
|
||||
|
||||
const attackerIsNpc = final_blow_char_id === 0;
|
||||
const victimAffiliation = victim_alliance_ticker || victim_corp_ticker || null;
|
||||
const attackerAffiliation = attackerIsNpc
|
||||
? ''
|
||||
: final_blow_alliance_ticker || final_blow_corp_ticker || '';
|
||||
|
||||
const killValueFormatted =
|
||||
total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
|
||||
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
|
||||
|
||||
// Build victim images
|
||||
const {
|
||||
victimPortraitUrl,
|
||||
victimCorpLogoUrl,
|
||||
victimAllianceLogoUrl,
|
||||
victimShipUrl,
|
||||
} = buildVictimImageUrls({
|
||||
victim_char_id,
|
||||
victim_ship_type_id,
|
||||
victim_corp_id,
|
||||
victim_alliance_id,
|
||||
});
|
||||
|
||||
// Build attacker images
|
||||
const {
|
||||
attackerPortraitUrl,
|
||||
attackerCorpLogoUrl,
|
||||
attackerAllianceLogoUrl,
|
||||
} = buildAttackerImageUrls({
|
||||
final_blow_char_id,
|
||||
final_blow_corp_id,
|
||||
final_blow_alliance_id,
|
||||
});
|
||||
|
||||
// Primary image for victim
|
||||
const { url: victimPrimaryImageUrl, tooltip: victimPrimaryTooltip } =
|
||||
getPrimaryLogoAndTooltip(
|
||||
victimAllianceLogoUrl,
|
||||
victimCorpLogoUrl,
|
||||
victim_alliance_name,
|
||||
victim_corp_name,
|
||||
'Victim'
|
||||
);
|
||||
|
||||
// Primary image for attacker
|
||||
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } =
|
||||
getAttackerPrimaryImageAndTooltip(
|
||||
attackerIsNpc,
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id
|
||||
);
|
||||
|
||||
const attackerSubscript = getAttackerSubscript(killDetails);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
classes.killRowContainer,
|
||||
'w-full text-sm py-1 px-2',
|
||||
'flex flex-col sm:flex-row'
|
||||
)}
|
||||
>
|
||||
<div className="w-full flex flex-col sm:flex-row items-start gap-2">
|
||||
{/* Victim Section */}
|
||||
<div className="flex items-start gap-1 min-w-0">
|
||||
{victimShipUrl && (
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimShipUrl}
|
||||
alt="VictimShip"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{victimPrimaryImageUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={victimPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimPrimaryImageUrl}
|
||||
alt="VictimPrimaryLogo"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
<VictimRowSubInfo
|
||||
victimCharName={victim_char_name}
|
||||
victimCharacterId={victim_char_id}
|
||||
victimPortraitUrl={victimPortraitUrl}
|
||||
/>
|
||||
<div className="flex flex-col text-stone-200 leading-4 min-w-0 overflow-hidden">
|
||||
<div className="truncate font-semibold">
|
||||
{victim_char_name}
|
||||
{victimAffiliation && (
|
||||
<span className="ml-1 text-stone-400">/ {victimAffiliation}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-stone-300">
|
||||
{victim_ship_name}
|
||||
{killValueFormatted && (
|
||||
<>
|
||||
<span className="ml-1 text-stone-400">/</span>
|
||||
<span className="ml-1 text-green-400">{killValueFormatted}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-stone-400">
|
||||
{!onlyOneSystem && systemName && <span>{systemName}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Attacker Section */}
|
||||
<div className="flex items-start gap-1 min-w-0 sm:ml-auto">
|
||||
<div className="flex flex-col items-end leading-4 min-w-0 overflow-hidden text-right">
|
||||
{!attackerIsNpc && (
|
||||
<div className="truncate font-semibold">
|
||||
{final_blow_char_name}
|
||||
{attackerAffiliation && (
|
||||
<span className="ml-1 text-stone-400">/ {attackerAffiliation}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!attackerIsNpc && final_blow_ship_name && (
|
||||
<div className="truncate text-stone-300">{final_blow_ship_name}</div>
|
||||
)}
|
||||
<div className="truncate text-red-400">{killTimeAgo}</div>
|
||||
</div>
|
||||
{(!attackerIsNpc && attackerPortraitUrl && final_blow_char_id > 0) && (
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('character', final_blow_char_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={attackerPortraitUrl}
|
||||
alt="AttackerPortrait"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{attackerPrimaryImageUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={attackerPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={attackerPrimaryImageUrl}
|
||||
alt={attackerIsNpc ? 'NpcShip' : 'AttackerPrimaryLogo'}
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
{attackerSubscript && (
|
||||
<span
|
||||
className={clsx(
|
||||
attackerSubscript.cssClass,
|
||||
classes.attackerCountLabel
|
||||
)}
|
||||
>
|
||||
{attackerSubscript.label}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
.killRowContainer {
|
||||
@apply flex items-center whitespace-nowrap overflow-hidden;
|
||||
&:not(:last-child) {
|
||||
@apply border-b border-stone-800;
|
||||
}
|
||||
@apply bg-transparent transition-all hover:bg-stone-900 hover:border-stone-700;
|
||||
}
|
||||
|
||||
.killRowImage {
|
||||
@apply border border-stone-800 rounded-[4px] object-contain;
|
||||
}
|
||||
|
||||
.attackerCountLabel {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
font-size: 10px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.attackerCountLabelCompact {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
font-size: 0.6rem;
|
||||
line-height: 1;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 1px 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import React, { useRef } from 'react';
|
||||
import {
|
||||
LayoutEventBlocker,
|
||||
SystemView,
|
||||
TooltipPosition,
|
||||
WdCheckbox,
|
||||
WdImgButton,
|
||||
WdTooltipWrapper,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
||||
|
||||
interface KillsHeaderProps {
|
||||
systemId?: string;
|
||||
onOpenSettings: () => void;
|
||||
}
|
||||
|
||||
export const KillsHeader: React.FC<KillsHeaderProps> = ({ systemId, onOpenSettings }) => {
|
||||
const [settings, setSettings] = useKillsWidgetSettings();
|
||||
const { showAll } = settings;
|
||||
|
||||
const onToggleShowAllVisible = () => {
|
||||
setSettings(prev => ({ ...prev, showAll: !prev.showAll }));
|
||||
};
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const compact = useMaxWidth(headerRef, 150);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between text-xs" ref={headerRef}>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="text-stone-400">
|
||||
Kills
|
||||
{systemId && !showAll && ' in '}
|
||||
</div>
|
||||
{systemId && !showAll && <SystemView systemId={systemId} className="select-none text-center" hideRegion />}
|
||||
</div>
|
||||
|
||||
<LayoutEventBlocker className="flex items-center gap-2 justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<WdTooltipWrapper content="Show all systems" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compact ? 'All' : 'Show all systems'}
|
||||
value={showAll}
|
||||
onChange={onToggleShowAllVisible}
|
||||
classNameLabel="whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300"
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
<WdImgButton
|
||||
className={PrimeIcons.SLIDERS_H}
|
||||
onClick={onOpenSettings}
|
||||
tooltip={{
|
||||
content: 'Open Kills Settings',
|
||||
position: TooltipPosition.top,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { CompactKillRow } from './CompactKillRow';
|
||||
import { FullKillRow } from './FullKillRow';
|
||||
|
||||
export interface KillRowProps {
|
||||
killDetails: DetailedKill;
|
||||
systemName: string;
|
||||
isCompact?: boolean;
|
||||
onlyOneSystem?: boolean;
|
||||
}
|
||||
|
||||
export const KillRow: React.FC<KillRowProps> = ({
|
||||
killDetails,
|
||||
systemName,
|
||||
isCompact = false,
|
||||
onlyOneSystem = false,
|
||||
}) => {
|
||||
if (isCompact) {
|
||||
return <CompactKillRow killDetails={killDetails} systemName={systemName} onlyOneSystem={onlyOneSystem} />;
|
||||
}
|
||||
|
||||
return <FullKillRow killDetails={killDetails} systemName={systemName} onlyOneSystem={onlyOneSystem} />;
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Button } from 'primereact/button';
|
||||
import { WdImgButton, SystemView, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
|
||||
import {
|
||||
AddSystemDialog,
|
||||
SearchOnSubmitCallback,
|
||||
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
|
||||
|
||||
interface KillsSettingsDialogProps {
|
||||
visible: boolean;
|
||||
setVisible: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visible, setVisible }) => {
|
||||
const [globalSettings, setGlobalSettings] = useKillsWidgetSettings();
|
||||
const localRef = useRef({
|
||||
compact: globalSettings.compact,
|
||||
showAll: globalSettings.showAll,
|
||||
whOnly: globalSettings.whOnly,
|
||||
excludedSystems: globalSettings.excludedSystems || [],
|
||||
});
|
||||
|
||||
const [, forceRender] = useState(0);
|
||||
|
||||
const [addSystemDialogVisible, setAddSystemDialogVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
localRef.current = {
|
||||
compact: globalSettings.compact,
|
||||
showAll: globalSettings.showAll,
|
||||
whOnly: globalSettings.whOnly,
|
||||
excludedSystems: globalSettings.excludedSystems || [],
|
||||
};
|
||||
forceRender(n => n + 1);
|
||||
}
|
||||
}, [visible, globalSettings]);
|
||||
|
||||
const handleCompactChange = useCallback((checked: boolean) => {
|
||||
localRef.current = {
|
||||
...localRef.current,
|
||||
compact: checked,
|
||||
};
|
||||
forceRender(n => n + 1);
|
||||
}, []);
|
||||
|
||||
const handleWHChange = useCallback((checked: boolean) => {
|
||||
localRef.current = {
|
||||
...localRef.current,
|
||||
whOnly: checked,
|
||||
};
|
||||
forceRender(n => n + 1);
|
||||
}, []);
|
||||
|
||||
const handleRemoveSystem = useCallback((sysId: number) => {
|
||||
localRef.current = {
|
||||
...localRef.current,
|
||||
excludedSystems: localRef.current.excludedSystems.filter(id => id !== sysId),
|
||||
};
|
||||
forceRender(n => n + 1);
|
||||
}, []);
|
||||
|
||||
const handleAddSystemSubmit: SearchOnSubmitCallback = useCallback(item => {
|
||||
if (localRef.current.excludedSystems.includes(item.value)) {
|
||||
return;
|
||||
}
|
||||
localRef.current = {
|
||||
...localRef.current,
|
||||
excludedSystems: [...localRef.current.excludedSystems, item.value],
|
||||
};
|
||||
forceRender(n => n + 1);
|
||||
}, []);
|
||||
|
||||
const handleApply = useCallback(() => {
|
||||
setGlobalSettings(prev => ({
|
||||
...prev,
|
||||
...localRef.current,
|
||||
}));
|
||||
setVisible(false);
|
||||
}, [setGlobalSettings, setVisible]);
|
||||
|
||||
const handleHide = useCallback(() => {
|
||||
setVisible(false);
|
||||
}, [setVisible]);
|
||||
|
||||
const localData = localRef.current;
|
||||
const excluded = localData.excludedSystems || [];
|
||||
|
||||
return (
|
||||
<Dialog header="Kills Settings" visible={visible} style={{ width: '440px' }} draggable={false} onHide={handleHide}>
|
||||
<div className="flex flex-col gap-3 p-2.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="kills-compact-mode"
|
||||
checked={localData.compact}
|
||||
onChange={e => handleCompactChange(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="kills-compact-mode" className="cursor-pointer">
|
||||
Use compact mode
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="kills-wormhole-only-mode"
|
||||
checked={localData.whOnly}
|
||||
onChange={e => handleWHChange(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="kills-wh-only-mode" className="cursor-pointer">
|
||||
Only show wormhole kills
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm text-stone-400">Excluded Systems</label>
|
||||
<WdImgButton
|
||||
className={PrimeIcons.PLUS_CIRCLE}
|
||||
onClick={() => setAddSystemDialogVisible(true)}
|
||||
tooltip={{ content: 'Add system to excluded list' }}
|
||||
/>
|
||||
</div>
|
||||
{excluded.length === 0 && <div className="text-stone-500 text-xs italic">No systems excluded.</div>}
|
||||
{excluded.map(sysId => (
|
||||
<div key={sysId} className="flex items-center justify-between border-b border-stone-600 py-1 px-1 text-xs">
|
||||
<SystemView systemId={sysId.toString()} hideRegion compact/>
|
||||
|
||||
<WdImgButton
|
||||
className={PrimeIcons.TRASH}
|
||||
onClick={() => handleRemoveSystem(sysId)}
|
||||
tooltip={{ content: 'Remove from excluded', position: TooltipPosition.top }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end mt-4">
|
||||
<Button onClick={handleApply} label="Apply" outlined size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddSystemDialog
|
||||
title="Add system to kills exclude list"
|
||||
visible={addSystemDialogVisible}
|
||||
setVisible={() => setAddSystemDialogVisible(false)}
|
||||
onSubmit={handleAddSystemSubmit}
|
||||
excludedSystems={excluded}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { zkillLink } from '../helpers';
|
||||
import classes from './SystemKillRow.module.scss';
|
||||
|
||||
interface VictimRowSubInfoProps {
|
||||
victimCharacterId: number | null;
|
||||
victimPortraitUrl: string | null;
|
||||
victimCharName?: string;
|
||||
}
|
||||
|
||||
export const VictimRowSubInfo: React.FC<VictimRowSubInfoProps> = ({
|
||||
victimCharacterId = 0,
|
||||
victimPortraitUrl,
|
||||
victimCharName,
|
||||
}) => {
|
||||
if (!victimPortraitUrl || !victimCharacterId || victimCharacterId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-1">
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('character', victimCharacterId)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimPortraitUrl}
|
||||
alt={victimCharName || 'Victim Portrait'}
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './linkHelpers';
|
||||
export * from './killRowUtils';
|
||||
@@ -0,0 +1,47 @@
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
|
||||
/** Returns "5m ago", "3h ago", "2.5d ago", etc. */
|
||||
export function formatTimeMixed(killTime: string): string {
|
||||
const killDate = new Date(killTime);
|
||||
const diffMs = Date.now() - killDate.getTime();
|
||||
const diffHours = diffMs / (1000 * 60 * 60);
|
||||
|
||||
if (diffHours < 1) {
|
||||
const mins = Math.round(diffHours * 60);
|
||||
return `${mins}m ago`;
|
||||
} else if (diffHours < 24) {
|
||||
const hours = Math.round(diffHours);
|
||||
return `${hours}h ago`;
|
||||
} else {
|
||||
const days = diffHours / 24;
|
||||
const roundedDays = days.toFixed(1);
|
||||
return `${roundedDays}d ago`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Formats integer ISK values into k/M/B/T. */
|
||||
export function formatISK(value: number): string {
|
||||
if (value >= 1_000_000_000_000) {
|
||||
return `${(value / 1_000_000_000_000).toFixed(2)}T`;
|
||||
} else if (value >= 1_000_000_000) {
|
||||
return `${(value / 1_000_000_000).toFixed(2)}B`;
|
||||
} else if (value >= 1_000_000) {
|
||||
return `${(value / 1_000_000).toFixed(2)}M`;
|
||||
} else if (value >= 1_000) {
|
||||
return `${(value / 1_000).toFixed(2)}k`;
|
||||
}
|
||||
return Math.round(value).toString();
|
||||
}
|
||||
|
||||
export function getAttackerSubscript(kill: DetailedKill) {
|
||||
if (kill.npc) {
|
||||
return { label: 'npc', cssClass: 'text-purple-400' };
|
||||
}
|
||||
const count = kill.attacker_count ?? 0;
|
||||
if (count === 1) {
|
||||
return { label: 'solo', cssClass: 'text-green-400' };
|
||||
} else if (count > 1) {
|
||||
return { label: String(count), cssClass: 'text-white' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
const ZKILL_URL = 'https://zkillboard.com';
|
||||
const BASE_IMAGE_URL = 'https://images.evetech.net';
|
||||
|
||||
export function zkillLink(type: 'kill' | 'character' | 'corporation' | 'alliance', id?: number | null): string {
|
||||
if (!id) return `${ZKILL_URL}`;
|
||||
if (type === 'kill') return `${ZKILL_URL}/kill/${id}/`;
|
||||
if (type === 'character') return `${ZKILL_URL}/character/${id}/`;
|
||||
if (type === 'corporation') return `${ZKILL_URL}/corporation/${id}/`;
|
||||
if (type === 'alliance') return `${ZKILL_URL}/alliance/${id}/`;
|
||||
return `${ZKILL_URL}`;
|
||||
}
|
||||
|
||||
export function eveImageUrl(
|
||||
category: 'characters' | 'corporations' | 'alliances' | 'types',
|
||||
id?: number | null,
|
||||
variation: string = 'icon',
|
||||
size?: number,
|
||||
): string | null {
|
||||
if (!id || id <= 0) {
|
||||
return null;
|
||||
}
|
||||
let url = `${BASE_IMAGE_URL}/${category}/${id}/${variation}`;
|
||||
if (size) {
|
||||
url += `?size=${size}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function buildVictimImageUrls(args: {
|
||||
victim_char_id?: number | null;
|
||||
victim_ship_type_id?: number | null;
|
||||
victim_corp_id?: number | null;
|
||||
victim_alliance_id?: number | null;
|
||||
}) {
|
||||
const { victim_char_id, victim_ship_type_id, victim_corp_id, victim_alliance_id } = args;
|
||||
|
||||
const victimPortraitUrl = eveImageUrl('characters', victim_char_id, 'portrait', 64);
|
||||
const victimShipUrl = eveImageUrl('types', victim_ship_type_id, 'render', 64);
|
||||
const victimCorpLogoUrl = eveImageUrl('corporations', victim_corp_id, 'logo', 32);
|
||||
const victimAllianceLogoUrl = eveImageUrl('alliances', victim_alliance_id, 'logo', 32);
|
||||
|
||||
return {
|
||||
victimPortraitUrl,
|
||||
victimShipUrl,
|
||||
victimCorpLogoUrl,
|
||||
victimAllianceLogoUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAttackerShipUrl(final_blow_ship_type_id?: number | null): string | null {
|
||||
return eveImageUrl('types', final_blow_ship_type_id, 'render', 64);
|
||||
}
|
||||
|
||||
export function buildAttackerImageUrls(args: {
|
||||
final_blow_char_id?: number | null;
|
||||
final_blow_corp_id?: number | null;
|
||||
final_blow_alliance_id?: number | null;
|
||||
}) {
|
||||
const { final_blow_char_id, final_blow_corp_id, final_blow_alliance_id } = args;
|
||||
|
||||
const attackerPortraitUrl = eveImageUrl('characters', final_blow_char_id, 'portrait', 64);
|
||||
const attackerCorpLogoUrl = eveImageUrl('corporations', final_blow_corp_id, 'logo', 32);
|
||||
const attackerAllianceLogoUrl = eveImageUrl('alliances', final_blow_alliance_id, 'logo', 32);
|
||||
|
||||
return {
|
||||
attackerPortraitUrl,
|
||||
attackerCorpLogoUrl,
|
||||
attackerAllianceLogoUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export function getPrimaryLogoAndTooltip(
|
||||
allianceUrl: string | null,
|
||||
corpUrl: string | null,
|
||||
allianceName: string,
|
||||
corpName: string,
|
||||
fallback: string,
|
||||
) {
|
||||
let url: string | null = null;
|
||||
let tooltip = '';
|
||||
|
||||
if (allianceUrl) {
|
||||
url = allianceUrl;
|
||||
tooltip = allianceName || fallback;
|
||||
} else if (corpUrl) {
|
||||
url = corpUrl;
|
||||
tooltip = corpName || fallback;
|
||||
}
|
||||
|
||||
return { url, tooltip };
|
||||
}
|
||||
|
||||
export function getAttackerPrimaryImageAndTooltip(
|
||||
isNpc: boolean,
|
||||
allianceUrl: string | null,
|
||||
corpUrl: string | null,
|
||||
allianceName: string,
|
||||
corpName: string,
|
||||
finalBlowShipTypeId: number | null,
|
||||
npcFallback: string = 'NPC Attacker',
|
||||
) {
|
||||
if (isNpc) {
|
||||
const shipUrl = buildAttackerShipUrl(finalBlowShipTypeId);
|
||||
return {
|
||||
url: shipUrl,
|
||||
tooltip: npcFallback,
|
||||
};
|
||||
}
|
||||
|
||||
return getPrimaryLogoAndTooltip(allianceUrl, corpUrl, allianceName, corpName, 'Attacker');
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
|
||||
export interface KillsWidgetSettings {
|
||||
compact: boolean;
|
||||
showAll: boolean;
|
||||
whOnly: boolean;
|
||||
excludedSystems: number[];
|
||||
version: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
|
||||
compact: true,
|
||||
showAll: false,
|
||||
whOnly: true,
|
||||
excludedSystems: [],
|
||||
version: 0,
|
||||
};
|
||||
|
||||
function mergeWithDefaults(settings?: Partial<KillsWidgetSettings>): KillsWidgetSettings {
|
||||
if (!settings) {
|
||||
return DEFAULT_KILLS_WIDGET_SETTINGS;
|
||||
}
|
||||
|
||||
return {
|
||||
...DEFAULT_KILLS_WIDGET_SETTINGS,
|
||||
...settings,
|
||||
excludedSystems: Array.isArray(settings.excludedSystems) ? settings.excludedSystems : [],
|
||||
};
|
||||
}
|
||||
|
||||
export function useKillsWidgetSettings() {
|
||||
const [rawValue, setRawValue] = useLocalStorageState<KillsWidgetSettings | undefined>('kills:widget:settings');
|
||||
|
||||
const value = useMemo<KillsWidgetSettings>(() => {
|
||||
return mergeWithDefaults(rawValue);
|
||||
}, [rawValue]);
|
||||
|
||||
const setValue = useCallback(
|
||||
(newVal: KillsWidgetSettings | ((prev: KillsWidgetSettings) => KillsWidgetSettings)) => {
|
||||
setRawValue(prev => {
|
||||
const mergedPrev = mergeWithDefaults(prev);
|
||||
|
||||
const nextUnmerged = typeof newVal === 'function' ? newVal(mergedPrev) : newVal;
|
||||
|
||||
return mergeWithDefaults(nextUnmerged);
|
||||
});
|
||||
},
|
||||
[setRawValue],
|
||||
);
|
||||
|
||||
return [value, setValue] as const;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useKillsWidgetSettings } from './useKillsWidgetSettings';
|
||||
|
||||
interface UseSystemKillsProps {
|
||||
systemId?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
outCommand: (payload: any) => Promise<any>;
|
||||
showAllVisible?: boolean;
|
||||
sinceHours?: number;
|
||||
}
|
||||
|
||||
function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceHours: number): DetailedKill[] {
|
||||
const cutoff = Date.now() - sinceHours * 60 * 60 * 1000;
|
||||
const byId: Record<string, DetailedKill> = {};
|
||||
|
||||
for (const kill of [...existing, ...incoming]) {
|
||||
if (!kill.kill_time) {
|
||||
continue;
|
||||
}
|
||||
const killTimeMs = new Date(kill.kill_time).valueOf();
|
||||
|
||||
if (killTimeMs >= cutoff) {
|
||||
byId[kill.killmail_id] = kill;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(byId);
|
||||
}
|
||||
|
||||
export function useSystemKills({ systemId, outCommand, showAllVisible = false, sinceHours = 24 }: UseSystemKillsProps) {
|
||||
const { data, update } = useMapRootState();
|
||||
const { detailedKills = {}, systems = [] } = data;
|
||||
|
||||
const [settings] = useKillsWidgetSettings();
|
||||
const excludedSystems = settings.excludedSystems;
|
||||
|
||||
// When showing all visible kills, filter out excluded systems;
|
||||
// when showAllVisible is false, ignore the exclusion filter.
|
||||
const effectiveSystemIds = useMemo(() => {
|
||||
if (showAllVisible) {
|
||||
return systems.map(s => s.id).filter(id => !excludedSystems.includes(Number(id)));
|
||||
}
|
||||
return systems.map(s => s.id);
|
||||
}, [systems, excludedSystems, showAllVisible]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const didFallbackFetch = useRef(Object.keys(detailedKills).length !== 0);
|
||||
|
||||
const mergeKillsIntoGlobal = useCallback(
|
||||
(killsMap: Record<string, DetailedKill[]>) => {
|
||||
update(prev => {
|
||||
const oldMap = prev.detailedKills ?? {};
|
||||
const updated: Record<string, DetailedKill[]> = { ...oldMap };
|
||||
|
||||
for (const [sid, newKills] of Object.entries(killsMap)) {
|
||||
const existing = updated[sid] ?? [];
|
||||
const combined = combineKills(existing, newKills, sinceHours);
|
||||
updated[sid] = combined;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
detailedKills: updated,
|
||||
};
|
||||
});
|
||||
},
|
||||
[update, sinceHours],
|
||||
);
|
||||
|
||||
const fetchKills = useCallback(
|
||||
async (forceFallback = false) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
let eventType: OutCommand;
|
||||
let requestData: Record<string, unknown>;
|
||||
|
||||
if (showAllVisible || forceFallback) {
|
||||
eventType = OutCommand.getSystemsKills;
|
||||
requestData = {
|
||||
system_ids: effectiveSystemIds,
|
||||
since_hours: sinceHours,
|
||||
};
|
||||
} else if (systemId) {
|
||||
eventType = OutCommand.getSystemKills;
|
||||
requestData = {
|
||||
system_id: systemId,
|
||||
since_hours: sinceHours,
|
||||
};
|
||||
} else {
|
||||
// If there's no system and not showing all, do nothing
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await outCommand({
|
||||
type: eventType,
|
||||
data: requestData,
|
||||
});
|
||||
|
||||
// Single system => `resp.kills`
|
||||
if (resp?.kills) {
|
||||
const arr = resp.kills as DetailedKill[];
|
||||
const sid = systemId ?? 'unknown';
|
||||
mergeKillsIntoGlobal({ [sid]: arr });
|
||||
}
|
||||
// multiple systems => `resp.systems_kills`
|
||||
else if (resp?.systems_kills) {
|
||||
mergeKillsIntoGlobal(resp.systems_kills as Record<string, DetailedKill[]>);
|
||||
} else {
|
||||
console.warn('[useSystemKills] Unexpected kills response =>', resp);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[useSystemKills] Failed to fetch kills:', err);
|
||||
setError(err instanceof Error ? err.message : 'Error fetching kills');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[showAllVisible, systemId, outCommand, effectiveSystemIds, sinceHours, mergeKillsIntoGlobal],
|
||||
);
|
||||
|
||||
const debouncedFetchKills = useMemo(
|
||||
() =>
|
||||
debounce(fetchKills, 500, {
|
||||
leading: true,
|
||||
trailing: false,
|
||||
}),
|
||||
[fetchKills],
|
||||
);
|
||||
|
||||
const finalKills = useMemo(() => {
|
||||
if (showAllVisible) {
|
||||
return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []);
|
||||
} else if (systemId) {
|
||||
return detailedKills[systemId] ?? [];
|
||||
} else if (didFallbackFetch.current) {
|
||||
// if we already did a fallback, we may have data for multiple systems
|
||||
return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []);
|
||||
}
|
||||
return [];
|
||||
}, [showAllVisible, systemId, effectiveSystemIds, detailedKills]);
|
||||
|
||||
const effectiveIsLoading = isLoading && finalKills.length === 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!systemId && !showAllVisible && !didFallbackFetch.current) {
|
||||
didFallbackFetch.current = true;
|
||||
// Cancel any queued debounced calls, then do the fallback.
|
||||
debouncedFetchKills.cancel();
|
||||
fetchKills(true); // forceFallback => fetch as though showAllVisible is true
|
||||
}
|
||||
}, [systemId, showAllVisible, debouncedFetchKills, fetchKills]);
|
||||
|
||||
useEffect(() => {
|
||||
if (effectiveSystemIds.length === 0) return;
|
||||
|
||||
if (showAllVisible || systemId) {
|
||||
debouncedFetchKills();
|
||||
// Clean up the debounce on unmount or changes
|
||||
return () => debouncedFetchKills.cancel();
|
||||
}
|
||||
}, [showAllVisible, systemId, effectiveSystemIds, debouncedFetchKills]);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
debouncedFetchKills.cancel();
|
||||
fetchKills(); // immediate (non-debounced) call
|
||||
}, [debouncedFetchKills, fetchKills]);
|
||||
|
||||
return {
|
||||
kills: finalKills,
|
||||
isLoading: effectiveIsLoading,
|
||||
error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SystemKills';
|
||||
@@ -1,28 +1,29 @@
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { SystemViewStandalone, WHClassView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SystemViewStandalone, TooltipPosition, WHClassView } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
import {
|
||||
k162Types,
|
||||
renderK162Type,
|
||||
} from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
|
||||
import { renderK162Type } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { renderName } from './renderName.tsx';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
|
||||
|
||||
export const renderInfoColumn = (row: SystemSignature) => {
|
||||
if (!row.group || row.group === SignatureGroup.Wormhole) {
|
||||
let k162TypeOption = null;
|
||||
if (row.custom_info) {
|
||||
const customInfo = JSON.parse(row.custom_info);
|
||||
if (customInfo.k162Type) {
|
||||
k162TypeOption = k162Types.find(x => x.value === customInfo.k162Type);
|
||||
}
|
||||
}
|
||||
const customInfo = parseSignatureCustomInfo(row.custom_info);
|
||||
|
||||
const k162TypeOption = customInfo.k162Type ? K162_TYPES_MAP[customInfo.k162Type] : null;
|
||||
|
||||
return (
|
||||
<div className="flex justify-start items-center gap-[4px]">
|
||||
{customInfo.isEOL && (
|
||||
<WdTooltipWrapper offset={5} position={TooltipPosition.top} content="Signature marked as EOL">
|
||||
<div className="pi pi-clock text-fuchsia-400 text-[11px] mr-[2px]"></div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{row.type && (
|
||||
<WHClassView
|
||||
className="text-[11px]"
|
||||
@@ -34,7 +35,7 @@ export const renderInfoColumn = (row: SystemSignature) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!row.linked_system && row.type === 'K162' && !!k162TypeOption && <>{renderK162Type(k162TypeOption)}</>}
|
||||
{!row.linked_system && row.type === 'K162' && k162TypeOption && renderK162Type(k162TypeOption)}
|
||||
|
||||
{row.linked_system && (
|
||||
<>
|
||||
|
||||
@@ -70,6 +70,11 @@ export const SystemStructures: React.FC = () => {
|
||||
<WdImgButton
|
||||
className={`${PrimeIcons.CLOCK} text-sky-400 hover:text-sky-200 transition duration-300`}
|
||||
onClick={handlePasteTimer}
|
||||
tooltip={{
|
||||
position: TooltipPosition.left,
|
||||
// @ts-ignore
|
||||
content: 'Add Structures/Timer',
|
||||
}}
|
||||
/>
|
||||
<WdImgButton
|
||||
className={PrimeIcons.QUESTION_CIRCLE}
|
||||
@@ -79,14 +84,14 @@ export const SystemStructures: React.FC = () => {
|
||||
content: (
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title={<b className="text-slate-50">How to add/update structures?</b>}>
|
||||
In game, select one or more structures in D-Scan and press Ctrl+C,
|
||||
In game, select one or more structures in D-Scan and then
|
||||
<br />
|
||||
then click on this widget and press Ctrl+V
|
||||
use the blue add structure data button
|
||||
</InfoDrawer>
|
||||
<InfoDrawer title={<b className="text-slate-50">How to add a timer?</b>}>
|
||||
In game, select a structure with an active timer, right click to copy, and then use the
|
||||
In game, select a structure with an active timer, right click to copy, and then
|
||||
<span className="text-blue-500"> blue </span>
|
||||
add timer button
|
||||
use the blue add structure data button
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
.TableRowCompact {
|
||||
height: 8px;
|
||||
max-height: 8px;
|
||||
font-size: 12px !important;
|
||||
line-height: 8px;
|
||||
}
|
||||
|
||||
.Table {
|
||||
font-size: 12px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.TableRowCompact {
|
||||
height: 8px;
|
||||
max-height: 8px;
|
||||
font-size: 12px !important;
|
||||
line-height: 8px;
|
||||
}
|
||||
|
||||
.Tooltip {
|
||||
white-space: pre-line; // or pre-wrap
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
|
||||
.Table {
|
||||
font-size: 12px;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.Table .p-datatable-tbody > tr > td {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.Tooltip {
|
||||
white-space: pre-line;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ export const SystemStructuresContent: React.FC<SystemStructuresContentProps> = (
|
||||
size="small"
|
||||
sortMode="single"
|
||||
rowHover
|
||||
style={{ tableLayout: 'fixed', width: '100%' }}
|
||||
onRowClick={handleRowClick}
|
||||
onRowDoubleClick={handleRowDoubleClick}
|
||||
rowClassName={rowData => {
|
||||
@@ -74,11 +75,56 @@ export const SystemStructuresContent: React.FC<SystemStructuresContentProps> = (
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Column header="Type" body={renderTypeCell} style={{ width: '160px' }} />
|
||||
<Column field="name" header="Name" style={{ width: '120px' }} />
|
||||
<Column header="Owner" body={renderOwnerCell} style={{ width: '120px' }} />
|
||||
<Column field="status" header="Status" style={{ width: '100px' }} />
|
||||
<Column header="Timer" body={renderTimerCell} style={{ width: '110px' }} />
|
||||
<Column
|
||||
header="Type"
|
||||
body={renderTypeCell}
|
||||
style={{
|
||||
width: '160px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
field="name"
|
||||
header="Name"
|
||||
style={{
|
||||
width: '120px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
header="Owner"
|
||||
body={renderOwnerCell}
|
||||
style={{
|
||||
width: '120px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
field="status"
|
||||
header="Status"
|
||||
style={{
|
||||
width: '100px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
header="Timer"
|
||||
body={renderTimerCell}
|
||||
style={{
|
||||
width: '110px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
body={(rowData: StructureItem) => (
|
||||
<i
|
||||
@@ -90,7 +136,13 @@ export const SystemStructuresContent: React.FC<SystemStructuresContentProps> = (
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
style={{ width: '40px', textAlign: 'center' }}
|
||||
style={{
|
||||
width: '40px',
|
||||
textAlign: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Button } from 'primereact/button';
|
||||
import { AutoComplete } from 'primereact/autocomplete';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { StructureItem, StructureStatus, statusesRequiringTimer, formatToISO } from '../helpers';
|
||||
@@ -53,7 +54,9 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
|
||||
// If user typed more text but we have partial match in prevResults
|
||||
if (newQuery.startsWith(prevQuery) && prevResults.length > 0) {
|
||||
const filtered = prevResults.filter(item => item.label.toLowerCase().includes(newQuery.toLowerCase()));
|
||||
const filtered = prevResults.filter(item =>
|
||||
item.label.toLowerCase().includes(newQuery.toLowerCase()),
|
||||
);
|
||||
setOwnerSuggestions(filtered);
|
||||
return;
|
||||
}
|
||||
@@ -74,12 +77,18 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
[prevQuery, prevResults, outCommand],
|
||||
);
|
||||
|
||||
const handleChange = (field: keyof StructureItem, val: string) => {
|
||||
const handleChange = (field: keyof StructureItem, val: string | Date) => {
|
||||
// If we want to forbid changing structureTypeId or structureType from the dialog, do so here:
|
||||
if (field === 'structureTypeId' || field === 'structureType') return;
|
||||
|
||||
setEditData(prev => {
|
||||
if (!prev) return null;
|
||||
|
||||
// If this is the endTime (Date from Calendar), we store as ISO or string:
|
||||
if (field === 'endTime' && val instanceof Date) {
|
||||
return { ...prev, endTime: val.toISOString() };
|
||||
}
|
||||
|
||||
return { ...prev, [field]: val };
|
||||
});
|
||||
};
|
||||
@@ -87,7 +96,9 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
// when user picks a corp from auto-complete
|
||||
const handleSelectOwner = (selected: { label: string; value: string }) => {
|
||||
setOwnerInput(selected.label);
|
||||
setEditData(prev => (prev ? { ...prev, ownerName: selected.label, ownerId: selected.value } : null));
|
||||
setEditData(prev =>
|
||||
prev ? { ...prev, ownerName: selected.label, ownerId: selected.value } : null,
|
||||
);
|
||||
};
|
||||
|
||||
const handleStatusChange = (val: string) => {
|
||||
@@ -107,7 +118,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
if (!statusesRequiringTimer.includes(editData.status)) {
|
||||
editData.endTime = '';
|
||||
} else if (editData.endTime) {
|
||||
// convert to full ISO
|
||||
// convert to full ISO if not already
|
||||
editData.endTime = formatToISO(editData.endTime);
|
||||
}
|
||||
|
||||
@@ -146,7 +157,11 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
<div className="flex flex-col gap-2 text-[14px]">
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Type:</span>
|
||||
<input readOnly className="p-inputtext p-component cursor-not-allowed" value={editData.structureType ?? ''} />
|
||||
<input
|
||||
readOnly
|
||||
className="p-inputtext p-component cursor-not-allowed"
|
||||
value={editData.structureType ?? ''}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Name:</span>
|
||||
@@ -186,17 +201,21 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
<option value="Reinforced">Reinforced</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{statusesRequiringTimer.includes(editData.status) && (
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>End Time:</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
className="p-inputtext p-component"
|
||||
value={editData.endTime ? editData.endTime.replace('Z', '').slice(0, 16) : ''}
|
||||
onChange={e => handleChange('endTime', e.target.value)}
|
||||
<span>Timer <br /> (Eve Time):</span>
|
||||
<Calendar
|
||||
value={editData.endTime ? new Date(editData.endTime) : undefined}
|
||||
onChange={(e) => handleChange('endTime', e.value ?? '')}
|
||||
showTime
|
||||
hourFormat="24"
|
||||
dateFormat="yy-mm-dd"
|
||||
showIcon
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label className="grid grid-cols-[100px_1fr] gap-2 items-start mt-2">
|
||||
<span className="mt-1">Notes:</span>
|
||||
<textarea
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './SystemInfo';
|
||||
export * from './RoutesWidget';
|
||||
export * from './SystemSignatures';
|
||||
export * from './SystemStructures';
|
||||
export * from './SystemKills';
|
||||
|
||||
@@ -2,7 +2,9 @@ import clsx from 'clsx';
|
||||
import classes from './PassageCard.module.scss';
|
||||
import { Passage } from '@/hooks/Mapper/types';
|
||||
import { TimeAgo } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type PassageCardType = {
|
||||
// compact?: boolean;
|
||||
@@ -26,6 +28,11 @@ export const getShipName = (name: string) => {
|
||||
export const PassageCard = ({ inserted_at, character: char, ship }: PassageCardType) => {
|
||||
const isOwn = false;
|
||||
|
||||
const insertedAt = useMemo(() => {
|
||||
const date = new Date(inserted_at);
|
||||
return date.toLocaleString();
|
||||
}, [inserted_at]);
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')}>
|
||||
<div className="flex flex-col justify-between px-2 py-1 gap-1">
|
||||
@@ -76,7 +83,9 @@ export const PassageCard = ({ inserted_at, character: char, ship }: PassageCardT
|
||||
{/*time and class*/}
|
||||
<div className="flex justify-between">
|
||||
<span className="text-stone-400">
|
||||
<TimeAgo timestamp={inserted_at} />
|
||||
<WdTooltipWrapper content={insertedAt}>
|
||||
<TimeAgo timestamp={inserted_at} />
|
||||
</WdTooltipWrapper>
|
||||
</span>
|
||||
|
||||
<div className="text-stone-400">{kgToTons(parseInt(ship.ship_type_info.mass))}</div>
|
||||
|
||||
@@ -3,7 +3,12 @@ import { Dialog } from 'primereact/dialog';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { TabPanel, TabView } from 'primereact/tabview';
|
||||
import { PrettySwitchbox } from './components';
|
||||
import { InterfaceStoredSettingsProps, useMapRootState, InterfaceStoredSettings } from '@/hooks/Mapper/mapRootProvider';
|
||||
import {
|
||||
InterfaceStoredSettingsProps,
|
||||
useMapRootState,
|
||||
InterfaceStoredSettings,
|
||||
AvailableThemes
|
||||
} from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { WidgetsSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components/WidgetsSettings/WidgetsSettings.tsx';
|
||||
@@ -112,8 +117,8 @@ const UI_CHECKBOXES_PROPS: SettingsListItem[] = [
|
||||
];
|
||||
|
||||
const THEME_OPTIONS = [
|
||||
{ label: 'Default', value: 'default' },
|
||||
{ label: 'Pathfinder', value: 'pathfinder' },
|
||||
{ label: 'Default', value: AvailableThemes.default },
|
||||
{ label: 'Pathfinder', value: AvailableThemes.pathfinder },
|
||||
];
|
||||
|
||||
const THEME_SETTING: SettingsListItem = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import styles from './MapSettings.module.scss';
|
||||
|
||||
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
interface PrettySwitchboxProps {
|
||||
|
||||
@@ -53,6 +53,7 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
...out,
|
||||
custom_info: JSON.stringify({
|
||||
k162Type: values.k162Type,
|
||||
isEOL: values.isEOL,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -127,14 +128,17 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
const { linked_system, custom_info, ...rest } = signatureData;
|
||||
|
||||
let k162Type = null;
|
||||
let isEOL = false;
|
||||
if (custom_info) {
|
||||
const customInfo = JSON.parse(custom_info);
|
||||
k162Type = customInfo.k162Type;
|
||||
isEOL = customInfo.isEOL;
|
||||
}
|
||||
|
||||
signatureForm.reset({
|
||||
linked_system: linked_system?.solar_system_id.toString() ?? undefined,
|
||||
k162Type: k162Type,
|
||||
isEOL: isEOL,
|
||||
...rest,
|
||||
});
|
||||
}, [signatureForm, signatureData]);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { InputSwitch } from 'primereact/inputswitch';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
|
||||
export interface SignatureEOLCheckboxProps {
|
||||
name: string;
|
||||
defaultValue?: boolean;
|
||||
}
|
||||
|
||||
export const SignatureEOLCheckbox = ({ name, defaultValue = false }: SignatureEOLCheckboxProps) => {
|
||||
const { control } = useFormContext<SystemSignature>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
// @ts-ignore
|
||||
name={name}
|
||||
control={control}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field }) => {
|
||||
return <InputSwitch className="my-1" checked={!!field.value} onChange={e => field.onChange(e.value)} />;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SignatureEOLCheckbox.tsx';
|
||||
@@ -3,6 +3,7 @@ import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { SignatureWormholeTypeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureWormholeTypeSelect';
|
||||
import { SignatureK162TypeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
|
||||
import { SignatureLeadsToSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLeadsToSelect';
|
||||
import { SignatureEOLCheckbox } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureEOLCheckbox';
|
||||
|
||||
export const SignatureGroupContentWormholes = () => {
|
||||
const { watch } = useFormContext<SystemSignature>();
|
||||
@@ -26,6 +27,11 @@ export const SignatureGroupContentWormholes = () => {
|
||||
<span>Leads To:</span>
|
||||
<SignatureLeadsToSelect name="linked_system" />
|
||||
</label>
|
||||
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
|
||||
<span>EOL:</span>
|
||||
<SignatureEOLCheckbox name="isEOL" />
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,100 +3,8 @@ import clsx from 'clsx';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useMemo } from 'react';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { WHClassView } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
export const k162Types = [
|
||||
{
|
||||
label: 'Hi-Sec',
|
||||
value: 'hs',
|
||||
whClassName: 'A641',
|
||||
},
|
||||
{
|
||||
label: 'Low-Sec',
|
||||
value: 'ls',
|
||||
whClassName: 'J377',
|
||||
},
|
||||
{
|
||||
label: 'Null-Sec',
|
||||
value: 'ns',
|
||||
whClassName: 'C248',
|
||||
},
|
||||
{
|
||||
label: 'C1',
|
||||
value: 'c1',
|
||||
whClassName: 'E004',
|
||||
},
|
||||
{
|
||||
label: 'C2',
|
||||
value: 'c2',
|
||||
whClassName: 'D382',
|
||||
},
|
||||
{
|
||||
label: 'C3',
|
||||
value: 'c3',
|
||||
whClassName: 'L477',
|
||||
},
|
||||
{
|
||||
label: 'C4',
|
||||
value: 'c4',
|
||||
whClassName: 'M001',
|
||||
},
|
||||
{
|
||||
label: 'C5',
|
||||
value: 'c5',
|
||||
whClassName: 'L614',
|
||||
},
|
||||
{
|
||||
label: 'C6',
|
||||
value: 'c6',
|
||||
whClassName: 'G008',
|
||||
},
|
||||
{
|
||||
label: 'C13',
|
||||
value: 'c13',
|
||||
whClassName: 'A009',
|
||||
},
|
||||
{
|
||||
label: 'Thera',
|
||||
value: 'thera',
|
||||
whClassName: 'F353',
|
||||
},
|
||||
{
|
||||
label: 'Pochven',
|
||||
value: 'pochven',
|
||||
whClassName: 'F216',
|
||||
},
|
||||
];
|
||||
|
||||
const renderNoValue = () => <div className="flex gap-2 items-center">-Unknown-</div>;
|
||||
|
||||
// @ts-ignore
|
||||
export const renderK162Type = (option: {
|
||||
label?: string;
|
||||
value: string;
|
||||
security?: string;
|
||||
system_class?: number;
|
||||
whClassName?: string;
|
||||
}) => {
|
||||
if (!option) {
|
||||
return renderNoValue();
|
||||
}
|
||||
const { value, whClassName = '' } = option;
|
||||
if (value == null) {
|
||||
return renderNoValue();
|
||||
}
|
||||
|
||||
return (
|
||||
<WHClassView
|
||||
classNameWh="!text-[11px] !font-bold"
|
||||
hideWhClassName
|
||||
hideTooltip
|
||||
whClassName={whClassName}
|
||||
noOffset
|
||||
useShortTitle
|
||||
/>
|
||||
);
|
||||
};
|
||||
import { K162_TYPES } from '@/hooks/Mapper/constants.ts';
|
||||
import { renderK162Type } from '.';
|
||||
|
||||
export interface SignatureK162TypeSelectProps {
|
||||
name: string;
|
||||
@@ -107,7 +15,7 @@ export const SignatureK162TypeSelect = ({ name, defaultValue = '' }: SignatureK1
|
||||
const { control } = useFormContext<SystemSignature>();
|
||||
|
||||
const options = useMemo(() => {
|
||||
return [{ value: null }, ...k162Types];
|
||||
return [{ value: null }, ...K162_TYPES];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './SignatureK162TypeSelect.tsx';
|
||||
export * from './renderK162Type.tsx';
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { WHClassView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { K162Type } from '@/hooks/Mapper/constants.ts';
|
||||
|
||||
const renderNoValue = () => <div className="flex gap-2 items-center">-Unknown-</div>;
|
||||
|
||||
export const renderK162Type = (option: K162Type) => {
|
||||
if (!option) {
|
||||
return renderNoValue();
|
||||
}
|
||||
|
||||
const { value, whClassName = '' } = option;
|
||||
if (value == null) {
|
||||
return renderNoValue();
|
||||
}
|
||||
|
||||
return (
|
||||
<WHClassView
|
||||
classNameWh="!text-[11px] !font-bold"
|
||||
hideWhClassName
|
||||
hideTooltip
|
||||
whClassName={whClassName}
|
||||
noOffset
|
||||
useShortTitle
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -14,9 +14,10 @@ import classes from './MapWrapper.module.scss';
|
||||
import { Connections } from '@/hooks/Mapper/components/mapRootContent/components/Connections';
|
||||
import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { Node, XYPosition } from 'reactflow';
|
||||
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useCommandsSystems } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { emitMapEvent, useMapEventListener } from '@/hooks/Mapper/events';
|
||||
|
||||
import { STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/MapRootProvider';
|
||||
@@ -32,7 +33,7 @@ export const MapWrapper = () => {
|
||||
const {
|
||||
update,
|
||||
outCommand,
|
||||
data: { selectedConnections, selectedSystems, hubs, systems },
|
||||
data: { selectedConnections, selectedSystems, hubs, systems, connections, linkSignatureToSystem },
|
||||
interfaceSettings: {
|
||||
isShowMenu,
|
||||
isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap,
|
||||
@@ -46,25 +47,19 @@ export const MapWrapper = () => {
|
||||
const { deleteSystems } = useDeleteSystems();
|
||||
const { mapRef, runCommand } = useCommonMapEventProcessor();
|
||||
|
||||
const { updateLinkSignatureToSystem } = useCommandsSystems();
|
||||
const { open, ...systemContextProps } = useContextMenuSystemHandlers({ systems, hubs, outCommand });
|
||||
const { handleSystemMultipleContext, ...systemMultipleCtxProps } = useContextMenuSystemMultipleHandlers();
|
||||
|
||||
const [openSettings, setOpenSettings] = useState<string | null>(null);
|
||||
const [openLinkSignatures, setOpenLinkSignatures] = useState<any | null>(null);
|
||||
const [openCustomLabel, setOpenCustomLabel] = useState<string | null>(null);
|
||||
const [openAddSystem, setOpenAddSystem] = useState<XYPosition | null>(null);
|
||||
const [selectedConnection, setSelectedConnection] = useState<SolarSystemConnection | null>(null);
|
||||
|
||||
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems });
|
||||
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems };
|
||||
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, connections, deleteSystems });
|
||||
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, connections, deleteSystems };
|
||||
|
||||
useMapEventListener(event => {
|
||||
switch (event.name) {
|
||||
case Commands.linkSignatureToSystem:
|
||||
setOpenLinkSignatures(event.data);
|
||||
return true;
|
||||
}
|
||||
|
||||
runCommand(event);
|
||||
});
|
||||
|
||||
@@ -130,6 +125,11 @@ export const MapWrapper = () => {
|
||||
setOpenAddSystem(coordinates);
|
||||
}, []);
|
||||
|
||||
const canRemoveConnection = useCallback((connectionId: string) => {
|
||||
const { connections } = ref.current;
|
||||
return !connections.some(x => x.id === connectionId);
|
||||
}, []);
|
||||
|
||||
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
|
||||
async item => {
|
||||
if (ref.current.systems.some(x => x.system_static_info.solar_system_id === item.value)) {
|
||||
@@ -166,6 +166,7 @@ export const MapWrapper = () => {
|
||||
isSoftBackground={isSoftBackground}
|
||||
theme={theme}
|
||||
onAddSystem={onAddSystem}
|
||||
canRemoveConnection={canRemoveConnection}
|
||||
/>
|
||||
|
||||
{openSettings != null && (
|
||||
@@ -176,8 +177,8 @@ export const MapWrapper = () => {
|
||||
<SystemCustomLabelDialog systemId={openCustomLabel} visible setVisible={() => setOpenCustomLabel(null)} />
|
||||
)}
|
||||
|
||||
{openLinkSignatures != null && (
|
||||
<SystemLinkSignatureDialog data={openLinkSignatures} setVisible={() => setOpenLinkSignatures(null)} />
|
||||
{linkSignatureToSystem != null && (
|
||||
<SystemLinkSignatureDialog data={linkSignatureToSystem} setVisible={() => updateLinkSignatureToSystem(null)} />
|
||||
)}
|
||||
|
||||
<AddSystemDialog
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
border-style: solid;
|
||||
border-color: #272727;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border-radius: 3px;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.CharName {
|
||||
@@ -26,7 +26,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.CharIcon {}
|
||||
.CharIcon {
|
||||
border-radius: 0 !important;
|
||||
border: 1px solid #2b2b2b;
|
||||
}
|
||||
|
||||
.CharRow {
|
||||
display: grid;
|
||||
|
||||
@@ -3,13 +3,13 @@ import clsx from 'clsx';
|
||||
import classes from './CharacterCard.module.scss';
|
||||
import { SystemView } from '@/hooks/Mapper/components/ui-kit/SystemView';
|
||||
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
|
||||
type CharacterCardProps = {
|
||||
compact?: boolean;
|
||||
showShipName?: boolean;
|
||||
showSystem?: boolean;
|
||||
showShipName?: boolean;
|
||||
useSystemsCache?: boolean;
|
||||
} & CharacterTypeRaw &
|
||||
WithIsOwnCharacter;
|
||||
@@ -18,16 +18,16 @@ const SHIP_NAME_RX = /u'|'/g;
|
||||
export const getShipName = (name: string) => {
|
||||
return name
|
||||
.replace(SHIP_NAME_RX, '')
|
||||
.replace(/\\u([\dA-Fa-f]{4})/g, (_, grp) => {
|
||||
return String.fromCharCode(parseInt(grp, 16));
|
||||
})
|
||||
.replace(/\\x([\dA-Fa-f]{2})/g, (_, grp) => {
|
||||
return String.fromCharCode(parseInt(grp, 16));
|
||||
});
|
||||
.replace(/\\u([\dA-Fa-f]{4})/g, (_, grp) =>
|
||||
String.fromCharCode(parseInt(grp, 16))
|
||||
)
|
||||
.replace(/\\x([\dA-Fa-f]{2})/g, (_, grp) =>
|
||||
String.fromCharCode(parseInt(grp, 16))
|
||||
);
|
||||
};
|
||||
|
||||
export const CharacterCard = ({
|
||||
compact,
|
||||
compact = false,
|
||||
isOwn,
|
||||
showSystem,
|
||||
showShipName,
|
||||
@@ -41,57 +41,105 @@ export const CharacterCard = ({
|
||||
});
|
||||
}, [char]);
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')} onClick={handleSelect}>
|
||||
<div className="flex px-2 py-1 gap-1">
|
||||
{!compact && (
|
||||
<span
|
||||
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
|
||||
style={{ backgroundImage: `url(https://images.evetech.net/characters/${char.eve_id}/portrait)` }}
|
||||
const shipNameText = char.ship?.ship_name ? getShipName(char.ship.ship_name) : '';
|
||||
const tickerText = char.alliance_id ? char.alliance_ticker : char.corporation_ticker;
|
||||
const shipType = char.ship?.ship_type_info?.name;
|
||||
const locationShown = showSystem && char.location?.solar_system_id;
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.CharacterCard, 'w-full text-xs box-border')}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<div className="w-full px-2 py-1 flex items-center gap-2" style={{ minWidth: 0 }}>
|
||||
<img
|
||||
src={`https://images.evetech.net/characters/${char.eve_id}/portrait`}
|
||||
alt={`${char.name} portrait`}
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
borderRadius: 0,
|
||||
flexShrink: 0,
|
||||
border: '1px solid #2b2b2b',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div
|
||||
className={clsx(classes.CharRow, 'w-full', {
|
||||
[classes.TwoColumns]: !char.ship,
|
||||
[classes.ThreeColumns]: char.ship,
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={clsx(classes.CharName, 'text-ellipsis overflow-hidden whitespace-nowrap', {
|
||||
[classes.CardBorderLeftIsOwn]: isOwn,
|
||||
})}
|
||||
title={char.name}
|
||||
>
|
||||
{char.name}
|
||||
</span>
|
||||
|
||||
{char.alliance_id && <span className="text-gray-400">[{char.alliance_ticker}]</span>}
|
||||
{!char.alliance_id && <span className="text-gray-400">[{char.corporation_ticker}]</span>}
|
||||
|
||||
{char.ship?.ship_type_info && (
|
||||
<div
|
||||
className="flex-grow text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
title={char.ship.ship_type_info.name}
|
||||
>
|
||||
{char.ship.ship_type_info.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showShipName && !compact && char.ship?.ship_name && (
|
||||
<div className="grid w-full">
|
||||
<span className="text-ellipsis overflow-hidden whitespace-nowrap">
|
||||
{getShipName(char.ship.ship_name)}
|
||||
<div className="flex flex-grow overflow-hidden text-left" style={{ minWidth: 0 }}>
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className={clsx(isOwn ? 'text-orange-400' : 'text-gray-200')}>
|
||||
{char.name}
|
||||
</span>{" "}
|
||||
<span className="text-gray-400">
|
||||
{(!locationShown && showShipName && shipNameText)
|
||||
? `- ${shipNameText}`
|
||||
: `[${tickerText}]`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showSystem && !compact && char.location?.solar_system_id && (
|
||||
<SystemView systemId={char.location.solar_system_id.toString()} useSystemsCache={useSystemsCache} />
|
||||
</div>
|
||||
{shipType && (
|
||||
<div
|
||||
className="text-gray-300 overflow-hidden text-ellipsis whitespace-nowrap flex-shrink-0"
|
||||
style={{ maxWidth: '120px' }}
|
||||
title={shipType}
|
||||
>
|
||||
{shipType}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.CharacterCard, 'w-full text-xs box-border')}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<div className="w-full px-2 py-1 flex items-center gap-2" style={{ minWidth: 0 }}>
|
||||
<span
|
||||
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
|
||||
style={{
|
||||
backgroundImage: `url(https://images.evetech.net/characters/${char.eve_id}/portrait)`,
|
||||
minWidth: '33px',
|
||||
minHeight: '33px',
|
||||
width: '33px',
|
||||
height: '33px',
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col flex-grow overflow-hidden" style={{ minWidth: 0 }}>
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className={clsx(isOwn ? 'text-orange-400' : 'text-gray-200')}>
|
||||
{char.name}
|
||||
</span>{" "}
|
||||
<span className="text-gray-400">[{tickerText}]</span>
|
||||
</div>
|
||||
{locationShown ? (
|
||||
<div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<SystemView
|
||||
systemId={char?.location?.solar_system_id?.toString() || ''}
|
||||
useSystemsCache={useSystemsCache}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
shipNameText && (
|
||||
<div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{shipNameText}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{shipType && (
|
||||
<div className="flex-shrink-0 self-start">
|
||||
<div
|
||||
className="text-gray-300 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
style={{ maxWidth: '200px' }}
|
||||
title={shipType}
|
||||
>
|
||||
{shipType}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,12 +2,12 @@ import classes from './WdCheckbox.module.scss';
|
||||
import { Checkbox, CheckboxChangeEvent } from 'primereact/checkbox';
|
||||
import { WithClassName } from '@/hooks/Mapper/types/common';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
let counter = 0;
|
||||
|
||||
export interface WdCheckboxProps {
|
||||
label: string;
|
||||
label: React.ReactNode | string;
|
||||
classNameLabel?: string;
|
||||
value: boolean;
|
||||
labelSide?: 'left' | 'right';
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { WdCheckbox, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
/**
|
||||
* Display modes for the responsive checkbox.
|
||||
*
|
||||
* - "full": show the full label (e.g. "Show offline" or "Show ship name")
|
||||
* - "abbr": show the abbreviated label (e.g. "Offline" or "Ship name")
|
||||
* - "checkbox": show only the checkbox (no text)
|
||||
* - "hide": do not render the checkbox at all
|
||||
*/
|
||||
export type WdDisplayMode = 'full' | 'abbr' | 'checkbox' | 'hide';
|
||||
|
||||
export interface WdResponsiveCheckboxProps {
|
||||
tooltipContent: string;
|
||||
size: 'xs' | 'normal' | 'm';
|
||||
labelFull: string;
|
||||
labelAbbreviated: string;
|
||||
value: boolean;
|
||||
onChange: () => void;
|
||||
classNameLabel?: string;
|
||||
containerClassName?: string;
|
||||
labelSide?: 'left' | 'right';
|
||||
displayMode: WdDisplayMode;
|
||||
}
|
||||
|
||||
export const WdResponsiveCheckbox: React.FC<WdResponsiveCheckboxProps> = ({
|
||||
tooltipContent,
|
||||
size,
|
||||
labelFull,
|
||||
labelAbbreviated,
|
||||
value,
|
||||
onChange,
|
||||
classNameLabel,
|
||||
containerClassName,
|
||||
labelSide = 'left',
|
||||
displayMode,
|
||||
}) => {
|
||||
if (displayMode === 'hide') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label =
|
||||
displayMode === 'full'
|
||||
? labelFull
|
||||
: displayMode === 'abbr'
|
||||
? labelAbbreviated
|
||||
: displayMode === 'checkbox'
|
||||
? ''
|
||||
: labelFull;
|
||||
|
||||
const checkbox = (
|
||||
<div className={clsx('min-w-0', containerClassName)}>
|
||||
<WdCheckbox
|
||||
size={size}
|
||||
labelSide={labelSide}
|
||||
label={label}
|
||||
value={value}
|
||||
classNameLabel={classNameLabel}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return tooltipContent ? <WdTooltipWrapper content={tooltipContent}>{checkbox}</WdTooltipWrapper> : checkbox;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './WdResponsiveCheckbox';
|
||||
@@ -1,20 +1,8 @@
|
||||
import React, {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
MouseEvent,
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import classes from './WdTooltip.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { WithClassName } from '@/hooks/Mapper/types/common.ts';
|
||||
import classes from './WdTooltip.module.scss';
|
||||
|
||||
export enum TooltipPosition {
|
||||
default = 'default',
|
||||
@@ -24,16 +12,12 @@ export enum TooltipPosition {
|
||||
bottom = 'bottom',
|
||||
}
|
||||
|
||||
export interface TooltipProps {
|
||||
export interface TooltipProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'content'> {
|
||||
position?: TooltipPosition;
|
||||
offset?: number;
|
||||
content: (() => React.ReactNode) | React.ReactNode;
|
||||
targetSelector?: string;
|
||||
}
|
||||
|
||||
export interface WdTooltipHandlers {
|
||||
show: MouseEventHandler;
|
||||
hide: MouseEventHandler;
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
export interface OffsetPosition {
|
||||
@@ -41,26 +25,52 @@ export interface OffsetPosition {
|
||||
left: number;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
export const WdTooltip = forwardRef((props: TooltipProps & WithClassName, ref: ForwardedRef<WdTooltipHandlers>) => {
|
||||
const { content, targetSelector, position: tPosition = TooltipPosition.default, className, offset = 5 } = props;
|
||||
export interface WdTooltipHandlers {
|
||||
show: (e?: React.MouseEvent) => void;
|
||||
hide: () => void;
|
||||
getIsMouseInside: () => boolean;
|
||||
}
|
||||
|
||||
interface TriggerInfo {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
rect: DOMRect;
|
||||
}
|
||||
|
||||
const LEAVE_DELAY = 100;
|
||||
|
||||
export const WdTooltip = forwardRef(function WdTooltip(
|
||||
{
|
||||
content,
|
||||
targetSelector,
|
||||
position: tPosition = TooltipPosition.default,
|
||||
offset = 5,
|
||||
interactive = false,
|
||||
className,
|
||||
...restProps
|
||||
}: TooltipProps,
|
||||
ref: ForwardedRef<WdTooltipHandlers>,
|
||||
) {
|
||||
// Always initialize position so we never have a null value.
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [position, setPosition] = useState<OffsetPosition | null>(null);
|
||||
const [ev, setEv] = useState<MouseEvent>();
|
||||
const [pos, setPos] = useState<OffsetPosition | null>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const calcTooltipPosition = useCallback(({ x, y }: { x: number; y: number }) => {
|
||||
let newLeft = x;
|
||||
let newTop = y;
|
||||
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
|
||||
|
||||
if (!tooltipRef.current) {
|
||||
return { left: newLeft, top: newTop };
|
||||
}
|
||||
const [triggerInfo, setTriggerInfo] = useState<TriggerInfo | null>(null);
|
||||
|
||||
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const calcTooltipPosition = useCallback(({ x, y }: { x: number; y: number }) => {
|
||||
if (!tooltipRef.current) return { left: x, top: y };
|
||||
|
||||
const tooltipWidth = tooltipRef.current.offsetWidth;
|
||||
const tooltipHeight = tooltipRef.current.offsetHeight;
|
||||
|
||||
let newLeft = x;
|
||||
let newTop = y;
|
||||
|
||||
if (newLeft < 0) {
|
||||
newLeft = 10;
|
||||
}
|
||||
@@ -69,141 +79,218 @@ export const WdTooltip = forwardRef((props: TooltipProps & WithClassName, ref: F
|
||||
newTop = 10;
|
||||
}
|
||||
|
||||
if (newLeft + tooltipWidth + 10 > window.innerWidth) {
|
||||
const rightEdge = newLeft + tooltipWidth + 10;
|
||||
if (rightEdge > window.innerWidth) {
|
||||
newLeft = window.innerWidth - tooltipWidth - 10;
|
||||
}
|
||||
if (newTop + tooltipHeight + 10 > window.innerHeight) {
|
||||
|
||||
const bottomEdge = newTop + tooltipHeight + 10;
|
||||
if (bottomEdge > window.innerHeight) {
|
||||
newTop = window.innerHeight - tooltipHeight - 10;
|
||||
}
|
||||
|
||||
return { left: newLeft, top: newTop };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !ev) {
|
||||
const scheduleHide = useCallback(() => {
|
||||
if (!interactive) {
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
return;
|
||||
}
|
||||
if (!hideTimeoutRef.current) {
|
||||
hideTimeoutRef.current = setTimeout(() => {
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
}, LEAVE_DELAY);
|
||||
}
|
||||
}, [interactive]);
|
||||
|
||||
const { clientX, clientY, target } = ev;
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (e?: React.MouseEvent) => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
if (e) {
|
||||
// Use e.currentTarget (or fallback to e.target) to determine the trigger element.
|
||||
const triggerEl = (e.currentTarget as HTMLElement) || (e.target as HTMLElement);
|
||||
if (triggerEl) {
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
setTriggerInfo({ clientX: e.clientX, clientY: e.clientY, rect });
|
||||
setPos(calcTooltipPosition({ x: e.clientX, y: e.clientY }));
|
||||
}
|
||||
}
|
||||
setVisible(true);
|
||||
},
|
||||
hide: () => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
}
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
},
|
||||
getIsMouseInside: () => isMouseInsideTooltip,
|
||||
}));
|
||||
|
||||
const targetBounds = (target as HTMLElement).getBoundingClientRect();
|
||||
const tooltipBounds = tooltipRef.current.getBoundingClientRect();
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !triggerInfo) return;
|
||||
|
||||
let offsetX = clientX;
|
||||
let offsetY = clientY;
|
||||
const tooltipEl = tooltipRef.current;
|
||||
const { rect } = triggerInfo;
|
||||
let x = triggerInfo.clientX;
|
||||
let y = triggerInfo.clientY;
|
||||
|
||||
if (tPosition === TooltipPosition.left) {
|
||||
offsetX = targetBounds.left - tooltipBounds.width - offset;
|
||||
offsetY = targetBounds.y + targetBounds.height / 2 - tooltipBounds.height / 2;
|
||||
const tooltipBounds = tooltipEl.getBoundingClientRect();
|
||||
x = rect.left - tooltipBounds.width - offset;
|
||||
y = rect.top + rect.height / 2 - tooltipBounds.height / 2;
|
||||
|
||||
if (offsetX <= 0) {
|
||||
offsetX = targetBounds.left + targetBounds.width + offset;
|
||||
if (x <= 0) {
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
|
||||
setPosition(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.right) {
|
||||
offsetX = targetBounds.left + targetBounds.width + offset;
|
||||
offsetY = targetBounds.y + targetBounds.height / 2 - tooltipBounds.height / 2;
|
||||
|
||||
setPosition(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
x = rect.left + rect.width + offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.top) {
|
||||
offsetY = targetBounds.top - tooltipBounds.height - offset;
|
||||
offsetX = targetBounds.x + targetBounds.width / 2 - tooltipBounds.width / 2;
|
||||
|
||||
setPosition(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
// default case
|
||||
setPosition(calcTooltipPosition({ x: clientX, y: clientY }));
|
||||
}, [calcTooltipPosition, ev, tPosition, offset]);
|
||||
if (tPosition === TooltipPosition.bottom) {
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: e => {
|
||||
setEv(e);
|
||||
setVisible(true);
|
||||
setPosition(null);
|
||||
},
|
||||
hide: () => {
|
||||
setVisible(false);
|
||||
},
|
||||
}));
|
||||
// Default case: use stored coordinates.
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
}, [calcTooltipPosition, triggerInfo, tPosition, offset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (targetSelector == null) {
|
||||
return;
|
||||
}
|
||||
if (!targetSelector) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const targetElement = e.target as HTMLElement;
|
||||
|
||||
if (!targetElement) {
|
||||
setVisible(false);
|
||||
const handleMouseMove = (evt: MouseEvent) => {
|
||||
const targetEl = evt.target as HTMLElement | null;
|
||||
if (!targetEl) {
|
||||
scheduleHide();
|
||||
return;
|
||||
}
|
||||
|
||||
const nodesFound = [...(targetElement?.parentElement?.querySelectorAll(targetSelector) ?? [])];
|
||||
const triggerEl = targetEl.closest(targetSelector);
|
||||
const insideTooltip = interactive && tooltipRef.current?.contains(targetEl);
|
||||
|
||||
if (!nodesFound.includes(targetElement)) {
|
||||
setVisible(false);
|
||||
if (!triggerEl && !insideTooltip) {
|
||||
scheduleHide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
setVisible(true);
|
||||
if (tooltipRef.current) {
|
||||
const { clientX, clientY } = e;
|
||||
const tooltipWidth = tooltipRef.current.offsetWidth;
|
||||
const tooltipHeight = tooltipRef.current.offsetHeight;
|
||||
let newLeft = clientX + 10;
|
||||
let newTop = clientY + 10;
|
||||
if (newLeft + tooltipWidth + 10 > window.innerWidth) {
|
||||
newLeft = window.innerWidth - tooltipWidth - 10;
|
||||
|
||||
if (triggerEl && tooltipRef.current) {
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
const tooltipEl = tooltipRef.current;
|
||||
|
||||
let x = evt.clientX;
|
||||
let y = evt.clientY;
|
||||
|
||||
switch (tPosition) {
|
||||
case TooltipPosition.left:
|
||||
x = rect.left - tooltipEl.offsetWidth - offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
|
||||
if (x <= 0) {
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
break;
|
||||
case TooltipPosition.right:
|
||||
x = rect.left + rect.width + offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
break;
|
||||
case TooltipPosition.top:
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
break;
|
||||
case TooltipPosition.bottom:
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
break;
|
||||
}
|
||||
if (newTop + tooltipHeight + 10 > window.innerHeight) {
|
||||
newTop = window.innerHeight - tooltipHeight - 10;
|
||||
}
|
||||
setPosition({ top: newTop, left: newLeft });
|
||||
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
}
|
||||
};
|
||||
|
||||
const deb = debounce(handleMouseMove, 10);
|
||||
const debounced = debounce(handleMouseMove, 15);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
document.addEventListener('mousemove', deb);
|
||||
document.addEventListener('mousemove', debounced);
|
||||
return () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
document.removeEventListener('mousemove', deb);
|
||||
document.removeEventListener('mousemove', debounced);
|
||||
debounced.cancel();
|
||||
};
|
||||
}, [targetSelector]);
|
||||
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition, scheduleHide]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return createPortal(
|
||||
visible && (
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
className={clsx(
|
||||
classes.tooltip,
|
||||
'pointer-events-none',
|
||||
'absolute px-2 py-2',
|
||||
'border rounded border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
||||
{ ['invisible']: position === null },
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
top: position?.top ?? 0,
|
||||
left: position?.left ?? 0,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
>
|
||||
{typeof content === 'function' ? content() : content}
|
||||
</div>
|
||||
),
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
className={clsx(
|
||||
classes.tooltip,
|
||||
interactive ? 'pointer-events-auto' : 'pointer-events-none',
|
||||
'absolute p-1 border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
||||
className,
|
||||
pos === null ? 'invisible' : '',
|
||||
)}
|
||||
style={{
|
||||
top: pos?.top ?? 0,
|
||||
left: pos?.left ?? 0,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (interactive && hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
setIsMouseInsideTooltip(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsMouseInsideTooltip(false);
|
||||
if (interactive) {
|
||||
scheduleHide();
|
||||
}
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
{typeof content === 'function' ? content() : content}
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
});
|
||||
|
||||
WdTooltip.displayName = 'WdTooltip';
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
/* WdTooltipWrapper.module.scss */
|
||||
|
||||
.WdTooltipWrapperRoot {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.wdTooltipSizeXs {
|
||||
font-size: 0.7rem;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.wdTooltipSizeSm {
|
||||
font-size: 0.8rem;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.wdTooltipSizeMd {
|
||||
font-size: 0.9rem;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.wdTooltipSizeLg {
|
||||
font-size: 1rem !important;
|
||||
min-width: 350px;
|
||||
}
|
||||
@@ -1,49 +1,54 @@
|
||||
import React, { HTMLProps, MouseEventHandler, useCallback, useRef } from 'react';
|
||||
|
||||
import classes from './WdTooltipWrapper.module.scss';
|
||||
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts';
|
||||
import { TooltipProps, WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { forwardRef, HTMLProps, ReactNode, useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { WdTooltip, WdTooltipHandlers, TooltipProps } from '@/hooks/Mapper/components/ui-kit';
|
||||
import classes from './WdTooltipWrapper.module.scss';
|
||||
|
||||
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
|
||||
|
||||
export type WdTooltipWrapperProps = {
|
||||
content?: (() => React.ReactNode) | React.ReactNode;
|
||||
} & WithChildren &
|
||||
WithClassName &
|
||||
HTMLProps<HTMLDivElement> &
|
||||
content?: (() => ReactNode) | ReactNode;
|
||||
size?: TooltipSize;
|
||||
interactive?: boolean;
|
||||
} & Omit<HTMLProps<HTMLDivElement>, 'content' | 'size'> &
|
||||
Omit<TooltipProps, 'content'>;
|
||||
|
||||
export const WdTooltipWrapper = ({
|
||||
className,
|
||||
children,
|
||||
content,
|
||||
offset,
|
||||
position,
|
||||
targetSelector,
|
||||
...props
|
||||
}: WdTooltipWrapperProps) => {
|
||||
const tooltipRef = useRef<WdTooltipHandlers>(null);
|
||||
const handleShowDeleteTooltip: MouseEventHandler = useCallback(e => tooltipRef.current?.show(e), []);
|
||||
const handleHideDeleteTooltip: MouseEventHandler = useCallback(e => tooltipRef.current?.hide(e), []);
|
||||
export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperProps>(
|
||||
({ className, children, content, offset, position, targetSelector, interactive, size, ...props }, forwardedRef) => {
|
||||
const suffix = useMemo(() => Math.random().toString(36).slice(2, 7), []);
|
||||
const autoClass = `wdTooltipAutoTrigger-${suffix}`;
|
||||
const finalTargetSelector = targetSelector || `.${autoClass}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={clsx(classes.WdTooltipWrapperRoot, className)}
|
||||
{...props}
|
||||
{...(content && {
|
||||
onMouseEnter: handleShowDeleteTooltip,
|
||||
onMouseLeave: handleHideDeleteTooltip,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
return (
|
||||
<div className={clsx(classes.WdTooltipWrapperRoot, className)} {...props}>
|
||||
{targetSelector ? <>{children}</> : <div className={autoClass}>{children}</div>}
|
||||
|
||||
<WdTooltip
|
||||
ref={forwardedRef}
|
||||
offset={offset}
|
||||
position={position}
|
||||
content={content}
|
||||
interactive={interactive}
|
||||
targetSelector={finalTargetSelector}
|
||||
className={size ? sizeClass(size) : undefined}
|
||||
/>
|
||||
</div>
|
||||
<WdTooltip
|
||||
ref={tooltipRef}
|
||||
offset={offset}
|
||||
position={position}
|
||||
content={content}
|
||||
targetSelector={targetSelector}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
WdTooltipWrapper.displayName = 'WdTooltipWrapper';
|
||||
|
||||
function sizeClass(size: TooltipSize) {
|
||||
switch (size) {
|
||||
case 'xs':
|
||||
return classes.wdTooltipSizeXs;
|
||||
case 'sm':
|
||||
return classes.wdTooltipSizeSm;
|
||||
case 'md':
|
||||
return classes.wdTooltipSizeMd;
|
||||
case 'lg':
|
||||
return classes.wdTooltipSizeLg;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,14 +76,22 @@ export const WindowWrapper = ({ onResize, onDrag, ...window }: WindowWrapperProp
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export type ViewPortProps = { w: number; h: number };
|
||||
export type WindowsManagerOnChange = (props: { windows: WindowProps[]; viewPort: ViewPortProps }) => void;
|
||||
|
||||
type WindowManagerProps = {
|
||||
windows: WindowProps[];
|
||||
viewPort?: ViewPortProps;
|
||||
dragSelector?: string;
|
||||
onChange?(windows: WindowProps[]): void;
|
||||
onChange?: WindowsManagerOnChange;
|
||||
};
|
||||
|
||||
export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWindows, dragSelector, onChange }) => {
|
||||
export const WindowManager: React.FC<WindowManagerProps> = ({
|
||||
windows: initialWindows,
|
||||
viewPort,
|
||||
dragSelector,
|
||||
onChange,
|
||||
}) => {
|
||||
const [windows, setWindows] = useState(
|
||||
initialWindows.map((window, index) => ({
|
||||
...window,
|
||||
@@ -91,6 +99,16 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
|
||||
})),
|
||||
);
|
||||
|
||||
const refPrevSize = useRef({ w: 0, h: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewPort) {
|
||||
return;
|
||||
}
|
||||
|
||||
refPrevSize.current = viewPort;
|
||||
}, [viewPort]);
|
||||
|
||||
useEffect(() => {
|
||||
setWindows(initialWindows.slice(0));
|
||||
}, [initialWindows]);
|
||||
@@ -102,14 +120,15 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
|
||||
const startMousePositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const startWindowStateRef = useRef<{ x: number; y: number; width: number; height: number }>(DefaultWindowState);
|
||||
|
||||
const ref = useRef({ windows, onChange });
|
||||
ref.current = { windows, onChange };
|
||||
|
||||
const refPrevSize = useRef({ w: 0, h: 0 });
|
||||
const ref = useRef({ windows, viewPort, onChange });
|
||||
ref.current = { windows, viewPort, onChange };
|
||||
|
||||
const onDebouncedChange = useMemo(() => {
|
||||
return debounce(() => {
|
||||
ref.current.onChange?.(ref.current.windows);
|
||||
ref.current.onChange?.({
|
||||
windows: ref.current.windows,
|
||||
viewPort: refPrevSize.current,
|
||||
});
|
||||
}, 20);
|
||||
}, []);
|
||||
|
||||
@@ -336,7 +355,7 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
|
||||
|
||||
// Handle resize of the container and reposition windows
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
if (ref.current.viewPort == null && containerRef.current) {
|
||||
refPrevSize.current = { w: containerRef.current.clientWidth, h: containerRef.current.clientHeight };
|
||||
}
|
||||
|
||||
@@ -384,6 +403,14 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
|
||||
next.position.y = container.clientHeight - next.size.height - SNAP_GAP;
|
||||
}
|
||||
|
||||
if (next.position.y < SNAP_GAP) {
|
||||
next.position.y = 0;
|
||||
}
|
||||
|
||||
if (next.position.x < SNAP_GAP) {
|
||||
next.position.x = SNAP_GAP;
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,3 +11,5 @@ export * from './WdImgButton';
|
||||
export * from './WdTooltip';
|
||||
export * from './WdCheckbox';
|
||||
export * from './TimeAgo';
|
||||
export * from './WdTooltipWrapper';
|
||||
export * from './WdResponsiveCheckBox';
|
||||
|
||||
@@ -65,3 +65,77 @@ export const REGIONS_MAP: Record<number, Spaces> = {
|
||||
[Regions.TashMurkon]: Spaces.Amarr,
|
||||
[Regions.VergeVendor]: Spaces.Gallente,
|
||||
};
|
||||
|
||||
export type K162Type = {
|
||||
label: string;
|
||||
value: string;
|
||||
whClassName: string;
|
||||
};
|
||||
|
||||
export const K162_TYPES: K162Type[] = [
|
||||
{
|
||||
label: 'Hi-Sec',
|
||||
value: 'hs',
|
||||
whClassName: 'A641',
|
||||
},
|
||||
{
|
||||
label: 'Low-Sec',
|
||||
value: 'ls',
|
||||
whClassName: 'J377',
|
||||
},
|
||||
{
|
||||
label: 'Null-Sec',
|
||||
value: 'ns',
|
||||
whClassName: 'C248',
|
||||
},
|
||||
{
|
||||
label: 'C1',
|
||||
value: 'c1',
|
||||
whClassName: 'E004',
|
||||
},
|
||||
{
|
||||
label: 'C2',
|
||||
value: 'c2',
|
||||
whClassName: 'D382',
|
||||
},
|
||||
{
|
||||
label: 'C3',
|
||||
value: 'c3',
|
||||
whClassName: 'L477',
|
||||
},
|
||||
{
|
||||
label: 'C4',
|
||||
value: 'c4',
|
||||
whClassName: 'M001',
|
||||
},
|
||||
{
|
||||
label: 'C5',
|
||||
value: 'c5',
|
||||
whClassName: 'L614',
|
||||
},
|
||||
{
|
||||
label: 'C6',
|
||||
value: 'c6',
|
||||
whClassName: 'G008',
|
||||
},
|
||||
{
|
||||
label: 'C13',
|
||||
value: 'c13',
|
||||
whClassName: 'A009',
|
||||
},
|
||||
{
|
||||
label: 'Thera',
|
||||
value: 'thera',
|
||||
whClassName: 'F353',
|
||||
},
|
||||
{
|
||||
label: 'Pochven',
|
||||
value: 'pochven',
|
||||
whClassName: 'F216',
|
||||
},
|
||||
];
|
||||
|
||||
export const K162_TYPES_MAP: { [key: string]: K162Type } = K162_TYPES.reduce(
|
||||
(acc, x) => ({ ...acc, [x.value]: x }),
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SignatureCustomInfo } from '@/hooks/Mapper/types';
|
||||
|
||||
export const parseSignatureCustomInfo = (str: string | undefined): SignatureCustomInfo => {
|
||||
if (str == null || str === '') {
|
||||
return {};
|
||||
}
|
||||
|
||||
return JSON.parse(str);
|
||||
};
|
||||
7
assets/js/hooks/Mapper/hooks/useTheme.ts
Normal file
7
assets/js/hooks/Mapper/hooks/useTheme.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AvailableThemes, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export const useTheme = (): AvailableThemes => {
|
||||
const { interfaceSettings } = useMapRootState();
|
||||
|
||||
return interfaceSettings.theme;
|
||||
};
|
||||
@@ -1,19 +1,27 @@
|
||||
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
|
||||
import { createContext, Dispatch, ForwardedRef, forwardRef, SetStateAction, useContext, useEffect } from 'react';
|
||||
import { MapUnionTypes, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
|
||||
import {
|
||||
CommandLinkSignatureToSystem,
|
||||
MapUnionTypes,
|
||||
OutCommandHandler,
|
||||
SolarSystemConnection,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import { useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks';
|
||||
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
import {
|
||||
ToggleWidgetVisibility,
|
||||
UpdateWidgetSettingsFunc,
|
||||
useStoreWidgets,
|
||||
WindowStoreInfo,
|
||||
} from '@/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts';
|
||||
import { WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
|
||||
import { DetailedKill } from '../types/kills';
|
||||
|
||||
export type MapRootData = MapUnionTypes & {
|
||||
selectedSystems: string[];
|
||||
selectedConnections: Pick<SolarSystemConnection, 'source' | 'target'>[];
|
||||
linkSignatureToSystem: CommandLinkSignatureToSystem | null;
|
||||
detailedKills: Record<string, DetailedKill[]>;
|
||||
};
|
||||
|
||||
const INITIAL_DATA: MapRootData = {
|
||||
@@ -29,13 +37,20 @@ const INITIAL_DATA: MapRootData = {
|
||||
routes: undefined,
|
||||
kills: [],
|
||||
connections: [],
|
||||
|
||||
detailedKills: {},
|
||||
selectedSystems: [],
|
||||
selectedConnections: [],
|
||||
userPermissions: {},
|
||||
options: {},
|
||||
isSubscriptionActive: false,
|
||||
linkSignatureToSystem: null,
|
||||
};
|
||||
|
||||
export enum AvailableThemes {
|
||||
default = 'default',
|
||||
pathfinder = 'pathfinder',
|
||||
}
|
||||
|
||||
export enum InterfaceStoredSettingsProps {
|
||||
isShowMenu = 'isShowMenu',
|
||||
isShowMinimap = 'isShowMinimap',
|
||||
@@ -55,7 +70,7 @@ export type InterfaceStoredSettings = {
|
||||
isShowUnsplashedSignatures: boolean;
|
||||
isShowBackgroundPattern: boolean;
|
||||
isSoftBackground: boolean;
|
||||
theme: string;
|
||||
theme: AvailableThemes;
|
||||
};
|
||||
|
||||
export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
|
||||
@@ -66,7 +81,7 @@ export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
|
||||
isShowUnsplashedSignatures: false,
|
||||
isShowBackgroundPattern: true,
|
||||
isSoftBackground: false,
|
||||
theme: 'default',
|
||||
theme: AvailableThemes.default,
|
||||
};
|
||||
|
||||
export interface MapRootContextProps {
|
||||
@@ -77,7 +92,7 @@ export interface MapRootContextProps {
|
||||
setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>;
|
||||
windowsSettings: WindowStoreInfo;
|
||||
toggleWidgetVisibility: ToggleWidgetVisibility;
|
||||
updateWidgetSettings: UpdateWidgetSettingsFunc;
|
||||
updateWidgetSettings: WindowsManagerOnChange;
|
||||
resetWidgets: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandAddSystems, CommandRemoveSystems, CommandUpdateSystems } from '@/hooks/Mapper/types';
|
||||
import {
|
||||
CommandAddSystems,
|
||||
CommandRemoveSystems,
|
||||
CommandUpdateSystems,
|
||||
CommandLinkSignatureToSystem,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
export const useCommandsSystems = () => {
|
||||
const {
|
||||
update,
|
||||
data: { systems, systemSignatures },
|
||||
data: { systems, systemSignatures, detailedKills },
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
|
||||
const { addSystemStatic } = useLoadSystemStatic({ systems: [] });
|
||||
|
||||
const ref = useRef({ systems, systemSignatures, update, addSystemStatic });
|
||||
ref.current = { systems, systemSignatures, update, addSystemStatic };
|
||||
const ref = useRef({ systems, systemSignatures, update, addSystemStatic, detailedKills });
|
||||
ref.current = { systems, systemSignatures, update, addSystemStatic, detailedKills };
|
||||
|
||||
const addSystems = useCallback((systemsToAdd: CommandAddSystems) => {
|
||||
const { update, addSystemStatic, systems } = ref.current;
|
||||
@@ -74,5 +80,28 @@ export const useCommandsSystems = () => {
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
return { addSystems, removeSystems, updateSystems, updateSystemSignatures };
|
||||
const updateLinkSignatureToSystem = useCallback(async (command: CommandLinkSignatureToSystem) => {
|
||||
const { update } = ref.current;
|
||||
update({ linkSignatureToSystem: command }, true);
|
||||
}, []);
|
||||
|
||||
const updateDetailedKills = useCallback((newKillsMap: Record<string, DetailedKill[]>) => {
|
||||
const { update, detailedKills } = ref.current;
|
||||
|
||||
const updated = { ...detailedKills };
|
||||
for (const [systemId, killsArr] of Object.entries(newKillsMap)) {
|
||||
updated[systemId] = killsArr;
|
||||
}
|
||||
|
||||
update({ detailedKills: updated }, true);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
addSystems,
|
||||
removeSystems,
|
||||
updateSystems,
|
||||
updateSystemSignatures,
|
||||
updateLinkSignatureToSystem,
|
||||
updateDetailedKills,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ export const useMapInit = () => {
|
||||
hubs,
|
||||
user_permissions,
|
||||
options,
|
||||
is_subscription_active,
|
||||
}: CommandInit) => {
|
||||
const updateData: Partial<MapRootData> = {};
|
||||
|
||||
@@ -65,6 +66,8 @@ export const useMapInit = () => {
|
||||
updateData.options = options;
|
||||
}
|
||||
|
||||
updateData.isSubscriptionActive = is_subscription_active;
|
||||
|
||||
if (system_static_infos) {
|
||||
system_static_infos.forEach(static_info => {
|
||||
addSystemStatic(static_info);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
CommandCharactersUpdated,
|
||||
CommandCharacterUpdated,
|
||||
CommandInit,
|
||||
CommandLinkSignatureToSystem,
|
||||
CommandMapUpdated,
|
||||
CommandPresentCharacters,
|
||||
CommandRemoveConnections,
|
||||
@@ -29,10 +30,18 @@ import {
|
||||
} from './api';
|
||||
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { DetailedKill } from '../../types/kills';
|
||||
|
||||
export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const mapInit = useMapInit();
|
||||
const { addSystems, removeSystems, updateSystems, updateSystemSignatures } = useCommandsSystems();
|
||||
const {
|
||||
addSystems,
|
||||
removeSystems,
|
||||
updateSystems,
|
||||
updateSystemSignatures,
|
||||
updateLinkSignatureToSystem,
|
||||
updateDetailedKills,
|
||||
} = useCommandsSystems();
|
||||
const { addConnections, removeConnections, updateConnection } = useCommandsConnections();
|
||||
const { charactersUpdated, characterAdded, characterRemoved, characterUpdated, presentCharacters } =
|
||||
useCommandsCharacters();
|
||||
@@ -93,7 +102,9 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
break;
|
||||
|
||||
case Commands.linkSignatureToSystem: // USED
|
||||
// do nothing here
|
||||
setTimeout(() => {
|
||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||
}, 200);
|
||||
break;
|
||||
|
||||
case Commands.centerSystem: // USED
|
||||
@@ -108,6 +119,10 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.detailedKillsUpdated:
|
||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
} from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { SNAP_GAP } from '@/hooks/Mapper/components/ui-kit/WindowManager';
|
||||
import { SNAP_GAP, WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
|
||||
|
||||
export type StoredWindowProps = Omit<WindowProps, 'content'>;
|
||||
export type WindowStoreInfo = {
|
||||
version: number;
|
||||
windows: StoredWindowProps[];
|
||||
visible: WidgetsIds[];
|
||||
viewPort?: { w: number; h: number } | undefined;
|
||||
};
|
||||
export type UpdateWidgetSettingsFunc = (widgets: WindowProps[]) => void;
|
||||
export type ToggleWidgetVisibility = (widgetId: WidgetsIds) => void;
|
||||
@@ -33,7 +34,7 @@ export const useStoreWidgets = () => {
|
||||
const ref = useRef({ windowsSettings, setWindowsSettings });
|
||||
ref.current = { windowsSettings, setWindowsSettings };
|
||||
|
||||
const updateWidgetSettings: UpdateWidgetSettingsFunc = useCallback(newWindows => {
|
||||
const updateWidgetSettings: WindowsManagerOnChange = useCallback(({ windows, viewPort }) => {
|
||||
const { setWindowsSettings } = ref.current;
|
||||
|
||||
setWindowsSettings(({ version, visible /*, windows*/ }: WindowStoreInfo) => {
|
||||
@@ -41,13 +42,14 @@ export const useStoreWidgets = () => {
|
||||
version,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
windows: DEFAULT_WIDGETS.map(({ content, ...x }) => {
|
||||
const windowProp = newWindows.find(j => j.id === x.id);
|
||||
const windowProp = windows.find(j => j.id === x.id);
|
||||
if (windowProp) {
|
||||
return windowProp;
|
||||
}
|
||||
|
||||
return x;
|
||||
}),
|
||||
viewPort,
|
||||
visible,
|
||||
};
|
||||
});
|
||||
@@ -92,7 +94,7 @@ export const useStoreWidgets = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { version, windows, visible } = JSON.parse(raw) as WindowStoreInfo;
|
||||
const { version, windows, visible, viewPort } = JSON.parse(raw) as WindowStoreInfo;
|
||||
if (!version || CURRENT_WINDOWS_VERSION > version) {
|
||||
setWindowsSettings(getDefaultWidgetProps());
|
||||
}
|
||||
@@ -104,6 +106,7 @@ export const useStoreWidgets = () => {
|
||||
version: CURRENT_WINDOWS_VERSION,
|
||||
windows: out as WindowProps[],
|
||||
visible,
|
||||
viewPort,
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -2,3 +2,38 @@ export type Kill = {
|
||||
solar_system_id: number;
|
||||
kills: number;
|
||||
};
|
||||
|
||||
export interface DetailedKill {
|
||||
killmail_id: number;
|
||||
solar_system_id: number;
|
||||
kill_time?: string;
|
||||
|
||||
zkb?: Record<string, unknown>;
|
||||
|
||||
victim_char_id?: number | null;
|
||||
victim_char_name?: string;
|
||||
victim_corp_id?: number | null;
|
||||
victim_corp_ticker?: string;
|
||||
victim_corp_name?: string;
|
||||
victim_alliance_id?: number | null;
|
||||
victim_alliance_ticker?: string;
|
||||
victim_alliance_name?: string;
|
||||
victim_ship_type_id?: number | null;
|
||||
victim_ship_name?: string;
|
||||
|
||||
|
||||
final_blow_char_id?: number | null;
|
||||
final_blow_char_name?: string;
|
||||
final_blow_corp_id?: number | null;
|
||||
final_blow_corp_ticker?: string;
|
||||
final_blow_corp_name?: string;
|
||||
final_blow_alliance_id?: number | null;
|
||||
final_blow_alliance_ticker?: string;
|
||||
final_blow_alliance_name?: string;
|
||||
final_blow_ship_type_id?: number | null;
|
||||
final_blow_ship_name?: string;
|
||||
|
||||
attacker_count?: number | null;
|
||||
total_value?: number | null;
|
||||
npc?: boolean;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
|
||||
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { Kill } from '@/hooks/Mapper/types/kills.ts';
|
||||
import { UserPermissions } from '@/hooks/Mapper/types';
|
||||
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
|
||||
import { SignatureGroup, UserPermissions } from '@/hooks/Mapper/types';
|
||||
|
||||
export enum Commands {
|
||||
init = 'init',
|
||||
@@ -21,6 +21,7 @@ export enum Commands {
|
||||
updateConnection = 'update_connection',
|
||||
mapUpdated = 'map_updated',
|
||||
killsUpdated = 'kills_updated',
|
||||
detailedKillsUpdated = 'detailed_kills_updated',
|
||||
routes = 'routes',
|
||||
centerSystem = 'center_system',
|
||||
selectSystem = 'select_system',
|
||||
@@ -43,6 +44,7 @@ export type Command =
|
||||
| Commands.updateConnection
|
||||
| Commands.mapUpdated
|
||||
| Commands.killsUpdated
|
||||
| Commands.detailedKillsUpdated
|
||||
| Commands.routes
|
||||
| Commands.selectSystem
|
||||
| Commands.centerSystem
|
||||
@@ -76,9 +78,11 @@ export type CommandCharacterRemoved = CharacterTypeRaw;
|
||||
export type CommandCharacterUpdated = CharacterTypeRaw;
|
||||
export type CommandPresentCharacters = string[];
|
||||
export type CommandUpdateConnection = SolarSystemConnection;
|
||||
export type CommandSignaturesUpdated = string;
|
||||
export type CommandMapUpdated = Partial<CommandInit>;
|
||||
export type CommandRoutes = RoutesList;
|
||||
export type CommandKillsUpdated = Kill[];
|
||||
export type CommandDetailedKillsUpdated = Record<string, DetailedKill[]>;
|
||||
export type CommandSelectSystem = string | undefined;
|
||||
export type CommandCenterSystem = string | undefined;
|
||||
export type CommandLinkSignatureToSystem = {
|
||||
@@ -103,6 +107,7 @@ export interface CommandData {
|
||||
[Commands.mapUpdated]: CommandMapUpdated;
|
||||
[Commands.routes]: CommandRoutes;
|
||||
[Commands.killsUpdated]: CommandKillsUpdated;
|
||||
[Commands.detailedKillsUpdated]: CommandDetailedKillsUpdated;
|
||||
[Commands.selectSystem]: CommandSelectSystem;
|
||||
[Commands.centerSystem]: CommandCenterSystem;
|
||||
[Commands.linkSignatureToSystem]: CommandLinkSignatureToSystem;
|
||||
@@ -151,6 +156,8 @@ export enum OutCommand {
|
||||
linkSignatureToSystem = 'link_signature_to_system',
|
||||
getCorporationNames = 'get_corporation_names',
|
||||
getCorporationTicker = 'get_corporation_ticker',
|
||||
getSystemKills = 'get_system_kills',
|
||||
getSystemsKills = 'get_systems_kills',
|
||||
|
||||
// Only UI commands
|
||||
openSettings = 'open_settings',
|
||||
@@ -161,4 +168,5 @@ export enum OutCommand {
|
||||
searchSystems = 'search_systems',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type OutCommandHandler = <T = any>(event: { type: OutCommand; data: any }) => Promise<T>;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user