From b27ab584a2e3d35ad7c3899cb9b20584f96a447f Mon Sep 17 00:00:00 2001 From: EliasKnudsen <38568225+EliasKnudsen@users.noreply.github.com> Date: Thu, 4 Apr 2024 12:39:43 +0200 Subject: [PATCH] 120 style front page to align with figma design (#201) * feat(frontend): :sparkles: responsive, camera follows node, no interaction enables on globe, select satellite on dropdown #120 * frontpage #120 * Change satellitedatatable name #120 * Prettier and lint #120 * Lint and prettier #120 --- frontend/.env.production | 2 +- frontend/package-lock.json | 290 ++++++++++++++++++ frontend/package.json | 2 + frontend/src/app/page.tsx | 16 +- frontend/src/components/Combobox.tsx | 80 +++++ frontend/src/components/map/MyGlobe.tsx | 68 ++-- .../src/components/map/SatelliteFetcher.tsx | 23 +- .../SatelliteDataTableMultiple.tsx | 140 +++++++++ frontend/src/components/ui/command.tsx | 158 ++++++++++ frontend/src/components/ui/dialog.tsx | 122 ++++++++ frontend/src/components/ui/popover.tsx | 31 ++ frontend/src/lib/store.ts | 7 + 12 files changed, 883 insertions(+), 56 deletions(-) create mode 100644 frontend/src/components/Combobox.tsx create mode 100644 frontend/src/components/satelliteData/SatelliteDataTableMultiple.tsx create mode 100644 frontend/src/components/ui/command.tsx create mode 100644 frontend/src/components/ui/dialog.tsx create mode 100644 frontend/src/components/ui/popover.tsx diff --git a/frontend/.env.production b/frontend/.env.production index ba9e7ab..e75b046 100644 --- a/frontend/.env.production +++ b/frontend/.env.production @@ -1,2 +1,2 @@ # Database url -HOST_URL=http://backend-app:1337 +HOST_URL=http://localhost:1337 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c4420ad..c4570c6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@strapi/blocks-react-renderer": "^1.0.0", @@ -23,6 +24,7 @@ "chartjs-adapter-luxon": "^1.3.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "cmdk": "0.2.0", "framer-motion": "^11.0.24", "globe.gl": "^2.32.2", "gsap": "^3.12.5", @@ -4241,6 +4243,43 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz", + "integrity": "sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", @@ -7668,6 +7707,252 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-0.2.0.tgz", + "integrity": "sha512-JQpKvEOb86SnvMZbYaFKYhvzFntWBeSZdyii0rZPhKJj9uwJBxu4DaVYDrRN7r3mPop56oPhRw+JYWTKs66TYw==", + "dependencies": { + "@radix-ui/react-dialog": "1.0.0", + "command-score": "0.1.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.0.tgz", + "integrity": "sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.0", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-portal": "1.0.0", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-slot": "1.0.0", + "@radix-ui/react-use-controllable-state": "1.0.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.4" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz", + "integrity": "sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-escape-keydown": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz", + "integrity": "sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.0.tgz", + "integrity": "sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.0", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", + "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-portal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.0.tgz", + "integrity": "sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-presence": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz", + "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", + "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-slot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", + "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz", + "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.0.tgz", + "integrity": "sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/cmdk/node_modules/react-remove-scroll": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.4.tgz", + "integrity": "sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/code-block-writer": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-10.1.1.tgz", @@ -7795,6 +8080,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/command-score": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/command-score/-/command-score-0.1.2.tgz", + "integrity": "sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w==" + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index cef4828..17c961c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@strapi/blocks-react-renderer": "^1.0.0", @@ -28,6 +29,7 @@ "chartjs-adapter-luxon": "^1.3.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "cmdk": "0.2.0", "framer-motion": "^11.0.24", "globe.gl": "^2.32.2", "gsap": "^3.12.5", diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 5aa6866..1da071b 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -3,9 +3,8 @@ import ColoredSection from "@/components/ui/coloredSection"; import Image from "next/image"; import Link from "next/link"; - -import SatelliteFetcher from "@/components/map/SatelliteFetcher"; -import SatelliteDataTable from "@/components/satelliteData/SatelliteDataTable"; +import SatelliteDataTable from "@/components/satelliteData/SatelliteDataTableMultiple"; +import fetchSatelliteData from "@components/map/SatelliteFetcher"; import fetchMostRecentImage from "@/lib/data/fetchMostRecentImage"; @@ -16,15 +15,10 @@ export default async function Home() { <>
- - - - - - +
- -
void; +} + +export function Combobox({ data, onSelect }: ComboboxDemoProps) { + console.log(); + + const [open, setOpen] = useState(false); + const [value, setValue] = useState(""); + + const handleSelect = (currentValue: SatelliteData) => { + setValue(currentValue.name === value ? "" : currentValue.name); + setOpen(false); + onSelect(currentValue); // Call the passed onSelect function with the selected value + }; + + return ( + + + + + + + + No framework found. + + {data.map((item) => ( + handleSelect(item)} + > + + {item.name} + + ))} + + + + + ); +} diff --git a/frontend/src/components/map/MyGlobe.tsx b/frontend/src/components/map/MyGlobe.tsx index 5f59312..9859cb0 100644 --- a/frontend/src/components/map/MyGlobe.tsx +++ b/frontend/src/components/map/MyGlobe.tsx @@ -3,6 +3,7 @@ import Globe, { GlobeInstance } from "globe.gl"; import * as THREE from "three"; import React, { useEffect } from "react"; import * as satellite from "satellite.js"; +import { SatelliteData } from "@/lib/mapHelpers"; // made with the following packages: // https://www.npmjs.com/package/globe.gl @@ -14,9 +15,9 @@ import * as satellite from "satellite.js"; const EARTH_RADIUS_KM = 6371; // km const SAT_SIZE = 500; // km const TIME_STEP = 1 * 1000; // per frame -const SATELLITE_AMOUNT = 100; // amount of satellites to display +//const SATELLITE_AMOUNT = 100; // amount of satellites to display -function mapRawDataToTleData(rawData: string): string[][] { +export function mapRawDataToTleData(rawData: string): string[][] { return ( rawData // Remove any carriage returns @@ -34,11 +35,15 @@ function mapRawDataToTleData(rawData: string): string[][] { ); } +interface MyGlobeProps { + satelliteDatas: SatelliteData[]; // Existing prop: a string of TLE strings + selectedSatellite?: SatelliteData; +} + export default function MyGlobe({ - satelliteDatas: satelliteDatas, -}: { - satelliteDatas: string; // Expects a string of TLE strings such as the example files in the datasets folder -}) { + satelliteDatas, + selectedSatellite, +}: MyGlobeProps) { const chart = React.useRef(null); // useEffect is used because we want to run the code only once when the component is mounted @@ -53,21 +58,26 @@ export default function MyGlobe({ .objectLng("lng") .objectAltitude("alt") .objectFacesSurface(false) - .objectLabel("name") .backgroundColor("rgba(0,0,0,0)") - .width(window.innerWidth / 2); - - window.addEventListener("resize", (event) => { - let target = event.target as Window; - if (target.innerWidth != null && target.innerHeight != null) { - myGlobe.width(target.innerWidth / 2); - // myGlobe.height(event.target.innerHeight / 2); - } - }); + .objectLabel("name"); // Set initial camera distance setTimeout(() => myGlobe.pointOfView({ altitude: 3.5 })); + const handleResize = () => { + //Making it responsive like this + + if (window.innerWidth <= 768) { + myGlobe.width(window.innerWidth); + myGlobe.height(window.innerHeight / 2); + } else { + myGlobe.width(window.innerWidth / 2); + myGlobe.height(window.innerHeight / 2); + } + }; + handleResize(); + window.addEventListener("resize", handleResize); + // Disable OrbitControls and enable auto-rotation // myGlobe.controls().autoRotate = true; myGlobe.controls().enabled = true; @@ -92,20 +102,7 @@ export default function MyGlobe({ myGlobe.objectThreeObject( () => new THREE.Mesh(satGeometry, satMaterial), ); - - const tleData = mapRawDataToTleData(satelliteDatas); - const satData = tleData - .map(([name, ...tle]) => ({ - satrec: satellite.twoline2satrec( - ...(tle as [string, string]), // spread the array as arguments to the function - ), - name: name.trim().replace(/^0 /, ""), // remove leading 0 from name - })) - // exclude those that can't be propagated - .filter( - (d) => !!satellite.propagate(d.satrec, new Date()).position, - ) - .slice(0, SATELLITE_AMOUNT); + const satData = satelliteDatas; // time ticker let time = new Date(); @@ -115,6 +112,7 @@ export default function MyGlobe({ time = new Date(+time + TIME_STEP); // Update satellite positions + const gmst = satellite.gstime(time); satData.forEach((d: any) => { const eci = satellite.propagate(d.satrec, time); @@ -123,16 +121,24 @@ export default function MyGlobe({ eci.position as satellite.EciVec3, gmst, ); + d.lat = THREE.MathUtils.radToDeg(gdPos.latitude); d.lng = THREE.MathUtils.radToDeg(gdPos.longitude); d.alt = gdPos.height / EARTH_RADIUS_KM; + + if (d.name == selectedSatellite?.name) { + myGlobe.pointOfView( + { lat: d.lat, lng: d.lng, altitude: 2 }, + 0, + ); + } } }); myGlobe.objectsData(satData); })(); } - }, [satelliteDatas]); + }, [selectedSatellite, satelliteDatas]); return ( <> diff --git a/frontend/src/components/map/SatelliteFetcher.tsx b/frontend/src/components/map/SatelliteFetcher.tsx index 6fc4209..d33c297 100644 --- a/frontend/src/components/map/SatelliteFetcher.tsx +++ b/frontend/src/components/map/SatelliteFetcher.tsx @@ -1,12 +1,8 @@ -export const runtime = "edge"; +"use server"; import { gql } from "@/__generated__/gql"; import { getClient } from "@/lib/ApolloClient"; // Dynamic import because of leaflet and globe.gl ssr problem with next.js -import dynamic from "next/dynamic"; -const MyGlobe = dynamic(() => import("@/components/map/MyGlobe"), { - ssr: false, -}); // Example Datasources import { exampleData } from "./exampleSatData"; @@ -28,16 +24,17 @@ const GET_ALL_SATELLITE_DATA = } `); -export default async function SatelliteFetcher({ - useExampleData, - filterList = [], -}: { +interface SatelliteFetcherInterface { useExampleData: boolean; filterList?: string[]; -}) { - // Fetch the data, either from the example file or from strapi then celestrak +} + +export default async function fetchSatelliteData({ + useExampleData, + filterList = [], +}: SatelliteFetcherInterface): Promise { if (useExampleData) { - return ; + return exampleData; } else { let graphqlData; @@ -96,6 +93,6 @@ export default async function SatelliteFetcher({ let combinedSatelliteDatas = data.join("\n"); - return ; + return combinedSatelliteDatas; } } diff --git a/frontend/src/components/satelliteData/SatelliteDataTableMultiple.tsx b/frontend/src/components/satelliteData/SatelliteDataTableMultiple.tsx new file mode 100644 index 0000000..7074730 --- /dev/null +++ b/frontend/src/components/satelliteData/SatelliteDataTableMultiple.tsx @@ -0,0 +1,140 @@ +// Ensure all necessary imports are present +"use client"; +import React, { useState, useEffect } from "react"; +import dynamic from "next/dynamic"; +import { Combobox } from "../Combobox"; // Adjust the path as necessary +import { + mapRawDataToTleData, + mapTleToSatData, + SatelliteData, +} from "@/lib/mapHelpers"; +import { convertSatrec, SatelliteInfo } from "@/lib/convertSatrec"; + +// Define the MyGlobe component with dynamic import +const MyGlobe = dynamic(() => import("@/components/map/MyGlobe"), { + ssr: false, +}); +const updateInterval = 10; + +interface ClientOnlyComponentProps { + fetchSatelliteData: ({ + // eslint-disable-next-line no-unused-vars + useExampleData, + }: { + useExampleData: boolean; + filterList?: string[]; + }) => Promise; +} + +const SatelliteDataTableMultiple: React.FC = ({ + fetchSatelliteData, +}) => { + const [satelliteData, setSatelliteData] = useState([]); + const [selectedSatellite, setSelectedSatellite] = useState< + SatelliteData | undefined + >(); + const [satelliteInfo, setSatelliteInfo] = useState( + null, + ); + + // Fetch satellite data on component mount + useEffect(() => { + const fetchData = async () => { + const rawData = await fetchSatelliteData({ useExampleData: true }); + + const tleData = mapRawDataToTleData(rawData); + + const mappedData = mapTleToSatData(tleData).slice(0, 10); + setSatelliteData(mappedData); + if (mappedData.length > 0) { + updateSatelliteInfo(mappedData[0]); + setSelectedSatellite(mappedData[0]); // Ensure the first satellite is selected by default + } + }; + + fetchData(); + }, [fetchSatelliteData]); + + // Function to update satellite info based on selected satellite + const updateSatelliteInfo = (satellite: SatelliteData) => { + const sat = satelliteData.find((s) => s.name === satellite.name); + if (sat) { + const info = convertSatrec(sat.satrec, sat.name); + setSatelliteInfo(info); + } + }; + + // Handle satellite selection from Combobox + const handleSelectSatellite = (value: SatelliteData) => { + setSelectedSatellite(value); + updateSatelliteInfo(value); + }; + + // Map satellite data for Combobox options + + useEffect(() => { + const intervalId = setInterval(() => { + // Access satellite data by name + if (selectedSatellite) { + const updatedInfo = convertSatrec( + selectedSatellite.satrec, + selectedSatellite.name, + ); + setSatelliteInfo(updatedInfo); + } + }, updateInterval); + + // Clear interval on component unmount + return () => clearInterval(intervalId); + }, [selectedSatellite]); + + return satelliteData.length > 0 && selectedSatellite && satelliteInfo ? ( + <> +
+
+ +
{/* Include the dropdown arrow icon here */}
+
+ +
+
+

{satelliteInfo.velocity} km/s

+

Velocity

+
+
+

{satelliteInfo.altitude} km

+

Altitude

+
+
+

+ {satelliteInfo.latitudeDeg}° N +

+

Latitude

+
+
+

+ {satelliteInfo.longitudeDeg}° E +

+

Longitude

+
+
+ +
+

Above {satelliteInfo.country}

+
+
{/* Include the flag icon here */}
+
+ + + ) : ( +
Loading...
+ ); +}; + +export default SatelliteDataTableMultiple; diff --git a/frontend/src/components/ui/command.tsx b/frontend/src/components/ui/command.tsx new file mode 100644 index 0000000..01543eb --- /dev/null +++ b/frontend/src/components/ui/command.tsx @@ -0,0 +1,158 @@ +"use client"; + +import * as React from "react"; +import { type DialogProps } from "@radix-ui/react-dialog"; +import { Command as CommandPrimitive } from "cmdk"; +import { Search } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..47bd7e3 --- /dev/null +++ b/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client"; + +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/frontend/src/components/ui/popover.tsx b/frontend/src/components/ui/popover.tsx new file mode 100644 index 0000000..f2de260 --- /dev/null +++ b/frontend/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +"use client"; + +import * as React from "react"; +import * as PopoverPrimitive from "@radix-ui/react-popover"; + +import { cn } from "@/lib/utils"; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent }; diff --git a/frontend/src/lib/store.ts b/frontend/src/lib/store.ts index 0161b2c..130e774 100644 --- a/frontend/src/lib/store.ts +++ b/frontend/src/lib/store.ts @@ -5,12 +5,17 @@ import { satLoader } from "@/lib/getSatelliteData"; interface SatelliteStore { satelliteData: Record; + satelliteNames: string[]; + setSatelliteData: (satName: string, data: SatelliteData) => void; fetchAndSetSatelliteData: (satName: string) => Promise; + + getSatelliteNames: () => void; } export const useSatelliteStore = create((set) => ({ satelliteData: {}, + satelliteNames: [], setSatelliteData: (satName, data) => set((state) => ({ satelliteData: { ...state.satelliteData, [satName]: data }, @@ -22,4 +27,6 @@ export const useSatelliteStore = create((set) => ({ satelliteData: { ...state.satelliteData, [satName]: newData }, })); }, + + getSatelliteNames: () => {}, }));