211 lines
5.9 KiB
TypeScript
211 lines
5.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { getSecurityColor } from '../utils/securityColors';
|
|
|
|
interface MapNodeProps {
|
|
id: string;
|
|
name: string;
|
|
position: { x: number; y: number };
|
|
onClick: () => void;
|
|
onDoubleClick?: (e: React.MouseEvent) => void;
|
|
onDragStart?: (e: React.MouseEvent) => void;
|
|
onDrag?: (e: React.MouseEvent) => void;
|
|
onDragEnd?: (e: React.MouseEvent) => void;
|
|
onContextMenu?: (e: React.MouseEvent) => void;
|
|
type: 'region' | 'system';
|
|
security?: number;
|
|
signatures?: number;
|
|
isDraggable?: boolean;
|
|
disableNavigate?: boolean;
|
|
}
|
|
|
|
export const MapNode: React.FC<MapNodeProps> = ({
|
|
id,
|
|
name,
|
|
position,
|
|
onClick,
|
|
onDoubleClick,
|
|
onDragStart,
|
|
onDrag,
|
|
onDragEnd,
|
|
onContextMenu,
|
|
type,
|
|
security,
|
|
signatures,
|
|
isDraggable = false,
|
|
disableNavigate = false,
|
|
}) => {
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
|
|
const handleMouseDown = (e: React.MouseEvent) => {
|
|
console.log('MapNode handleMouseDown', { isDraggable, type, isDragging });
|
|
if (!isDraggable || type !== 'system') return;
|
|
e.stopPropagation();
|
|
onDragStart?.(e);
|
|
};
|
|
|
|
const handleMouseMove = (e: React.MouseEvent) => {
|
|
console.log('MapNode handleMouseMove', { isDragging, isDraggable, type });
|
|
if (!isDragging || !isDraggable || type !== 'system') return;
|
|
e.stopPropagation();
|
|
onDrag?.(e);
|
|
};
|
|
|
|
const handleMouseUp = (e: React.MouseEvent) => {
|
|
console.log('MapNode handleMouseUp', { isDragging, isDraggable, type });
|
|
if (!isDragging || !isDraggable || type !== 'system') return;
|
|
e.stopPropagation();
|
|
setIsDragging(false);
|
|
onDragEnd?.(e);
|
|
};
|
|
|
|
const handleMouseLeave = (e: React.MouseEvent) => {
|
|
console.log('MapNode handleMouseLeave', { isDragging, isDraggable, type });
|
|
if (!isDragging || !isDraggable || type !== 'system') return;
|
|
e.stopPropagation();
|
|
setIsDragging(false);
|
|
onDragEnd?.(e);
|
|
};
|
|
|
|
const nodeColor = security !== undefined ? getSecurityColor(security) : '#a855f7';
|
|
|
|
if (type === 'region') {
|
|
const pillWidth = Math.max(name.length * 5, 40);
|
|
const pillHeight = 18;
|
|
|
|
return (
|
|
<g
|
|
transform={`translate(${position.x}, ${position.y})`}
|
|
className="cursor-pointer select-none"
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onClick();
|
|
}}
|
|
>
|
|
{/* Glow effect */}
|
|
<rect
|
|
x={-pillWidth / 2 - 3}
|
|
y={-pillHeight / 2 - 3}
|
|
width={pillWidth + 6}
|
|
height={pillHeight + 6}
|
|
rx={(pillHeight + 6) / 2}
|
|
fill={nodeColor}
|
|
opacity={isHovered ? 0.3 : 0.1}
|
|
filter="url(#glow)"
|
|
className="transition-all duration-300"
|
|
/>
|
|
|
|
{/* Main pill */}
|
|
<rect
|
|
x={-pillWidth / 2}
|
|
y={-pillHeight / 2}
|
|
width={pillWidth}
|
|
height={pillHeight}
|
|
rx={pillHeight / 2}
|
|
fill={nodeColor}
|
|
stroke="#ffffff"
|
|
strokeWidth="1.5"
|
|
filter="url(#glow)"
|
|
className={`transition-all duration-300 ${isHovered ? 'drop-shadow-lg' : ''
|
|
}`}
|
|
/>
|
|
|
|
{/* Text inside pill - made smaller */}
|
|
<text
|
|
x="0"
|
|
y="3"
|
|
textAnchor="middle"
|
|
fill="#ffffff"
|
|
fontSize="8"
|
|
fontWeight="bold"
|
|
className="transition-all duration-300 pointer-events-none select-none"
|
|
style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.8)' }}
|
|
>
|
|
{name}
|
|
</text>
|
|
</g>
|
|
);
|
|
} else {
|
|
const nodeSize = 6;
|
|
const textOffset = 20;
|
|
|
|
return (
|
|
<g
|
|
transform={`translate(${position.x}, ${position.y})`}
|
|
className={`cursor-pointer select-none ${isDraggable ? 'cursor-move' : ''}`}
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
onMouseDown={handleMouseDown}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onClick();
|
|
}}
|
|
onDoubleClick={(e) => {
|
|
e.stopPropagation();
|
|
onDoubleClick?.(e);
|
|
}}
|
|
onContextMenu={(e) => {
|
|
e.stopPropagation();
|
|
onContextMenu?.(e);
|
|
}}
|
|
>
|
|
{/* Node glow effect */}
|
|
<circle
|
|
r={isHovered ? nodeSize + 4 : nodeSize}
|
|
fill={nodeColor}
|
|
opacity={isHovered ? 0.3 : 0.1}
|
|
filter="url(#glow)"
|
|
className="transition-all duration-300"
|
|
/>
|
|
|
|
{/* Main node - removed stroke to eliminate white border */}
|
|
<circle
|
|
r={nodeSize}
|
|
fill={nodeColor}
|
|
filter="url(#glow)"
|
|
className={`transition-all duration-300 ${isHovered ? 'drop-shadow-lg' : ''
|
|
}`}
|
|
/>
|
|
|
|
{/* Inner core */}
|
|
<circle
|
|
r={nodeSize - 3}
|
|
fill={isHovered ? '#ffffff' : nodeColor}
|
|
opacity={0.8}
|
|
className="transition-all duration-300"
|
|
/>
|
|
|
|
{/* Node label */}
|
|
<text
|
|
x="0"
|
|
y={textOffset}
|
|
textAnchor="middle"
|
|
fill="#ffffff"
|
|
fontSize="10"
|
|
fontWeight="bold"
|
|
className={`transition-all duration-300 ${isHovered ? 'fill-purple-200' : 'fill-white'
|
|
} pointer-events-none select-none`}
|
|
style={{ textShadow: '2px 2px 4px rgba(0,0,0,0.8)' }}
|
|
>
|
|
{name} {security !== undefined && (
|
|
<tspan fill={getSecurityColor(security)}>{security.toFixed(1)}</tspan>
|
|
)}
|
|
</text>
|
|
<text
|
|
x="0"
|
|
y={textOffset + 15}
|
|
textAnchor="middle"
|
|
fill="#a3a3a3"
|
|
fontSize="12"
|
|
className="pointer-events-none select-none"
|
|
style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.8)' }}
|
|
>
|
|
{signatures !== undefined && signatures > 0 && `📡 ${signatures}`}
|
|
</text>
|
|
</g>
|
|
);
|
|
}
|
|
};
|