Build a Ride-Booking App with React

Build an Uber-like app with an interactive map, animated drivers, and fare estimates.

Introduction

30 Second Summary

Every time you open a ride-hailing app, you see little cars moving around a map in real time. That map experience is what makes these apps feel alive and trustworthy.

In this project, you will build a ride-booking web app that displays nearby drivers on an interactive map, lets you select pickup and destination points, and calculates a fare estimate based on distance.

What You'll Build

A React application with a fullscreen interactive map powered by MapLibre GL JS, featuring animated driver markers, click-to-select locations, route visualization, and fare estimation.

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

  • A fullscreen interactive map with animated car markers representing nearby drivers.
  • A ride booking panel where you click the map to set pickup and destination points.
  • A route line and fare estimate calculated from the distance between your two points using the Haversine formula.
  • Secret Mission: Animate the nearest driver to approach your pickup location after you confirm the ride.

Are there any prerequisites?

Basic familiarity with JavaScript and React is helpful (understanding components and state). No mapping experience is needed.

Set Up Your Development Environment

Before you can build a ride-booking app, you need the tools to run a React project on your machine.

In this step, you'll install Node.js (the JavaScript runtime), scaffold a new React project using Vite, and start a local development server. By the end, you'll have a working React app running in your browser.

In this step, get ready to:
  • Install and verify Node.js.
  • Scaffold a React + TypeScript project with Vite.
  • Start the development server and see your app in the browser.
Verify Node.js is installed

Node.js is the JavaScript runtime that powers your development tools. Vite and React both depend on it. This project requires version 20.19 or higher.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Check your Node.js version by running this command:
node --version

Look at the output and find your situation below.

✔️ I see version 20.19 or higher

You're all set. Your output looks something like v20.19.0 or v22.12.0 or any number at or above 20.19. Move on to the next substep.

ⓧ I see an older version

Your Node.js version is below 20.19. Vite 8 requires at least this version to run correctly.

  • Visit https://nodejs.org and download the latest LTS installer for your operating system.
  • Run the installer, accepting the default options.
  • Close and reopen your terminal, then run node --version again to confirm the new version appears.

Why close and reopen the terminal?

Your terminal loads the path to installed programs when it first opens. After installing a new version of Node.js, you need a fresh terminal session for it to find the updated binary.

ⓧ Command not found

Node.js is not installed on your machine yet. Let's fix that.

  • Visit https://nodejs.org and download the LTS installer for your operating system.
  • Run the installer, accepting the default options.
  • Close and reopen your terminal after installation completes.
  • Run node --version to confirm it prints a version number at or above v20.19.0.
Scaffold the React project with Vite

Vite is a modern build tool that creates React projects with fast hot-reload and instant server start. You'll use its scaffolding command to generate a complete React + TypeScript project in one step.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Scaffold your new project by running this command:
npm create vite@latest uber-clone -- --template react-ts

What does this command do?

npm create vite@latest runs the Vite project generator. uber-clone is the folder name it creates. --template react-ts tells Vite to use the React + TypeScript template.

  • Confirm the folder was created by running:
ls

You should see uber-clone listed in the output.

Don't see uber-clone in the output?

Make sure you ran cd ~/Desktop first. If the scaffold command failed, check that Node.js is installed correctly by running node --version.

help me troubleshoot why my Vite scaffold command failed

Install dependencies and start the dev server

The scaffold created your project structure, but the packages it depends on aren't downloaded yet. You need to install them before the app can run.

  • Navigate into the project folder and install dependencies by running these commands:
cd uber-clone
npm install

What does npm install do?

npm install reads the package.json file in your project and downloads all the libraries your app needs (React, TypeScript, Vite plugins) into a node_modules folder.

Before starting the dev server, you'll prepare two project files with clean starting code for the ride-booking app.

  • Open the uber-clone project in your code editor. Press Cmd+Space (macOS) or the Windows key (Windows), type VS Code, and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the uber-clone folder, and click Open.
  • In the Explorer sidebar, expand the src folder and open index.css.
  • Select all the existing content (Cmd+A on macOS, Ctrl+A on Windows) and replace it with this CSS reset:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  overflow: hidden;
}

What does this CSS do?

The * selector resets all margins and padding so the map can fill the entire screen without gaps. overflow: hidden on the body prevents scrollbars from appearing when the map takes up the full viewport.

  • Save index.css.
  • Now open main.tsx in the src folder.
  • Select all the existing content and replace it with this code:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

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

What does this file do?

main.tsx is the entry point of your React app. It finds the root element in index.html and renders the App component inside it. StrictMode helps catch potential problems during development.

  • Save main.tsx.

Now let's start the development server to see your app running.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
  • Start the development server by running:
npm run dev
  • Open your browser and navigate to http://localhost:5173.

You should see the default Vite + React page with the React logo and a counter button.

Not seeing the page in your browser?

Make sure the terminal shows a local URL (usually http://localhost:5173). If the port is different, use the URL shown in your terminal output.

If you see an error about missing modules, run npm install again and then npm run dev.

help me debug why my Vite dev server isn't starting

✔️ Awesome, I've got everything!

Great. Double check that you've saved both index.css and main.tsx before moving on.

ⓧ I'd like to double check the full code

Here are the two files you modified. Make sure yours match exactly.

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  overflow: hidden;
}
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

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

Your development environment is ready and the React app is running. Next up, you'll replace the default page with a fullscreen interactive map.

Add the Map to Your App

Your development environment is ready and the default Vite app is running in your browser. Now it's time to replace that spinning logo with something much more exciting.

Every ride-booking app revolves around a map. In this step, you'll install a mapping library and render a fullscreen interactive map that users can pan and zoom.

In this step, get ready to:
  • Install the MapLibre GL JS and react-map-gl libraries.
  • Replace the default App component with a fullscreen interactive map.
  • Verify the map renders and responds to pan and zoom interactions.
Install the mapping libraries

Your app needs two packages to render a map. MapLibre GL JS is the open-source map rendering engine. react-map-gl is a React wrapper (originally created by Uber's visualization team) that lets you use MapLibre with familiar React components.

  • Install both packages by running this command in your terminal (make sure you're inside the uber-clone folder):
npm install react-map-gl maplibre-gl

Why MapLibre instead of Mapbox?

MapLibre GL JS is an MIT-licensed fork of Mapbox GL JS. It provides the same WebGL-based vector tile rendering without requiring an API key or paid account. For this project, that means zero sign-up friction.

  • You should see output confirming both packages were added to your package.json.

Seeing install errors?

Make sure your terminal is inside the uber-clone folder (not its parent). Run ls and confirm you see package.json in the output.

If you see permission errors, try closing and reopening your terminal, then run the install command again.

npm install failing for you?

Replace the App component with a fullscreen map

The default Vite scaffold shows a counter demo. You'll replace the entire contents of App.tsx with a single Map component that fills the browser window.

  • Open src/App.tsx in VS Code (find it in the Explorer sidebar on the left).
  • Select all the existing code with Cmd+A (macOS) or Ctrl+A (Windows), then delete it.
  • Paste in the following code:
import Map from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";

function App() {
  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <Map
        initialViewState={{
          longitude: -74.006,
          latitude: 40.7128,
          zoom: 14,
        }}
        style={{ width: "100%", height: "100%" }}
        mapStyle="https://demotiles.maplibre.org/style.json"
      />
    </div>
  );
}

export default App;

What does this code do?

  • import Map from "react-map-gl/maplibre" imports the Map component from the MapLibre-specific endpoint. This tells react-map-gl to use MapLibre as the rendering engine.
  • import "maplibre-gl/dist/maplibre-gl.css" loads the CSS that styles map controls and markers correctly.
  • initialViewState sets where the map opens. These coordinates center on Lower Manhattan in New York City.
  • mapStyle points to MapLibre's free demo tiles. No API key needed.
  • Save App.tsx with Cmd+S (macOS) or Ctrl+S (Windows).
  • Switch to your browser. The page should auto-refresh thanks to Vite's hot module replacement.
  • You should see an interactive map filling the entire browser window. Try clicking and dragging to pan, and scrolling to zoom.

Don't see a map?

Check your browser's developer console for errors. Press Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows) to open it.

If you see a blank white page, make sure you included the CSS import line (import "maplibre-gl/dist/maplibre-gl.css"). Without it, the map renders with zero height.

If you see module errors, confirm that npm install react-map-gl maplibre-gl completed successfully in the earlier substep.

My map isn't showing up in the browser. Help me debug.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete src/App.tsx file:

import Map from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";

function App() {
  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <Map
        initialViewState={{
          longitude: -74.006,
          latitude: 40.7128,
          zoom: 14,
        }}
        style={{ width: "100%", height: "100%" }}
        mapStyle="https://demotiles.maplibre.org/style.json"
      />
    </div>
  );
}

export default App;

You now have a fullscreen interactive map running in your browser. Next up, you'll populate it with driver markers that show nearby cars available for rides.

Display Driver Markers on the Map

Your map is live and interactive. Now it's time to populate it with drivers.

In a real ride-booking app, driver locations come from a server via WebSocket connections. For this project, you'll create simulated driver data and render each one as a car marker on the map. The patterns are identical to what you'd use with live data.

In this step, get ready to:
  • Create a drivers data file with simulated driver positions.
  • Build a DriverMarker component that renders a car icon at each driver's location.
  • Render all driver markers on the map.
Create the drivers data file

Each driver needs a position (latitude and longitude) and a heading (the direction they're facing). You'll store this data in its own file so it stays separate from your component logic.

  • Create a new folder called src/data inside your uber-clone project.
  • Inside src/data, create a new file called drivers.ts.
  • Add the following code to drivers.ts:
export interface Driver {
  id: number;
  name: string;
  latitude: number;
  longitude: number;
  heading: number;
}

export const initialDrivers: Driver[] = [
  { id: 1, name: "Alex", latitude: 40.7128, longitude: -74.006, heading: 45 },
  { id: 2, name: "Maria", latitude: 40.7148, longitude: -74.002, heading: 120 },
  { id: 3, name: "James", latitude: 40.7100, longitude: -74.009, heading: 200 },
  { id: 4, name: "Sara", latitude: 40.7165, longitude: -74.012, heading: 310 },
  { id: 5, name: "Chen", latitude: 40.7090, longitude: -74.004, heading: 80 },
];

What does this code do?

The Driver interface defines the shape of each driver object. Every driver has a unique id, a name, GPS coordinates (latitude and longitude), and a heading in degrees.

The heading represents which direction the car is pointing (0 = north, 90 = east, 180 = south, 270 = west). You'll use this later to rotate the car emoji so it looks like it's facing the right way.

  • Save src/data/drivers.ts.
Build the DriverMarker component

Each driver needs its own marker on the map. You'll create a reusable component that takes a driver object and renders a positioned car emoji using react-map-gl's Marker component.

  • Create a new folder called src/components inside your uber-clone project.
  • Inside src/components, create a new file called DriverMarker.tsx.
  • Add the following code to DriverMarker.tsx:
import { Marker } from "react-map-gl/maplibre";
import type { Driver } from "../data/drivers";

interface DriverMarkerProps {
  driver: Driver;
}

export function DriverMarker({ driver }: DriverMarkerProps) {
  return (
    <Marker
      longitude={driver.longitude}
      latitude={driver.latitude}
      anchor="center"
    >
      <div
        style={{
          fontSize: "24px",
          transform: `rotate(${driver.heading}deg)`,
          transition: "transform 1s ease",
        }}
      >
        🚗
      </div>
    </Marker>
  );
}

What does this code do?

The Marker component from react-map-gl positions an element at specific GPS coordinates on the map. You pass it a longitude and latitude, and it handles all the map projection math.

The transform: rotate() CSS rotates the car emoji to match the driver's heading. The transition property makes rotation changes animate smoothly over one second. This will look great once you add movement in the next step.

  • Save src/components/DriverMarker.tsx.
Render all driver markers on the map

Now you'll connect the data and the component by importing both into App.tsx and rendering a DriverMarker for each driver inside the Map component.

  • Open src/App.tsx.
  • Find these import lines at the top of the file:
import Map from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";
  • Add two new import lines below them, so your imports look like this:
import Map from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";
import { initialDrivers } from "./data/drivers";
import { DriverMarker } from "./components/DriverMarker";
  • Next, find the self-closing <Map /> tag and change it to an opening and closing tag pair. Add the driver markers inside it:
      <Map
        initialViewState={{
          longitude: -74.006,
          latitude: 40.7128,
          zoom: 14,
        }}
        style={{ width: "100%", height: "100%" }}
        mapStyle="https://demotiles.maplibre.org/style.json"
      >
        {initialDrivers.map((driver) => (
          <DriverMarker key={driver.id} driver={driver} />
        ))}
      </Map>

What does this code do?

The .map() function loops through the initialDrivers array and renders one DriverMarker component for each driver. Each marker receives a unique key prop (the driver's id) so React can efficiently track and update them.

Placing the markers inside the <Map> component is what makes them position themselves on the map coordinates. react-map-gl handles converting latitude/longitude into pixel positions as the user pans and zooms.

  • Save src/App.tsx.
  • Check your browser. You should see five car emoji markers scattered around the map.

Don't see any car markers?

Make sure your <Map> tag is no longer self-closing. It needs an opening <Map ...> and a closing </Map> with the driver markers between them.

Check that the import paths match your folder structure exactly: ./data/drivers and ./components/DriverMarker.

help me debug why my driver markers aren't showing on the map

✔️ Awesome, I've got everything!

Great. Double check you've saved all three files before moving on.

ⓧ I'd like to double check the full code

Here's the complete src/data/drivers.ts:

export interface Driver {
  id: number;
  name: string;
  latitude: number;
  longitude: number;
  heading: number;
}

export const initialDrivers: Driver[] = [
  { id: 1, name: "Alex", latitude: 40.7128, longitude: -74.006, heading: 45 },
  { id: 2, name: "Maria", latitude: 40.7148, longitude: -74.002, heading: 120 },
  { id: 3, name: "James", latitude: 40.7100, longitude: -74.009, heading: 200 },
  { id: 4, name: "Sara", latitude: 40.7165, longitude: -74.012, heading: 310 },
  { id: 5, name: "Chen", latitude: 40.7090, longitude: -74.004, heading: 80 },
];

Here's the complete src/components/DriverMarker.tsx:

import { Marker } from "react-map-gl/maplibre";
import type { Driver } from "../data/drivers";

interface DriverMarkerProps {
  driver: Driver;
}

export function DriverMarker({ driver }: DriverMarkerProps) {
  return (
    <Marker
      longitude={driver.longitude}
      latitude={driver.latitude}
      anchor="center"
    >
      <div
        style={{
          fontSize: "24px",
          transform: `rotate(${driver.heading}deg)`,
          transition: "transform 1s ease",
        }}
      >
        🚗
      </div>
    </Marker>
  );
}

Here's the complete src/App.tsx:

import Map from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";
import { initialDrivers } from "./data/drivers";
import { DriverMarker } from "./components/DriverMarker";

function App() {
  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <Map
        initialViewState={{
          longitude: -74.006,
          latitude: 40.7128,
          zoom: 14,
        }}
        style={{ width: "100%", height: "100%" }}
        mapStyle="https://demotiles.maplibre.org/style.json"
      >
        {initialDrivers.map((driver) => (
          <DriverMarker key={driver.id} driver={driver} />
        ))}
      </Map>
    </div>
  );
}

export default App;

Your map now shows five drivers sitting at fixed positions. Next up, you'll bring them to life by animating their movement across the map.

Animate the Drivers

Your map shows five car markers, but they are frozen in place. A real ride-booking app needs drivers that move around the city in real time.

In this step, you will build a custom React hook that updates every driver's position once per second using trigonometry. The result is a lively map with cars roaming the streets.

In this step, get ready to:
  • Create a useDriverSimulation custom hook that moves drivers at a regular interval.
  • Connect the hook to your App component so the map comes alive.
  • Verify that car markers smoothly move around the map every second.
Create the useDriverSimulation hook

React custom hooks let you extract stateful logic into a reusable function. You will create one that owns the drivers array in state and updates every driver's position on a timer.

  • Create a new folder inside src called hooks.
  • Inside src/hooks, create a new file called useDriverSimulation.ts and paste the following code:
import { useState, useEffect, useRef } from "react";
import { Driver, initialDrivers } from "../data/drivers";

export function useDriverSimulation() {
  const [drivers, setDrivers] = useState<Driver[]>(initialDrivers);
  const intervalRef = useRef<number | null>(null);

  useEffect(() => {
    intervalRef.current = window.setInterval(() => {
      setDrivers((prevDrivers) =>
        prevDrivers.map((driver) => {
          // Move each driver forward based on their heading
          const speed = 0.0001;
          const headingRad = (driver.heading * Math.PI) / 180;
          // Slightly randomize the heading so drivers wander
          const newHeading = driver.heading + (Math.random() - 0.5) * 30;

          return {
            ...driver,
            latitude: driver.latitude + Math.cos(headingRad) * speed,
            longitude: driver.longitude + Math.sin(headingRad) * speed,
            heading: newHeading % 360,
          };
        })
      );
    }, 1000);

    // Clean up the interval when the component unmounts
    return () => {
      if (intervalRef.current !== null) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);

  return { drivers, setDrivers };
}

What does this code do?

This hook manages the array of drivers in React state. Every 1000 milliseconds, the setInterval callback runs and maps over each driver to calculate a new position.

  • speed controls how far a driver moves per tick (a tiny fraction of a degree).
  • headingRad converts the driver's heading from degrees to radians so you can use Math.cos and Math.sin to move in that direction.
  • newHeading adds a random jitter so drivers wander naturally instead of traveling in a straight line.
  • The cleanup function clears the interval when the component unmounts, preventing memory leaks.
  • Save src/hooks/useDriverSimulation.ts.

Seeing a red underline on the import?

Make sure the file path is ../data/drivers (with two dots). Your hook lives in src/hooks/ so it needs to go up one directory to reach src/data/.

Also confirm the folder is named hooks (plural) and the file extension is .ts.

Still stuck? Help me debug this import error in my useDriverSimulation hook.

Connect the hook to App

The hook is ready, but your App component still imports the static initialDrivers array directly. You need to swap that out for the live data from your hook.

  • Open src/App.tsx and replace the entire file with this updated code:
import Map from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";
import { useDriverSimulation } from "./hooks/useDriverSimulation";
import { DriverMarker } from "./components/DriverMarker";

function App() {
  // Get live driver positions from our simulation hook
  const { drivers } = useDriverSimulation();

  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <Map
        initialViewState={{
          longitude: -74.006,
          latitude: 40.7128,
          zoom: 14,
        }}
        style={{ width: "100%", height: "100%" }}
        mapStyle="https://demotiles.maplibre.org/style.json"
      >
        {drivers.map((driver) => (
          <DriverMarker key={driver.id} driver={driver} />
        ))}
      </Map>
    </div>
  );
}

export default App;

What changed here?

  • The import of initialDrivers from ./data/drivers is gone. The hook handles that internally.
  • You now import and call useDriverSimulation(), which returns a drivers array that updates every second.
  • The JSX maps over drivers (live state) instead of initialDrivers (static data). React re-renders the markers each time the state changes.
  • Save src/App.tsx.
  • Check your browser. The five car markers should now be moving around the map, drifting in different directions every second.

Why do the cars rotate smoothly?

Your DriverMarker component already has transition: "transform 1s ease" on the car emoji. When the heading changes, CSS animates the rotation over one second. This makes the movement feel natural rather than jumpy.

Cars not moving?

  • Make sure you saved both files (useDriverSimulation.ts and App.tsx).
  • Check your terminal for errors. A common issue is a typo in the import path. The hook import should be ./hooks/useDriverSimulation (no file extension needed).
  • If you see the cars but they are not moving, confirm you removed the old import { initialDrivers } line and are mapping over drivers from the hook.

Need more help? My driver markers are not animating after connecting the useDriverSimulation hook.

✔️ Awesome, I've got everything!

Great. Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the full src/hooks/useDriverSimulation.ts:

import { useState, useEffect, useRef } from "react";
import { Driver, initialDrivers } from "../data/drivers";

export function useDriverSimulation() {
  const [drivers, setDrivers] = useState<Driver[]>(initialDrivers);
  const intervalRef = useRef<number | null>(null);

  useEffect(() => {
    intervalRef.current = window.setInterval(() => {
      setDrivers((prevDrivers) =>
        prevDrivers.map((driver) => {
          const speed = 0.0001;
          const headingRad = (driver.heading * Math.PI) / 180;
          const newHeading = driver.heading + (Math.random() - 0.5) * 30;

          return {
            ...driver,
            latitude: driver.latitude + Math.cos(headingRad) * speed,
            longitude: driver.longitude + Math.sin(headingRad) * speed,
            heading: newHeading % 360,
          };
        })
      );
    }, 1000);

    return () => {
      if (intervalRef.current !== null) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);

  return { drivers, setDrivers };
}

Here is the full src/App.tsx:

import Map from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";
import { useDriverSimulation } from "./hooks/useDriverSimulation";
import { DriverMarker } from "./components/DriverMarker";

function App() {
  const { drivers } = useDriverSimulation();

  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <Map
        initialViewState={{
          longitude: -74.006,
          latitude: 40.7128,
          zoom: 14,
        }}
        style={{ width: "100%", height: "100%" }}
        mapStyle="https://demotiles.maplibre.org/style.json"
      >
        {drivers.map((driver) => (
          <DriverMarker key={driver.id} driver={driver} />
        ))}
      </Map>
    </div>
  );
}

export default App;

Your map is alive. Five drivers are roaming around, changing direction every second. Next up, you will build a booking panel that lets users set a pickup and destination by clicking the map.

Build the Ride Booking Panel

Your map has animated drivers cruising around, but there is no way for a user to actually book a ride.

In this step, you will build the booking panel overlay and make the map interactive. Users will click the map to set a pickup and destination, seeing colored markers appear at each point.

In this step, get ready to:
  • Create a BookingPanel component with a styled floating overlay.
  • Add click-to-set functionality so the map responds to user clicks.
  • Display green and red markers on the map for pickup and destination.
Create the BookingPanel component

The BookingPanel needs a shared type that represents a geographic coordinate. You will create a small utility file first, then the panel component itself.

  • Create a new folder called utils inside your src folder.
  • Create a file called geo.ts inside src/utils/ with this code:
export interface LatLng {
  latitude: number;
  longitude: number;
}

What does this code do?

The LatLng interface defines a standard shape for geographic coordinates. Both the BookingPanel and App will use this type to pass location data around consistently.

  • Now create the panel component. Create a file called BookingPanel.tsx inside src/components/ with this code:
import type { LatLng } from "../utils/geo";

interface BookingPanelProps {
  pickup: LatLng | null;
  destination: LatLng | null;
  onRequestRide: () => void;
}

export function BookingPanel({ pickup, destination, onRequestRide }: BookingPanelProps) {
  return (
    <div style={panelStyle}>
      <h2 style={{ margin: "0 0 12px 0", fontSize: "18px" }}>Book a Ride</h2>

      <div style={inputStyle}>
        <span style={{ color: "#22c55e", marginRight: "8px" }}>●</span>
        {pickup
          ? `${pickup.latitude.toFixed(4)}, ${pickup.longitude.toFixed(4)}`
          : "Click map to set pickup"}
      </div>

      <div style={inputStyle}>
        <span style={{ color: "#ef4444", marginRight: "8px" }}>●</span>
        {destination
          ? `${destination.latitude.toFixed(4)}, ${destination.longitude.toFixed(4)}`
          : "Click map to set destination"}
      </div>
    </div>
  );
}

const panelStyle: React.CSSProperties = {
  position: "absolute",
  bottom: "24px",
  left: "24px",
  background: "white",
  borderRadius: "12px",
  padding: "20px",
  boxShadow: "0 4px 20px rgba(0,0,0,0.15)",
  width: "300px",
  zIndex: 1,
};

const inputStyle: React.CSSProperties = {
  background: "#f3f4f6",
  borderRadius: "8px",
  padding: "10px 12px",
  marginBottom: "8px",
  fontSize: "13px",
  color: "#374151",
};

What does this code do?

The BookingPanel component receives three props: the current pickup point, the current destination point, and a callback for when the user requests a ride.

  • Each input row uses a colored dot (green for pickup, red for destination) and shows either the coordinates or a placeholder message.
  • The panelStyle positions the panel as a floating card in the bottom-left corner of the screen, overlaying the map.
  • The toFixed(4) call formats coordinates to 4 decimal places so they look clean in the panel.
  • Save both src/utils/geo.ts and src/components/BookingPanel.tsx.

Seeing a red squiggly on the import?

If VS Code shows an error on import type { LatLng } from "../utils/geo", double check that your geo.ts file is saved inside src/utils/ (not src/components/ or the project root).

help me fix my import path for geo.ts

Add click-to-set functionality

The map needs to respond to user clicks. The first click sets the pickup location, the second click sets the destination, and a third click resets the cycle.

  • Replace the entire contents of src/App.tsx with this code:
import { useState, useCallback } from "react";
import Map from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";
import { useDriverSimulation } from "./hooks/useDriverSimulation";
import { DriverMarker } from "./components/DriverMarker";
import { BookingPanel } from "./components/BookingPanel";
import type { LatLng } from "./utils/geo";
import type { MapLayerMouseEvent } from "react-map-gl/maplibre";

function App() {
  const { drivers } = useDriverSimulation();
  // Track the user's selected pickup and destination points
  const [pickup, setPickup] = useState<LatLng | null>(null);
  const [destination, setDestination] = useState<LatLng | null>(null);

  // Handle map clicks: first click = pickup, second = destination, third = reset
  const handleMapClick = useCallback(
    (event: MapLayerMouseEvent) => {
      const { lng, lat } = event.lngLat;
      const point: LatLng = { latitude: lat, longitude: lng };

      if (!pickup) {
        setPickup(point);
      } else if (!destination) {
        setDestination(point);
      } else {
        setPickup(point);
        setDestination(null);
      }
    },
    [pickup, destination]
  );

  // Placeholder handler for ride requests (expanded in Secret Mission)
  const handleRequestRide = () => {
    alert("Ride requested! (Secret Mission: animate the driver approaching)");
  };

  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <Map
        initialViewState={{
          longitude: -74.006,
          latitude: 40.7128,
          zoom: 14,
        }}
        style={{ width: "100%", height: "100%" }}
        mapStyle="https://demotiles.maplibre.org/style.json"
        onClick={handleMapClick}
      >
        {drivers.map((driver) => (
          <DriverMarker key={driver.id} driver={driver} />
        ))}
      </Map>

      <BookingPanel
        pickup={pickup}
        destination={destination}
        onRequestRide={handleRequestRide}
      />
    </div>
  );
}

export default App;

What does this code do?

  • Two pieces of state (pickup and destination) track the user's selected coordinates. Both start as null.
  • The handleMapClick callback uses useCallback to avoid recreating the function on every render. It checks which point is missing and fills it in order.
  • The onClick={handleMapClick} prop on the Map component fires the callback whenever the user clicks anywhere on the map.
  • The BookingPanel renders outside the Map (as a sibling div), so it floats over the map as an overlay.
  • Save src/App.tsx and check your browser.
  • You should see a white rounded panel in the bottom-left corner with the title "Book a Ride" and two input rows showing "Click map to set pickup" and "Click map to set destination".
  • Click anywhere on the map. The panel's pickup row should update to show coordinates.
  • Click a second spot. The destination row should update with those coordinates.

Panel not appearing?

Make sure the BookingPanel is rendered OUTSIDE the <Map> component (as a sibling inside the outer div). If it is inside the Map, it may not render as expected.

Also check that your BookingPanel.tsx file exports the function with the export keyword. Without it, the import in App.tsx will fail silently.

help me debug my BookingPanel not showing up

Display colored markers for pickup and destination

The panel updates with coordinates, but the user has no visual feedback on the map itself. Green and red circle markers will show exactly where the pickup and destination are.

  • In src/App.tsx, find this import line at the top:
import Map from "react-map-gl/maplibre";
  • Replace it with this to also import the Marker component:
import Map, { Marker } from "react-map-gl/maplibre";
  • Inside the <Map> component, find the closing of the drivers map block:
        {drivers.map((driver) => (
          <DriverMarker key={driver.id} driver={driver} />
        ))}
  • Add the following code directly below that block (still inside <Map>):
        {pickup && (
          <Marker longitude={pickup.longitude} latitude={pickup.latitude} anchor="center">
            <div style={{ width: "16px", height: "16px", background: "#22c55e", borderRadius: "50%", border: "3px solid white", boxShadow: "0 2px 6px rgba(0,0,0,0.3)" }} />
          </Marker>
        )}

        {destination && (
          <Marker longitude={destination.longitude} latitude={destination.latitude} anchor="center">
            <div style={{ width: "16px", height: "16px", background: "#ef4444", borderRadius: "50%", border: "3px solid white", boxShadow: "0 2px 6px rgba(0,0,0,0.3)" }} />
          </Marker>
        )}

What does this code do?

  • Each marker only renders when its state is not null (the {pickup && ...} pattern).
  • The green circle (#22c55e) represents the pickup point. The red circle (#ef4444) represents the destination.
  • The white border and box shadow make the markers pop against any map background color.
  • Save src/App.tsx and refresh your browser.
  • Click the map once. You should see a green circle marker appear at that location and the panel's pickup row update with coordinates.
  • Click a second location. A red circle marker should appear and the destination row should update.
  • Click a third time. The green marker jumps to the new spot and the red marker disappears (the cycle resets).

Markers not appearing on click?

Check that you imported Marker from react-map-gl/maplibre (not from react-map-gl directly). The import must include the /maplibre path.

Make sure the pickup/destination marker JSX is inside the <Map> component (as children), not outside it. The Marker component must be a child of Map to render on the map canvas.

help me debug my pickup and destination markers

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files: src/utils/geo.ts, src/components/BookingPanel.tsx, and src/App.tsx.

ⓧ I'd like to double check the full code

export interface LatLng {
  latitude: number;
  longitude: number;
}
import type { LatLng } from "../utils/geo";

interface BookingPanelProps {
  pickup: LatLng | null;
  destination: LatLng | null;
  onRequestRide: () => void;
}

export function BookingPanel({ pickup, destination, onRequestRide }: BookingPanelProps) {
  return (
    <div style={panelStyle}>
      <h2 style={{ margin: "0 0 12px 0", fontSize: "18px" }}>Book a Ride</h2>

      <div style={inputStyle}>
        <span style={{ color: "#22c55e", marginRight: "8px" }}>●</span>
        {pickup
          ? `${pickup.latitude.toFixed(4)}, ${pickup.longitude.toFixed(4)}`
          : "Click map to set pickup"}
      </div>

      <div style={inputStyle}>
        <span style={{ color: "#ef4444", marginRight: "8px" }}>●</span>
        {destination
          ? `${destination.latitude.toFixed(4)}, ${destination.longitude.toFixed(4)}`
          : "Click map to set destination"}
      </div>
    </div>
  );
}

const panelStyle: React.CSSProperties = {
  position: "absolute",
  bottom: "24px",
  left: "24px",
  background: "white",
  borderRadius: "12px",
  padding: "20px",
  boxShadow: "0 4px 20px rgba(0,0,0,0.15)",
  width: "300px",
  zIndex: 1,
};

const inputStyle: React.CSSProperties = {
  background: "#f3f4f6",
  borderRadius: "8px",
  padding: "10px 12px",
  marginBottom: "8px",
  fontSize: "13px",
  color: "#374151",
};
import { useState, useCallback } from "react";
import Map, { Marker } from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";
import { useDriverSimulation } from "./hooks/useDriverSimulation";
import { DriverMarker } from "./components/DriverMarker";
import { BookingPanel } from "./components/BookingPanel";
import type { LatLng } from "./utils/geo";
import type { MapLayerMouseEvent } from "react-map-gl/maplibre";

function App() {
  const { drivers } = useDriverSimulation();
  const [pickup, setPickup] = useState<LatLng | null>(null);
  const [destination, setDestination] = useState<LatLng | null>(null);

  const handleMapClick = useCallback(
    (event: MapLayerMouseEvent) => {
      const { lng, lat } = event.lngLat;
      const point: LatLng = { latitude: lat, longitude: lng };

      if (!pickup) {
        setPickup(point);
      } else if (!destination) {
        setDestination(point);
      } else {
        setPickup(point);
        setDestination(null);
      }
    },
    [pickup, destination]
  );

  const handleRequestRide = () => {
    alert("Ride requested! (Secret Mission: animate the driver approaching)");
  };

  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <Map
        initialViewState={{
          longitude: -74.006,
          latitude: 40.7128,
          zoom: 14,
        }}
        style={{ width: "100%", height: "100%" }}
        mapStyle="https://demotiles.maplibre.org/style.json"
        onClick={handleMapClick}
      >
        {drivers.map((driver) => (
          <DriverMarker key={driver.id} driver={driver} />
        ))}

        {pickup && (
          <Marker longitude={pickup.longitude} latitude={pickup.latitude} anchor="center">
            <div style={{ width: "16px", height: "16px", background: "#22c55e", borderRadius: "50%", border: "3px solid white", boxShadow: "0 2px 6px rgba(0,0,0,0.3)" }} />
          </Marker>
        )}

        {destination && (
          <Marker longitude={destination.longitude} latitude={destination.latitude} anchor="center">
            <div style={{ width: "16px", height: "16px", background: "#ef4444", borderRadius: "50%", border: "3px solid white", boxShadow: "0 2px 6px rgba(0,0,0,0.3)" }} />
          </Marker>
        )}
      </Map>

      <BookingPanel
        pickup={pickup}
        destination={destination}
        onRequestRide={handleRequestRide}
      />
    </div>
  );
}

export default App;

Your app now responds to user interaction. The map captures clicks, the panel reflects what the user selected, and colored markers provide clear visual feedback. Next up, you will draw a route line between those two points and calculate a fare estimate.

Draw the Route and Calculate the Fare

Your map now shows pickup and destination markers wherever you click. The next piece of a ride-booking app is showing the rider what they're paying for.

In this step, you'll draw a visible route line between the two points and calculate a fare estimate based on the real-world distance. This is the same pattern that production ride-hailing apps use to quote prices before a rider confirms.

In this step, get ready to:
  • Draw a dashed route line between pickup and destination using a GeoJSON layer.
  • Calculate the distance between two points using the Haversine formula.
  • Display a fare estimate and Request Ride button in the booking panel.
Draw the route line between pickup and destination

Map libraries like MapLibre GL JS render geographic shapes using a format called GeoJSON. You define the shape's coordinates in a GeoJSON object, then hand it to the map as a data source with a visual layer on top.

  • In src/App.tsx, find the import line at the top of the file:
import Map, { Marker } from "react-map-gl/maplibre";
  • Add Source and Layer to the import so it looks like this:
import Map, { Marker, Source, Layer } from "react-map-gl/maplibre";
  • Next, find the handleRequestRide function inside the App component. Copy the following code directly below it:
  const routeGeoJSON = pickup && destination ? {
    type: "Feature" as const,
    properties: {},
    geometry: {
      type: "LineString" as const,
      coordinates: [
        [pickup.longitude, pickup.latitude],
        [destination.longitude, destination.latitude],
      ],
    },
  } : null;

What does this code do?

This creates a GeoJSON Feature object with a LineString geometry connecting your two points. GeoJSON always uses [longitude, latitude] order (the opposite of how we usually say it).

The variable is null when either point is missing, so the route only appears once both are set.

  • Now scroll down to the JSX inside the <Map> component. Find the closing tag for the destination Marker block (the red dot). Copy this code directly below it, before the closing </Map> tag:
        {routeGeoJSON && (
          <Source id="route" type="geojson" data={routeGeoJSON}>
            <Layer
              id="route-line"
              type="line"
              paint={{
                "line-color": "#000",
                "line-width": 4,
                "line-dasharray": [2, 1],
              }}
            />
          </Source>
        )}

What does this code do?

  • Source feeds your GeoJSON data into the map's rendering engine.
  • Layer tells the map how to visually display that data. Here you're drawing a black dashed line (line-dasharray: [2, 1]) that's 4 pixels wide.
  • Save src/App.tsx and switch to your browser.
  • Click two spots on the map (one for pickup, one for destination). You should see a dashed black line connecting the green and red dots.

Don't see the route line?

  • Make sure you placed the routeGeoJSON variable inside the App function body, not outside it.
  • Check that the Source and Layer JSX is inside the <Map> component, not outside it.
  • Confirm you added Source and Layer to your import statement at the top of the file.

Why isn't my route line showing on the map?

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

import { useState, useCallback } from "react";
import Map, { Marker, Source, Layer } from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";
import { useDriverSimulation } from "./hooks/useDriverSimulation";
import { DriverMarker } from "./components/DriverMarker";
import { BookingPanel } from "./components/BookingPanel";
import type { LatLng } from "./utils/geo";
import type { MapLayerMouseEvent } from "react-map-gl/maplibre";

function App() {
  const { drivers } = useDriverSimulation();
  const [pickup, setPickup] = useState<LatLng | null>(null);
  const [destination, setDestination] = useState<LatLng | null>(null);

  const handleMapClick = useCallback(
    (event: MapLayerMouseEvent) => {
      const { lng, lat } = event.lngLat;
      const point: LatLng = { latitude: lat, longitude: lng };

      if (!pickup) {
        setPickup(point);
      } else if (!destination) {
        setDestination(point);
      } else {
        setPickup(point);
        setDestination(null);
      }
    },
    [pickup, destination]
  );

  const handleRequestRide = () => {
    alert("Ride requested! (Secret Mission: animate the driver approaching)");
  };

  const routeGeoJSON = pickup && destination ? {
    type: "Feature" as const,
    properties: {},
    geometry: {
      type: "LineString" as const,
      coordinates: [
        [pickup.longitude, pickup.latitude],
        [destination.longitude, destination.latitude],
      ],
    },
  } : null;

  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <Map
        initialViewState={{
          longitude: -74.006,
          latitude: 40.7128,
          zoom: 14,
        }}
        style={{ width: "100%", height: "100%" }}
        mapStyle="https://demotiles.maplibre.org/style.json"
        onClick={handleMapClick}
      >
        {drivers.map((driver) => (
          <DriverMarker key={driver.id} driver={driver} />
        ))}

        {pickup && (
          <Marker longitude={pickup.longitude} latitude={pickup.latitude} anchor="center">
            <div style={{ width: "16px", height: "16px", background: "#22c55e", borderRadius: "50%", border: "3px solid white", boxShadow: "0 2px 6px rgba(0,0,0,0.3)" }} />
          </Marker>
        )}

        {destination && (
          <Marker longitude={destination.longitude} latitude={destination.latitude} anchor="center">
            <div style={{ width: "16px", height: "16px", background: "#ef4444", borderRadius: "50%", border: "3px solid white", boxShadow: "0 2px 6px rgba(0,0,0,0.3)" }} />
          </Marker>
        )}

        {routeGeoJSON && (
          <Source id="route" type="geojson" data={routeGeoJSON}>
            <Layer
              id="route-line"
              type="line"
              paint={{
                "line-color": "#000",
                "line-width": 4,
                "line-dasharray": [2, 1],
              }}
            />
          </Source>
        )}
      </Map>

      <BookingPanel
        pickup={pickup}
        destination={destination}
        onRequestRide={handleRequestRide}
      />
    </div>
  );
}

export default App;
Calculate the distance using the Haversine formula

To quote a fare, you need the distance between two points on Earth. The Haversine formula calculates the shortest distance between two latitude/longitude coordinates on a sphere. It accounts for the curvature of the Earth.

  • Open src/utils/geo.ts. You'll see the LatLng interface you created earlier. Add the following functions below it:
export function haversineDistance(point1: LatLng, point2: LatLng): number {
  // Earth's radius in kilometers
  const R = 6371;
  // Convert latitude/longitude differences to radians
  const dLat = toRad(point2.latitude - point1.latitude);
  const dLon = toRad(point2.longitude - point1.longitude);
  // Haversine formula
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(toRad(point1.latitude)) *
      Math.cos(toRad(point2.latitude)) *
      Math.sin(dLon / 2) *
      Math.sin(dLon / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return R * c;
}

function toRad(deg: number): number {
  return deg * (Math.PI / 180);
}

What does this code do?

  • haversineDistance takes two points and returns the distance in kilometers. It uses Earth's radius (6,371 km) to convert angular differences into actual surface distance.
  • toRad is a helper that converts degrees to radians, which the trigonometry functions need.
  • Now add the fare calculation function directly below toRad:
export function calculateFare(distanceKm: number): number {
  const baseFare = 2.5;
  const perKmRate = 1.5;
  const minimumFare = 5.0;
  // Total fare = base charge + distance-based charge
  const fare = baseFare + distanceKm * perKmRate;
  return Math.max(fare, minimumFare);
}

What does this code do?

calculateFare uses a simple pricing model: a $2.50 base fare plus $1.50 per kilometer, with a $5.00 minimum. Math.max ensures short rides never cost less than the minimum.

  • Save src/utils/geo.ts.

You won't see a change yet. The next substep uses these functions in the BookingPanel.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before continuing.

ⓧ I'd like to double check the full code

export interface LatLng {
  latitude: number;
  longitude: number;
}

export function haversineDistance(point1: LatLng, point2: LatLng): number {
  const R = 6371;
  const dLat = toRad(point2.latitude - point1.latitude);
  const dLon = toRad(point2.longitude - point1.longitude);
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(toRad(point1.latitude)) *
      Math.cos(toRad(point2.latitude)) *
      Math.sin(dLon / 2) *
      Math.sin(dLon / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return R * c;
}

function toRad(deg: number): number {
  return deg * (Math.PI / 180);
}

export function calculateFare(distanceKm: number): number {
  const baseFare = 2.5;
  const perKmRate = 1.5;
  const minimumFare = 5.0;
  const fare = baseFare + distanceKm * perKmRate;
  return Math.max(fare, minimumFare);
}
Display the fare estimate in the BookingPanel

Now you'll wire the distance and fare calculations into the panel so the rider sees pricing before they confirm.

  • Open src/components/BookingPanel.tsx. Find the existing import line at the top:
import type { LatLng } from "../utils/geo";
  • Add a second import line directly below it to bring in the utility functions:
import { haversineDistance, calculateFare } from "../utils/geo";
  • Inside the BookingPanel function, find the opening line of the return statement. Add these two lines above it (between the function's opening brace and the return):
  // Calculate distance and fare when both points exist
  const distance = pickup && destination ? haversineDistance(pickup, destination) : null;
  const fare = distance !== null ? calculateFare(distance) : null;
  • Now find the closing </div> for the destination input row (the one with the red dot). Add the following fare display block directly below it, before the panel's closing </div>:
      {distance !== null && fare !== null && (
        <div style={{ marginTop: "12px" }}>
          <p style={{ margin: "4px 0", fontSize: "14px", color: "#6b7280" }}>
            Distance: {distance.toFixed(2)} km
          </p>
          <p style={{ margin: "4px 0", fontSize: "20px", fontWeight: "bold" }}>
            ${fare.toFixed(2)}
          </p>
          <button onClick={onRequestRide} style={buttonStyle}>
            Request Ride
          </button>
        </div>
      )}
  • Finally, add the buttonStyle constant at the bottom of the file, below the existing inputStyle:
const buttonStyle: React.CSSProperties = {
  width: "100%",
  padding: "12px",
  background: "#000",
  color: "#fff",
  border: "none",
  borderRadius: "8px",
  fontSize: "16px",
  fontWeight: "bold",
  cursor: "pointer",
  marginTop: "8px",
};

What does this code do?

  • The distance and fare variables are computed only when both pickup and destination exist. Otherwise they stay null.
  • The conditional render ({distance !== null && fare !== null && ...}) means the fare section only appears once both map clicks have been made.
  • The buttonStyle creates a full-width black button that matches the Uber design aesthetic.
  • Save src/components/BookingPanel.tsx and switch to your browser.
  • Click two spots on the map. You should see the distance in kilometers, a dollar fare amount, and a black Request Ride button appear in the booking panel.

Fare not showing up?

  • Check your terminal for TypeScript errors. A common cause is a typo in the import path (../utils/geo).
  • Make sure you exported haversineDistance and calculateFare with the export keyword in geo.ts.
  • Verify you placed the distance and fare lines above the return statement, not inside the JSX.

Why isn't my fare calculation appearing in the BookingPanel?

✔️ Awesome, I've got everything!

Great. Confirm you've saved the file and the fare displays correctly.

ⓧ I'd like to double check the full code

import type { LatLng } from "../utils/geo";
import { haversineDistance, calculateFare } from "../utils/geo";

interface BookingPanelProps {
  pickup: LatLng | null;
  destination: LatLng | null;
  onRequestRide: () => void;
}

export function BookingPanel({ pickup, destination, onRequestRide }: BookingPanelProps) {
  const distance = pickup && destination ? haversineDistance(pickup, destination) : null;
  const fare = distance !== null ? calculateFare(distance) : null;

  return (
    <div style={panelStyle}>
      <h2 style={{ margin: "0 0 12px 0", fontSize: "18px" }}>Book a Ride</h2>

      <div style={inputStyle}>
        <span style={{ color: "#22c55e", marginRight: "8px" }}>●</span>
        {pickup
          ? `${pickup.latitude.toFixed(4)}, ${pickup.longitude.toFixed(4)}`
          : "Click map to set pickup"}
      </div>

      <div style={inputStyle}>
        <span style={{ color: "#ef4444", marginRight: "8px" }}>●</span>
        {destination
          ? `${destination.latitude.toFixed(4)}, ${destination.longitude.toFixed(4)}`
          : "Click map to set destination"}
      </div>

      {distance !== null && fare !== null && (
        <div style={{ marginTop: "12px" }}>
          <p style={{ margin: "4px 0", fontSize: "14px", color: "#6b7280" }}>
            Distance: {distance.toFixed(2)} km
          </p>
          <p style={{ margin: "4px 0", fontSize: "20px", fontWeight: "bold" }}>
            ${fare.toFixed(2)}
          </p>
          <button onClick={onRequestRide} style={buttonStyle}>
            Request Ride
          </button>
        </div>
      )}
    </div>
  );
}

const panelStyle: React.CSSProperties = {
  position: "absolute",
  bottom: "24px",
  left: "24px",
  background: "white",
  borderRadius: "12px",
  padding: "20px",
  boxShadow: "0 4px 20px rgba(0,0,0,0.15)",
  width: "300px",
  zIndex: 1,
};

const inputStyle: React.CSSProperties = {
  background: "#f3f4f6",
  borderRadius: "8px",
  padding: "10px 12px",
  marginBottom: "8px",
  fontSize: "13px",
  color: "#374151",
};

const buttonStyle: React.CSSProperties = {
  width: "100%",
  padding: "12px",
  background: "#000",
  color: "#fff",
  border: "none",
  borderRadius: "8px",
  fontSize: "16px",
  fontWeight: "bold",
  cursor: "pointer",
  marginTop: "8px",
};

Your ride-booking app now shows a route, calculates real distances, and quotes a fare. That is the complete core experience. Next up, you'll tackle the Secret Mission and animate a driver moving toward your pickup point after you confirm the ride.

Secret mission

Driver Approaching Animation

Your app can book a ride, but what happens after you press the button? In a real ride-hailing app, the nearest driver starts heading your way. Build a driver approaching animation that moves the closest car toward your pickup point, complete with status messages.

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 or cloud resources to worry about.

Resources you used:

  • The uber-clone project folder on your Desktop (includes source code and node_modules).
  • Node.js (if you installed it for this project).

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend the app with the Secret Mission.

  • Your project folder stays on your Desktop ready to use.
  • Node.js stays installed on your machine and is useful for any future JavaScript or TypeScript projects.
  • If the dev server is still running in your terminal, you can leave it running or stop it anytime with Ctrl+C.

Pause - I'll come back to this later

Shut down the dev server to free up your terminal. Your files stay on disk so you can resume anytime.

  • In the terminal where the dev server is running, press Ctrl+C to stop it.
  • Your project folder and all code remain on your Desktop.
  • To resume later, open a terminal, navigate to the uber-clone folder, and run npm run dev.

Delete - I don't want to use this again

Remove all project files and start fresh. This permanently deletes your code.

  • Stop the dev server by pressing Ctrl+C in the terminal if it is still running.
  • Delete the project folder by running the command for your operating system.

macOS

  • Remove the project folder by running:
rm -rf ~/Desktop/uber-clone

Windows

  • Remove the project folder by running:
rmdir /s /q %USERPROFILE%\Desktop\uber-clone
  • Node.js can stay installed on your machine since it is useful for other JavaScript projects. If you want to remove it, follow the official uninstall guide for your operating system at nodejs.org.

That's a Wrap!

That's a Wrap!

Nice work! You just built a ride-booking app from scratch using React and MapLibre GL JS.

You've learned how to:

  • Built a fullscreen interactive map application using React and MapLibre GL JS with animated driver markers that simulate real-time GPS movement.
  • Implemented click-to-select pickup and destination points on a map, drawing a route line between them using GeoJSON layers.
  • Calculated distances with the Haversine formula and built a fare estimation system based on distance.
  • Secret Mission: Animated the nearest driver approaching your pickup point after confirming a ride.

Ready to quiz yourself?