cosmo/frontend/src/App.tsx

159 lines
5.7 KiB
TypeScript
Raw Normal View History

/**
* Cosmo - Deep Space Explorer
* Main application component
*/
2025-11-27 10:14:25 +00:00
import { useState, useCallback } from 'react';
2025-11-29 16:58:58 +00:00
import { useNavigate } from 'react-router-dom';
import { useSpaceData } from './hooks/useSpaceData';
2025-11-27 10:14:25 +00:00
import { useHistoricalData } from './hooks/useHistoricalData';
import { useTrajectory } from './hooks/useTrajectory';
2025-11-29 16:58:58 +00:00
import { useScreenshot } from './hooks/useScreenshot';
2025-11-29 15:10:00 +00:00
import { Header } from './components/Header';
import { Scene } from './components/Scene';
import { ProbeList } from './components/ProbeList';
2025-11-27 10:14:25 +00:00
import { TimelineController } from './components/TimelineController';
import { Loading } from './components/Loading';
2025-11-29 16:58:58 +00:00
import { InterstellarTicker } from './components/InterstellarTicker';
import { ControlPanel } from './components/ControlPanel';
import type { CelestialBody } from './types';
2025-11-29 15:10:00 +00:00
// Timeline configuration - will be fetched from backend later
const TIMELINE_DAYS = 30; // Total days in timeline range
function App() {
2025-11-29 16:58:58 +00:00
const navigate = useNavigate();
2025-11-27 10:14:25 +00:00
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [isTimelineMode, setIsTimelineMode] = useState(false);
2025-11-29 16:58:58 +00:00
const [showOrbits, setShowOrbits] = useState(true);
const [isSoundOn, setIsSoundOn] = useState(false);
const [showDanmaku, setShowDanmaku] = useState(true);
const { takeScreenshot } = useScreenshot();
2025-11-27 10:14:25 +00:00
// Use real-time data or historical data based on mode
const { bodies: realTimeBodies, loading: realTimeLoading, error: realTimeError } = useSpaceData();
const { bodies: historicalBodies, loading: historicalLoading, error: historicalError } = useHistoricalData(selectedDate);
const bodies = isTimelineMode ? historicalBodies : realTimeBodies;
const loading = isTimelineMode ? historicalLoading : realTimeLoading;
const error = isTimelineMode ? historicalError : realTimeError;
const [selectedBody, setSelectedBody] = useState<CelestialBody | null>(null);
const { trajectoryPositions } = useTrajectory(selectedBody);
2025-11-27 10:14:25 +00:00
// Handle time change from timeline controller
const handleTimeChange = useCallback((date: Date) => {
setSelectedDate(date);
}, []);
// Toggle timeline mode
const toggleTimelineMode = useCallback(() => {
setIsTimelineMode((prev) => !prev);
if (!isTimelineMode) {
2025-11-29 15:10:00 +00:00
// Entering timeline mode, set initial date to now (will play backward)
setSelectedDate(new Date());
2025-11-27 10:14:25 +00:00
} else {
setSelectedDate(null);
}
}, [isTimelineMode]);
// Filter probes and planets from all bodies
const probes = bodies.filter((b) => b.type === 'probe');
2025-11-29 15:10:00 +00:00
const planets = bodies.filter((b) =>
b.type === 'planet' || b.type === 'dwarf_planet' || b.type === 'satellite'
);
const handleBodySelect = (body: CelestialBody | null) => {
setSelectedBody(body);
};
2025-11-29 15:17:41 +00:00
// Only show full screen loading when we have no data
// This prevents flashing when timeline is playing and fetching new data
if (loading && bodies.length === 0) {
return <Loading />;
}
if (error) {
return (
<div className="w-full h-full flex items-center justify-center bg-black text-white">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4"></h1>
<p className="text-red-400">{error}</p>
<p className="mt-4 text-sm text-gray-400">
API http://localhost:8000
</p>
</div>
</div>
);
}
return (
<div className="w-full h-full relative">
2025-11-29 16:58:58 +00:00
{/* Header with simplified branding */}
2025-11-29 15:10:00 +00:00
<Header
bodyCount={bodies.length}
selectedBodyName={selectedBody?.name}
/>
2025-11-29 16:58:58 +00:00
{/* Right Control Panel */}
<ControlPanel
isTimelineMode={isTimelineMode}
onToggleTimeline={toggleTimelineMode}
showOrbits={showOrbits}
onToggleOrbits={() => setShowOrbits(!showOrbits)}
isSoundOn={isSoundOn}
onToggleSound={() => setIsSoundOn(!isSoundOn)}
showDanmaku={showDanmaku}
onToggleDanmaku={() => setShowDanmaku(!showDanmaku)}
onLogin={() => navigate('/login')}
onScreenshot={takeScreenshot}
/>
{/* Probe List Sidebar */}
<ProbeList
probes={probes}
planets={planets}
onBodySelect={handleBodySelect}
selectedBody={selectedBody}
/>
{/* 3D Scene */}
<Scene
bodies={bodies}
selectedBody={selectedBody}
trajectoryPositions={trajectoryPositions}
2025-11-29 16:58:58 +00:00
showOrbits={showOrbits}
onBodySelect={handleBodySelect}
/>
2025-11-27 10:14:25 +00:00
{/* Timeline Controller */}
{isTimelineMode && (
<TimelineController
onTimeChange={handleTimeChange}
2025-11-29 15:10:00 +00:00
maxDate={new Date()} // Start point (now)
minDate={new Date(Date.now() - TIMELINE_DAYS * 24 * 60 * 60 * 1000)} // End point (past)
2025-11-27 10:14:25 +00:00
/>
)}
2025-11-29 16:58:58 +00:00
{/* Interstellar Ticker Sound (Controlled) */}
<InterstellarTicker isPlaying={isSoundOn} />
{/* Instructions overlay (Only show when exploring freely) */}
{!selectedBody && (
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-30 text-white text-xs bg-black/50 backdrop-blur-sm border border-white/10 px-4 py-2 rounded-full flex items-center gap-4 pointer-events-none">
<div className="flex items-center gap-2">
<span className="bg-white/20 p-1 rounded text-[10px]"></span>
</div>
<div className="flex items-center gap-2">
<span className="bg-white/20 p-1 rounded text-[10px]"></span>
</div>
<div className="flex items-center gap-2">
<span className="bg-white/20 p-1 rounded text-[10px]"></span>
</div>
</div>
)}
</div>
);
}
export default App;