diff --git a/.gitignore b/.gitignore
index 4d29575..349d8f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,9 @@
# testing
/coverage
+# api key
+.env
+
# production
/build
diff --git a/package-lock.json b/package-lock.json
index 7e0ffa8..c8c43c2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,8 +11,7 @@
"@testing-library/dom": "^10.4.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.126",
- "@types/react": "^19.1.4",
- "@types/react-dom": "^19.1.5",
+ "dotenv": "^16.5.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.6.0",
@@ -27,7 +26,8 @@
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^29.5.14",
- "@types/react": "^19.0.10",
+ "@types/react": "^19.1.4",
+ "@types/react-dom": "^19.1.5",
"@types/sass": "^1.45.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.0.1",
@@ -4120,6 +4120,8 @@
"version": "19.1.4",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz",
"integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4128,6 +4130,8 @@
"version": "19.1.5",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz",
"integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==",
+ "dev": true,
+ "license": "MIT",
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -6471,7 +6475,8 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@@ -6862,11 +6867,15 @@
}
},
"node_modules/dotenv": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
- "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
+ "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
+ "license": "BSD-2-Clause",
"engines": {
- "node": ">=10"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
}
},
"node_modules/dotenv-expand": {
@@ -13422,6 +13431,7 @@
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -13686,6 +13696,15 @@
}
}
},
+ "node_modules/react-scripts/node_modules/dotenv": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
+ "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
diff --git a/package.json b/package.json
index 3f33462..62cb491 100644
--- a/package.json
+++ b/package.json
@@ -6,8 +6,7 @@
"@testing-library/dom": "^10.4.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.126",
- "@types/react": "^19.1.4",
- "@types/react-dom": "^19.1.5",
+ "dotenv": "^16.5.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.6.0",
@@ -48,7 +47,8 @@
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^29.5.14",
- "@types/react": "^19.0.10",
+ "@types/react": "^19.1.4",
+ "@types/react-dom": "^19.1.5",
"@types/sass": "^1.45.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.0.1",
diff --git a/src/App.test.tsx b/src/App.test.tsx
deleted file mode 100644
index d76787e..0000000
--- a/src/App.test.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from "react";
-import { render, screen } from "@testing-library/react";
-import App from "./App";
-
-test("renders learn react link", () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/src/App.tsx b/src/App.tsx
index 32ac87a..faa5b5d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,24 +1,11 @@
import React from "react";
import "./App.scss";
+import MissionManifest, {
+ rovers,
+} from "./Components/MissionManifest/MissionManifest";
function App() {
- return (
-
- );
+ return ;
}
export default App;
diff --git a/src/Components/MissionManifest/MissionManifest.scss b/src/Components/MissionManifest/MissionManifest.scss
new file mode 100644
index 0000000..ce209ab
--- /dev/null
+++ b/src/Components/MissionManifest/MissionManifest.scss
@@ -0,0 +1,81 @@
+@import "../../Styles/GlobalStyles.scss";
+
+body{
+ background-color: $blueBackground;
+}
+#mission-manifest-container {
+ font-family: $MartianMono;
+ width: 40vw;
+ margin: 40px auto;
+ background-color: $orangeBackground;
+ color: black;
+ h2 {
+ font-weight: 900;
+ }
+ padding: 20px 25px 30px 25px;
+}
+
+#rover-mission-info-container {
+ display: grid;
+ grid-template-columns: 3fr 5fr;
+ grid-auto-rows: auto;
+ p {
+ font-size: 0.8rem;
+ border: 1px solid black;
+ padding: 5px 9px;
+ }
+}
+.manifest-fieldname{
+ font-weight: 900;
+}
+
+// Media Queries
+
+@media(max-width: 898px) {
+ #mission-manifest-container {
+ width: 50vw;
+ padding: 12px 20px 22px 20px;
+
+ h2{
+ font-size: 1.3rem;
+ }
+ }
+ #rover-mission-info-container p {
+ font-size: 0.7rem;
+ }
+}
+
+@media(max-width: 656px) {
+ #mission-manifest-container {
+ width: 60vw;
+ padding: 12px 20px 22px 20px;
+
+ h2{
+ font-size: 1.3rem;
+ }
+ }
+ #rover-mission-info-container p {
+ font-size: 0.7rem;
+ }
+}
+
+@media(max-width: 320px) {
+ #mission-manifest-container {
+ width: 70vw;
+ padding: 12px 20px 22px 20px;
+
+ h2{
+ font-size: 1.3rem;
+ }
+ }
+ #rover-mission-info-container p {
+ font-size: 0.7rem;
+ }
+}
+
+@media(max-width: 507px) {
+ #rover-mission-info-container p {
+ display: flex;
+ align-items: center;
+ }
+}
diff --git a/src/Components/MissionManifest/MissionManifest.spec.tsx b/src/Components/MissionManifest/MissionManifest.spec.tsx
new file mode 100644
index 0000000..ebfb5f7
--- /dev/null
+++ b/src/Components/MissionManifest/MissionManifest.spec.tsx
@@ -0,0 +1,96 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import MissionManifest, { rovers } from "./MissionManifest";
+
+const mockManifestResponse = {
+ photo_manifest: {
+ name: "Opportunity",
+ landing_date: "2004-01-25",
+ launch_date: "2003-07-07",
+ status: "complete",
+ max_sol: 5111,
+ max_date: "2018-06-11",
+ total_photos: 198439,
+ },
+};
+
+beforeEach(() => {
+ global.fetch = jest.fn().mockResolvedValue({
+ json: jest.fn().mockResolvedValue(mockManifestResponse),
+ });
+});
+
+test("renders loading message on initial render", () => {
+ render();
+ expect(screen.getByText("Loading...")).toBeInTheDocument();
+});
+
+test("opportunity rover renders correct p elements", async () => {
+ render();
+ const expectedTexts = [
+ "Rover Name: ",
+ mockManifestResponse.photo_manifest.name,
+ "Landing Date: ",
+ mockManifestResponse.photo_manifest.landing_date,
+ "Launch Date: ",
+ mockManifestResponse.photo_manifest.launch_date,
+ "Status: ",
+ mockManifestResponse.photo_manifest.status,
+ "Max Sol: ",
+ mockManifestResponse.photo_manifest.max_sol.toString(),
+ "Max Date: ",
+ mockManifestResponse.photo_manifest.max_date,
+ "Total photos: ",
+ mockManifestResponse.photo_manifest.total_photos.toString(),
+ ];
+
+ const missionManifestContainer = await screen.findByTestId(
+ "mission-manifest-container",
+ );
+ const pElements = missionManifestContainer.querySelectorAll("p");
+
+ expect(pElements.length).toBe(14);
+ pElements.forEach((p, index) => {
+ expect(p.textContent).toBe(expectedTexts[index]);
+ console.log(p.textContent);
+ });
+});
+
+test("opportunity rover renders correct Heading", async () => {
+ render();
+
+ const heading = await screen.findByText("MISSION MANIFEST");
+
+ expect(heading).toBeInTheDocument();
+});
+
+test("invalid rover renders correct p elements", async () => {
+ const mockInvalidFetchResponse = {
+ photo_manifest: {
+ name: undefined,
+ landing_date: undefined,
+ launch_date: undefined,
+ status: undefined,
+ max_sol: undefined,
+ max_date: undefined,
+ total_photos: undefined,
+ },
+ };
+ global.fetch = jest.fn().mockResolvedValue({
+ json: jest.fn().mockResolvedValue(mockInvalidFetchResponse),
+ });
+
+ render();
+
+ const missionManifestContainer = await screen.findByTestId(
+ "mission-manifest-container",
+ );
+ const pElements = missionManifestContainer.querySelectorAll("p");
+ let NaCounter = 0;
+
+ pElements.forEach((p) => {
+ if (p.textContent === "N/A") NaCounter++;
+ });
+
+ expect(NaCounter).toBe(7);
+});
diff --git a/src/Components/MissionManifest/MissionManifest.tsx b/src/Components/MissionManifest/MissionManifest.tsx
new file mode 100644
index 0000000..f2064ba
--- /dev/null
+++ b/src/Components/MissionManifest/MissionManifest.tsx
@@ -0,0 +1,79 @@
+import React from "react";
+import { useState } from "react";
+import { useEffect } from "react";
+import "./MissionManifest.scss";
+
+export enum rovers {
+ CURIOSITY = "curiosity",
+ OPPORTUNITY = "opportunity",
+ SPIRIT = "spirit",
+}
+
+export interface MissionManifestProps {
+ roverType: rovers;
+}
+
+type ManifestData = {
+ name: string;
+ landingDate: string;
+ launchDate: string;
+ status: string;
+ maxSol: number;
+ maxDate: string;
+ totalPhotos: number;
+};
+
+function MissionManifest(props: MissionManifestProps) {
+ const apiKey = process.env.REACT_APP_API_KEY;
+ const [manifestData, setManifestData] = useState(null);
+
+ useEffect(() => {
+ fetch(
+ `https://api.nasa.gov/mars-photos/api/v1/manifests/${props.roverType}?api_key=${apiKey}`,
+ )
+ .then((response) => response.json())
+ .then((response) => {
+ const data = response.photo_manifest;
+ setManifestData({
+ name: data.name,
+ landingDate: data.landing_date,
+ launchDate: data.launch_date,
+ status: data.status,
+ maxSol: data.max_sol,
+ maxDate: data.max_date,
+ totalPhotos: data.total_photos,
+ });
+ });
+ }, [props.roverType]);
+
+ if (!manifestData) {
+ return Loading...
;
+ } else {
+ return (
+
+
MISSION MANIFEST
+
+
Rover Name:
+
{manifestData.name ?? "N/A"}
+
Landing Date:
+
{manifestData.landingDate ?? "N/A"}
+
Launch Date:
+
{manifestData.launchDate ?? "N/A"}
+
Status:
+
{manifestData.status ?? "N/A"}
+
Max Sol:
+
{manifestData.maxSol ?? "N/A"}
+
Max Date:
+
{manifestData.maxDate ?? "N/A"}
+
Total photos:
+
{manifestData.totalPhotos ?? "N/A"}
+
+
+ );
+ }
+}
+
+export default MissionManifest;
diff --git a/src/Styles/GlobalStyles.scss b/src/Styles/GlobalStyles.scss
new file mode 100644
index 0000000..321705f
--- /dev/null
+++ b/src/Styles/GlobalStyles.scss
@@ -0,0 +1,14 @@
+
+@import url('https://fonts.googleapis.com/css2?family=Martian+Mono:wght@100..800&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Martian+Mono:wght@100..800&family=Public+Sans:ital,wght@0,100..900;1,100..900&display=swap');
+
+$orangeBackground: #E4965B;
+$blueBackground: #062356;
+$greyBackground: #878888;
+
+$MartianMono: "Martian Mono", monospace;
+$PublicSans: "Public Sans", sans-serif;
+$whiteFontColor: #FFFFF2;
+$boldFontWeight: 900;
+
+$buttonColor: #878888;