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 ( -
-
-

- Edit src/App.tsx and save to reload. -

- - Learn React - -
-
- ); + 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;