mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-04 23:05:35 +00:00
Compare commits
18 Commits
v1.44.8
...
design-and
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba5b2fb8d9 | ||
|
|
deb47b66f6 | ||
|
|
78eefcd6a7 | ||
|
|
eec78d38a8 | ||
|
|
73f8b1f06b | ||
|
|
f96cb01860 | ||
|
|
6800be1bb6 | ||
|
|
143f0a5b3a | ||
|
|
b6495504f8 | ||
|
|
2f07ec1b74 | ||
|
|
7073a0e8e6 | ||
|
|
bb0d91a3c7 | ||
|
|
1cb12b97ba | ||
|
|
860d20dc66 | ||
|
|
a850071965 | ||
|
|
fc41573e70 | ||
|
|
97f1808fb5 | ||
|
|
d31046eebb |
76
CHANGELOG.md
76
CHANGELOG.md
@@ -2,6 +2,82 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.46.0](https://github.com/wanderer-industries/wanderer/compare/v1.45.5...v1.46.0) (2025-02-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Added WANDERER_RESTRICT_MAPS_CREATION env support
|
||||
|
||||
## [v1.45.5](https://github.com/wanderer-industries/wanderer/compare/v1.45.4...v1.45.5) (2025-02-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* restore styling for local characters list (#152)
|
||||
|
||||
## [v1.45.4](https://github.com/wanderer-industries/wanderer/compare/v1.45.3...v1.45.4) (2025-02-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* remove snap to grid customization (#153)
|
||||
|
||||
## [v1.45.3](https://github.com/wanderer-industries/wanderer/compare/v1.45.2...v1.45.3) (2025-02-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* color and formatting fixes for local character (#150)
|
||||
|
||||
## [v1.45.2](https://github.com/wanderer-industries/wanderer/compare/v1.45.1...v1.45.2) (2025-02-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* fix route list hover and on the map character list (#149)
|
||||
|
||||
* correct formatting for on the map character list
|
||||
|
||||
* fix hover for route list
|
||||
|
||||
## [v1.45.1](https://github.com/wanderer-industries/wanderer/compare/v1.45.0...v1.45.1) (2025-02-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* kill count subscript position on firefox, and remove kill filter for single system (#148)
|
||||
|
||||
## [v1.45.0](https://github.com/wanderer-industries/wanderer/compare/v1.44.9...v1.45.0) (2025-02-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* allow filtering of k-space kills (#147)
|
||||
|
||||
## [v1.44.9](https://github.com/wanderer-industries/wanderer/compare/v1.44.8...v1.44.9) (2025-02-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* improve local character header shrink behavior (#146)
|
||||
|
||||
## [v1.44.8](https://github.com/wanderer-industries/wanderer/compare/v1.44.7...v1.44.8) (2025-02-04)
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ Now you can visit [`localhost:8000`](http://localhost:8000) from your browser.
|
||||
- `root@0d0a785313b6:/app# apt update`
|
||||
- `root@0d0a785313b6:/app# curl -sL https://deb.nodesource.com/setup_18.x | bash -`
|
||||
- `root@0d0a785313b6:/app# apt-get install nodejs inotify-tools -y`
|
||||
- `root@0d0a785313b6:/app# npm install -g yarn`
|
||||
- `root@0d0a785313b6:/app# mix setup`
|
||||
|
||||
- See how to run server in #Run section
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './useSystemInfo';
|
||||
export * from './useGetOwnOnlineCharacters';
|
||||
export * from './useElementWidth';
|
||||
|
||||
43
assets/js/hooks/Mapper/components/hooks/useElementWidth.ts
Normal file
43
assets/js/hooks/Mapper/components/hooks/useElementWidth.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useState, useLayoutEffect, RefObject } from 'react';
|
||||
|
||||
/**
|
||||
* useElementWidth
|
||||
*
|
||||
* A custom hook that accepts a ref to an HTML element and returns its current width.
|
||||
* It uses a ResizeObserver and window resize listener to update the width when necessary.
|
||||
*
|
||||
* @param ref - A RefObject pointing to an HTML element.
|
||||
* @returns The current width of the element.
|
||||
*/
|
||||
export function useElementWidth<T extends HTMLElement>(ref: RefObject<T>): number {
|
||||
const [width, setWidth] = useState<number>(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const updateWidth = () => {
|
||||
if (ref.current) {
|
||||
const newWidth = ref.current.getBoundingClientRect().width;
|
||||
if (newWidth > 0) {
|
||||
setWidth(newWidth);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateWidth(); // Initial measurement
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
const id = setTimeout(updateWidth, 100);
|
||||
return () => clearTimeout(id);
|
||||
});
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
window.addEventListener("resize", updateWidth);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("resize", updateWidth);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
return width;
|
||||
}
|
||||
@@ -125,7 +125,7 @@ const MapComp = ({
|
||||
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
|
||||
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
|
||||
const { update } = useMapState();
|
||||
const { variant, gap, size, color, snapSize } = useBackgroundVars(theme);
|
||||
const { variant, gap, size, color } = useBackgroundVars(theme);
|
||||
const { isPanAndDrag, nodeComponent, connectionMode } = getBehaviorForTheme(theme || 'default');
|
||||
|
||||
const nodeTypes = useMemo(() => {
|
||||
@@ -282,7 +282,6 @@ const MapComp = ({
|
||||
nodeTypes={nodeTypes}
|
||||
connectionMode={connectionMode}
|
||||
snapToGrid
|
||||
snapGrid={[snapSize, snapSize]}
|
||||
nodeDragThreshold={10}
|
||||
onNodeDragStop={handleDragStop}
|
||||
onSelectionDragStop={handleSelectionDragStop}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
.TooltipActive {
|
||||
pointer-events: auto !important;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.localCounter {
|
||||
mix-blend-mode: screen;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
color: var(--rf-node-local-counter);
|
||||
|
||||
&.hasUserCharacters {
|
||||
color: var(--rf-has-user-characters);
|
||||
}
|
||||
|
||||
& > i {
|
||||
font-size: 9px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-weight: var(--rf-local-counter-font-weight, 500);
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Pathfinder {
|
||||
.localCounter {
|
||||
@-moz-document url-prefix() {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
& > span {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,25 +2,23 @@ import { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
|
||||
import { LocalCharactersList, CharItemProps } from '../../../mapInterface/widgets/LocalCharacters/components';
|
||||
import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widgets/LocalCharacters/components';
|
||||
import { useLocalCharactersItemTemplate } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters';
|
||||
import { useLocalCharacterWidgetSettings } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalWidgetSettings';
|
||||
import classes from './SolarSystemLocalCounter.module.scss';
|
||||
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useTheme } from '@/hooks/Mapper/hooks/useTheme.ts';
|
||||
|
||||
interface LocalCounterProps {
|
||||
localCounterCharacters: Array<CharItemProps>;
|
||||
classes: { [key: string]: string };
|
||||
hasUserCharacters: boolean;
|
||||
showIcon?: boolean;
|
||||
}
|
||||
|
||||
export function LocalCounter({
|
||||
localCounterCharacters,
|
||||
hasUserCharacters,
|
||||
classes,
|
||||
showIcon = true,
|
||||
}: LocalCounterProps) {
|
||||
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
|
||||
const [settings] = useLocalCharacterWidgetSettings();
|
||||
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
|
||||
const theme = useTheme();
|
||||
|
||||
const pilotTooltipContent = useMemo(() => {
|
||||
return (
|
||||
@@ -42,23 +40,26 @@ export function LocalCounter({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.LocalCounterLayer} style={{ zIndex: 9999 }}>
|
||||
<div
|
||||
className={clsx(classes.TooltipActive, {
|
||||
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
|
||||
})}
|
||||
>
|
||||
<WdTooltipWrapper
|
||||
// @ts-ignore
|
||||
content={pilotTooltipContent}
|
||||
position={TooltipPosition.right}
|
||||
offset={180}
|
||||
interactive={true}
|
||||
offset={0}
|
||||
>
|
||||
<div
|
||||
className={clsx(classes.localCounter, {
|
||||
[classes.hasUserCharacters]: hasUserCharacters,
|
||||
})}
|
||||
>
|
||||
{showIcon && <i className="pi pi-users" style={{ fontSize: '0.50rem' }} />}
|
||||
{showIcon && <i className="pi pi-users" />}
|
||||
<span>{localCounterCharacters.length}</span>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ $tooltip-bg: #202020;
|
||||
border: 1px solid darken($pastel-blue, 10%);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
|
||||
&.Mataria,
|
||||
@@ -91,8 +91,8 @@ $tooltip-bg: #202020;
|
||||
&.eve-system-status-home {
|
||||
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-home),
|
||||
45deg,
|
||||
var(--eve-solar-system-status-color-background),
|
||||
transparent
|
||||
);
|
||||
&.selected {
|
||||
@@ -273,7 +273,7 @@ $tooltip-bg: #202020;
|
||||
height: 19px;
|
||||
|
||||
.hasLocalCounter {
|
||||
margin-right: 1.25rem;
|
||||
margin-right: 2px;
|
||||
&.countAbove9 {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
@@ -319,7 +319,8 @@ $tooltip-bg: #202020;
|
||||
|
||||
.Handlers {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
@@ -332,6 +333,7 @@ $tooltip-bg: #202020;
|
||||
border: 1px solid $pastel-blue;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
pointer-events: auto;
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
@@ -375,38 +377,3 @@ $tooltip-bg: #202020;
|
||||
}
|
||||
}
|
||||
|
||||
.LocalCounterLayer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
padding: 8;
|
||||
|
||||
.localCounter {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
top: 10.5px;
|
||||
right: 8px;
|
||||
mix-blend-mode: screen;
|
||||
gap: 2px;
|
||||
color: var(--rf-node-local-counter, #5cb85c);
|
||||
|
||||
&.hasUserCharacters {
|
||||
color: var(--rf-has-user-characters, #fbbf24);
|
||||
}
|
||||
|
||||
& > i {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-weight: var(--rf-local-counter-font-weight, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
|
||||
{ [classes.selected]: nodeVars.selected },
|
||||
)}
|
||||
onMouseDownCapture={e => nodeVars.dbClick(e)}
|
||||
>
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
@@ -120,18 +121,18 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div
|
||||
className={clsx('flex items-center gap-1', {
|
||||
[classes.hasLocalCounter]: nodeVars.charactersInSystem.length > 0,
|
||||
[classes.countAbove9]: nodeVars.charactersInSystem.length > 9,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<div className={clsx('flex items-center gap-1')}>
|
||||
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
|
||||
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LocalCounter
|
||||
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||
localCounterCharacters={localCounterCharacters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -158,7 +159,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div onMouseDownCapture={e => nodeVars.dbClick(e)} className={classes.Handlers}>
|
||||
<div className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
@@ -200,11 +201,6 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
id="d"
|
||||
/>
|
||||
</div>
|
||||
<LocalCounter
|
||||
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||
localCounterCharacters={localCounterCharacters}
|
||||
classes={classes}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,3 +4,17 @@
|
||||
Only override what's different from the base
|
||||
Currently none required
|
||||
---------------------------------------------- */
|
||||
|
||||
.RootCustomNode {
|
||||
&.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
<div className={classes.Bookmarks}>
|
||||
{nodeVars.labelCustom !== '' && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -65,6 +65,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
|
||||
{ [classes.selected]: nodeVars.selected },
|
||||
)}
|
||||
onMouseDownCapture={e => nodeVars.dbClick(e)}
|
||||
>
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
@@ -130,18 +131,18 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div
|
||||
className={clsx('flex items-center gap-1', {
|
||||
[classes.hasLocalCounter]: nodeVars.charactersInSystem.length > 0,
|
||||
[classes.countAbove9]: nodeVars.charactersInSystem.length > 9,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<div className={clsx('flex items-center gap-1')}>
|
||||
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
|
||||
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LocalCounter
|
||||
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||
localCounterCharacters={localCounterCharacters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -168,7 +169,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
</>
|
||||
)}
|
||||
|
||||
<div onMouseDownCapture={e => nodeVars.dbClick(e)} className={classes.Handlers}>
|
||||
<div className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
@@ -210,11 +211,6 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
id="d"
|
||||
/>
|
||||
</div>
|
||||
<LocalCounter
|
||||
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||
localCounterCharacters={localCounterCharacters}
|
||||
classes={classes}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -31,6 +31,6 @@
|
||||
--window-corner: #72716f;
|
||||
|
||||
--rf-local-counter-font-weight: 500;
|
||||
--rf-node-local-counter: #5cb85c;
|
||||
--rf-has-user-characters: #fbbf24;
|
||||
--rf-node-local-counter: inherit;
|
||||
--rf-has-user-characters: #ffc75d;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
|
||||
$friendlyBase: #3bbd39;
|
||||
$friendlyAlpha: #3bbd3952;
|
||||
$friendlyBase: #3bbd39;
|
||||
$friendlyAlpha: #3bbd3952;
|
||||
$friendlyDark20: darken($friendlyBase, 20%);
|
||||
$friendlyDark30: darken($friendlyBase, 30%);
|
||||
$friendlyDark5: darken($friendlyBase, 5%);
|
||||
|
||||
$lookingForBase: #43c2fd;
|
||||
$lookingForBase: #43c2fd;
|
||||
$lookingForAlpha: rgba(67, 176, 253, 0.48);
|
||||
$lookingForDark15: darken($lookingForBase, 15%);
|
||||
|
||||
$homeBase: rgb(197, 253, 67);
|
||||
$homeAlpha: rgba(197, 253, 67, 0.32);
|
||||
$homeBase: rgb(179, 253, 67);
|
||||
$homeAlpha: rgba(186, 248, 48, 0.32);
|
||||
$homeBackground: #a0fa5636;
|
||||
$homeDark30: darken($homeBase, 30%);
|
||||
|
||||
|
||||
:root {
|
||||
--pastel-blue: #5a7d9a;
|
||||
--pastel-pink: #d291bc;
|
||||
@@ -98,6 +98,7 @@ $homeDark30: darken($homeBase, 30%);
|
||||
--eve-solar-system-status-color-unknown: transparent;
|
||||
--eve-solar-system-status-home: #{$homeAlpha};
|
||||
--eve-solar-system-status-color-home: #{$homeBase};
|
||||
--eve-solar-system-status-color-background: #{$homeBackground};
|
||||
--eve-solar-system-status-color-home-dark30: #{$homeDark30};
|
||||
--eve-solar-system-status-friendly: #{$friendlyAlpha};
|
||||
--eve-solar-system-status-color-friendly: #{$friendlyBase};
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
@import './eve-common';
|
||||
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
|
||||
|
||||
$homeBase: rgb(197, 253, 67);
|
||||
$homeAlpha: rgba(197, 253, 67, 0.32);
|
||||
$homeDark30: darken($homeBase, 30%);
|
||||
|
||||
.pathfinder-theme {
|
||||
/* -- Override values from the default theme -- */
|
||||
--rf-bg-color: #000000;
|
||||
@@ -41,4 +45,10 @@
|
||||
--eve-wh-type-color-c6: #d9534f;
|
||||
--eve-wh-type-color-c13: #7986cb;
|
||||
--eve-wh-type-color-drifter: #44aa82;
|
||||
|
||||
--rf-node-local-counter: #5cb85c;
|
||||
--rf-has-user-characters: #ffc75d;
|
||||
|
||||
--eve-solar-system-status-home: #{$homeAlpha};
|
||||
--eve-solar-system-status-color-home: #{$homeBase};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.VirtualScroller {
|
||||
height: 100% !important;
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import clsx from 'clsx';
|
||||
import { LayoutEventBlocker, WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters.ts';
|
||||
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters';
|
||||
import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { UserPermission } from '@/hooks/Mapper/types/permissions';
|
||||
import { LocalCharactersList } from './components/LocalCharactersList';
|
||||
import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters';
|
||||
import { useLocalCharacterWidgetSettings } from './hooks/useLocalWidgetSettings';
|
||||
import { LocalCharactersHeader } from './components/LocalCharactersHeader';
|
||||
import classes from './LocalCharacters.module.scss';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const LocalCharacters = () => {
|
||||
const {
|
||||
@@ -18,11 +17,9 @@ export const LocalCharacters = () => {
|
||||
} = useMapRootState();
|
||||
|
||||
const [settings, setSettings] = useLocalCharacterWidgetSettings();
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
|
||||
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
|
||||
|
||||
const showOffline = useMemo(
|
||||
() => !restrictOfflineShowing || isAdminOrManager,
|
||||
[isAdminOrManager, restrictOfflineShowing],
|
||||
@@ -42,7 +39,6 @@ export const LocalCharacters = () => {
|
||||
if (!showOffline || !settings.showOffline) {
|
||||
return filtered.filter(c => c.online);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [
|
||||
characters,
|
||||
@@ -58,52 +54,18 @@ export const LocalCharacters = () => {
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
const showList = sorted.length > 0 && selectedSystems.length === 1;
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const compact = useMaxWidth(ref, 145);
|
||||
|
||||
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
|
||||
|
||||
return (
|
||||
<Widget
|
||||
label={
|
||||
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
|
||||
<span className="select-none">Local{showList ? ` [${sorted.length}]` : ''}</span>
|
||||
<LayoutEventBlocker className="flex items-center gap-2">
|
||||
{showOffline && (
|
||||
<WdTooltipWrapper content="Show offline characters in system">
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compact ? '' : 'Show offline'}
|
||||
value={settings.showOffline}
|
||||
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
|
||||
onChange={() => setSettings(prev => ({ ...prev, showOffline: !prev.showOffline }))}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{settings.compact && (
|
||||
<WdTooltipWrapper content="Show ship name in compact rows">
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compact ? '' : 'Show ship name'}
|
||||
value={settings.showShipName}
|
||||
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
|
||||
onChange={() => setSettings(prev => ({ ...prev, showShipName: !prev.showShipName }))}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={clsx('w-4 h-4 cursor-pointer', {
|
||||
['hero-bars-2']: settings.compact,
|
||||
['hero-bars-3']: !settings.compact,
|
||||
})}
|
||||
onClick={() => setSettings(prev => ({ ...prev, compact: !prev.compact }))}
|
||||
/>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
<LocalCharactersHeader
|
||||
sortedCount={sorted.length}
|
||||
showList={showList}
|
||||
showOffline={showOffline}
|
||||
settings={settings}
|
||||
setSettings={setSettings}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isNotSelectedSystem && (
|
||||
@@ -111,19 +73,20 @@ export const LocalCharacters = () => {
|
||||
System is not selected
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isNobodyHere && !isNotSelectedSystem && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
|
||||
Nobody here
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showList && (
|
||||
<LocalCharactersList
|
||||
items={sorted}
|
||||
itemSize={settings.compact ? 26 : 41}
|
||||
itemTemplate={itemTemplate}
|
||||
containerClassName="w-full h-full overflow-x-hidden overflow-y-auto"
|
||||
containerClassName={clsx(
|
||||
'w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none',
|
||||
classes.VirtualScroller,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Widget>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// .VirtualScroller {
|
||||
// height: 100% !important;
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import { LayoutEventBlocker, TooltipPosition, WdCheckbox, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
interface LocalCharactersHeaderProps {
|
||||
sortedCount: number;
|
||||
showList: boolean;
|
||||
showOffline: boolean;
|
||||
settings: {
|
||||
compact: boolean;
|
||||
showOffline: boolean;
|
||||
showShipName: boolean;
|
||||
};
|
||||
setSettings: (fn: (prev: any) => any) => void;
|
||||
}
|
||||
|
||||
export const LocalCharactersHeader: React.FC<LocalCharactersHeaderProps> = ({
|
||||
sortedCount,
|
||||
showList,
|
||||
showOffline,
|
||||
settings,
|
||||
setSettings,
|
||||
}) => {
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const compactOffline = useMaxWidth(headerRef, 145);
|
||||
const compactShipName = useMaxWidth(headerRef, 195);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center text-xs justify-between" ref={headerRef}>
|
||||
<div className="flex-shrink-0 select-none mr-2">Local{showList ? ` [${sortedCount}]` : ''}</div>
|
||||
<LayoutEventBlocker className="flex items-center gap-2 justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
{showOffline && (
|
||||
<WdTooltipWrapper content="Show offline characters in system" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compactOffline ? '' : 'Offline'}
|
||||
value={settings.showOffline}
|
||||
onChange={() => setSettings((prev: any) => ({ ...prev, showOffline: !prev.showOffline }))}
|
||||
classNameLabel={clsx('whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300', {
|
||||
truncate: compactOffline,
|
||||
})}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{settings.compact && (
|
||||
<WdTooltipWrapper content="Show ship name in compact rows" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compactShipName ? '' : 'Ship name'}
|
||||
value={settings.showShipName}
|
||||
onChange={() => setSettings((prev: any) => ({ ...prev, showShipName: !prev.showShipName }))}
|
||||
classNameLabel={clsx('whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300', {
|
||||
truncate: compactShipName,
|
||||
})}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
<WdTooltipWrapper content="Enable compact mode" position={TooltipPosition.top}>
|
||||
<span
|
||||
className={clsx('w-4 h-4 min-w-[1rem] cursor-pointer', {
|
||||
'hero-bars-2': settings.compact,
|
||||
'hero-bars-3': !settings.compact,
|
||||
})}
|
||||
onClick={() => setSettings((prev: any) => ({ ...prev, compact: !prev.compact }))}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,6 @@
|
||||
.VirtualScroller {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.CharacterRow {
|
||||
//border-left-width: 1px;
|
||||
|
||||
&.CardBorderLeftIsOwn {
|
||||
border-left-color: rgb(251 146 60 / 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import classes from './LocalCharactersItemTemplate.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { CharItemProps } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/components';
|
||||
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
|
||||
export type LocalCharactersItemTemplateProps = { showShipName: boolean } & CharItemProps &
|
||||
VirtualScrollerTemplateOptions;
|
||||
|
||||
export const LocalCharactersItemTemplate = ({ showShipName, ...options }: LocalCharactersItemTemplateProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
classes.CharacterRow,
|
||||
'box-border flex items-center w-full whitespace-nowrap overflow-hidden text-ellipsis min-w-[0px]',
|
||||
{
|
||||
'surface-hover': options.odd,
|
||||
'border-b border-gray-600 border-opacity-20': !options.last,
|
||||
'bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10': options.online,
|
||||
},
|
||||
)}
|
||||
style={{ height: `${options.props.itemSize}px` }}
|
||||
>
|
||||
<CharacterCard showShipName={showShipName} {...options} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './LocalCharactersItemTemplate.tsx';
|
||||
@@ -5,23 +5,24 @@ import { CharItemProps } from './types';
|
||||
|
||||
type LocalCharactersListProps = {
|
||||
items: Array<CharItemProps>;
|
||||
|
||||
itemSize: number;
|
||||
|
||||
itemTemplate: (char: CharItemProps, options: VirtualScrollerTemplateOptions) => React.ReactNode;
|
||||
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
export function LocalCharactersList({ items, itemSize, itemTemplate, containerClassName }: LocalCharactersListProps) {
|
||||
export const LocalCharactersList = ({
|
||||
items,
|
||||
itemSize,
|
||||
itemTemplate,
|
||||
containerClassName,
|
||||
}: LocalCharactersListProps) => {
|
||||
return (
|
||||
<VirtualScroller
|
||||
items={items}
|
||||
itemSize={itemSize}
|
||||
orientation="vertical"
|
||||
className={clsx('w-full h-full', containerClassName)}
|
||||
autoSize={false}
|
||||
itemTemplate={itemTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './LocalCharactersItemTemplate';
|
||||
export * from './LocalCharactersList';
|
||||
export * from './types';
|
||||
|
||||
@@ -1,33 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import clsx from 'clsx';
|
||||
import classes from './useLocalCharacters.module.scss';
|
||||
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { CharItemProps } from '../components';
|
||||
import { CharItemProps, LocalCharactersItemTemplate } from '../components';
|
||||
|
||||
export function useLocalCharactersItemTemplate(showShipName: boolean) {
|
||||
return useCallback(
|
||||
(char: CharItemProps, options: VirtualScrollerTemplateOptions) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.CharacterRow, 'box-border flex items-center', {
|
||||
'surface-hover': options.odd,
|
||||
'border-b border-gray-600 border-opacity-20': !options.last,
|
||||
'bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10': char.online,
|
||||
})}
|
||||
style={{
|
||||
height: `${options.props.itemSize}px`,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
minWidth: 0,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<CharacterCard showShipName={showShipName} {...char} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
(char: CharItemProps, options: VirtualScrollerTemplateOptions) => (
|
||||
<LocalCharactersItemTemplate {...char} {...options} showShipName={showShipName} />
|
||||
),
|
||||
[showShipName],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classes from './RoutesList.module.scss';
|
||||
import { Route, SystemStaticInfoShort } from '@/hooks/Mapper/types/routes.ts';
|
||||
import clsx from 'clsx';
|
||||
import { SystemViewStandalone, WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SystemViewStandalone, TooltipPosition, WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { getBackgroundClass, getShapeClass } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { MouseEvent, useCallback, useRef, useState } from 'react';
|
||||
import { Commands } from '@/hooks/Mapper/types';
|
||||
@@ -46,9 +46,11 @@ export const RouteSystem = ({
|
||||
<>
|
||||
<WdTooltip
|
||||
ref={tooltipRef}
|
||||
position={TooltipPosition.top}
|
||||
// targetSelector={`.tooltip-route-sys_${destination}_${solar_system_id}`}
|
||||
content={() => (
|
||||
<SystemViewStandalone
|
||||
className="mx-[4px]"
|
||||
security={security}
|
||||
system_class={system_class}
|
||||
class_title={class_title}
|
||||
@@ -63,8 +65,8 @@ export const RouteSystem = ({
|
||||
tooltipRef.current?.show(e);
|
||||
onMouseEnter?.(solar_system_id);
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
tooltipRef.current?.hide(e);
|
||||
onMouseLeave={() => {
|
||||
tooltipRef.current?.hide();
|
||||
onMouseLeave?.();
|
||||
}}
|
||||
onContextMenu={handleContext}
|
||||
|
||||
@@ -184,7 +184,7 @@ export const RoutesWidgetComp = () => {
|
||||
}, [data, update]);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const compact = useMaxWidth(ref, 155);
|
||||
const compact = useMaxWidth(ref, 170);
|
||||
const [openAddSystem, setOpenAddSystem] = useState<boolean>(false);
|
||||
|
||||
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
|
||||
@@ -217,14 +217,14 @@ export const RoutesWidgetComp = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<WdTooltipWrapper content="Show shortest route">
|
||||
<WdTooltipWrapper content="Show shortest route" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compact ? '' : 'Show shortest'}
|
||||
value={!isSecure}
|
||||
onChange={handleSecureChange}
|
||||
classNameLabel={clsx('text-red-400')}
|
||||
classNameLabel="text-red-400 whitespace-nowrap"
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
<WdImgButton
|
||||
|
||||
@@ -6,6 +6,7 @@ import { KillsHeader } from './components/SystemKillsHeader';
|
||||
import { useKillsWidgetSettings } from './hooks/useKillsWidgetSettings';
|
||||
import { useSystemKills } from './hooks/useSystemKills';
|
||||
import { KillsSettingsDialog } from './components/SystemKillsSettingsDialog';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
|
||||
|
||||
export const SystemKills: React.FC = () => {
|
||||
const {
|
||||
@@ -14,7 +15,6 @@ export const SystemKills: React.FC = () => {
|
||||
} = useMapRootState();
|
||||
|
||||
const [systemId] = selectedSystems || [];
|
||||
|
||||
const [settingsDialogVisible, setSettingsDialogVisible] = useState(false);
|
||||
|
||||
const systemNameMap = useMemo(() => {
|
||||
@@ -37,58 +37,81 @@ export const SystemKills: React.FC = () => {
|
||||
const isNothingSelected = !systemId && !visible;
|
||||
const showLoading = isLoading && kills.length === 0;
|
||||
|
||||
const filteredKills = useMemo(() => {
|
||||
if (!settings.whOnly || !visible) return kills;
|
||||
return kills.filter(kill => {
|
||||
const system = systems.find(
|
||||
sys => sys.system_static_info.solar_system_id === kill.solar_system_id
|
||||
);
|
||||
if (!system) {
|
||||
console.warn(`System with id ${kill.solar_system_id} not found.`);
|
||||
return false;
|
||||
}
|
||||
return isWormholeSpace(system.system_static_info.system_class);
|
||||
});
|
||||
}, [kills, settings.whOnly, systems]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col min-h-0">
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
<Widget label={<KillsHeader systemId={systemId} onOpenSettings={() => setSettingsDialogVisible(true)} />}>
|
||||
{!isSubscriptionActive && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
Kills available with 'Active' map subscription only (contact map administrators)
|
||||
</div>
|
||||
)}
|
||||
{isSubscriptionActive && (
|
||||
<>
|
||||
{isNothingSelected && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
<Widget
|
||||
label={
|
||||
<KillsHeader
|
||||
systemId={systemId}
|
||||
onOpenSettings={() => setSettingsDialogVisible(true)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="relative h-full">
|
||||
{!isSubscriptionActive ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
Kills available with 'Active' map subscription only (contact map administrators)
|
||||
</span>
|
||||
</div>
|
||||
) : isNothingSelected ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
No system selected (or toggle “Show all systems”)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isNothingSelected && showLoading && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
</span>
|
||||
</div>
|
||||
) : showLoading ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
Loading Kills...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isNothingSelected && !showLoading && error && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-red-400 text-sm">
|
||||
</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isNothingSelected && !showLoading && !error && (!kills || kills.length === 0) && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
</span>
|
||||
</div>
|
||||
) : !filteredKills || filteredKills.length === 0 ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
No kills found
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isNothingSelected && !showLoading && !error && (
|
||||
<div className="flex-1 flex flex-col overflow-y-auto">
|
||||
<SystemKillsContent
|
||||
key={settings.compact ? 'compact' : 'normal'}
|
||||
kills={kills}
|
||||
systemNameMap={systemNameMap}
|
||||
compact={settings.compact}
|
||||
onlyOneSystem={!visible}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<SystemKillsContent
|
||||
key={settings.compact ? 'compact' : 'normal'}
|
||||
kills={filteredKills}
|
||||
systemNameMap={systemNameMap}
|
||||
compact={settings.compact}
|
||||
onlyOneSystem={!visible}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Widget>
|
||||
</div>
|
||||
|
||||
<KillsSettingsDialog visible={settingsDialogVisible} setVisible={setSettingsDialogVisible} />
|
||||
<KillsSettingsDialog
|
||||
visible={settingsDialogVisible}
|
||||
setVisible={setSettingsDialogVisible}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ export const SystemKillsContent: React.FC<SystemKillsContentProps> = ({
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-col w-full text-stone-200 text-xs transition-all duration-300',
|
||||
compact ? 'p-1' : 'p-1',
|
||||
compact ? 'p-1' : 'p-1'
|
||||
)}
|
||||
>
|
||||
{sortedKills.map(kill => {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { zkillLink } from '../helpers';
|
||||
import classes from './SystemKillRow.module.scss';
|
||||
|
||||
interface AttackerRowSubInfoProps {
|
||||
finalBlowCharId: number | null | undefined;
|
||||
finalBlowCharName?: string;
|
||||
attackerPortraitUrl: string | null;
|
||||
|
||||
finalBlowCorpId: number | null | undefined;
|
||||
finalBlowCorpName?: string;
|
||||
attackerCorpLogoUrl: string | null;
|
||||
|
||||
finalBlowAllianceId: number | null | undefined;
|
||||
finalBlowAllianceName?: string;
|
||||
attackerAllianceLogoUrl: string | null;
|
||||
|
||||
containerHeight?: number;
|
||||
}
|
||||
|
||||
export const AttackerRowSubInfo: React.FC<AttackerRowSubInfoProps> = ({
|
||||
finalBlowCharId = 0,
|
||||
finalBlowCharName,
|
||||
attackerPortraitUrl,
|
||||
containerHeight = 8,
|
||||
}) => {
|
||||
if (!attackerPortraitUrl || finalBlowCharId === null || finalBlowCharId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const containerClass = `h-${containerHeight}`;
|
||||
|
||||
return (
|
||||
<div className={clsx('flex items-start gap-1', containerClass)}>
|
||||
<div className="relative shrink-0 w-auto h-full overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('character', finalBlowCharId)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block h-full"
|
||||
>
|
||||
<img
|
||||
src={attackerPortraitUrl}
|
||||
alt={finalBlowCharName || 'AttackerPortrait'}
|
||||
className={clsx(classes.killRowImage, 'h-full w-auto object-contain')}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -21,10 +21,15 @@ export interface CompactKillRowProps {
|
||||
onlyOneSystem: boolean;
|
||||
}
|
||||
|
||||
export const CompactKillRow: React.FC<CompactKillRowProps> = ({ killDetails, systemName, onlyOneSystem }) => {
|
||||
export const CompactKillRow: React.FC<CompactKillRowProps> = ({
|
||||
killDetails,
|
||||
systemName,
|
||||
onlyOneSystem,
|
||||
}) => {
|
||||
const {
|
||||
killmail_id = 0,
|
||||
|
||||
// Victim
|
||||
victim_char_name = 'Unknown Pilot',
|
||||
victim_alliance_ticker = '',
|
||||
victim_corp_ticker = '',
|
||||
@@ -36,6 +41,7 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({ killDetails, sys
|
||||
victim_alliance_id = 0,
|
||||
victim_ship_type_id = 0,
|
||||
|
||||
// Attacker
|
||||
final_blow_char_id = 0,
|
||||
final_blow_char_name = '',
|
||||
final_blow_alliance_ticker = '',
|
||||
@@ -51,66 +57,109 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({ killDetails, sys
|
||||
} = killDetails || {};
|
||||
|
||||
const attackerIsNpc = final_blow_char_id === 0;
|
||||
const victimAffiliationTicker = victim_alliance_ticker || victim_corp_ticker || 'No Ticker';
|
||||
const killValueFormatted = total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
|
||||
|
||||
// Tickers & strings
|
||||
const victimAffiliationTicker =
|
||||
victim_alliance_ticker || victim_corp_ticker || 'No Ticker';
|
||||
const killValueFormatted =
|
||||
total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
|
||||
const attackerName = attackerIsNpc ? '' : final_blow_char_name;
|
||||
const attackerTicker = attackerIsNpc ? '' : final_blow_alliance_ticker || final_blow_corp_ticker || '';
|
||||
const attackerTicker = attackerIsNpc
|
||||
? ''
|
||||
: final_blow_alliance_ticker || final_blow_corp_ticker || '';
|
||||
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
|
||||
const attackerSubscript = getAttackerSubscript(killDetails);
|
||||
|
||||
const { victimCorpLogoUrl, victimAllianceLogoUrl } = buildVictimImageUrls({
|
||||
// Victim images, including the ship
|
||||
const {
|
||||
victimCorpLogoUrl,
|
||||
victimAllianceLogoUrl,
|
||||
victimShipUrl,
|
||||
} = buildVictimImageUrls({
|
||||
victim_char_id,
|
||||
victim_ship_type_id,
|
||||
victim_corp_id,
|
||||
victim_alliance_id,
|
||||
});
|
||||
|
||||
// Attacker corp/alliance
|
||||
const { attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
|
||||
final_blow_char_id,
|
||||
final_blow_corp_id,
|
||||
final_blow_alliance_id,
|
||||
});
|
||||
|
||||
const { url: victimPrimaryLogoUrl, tooltip: victimPrimaryTooltip } = getPrimaryLogoAndTooltip(
|
||||
victimAllianceLogoUrl,
|
||||
victimCorpLogoUrl,
|
||||
victim_alliance_name,
|
||||
victim_corp_name,
|
||||
'Victim',
|
||||
);
|
||||
// Victim corp/alliance logo
|
||||
const { url: victimPrimaryLogoUrl, tooltip: victimPrimaryTooltip } =
|
||||
getPrimaryLogoAndTooltip(
|
||||
victimAllianceLogoUrl,
|
||||
victimCorpLogoUrl,
|
||||
victim_alliance_name,
|
||||
victim_corp_name,
|
||||
'Victim'
|
||||
);
|
||||
|
||||
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } = getAttackerPrimaryImageAndTooltip(
|
||||
attackerIsNpc,
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id || 0,
|
||||
);
|
||||
// Attacker corp/alliance or NPC ship
|
||||
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } =
|
||||
getAttackerPrimaryImageAndTooltip(
|
||||
attackerIsNpc,
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'h-10 flex items-center border-b border-stone-800',
|
||||
'text-xs whitespace-nowrap overflow-hidden leading-none',
|
||||
'text-xs whitespace-nowrap overflow-hidden leading-none'
|
||||
)}
|
||||
>
|
||||
{victimPrimaryLogoUrl && (
|
||||
<WdTooltipWrapper content={victimPrimaryTooltip} position={TooltipPosition.top}>
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative shrink-0 w-8 h-8 overflow-hidden"
|
||||
<div className="flex items-center gap-1">
|
||||
{victimShipUrl && (
|
||||
<div className="relative shrink-0 w-8 h-8 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimShipUrl}
|
||||
alt="VictimShip"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{victimPrimaryLogoUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={victimPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<img
|
||||
src={victimPrimaryLogoUrl}
|
||||
alt="VictimPrimaryLogo"
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
/>
|
||||
</a>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative block shrink-0 w-8 h-8 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={victimPrimaryLogoUrl}
|
||||
alt="VictimPrimaryLogo"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col ml-2 min-w-0 overflow-hidden leading-[1rem]">
|
||||
<div className="truncate text-stone-200">
|
||||
{victim_char_name}
|
||||
@@ -126,47 +175,53 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({ killDetails, sys
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center ml-auto gap-2">
|
||||
<div className="flex flex-col items-end min-w-0 overflow-hidden text-right leading-[1rem]">
|
||||
{!attackerIsNpc && (attackerName || attackerTicker) && (
|
||||
<div className="truncate text-stone-200">
|
||||
{attackerName}
|
||||
{attackerTicker && <span className="ml-1 text-stone-400">/ {attackerTicker}</span>}
|
||||
{attackerTicker && (
|
||||
<span className="ml-1 text-stone-400">/ {attackerTicker}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="truncate text-stone-400">
|
||||
{!onlyOneSystem && systemName ? (
|
||||
<>
|
||||
{systemName} / <span className="ml-1 text-red-400">{killTimeAgo}</span>
|
||||
{systemName} /{' '}
|
||||
<span className="ml-1 text-red-400">{killTimeAgo}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-red-400">{killTimeAgo}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{attackerPrimaryImageUrl && (
|
||||
<WdTooltipWrapper content={attackerPrimaryTooltip} position={TooltipPosition.top}>
|
||||
<WdTooltipWrapper
|
||||
content={attackerPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative shrink-0 w-8 h-8 overflow-hidden"
|
||||
className="relative block shrink-0 w-8 h-8 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={attackerPrimaryImageUrl}
|
||||
alt={attackerIsNpc ? 'NpcShip' : 'AttackerPrimaryLogo'}
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
{attackerSubscript && (
|
||||
<span
|
||||
className={clsx(
|
||||
classes.attackerCountLabel,
|
||||
attackerSubscript.cssClass,
|
||||
'text-[0.6rem] leading-none px-[2px]',
|
||||
'text-[0.6rem] leading-none px-[2px]'
|
||||
)}
|
||||
style={{ bottom: 0, right: 0 }}
|
||||
>
|
||||
{attackerSubscript.label}
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// FullKillRow.tsx
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
@@ -23,10 +22,14 @@ export interface FullKillRowProps {
|
||||
onlyOneSystem: boolean;
|
||||
}
|
||||
|
||||
export const FullKillRow: React.FC<FullKillRowProps> = ({ killDetails, systemName, onlyOneSystem }) => {
|
||||
export const FullKillRow: React.FC<FullKillRowProps> = ({
|
||||
killDetails,
|
||||
systemName,
|
||||
onlyOneSystem,
|
||||
}) => {
|
||||
const {
|
||||
killmail_id = 0,
|
||||
|
||||
// Victim data
|
||||
victim_char_name = '',
|
||||
victim_alliance_ticker = '',
|
||||
victim_corp_ticker = '',
|
||||
@@ -37,7 +40,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({ killDetails, systemNam
|
||||
victim_ship_type_id = 0,
|
||||
victim_corp_name = '',
|
||||
victim_alliance_name = '',
|
||||
|
||||
// Attacker data
|
||||
final_blow_char_id = 0,
|
||||
final_blow_char_name = '',
|
||||
final_blow_alliance_ticker = '',
|
||||
@@ -48,57 +51,80 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({ killDetails, systemNam
|
||||
final_blow_alliance_id = 0,
|
||||
final_blow_ship_name = '',
|
||||
final_blow_ship_type_id = 0,
|
||||
|
||||
total_value = 0,
|
||||
kill_time = '',
|
||||
} = killDetails || {};
|
||||
|
||||
const attackerIsNpc = final_blow_char_id === 0;
|
||||
const victimAffiliation = victim_alliance_ticker || victim_corp_ticker;
|
||||
const attackerAffiliation = attackerIsNpc ? '' : final_blow_alliance_ticker || final_blow_corp_ticker || '';
|
||||
const victimAffiliation = victim_alliance_ticker || victim_corp_ticker || null;
|
||||
const attackerAffiliation = attackerIsNpc
|
||||
? ''
|
||||
: final_blow_alliance_ticker || final_blow_corp_ticker || '';
|
||||
|
||||
const killValueFormatted = total_value !== null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
|
||||
const killValueFormatted =
|
||||
total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
|
||||
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
|
||||
|
||||
const { victimPortraitUrl, victimCorpLogoUrl, victimAllianceLogoUrl } = buildVictimImageUrls({
|
||||
// Build victim images
|
||||
const {
|
||||
victimPortraitUrl,
|
||||
victimCorpLogoUrl,
|
||||
victimAllianceLogoUrl,
|
||||
victimShipUrl,
|
||||
} = buildVictimImageUrls({
|
||||
victim_char_id,
|
||||
victim_ship_type_id,
|
||||
victim_corp_id,
|
||||
victim_alliance_id,
|
||||
});
|
||||
const { attackerPortraitUrl, attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
|
||||
|
||||
// Build attacker images
|
||||
const {
|
||||
attackerPortraitUrl,
|
||||
attackerCorpLogoUrl,
|
||||
attackerAllianceLogoUrl,
|
||||
} = buildAttackerImageUrls({
|
||||
final_blow_char_id,
|
||||
final_blow_corp_id,
|
||||
final_blow_alliance_id,
|
||||
});
|
||||
|
||||
const { url: victimPrimaryImageUrl, tooltip: victimPrimaryTooltip } = getPrimaryLogoAndTooltip(
|
||||
victimAllianceLogoUrl,
|
||||
victimCorpLogoUrl,
|
||||
victim_alliance_name,
|
||||
victim_corp_name,
|
||||
'Victim',
|
||||
);
|
||||
// Primary image for victim
|
||||
const { url: victimPrimaryImageUrl, tooltip: victimPrimaryTooltip } =
|
||||
getPrimaryLogoAndTooltip(
|
||||
victimAllianceLogoUrl,
|
||||
victimCorpLogoUrl,
|
||||
victim_alliance_name,
|
||||
victim_corp_name,
|
||||
'Victim'
|
||||
);
|
||||
|
||||
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } = getAttackerPrimaryImageAndTooltip(
|
||||
attackerIsNpc,
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id || 0,
|
||||
);
|
||||
// Primary image for attacker
|
||||
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } =
|
||||
getAttackerPrimaryImageAndTooltip(
|
||||
attackerIsNpc,
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id
|
||||
);
|
||||
|
||||
const attackerSubscript = getAttackerSubscript(killDetails);
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.killRowContainer, 'h-18 w-full justify-between items-start text-sm py-[4px]')}>
|
||||
{/* ---------------- Victim Side ---------------- */}
|
||||
<div className="flex items-start gap-1 min-w-0 h-full">
|
||||
{/* Victim top-level logo (corp or alliance), with tooltip */}
|
||||
{victimPrimaryImageUrl && (
|
||||
<WdTooltipWrapper content={victimPrimaryTooltip} position={TooltipPosition.top}>
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
<div
|
||||
className={clsx(
|
||||
classes.killRowContainer,
|
||||
'w-full text-sm py-1 px-2',
|
||||
'flex flex-col sm:flex-row'
|
||||
)}
|
||||
>
|
||||
<div className="w-full flex flex-col sm:flex-row items-start gap-2">
|
||||
{/* Victim Section */}
|
||||
<div className="flex items-start gap-1 min-w-0">
|
||||
{victimShipUrl && (
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
@@ -106,91 +132,136 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({ killDetails, systemNam
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimPrimaryImageUrl}
|
||||
alt="VictimPrimaryLogo"
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
src={victimShipUrl}
|
||||
alt="VictimShip"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
<VictimRowSubInfo
|
||||
victimCharName={victim_char_name}
|
||||
victimCharacterId={victim_char_id}
|
||||
victimPortraitUrl={victimPortraitUrl}
|
||||
/>
|
||||
<div className="flex flex-col text-stone-200 leading-4 min-w-0 overflow-hidden">
|
||||
<div className="truncate">
|
||||
<span className="font-semibold">{victim_char_name}</span>
|
||||
{victimAffiliation && <span className="ml-1 text-stone-400">/ {victimAffiliation}</span>}
|
||||
</div>
|
||||
<div className="truncate text-stone-300">
|
||||
{victim_ship_name}
|
||||
{killValueFormatted && (
|
||||
<>
|
||||
<span className="ml-1 text-stone-400">/</span>
|
||||
<span className="ml-1 text-green-400">{killValueFormatted}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-stone-400">{!onlyOneSystem && systemName && <span>{systemName}</span>}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-1 min-w-0 h-full">
|
||||
<div className="flex flex-col items-end leading-4 min-w-0 overflow-hidden text-right">
|
||||
{!attackerIsNpc && (
|
||||
<div className="truncate font-semibold">
|
||||
{final_blow_char_name}
|
||||
{attackerAffiliation && <span className="ml-1 text-stone-400">/ {attackerAffiliation}</span>}
|
||||
</div>
|
||||
)}
|
||||
{!attackerIsNpc && final_blow_ship_name && (
|
||||
<div className="truncate text-stone-300">{final_blow_ship_name}</div>
|
||||
)}
|
||||
<div className="truncate text-red-400">{killTimeAgo}</div>
|
||||
</div>
|
||||
|
||||
{!attackerIsNpc && attackerPortraitUrl && final_blow_char_id !== null && final_blow_char_id > 0 && (
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('character', final_blow_char_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
{victimPrimaryImageUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={victimPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<img
|
||||
src={attackerPortraitUrl}
|
||||
alt="AttackerPortrait"
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
/>
|
||||
</a>
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimPrimaryImageUrl}
|
||||
alt="VictimPrimaryLogo"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
<VictimRowSubInfo
|
||||
victimCharName={victim_char_name}
|
||||
victimCharacterId={victim_char_id}
|
||||
victimPortraitUrl={victimPortraitUrl}
|
||||
/>
|
||||
<div className="flex flex-col text-stone-200 leading-4 min-w-0 overflow-hidden">
|
||||
<div className="truncate font-semibold">
|
||||
{victim_char_name}
|
||||
{victimAffiliation && (
|
||||
<span className="ml-1 text-stone-400">/ {victimAffiliation}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-stone-300">
|
||||
{victim_ship_name}
|
||||
{killValueFormatted && (
|
||||
<>
|
||||
<span className="ml-1 text-stone-400">/</span>
|
||||
<span className="ml-1 text-green-400">{killValueFormatted}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-stone-400">
|
||||
{!onlyOneSystem && systemName && <span>{systemName}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{attackerPrimaryImageUrl && (
|
||||
<WdTooltipWrapper content={attackerPrimaryTooltip} position={TooltipPosition.top}>
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
</div>
|
||||
{/* Attacker Section */}
|
||||
<div className="flex items-start gap-1 min-w-0 sm:ml-auto">
|
||||
<div className="flex flex-col items-end leading-4 min-w-0 overflow-hidden text-right">
|
||||
{!attackerIsNpc && (
|
||||
<div className="truncate font-semibold">
|
||||
{final_blow_char_name}
|
||||
{attackerAffiliation && (
|
||||
<span className="ml-1 text-stone-400">/ {attackerAffiliation}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!attackerIsNpc && final_blow_ship_name && (
|
||||
<div className="truncate text-stone-300">{final_blow_ship_name}</div>
|
||||
)}
|
||||
<div className="truncate text-red-400">{killTimeAgo}</div>
|
||||
</div>
|
||||
{(!attackerIsNpc && attackerPortraitUrl && final_blow_char_id > 0) && (
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
href={zkillLink('character', final_blow_char_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={attackerPrimaryImageUrl}
|
||||
alt={attackerIsNpc ? 'NpcShip' : 'AttackerPrimaryLogo'}
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
src={attackerPortraitUrl}
|
||||
alt="AttackerPortrait"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
{attackerSubscript && (
|
||||
<span className={clsx(attackerSubscript.cssClass, classes.attackerCountLabel)}>
|
||||
{attackerSubscript.label}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
)}
|
||||
{attackerPrimaryImageUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={attackerPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={attackerPrimaryImageUrl}
|
||||
alt={attackerIsNpc ? 'NpcShip' : 'AttackerPrimaryLogo'}
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
{attackerSubscript && (
|
||||
<span
|
||||
className={clsx(
|
||||
attackerSubscript.cssClass,
|
||||
classes.attackerCountLabel
|
||||
)}
|
||||
>
|
||||
{attackerSubscript.label}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import {
|
||||
LayoutEventBlocker,
|
||||
SystemView,
|
||||
TooltipPosition,
|
||||
WdCheckbox,
|
||||
WdImgButton,
|
||||
TooltipPosition,
|
||||
SystemView,
|
||||
WdTooltipWrapper,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
||||
|
||||
interface KillsWidgetHeaderProps {
|
||||
interface KillsHeaderProps {
|
||||
systemId?: string;
|
||||
onOpenSettings: () => void;
|
||||
}
|
||||
|
||||
export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpenSettings }) => {
|
||||
export const KillsHeader: React.FC<KillsHeaderProps> = ({ systemId, onOpenSettings }) => {
|
||||
const [settings, setSettings] = useKillsWidgetSettings();
|
||||
const { showAll } = settings;
|
||||
|
||||
@@ -22,8 +24,11 @@ export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpen
|
||||
setSettings(prev => ({ ...prev, showAll: !prev.showAll }));
|
||||
};
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const compact = useMaxWidth(headerRef, 150);
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center text-xs w-full">
|
||||
<div className="flex w-full items-center justify-between text-xs" ref={headerRef}>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="text-stone-400">
|
||||
Kills
|
||||
@@ -32,24 +37,28 @@ export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpen
|
||||
{systemId && !showAll && <SystemView systemId={systemId} className="select-none text-center" hideRegion />}
|
||||
</div>
|
||||
|
||||
<LayoutEventBlocker className="flex gap-2 items-center">
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label="Show all systems"
|
||||
value={showAll}
|
||||
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
|
||||
onChange={onToggleShowAllVisible}
|
||||
/>
|
||||
<LayoutEventBlocker className="flex items-center gap-2 justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<WdTooltipWrapper content="Show all systems" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compact ? 'All' : 'Show all systems'}
|
||||
value={showAll}
|
||||
onChange={onToggleShowAllVisible}
|
||||
classNameLabel="whitespace-nowrap text-stone-400 hover:text-stone-200 transition duration-300"
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
<WdImgButton
|
||||
className={PrimeIcons.SLIDERS_H}
|
||||
onClick={onOpenSettings}
|
||||
tooltip={{
|
||||
content: 'Open Kills Settings',
|
||||
position: TooltipPosition.left,
|
||||
}}
|
||||
/>
|
||||
<WdImgButton
|
||||
className={PrimeIcons.SLIDERS_H}
|
||||
onClick={onOpenSettings}
|
||||
tooltip={{
|
||||
content: 'Open Kills Settings',
|
||||
position: TooltipPosition.top,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
|
||||
const localRef = useRef({
|
||||
compact: globalSettings.compact,
|
||||
showAll: globalSettings.showAll,
|
||||
whOnly: globalSettings.whOnly,
|
||||
excludedSystems: globalSettings.excludedSystems || [],
|
||||
});
|
||||
|
||||
@@ -31,6 +32,7 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
|
||||
localRef.current = {
|
||||
compact: globalSettings.compact,
|
||||
showAll: globalSettings.showAll,
|
||||
whOnly: globalSettings.whOnly,
|
||||
excludedSystems: globalSettings.excludedSystems || [],
|
||||
};
|
||||
forceRender(n => n + 1);
|
||||
@@ -45,6 +47,14 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
|
||||
forceRender(n => n + 1);
|
||||
}, []);
|
||||
|
||||
const handleWHChange = useCallback((checked: boolean) => {
|
||||
localRef.current = {
|
||||
...localRef.current,
|
||||
whOnly: checked,
|
||||
};
|
||||
forceRender(n => n + 1);
|
||||
}, []);
|
||||
|
||||
const handleRemoveSystem = useCallback((sysId: number) => {
|
||||
localRef.current = {
|
||||
...localRef.current,
|
||||
@@ -94,6 +104,18 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="kills-wormhole-only-mode"
|
||||
checked={localData.whOnly}
|
||||
onChange={e => handleWHChange(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="kills-wh-only-mode" className="cursor-pointer">
|
||||
Only show wormhole kills
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm text-stone-400">Excluded Systems</label>
|
||||
@@ -106,7 +128,7 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
|
||||
{excluded.length === 0 && <div className="text-stone-500 text-xs italic">No systems excluded.</div>}
|
||||
{excluded.map(sysId => (
|
||||
<div key={sysId} className="flex items-center justify-between border-b border-stone-600 py-1 px-1 text-xs">
|
||||
<SystemView systemId={sysId.toString()} hideRegion compact />
|
||||
<SystemView systemId={sysId.toString()} hideRegion compact/>
|
||||
|
||||
<WdImgButton
|
||||
className={PrimeIcons.TRASH}
|
||||
@@ -117,13 +139,11 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Apply + Close button row */}
|
||||
<div className="flex gap-2 justify-end mt-4">
|
||||
<Button onClick={handleApply} label="Apply" outlined size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AddSystemDialog for picking new systems to exclude */}
|
||||
<AddSystemDialog
|
||||
title="Add system to kills exclude list"
|
||||
visible={addSystemDialogVisible}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// VictimSubRowInfo.tsx
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { zkillLink } from '../helpers';
|
||||
@@ -15,13 +14,13 @@ export const VictimRowSubInfo: React.FC<VictimRowSubInfoProps> = ({
|
||||
victimPortraitUrl,
|
||||
victimCharName,
|
||||
}) => {
|
||||
if (!victimPortraitUrl || victimCharacterId === null || victimCharacterId <= 0) {
|
||||
if (!victimPortraitUrl || !victimCharacterId || victimCharacterId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-1 h-14">
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
<div className="flex items-start gap-1">
|
||||
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('character', victimCharacterId)}
|
||||
target="_blank"
|
||||
|
||||
@@ -96,7 +96,7 @@ export function getAttackerPrimaryImageAndTooltip(
|
||||
corpUrl: string | null,
|
||||
allianceName: string,
|
||||
corpName: string,
|
||||
finalBlowShipTypeId: number,
|
||||
finalBlowShipTypeId: number | null,
|
||||
npcFallback: string = 'NPC Attacker',
|
||||
) {
|
||||
if (isNpc) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import useLocalStorageState from 'use-local-storage-state';
|
||||
export interface KillsWidgetSettings {
|
||||
compact: boolean;
|
||||
showAll: boolean;
|
||||
whOnly: boolean;
|
||||
excludedSystems: number[];
|
||||
version: number;
|
||||
}
|
||||
@@ -11,6 +12,7 @@ export interface KillsWidgetSettings {
|
||||
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
|
||||
compact: true,
|
||||
showAll: false,
|
||||
whOnly: true,
|
||||
excludedSystems: [],
|
||||
version: 0,
|
||||
};
|
||||
|
||||
@@ -38,9 +38,14 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
const [settings] = useKillsWidgetSettings();
|
||||
const excludedSystems = settings.excludedSystems;
|
||||
|
||||
const visibleSystemIds = useMemo(() => {
|
||||
return systems.map(s => s.id).filter(id => !excludedSystems.includes(Number(id)));
|
||||
}, [systems, excludedSystems]);
|
||||
// When showing all visible kills, filter out excluded systems;
|
||||
// when showAllVisible is false, ignore the exclusion filter.
|
||||
const effectiveSystemIds = useMemo(() => {
|
||||
if (showAllVisible) {
|
||||
return systems.map(s => s.id).filter(id => !excludedSystems.includes(Number(id)));
|
||||
}
|
||||
return systems.map(s => s.id);
|
||||
}, [systems, excludedSystems, showAllVisible]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -80,7 +85,7 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
if (showAllVisible || forceFallback) {
|
||||
eventType = OutCommand.getSystemsKills;
|
||||
requestData = {
|
||||
system_ids: visibleSystemIds,
|
||||
system_ids: effectiveSystemIds,
|
||||
since_hours: sinceHours,
|
||||
};
|
||||
} else if (systemId) {
|
||||
@@ -106,7 +111,7 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
const sid = systemId ?? 'unknown';
|
||||
mergeKillsIntoGlobal({ [sid]: arr });
|
||||
}
|
||||
// multiple => `resp.systems_kills`
|
||||
// multiple systems => `resp.systems_kills`
|
||||
else if (resp?.systems_kills) {
|
||||
mergeKillsIntoGlobal(resp.systems_kills as Record<string, DetailedKill[]>);
|
||||
} else {
|
||||
@@ -119,7 +124,7 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[showAllVisible, systemId, outCommand, visibleSystemIds, sinceHours, mergeKillsIntoGlobal],
|
||||
[showAllVisible, systemId, outCommand, effectiveSystemIds, sinceHours, mergeKillsIntoGlobal],
|
||||
);
|
||||
|
||||
const debouncedFetchKills = useMemo(
|
||||
@@ -133,15 +138,15 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
|
||||
const finalKills = useMemo(() => {
|
||||
if (showAllVisible) {
|
||||
return visibleSystemIds.flatMap(sid => detailedKills[sid] ?? []);
|
||||
return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []);
|
||||
} else if (systemId) {
|
||||
return detailedKills[systemId] ?? [];
|
||||
} else if (didFallbackFetch.current) {
|
||||
// if we already did a fallback, we may have data for multiple systems
|
||||
return visibleSystemIds.flatMap(sid => detailedKills[sid] ?? []);
|
||||
return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []);
|
||||
}
|
||||
return [];
|
||||
}, [showAllVisible, systemId, didFallbackFetch, visibleSystemIds, detailedKills]);
|
||||
}, [showAllVisible, systemId, effectiveSystemIds, detailedKills]);
|
||||
|
||||
const effectiveIsLoading = isLoading && finalKills.length === 0;
|
||||
|
||||
@@ -150,19 +155,19 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
didFallbackFetch.current = true;
|
||||
// Cancel any queued debounced calls, then do the fallback.
|
||||
debouncedFetchKills.cancel();
|
||||
fetchKills(true); // forceFallback => fetch as though showAll
|
||||
fetchKills(true); // forceFallback => fetch as though showAllVisible is true
|
||||
}
|
||||
}, [systemId, showAllVisible, debouncedFetchKills, fetchKills, didFallbackFetch]);
|
||||
}, [systemId, showAllVisible, debouncedFetchKills, fetchKills]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visibleSystemIds.length === 0) return;
|
||||
if (effectiveSystemIds.length === 0) return;
|
||||
|
||||
if (showAllVisible || systemId) {
|
||||
debouncedFetchKills();
|
||||
// Clean up the debounce on unmount or changes
|
||||
return () => debouncedFetchKills.cancel();
|
||||
}
|
||||
}, [showAllVisible, systemId, visibleSystemIds, debouncedFetchKills]);
|
||||
}, [showAllVisible, systemId, effectiveSystemIds, debouncedFetchKills]);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
debouncedFetchKills.cancel();
|
||||
|
||||
@@ -3,7 +3,12 @@ import { Dialog } from 'primereact/dialog';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { TabPanel, TabView } from 'primereact/tabview';
|
||||
import { PrettySwitchbox } from './components';
|
||||
import { InterfaceStoredSettingsProps, useMapRootState, InterfaceStoredSettings } from '@/hooks/Mapper/mapRootProvider';
|
||||
import {
|
||||
InterfaceStoredSettingsProps,
|
||||
useMapRootState,
|
||||
InterfaceStoredSettings,
|
||||
AvailableThemes
|
||||
} from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { WidgetsSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components/WidgetsSettings/WidgetsSettings.tsx';
|
||||
@@ -112,8 +117,8 @@ const UI_CHECKBOXES_PROPS: SettingsListItem[] = [
|
||||
];
|
||||
|
||||
const THEME_OPTIONS = [
|
||||
{ label: 'Default', value: 'default' },
|
||||
{ label: 'Pathfinder', value: 'pathfinder' },
|
||||
{ label: 'Default', value: AvailableThemes.default },
|
||||
{ label: 'Pathfinder', value: AvailableThemes.pathfinder },
|
||||
];
|
||||
|
||||
const THEME_SETTING: SettingsListItem = {
|
||||
|
||||
@@ -18,17 +18,14 @@ const SHIP_NAME_RX = /u'|'/g;
|
||||
export const getShipName = (name: string) => {
|
||||
return name
|
||||
.replace(SHIP_NAME_RX, '')
|
||||
.replace(/\\u([\dA-Fa-f]{4})/g, (_, grp) => String.fromCharCode(parseInt(grp, 16)))
|
||||
.replace(/\\x([\dA-Fa-f]{2})/g, (_, grp) => String.fromCharCode(parseInt(grp, 16)));
|
||||
.replace(/\\u([\dA-Fa-f]{4})/g, (_, grp) =>
|
||||
String.fromCharCode(parseInt(grp, 16))
|
||||
)
|
||||
.replace(/\\x([\dA-Fa-f]{2})/g, (_, grp) =>
|
||||
String.fromCharCode(parseInt(grp, 16))
|
||||
);
|
||||
};
|
||||
|
||||
// A small divider between fields:
|
||||
const Divider = () => (
|
||||
<span className="mx-1 text-gray-400" aria-hidden="true">
|
||||
|
|
||||
</span>
|
||||
);
|
||||
|
||||
export const CharacterCard = ({
|
||||
compact = false,
|
||||
isOwn,
|
||||
@@ -44,135 +41,105 @@ export const CharacterCard = ({
|
||||
});
|
||||
}, [char]);
|
||||
|
||||
// Precompute the ship name (decoded):
|
||||
const shipNameText = char.ship?.ship_name ? getShipName(char.ship.ship_name) : '';
|
||||
const tickerText = char.alliance_id ? char.alliance_ticker : char.corporation_ticker;
|
||||
const shipType = char.ship?.ship_type_info?.name;
|
||||
const locationShown = showSystem && char.location?.solar_system_id;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// COMPACT MODE: Main line =
|
||||
// if (showShipName & haveShipName) => name | shipName (skip ticker)
|
||||
// else => name | [ticker]
|
||||
// -----------------------------------------------------------------------------
|
||||
const compactLine = (
|
||||
<>
|
||||
{/* Character Name (lighter shade) */}
|
||||
<span className="text-gray-200">{char.name}</span>
|
||||
<Divider />
|
||||
{showShipName && shipNameText ? (
|
||||
// Show the ship name in place of the ticker (use indigo color to match corp/alliance)
|
||||
<span className="text-indigo-300">{shipNameText}</span>
|
||||
) : (
|
||||
// Show the [ticker] (indigo)
|
||||
<span className="text-indigo-300">[{char.alliance_id ? char.alliance_ticker : char.corporation_ticker}]</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// NON-COMPACT MODE:
|
||||
// Line 1 => name | [ticker]
|
||||
// Line 2 => (shipName) always, if it exists
|
||||
// -----------------------------------------------------------------------------
|
||||
const nonCompactLine1 = (
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{/* Character Name (lighter shade) */}
|
||||
<span className="text-gray-200">{char.name}</span>
|
||||
<Divider />
|
||||
<span className="text-indigo-300">[{char.alliance_id ? char.alliance_ticker : char.corporation_ticker}]</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const nonCompactLine2 = (
|
||||
<>
|
||||
{shipNameText && (
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-gray-300">{shipNameText}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.CharacterCard, 'w-full text-xs box-border')} onClick={handleSelect}>
|
||||
if (compact) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'w-full px-2 py-1 overflow-hidden gap-1',
|
||||
compact ? 'grid items-center' : 'flex flex-col md:flex-row items-start',
|
||||
)}
|
||||
style={compact ? { gridTemplateColumns: 'auto 1fr auto', minWidth: 0 } : undefined}
|
||||
className={clsx(classes.CharacterCard, 'w-full text-xs box-border')}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
{compact ? (
|
||||
<div className="w-full px-2 py-1 flex items-center gap-2" style={{ minWidth: 0 }}>
|
||||
<img
|
||||
src={`https://images.evetech.net/characters/${char.eve_id}/portrait`}
|
||||
alt={`${char.name} portrait`}
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
// Remove circle shape for a square image:
|
||||
borderRadius: 0,
|
||||
marginRight: '4px',
|
||||
flexShrink: 0,
|
||||
// Slightly lighter than typical dark background:
|
||||
border: '1px solid #2b2b2b',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-grow overflow-hidden text-left" style={{ minWidth: 0 }}>
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className={clsx(isOwn ? 'text-orange-400' : 'text-gray-200')}>
|
||||
{char.name}
|
||||
</span>{" "}
|
||||
<span className="text-gray-400">
|
||||
{(!locationShown && showShipName && shipNameText)
|
||||
? `- ${shipNameText}`
|
||||
: `[${tickerText}]`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{shipType && (
|
||||
<div
|
||||
className="text-gray-300 overflow-hidden text-ellipsis whitespace-nowrap flex-shrink-0"
|
||||
style={{ maxWidth: '120px' }}
|
||||
title={shipType}
|
||||
>
|
||||
{shipType}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.CharacterCard, 'w-full text-xs box-border')}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<div className="w-full px-2 py-1 flex items-center gap-2" style={{ minWidth: 0 }}>
|
||||
<span
|
||||
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
|
||||
style={{
|
||||
// The SCSS below ensures the image is square with a border.
|
||||
backgroundImage: `url(https://images.evetech.net/characters/${char.eve_id}/portrait)`,
|
||||
minWidth: '33px',
|
||||
minHeight: '33px',
|
||||
width: '33px',
|
||||
height: '33px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/*
|
||||
Middle section:
|
||||
- In compact mode, everything is on one line (Name + possibly ShipName or ticker).
|
||||
- In non-compact mode, line 1 has (Name | Ticker), line 2 has shipName if it exists.
|
||||
*/}
|
||||
<div
|
||||
className={clsx('overflow-hidden text-ellipsis', {
|
||||
'text-left px-1': compact,
|
||||
'flex-grow': !compact,
|
||||
})}
|
||||
style={{ minWidth: 0 }}
|
||||
>
|
||||
{/* This left border highlights "isOwn" in the same way as older code. */}
|
||||
<div
|
||||
className={clsx('overflow-hidden whitespace-nowrap', {
|
||||
[classes.CardBorderLeftIsOwn]: isOwn,
|
||||
})}
|
||||
>
|
||||
{compact ? compactLine : nonCompactLine1}
|
||||
<div className="flex flex-col flex-grow overflow-hidden" style={{ minWidth: 0 }}>
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className={clsx(isOwn ? 'text-orange-400' : 'text-gray-200')}>
|
||||
{char.name}
|
||||
</span>{" "}
|
||||
<span className="text-gray-400">[{tickerText}]</span>
|
||||
</div>
|
||||
{locationShown ? (
|
||||
<div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<SystemView
|
||||
systemId={char?.location?.solar_system_id?.toString() || ''}
|
||||
useSystemsCache={useSystemsCache}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
shipNameText && (
|
||||
<div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{shipNameText}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{/* Non-compact second line always shows shipName if available */}
|
||||
{!compact && nonCompactLine2}
|
||||
{shipType && (
|
||||
<div className="flex-shrink-0 self-start">
|
||||
<div
|
||||
className="text-gray-300 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
style={{ maxWidth: '200px' }}
|
||||
title={shipType}
|
||||
>
|
||||
{shipType}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/*
|
||||
Right column for Ship Type (compact) or "pushed" to the right (non-compact).
|
||||
Ship Type remains text-yellow-400.
|
||||
*/}
|
||||
{char.ship?.ship_type_info?.name && (
|
||||
<div
|
||||
className={clsx('text-yellow-400 text-ellipsis overflow-hidden whitespace-nowrap', {
|
||||
'text-right px-1 flex-shrink-0': compact,
|
||||
'mt-1 md:mt-0 ml-auto': !compact,
|
||||
})}
|
||||
style={{ maxWidth: compact ? '120px' : '200px' }}
|
||||
title={char.ship.ship_type_info.name}
|
||||
>
|
||||
{char.ship.ship_type_info.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/*
|
||||
System row at the bottom if `showSystem && system exists`.
|
||||
*/}
|
||||
{showSystem && char.location?.solar_system_id && (
|
||||
<div className="px-2 pb-1">
|
||||
<SystemView systemId={char.location.solar_system_id.toString()} useSystemsCache={useSystemsCache} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,12 +2,12 @@ import classes from './WdCheckbox.module.scss';
|
||||
import { Checkbox, CheckboxChangeEvent } from 'primereact/checkbox';
|
||||
import { WithClassName } from '@/hooks/Mapper/types/common';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
let counter = 0;
|
||||
|
||||
export interface WdCheckboxProps {
|
||||
label: string;
|
||||
label: React.ReactNode | string;
|
||||
classNameLabel?: string;
|
||||
value: boolean;
|
||||
labelSide?: 'left' | 'right';
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { WdCheckbox, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
/**
|
||||
* Display modes for the responsive checkbox.
|
||||
*
|
||||
* - "full": show the full label (e.g. "Show offline" or "Show ship name")
|
||||
* - "abbr": show the abbreviated label (e.g. "Offline" or "Ship name")
|
||||
* - "checkbox": show only the checkbox (no text)
|
||||
* - "hide": do not render the checkbox at all
|
||||
*/
|
||||
export type WdDisplayMode = 'full' | 'abbr' | 'checkbox' | 'hide';
|
||||
|
||||
export interface WdResponsiveCheckboxProps {
|
||||
tooltipContent: string;
|
||||
size: 'xs' | 'normal' | 'm';
|
||||
labelFull: string;
|
||||
labelAbbreviated: string;
|
||||
value: boolean;
|
||||
onChange: () => void;
|
||||
classNameLabel?: string;
|
||||
containerClassName?: string;
|
||||
labelSide?: 'left' | 'right';
|
||||
displayMode: WdDisplayMode;
|
||||
}
|
||||
|
||||
export const WdResponsiveCheckbox: React.FC<WdResponsiveCheckboxProps> = ({
|
||||
tooltipContent,
|
||||
size,
|
||||
labelFull,
|
||||
labelAbbreviated,
|
||||
value,
|
||||
onChange,
|
||||
classNameLabel,
|
||||
containerClassName,
|
||||
labelSide = 'left',
|
||||
displayMode,
|
||||
}) => {
|
||||
if (displayMode === 'hide') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label =
|
||||
displayMode === 'full'
|
||||
? labelFull
|
||||
: displayMode === 'abbr'
|
||||
? labelAbbreviated
|
||||
: displayMode === 'checkbox'
|
||||
? ''
|
||||
: labelFull;
|
||||
|
||||
const checkbox = (
|
||||
<div className={clsx('min-w-0', containerClassName)}>
|
||||
<WdCheckbox
|
||||
size={size}
|
||||
labelSide={labelSide}
|
||||
label={label}
|
||||
value={value}
|
||||
classNameLabel={classNameLabel}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return tooltipContent ? <WdTooltipWrapper content={tooltipContent}>{checkbox}</WdTooltipWrapper> : checkbox;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './WdResponsiveCheckbox';
|
||||
@@ -31,6 +31,12 @@ export interface WdTooltipHandlers {
|
||||
getIsMouseInside: () => boolean;
|
||||
}
|
||||
|
||||
interface TriggerInfo {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
rect: DOMRect;
|
||||
}
|
||||
|
||||
const LEAVE_DELAY = 100;
|
||||
|
||||
export const WdTooltip = forwardRef(function WdTooltip(
|
||||
@@ -45,13 +51,14 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
}: TooltipProps,
|
||||
ref: ForwardedRef<WdTooltipHandlers>,
|
||||
) {
|
||||
// Always initialize position so we never have a null value.
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [pos, setPos] = useState<OffsetPosition | null>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
|
||||
|
||||
const [reactEvt, setReactEvt] = useState<React.MouseEvent>();
|
||||
const [triggerInfo, setTriggerInfo] = useState<TriggerInfo | null>(null);
|
||||
|
||||
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
@@ -64,8 +71,13 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
let newLeft = x;
|
||||
let newTop = y;
|
||||
|
||||
if (newLeft < 0) newLeft = 10;
|
||||
if (newTop < 0) newTop = 10;
|
||||
if (newLeft < 0) {
|
||||
newLeft = 10;
|
||||
}
|
||||
|
||||
if (newTop < 0) {
|
||||
newTop = 10;
|
||||
}
|
||||
|
||||
const rightEdge = newLeft + tooltipWidth + 10;
|
||||
if (rightEdge > window.innerWidth) {
|
||||
@@ -83,11 +95,13 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
const scheduleHide = useCallback(() => {
|
||||
if (!interactive) {
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
return;
|
||||
}
|
||||
if (!hideTimeoutRef.current) {
|
||||
hideTimeoutRef.current = setTimeout(() => {
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
}, LEAVE_DELAY);
|
||||
}
|
||||
}, [interactive]);
|
||||
@@ -98,10 +112,14 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
if (e && tooltipRef.current) {
|
||||
const { clientX, clientY } = e;
|
||||
setPos(calcTooltipPosition({ x: clientX, y: clientY }));
|
||||
setReactEvt(e);
|
||||
if (e) {
|
||||
// Use e.currentTarget (or fallback to e.target) to determine the trigger element.
|
||||
const triggerEl = (e.currentTarget as HTMLElement) || (e.target as HTMLElement);
|
||||
if (triggerEl) {
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
setTriggerInfo({ clientX: e.clientX, clientY: e.clientY, rect });
|
||||
setPos(calcTooltipPosition({ x: e.clientX, y: e.clientY }));
|
||||
}
|
||||
}
|
||||
setVisible(true);
|
||||
},
|
||||
@@ -110,52 +128,56 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
}
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
},
|
||||
getIsMouseInside: () => isMouseInsideTooltip,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !reactEvt) return;
|
||||
if (!tooltipRef.current || !triggerInfo) return;
|
||||
|
||||
const { clientX, clientY, target } = reactEvt;
|
||||
const tooltipEl = tooltipRef.current;
|
||||
const triggerEl = target as HTMLElement;
|
||||
const triggerBounds = triggerEl.getBoundingClientRect();
|
||||
|
||||
let x = clientX;
|
||||
let y = clientY;
|
||||
const { rect } = triggerInfo;
|
||||
let x = triggerInfo.clientX;
|
||||
let y = triggerInfo.clientY;
|
||||
|
||||
if (tPosition === TooltipPosition.left) {
|
||||
const tooltipBounds = tooltipEl.getBoundingClientRect();
|
||||
x = triggerBounds.left - tooltipBounds.width - offset;
|
||||
y = triggerBounds.y + triggerBounds.height / 2 - tooltipBounds.height / 2;
|
||||
x = rect.left - tooltipBounds.width - offset;
|
||||
y = rect.top + rect.height / 2 - tooltipBounds.height / 2;
|
||||
|
||||
if (x <= 0) {
|
||||
x = triggerBounds.left + triggerBounds.width + offset;
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
if (tPosition === TooltipPosition.right) {
|
||||
x = triggerBounds.left + triggerBounds.width + offset;
|
||||
y = triggerBounds.y + triggerBounds.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
if (tPosition === TooltipPosition.top) {
|
||||
x = triggerBounds.x + triggerBounds.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = triggerBounds.top - tooltipEl.offsetHeight - offset;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
if (tPosition === TooltipPosition.bottom) {
|
||||
x = triggerBounds.x + triggerBounds.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = triggerBounds.bottom + offset;
|
||||
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.right) {
|
||||
x = rect.left + rect.width + offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.top) {
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.bottom) {
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Default case: use stored coordinates.
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
}, [calcTooltipPosition, reactEvt, tPosition, offset]);
|
||||
}, [calcTooltipPosition, triggerInfo, tPosition, offset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!targetSelector) return;
|
||||
@@ -166,6 +188,7 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
scheduleHide();
|
||||
return;
|
||||
}
|
||||
|
||||
const triggerEl = targetEl.closest(targetSelector);
|
||||
const insideTooltip = interactive && tooltipRef.current?.contains(targetEl);
|
||||
|
||||
@@ -178,6 +201,7 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
setVisible(true);
|
||||
|
||||
if (triggerEl && tooltipRef.current) {
|
||||
@@ -188,30 +212,26 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
let y = evt.clientY;
|
||||
|
||||
switch (tPosition) {
|
||||
case TooltipPosition.left: {
|
||||
case TooltipPosition.left:
|
||||
x = rect.left - tooltipEl.offsetWidth - offset;
|
||||
y = rect.y + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
|
||||
if (x <= 0) {
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TooltipPosition.right: {
|
||||
case TooltipPosition.right:
|
||||
x = rect.left + rect.width + offset;
|
||||
y = rect.y + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
break;
|
||||
}
|
||||
case TooltipPosition.top: {
|
||||
x = rect.x + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
case TooltipPosition.top:
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
break;
|
||||
}
|
||||
case TooltipPosition.bottom: {
|
||||
x = rect.x + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
case TooltipPosition.bottom:
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
@@ -219,13 +239,10 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
};
|
||||
|
||||
const debounced = debounce(handleMouseMove, 15);
|
||||
const listener = (evt: Event) => {
|
||||
debounced(evt as MouseEvent);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', listener);
|
||||
document.addEventListener('mousemove', debounced);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', listener);
|
||||
document.removeEventListener('mousemove', debounced);
|
||||
debounced.cancel();
|
||||
};
|
||||
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition, scheduleHide]);
|
||||
@@ -247,8 +264,8 @@ export const WdTooltip = forwardRef(function WdTooltip(
|
||||
classes.tooltip,
|
||||
interactive ? 'pointer-events-auto' : 'pointer-events-none',
|
||||
'absolute p-1 border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
||||
pos === null ? 'invisible' : '',
|
||||
className,
|
||||
pos === null ? 'invisible' : '',
|
||||
)}
|
||||
style={{
|
||||
top: pos?.top ?? 0,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, HTMLProps, ReactNode } from 'react';
|
||||
import { forwardRef, HTMLProps, ReactNode, useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { WdTooltip, WdTooltipHandlers, TooltipProps } from '@/hooks/Mapper/components/ui-kit';
|
||||
import classes from './WdTooltipWrapper.module.scss';
|
||||
@@ -13,11 +13,8 @@ export type WdTooltipWrapperProps = {
|
||||
Omit<TooltipProps, 'content'>;
|
||||
|
||||
export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperProps>(
|
||||
(
|
||||
{ className, children, content, offset, position, targetSelector, interactive = false, size, ...props },
|
||||
forwardedRef,
|
||||
) => {
|
||||
const suffix = Math.random().toString(36).slice(2, 7);
|
||||
({ className, children, content, offset, position, targetSelector, interactive, size, ...props }, forwardedRef) => {
|
||||
const suffix = useMemo(() => Math.random().toString(36).slice(2, 7), []);
|
||||
const autoClass = `wdTooltipAutoTrigger-${suffix}`;
|
||||
const finalTargetSelector = targetSelector || `.${autoClass}`;
|
||||
|
||||
|
||||
@@ -11,3 +11,5 @@ export * from './WdImgButton';
|
||||
export * from './WdTooltip';
|
||||
export * from './WdCheckbox';
|
||||
export * from './TimeAgo';
|
||||
export * from './WdTooltipWrapper';
|
||||
export * from './WdResponsiveCheckBox';
|
||||
|
||||
7
assets/js/hooks/Mapper/hooks/useTheme.ts
Normal file
7
assets/js/hooks/Mapper/hooks/useTheme.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AvailableThemes, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export const useTheme = (): AvailableThemes => {
|
||||
const { interfaceSettings } = useMapRootState();
|
||||
|
||||
return interfaceSettings.theme;
|
||||
};
|
||||
@@ -46,6 +46,11 @@ const INITIAL_DATA: MapRootData = {
|
||||
linkSignatureToSystem: null,
|
||||
};
|
||||
|
||||
export enum AvailableThemes {
|
||||
default = 'default',
|
||||
pathfinder = 'pathfinder',
|
||||
}
|
||||
|
||||
export enum InterfaceStoredSettingsProps {
|
||||
isShowMenu = 'isShowMenu',
|
||||
isShowMinimap = 'isShowMinimap',
|
||||
@@ -65,7 +70,7 @@ export type InterfaceStoredSettings = {
|
||||
isShowUnsplashedSignatures: boolean;
|
||||
isShowBackgroundPattern: boolean;
|
||||
isSoftBackground: boolean;
|
||||
theme: string;
|
||||
theme: AvailableThemes;
|
||||
};
|
||||
|
||||
export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
|
||||
@@ -76,7 +81,7 @@ export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
|
||||
isShowUnsplashedSignatures: false,
|
||||
isShowBackgroundPattern: true,
|
||||
isSoftBackground: false,
|
||||
theme: 'default',
|
||||
theme: AvailableThemes.default,
|
||||
};
|
||||
|
||||
export interface MapRootContextProps {
|
||||
|
||||
@@ -107,6 +107,11 @@ admins =
|
||||
admins -> admins |> String.split(",")
|
||||
end
|
||||
|
||||
restrict_maps_creation =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_RESTRICT_MAPS_CREATION", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
config :wanderer_app,
|
||||
web_app_url: web_app_url,
|
||||
git_sha: System.get_env("GIT_SHA", "111"),
|
||||
@@ -124,6 +129,7 @@ config :wanderer_app,
|
||||
map_connection_auto_eol_hours: map_connection_auto_eol_hours,
|
||||
map_connection_eol_expire_timeout_mins: map_connection_eol_expire_timeout_mins,
|
||||
wallet_tracking_enabled: wallet_tracking_enabled,
|
||||
restrict_maps_creation: restrict_maps_creation,
|
||||
subscription_settings: %{
|
||||
plans: [
|
||||
%{
|
||||
|
||||
@@ -238,7 +238,7 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"corporation",
|
||||
{:transactions, character.corporation_id, transactions}
|
||||
{:transactions, character.corporation_id, transactions |> Enum.sort_by(& &1.date, :desc)}
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ defmodule WandererApp.Env do
|
||||
def corp_eve_id, do: get_key(:corp_id, -1)
|
||||
def subscription_settings, do: get_key(:subscription_settings)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "restrict_maps_creation"
|
||||
)
|
||||
def restrict_maps_creation?, do: get_key(:restrict_maps_creation, false)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "map-connection-auto-expire-hours"
|
||||
|
||||
@@ -62,7 +62,8 @@ defmodule WandererAppWeb.AdminLive do
|
||||
user_character_ids: user_character_ids,
|
||||
user_id: user_id,
|
||||
invite_link: nil,
|
||||
map_subscriptions_enabled?: WandererApp.Env.map_subscriptions_enabled?()
|
||||
map_subscriptions_enabled?: WandererApp.Env.map_subscriptions_enabled?(),
|
||||
restrict_maps_creation?: WandererApp.Env.restrict_maps_creation?()
|
||||
)}
|
||||
end
|
||||
|
||||
@@ -207,6 +208,22 @@ defmodule WandererAppWeb.AdminLive do
|
||||
|> put_flash(:info, "Character unlinked.")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"create-map",
|
||||
_params,
|
||||
socket
|
||||
) do
|
||||
WandererApp.Cache.put(
|
||||
"create_map_once",
|
||||
true
|
||||
)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> push_navigate(to: ~p"/maps/new")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(event, body, socket) do
|
||||
Logger.warning(fn -> "unhandled event: #{event} #{inspect(body)}" end)
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 2xl:grid-cols-4 pb-6">
|
||||
<div :if={@restrict_maps_creation?} class="card dark:bg-zinc-800 dark:border-zinc-600">
|
||||
<div class="card-body">
|
||||
<.button class="mt-2" type="button" phx-click="create-map">
|
||||
Create Map
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
<div :if={@map_subscriptions_enabled?} class="card dark:bg-zinc-800 dark:border-zinc-600">
|
||||
<div class="card-body">
|
||||
<div class="col-span-6">
|
||||
|
||||
@@ -13,7 +13,7 @@ defmodule WandererAppWeb.MapsLive do
|
||||
%{"user_id" => user_id} = _session,
|
||||
%{assigns: %{current_user: current_user}} = socket
|
||||
)
|
||||
when not is_nil(user_id) do
|
||||
when not is_nil(user_id) and is_connected?(socket) do
|
||||
{:ok, active_characters} = WandererApp.Api.Character.active_by_user(%{user_id: user_id})
|
||||
|
||||
user_characters =
|
||||
@@ -26,6 +26,7 @@ defmodule WandererAppWeb.MapsLive do
|
||||
characters: user_characters,
|
||||
importing: false,
|
||||
map_subscriptions_enabled?: WandererApp.Env.map_subscriptions_enabled?(),
|
||||
restrict_maps_creation?: WandererApp.Env.restrict_maps_creation?(),
|
||||
acls: [],
|
||||
location: nil
|
||||
)
|
||||
@@ -40,8 +41,16 @@ defmodule WandererAppWeb.MapsLive do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, url, socket) do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params, url)}
|
||||
def handle_params(params, url, socket) when is_connected?(socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:is_connected?, true)
|
||||
|> apply_action(socket.assigns.live_action, params, url)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(_params, _url, socket) do
|
||||
{:noreply, socket |> assign(:is_connected?, false)}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params, _url) do
|
||||
@@ -51,24 +60,32 @@ defmodule WandererAppWeb.MapsLive do
|
||||
end
|
||||
|
||||
defp apply_action(socket, :create, _params, url) do
|
||||
socket
|
||||
|> assign(:active_page, :maps)
|
||||
|> assign(:uri, URI.parse(url) |> Map.put(:path, ~p"/"))
|
||||
|> assign(:page_title, "Maps - Create")
|
||||
|> assign(:scopes, ["wormholes", "stargates", "none", "all"])
|
||||
|> assign(
|
||||
:form,
|
||||
AshPhoenix.Form.for_create(WandererApp.Api.Map, :new,
|
||||
forms: [
|
||||
auto?: true
|
||||
],
|
||||
prepare_source: fn form ->
|
||||
form
|
||||
|> Map.put("scope", "wormholes")
|
||||
end
|
||||
)
|
||||
)
|
||||
|> load_access_lists()
|
||||
allow_map_creation()
|
||||
|> case do
|
||||
true ->
|
||||
socket
|
||||
|> assign(:active_page, :maps)
|
||||
|> assign(:uri, URI.parse(url) |> Map.put(:path, ~p"/"))
|
||||
|> assign(:page_title, "Maps - Create")
|
||||
|> assign(:scopes, ["wormholes", "stargates", "none", "all"])
|
||||
|> assign(
|
||||
:form,
|
||||
AshPhoenix.Form.for_create(WandererApp.Api.Map, :new,
|
||||
forms: [
|
||||
auto?: true
|
||||
],
|
||||
prepare_source: fn form ->
|
||||
form
|
||||
|> Map.put("scope", "wormholes")
|
||||
end
|
||||
)
|
||||
)
|
||||
|> load_access_lists()
|
||||
|
||||
_ ->
|
||||
socket
|
||||
|> push_patch(to: ~p"/maps")
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_action(socket, :edit, %{"slug" => map_slug} = _params, url) do
|
||||
@@ -166,6 +183,9 @@ defmodule WandererAppWeb.MapsLive do
|
||||
)
|
||||
end
|
||||
|
||||
defp allow_map_creation(),
|
||||
do: not WandererApp.Env.restrict_maps_creation?() || WandererApp.Cache.take("create_map_once")
|
||||
|
||||
@impl true
|
||||
def handle_event("set-default", %{"id" => id}, socket) do
|
||||
send_update(LiveSelect.Component, options: socket.assigns.characters, id: id)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<%= if @maps != [] do %>
|
||||
<div class="gap-4 grid grid-cols-2 lg:grid-cols-5 md:grid-cols-4 sm:grid-cols-3 ">
|
||||
<.link
|
||||
:if={not @restrict_maps_creation?}
|
||||
class="card h-[250px] rounded-none bg-gradient-to-l from-stone-950 to-stone-900 hover:text-white transform transition duration-500"
|
||||
patch={~p"/maps/new"}
|
||||
>
|
||||
@@ -125,7 +126,7 @@
|
||||
</main>
|
||||
</div>
|
||||
<.modal
|
||||
:if={@live_action in [:create, :edit]}
|
||||
:if={@is_connected? && @live_action in [:create, :edit]}
|
||||
title={"#{(@live_action == :create && "Create") || "Edit"} Map"}
|
||||
class="!w-[500px]"
|
||||
id="add_map_modal"
|
||||
|
||||
Reference in New Issue
Block a user