From 87fe01e2813765202daf74130942aa2cf50a7981 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:32:25 +0000 Subject: [PATCH] feat: Add system map to system view Integrate a small, focused map of the system's region into the system view page, enabling navigation between systems. --- src/components/RegionOverviewMap.tsx | 286 +++++++++++++++++++++++++++ src/pages/SystemView.tsx | 42 +++- src/utils/systemRegionMapping.ts | 26 +++ 3 files changed, 350 insertions(+), 4 deletions(-) create mode 100644 src/components/RegionOverviewMap.tsx create mode 100644 src/utils/systemRegionMapping.ts diff --git a/src/components/RegionOverviewMap.tsx b/src/components/RegionOverviewMap.tsx new file mode 100644 index 0000000..9e9d5e3 --- /dev/null +++ b/src/components/RegionOverviewMap.tsx @@ -0,0 +1,286 @@ + +import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { MapNode } from './MapNode'; +import { Connection } from './Connection'; +import { useQuery } from '@tanstack/react-query'; +import { getSecurityColor } from '../utils/securityColors'; + +const pocketbaseUrl = `https://evebase.site.quack-lab.dev/api/collections/regionview/records`; + +interface SolarSystem { + solarSystemName: string; + x: number; + y: number; + security: number; + signatures: number; + connectedSystems: string[]; +} + +interface Position { + x: number; + y: number; +} + +interface ProcessedConnection { + key: string; + from: Position; + to: Position; + color: string; +} + +interface RegionOverviewMapProps { + regionName: string; + currentSystem: string; + className?: string; +} + +const fetchRegionData = async (regionName: string): Promise => { + const response = await fetch(`/${regionName}.json`); + if (!response.ok) { + throw new Error('Failed to fetch region data'); + } + const systems = await response.json(); + + const regionSignatures = await fetch(`${pocketbaseUrl}?filter=(sysregion%3D'${regionName}')`); + const regionSignaturesJson = await regionSignatures.json(); + + if (regionSignaturesJson.items.length > 0) { + for (const systemSigs of regionSignaturesJson.items) { + const system = systems.find(s => s.solarSystemName === systemSigs.sysname); + if (system) { + system.signatures = systemSigs.sigcount; + } + } + } + return systems; +}; + +export const RegionOverviewMap: React.FC = ({ + regionName, + currentSystem, + className = "" +}) => { + const navigate = useNavigate(); + const [viewBox, setViewBox] = useState({ x: 0, y: 0, width: 800, height: 600 }); + const [isPanning, setIsPanning] = useState(false); + const [lastPanPoint, setLastPanPoint] = useState({ x: 0, y: 0 }); + const [nodePositions, setNodePositions] = useState>({}); + const svgRef = useRef(null); + + const { data: systems, isLoading } = useQuery({ + queryKey: ['region-overview', regionName], + queryFn: () => fetchRegionData(regionName), + }); + + // Process connections + const processedConnections = useMemo(() => { + if (!systems || !nodePositions) return []; + + const connections = new Map(); + + systems.forEach(system => { + system.connectedSystems?.forEach(connectedSystem => { + const connectionKey = [system.solarSystemName, connectedSystem].sort().join('-'); + if (connections.has(connectionKey)) return; + + const fromPos = nodePositions[system.solarSystemName]; + const toPos = nodePositions[connectedSystem]; + if (!fromPos || !toPos) return; + + const toSystem = systems.find(s => s.solarSystemName === connectedSystem); + if (!toSystem) return; + + const avgSecurity = (system.security + toSystem.security) / 2; + const connectionColor = getSecurityColor(avgSecurity); + + connections.set(connectionKey, { + key: connectionKey, + from: fromPos, + to: toPos, + color: connectionColor + }); + }); + }); + + return Array.from(connections.values()); + }, [systems, nodePositions]); + + // Initialize positions and center on current system + useEffect(() => { + if (systems) { + const positions: Record = {}; + systems.forEach(system => { + positions[system.solarSystemName] = { + x: system.x, + y: system.y + }; + }); + setNodePositions(positions); + + // Find current system and center view on it + const currentSystemData = systems.find(s => s.solarSystemName === currentSystem); + if (currentSystemData) { + setViewBox({ + x: currentSystemData.x - 200, // Center with some padding + y: currentSystemData.y - 150, + width: 400, + height: 300 + }); + } + } + }, [systems, currentSystem]); + + const handleSystemClick = (systemName: string) => { + navigate(`/systems/${systemName}`); + }; + + 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 (!isPanning || !svgRef.current) return; + + const rect = svgRef.current.getBoundingClientRect(); + const currentPoint = { + x: e.clientX - rect.left, + y: e.clientY - rect.top + }; + + const deltaX = (lastPanPoint.x - currentPoint.x) * (viewBox.width / rect.width); + const deltaY = (lastPanPoint.y - currentPoint.y) * (viewBox.height / rect.height); + + setViewBox(prev => ({ + ...prev, + x: prev.x + deltaX, + y: prev.y + deltaY + })); + + setLastPanPoint(currentPoint); + }, [isPanning, lastPanPoint, viewBox.width, viewBox.height]); + + const handleMouseUp = useCallback(() => { + 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; // Inverted: scroll down = zoom in + 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 ( +
+
Loading region map...
+
+ ); + } + + return ( +
+
+

{regionName} Region

+

Click systems to navigate

+
+ + + + + + + + + + + + + {/* Render connections */} + {processedConnections.map(connection => ( + + ))} + + {/* Render systems */} + {systems?.map((system) => ( + handleSystemClick(system.solarSystemName)} + type="system" + security={system.security} + signatures={system.signatures} + /> + ))} + + {/* Highlight current system with a ring */} + {currentSystem && nodePositions[currentSystem] && ( + + + + )} + +
+ ); +}; diff --git a/src/pages/SystemView.tsx b/src/pages/SystemView.tsx index b62b3ea..d6f663d 100644 --- a/src/pages/SystemView.tsx +++ b/src/pages/SystemView.tsx @@ -1,10 +1,20 @@ + import { useParams, useNavigate } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; import SystemTracker from "@/components/SystemTracker"; +import { RegionOverviewMap } from "@/components/RegionOverviewMap"; +import { findSystemRegion } from "@/utils/systemRegionMapping"; const SystemView = () => { const { system } = useParams(); const navigate = useNavigate(); + const { data: regionName } = useQuery({ + queryKey: ['system-region', system], + queryFn: () => findSystemRegion(system!), + enabled: !!system, + }); + if (!system) { navigate("/"); return null; @@ -14,13 +24,37 @@ const SystemView = () => {
-

Cosmic Region Navigator

-

Viewing signatures for system: {system}

+

System: {system}

+

Viewing signatures and regional overview

+
+ +
+ {/* Main content - signatures */} +
+ +
+ + {/* Regional overview map */} +
+ {regionName ? ( + + ) : ( +
+
+
Loading region data...
+
Finding system location
+
+
+ )} +
-
); }; -export default SystemView; \ No newline at end of file +export default SystemView; diff --git a/src/utils/systemRegionMapping.ts b/src/utils/systemRegionMapping.ts new file mode 100644 index 0000000..2ccd06e --- /dev/null +++ b/src/utils/systemRegionMapping.ts @@ -0,0 +1,26 @@ + +// Utility to find which region a system belongs to +export const findSystemRegion = async (systemName: string): Promise => { + // List of all region files we have + const regions = [ + 'Yasna Zakh', 'Molden Heath', 'Period Basis', 'The Bleak Lands', + 'Cloud Ring', 'Paragon Soul' + ]; + + for (const region of regions) { + try { + const response = await fetch(`/${region}.json`); + if (response.ok) { + const systems = await response.json(); + const foundSystem = systems.find((s: any) => s.solarSystemName === systemName); + if (foundSystem) { + return region; + } + } + } catch (error) { + console.warn(`Failed to load region ${region}:`, error); + } + } + + return null; +};