Build a Universe Explorer Dashboard
Fetch real solar system data from a space API and build a filterable dashboard.
Introduction
30 Second Summary
Every planet, moon, and asteroid in our solar system has real scientific data attached to it. Mass, gravity, temperature, orbital speed. Scientists access this data through APIs, and now you can too.
In this project, you will build a Universe Explorer dashboard that pulls real celestial body data from a space science API and displays it in a filterable, card-based interface you can open in your browser.
What You'll Build
A web dashboard built with HTML, CSS, and JavaScript that connects to the Solar System OpenData API and renders celestial body data into an interactive, searchable grid.
By the end of this project, you'll have:
- A working Universe Explorer dashboard that fetches real data about planets, moons, and asteroids from the Solar System OpenData API.
- Hands-on experience making authenticated API requests and parsing JSON responses into a visual interface.
- A filterable, interactive UI that lets you browse by body type and click any card to see detailed information including moons and discovery history.
- Secret Mission: A "Compare Bodies" feature that lets users select two celestial bodies and view their properties side-by-side.
Are there any prerequisites?
None. This guide includes full setup for all required tools. You just need a computer with an internet connection.
Set Up Your Project
Before you can explore the solar system, you need a workspace to build in. This step creates your project folder, connects it to a local development server, and gets you authenticated with the space data API.
By the end of this step you will have three files ready to go, a live-reloading browser preview, and a free API token that unlocks real celestial body data.
In this step, get ready to:
- Create a project folder with the three files your dashboard needs.
- Install the Live Server extension in Cursor and start a local server.
- Generate a free API token from the Solar System OpenData API.
Create the project folder and files
Every web project starts with a folder that holds its files. You will create one folder and three empty files inside it.
- Open a terminal inside Cursor by pressing Ctrl+` (Windows) or Cmd+` (macOS).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir universe-explorer
cd universe-explorer
- Create the three project files by running this command:
macOS
touch index.html style.css app.js
Windows
New-Item index.html, style.css, app.js
- Confirm the files exist by running:
ls
You should see index.html, style.css, and app.js in the output.
Don't see the three files?
Make sure you ran cd universe-explorer before creating the files. If you are in the wrong directory, the files will end up somewhere else.
help me find where my files ended up
Open the project in Cursor and install Live Server
A live server watches your files and automatically refreshes the browser whenever you save a change. This saves you from manually refreshing the page after every edit.
- In Cursor, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the universe-explorer folder, then click Open.
- Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
- Type Live Server in the search bar.
- Find the extension named Live Server by Ritwick Dey (it has over 78 million installs).
- Click Install.
What is Live Server?
Live Server is a Cursor extension that launches a tiny web server on your computer. It serves your HTML, CSS, and JavaScript files to the browser and refreshes the page automatically whenever you save a change.
- In the Explorer sidebar on the left, click index.html to open it.
- Look at the bottom-right corner of the Cursor window for a Go Live button in the status bar.
- Click Go Live.
Your default browser opens to a local address (usually http://127.0.0.1:5500). The page is blank because your files are empty. That is expected.
Don't see the Go Live button?
Make sure you opened the universe-explorer folder (not just a single file). Live Server needs an open folder to work.
If you still do not see the button, try reloading Cursor by pressing Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows), then typing Reload Window and pressing Enter.
help me get Live Server working in Cursor
Generate a free API token
The Solar System OpenData API requires a free token to authenticate your requests. Without this token, the API returns a 401 error and refuses to share any data.
- Open your browser and go to the token generation page:
- https://api.le-systeme-solaire.net/generatekey.html
- Follow the on-page instructions to generate your token.
- Copy the token and save it somewhere safe (a text file or notes app). You will paste it into your code in a later step.
What is a bearer token?
A bearer token is a string you send in the header of your API request to prove you are allowed to access the data. The Solar System OpenData API introduced token-based authentication in September 2025 to manage usage.
Your token is free and does not expire, but treat it like a password. Do not share it publicly.
Token generation page not loading?
Try a different browser or disable any ad blockers that might interfere with the page. The page is lightweight and loads quickly under normal conditions.
help me get my API token
Your workspace is ready to go. You have three empty files, a live-reloading server, and an API token waiting to be used. Next up, you will build the HTML skeleton that gives your dashboard its structure.
Build the HTML Structure
Every web dashboard starts with an HTML skeleton that defines where each piece of content lives on the page. Your Universe Explorer needs a layout that can hold a header, filter controls, a grid of celestial body cards, and a detail panel for diving deeper into individual objects.
In this step, you will build the full HTML structure that your CSS and JavaScript will hook into later. The key is giving each section meaningful IDs and classes so your code can find and manipulate them.
In this step, get ready to:
- Create the page layout with a header, filter controls, and content containers.
- Add a detail panel for viewing expanded celestial body information.
Create the page layout with header and filter controls
The page layout defines the core structure your users will see. It includes a header with the app title, a dropdown for filtering celestial bodies by type, and placeholder containers for loading states and the card grid.
- Switch back to index.html in Cursor.
- Paste the following code into the file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Universe Explorer</title>
<!-- Link to our stylesheet -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Page header with title and subtitle -->
<header>
<h1>Universe Explorer</h1>
<p class="subtitle">Explore the Solar System with real science data</p>
</header>
<main>
<!-- Filter controls for selecting body type -->
<div class="controls">
<label for="type-filter">Filter by type:</label>
<select id="type-filter">
<option value="all">All Bodies</option>
<option value="Star">Star</option>
<option value="Planet">Planet</option>
<option value="Dwarf Planet">Dwarf Planet</option>
<option value="Moon">Moon</option>
<option value="Asteroid">Asteroid</option>
<option value="Comet">Comet</option>
</select>
<!-- Counter showing how many bodies match the filter -->
<span id="body-count" class="count"></span>
</div>
<!-- Loading state shown while fetching data -->
<div id="loading" class="loading">Loading celestial data...</div>
<!-- Error message container, hidden by default -->
<div id="error" class="error" hidden></div>
<!-- Grid where celestial body cards will be rendered -->
<div id="bodies-grid" class="bodies-grid"></div>
</main>
<script src="app.js"></script>
</body>
</html>
What does this code do?
This HTML sets up the full page skeleton your dashboard needs.
- The <head> links to style.css so your styles apply as soon as you write them.
- The <select id="type-filter"> dropdown lists every body type the API returns (Star, Planet, Dwarf Planet, Moon, Asteroid, Comet).
- The loading div shows a message while data is being fetched. The error div stays hidden until something goes wrong.
- The bodies-grid container is where JavaScript will inject the celestial body cards later.
- Save index.html.
- Check your browser. You should see the text "Universe Explorer" at the top, "Explore the Solar System with real science data" below it, a dropdown labelled "Filter by type:", and "Loading celestial data..." further down.
Don't see anything in the browser?
- Make sure you saved the file. Unsaved changes won't appear in the browser.
- Confirm Live Server is still running and your browser is pointed at the correct address.
- Check for any typos in the opening <!DOCTYPE html> line or the <body> tag.
Why isn't my HTML page showing anything in the browser?
Add the detail panel
When a user clicks on a celestial body card, they should see an expanded view with detailed properties. This overlay panel sits outside <main> because it covers the entire viewport as a modal.
- In index.html, find this line:
<script src="app.js"></script>
- Add the following code above that script line:
<!-- Detail panel overlay for expanded body info -->
<div id="detail-panel" class="detail-panel" hidden>
<div class="detail-content">
<!-- Close button to dismiss the panel -->
<button id="close-detail" class="close-btn">×</button>
<!-- Container where JavaScript injects detail HTML -->
<div id="detail-body"></div>
</div>
</div>
What does this code do?
- The hidden attribute keeps the panel invisible until JavaScript removes it when a card is clicked.
- The close-detail button uses the × HTML entity which renders as a multiplication sign (x). This is a common pattern for close buttons.
- The empty detail-body div is where your JavaScript will inject a body's full properties (mass, volume, moons, discovery history) when a card is selected.
- Save index.html.
You won't see any visible change in the browser. The detail panel is hidden until JavaScript shows it when a user clicks a card.
Seeing broken layout or misplaced text?
- Make sure the detail panel code is placed between the closing </main> tag and the <script> tag. Placing it inside <main> will affect layout later.
- Check that you have not accidentally deleted the <script src="app.js"></script> line. It must remain at the bottom of the body.
My HTML structure seems broken after adding the detail panel. Help me fix it.
✔️ Awesome, I've got everything!
Great. Double check you have saved index.html before moving on.
ⓧ I'd like to double check the full code
Here is the complete index.html file. Make sure yours matches exactly:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Universe Explorer</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Universe Explorer</h1>
<p class="subtitle">Explore the Solar System with real science data</p>
</header>
<main>
<div class="controls">
<label for="type-filter">Filter by type:</label>
<select id="type-filter">
<option value="all">All Bodies</option>
<option value="Star">Star</option>
<option value="Planet">Planet</option>
<option value="Dwarf Planet">Dwarf Planet</option>
<option value="Moon">Moon</option>
<option value="Asteroid">Asteroid</option>
<option value="Comet">Comet</option>
</select>
<span id="body-count" class="count"></span>
</div>
<div id="loading" class="loading">Loading celestial data...</div>
<div id="error" class="error" hidden></div>
<div id="bodies-grid" class="bodies-grid"></div>
</main>
<div id="detail-panel" class="detail-panel" hidden>
<div class="detail-content">
<button id="close-detail" class="close-btn">×</button>
<div id="detail-body"></div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
Your HTML skeleton is ready. Next up, you will style this structure with a dark space theme using CSS Grid and custom properties to make it look like a real mission control dashboard.
Style the Dashboard with a Space Theme
Your HTML structure is ready, but right now it looks like a plain unstyled page. A space dashboard deserves a dark, immersive theme that feels like peering into the cosmos.
In this step, you will write all the CSS for your dashboard. You will use CSS Grid to lay out celestial body cards, add hover effects, style a detail panel overlay, and make everything responsive on smaller screens.
In this step, get ready to:
- Apply a dark space-themed background with a CSS Grid card layout.
- Style the body cards with hover effects and responsive sizing.
- Style the filter controls, detail panel, and loading/error states.
Apply a dark background with CSS Grid for the card layout
The foundation of the space theme is a dark background with cool blue accents. You will also set up the grid that will hold all the celestial body cards once data arrives.
- Switch back to style.css in Cursor.
- Add the base layout styles by pasting the following code:
/* Reset default browser spacing */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Dark space background for the whole page */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0a0e1a;
color: #e0e0e0;
min-height: 100vh;
}
/* Gradient header with subtle border */
header {
text-align: center;
padding: 2rem 1rem;
background: linear-gradient(135deg, #0d1b2a, #1b2838);
border-bottom: 1px solid #1e3a5f;
}
header h1 {
font-size: 2rem;
color: #7ec8e3;
margin-bottom: 0.25rem;
}
.subtitle {
color: #8899aa;
font-size: 0.95rem;
}
/* Center the main content area */
main {
max-width: 1200px;
margin: 0 auto;
padding: 1.5rem;
}
/* Grid layout for celestial body cards */
.bodies-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1rem;
}
What does this code do?
The CSS reset removes default browser margins and padding so you start from a clean slate.
The body background #0a0e1a is a deep navy that mimics outer space. The header uses a linear gradient to create depth, and the icy blue #7ec8e3 accent on the title evokes starlight.
The .bodies-grid uses auto-fill with minmax(260px, 1fr) so cards automatically wrap to fill available space. On wide screens you get 4 columns. On narrow screens, fewer.
- Save style.css.
- Refresh your browser. You should see a dark navy background, a gradient header with "Universe Explorer" in icy blue, and the loading text in teal below it.
Still seeing a white background?
- Make sure you saved style.css. Unsaved changes will not appear in the browser.
- Confirm that the <link rel="stylesheet" href="style.css"> line exists in your index.html head section.
Help me figure out why my CSS isn't loading.
Style the cards with hover effects and responsive sizing
Each celestial body will appear as a card in the grid. The cards need hover effects so users know they are clickable, and a responsive media query so the layout still works on phones.
- Add the card styles and responsive media query below your existing code in style.css:
/* Individual celestial body cards */
.body-card {
background: #1b2838;
border: 1px solid #2a4a6b;
border-radius: 10px;
padding: 1.25rem;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s;
}
/* Lift the card on hover to signal it's clickable */
.body-card:hover {
transform: translateY(-3px);
border-color: #7ec8e3;
}
.body-card h3 {
color: #7ec8e3;
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
/* Small uppercase badge showing body type */
.body-card .body-type {
display: inline-block;
background: #0d1b2a;
color: #8899aa;
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
text-transform: uppercase;
margin-bottom: 0.75rem;
}
/* Two-column grid for stats inside each card */
.body-card .stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.4rem;
font-size: 0.85rem;
}
.body-card .stats span {
color: #8899aa;
}
.body-card .stats strong {
color: #c0d0e0;
}
/* Responsive adjustments for small screens */
@media (max-width: 600px) {
header h1 {
font-size: 1.5rem;
}
.bodies-grid {
grid-template-columns: 1fr;
}
.detail-content {
padding: 1.5rem;
}
}
What does this code do?
The .body-card uses transition: transform 0.2s, border-color 0.2s so the hover effect animates smoothly. On hover, the card lifts 3 pixels upward and the border glows blue.
The @media (max-width: 600px) block collapses the grid to a single column on mobile, making each card full-width. It also shrinks the header text and reduces detail panel padding.
- Save style.css and refresh your browser.
- You will not see cards yet because no data has loaded. You can verify the grid is ready by opening DevTools (Cmd+Option+I on macOS or Ctrl+Shift+I on Windows), selecting the .bodies-grid element, and confirming it shows display: grid in the Styles panel.
Grid not showing in DevTools?
- Make sure the .bodies-grid selector in your CSS matches the class bodies-grid in your HTML. Class names are case-sensitive.
- Check that your CSS file has no unclosed braces from the previous section. A missing } will break all rules below it.
Help me debug my CSS Grid layout.
Style the filter dropdown, detail panel, and loading/error states
The filter dropdown needs to match the dark theme, the loading and error messages need clear visual treatment, and the detail panel overlay needs to dim the background when open.
- Add the filter and state styles below your existing code in style.css (above the @media query):
/* Filter controls bar */
.controls {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.controls label {
color: #8899aa;
font-size: 0.9rem;
}
/* Dark-themed dropdown */
.controls select {
background: #1b2838;
color: #e0e0e0;
border: 1px solid #2a4a6b;
border-radius: 6px;
padding: 0.5rem 1rem;
font-size: 0.9rem;
cursor: pointer;
}
.controls select:focus {
outline: none;
border-color: #7ec8e3;
}
/* Body count pushed to the right */
.count {
color: #8899aa;
font-size: 0.85rem;
margin-left: auto;
}
/* Loading spinner text */
.loading {
text-align: center;
padding: 3rem;
color: #7ec8e3;
font-size: 1.1rem;
}
/* Error message box */
.error {
text-align: center;
padding: 1.5rem;
background: #2a1a1a;
border: 1px solid #5a2a2a;
border-radius: 8px;
color: #e88;
margin-bottom: 1rem;
}
What does this code do?
The .controls bar uses flexbox with flex-wrap: wrap so the filter and count wrap gracefully on smaller screens.
The dropdown inherits the dark theme colors. The :focus state uses the same icy blue border as the card hover, keeping the accent color consistent across the UI.
The error state uses a warm red palette (#2a1a1a background, #e88 text) to stand out against the cool blue theme, making problems immediately visible.
- Save style.css and refresh your browser.
- You should see the "Filter by type:" label and dropdown styled in dark navy with a subtle blue border. The "Loading celestial data..." text should appear in teal, centered below the controls.
Dropdown still looks like the browser default?
- Check that .controls select has the background: #1b2838 property. Without an explicit background, browsers use their default white select styling.
- Ensure there are no typos in the selector. It must be .controls select (with a space), not .controls-select.
Help me fix my dropdown styling.
- Now add the detail panel overlay styles below the error styles (still above the @media query):
/* Full-screen overlay that dims the background */
.detail-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
padding: 1rem;
}
/* Scrollable content box inside the overlay */
.detail-content {
background: #1b2838;
border: 1px solid #2a4a6b;
border-radius: 12px;
padding: 2rem;
max-width: 600px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
position: relative;
}
/* X button in top-right corner */
.close-btn {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
color: #8899aa;
font-size: 1.5rem;
cursor: pointer;
}
.close-btn:hover {
color: #e0e0e0;
}
.detail-content h2 {
color: #7ec8e3;
margin-bottom: 0.25rem;
}
.detail-content .detail-type {
color: #8899aa;
font-size: 0.85rem;
margin-bottom: 1.5rem;
}
/* Sections within the detail view */
.detail-section {
margin-bottom: 1.25rem;
}
.detail-section h4 {
color: #7ec8e3;
font-size: 0.85rem;
text-transform: uppercase;
margin-bottom: 0.5rem;
border-bottom: 1px solid #2a4a6b;
padding-bottom: 0.25rem;
}
.detail-section p {
font-size: 0.9rem;
line-height: 1.6;
color: #c0d0e0;
}
/* Moon name tags */
.moons-list {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.moons-list span {
background: #0d1b2a;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
color: #8899aa;
}
What does this code do?
The .detail-panel is a fixed overlay covering the entire viewport. The semi-transparent black background (rgba(0, 0, 0, 0.85)) dims everything behind it, focusing the user's attention on the detail card.
The .detail-content box uses max-height: 80vh with overflow-y: auto so it scrolls internally when a body has lots of moons or long orbital data. The .moons-list uses flexbox with wrapping to display moon names as small tags.
- Save style.css and refresh your browser.
- The detail panel is hidden by default (the HTML has hidden on it), so you will not see it yet. Your page should still show the styled header, filter dropdown, and loading text on the dark background.
Seeing a gray overlay covering the page?
- If the detail panel is visible, check that your index.html has the hidden attribute on the <div id="detail-panel"> element. Without it, the overlay shows immediately.
- Verify that your CSS file has no unclosed braces before the .detail-panel block. A missing } will cascade incorrect styles.
Help me debug my detail panel styles.
Why a fixed overlay instead of a new page?
A fixed overlay keeps the user in context. They can dismiss it and return to their filtered grid immediately, without losing their place or reloading data.
Make sure your final style.css has all the sections in the correct order: reset, body, header, main, controls, loading, error, bodies-grid, body-card, detail-panel, detail-content and children, moons-list, and the media query at the bottom.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete style.css file. Compare yours against it:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0a0e1a;
color: #e0e0e0;
min-height: 100vh;
}
header {
text-align: center;
padding: 2rem 1rem;
background: linear-gradient(135deg, #0d1b2a, #1b2838);
border-bottom: 1px solid #1e3a5f;
}
header h1 {
font-size: 2rem;
color: #7ec8e3;
margin-bottom: 0.25rem;
}
.subtitle {
color: #8899aa;
font-size: 0.95rem;
}
main {
max-width: 1200px;
margin: 0 auto;
padding: 1.5rem;
}
.controls {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.controls label {
color: #8899aa;
font-size: 0.9rem;
}
.controls select {
background: #1b2838;
color: #e0e0e0;
border: 1px solid #2a4a6b;
border-radius: 6px;
padding: 0.5rem 1rem;
font-size: 0.9rem;
cursor: pointer;
}
.controls select:focus {
outline: none;
border-color: #7ec8e3;
}
.count {
color: #8899aa;
font-size: 0.85rem;
margin-left: auto;
}
.loading {
text-align: center;
padding: 3rem;
color: #7ec8e3;
font-size: 1.1rem;
}
.error {
text-align: center;
padding: 1.5rem;
background: #2a1a1a;
border: 1px solid #5a2a2a;
border-radius: 8px;
color: #e88;
margin-bottom: 1rem;
}
.bodies-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1rem;
}
.body-card {
background: #1b2838;
border: 1px solid #2a4a6b;
border-radius: 10px;
padding: 1.25rem;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s;
}
.body-card:hover {
transform: translateY(-3px);
border-color: #7ec8e3;
}
.body-card h3 {
color: #7ec8e3;
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
.body-card .body-type {
display: inline-block;
background: #0d1b2a;
color: #8899aa;
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
text-transform: uppercase;
margin-bottom: 0.75rem;
}
.body-card .stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.4rem;
font-size: 0.85rem;
}
.body-card .stats span {
color: #8899aa;
}
.body-card .stats strong {
color: #c0d0e0;
}
.detail-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
padding: 1rem;
}
.detail-content {
background: #1b2838;
border: 1px solid #2a4a6b;
border-radius: 12px;
padding: 2rem;
max-width: 600px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
position: relative;
}
.close-btn {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
color: #8899aa;
font-size: 1.5rem;
cursor: pointer;
}
.close-btn:hover {
color: #e0e0e0;
}
.detail-content h2 {
color: #7ec8e3;
margin-bottom: 0.25rem;
}
.detail-content .detail-type {
color: #8899aa;
font-size: 0.85rem;
margin-bottom: 1.5rem;
}
.detail-section {
margin-bottom: 1.25rem;
}
.detail-section h4 {
color: #7ec8e3;
font-size: 0.85rem;
text-transform: uppercase;
margin-bottom: 0.5rem;
border-bottom: 1px solid #2a4a6b;
padding-bottom: 0.25rem;
}
.detail-section p {
font-size: 0.9rem;
line-height: 1.6;
color: #c0d0e0;
}
.moons-list {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.moons-list span {
background: #0d1b2a;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
color: #8899aa;
}
@media (max-width: 600px) {
header h1 {
font-size: 1.5rem;
}
.bodies-grid {
grid-template-columns: 1fr;
}
.detail-content {
padding: 1.5rem;
}
}
Your dashboard looks like a proper space control room. Next up, you will write the JavaScript that fetches real celestial body data from the API and fills those cards with planets, moons, and asteroids.
Fetch and Display Celestial Bodies
Your HTML structure and CSS styling are ready. Now it's time to bring the dashboard to life with real data from the Solar System OpenData API.
In this step, you'll write JavaScript that fetches celestial body data, handles errors gracefully, and renders each body as a card in your grid.
In this step, get ready to:
- Write an authenticated API request to fetch celestial bodies.
- Parse JSON responses and render body cards with key stats.
- Handle errors with try/catch and display user-friendly messages.
Write the fetchBodies function
The fetch API is built into every modern browser. It lets you make HTTP requests to external servers and get data back as a Promise.
- In Cursor, switch back to your app.js file.
- Add the following code to set up your API connection and fetch function:
const API_BASE = 'https://api.le-systeme-solaire.net/rest';
const API_TOKEN = 'your-api-token-here';
let allBodies = [];
async function fetchBodies() {
// Grab references to DOM elements for loading, error, and grid
const loading = document.getElementById('loading');
const error = document.getElementById('error');
const grid = document.getElementById('bodies-grid');
// Show loading state and clear previous content
loading.hidden = false;
error.hidden = true;
grid.innerHTML = '';
try {
// Make authenticated GET request to the API
const response = await fetch(`${API_BASE}/bodies/`, {
headers: {
'Authorization': `Bearer ${API_TOKEN}`
}
});
// Check if the response status is OK
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
// Parse JSON and store bodies globally
const data = await response.json();
allBodies = data.bodies;
loading.hidden = true;
renderBodies(allBodies);
} catch (err) {
// Display a user-friendly error message
loading.hidden = true;
error.textContent = `Failed to load data: ${err.message}. Please check your API token and try again.`;
error.hidden = false;
}
}
What does this code do?
API_BASE stores the base URL for the Solar System OpenData API. API_TOKEN holds your personal bearer token for authentication.
The fetchBodies() function uses async/await to pause execution until the API responds. The Authorization header sends your token so the API knows you're allowed access.
The try/catch block wraps the entire request. If anything goes wrong (network failure, invalid token, server error), the catch block shows a helpful message in the error div instead of crashing silently.
- Replace your-api-token-here with the API token you generated from the Solar System OpenData site in the previous step.
What is a REST API?
A REST API is a way for programs to request data from a remote server using standard HTTP methods (like GET). You send a request to a URL, and the server sends back structured data (usually JSON).
The Solar System OpenData API requires a bearer token in the request header. This is a common pattern called API authentication. It proves you have permission to access the data.
You won't see a change in the browser yet. The function exists but hasn't been called, and it depends on renderBodies() which you'll write next.
Render celestial body cards
The API returns an array of celestial body objects. Each object has fields like englishName, bodyType, gravity, and avgTemp. You need a function that turns each object into a visible card on the page.
- Add the renderBodies() function and the fetchBodies() call below your existing code in app.js:
function renderBodies(bodies) {
const grid = document.getElementById('bodies-grid');
const countEl = document.getElementById('body-count');
// Clear existing cards and update the count display
grid.innerHTML = '';
countEl.textContent = `${bodies.length} bodies`;
// Create a card for each celestial body
bodies.forEach(body => {
const card = document.createElement('div');
card.className = 'body-card';
card.addEventListener('click', () => showDetail(body));
// Format gravity and temperature, showing N/A if data is missing
const gravity = body.gravity ? `${body.gravity} m/s²` : 'N/A';
const temp = body.avgTemp ? `${body.avgTemp} K` : 'N/A';
card.innerHTML = `
<h3>${body.englishName || body.name}</h3>
<span class="body-type">${body.bodyType || 'Unknown'}</span>
<div class="stats">
<span>Gravity: <strong>${gravity}</strong></span>
<span>Temp: <strong>${temp}</strong></span>
</div>
`;
grid.appendChild(card);
});
}
// Kick off the initial data fetch when the page loads
fetchBodies();
What does this code do?
renderBodies() loops through every celestial body in the array and creates a DOM element for each one. Each card shows the body's English name, type badge, gravity, and mean temperature.
The ternary operators (e.g. body.gravity ? ... : 'N/A') handle cases where data is missing. Not every celestial body has temperature or gravity data recorded.
The fetchBodies() call at the bottom runs automatically when the browser loads your script. This triggers the full chain: fetch data, parse it, render cards.
- Save app.js.
- Return to your browser. You should see a grid of celestial body cards filling the page, each showing a name, type badge, gravity value, and temperature.
Seeing an error message instead of cards?
If you see a red error box with a message about your API token, double check that you replaced your-api-token-here with the actual token you generated. The token should be a long string of characters with no extra spaces or quotes around it.
If nothing happens at all (blank page, no loading text), open your browser's DevTools console by pressing Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows) and look for red error messages. A syntax error in your JavaScript will prevent the entire file from running.
Help me debug why my celestial body cards aren't loading.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file and the cards are displaying in your browser.
ⓧ I'd like to double check the full code
Here's the complete app.js file so far:
const API_BASE = 'https://api.le-systeme-solaire.net/rest';
const API_TOKEN = 'your-api-token-here';
let allBodies = [];
async function fetchBodies() {
const loading = document.getElementById('loading');
const error = document.getElementById('error');
const grid = document.getElementById('bodies-grid');
loading.hidden = false;
error.hidden = true;
grid.innerHTML = '';
try {
const response = await fetch(`${API_BASE}/bodies/`, {
headers: {
'Authorization': `Bearer ${API_TOKEN}`
}
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
const data = await response.json();
allBodies = data.bodies;
loading.hidden = true;
renderBodies(allBodies);
} catch (err) {
loading.hidden = true;
error.textContent = `Failed to load data: ${err.message}. Please check your API token and try again.`;
error.hidden = false;
}
}
function renderBodies(bodies) {
const grid = document.getElementById('bodies-grid');
const countEl = document.getElementById('body-count');
grid.innerHTML = '';
countEl.textContent = `${bodies.length} bodies`;
bodies.forEach(body => {
const card = document.createElement('div');
card.className = 'body-card';
card.addEventListener('click', () => showDetail(body));
const gravity = body.gravity ? `${body.gravity} m/s²` : 'N/A';
const temp = body.avgTemp ? `${body.avgTemp} K` : 'N/A';
card.innerHTML = `
<h3>${body.englishName || body.name}</h3>
<span class="body-type">${body.bodyType || 'Unknown'}</span>
<div class="stats">
<span>Gravity: <strong>${gravity}</strong></span>
<span>Temp: <strong>${temp}</strong></span>
</div>
`;
grid.appendChild(card);
});
}
fetchBodies();
Your dashboard is now pulling real data from space. Next up, you'll wire up the filter dropdown so you can browse by body type.
Add Filtering by Body Type
Your dashboard loads and displays every celestial body in the solar system. That is a lot of cards to scroll through.
In this step, you will wire up the filter dropdown so learners can instantly narrow the grid to just planets, moons, asteroids, or any other body type. The filtering will happen entirely on the client side, reusing the data already stored in memory instead of making a new API call each time.
In this step, get ready to:
- Write a filter function that reads the dropdown value and narrows the displayed bodies.
- Connect the dropdown to your filter function so changes apply instantly.
- Confirm the count indicator updates to reflect the filtered results.
Write the filterBodies function
Your HTML already has a <select> dropdown with options for each body type. Right now, changing the dropdown does nothing because there is no JavaScript listening for that change.
Why filter on the client side?
Your fetchBodies() function already downloaded every celestial body into the allBodies array. Instead of calling the API again each time the user picks a type, you can filter that array in memory and re-render instantly.
This pattern is called client-side filtering. It makes the UI feel snappy because there is no network round-trip.
- In app.js, find the closing brace of the renderBodies function.
- Add the following filterBodies function below it:
function filterBodies() {
// Read the currently selected value from the dropdown
const filterValue = document.getElementById('type-filter').value;
if (filterValue === 'all') {
// Show every body when "All Bodies" is selected
renderBodies(allBodies);
} else {
// Keep only bodies whose bodyType matches the selection
const filtered = allBodies.filter(body => body.bodyType === filterValue);
renderBodies(filtered);
}
}
What does this code do?
- document.getElementById('type-filter').value reads whichever option the user picked from the dropdown (e.g. "Planet" or "all").
- If the value is 'all', the full allBodies array is passed to renderBodies to show everything again.
- Otherwise, .filter() creates a new array containing only bodies whose bodyType matches the selected value.
The count indicator updates automatically because renderBodies already writes the array length into the #body-count span every time it runs.
Connect the dropdown to the filter function
The function exists, but nothing calls it yet. You need an event listener that fires filterBodies every time the dropdown value changes.
- In app.js, find the fetchBodies(); call at the bottom of the file.
- Add this event listener directly above it:
// Listen for dropdown changes and re-filter the grid
document.getElementById('type-filter').addEventListener('change', filterBodies);
What does this code do?
This grabs the <select> element by its id and attaches a listener for the change event. Every time the user picks a new option, the browser automatically calls filterBodies().
- Save app.js.
- Return to your browser and use the **Filter by type** dropdown to select **Planet**.
- You should see the grid shrink to only planet cards, and the count should read 8 bodies.
- Switch back to **All Bodies** to confirm the full list returns.
Dropdown not filtering?
- Make sure the event listener line is outside the filterBodies function, not inside it. It should be at the top level of app.js.
- Check that the string 'type-filter' matches the id on your <select> in index.html exactly.
- Open your browser console (press F12) and look for error messages. A typo in getElementById will show a null reference error.
Help me debug why my filter dropdown isn't working.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.js before moving on.
ⓧ I'd like to double check the full code
Here is the complete app.js file at this point. Compare yours against it:
const API_BASE = 'https://api.le-systeme-solaire.net/rest';
const API_TOKEN = 'your-api-token-here';
let allBodies = [];
async function fetchBodies() {
const loading = document.getElementById('loading');
const error = document.getElementById('error');
const grid = document.getElementById('bodies-grid');
loading.hidden = false;
error.hidden = true;
grid.innerHTML = '';
try {
const response = await fetch(`${API_BASE}/bodies/`, {
headers: {
'Authorization': `Bearer ${API_TOKEN}`
}
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
const data = await response.json();
allBodies = data.bodies;
loading.hidden = true;
renderBodies(allBodies);
} catch (err) {
loading.hidden = true;
error.textContent = `Failed to load data: ${err.message}. Please check your API token and try again.`;
error.hidden = false;
}
}
function renderBodies(bodies) {
const grid = document.getElementById('bodies-grid');
const countEl = document.getElementById('body-count');
grid.innerHTML = '';
countEl.textContent = `${bodies.length} bodies`;
bodies.forEach(body => {
const card = document.createElement('div');
card.className = 'body-card';
card.addEventListener('click', () => showDetail(body));
const gravity = body.gravity ? `${body.gravity} m/s²` : 'N/A';
const temp = body.avgTemp ? `${body.avgTemp} K` : 'N/A';
card.innerHTML = `
<h3>${body.englishName || body.name}</h3>
<span class="body-type">${body.bodyType || 'Unknown'}</span>
<div class="stats">
<span>Gravity: <strong>${gravity}</strong></span>
<span>Temp: <strong>${temp}</strong></span>
</div>
`;
grid.appendChild(card);
});
}
function filterBodies() {
const filterValue = document.getElementById('type-filter').value;
if (filterValue === 'all') {
renderBodies(allBodies);
} else {
const filtered = allBodies.filter(body => body.bodyType === filterValue);
renderBodies(filtered);
}
}
document.getElementById('type-filter').addEventListener('change', filterBodies);
fetchBodies();
Your dashboard now lets users slice through hundreds of celestial bodies with a single dropdown change. Next up, you will build the detail view so clicking any card reveals its full scientific profile.
Build the Detail View
Your dashboard now fetches real space data and lets you filter by body type.
Each card shows a quick summary, but the API returns much richer data for every celestial body. In this step, you will build a detail panel that reveals mass, volume, orbital mechanics, discovery history, and a list of moons when a card is clicked.
In this step, get ready to:
- Create the showDetail function that populates the detail panel with formatted scientific data.
- Format mass and volume in scientific notation for readability.
- Add close functionality so users can dismiss the panel and return to the grid.
Create the detail view function
The API returns detailed properties for each celestial body, including mass stored as a value-and-exponent pair. Your showDetail function will format this raw data into readable sections and inject it into the detail panel.
- In app.js, find the closing brace of the filterBodies function.
- Add the showDetail function below it by pasting this code:
function showDetail(body) {
const panel = document.getElementById('detail-panel');
const detailBody = document.getElementById('detail-body');
// Format mass as scientific notation (e.g. 5.97 x 10^24 kg)
const massStr = body.mass
? `${body.mass.massValue} x 10<sup>${body.mass.massExponent}</sup> kg`
: 'Unknown';
// Format volume as scientific notation
const volStr = body.vol
? `${body.vol.volValue} x 10<sup>${body.vol.volExponent}</sup> km³`
: 'Unknown';
// Build moons list or show fallback text
const moonsHtml = body.moons && body.moons.length > 0
? `<div class="moons-list">${body.moons.map(m => `<span>${m.moon}</span>`).join('')}</div>`
: '<p>No known moons</p>';
detailBody.innerHTML = `
<h2>${body.englishName || body.name}</h2>
<p class="detail-type">${body.bodyType || 'Unknown'}</p>
<div class="detail-section">
<h4>Physical Properties</h4>
<p>
Mass: ${massStr}<br>
Volume: ${volStr}<br>
Density: ${body.density ? body.density + ' g/cm³' : 'Unknown'}<br>
Gravity: ${body.gravity ? body.gravity + ' m/s²' : 'Unknown'}<br>
Mean Radius: ${body.meanRadius ? body.meanRadius + ' km' : 'Unknown'}<br>
Temperature: ${body.avgTemp ? body.avgTemp + ' K' : 'Unknown'}
</p>
</div>
<div class="detail-section">
<h4>Orbital Properties</h4>
<p>
Semi-major Axis: ${body.semimajorAxis ? body.semimajorAxis.toLocaleString() + ' km' : 'Unknown'}<br>
Orbital Period: ${body.sideralOrbit ? body.sideralOrbit + ' days' : 'Unknown'}<br>
Eccentricity: ${body.eccentricity || 'Unknown'}<br>
Inclination: ${body.inclination ? body.inclination + '°' : 'Unknown'}
</p>
</div>
<div class="detail-section">
<h4>Discovery</h4>
<p>
Discovered by: ${body.discoveredBy || 'Unknown / Ancient'}<br>
Discovery date: ${body.discoveryDate || 'Unknown / Ancient'}
</p>
</div>
<div class="detail-section">
<h4>Moons (${body.moons ? body.moons.length : 0})</h4>
${moonsHtml}
</div>
`;
panel.hidden = false;
}
What does this code do?
The function takes a body object (the full API data for one celestial body) and builds an HTML string to display inside the detail panel.
- Mass and volume come from the API as a value-plus-exponent pair. The function formats them as readable scientific notation like "5.97 x 10^24 kg" using an HTML <sup> tag for the exponent.
- Each property uses a ternary check. If the value exists, it displays with units. If not, it shows "Unknown" so the panel never looks broken.
- The moons section maps over the body.moons array, pulling each moon's name from m.moon and wrapping it in a styled span.
- Setting panel.hidden = false reveals the full-screen overlay that was already in your HTML.
- Save app.js and refresh your browser.
- Click any celestial body card. A dark overlay should appear with a white panel showing the body's name, type, and property sections.
Panel not appearing when you click a card?
- Open your browser's console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows). Look for errors mentioning showDetail is not defined. This means the function was placed inside another function by mistake.
- Check that the function name is exactly showDetail with a capital D. The click handler in renderBodies already references this exact name.
- Make sure your template literal backticks are not accidentally regular quotes. The multi-line HTML string requires backtick characters.
Help me debug why my detail panel isn't showing when I click a card.
Add close functionality
The panel shows up, but right now there is no way to dismiss it. You need a close button and the ability to click the dark backdrop to return to the grid.
- In app.js, find the line that says:
document.getElementById('type-filter').addEventListener('change', filterBodies);
fetchBodies();
- Replace that block with the following code, which adds the closeDetail function and wires up the close event listeners:
// Dismiss the detail panel
function closeDetail() {
document.getElementById('detail-panel').hidden = true;
}
// Wire up filter and close interactions
document.getElementById('type-filter').addEventListener('change', filterBodies);
document.getElementById('close-detail').addEventListener('click', closeDetail);
document.getElementById('detail-panel').addEventListener('click', (e) => {
// Close only when clicking the dark backdrop, not the panel content
if (e.target === document.getElementById('detail-panel')) {
closeDetail();
}
});
fetchBodies();
What does this code do?
- The closeDetail function sets the panel's hidden attribute back to true, which hides the overlay instantly.
- The first event listener calls closeDetail when the X button (#close-detail) is clicked.
- The second listener checks if the click target is the backdrop itself (the outer #detail-panel div). This lets users click outside the white content box to close it, without accidentally closing when clicking inside the panel.
- Save app.js and refresh your browser.
- Click a card to open the detail panel. Then click the X button in the top-right corner of the panel. The overlay should disappear.
- Open a card again and click the dark area outside the white panel. It should also close.
Clicking the backdrop doesn't close the panel?
- Make sure the if condition compares e.target to the detail-panel element using === (strict equality). Using == can cause unexpected behavior.
- If the X button works but the backdrop does not, check that the event listener is attached to detail-panel (the outer overlay div), not detail-content (the inner white box).
Help me fix my detail panel close functionality.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.js before moving on.
ⓧ I'd like to double check the full code
Here is the complete app.js file at this point:
const API_BASE = 'https://api.le-systeme-solaire.net/rest';
const API_TOKEN = 'your-api-token-here';
let allBodies = [];
async function fetchBodies() {
const loading = document.getElementById('loading');
const error = document.getElementById('error');
const grid = document.getElementById('bodies-grid');
loading.hidden = false;
error.hidden = true;
grid.innerHTML = '';
try {
const response = await fetch(`${API_BASE}/bodies/`, {
headers: {
'Authorization': `Bearer ${API_TOKEN}`
}
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
const data = await response.json();
allBodies = data.bodies;
loading.hidden = true;
renderBodies(allBodies);
} catch (err) {
loading.hidden = true;
error.textContent = `Failed to load data: ${err.message}. Please check your API token and try again.`;
error.hidden = false;
}
}
function renderBodies(bodies) {
const grid = document.getElementById('bodies-grid');
const countEl = document.getElementById('body-count');
grid.innerHTML = '';
countEl.textContent = `${bodies.length} bodies`;
bodies.forEach(body => {
const card = document.createElement('div');
card.className = 'body-card';
card.addEventListener('click', () => showDetail(body));
const gravity = body.gravity ? `${body.gravity} m/s²` : 'N/A';
const temp = body.avgTemp ? `${body.avgTemp} K` : 'N/A';
card.innerHTML = `
<h3>${body.englishName || body.name}</h3>
<span class="body-type">${body.bodyType || 'Unknown'}</span>
<div class="stats">
<span>Gravity: <strong>${gravity}</strong></span>
<span>Temp: <strong>${temp}</strong></span>
</div>
`;
grid.appendChild(card);
});
}
function filterBodies() {
const filterValue = document.getElementById('type-filter').value;
if (filterValue === 'all') {
renderBodies(allBodies);
} else {
const filtered = allBodies.filter(body => body.bodyType === filterValue);
renderBodies(filtered);
}
}
function showDetail(body) {
const panel = document.getElementById('detail-panel');
const detailBody = document.getElementById('detail-body');
const massStr = body.mass
? `${body.mass.massValue} x 10<sup>${body.mass.massExponent}</sup> kg`
: 'Unknown';
const volStr = body.vol
? `${body.vol.volValue} x 10<sup>${body.vol.volExponent}</sup> km³`
: 'Unknown';
const moonsHtml = body.moons && body.moons.length > 0
? `<div class="moons-list">${body.moons.map(m => `<span>${m.moon}</span>`).join('')}</div>`
: '<p>No known moons</p>';
detailBody.innerHTML = `
<h2>${body.englishName || body.name}</h2>
<p class="detail-type">${body.bodyType || 'Unknown'}</p>
<div class="detail-section">
<h4>Physical Properties</h4>
<p>
Mass: ${massStr}<br>
Volume: ${volStr}<br>
Density: ${body.density ? body.density + ' g/cm³' : 'Unknown'}<br>
Gravity: ${body.gravity ? body.gravity + ' m/s²' : 'Unknown'}<br>
Mean Radius: ${body.meanRadius ? body.meanRadius + ' km' : 'Unknown'}<br>
Temperature: ${body.avgTemp ? body.avgTemp + ' K' : 'Unknown'}
</p>
</div>
<div class="detail-section">
<h4>Orbital Properties</h4>
<p>
Semi-major Axis: ${body.semimajorAxis ? body.semimajorAxis.toLocaleString() + ' km' : 'Unknown'}<br>
Orbital Period: ${body.sideralOrbit ? body.sideralOrbit + ' days' : 'Unknown'}<br>
Eccentricity: ${body.eccentricity || 'Unknown'}<br>
Inclination: ${body.inclination ? body.inclination + '°' : 'Unknown'}
</p>
</div>
<div class="detail-section">
<h4>Discovery</h4>
<p>
Discovered by: ${body.discoveredBy || 'Unknown / Ancient'}<br>
Discovery date: ${body.discoveryDate || 'Unknown / Ancient'}
</p>
</div>
<div class="detail-section">
<h4>Moons (${body.moons ? body.moons.length : 0})</h4>
${moonsHtml}
</div>
`;
panel.hidden = false;
}
function closeDetail() {
document.getElementById('detail-panel').hidden = true;
}
document.getElementById('type-filter').addEventListener('change', filterBodies);
document.getElementById('close-detail').addEventListener('click', closeDetail);
document.getElementById('detail-panel').addEventListener('click', (e) => {
if (e.target === document.getElementById('detail-panel')) {
closeDetail();
}
});
fetchBodies();
Secret mission
Add a "Compare Bodies" Feature
Your Universe Explorer lets you browse and inspect celestial bodies one at a time. But what if you could pick two bodies and see how they stack up against each other? Build a comparison mode that highlights which body wins on gravity, radius, temperature, and more.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely locally, so there are no ongoing costs. Your API token from the Solar System OpenData API is free to keep.
Resources you used:
- Project folder on your Desktop containing index.html, style.css, and app.js.
- Solar System OpenData API token (free, no billing).
Keep everything running
No action needed. Choose this if you're still actively building or want to experiment further with the dashboard.
- Your project files stay on your Desktop, ready to open any time.
- Your API token remains active for future requests to the Solar System OpenData API.
- Cursor stays installed on your machine.
Pause - I'll come back to this later
Since this project is local with no running servers or paid services, there is nothing to pause. Your files and API token will be waiting for you whenever you return.
- Close the browser tab running your dashboard.
- Close Cursor if you want to free up memory.
Delete - I don't want to use this again
Remove all project files from your machine. This cannot be undone.
- Close any browser tabs and Cursor windows using the project.
- Delete your project folder by running this command in your terminal:
macOS
rm -rf ~/Desktop/universe-explorer
Windows
rmdir /s /q %USERPROFILE%\Desktop\universe-explorer
- Your API token is free and does not need to be revoked. If you prefer, you can discard the token value you saved and generate a new one at any time.
Nice Work!
Nice Work!
Nice work! You just built a Universe Explorer dashboard that pulls real astronomical data from the Solar System OpenData API and presents it in a polished, interactive interface.
You've learned how to:
- Made authenticated API requests using the fetch API with bearer token headers, and handled errors gracefully with try/catch.
- Parsed complex JSON responses and rendered them into a responsive CSS Grid of cards with client-side filtering by body type.
- Built an interactive detail view that displays mass, volume, orbital properties, discovery history, and moon lists for any celestial body.
- Added a Compare Bodies feature that lets users select two celestial bodies and view their properties side-by-side with winner highlighting.
Ready to quiz yourself?