Compare commits

..

61 Commits

Author SHA1 Message Date
CI
5eafe59dcb chore: release version v1.43.9
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-01-30 21:55:35 +00:00
Dmitry Popov
b38bcaa8cf fix(Core): Add discord link to 'Like' icon on main interface 2025-01-30 22:55:04 +01:00
CI
8a238a447d chore: release version v1.43.8
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-01-26 16:21:18 +00:00
Dmitry Popov
3731219216 fix(Core): Update shuttered constellations (required EVE DB data update on server). 2025-01-26 17:20:28 +01:00
CI
73d5fd5f67 chore: release version v1.43.7 2025-01-26 16:12:32 +00:00
Dmitry Popov
e8e4aed6d5 Signature EOL status support (#136)
* feat(Map): Added an ability to mark signature as EOL

* chore: release version v1.39.1

* fix(Map): Add correct styles for switch

* fix(Map): Refactor signatures code. Add ability to set EOL for signature marked as EOL

* feat(Map): Added EOL status for unsplashed signatures. Show precise time for connection passages on hover.

---------

Co-authored-by: achichenkov <aleksei.chichenkov@telleqt.ai>
2025-01-26 20:12:07 +04:00
CI
63571a462f chore: release version v1.43.6
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-01-22 17:41:42 +00:00
Dmitry Popov
606add4142 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-01-22 18:40:47 +01:00
Dmitry Popov
dac480b059 fix(Widgets): Fix widgets not visible on map 2025-01-22 18:40:37 +01:00
CI
5f67cb1dd7 chore: release version v1.43.5 2025-01-22 17:07:37 +00:00
Dmitry Popov
5886fff753 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-01-22 18:06:22 +01:00
Dmitry Popov
da2e12bdd1 fix(Audit): Fix signature added/removed system name 2025-01-22 18:06:03 +01:00
CI
05c3d20e56 chore: release version v1.43.4
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-01-21 08:48:20 +00:00
guarzo
4633d26517 fix: improve structure widget styling (#127) 2025-01-21 12:47:40 +04:00
CI
30b0556d47 chore: release version v1.43.3 2025-01-21 08:46:40 +00:00
Dmitry Popov
e094378dc5 chore: release version v1.43.2 2025-01-21 09:46:09 +01:00
CI
0c48189503 chore: release version v1.43.2 2025-01-21 07:50:24 +00:00
guarzo
a5c346627a fix: prevent constraint error for follow/toggle (#132) 2025-01-21 11:49:58 +04:00
CI
4e526040bf chore: release version v1.43.1
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-20 23:28:45 +00:00
Dmitry Popov
869c25cd60 chore: release version v1.42.4 2025-01-21 00:27:49 +01:00
CI
6aac698cd8 chore: release version v1.43.0 2025-01-20 23:04:11 +00:00
guarzo
230016b90f feat: add news post for structures widget (#131) 2025-01-21 03:03:31 +04:00
CI
4b1aef8dd9 chore: release version v1.42.5 2025-01-20 23:01:42 +00:00
Dmitry Popov
d34509d7a0 fix(Map): Fix link signatures on splash. Fix deleting connection on locked system remove. 2025-01-20 23:59:58 +01:00
CI
fca98ec232 chore: release version v1.42.4
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-20 10:43:19 +00:00
Dmitry Popov
e2814e95bd fix: Fix system statics list (required EVE DB data update). Add system name to signature added/removed audit log 2025-01-20 11:42:38 +01:00
CI
68a3f84704 chore: release version v1.42.3
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-01-17 08:18:30 +00:00
guarzo
4bc76feefc fix: change structure tooltip to avoid paste confusion (#125)
* fix: change structure tooltip to avoid paste confusion

* fix: clarify use of evetime and use primereact calendar
2025-01-17 12:18:04 +04:00
CI
da39a55fd0 chore: release version v1.42.2
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-16 23:30:13 +00:00
Dmitry Popov
ee3cf04cd4 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-01-17 00:29:40 +01:00
Dmitry Popov
d79e7fe2ff chore: release version v1.42.0 2025-01-17 00:29:36 +01:00
CI
8de9fdef32 chore: release version v1.42.1 2025-01-16 22:29:33 +00:00
Dmitry Popov
f51deeec2d Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-01-16 23:29:07 +01:00
Dmitry Popov
a971c69a96 fix(Map): Remove linked sig ID if system containing signature removed from map 2025-01-16 23:25:59 +01:00
CI
b7995f50de chore: release version v1.42.0 2025-01-16 21:53:41 +00:00
Dmitry Popov
14997a2959 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-01-16 22:50:48 +01:00
Dmitry Popov
8fef6bcf82 feat(Audit): Add 'Signatures added/removed' map audit events 2025-01-16 22:50:44 +01:00
CI
1f82d23963 chore: release version v1.41.0 2025-01-16 19:50:07 +00:00
Dmitry Popov
28317a2431 feat(Audit): Add 'ACL added/removed' map audit events 2025-01-16 20:49:34 +01:00
CI
6aac496a57 chore: release version v1.40.7
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-01-15 14:09:50 +00:00
Aleksei Chichenkov
ac9306b713 Merge pull request #123 from guarzo/guarzo/themesimple
refactor: simplify theme scss and add typing for hook
2025-01-15 17:09:23 +03:00
Gustav
d55e804efa refactor: simplify theme scss and add typing for hook 2025-01-14 21:58:23 -05:00
CI
08407a5679 chore: release version v1.40.6 2025-01-15 02:48:14 +00:00
CI
c37d175bec chore: release version v1.40.5
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-14 22:41:08 +00:00
Dmitry Popov
69c5326e72 fix(Map): Fix follow mode 2025-01-14 23:40:40 +01:00
CI
305f63e11d chore: release version v1.40.4
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-14 21:35:49 +00:00
guarzo
698fd5e083 fix: center system is not selected text for structures (#122) 2025-01-15 01:35:24 +04:00
CI
1af8342d30 chore: release version v1.40.3 2025-01-14 20:47:50 +00:00
Dmitry Popov
68b59da78e Merge remote-tracking branch 'origin/main' 2025-01-14 21:47:22 +01:00
Dmitry Popov
e784a3f850 fix(Map): Fix system revert issues 2025-01-14 21:47:05 +01:00
CI
a45e2f3fc2 chore: release version v1.40.2 2025-01-14 20:07:10 +00:00
Dmitry Popov
8a3d920c31 fix(Map): Fix issues with splashing signatures select & sig ID in temp names 2025-01-14 21:06:41 +01:00
CI
996d7c47bd chore: release version v1.40.1
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-14 14:16:42 +00:00
Aleksei Chichenkov
8d2b9db430 Merge pull request #117 from guarzo/guarzo/import
Clean-up theme swap logic for nodes
2025-01-14 17:15:53 +03:00
CI
423ce343c7 chore: release version v1.40.0 2025-01-14 14:13:09 +00:00
Aleksei Chichenkov
1c17912d9f Merge pull request #113 from guarzo/guarzo/structure
feat(widget): add structure widget #46
2025-01-14 17:12:36 +03:00
Gustav
6714eb5d9b feat: add structure widget with timer and associated api 2025-01-14 08:49:25 -05:00
CI
1620e1fd21 chore: release version v1.39.3 2025-01-14 11:47:37 +00:00
Aleksei Chichenkov
859014874f Merge pull request #121 from wanderer-industries/windows-part-2
fix(Map): Add style of corners for windows. Add ability to reset widg…
2025-01-14 14:47:12 +03:00
achichenkov
ef44881f06 fix(Map): Add style of corners for windows. Add ability to reset widgets. A lot of refactoring 2025-01-14 14:40:06 +03:00
Gustav
b0532325fa refactor: encapsulate theme behavior toggles and fix eslint issues 2025-01-13 07:08:39 -05:00
95 changed files with 3531 additions and 1077 deletions

View File

@@ -78,22 +78,23 @@ jobs:
fetch-depth: 0
- name: 😅 Cache deps
id: cache-deps
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: cache-elixir-deps
with:
path: deps
key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }}
path: |
deps
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-${{ env.cache-name }}-
${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-
- name: 😅 Cache compiled build
id: cache-build
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: cache-compiled-build
with:
path: |
**/_build
_build
key: ${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-${{ hashFiles( '**/lib/**/*.{ex,eex}', '**/config/*.exs', '**/mix.exs' ) }}
restore-keys: |
${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-
@@ -187,6 +188,8 @@ jobs:
push: true
context: .
file: ./Dockerfile
cache-from: type=gha
cache-to: type=gha,mode=max
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ matrix.platform }}
outputs: type=image,"name=${{ env.REGISTRY_IMAGE }}",push-by-digest=true,name-canonical=true,push=true

View File

@@ -2,6 +2,226 @@
<!-- changelog -->
## [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)
### Features:
* Audit: Add 'Signatures added/removed' map audit events
## [v1.41.0](https://github.com/wanderer-industries/wanderer/compare/v1.40.7...v1.41.0) (2025-01-16)
### Features:
* Audit: Add 'ACL added/removed' map audit events
## [v1.40.7](https://github.com/wanderer-industries/wanderer/compare/v1.40.6...v1.40.7) (2025-01-15)
## [v1.40.6](https://github.com/wanderer-industries/wanderer/compare/v1.40.5...v1.40.6) (2025-01-15)
### Bug Fixes:
* Map: Fix follow mode
* center system is not selected text for structures (#122)
* Map: Fix system revert issues
* Map: Fix issues with splashing signatures select & sig ID in temp names
## [v1.40.5](https://github.com/wanderer-industries/wanderer/compare/v1.40.4...v1.40.5) (2025-01-14)
### Bug Fixes:
* Map: Fix follow mode
## [v1.40.4](https://github.com/wanderer-industries/wanderer/compare/v1.40.3...v1.40.4) (2025-01-14)
### Bug Fixes:
* center system is not selected text for structures (#122)
## [v1.40.3](https://github.com/wanderer-industries/wanderer/compare/v1.40.2...v1.40.3) (2025-01-14)
### Bug Fixes:
* Map: Fix system revert issues
## [v1.40.2](https://github.com/wanderer-industries/wanderer/compare/v1.40.1...v1.40.2) (2025-01-14)
### Bug Fixes:
* Map: Fix issues with splashing signatures select & sig ID in temp names
## [v1.40.1](https://github.com/wanderer-industries/wanderer/compare/v1.40.0...v1.40.1) (2025-01-14)
## [v1.40.0](https://github.com/wanderer-industries/wanderer/compare/v1.39.3...v1.40.0) (2025-01-14)
### Features:
* add structure widget with timer and associated api
## [v1.39.3](https://github.com/wanderer-industries/wanderer/compare/v1.39.2...v1.39.3) (2025-01-14)
### Bug Fixes:
* Map: Add style of corners for windows. Add ability to reset widgets. A lot of refactoring
## [v1.39.2](https://github.com/wanderer-industries/wanderer/compare/v1.39.1...v1.39.2) (2025-01-13)

View File

@@ -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;
}
}

View File

@@ -1,8 +1,8 @@
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
import ReactFlow, {
Background,
ConnectionMode,
Edge,
EdgeChange,
MiniMap,
Node,
NodeChange,
@@ -23,12 +23,10 @@ import {
ContextMenuConnection,
ContextMenuRoot,
SolarSystemEdge,
SolarSystemNodeDefault,
SolarSystemNodeTheme,
useContextMenuConnectionHandlers,
useContextMenuRootHandlers,
} from './components';
import { wrapNode } from './utils/wrapNode';
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
@@ -77,9 +75,6 @@ const initialEdges = [
},
];
const edgeTypes = {
floating: SolarSystemEdge,
};
@@ -89,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;
@@ -118,28 +114,26 @@ 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);
const nodeTypes = useMemo(() => {
return {
custom:
theme !== '' && theme !== 'default'
? wrapNode(SolarSystemNodeTheme)
: wrapNode(SolarSystemNodeDefault),
};
}, [theme]);
useMapHandlers(refn, onSelectionChange);
useUpdateNodes(nodes);
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
const { update } = useMapState();
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,
};
}, [nodeComponent]);
const onConnect: OnConnect = useCallback(
params => {
@@ -228,7 +222,41 @@ const MapComp = ({
onNodesChange(nextChanges);
},
[getNode, onManualDelete, onNodesChange],
[getNode, getNodes, onManualDelete, onNodesChange],
);
const handleEdgesChange = useCallback(
(changes: EdgeChange[]) => {
const nextChanges = changes.reduce((acc, change) => {
if (change.type !== 'remove') {
return [...acc, change];
}
if (canRemoveConnection?.(change.id)) {
return [...acc, change];
}
const edge = getEdge(change.id);
if (!edge) {
return [...acc, change];
}
const sourceNode = getNode(edge.source);
const targetNode = getNode(edge.target);
if (!sourceNode || !targetNode) {
return [...acc, change];
}
if (sourceNode.data.locked || targetNode.data.locked) {
return acc;
}
return [...acc, change];
}, [] as EdgeChange[]);
onEdgesChange(nextChanges);
},
[getEdge, getNode, onEdgesChange],
);
useEffect(() => {
@@ -246,14 +274,14 @@ 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
defaultViewport={getViewPortFromStore()}
edgeTypes={edgeTypes}
nodeTypes={nodeTypes}
connectionMode={ConnectionMode.Loose}
connectionMode={connectionMode}
snapToGrid
nodeDragThreshold={10}
onNodeDragStop={handleDragStop}
@@ -286,6 +314,12 @@ const MapComp = ({
maxZoom={1.5}
elevateNodesOnSelect
deleteKeyCode={['Delete']}
{...(isPanAndDrag
? {
selectionOnDrag: true,
panOnDrag: [2],
}
: {})}
// TODO need create clear example with problem with that flag
// if system is not visible edge not drawing (and any render in Custom node is not happening)
// onlyRenderVisibleElements

View File

@@ -9,6 +9,7 @@ export type MapData = MapUnionTypes & {
visibleNodes: Set<string>;
showKSpaceBG: boolean;
isThickConnections: boolean;
linkedSigEveId: string;
};
interface MapProviderProps {

View File

@@ -6,7 +6,14 @@ $pastel-green: #88b04b;
$pastel-yellow: #ffdd59;
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020; // Dark background for tooltips
$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;

View File

@@ -1,12 +1,10 @@
import { memo } from 'react';
import { Handle, Position } from 'reactflow';
import { MapSolarSystemType } from '../../map.types';
import { Handle, Position, NodeProps } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeDefault.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
import {
MARKER_BOOKMARK_BG_STYLES,
STATUS_CLASSES,
@@ -15,64 +13,35 @@ import {
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
export const SolarSystemNodeDefault = memo(props => {
const {
charactersInSystem,
classTitle,
classTitleColor,
customName,
effectName,
hasUserCharacters,
hubs,
visible,
labelCustom,
labelsInfo,
locked,
isShattered,
isThickConnections,
isWormhole,
killsCount,
killsActivityType,
regionClass,
regionName,
status,
selected,
tag,
showHandlers,
systemName,
sortedStatics,
solarSystemId,
unsplashedLeft,
unsplashedRight,
dbClick: handleDbClick,
} = useSolarSystemNode(props);
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
return (
<>
{visible && (
{nodeVars.visible && (
<div className={classes.Bookmarks}>
{labelCustom !== '' && (
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{labelCustom}</span>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
</div>
)}
{isShattered && (
{nodeVars.isShattered && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
<span className={clsx('pi pi-chart-pie', classes.icon)} />
</div>
)}
{killsCount && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[killsActivityType!])}>
{nodeVars.killsCount && (
<div 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)}>{killsCount}</span>
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div>
</div>
)}
{labelsInfo.map(x => (
{nodeVars.labelsInfo.map(x => (
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
{x.shortName}
</div>
@@ -81,19 +50,30 @@ export const SolarSystemNodeDefault = memo(props => {
)}
<div
className={clsx(classes.RootCustomNode, regionClass && classes[regionClass], classes[STATUS_CLASSES[status]], {
[classes.selected]: selected,
})}
className={clsx(
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
classes[STATUS_CLASSES[nodeVars.status]],
{
[classes.selected]: nodeVars.selected,
},
)}
>
{visible && (
{nodeVars.visible && (
<>
<div className={classes.HeadRow}>
<div className={clsx(classes.classTitle, classTitleColor, '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]')}>
{classTitle ?? '-'}
<div
className={clsx(
classes.classTitle,
nodeVars.classTitleColor,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]',
)}
>
{nodeVars.classTitle ?? '-'}
</div>
{tag != null && tag !== '' && (
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{tag}</div>
{nodeVars.tag != null && nodeVars.tag !== '' && (
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{nodeVars.tag}</div>
)}
<div
@@ -102,53 +82,55 @@ export const SolarSystemNodeDefault = memo(props => {
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap font-sans',
)}
>
{systemName}
{nodeVars.systemName}
</div>
{isWormhole && (
{nodeVars.isWormhole && (
<div className={classes.statics}>
{sortedStatics.map(whClass => (
{nodeVars.sortedStatics.map(whClass => (
<WormholeClassComp key={whClass} id={whClass} />
))}
</div>
)}
{effectName !== null && isWormhole && (
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[effectName])} />
{nodeVars.effectName !== null && nodeVars.isWormhole && (
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[nodeVars.effectName])} />
)}
</div>
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
{customName && (
{nodeVars.customName && (
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-blue-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
{customName}
{nodeVars.customName}
</div>
)}
{!isWormhole && !customName && (
{!nodeVars.isWormhole && !nodeVars.customName && (
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-stone-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
{regionName}
{nodeVars.regionName}
</div>
)}
{isWormhole && !customName && <div />}
{nodeVars.isWormhole && !nodeVars.customName && <div />}
<div className="flex items-center justify-end">
<div className="flex gap-1 items-center">
{locked && <i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />}
{nodeVars.locked && (
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
)}
{hubs.includes(solarSystemId.toString()) && (
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
)}
{charactersInSystem.length > 0 && (
{nodeVars.charactersInSystem.length > 0 && (
<div
className={clsx(classes.localCounter, {
['text-amber-300']: hasUserCharacters,
['text-amber-300']: nodeVars.hasUserCharacters,
})}
>
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
<span className="font-sans">{charactersInSystem.length}</span>
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
</div>
)}
</div>
@@ -158,19 +140,19 @@ export const SolarSystemNodeDefault = memo(props => {
)}
</div>
{visible && (
{nodeVars.visible && (
<>
{unsplashedLeft.length > 0 && (
{nodeVars.unsplashedLeft.length > 0 && (
<div className={classes.Unsplashed}>
{unsplashedLeft.map(sig => (
{nodeVars.unsplashedLeft.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
))}
</div>
)}
{unsplashedRight.length > 0 && (
{nodeVars.unsplashedRight.length > 0 && (
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
{unsplashedRight.map(sig => (
{nodeVars.unsplashedRight.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
))}
</div>
@@ -178,44 +160,44 @@ export const SolarSystemNodeDefault = memo(props => {
</>
)}
<div onMouseDownCapture={handleDbClick} className={classes.Handlers}>
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleTop, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Top}
id="a"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleRight, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Right}
id="b"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleBottom, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Bottom}
id="c"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleLeft, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Left}
id="d"
/>

View File

@@ -1,407 +1,91 @@
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
@import './SolarSystemNodeDefault.module.scss';
$pastel-blue: #5a7d9a;
$pastel-pink: #d291bc;
$pastel-green: #88b04b;
$pastel-yellow: #ffdd59;
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020; // Dark background for tooltips
/* ---------------------------
Only override what's different
--------------------------- */
/* 1) .RootCustomNode:
- new background-color using CSS var
- plus color, font-family, and font-weight */
.RootCustomNode {
display: flex;
width: 130px;
height: 34px;
flex-direction: column;
padding: 2px 6px;
font-size: 10px;
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;
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;
overflow: hidden;
&.Mataria,
&.Amarria,
&.Gallente,
&.Caldaria {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-size: cover;
background-position: 50% 50%;
z-index: -1;
background-repeat: no-repeat;
border-radius: 3px;
}
}
&.Mataria {
&::before {
background-image: url('/images/mataria-180.png');
opacity: 0.6;
background-position-x: 1px;
background-position-y: -14px;
}
}
&.Caldaria {
&::before {
background-image: url('/images/caldaria-180.png');
opacity: 0.6;
background-position-x: 1px;
background-position-y: -10px;
}
}
&.Amarria {
&::before {
opacity: 0.45;
background-image: url('/images/amarr-180.png');
background-position-x: 0;
background-position-y: -13px;
}
}
&.Gallente {
&::before {
opacity: 0.5;
background-image: url('/images/gallente-180.png');
background-position-x: 1px;
background-position-y: 0;
}
}
&.selected {
border-color: $pastel-pink;
box-shadow: 0 0 10px #9a1af1c2;
}
&.tooltip {
background-color: $tooltip-bg;
color: $text-color;
padding: 5px 10px;
border-radius: 3px;
border: 1px solid $pastel-pink;
}
&.eve-system-status-home {
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-home),
transparent
);
&.selected {
border-color: var(--eve-solar-system-status-color-home);
}
}
&.eve-system-status-friendly {
border: 1px solid var(--eve-solar-system-status-color-friendly-dark20);
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-friendly-dark30),
transparent
);
&.selected {
border-color: var(--eve-solar-system-status-color-friendly-dark5);
}
}
&.eve-system-status-lookingFor {
border: 1px solid var(--eve-solar-system-status-color-lookingFor-dark15);
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
&.selected {
border-color: $pastel-pink;
}
}
&.eve-system-status-warning {
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-warning),
transparent
);
}
&.eve-system-status-dangerous {
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-dangerous),
transparent
);
}
&.eve-system-status-target {
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-target),
transparent
);
}
}
/* 2) .Bookmarks:
- add var-based font family/weight
*/
.Bookmarks {
position: absolute;
width: 100%;
z-index: 1;
display: flex;
left: 4px;
& > .Bookmark {
min-width: 13px;
height: 22px;
position: relative;
top: -13px;
border-radius: 5px;
color: #ffffff;
font-size: 8px;
text-align: center;
padding-top: 2px;
font-weight: bolder;
padding-left: 3px;
padding-right: 3px;
//background-color: #833ca4;
&:not(:first-child) {
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
}
}
.BookmarkWithIcon {
display: flex;
justify-content: center;
align-items: center;
margin-top: -2px;
text-shadow: 0 0 3px rgba(0, 0, 0, 1);
padding-right: 2px;
& > .icon {
width: 8px;
height: 8px;
font-size: 8px;
}
& > .text {
margin-top: 1px;
font-size: 9px;
}
}
}
.Unsplashed {
position: absolute;
width: calc(50% - 4px);
z-index: -1;
display: flex;
flex-wrap: wrap;
gap: 2px;
left: 2px;
&--right {
left: calc(50% + 6px);
}
& > .Signature {
width: 13px;
height: 4px;
position: relative;
top: 3px;
border-radius: 5px;
color: #ffffff;
font-size: 8px;
text-align: center;
padding-top: 2px;
font-weight: bolder;
padding-left: 3px;
padding-right: 3px;
display: block;
background-color: #833ca4;
&:not(:first-child) {
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
}
}
}
.icon {
width: 8px;
height: 8px;
font-size: 8px;
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 {
display: flex;
align-items: center;
gap: 3px;
font-size: 11px;
line-height: 14px;
font-weight: 500;
position: relative;
top: 1px;
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
.classTitle {
font-size: 11px;
font-weight: bold;
text-shadow: 0 0 2px rgb(0 0 0 / 73%);
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);
color: var(--rf-tag-color, #38BDF8);
}
/* Firefox kostyl */
@-moz-document url-prefix() {
.classSystemName {
font-family: inherit !important;
font-weight: bold;
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
}
.classSystemName {
font-family: inherit !important;
font-weight: bold;
}
.solarSystemName {
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 {
display: flex;
justify-content: space-between;
align-items: center;
height: 19px;
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)
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)
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;
//align-items: center;
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);
}
& > i {
position: relative;
top: 1px;
}
& > span {
font-size: 9px;
line-height: 9px;
font-weight: 500;
//margin-top: 1px;
}
}
}
.effect {
width: 8px;
height: 8px;
margin-top: -2px;
box-sizing: border-box;
border-radius: 2px;
margin-left: 1px;
}
.statics {
display: flex;
gap: 2px;
font-size: 8px;
& > * {
line-height: 10px;
}
/* Firefox kostyl */
@-moz-document url-prefix() {
position: relative;
top: -1px;
}
}
.Handlers {
position: absolute;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.Handle {
min-width: initial;
min-height: initial;
border: 1px solid $pastel-blue;
width: 5px;
height: 5px;
&.selected {
border-color: $pastel-pink;
}
&.HandleTop {
top: -2px;
}
&.HandleRight {
right: -2px;
}
&.HandleBottom {
bottom: -2px;
}
&.HandleLeft {
left: -2px;
}
&.Tick {
width: 7px;
height: 7px;
&.HandleTop {
top: -3px;
}
&.HandleRight {
right: -3px;
}
&.HandleBottom {
bottom: -3px;
}
&.HandleLeft {
left: -3px;
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
}
}
}

View File

@@ -1,12 +1,10 @@
import { memo } from 'react';
import { Handle, Position } from 'reactflow';
import { MapSolarSystemType } from '../../map.types';
import { Handle, Position, NodeProps } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeTheme.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
import {
MARKER_BOOKMARK_BG_STYLES,
STATUS_CLASSES,
@@ -15,64 +13,35 @@ import {
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
export const SolarSystemNodeTheme = memo(props => {
const {
charactersInSystem,
classTitle,
classTitleColor,
customName,
effectName,
hasUserCharacters,
hubs,
visible,
labelCustom,
labelsInfo,
locked,
isShattered,
isThickConnections,
isWormhole,
killsCount,
killsActivityType,
regionClass,
regionName,
status,
selected,
tag,
showHandlers,
systemName,
sortedStatics,
solarSystemId,
unsplashedLeft,
unsplashedRight,
dbClick: handleDbClick,
} = useSolarSystemNode(props);
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
return (
<>
{visible && (
{nodeVars.visible && (
<div className={classes.Bookmarks}>
{labelCustom !== '' && (
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{labelCustom}</span>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
</div>
)}
{isShattered && (
{nodeVars.isShattered && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
<span className={clsx('pi pi-chart-pie', classes.icon)} />
</div>
)}
{killsCount && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[killsActivityType!])}>
{nodeVars.killsCount && (
<div 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)}>{killsCount}</span>
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div>
</div>
)}
{labelsInfo.map(x => (
{nodeVars.labelsInfo.map(x => (
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
{x.shortName}
</div>
@@ -81,18 +50,31 @@ export const SolarSystemNodeTheme = memo(props => {
)}
<div
className={clsx(classes.RootCustomNode, regionClass && classes[regionClass], classes[STATUS_CLASSES[status]], {
[classes.selected]: selected,
})}
className={clsx(
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
classes[STATUS_CLASSES[nodeVars.status]],
{
[classes.selected]: nodeVars.selected,
},
)}
>
{visible && (
{nodeVars.visible && (
<>
<div className={classes.HeadRow}>
<div className={clsx(classes.classTitle, classTitleColor, '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]')}>
{classTitle ?? '-'}
<div
className={clsx(
classes.classTitle,
nodeVars.classTitleColor,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]',
)}
>
{nodeVars.classTitle ?? '-'}
</div>
{tag != null && tag !== '' && <div className={clsx(classes.TagTitle)}>{tag}</div>}
{nodeVars.tag != null && nodeVars.tag !== '' && (
<div className={clsx(classes.TagTitle)}>{nodeVars.tag}</div>
)}
<div
className={clsx(
@@ -100,63 +82,65 @@ export const SolarSystemNodeTheme = memo(props => {
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap',
)}
>
{systemName}
{nodeVars.systemName}
</div>
{isWormhole && (
{nodeVars.isWormhole && (
<div className={classes.statics}>
{sortedStatics.map(whClass => (
{nodeVars.sortedStatics.map(whClass => (
<WormholeClassComp key={whClass} id={whClass} />
))}
</div>
)}
{effectName !== null && isWormhole && (
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[effectName])} />
{nodeVars.effectName !== null && nodeVars.isWormhole && (
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[nodeVars.effectName])} />
)}
</div>
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
{customName && (
{nodeVars.customName && (
<div
className={clsx(
classes.CustomName,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
)}
>
{customName}
{nodeVars.customName}
</div>
)}
{!isWormhole && !customName && (
{!nodeVars.isWormhole && !nodeVars.customName && (
<div
className={clsx(
classes.RegionName,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
)}
>
{regionName}
{nodeVars.regionName}
</div>
)}
{isWormhole && !customName && <div />}
{nodeVars.isWormhole && !nodeVars.customName && <div />}
<div className="flex items-center justify-end">
<div className="flex gap-1 items-center">
{locked && <i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />}
{nodeVars.locked && (
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
)}
{hubs.includes(solarSystemId.toString()) && (
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
)}
{charactersInSystem.length > 0 && (
{nodeVars.charactersInSystem.length > 0 && (
<div
className={clsx(classes.localCounter, {
[classes.hasUserCharacters]: hasUserCharacters,
[classes.hasUserCharacters]: nodeVars.hasUserCharacters,
})}
>
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
<span className="font-sans">{charactersInSystem.length}</span>
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
</div>
)}
</div>
@@ -166,19 +150,19 @@ export const SolarSystemNodeTheme = memo(props => {
)}
</div>
{visible && (
{nodeVars.visible && (
<>
{unsplashedLeft.length > 0 && (
{nodeVars.unsplashedLeft.length > 0 && (
<div className={classes.Unsplashed}>
{unsplashedLeft.map(sig => (
{nodeVars.unsplashedLeft.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
))}
</div>
)}
{unsplashedRight.length > 0 && (
{nodeVars.unsplashedRight.length > 0 && (
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
{unsplashedRight.map(sig => (
{nodeVars.unsplashedRight.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
))}
</div>
@@ -186,44 +170,44 @@ export const SolarSystemNodeTheme = memo(props => {
</>
)}
<div onMouseDownCapture={handleDbClick} className={classes.Handlers}>
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleTop, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Top}
id="a"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleRight, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Right}
id="b"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleBottom, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Bottom}
id="c"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleLeft, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Left}
id="d"
/>

View File

@@ -15,4 +15,8 @@
font-weight: bolder;
display: block;
}
& > .Eol {
display: block;
}
}

View File

@@ -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>

View File

@@ -0,0 +1,32 @@
import { SolarSystemNodeDefault, SolarSystemNodeTheme } from '../components/SolarSystemNode';
import type { NodeProps } from 'reactflow';
import type { ComponentType } from 'react';
import { MapSolarSystemType } from '../map.types';
import { ConnectionMode } from 'reactflow';
export type SolarSystemNodeComponent = ComponentType<NodeProps<MapSolarSystemType>>;
interface ThemeBehavior {
isPanAndDrag: boolean;
nodeComponent: SolarSystemNodeComponent;
connectionMode: ConnectionMode;
}
const THEME_BEHAVIORS: {
[key: string]: ThemeBehavior;
} = {
default: {
isPanAndDrag: false,
nodeComponent: SolarSystemNodeDefault,
connectionMode: ConnectionMode.Loose,
},
pathfinder: {
isPanAndDrag: true,
nodeComponent: SolarSystemNodeTheme,
connectionMode: ConnectionMode.Loose,
},
};
export function getBehaviorForTheme(themeName: string) {
return THEME_BEHAVIORS[themeName] ?? THEME_BEHAVIORS.default;
}

View File

@@ -61,7 +61,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
break;
case Commands.updateSystems:
setTimeout(() => mapUpdateSystems(data as CommandUpdateSystems), 100);
mapUpdateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems:
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
@@ -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);

View File

@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { MapSolarSystemType } from '../map.types';
import { NodeProps } from 'reactflow';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
@@ -9,7 +10,7 @@ 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 { OutCommand } from '@/hooks/Mapper/types';
import { CharacterTypeRaw, OutCommand } from '@/hooks/Mapper/types';
import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants';
function getActivityType(count: number) {
@@ -30,8 +31,8 @@ function sortedLabels(labels: string[]) {
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x]);
}
export function useSolarSystemNode(props: any) {
const { data, selected, id } = props;
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
const { id, data, selected } = props;
const {
system_static_info,
system_signatures,
@@ -57,7 +58,6 @@ export function useSolarSystemNode(props: any) {
solar_system_name,
} = system_static_info;
// Global map state
const {
interfaceSettings,
data: { systemSignatures: mapSystemSignatures },
@@ -85,7 +85,6 @@ export function useSolarSystemNode(props: any) {
outCommand,
} = useMapState();
// logic
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
const systemSignatures = useMemo(
@@ -142,12 +141,12 @@ export function useSolarSystemNode(props: any) {
return '';
}
if (isShowLinkedSigIdTempName) {
return temporary_name ? `${linkedSigPrefix}${temporary_name}` : linkedSigPrefix;
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
return temporary_name ? `${linkedSigPrefix}${temporary_name}` : `${linkedSigPrefix}${solar_system_name}`;
}
return temporary_name;
}, [isShowLinkedSigIdTempName, isTempSystemNameEnabled, linkedSigPrefix, temporary_name]);
}, [isShowLinkedSigIdTempName, isTempSystemNameEnabled, linkedSigPrefix, solar_system_name, temporary_name]);
const systemName = useMemo(() => {
if (isTempSystemNameEnabled && temporaryName) {
@@ -156,7 +155,7 @@ export function useSolarSystemNode(props: any) {
return solar_system_name;
}, [isTempSystemNameEnabled, solar_system_name, temporaryName]);
const customName = (isTempSystemNameEnabled && temporaryName && name) || (solar_system_name !== name && name);
const customName = (isTempSystemNameEnabled && temporaryName && name) || (solar_system_name !== name && name) || null;
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
if (!isShowUnsplashedSignatures) {
@@ -174,10 +173,9 @@ export function useSolarSystemNode(props: any) {
}, [isShowUnsplashedSignatures, systemSignatures]);
const nodeVars = {
// original props
id,
selected,
// computed
visible,
isWormhole,
classTitleColor,
@@ -214,3 +212,43 @@ export function useSolarSystemNode(props: any) {
return nodeVars;
}
export interface SolarSystemNodeVars {
id: string;
selected: boolean;
visible: boolean;
isWormhole: boolean;
classTitleColor: string | null;
killsCount: number | null;
killsActivityType: string | null;
hasUserCharacters: boolean;
showHandlers: boolean;
regionClass: string | null;
systemName: string;
customName?: string | null;
labelCustom: string | null;
isShattered: boolean;
tag?: string | null;
status?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
labelsInfo: Array<any>;
dbClick: (event?: void) => void;
sortedStatics: Array<string | number>;
effectName: string | null;
regionName: string | null;
solarSystemId: number;
solarSystemName: string | null;
locked: boolean;
hubs: string[] | number[];
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>;
isThickConnections: boolean;
classTitle: string | null;
temporaryName?: string | null;
}

View File

@@ -27,5 +27,5 @@
--text-color: #ffffff;
--tooltip-bg: #202020;
--window-corner: #72716f;
}

View File

@@ -1,4 +1,3 @@
@import './eve-common-variables';
@import './eve-common';
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
@@ -40,7 +39,6 @@
--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;
@@ -48,4 +46,6 @@
--rf-tag-color: #fbbf24;
--rf-has-user-characters: #5cb85c;
}
--window-corner: #72716f;
}

View File

@@ -1,11 +0,0 @@
// wrapNode.ts
import { NodeProps } from 'reactflow';
import { SolarSystemNodeProps } from '../components/SolarSystemNode';
export function wrapNode<T>(
SolarSystemNode: React.FC<SolarSystemNodeProps<T>>
): React.FC<NodeProps<T>> {
return function NodeAdapter(props) {
return <SolarSystemNode {...props} />;
};
}

View File

@@ -1,63 +1,25 @@
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
import { useMemo, useState } from 'react';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { useMemo } from 'react';
import { WindowManager } from '@/hooks/Mapper/components/ui-kit/WindowManager';
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
import { CURRENT_WINDOWS_VERSION, DEFAULT_WIDGETS } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
import { DEFAULT_WIDGETS } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
type WindowsLS = {
windows: WindowProps[];
version: number;
};
const saveWindowsToLS = (toSaveItems: WindowProps[]) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const out = toSaveItems.map(({ content, ...rest }) => rest);
localStorage.setItem(SESSION_KEY.windows, JSON.stringify({ version: CURRENT_WINDOWS_VERSION, windows: out }));
};
const restoreWindowsFromLS = (): WindowProps[] => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const raw = localStorage.getItem(SESSION_KEY.windows);
if (!raw) {
console.warn('No windows found in local storage!!');
return DEFAULT_WIDGETS;
}
const { version, windows } = JSON.parse(raw) as WindowsLS;
if (!version || CURRENT_WINDOWS_VERSION > version) {
return DEFAULT_WIDGETS;
}
// eslint-disable-next-line no-debugger
const out = (windows as Omit<WindowProps, 'content'>[])
.filter(x => DEFAULT_WIDGETS.find(def => def.id === x.id))
.map(x => {
const content = DEFAULT_WIDGETS.find(def => def.id === x.id)?.content;
return { ...x, content: content! };
});
return out;
};
export const MapInterface = () => {
const [items, setItems] = useState<WindowProps[]>(restoreWindowsFromLS);
const { windowsVisible } = useMapRootState();
// const [items, setItems] = useState<WindowProps[]>(restoreWindowsFromLS);
const { windowsSettings, updateWidgetSettings } = useMapRootState();
const itemsFiltered = useMemo(() => {
return items.filter(x => windowsVisible.some(j => x.id === j));
}, [items, windowsVisible]);
const items = useMemo(() => {
return windowsSettings.windows
.map(x => {
const content = DEFAULT_WIDGETS.find(y => y.id === x.id)?.content;
return {
...x,
content: content!,
};
})
.filter(x => windowsSettings.visible.some(j => x.id === j));
}, [windowsSettings]);
return (
<WindowManager
windows={itemsFiltered}
dragSelector=".react-grid-dragHandleExample"
onChange={x => {
saveWindowsToLS(x);
setItems(x);
}}
/>
);
return <WindowManager windows={items} dragSelector=".react-grid-dragHandleExample" onChange={updateWidgetSettings} />;
};

View File

@@ -1,4 +1,4 @@
import { useCallback, useRef } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { Dialog } from 'primereact/dialog';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
@@ -58,7 +58,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
<Dialog
header="Select signature to link"
visible
draggable={false}
draggable={true}
style={{ width: '500px' }}
onHide={handleHide}
contentClassName="!p-0"

View File

@@ -4,17 +4,27 @@ import {
RoutesWidget,
SystemInfo,
SystemSignatures,
SystemStructures,
} from '@/hooks/Mapper/components/mapInterface/widgets';
export const CURRENT_WINDOWS_VERSION = 2;
export const CURRENT_WINDOWS_VERSION = 8;
export const WINDOWS_LOCAL_STORE_KEY = 'windows:settings:v2';
export enum WidgetsIds {
info = 'info',
signatures = 'signatures',
local = 'local',
routes = 'routes',
structures = 'structures',
}
export const STORED_VISIBLE_WIDGETS_DEFAULT = [
WidgetsIds.info,
WidgetsIds.local,
WidgetsIds.routes,
WidgetsIds.signatures,
];
export const DEFAULT_WIDGETS: WindowProps[] = [
{
id: WidgetsIds.info,
@@ -44,6 +54,13 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
zIndex: 0,
content: () => <RoutesWidget />,
},
{
id: WidgetsIds.structures,
position: { x: 10, y: 730 },
size: { width: 510, height: 200 },
zIndex: 0,
content: () => <SystemStructures />,
},
];
type WidgetsCheckboxesType = {
@@ -68,4 +85,8 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
id: WidgetsIds.routes,
label: 'Routes',
},
{
id: WidgetsIds.structures,
label: 'Structures',
},
];

View File

@@ -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 && (
<>

View File

@@ -0,0 +1,118 @@
import React, { useCallback, ClipboardEvent, useRef } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import {
LayoutEventBlocker,
WdImgButton,
TooltipPosition,
InfoDrawer,
SystemView,
} from '@/hooks/Mapper/components/ui-kit';
import { PrimeIcons } from 'primereact/api';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemStructuresContent } from './SystemStructuresContent/SystemStructuresContent';
import { useSystemStructures } from './hooks/useSystemStructures';
import { processSnippetText } from './helpers';
export const SystemStructures: React.FC = () => {
const {
data: { selectedSystems },
outCommand,
} = useMapRootState();
const [systemId] = selectedSystems;
const isNotSelectedSystem = selectedSystems.length !== 1;
const { structures, handleUpdateStructures } = useSystemStructures({ systemId, outCommand });
const labelRef = useRef<HTMLDivElement>(null);
const isCompact = useMaxWidth(labelRef, 260);
const processClipboard = useCallback(
(text: string) => {
const updated = processSnippetText(text, structures);
handleUpdateStructures(updated);
},
[structures, handleUpdateStructures],
);
const handlePaste = useCallback(
(e: ClipboardEvent<HTMLDivElement>) => {
e.preventDefault();
processClipboard(e.clipboardData.getData('text'));
},
[processClipboard],
);
const handlePasteTimer = useCallback(async () => {
try {
const text = await navigator.clipboard.readText();
processClipboard(text);
} catch (err) {
console.error('Clipboard read error:', err);
}
}, [processClipboard]);
function renderWidgetLabel() {
return (
<div className="flex justify-between items-center text-xs w-full h-full" ref={labelRef}>
<div className="flex justify-between items-center gap-1">
{!isCompact && (
<div className="flex whitespace-nowrap text-ellipsis overflow-hidden text-stone-400">
Structures
{!isNotSelectedSystem && ' in'}
</div>
)}
{!isNotSelectedSystem && <SystemView systemId={systemId} className="select-none text-center" hideRegion />}
</div>
<LayoutEventBlocker className="flex gap-2.5">
<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}
tooltip={{
position: TooltipPosition.left,
// @ts-ignore
content: (
<div className="flex flex-col gap-1">
<InfoDrawer title={<b className="text-slate-50">How to add/update structures?</b>}>
In game, select one or more structures in D-Scan and then
<br />
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
<span className="text-blue-500"> blue </span>
use the blue add structure data button
</InfoDrawer>
</div>
),
}}
/>
</LayoutEventBlocker>
</div>
);
}
return (
<div tabIndex={0} onPaste={handlePaste} className="h-full flex flex-col" style={{ outline: 'none' }}>
<Widget label={renderWidgetLabel()}>
{isNotSelectedSystem ? (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
System is not selected
</div>
) : (
<SystemStructuresContent structures={structures} onUpdateStructures={handleUpdateStructures} />
)}
</Widget>
</div>
);
};

View File

@@ -0,0 +1,24 @@
.TableRowCompact {
height: 8px;
max-height: 8px;
font-size: 12px !important;
line-height: 8px;
}
.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;
}

View File

@@ -0,0 +1,170 @@
import React, { useState, useCallback, useMemo } from 'react';
import { DataTable, DataTableRowClickEvent } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { PrimeIcons } from 'primereact/api';
import clsx from 'clsx';
import { SystemStructuresDialog } from '../SystemStructuresDialog/SystemStructuresDialog';
import { StructureItem } from '../helpers/structureTypes';
import { useHotkey } from '@/hooks/Mapper/hooks';
import classes from './SystemStructuresContent.module.scss';
import { renderOwnerCell, renderTypeCell, renderTimerCell } from '../renders/cellRenders';
interface SystemStructuresContentProps {
structures: StructureItem[];
onUpdateStructures: (newList: StructureItem[]) => void;
}
export const SystemStructuresContent: React.FC<SystemStructuresContentProps> = ({ structures, onUpdateStructures }) => {
const [selectedRow, setSelectedRow] = useState<StructureItem | null>(null);
const [editingItem, setEditingItem] = useState<StructureItem | null>(null);
const [showEditDialog, setShowEditDialog] = useState(false);
const handleRowClick = (e: DataTableRowClickEvent) => {
const row = e.data as StructureItem;
setSelectedRow(prev => (prev?.id === row.id ? null : row));
};
const handleRowDoubleClick = (e: DataTableRowClickEvent) => {
setEditingItem(e.data as StructureItem);
setShowEditDialog(true);
};
// Press Delete => remove selected row
const handleDeleteSelected = useCallback(
(e: KeyboardEvent) => {
if (!selectedRow) return;
e.preventDefault();
e.stopPropagation();
const newList = structures.filter(s => s.id !== selectedRow.id);
onUpdateStructures(newList);
setSelectedRow(null);
},
[selectedRow, structures, onUpdateStructures],
);
useHotkey(false, ['Delete', 'Backspace'], handleDeleteSelected);
const visibleStructures = useMemo(() => {
return structures;
}, [structures]);
return (
<div className="flex flex-col gap-2 p-2 text-xs text-stone-200 h-full">
{visibleStructures.length === 0 ? (
<div className="flex-1 flex justify-center items-center text-stone-400/80 text-sm">No structures</div>
) : (
<div className="flex-1">
<DataTable
value={visibleStructures}
dataKey="id"
className={clsx(classes.Table, 'w-full select-none h-full')}
size="small"
sortMode="single"
rowHover
style={{ tableLayout: 'fixed', width: '100%' }}
onRowClick={handleRowClick}
onRowDoubleClick={handleRowDoubleClick}
rowClassName={rowData => {
const isSelected = selectedRow?.id === rowData.id;
return clsx(
classes.TableRowCompact,
'transition-colors duration-200 cursor-pointer',
isSelected ? 'bg-amber-500/50 hover:bg-amber-500/70' : 'hover:bg-purple-400/20',
);
}}
>
<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
className={clsx(PrimeIcons.PENCIL, 'text-[14px] cursor-pointer')}
title="Edit"
onClick={() => {
setEditingItem(rowData);
setShowEditDialog(true);
}}
/>
)}
style={{
width: '40px',
textAlign: 'center',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
/>
</DataTable>
</div>
)}
{showEditDialog && editingItem && (
<SystemStructuresDialog
visible={showEditDialog}
structure={editingItem}
onClose={() => setShowEditDialog(false)}
onSave={(updatedItem: StructureItem) => {
const newList = structures.map(s => (s.id === updatedItem.id ? updatedItem : s));
onUpdateStructures(newList);
setShowEditDialog(false);
}}
onDelete={(deleteId: string) => {
const newList = structures.filter(s => s.id !== deleteId);
onUpdateStructures(newList);
setShowEditDialog(false);
}}
/>
)}
</div>
);
};

View File

@@ -0,0 +1,31 @@
.systemStructureDialog {
.p-dialog-content {
background-color: var(--surface-800) !important;
}
.p-dialog-header {
background-color: var(--surface-700);
color: var(--text-color);
}
.p-dialog-header-icon,
.p-dialog-header-title {
color: var(--gray-200);
}
.p-inputtext {
background-color: #2a2a2a !important;
color: #ddd !important;
font-size: 12px !important;
padding: 0.25rem 0.5rem !important;
}
.p-dialog-footer {
.p-button {
font-size: 12px !important;
padding: 0.3rem 0.75rem !important;
}
}
}

View File

@@ -0,0 +1,235 @@
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';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
interface StructuresEditDialogProps {
visible: boolean;
structure?: StructureItem;
onClose: () => void;
onSave: (updatedItem: StructureItem) => void;
onDelete: (id: string) => void;
}
export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
visible,
structure,
onClose,
onSave,
onDelete,
}) => {
const [editData, setEditData] = useState<StructureItem | null>(null);
const [ownerInput, setOwnerInput] = useState('');
const [ownerSuggestions, setOwnerSuggestions] = useState<{ label: string; value: string }[]>([]);
const { outCommand } = useMapRootState();
const [prevQuery, setPrevQuery] = useState('');
const [prevResults, setPrevResults] = useState<{ label: string; value: string }[]>([]);
useEffect(() => {
if (structure) {
setEditData(structure);
setOwnerInput(structure.ownerName ?? '');
} else {
setEditData(null);
setOwnerInput('');
}
}, [structure]);
// Searching corporation owners via auto-complete
const searchOwners = useCallback(
async (e: { query: string }) => {
const newQuery = e.query.trim();
if (!newQuery) {
setOwnerSuggestions([]);
return;
}
// 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()),
);
setOwnerSuggestions(filtered);
return;
}
try {
const { results = [] } = await outCommand({
type: OutCommand.getCorporationNames,
data: { search: newQuery },
});
setOwnerSuggestions(results);
setPrevQuery(newQuery);
setPrevResults(results);
} catch (err) {
console.error('Failed to fetch owners:', err);
setOwnerSuggestions([]);
}
},
[prevQuery, prevResults, outCommand],
);
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 };
});
};
// 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,
);
};
const handleStatusChange = (val: string) => {
setEditData(prev => {
if (!prev) return null;
const newStatus = val as StructureStatus;
// If new status doesn't require a timer, we clear out endTime
const newEndTime = statusesRequiringTimer.includes(newStatus) ? prev.endTime : '';
return { ...prev, status: newStatus, endTime: newEndTime };
});
};
const handleSaveClick = async () => {
if (!editData) return;
// If status doesn't require a timer, clear endTime
if (!statusesRequiringTimer.includes(editData.status)) {
editData.endTime = '';
} else if (editData.endTime) {
// convert to full ISO if not already
editData.endTime = formatToISO(editData.endTime);
}
// fetch corporation ticker if we have an ownerId
if (editData.ownerId) {
try {
const { ticker } = await outCommand({
type: OutCommand.getCorporationTicker,
data: { corp_id: editData.ownerId },
});
editData.ownerTicker = ticker ?? '';
} catch (err) {
console.error('Failed to fetch ticker:', err);
editData.ownerTicker = '';
}
}
onSave(editData);
};
const handleDeleteClick = () => {
if (!editData) return;
onDelete(editData.id);
onClose();
};
if (!editData) return null;
return (
<Dialog
visible={visible}
onHide={onClose}
header={`Edit Structure - ${editData.name ?? ''}`}
className={clsx('myStructuresDialog', 'text-stone-200 w-full max-w-md')}
>
<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 ?? ''}
/>
</label>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
<span>Name:</span>
<input
className="p-inputtext p-component"
value={editData.name ?? ''}
onChange={e => handleChange('name', e.target.value)}
/>
</label>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
<span>Owner:</span>
<AutoComplete
id="owner"
value={ownerInput}
suggestions={ownerSuggestions}
completeMethod={searchOwners}
minLength={3}
delay={400}
field="label"
placeholder="Corporation name..."
onChange={e => setOwnerInput(e.value)}
onSelect={e => handleSelectOwner(e.value)}
/>
</label>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
<span>Status:</span>
<select
className="p-inputtext p-component"
value={editData.status}
onChange={e => handleStatusChange(e.target.value)}
>
<option value="Powered">Powered</option>
<option value="Anchoring">Anchoring</option>
<option value="Unanchoring">Unanchoring</option>
<option value="Low Power">Low Power</option>
<option value="Abandoned">Abandoned</option>
<option value="Reinforced">Reinforced</option>
</select>
</label>
{statusesRequiringTimer.includes(editData.status) && (
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
<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
className="p-inputtext p-component resize-none h-24"
value={editData.notes ?? ''}
onChange={e => handleChange('notes', e.target.value)}
/>
</label>
</div>
<div className="flex justify-end items-center gap-2 mt-4">
<Button label="Delete" severity="danger" className="p-button-sm" onClick={handleDeleteClick} />
<Button label="Save" className="p-button-sm" onClick={handleSaveClick} />
</div>
</Dialog>
);
};

View File

@@ -0,0 +1,4 @@
export * from './parserHelper';
export * from './pasteParser';
export * from './structureTypes';
export * from './structureUtils';

View File

@@ -0,0 +1,92 @@
import { StructureStatus, StructureItem, STRUCTURE_TYPE_MAP } from './structureTypes';
import { formatToISO } from './structureUtils';
// Up to you if you'd like to keep a separate constant here or not
export const statusesRequiringTimer: StructureStatus[] = ['Anchoring', 'Reinforced'];
/**
* parseFormatOneLine(line):
* - Splits by tabs
* - First col => structureTypeId
* - Second col => rawName
* - Third col => structureTypeName
*/
export function parseFormatOneLine(line: string): StructureItem | null {
const columns = line
.split('\t')
.map(c => c.trim())
.filter(Boolean);
// Expecting e.g. "35832 J214811 - SomeName Astrahus"
if (columns.length < 3) {
return null;
}
const [rawTypeId, rawName, rawTypeName] = columns;
if (columns.length != 4) {
return null;
}
if (!STRUCTURE_TYPE_MAP[rawTypeId]) {
return null;
}
if (rawTypeName != STRUCTURE_TYPE_MAP[rawTypeId]) {
return null;
}
const name = rawName.replace(/^J\d{6}\s*-\s*/, '').trim();
return {
id: crypto.randomUUID(),
structureTypeId: rawTypeId,
structureType: rawTypeName,
name,
ownerName: '',
notes: '',
status: 'Powered', // Default
endTime: '', // No timer by default
};
}
export function matchesThreeLineSnippet(lines: string[]): boolean {
if (lines.length < 3) return false;
return /until\s+\d{4}\.\d{2}\.\d{2}/i.test(lines[2]);
}
/**
* parseThreeLineSnippet:
* - Example lines:
* line1: "J214811 - Folgers"
* line2: "1,475 km"
* line3: "Reinforced until 2025.01.13 23:51"
*/
export function parseThreeLineSnippet(lines: string[]): StructureItem {
const [line1, , line3] = lines;
let status: StructureStatus = 'Reinforced';
let endTime: string | undefined;
// e.g. "Reinforced until 2025.01.13 23:27"
const match = line3.match(/^(?<stat>\w+)\s+until\s+(?<dateTime>[\d.]+\s+[\d:]+)/i);
if (match?.groups?.stat) {
const candidateStatus = match.groups.stat as StructureStatus;
if (statusesRequiringTimer.includes(candidateStatus)) {
status = candidateStatus;
}
}
if (match?.groups?.dateTime) {
let dt = match.groups.dateTime.trim().replace(/\./g, '-'); // "2025-01-13 23:27"
dt = dt.replace(' ', 'T'); // "2025-01-13T23:27"
endTime = formatToISO(dt); // => "2025-01-13T23:27:00Z"
}
return {
id: crypto.randomUUID(),
name: line1.replace(/^J\d{6}\s*-\s*/, '').trim(),
status,
endTime,
};
}

View File

@@ -0,0 +1,56 @@
import { StructureItem } from './structureTypes';
import { parseThreeLineSnippet, parseFormatOneLine, matchesThreeLineSnippet } from './parserHelper';
export function processSnippetText(rawText: string, existingStructures: StructureItem[]): StructureItem[] {
if (!rawText) {
return existingStructures.slice();
}
const lines = rawText
.split(/\r?\n/)
.map(line => line.trim())
.filter(Boolean);
if (lines.length === 3 && matchesThreeLineSnippet(lines)) {
return applyThreeLineSnippet(lines, existingStructures);
} else {
return applySingleLineParse(lines, existingStructures);
}
}
function applyThreeLineSnippet(snippetLines: string[], existingStructures: StructureItem[]): StructureItem[] {
const updatedList = [...existingStructures];
const snippetItem = parseThreeLineSnippet(snippetLines);
const existingIndex = updatedList.findIndex(s => s.name.trim() === snippetItem.name.trim());
if (existingIndex !== -1) {
const existing = updatedList[existingIndex];
updatedList[existingIndex] = {
...existing,
status: snippetItem.status,
endTime: snippetItem.endTime,
};
}
return updatedList;
}
function applySingleLineParse(lines: string[], existingStructures: StructureItem[]): StructureItem[] {
const updatedList = [...existingStructures];
const newItems: StructureItem[] = [];
for (const line of lines) {
const item = parseFormatOneLine(line);
if (!item) continue;
const isDuplicate = updatedList.some(
s => s.structureTypeId === item.structureTypeId && s.name.trim() === item.name.trim(),
);
if (!isDuplicate) {
newItems.push(item);
}
}
return [...updatedList, ...newItems];
}

View File

@@ -0,0 +1,32 @@
export type StructureStatus = 'Powered' | 'Anchoring' | 'Unanchoring' | 'Low Power' | 'Abandoned' | 'Reinforced';
export interface StructureItem {
id: string;
systemId?: string;
structureTypeId?: string;
structureType?: string;
name: string;
ownerName?: string;
ownerId?: string;
ownerTicker?: string;
notes?: string;
status: StructureStatus;
endTime?: string;
}
export const STRUCTURE_TYPE_MAP: Record<string, string> = {
'35825': 'Raitaru',
'35826': 'Azbel',
'35827': 'Sotiyo',
'35832': 'Astrahus',
'35833': 'Fortizar',
'35834': 'Keepstar',
'35835': 'Athanor',
'35836': 'Tatara',
'40340': 'Upwell Palatine Keepstar',
'47512': "'Moreau' Fortizar",
'47513': "'Draccous' Fortizar",
'47514': "'Horizon' Fortizar",
'47515': "'Marginis' Fortizar",
'47516': "'Prometheus' Fortizar",
};

View File

@@ -0,0 +1,59 @@
import { StructureItem } from './structureTypes';
export function getActualStructures(oldList: StructureItem[], newList: StructureItem[]) {
const oldMap = new Map(oldList.map(s => [s.id, s]));
const newMap = new Map(newList.map(s => [s.id, s]));
const added: StructureItem[] = [];
const updated: StructureItem[] = [];
const removed: StructureItem[] = [];
for (const newItem of newList) {
const oldItem = oldMap.get(newItem.id);
if (!oldItem) {
added.push(newItem);
} else if (JSON.stringify(oldItem) !== JSON.stringify(newItem)) {
updated.push(newItem);
}
}
for (const oldItem of oldList) {
if (!newMap.has(oldItem.id)) {
removed.push(oldItem);
}
}
return { added, updated, removed };
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function mapServerStructure(serverData: any): StructureItem {
const { owner_id, owner_ticker, structure_type_id, structure_type, owner_name, end_time, system_id, ...rest } =
serverData;
return {
...rest,
ownerId: owner_id,
ownerTicker: owner_ticker,
ownerName: owner_name,
structureType: structure_type,
structureTypeId: structure_type_id,
endTime: end_time ?? '',
systemId: system_id,
};
}
export function formatToISO(datetimeLocal: string): string {
if (!datetimeLocal) return '';
// If missing seconds, add :00
let iso = datetimeLocal;
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(iso)) {
iso += ':00';
}
// Ensure trailing 'Z'
if (!iso.endsWith('Z')) {
iso += 'Z';
}
return iso;
}

View File

@@ -0,0 +1,85 @@
import { useEffect, useState, useCallback } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { mapServerStructure, getActualStructures, StructureItem, statusesRequiringTimer } from '../helpers';
interface UseSystemStructuresProps {
systemId: string | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
outCommand: (payload: any) => Promise<any>;
}
export function useSystemStructures({ systemId, outCommand }: UseSystemStructuresProps) {
const [structures, setStructures] = useState<StructureItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchStructures = useCallback(async () => {
if (!systemId) {
setStructures([]);
return;
}
setIsLoading(true);
setError(null);
try {
const { structures: fetched = [] } = await outCommand({
type: OutCommand.getStructures,
data: { system_id: systemId },
});
const mappedStructures = fetched.map(mapServerStructure);
setStructures(mappedStructures);
} catch (err) {
console.error('Failed to get structures:', err);
setError('Error fetching structures');
} finally {
setIsLoading(false);
}
}, [systemId, outCommand]);
useEffect(() => {
fetchStructures();
}, [fetchStructures]);
const sanitizeEndTimers = useCallback((item: StructureItem) => {
if (!statusesRequiringTimer.includes(item.status)) {
item.endTime = '';
}
return item;
}, []);
const sanitizeIds = useCallback((item: StructureItem) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, ...rest } = item;
return rest;
}, []);
const handleUpdateStructures = useCallback(
async (newList: StructureItem[]) => {
const { added, updated, removed } = getActualStructures(structures, newList);
const sanitizedAdded = added.map(sanitizeIds);
const sanitizedUpdated = updated.map(sanitizeEndTimers);
try {
const { structures: updatedStructures = [] } = await outCommand({
type: OutCommand.updateStructures,
data: {
system_id: systemId,
added: sanitizedAdded,
updated: sanitizedUpdated,
removed,
},
});
const finalStructures = updatedStructures.map(mapServerStructure);
setStructures(finalStructures);
} catch (err) {
console.error('Failed to update structures:', err);
}
},
[structures, systemId, outCommand, sanitizeIds, sanitizeEndTimers],
);
return { structures, handleUpdateStructures, isLoading, error };
}

View File

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

View File

@@ -0,0 +1,50 @@
// File: TimerCell.tsx
import React, { useEffect, useState } from 'react';
import { StructureStatus } from '../helpers/structureTypes';
import { statusesRequiringTimer } from '../helpers';
interface TimerCellProps {
endTime?: string;
status: StructureStatus;
}
function TimerCellImpl({ endTime, status }: TimerCellProps) {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
if (!endTime || !statusesRequiringTimer.includes(status)) {
return;
}
const intervalId = setInterval(() => {
setNow(Date.now());
}, 1000);
return () => clearInterval(intervalId);
}, [endTime, status]);
if (!statusesRequiringTimer.includes(status)) {
return <span className="text-stone-400"></span>;
}
if (!endTime) {
return <span className="text-sky-400">Set Timer</span>;
}
const msLeft = new Date(endTime).getTime() - now;
if (msLeft <= 0) {
return <span className="text-red-500">00:00:00</span>;
}
const sec = Math.floor(msLeft / 1000) % 60;
const min = Math.floor(msLeft / (1000 * 60)) % 60;
const hr = Math.floor(msLeft / (1000 * 3600));
const pad = (n: number) => n.toString().padStart(2, '0');
return (
<span className="text-sky-400">
{pad(hr)}:{pad(min)}:{pad(sec)}
</span>
);
}
export const TimerCell = React.memo(TimerCellImpl);

View File

@@ -0,0 +1,36 @@
import { StructureItem } from '../helpers';
import { TimerCell } from './TimerCell';
export function renderTimerCell(row: StructureItem) {
return <TimerCell endTime={row.endTime} status={row.status} />;
}
export function renderOwnerCell(row: StructureItem) {
return (
<div className="flex items-center gap-2">
{row.ownerId && (
<img
src={`https://images.evetech.net/corporations/${row.ownerId}/logo?size=32`}
alt="corp icon"
className="w-5 h-5 object-contain"
/>
)}
<span>{row.ownerTicker || row.ownerName}</span>
</div>
);
}
export function renderTypeCell(row: StructureItem) {
return (
<div className="flex items-center gap-1">
{row.structureTypeId && (
<img
src={`https://images.evetech.net/types/${row.structureTypeId}/icon`}
alt="icon"
className="w-5 h-5 object-contain"
/>
)}
<span>{row.structureType ?? ''}</span>
</div>
);
}

View File

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

View File

@@ -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>

View File

@@ -13,6 +13,7 @@
.p-tabview-panels {
padding: 6px 1rem !important;
flex-grow: 1;
height: 100%;
}
.p-tabview-nav-container {

View File

@@ -221,11 +221,7 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<div className={styles.verticalTabsContainer}>
<TabView
activeIndex={activeIndex}
onTabChange={e => setActiveIndex(e.index)}
className={styles.verticalTabView}
>
<TabView activeIndex={activeIndex} onTabChange={e => setActiveIndex(e.index)}>
<TabPanel header="Common" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">{renderSettingsList(COMMON_CHECKBOXES_PROPS)}</div>
</TabPanel>
@@ -246,7 +242,7 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
{renderSettingsList(UI_CHECKBOXES_PROPS)}
</TabPanel>
<TabPanel header="Widgets" headerClassName={styles.verticalTabHeader}>
<TabPanel header="Widgets" className="h-full" headerClassName={styles.verticalTabHeader}>
<WidgetsSettings />
</TabPanel>

View File

@@ -1,4 +1,5 @@
import styles from './MapSettings.module.scss';
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
interface PrettySwitchboxProps {

View File

@@ -3,35 +3,35 @@ import { WIDGETS_CHECKBOXES_PROPS, WidgetsIds } from '@/hooks/Mapper/components/
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback } from 'react';
import { Button } from 'primereact/button';
export interface WidgetsSettingsProps {}
// eslint-disable-next-line no-empty-pattern
export const WidgetsSettings = ({}: WidgetsSettingsProps) => {
const { windowsVisible, setWindowsVisible } = useMapRootState();
const { windowsSettings, toggleWidgetVisibility, resetWidgets } = useMapRootState();
const handleWidgetSettingsChange = useCallback(
(widget: WidgetsIds, checked: boolean) => {
setWindowsVisible(prev => {
if (checked) {
return [...prev, widget];
}
return prev.filter(x => x !== widget);
});
},
[setWindowsVisible],
(widget: WidgetsIds) => toggleWidgetVisibility(widget),
[toggleWidgetVisibility],
);
return (
<div className="">
{WIDGETS_CHECKBOXES_PROPS.map(widget => (
<PrettySwitchbox
key={widget.id}
label={widget.label}
checked={windowsVisible.some(x => x === widget.id)}
setChecked={checked => handleWidgetSettingsChange(widget.id, checked)}
/>
))}
<div className="flex flex-col h-full gap-2">
<div>
{WIDGETS_CHECKBOXES_PROPS.map(widget => (
<PrettySwitchbox
key={widget.id}
label={widget.label}
checked={windowsSettings.visible.some(x => x === widget.id)}
setChecked={() => handleWidgetSettingsChange(widget.id)}
/>
))}
</div>
<div className="grid grid-cols-[1fr_auto]">
<div />
<Button className="py-[4px]" onClick={resetWidgets} outlined size="small" label="Reset Widgets"></Button>
</div>
</div>
);
};

View File

@@ -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]);

View File

@@ -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)} />;
}}
/>
);
};

View File

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

View File

@@ -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>
</>
);
};

View File

@@ -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 (

View File

@@ -1 +1,2 @@
export * from './SignatureK162TypeSelect.tsx';
export * from './renderK162Type.tsx';

View File

@@ -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
/>
);
};

View File

@@ -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);
});
@@ -93,9 +88,6 @@ export const MapWrapper = () => {
case OutCommand.openSettings:
setOpenSettings(event.data.system_id);
break;
case OutCommand.linkSignatureToSystem:
setOpenLinkSignatures(event.data);
break;
default:
return outCommand(event);
}
@@ -133,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)) {
@@ -169,6 +166,7 @@ export const MapWrapper = () => {
isSoftBackground={isSoftBackground}
theme={theme}
onAddSystem={onAddSystem}
canRemoveConnection={canRemoveConnection}
/>
{openSettings != null && (
@@ -179,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

View File

@@ -35,21 +35,73 @@
.topLeft {
top: -7.5px;
left: -7.5px;
&::after {
position: relative;
top: 7.5px;
left: 7.5px;
display: block;
content: " ";
width: 5px;
height: 5px;
border-left: 1px solid var(--window-corner);
border-top: 1px solid var(--window-corner);
pointer-events: none;
}
}
.topRight {
top: -7.5px;
right: -7.5px;
&::after {
position: relative;
top: 7.5px;
right: -2.5px;
display: block;
content: " ";
width: 5px;
height: 5px;
border-right: 1px solid var(--window-corner);
border-top: 1px solid var(--window-corner);
pointer-events: none;
}
}
.bottomLeft {
bottom: -7.5px;
left: -7.5px;
&::after {
position: relative;
top: 2.5px;
left: 7.5px;
display: block;
content: " ";
width: 5px;
height: 5px;
border-left: 1px solid var(--window-corner);
border-bottom: 1px solid var(--window-corner);
pointer-events: none;
}
}
.bottomRight {
bottom: -7.5px;
right: -7.5px;
&::after {
position: relative;
top: 2.5px;
right: -2.5px;
display: block;
content: " ";
width: 5px;
height: 5px;
border-right: 1px solid var(--window-corner);
border-bottom: 1px solid var(--window-corner);
pointer-events: none;
}
}
.top {

View File

@@ -5,7 +5,7 @@ import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/type
const MIN_WINDOW_SIZE = 100;
const SNAP_THRESHOLD = 10;
const SNAP_GAP = 10;
export const SNAP_GAP = 10;
export enum ActionType {
Drag = 'drag',
@@ -92,12 +92,7 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
);
useEffect(() => {
setWindows(
initialWindows.map((window, index) => ({
...window,
zIndex: index + 1,
})),
);
setWindows(initialWindows.slice(0));
}, [initialWindows]);
const containerRef = useRef<HTMLDivElement | null>(null);
@@ -389,6 +384,10 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
next.position.y = container.clientHeight - next.size.height - SNAP_GAP;
}
if (next.position.y < 0) {
next.position.y = 0;
}
return next;
});
});

View File

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

View File

@@ -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);
};

View File

@@ -4,11 +4,18 @@ import { MapUnionTypes, OutCommandHandler, SolarSystemConnection } from '@/hooks
import { useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
import useLocalStorageState from 'use-local-storage-state';
import { WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
import {
ToggleWidgetVisibility,
UpdateWidgetSettingsFunc,
useStoreWidgets,
WindowStoreInfo,
} from '@/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts';
import { CommandLinkSignatureToSystem } from '@/hooks/Mapper/types';
export type MapRootData = MapUnionTypes & {
selectedSystems: string[];
selectedConnections: Pick<SolarSystemConnection, 'source' | 'target'>[];
linkSignatureToSystem: CommandLinkSignatureToSystem | null;
};
const INITIAL_DATA: MapRootData = {
@@ -29,6 +36,7 @@ const INITIAL_DATA: MapRootData = {
selectedConnections: [],
userPermissions: {},
options: {},
linkSignatureToSystem: null,
};
export enum InterfaceStoredSettingsProps {
@@ -64,21 +72,16 @@ export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
theme: 'default',
};
export const STORED_VISIBLE_WIDGETS_DEFAULT = [
WidgetsIds.info,
WidgetsIds.local,
WidgetsIds.routes,
WidgetsIds.signatures,
];
export interface MapRootContextProps {
update: ContextStoreDataUpdate<MapRootData>;
data: MapRootData;
outCommand: OutCommandHandler;
interfaceSettings: InterfaceStoredSettings;
setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>;
windowsVisible: WidgetsIds[];
setWindowsVisible: Dispatch<SetStateAction<WidgetsIds[]>>;
windowsSettings: WindowStoreInfo;
toggleWidgetVisibility: ToggleWidgetVisibility;
updateWidgetSettings: UpdateWidgetSettingsFunc;
resetWidgets: () => void;
}
const MapRootContext = createContext<MapRootContextProps>({
@@ -112,10 +115,7 @@ export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProvide
defaultValue: STORED_INTERFACE_DEFAULT_VALUES,
},
);
const [windowsVisible, setWindowsVisible] = useLocalStorageState<WidgetsIds[]>('windows:visible', {
defaultValue: STORED_VISIBLE_WIDGETS_DEFAULT,
});
const { windowsSettings, toggleWidgetVisibility, updateWidgetSettings, resetWidgets } = useStoreWidgets();
useEffect(() => {
let foundNew = false;
@@ -143,8 +143,10 @@ export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProvide
outCommand: outCommand,
setInterfaceSettings,
interfaceSettings,
windowsVisible,
setWindowsVisible,
windowsSettings,
updateWidgetSettings,
toggleWidgetVisibility,
resetWidgets,
}}
>
<MapRootHandlers ref={fwdRef}>{children}</MapRootHandlers>

View File

@@ -1,6 +1,11 @@
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';
@@ -74,5 +79,10 @@ export const useCommandsSystems = () => {
[outCommand],
);
return { addSystems, removeSystems, updateSystems, updateSystemSignatures };
const updateLinkSignatureToSystem = useCallback(async (command: CommandLinkSignatureToSystem) => {
const { update } = ref.current;
update({ linkSignatureToSystem: command }, true);
}, []);
return { addSystems, removeSystems, updateSystems, updateSystemSignatures, updateLinkSignatureToSystem };
};

View File

@@ -32,7 +32,8 @@ import { emitMapEvent } from '@/hooks/Mapper/events';
export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
const mapInit = useMapInit();
const { addSystems, removeSystems, updateSystems, updateSystemSignatures } = useCommandsSystems();
const { addSystems, removeSystems, updateSystems, updateSystemSignatures, updateLinkSignatureToSystem } =
useCommandsSystems();
const { addConnections, removeConnections, updateConnection } = useCommandsConnections();
const { charactersUpdated, characterAdded, characterRemoved, characterUpdated, presentCharacters } =
useCommandsCharacters();
@@ -93,7 +94,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

View File

@@ -0,0 +1,118 @@
import useLocalStorageState from 'use-local-storage-state';
import {
CURRENT_WINDOWS_VERSION,
DEFAULT_WIDGETS,
STORED_VISIBLE_WIDGETS_DEFAULT,
WidgetsIds,
WINDOWS_LOCAL_STORE_KEY,
} 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';
export type StoredWindowProps = Omit<WindowProps, 'content'>;
export type WindowStoreInfo = {
version: number;
windows: StoredWindowProps[];
visible: WidgetsIds[];
};
export type UpdateWidgetSettingsFunc = (widgets: WindowProps[]) => void;
export type ToggleWidgetVisibility = (widgetId: WidgetsIds) => void;
export const getDefaultWidgetProps = () => ({
version: CURRENT_WINDOWS_VERSION,
visible: STORED_VISIBLE_WIDGETS_DEFAULT,
windows: DEFAULT_WIDGETS,
});
export const useStoreWidgets = () => {
const [windowsSettings, setWindowsSettings] = useLocalStorageState<WindowStoreInfo>(WINDOWS_LOCAL_STORE_KEY, {
defaultValue: getDefaultWidgetProps(),
});
const ref = useRef({ windowsSettings, setWindowsSettings });
ref.current = { windowsSettings, setWindowsSettings };
const updateWidgetSettings: UpdateWidgetSettingsFunc = useCallback(newWindows => {
const { setWindowsSettings } = ref.current;
setWindowsSettings(({ version, visible /*, windows*/ }: WindowStoreInfo) => {
return {
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);
if (windowProp) {
return windowProp;
}
return x;
}),
visible,
};
});
}, []);
const toggleWidgetVisibility: ToggleWidgetVisibility = useCallback(widgetId => {
const { setWindowsSettings } = ref.current;
setWindowsSettings(({ visible, windows, ...x }) => {
const isCheckedPrev = visible.includes(widgetId);
if (!isCheckedPrev) {
const maxZIndex = Math.max(...windows.map(w => w.zIndex));
return {
...x,
windows: windows.map(wnd => {
if (wnd.id === widgetId) {
return { ...wnd, position: { x: SNAP_GAP, y: SNAP_GAP }, zIndex: maxZIndex + 1 };
}
return wnd;
}),
visible: [...visible, widgetId],
};
}
return {
...x,
windows,
visible: visible.filter(x => x !== widgetId),
};
});
}, []);
useEffect(() => {
const { setWindowsSettings } = ref.current;
const raw = localStorage.getItem(WINDOWS_LOCAL_STORE_KEY);
if (!raw) {
console.warn('No windows found in local storage!!');
setWindowsSettings(getDefaultWidgetProps());
return;
}
const { version, windows, visible } = JSON.parse(raw) as WindowStoreInfo;
if (!version || CURRENT_WINDOWS_VERSION > version) {
setWindowsSettings(getDefaultWidgetProps());
}
// eslint-disable-next-line no-debugger
const out = windows.filter(x => DEFAULT_WIDGETS.find(def => def.id === x.id));
setWindowsSettings({
version: CURRENT_WINDOWS_VERSION,
windows: out as WindowProps[],
visible,
});
}, []);
const resetWidgets = useCallback(() => ref.current.setWindowsSettings(getDefaultWidgetProps()), []);
return {
windowsSettings,
updateWidgetSettings,
toggleWidgetVisibility,
resetWidgets,
};
};

View File

@@ -118,6 +118,7 @@ export enum OutCommand {
deleteHub = 'delete_hub',
getRoutes = 'get_routes',
getCharacterJumps = 'get_character_jumps',
getStructures = 'get_structures',
getSignatures = 'get_signatures',
getSystemStaticInfos = 'get_system_static_infos',
getConnectionInfo = 'get_connection_info',
@@ -127,6 +128,7 @@ export enum OutCommand {
updateConnectionShipSizeType = 'update_connection_ship_size_type',
updateConnectionLocked = 'update_connection_locked',
updateConnectionCustomInfo = 'update_connection_custom_info',
updateStructures = 'update_structures',
updateSignatures = 'update_signatures',
updateSystemName = 'update_system_name',
updateSystemTemporaryName = 'update_system_temporary_name',
@@ -147,6 +149,8 @@ export enum OutCommand {
openUserSettings = 'open_user_settings',
getPassages = 'get_passages',
linkSignatureToSystem = 'link_signature_to_system',
getCorporationNames = 'get_corporation_names',
getCorporationTicker = 'get_corporation_ticker',
// Only UI commands
openSettings = 'open_settings',

View File

@@ -26,15 +26,20 @@ export type GroupType = {
h: number;
};
export type SignatureCustomInfo = {
k162Type?: string;
isEOL?: boolean;
};
export type SystemSignature = {
eve_id: string;
kind: SignatureKind;
name: string;
// SignatureCustomInfo
custom_info?: string;
description?: string;
group: SignatureGroup;
type: string;
k162Type?: string;
linked_system?: SolarSystemStaticInfoRaw;
inserted_at?: string;
updated_at?: string;

View File

@@ -117,6 +117,7 @@ export type SolarSystemRawType = {
status: number;
name: string | null;
temporary_name: string | null;
linked_sig_eve_id: string | null;
system_static_info: SolarSystemStaticInfoRaw;
system_signatures: SystemSignature[];
@@ -130,4 +131,3 @@ export type SearchSystemItem = {
system_static_info: SolarSystemStaticInfoRaw;
value: number;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -64,7 +64,19 @@ map_subscription_characters_limit =
map_subscription_hubs_limit =
config_dir
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_HUBS_LIMIT", 100)
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_HUBS_LIMIT", 10)
map_subscription_base_price =
config_dir
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_BASE_PRICE", 100_000_000)
map_subscription_extra_characters_100_price =
config_dir
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_CHARACTERS_100_PRICE", 50_000_000)
map_subscription_extra_hubs_10_price =
config_dir
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_HUBS_10_PRICE", 10_000_000)
map_connection_auto_expire_hours =
config_dir
@@ -76,7 +88,7 @@ map_connection_auto_eol_hours =
map_connection_eol_expire_timeout_mins =
config_dir
|> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_EOL_EXPIRE_TIMEOUT_MINS", 30)
|> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_EOL_EXPIRE_TIMEOUT_MINS", 60)
wallet_tracking_enabled =
config_dir
@@ -117,16 +129,16 @@ config :wanderer_app,
},
%{
id: "omega",
characters_limit: 300,
hubs_limit: 20,
base_price: 250_000_000,
characters_limit: map_subscription_characters_limit * 2,
hubs_limit: map_subscription_hubs_limit * 2,
base_price: map_subscription_base_price,
month_3_discount: 0.2,
month_6_discount: 0.4,
month_12_discount: 0.5
}
],
extra_characters_100: 75_000_000,
extra_hubs_10: 25_000_000
extra_characters_100: map_subscription_extra_characters_100_price,
extra_hubs_10: map_subscription_extra_hubs_10_price
}
config :ueberauth, Ueberauth,

View File

@@ -16,6 +16,7 @@ defmodule WandererApp.Api do
resource WandererApp.Api.MapState
resource WandererApp.Api.MapSystem
resource WandererApp.Api.MapSystemSignature
resource WandererApp.Api.MapSystemStructure
resource WandererApp.Api.MapCharacterSettings
resource WandererApp.Api.MapSubscription
resource WandererApp.Api.MapTransaction

View File

@@ -0,0 +1,172 @@
defmodule WandererApp.Api.MapSystemStructure do
@moduledoc """
Ash resource representing a structure in a given map system.
"""
use Ash.Resource,
domain: WandererApp.Api,
data_layer: AshPostgres.DataLayer
postgres do
repo(WandererApp.Repo)
table("map_system_structures_v1")
end
code_interface do
define(:all_active, action: :all_active)
define(:create, action: :create)
define(:update, action: :update)
define(:by_id,
get_by: [:id],
action: :read
)
define(:by_system_id,
action: :by_system_id,
args: [:system_id]
)
end
actions do
default_accept [
:system_id,
:solar_system_name,
:solar_system_id,
:structure_type_id,
:structure_type,
:character_eve_id,
:name,
:notes,
:owner_name,
:owner_ticker,
:owner_id,
:status,
:end_time
]
defaults [:read, :destroy]
read :all_active do
prepare build(sort: [updated_at: :desc])
end
read :by_system_id do
argument :system_id, :string, allow_nil?: false
filter(expr(system_id == ^arg(:system_id)))
end
create :create do
primary? true
accept [
:system_id,
:solar_system_name,
:solar_system_id,
:structure_type_id,
:structure_type,
:character_eve_id,
:name,
:notes,
:owner_name,
:owner_ticker,
:owner_id,
:status,
:end_time
]
argument :system_id, :uuid, allow_nil?: false
change manage_relationship(:system_id, :system,
on_lookup: :relate,
on_no_match: nil
)
end
update :update do
primary? true
require_atomic? false
accept [
:system_id,
:solar_system_name,
:solar_system_id,
:structure_type_id,
:structure_type,
:character_eve_id,
:name,
:notes,
:owner_name,
:owner_ticker,
:owner_id,
:status,
:end_time
]
end
end
attributes do
uuid_primary_key :id
attribute :structure_type_id, :string do
allow_nil? false
end
attribute :structure_type, :string do
allow_nil? false
end
attribute :character_eve_id, :string do
allow_nil? false
end
attribute :solar_system_name, :string do
allow_nil? false
end
attribute :solar_system_id, :integer do
allow_nil? false
end
attribute :name, :string do
allow_nil? false
end
attribute :notes, :string do
allow_nil? true
end
attribute :owner_name, :string do
allow_nil? true
end
attribute :owner_ticker, :string do
allow_nil? true
end
attribute :owner_id, :string do
allow_nil? true
end
attribute :status, :string do
allow_nil? true
end
attribute :end_time, :utc_datetime_usec do
allow_nil? true
end
create_timestamp :inserted_at
update_timestamp :updated_at
end
relationships do
belongs_to :system, WandererApp.Api.MapSystem do
attribute_writable? true
end
end
end

View File

@@ -95,7 +95,9 @@ defmodule WandererApp.Api.UserActivity do
:map_acl_member_updated,
:map_connection_added,
:map_connection_updated,
:map_connection_removed
:map_connection_removed,
:signatures_added,
:signatures_removed
]
)
@@ -108,8 +110,6 @@ defmodule WandererApp.Api.UserActivity do
update_timestamp(:updated_at)
end
relationships do
belongs_to :character, WandererApp.Api.Character do
allow_nil? true

View File

@@ -68,7 +68,7 @@ defmodule WandererApp.Map do
end
def get_characters_limit(map_id),
do: {:ok, map_id |> get_map!() |> Map.get(:characters_limit, 100)}
do: {:ok, map_id |> get_map!() |> Map.get(:characters_limit, 50)}
def is_subscription_active?(map_id) do
{:ok, %{plan: plan}} = WandererApp.Map.SubscriptionManager.get_active_map_subscription(map_id)

View File

@@ -206,8 +206,24 @@ defmodule WandererApp.Map.Server.SystemsImpl do
user_id,
character_id
) do
connections_to_remove =
filtered_ids =
removed_ids
|> Enum.map(fn solar_system_id ->
WandererApp.Map.find_system_by_location(map_id, %{solar_system_id: solar_system_id})
end)
|> Enum.filter(fn system -> not is_nil(system) && not system.locked end)
|> Enum.map(&{&1.solar_system_id, &1.id})
solar_system_ids_to_remove =
filtered_ids
|> Enum.map(fn {solar_system_id, _} -> solar_system_id end)
system_ids_to_remove =
filtered_ids
|> Enum.map(fn {_, system_id} -> system_id end)
connections_to_remove =
solar_system_ids_to_remove
|> Enum.map(fn solar_system_id ->
WandererApp.Map.find_connections(map_id, solar_system_id)
end)
@@ -215,9 +231,9 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|> Enum.uniq_by(& &1.id)
:ok = WandererApp.Map.remove_connections(map_id, connections_to_remove)
:ok = WandererApp.Map.remove_systems(map_id, removed_ids)
:ok = WandererApp.Map.remove_systems(map_id, solar_system_ids_to_remove)
removed_ids
solar_system_ids_to_remove
|> Enum.each(fn solar_system_id ->
map_id
|> WandererApp.MapSystemRepo.remove_from_map(solar_system_id)
@@ -237,7 +253,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
WandererApp.MapConnectionRepo.destroy(map_id, connection)
end)
removed_ids
solar_system_ids_to_remove
|> Enum.map(fn solar_system_id ->
WandererApp.Api.MapSystemSignature.by_linked_system_id!(solar_system_id)
end)
@@ -250,10 +266,28 @@ defmodule WandererApp.Map.Server.SystemsImpl do
Impl.broadcast!(map_id, :signatures_updated, system.solar_system_id)
end)
@ddrt.delete(removed_ids, rtree_name)
linked_system_ids =
system_ids_to_remove
|> Enum.map(fn system_id ->
WandererApp.Api.MapSystemSignature.by_system_id!(system_id)
|> Enum.filter(fn s -> not is_nil(s.linked_system_id) end)
|> Enum.map(fn s -> s.linked_system_id end)
end)
|> List.flatten()
|> Enum.uniq()
linked_system_ids
|> Enum.each(fn linked_system_id ->
WandererApp.Map.Server.update_system_linked_sig_eve_id(map_id, %{
solar_system_id: linked_system_id,
linked_sig_eve_id: nil
})
end)
@ddrt.delete(solar_system_ids_to_remove, rtree_name)
Impl.broadcast!(map_id, :remove_connections, connections_to_remove)
Impl.broadcast!(map_id, :systems_removed, removed_ids)
Impl.broadcast!(map_id, :systems_removed, solar_system_ids_to_remove)
case not is_nil(user_id) do
true ->
@@ -262,12 +296,12 @@ defmodule WandererApp.Map.Server.SystemsImpl do
character_id: character_id,
user_id: user_id,
map_id: map_id,
solar_system_ids: removed_ids
solar_system_ids: solar_system_ids_to_remove
})
:telemetry.execute(
[:wanderer_app, :map, :systems, :remove],
%{count: removed_ids |> Enum.count()}
%{count: solar_system_ids_to_remove |> Enum.count()}
)
:ok

View File

@@ -55,18 +55,33 @@ defmodule WandererApp.Maps do
def get_available_maps(current_user) do
case WandererApp.Api.Map.available(%{}, actor: current_user) do
{:ok, maps} -> {:ok, maps |> _filter_blocked_maps(current_user)}
{:ok, maps} -> {:ok, maps |> filter_blocked_maps(current_user)}
_ -> {:ok, []}
end
end
def get_tracked_map_characters(map_id, current_user) do
case WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_filtered(
map_id,
current_user.characters |> Enum.map(& &1.id)
) do
{:ok, settings} ->
{:ok,
settings
|> Enum.map(fn s -> s |> Ash.load!(:character) |> Map.get(:character) end)}
_ ->
{:ok, []}
end
end
def load_characters(map, character_settings, user_id) do
{:ok, user_characters} =
WandererApp.Api.Character.active_by_user(%{user_id: user_id})
characters =
map
|> _get_map_available_characters(user_characters)
|> get_map_available_characters(user_characters)
|> Enum.map(fn c ->
map_character(c, character_settings |> Enum.find(&(&1.character_id == c.id)))
end)
@@ -146,7 +161,7 @@ defmodule WandererApp.Maps do
}}
end
defp _get_map_available_characters(map, user_characters) do
defp get_map_available_characters(map, user_characters) do
{:ok,
%{
map_acl_owner_ids: map_acl_owner_ids,
@@ -164,7 +179,7 @@ defmodule WandererApp.Maps do
end)
end
defp _filter_blocked_maps(maps, current_user) do
defp filter_blocked_maps(maps, current_user) do
user_character_ids = current_user.characters |> Enum.map(& &1.id)
user_character_eve_ids = current_user.characters |> Enum.map(& &1.eve_id)

View File

@@ -0,0 +1,129 @@
defmodule WandererApp.Structure do
@moduledoc """
Encapsulates the logic for parsing and updating system structures.
"""
require Logger
alias WandererApp.Api.MapSystemStructure
alias WandererApp.Character
def update_structures(system, added, updated, removed, user_characters) do
first_char_eve_id = List.first(user_characters)
added_structs =
parse_structures(added, first_char_eve_id, system)
|> Enum.map(&Map.delete(&1, :id))
updated_structs = parse_structures(updated, first_char_eve_id, system)
removed_structs = parse_structures(removed, first_char_eve_id, system)
remove_structures(system.id, Enum.map(removed_structs, & &1.id))
update_structures_in_db(system.id, updated_structs, Enum.map(updated_structs, & &1.id))
add_structures(added_structs)
:ok
end
def search_corporation_names([], _search), do: {:ok, []}
def search_corporation_names([first_char | _], search) when is_binary(search) do
Character.search(first_char.id, params: [search: search, categories: "corporation"])
end
def search_corporation_names(_user_chars, _search), do: {:ok, []}
defp parse_structures(list_of_maps, character_eve_id, system) do
Logger.debug(fn ->
"[Structure] parse_structures =>\n" <> inspect(list_of_maps, pretty: true)
end)
Enum.map(list_of_maps, fn item ->
%{
id: Map.get(item, "id"),
system_id: system.id,
solar_system_id: system.solar_system_id,
solar_system_name: system.name,
structure_type_id: Map.get(item, "structureTypeId") || "???",
structure_type: Map.get(item, "structureType"),
character_eve_id: character_eve_id,
name: Map.get(item, "name"),
notes: Map.get(item, "notes"),
owner_name: Map.get(item, "ownerName"),
owner_ticker: Map.get(item, "ownerTicker"),
owner_id: Map.get(item, "ownerId"),
status: Map.get(item, "status"),
end_time: parse_end_time(Map.get(item, "endTime"))
}
end)
end
defp parse_end_time(str) when is_binary(str) do
# Log everything we can about the incoming string
Logger.debug("[parse_end_time] raw input => #{inspect(str)} (length=#{String.length(str)})")
if String.trim(str) == "" do
Logger.debug("[parse_end_time] It's empty (or whitespace only). Returning nil.")
nil
else
# Attempt to parse
case DateTime.from_iso8601(str) do
{:ok, dt, _offset} ->
Logger.debug("[parse_end_time] Successfully parsed => #{inspect(dt)}")
dt
{:error, reason} ->
Logger.error("[parse_end_time] Invalid ISO string: #{inspect(str)}, reason: #{inspect(reason)}")
nil
end
end
end
defp parse_end_time(other) do
Logger.error("[parse_end_time] Received non-string => #{inspect(other)}. Returning nil.")
nil
end
defp remove_structures(system_id, removed_ids) do
MapSystemStructure.by_system_id!(system_id)
|> Enum.filter(fn s -> s.id in removed_ids end)
|> Enum.each(&Ash.destroy!/1)
end
defp update_structures_in_db(system_id, updated_structs, updated_ids) do
existing_records = MapSystemStructure.by_system_id!(system_id)
Enum.each(existing_records, fn existing ->
if existing.id in updated_ids do
updated_data = Enum.find(updated_structs, fn u -> u.id == existing.id end)
if updated_data do
Logger.debug(fn ->
"[Structure] about to update =>\n" <>
inspect(updated_data, pretty: true)
end)
updated_data = Map.delete(updated_data, :id) # remove PK so Ash doesn't treat it as a new record
new_record = MapSystemStructure.update(existing, updated_data)
Logger.debug(fn ->
"[Structure] updated record =>\n" <> inspect(new_record, pretty: true)
end)
end
end
end)
end
defp add_structures(added_structs) do
Enum.each(added_structs, fn struct_map ->
Logger.debug(fn ->
"[Structure] Creating structure =>\n" <> inspect(struct_map, pretty: true)
end)
MapSystemStructure.create!(struct_map)
end)
end
end

View File

@@ -66,16 +66,12 @@ defmodule WandererAppWeb.Layouts do
def feedback_container(assigns) do
~H"""
<div
id="feeback-container"
data-az-l="6e9c41f4-8f3f-4e3b-bbc6-e808f9e46808"
class={[
"flex flex-col p-4 items-center absolute bottom-40 left-1 gap-2 tooltip tooltip-right text-gray-400 hover:text-white"
]}
data-tip="Leave Feedback"
<.link
href="https://discord.gg/cafERvDD2k"
class="flex flex-col p-4 items-center absolute bottom-40 left-1 gap-2 tooltip tooltip-right text-gray-400 hover:text-white"
>
<.icon name="hero-hand-thumb-up-solid" class="h-4 w-4" />
</div>
</.link>
"""
end

View File

@@ -74,7 +74,7 @@ defmodule WandererAppWeb.UserActivity do
</p>
<p class="text-sm text-[var(--color-gray-4)] w-[15%]">
<%= _get_event_name(@activity.event_type) %>
<%= get_event_name(@activity.event_type) %>
</p>
<.activity_event event_type={@activity.event_type} event_data={@activity.event_data} />
@@ -115,7 +115,7 @@ defmodule WandererAppWeb.UserActivity do
<div class="w-[40%]">
<div class="flex items-center gap-1">
<h6 class="text-base leading-[150%] font-semibold dark:text-white">
<%= _get_event_data(@event_type, Jason.decode!(@event_data) |> Map.drop(["character_id"])) %>
<%= get_event_data(@event_type, Jason.decode!(@event_data) |> Map.drop(["character_id"])) %>
</h6>
</div>
</div>
@@ -129,107 +129,128 @@ defmodule WandererAppWeb.UserActivity do
{:noreply, socket}
end
defp _get_event_name(:hub_added), do: "Hub Added"
defp _get_event_name(:hub_removed), do: "Hub Removed"
defp _get_event_name(:map_connection_added), do: "Connection Added"
defp _get_event_name(:map_connection_updated), do: "Connection Updated"
defp _get_event_name(:map_connection_removed), do: "Connection Removed"
defp _get_event_name(:map_acl_added), do: "Acl Added"
defp _get_event_name(:map_acl_removed), do: "Acl Removed"
defp _get_event_name(:system_added), do: "System Added"
defp _get_event_name(:system_updated), do: "System Updated"
defp _get_event_name(:systems_removed), do: "System(s) Removed"
defp _get_event_name(name), do: name
defp get_event_name(:hub_added), do: "Hub Added"
defp get_event_name(:hub_removed), do: "Hub Removed"
defp get_event_name(:map_connection_added), do: "Connection Added"
defp get_event_name(:map_connection_updated), do: "Connection Updated"
defp get_event_name(:map_connection_removed), do: "Connection Removed"
defp get_event_name(:map_acl_added), do: "Acl Added"
defp get_event_name(:map_acl_removed), do: "Acl Removed"
defp get_event_name(:system_added), do: "System Added"
defp get_event_name(:system_updated), do: "System Updated"
defp get_event_name(:systems_removed), do: "System(s) Removed"
defp get_event_name(:signatures_added), do: "Signatures Added"
defp get_event_name(:signatures_removed), do: "Signatures Removed"
defp get_event_name(name), do: name
# defp _get_event_data(:hub_added, data), do: Jason.encode!(data)
# defp _get_event_data(:hub_removed, data), do: data
defp get_event_data(:map_acl_added, %{"acl_id" => acl_id}) do
{:ok, acl} = WandererApp.AccessListRepo.get(acl_id)
"#{acl.name}"
end
# defp _get_event_data(:map_acl_added, data), do: data
# defp _get_event_data(:map_acl_removed, data), do: data
# defp _get_event_data(:system_added, data), do: data
defp get_event_data(:map_acl_removed, %{"acl_id" => acl_id}) do
{:ok, acl} = WandererApp.AccessListRepo.get(acl_id)
"#{acl.name}"
end
# defp get_event_data(:map_acl_removed, data), do: data
# defp get_event_data(:system_added, data), do: data
#
defp _get_event_data(:system_updated, %{
defp get_event_data(:system_updated, %{
"key" => "labels",
"solar_system_id" => solar_system_id,
"value" => value
}) do
system_name = _get_system_name(solar_system_id)
system_name = get_system_name(solar_system_id)
try do
%{"customLabel" => customLabel, "labels" => labels} = Jason.decode!(value)
"#{system_name} labels - #{inspect(labels)}, customLabel - #{customLabel}"
"#{system_name}: labels - #{inspect(labels)}, customLabel - #{customLabel}"
rescue
_ ->
"#{system_name} labels - #{inspect(value)}"
"#{system_name}: labels - #{inspect(value)}"
end
end
defp _get_event_data(:system_added, %{
defp get_event_data(:system_added, %{
"solar_system_id" => solar_system_id
}),
do: _get_system_name(solar_system_id)
do: get_system_name(solar_system_id)
defp _get_event_data(:hub_added, %{
defp get_event_data(:hub_added, %{
"solar_system_id" => solar_system_id
}),
do: _get_system_name(solar_system_id)
do: get_system_name(solar_system_id)
defp _get_event_data(:hub_removed, %{
defp get_event_data(:hub_removed, %{
"solar_system_id" => solar_system_id
}),
do: _get_system_name(solar_system_id)
do: get_system_name(solar_system_id)
defp _get_event_data(:system_updated, %{
defp get_event_data(:system_updated, %{
"key" => key,
"solar_system_id" => solar_system_id,
"value" => value
}) do
system_name = _get_system_name(solar_system_id)
"#{system_name} #{key} - #{inspect(value)}"
system_name = get_system_name(solar_system_id)
"#{system_name}: #{key} - #{inspect(value)}"
end
defp _get_event_data(:systems_removed, %{
defp get_event_data(:systems_removed, %{
"solar_system_ids" => solar_system_ids
}),
do:
solar_system_ids
|> Enum.map(&_get_system_name/1)
|> Enum.map(&get_system_name/1)
|> Enum.join(", ")
defp _get_event_data(:map_connection_added, %{
defp get_event_data(signatures_event, %{
"solar_system_id" => solar_system_id,
"signatures" => signatures
})
when signatures_event in [:signatures_added, :signatures_removed],
do: "#{get_system_name(solar_system_id)}: #{signatures |> Enum.join(", ")}"
defp get_event_data(signatures_event, %{
"signatures" => signatures
})
when signatures_event in [:signatures_added, :signatures_removed],
do: signatures |> Enum.join(", ")
defp get_event_data(:map_connection_added, %{
"solar_system_source_id" => solar_system_source_id,
"solar_system_target_id" => solar_system_target_id
}) do
source_system_name = _get_system_name(solar_system_source_id)
target_system_name = _get_system_name(solar_system_target_id)
source_system_name = get_system_name(solar_system_source_id)
target_system_name = get_system_name(solar_system_target_id)
"[#{source_system_name}:#{target_system_name}]"
end
defp _get_event_data(:map_connection_removed, %{
defp get_event_data(:map_connection_removed, %{
"solar_system_source_id" => solar_system_source_id,
"solar_system_target_id" => solar_system_target_id
}) do
source_system_name = _get_system_name(solar_system_source_id)
target_system_name = _get_system_name(solar_system_target_id)
source_system_name = get_system_name(solar_system_source_id)
target_system_name = get_system_name(solar_system_target_id)
"[#{source_system_name}:#{target_system_name}]"
end
defp _get_event_data(:map_connection_updated, %{
defp get_event_data(:map_connection_updated, %{
"key" => key,
"solar_system_source_id" => solar_system_source_id,
"solar_system_target_id" => solar_system_target_id,
"value" => value
}) do
source_system_name = _get_system_name(solar_system_source_id)
target_system_name = _get_system_name(solar_system_target_id)
source_system_name = get_system_name(solar_system_source_id)
target_system_name = get_system_name(solar_system_target_id)
"[#{source_system_name}:#{target_system_name}] #{key} - #{inspect(value)}"
end
defp _get_event_data(_name, data), do: Jason.encode!(data)
defp get_event_data(_name, data), do: Jason.encode!(data)
defp _get_system_name(solar_system_id) do
defp get_system_name(solar_system_id) do
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
{:ok, nil} ->
solar_system_id

View File

@@ -44,51 +44,46 @@ defmodule WandererAppWeb.APIController do
end
end
# -----------------------------------------------------------------
# Map
# -----------------------------------------------------------------
@doc """
GET /api/map/systems
@doc """
GET /api/map/systems
Requires either `?map_id=<UUID>` **OR** `?slug=<map-slug>` in the query params.
Requires either `?map_id=<UUID>` **OR** `?slug=<map-slug>` in the query params.
If `?all=true` is provided, **all** systems are returned.
Otherwise, only "visible" systems are returned.
If `?all=true` is provided, **all** systems are returned.
Otherwise, only "visible" systems are returned.
Examples:
GET /api/map/systems?map_id=466e922b-e758-485e-9b86-afae06b88363
GET /api/map/systems?slug=my-unique-wormhole-map
GET /api/map/systems?map_id=<UUID>&all=true
"""
def list_systems(conn, params) do
with {:ok, map_id} <- fetch_map_id(params) do
# Decide which function to call based on the "all" param
repo_fun =
if params["all"] == "true" do
&MapSystemRepo.get_all_by_map/1
else
&MapSystemRepo.get_visible_by_map/1
Examples:
GET /api/map/systems?map_id=466e922b-e758-485e-9b86-afae06b88363
GET /api/map/systems?slug=my-unique-wormhole-map
GET /api/map/systems?map_id=<UUID>&all=true
"""
def list_systems(conn, params) do
with {:ok, map_id} <- fetch_map_id(params) do
repo_fun =
if params["all"] == "true" do
&MapSystemRepo.get_all_by_map/1
else
&MapSystemRepo.get_visible_by_map/1
end
case repo_fun.(map_id) do
{:ok, systems} ->
data = Enum.map(systems, &map_system_to_json/1)
json(conn, %{data: data})
{:error, reason} ->
conn
|> put_status(:not_found)
|> json(%{error: "Could not fetch systems for map_id=#{map_id}: #{inspect(reason)}"})
end
case repo_fun.(map_id) do
{:ok, systems} ->
data = Enum.map(systems, &map_system_to_json/1)
json(conn, %{data: data})
{:error, reason} ->
else
{:error, msg} ->
conn
|> put_status(:not_found)
|> json(%{error: "Could not fetch systems for map_id=#{map_id}: #{inspect(reason)}"})
|> put_status(:bad_request)
|> json(%{error: msg})
end
else
{:error, msg} ->
conn
|> put_status(:bad_request)
|> json(%{error: msg})
end
end
@doc """
GET /api/map/system
@@ -180,6 +175,97 @@ end
end
end
def show_structure_timers(conn, params) do
with {:ok, map_id} <- fetch_map_id(params) do
system_id_str = params["system_id"]
case system_id_str do
nil ->
handle_all_structure_timers(conn, map_id)
_ ->
case parse_int(system_id_str) do
{:ok, system_id} ->
handle_single_structure_timers(conn, map_id, system_id)
{:error, reason} ->
conn
|> put_status(:bad_request)
|> json(%{error: "system_id must be int: #{reason}"})
end
end
else
{:error, msg} ->
conn
|> put_status(:bad_request)
|> json(%{error: msg})
end
end
defp handle_all_structure_timers(conn, map_id) do
case MapSystemRepo.get_visible_by_map(map_id) do
{:ok, systems} ->
all_timers =
systems
|> Enum.flat_map(&get_timers_for_system/1)
json(conn, %{data: all_timers})
{:error, reason} ->
conn
|> put_status(:not_found)
|> json(%{error: "Could not fetch visible systems for map_id=#{map_id}: #{inspect(reason)}"})
end
end
defp handle_single_structure_timers(conn, map_id, system_id) do
case MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id) do
{:ok, map_system} ->
timers = get_timers_for_system(map_system)
json(conn, %{data: timers})
{:error, :not_found} ->
conn
|> put_status(:not_found)
|> json(%{error: "No system with solar_system_id=#{system_id} in map=#{map_id}"})
{:error, reason} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Failed to retrieve system: #{inspect(reason)}"})
end
end
defp get_timers_for_system(map_system) do
structures = WandererApp.Api.MapSystemStructure.by_system_id!(map_system.id)
structures
|> Enum.filter(&timer_needed?/1)
|> Enum.map(&structure_to_timer_json/1)
end
defp timer_needed?(structure) do
structure.status in ["Anchoring", "Reinforced"] and not is_nil(structure.end_time)
end
defp structure_to_timer_json(s) do
Map.take(s, [
:system_id,
:solar_system_name,
:solar_system_id,
:structure_type_id,
:structure_type,
:character_eve_id,
:name,
:notes,
:owner_name,
:owner_ticker,
:owner_id,
:status,
:end_time
])
end
defp get_tracked_by_map_ids(map_id) do
case MapCharacterSettingsRepo.get_tracked_by_map_all(map_id) do
{:ok, settings_list} ->
@@ -214,7 +300,8 @@ end
end
end
defp fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
defp fetch_map_id(_),
do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
defp require_param(params, key) do
case params[key] do

View File

@@ -156,7 +156,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
%{result: characters} = socket.assigns.characters
{:ok, map_characters} = get_tracked_map_characters(map_id, current_user)
{:ok, map_characters} = WandererApp.Maps.get_tracked_map_characters(map_id, current_user)
user_character_eve_ids = map_characters |> Enum.map(& &1.eve_id)
@@ -204,7 +204,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
{:ok, all_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
# Find and filter user's characters
{:ok, user_characters} = get_tracked_map_characters(map_id, current_user)
{:ok, user_characters} = WandererApp.Maps.get_tracked_map_characters(map_id, current_user)
user_char_ids = Enum.map(user_characters, & &1.id)
my_settings =
@@ -213,7 +213,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
s.character_id in user_char_ids
end)
existing = Enum.find(my_settings, &(&1.character_id == clicked_char_id))
existing = Enum.find(all_settings, &(&1.character_id == clicked_char_id))
{:ok, target_setting} =
if not is_nil(existing) do
@@ -229,7 +229,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
# If the target_setting is already followed => unfollow it
if target_setting.followed do
{:ok, updated} = WandererApp.MapCharacterSettingsRepo.unfollow(target_setting)
{:ok, _updated} = WandererApp.MapCharacterSettingsRepo.unfollow(target_setting)
else
# Only unfollow other rows from the current user
for s <- my_settings, s.id != target_setting.id, s.followed == true do
@@ -245,13 +245,13 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
:ok = add_characters([char], map_id, true)
end
{:ok, updated} = WandererApp.MapCharacterSettingsRepo.follow(target_setting)
{:ok, _updated} = WandererApp.MapCharacterSettingsRepo.follow(target_setting)
end
# re-fetch or re-map to confirm final results in UI
%{result: characters} = socket.assigns.characters
{:ok, tracked_characters} = get_tracked_map_characters(map_id, current_user)
{:ok, tracked_characters} = WandererApp.Maps.get_tracked_map_characters(map_id, current_user)
user_eve_ids = Enum.map(tracked_characters, & &1.eve_id)
{:ok, final_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
@@ -316,21 +316,6 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
def has_tracked_characters?([]), do: false
def has_tracked_characters?(_user_characters), do: true
def get_tracked_map_characters(map_id, current_user) do
case WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_filtered(
map_id,
current_user.characters |> Enum.map(& &1.id)
) do
{:ok, settings} ->
{:ok,
settings
|> Enum.map(fn s -> s |> Ash.load!(:character) |> Map.get(:character) end)}
_ ->
{:ok, []}
end
end
def map_ui_character(character),
do:
character

View File

@@ -119,6 +119,10 @@ defmodule WandererAppWeb.MapCoreEventHandler do
),
do: socket
def handle_server_event(%{event: :structures_updated, payload: _solar_system_id}, socket) do
socket
end
def handle_server_event(event, socket) do
Logger.warning(fn -> "unhandled map core event: #{inspect(event)} #{inspect(socket)} " end)
socket
@@ -266,6 +270,8 @@ defmodule WandererAppWeb.MapCoreEventHandler do
current_user.characters |> Enum.map(& &1.id)
)
{:ok, map_user_settings} = WandererApp.MapUserSettingsRepo.get(map_id, current_user.id)
{:ok, character_settings} =
case WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id) do
{:ok, settings} -> {:ok, settings}
@@ -298,6 +304,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
socket
|> assign(
map_id: map_id,
map_user_settings: map_user_settings,
page_title: map_name,
user_permissions: user_permissions,
tracked_character_ids: tracked_character_ids,
@@ -330,9 +337,8 @@ defmodule WandererAppWeb.MapCoreEventHandler do
} = socket
) do
with {:ok, _} <- current_user |> WandererApp.Api.User.update_last_map(%{last_map_id: map_id}),
{:ok, map_user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user.id),
{:ok, tracked_map_characters} <-
MapCharactersEventHandler.get_tracked_map_characters(map_id, current_user),
WandererApp.Maps.get_tracked_map_characters(map_id, current_user),
{:ok, characters_limit} <- map_id |> WandererApp.Map.get_characters_limit(),
{:ok, present_character_ids} <-
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", []),
@@ -410,7 +416,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
socket
|> map_start(%{
map_id: map_id,
map_user_settings: map_user_settings,
user_characters: user_character_eve_ids,
initial_data: initial_data,
events: events
@@ -433,7 +438,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
socket,
%{
map_id: map_id,
map_user_settings: map_user_settings,
user_characters: user_character_eve_ids,
initial_data: initial_data,
events: events
@@ -464,7 +468,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
socket
|> assign(
map_loaded?: true,
map_user_settings: map_user_settings,
user_characters: user_character_eve_ids,
has_tracked_characters?: has_tracked_characters?
)
@@ -538,21 +541,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
}
end
defp get_tracked_map_characters(map_id, current_user) do
case WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_filtered(
map_id,
current_user.characters |> Enum.map(& &1.id)
) do
{:ok, settings} ->
{:ok,
settings
|> Enum.map(fn s -> s |> Ash.load!(:character) |> Map.get(:character) end)}
_ ->
{:ok, []}
end
end
defp filter_map_characters(
characters,
user_character_eve_ids,

View File

@@ -81,6 +81,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
},
%{
assigns: %{
current_user: current_user,
map_id: map_id,
map_user_settings: map_user_settings,
user_characters: user_characters,
@@ -129,7 +130,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
if delete_connection_with_sigs && not is_nil(s.linked_system_id) do
map_id
|> WandererApp.Map.Server.delete_connection(%{
solar_system_source_id: solar_system_id |> String.to_integer(),
solar_system_source_id: system.solar_system_id,
solar_system_target_id: s.linked_system_id
})
end
@@ -161,10 +162,40 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
end)
added_signatures
|> Enum.map(fn s ->
|> Enum.each(fn s ->
s |> WandererApp.Api.MapSystemSignature.create!()
end)
added_signatures_eve_ids =
added_signatures
|> Enum.map(fn s -> s.eve_id end)
first_tracked_character =
current_user.characters
|> Enum.find(fn c -> c.eve_id === first_character_eve_id end)
if not is_nil(first_tracked_character) &&
not (added_signatures_eve_ids |> Enum.empty?()) do
WandererApp.User.ActivityTracker.track_map_event(:signatures_added, %{
character_id: first_tracked_character.id,
user_id: current_user.id,
map_id: map_id,
solar_system_id: system.solar_system_id,
signatures: added_signatures_eve_ids
})
end
if not is_nil(first_tracked_character) &&
not (removed_signatures_eve_ids |> Enum.empty?()) do
WandererApp.User.ActivityTracker.track_map_event(:signatures_removed, %{
character_id: first_tracked_character.id,
user_id: current_user.id,
map_id: map_id,
solar_system_id: system.solar_system_id,
signatures: removed_signatures_eve_ids
})
end
Phoenix.PubSub.broadcast!(WandererApp.PubSub, map_id, %{
event: :signatures_updated,
payload: system.solar_system_id

View File

@@ -0,0 +1,153 @@
defmodule WandererAppWeb.MapStructuresEventHandler do
use WandererAppWeb, :live_component
use Phoenix.Component
require Logger
alias WandererAppWeb.MapEventHandler
alias WandererApp.Api.MapSystem
alias WandererApp.Structure
def handle_ui_event("get_structures", %{"system_id" => solar_system_id}, %{assigns: %{map_id: map_id}} = socket) do
case MapSystem.read_by_map_and_solar_system(%{
map_id: map_id,
solar_system_id: String.to_integer(solar_system_id)
}) do
{:ok, system} ->
{:reply, %{structures: get_system_structures(system.id)}, socket}
_ ->
{:reply, %{structures: []}, socket}
end
end
def handle_ui_event(
"update_structures",
%{
"system_id" => solar_system_id,
"added" => added_structures,
"updated" => updated_structures,
"removed" => removed_structures
},
%{
assigns: %{
map_id: map_id,
user_characters: user_characters,
user_permissions: %{update_system: true}
}
} = socket
) do
with {:ok, system} <- get_map_system(map_id, solar_system_id),
:ok <- ensure_user_has_tracked_character(user_characters) do
Logger.debug(fn ->
"[handle_ui_event:update_structures] loaded map_system =>\n" <>
inspect(system, pretty: true)
end)
Structure.update_structures(
system,
added_structures,
updated_structures,
removed_structures,
user_characters
)
broadcast_structures_updated(system, map_id)
{:reply, %{structures: get_system_structures(system.id)}, socket}
else
:no_tracked_character ->
{:reply,
%{structures: []},
put_flash(socket, :error, "You must have at least one tracked character to work with structures.")}
_ ->
{:noreply, socket}
end
end
def handle_ui_event("get_corporation_names", %{"search" => search}, %{assigns: %{current_user: current_user}} = socket) do
user_chars = current_user.characters
case Structure.search_corporation_names(user_chars, search) do
{:ok, results} ->
{:reply, %{results: results}, socket}
{:error, reason} ->
Logger.warning("[MapStructuresEventHandler] corp search failed: #{inspect(reason)}")
{:reply, %{results: []}, socket}
_ ->
{:reply, %{results: []}, socket}
end
end
def handle_ui_event("get_corporation_ticker", %{"corp_id" => corp_id}, socket) do
case WandererApp.Esi.get_corporation_info(corp_id) do
{:ok, %{"ticker" => ticker}} ->
{:reply, %{ticker: ticker}, socket}
_ ->
{:reply, %{ticker: nil}, socket}
end
end
defp get_map_system(map_id, solar_system_id) do
case MapSystem.read_by_map_and_solar_system(%{
map_id: map_id,
solar_system_id: String.to_integer(solar_system_id)
}) do
{:ok, system} -> {:ok, system}
_ -> :error
end
end
defp ensure_user_has_tracked_character(user_characters) do
if Enum.empty?(user_characters) or is_nil(List.first(user_characters)) do
:no_tracked_character
else
:ok
end
end
defp broadcast_structures_updated(system, map_id) do
Phoenix.PubSub.broadcast!(
WandererApp.PubSub,
map_id,
%{event: :structures_updated, payload: system.solar_system_id}
)
end
def get_system_structures(system_id) do
results =
WandererApp.Api.MapSystemStructure.by_system_id!(system_id)
|> Enum.map(fn record ->
record
|> Map.take([
:id,
:system_id,
:solar_system_id,
:solar_system_name,
:structure_type_id,
:character_eve_id,
:name,
:notes,
:owner_name,
:owner_ticker,
:owner_id,
:status,
:end_time,
:inserted_at,
:updated_at,
:structure_type
])
|> Map.update!(:inserted_at, &Calendar.strftime(&1, "%Y/%m/%d %H:%M:%S"))
|> Map.update!(:updated_at, &Calendar.strftime(&1, "%Y/%m/%d %H:%M:%S"))
end)
Logger.debug(fn ->
"[get_system_structures] => returning:\n" <> inspect(results, pretty: true)
end)
results
end
end

View File

@@ -21,57 +21,63 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|> MapEventHandler.push_map_event("remove_systems", solar_system_ids)
def handle_server_event(
%{
event: :maybe_select_system,
payload: %{
character_id: character_id,
solar_system_id: solar_system_id
}
},
%{assigns: %{current_user: current_user, map_id: map_id, map_user_settings: map_user_settings}} = socket
) do
%{
event: :maybe_select_system,
payload: %{
character_id: character_id,
solar_system_id: solar_system_id
}
},
%{
assigns: %{
current_user: current_user,
map_id: map_id,
map_user_settings: map_user_settings
}
} = socket
) do
is_user_character =
current_user.characters
|> Enum.map(& &1.id)
|> Enum.member?(character_id)
is_user_character =
current_user.characters
|> Enum.map(& &1.id)
|> Enum.member?(character_id)
is_select_on_spash =
map_user_settings
|> WandererApp.MapUserSettingsRepo.to_form_data!()
|> WandererApp.MapUserSettingsRepo.get_boolean_setting("select_on_spash")
is_select_on_spash =
map_user_settings
|> WandererApp.MapUserSettingsRepo.to_form_data!()
|> WandererApp.MapUserSettingsRepo.get_boolean_setting("select_on_spash")
is_followed =
case WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character_id) do
{:ok, setting} -> setting.followed == true
_ -> false
end
is_followed =
case WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character_id) do
{:ok, setting} -> setting.followed == true
_ -> false
end
must_select? = is_user_character && (is_select_on_spash || is_followed)
must_select? = is_user_character && (is_select_on_spash || is_followed)
if not must_select? do
if not must_select? do
socket
else
# Check if we already selected this exact system for this char:
last_selected =
WandererApp.Cache.lookup!(
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
nil
)
if last_selected == solar_system_id do
# same system => skip
socket
else
# Check if we already selected this exact system for this char:
last_selected =
WandererApp.Cache.lookup!(
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
nil
)
# new system => update cache + push event
WandererApp.Cache.put(
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
solar_system_id
)
if last_selected == solar_system_id do
# same system => skip
socket
else
# new system => update cache + push event
WandererApp.Cache.put(
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
solar_system_id
)
socket
|> MapEventHandler.push_map_event("select_system", solar_system_id)
end
socket
|> MapEventHandler.push_map_event("select_system", solar_system_id)
end
end
end
def handle_server_event(%{event: :kills_updated, payload: kills}, socket) do

View File

@@ -171,7 +171,9 @@ defmodule WandererAppWeb.MapAuditLive do
{"ACL Removed", :map_acl_removed},
{"Connection Added", :map_connection_added},
{"Connection Updated", :map_connection_updated},
{"Connection Removed", :map_connection_removed}
{"Connection Removed", :map_connection_removed},
{"Signatures Added", :signatures_added},
{"Signatures Removed", :signatures_removed}
])
|> load_activity(1)
end

View File

@@ -1,20 +1,3 @@
<main
id="map-events-list"
class="pt-20 w-full h-full col-span-2 lg:col-span-1 p-4 pl-20 pb-20 overflow-auto"
>
<div class="flex flex-col gap-4 w-full">
<.live_component
module={UserActivity}
id="user-activity"
notify_to={self()}
can_undo_types={@can_undo_types}
stream={@streams.activity}
page={@page}
end_of_stream?={@end_of_stream?}
event_name="activity_event"
/>
</div>
</main>
<nav class="fixed top-0 z-100 px-6 pl-20 flex items-center justify-between w-full h-12 pointer-events-auto border-b border-stone-800 bg-opacity-70 bg-neutral-900">
<span className="w-full font-medium text-sm">
<.link navigate={~p"/#{@map_slug}"} class="text-neutral-100">
@@ -113,3 +96,20 @@
</div>
</div>
</nav>
<main
id="map-events-list"
class="pt-20 w-full h-full col-span-2 lg:col-span-1 p-4 pl-20 pb-20 overflow-auto"
>
<div class="flex flex-col gap-4 w-full">
<.live_component
module={UserActivity}
id="user-activity"
notify_to={self()}
can_undo_types={@can_undo_types}
stream={@streams.activity}
page={@page}
end_of_stream?={@end_of_stream?}
event_name="activity_event"
/>
</div>
</main>

View File

@@ -10,7 +10,8 @@ defmodule WandererAppWeb.MapEventHandler do
MapCoreEventHandler,
MapRoutesEventHandler,
MapSignaturesEventHandler,
MapSystemsEventHandler
MapSystemsEventHandler,
MapStructuresEventHandler,
}
@map_characters_events [
@@ -103,6 +104,17 @@ defmodule WandererAppWeb.MapEventHandler do
"unlink_signature"
]
@map_structures_events [
:structures_updated,
]
@map_structures_ui_events [
"update_structures",
"get_structures",
"get_corporation_names",
"get_corporation_ticker",
]
def handle_event(socket, %{event: event_name} = event)
when event_name in @map_characters_events,
do: MapCharactersEventHandler.handle_server_event(event, socket)
@@ -123,6 +135,10 @@ defmodule WandererAppWeb.MapEventHandler do
when event_name in @map_routes_events,
do: MapRoutesEventHandler.handle_server_event(event, socket)
def handle_event(socket, %{event: event_name} = event)
when event_name in @map_structures_events,
do: MapSignaturesEventHandler.handle_server_event(event, socket)
def handle_event(socket, %{event: event_name} = event)
when event_name in @map_signatures_events,
do: MapSignaturesEventHandler.handle_server_event(event, socket)
@@ -175,6 +191,10 @@ defmodule WandererAppWeb.MapEventHandler do
when event in @map_signatures_ui_events,
do: MapSignaturesEventHandler.handle_ui_event(event, body, socket)
def handle_ui_event(event, body, socket)
when event in @map_structures_ui_events,
do: MapStructuresEventHandler.handle_ui_event(event, body, socket)
def handle_ui_event(event, body, socket)
when event in @map_activity_ui_events,
do: MapActivityEventHandler.handle_ui_event(event, body, socket)

View File

@@ -112,7 +112,7 @@ defmodule WandererAppWeb.MapsLive do
subscription_form = %{
"plan" => "omega",
"period" => "1",
"characters_limit" => "300",
"characters_limit" => "100",
"hubs_limit" => "10",
"auto_renew?" => true
}
@@ -636,6 +636,33 @@ defmodule WandererAppWeb.MapsLive do
{:map_acl_updated, added_acls, removed_acls}
)
{:ok, tracked_characters} =
WandererApp.Maps.get_tracked_map_characters(map.id, current_user)
first_tracked_character_id = Enum.map(tracked_characters, & &1.id) |> List.first()
added_acls
|> Enum.each(fn acl_id ->
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:map_acl_added, %{
character_id: first_tracked_character_id,
user_id: current_user.id,
map_id: map.id,
acl_id: acl_id
})
end)
removed_acls
|> Enum.each(fn acl_id ->
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:map_acl_removed, %{
character_id: first_tracked_character_id,
user_id: current_user.id,
map_id: map.id,
acl_id: acl_id
})
end)
{:noreply,
socket
|> assign_async(:maps, fn ->

View File

@@ -580,11 +580,9 @@
>
<div :if={is_nil(@selected_subscription)}>
Add subscription
<div class="badge badge-secondary">Limited time offer: 50%</div>
</div>
<div :if={not is_nil(@selected_subscription)}>
Edit subscription
<div class="badge badge-secondary">Limited time offer: 50%</div>
</div>
<.form
:let={f}
@@ -609,7 +607,7 @@
label="Characters limit"
show_value={true}
type="range"
min="300"
min="100"
max="5000"
step="100"
class="range range-xs"

View File

@@ -129,6 +129,9 @@ scope "/api/map", WandererAppWeb do
# GET /api/map/characters?map_id=... or slug=...
get "/characters", APIController, :tracked_characters_with_info
# GET /api/map/structure-timers?map_id=... or slug=... and optionally ?system_id=...
get "/structure-timers", APIController, :show_structure_timers
end
scope "/api/common", WandererAppWeb do

View File

@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.39.2"
@version "1.43.9"
def project do
[

View File

@@ -0,0 +1,138 @@
%{
title: "How to Add a Custom Theme",
author: "Wanderer Community",
cover_image_uri: "/images/news/01-13-theme-swap/theme-selector.png",
tags: ~w(themes),
description: "",
}
---
# How to Add a Custom Theme
Adding a custom theme to your map is a great way to give it a unique look and feel. In this guide, well walk you through the necessary steps to create and enable a brand-new theme, from updating the theme selector to creating custom SCSS files.
---
1. Add Your Theme to the Theme Selector
Open the file:
assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettings.tsx
In this file, youll find an array called `THEME_OPTIONS`. Simply add your new theme to this array, giving it both a `label` and a `value`. For example:
```ts
const THEME_OPTIONS = [
{ label: 'Default', value: 'default' },
{ label: 'Pathfinder', value: 'pathfinder' },
{ label: 'YourTheme', value: 'yourtheme' },
];
```
This ensures your new theme will appear in the theme selection menu.
---
2. Create the SCSS File for Your Theme
Next, youll need to create a new SCSS file to define your themes custom styles. Navigate to:
```
assets/js/hooks/Mapper/components/map/styles
```
and add a new file. You can use `pathfinder-theme.scss` as a reference. The **filename must be in the format**:
```
yourthemename-theme.scss
```
> **Why the specific format?**
> The system looks for theme files following this naming pattern. If you choose a different format, it will not load correctly.
# 2.1. Define Your CSS Variables
Inside your theme SCSS file, you can override the variables below to customize colors, backgrounds, patterns, text, and more. For example:
```scss
// yourtheme-theme.scss
:root {
/* Main pane background color */
--rf-bg-color: #222;
--rf-soft-bg-color: #333;
/* Background pattern settings */
--rf-bg-variant: lines;
--rf-bg-gap: 10px;
--rf-bg-size: 1px;
--rf-bg-pattern-color: rgba(255, 255, 255, 0.15);
/* Node (system) appearance */
--rf-node-bg-color: #444;
--rf-node-soft-bg-color: #555;
--rf-node-font-family: "Roboto", sans-serif;
--rf-text-color: #f5f5f5;
--rf-region-name: #a3e4d7;
--rf-custom-name: #d7bde2;
--rf-tag-color: #e59866;
--rf-has-user-character: #f9e79f;
/* Eve-specific overrides */
--eve-effect-nullsec: #ff0000;
--eve-wh-type-color-C1: #aaffaa;
/* ...etc... */
}
```
> **Tip:** Feel free to rename or add new custom variables as necessary, but keep in mind the defaults and naming conventions used throughout the existing code.
---
3. Customize Node-Related Styles
If you want to override more specific aspects of the node styling, review the file:
```
assets/js/hooks/Mapper/components/map/components/SolarSystemNode/SolarSystemNodeTheme.module.scss
```
This file shows which variables are already set up for styling through CSS variables. If the element you want to style already has a variable reference, you can simply override that variable in your SCSS theme file.
---
4. Update Theme Behavior (Optional)
Finally, if your theme requires special interactions, you can update the theme behavior in:
```
assets/js/hooks/Mapper/components/map/helpers/getThemeBehavior.ts
```
By default, some overrides are already set up. For example:
- `isPanAndDrag: true` sets left-click to select, and right-click to pan. (When `false`, it uses the default behavior)
- `nodeComponent: SolarSystemNodeTheme` specifies a special node component that uses theme CSS overrides -- you could also provide your own node component here
- `connectionMode: ConnectionMode.Loose` allows you to control how strict the connection handles are.
If your theme needs custom logic—like a different node component or a unique interaction pattern—this is where youd implement it.
---
Summary
1. **Add your theme** to `THEME_OPTIONS` in `MapSettings.tsx`.
2. **Create a custom SCSS file** with the pattern `yourtheme-theme.scss` and override any relevant variables.
3. **Check for additional styling** in `SolarSystemNodeTheme.module.scss` to see if there are more elements youd like to override.
4. (Optional) **Modify the theme behavior** in `getThemeBehavior.ts` if you want your theme to have unique interaction patterns or different default behaviors.
By following these steps, youll be able to quickly add your own themed experience to the map. If you need to make further changes (like adding new variables or hooking into different node components), just follow the same pattern and refer to existing examples in the codebase. Happy theming!
---
### Example of heavily customize node component and theme
![Faoble Theme]("/images/news/01-13-theme-swap/faoble-theme.png")

View File

@@ -0,0 +1,151 @@
%{
title: "Managing Upwell Structures & Timers with the Structures Widget",
author: "Wanderer Team",
cover_image_uri: "/images/news/01-20-structure-widget/cover.png",
tags: ~w(interface guide map structures),
description: "Learn how to track structure information using the Structures Widget."
}
---
### Introduction
Upwell structures like **Astrahus**, **Athanor**, and more are key strategic points in EVE Online. Staying informed about their statuses—whether theyre anchoring, powered, or reinforced—helps you plan defenses, coordinate attacks, and align with allies. Our **Structures Widget** simplifies the process by allowing you to:
- Copy structure information directly from the in-game Directional Scanner (`D-Scan`) and paste it into the widget.
- Keep track of **anchoring** or **reinforced** timers, including exact vulnerability windows.
- Share real-time data across the map with your corporation or alliance, ensuring everyone is on the same page.
In this guide, well explore how to enable the Structures Widget, manage structure data, and make use of the built-in API for remote structure updates.
---
### 1. Enabling the Structure Widget
![Enabling the Structures Widget](/images/news/01-20-structure-widget/enable-widget.png "Enable Structures Widget")
1. **Open the Map:**
2. **Locate the Widget Settings:** By default, the structure widget panel is not visible. Enable it by going to menu -> map settings -> widgets.
3. **Add the Structures Widget:** Click the checkbox for **Structures** from the list of available widgets.
> **Tip:** Rearrange your widgets by dragging them around the panel to suit your workflow.
---
### 2. Overview of the Structures Widget
![Structures Widget Overview](/images/news/01-20-structure-widget/cover.png "Structures Widget")
Once enabled, the **Structures Widget** appears in the map. It shows:
- **Structure Type** (Astrahus, Fortizar, etc.)
- **Structure Name** (auto-detected if you paste from D-Scan)
- **Owner** (Corporation ticker)
- **Status** (Powered, Anchoring, Low Power, Reinforced, etc.)
- **Timer** (Reinforced or anchoring end time)
You can **click** or **double-click** on an entry to edit details like the structures owner or add notes about the structures purpose or location.
---
### 3. Adding Structures via Copy & Paste
A fast way to add structure data is by copying from in-game D-Scan or show-info panels:
1. **In EVE Online:** Open the D-Scan window or structure context menu, select the relevant lines of text, and press **Ctrl + C**.
2. **In the Widget:** Focus on the Structures Widget, click in the widget area, and press **Ctrl + V** to paste or use the **blue** add structure info button.
3. The widget automatically parses the structure names and types. You can also add owners and notes manually.
This eliminates manual typing and reduces the chance of errors, especially useful when scanning multiple systems.
---
### 4. Tracking Reinforced Timers
When a structure is in a **Reinforced** or **Anchoring** state, we have a timer to note when it becomes vulnerable or completes anchoring:
- **Timer Field:** If the structures status is set to “Reinforced” or “Anchoring,” the widget enables a **Calendar** pop-up where you can set the _end time_.
Keep your fleet prepared by referencing this schedule. When the timer hits zero, the structure becomes vulnerable (or finishes anchoring).
---
### 5. Editing and Deleting Structures
1. **Single-click** a structure entry to select it.
2. Press **Delete** (or **Backspace**) to remove it entirely—useful when clearing out old data or removing outdated structures.
3. **Double-click** to open the **Edit Dialog**:
- Change **Name**, **Owner**, or **Status**.
- Update or remove **Reinforced** timers.
- Add or edit **Notes**.
Any changes made here are immediately visible to other map users.
---
### 6. API Integration for Automated Timers
Beyond the in-app widget, there is a dedicated API endpoint to fetch or update structure timers programmatically. This allows advanced users and third-party applications to seamlessly incorporate structure data.
**Example API Request/Response**:
```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
"https://wanderer.yourdomain.space/api/map/structure-timers?slug=yourmap"
"data": [
{
"name": "Overlook Hotel",
"status": "Reinforced",
"notes": null,
"owner_id": null,
"solar_system_id": 31000515,
"solar_system_name": "J114942",
"character_eve_id": "2122839817",
"system_id": "4865aec4-b69d-4524-91d3-250b0556322b",
"end_time": "2025-01-22T23:42:03.000000Z",
"owner_name": null,
"owner_ticker": null,
"structure_type": "Astrahus",
"structure_type_id": "35832"
},
{
"name": "Some Structure",
"status": "Reinforced",
"notes": null,
"owner_id": null,
"solar_system_id": 3100229,
"solar_system_name": "somecustomname",
"character_eve_id": "some name",
"system_id": "ae779ed6-92b3-4349-899d-f1bdf299082f",
"end_time": "2025-01-16T03:04:00.000000Z",
"owner_name": null,
"owner_ticker": null,
"structure_type": "Athanor",
"structure_type_id": "35835"
}
]
```
With this API, you could, for example, build automated pings on Slack/Discord when timers are about to expire or display status updates on a custom web dashboard.
> **Note:** Ensure your API token (`Bearer YOUR_API_TOKEN`) matches the api key generated for you map.
---
### 7. Best Practices & Tips
- **Keep Data Fresh:** Update timers as soon as possible after a structure enters reinforcement. This keeps your corporation or alliance fully informed.
- **Use Notes Effectively:** Add details such as final reinforcement phases or relevant system intel (e.g., known hostiles, safe spots) to help allies plan more effectively.
---
## Conclusion
The **Structures Widget** is your central hub for monitoring, updating, and sharing information about Upwell structures across New Eden. From real-time timer tracking to simple copy-and-paste integration with D-Scan, this widget streamlines group operations and cuts down on manual data entry.
Whether youre a solo explorer managing a personal citadel network or a fleet commander overseeing multiple staging systems, the Structures Widget and its accompanying API ensure youll always have up-to-date intel on the structures that matter most.
Fly safe,
**The Wanderer Team**

View File

@@ -1,8 +1,9 @@
{
"21000325": true,
"21000326": true,
"21000327": true,
"21000328": true,
"21000329": true,
"21000330": true
}
"21000325": true,
"21000326": true,
"21000327": true,
"21000328": true,
"21000329": true,
"21000330": true,
"21000333": true
}

View File

@@ -222,7 +222,7 @@
{
"mass_regen": 500000000,
"dest": "hs",
"src": ["c3"],
"src": ["c3", "c4-shattered"],
"static": true,
"max_mass_per_jump": 300000000,
"lifetime": "24",

View File

@@ -0,0 +1,49 @@
defmodule WandererApp.Repo.Migrations.AddSystemStructures do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
create table(:map_system_structures_v1, primary_key: false) do
add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
add :structure_type_id, :text, null: false
add :structure_type, :text, null: false
add :character_eve_id, :text, null: false
add :solar_system_name, :text, null: false
add :solar_system_id, :bigint, null: false
add :name, :text, null: false
add :notes, :text
add :owner_name, :text
add :owner_ticker, :text
add :owner_id, :text
add :status, :text
add :end_time, :utc_datetime_usec
add :inserted_at, :utc_datetime_usec,
null: false,
default: fragment("(now() AT TIME ZONE 'utc')")
add :updated_at, :utc_datetime_usec,
null: false,
default: fragment("(now() AT TIME ZONE 'utc')")
add :system_id,
references(:map_system_v1,
column: :id,
name: "map_system_structures_v1_system_id_fkey",
type: :uuid,
prefix: "public"
)
end
end
def down do
drop constraint(:map_system_structures_v1, "map_system_structures_v1_system_id_fkey")
drop table(:map_system_structures_v1)
end
end

View File

@@ -0,0 +1,198 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"primary_key?": true,
"references": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "structure_type_id",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "structure_type",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "character_eve_id",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "solar_system_name",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "solar_system_id",
"type": "bigint"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "name",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "notes",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "owner_name",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "owner_ticker",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "owner_id",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "status",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "end_time",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "map_system_structures_v1_system_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "map_system_v1"
},
"size": null,
"source": "system_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "B9DA704034C53F0EC20C28EED99D579A34034655225EDC3BC7E57719B276F83F",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.WandererApp.Repo",
"schema": null,
"table": "map_system_structures_v1"
}