# Level 1 Team Project

## Air  Quality Monitoring Simulator

<figure><img src="/files/E78g6NGltbh8kRWQigKI" alt=""><figcaption></figcaption></figure>

### Step 1: Project Setup

1. Install Vite + React

```bash
// 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'.

3. 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)

```jsx
// 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)&#x20;

Create src/<mark style="color:orange;">style.css</mark> at src foler.

```css
// 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

```bash
// leaflet module install
npm install leaflet react-leaflet
```

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

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

### Step 5: Update App.jsx

Our`App.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

```jsx
// 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

```css
// 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

```bash
// Some code
npm install leaflet react-leaflet axios
```

In '<mark style="color:orange;">index.html</mark>' inside , add: Add CDN link in `index.html`.

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

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

```jsx
// 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:

```bash
npm install express cors axios
```

Create a new file at <mark style="color:red;">root</mark> directory: server.js

```bash
touch server.js
```

```javascript
// 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:

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

To:

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

### Step 3: Run your backend and frontend

In one terminal, run:

```bash
node serverMock.js
```

In another terminal, run your React app:

```bash
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:

```jsx
// 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:**

```jsx
// 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:

```jsx
// 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:

```jsx
// 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:

<figure><img src="/files/6sWjm9bkpQ7UeDkONRsy" alt=""><figcaption></figcaption></figure>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://reactjs.koida.tech/final-term-project/level-1-team-project-2.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
