import { useState, useEffect, useRef, useCallback } from "react"; import { Canvas, useThree, useFrame } from "@react-three/fiber"; import { OrbitControls, Stars as BackgroundStars } from "@react-three/drei"; import { getSkyData } from "./api"; import { Sidebar } from "./components/Sidebar"; import { InfoPanel } from "./components/InfoPanel"; import { Compass } from "./components/StarMap/Compass"; import { TimeController } from "./components/TimeController"; import { MAJOR_STARS } from "./data/stars"; import { DSOs } from "./components/StarMap/DSOs"; import { Stars } from "./components/StarMap/Stars"; import { Planets } from "./components/StarMap/Planets"; import { Meteors } from "./components/StarMap/Meteors"; import { CONSTELLATIONS } from "./data/constellations"; import { MilkyWay } from "./components/StarMap/MilkyWay"; import { Constellations } from "./components/StarMap/Constellations"; function CompassUpdater({ compassRef }) { const { camera } = useThree(); useFrame(() => { if (compassRef.current) { const azimuth = Math.atan2(camera.position.x, camera.position.z); const rotationDeg = azimuth * (180 / Math.PI); compassRef.current.style.transform = `rotate(${rotationDeg}deg)`; } }); return null; } function ContextMenu({ visible, x, y, uiHidden, onToggleUI, onFullScreen, onRefresh, onClose, }) { if (!visible) return null; const style = { position: "absolute", top: y, left: x, zIndex: 1000, background: "rgba(20, 20, 30, 0.95)", backdropFilter: "blur(10px)", border: "1px solid rgba(255, 255, 255, 0.1)", borderRadius: "8px", padding: "5px 0", minWidth: "180px", boxShadow: "0 8px 32px rgba(0,0,0,0.5)", color: "white", fontSize: "0.9rem", userSelect: "none", }; const itemStyle = { padding: "10px 15px", cursor: "pointer", display: "flex", alignItems: "center", gap: "10px", transition: "background 0.2s", }; const hoverBg = (e) => (e.target.style.background = "rgba(255,255,255,0.1)"); const leaveBg = (e) => (e.target.style.background = "transparent"); return (
{uiHidden ? "Show UI Components" : "Hide UI components"}
Toggle Full Screen
Hard Reset
); } function App() { const [skyData, setSkyData] = useState(null); const [loading, setLoading] = useState(false); const [params, setParams] = useState({ latitude: 27.7172, longitude: 85.324, time: new Date().toISOString().slice(0, 16), locationName: "Kathmandu, Nepal", starCount: 0, }); const [fov, setFov] = useState(60); const compassRef = useRef(null); const [selectedConstellation, setSelectedConstellation] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const controlsRef = useRef(); const [showUI, setShowUI] = useState(true); const [menuState, setMenuState] = useState({ visible: false, x: 0, y: 0 }); const [viewSettings, setViewSettings] = useState({ showGrid: true, showStars: true, showPlanets: true, showPlanetLabels: false, showConstellations: true, showTier1: true, showTier2: true, showTier3: false, showConstellationLabels: false, showDSOs: true, showDSOLabels: false, showMilkyWay: true, showMeteors: true, }); const fetchSky = async () => { if (loading) return; setLoading(true); try { const isoTime = new Date(params.time).toISOString(); const data = await getSkyData( parseFloat(params.latitude), parseFloat(params.longitude), isoTime, ); setSkyData(data); setParams((prev) => ({ ...prev, starCount: data.star_count })); } catch (err) { console.error(err); } finally { setLoading(false); } }; useEffect(() => { if (params.locationName) fetchSky(); }, [params.locationName]); useEffect(() => { const timer = setTimeout(() => fetchSky(), 50); return () => clearTimeout(timer); }, [params.time]); const handleContextMenu = useCallback((e) => { e.preventDefault(); setMenuState({ visible: true, x: e.clientX, y: e.clientY }); }, []); const closeMenu = useCallback( () => setMenuState({ ...menuState, visible: false }), [menuState], ); const toggleFullScreen = () => { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch((err) => { console.log( `Error attempting to enable full-screen mode: ${err.message}`, ); }); } else { document.exitFullscreen(); } closeMenu(); }; const handleHardRefresh = () => { if ( window.confirm("This will clear all settings and reload. Are you sure?") ) { localStorage.clear(); sessionStorage.clear(); document.cookie.split(";").forEach((c) => { document.cookie = c .replace(/^ +/, "") .replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); }); window.location.reload(true); } closeMenu(); }; const handleTimeChange = useCallback( (newTimeStr) => setParams((prev) => ({ ...prev, time: newTimeStr })), [], ); const handleReset = useCallback(() => { setFov(60); if (controlsRef.current) controlsRef.current.reset(); }, []); const handleWheel = (e) => { setFov((prev) => Math.max(10, Math.min(100, prev + e.deltaY * 0.05))); }; const handleObjectSearch = useCallback( (name) => { if (!skyData || !controlsRef.current) return; const query = name.toLowerCase(); const constellation = CONSTELLATIONS.find( (c) => c.name.toLowerCase() === query, ); if (constellation) { const idx = skyData.stars.ids.indexOf(constellation.anchor); if (idx > -1) moveCamera(skyData.stars.altitude[idx], skyData.stars.azimuth[idx]); setSelectedConstellation(constellation); return; } const star = MAJOR_STARS.find((s) => s.name.toLowerCase() === query); if (star) { const idx = skyData.stars.ids.indexOf(star.id); if (idx > -1) moveCamera(skyData.stars.altitude[idx], skyData.stars.azimuth[idx]); return; } alert("Object not found or below horizon."); }, [skyData], ); const moveCamera = (alt, az) => { const theta = az * (Math.PI / 180) + Math.PI; const phi = (90 - alt) * (Math.PI / 180); controlsRef.current.setAzimuthalAngle(theta); controlsRef.current.setPolarAngle(phi); controlsRef.current.update(); }; return (
{ setShowUI(!showUI); closeMenu(); }} onFullScreen={toggleFullScreen} onRefresh={handleHardRefresh} onClose={closeMenu} /> {showUI && ( <>

FOV: {Math.round(fov)}° (Scroll to Zoom)

N
S
setSelectedConstellation(null)} /> )} {skyData && } {skyData && skyData.milkyway && ( )} {skyData && skyData.planets && ( )} {skyData && ( )} {skyData && skyData.dsos && ( )} {viewSettings.showMeteors && }
); } export default App; function CameraUpdater({ fov }) { const { camera } = useThree(); useEffect(() => { camera.fov = fov; camera.updateProjectionMatrix(); }, [fov, camera]); return null; }