Compare commits

..

7 Commits

Author SHA1 Message Date
CI
79b284c46d chore: release version v1.47.2 2025-02-11 08:31:23 +00:00
guarzo
b29e57b3a4 fix: lazy load kills widget (#157)
* fix: lazy load kills widget

* fix: updates for eslint and pr feedback
2025-02-11 12:28:24 +04:00
CI
c6f4baeee3 chore: release version v1.47.1
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64/v8) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-02-09 16:27:54 +00:00
Dmitry Popov
6d341be072 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-02-09 17:27:27 +01:00
Dmitry Popov
2437ec9c84 fix(Connections): Fixed connections auto-refresh after update 2025-02-09 17:27:22 +01:00
CI
7e692b5805 chore: release version v1.47.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64/v8) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-02-09 10:13:17 +00:00
Dmitry Popov
01b7370ecd feat(Map): Added check for active map subscription to using Map APIs 2025-02-09 11:12:43 +01:00
19 changed files with 384 additions and 324 deletions

View File

@@ -2,6 +2,37 @@
<!-- changelog -->
## [v1.47.2](https://github.com/wanderer-industries/wanderer/compare/v1.47.1...v1.47.2) (2025-02-11)
### Bug Fixes:
* lazy load kills widget (#157)
* lazy load kills widget
* updates for eslint and pr feedback
## [v1.47.1](https://github.com/wanderer-industries/wanderer/compare/v1.47.0...v1.47.1) (2025-02-09)
### Bug Fixes:
* Connections: Fixed connections auto-refresh after update
## [v1.47.0](https://github.com/wanderer-industries/wanderer/compare/v1.46.1...v1.47.0) (2025-02-09)
### Features:
* Map: Added check for active map subscription to using Map APIs
## [v1.46.1](https://github.com/wanderer-industries/wanderer/compare/v1.46.0...v1.46.1) (2025-02-09)

View File

@@ -84,7 +84,6 @@ interface MapCompProps {
onCommand: OutCommandHandler;
onSelectionChange: OnMapSelectionChange;
onManualDelete(systems: string[]): void;
canRemoveConnection?(connectionId: string): boolean;
onConnectionInfoClick?(e: SolarSystemConnection): void;
onAddSystem?: OnMapAddSystemCallback;
onSelectionContextMenu?: NodeSelectionMouseHandler;
@@ -114,9 +113,8 @@ const MapComp = ({
isSoftBackground,
theme,
onAddSystem,
canRemoveConnection,
}: MapCompProps) => {
const { getEdge, getNode, getNodes } = useReactFlow();
const { getNode, getNodes } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
@@ -224,40 +222,6 @@ const MapComp = ({
[getNode, getNodes, onManualDelete, onNodesChange],
);
const handleEdgesChange = useCallback(
(changes: EdgeChange[]) => {
const nextChanges = changes.reduce((acc, change) => {
if (change.type !== 'remove') {
return [...acc, change];
}
if (canRemoveConnection?.(change.id)) {
return [...acc, change];
}
const edge = getEdge(change.id);
if (!edge) {
return [...acc, change];
}
const sourceNode = getNode(edge.source);
const targetNode = getNode(edge.target);
if (!sourceNode || !targetNode) {
return [...acc, change];
}
if (sourceNode.data.locked || targetNode.data.locked) {
return acc;
}
return [...acc, change];
}, [] as EdgeChange[]);
onEdgesChange(nextChanges);
},
[canRemoveConnection, getEdge, getNode, onEdgesChange],
);
useEffect(() => {
update(x => ({
...x,
@@ -273,7 +237,7 @@ const MapComp = ({
nodes={nodes}
edges={edges}
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
// TODO we need save into session all of this
// and on any action do either

View File

@@ -1,7 +1,7 @@
import { SystemKillsContent } from '../../../mapInterface/widgets/SystemKills/SystemKillsContent/SystemKillsContent';
import { useKillsCounter } from '../../hooks/useKillsCounter';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
@@ -11,16 +11,32 @@ type KillsBookmarkTooltipProps = {
systemId: string;
className?: string;
size?: TooltipSize;
timeRange?: number;
} & WithChildren &
WithClassName;
export const KillsCounter = ({ killsCount, systemId, className, children, size = 'xs' }: KillsBookmarkTooltipProps) => {
export const KillsCounter = ({
killsCount,
systemId,
className,
children,
size = 'xs',
timeRange = 1,
}: KillsBookmarkTooltipProps) => {
const { isLoading, kills: detailedKills, systemNameMap } = useKillsCounter({ realSystemId: systemId });
if (!killsCount || detailedKills.length === 0 || !systemId || isLoading) return null;
const tooltipContent = (
<SystemKillsContent kills={detailedKills} systemNameMap={systemNameMap} compact={true} onlyOneSystem={true} />
<SystemKillsContent
kills={detailedKills}
systemNameMap={systemNameMap}
compact={true}
onlyOneSystem={true}
autoSize={true}
timeRange={timeRange}
limit={killsCount}
/>
);
return (

View File

@@ -10,5 +10,6 @@ export const convertSystem2Node = (sys: SolarSystemRawType): Node => {
position: sys.position,
data: sys,
draggable: !sys.locked,
deletable: !sys.locked,
};
};

View File

@@ -7,8 +7,9 @@ import { useKillsWidgetSettings } from './hooks/useKillsWidgetSettings';
import { useSystemKills } from './hooks/useSystemKills';
import { KillsSettingsDialog } from './components/SystemKillsSettingsDialog';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
export const SystemKills: React.FC = () => {
export const SystemKills: React.FC = React.memo(() => {
const {
data: { selectedSystems, systems, isSubscriptionActive },
outCommand,
@@ -25,6 +26,16 @@ export const SystemKills: React.FC = () => {
return map;
}, [systems]);
const systemBySolarSystemId = useMemo(() => {
const map: Record<number, SolarSystemRawType> = {};
systems.forEach(sys => {
if (sys.system_static_info?.solar_system_id != null) {
map[sys.system_static_info.solar_system_id] = sys;
}
});
return map;
}, [systems]);
const [settings] = useKillsWidgetSettings();
const visible = settings.showAll;
@@ -40,78 +51,61 @@ export const SystemKills: React.FC = () => {
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
);
const system = systemBySolarSystemId[kill.solar_system_id];
if (!system) {
console.warn(`System with id ${kill.solar_system_id} not found.`);
return false;
}
return isWormholeSpace(system.system_static_info.system_class);
});
}, [kills, settings.whOnly, systems]);
}, [kills, settings.whOnly, systemBySolarSystemId, visible]);
return (
<div className="h-full flex flex-col min-h-0">
<div className="flex flex-col flex-1 min-h-0">
<Widget
label={
<KillsHeader
systemId={systemId}
onOpenSettings={() => setSettingsDialogVisible(true)}
/>
}
>
<div className="relative h-full">
{!isSubscriptionActive ? (
<div className="absolute inset-0 flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
Kills available with &#39;Active&#39; map subscription only (contact map administrators)
</span>
</div>
) : isNothingSelected ? (
<div className="absolute inset-0 flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
No system selected (or toggle Show all systems)
</span>
</div>
) : showLoading ? (
<div className="absolute inset-0 flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
Loading Kills...
</span>
</div>
) : error ? (
<div className="absolute inset-0 flex items-center justify-center">
<span className="select-none text-center text-red-400 text-sm">
{error}
</span>
</div>
) : !filteredKills || filteredKills.length === 0 ? (
<div className="absolute inset-0 flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
No kills found
</span>
</div>
) : (
<div className="h-full overflow-y-auto">
<SystemKillsContent
key={settings.compact ? 'compact' : 'normal'}
kills={filteredKills}
systemNameMap={systemNameMap}
compact={settings.compact}
onlyOneSystem={!visible}
/>
</div>
)}
</div>
<Widget label={<KillsHeader systemId={systemId} onOpenSettings={() => setSettingsDialogVisible(true)} />}>
{!isSubscriptionActive ? (
<div className="w-full h-full flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
Kills available with &#39;Active&#39; map subscription only (contact map administrators)
</span>
</div>
) : isNothingSelected ? (
<div className="w-full h-full flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
No system selected (or toggle Show all systems)
</span>
</div>
) : showLoading ? (
<div className="w-full h-full flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">Loading Kills...</span>
</div>
) : error ? (
<div className="w-full h-full flex items-center justify-center">
<span className="select-none text-center text-red-400 text-sm">{error}</span>
</div>
) : !filteredKills || filteredKills.length === 0 ? (
<div className="w-full h-full flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">No kills found</span>
</div>
) : (
<div className="w-full h-full" style={{ height: '100%' }}>
<SystemKillsContent
key={settings.compact ? 'compact' : 'normal'}
kills={filteredKills}
systemNameMap={systemNameMap}
compact={settings.compact}
onlyOneSystem={!visible}
timeRange={settings.timeRange}
/>
</div>
)}
</Widget>
</div>
<KillsSettingsDialog
visible={settingsDialogVisible}
setVisible={setSettingsDialogVisible}
/>
{settingsDialogVisible && <KillsSettingsDialog visible setVisible={setSettingsDialogVisible} />}
</div>
);
};
});
SystemKills.displayName = 'SystemKills';

View File

@@ -14,3 +14,7 @@
white-space: pre-line;
line-height: 1.2rem;
}
.VirtualScroller {
height: 100% !important;
}

View File

@@ -1,13 +1,17 @@
import React, { useMemo } from 'react';
import React, { useMemo, useRef, useEffect, useState } from 'react';
import clsx from 'clsx';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { KillRow } from '../components/SystemKillsRow';
import { VirtualScroller } from 'primereact/virtualscroller';
import { useSystemKillsItemTemplate } from '../hooks/useSystemKillsTemplate';
interface SystemKillsContentProps {
export interface SystemKillsContentProps {
kills: DetailedKill[];
systemNameMap: Record<string, string>;
compact?: boolean;
onlyOneSystem?: boolean;
autoSize?: boolean;
timeRange: number;
limit?: number;
}
export const SystemKillsContent: React.FC<SystemKillsContentProps> = ({
@@ -15,36 +19,73 @@ export const SystemKillsContent: React.FC<SystemKillsContentProps> = ({
systemNameMap,
compact = false,
onlyOneSystem = false,
autoSize = false,
timeRange = 1,
limit,
}) => {
const sortedKills = useMemo(() => {
return [...kills].sort((a, b) => {
const processedKills = useMemo(() => {
const validKills = kills.filter(kill => kill.kill_time);
const sortedKills = validKills.sort((a, b) => {
const timeA = a.kill_time ? new Date(a.kill_time).getTime() : 0;
const timeB = b.kill_time ? new Date(b.kill_time).getTime() : 0;
return timeB - timeA;
});
}, [kills]);
if (limit != null) {
return sortedKills.slice(0, limit);
} else {
const now = Date.now();
const cutoff = now - timeRange * 60 * 60 * 1000;
return sortedKills.filter(kill => {
if (!kill.kill_time) return false;
const killTime = new Date(kill.kill_time).getTime();
return killTime >= cutoff;
});
}
}, [kills, timeRange, limit]);
const itemSize = compact ? 35 : 50;
const computedHeight = autoSize ? Math.max(processedKills.length, 1) * itemSize + 5 : undefined;
const containerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const scrollerRef = useRef<any>(null);
const [containerHeight, setContainerHeight] = useState<number>(0);
useEffect(() => {
if (!autoSize && containerRef.current) {
const measure = () => {
const newHeight = containerRef.current?.clientHeight ?? 0;
setContainerHeight(newHeight);
scrollerRef.current?.refresh?.();
};
measure();
const observer = new ResizeObserver(measure);
observer.observe(containerRef.current);
window.addEventListener('resize', measure);
return () => {
observer.disconnect();
window.removeEventListener('resize', measure);
};
}
}, [autoSize]);
const itemTemplate = useSystemKillsItemTemplate(systemNameMap, compact, onlyOneSystem);
return (
<div
className={clsx(
'flex flex-col w-full text-stone-200 text-xs transition-all duration-300',
compact ? 'p-1' : 'p-1'
)}
>
{sortedKills.map(kill => {
const systemIdStr = String(kill.solar_system_id);
const systemName = systemNameMap[systemIdStr] || `System ${systemIdStr}`;
return (
<KillRow
key={kill.killmail_id}
killDetails={kill}
systemName={systemName}
isCompact={compact}
onlyOneSystem={onlyOneSystem}
/>
);
})}
<div ref={autoSize ? undefined : containerRef} className="w-full h-full">
<VirtualScroller
ref={autoSize ? undefined : scrollerRef}
items={processedKills}
itemSize={itemSize}
itemTemplate={itemTemplate}
autoSize={autoSize}
style={{ height: autoSize ? `${computedHeight}px` : containerHeight ? `${containerHeight}px` : '100%' }}
className={clsx('w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none')}
/>
</div>
);
};

View File

@@ -21,14 +21,9 @@ 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 = '',
@@ -40,7 +35,6 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({
victim_corp_id = 0,
victim_alliance_id = 0,
victim_ship_type_id = 0,
// Attacker
final_blow_char_id = 0,
final_blow_char_name = '',
@@ -51,70 +45,54 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({
final_blow_corp_id = 0,
final_blow_corp_name = '',
final_blow_ship_type_id = 0,
kill_time = '',
total_value = 0,
} = killDetails || {};
const attackerIsNpc = final_blow_char_id === 0;
// Tickers & strings
const victimAffiliationTicker =
victim_alliance_ticker || victim_corp_ticker || 'No Ticker';
const killValueFormatted =
total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
const 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);
// Victim images, including the ship
const {
victimCorpLogoUrl,
victimAllianceLogoUrl,
victimShipUrl,
} = buildVictimImageUrls({
const { victimCorpLogoUrl, victimAllianceLogoUrl, victimShipUrl } = buildVictimImageUrls({
victim_char_id,
victim_ship_type_id,
victim_corp_id,
victim_alliance_id,
});
// Attacker corp/alliance
const { attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
final_blow_char_id,
final_blow_corp_id,
final_blow_alliance_id,
});
// Victim corp/alliance logo
const { url: victimPrimaryLogoUrl, tooltip: victimPrimaryTooltip } =
getPrimaryLogoAndTooltip(
victimAllianceLogoUrl,
victimCorpLogoUrl,
victim_alliance_name,
victim_corp_name,
'Victim'
);
const { url: victimPrimaryLogoUrl, tooltip: victimPrimaryTooltip } = getPrimaryLogoAndTooltip(
victimAllianceLogoUrl,
victimCorpLogoUrl,
victim_alliance_name,
victim_corp_name,
'Victim',
);
// Attacker corp/alliance or NPC ship
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } =
getAttackerPrimaryImageAndTooltip(
attackerIsNpc,
attackerAllianceLogoUrl,
attackerCorpLogoUrl,
final_blow_alliance_name,
final_blow_corp_name,
final_blow_ship_type_id
);
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',
)}
>
<div className="flex items-center gap-1">
@@ -129,19 +107,13 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({
<img
src={victimShipUrl}
alt="VictimShip"
className={clsx(
classes.killRowImage,
'w-full h-full object-contain'
)}
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
</a>
</div>
)}
{victimPrimaryLogoUrl && (
<WdTooltipWrapper
content={victimPrimaryTooltip}
position={TooltipPosition.top}
>
<WdTooltipWrapper content={victimPrimaryTooltip} position={TooltipPosition.top}>
<a
href={zkillLink('kill', killmail_id)}
target="_blank"
@@ -151,16 +123,13 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({
<img
src={victimPrimaryLogoUrl}
alt="VictimPrimaryLogo"
className={clsx(
classes.killRowImage,
'w-full h-full object-contain'
)}
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="flex flex-col ml-2 flex-1 min-w-0 overflow-hidden leading-[1rem]">
<div className="truncate text-stone-200">
{victim_char_name}
<span className="text-stone-400"> / {victimAffiliationTicker}</span>
@@ -176,20 +145,17 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({
</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]">
<div className="flex flex-col items-end flex-1 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>
@@ -197,10 +163,7 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({
</div>
</div>
{attackerPrimaryImageUrl && (
<WdTooltipWrapper
content={attackerPrimaryTooltip}
position={TooltipPosition.top}
>
<WdTooltipWrapper content={attackerPrimaryTooltip} position={TooltipPosition.top}>
<a
href={zkillLink('kill', killmail_id)}
target="_blank"
@@ -210,17 +173,14 @@ export const CompactKillRow: React.FC<CompactKillRowProps> = ({
<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]',
)}
>
{attackerSubscript.label}

View File

@@ -22,11 +22,7 @@ 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
@@ -57,21 +53,13 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
const attackerIsNpc = final_blow_char_id === 0;
const victimAffiliation = victim_alliance_ticker || victim_corp_ticker || null;
const attackerAffiliation = attackerIsNpc
? ''
: final_blow_alliance_ticker || final_blow_corp_ticker || '';
const 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';
// Build victim images
const {
victimPortraitUrl,
victimCorpLogoUrl,
victimAllianceLogoUrl,
victimShipUrl,
} = buildVictimImageUrls({
const { victimPortraitUrl, victimCorpLogoUrl, victimAllianceLogoUrl, victimShipUrl } = buildVictimImageUrls({
victim_char_id,
victim_ship_type_id,
victim_corp_id,
@@ -79,47 +67,35 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
});
// Build attacker images
const {
attackerPortraitUrl,
attackerCorpLogoUrl,
attackerAllianceLogoUrl,
} = buildAttackerImageUrls({
const { attackerPortraitUrl, attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
final_blow_char_id,
final_blow_corp_id,
final_blow_alliance_id,
});
// Primary image for victim
const { url: victimPrimaryImageUrl, tooltip: victimPrimaryTooltip } =
getPrimaryLogoAndTooltip(
victimAllianceLogoUrl,
victimCorpLogoUrl,
victim_alliance_name,
victim_corp_name,
'Victim'
);
const { url: victimPrimaryImageUrl, tooltip: victimPrimaryTooltip } = getPrimaryLogoAndTooltip(
victimAllianceLogoUrl,
victimCorpLogoUrl,
victim_alliance_name,
victim_corp_name,
'Victim',
);
// Primary image for attacker
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } =
getAttackerPrimaryImageAndTooltip(
attackerIsNpc,
attackerAllianceLogoUrl,
attackerCorpLogoUrl,
final_blow_alliance_name,
final_blow_corp_name,
final_blow_ship_type_id
);
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } = getAttackerPrimaryImageAndTooltip(
attackerIsNpc,
attackerAllianceLogoUrl,
attackerCorpLogoUrl,
final_blow_alliance_name,
final_blow_corp_name,
final_blow_ship_type_id,
);
const attackerSubscript = getAttackerSubscript(killDetails);
return (
<div
className={clsx(
classes.killRowContainer,
'w-full text-sm py-1 px-2',
'flex flex-col sm:flex-row'
)}
>
<div className={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">
@@ -134,19 +110,13 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
<img
src={victimShipUrl}
alt="VictimShip"
className={clsx(
classes.killRowImage,
'w-full h-full object-contain'
)}
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
</a>
</div>
)}
{victimPrimaryImageUrl && (
<WdTooltipWrapper
content={victimPrimaryTooltip}
position={TooltipPosition.top}
>
<WdTooltipWrapper content={victimPrimaryTooltip} position={TooltipPosition.top}>
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
<a
href={zkillLink('kill', killmail_id)}
@@ -157,10 +127,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
<img
src={victimPrimaryImageUrl}
alt="VictimPrimaryLogo"
className={clsx(
classes.killRowImage,
'w-full h-full object-contain'
)}
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
</a>
</div>
@@ -171,12 +138,10 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
victimCharacterId={victim_char_id}
victimPortraitUrl={victimPortraitUrl}
/>
<div className="flex flex-col text-stone-200 leading-4 min-w-0 overflow-hidden">
<div className="flex flex-col flex-1 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>
)}
{victimAffiliation && <span className="ml-1 text-stone-400">/ {victimAffiliation}</span>}
</div>
<div className="truncate text-stone-300">
{victim_ship_name}
@@ -187,20 +152,15 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
</>
)}
</div>
<div className="truncate text-stone-400">
{!onlyOneSystem && systemName && <span>{systemName}</span>}
</div>
<div className="truncate text-stone-400">{!onlyOneSystem && systemName && <span>{systemName}</span>}</div>
</div>
</div>
{/* Attacker Section */}
<div className="flex items-start gap-1 min-w-0 sm:ml-auto">
<div className="flex flex-col items-end leading-4 min-w-0 overflow-hidden text-right">
<div className="flex flex-col flex-1 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>
)}
{attackerAffiliation && <span className="ml-1 text-stone-400">/ {attackerAffiliation}</span>}
</div>
)}
{!attackerIsNpc && final_blow_ship_name && (
@@ -208,7 +168,7 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
)}
<div className="truncate text-red-400">{killTimeAgo}</div>
</div>
{(!attackerIsNpc && attackerPortraitUrl && final_blow_char_id > 0) && (
{!attackerIsNpc && attackerPortraitUrl && final_blow_char_id &&final_blow_char_id > 0 && (
<div className="relative shrink-0 w-12 h-12 sm:w-14 sm:h-14 overflow-hidden">
<a
href={zkillLink('character', final_blow_char_id)}
@@ -219,19 +179,13 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
<img
src={attackerPortraitUrl}
alt="AttackerPortrait"
className={clsx(
classes.killRowImage,
'w-full h-full object-contain'
)}
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
/>
</a>
</div>
)}
{attackerPrimaryImageUrl && (
<WdTooltipWrapper
content={attackerPrimaryTooltip}
position={TooltipPosition.top}
>
<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)}
@@ -242,18 +196,10 @@ export const FullKillRow: React.FC<FullKillRowProps> = ({
<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(
attackerSubscript.cssClass,
classes.attackerCountLabel
)}
>
<span className={clsx(attackerSubscript.cssClass, classes.attackerCountLabel)}>
{attackerSubscript.label}
</span>
)}

View File

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

View File

@@ -10,7 +10,7 @@ export interface KillRowProps {
onlyOneSystem?: boolean;
}
export const KillRow: React.FC<KillRowProps> = ({
const KillRowComponent: React.FC<KillRowProps> = ({
killDetails,
systemName,
isCompact = false,
@@ -19,6 +19,7 @@ export const KillRow: React.FC<KillRowProps> = ({
if (isCompact) {
return <CompactKillRow killDetails={killDetails} systemName={systemName} onlyOneSystem={onlyOneSystem} />;
}
return <FullKillRow killDetails={killDetails} systemName={systemName} onlyOneSystem={onlyOneSystem} />;
};
export const KillRow = React.memo(KillRowComponent);

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import { InputSwitch } from 'primereact/inputswitch';
import { WdImgButton, SystemView, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { PrimeIcons } from 'primereact/api';
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
@@ -21,10 +22,10 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
showAll: globalSettings.showAll,
whOnly: globalSettings.whOnly,
excludedSystems: globalSettings.excludedSystems || [],
timeRange: globalSettings.timeRange,
});
const [, forceRender] = useState(0);
const [addSystemDialogVisible, setAddSystemDialogVisible] = useState(false);
useEffect(() => {
@@ -34,6 +35,7 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
showAll: globalSettings.showAll,
whOnly: globalSettings.whOnly,
excludedSystems: globalSettings.excludedSystems || [],
timeRange: globalSettings.timeRange,
};
forceRender(n => n + 1);
}
@@ -55,6 +57,15 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
forceRender(n => n + 1);
}, []);
// Updated handler to set time range as a number: 1 or 24
const handleTimeRangeChange = useCallback((newTimeRange: 1 | 24) => {
localRef.current = {
...localRef.current,
timeRange: newTimeRange,
};
forceRender(n => n + 1);
}, []);
const handleRemoveSystem = useCallback((sysId: number) => {
localRef.current = {
...localRef.current,
@@ -111,11 +122,18 @@ export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visibl
checked={localData.whOnly}
onChange={e => handleWHChange(e.target.checked)}
/>
<label htmlFor="kills-wh-only-mode" className="cursor-pointer">
<label htmlFor="kills-wormhole-only-mode" className="cursor-pointer">
Only show wormhole kills
</label>
</div>
{/* Time Range Toggle using InputSwitch */}
<div className="flex items-center gap-2">
<span className="text-sm">Time Range:</span>
<InputSwitch checked={localData.timeRange === 24} onChange={e => handleTimeRangeChange(e.value ? 24 : 1)} />
<span className="text-sm">{localData.timeRange === 24 ? '24 Hours' : '1 Hour'}</span>
</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>
@@ -128,7 +146,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}

View File

@@ -7,6 +7,7 @@ export interface KillsWidgetSettings {
whOnly: boolean;
excludedSystems: number[];
version: number;
timeRange: number;
}
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
@@ -14,7 +15,8 @@ export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
showAll: false,
whOnly: true,
excludedSystems: [],
version: 0,
version: 1,
timeRange: 1,
};
function mergeWithDefaults(settings?: Partial<KillsWidgetSettings>): KillsWidgetSettings {

View File

@@ -0,0 +1,17 @@
// useSystemKillsItemTemplate.tsx
import { useCallback } from 'react';
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { KillItemTemplate } from '../components/KillItemTemplate';
export function useSystemKillsItemTemplate(
systemNameMap: Record<string, string>,
compact: boolean,
onlyOneSystem: boolean,
) {
return useCallback(
(kill: DetailedKill, options: VirtualScrollerTemplateOptions) =>
KillItemTemplate(systemNameMap, compact, onlyOneSystem, kill, options),
[systemNameMap, compact, onlyOneSystem],
);
}

View File

@@ -33,7 +33,7 @@ export const MapWrapper = () => {
const {
update,
outCommand,
data: { selectedConnections, selectedSystems, hubs, systems, connections, linkSignatureToSystem },
data: { selectedConnections, selectedSystems, hubs, systems, linkSignatureToSystem },
interfaceSettings: {
isShowMenu,
isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap,
@@ -56,8 +56,8 @@ export const MapWrapper = () => {
const [openAddSystem, setOpenAddSystem] = useState<XYPosition | null>(null);
const [selectedConnection, setSelectedConnection] = useState<SolarSystemConnection | null>(null);
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, connections, deleteSystems });
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, connections, deleteSystems };
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems });
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems };
useMapEventListener(event => {
runCommand(event);
@@ -125,11 +125,6 @@ export const MapWrapper = () => {
setOpenAddSystem(coordinates);
}, []);
const canRemoveConnection = useCallback((connectionId: string) => {
const { connections } = ref.current;
return !connections.some(x => x.id === connectionId);
}, []);
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
async item => {
if (ref.current.systems.some(x => x.system_static_info.solar_system_id === item.value)) {
@@ -166,7 +161,6 @@ export const MapWrapper = () => {
isSoftBackground={isSoftBackground}
theme={theme}
onAddSystem={onAddSystem}
canRemoveConnection={canRemoveConnection}
/>
{openSettings != null && (

View File

@@ -13,27 +13,19 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do
case header do
"Bearer " <> incoming_token ->
case fetch_map_id(conn.query_params) do
{:ok, map_id} ->
case WandererApp.Api.Map.by_id(map_id) do
{:ok, map} ->
if map.public_api_key == incoming_token do
conn
else
conn
|> send_resp(401, "Unauthorized (invalid token for map)")
|> halt()
end
{:error, _reason} ->
conn
|> send_resp(404, "Map not found")
|> halt()
case fetch_map(conn.query_params) do
{:ok, map} ->
if map.public_api_key == incoming_token do
conn
else
conn
|> send_resp(401, "Unauthorized (invalid token for map)")
|> halt()
end
{:error, msg} ->
{:error, _reason} ->
conn
|> send_resp(400, msg)
|> send_resp(404, "Map not found")
|> halt()
end
@@ -44,6 +36,19 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do
end
end
defp fetch_map(query_params) do
case fetch_map_id(query_params) do
{:ok, {:map, map}} ->
{:ok, map}
{:ok, map_id} ->
WandererApp.Api.Map.by_id(map_id)
error ->
error
end
end
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
{:ok, mid}
end
@@ -51,7 +56,7 @@ defmodule WandererAppWeb.Plugs.CheckMapApiKey do
defp fetch_map_id(%{"slug" => slug}) when is_binary(slug) and slug != "" do
case WandererApp.Api.Map.get_map_by_slug(slug) do
{:ok, map} ->
{:ok, map.id}
{:ok, {:map, map}}
{:error, _reason} ->
{:error, "No map found for slug=#{slug}"}

View File

@@ -0,0 +1,46 @@
defmodule WandererAppWeb.Plugs.CheckMapSubscription do
@moduledoc """
A plug that checks the Map has active subscription
Halts with 401 if no active subscription.
"""
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
case fetch_map_id(conn.query_params) do
{:ok, map_id} ->
{:ok, is_subscription_active} = map_id |> WandererApp.Map.is_subscription_active?()
if is_subscription_active do
conn
else
conn
|> send_resp(401, "Unauthorized (map subscription not active)")
|> halt()
end
{:error, msg} ->
conn
|> send_resp(400, msg)
|> halt()
end
end
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
{:ok, mid}
end
defp fetch_map_id(%{"slug" => slug}) when is_binary(slug) and slug != "" do
case WandererApp.Api.Map.get_map_by_slug(slug) do
{:ok, map} ->
{:ok, map.id}
{:error, _reason} ->
{:error, "No map found for slug=#{slug}"}
end
end
defp fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
end

View File

@@ -24,7 +24,6 @@ defmodule WandererAppWeb.Router do
@font_src ~w('self' https://fonts.gstatic.com data: https://web.ccpgamescdn.com https://w.appzi.io )
@script_src ~w('self' )
pipeline :admin_bauth do
plug :admin_basic_auth
end
@@ -112,6 +111,7 @@ defmodule WandererAppWeb.Router do
pipeline :api_map do
plug WandererAppWeb.Plugs.CheckMapApiKey
plug WandererAppWeb.Plugs.CheckMapSubscription
end
pipeline :api_kills do
@@ -145,7 +145,6 @@ defmodule WandererAppWeb.Router do
# GET /api/common/system-static-info?id=...
get "/system-static-info", CommonAPIController, :show_system_static
end
scope "/", WandererAppWeb do

View File

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