diff --git a/frontend/package.json b/frontend/package.json index 45a7a15..ab91f44 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,7 +19,6 @@ "dependencies": { "@apollo/client": "^3.9.0-alpha.5", "@apollo/experimental-nextjs-app-support": "^0.7.0", - "@gsap/react": "^2.1.0", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -47,7 +46,6 @@ "framer-motion": "^11.0.24", "globe.gl": "^2.32.2", "gql.tada": "^1.6.2", - "gsap": "^3.12.5", "lucide-react": "^0.314.0", "luxon": "^3.4.4", "next": "14.1.0", diff --git a/frontend/src/app/_homeComponents/FeaturedImage.tsx b/frontend/src/app/_homeComponents/FeaturedImage.tsx index 4a54d6f..55b63f5 100644 --- a/frontend/src/app/_homeComponents/FeaturedImage.tsx +++ b/frontend/src/app/_homeComponents/FeaturedImage.tsx @@ -5,34 +5,10 @@ import { getClient } from "@/lib/ApolloClient"; const STRAPI_URL = process.env.BACKEND_INTERNAL_URL; -const GET_FEATURED_IMAGE = graphql(` - query FeaturedImage { - featuredImage { - data { - attributes { - description - featuredImage { - data { - attributes { - url - } - } - } - satellite { - data { - attributes { - name - slug - } - } - } - title - } - } - } - } -`); - +/** + * Retrieves the featured image data from the GraphQL API and renders it on the page. + * @returns The JSX element representing the featured image component. + */ export default async function featuredImage() { const graphqlData = await getClient().query({ query: GET_FEATURED_IMAGE, @@ -85,3 +61,31 @@ export default async function featuredImage() { ); } + +const GET_FEATURED_IMAGE = graphql(` + query FeaturedImage { + featuredImage { + data { + attributes { + description + featuredImage { + data { + attributes { + url + } + } + } + satellite { + data { + attributes { + name + slug + } + } + } + title + } + } + } + } +`); diff --git a/frontend/src/app/_homeComponents/FeaturedProjectCard.tsx b/frontend/src/app/_homeComponents/FeaturedProjectCard.tsx deleted file mode 100644 index afea07a..0000000 --- a/frontend/src/app/_homeComponents/FeaturedProjectCard.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import Link from "next/link"; -import Image from "next/image"; -import { PlaceholderImage } from "@/components/shared/CardWithContent"; - -const STRAPI_URL = process.env.BACKEND_INTERNAL_URL; - -export default function FeaturedProjectCard({ - title, - imageURL, - projectSlug, -}: { - title: string; - imageURL?: string; - projectSlug?: string; - key: string; -}) { - return ( - -
-
- {imageURL ? ( - Project Image - ) : ( -
- -
- )} -
-
-

{title}

-
-
- - ); -} diff --git a/frontend/src/app/_homeComponents/FeaturedProjects.tsx b/frontend/src/app/_homeComponents/FeaturedProjects.tsx index e7966b6..11af6d0 100644 --- a/frontend/src/app/_homeComponents/FeaturedProjects.tsx +++ b/frontend/src/app/_homeComponents/FeaturedProjects.tsx @@ -1,11 +1,15 @@ -import fetchFeaturedProjects from "@/lib/data/fetchFeaturedProjects"; import Link from "next/link"; import { Button } from "@components/shadcn/button"; import CardWithContent from "@components/shared/CardWithContent"; import { PagePaddingOnlyHorizontal } from "@/components/layout/PageLayout"; +import { graphql } from "@/lib/tada/graphql"; +import { getClient } from "@lib/ApolloClient"; const STRAPI_URL = process.env.BACKEND_INTERNAL_URL; +/** + * Displays the title and text content from Strapi, alongside three featured projects as cards. + */ export default async function FeaturedProjects() { const featuredProjects = await fetchFeaturedProjects(); @@ -69,3 +73,70 @@ export default async function FeaturedProjects() { ); } + +const GET_FEATURED_PROJECTS = graphql(` + query HomeFeaturedProjects { + homeFeaturedProjects { + data { + attributes { + title + textContent + featuredProject1 { + data { + attributes { + title + previewImage { + data { + attributes { + url + } + } + } + slug + } + } + } + featuredProject2 { + data { + attributes { + title + previewImage { + data { + attributes { + url + } + } + } + slug + } + } + } + featuredProject3 { + data { + attributes { + title + previewImage { + data { + attributes { + url + } + } + } + slug + } + } + } + } + } + } + } +`); + +async function fetchFeaturedProjects() { + const client = getClient(); + const { data } = await client.query({ + query: GET_FEATURED_PROJECTS, + }); + + return data.homeFeaturedProjects?.data?.attributes; +} diff --git a/frontend/src/app/_homeComponents/GlobeWithStats.tsx b/frontend/src/app/_homeComponents/GlobeWithStats.tsx index fbbeb2c..cd5a582 100644 --- a/frontend/src/app/_homeComponents/GlobeWithStats.tsx +++ b/frontend/src/app/_homeComponents/GlobeWithStats.tsx @@ -7,6 +7,10 @@ const SatelliteGlobeNoSSR = dynamic(() => import("./SatelliteGlobe"), { ssr: false, }); +/** + * Renders a 3D Globe with statistics next to it. + * Allows the user to select a satellite to view. + */ export default function GlobeWithStats() { return ( <> diff --git a/frontend/src/components/ui/hero.tsx b/frontend/src/app/_homeComponents/Hero.tsx similarity index 90% rename from frontend/src/components/ui/hero.tsx rename to frontend/src/app/_homeComponents/Hero.tsx index d606918..e50d309 100644 --- a/frontend/src/components/ui/hero.tsx +++ b/frontend/src/app/_homeComponents/Hero.tsx @@ -7,6 +7,10 @@ interface HeroProps extends React.HTMLAttributes { className?: string; } +/** + * Represents a Hero component that displays a title, description, and optional image. + * Spans the whole height of the viewport. + */ const Hero = React.forwardRef( ({ title, description, imageUrl, className, children, ...props }, ref) => (
); } + +const GET_MISSION_STATEMENT = graphql(` + query HomeMissionStatement { + homeMissionStatement { + data { + attributes { + title + textContent + } + } + } + } +`); + +async function fetchMissionStatement() { + const client = getClient(); // Ensure getClient properly typed to return ApolloClient + const response = await client.query({ + // This ensures that TypeScript expects the right structure + query: GET_MISSION_STATEMENT, + }); + + const missionStatement = + response.data.homeMissionStatement?.data?.attributes; + + return { + title: missionStatement?.title, + textContent: missionStatement?.textContent, + }; +} diff --git a/frontend/src/app/_homeComponents/SatDropdown.tsx b/frontend/src/app/_homeComponents/SatDropdown.tsx index 0faa12c..656113d 100644 --- a/frontend/src/app/_homeComponents/SatDropdown.tsx +++ b/frontend/src/app/_homeComponents/SatDropdown.tsx @@ -5,6 +5,11 @@ import { cn } from "@/lib/utils"; import { satLoaderById } from "@/lib/getSatelliteData"; import { SatelliteNumber } from "@/lib/store"; import { useSatelliteStore } from "@/lib/store"; + +/** + * Renders a dropdown menu to select a satellite. + * Allows the user to select a satellite by NORAD ID or from a list of satellites. + */ export default function SatDropdown() { const selectedSatellite = useSatelliteStore( (state) => state.selectedSatellite, diff --git a/frontend/src/app/_homeComponents/SatelliteGlobe.tsx b/frontend/src/app/_homeComponents/SatelliteGlobe.tsx index 7984eea..ff6b964 100644 --- a/frontend/src/app/_homeComponents/SatelliteGlobe.tsx +++ b/frontend/src/app/_homeComponents/SatelliteGlobe.tsx @@ -17,6 +17,11 @@ interface initpostype { satNumber: number; } +/** + * Renders a 3D globe with satellite positions and allows interaction with the satellites. + * Uses the globe.gl library to render the globe and satellites. + * https://github.com/vasturiano/globe.gl + */ export default function SatelliteGlobe() { const chart = useRef(null); const globeRef = useRef(); @@ -30,6 +35,7 @@ export default function SatelliteGlobe() { // Initialize the globe useEffect(() => { if (chart.current && !globeRef.current) { + // Create the globe instance globeRef.current = Globe()(chart.current) .globeImageUrl("/images/earth-blue-marble.jpg") .backgroundImageUrl("/images/night-sky.png") @@ -57,6 +63,7 @@ export default function SatelliteGlobe() { } }); + // Enable globe controls globeRef.current.controls().enabled = true; globeRef.current.controls().enableZoom = false; globeRef.current.controls().enablePan = false; diff --git a/frontend/src/app/_homeComponents/SatelliteSelector.tsx b/frontend/src/app/_homeComponents/SatelliteSelector.tsx index fa8c905..71e2dc9 100644 --- a/frontend/src/app/_homeComponents/SatelliteSelector.tsx +++ b/frontend/src/app/_homeComponents/SatelliteSelector.tsx @@ -2,6 +2,10 @@ import React from "react"; import SatDropdown from "./SatDropdown"; +/** + * Renders the SatelliteSelector component. + * Allows the user to select a satellite to view. + */ export default function SatelliteSelector() { return (
diff --git a/frontend/src/app/_homeComponents/ScrollIndicator.tsx b/frontend/src/app/_homeComponents/ScrollIndicator.tsx index d872b5b..55d2e57 100644 --- a/frontend/src/app/_homeComponents/ScrollIndicator.tsx +++ b/frontend/src/app/_homeComponents/ScrollIndicator.tsx @@ -4,23 +4,9 @@ import React, { useEffect, useRef } from "react"; import type { SVGProps } from "react"; import { inView } from "framer-motion"; -export function UiwDown(props: SVGProps) { - return ( - - - - ); -} - +/** + * Renders a arrow pointing down to indicate that the user can scroll down. + */ export default function ScrollIndicator() { const ref = useRef(null); const { scrollYProgress } = useScroll({ @@ -57,3 +43,20 @@ export default function ScrollIndicator() { ); } + +export function UiwDown(props: SVGProps) { + return ( + + + + ); +} diff --git a/frontend/src/app/_homeComponents/TeamSection.tsx b/frontend/src/app/_homeComponents/TeamSection.tsx index f44962f..701029d 100644 --- a/frontend/src/app/_homeComponents/TeamSection.tsx +++ b/frontend/src/app/_homeComponents/TeamSection.tsx @@ -4,26 +4,11 @@ import Image from "next/image"; const STRAPI_URL = process.env.BACKEND_INTERNAL_URL; -const GET_TEAM_DATA = graphql(` - query Query($publicationState: PublicationState) { - hero(publicationState: $publicationState) { - data { - attributes { - title - text - image { - data { - attributes { - url - } - } - } - } - } - } - } -`); - +/** + * Renders the team section of the website. + * Text and Image is fetched from Strapi + * Displays them in a flex container, either in a column or row depending on screen size. + */ export default async function TeamSection() { const graphqlData = await getClient().query({ query: GET_TEAM_DATA, @@ -75,3 +60,23 @@ export default async function TeamSection() {
); } + +const GET_TEAM_DATA = graphql(` + query Query($publicationState: PublicationState) { + hero(publicationState: $publicationState) { + data { + attributes { + title + text + image { + data { + attributes { + url + } + } + } + } + } + } + } +`); diff --git a/frontend/src/app/blog/BlogPaginator.tsx b/frontend/src/app/blog/BlogPaginator.tsx index 388eb67..bdd7d48 100644 --- a/frontend/src/app/blog/BlogPaginator.tsx +++ b/frontend/src/app/blog/BlogPaginator.tsx @@ -10,11 +10,18 @@ import { } from "../../components/shadcn/pagination"; import { useRouter, useSearchParams } from "next/navigation"; +/** + * Renders a pagination component for a blog page. + * The component allows the user to navigate between pages of blog articles. + * + * @param totalArticles - The total number of articles. + */ export default function BlogPaginator({ totalArticles, }: { totalArticles: Number; }) { + // Get the current search parameters const searchParams = useSearchParams(); const router = useRouter(); const tag = useSearchParams().get("tag"); diff --git a/frontend/src/app/blog/BlogpageButtons.tsx b/frontend/src/app/blog/BlogpageButtons.tsx index 9dbc62a..a3668db 100644 --- a/frontend/src/app/blog/BlogpageButtons.tsx +++ b/frontend/src/app/blog/BlogpageButtons.tsx @@ -4,6 +4,13 @@ import { Button } from "../../components/shadcn/button"; import { useRouter, useSearchParams } from "next/navigation"; import { cn } from "@/lib/utils"; +/** + * Renders a set of buttons for filtering blog posts. + * + * @param {Object} props - The component props. + * @param {string} [props.className] - The CSS class names for the component. + * @returns {JSX.Element} The rendered component. + */ export default function BlogpageButtons({ className }: { className?: string }) { const [activeButton, setActiveButton] = useState("All Posts"); const router = useRouter(); diff --git a/frontend/src/app/blog/blogDataCards.tsx b/frontend/src/app/blog/blogDataCards.tsx index 3c4f843..c39ea4f 100644 --- a/frontend/src/app/blog/blogDataCards.tsx +++ b/frontend/src/app/blog/blogDataCards.tsx @@ -7,6 +7,13 @@ import CardGrid from "@/components/shared/CardGrid"; const STRAPI_URL = process.env.BACKEND_INTERNAL_URL; +/** + * Renders a grid of blog data cards based on the provided articles. + * + * @param {Object} props - The component props. + * @param {ArticlesDataType} props.articles - The array of articles to display. + * @returns {JSX.Element} The rendered blog data cards. + */ export default async function BlogDataCards({ articles, }: { diff --git a/frontend/src/app/blog/page.tsx b/frontend/src/app/blog/page.tsx index b0a9a11..927ef0c 100644 --- a/frontend/src/app/blog/page.tsx +++ b/frontend/src/app/blog/page.tsx @@ -5,69 +5,23 @@ import { PageSubtitle, PageHeader, } from "@/components/layout/PageHeader"; - import { PagePadding } from "@/components/layout/PageLayout"; import React from "react"; import { getClient } from "@/lib/ApolloClient"; import { ResultOf, graphql } from "@/lib/tada/graphql"; +// Type for the result of the GET_ARTICLES query +// Used in BlogDataCards.tsx type articlesFetchType = ResultOf; export type ArticlesDataType = NonNullable< articlesFetchType["articles"] >["data"]; -const GET_ARTICLES = graphql(` - query GET_ARTICLES( - $pagination: PaginationArg - $filters: ArticleFiltersInput - ) { - articles( - sort: ["datePublished:desc"] - pagination: $pagination - filters: $filters - ) { - data { - id - attributes { - author { - data { - attributes { - name - avatar { - data { - attributes { - url - } - } - } - } - } - } - previewTitle - datePublished - body - coverImage { - data { - attributes { - url - } - } - } - createdAt - publishedAt - slug - Tag - } - } - meta { - pagination { - total - } - } - } - } -`); - +/** + * Renders the Blog page. + * Retrieves blog posts from the GraphQL API and displays them in a grid of cards. + * If there are no blog posts to show, a message is displayed instead. + */ export default async function BlogPage({ searchParams, }: { @@ -125,3 +79,55 @@ export default async function BlogPage({ ); } + +const GET_ARTICLES = graphql(` + query GET_ARTICLES( + $pagination: PaginationArg + $filters: ArticleFiltersInput + ) { + articles( + sort: ["datePublished:desc"] + pagination: $pagination + filters: $filters + ) { + data { + id + attributes { + author { + data { + attributes { + name + avatar { + data { + attributes { + url + } + } + } + } + } + } + previewTitle + datePublished + body + coverImage { + data { + attributes { + url + } + } + } + createdAt + publishedAt + slug + Tag + } + } + meta { + pagination { + total + } + } + } + } +`); diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 0ef123a..c537b67 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -7,6 +7,9 @@ import Footer from "@/components/layout/Footer"; import React from "react"; import { ApolloWrapper } from "@/components/wrappers/ApolloWrapper"; import InitializeZustandWithSatEntries from "@/components/satelliteData/SatelliteInitialFetch"; +import ErrorBoundaryNavigation from "@/components/layout/ErrorBoundaryNavigation"; +import Starfield from "@/components/layout/Starfield"; +import { SatelliteEntry } from "@/lib/store"; // imports to get satellites from strapi and fetch the data serverside import fetchSatelliteNamesAndId from "@/lib/data/fetchSatelliteNamesAndId"; @@ -23,16 +26,12 @@ export const metadata: Metadata = { }, }; -import ErrorBoundaryNavigation from "@/components/layout/ErrorBoundaryNavigation"; -import Starfield from "@/components/layout/Starfield"; -import { SatelliteEntry } from "@/lib/store"; - export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - // fetch satellite names and id to be set in the store in the navbar + // fetch satellite names and id to be set in the store const satellites = await fetchSatelliteNamesAndId(); let satData: SatelliteEntry[] = []; diff --git a/frontend/src/app/loading.tsx b/frontend/src/app/loading.tsx index 6c732ad..fcd3b02 100644 --- a/frontend/src/app/loading.tsx +++ b/frontend/src/app/loading.tsx @@ -1,6 +1,9 @@ import React from "react"; import styles from "./loading.module.css"; +/** + * Renders a loading spinner. + */ export default function Loading() { return (
diff --git a/frontend/src/app/projects/page.tsx b/frontend/src/app/projects/page.tsx index 5ae046c..9dbd5c5 100644 --- a/frontend/src/app/projects/page.tsx +++ b/frontend/src/app/projects/page.tsx @@ -10,35 +10,11 @@ import { graphql } from "@/lib/tada/graphql"; import CardGrid from "@/components/shared/CardGrid"; const STRAPI_URL = process.env.BACKEND_INTERNAL_URL; -const GET_PROJECTS = graphql(` - query GET_PROJECTS { - projects(sort: ["publishedAt:desc"]) { - data { - id - attributes { - title - content - satellites { - data { - attributes { - catalogNumberNORAD - } - } - } - slug - previewImage { - data { - attributes { - url - } - } - } - } - } - } - } -`); - +/** + * Renders the Projects page. + * Retrieves project data from the GraphQL API and displays it in a grid of cards. + * If there are no projects to show, a message is displayed instead. + */ export default async function ProjectsPage() { const graphqlData = await getClient().query({ query: GET_PROJECTS, @@ -92,3 +68,32 @@ export default async function ProjectsPage() {
); } + +const GET_PROJECTS = graphql(` + query GET_PROJECTS { + projects(sort: ["publishedAt:desc"]) { + data { + id + attributes { + title + content + satellites { + data { + attributes { + catalogNumberNORAD + } + } + } + slug + previewImage { + data { + attributes { + url + } + } + } + } + } + } + } +`); diff --git a/frontend/src/app/satellites/SatelliteResponsiveTable.tsx b/frontend/src/app/satellites/SatelliteResponsiveTable.tsx index ea8a6a5..72bd56d 100644 --- a/frontend/src/app/satellites/SatelliteResponsiveTable.tsx +++ b/frontend/src/app/satellites/SatelliteResponsiveTable.tsx @@ -14,12 +14,22 @@ import { TableCell, } from "@/components/shadcn/table"; import { useRouter } from "next/navigation"; +import { SatellitesResult } from "./page"; +import { SatelliteName, SatelliteNumber } from "@/lib/store"; +/** + * Renders a table displaying satellite information that updates continuously. + * + * @param {Object} props - The component props. + * @param {Array} props.satellites - The array of satellite objects to display. + * @param {boolean} props.inOrbit - Indicates whether the satellites are in orbit or not. + * @returns {JSX.Element} The rendered SatelliteResponsiveTable component. + */ export default function SatelliteResponsiveTable({ satellites, inOrbit, }: { - satellites: any; + satellites: SatellitesResult | undefined; inOrbit: boolean; }) { const router = useRouter(); @@ -66,33 +76,43 @@ export default function SatelliteResponsiveTable({ )} - + {inOrbit - ? satellites.map((satellite: any) => ( + ? (satellites ?? []).map((satellite) => ( - handleRowClick(satellite.attributes.slug) + handleRowClick( + satellite.attributes?.slug ?? "", + ) } /> )) - : satellites.map((satellite: any) => ( + : (satellites ?? []).map((satellite) => ( - handleRowClick(satellite.attributes.slug) + handleRowClick( + satellite.attributes?.slug ?? "", + ) } > - {satellite.attributes.name} + {satellite.attributes?.name} - {satellite.attributes.missionStatus + {satellite.attributes?.missionStatus ? satellite.attributes.missionStatus : "Unknown"} diff --git a/frontend/src/components/ui/launchDateCountDown.tsx b/frontend/src/app/satellites/[satelliteSlug]/launchDateCountDown.tsx similarity index 90% rename from frontend/src/components/ui/launchDateCountDown.tsx rename to frontend/src/app/satellites/[satelliteSlug]/launchDateCountDown.tsx index 26a1a7f..e791be4 100644 --- a/frontend/src/components/ui/launchDateCountDown.tsx +++ b/frontend/src/app/satellites/[satelliteSlug]/launchDateCountDown.tsx @@ -2,11 +2,18 @@ import React, { useState, useEffect } from "react"; type LaunchDateCountDownProps = { - launchDateString: string | undefined; + launchDate: string | Date | undefined; }; +/** + * Countdown component that displays the time remaining until a launch date or the time since a launch has occurred. + * + * @component + * @param {string} props.launchDate - The launch date in string format. + * @returns {JSX.Element} - The countdown component. + */ const LaunchDateCountDown: React.FC = ({ - launchDateString, + launchDate: launchDateString, }) => { const [displayTime, setDisplayTime] = useState([ "0 days", diff --git a/frontend/src/app/satellites/[satelliteSlug]/page.tsx b/frontend/src/app/satellites/[satelliteSlug]/page.tsx index 01fa056..21857ed 100644 --- a/frontend/src/app/satellites/[satelliteSlug]/page.tsx +++ b/frontend/src/app/satellites/[satelliteSlug]/page.tsx @@ -1,29 +1,18 @@ import React from "react"; import BlockRendererClient from "@/components/shared/BlockRendererClient"; -import fetchSatelliteInfo from "@/lib/data/fetchSatelliteInfo"; -import { BlocksContent } from "@strapi/blocks-react-renderer"; import RelatedProjectsAndSatellites from "@/components/shared/RelatedProjectsAndSatellites"; import Map2d from "@/app/satellites/[satelliteSlug]/_2dmap/Map2d"; import SatelliteDataHome from "@/components/satelliteData/SatelliteDataHome"; -import LaunchDateCountDown from "@/components/ui/launchDateCountDown"; +import LaunchDateCountDown from "@/app/satellites/[satelliteSlug]/launchDateCountDown"; import { PageHeader, PageSubtitle, PageHeaderAndSubtitle, } from "@/components/layout/PageHeader"; import Image from "next/image"; -import { SatelliteName, SatelliteNumber } from "@/lib/store"; - -export interface SatelliteInfo { - launchDate: string | undefined; - name: SatelliteName; - content: BlocksContent; - relatedProjects?: ProjectOrSatellite[]; - noradId: SatelliteNumber | undefined; - missionStatus: string | undefined; - massKg: number | undefined; - satelliteImage: string | undefined; -} +import { SatelliteNumber } from "@/lib/store"; +import { graphql } from "@/lib/tada/graphql"; +import { getClient } from "@/lib/ApolloClient"; export interface ProjectOrSatellite { id: string; @@ -40,57 +29,84 @@ export default async function SatelliteInfoPage({ }: { params: { satelliteSlug: string }; }) { - const satelliteInfo: SatelliteInfo = await fetchSatelliteInfo({ - params: params, + const graphqlData = await getClient().query({ + query: GET_SATELLITE_INFO, + variables: { + filters: { + slug: { + eq: params.satelliteSlug, + }, + }, + }, }); - if (!satelliteInfo) return
Loading...
; + // Map all related projects + let relatedProjects: ProjectOrSatellite[] = []; + graphqlData?.data?.satellites?.data[0]?.attributes?.projects?.data.map( + (project: any) => { + relatedProjects.push({ + id: project.id, + title: project.attributes?.title, + previewImage: + project.attributes?.previewImage?.data?.attributes?.url, + slug: project.attributes?.slug, + isProject: true, + }); + }, + ); - console.log(satelliteInfo); + // Get the satellite attributes + let satAttributes = graphqlData?.data?.satellites?.data[0]?.attributes; - let imageURL = undefined; - if (STRAPI_URL && satelliteInfo.satelliteImage) { - imageURL = STRAPI_URL + satelliteInfo.satelliteImage; + // If the satellite is not found return a message + if (!satAttributes?.catalogNumberNORAD) { + return
Satellite not found
; } - if (Number.isNaN(satelliteInfo.noradId)) { - return
Satellite not found
; + // Get the NORAD ID + let noradId = Number(satAttributes?.catalogNumberNORAD) as SatelliteNumber; + + // Get the satellite image + let satelliteImage = satAttributes?.satelliteImage?.data?.attributes?.url; + let imageURL = undefined; + if (STRAPI_URL && satelliteImage) { + imageURL = STRAPI_URL + satelliteImage; } return ( <>
- {satelliteInfo.name} + {satAttributes?.name} - {satelliteInfo.missionStatus - ? "Mission Status: " + satelliteInfo.missionStatus + {satAttributes?.missionStatus + ? "Mission Status: " + satAttributes?.missionStatus : null} {/* Container for satname, stats and sat image */} - {satelliteInfo.noradId ? ( + {noradId ? (
{/* Stats Container */}
- {satelliteInfo.noradId ? ( + {noradId ? ( ) : null}

- {satelliteInfo.massKg + {satAttributes?.massKg ? "Mass: " + - satelliteInfo.massKg + + satAttributes?.massKg + " kg" : null}

@@ -105,7 +121,7 @@ export default async function SatelliteInfoPage({ {imageURL ? ( {satelliteInfo.name}
) : null} {/* Container for map */} - {satelliteInfo.noradId ? ( + {noradId ? (
- +
) : null} {/* Container for body content */}
- +
{/* Related projects */}
- {satelliteInfo.relatedProjects?.length != 0 ? ( + {relatedProjects?.length != 0 ? ( <>

Related Projects

- {satelliteInfo.relatedProjects?.map( + {relatedProjects?.map( (project: ProjectOrSatellite) => ( ); } + +const GET_SATELLITE_INFO = graphql(` + query GET_SATELLITE_INFO($filters: SatelliteFiltersInput) { + satellites(filters: $filters) { + data { + id + attributes { + catalogNumberNORAD + content + name + massKg + missionStatus + satelliteImage { + data { + attributes { + url + } + } + } + projects { + data { + attributes { + title + previewImage { + data { + attributes { + url + } + } + } + slug + } + id + } + } + launchDate + } + } + } + } +`); diff --git a/frontend/src/app/satellites/page.tsx b/frontend/src/app/satellites/page.tsx index caa68c2..dd990e9 100644 --- a/frontend/src/app/satellites/page.tsx +++ b/frontend/src/app/satellites/page.tsx @@ -1,6 +1,7 @@ import { getClient } from "@/lib/ApolloClient"; import SatelliteResponsiveTable from "./SatelliteResponsiveTable"; import { graphql } from "@/lib/tada/graphql"; +import { ResultOf } from "@/lib/tada/graphql"; const GET_SATELLITES = graphql(` query GET_SATELLITES { @@ -11,12 +12,24 @@ const GET_SATELLITES = graphql(` catalogNumberNORAD name slug + missionStatus } } } } `); +// Type for the result of the GET_SATELLITES query +// Used in SateliteResponsiveTable.tsx +export type SatellitesResult = NonNullable< + ResultOf["satellites"] +>["data"]; + +/** + * Renders the Satellites page. + * This page fetches satellite data from the server and displays it in two tables: + * one for satellites in orbit and another for satellites not in orbit. + */ export default async function Satellites() { try { const graphqlData = await getClient().query({ @@ -27,13 +40,18 @@ export default async function Satellites() { (data) => data.attributes?.catalogNumberNORAD == null, ); + let satellitesInOrbit = graphqlData.data.satellites?.data.filter( + (data) => data.attributes?.catalogNumberNORAD !== null, + ); + let satellitesNotInOrbit = graphqlData.data.satellites?.data.filter( + (data) => data.attributes?.catalogNumberNORAD == null, + ); + return ( <> {/* Table for satellites in orbit */} data.attributes?.catalogNumberNORAD !== null, - )} + satellites={satellitesInOrbit} inOrbit={true} > @@ -42,10 +60,7 @@ export default async function Satellites() { {/* Table for satellites not in orbit */} {noNoradIdArray != undefined && noNoradIdArray.length > 0 ? ( - data.attributes?.catalogNumberNORAD == null, - )} + satellites={satellitesNotInOrbit} inOrbit={false} > ) : null} diff --git a/frontend/src/app/sitemap.ts b/frontend/src/app/sitemap.ts index d6867a9..9f61971 100644 --- a/frontend/src/app/sitemap.ts +++ b/frontend/src/app/sitemap.ts @@ -7,51 +7,16 @@ import { TadaDocumentNode } from "gql.tada"; // Needs to be dynamic as we use the rsc apollo client export const dynamic = "force-dynamic"; -const GET_ARTICLE_SLUGS = graphql(` - query GetAllArticleSlugs { - articles { - data { - attributes { - slug - } - } - } - } -`); - -const GET_PROJECT_SLUGS = graphql(` - query GetAllProjectSlugs { - projects { - data { - attributes { - slug - } - } - } - } -`); - -const GET_SATELLITE_SLUGS = graphql(` - query GetAllSatelliteSlugs { - satellites { - data { - attributes { - slug - } - } - } - } -`); - export default async function sitemap(): Promise { // All routes let routes: MetadataRoute.Sitemap = []; + // Add the homepage if (env.OUTWARD_FACING_URL) { routes.push({ url: env.OUTWARD_FACING_URL }); } - // Add all root routes for blo, projects and satellites + // Add all root routes for blog, projects and satellites routes.push({ url: `${env.OUTWARD_FACING_URL}/blog` }); routes.push({ url: `${env.OUTWARD_FACING_URL}/projects` }); routes.push({ url: `${env.OUTWARD_FACING_URL}/satellites` }); @@ -78,6 +43,7 @@ export default async function sitemap(): Promise { return routes; } +// Type definitions for the GraphQL queries interface Inner { data: { attributes: { @@ -86,6 +52,7 @@ interface Inner { }[]; } +// Type guard to check if the object is an Inner object function isInner(obj: any): obj is Inner { if (obj.data && obj.data[0].attributes && obj.data[0].attributes.slug) { return true; @@ -97,6 +64,11 @@ type QueryType = { [key: string]: Inner | null; }; +/** + * Retrieves the slugs from the provided query. + * @param query - The query to be executed. + * @returns A promise that resolves to an array of slugs. + */ async function getSlugs(query: TadaDocumentNode): Promise { let response = await getClient().query({ query: query, @@ -109,6 +81,11 @@ async function getSlugs(query: TadaDocumentNode): Promise { return []; } +/** + * Extracts slugs from the provided data. + * @param datas - The data to extract slugs from. + * @returns An array of extracted slugs. + */ function extractSlugs(datas: Inner | null): string[] { let data = datas?.data; if (typeof data !== "undefined" && data !== null) { @@ -120,3 +97,39 @@ function extractSlugs(datas: Inner | null): string[] { } return []; } + +const GET_ARTICLE_SLUGS = graphql(` + query GetAllArticleSlugs { + articles { + data { + attributes { + slug + } + } + } + } +`); + +const GET_PROJECT_SLUGS = graphql(` + query GetAllProjectSlugs { + projects { + data { + attributes { + slug + } + } + } + } +`); + +const GET_SATELLITE_SLUGS = graphql(` + query GetAllSatelliteSlugs { + satellites { + data { + attributes { + slug + } + } + } + } +`); diff --git a/frontend/src/components/layout/ErrorBoundaryNavigation.tsx b/frontend/src/components/layout/ErrorBoundaryNavigation.tsx index fb92d91..8398e5b 100644 --- a/frontend/src/components/layout/ErrorBoundaryNavigation.tsx +++ b/frontend/src/components/layout/ErrorBoundaryNavigation.tsx @@ -3,6 +3,14 @@ import React from "react"; import { ErrorBoundary } from "react-error-boundary"; +/** + * ErrorBoundaryNavigation component wraps its children with an ErrorBoundary component, + * which catches any errors that occur within its children and displays a fallback UI. + * + * @component + * @param {React.ReactNode} children - The content to be wrapped by the ErrorBoundary component. + * @returns {JSX.Element} The ErrorBoundaryNavigation component. + */ export default function ErrorBoundaryNavigation({ children, }: { diff --git a/frontend/src/components/layout/Footer.tsx b/frontend/src/components/layout/Footer.tsx index 7bc5ee7..4d6f1fc 100644 --- a/frontend/src/components/layout/Footer.tsx +++ b/frontend/src/components/layout/Footer.tsx @@ -1,6 +1,10 @@ import { env } from "process"; import NTNULogo from "./NTNULogo"; +/** + * The footer component at the bottom of the page. + * It includes the NTNU logo, social media links, and contact information. + */ export default function Footer() { return (