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)
setSelectedConstellation(null)}
/>
>
)}
);
}
export default App;
function CameraUpdater({ fov }) {
const { camera } = useThree();
useEffect(() => {
camera.fov = fov;
camera.updateProjectionMatrix();
}, [fov, camera]);
return null;
}