diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 8dc33d1..8ee1d01 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -34,8 +34,16 @@ export default async function RootLayout({ if (satellites) { for (const sat of satellites) { if (sat.id) { - const data = await satLoaderById(sat.id); - satData.push({ name: sat.name, id: sat.id, data }); + try { + const data = await satLoaderById(sat.id); + satData.push({ name: sat.name, id: sat.id, data }); + } catch (e) { + console.error( + "Either CelesTrak has IP banned the server, or the satellite data is not available for the provided NORAD ID: " + + sat.id + + ", or CelesTrak is down.", + ); + } } } } diff --git a/frontend/src/components/homeComponents/SatDropdown.tsx b/frontend/src/components/homeComponents/SatDropdown.tsx index bd6a0bd..98af400 100644 --- a/frontend/src/components/homeComponents/SatDropdown.tsx +++ b/frontend/src/components/homeComponents/SatDropdown.tsx @@ -1,29 +1,93 @@ +"use client"; import React, { useState } from "react"; import { motion } from "framer-motion"; import { cn } from "@/lib/utils"; +import { satLoaderById } from "@/lib/getSatelliteData"; type DropdownProps = { satelliteNames: string[]; selectedSatellite: string; // eslint-disable-next-line no-unused-vars setSelectedSatellite: (satellite: string) => void; + // eslint-disable-next-line no-unused-vars + setSatellites: (satellites: any) => void; }; export default function SatDropdown({ satelliteNames, selectedSatellite, setSelectedSatellite, + setSatellites, }: DropdownProps) { const [isOpen, setIsOpen] = useState(false); + const [noradID, setNoradID] = useState(""); + const [error, setError] = useState(""); - const toggleDropdown = () => setIsOpen(!isOpen); + const toggleDropdown = () => { + setError(""); + setIsOpen(!isOpen); + }; const handleSelect = (satellite: string) => { setSelectedSatellite(satellite); setIsOpen(false); }; - // Animation variants for the dropdown content + const handleAddSatellite = async (noradID: string) => { + if (!noradID) { + setError("Please enter a valid NORAD ID."); + return; + } + + try { + const data = await satLoaderById(noradID); + if (data) { + const newSatellite = { + name: data.name, + id: noradID, + data: data, + }; + setSatellites([newSatellite]); + setSelectedSatellite(newSatellite.name); + setError(""); + } else { + throw new Error("No data returned for the provided NORAD ID."); + } + } catch (e) { + console.error( + "Failed to fetch satellite data for NORAD ID:", + noradID, + "\n", + (e as Error).message, + ); + if ( + (e as Error).message === + "403 - Forbidden: Access is denied. You are likely IP banned temporarily for making too many requests." + ) { + setError( + "403 - Forbidden: Access is denied. You are likely IP banned temporarily for making too many requests.", + ); + return; + } else { + setError(`Satellite with NORAD ID ${noradID} does not exist.`); + } + } + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + handleAddSatellite(noradID); + } + }; + + const handleInputChange = (event: React.ChangeEvent) => { + const value = event.target.value; + // Allow only numeric input + if (/^\d*$/.test(value)) { + setNoradID(value); + } + }; + const variants = { open: { opacity: 1, height: "auto", maxHeight: "250px" }, collapsed: { opacity: 0, height: 0 }, @@ -65,6 +129,39 @@ export default function SatDropdown({ : `${satellite} (Selected)`} ))} +
+
+ + + + + + +
+ +
+ + {error &&
{error}
} ); diff --git a/frontend/src/components/homeComponents/SatelliteSelector.tsx b/frontend/src/components/homeComponents/SatelliteSelector.tsx index 8a33ce0..eeb4f20 100644 --- a/frontend/src/components/homeComponents/SatelliteSelector.tsx +++ b/frontend/src/components/homeComponents/SatelliteSelector.tsx @@ -11,6 +11,7 @@ export default function SatelliteSelector() { const setSelectedSatellite = useSatelliteStore( (state) => state.setSelectedSatellite, ); + const setSatellites = useSatelliteStore((state) => state.setSatellites); return (
@@ -18,6 +19,7 @@ export default function SatelliteSelector() { satelliteNames={satelliteNames} selectedSatellite={selectedSatellite} setSelectedSatellite={setSelectedSatellite} + setSatellites={setSatellites} />
); diff --git a/frontend/src/components/satelliteData/SatelliteInitialFetch.tsx b/frontend/src/components/satelliteData/SatelliteInitialFetch.tsx index ad54566..4878dfb 100644 --- a/frontend/src/components/satelliteData/SatelliteInitialFetch.tsx +++ b/frontend/src/components/satelliteData/SatelliteInitialFetch.tsx @@ -3,27 +3,26 @@ import { useSatelliteStore } from "@/lib/store"; import { useEffect } from "react"; interface SatelliteInitialFetchProps { - satData: { name: string; id: string; data: any }[]; + satData: { name: string; id: string; data: any; selected?: boolean }[]; // Adjusted to possibly include 'selected' } export default function SatelliteInitialFetch({ satData, }: SatelliteInitialFetchProps) { const setSatellites = useSatelliteStore((state) => state.setSatellites); - const setSatelliteData = useSatelliteStore( - (state) => state.setSatelliteData, - ); useEffect(() => { - // Set the satellite data in the store - setSatellites(satData); - satData.forEach((sat) => { - if (!sat.data) return; - setSatelliteData(sat.name, sat.data); - }); + // Convert incoming data to the expected format by the store + const satellites = satData.map((sat) => ({ + name: sat.name, + id: sat.id, + data: sat.data, // Assuming data is optional and handled correctly by setSatellites + selected: sat.selected, // Optional, handle as boolean; ensure it's true for exactly one sat or none + })); - console.log("Satellite data fetched and set in store", satData); - }, [satData]); + // Set the satellite data in the store with potential initial data and selected satellite + setSatellites(satellites); + }, [satData, setSatellites]); - return <>; + return <>; // The component doesn't render anything } diff --git a/frontend/src/components/satelliteData/SatelliteStatsTableRow.tsx b/frontend/src/components/satelliteData/SatelliteStatsTableRow.tsx index 47e8ad0..986c6f6 100644 --- a/frontend/src/components/satelliteData/SatelliteStatsTableRow.tsx +++ b/frontend/src/components/satelliteData/SatelliteStatsTableRow.tsx @@ -36,7 +36,12 @@ export default function SatelliteStatsTableRow({ // Display loading message if satellite info is not available if (!satelliteInfo) { return ( - + { + handleRowClick(); + }} + > {satName} Loading... diff --git a/frontend/src/lib/getSatelliteData.ts b/frontend/src/lib/getSatelliteData.ts index bab7ac4..fd20a9e 100644 --- a/frontend/src/lib/getSatelliteData.ts +++ b/frontend/src/lib/getSatelliteData.ts @@ -1,6 +1,5 @@ import { twoline2satrec } from "satellite.js"; import { SatRec } from "satellite.js"; -import { exampleData } from "@/components/satelliteData/exampleSatData"; // Satellite data interface interface SatelliteData { @@ -33,28 +32,7 @@ function mapTleToSatData(tleString: string): SatelliteData[] { return satellites; } -// Fetch satellite data from Celestrak by satellite name -// eslint-disable-next-line no-unused-vars -async function fetchSatelliteData(satName: string): Promise { - const response = await fetch( - `https://celestrak.org/NORAD/elements/gp.php?NAME=${satName}&FORMAT=TLE`, - { - next: { - revalidate: 60 * 60 * 24, // revalidate every 24 hours - }, - }, - ); - if (!response.ok) { - throw new Error( - `Failed to fetch satellite data: ${response.statusText}`, - ); - } - const data = await response.text(); - return mapTleToSatData(data); -} - // fetch satellite data from celestrak by id -// eslint-disable-next-line no-unused-vars async function fetchSatelliteDataById(satId: string): Promise { const response = await fetch( `https://celestrak.org/NORAD/elements/gp.php?CATNR=${satId}&FORMAT=TLE`, @@ -64,6 +42,11 @@ async function fetchSatelliteDataById(satId: string): Promise { }, }, ); + if (response.status === 403) { + throw new Error( + "403 - Forbidden: Access is denied. You are likely IP banned temporarily for making too many requests.", + ); + } if (!response.ok) { throw new Error( `Failed to fetch satellite data from celestrak: ${response.statusText}`, @@ -79,28 +62,7 @@ function isStale(timestamp: Date): boolean { return now.getTime() - timestamp.getTime() > 24 * 60 * 60 * 1000; } -export async function satLoader(satName: string): Promise { - // The logic to check if data is stale and needs to be fetched - if ( - !cachedData || - isStale(cachedData.timestamp) || - !(satName in cachedData.data) - ) { - // Fetch the data and update the cache - // const newDataArray = await fetchSatelliteData(satName); - const newDataArray = mapTleToSatData(exampleData); - const satExample = newDataArray.find((sat) => sat.name == satName); - const newData = satExample || newDataArray[0]; - - cachedData = { - data: { ...cachedData.data, [satName]: newData }, - timestamp: new Date(), - }; - } - - return cachedData.data[satName]; -} - +// Load satellite data by id export async function satLoaderById(satId: string): Promise { // The logic to check if data is stale and needs to be fetched if ( diff --git a/frontend/src/lib/store.ts b/frontend/src/lib/store.ts index cd20361..9b8cbc9 100644 --- a/frontend/src/lib/store.ts +++ b/frontend/src/lib/store.ts @@ -1,15 +1,15 @@ /* eslint-disable no-unused-vars */ import { create } from "zustand"; import type { SatelliteData } from "@/lib/getSatelliteData"; -import { satLoaderById } from "@/lib/getSatelliteData"; // Satellite entry for setSatellites interface SatelliteEntry { name: string; id: string; + data?: SatelliteData; } -// Define the state and actions separately +// Define the state interface SatelliteState { satelliteData: Record; satelliteNameToId: Record; @@ -17,9 +17,9 @@ interface SatelliteState { selectedSatellite: string; } +// Define the actions interface SatelliteActions { setSatelliteData: (satName: string, data: SatelliteData) => void; - fetchAndSetSatelliteData: (satName: string) => Promise; setSelectedSatellite: (satName: string) => void; setSatellites: (satellites: SatelliteEntry[]) => void; } @@ -27,46 +27,65 @@ interface SatelliteActions { type SatelliteStore = SatelliteState & SatelliteActions; // Create satellite store -export const useSatelliteStore = create((set, get) => ({ +export const useSatelliteStore = create((set) => ({ satelliteData: {}, satelliteNames: [], satelliteNameToId: {}, selectedSatellite: "", - fetchAndSetSatelliteData: async (satName) => { - const satId = get().satelliteNameToId[satName]; - const newData = await satLoaderById(satId); - set((state) => ({ - satelliteData: { ...state.satelliteData, [satName]: newData }, - })); + // Set the satellite names and id mapping, and selected satellite + setSatellites: (satellites) => { + set((state) => { + const newNames = satellites.map((sat) => sat.name); + const newNameToId = satellites.reduce>( + (acc, sat) => { + acc[sat.name] = sat.id; + return acc; + }, + {}, + ); + + const newSatelliteData = satellites.reduce< + Record + >( + (acc, sat) => { + if (sat.data) { + acc[sat.name] = sat.data; + } + return acc; + }, + { ...state.satelliteData }, + ); + + const mergedNames = Array.from( + new Set([...state.satelliteNames, ...newNames]), + ); + const mergedNameToId = { + ...state.satelliteNameToId, + ...newNameToId, + }; + const selectedSatellite = state.selectedSatellite || newNames[0]; + + return { + satelliteNames: mergedNames, + satelliteNameToId: mergedNameToId, + satelliteData: newSatelliteData, + selectedSatellite: selectedSatellite, + }; + }); }, + // Set the satellite data for a specific satellite setSatelliteData: (satName, data) => { set((state) => ({ satelliteData: { ...state.satelliteData, [satName]: data }, })); }, + // Set the selected satellite setSelectedSatellite: (satName) => { set(() => ({ selectedSatellite: satName, })); }, - - setSatellites: (satellites) => { - const names = satellites.map((sat) => sat.name); - const nameToId = satellites.reduce>( - (acc, sat) => { - acc[sat.name] = sat.id; - return acc; - }, - {}, - ); - - set(() => ({ - satelliteNames: names, - satelliteNameToId: nameToId, - selectedSatellite: names[0] || "", - })); - }, }));