diff --git a/src/components/GalaxyMap.tsx b/src/components/GalaxyMap.tsx index 7766940..02b9e57 100644 --- a/src/components/GalaxyMap.tsx +++ b/src/components/GalaxyMap.tsx @@ -28,7 +28,9 @@ const fetchUniverseData = async (): Promise => { export const GalaxyMap = () => { const navigate = useNavigate(); - const [draggedNode, setDraggedNode] = useState(null); + const [viewBox, setViewBox] = useState({ x: 0, y: 0, width: 1200, height: 800 }); + const [isPanning, setIsPanning] = useState(false); + const [lastPanPoint, setLastPanPoint] = useState({ x: 0, y: 0 }); const [nodePositions, setNodePositions] = useState>({}); const svgRef = useRef(null); @@ -52,30 +54,72 @@ export const GalaxyMap = () => { }, [regions]); const handleNodeClick = (regionName: string) => { - navigate(`/regions/${regionName}`); + // Encode region name to handle spaces and special characters + const encodedRegion = encodeURIComponent(regionName); + navigate(`/regions/${encodedRegion}`); }; - const handleMouseDown = useCallback((nodeId: string) => { - setDraggedNode(nodeId); + const handleMouseDown = useCallback((e: React.MouseEvent) => { + if (!svgRef.current) return; + setIsPanning(true); + const rect = svgRef.current.getBoundingClientRect(); + setLastPanPoint({ + x: e.clientX - rect.left, + y: e.clientY - rect.top + }); }, []); const handleMouseMove = useCallback((e: React.MouseEvent) => { - if (!draggedNode || !svgRef.current) return; + if (!isPanning || !svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + const currentPoint = { + x: e.clientX - rect.left, + y: e.clientY - rect.top + }; - setNodePositions(prev => ({ + const deltaX = (lastPanPoint.x - currentPoint.x) * (viewBox.width / rect.width); + const deltaY = (lastPanPoint.y - currentPoint.y) * (viewBox.height / rect.height); + + setViewBox(prev => ({ ...prev, - [draggedNode]: { x, y } + x: prev.x + deltaX, + y: prev.y + deltaY })); - }, [draggedNode]); + + setLastPanPoint(currentPoint); + }, [isPanning, lastPanPoint, viewBox.width, viewBox.height]); const handleMouseUp = useCallback(() => { - setDraggedNode(null); + setIsPanning(false); }, []); + const handleWheel = useCallback((e: React.WheelEvent) => { + e.preventDefault(); + if (!svgRef.current) return; + + const rect = svgRef.current.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const scale = e.deltaY > 0 ? 1.1 : 0.9; + const newWidth = viewBox.width * scale; + const newHeight = viewBox.height * scale; + + const mouseXInSVG = viewBox.x + (mouseX / rect.width) * viewBox.width; + const mouseYInSVG = viewBox.y + (mouseY / rect.height) * viewBox.height; + + const newX = mouseXInSVG - (mouseX / rect.width) * newWidth; + const newY = mouseYInSVG - (mouseY / rect.height) * newHeight; + + setViewBox({ + x: newX, + y: newY, + width: newWidth, + height: newHeight + }); + }, [viewBox]); + if (isLoading) { return (
@@ -105,11 +149,13 @@ export const GalaxyMap = () => { ref={svgRef} width="100%" height="100%" - viewBox="0 0 1200 800" - className="cursor-crosshair" + viewBox={`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`} + className="cursor-grab active:cursor-grabbing" + onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} + onWheel={handleWheel} > @@ -146,8 +192,6 @@ export const GalaxyMap = () => { name={region.regionName} position={nodePositions[region.regionName] || { x: 0, y: 0 }} onClick={() => handleNodeClick(region.regionName)} - onMouseDown={() => handleMouseDown(region.regionName)} - isDragging={draggedNode === region.regionName} type="region" security={region.security} /> diff --git a/src/components/MapNode.tsx b/src/components/MapNode.tsx index b5909fc..714f927 100644 --- a/src/components/MapNode.tsx +++ b/src/components/MapNode.tsx @@ -1,3 +1,4 @@ + import React, { useState } from 'react'; import { getSecurityColor } from '../utils/securityColors'; @@ -6,8 +7,6 @@ interface MapNodeProps { name: string; position: { x: number; y: number }; onClick: () => void; - onMouseDown: () => void; - isDragging: boolean; type: 'region' | 'system'; security?: number; } @@ -17,8 +16,6 @@ export const MapNode: React.FC = ({ name, position, onClick, - onMouseDown, - isDragging, type, security }) => { @@ -40,15 +37,9 @@ export const MapNode: React.FC = ({ className="cursor-pointer select-none" onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} - onMouseDown={(e) => { - e.preventDefault(); - onMouseDown(); - }} onClick={(e) => { e.stopPropagation(); - if (!isDragging) { - onClick(); - } + onClick(); }} > {/* Glow effect */} @@ -77,7 +68,7 @@ export const MapNode: React.FC = ({ filter="url(#glow)" className={`transition-all duration-300 ${ isHovered ? 'drop-shadow-lg' : '' - } ${isDragging ? 'opacity-80' : ''}`} + }`} /> {/* Text inside pill */} @@ -88,7 +79,7 @@ export const MapNode: React.FC = ({ fill="#ffffff" fontSize="14" fontWeight="bold" - className={`transition-all duration-300 pointer-events-none select-none`} + className="transition-all duration-300 pointer-events-none select-none" style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.8)' }} > {name} @@ -98,7 +89,7 @@ export const MapNode: React.FC = ({ } else { // Render system as a dot with external label const nodeSize = 8; - const textOffset = 15; + const textOffset = 20; // Increased from 15 to move text further down return ( = ({ className="cursor-pointer select-none" onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} - onMouseDown={(e) => { - e.preventDefault(); - onMouseDown(); - }} onClick={(e) => { e.stopPropagation(); - if (!isDragging) { - onClick(); - } + onClick(); }} > {/* Node glow effect */} @@ -135,7 +120,7 @@ export const MapNode: React.FC = ({ filter="url(#glow)" className={`transition-all duration-300 ${ isHovered ? 'drop-shadow-lg' : '' - } ${isDragging ? 'opacity-80' : ''}`} + }`} /> {/* Inner core */} diff --git a/src/components/RegionMap.tsx b/src/components/RegionMap.tsx index d2c66cf..44a0dc0 100644 --- a/src/components/RegionMap.tsx +++ b/src/components/RegionMap.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useCallback, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { MapNode } from './MapNode'; import { Connection } from './Connection'; import { Button } from '@/components/ui/button'; @@ -12,7 +12,7 @@ interface System { x: string; y: string; security: number; - connectsTo: string[]; + connectedSystems: string[]; } interface Position { @@ -25,19 +25,25 @@ interface RegionMapProps { } const fetchRegionData = async (regionName: string): Promise => { - const response = await fetch(`/${regionName}.json`); + // Decode the region name to handle spaces and special characters + const decodedRegionName = decodeURIComponent(regionName); + const response = await fetch(`/${decodedRegionName}.json`); if (!response.ok) { - throw new Error(`Failed to fetch ${regionName} data`); + throw new Error(`Failed to fetch ${decodedRegionName} data`); } return response.json(); }; export const RegionMap: React.FC = ({ regionName }) => { const navigate = useNavigate(); - const [draggedNode, setDraggedNode] = useState(null); + const [viewBox, setViewBox] = useState({ x: 0, y: 0, width: 1200, height: 800 }); + const [isPanning, setIsPanning] = useState(false); + const [lastPanPoint, setLastPanPoint] = useState({ x: 0, y: 0 }); const [nodePositions, setNodePositions] = useState>({}); const svgRef = useRef(null); + const decodedRegionName = decodeURIComponent(regionName); + const { data: systems, isLoading, error } = useQuery({ queryKey: ['region', regionName], queryFn: () => fetchRegionData(regionName), @@ -58,34 +64,95 @@ export const RegionMap: React.FC = ({ regionName }) => { }, [systems]); const handleSystemClick = (systemName: string) => { - navigate(`/systems/${systemName}`); + const encodedSystem = encodeURIComponent(systemName); + navigate(`/systems/${encodedSystem}`); }; - const handleMouseDown = useCallback((nodeId: string) => { - setDraggedNode(nodeId); + const handleRegionClick = (regionName: string) => { + const encodedRegion = encodeURIComponent(regionName); + navigate(`/regions/${encodedRegion}`); + }; + + const handleMouseDown = useCallback((e: React.MouseEvent) => { + if (!svgRef.current) return; + setIsPanning(true); + const rect = svgRef.current.getBoundingClientRect(); + setLastPanPoint({ + x: e.clientX - rect.left, + y: e.clientY - rect.top + }); }, []); const handleMouseMove = useCallback((e: React.MouseEvent) => { - if (!draggedNode || !svgRef.current) return; + if (!isPanning || !svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + const currentPoint = { + x: e.clientX - rect.left, + y: e.clientY - rect.top + }; - setNodePositions(prev => ({ + const deltaX = (lastPanPoint.x - currentPoint.x) * (viewBox.width / rect.width); + const deltaY = (lastPanPoint.y - currentPoint.y) * (viewBox.height / rect.height); + + setViewBox(prev => ({ ...prev, - [draggedNode]: { x, y } + x: prev.x + deltaX, + y: prev.y + deltaY })); - }, [draggedNode]); + + setLastPanPoint(currentPoint); + }, [isPanning, lastPanPoint, viewBox.width, viewBox.height]); const handleMouseUp = useCallback(() => { - setDraggedNode(null); + setIsPanning(false); }, []); + const handleWheel = useCallback((e: React.WheelEvent) => { + e.preventDefault(); + if (!svgRef.current) return; + + const rect = svgRef.current.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const scale = e.deltaY > 0 ? 1.1 : 0.9; + const newWidth = viewBox.width * scale; + const newHeight = viewBox.height * scale; + + const mouseXInSVG = viewBox.x + (mouseX / rect.width) * viewBox.width; + const mouseYInSVG = viewBox.y + (mouseY / rect.height) * viewBox.height; + + const newX = mouseXInSVG - (mouseX / rect.width) * newWidth; + const newY = mouseYInSVG - (mouseY / rect.height) * newHeight; + + setViewBox({ + x: newX, + y: newY, + width: newWidth, + height: newHeight + }); + }, [viewBox]); + + // Get system names that exist in current region + const systemNamesInRegion = new Set(systems?.map(s => s.solarSystemName) || []); + + // Find cross-region connections and extract region names + const crossRegionConnections = new Set(); + systems?.forEach(system => { + system.connectedSystems?.forEach(connectedSystem => { + if (!systemNamesInRegion.has(connectedSystem)) { + // This is a cross-region connection - we'll need to determine the region + // For now, we'll just store the system name and handle it in rendering + crossRegionConnections.add(connectedSystem); + } + }); + }); + if (isLoading) { return (
-
Loading {regionName} data...
+
Loading {decodedRegionName} data...
); } @@ -93,7 +160,7 @@ export const RegionMap: React.FC = ({ regionName }) => { if (error) { return (
-
Error loading {regionName} data
+
Error loading {decodedRegionName} data
); } @@ -113,7 +180,7 @@ export const RegionMap: React.FC = ({ regionName }) => { Back to Galaxy
-

{regionName} Region

+

{decodedRegionName} Region

Explore the systems within this region

@@ -123,11 +190,13 @@ export const RegionMap: React.FC = ({ regionName }) => { ref={svgRef} width="100%" height="100%" - viewBox="0 0 1200 800" - className="cursor-crosshair" + viewBox={`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`} + className="cursor-grab active:cursor-grabbing" + onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} + onWheel={handleWheel} > @@ -141,8 +210,9 @@ export const RegionMap: React.FC = ({ regionName }) => { {/* Render connections first (behind nodes) */} {systems?.map((system) => - system.connectsTo?.map((connectedSystem) => { + system.connectedSystems?.map((connectedSystem) => { const fromPos = nodePositions[system.solarSystemName]; + // Only render connection if target system is in current region const toPos = nodePositions[connectedSystem]; if (!fromPos || !toPos) return null; @@ -164,8 +234,6 @@ export const RegionMap: React.FC = ({ regionName }) => { name={system.solarSystemName} position={nodePositions[system.solarSystemName] || { x: 0, y: 0 }} onClick={() => handleSystemClick(system.solarSystemName)} - onMouseDown={() => handleMouseDown(system.solarSystemName)} - isDragging={draggedNode === system.solarSystemName} type="system" security={system.security} />