Earthquake Activity Dashboard

Build a live earthquake dashboard with React, Recharts, and the USGS API.

Introduction

30 Second Summary

Every day, thousands of earthquakes ripple across the planet. Most go unnoticed, but the raw data is publicly available through a government feed that anyone can tap into.

In this project, you will build a React-powered Earthquake Activity Dashboard using live USGS data. You will fetch real earthquake events, filter them by magnitude and date, and visualize magnitude distribution with an interactive bar chart.

What You'll Build

Open your finished dashboard and you see recent earthquakes from around the world laid out in a filterable table, stat cards showing total count and average magnitude, and a bar chart that reveals how quakes cluster across magnitude ranges.

By the end of this project, you'll have:

  • A filter panel where you adjust magnitude, date range, and sort order, then watch the entire dashboard re-query the USGS API and refresh with new results.
  • Stat cards and a Recharts bar chart that turn raw earthquake rows into instant visual insight, showing total count, average magnitude, strongest quake, and magnitude distribution at a glance.
  • A custom React hook and multi-component architecture that cleanly separate data fetching, filtering, statistics, and display.
  • An optional Secret Mission to push your skills further.

Are there any prerequisites?

This project assumes beginner-to-intermediate React experience: components, props, useState, useEffect, conditional rendering, lists/keys, and basic API calls. You will also need Node.js and VS Code installed.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up the Project and Install Recharts

Building a dashboard that fetches live data and renders interactive charts requires a solid foundation. You need a React app to manage the UI, and a charting library to turn numbers into visuals.

In this step, you will scaffold a new React project using Vite (a fast build tool), install Recharts for data visualization, and clear out the default starter content so you have a clean slate.

In this step, get ready to:
  • Scaffold a Vite + React project and install dependencies.
  • Install Recharts, the charting library you will use throughout this project.
  • Clear the default starter content and verify the dev server runs.
Scaffold a new Vite + React project
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your earthquake dashboard project by running this command:
npm create vite@latest earthquake-dashboard -- --template react

What does this command do?

npm create vite@latest downloads and runs Vite's project scaffolding tool.

The --template react flag tells it to generate a React project with JavaScript. The -- before it separates npm's arguments from Vite's arguments.

npm asks to install a package

npm may prompt you to install the create-vite package. Enter y and press Enter to proceed.

The scaffold may also ask whether to install dependencies and start the dev server. Enter No for now. You will install dependencies manually in the next command so you can see the output.

You should see a message confirming the project was scaffolded in the earthquake-dashboard folder.

  • Move into your new project folder and install its dependencies by running these commands:
cd earthquake-dashboard
npm install

You should see output listing the installed packages, ending with a line like "added X packages".

Seeing an EBADENGINE warning?

This warning means your Node.js version is older than what Vite requires. Vite needs Node.js version 20.19+ or 22.12+. Check your version by running node --version and upgrade if needed.

help me fix an EBADENGINE warning when installing Vite

  • Open your project in VS Code by running:
code .

code command not found?

If code . does not work, open VS Code manually.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the earthquake-dashboard folder, and click Open.

help me set up the code command in my terminal

Install Recharts

Recharts is a React charting library that uses a declarative JSX API. You build charts the same way you build React components, by composing <BarChart>, <Bar>, and <XAxis> elements.

  • Open a terminal inside VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
  • Install Recharts and its peer dependency by running:
npm install recharts react-is

Why install react-is separately?

Recharts depends on react-is, a utility package from the React team that helps identify React element types. It is a peer dependency, meaning Recharts expects you to provide it. Including it in the install command ensures it matches your React version.

You should see output confirming additional packages were added.

Install failed?

Make sure your terminal is inside the earthquake-dashboard folder. Run pwd (macOS/Linux) or cd (Windows) to check your current directory. You should see a path ending in earthquake-dashboard.

help me troubleshoot a failed npm install for recharts

Clear the starter content and verify the dev server

Vite's default template includes demo content (logos, a counter, sample CSS) that you do not need. You will replace it with a clean starting point for the dashboard.

  • In the VS Code sidebar, click on src/App.jsx to open it.
  • Select all the existing content by pressing Cmd+A (macOS) or Ctrl+A (Windows), then replace it with this code:
// Main App component - starting point for the dashboard
function App() {
  return (
    <div>
      <h1>Earthquake Dashboard</h1>
    </div>
  );
}

export default App;

What does this code do?

This is the simplest possible React component. It renders a single <h1> heading that says "Earthquake Dashboard". You will build on this component throughout the project, adding filters, stat cards, charts, and a data table.

  • Save App.jsx by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Open src/App.css from the sidebar. Select all content (Cmd+A / Ctrl+A) and delete it so the file is empty, then save.
  • Open src/index.css and do the same. Select all, delete, and save so the file is empty.

Before you run the dev server, what do you think you will see in the browser?

  • Start the dev server by running this command in your VS Code terminal:
npm run dev

The terminal shows a local URL. Open http://localhost:5173 in your browser. You should see a plain page with just the text "Earthquake Dashboard" as a heading.

Keep the dev server running

Leave this terminal running for the rest of the project. Vite automatically refreshes your browser whenever you save a file, so you will see changes instantly. If you need to run other commands, open a second terminal in VS Code by pressing Ctrl+` again.

Don't see the heading?

Make sure you saved all three files (App.jsx, App.css, index.css). Unsaved changes will not appear in the browser.

If you see errors in the terminal, check that App.jsx has the exact code shown above. A missing parenthesis or curly brace will break the component.

help me debug my Vite React app not showing in the browser

✔️ Awesome, I've got everything!

Great. Double check that you have saved all your files before moving on.

ⓧ I'd like to double check the full code

Here is the complete src/App.jsx you should have at this point. The src/App.css and src/index.css files should both be completely empty.

function App() {
  return (
    <div>
      <h1>Earthquake Dashboard</h1>
    </div>
  );
}

export default App;

Your src/main.jsx file was generated by Vite and should look like this:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.jsx";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <App />
  </StrictMode>
);

Your project is scaffolded, Recharts is installed, and the dev server is running. Next up, you will fetch live earthquake data from the USGS API and display it in a table.

Fetch and Display Raw Earthquake Data

Your React project is up and running, but the dashboard is just a heading with nothing behind it. Before you can filter earthquakes by magnitude or chart their distribution, you need actual earthquake data on screen.

In this step, you will fetch live earthquake data from the USGS government API using a custom React hook. Then you will build a table component to display real earthquakes in your browser.

In this step, get ready to:
  • Create a useEarthquakes custom hook to fetch real earthquake data from the USGS API.
  • Build a QuakeTable component that displays earthquakes in a table with location, magnitude, depth, and time.
  • Wire the hook and component into App.jsx to see live data in your browser.
Create the useEarthquakes custom hook

A custom hook is a regular JavaScript function whose name starts with use. It lets you extract stateful logic (like fetching data) out of a component so multiple components can reuse it.

The USGS provides a free, public earthquake API that returns GeoJSON data. No API key or account is required.

  • In the VS Code Explorer sidebar, right-click the src folder and select New File.
  • Type hooks/useEarthquakes.js as the filename and press Enter. VS Code creates the hooks folder automatically.
  • Set up the hook shell by pasting this code into the new file:
import { useState, useEffect } from "react";

// USGS earthquake API endpoint
const BASE_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query";

export function useEarthquakes(filters) {
  // Track earthquake data, loading status, and any errors
  const [quakes, setQuakes] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // Fetch logic goes here next

  return { quakes, loading, error };
}

What does this code do?

  • BASE_URL stores the USGS earthquake API endpoint. Every request hits this URL with different query parameters.
  • Three pieces of state track the data: quakes holds the earthquake array, loading tracks whether a fetch is in progress, and error captures any failure message.
  • The function returns all three values so any component that calls this hook can react to each state.

The hook shell is in place, but it never actually fetches data. Time to add the fetch logic.

  • In useEarthquakes.js, find the comment // Fetch logic goes here next.
  • Replace that comment with this useEffect block:
  useEffect(() => {
    // Build the API query parameters
    const params = new URLSearchParams({
      format: "geojson",
      limit: "50",
      orderby: "time",
    });

    setLoading(true);
    setError(null);

    // Fetch earthquake data and update state
    fetch(`${BASE_URL}?${params}`)
      .then((res) => {
        if (!res.ok) {
          throw new Error(`API error: ${res.status}`);
        }
        return res.json();
      })
      .then((data) => {
        setQuakes(data.features);
        setLoading(false);
      })
      .catch((err) => {
        setError(err.message);
        setLoading(false);
      });
  }, []);

What does this effect do?

  • URLSearchParams builds the query string with three parameters: format=geojson for a JSON response, orderby=time for newest-first, and limit=50 to cap the results.
  • The fetch call hits the USGS endpoint and stores the features array from the GeoJSON response. Each feature is one earthquake event.
  • The empty dependency array [] means this effect runs once when the component mounts. You will add filter dependencies in a later step.
  • Save useEarthquakes.js.

Seeing a red underline on the import?

If VS Code shows a warning on the import line, make sure the filename is useEarthquakes.js (not .jsx). This file contains plain JavaScript with no JSX, so it uses the .js extension.

help me fix a warning in useEarthquakes.js

✔️ Awesome, I've got everything!

Great. Make sure you have saved useEarthquakes.js before moving on.

ⓧ I'd like to double check the full code

Scroll up and confirm your useEarthquakes.js contains these two sections in order:

  • The hook shell: imports at the top, BASE_URL constant, the useEarthquakes function with three useState calls, and a return statement at the bottom.
  • The useEffect block between the state declarations and the return, containing URLSearchParams, the fetch call with .then() and .catch(), and an empty dependency array [].
Build the QuakeTable component

The hook fetches data, but nothing displays it yet. You need a component that takes the earthquake array and renders it as a readable table.

  • In the Explorer sidebar, right-click the src folder and select New File.
  • Type components/QuakeTable.jsx and press Enter. VS Code creates the components folder automatically.
  • Set up the table structure by pasting this code into the new file:
function QuakeTable({ quakes }) {
  return (
    <div className="table-container">
      <h2>Earthquake Events</h2>
      <table className="quake-table">
        <thead>
          <tr>
            <th>Location</th>
            <th>Magnitude</th>
            <th>Depth (km)</th>
            <th>Time</th>
          </tr>
        </thead>
        <tbody>
          {/* Earthquake rows will be mapped here */}
        </tbody>
      </table>
    </div>
  );
}

export default QuakeTable;

What does this code do?

  • QuakeTable receives a quakes array as a prop. Each item in the array is one earthquake event from the USGS API.
  • The <thead> defines four columns: Location, Magnitude, Depth, and Time. The <tbody> is empty for now. You will add the row mapping next.

The table headers are set, but the body is empty. Each earthquake in the USGS response is a GeoJSON feature with data tucked inside properties and geometry.

  • In QuakeTable.jsx, find the comment {/* Earthquake rows will be mapped here */} inside the <tbody>.
  • Replace that comment with this mapping code:
          {quakes.map((q) => (
            <tr key={q.id}>
              <td>{q.properties.place || "Unknown"}</td>
              <td>{q.properties.mag != null ? q.properties.mag.toFixed(1) : "N/A"}</td>
              <td>
                {q.geometry.coordinates[2] != null
                  ? q.geometry.coordinates[2].toFixed(1)
                  : "N/A"}
              </td>
              <td>{new Date(q.properties.time).toLocaleString()}</td>
            </tr>
          ))}

Where does each column's data come from?

  • Location comes from q.properties.place. It falls back to "Unknown" if the USGS data is missing a place name.
  • Magnitude comes from q.properties.mag, rounded to one decimal place.
  • Depth is the third coordinate in q.geometry.coordinates[2]. GeoJSON stores coordinates as [longitude, latitude, depth].
  • Time comes from q.properties.time, a Unix timestamp in milliseconds. new Date(...).toLocaleString() converts it to a human-readable date.
  • Save QuakeTable.jsx.

Seeing a syntax error in the JSX?

Make sure the mapping code sits between the opening <tbody> and closing </tbody> tags, and that the original comment has been fully removed. A stray {/* left behind will break the JSX.

help me fix a syntax error in QuakeTable.jsx

✔️ Awesome, I've got everything!

Great. Make sure you have saved QuakeTable.jsx before moving on.

ⓧ I'd like to double check the full code

Scroll up and confirm your QuakeTable.jsx contains these two sections in order:

  • The component function returning a <div> with a heading and a <table> that has a <thead> with four column headers (Location, Magnitude, Depth, Time).
  • Inside <tbody>, a quakes.map() that renders a <tr> for each earthquake with four <td> cells pulling from properties and geometry.
Wire the hook and table into App.jsx

You have a hook that fetches data and a component that renders it. Now you need to connect them inside App.jsx so the dashboard actually shows earthquake data.

  • Switch back to App.jsx in VS Code.
  • Replace the entire contents of the file with this code:
import { useEarthquakes } from "./hooks/useEarthquakes";
import QuakeTable from "./components/QuakeTable";
import "./App.css";

function App() {
  // Fetch earthquake data using the custom hook
  const { quakes, loading, error } = useEarthquakes();

  return (
    <div className="dashboard">
      <header className="dashboard-header">
        <h1>Earthquake Dashboard</h1>
      </header>
      {/* Show loading, error, or the earthquake table */}
      {loading && <p>Loading...</p>}
      {error && <p className="error-message">Error: {error}</p>}
      {!loading && !error && <QuakeTable quakes={quakes} />}
    </div>
  );
}

export default App;

What does this code do?

  • useEarthquakes() is called at the top of the component. It kicks off the API fetch and returns quakes, loading, and error.
  • Three conditional renders handle each state: a "Loading..." message while the fetch is in progress, an error message if something goes wrong, and the QuakeTable when data arrives successfully.
  • Save App.jsx.

The table will render, but it will look unstyled. Add some basic CSS to make it readable.

  • Open src/App.css in VS Code. The file is currently empty.
  • Add the table and layout styles by pasting this CSS:
.dashboard {
  max-width: 1100px;
  margin: 0 auto;
  padding: 24px 16px;
}

.table-container {
  background: #fff;
  padding: 20px;
  border-radius: 8px;
  overflow-x: auto;
}

.quake-table {
  width: 100%;
  border-collapse: collapse;
}

.quake-table th, .quake-table td {
  padding: 10px 12px;
  border-bottom: 1px solid #eee;
  font-size: 0.9rem;
  text-align: left;
}

.error-message {
  color: red;
}

What do these styles do?

  • .dashboard centers the content and caps it at 1100px so the table does not stretch across ultra-wide screens.
  • .table-container gives the table a white card background. overflow-x: auto adds horizontal scrolling on narrow screens.
  • .quake-table collapses cell borders and adds padding so rows are easy to scan.
  • Save App.css.

Before you check the browser, take a moment to predict what you will see. You have a hook that fetches 50 recent earthquakes, a table component that maps them into rows, and an App that shows loading, error, or the table. What do you expect?

  • Return to your browser at http://localhost:5173. The page should refresh automatically thanks to Vite's hot module replacement.

You should see a brief "Loading..." flash, then a table with four columns: Location, Magnitude, Depth (km), and Time. The table is populated with real, recent earthquakes from around the world.

Seeing a blank page or an error?

  • Check your browser's developer console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows) for error messages. A common cause is a typo in the import path.
  • Confirm the file at src/hooks/useEarthquakes.js uses the .js extension, matching the import in App.jsx.
  • If you see "Loading..." stuck on screen, your network might be blocking the USGS API. Try opening https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&limit=5 directly in your browser to confirm you can reach the API.

help me debug why my earthquake table is not showing up

Your dashboard is pulling live earthquake data from the USGS and displaying it in a table. But 50 unfiltered earthquakes are hard to make sense of. Next up, you will add a filter panel so you can narrow by magnitude, date range, and sort order.

Add Filters That Actually Change the Data

Your earthquake table is live, pulling real data from the USGS and displaying it. That's a solid foundation to build on.

Right now, the dashboard dumps 50 unfiltered earthquakes with no way to narrow them down. You can't answer basic questions like "how many magnitude 4+ earthquakes happened this week?" In this step, you'll build a filter panel that re-queries the USGS API with different parameters every time the user changes a filter.

In this step, get ready to:
  • Create a FilterPanel component with magnitude, date range, and sort controls.
  • Update the useEarthquakes hook to build API URLs from filter parameters.
  • Wire filter state through App.jsx so applying filters re-fetches data.
Create the FilterPanel component

The filter panel needs to hold its own local form state so the user can adjust multiple controls before committing. When they click Apply Filters, the component sends all the filter values up to the parent at once.

  • Create a new file at src/components/FilterPanel.jsx and paste this code to set up the component with a magnitude slider:
import { useState } from "react";

function FilterPanel({ currentFilters, onApply }) {
  // Local copy so controls update without triggering API calls
  const [localFilters, setLocalFilters] = useState({ ...currentFilters });

  function handleChange(field, value) {
    setLocalFilters((prev) => ({ ...prev, [field]: value }));
  }

  function handleSubmit(e) {
    e.preventDefault();
    onApply(localFilters);
  }

  return (
    <form className="filter-panel" onSubmit={handleSubmit}>
      <div className="filter-group">
        <label htmlFor="minmag">Min Magnitude: {localFilters.minmagnitude}</label>
        <input id="minmag" type="range" min="0" max="8" step="0.5"
          value={localFilters.minmagnitude}
          onChange={(e) => handleChange("minmagnitude", Number(e.target.value))}
        />
      </div>
      <button type="submit" className="apply-button">Apply Filters</button>
    </form>
  );
}

export default FilterPanel;

What does this code do?

The component receives currentFilters as a prop and copies them into localFilters state. This way, dragging the slider updates the display instantly without firing an API call on every tiny movement.

The handleSubmit function prevents the default form reload and passes the current filter values up to the parent through onApply.

The magnitude slider is just one filter. The panel also needs date range inputs and a sort order dropdown.

  • In FilterPanel.jsx, find the line that says <button type="submit".
  • Add these three filter groups directly above that button line:
      <div className="filter-group">
        <label htmlFor="startdate">Start Date</label>
        <input
          id="startdate"
          type="date"
          value={localFilters.starttime}
          onChange={(e) => handleChange("starttime", e.target.value)}
        />
      </div>
      <div className="filter-group">
        <label htmlFor="enddate">End Date</label>
        <input
          id="enddate"
          type="date"
          value={localFilters.endtime}
          onChange={(e) => handleChange("endtime", e.target.value)}
        />
      </div>
      <div className="filter-group">
        <label htmlFor="sortorder">Sort By</label>
        <select
          id="sortorder"
          value={localFilters.orderby}
          onChange={(e) => handleChange("orderby", e.target.value)}
        >
          <option value="time">Newest First</option>
          <option value="time-asc">Oldest First</option>
          <option value="magnitude">Strongest First</option>
          <option value="magnitude-asc">Weakest First</option>
        </select>
      </div>

What are these controls?

  • Two date inputs let the user pick a start and end date. The USGS API accepts these as starttime and endtime query parameters.
  • A select dropdown maps user-friendly labels like "Newest First" to API values like time and magnitude.
  • Save FilterPanel.jsx.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

Your FilterPanel.jsx should have these sections in order. Scroll up and confirm each one is present:

  • The import { useState } line at the top.
  • The localFilters state, handleChange, and handleSubmit functions.
  • A form with four filter groups: magnitude range, start date, end date, and sort order.
  • The Apply Filters submit button at the end of the form.
  • The export default FilterPanel line at the bottom.

The FilterPanel needs some CSS to lay out the controls in a row. You'll add these styles to App.css.

  • In src/App.css, find the closing } of the .dashboard rule.
  • Add these styles directly below it, before the .table-container rule:
/* Filter panel layout: horizontal row of controls */
.filter-panel {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: flex-end;
  background: #fff;
  padding: 16px;
  border-radius: 8px;
  margin-bottom: 24px;
}

/* Each filter input and its label */
.filter-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
  • Add these remaining filter styles directly below the .filter-group rule you just pasted:
.filter-group label {
  font-size: 0.8rem;
  font-weight: 600;
  color: #555;
}

.filter-group input[type="range"] { width: 140px; }

.filter-group input[type="date"],
.filter-group select {
  padding: 6px 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 0.85rem;
}

.apply-button {
  padding: 8px 20px;
  background: #4a90d9;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.85rem;
  font-weight: 600;
}

What do these styles do?

The .filter-panel uses flexbox to lay out the controls in a horizontal row that wraps onto new lines on smaller screens. Each .filter-group stacks a label above its input.

  • Save App.css.
  • Confirm the new component file exists by running:
ls src/components/

You should see both FilterPanel.jsx and QuakeTable.jsx listed.

Don't see FilterPanel.jsx?

  • Make sure you created the file inside the src/components/ folder, not in src/ or the project root.
  • Check that the filename is exactly FilterPanel.jsx with a capital F and P.

Help me find where my FilterPanel.jsx file ended up.

Update the hook to build dynamic API URLs

The useEarthquakes hook already accepts a filters parameter, but it ignores it completely. The query parameters are hardcoded. You need to build the API URL dynamically so every filter the user picks translates into a real API query.

  • In src/hooks/useEarthquakes.js, find the URLSearchParams block inside useEffect:
    const params = new URLSearchParams({
      format: "geojson",
      limit: "50",
      orderby: "time",
    });
  • Replace that block with this version that reads from the filters parameter:
    const params = new URLSearchParams({
      format: "geojson",
      limit: "200",
      minmagnitude: String(filters.minmagnitude),
      starttime: filters.starttime,
      endtime: filters.endtime,
      orderby: filters.orderby,
    });
  • Now scroll to the bottom of the useEffect call and find the empty dependency array:
  }, []);
  • Replace it with the filter values so the hook re-fetches whenever a filter changes:
  }, [filters.minmagnitude, filters.starttime, filters.endtime, filters.orderby]);

Why list each filter value separately?

Listing filters.minmagnitude, filters.starttime, etc. instead of just filters avoids unnecessary re-fetches. React compares each value individually, so the effect only re-runs when an actual filter value changes, not when a new object reference is created.

  • Save useEarthquakes.js.
Wire filter state in App.jsx

The FilterPanel exists and the hook knows how to use filters. Now you need to connect them through App.jsx: create the filter state, pass it to the hook, and render the FilterPanel.

  • In src/App.jsx, replace the import lines at the top with these updated imports, and add the getDefaultDates helper function below them:
import { useState } from "react";
import { useEarthquakes } from "./hooks/useEarthquakes";
import FilterPanel from "./components/FilterPanel";
import QuakeTable from "./components/QuakeTable";
import "./App.css";

// Calculate default date range: today minus 7 days
function getDefaultDates() {
  const end = new Date();
  const start = new Date();
  start.setDate(start.getDate() - 7);
  return {
    starttime: start.toISOString().split("T")[0],
    endtime: end.toISOString().split("T")[0],
  };
}

What does getDefaultDates do?

This helper calculates today's date and 7 days ago, then formats both as YYYY-MM-DD strings. The USGS API expects dates in this ISO format for the starttime and endtime parameters.

  • Replace the entire App function (from function App() through export default App) with this updated version:
function App() {
  // Set default filters: magnitude 2+, last 7 days, newest first
  const defaultDates = getDefaultDates();
  const [filters, setFilters] = useState({
    minmagnitude: 2,
    starttime: defaultDates.starttime,
    endtime: defaultDates.endtime,
    orderby: "time",
  });
  // Pass filters to the hook so it queries with these parameters
  const { quakes, loading, error } = useEarthquakes(filters);

  // Called when the user clicks Apply Filters
  function handleApplyFilters(newFilters) {
    setFilters(newFilters);
  }

  return (
    <div className="dashboard">
      <header className="dashboard-header">
        <h1>Earthquake Dashboard</h1>
      </header>
      <FilterPanel currentFilters={filters} onApply={handleApplyFilters} />
      {loading && <p>Loading...</p>}
      {error && <p className="error-message">Error: {error}</p>}
      {!loading && !error && <QuakeTable quakes={quakes} />}
    </div>
  );
}

export default App;

How does the data flow?

  • The filters state lives in App and flows down to both useEarthquakes (which uses them to build the API URL) and FilterPanel (which displays them as the initial control values).
  • When the user clicks Apply Filters, handleApplyFilters updates the filters state, which triggers the hook's useEffect to re-fetch from the USGS API with the new parameters.
  • Save App.jsx.

Before you check the browser, think about this: what do you expect to see when the page reloads with the default filters set to magnitude 2+ and the last 7 days?

  • Switch to your browser. The page should auto-refresh thanks to Vite's dev server.

You should see a row of filter controls above the earthquake table: a magnitude slider, two date pickers, a sort dropdown, and a blue Apply Filters button. The table loads with magnitude 2+ earthquakes from the past week.

Now for the real test. Do you think changing the minimum magnitude to 4.0 and clicking Apply Filters will change the data, or just the slider position?

  • Drag the Min Magnitude slider to 4.0.
  • Click Apply Filters.

The table reloads with far fewer earthquakes, all magnitude 4.0 or higher. The filter panel re-queried the USGS API with a minmagnitude=4 parameter, and the API returned only matching results.

Filters not changing the data?

  • Check that you updated the dependency array in useEarthquakes.js from [] to the array with all four filter values. An empty array means the effect only runs once.
  • Make sure App.jsx passes filters to useEarthquakes(filters), not useEarthquakes() with no argument.
  • Open the browser's DevTools console and check for error messages. A typo in a filter property name (like minMagnitude instead of minmagnitude) can cause the API to ignore the parameter.

Help me debug why my earthquake filter panel isn't updating the table data.

✔️ Awesome, I've got everything!

Great. Make sure all three files are saved before moving on.

ⓧ I'd like to double check the full code

Each file should contain these sections. Scroll up through your code and confirm they match:

App.jsx:

  • Imports for useState, useEarthquakes, FilterPanel, QuakeTable, and App.css.
  • The getDefaultDates function that calculates start and end dates.
  • The App function with filters state, handleApplyFilters, and the JSX rendering FilterPanel above QuakeTable.

useEarthquakes.js:

  • URLSearchParams includes limit: "200" (not "50") and reads minmagnitude, starttime, endtime, and orderby from filters.
  • The dependency array lists all four filter properties.

FilterPanel.jsx:

  • Four filter groups: magnitude range, start date, end date, and sort order.
  • A submit button labeled Apply Filters.

Your filters are live and talking directly to the USGS API. Next up, you'll turn those raw numbers into visual insight with stat cards and a magnitude distribution chart.

Add Stats and a Magnitude Distribution Chart

Your filters are working and the table updates every time you click Apply Filters. But scrolling through rows of earthquake data to spot patterns is slow and error-prone.

Stat cards and a bar chart turn raw rows into instant insight. In this step, you will build a StatsBar component that derives key numbers from the data and a MagnitudeChart component that visualizes earthquake distribution using Recharts.

In this step, get ready to:
  • Create a StatsBar component that computes total count, average magnitude, and strongest quake from the data.
  • Build a MagnitudeChart component that groups earthquakes into magnitude ranges and renders a bar chart.
  • Style the full dashboard layout with stat cards in a row, chart below, and table at the bottom.
Create the StatsBar component

The table shows every earthquake, but it cannot answer questions at a glance like "how many quakes are there?" or "what was the strongest one?" Derived state solves this. You compute summary values from the raw data and display them as stat cards.

  • Create a new file at src/components/StatsBar.jsx.
  • Add the function declaration and the three derived-state computations by pasting this code:
function StatsBar({ quakes }) {
  // Count total earthquakes
  const totalCount = quakes.length;

  // Calculate average magnitude across all quakes
  const avgMagnitude =
    totalCount > 0
      ? (
          quakes.reduce((sum, q) => sum + (q.properties.mag || 0), 0) /
          totalCount
        ).toFixed(1)
      : "0.0";

  // Find the strongest earthquake by magnitude
  const strongest =
    totalCount > 0
      ? quakes.reduce((max, q) =>
          (q.properties.mag || 0) > (max.properties.mag || 0) ? q : max
        )
      : null;

What does this code do?

Each variable computes a different summary from the same quakes array.

  • totalCount is the simplest: how many earthquakes matched the current filters.
  • avgMagnitude sums every magnitude with reduce, divides by the count, and rounds to one decimal place.
  • strongest uses reduce again to find the single earthquake with the highest magnitude.
  • Directly below the strongest variable, add the JSX return and the export by pasting this code:
  // Render three stat cards
  return (
    <div className="stats-bar">
      <div className="stat-card">
        <span className="stat-value">{totalCount}</span>
        <span className="stat-label">Total Earthquakes</span>
      </div>
      <div className="stat-card">
        <span className="stat-value">{avgMagnitude}</span>
        <span className="stat-label">Avg Magnitude</span>
      </div>
      <div className="stat-card">
        <span className="stat-value">
          {strongest ? strongest.properties.mag.toFixed(1) : "N/A"}
        </span>
        <span className="stat-label">
          Strongest{strongest ? `: ${strongest.properties.place}` : ""}
        </span>
      </div>
    </div>
  );
}

export default StatsBar;

What does this JSX do?

Each stat-card div renders a large number (stat-value) and a label (stat-label). The strongest card also shows the location name so the learner instantly knows where it happened.

  • Save StatsBar.jsx.

✔️ 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

Scroll up and confirm both chunks are present in order: the function declaration with three derived-state variables, then the JSX return with three stat-card divs, then export default StatsBar at the bottom.

Now wire StatsBar into your app so it actually renders on screen.

  • Switch back to src/App.jsx.
  • Find this import line near the top:
import FilterPanel from "./components/FilterPanel";
  • Add this new import directly below it:
import StatsBar from "./components/StatsBar";
  • Now scroll down to the return statement and find these three lines:
      {loading && <p>Loading...</p>}
      {error && <p className="error-message">Error: {error}</p>}
      {!loading && !error && <QuakeTable quakes={quakes} />}
  • Replace all three lines with this updated render logic:
      {error && <p className="error-message">Error: {error}</p>}

      {/* Only render components when data is available */}
      {quakes.length > 0 && (
        <>
          <StatsBar quakes={quakes} />
          <QuakeTable quakes={quakes} />
        </>
      )}

Why change the conditional?

The old conditional {!loading && !error && ...} only checked loading and error state. The new quakes.length > 0 check is simpler and prevents the stat cards from rendering with empty data.

  • Save App.jsx and check your browser at http://localhost:5173.

You should see three unstyled stat cards above the table: a total count, an average magnitude, and the strongest quake with its location.

Don't see the stat cards?

  • Check that the import path in App.jsx matches the filename exactly: ./components/StatsBar.
  • Make sure you saved both StatsBar.jsx and App.jsx. Unsaved files show a dot on the tab in VS Code.
  • If the page is blank, check the terminal for errors. A missing closing brace in StatsBar.jsx will crash the app.

Why aren't my StatsBar stat cards showing up?

Build the magnitude distribution chart

The stat cards give you summary numbers, but they cannot show how earthquakes cluster across magnitude ranges. A bar chart makes patterns visible instantly. You will use Recharts to build one.

  • Create a new file at src/components/MagnitudeChart.jsx.
  • Add the Recharts imports and the helper function that groups quakes into magnitude buckets by pasting this code:
import {
  BarChart,
  Bar,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
} from "recharts";

function buildChartData(quakes) {
  // Define magnitude buckets for grouping
  const buckets = [
    { range: "0-1", min: 0, max: 1, count: 0 },
    { range: "1-2", min: 1, max: 2, count: 0 },
    { range: "2-3", min: 2, max: 3, count: 0 },
    { range: "3-4", min: 3, max: 4, count: 0 },
    { range: "4-5", min: 4, max: 5, count: 0 },
    { range: "5-6", min: 5, max: 6, count: 0 },
    { range: "6-7", min: 6, max: 7, count: 0 },
    { range: "7+", min: 7, max: Infinity, count: 0 },
  ];
  // Count how many quakes fall into each range
  quakes.forEach((q) => {
    const mag = q.properties.mag || 0;
    const bucket = buckets.find((b) => mag >= b.min && mag < b.max);
    if (bucket) bucket.count++;
  });
  return buckets.map(({ range, count }) => ({ range, count }));
}

What does buildChartData do?

Recharts needs data as an array of objects with keys it can map to axes. This helper transforms raw earthquake features into eight magnitude buckets (0-1, 1-2, and so on up to 7+).

Each quake is placed into its matching bucket using find. The final map strips out the min and max fields, returning only range and count for the chart.

  • Directly below the buildChartData function, add the chart component and export by pasting this code:
function MagnitudeChart({ quakes }) {
  // Transform quakes into chart-ready bucket data
  const data = buildChartData(quakes);

  return (
    <div className="chart-card">
      <h2>Magnitude Distribution</h2>
      <ResponsiveContainer width="100%" height={300}>
        <BarChart data={data}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="range" />
          <YAxis allowDecimals={false} />
          <Tooltip />
          <Bar dataKey="count" fill="#4a90d9" name="Earthquakes" />
        </BarChart>
      </ResponsiveContainer>
    </div>
  );
}

export default MagnitudeChart;

What are these Recharts components?

  • ResponsiveContainer makes the chart resize with the browser window instead of having a fixed pixel width.
  • XAxis with dataKey="range" labels each bar with the magnitude range ("0-1", "1-2", etc.).
  • Bar with dataKey="count" draws one bar per bucket, sized by the number of earthquakes in that range.
  • Tooltip shows the exact count when you hover over a bar.
  • Save MagnitudeChart.jsx.

✔️ 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

Scroll up and confirm both chunks are present in order: the Recharts imports and buildChartData function, then the MagnitudeChart component and export default MagnitudeChart at the bottom.

Now wire MagnitudeChart into your app the same way you wired in StatsBar.

  • Switch back to src/App.jsx.
  • Find this import line you just added:
import StatsBar from "./components/StatsBar";
  • Add this new import directly below it:
import MagnitudeChart from "./components/MagnitudeChart";
  • Scroll down to the return statement and find this line inside the fragment:
          <StatsBar quakes={quakes} />
  • Add this line directly below it, so the chart sits between the stats and the table:
          <MagnitudeChart quakes={quakes} />
  • Save App.jsx and check your browser.

You should see the stat cards, then a bar chart showing earthquake counts per magnitude range, then the data table below. The chart bars are interactive. Hover over a bar to see the tooltip with the exact count.

Chart not showing up?

  • If you see a blank space where the chart should be, make sure ResponsiveContainer has both width="100%" and height={300}. Without an explicit height, the container collapses to zero.
  • Check the terminal for import errors. The Recharts import must list all seven components: BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, and ResponsiveContainer.

Why isn't my Recharts bar chart rendering?

Style the dashboard layout

The components are working but the layout is rough. The stat cards need to sit in a row, the chart needs a card container, and the table needs polished headers and hover highlights.

  • Open src/App.css.
  • Select all the existing content and delete it.
  • Paste the global reset, layout, and filter panel styles by adding this code:
/* Global reset */
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: #f0f2f5;
  color: #1a1a2e;
}

/* Dashboard layout */
.dashboard { max-width: 1100px; margin: 0 auto; padding: 24px 16px; }

.dashboard-header { margin-bottom: 24px; }
.dashboard-header h1 { font-size: 1.8rem; color: #1a1a2e; }

/* Filter panel */
.filter-panel {
  display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-end;
  background: #fff; padding: 16px; border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.08); margin-bottom: 24px;
}

.filter-group { display: flex; flex-direction: column; gap: 4px; }
.filter-group label { font-size: 0.8rem; font-weight: 600; color: #555; }
.filter-group input[type="range"] { width: 140px; }
.filter-group input[type="date"], .filter-group select { padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.85rem; }

.apply-button { padding: 8px 20px; background: #4a90d9; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85rem; font-weight: 600; }
.apply-button:hover { background: #357abd; }

What changed from the old CSS?

The global reset (* { margin: 0; ... }) removes default browser spacing so every element starts from a clean slate. The body rule adds a light gray background and a system font stack. The filter panel gains a subtle box-shadow to look like a card, and the apply button gets a hover state.

  • Directly below the apply-button styles, add the stat card, chart, table, and error styles by pasting this code:
/* Stat cards */
.stats-bar { display: flex; gap: 16px; margin-bottom: 24px; }
.stat-card { flex: 1; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); text-align: center; border-top: 3px solid #4a90d9; }
.stat-value { display: block; font-size: 2rem; font-weight: 700; color: #1a1a2e; }
.stat-label { display: block; font-size: 0.8rem; color: #666; margin-top: 4px; }

/* Chart card */
.chart-card { background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); margin-bottom: 24px; }
.chart-card h2 { font-size: 1.1rem; margin-bottom: 16px; color: #1a1a2e; }

/* Data table */
.table-container { background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow-x: auto; }
.table-container h2 { font-size: 1.1rem; margin-bottom: 16px; color: #1a1a2e; }
.quake-table { width: 100%; border-collapse: collapse; }
.quake-table th { text-align: left; padding: 10px 12px; background: #f7f8fa; border-bottom: 2px solid #e2e5ea; font-size: 0.8rem; font-weight: 600; color: #555; }
.quake-table td { padding: 10px 12px; border-bottom: 1px solid #eee; font-size: 0.9rem; }
.quake-table tbody tr:nth-child(even) { background: #fafbfc; }
.quake-table tbody tr:hover { background: #eef3fb; }

/* Error state */
.error-message { background: #fde8e8; color: #c53030; padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; }

What do these styles do?

  • stats-bar uses display: flex to lay the three stat cards out side by side in a row.
  • Each stat-card has flex: 1 so all three cards share the available width equally. The blue top border (border-top: 3px solid #4a90d9) adds a visual accent.
  • The table now has alternating row colors (tr:nth-child(even)) and a hover highlight (tr:hover) for easier scanning.

One more small update. The starter index.css file needs font-rendering settings for crisp text.

  • Open src/index.css and replace its contents with this code:
:root {
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
  • Save both App.css and index.css, then check your browser.

The dashboard should look polished now. Three stat cards sit in a row with blue top borders. The bar chart sits in its own card below. The table has a clean header, alternating row colors, and a blue hover highlight.

Stat cards stacking vertically instead of in a row?

  • Make sure the .stats-bar rule has display: flex. Without it, the cards default to stacking vertically.
  • Check that you deleted all the old CSS before pasting the new styles. Leftover rules can conflict.

Why is my dashboard layout broken after updating the CSS?

Before you test the filters, what do you think will happen to the stat cards and the chart when you increase the minimum magnitude to 4.0 and click Apply Filters?

  • Move the Min Magnitude slider to 4.0.
  • Click Apply Filters.

The total count in the stat cards drops. The average magnitude goes up. The bar chart redraws with the lower-magnitude buckets empty and the 4-5 and higher buckets showing counts. Everything updates together because all three components receive the same filtered quakes array.

Your dashboard now has stat cards, a magnitude distribution chart, and a filtered data table. All three react to filter changes in real time. Next up, you will polish the header, add a loading indicator, and build an empty-state message for when the filters return zero results.

Polish the Dashboard Layout and UX

Your dashboard has working filters, stat cards, a bar chart, and a data table. Every component updates together when you apply new filter values.

The features work, but the experience feels rough. There is no indication of which filters are active, no feedback while data loads, and no message when a search comes back empty. This step closes those gaps and turns a working prototype into something portfolio-ready.

In this step, get ready to:
  • Add a header subtitle that summarizes the active filters and a loading indicator that appears during API calls.
  • Polish the CSS for the table headers, subtitle, loading indicator, and empty state.
  • Add an empty state message when the API returns zero earthquakes for the current filters.
Add a subtitle and loading indicator to the header

Right now the dashboard header is just a title. The user has no way to see which filters are currently applied without scanning each individual control.

  • In src/App.jsx, find the <header> block inside the return statement.
  • Find this line:
        <h1>Earthquake Dashboard</h1>
      </header>
  • Add a subtitle paragraph between the </h1> and </header> tags by copying the following code:
        <p className="dashboard-subtitle">
          Showing M{filters.minmagnitude}+ earthquakes from{" "}
          {filters.starttime} to {filters.endtime}, sorted by{" "}
          {filters.orderby}
          {loading && <span className="loading-indicator"> Loading...</span>}
        </p>

What does this code do?

The subtitle reads the current filter state and renders it as a human-friendly summary. It interpolates filters.minmagnitude, filters.starttime, filters.endtime, and filters.orderby directly into the text.

The loading && short-circuit renders the "Loading..." span only while the API call is in flight. Once data arrives, it disappears automatically.

  • Save App.jsx.
  • Switch back to your browser. You should see a line of text under the title showing something like "Showing M2+ earthquakes from 2026-08-12 to 2026-08-19, sorted by time".

Don't see the subtitle text?

  • Make sure the new <p> element sits inside the <header> block, after the </h1> closing tag and before </header>.
  • Check for missing curly braces around the JavaScript expressions. Each value like {filters.minmagnitude} needs its own pair of braces.
  • Help me debug my dashboard subtitle.
Style the new elements and polish the table headers

The subtitle text works but it is unstyled. The table headers also need some polish to look more intentional.

  • In src/App.css, add the following three new rules after the .dashboard-header h1 rule:
.dashboard-subtitle {
  color: #666;
  font-size: 0.9rem;
  margin-top: 4px;
}

.loading-indicator {
  color: #4a90d9;
  font-weight: 600;
}

.empty-state {
  text-align: center;
  padding: 48px 16px;
  color: #666;
  background: #fff;
  border-radius: 8px;
  border: 1px dashed #ccc;
}

What do these styles do?

  • .dashboard-subtitle makes the filter summary smaller and muted so it sits comfortably below the main title.
  • .loading-indicator colors the "Loading..." text blue and bolds it so it stands out in the subtitle.
  • .empty-state centers the no-results message inside a dashed-border box. You will use this class in the next substep.
  • Save App.css and switch back to your browser.
  • The subtitle below the title should now appear smaller and gray. If you catch the loading state, you will see "Loading..." flash in blue.

Next, the table headers need a few refinements to look polished. Uppercase labels, tighter letter spacing, and a sticky position will keep the headers visible when the table scrolls.

  • In App.css, scroll down and find the .quake-table th rule. It currently looks like this:
.quake-table th { text-align: left; padding: 10px 12px; background: #f7f8fa; border-bottom: 2px solid #e2e5ea; font-size: 0.8rem; font-weight: 600; color: #555; }
  • Replace the entire .quake-table th rule with this expanded version that adds uppercase text, letter spacing, and a sticky header:
.quake-table th {
  text-align: left;
  padding: 10px 12px;
  background: #f7f8fa;
  border-bottom: 2px solid #e2e5ea;
  font-size: 0.8rem;
  font-weight: 600;
  color: #555;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  position: sticky;
  top: 0;
}

What changed?

  • text-transform: uppercase makes the column labels all caps for a clean dashboard look.
  • letter-spacing: 0.5px adds a small gap between uppercase letters so they are easier to read.
  • position: sticky and top: 0 lock the headers to the top of the table container so they stay visible as the user scrolls through earthquake rows.
  • Save App.css and check your browser.
  • The table headers should now read "LOCATION", "MAGNITUDE", "DEPTH (KM)", and "TIME" in uppercase letters.

Headers still lowercase?

  • Make sure you replaced the entire rule, not just appended the new properties outside the closing brace.
  • Check that the selector reads .quake-table th with no typos.
  • Help me fix my table header CSS.

✔️ Awesome, I've got everything!

Great. Double check you have saved App.css.

ⓧ I'd like to double check the full code

Your App.css is over 30 lines, so scroll through and confirm these changes are present in order:

  • After .dashboard-header h1: three new rules for .dashboard-subtitle, .loading-indicator, and .empty-state.
  • The .quake-table th rule now includes text-transform: uppercase, letter-spacing: 0.5px, position: sticky, and top: 0.
Add a no-results empty state

If a user sets minimum magnitude to 8 and the USGS API returns nothing, the dashboard currently shows a blank page. A helpful empty state tells the user what happened and what to try instead.

  • Return to src/App.jsx.
  • Find this line inside the return statement:
      {quakes.length > 0 && (
  • Add the following empty state block directly above that line:
      {!loading && !error && quakes.length === 0 && (
        <div className="empty-state">
          <p>
            No earthquakes found for these filters. Try lowering the minimum
            magnitude or expanding the date range.
          </p>
        </div>
      )}

Why three conditions?

The condition !loading && !error && quakes.length === 0 makes sure the empty state only appears when the fetch is complete, no error occurred, and the results are genuinely empty. Without these guards, the empty state would flash on screen during every loading cycle.

  • Save App.jsx.

Before you test this, what do you think will happen if you set the minimum magnitude slider to 8 and click Apply Filters?

  • In your browser, drag the Min Magnitude slider all the way to 8.
  • Click Apply Filters.

The stat cards, chart, and table all disappear. In their place you should see a centered message inside a dashed border: "No earthquakes found for these filters. Try lowering the minimum magnitude or expanding the date range."

Still seeing a blank page instead of the empty state?

  • Make sure the empty state block sits between the {error && ...} block and the {quakes.length > 0 && ...} block.
  • Check that you wrote quakes.length === 0 with triple equals, not quakes.length = 0 (assignment).
  • Confirm the .empty-state CSS rule exists in App.css from the previous substep.
  • Help me debug my empty state.
  • Set the Min Magnitude slider back to 2 and click Apply Filters to restore the full dashboard view.

✔️ Awesome, I've got everything!

Great. Double check you have saved App.jsx.

ⓧ I'd like to double check the full code

Your App.jsx is over 30 lines. Scroll through and confirm these sections are present in the return statement, in order:

  • The <header> block contains the <h1> title followed by a <p className="dashboard-subtitle"> with the filter summary and loading indicator.
  • The FilterPanel component sits below the header.
  • The error message block: {error && ...}.
  • The empty state block: {!loading && !error && quakes.length === 0 && ...}.
  • The main content block: {quakes.length > 0 && ...} containing StatsBar, MagnitudeChart, and QuakeTable.
Verify the polished dashboard

Take one final look at the dashboard with all the polish applied. You should see all of these details working together:

  • A subtitle under the title showing the active filter summary and the sort order.
  • A blue "Loading..." flash in the subtitle when you click Apply Filters.
  • Uppercase table headers that stay pinned at the top of the table.
  • Alternating row colors and a blue highlight when you hover over a row.
  • Stat cards with blue top borders and large number displays.
  • The magnitude chart wrapped in a white card with a subtle shadow.

Your earthquake dashboard is feature-complete and polished. The whole layout looks intentionally designed, with clear visual feedback at every stage. Next up, push your skills further with an optional Secret Mission.

Secret mission

Significant Earthquake Highlight Mode

Your dashboard shows the data, but can you make it call attention to the quakes that really matter? Add a highlight mode that visually flags magnitude 4.5+ earthquakes in the table and splits the bar chart into regular vs. significant series.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses the free USGS Earthquake API, so there are no ongoing costs.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The earthquake-dashboard project folder on your Desktop.
  • The Vite dev server running at http://localhost:5173.
  • Node packages installed inside the project folder (React, Recharts, Vite).

Keep everything running

No action needed. Choose this if you're still actively building or want to keep experimenting with the dashboard.

  • Your dev server stays running at http://localhost:5173.
  • Your earthquake-dashboard folder and all its files remain on your Desktop.
  • The USGS API is free and requires no account, so there is nothing to maintain on that side.

Pause - I'll come back to this later

Stop the dev server to free up your terminal and the port, but keep all your project files so you can pick up where you left off.

  • Switch to the terminal where the dev server is running.
  • Press Ctrl+C to stop the server.
  • Your project files stay on your Desktop. To resume later, open a terminal in the earthquake-dashboard folder and run npm run dev.

Delete - I don't want to use this again

Remove all project files and stop the dev server. This frees up disk space and clears the project completely.

  • Switch to the terminal where the dev server is running.
  • Press Ctrl+C to stop the server.
  • Navigate to your Desktop and delete the project folder by running these commands:

macOS

cd ~/Desktop
rm -rf earthquake-dashboard

Windows

cd ~/Desktop
rmdir /s /q earthquake-dashboard
  • Confirm the folder is gone by running ls on your Desktop. You should no longer see earthquake-dashboard in the output.

Nice Work!

Nice Work!

You just built a fully functional earthquake activity dashboard powered by live government data. From an empty project folder to interactive charts, multi-criteria filters, and derived statistics, you now have a portfolio-ready React app that pulls real-world data and makes it useful.

You've learned how to:

  • Build a custom React hook (useEarthquakes) that encapsulates API fetching, query parameter construction, and state management in one reusable package.
  • Implement multi-criteria filtering with magnitude, date range, and sort order controls that compose together to re-query the USGS Earthquake API, and compute derived state like average magnitude and strongest quake from the filtered results.
  • Design a multi-component architecture with FilterPanel, StatsBar, MagnitudeChart, and QuakeTable, and build interactive data visualizations with Recharts.
  • Secret Mission: Added a significant earthquake highlight mode with conditional row styling, a toggle switch, and a stacked bar chart with two data series.

Ready to quiz yourself?