feat: Implement interactive map views

Create interactive map views using SVG for nodes and connections. Implement navigation to region and system pages on click.
This commit is contained in:
gpt-engineer-app[bot]
2025-06-13 23:32:09 +00:00
parent 2320c73cc2
commit 1fdf39d60a
8 changed files with 519 additions and 9 deletions

View File

@@ -0,0 +1,37 @@
import React from 'react';
interface ConnectionProps {
from: { x: number; y: number };
to: { x: number; y: number };
}
export const Connection: React.FC<ConnectionProps> = ({ from, to }) => {
return (
<g>
{/* Glow effect */}
<line
x1={from.x}
y1={from.y}
x2={to.x}
y2={to.y}
stroke="#8b5cf6"
strokeWidth="3"
opacity="0.3"
filter="url(#glow)"
/>
{/* Main line */}
<line
x1={from.x}
y1={from.y}
x2={to.x}
y2={to.y}
stroke="#a855f7"
strokeWidth="1"
opacity="0.7"
className="transition-all duration-300"
/>
</g>
);
};

View File

@@ -0,0 +1,100 @@
import React, { useState, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { MapNode } from './MapNode';
import { Connection } from './Connection';
import { galaxyData } from '../data/galaxyData';
interface Position {
x: number;
y: number;
}
export const GalaxyMap = () => {
const navigate = useNavigate();
const [draggedNode, setDraggedNode] = useState<string | null>(null);
const [nodePositions, setNodePositions] = useState<Record<string, Position>>(galaxyData.nodePositions);
const svgRef = useRef<SVGSVGElement>(null);
const handleNodeClick = (regionId: string) => {
navigate(`/regions/${regionId}`);
};
const handleMouseDown = useCallback((nodeId: string) => {
setDraggedNode(nodeId);
}, []);
const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (!draggedNode || !svgRef.current) return;
const rect = svgRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setNodePositions(prev => ({
...prev,
[draggedNode]: { x, y }
}));
}, [draggedNode]);
const handleMouseUp = useCallback(() => {
setDraggedNode(null);
}, []);
return (
<div className="w-full h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 overflow-hidden relative">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-purple-900/20 via-slate-900/40 to-black"></div>
<div className="relative z-10 p-8">
<h1 className="text-4xl font-bold text-white mb-2 text-center">Galaxy Map</h1>
<p className="text-purple-200 text-center mb-8">Navigate the known regions of space</p>
<div className="w-full h-[calc(100vh-200px)] border border-purple-500/30 rounded-lg overflow-hidden bg-black/20 backdrop-blur-sm">
<svg
ref={svgRef}
width="100%"
height="100%"
viewBox="0 0 1200 800"
className="cursor-crosshair"
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
{/* Render connections first (behind nodes) */}
{galaxyData.connections.map((connection, index) => (
<Connection
key={index}
from={nodePositions[connection.from]}
to={nodePositions[connection.to]}
/>
))}
{/* Render nodes */}
{galaxyData.regions.map((region) => (
<MapNode
key={region.id}
id={region.id}
name={region.name}
position={nodePositions[region.id]}
onClick={() => handleNodeClick(region.id)}
onMouseDown={() => handleMouseDown(region.id)}
isDragging={draggedNode === region.id}
type="region"
/>
))}
</svg>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,95 @@
import React, { useState } from 'react';
interface MapNodeProps {
id: string;
name: string;
position: { x: number; y: number };
onClick: () => void;
onMouseDown: () => void;
isDragging: boolean;
type: 'region' | 'system';
}
export const MapNode: React.FC<MapNodeProps> = ({
id,
name,
position,
onClick,
onMouseDown,
isDragging,
type
}) => {
const [isHovered, setIsHovered] = useState(false);
const nodeSize = type === 'region' ? 12 : 8;
const textOffset = type === 'region' ? 20 : 15;
const nodeColor = type === 'region'
? (isHovered ? '#8b5cf6' : '#a855f7')
: (isHovered ? '#06b6d4' : '#0891b2');
return (
<g
transform={`translate(${position.x}, ${position.y})`}
className="cursor-pointer select-none"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onMouseDown={(e) => {
e.preventDefault();
onMouseDown();
}}
onClick={(e) => {
e.stopPropagation();
if (!isDragging) {
onClick();
}
}}
>
{/* Node glow effect */}
<circle
r={nodeSize + 6}
fill={nodeColor}
opacity={isHovered ? 0.3 : 0.1}
filter="url(#glow)"
className="transition-all duration-300"
/>
{/* Main node */}
<circle
r={nodeSize}
fill={nodeColor}
stroke="#ffffff"
strokeWidth="2"
filter="url(#glow)"
className={`transition-all duration-300 ${
isHovered ? 'drop-shadow-lg' : ''
} ${isDragging ? 'opacity-80' : ''}`}
/>
{/* Inner core */}
<circle
r={nodeSize - 4}
fill={isHovered ? '#ffffff' : nodeColor}
opacity={0.8}
className="transition-all duration-300"
/>
{/* Node label */}
<text
x="0"
y={textOffset}
textAnchor="middle"
fill="#ffffff"
fontSize={type === 'region' ? '14' : '12'}
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}
</text>
</g>
);
};

View File

@@ -0,0 +1,122 @@
import React, { useState, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { MapNode } from './MapNode';
import { Connection } from './Connection';
import { Button } from '@/components/ui/button';
import { ArrowLeft } from 'lucide-react';
interface Position {
x: number;
y: number;
}
interface RegionMapProps {
regionData: {
name: string;
systems: Array<{ id: string; name: string }>;
nodePositions: Record<string, Position>;
connections: Array<{ from: string; to: string }>;
};
}
export const RegionMap: React.FC<RegionMapProps> = ({ regionData }) => {
const navigate = useNavigate();
const [draggedNode, setDraggedNode] = useState<string | null>(null);
const [nodePositions, setNodePositions] = useState<Record<string, Position>>(regionData.nodePositions);
const svgRef = useRef<SVGSVGElement>(null);
const handleSystemClick = (systemId: string) => {
navigate(`/systems/${systemId}`);
};
const handleMouseDown = useCallback((nodeId: string) => {
setDraggedNode(nodeId);
}, []);
const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (!draggedNode || !svgRef.current) return;
const rect = svgRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setNodePositions(prev => ({
...prev,
[draggedNode]: { x, y }
}));
}, [draggedNode]);
const handleMouseUp = useCallback(() => {
setDraggedNode(null);
}, []);
return (
<div className="w-full h-screen bg-gradient-to-br from-slate-900 via-cyan-900 to-slate-900 overflow-hidden relative">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-cyan-900/20 via-slate-900/40 to-black"></div>
<div className="relative z-10 p-8">
<div className="flex items-center gap-4 mb-6">
<Button
variant="outline"
onClick={() => navigate('/')}
className="bg-black/20 border-cyan-500/30 text-cyan-200 hover:bg-cyan-500/20"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Galaxy
</Button>
<div>
<h1 className="text-4xl font-bold text-white">{regionData.name} Region</h1>
<p className="text-cyan-200">Explore the systems within this region</p>
</div>
</div>
<div className="w-full h-[calc(100vh-200px)] border border-cyan-500/30 rounded-lg overflow-hidden bg-black/20 backdrop-blur-sm">
<svg
ref={svgRef}
width="100%"
height="100%"
viewBox="0 0 1200 800"
className="cursor-crosshair"
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
{/* Render connections first (behind nodes) */}
{regionData.connections.map((connection, index) => (
<Connection
key={index}
from={nodePositions[connection.from]}
to={nodePositions[connection.to]}
/>
))}
{/* Render systems */}
{regionData.systems.map((system) => (
<MapNode
key={system.id}
id={system.id}
name={system.name}
position={nodePositions[system.id]}
onClick={() => handleSystemClick(system.id)}
onMouseDown={() => handleMouseDown(system.id)}
isDragging={draggedNode === system.id}
type="system"
/>
))}
</svg>
</div>
</div>
</div>
);
};