ReactJS-The Beginner Master Class
  • React In the Beginning
    • Lesson 1 - Demo: Build a Simple React App - Fast
    • Lesson 2 - HTML Review
    • Lesson 3 - CSS Review
    • Lesson 4 - Modern JavaScript (JSX) Patterns
    • Lesson 5 - Set up Dev Environment
    • Hands-on Practice
  • React Fundamentals
    • Lesson 1 - Understanding Old vs New Way of Building Web Apps - SPAs
    • Lesson 2 - Motivation for Using React as the Solution to Vanilla JS
    • Lesson 3 - What is ReactJS - How it Works
    • React Code Along from Scratch
    • Lesson 4 - Create and Run a React Project with Vite - Full Overview
    • Lesson 5 - Create Hook by State Management in React
    • Lesson 6 - React Project File and Folder Walkthrough
    • Lesson 7 - JSX and How React Treats the DOM & JSX Compilation(by Babeljs) - Overview
    • Lesson 8 - Understanding the Main Files - App.jsx and main.jsx
    • Lesson 9 - Props and One-Way Data Flow - Overview
    • Lesson 10 - Google Bookshelf App - Ver 1.0
    • Hands-on Practice I
    • Hands-on Practice II
  • React State and Styling
    • Lesson 1 - Pulling Book Data from a Different Data File
    • Lesson 2 - Overview of How State Works in React
    • Lesson 3 - RandomQuote App
    • Lesson 4 - Lifting State Up - React Pattern Overview
    • Hands-On - Simple Counter
  • Forms and Interactivity - Grocery List App
    • Lesson 1 - Setup a Simple Form and Input
    • Lesson 2 - Build Form Profile App Using Multi-input Form Data
    • Hands-on : Build a Grocery List App
  • Connecting to the Backend - Consuming APIs - UseEffect Hook
    • Lesson 1 - Connecting to the Back End - Understanding Side Effects, Hooks and useEffect - Overview
    • Lesson 2 - Fetching Data from the Backend the Right Way with useEffect Hook
    • Lesson 3 - Setting Up Loading State
    • Hands-on :Use Dependency Array and Adding Values that Control Side Effects
  • Solo Project 1
  • RESTful APIs :Build a BookSearch App -Ver 2.0
    • Lesson 1: Build and test RESTful APIs with Postman
    • Lesson 2 - BookShelf App Structure
    • Lesson 3 - Create NavBar.jsx Component
    • Lesson 4 - Create Footer Component
    • Lesson 5 - Create BookList.jsx Component
    • Lesson 6 - Create BookCard.jsx Component
    • Lesson 7 - Creating Custom Hooks - useBooks and api-client
    • Lesson 8 - Controlling Network Activities in React with AbortController
    • Lesson 9 - Show Book Details in a Modal - Working
    • Lesson 10 - Bookshelf App Summary
  • Multi-Page Applications (MPAs)
    • Build a Multi-Page React Application
    • Multi-Page React Application
    • Hands-on Practice
  • Backend Frameworks-NEXT.JS
    • Migrating from React to Next.js
    • Lesson 1: Key Concepts of NodeJS and Express for Backend Web Development
    • Lesson 2: How to set up a new Next.js project
    • Lesson 3: How to create Layouts and Pages
    • Hands-on Practice 1
    • Hands on Practice 2
      • New Project & Folder Structure
      • File-Based Routing
      • Server vs Client Components & Router Hooks
      • Start On The Navbar
      • Navbar Links, Dropdowns & React Icons
      • Active Links & Conditional Rendering
      • Homepage Components
      • Properties Page
      • Property Card Dynamic Data
      • Home Property Listings
      • Custom Not Found & Loading Pages
  • Git and GitHubs
    • Git Installation
    • Git Commands
    • GitHub Repository
    • Hands-on Practice
  • Database in Application
    • React Supabase CRUD
    • Hands-on: Note Taking App
  • NoSQL Database
    • Installing MongoDB Community Edition
    • System Env Path Setting
    • How to use MongoDB Shell
    • How to Connect and Use Mongosh in VS Code via MongoDB Extension
    • MongoDB Guide
  • Solo Project 2
  • Deployment and Web Hosting
    • Lesson 1. React+Vite+TS+Shadcn Project
    • Lesson 2. Deploying a React Vite App to Vercel from Vercel CLI
    • Lesson 3 Connecting to GitHub Repo and Automate Deployment
  • Solo Project 3
  • Final Term Project
    • Level 1 Team Project
    • Level 1 Team Project
    • Level 1 Team Project
    • Level 1 Team Project
    • Level 2 Team Project
    • Level 2 Team Project
    • Level 3 Team Project
    • Level 3 Team Project
Powered by GitBook
On this page
  • Air Quality Monitoring Simulator
  • Step 1: Project Setup
  • Step 2: Setup App.jsx (you already have this, we will improve a bit)
  • Step 3: Improve Styling (style.css)
  • Step 4. Install Leaflet packages and Include Leaflet CSS
  • Step 5: Update App.jsx
  • Step 6: Improve style.css for Map
  • Challenge: Next Enhancements (Highly Recommended!)
  • Step 6. Install dependencies and Add Leaflet CSS
  • Step 7: Update App.jsx with Real-Time AQI Simulation & API Integration
  • Solution: Create Your Own Local Backend Proxy (Best Practice)
  • Step 2: Update your React App.jsx
  • Step 3: Run your backend and frontend
  • Solution 2: Local Mock Data (For Safe Classroom Project)
  • Switch map mode between Dark Mode and Light Mode
  1. Final Term Project

Level 1 Team Project

PreviousLevel 1 Team ProjectNextLevel 1 Team Project

Last updated 1 month ago

Air Quality Monitoring Simulator

Step 1: Project Setup

  1. Install Vite + React

// setup
// In your terminal, run:
npm create vite@latest air-quality-monitor -- --template react
cd air-quality-monitor
npm install

2. Clean up the boilerplate // Delete unnecessary files in 'src' and keep 'main.jsx', 'App.jsx', and 'App.css'.

  1. Add style.css file (external stylesheet)

// Create a new file in 'src/style.css'. We'll move common styles here for better structure.

Step 2: Setup App.jsx (you already have this, we will improve a bit)

// File: src/App.jsx
import React, { useState, useEffect, useMemo } from 'react';
import './App.css';
import './style.css';

export default function App() {
  const [cities, setCities] = useState([]);
  const [selectedCountry, setSelectedCountry] = useState('');
  const [selectedCity, setSelectedCity] = useState('');
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setTimeout(() => {
      setCities([
        { id: '1', name: 'New York', country: 'USA', airQualityIndex: 42, coordinates: [40.7128, -74.0060] },
        { id: '2', name: 'Los Angeles', country: 'USA', airQualityIndex: 58, coordinates: [34.0522, -118.2437] },
        { id: '3', name: 'London', country: 'UK', airQualityIndex: 35, coordinates: [51.5074, -0.1278] },
        { id: '4', name: 'Tokyo', country: 'Japan', airQualityIndex: 22, coordinates: [35.6895, 139.6917] },
      ]);
      setIsLoading(false);
    }, 1000);
  }, []);

  const countries = useMemo(() => Array.from(new Set(cities.map(city => city.country))), [cities]);

  const filteredCities = useMemo(() => cities.filter(city =>
    (selectedCountry ? city.country === selectedCountry : true) &&
    (selectedCity ? city.name === selectedCity : true)
  ), [cities, selectedCountry, selectedCity]);

  if (isLoading) return <div className="loading">Loading...</div>;
  if (error) return <div className="error">Error: {error.message}</div>;

  return (
    <div className="app-container">
      <h1 className="heading">Air Quality Dashboard</h1>
      <div className="filters">
        <select className="input" value={selectedCountry} onChange={e => setSelectedCountry(e.target.value)}>
          <option value="">Select Country</option>
          {countries.map(country => (
            <option key={country} value={country}>{country}</option>
          ))}
        </select>
        <select className="input" value={selectedCity} onChange={e => setSelectedCity(e.target.value)}>
          <option value="">Select City</option>
          {filteredCities.map(city => (
            <option key={city.id} value={city.name}>{city.name}</option>
          ))}
        </select>
      </div>
      <div className="map">
        <div className="map-placeholder">Map will be displayed here</div>
      </div>
    </div>
  );
}

Step 3: Improve Styling (style.css)

Create src/style.css at src foler.

// style.css
body {
  margin: 0;
  padding: 0;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background-color: #f8fafc;
  color: #333;
}

.loading {
  font-size: 1.5rem;
  color: #0369a1;
}

.error {
  color: red;
  font-size: 1.2rem;
}

button, select {
  border-radius: 8px;
  border: 1px solid #ccc;
  padding: 10px;
  font-size: 1rem;
  transition: background-color 0.3s ease;
}

button:hover, select:hover {
  background-color: #e0f2fe;
  cursor: pointer;
}

We would like to expand our app to include the following functionality:

  • 🎯 Integrate Leaflet.js for real maps.

  • πŸ”Œ Use OpenAQ API for real-time air quality data.

  • 🎨 Add dynamic map markers and city info popup.

  • πŸ” Add search box for better UX.

Step 4. Install Leaflet packages and Include Leaflet CSS

// leaflet module install
npm install leaflet react-leaflet

In your index.html, inside <head>, add:

// Sindex.html
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />

Step 5: Update App.jsx

OurApp.jsx is now fully set up to:

  • Import Leaflet components

  • Dynamically place markers based on city data

  • Show popup with city name, AQI, and country

// updated App.jsx
import React, { useState, useEffect, useMemo } from 'react';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import './App.css';
import './style.css';

// Fix default icon issue in Leaflet
import iconUrl from 'leaflet/dist/images/marker-icon.png';
import iconShadow from 'leaflet/dist/images/marker-shadow.png';
const DefaultIcon = L.icon({ iconUrl, shadowUrl: iconShadow });
L.Marker.prototype.options.icon = DefaultIcon;

export default function App() {
  const [cities, setCities] = useState([]);
  const [selectedCountry, setSelectedCountry] = useState('');
  const [selectedCity, setSelectedCity] = useState('');
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setTimeout(() => {
      setCities([
        { id: '1', name: 'New York', country: 'USA', airQualityIndex: 42, coordinates: [40.7128, -74.0060] },
        { id: '2', name: 'Los Angeles', country: 'USA', airQualityIndex: 58, coordinates: [34.0522, -118.2437] },
        { id: '3', name: 'London', country: 'UK', airQualityIndex: 35, coordinates: [51.5074, -0.1278] },
        { id: '4', name: 'Tokyo', country: 'Japan', airQualityIndex: 22, coordinates: [35.6895, 139.6917] },
      ]);
      setIsLoading(false);
    }, 1000);
  }, []);

  const countries = useMemo(() => Array.from(new Set(cities.map(city => city.country))), [cities]);

  const filteredCities = useMemo(() => cities.filter(city =>
    (selectedCountry ? city.country === selectedCountry : true) &&
    (selectedCity ? city.name === selectedCity : true)
  ), [cities, selectedCountry, selectedCity]);

  const mapCenter = filteredCities.length > 0 ? filteredCities[0].coordinates : [20, 0]; // Default global view

  if (isLoading) return <div className="loading">Loading...</div>;
  if (error) return <div className="error">Error: {error.message}</div>;

  return (
    <div className="app-container">
      <h1 className="heading">Air Quality Dashboard</h1>
      <div className="filters">
        <select className="input" value={selectedCountry} onChange={e => setSelectedCountry(e.target.value)}>
          <option value="">Select Country</option>
          {countries.map(country => (
            <option key={country} value={country}>{country}</option>
          ))}
        </select>
        <select className="input" value={selectedCity} onChange={e => setSelectedCity(e.target.value)}>
          <option value="">Select City</option>
          {filteredCities.map(city => (
            <option key={city.id} value={city.name}>{city.name}</option>
          ))}
        </select>
      </div>

      <MapContainer center={mapCenter} zoom={2} className="map">
        <TileLayer
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
          attribution="&copy; OpenStreetMap contributors"
        />
        {filteredCities.map(city => (
          <Marker key={city.id} position={city.coordinates}>
            <Popup>
              <strong>{city.name}</strong><br />
              AQI: {city.airQualityIndex}<br />
              Country: {city.country}
            </Popup>
          </Marker>
        ))}
      </MapContainer>
    </div>
  );
}

Step 6: Improve style.css for Map

Our external CSS now ensures:

  • Map height and width are controlled

  • Inputs and buttons remain pretty

  • Responsive layout is prepared

// update style.css
body {
  margin: 0;
  padding: 0;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background-color: #f8fafc;
  color: #333;
}

.loading {
  font-size: 1.5rem;
  color: #0369a1;
}

.error {
  color: red;
  font-size: 1.2rem;
}

button, select {
  border-radius: 8px;
  border: 1px solid #ccc;
  padding: 10px;
  font-size: 1rem;
  transition: background-color 0.3s ease;
}

button:hover, select:hover {
  background-color: #e0f2fe;
  cursor: pointer;
}

.map {
  height: 500px;
  width: 100%;
  border-radius: 8px;
  margin-top: 20px;
}

Challenge: Next Enhancements (Highly Recommended!)

  • πŸš€ Auto-refresh AQI data (Simulate or connect to real API)

  • 🎨 Custom map styling (e.g., dark mode tiles)

  • πŸ” Search bar to quickly find cities

  • 🌐 Mobile responsiveness

  • 🧩 Cluster markers for crowded areas

Step 6. Install dependencies and Add Leaflet CSS

// Some code
npm install leaflet react-leaflet axios

In 'index.html' inside , add: Add CDN link in index.html.

<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
// Updated style.css

body {
  margin: 0;
  padding: 0;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background-color: #0f172a; /* Dark mode background */
  color: #f8fafc; /* Light text for dark mode */
}

.loading {
  font-size: 1.5rem;
  color: #38bdf8;
}

.error {
  color: red;
  font-size: 1.2rem;
}

button, select {
  border-radius: 8px;
  border: 1px solid #ccc;
  padding: 10px;
  font-size: 1rem;
  transition: background-color 0.3s ease;
  background-color: #1e293b;
  color: #f8fafc;
}

button:hover, select:hover {
  background-color: #334155;
  cursor: pointer;
}

.map {
  height: 500px;
  width: 100%;
  border-radius: 8px;
  margin-top: 20px;
}

Step 7: Update App.jsx with Real-Time AQI Simulation & API Integration

// Some code
import React, { useState, useEffect, useMemo } from 'react';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import L from 'leaflet';
import axios from 'axios';
import 'leaflet/dist/leaflet.css';
import './App.css';
import './style.css';

// Fix default icon issue in Leaflet
import iconUrl from 'leaflet/dist/images/marker-icon.png';
import iconShadow from 'leaflet/dist/images/marker-shadow.png';
const DefaultIcon = L.icon({ iconUrl, shadowUrl: iconShadow });
L.Marker.prototype.options.icon = DefaultIcon;

export default function App() {
  const [cities, setCities] = useState([]);
  const [selectedCountry, setSelectedCountry] = useState('');
  const [selectedCity, setSelectedCity] = useState('');
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  // Fetch initial data from OpenAQ API (optional)
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('https://api.openaq.org/v2/latest?limit=10');
        const cityData = response.data.results.map((location, index) => ({
          id: String(index + 1),
          name: location.city || 'Unknown',
          country: location.country || 'Unknown',
          airQualityIndex: Math.floor(Math.random() * 100), // Simulate AQI since API gives measurements not AQI directly
          coordinates: [location.coordinates.latitude, location.coordinates.longitude]
        }));
        setCities(cityData);
        setIsLoading(false);
      } catch (err) {
        console.error(err);
        setError(err);
        setIsLoading(false);
      }
    };
    fetchData();
  }, []);

  // Simulate real-time AQI updates every 10 seconds
  useEffect(() => {
    const interval = setInterval(() => {
      setCities(prevCities =>
        prevCities.map(city => ({
          ...city,
          airQualityIndex: Math.max(0, Math.min(500, city.airQualityIndex + Math.floor(Math.random() * 20 - 10))) // Clamp between 0 and 500
        }))
      );
    }, 10000); // 10 seconds

    return () => clearInterval(interval);
  }, []);

  const countries = useMemo(() => Array.from(new Set(cities.map(city => city.country))), [cities]);

  const filteredCities = useMemo(() => cities.filter(city =>
    (selectedCountry ? city.country === selectedCountry : true) &&
    (selectedCity ? city.name === selectedCity : true)
  ), [cities, selectedCountry, selectedCity]);

  const mapCenter = filteredCities.length > 0 ? filteredCities[0].coordinates : [20, 0];

  if (isLoading) return <div className="loading">Loading...</div>;
  if (error) return <div className="error">Error: {error.message}</div>;

  return (
    <div className="app-container">
      <h1 className="heading">Air Quality Dashboard</h1>
      <div className="filters">
        <select className="input" value={selectedCountry} onChange={e => setSelectedCountry(e.target.value)}>
          <option value="">Select Country</option>
          {countries.map(country => (
            <option key={country} value={country}>{country}</option>
          ))}
        </select>
        <select className="input" value={selectedCity} onChange={e => setSelectedCity(e.target.value)}>
          <option value="">Select City</option>
          {filteredCities.map(city => (
            <option key={city.id} value={city.name}>{city.name}</option>
          ))}
        </select>
      </div>

      <MapContainer center={mapCenter} zoom={2} className="map">
        {/* Dark mode tile layer */}
        <TileLayer
          url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
          attribution="&copy; OpenStreetMap contributors & CartoDB"
        />
        {filteredCities.map(city => (
          <Marker key={city.id} position={city.coordinates}>
            <Popup>
              <strong>{city.name}</strong><br />
              AQI: {city.airQualityIndex}<br />
              Country: {city.country}
            </Popup>
          </Marker>
        ))}
      </MapContainer>
    </div>
  );
}

When we run app, we encounter the following issues:

// Some code
AppAQIRealTime.jsx:46 
 GET https://corsproxy.io/?https://api.openaq.org/v2/latest?limit=10 410 (Gone)
fetchData	@	AppAQIRealTime.jsx:46
(anonymous)	@	AppAQIRealTime.jsx:65
<App>		
(anonymous)	@	main.jsx:12
// Some code
AppAQIRealTime.jsx:60 
AxiosError {message: 'Request failed with status code 410', name: 'AxiosError', code: 'ERR_BAD_REQUEST', config: {…}, request: XMLHttpRequest, …}
fetchData	@	AppAQIRealTime.jsx:60
await in fetchData		
(anonymous)	@	AppAQIRealTime.jsx:65
<App>		
(anonymous)	@	main.jsx:12

Solution: Create Your Own Local Backend Proxy (Best Practice)

If you want to practice backend/frontend separation (good for your portfolio), do this:

Step 1: Create a simple Express server

In your project root, run:

npm install express cors axios

Create a new file at root directory: server.js

touch server.js
// server.js
import express from 'express'
import cors from 'cors'
import axios from 'axios'

const app = express()
app.use(cors())

app.get('/api/airquality', async (req, res) => {
  try {
    const response = await axios.get('https://api.openaq.org/v2/locations', {
      params: {
        limit: 20,
        page: 1,
        sort: 'desc',
        order_by: 'lastUpdated',
      },
      headers: {
        Accept: 'application/json',
        'User-Agent': 'air-quality-monitoring-app',
      },
    })
    res.json(response.data)
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

const PORT = 4000
app.listen(PORT, () =>
  console.log(`Server running on http://localhost:${PORT}`),
)

Step 2: Update your React App.jsx

Change:

// old App.jsx
const response = await axios.get('https://api.openaq.org/v2/latest?limit=10');

To:

// updated App.jsx
const response = await axios.get('http://localhost:4000/api/airquality');

Step 3: Run your backend and frontend

In one terminal, run:

node serverMock.js

In another terminal, run your React app:

npm run dev

Solution 2: Local Mock Data (For Safe Classroom Project)

We're already simulating AQI, and to avoid unstable external APIs, let’s mock the data locally.

Step 1: Update your serverMock.js like this:

// serverMock.js
import express from 'express'
import cors from 'cors'
import axios from 'axios'

const app = express()
app.use(cors())

app.get('/api/airquality', (req, res) => {
  // Local mock data
  const mockData = {
    results: [
      {
        city: 'New York',
        country: 'USA',
        coordinates: { latitude: 40.7128, longitude: -74.006 },
      },
      {
        city: 'Los Angeles',
        country: 'USA',
        coordinates: { latitude: 34.0522, longitude: -118.2437 },
      },
      {
        city: 'London',
        country: 'UK',
        coordinates: { latitude: 51.5074, longitude: -0.1278 },
      },
      {
        city: 'Tokyo',
        country: 'Japan',
        coordinates: { latitude: 35.6895, longitude: 139.6917 },
      },
      {
        city: 'Paris',
        country: 'France',
        coordinates: { latitude: 48.8566, longitude: 2.3522 },
      },
    ],
  }
  res.json(mockData)
})

const PORT = 4000
app.listen(PORT, () =>
  console.log(`βœ… Backend server running at http://localhost:${PORT}`),
)

Switch map mode between Dark Mode and Light Mode

We will:

  1. Add a toggle button to switch between dark and light map styles.

  2. Update your <TileLayer /> component to dynamically use the selected mode.

Step 1: Add State for Map Mode

In your App.jsx, at the top inside your component, add state for map mode:

// App.jsx snippet
const [mapMode, setMapMode] = useState('dark');

Step 2: Add Toggle Button

Above your map (maybe below filters), add a button to switch modes:

// App.jsx snippet

<div className="filters">
  <select className="input" value={selectedCountry} onChange={e => setSelectedCountry(e.target.value)}>
    <option value="">Select Country</option>
    {countries.map(country => (
      <option key={country} value={country}>{country}</option>
    ))}
  </select>
  <select className="input" value={selectedCity} onChange={e => setSelectedCity(e.target.value)}>
    <option value="">Select City</option>
    {filteredCities.map(city => (
      <option key={city.id} value={city.name}>{city.name}</option>
    ))}
  </select>
  
  {/* Add New toggle button */}
  <button onClick={() => setMapMode(prevMode => prevMode === 'dark' ? 'light' : 'dark')}>
    Switch to {mapMode === 'dark' ? 'Light' : 'Dark'} Mode
  </button>
</div>

Step 3: Update TileLayer to Use Dynamic Map Style

Now go to your <TileLayer /> component and replace the static URL with a dynamic one:

// App.jsx snippet
<TileLayer
  url={
    mapMode === 'dark'
      ? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
      : 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
  }
  attribution="&copy; OpenStreetMap contributors & CartoDB"
/>

Now when you click the button:

  • πŸŒ— Dark mode: CartoDB dark tiles.

  • 🌞 Light mode: OpenStreetMap standard tiles.

  • βœ… Seamless switch!

  • βœ… Works even while map is active.

Output: