Fix: Header size and refactor components

Ensure consistent header size across all pages and refactor `SystemView` and `RegionMap` components.
This commit is contained in:
gpt-engineer-app[bot]
2025-06-14 20:27:11 +00:00
parent 1f311d5b1a
commit c6079c3543
9 changed files with 318 additions and 283 deletions

View File

@@ -0,0 +1,25 @@
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
interface CleanModeToggleProps {
cleanMode: boolean;
onToggle: (enabled: boolean) => void;
}
const CleanModeToggle = ({ cleanMode, onToggle }: CleanModeToggleProps) => {
return (
<div className="flex items-center space-x-2">
<Switch
id="clean-mode"
checked={cleanMode}
onCheckedChange={onToggle}
/>
<Label htmlFor="clean-mode" className="text-sm text-slate-300">
Clean mode
</Label>
</div>
);
};
export default CleanModeToggle;

View File

@@ -1,4 +1,3 @@
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { MapNode } from './MapNode';
@@ -124,7 +123,7 @@ export const GalaxyMap = () => {
if (isLoading) {
return (
<div className="w-full h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 flex items-center justify-center">
<div className="w-full h-full flex items-center justify-center">
<div className="text-white text-xl">Loading universe data...</div>
</div>
);
@@ -132,80 +131,73 @@ export const GalaxyMap = () => {
if (error) {
return (
<div className="w-full h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 flex items-center justify-center">
<div className="w-full h-full flex items-center justify-center">
<div className="text-red-400 text-xl">Error loading universe data</div>
</div>
);
}
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="w-full h-full p-4">
<div className="w-full h-full border border-purple-500/30 rounded-lg overflow-hidden bg-black/20 backdrop-blur-sm">
<svg
ref={svgRef}
width="100%"
height="100%"
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}
>
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<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>
{/* Render connections first (behind nodes) */}
{regions?.map((region) =>
region.connectsTo?.map((connectedRegion) => {
const fromPos = nodePositions[region.regionName];
const toPos = nodePositions[connectedRegion];
if (!fromPos || !toPos) return null;
<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={`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`}
className="cursor-grab active:cursor-grabbing"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onWheel={handleWheel}
>
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
// Get colors for both regions
const toRegion = regions.find(r => r.regionName === connectedRegion);
const avgSecurity = (region.security + toRegion.security) / 2;
const color = getSecurityColor(avgSecurity);
{/* Render connections first (behind nodes) */}
{regions?.map((region) =>
region.connectsTo?.map((connectedRegion) => {
const fromPos = nodePositions[region.regionName];
const toPos = nodePositions[connectedRegion];
if (!fromPos || !toPos) return null;
return (
<Connection
key={`${region.regionName}-${connectedRegion}`}
from={fromPos}
to={toPos}
color={color}
/>
);
})
)}
// Get colors for both regions
const toRegion = regions.find(r => r.regionName === connectedRegion);
const avgSecurity = (region.security + toRegion.security) / 2;
const color = getSecurityColor(avgSecurity);
return (
<Connection
key={`${region.regionName}-${connectedRegion}`}
from={fromPos}
to={toPos}
color={color}
/>
);
})
)}
{/* Render fromCodes */}
{regions?.map((region) => (
<MapNode
key={region.regionName}
id={region.regionName}
name={region.regionName}
position={nodePositions[region.regionName] || { x: 0, y: 0 }}
onClick={() => handleNodeClick(region.regionName)}
type="region"
security={region.security}
/>
))}
</svg>
</div>
{/* Render regions */}
{regions?.map((region) => (
<MapNode
key={region.regionName}
id={region.regionName}
name={region.regionName}
position={nodePositions[region.regionName] || { x: 0, y: 0 }}
onClick={() => handleNodeClick(region.regionName)}
type="region"
security={region.security}
/>
))}
</svg>
</div>
</div>
);

61
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,61 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
interface HeaderProps {
title: string;
breadcrumbs?: Array<{
label: string;
path?: string;
}>;
}
const Header = ({ title, breadcrumbs = [] }: HeaderProps) => {
const navigate = useNavigate();
return (
<div className="flex-shrink-0 py-4 px-4 border-b border-purple-500/20">
{/* Breadcrumb Navigation */}
{breadcrumbs.length > 0 && (
<div className="mb-3">
<Breadcrumb>
<BreadcrumbList>
{breadcrumbs.map((crumb, index) => (
<React.Fragment key={index}>
{index > 0 && <BreadcrumbSeparator className="text-slate-400" />}
<BreadcrumbItem>
{crumb.path ? (
<BreadcrumbLink
onClick={() => navigate(crumb.path!)}
className="text-purple-200 hover:text-white cursor-pointer"
>
{crumb.label}
</BreadcrumbLink>
) : (
<BreadcrumbPage className="text-white font-semibold">
{crumb.label}
</BreadcrumbPage>
)}
</BreadcrumbItem>
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
</div>
)}
{/* Title */}
<h1 className="text-2xl font-bold text-white">{title}</h1>
</div>
);
};
export default Header;

View File

@@ -3,27 +3,9 @@ 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';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
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[];
}
import { useRegionData } from '../hooks/useRegionData';
import Header from './Header';
interface Position {
x: number;
@@ -43,27 +25,6 @@ interface RegionMapProps {
isCompact?: boolean;
}
const fetchRegionData = async (regionName: string): Promise<SolarSystem[]> => {
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}')&perPage=1000`);
const regionSignaturesJson = await regionSignatures.json();
console.log(regionSignaturesJson);
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;
};
const RegionMap = ({ regionName, focusSystem, isCompact = false }: RegionMapProps) => {
const navigate = useNavigate();
const [viewBox, setViewBox] = useState({ x: 0, y: 0, width: 1200, height: 800 });
@@ -72,10 +33,7 @@ const RegionMap = ({ regionName, focusSystem, isCompact = false }: RegionMapProp
const [nodePositions, setNodePositions] = useState<Record<string, Position>>({});
const svgRef = useRef<SVGSVGElement>(null);
const { data: systems, isLoading, error } = useQuery({
queryKey: ['region', regionName],
queryFn: () => fetchRegionData(regionName),
});
const { data: systems, isLoading, error } = useRegionData(regionName);
// Process connections once when systems or nodePositions change
const processedConnections = useMemo(() => {
@@ -306,81 +264,64 @@ const RegionMap = ({ regionName, focusSystem, isCompact = false }: RegionMapProp
<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">
{/* Breadcrumb Navigation */}
<div className="mb-6">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
onClick={() => navigate("/")}
className="text-purple-200 hover:text-white cursor-pointer"
>
Universe
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="text-slate-400" />
<BreadcrumbItem>
<BreadcrumbPage className="text-white font-semibold">
{regionName}
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="relative z-10 h-full flex flex-col">
<Header
title={regionName}
breadcrumbs={[
{ label: "Universe", path: "/" },
{ label: regionName }
]}
/>
<div className="mb-8">
<h1 className="text-4xl font-bold text-white mb-2">{regionName}</h1>
<p className="text-purple-200">Solar systems in this region</p>
</div>
<div className="flex-1 overflow-hidden p-4">
<div className="w-full h-full border border-purple-500/30 rounded-lg overflow-hidden bg-black/20 backdrop-blur-sm">
<svg
ref={svgRef}
width="100%"
height="100%"
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}
>
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
<feMerge>
<feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<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={`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`}
className="cursor-grab active:cursor-grabbing"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onWheel={handleWheel}
>
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
<feMerge>
<feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
{/* Render connections */}
{processedConnections.map(connection => (
<Connection
key={connection.key}
from={connection.from}
to={connection.to}
color={connection.color}
/>
))}
{/* Render connections */}
{processedConnections.map(connection => (
<Connection
key={connection.key}
from={connection.from}
to={connection.to}
color={connection.color}
/>
))}
{/* Render systems */}
{systems?.map((system) => (
<MapNode
key={system.solarSystemName}
id={system.solarSystemName}
name={system.solarSystemName}
position={nodePositions[system.solarSystemName] || { x: 0, y: 0 }}
onClick={() => handleSystemClick(system.solarSystemName)}
type="system"
security={system.security}
signatures={system.signatures}
/>
))}
</svg>
{/* Render systems */}
{systems?.map((system) => (
<MapNode
key={system.solarSystemName}
id={system.solarSystemName}
name={system.solarSystemName}
position={nodePositions[system.solarSystemName] || { x: 0, y: 0 }}
onClick={() => handleSystemClick(system.solarSystemName)}
type="system"
security={system.security}
signatures={system.signatures}
/>
))}
</svg>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,41 @@
import { useQuery } from '@tanstack/react-query';
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[];
}
const fetchRegionData = async (regionName: string): Promise<SolarSystem[]> => {
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}')&perPage=1000`);
const regionSignaturesJson = await regionSignatures.json();
console.log(regionSignaturesJson);
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 useRegionData = (regionName: string) => {
return useQuery({
queryKey: ['region', regionName],
queryFn: () => fetchRegionData(regionName),
});
};

View File

@@ -1,8 +1,18 @@
import { GalaxyMap } from '../components/GalaxyMap';
import Header from '../components/Header';
const Index = () => {
return <GalaxyMap />;
return (
<div className="h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 overflow-hidden">
<div className="h-full flex flex-col">
<Header title="Galaxy Map" />
<div className="flex-1 overflow-hidden">
<GalaxyMap />
</div>
</div>
</div>
);
};
export default Index;

View File

@@ -5,16 +5,10 @@ import { toast } from "@/hooks/use-toast";
import { useQueryClient } from "@tanstack/react-query";
import SystemTracker from "@/components/SystemTracker";
import RegionMap from "@/components/RegionMap";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import CleanModeToggle from "@/components/CleanModeToggle";
import Header from "@/components/Header";
import { parseSignature, parseScannedPercentage } from "@/utils/signatureParser";
import { getSystemId } from "@/utils/systemApi";
import pb from "@/lib/pocketbase";
interface Signature {
@@ -38,42 +32,6 @@ const SystemView = () => {
return null;
}
const parseSignature = (text: string): Omit<Signature, 'system' | 'sysid'> | null => {
const parts = text.split('\t');
if (parts.length < 4) return null;
// Validate signature identifier format (3 letters, dash, 3 numbers)
const signatureIdentifierRegex = /^\w{3}-\d{3}$/;
if (!signatureIdentifierRegex.test(parts[0])) {
return null;
}
return {
identifier: parts[0],
type: parts[2],
name: parts[3],
scanned: parts.length > 4 ? parts[4] : undefined,
dangerous: false // TODO: Implement dangerous signature detection
};
};
const getSystemId = async (systemName: string): Promise<string> => {
const url = `https://evebase.site.quack-lab.dev/api/collections/regionview/records?filter=(sysname='${encodeURIComponent(systemName)}')`;
const response = await fetch(url);
const data = await response.json();
if (data.items && data.items.length > 0) {
return data.items[0].id;
}
throw new Error(`System ${systemName} not found`);
};
const parseScannedPercentage = (scannedString?: string): number => {
if (!scannedString) return 0;
const match = scannedString.match(/(\d+(?:\.\d+)?)%/);
return match ? parseFloat(match[1]) : 0;
};
const saveSignature = async (signature: Signature): Promise<void> => {
try {
// Check if signature already exists
@@ -209,66 +167,27 @@ const SystemView = () => {
};
}, [system, cleanMode]);
const breadcrumbs = [
{ label: "Universe", path: "/" },
...(region ? [{ label: region, path: `/regions/${region}` }] : []),
{ label: system }
];
return (
<div className="h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 overflow-hidden">
<div className="h-full flex flex-col">
<div className="flex-shrink-0 py-6 px-4">
{/* Breadcrumb Navigation */}
<div className="mb-4">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
onClick={() => navigate("/")}
className="text-purple-200 hover:text-white cursor-pointer"
>
Universe
</BreadcrumbLink>
</BreadcrumbItem>
{region && (
<>
<BreadcrumbSeparator className="text-slate-400" />
<BreadcrumbItem>
<BreadcrumbLink
onClick={() => navigate(`/regions/${region}`)}
className="text-purple-200 hover:text-white cursor-pointer"
>
{region}
</BreadcrumbLink>
</BreadcrumbItem>
</>
)}
<BreadcrumbSeparator className="text-slate-400" />
<BreadcrumbItem>
<BreadcrumbPage className="text-white font-semibold">
{system}
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
{/* System Title and Controls */}
<div className="text-center">
<h1 className="text-4xl font-bold text-white mb-2">System: {system}</h1>
<div className="flex items-center justify-center gap-4 text-slate-300">
<p>Press Ctrl+V to paste signatures</p>
<div className="flex items-center space-x-2">
<Switch
id="clean-mode"
checked={cleanMode}
onCheckedChange={setCleanMode}
/>
<Label htmlFor="clean-mode" className="text-sm text-slate-300">
Clean mode
</Label>
</div>
</div>
<Header title={`System: ${system}`} breadcrumbs={breadcrumbs} />
{/* Controls */}
<div className="flex-shrink-0 px-4 py-3 border-b border-purple-500/20">
<div className="flex items-center justify-center gap-4 text-slate-300">
<p>Press Ctrl+V to paste signatures</p>
<CleanModeToggle cleanMode={cleanMode} onToggle={setCleanMode} />
</div>
</div>
<div className="flex-1 overflow-hidden px-4 pb-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 h-full">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 h-full pt-6">
{/* Main content - signatures */}
<div className="lg:col-span-2 space-y-6 overflow-y-auto">
<SystemTracker system={system} />

View File

@@ -0,0 +1,35 @@
interface Signature {
identifier: string;
type: string;
name: string;
system: string;
sysid: string;
dangerous?: boolean;
scanned?: string;
}
export const parseSignature = (text: string): Omit<Signature, 'system' | 'sysid'> | null => {
const parts = text.split('\t');
if (parts.length < 4) return null;
// Validate signature identifier format (3 letters, dash, 3 numbers)
const signatureIdentifierRegex = /^\w{3}-\d{3}$/;
if (!signatureIdentifierRegex.test(parts[0])) {
return null;
}
return {
identifier: parts[0],
type: parts[2],
name: parts[3],
scanned: parts.length > 4 ? parts[4] : undefined,
dangerous: false // TODO: Implement dangerous signature detection
};
};
export const parseScannedPercentage = (scannedString?: string): number => {
if (!scannedString) return 0;
const match = scannedString.match(/(\d+(?:\.\d+)?)%/);
return match ? parseFloat(match[1]) : 0;
};

11
src/utils/systemApi.ts Normal file
View File

@@ -0,0 +1,11 @@
export const getSystemId = async (systemName: string): Promise<string> => {
const url = `https://evebase.site.quack-lab.dev/api/collections/regionview/records?filter=(sysname='${encodeURIComponent(systemName)}')`;
const response = await fetch(url);
const data = await response.json();
if (data.items && data.items.length > 0) {
return data.items[0].id;
}
throw new Error(`System ${systemName} not found`);
};