Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Netmanager-app Map Page Integration #2482

Conversation

AKATWIJUKA-ELIA
Copy link

@AKATWIJUKA-ELIA AKATWIJUKA-ELIA commented Feb 19, 2025

Summary of Changes (What does this PR do?)

  • I installed mapbox dependency like npm install mapbox-gl
  • I'm currently working on integrating the data and other functionality as stated in the issue [Netmanager-app] Implement the Map Page for NetManager App #2451 In airqo-platform/AirQo-frontend;·
  • I have Added Search Functionality for different Locations and displaying suggestions as the user types
    Screenshot 2025-02-16 190115
  • Ihave Added Layers to show AirQuality in different Locations like below
  • I have Added Imoji Face Icons to the map according to the Air Quality like Below and Implemented the Flat Map
    Screenshot 2025-02-19 182537
  • Adding Icons For Showing Different Layers

Screenshot 2025-02-21 174213

Status of maturity (all need to be checked before merging):

  • I've tested this locally
  • I consider this code done
  • This change ready to hit production in its current state
  • The title of the PR states what changed and the related issues number (used for the release note).
  • I've included issue number in the "Closes #ISSUE-NUMBER" part of the "What are the relevant tickets?" section to link the issue.
  • I've updated corresponding documentation for the changes in this PR.
  • I have written unit and/or e2e tests for my change(s).

How should this be manually tested?

npm install mapbox-gl

npm run dev

Summary by CodeRabbit

  • New Features
    • Introduced an interactive network map view that visualizes air quality data in real time, with dynamic location search and navigational capabilities.
    • Added new UI controls including custom zoom buttons, an icon-based toolbar, and a modal for selecting map layers and styles.
    • Enhanced the overall map experience with improved data processing and seamless integration of visual assets.

Copy link

coderabbitai bot commented Feb 19, 2025

📝 Walkthrough

Walkthrough

A new map feature has been integrated into the netmanager-app. The pull request introduces the Netmap component as a wrapper for the main NetManagerMap React component that renders a Mapbox map with interactive air quality data. New utility functions, UI components (e.g., modals, buttons, custom zoom control), several SVG icon components, and API helpers for fetching map data have also been added. Additionally, dependency updates and alias configurations have been enhanced.

Changes

File(s) Change Summary
app/…/network-map/page.tsx Added Netmap component that wraps NetManagerMap for authenticated map view.
components/NetManagerMap/page.tsx Introduced the Mapbox map component for visualizing air quality data with interactive features.
package.json Upgraded framer-motion from ^12.4.3 to ^12.4.7 and added dependency mapbox-gl@^3.10.0.
lib/utils.ts Added new utility functions: ConvertToGeojson for converting air quality data and useRefreshMap for refreshing the map.
components.json Added new alias mappings for icons and images.
components/NetManagerMap/components/(IconButton, LayerModal, zoom.tsx) Added new UI components: IconButton, LayerModal (with supporting Option component), and CustomZoomControl for managing map zoom actions.
components/NetManagerMap/data/constants.js Introduced constants: BOUNDARY_URL, mapStyles, and mapDetails for map configuration and imagery.
public/icons/map/(downArrow, gpsIcon, homeIcon, layerIcon, menuIcon, minusIcon, plusIcon, refreshIcon, shareIcon, upArrow) Added multiple new SVG icon components for map controls with customizable props.
core/apis/MapData.ts Added API functions: GetAirQuoData, FetchSuggestions, and UserClick for handling air quality data and location suggestions.

Sequence Diagram(s)

sequenceDiagram
    participant U as User
    participant NM as Netmap
    participant NMM as NetManagerMap
    participant API as MapData API

    U->>NM: Open Map Page
    NM->>NMM: Render map component
    NMM->>API: Request air quality data
    API-->>NMM: Return GeoJSON data
    U->>NMM: Perform search / interact with map
    NMM->>API: Fetch suggestions and location data
    API-->>NMM: Return suggestions
    NMM->>U: Display interactive map with popups
Loading

Suggested labels

netmanager

Suggested reviewers

  • Baalmart
  • OchiengPaul442
  • Codebmk

Poem

In the code, a map unfolds,
Guiding users where data beholds.
Components and icons, sleek and neat,
Make air quality insights a visual treat.
With each function and API call in line,
Our project now continues to shine.
Mapping the future one line at a time.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • JIRA integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @coderabbitai title anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

gitguardian bot commented Feb 19, 2025

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
- - Generic High Entropy Secret 769b61a netmanager-app/components/NetManagerMap/page.tsx View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
netmanager-app/components/NetManagerMap/page.tsx (1)

65-107: Optimize event listener cleanup.

The mouseenter/mouseleave event listeners should be properly cleaned up to prevent memory leaks.

Add cleanup logic:

+useEffect(() => {
+  const map = mapRef.current;
+  if (!map) return;
+
+  const onMouseEnter = (e: mapboxgl.MapMouseEvent) => {
+    // Your existing mouseenter logic
+  };
+
+  const onMouseLeave = () => {
+    // Your existing mouseleave logic
+  };
+
+  map.on('mouseenter', 'data-layer', onMouseEnter);
+  map.on('mouseleave', 'data-layer', onMouseLeave);
+
+  return () => {
+    map.off('mouseenter', 'data-layer', onMouseEnter);
+    map.off('mouseleave', 'data-layer', onMouseLeave);
+  };
+}, []);
🧰 Tools
🪛 Biome (1.9.4)

[error] 71-71: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

netmanager-app/components/sidebar.tsx (1)

464-464: Enhance collapse animation transition.

The collapse animation could be smoother with CSS transitions.

Add transition properties:

-<ChevronRight onClick={handleCollapsible} className={` ${navcollapse?"drop":"revert" } flex cursor-pointer  -ml-3 bg-white  text-black border border-3 border-gray-200 rounded-full drop-shadow-lg mt-10 `}/>
+<ChevronRight 
+  onClick={handleCollapsible} 
+  className={`
+    ${navcollapse ? "drop" : "revert"} 
+    flex cursor-pointer -ml-3 bg-white text-black 
+    border border-3 border-gray-200 rounded-full 
+    drop-shadow-lg mt-10
+    transition-all duration-300 ease-in-out
+  `}
+/>
netmanager-app/app/globals.css (1)

82-84: Clarify the intent of the .revert class.
While the transition effect is applied, if the purpose is to reverse the rotation applied by .drop, consider explicitly setting a transform (e.g., transform: rotate(0deg)), which would ensure a consistent and predictable reverse animation.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d07a316 and 178e929.

⛔ Files ignored due to path filters (8)
  • netmanager-app/package-lock.json is excluded by !**/package-lock.json
  • netmanager-app/public/images/map/GoodAir.png is excluded by !**/*.png
  • netmanager-app/public/images/map/Hazardous.png is excluded by !**/*.png
  • netmanager-app/public/images/map/Moderate.png is excluded by !**/*.png
  • netmanager-app/public/images/map/Unhealthy.png is excluded by !**/*.png
  • netmanager-app/public/images/map/UnhealthySG.png is excluded by !**/*.png
  • netmanager-app/public/images/map/UnknownAQ.png is excluded by !**/*.png
  • netmanager-app/public/images/map/VeryUnhealthy.png is excluded by !**/*.png
📒 Files selected for processing (5)
  • netmanager-app/app/(authenticated)/network-map/page.tsx (1 hunks)
  • netmanager-app/app/globals.css (1 hunks)
  • netmanager-app/components/NetManagerMap/page.tsx (1 hunks)
  • netmanager-app/components/sidebar.tsx (11 hunks)
  • netmanager-app/package.json (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
netmanager-app/components/NetManagerMap/page.tsx

[error] 71-71: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 158-163: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.

(lint/complexity/noUselessLoneBlockStatements)

🔇 Additional comments (3)
netmanager-app/app/(authenticated)/network-map/page.tsx (1)

1-10: Clean implementation of the wrapper component!

The component follows React best practices with a clear separation of concerns.

netmanager-app/package.json (1)

42-42:

✅ Verification successful

Verify Mapbox GL version compatibility.

Let's check if version 3.10.0 is the latest stable version and compatible with the project's React version.


🌐 Web query:

What is the latest stable version of mapbox-gl? Are there any known compatibility issues with React 18?

💡 Result:

The latest stable version of mapbox-gl is v3.10.0, as indicated by its GitHub releases and official documentation[1][10]. This version includes features like data-driven theme properties, improved SVG rendering, and bug fixes for querying features and WebGL context loss[1].

Compatibility with React 18

  • Known Issues:

    • Older versions of react-map-gl (the React wrapper for Mapbox GL JS) had compatibility issues with React 18 due to lifecycle method changes[8].
    • The react-map-gl v7 beta and later versions resolve these issues[5][6].
  • Workarounds:

    • Use react-map-gl v7+ for full React 18 compatibility[6].
    • For projects stuck on older react-map-gl versions (e.g., v6), ensure proper cleanup of map instances in useEffect hooks to avoid memory leaks[8].

For new projects, install via:

npm install react-map-gl@7 mapbox-gl

This combination ensures compatibility with React 18 while leveraging Mapbox GL JS v3.10.0[6][7].

Citations:


Mapbox GL Version Verified

The dependency "mapbox-gl": "^3.10.0" in netmanager-app/package.json is confirmed as the latest stable release. There are no direct compatibility issues with React 18 reported for Mapbox GL itself. However, if you're using the react-map-gl wrapper, please ensure you have upgraded to v7+ to avoid known lifecycle issues with older versions.

netmanager-app/app/globals.css (1)

78-81: Solid styling for the .drop class.
The class clearly defines a 180° rotation with a smooth 0.2-second ease-in-out transition, which should work well for visually toggling elements.

const mapRef = useRef<mapboxgl.Map | null>(null);
const [query, setQuery] = useState("");
const [locationId,setlocationId] =useState("")
const [suggestions, setSuggestions] = useState<any[]>([]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add TypeScript types for better type safety.

The code uses any type and lacks proper TypeScript interfaces. This reduces type safety and IDE support.

Add proper TypeScript interfaces:

interface Measurement {
  _id: string;
  site_id: string;
  time: string;
  siteDetails: {
    name: string;
    approximate_latitude: number;
    approximate_longitude: number;
  };
  aqi_category: string;
  aqi_color: string;
  pm2_5: {
    value: number;
  };
}

interface GeoJsonFeature {
  type: "Feature";
  geometry: {
    type: "Point";
    coordinates: [number, number];
  };
  properties: {
    id: string;
    site_id: string;
    time: string;
    location_name: string;
    aqi_category: string;
    aqi_color: string;
    value: number;
  };
}

interface GeoJsonData {
  type: "FeatureCollection";
  features: GeoJsonFeature[];
}

Also applies to: 236-266

const [query, setQuery] = useState("");
const [locationId,setlocationId] =useState("")
const [suggestions, setSuggestions] = useState<any[]>([]);
const token = 'pk.eyJ1IjoiZWxpYWxpZ2h0IiwiYSI6ImNtNzJsMnZnbjBhajIyanIwN3A3eWY2YmUifQ.x0x411yjbETiJ-F8ebivHQ'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Move Mapbox token to environment variables.

The Mapbox access token should not be hardcoded in the source code. This is a security risk as the token could be exposed in version control.

Move the token to environment variables:

-const token = 'pk.eyJ1IjoiZWxpYWxpZ2h0IiwiYSI6ImNtNzJsMnZnbjBhajIyanIwN3A3eWY2YmUifQ.x0x411yjbETiJ-F8ebivHQ'
+const token = process.env.NEXT_PUBLIC_MAPBOX_TOKEN

Add to your environment files:

NEXT_PUBLIC_MAPBOX_TOKEN=your_token_here
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const token = 'pk.eyJ1IjoiZWxpYWxpZ2h0IiwiYSI6ImNtNzJsMnZnbjBhajIyanIwN3A3eWY2YmUifQ.x0x411yjbETiJ-F8ebivHQ'
const token = process.env.NEXT_PUBLIC_MAPBOX_TOKEN

Comment on lines 50 to 51
const response = await fetch('https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=NQ9PFPCJXNEREMUR');
const jsonData = await response.json();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling for API calls.

The API call lacks proper error handling and uses a hardcoded token. Consider implementing retry logic and error boundaries.

Implement better error handling:

-const response = await fetch('https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=NQ9PFPCJXNEREMUR');
-const jsonData = await response.json();
+try {
+  const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/devices/readings/map`, {
+    headers: {
+      'Authorization': `Bearer ${sessionToken}`
+    }
+  });
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  }
+  const jsonData = await response.json();
+} catch (error) {
+  console.error('Failed to fetch map data:', error);
+  // Handle error state
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await fetch('https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=NQ9PFPCJXNEREMUR');
const jsonData = await response.json();
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/devices/readings/map`, {
headers: {
'Authorization': `Bearer ${sessionToken}`
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const jsonData = await response.json();
} catch (error) {
console.error('Failed to fetch map data:', error);
// Handle error state
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
netmanager-app/components/NetManagerMap/page.tsx (5)

72-77: Simplify conditional logic.

The feature check can be simplified and the empty block removed.

-const feature = e.features ? e.features[0] : null;
-if (feature && feature.properties) {
-    
-}
-      
-if (!feature) return;
+const feature = e.features?.[0];
+if (!feature?.properties) return;
🧰 Tools
🪛 Biome (1.9.4)

[error] 73-73: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


160-164: Remove redundant block statement.

The block statement doesn't serve any purpose and can be safely removed.

-}{
-  <div>
-    Loading Data
-  </div>
-}
🧰 Tools
🪛 Biome (1.9.4)

[error] 160-165: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.

(lint/complexity/noUselessLoneBlockStatements)


191-196: Remove console.log statements and improve error handling.

Console logs should be removed in production code, and error handling can be improved.

-console.log("data :",data.suggestions)
 if (data.suggestions) {
   setSuggestions(data.suggestions);
 }
-console.log("Number of Suggesstions", suggestions.length)
+// Consider adding error state handling here

222-237: Add loading state for location selection.

The location selection could benefit from a loading state to improve user experience.

+const [isLocationLoading, setIsLocationLoading] = useState(false);
 const handleLocationClick = (locationid: any) => {
+  setIsLocationLoading(true);
   fetch(`https://api.mapbox.com/search/searchbox/v1/retrieve/${locationid}?&access_token=${token}&session_token=${sessionToken}`)
     .then(response => response.json())
     .then(data => {
-      console.log(data.features[0].geometry.coordinates)
       if (mapRef.current) {
         mapRef.current.flyTo({
           center: data.features[0].geometry.coordinates, 
           zoom: 12,
           essential: true 
         });
       }
+      setIsLocationLoading(false);
     })
-    .catch(error => console.error("Error fetching location:", error));
+    .catch(error => {
+      console.error("Error fetching location:", error);
+      setIsLocationLoading(false);
+    });
 }

324-328: Improve loading state UI.

The loading state UI could be enhanced with a proper loading component.

-{ mapContainerRef ?(
+{mapContainerRef ? (
   <div ref={mapContainerRef} className="rounded-lg map-container w-full md:h-full"/>
-):(
-  <div className='text-black'>Loading...</div>
+) : (
+  <div className="flex items-center justify-center h-full">
+    <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
+    <span className="ml-2 text-black">Loading map...</span>
+  </div>
 )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 178e929 and 1a657e7.

📒 Files selected for processing (1)
  • netmanager-app/components/NetManagerMap/page.tsx (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
netmanager-app/components/NetManagerMap/page.tsx

[error] 73-73: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 160-165: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.

(lint/complexity/noUselessLoneBlockStatements)

🔇 Additional comments (1)
netmanager-app/components/NetManagerMap/page.tsx (1)

13-13: Add TypeScript types for better type safety.

The code uses any type for suggestions array. This reduces type safety and IDE support.

Comment on lines 51 to 60
try {
const response = await fetch(`https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=${AirQoToken}`);
const jsonData = await response.json();
const geojsonData = ConvertToGeojson(jsonData);
console.log('GeoJson Data : ',geojsonData)

mapRef.current.addSource('data', {
type: 'geojson',
data: geojsonData
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling for API calls and add loading state.

The API call could benefit from better error handling and a loading state indicator.

 try {
+  const [isLoading, setIsLoading] = useState(false);
+  setIsLoading(true);
   const response = await fetch(`https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=${AirQoToken}`);
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  }
   const jsonData = await response.json();
   const geojsonData = ConvertToGeojson(jsonData);
-  console.log('GeoJson Data : ',geojsonData)
+  setIsLoading(false);
   mapRef.current.addSource('data', {
     type: 'geojson',
     data: geojsonData
   });

Committable suggestion skipped: line range outside the PR's diff.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🔭 Outside diff range comments (1)
netmanager-app/lib/utils.ts (1)

13-44: ⚠️ Potential issue

Fix the array manipulation in transformDataToGeoJson.

The function has a critical bug where forEach is incorrectly used instead of push to add items to the features array. This will result in no features being added to the collection.

Apply this fix:

-  data.map((feature) => {
+  data.forEach((feature) => {
     if (filter(feature)) {
-      features.forEach({
+      features.push({
         type: "Feature",
         properties: { ...rest, ...feature },
         geometry: {
           type: "Point",
           coordinates: (coordinateGetter && coordinateGetter(feature)) || [
             feature[longitude] || 0,
             feature[latitude] || 0,
           ],
         },
       });
     }
   });
🧰 Tools
🪛 Biome (1.9.4)

[error] 31-31: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

♻️ Duplicate comments (1)
netmanager-app/components/NetManagerMap/page.tsx (1)

52-61: 🛠️ Refactor suggestion

Enhance data fetching with proper error handling and loading states.

The current implementation could benefit from better error handling and user feedback during data loading.

Apply these improvements:

+const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'https://staging-analytics.airqo.net/api/v2';
+const [isDataLoading, setIsDataLoading] = useState(false);
+const [error, setError] = useState<string | null>(null);

 try {
+  setIsDataLoading(true);
+  setError(null);
-  const response = await fetch(`https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=${AirQoToken}`);
+  const response = await fetch(`${API_URL}/devices/readings/map?token=${AirQoToken}`);
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  }
   const jsonData = await response.json();
   const geojsonData = ConvertToGeojson(jsonData);
-  console.log('GeoJson Data : ',geojsonData)
   mapRef.current.addSource('data', {
     type: 'geojson',
     data: geojsonData
   });
+} catch (error) {
+  setError(error instanceof Error ? error.message : 'Failed to fetch data');
+  console.error('Error fetching GeoJSON:', error);
+} finally {
+  setIsDataLoading(false);
 }
🧹 Nitpick comments (2)
netmanager-app/components/NetManagerMap/page.tsx (2)

39-47: Improve map initialization with configuration options and loading state.

Consider extracting map configuration and adding loading state management.

Apply these improvements:

+const DEFAULT_MAP_CONFIG = {
+  style: 'mapbox://styles/mapbox/navigation-day-v1',
+  center: [18.5, 3],
+  zoom: 3,
+};

+const [isLoading, setIsLoading] = useState(true);

 useEffect(() => {
   mapboxgl.accessToken = token;
   if (mapContainerRef.current) {
     mapRef.current = new mapboxgl.Map({
       container: mapContainerRef.current,
-      style:'mapbox://styles/mapbox/navigation-day-v1',
-      center: [18.5, 3],
-      zoom: 3
+      ...DEFAULT_MAP_CONFIG
     });
+    setIsLoading(false);
   }

237-299: Enhance UI rendering with better accessibility and loading states.

The UI could benefit from:

  • ARIA labels for accessibility
  • Better loading states
  • Simplified conditional rendering

Apply these improvements:

 return (
-  <div className="flex flex-col-reverse md:flex md:flex-row min-h-screen md:h-screen -ml-5 ">
+  <div className="flex flex-col-reverse md:flex md:flex-row min-h-screen md:h-screen -ml-5" role="main">
     <div className='flex flex-grow md:flex-grow-0 border rounded-lg md:w-[24%]'>
       <div className="flex flex-col border gap-2 p-1 rounded-lg w-full">
         <div className="flex flex-col gap-3">
-          <h1 className="font-bold">Net Manager Map</h1>
+          <h1 className="font-bold" id="map-title">Net Manager Map</h1>
           <div className="relative w-full">
             <Input
               placeholder="Search all locations"
               type="text"
               value={query}
-              onChange={SearchSuggestions}
+              onChange={handleSearchInput}
+              aria-label="Search locations"
               name="Search"
               className="w-full pr-10"
             />
           </div>
         </div>
+        {isLoading && <div className="flex justify-center p-4">Loading map...</div>}
+        {error && <div className="text-red-500 p-4">{error}</div>}
         <div className="flex w-full">
-          {suggestions ? (
+          {isDataLoading ? (
+            <div className="bg-white w-full border border-gray-300 mt-1 rounded-md shadow-md p-2">
+              Loading suggestions...
+            </div>
+          ) : suggestions.length > 0 ? (
             <div
               id="search-suggestions"
               className="w-full"
+              role="listbox"
+              aria-label="Location suggestions"
             >
               {suggestions.map((item, index) => (
                 <div
                   key={index}
                   className="bg-white w-full border border-gray-300 mt-1 rounded-md shadow-md max-h-40 overflow-y-auto p-2 hover:bg-gray-200 cursor-pointer"
                   onClick={() => handleLocationClick(item.mapbox_id)}
+                  role="option"
+                  aria-selected="false"
                 >
                   <h1 className="text">{item.name}</h1>
                   <h1 className="text-gray-300">{item.place_formatted}</h1>
                 </div>
               ))}
             </div>
           ) : (
-            <div className="bg-white w-full border border-gray-300 mt-1 rounded-md shadow-md max-h-40 overflow-y-auto p-2 hover:bg-gray-200 cursor-pointer">
-              Loading Suggestions...
+            <div className="bg-white w-full border border-gray-300 mt-1 rounded-md shadow-md p-2">
+              No suggestions found
             </div>
           )}
         </div>
       </div>
     </div>
   </div>
 );
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a657e7 and 041591c.

⛔ Files ignored due to path filters (2)
  • platform/package-lock.json is excluded by !**/package-lock.json
  • platform/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (2)
  • netmanager-app/components/NetManagerMap/page.tsx (1 hunks)
  • netmanager-app/lib/utils.ts (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
netmanager-app/components/NetManagerMap/page.tsx

[error] 74-74: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

Comment on lines 46 to 76
export const ConvertToGeojson=(data: any)=>{
return {
type: "FeatureCollection" as const,
features: data.measurements.map((item: any) => ({
type: "Feature",
geometry: {
type: "Point",
coordinates: [
item.siteDetails.approximate_longitude,
item.siteDetails.approximate_latitude
]
},
properties: {
id: item._id,
site_id:item.site_id,
time: new Date(item.time).toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric"
}),
location_name:item.siteDetails.name,
aqi_category: item.aqi_category,
aqi_color:item.aqi_color.startsWith("#") ? item.aqi_color : `#${item.aqi_color}`,
value:item.pm2_5 ?.value??0,
}
}))
}


}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance type safety and error handling in ConvertToGeojson.

The function could benefit from the following improvements:

  • Type safety using proper interfaces
  • Error handling for undefined properties
  • Consistent formatting

Apply these improvements:

+interface SiteDetails {
+  approximate_longitude: number;
+  approximate_latitude: number;
+  name: string;
+}

+interface Measurement {
+  _id: string;
+  site_id: string;
+  time: string;
+  siteDetails: SiteDetails;
+  aqi_category: string;
+  aqi_color: string;
+  pm2_5?: { value: number };
+}

+interface GeoJsonData {
+  measurements: Measurement[];
+}

-export const ConvertToGeojson=(data: any)=>{
+export const ConvertToGeojson = (data: GeoJsonData) => {
+  if (!data?.measurements?.length) {
+    throw new Error('Invalid data format: measurements array is required');
+  }

   return {
     type: "FeatureCollection" as const,
     features: data.measurements.map((item) => ({
       type: "Feature",
       geometry: {
         type: "Point",
         coordinates: [
-          item.siteDetails.approximate_longitude,
-          item.siteDetails.approximate_latitude
+          item.siteDetails?.approximate_longitude ?? 0,
+          item.siteDetails?.approximate_latitude ?? 0
         ]
       },
       properties: {
         id: item._id,
         site_id: item.site_id,
         time: new Date(item.time).toLocaleDateString("en-US", {
           weekday: "long",
           year: "numeric",
           month: "long",
           day: "numeric"
         }),
-        location_name: item.siteDetails.name,
+        location_name: item.siteDetails?.name ?? 'Unknown Location',
         aqi_category: item.aqi_category,
-        aqi_color: item.aqi_color.startsWith("#") ? item.aqi_color : `#${item.aqi_color}`,
+        aqi_color: item.aqi_color?.startsWith("#") ? item.aqi_color : `#${item.aqi_color ?? '000000'}`,
         value: item.pm2_5?.value ?? 0,
       }
     }))
   };
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const ConvertToGeojson=(data: any)=>{
return {
type: "FeatureCollection" as const,
features: data.measurements.map((item: any) => ({
type: "Feature",
geometry: {
type: "Point",
coordinates: [
item.siteDetails.approximate_longitude,
item.siteDetails.approximate_latitude
]
},
properties: {
id: item._id,
site_id:item.site_id,
time: new Date(item.time).toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric"
}),
location_name:item.siteDetails.name,
aqi_category: item.aqi_category,
aqi_color:item.aqi_color.startsWith("#") ? item.aqi_color : `#${item.aqi_color}`,
value:item.pm2_5 ?.value??0,
}
}))
}
}
interface SiteDetails {
approximate_longitude: number;
approximate_latitude: number;
name: string;
}
interface Measurement {
_id: string;
site_id: string;
time: string;
siteDetails: SiteDetails;
aqi_category: string;
aqi_color: string;
pm2_5?: { value: number };
}
interface GeoJsonData {
measurements: Measurement[];
}
export const ConvertToGeojson = (data: GeoJsonData) => {
if (!data?.measurements?.length) {
throw new Error('Invalid data format: measurements array is required');
}
return {
type: "FeatureCollection" as const,
features: data.measurements.map((item) => ({
type: "Feature",
geometry: {
type: "Point",
coordinates: [
item.siteDetails?.approximate_longitude ?? 0,
item.siteDetails?.approximate_latitude ?? 0
]
},
properties: {
id: item._id,
site_id: item.site_id,
time: new Date(item.time).toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric"
}),
location_name: item.siteDetails?.name ?? 'Unknown Location',
aqi_category: item.aqi_category,
aqi_color: item.aqi_color?.startsWith("#") ? item.aqi_color : `#${item.aqi_color ?? '000000'}`,
value: item.pm2_5?.value ?? 0,
}
}))
};
};

Comment on lines 164 to 217
const SearchSuggestions=(e: React.ChangeEvent<HTMLInputElement>)=>{
const value = e.target.value;
setQuery(value);

if (value.trim() === "") {
setSuggestions([]);
if (mapRef.current) {
mapRef.current.flyTo({
center: [18.5,5],
zoom: 3.0,
essential: true
});
}
return;
}
const GetSuggestions=(latitude?: number, longitude?: number)=>{


if (latitude !== undefined && longitude !== undefined) {
const proximityParam = `${longitude},${latitude}`;
fetch(`https://api.mapbox.com/search/searchbox/v1/suggest?q=${value.toLowerCase()}&access_token=${token}&proximity=${proximityParam}&session_token=${sessionToken}`)
.then(response => response.json())

.then(data => {
console.log("data :",data.suggestions)
if (data.suggestions) {
setSuggestions(data.suggestions);
}
console.log("Number of Suggesstions", suggestions.length)
})
.catch(error => console.error("Error fetching suggestions:", error));
}
}
const fetchUserLocation = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
console.log("User Location:", latitude, longitude);
GetSuggestions(latitude, longitude);
},
(error) => {
console.error("Error getting user location:", error);
GetSuggestions();
}
);
} else {
console.error("Geolocation is not supported by this browser.");
GetSuggestions();
}
};
fetchUserLocation();

}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize search functionality with debouncing and type safety.

The search implementation could benefit from:

  • Debouncing to prevent excessive API calls
  • Type safety for suggestions
  • Removal of console.log statements

Apply these improvements:

+interface MapboxSuggestion {
+  mapbox_id: string;
+  name: string;
+  place_formatted: string;
+}

+const useDebounce = <T>(value: T, delay: number): T => {
+  const [debouncedValue, setDebouncedValue] = useState<T>(value);
+
+  useEffect(() => {
+    const handler = setTimeout(() => {
+      setDebouncedValue(value);
+    }, delay);
+
+    return () => {
+      clearTimeout(handler);
+    };
+  }, [value, delay]);
+
+  return debouncedValue;
+};

-const [suggestions, setSuggestions] = useState<any[]>([]);
+const [suggestions, setSuggestions] = useState<MapboxSuggestion[]>([]);
+const debouncedQuery = useDebounce(query, 300);

-const SearchSuggestions=(e: React.ChangeEvent<HTMLInputElement>)=>{
+const handleSearchInput = (e: React.ChangeEvent<HTMLInputElement>) => {
   const value = e.target.value;
   setQuery(value);
 };

+useEffect(() => {
   if (value.trim() === "") {
     setSuggestions([]);
     if (mapRef.current) {
       mapRef.current.flyTo({
         center: [18.5,5],
         zoom: 3.0,
         essential: true
       });
     }
     return;
   }
   fetchUserLocation();
-}, [debouncedQuery]);

Committable suggestion skipped: line range outside the PR's diff.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🔭 Outside diff range comments (2)
netmanager-app/public/icons/map/gpsIcon.js (1)

1-24: 🛠️ Refactor suggestion

Create a shared base component for SVG icons.

There's significant code duplication between icon components. Consider creating a base SVG icon component.

// components/icons/base/BaseSvgIcon.tsx
interface BaseSvgIconProps {
  width?: string;
  height?: string;
  fill?: string;
  path: string;
  viewBox?: string;
}

export const BaseSvgIcon: React.FC<BaseSvgIconProps> = ({
  width = '24',
  height = '24',
  fill = '#536A87',
  path,
  viewBox = '0 0 24 24'
}) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width={width}
    height={height}
    viewBox={viewBox}
    fill="none"
  >
    <path
      d={path}
      stroke={fill}
      strokeWidth="1.5"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
  </svg>
);

// public/icons/map/GpsIcon.tsx
import { BaseSvgIcon } from '@/components/icons/base/BaseSvgIcon';

const GPS_ICON_PATH = "M20 12C20 16.4183 16.4183 20 12 20M20 12C20 7.58172 16.4183 4 12 4M20 12H22M12 20C7.58172 20 4 16.4183 4 12M12 20V22M4 12C4 7.58172 7.58172 4 12 4M4 12H2M12 4V2M15 12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12Z";

export const GpsIcon: React.FC<Omit<BaseSvgIconProps, 'path'>> = (props) => (
  <BaseSvgIcon {...props} path={GPS_ICON_PATH} />
);
netmanager-app/public/icons/map/refreshIcon.js (1)

1-24: 🛠️ Refactor suggestion

Apply shared icon component pattern.

Use the same BaseSvgIcon pattern suggested for the GPS icon.

// public/icons/map/RefreshIcon.tsx
import { BaseSvgIcon } from '@/components/icons/base/BaseSvgIcon';

const REFRESH_ICON_PATH = "M20.452 12.8927C20.1742 15.5026 18.6954 17.9483 16.2484 19.3611C12.1829 21.7083 6.98442 20.3153 4.63721 16.2499L4.38721 15.8168M3.54515 11.1066C3.82295 8.49674 5.30174 6.05102 7.74873 4.63825C11.8142 2.29104 17.0127 3.68398 19.3599 7.74947L19.6099 8.18248M3.49219 18.0657L4.22424 15.3336L6.95629 16.0657M17.0414 7.93364L19.7735 8.66569L20.5055 5.93364";

export const RefreshIcon: React.FC<Omit<BaseSvgIconProps, 'path'>> = (props) => (
  <BaseSvgIcon {...props} path={REFRESH_ICON_PATH} />
);
♻️ Duplicate comments (1)
netmanager-app/components/NetManagerMap/page.tsx (1)

181-234: 🛠️ Refactor suggestion

Optimize search functionality with debouncing and type safety.

The search implementation could benefit from debouncing to prevent excessive API calls and proper TypeScript types for better type safety.

See previous review comment about implementing debouncing and adding proper TypeScript types for the search functionality.

🧹 Nitpick comments (9)
netmanager-app/public/icons/map/upArrow.js (1)

1-24: Consider creating a reusable icon component.

Given the similar structure across icon components, consider creating a reusable base component to reduce duplication and maintain consistency.

  1. Create a base IconComponent:
// components/common/Icon.tsx
interface IconProps {
  name: 'up-arrow' | 'down-arrow' | 'plus' | 'minus';
  width?: string;
  height?: string;
  fill?: string;
}

const PATHS = {
  'up-arrow': "M18 15L12 9L6 15",
  'down-arrow': "M6 9L12 15L18 9",
  'plus': "M12 5V19M5 12H19",
  'minus': "M5 12H19"
};

export const Icon = ({ 
  name, 
  width = '24', 
  height = '24', 
  fill = '#536A87' 
}: IconProps) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width={width}
    height={height}
    viewBox="0 0 24 24"
    fill="none"
    role="img"
    aria-label={name}
  >
    <path
      d={PATHS[name]}
      stroke={fill}
      strokeWidth="1.5"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
  </svg>
);
  1. Then use it like this:
<Icon name="up-arrow" />

For the immediate fix:

-const upArrow = ({ width, height, fill }) => {
+const UpArrow = ({ width = '24', height = '24', fill = '#536A87' }) => {
   return (
     <svg
       xmlns="http://www.w3.org/2000/svg"
       width={width || '24'}
       height={height || '24'}
       viewBox="0 0 24 24"
       fill="none"
+      role="img"
+      aria-label="up arrow"
     >
       <path
         d="M18 15L12 9L6 15"
-        stroke={fill || '#536A87'}
+        stroke={fill}
         strokeWidth="1.5"
         strokeLinecap="round"
         strokeLinejoin="round"
       />
     </svg>
   );
 };
netmanager-app/public/icons/map/downArrow.js (1)

1-24: Add tests for icon components.

Consider adding tests to ensure icons render correctly with different props and maintain accessibility standards.

Here's a suggested test structure:

// __tests__/components/common/Icon.test.tsx
import { render, screen } from '@testing-library/react';
import { Icon } from '../components/common/Icon';

describe('Icon', () => {
  it('renders with default props', () => {
    render(<Icon name="down-arrow" />);
    const svg = screen.getByRole('img', { name: 'down arrow' });
    expect(svg).toBeInTheDocument();
    expect(svg).toHaveAttribute('width', '24');
    expect(svg).toHaveAttribute('height', '24');
  });

  it('renders with custom props', () => {
    render(<Icon name="down-arrow" width="32" height="32" fill="#000" />);
    const svg = screen.getByRole('img');
    expect(svg).toHaveAttribute('width', '32');
    expect(svg).toHaveAttribute('height', '32');
    const path = svg.querySelector('path');
    expect(path).toHaveAttribute('stroke', '#000');
  });
});

Would you like me to help set up the test infrastructure or create an issue to track this task?

netmanager-app/components/NetManagerMap/components/IconButton.tsx (2)

1-1: Use more specific type imports.

Import specific types instead of the entire React namespace.

-import type * as React from "react"
+import type { FC, MouseEvent, ReactNode } from "react"

3-7: Update interface to use more specific types.

 interface IconButtonProps {
-  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
+  onClick: (event: MouseEvent<HTMLButtonElement>) => void
   title: string
-  icon: React.ReactNode
+  icon: ReactNode
 }
netmanager-app/public/icons/map/shareIcon.js (1)

3-21: Convert to TypeScript and follow React naming conventions.

The component should be converted to TypeScript and renamed to follow React naming conventions.

-const shareIcon = ({ width, height, fill }) => {
+interface ShareIconProps {
+  width?: string;
+  height?: string;
+  fill?: string;
+}
+
+const ShareIcon: React.FC<ShareIconProps> = ({ width, height, fill }) => {
netmanager-app/components/NetManagerMap/components/zoom.tsx (2)

9-12: Add TypeScript type definitions for class properties.

The class properties should be properly typed for better type safety.

+interface MapRef extends mapboxgl.Map {}

 export class CustomZoomControl {
+  private map: MapRef | null;
+  private container: HTMLDivElement;
+  private zoomInButton: HTMLButtonElement;
+  private zoomOutButton: HTMLButtonElement;

   constructor() {

75-86: Enhance URL state management with error handling.

The URL state management could be more robust with proper error handling and validation.

 updateUrlWithMapState = () => {
   if (!this.map) return;
   const center = this.map.getCenter();
   const zoom = this.map.getZoom();
-  if (!center || isNaN(zoom)) return;
+  try {
+    if (!center || isNaN(zoom)) {
+      throw new Error('Invalid map state');
+    }
 
-  const url = new URL(window.location);
-  url.searchParams.set('lat', center.lat.toFixed(4));
-  url.searchParams.set('lng', center.lng.toFixed(4));
-  url.searchParams.set('zm', zoom.toFixed(2));
-  window.history.pushState({}, '', url);
+    const url = new URL(window.location.href);
+    url.searchParams.set('lat', center.lat.toFixed(4));
+    url.searchParams.set('lng', center.lng.toFixed(4));
+    url.searchParams.set('zm', zoom.toFixed(2));
+    window.history.pushState({ lat: center.lat, lng: center.lng, zoom }, '', url);
+  } catch (error) {
+    console.error('Failed to update URL with map state:', error);
+  }
 };
netmanager-app/lib/utils.ts (1)

81-115: Improve error handling and type safety in useRefreshMap.

The function should have proper TypeScript types and better error handling.

+interface ToastMessage {
+  message: string;
+  type: 'success' | 'error';
+  bgColor: string;
+}
+
 export const useRefreshMap = (
-  setToastMessage,
-  mapRef,
-  dispatch,
-  selectedNode,
+  setToastMessage: (message: ToastMessage) => void,
+  mapRef: React.RefObject<mapboxgl.Map>,
+  dispatch: Function,
+  selectedNode: any
 ) =>
   useCallback(() => {
     const map = mapRef.current;
 
     if (map) {
       try {
+        if (!map.getStyle()?.sprite) {
+          throw new Error('Invalid map style');
+        }
         const originalStyle =
           map.getStyle().sprite.split('/').slice(0, -1).join('/') +
           '/style.json';
netmanager-app/components/NetManagerMap/page.tsx (1)

73-73: Remove console.log statements.

There are several console.log statements throughout the code that should be removed or replaced with proper logging for production.

Consider using a proper logging library or removing these debug statements:

  • Line 73: console.log('GeoJson Data : ',geojsonData)
  • Line 205: console.log("data :",data.suggestions)
  • Line 209: console.log("Number of Suggesstions", suggestions.length)
  • Line 219: console.log("User Location:", latitude, longitude)
  • Line 223: console.error("Error getting user location:", error)
  • Line 228: console.error("Geolocation is not supported by this browser.")
  • Line 240: console.log(data.features[0].geometry.coordinates)

Also applies to: 205-205, 209-209, 219-219, 223-223, 228-228, 240-240

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 041591c and 96a4f9e.

⛔ Files ignored due to path filters (1)
  • netmanager-app/public/images/map/Invalid.png is excluded by !**/*.png
📒 Files selected for processing (17)
  • netmanager-app/components.json (1 hunks)
  • netmanager-app/components/NetManagerMap/components/IconButton.tsx (1 hunks)
  • netmanager-app/components/NetManagerMap/components/LayerModal.js (1 hunks)
  • netmanager-app/components/NetManagerMap/components/zoom.tsx (1 hunks)
  • netmanager-app/components/NetManagerMap/data/constants.js (1 hunks)
  • netmanager-app/components/NetManagerMap/page.tsx (1 hunks)
  • netmanager-app/lib/utils.ts (1 hunks)
  • netmanager-app/public/icons/map/downArrow.js (1 hunks)
  • netmanager-app/public/icons/map/gpsIcon.js (1 hunks)
  • netmanager-app/public/icons/map/homeIcon.js (1 hunks)
  • netmanager-app/public/icons/map/layerIcon.js (1 hunks)
  • netmanager-app/public/icons/map/menuIcon.js (1 hunks)
  • netmanager-app/public/icons/map/minusIcon.js (1 hunks)
  • netmanager-app/public/icons/map/plusIcon.js (1 hunks)
  • netmanager-app/public/icons/map/refreshIcon.js (1 hunks)
  • netmanager-app/public/icons/map/shareIcon.js (1 hunks)
  • netmanager-app/public/icons/map/upArrow.js (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • netmanager-app/public/icons/map/layerIcon.js
  • netmanager-app/components/NetManagerMap/data/constants.js
🧰 Additional context used
🪛 Biome (1.9.4)
netmanager-app/components/NetManagerMap/components/LayerModal.js

[error] 158-158: This property value named onMapDetailsSelect is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named onMapDetailsSelect

(lint/suspicious/noDuplicateObjectKeys)


[error] 159-159: This property value named onStyleSelect is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named onStyleSelect

(lint/suspicious/noDuplicateObjectKeys)

netmanager-app/components/NetManagerMap/page.tsx

[error] 91-91: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (4)
netmanager-app/public/icons/map/homeIcon.js (1)

3-21: Convert to TypeScript and follow React naming conventions.

The component should be converted to TypeScript and renamed to follow React naming conventions.

netmanager-app/lib/utils.ts (1)

46-74: Enhance type safety and error handling in ConvertToGeojson.

The function needs proper type definitions and error handling.

netmanager-app/components/NetManagerMap/page.tsx (1)

23-24: LGTM! Good use of environment variables for tokens.

The Mapbox and AirQo tokens are correctly stored in environment variables, which is a secure practice.

netmanager-app/components.json (1)

18-20: LGTM! Well-structured alias additions.

The new aliases for icons and images follow the existing pattern and improve code organization.

Comment on lines 3 to 21
const minusIcon = ({ width, height, fill }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width || '24'}
height={height || '24'}
viewBox="0 0 24 24"
fill="none"
>
<path
d="M5 12H19"
stroke={fill || '#536A87'}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Follow React component naming convention and enhance accessibility.

  1. Component names should start with uppercase (e.g., MinusIcon)
  2. Consider adding TypeScript or PropTypes for type safety
  3. Add accessibility attributes for screen readers
-const minusIcon = ({ width, height, fill }) => {
+const MinusIcon = ({ width = '24', height = '24', fill = '#536A87' }) => {
   return (
     <svg
       xmlns="http://www.w3.org/2000/svg"
       width={width || '24'}
       height={height || '24'}
       viewBox="0 0 24 24"
       fill="none"
+      role="img"
+      aria-label="minus"
     >
       <path
         d="M5 12H19"
-        stroke={fill || '#536A87'}
+        stroke={fill}
         strokeWidth="1.5"
         strokeLinecap="round"
         strokeLinejoin="round"
       />
     </svg>
   );
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const minusIcon = ({ width, height, fill }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width || '24'}
height={height || '24'}
viewBox="0 0 24 24"
fill="none"
>
<path
d="M5 12H19"
stroke={fill || '#536A87'}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
const MinusIcon = ({ width = '24', height = '24', fill = '#536A87' }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width || '24'}
height={height || '24'}
viewBox="0 0 24 24"
fill="none"
role="img"
aria-label="minus"
>
<path
d="M5 12H19"
stroke={fill}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};

Comment on lines 3 to 21
const plusIcon = ({ width, height, fill }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width || '24'}
height={height || '24'}
viewBox="0 0 24 24"
fill="none"
>
<path
d="M12 5V19M5 12H19"
stroke={fill || '#536A87'}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Maintain consistency with other icons and enhance accessibility.

  1. Component name should start with uppercase (e.g., PlusIcon)
  2. Stroke width (2) differs from MinusIcon (1.5) - consider maintaining consistency
  3. Add accessibility attributes for screen readers
-const plusIcon = ({ width, height, fill }) => {
+const PlusIcon = ({ width = '24', height = '24', fill = '#536A87' }) => {
   return (
     <svg
       xmlns="http://www.w3.org/2000/svg"
       width={width || '24'}
       height={height || '24'}
       viewBox="0 0 24 24"
       fill="none"
+      role="img"
+      aria-label="plus"
     >
       <path
         d="M12 5V19M5 12H19"
-        stroke={fill || '#536A87'}
+        stroke={fill}
-        strokeWidth="2"
+        strokeWidth="1.5"
         strokeLinecap="round"
         strokeLinejoin="round"
       />
     </svg>
   );
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const plusIcon = ({ width, height, fill }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width || '24'}
height={height || '24'}
viewBox="0 0 24 24"
fill="none"
>
<path
d="M12 5V19M5 12H19"
stroke={fill || '#536A87'}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
const PlusIcon = ({ width = '24', height = '24', fill = '#536A87' }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width || '24'}
height={height || '24'}
viewBox="0 0 24 24"
fill="none"
role="img"
aria-label="plus"
>
<path
d="M12 5V19M5 12H19"
stroke={fill}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};

);
};

export default menuIcon;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Update export to match new component name.

-export default menuIcon;
+export default MenuIcon;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default menuIcon;
export default MenuIcon;

Comment on lines 3 to 21
const menuIcon = ({ width, height, fill }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width || '24'}
height={height || '24'}
viewBox="0 0 24 24"
fill="none"
>
<path
d="M3 12H21M3 6H21M3 18H15"
stroke={fill || '#536A87'}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Convert to TypeScript and follow React naming conventions.

The component should follow React naming conventions (PascalCase) and include TypeScript types for better type safety.

-const menuIcon = ({ width, height, fill }) => {
+interface MenuIconProps {
+  width?: string;
+  height?: string;
+  fill?: string;
+}
+
+const MenuIcon: React.FC<MenuIconProps> = ({ 
+  width = '24',
+  height = '24',
+  fill = '#536A87'
+}) => {
   return (
     <svg
       xmlns="http://www.w3.org/2000/svg"
-      width={width || '24'}
-      height={height || '24'}
+      width={width}
+      height={height}
       viewBox="0 0 24 24"
       fill="none"
     >
       <path
         d="M3 12H21M3 6H21M3 18H15"
-        stroke={fill || '#536A87'}
+        stroke={fill}
         strokeWidth="1.5"
         strokeLinecap="round"
         strokeLinejoin="round"
       />
     </svg>
   );
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const menuIcon = ({ width, height, fill }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width || '24'}
height={height || '24'}
viewBox="0 0 24 24"
fill="none"
>
<path
d="M3 12H21M3 6H21M3 18H15"
stroke={fill || '#536A87'}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
interface MenuIconProps {
width?: string;
height?: string;
fill?: string;
}
const MenuIcon: React.FC<MenuIconProps> = ({
width = '24',
height = '24',
fill = '#536A87'
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
viewBox="0 0 24 24"
fill="none"
>
<path
d="M3 12H21M3 6H21M3 18H15"
stroke={fill}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};

Comment on lines 11 to 17
<button
onClick={onClick}
title={title}
className="inline-flex items-center justify-center p-2 md:p-3 mr-2 text-gray-600 rounded-full bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 shadow-md"
>
{icon}
</button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance button accessibility and form handling.

Add explicit button type and aria-label for better accessibility.

     <button
+      type="button"
       onClick={onClick}
       title={title}
+      aria-label={title}
       className="inline-flex items-center justify-center p-2 md:p-3 mr-2 text-gray-600 rounded-full bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 shadow-md"
     >
       {icon}
     </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={onClick}
title={title}
className="inline-flex items-center justify-center p-2 md:p-3 mr-2 text-gray-600 rounded-full bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 shadow-md"
>
{icon}
</button>
<button
type="button"
onClick={onClick}
title={title}
aria-label={title}
className="inline-flex items-center justify-center p-2 md:p-3 mr-2 text-gray-600 rounded-full bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 shadow-md"
>
{icon}
</button>

image: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, // Allow both string and object
})
).isRequired,
mapDetails: PropTypes.array,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance type safety for mapDetails prop.

The mapDetails prop is defined as a generic array without shape validation, which could lead to runtime errors.

Apply this diff to add proper shape validation:

-   mapDetails: PropTypes.array,
+   mapDetails: PropTypes.arrayOf(
+     PropTypes.shape({
+       name: PropTypes.string.isRequired,
+       image: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
+     })
+   ).isRequired,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mapDetails: PropTypes.array,
mapDetails: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
image: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
})
).isRequired,

Comment on lines 155 to 170
LayerModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onMapDetailsSelect: PropTypes.func.isRequired,
onStyleSelect: PropTypes.func.isRequired,
mapStyles: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
image: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, // Allow both string and object
})
).isRequired,
mapDetails: PropTypes.array,
disabled: PropTypes.string,
onMapDetailsSelect: PropTypes.func.isRequired,
onStyleSelect: PropTypes.func.isRequired,
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix duplicate PropTypes definitions.

The PropTypes for onMapDetailsSelect and onStyleSelect are defined twice. This can lead to confusion and potential issues.

Apply this diff to remove the duplicates:

  LayerModal.propTypes = {
    isOpen: PropTypes.bool.isRequired,
    onClose: PropTypes.func.isRequired,
    onMapDetailsSelect: PropTypes.func.isRequired,
    onStyleSelect: PropTypes.func.isRequired,
    mapStyles: PropTypes.arrayOf(
      PropTypes.shape({
        name: PropTypes.string.isRequired,
        image: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
      })
    ).isRequired,
    mapDetails: PropTypes.array,
    disabled: PropTypes.string,
-   onMapDetailsSelect: PropTypes.func.isRequired,
-   onStyleSelect: PropTypes.func.isRequired,
};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LayerModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onMapDetailsSelect: PropTypes.func.isRequired,
onStyleSelect: PropTypes.func.isRequired,
mapStyles: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
image: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, // Allow both string and object
})
).isRequired,
mapDetails: PropTypes.array,
disabled: PropTypes.string,
onMapDetailsSelect: PropTypes.func.isRequired,
onStyleSelect: PropTypes.func.isRequired,
};
LayerModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onMapDetailsSelect: PropTypes.func.isRequired,
onStyleSelect: PropTypes.func.isRequired,
mapStyles: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
image: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, // Allow both string and object
})
).isRequired,
mapDetails: PropTypes.array,
disabled: PropTypes.string,
};
🧰 Tools
🪛 Biome (1.9.4)

[error] 158-158: This property value named onMapDetailsSelect is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named onMapDetailsSelect

(lint/suspicious/noDuplicateObjectKeys)


[error] 159-159: This property value named onStyleSelect is later overwritten by an object member with the same name.

Overwritten with this value.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named onStyleSelect

(lint/suspicious/noDuplicateObjectKeys)

Comment on lines 61 to 68
useEffect(() => {
if (mapStyles[0]) {
setSelectedStyle(mapStyles[0]);
}
if (mapDetails[0]) {
setSelectedMapDetail(mapDetails[0]);
}
}, [mapStyles, mapDetails]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for empty arrays in useEffect.

The current implementation assumes that mapStyles and mapDetails arrays have at least one element, which could lead to runtime errors.

Apply this diff to add proper validation:

  useEffect(() => {
-   if (mapStyles[0]) {
+   if (mapStyles.length > 0) {
      setSelectedStyle(mapStyles[0]);
    }
-   if (mapDetails[0]) {
+   if (mapDetails.length > 0) {
      setSelectedMapDetail(mapDetails[0]);
    }
  }, [mapStyles, mapDetails]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (mapStyles[0]) {
setSelectedStyle(mapStyles[0]);
}
if (mapDetails[0]) {
setSelectedMapDetail(mapDetails[0]);
}
}, [mapStyles, mapDetails]);
useEffect(() => {
if (mapStyles.length > 0) {
setSelectedStyle(mapStyles[0]);
}
if (mapDetails.length > 0) {
setSelectedMapDetail(mapDetails[0]);
}
}, [mapStyles, mapDetails]);

Comment on lines 69 to 175
type: 'geojson',
data: geojsonData
});
const popup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false,

});
// Listen for when the mouse enters a feature in your 'data-layer'
mapRef.current.on('mouseenter', 'data-layer', (e) => {
if (mapRef.current) {
mapRef.current.getCanvas().style.cursor = 'pointer';
}
// Get the feature under the mouse pointer (the first feature).
const feature = e.features ? e.features[0] : null;
if (feature && feature.properties) {

}

if (!feature) return;

const htmlContent = `
<div style="font-size: 14px; border-radius: 25px; background-color: white; padding: 10px; rgba(0, 0, 0, 0.2); max-width: full;">
<div>${feature.properties?.time ?? 'Unknown time'}</div><br>
<div style="display: flex; justify-content: space-between;gap: 20px;">
<strong style="display: flex; ">${feature.properties?.location_name ?? 'Unknown Location'}</strong>
<div style="display: flex;font-weight: bold; ">${feature.properties?.value.toFixed(2) ?? 'N/A'}µg/m³</div>
</div>

<div style=" display: flex;gap: 10px; ">
<h1 style="font-weight: bold; color: ${feature.properties?.aqi_color ?? 'black'};">Air Quality is ${feature.properties?.aqi_category ?? 'N/A'}</h1>
<img src="${AirQuality.goodair}" style="background-color: ${feature.properties?.aqi_color ?? 'green'};width: 30px; height: 30px;border-radius: 50%;font-size: 18px;"></img>
</div>
</div>
`;

// Set the popup at the feature's coordinates and add the HTML content.
popup
.setLngLat((feature.geometry as GeoJSON.Point).coordinates as [number, number])
.setHTML(htmlContent)
if (mapRef.current) {
popup.addTo(mapRef.current);
}
});

// When the mouse leaves the feature, remove the popup.
mapRef.current.on('mouseleave', 'data-layer', () => {
if (mapRef.current) {
mapRef.current.getCanvas().style.cursor = '';
}
popup.remove();
});
Object.entries(AirQuality).forEach(([key, url]) => {
if (mapRef.current) {
mapRef.current.loadImage(url, (error, image) => {
if (error) throw error;

if (mapRef.current && image && !mapRef.current.hasImage(key)) {
mapRef.current.addImage(key, image);
}
});
}
});
mapRef.current.addLayer({
'id': 'circle-layer',
'type': 'circle',
'source': 'data',
'paint': {
'circle-color': 'white',
'circle-radius': 30,
'circle-opacity': 1
}
});
mapRef.current.addLayer({
'id': 'data-layer',
'type': 'symbol',
'source': 'data',
'layout': {
'icon-image': [
'match',
['get', 'aqi_category'],
'Good', 'goodair',
'Moderate', 'moderate',
'Unhealthy', 'unhealthy',
'Unhealthy for Sensitive Groups', 'unhealthySG',
'Hazardous','Hazardous',
'Very Unhealthy', 'veryunhealthy',
'unknownAQ'
],
'icon-size': 0.05,
'icon-allow-overlap': true
},

'filter': ['has', 'aqi_category']
});


} catch (error) {
console.error('Error fetching GeoJSON:', error);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling for API calls.

The current error handling could be improved to handle non-200 responses and network errors more gracefully.

Apply this diff to enhance error handling:

  try {
+   if (!AirQoToken) {
+     throw new Error('AirQo token is not configured');
+   }
    const response = await fetch(`https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=${AirQoToken}`);
+   if (!response.ok) {
+     throw new Error(`HTTP error! status: ${response.status}`);
+   }
    const jsonData = await response.json();
+   if (!jsonData || !Array.isArray(jsonData)) {
+     throw new Error('Invalid data format received from API');
+   }
    const geojsonData = ConvertToGeojson(jsonData);
-   console.log('GeoJson Data : ',geojsonData)
    // ... rest of the code
  } catch (error) {
-   console.error('Error fetching GeoJSON:', error);
+   console.error('Error fetching or processing map data:', error);
+   // TODO: Add user-friendly error handling
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const response = await fetch(`https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=${AirQoToken}`);
const jsonData = await response.json();
const geojsonData = ConvertToGeojson(jsonData);
console.log('GeoJson Data : ',geojsonData)
mapRef.current.addSource('data', {
type: 'geojson',
data: geojsonData
});
const popup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false,
});
// Listen for when the mouse enters a feature in your 'data-layer'
mapRef.current.on('mouseenter', 'data-layer', (e) => {
if (mapRef.current) {
mapRef.current.getCanvas().style.cursor = 'pointer';
}
// Get the feature under the mouse pointer (the first feature).
const feature = e.features ? e.features[0] : null;
if (feature && feature.properties) {
}
if (!feature) return;
const htmlContent = `
<div style="font-size: 14px; border-radius: 25px; background-color: white; padding: 10px; rgba(0, 0, 0, 0.2); max-width: full;">
<div>${feature.properties?.time ?? 'Unknown time'}</div><br>
<div style="display: flex; justify-content: space-between;gap: 20px;">
<strong style="display: flex; ">${feature.properties?.location_name ?? 'Unknown Location'}</strong>
<div style="display: flex;font-weight: bold; ">${feature.properties?.value.toFixed(2) ?? 'N/A'}µg/m³</div>
</div>
<div style=" display: flex;gap: 10px; ">
<h1 style="font-weight: bold; color: ${feature.properties?.aqi_color ?? 'black'};">Air Quality is ${feature.properties?.aqi_category ?? 'N/A'}</h1>
<img src="${AirQuality.goodair}" style="background-color: ${feature.properties?.aqi_color ?? 'green'};width: 30px; height: 30px;border-radius: 50%;font-size: 18px;"></img>
</div>
</div>
`;
// Set the popup at the feature's coordinates and add the HTML content.
popup
.setLngLat((feature.geometry as GeoJSON.Point).coordinates as [number, number])
.setHTML(htmlContent)
if (mapRef.current) {
popup.addTo(mapRef.current);
}
});
// When the mouse leaves the feature, remove the popup.
mapRef.current.on('mouseleave', 'data-layer', () => {
if (mapRef.current) {
mapRef.current.getCanvas().style.cursor = '';
}
popup.remove();
});
Object.entries(AirQuality).forEach(([key, url]) => {
if (mapRef.current) {
mapRef.current.loadImage(url, (error, image) => {
if (error) throw error;
if (mapRef.current && image && !mapRef.current.hasImage(key)) {
mapRef.current.addImage(key, image);
}
});
}
});
mapRef.current.addLayer({
'id': 'circle-layer',
'type': 'circle',
'source': 'data',
'paint': {
'circle-color': 'white',
'circle-radius': 30,
'circle-opacity': 1
}
});
mapRef.current.addLayer({
'id': 'data-layer',
'type': 'symbol',
'source': 'data',
'layout': {
'icon-image': [
'match',
['get', 'aqi_category'],
'Good', 'goodair',
'Moderate', 'moderate',
'Unhealthy', 'unhealthy',
'Unhealthy for Sensitive Groups', 'unhealthySG',
'Hazardous','Hazardous',
'Very Unhealthy', 'veryunhealthy',
'unknownAQ'
],
'icon-size': 0.05,
'icon-allow-overlap': true
},
'filter': ['has', 'aqi_category']
});
} catch (error) {
console.error('Error fetching GeoJSON:', error);
}
try {
if (!AirQoToken) {
throw new Error('AirQo token is not configured');
}
const response = await fetch(`https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=${AirQoToken}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const jsonData = await response.json();
if (!jsonData || !Array.isArray(jsonData)) {
throw new Error('Invalid data format received from API');
}
const geojsonData = ConvertToGeojson(jsonData);
// ... rest of the code
mapRef.current.addSource('data', {
type: 'geojson',
data: geojsonData
});
const popup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false,
});
// Listen for when the mouse enters a feature in your 'data-layer'
mapRef.current.on('mouseenter', 'data-layer', (e) => {
if (mapRef.current) {
mapRef.current.getCanvas().style.cursor = 'pointer';
}
// Get the feature under the mouse pointer (the first feature).
const feature = e.features ? e.features[0] : null;
if (feature && feature.properties) {
// (Optional: Add any logic based on feature properties here)
}
if (!feature) return;
const htmlContent = `
<div style="font-size: 14px; border-radius: 25px; background-color: white; padding: 10px; rgba(0, 0, 0, 0.2); max-width: full;">
<div>${feature.properties?.time ?? 'Unknown time'}</div><br>
<div style="display: flex; justify-content: space-between; gap: 20px;">
<strong style="display: flex;">${feature.properties?.location_name ?? 'Unknown Location'}</strong>
<div style="display: flex; font-weight: bold;">${feature.properties?.value.toFixed(2) ?? 'N/A'}µg/m³</div>
</div>
<div style="display: flex; gap: 10px;">
<h1 style="font-weight: bold; color: ${feature.properties?.aqi_color ?? 'black'};">Air Quality is ${feature.properties?.aqi_category ?? 'N/A'}</h1>
<img src="${AirQuality.goodair}" style="background-color: ${feature.properties?.aqi_color ?? 'green'}; width: 30px; height: 30px; border-radius: 50%; font-size: 18px;">
</div>
</div>
`;
// Set the popup at the feature's coordinates and add the HTML content.
popup
.setLngLat((feature.geometry as GeoJSON.Point).coordinates as [number, number])
.setHTML(htmlContent);
if (mapRef.current) {
popup.addTo(mapRef.current);
}
});
// When the mouse leaves the feature, remove the popup.
mapRef.current.on('mouseleave', 'data-layer', () => {
if (mapRef.current) {
mapRef.current.getCanvas().style.cursor = '';
}
popup.remove();
});
Object.entries(AirQuality).forEach(([key, url]) => {
if (mapRef.current) {
mapRef.current.loadImage(url, (error, image) => {
if (error) throw error;
if (mapRef.current && image && !mapRef.current.hasImage(key)) {
mapRef.current.addImage(key, image);
}
});
}
});
mapRef.current.addLayer({
'id': 'circle-layer',
'type': 'circle',
'source': 'data',
'paint': {
'circle-color': 'white',
'circle-radius': 30,
'circle-opacity': 1
}
});
mapRef.current.addLayer({
'id': 'data-layer',
'type': 'symbol',
'source': 'data',
'layout': {
'icon-image': [
'match',
['get', 'aqi_category'],
'Good', 'goodair',
'Moderate', 'moderate',
'Unhealthy', 'unhealthy',
'Unhealthy for Sensitive Groups', 'unhealthySG',
'Hazardous', 'Hazardous',
'Very Unhealthy', 'veryunhealthy',
'unknownAQ'
],
'icon-size': 0.05,
'icon-allow-overlap': true
},
'filter': ['has', 'aqi_category']
});
} catch (error) {
console.error('Error fetching or processing map data:', error);
// TODO: Add user-friendly error handling
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 91-91: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
netmanager-app/components/NetManagerMap/page.tsx (2)

71-75: Remove console.log statements and improve error handling.

Remove debugging console.log statements and add proper error handling.

Apply this diff:

-const Data = await GetAirQuoData(AirQoToken?AirQoToken:"")
-console.log(Data)
-const geojsonData = ConvertToGeojson(Data);
-console.log('GeoJson Data : ',geojsonData)
+const Data = await GetAirQuoData(AirQoToken ?? "");
+if (!Data) {
+  throw new Error('Failed to fetch air quality data');
+}
+const geojsonData = ConvertToGeojson(Data);

91-92: Use optional chaining for better readability.

The feature access can be simplified using optional chaining.

-const feature = e.features ? e.features[0] : null;
+const feature = e.features?.[0];
🧰 Tools
🪛 Biome (1.9.4)

[error] 92-92: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 396a0d1 and 237afd1.

⛔ Files ignored due to path filters (1)
  • netmanager-app/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • netmanager-app/api/MapData.ts (1 hunks)
  • netmanager-app/components/NetManagerMap/page.tsx (1 hunks)
  • netmanager-app/package.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • netmanager-app/package.json
🧰 Additional context used
🪛 Biome (1.9.4)
netmanager-app/api/MapData.ts

[error] 21-21: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)


[error] 10-13: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.

(lint/complexity/noUselessLoneBlockStatements)

netmanager-app/components/NetManagerMap/page.tsx

[error] 92-92: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (1)
netmanager-app/components/NetManagerMap/page.tsx (1)

24-25: Move sensitive tokens to environment variables.

Good practice using environment variables for sensitive tokens.

Comment on lines 46 to 59
export const UserClick = async(access_token:string,sessionToken:string,locationid: string,)=>{
if (!access_token) {
console.log("Missing Access Token");
return;
}

const response = await axios.get((`https://api.mapbox.com/search/searchbox/v1/retrieve/${locationid}?&access_token=${access_token}&session_token=${sessionToken}`))
const data = response.data;
if(data){
return data;
}


}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling and type safety.

The function needs several improvements:

  1. Add proper return type
  2. Add try-catch block
  3. Move API URL to environment variables
  4. Consistent error handling with other functions

Apply this diff to improve the implementation:

-export const UserClick = async(access_token:string,sessionToken:string,locationid: string,)=>{
+interface LocationData {
+  features: Array<{
+    geometry: {
+      coordinates: [number, number];
+    };
+  }>;
+}
+
+export const UserClick = async(
+  access_token: string,
+  sessionToken: string,
+  locationid: string
+): Promise<LocationData | undefined> => {
+  const API_URL = process.env.NEXT_PUBLIC_MAPBOX_API_URL;
+  if (!API_URL) {
+    console.error('API URL is not configured');
+    return undefined;
+  }
   if (!access_token) {
-    console.log("Missing Access Token");
-    return;
+    console.error("Missing Access Token");
+    return undefined;
   }
-  const response = await axios.get((`https://api.mapbox.com/search/searchbox/v1/retrieve/${locationid}?&access_token=${access_token}&session_token=${sessionToken}`))
-  const data = response.data;
-  if(data){
-    return data;
+  try {
+    const response = await axios.get(
+      `${API_URL}/search/searchbox/v1/retrieve/${locationid}?access_token=${access_token}&session_token=${sessionToken}`
+    );
+    return response.data;
+  } catch (error) {
+    console.error("Error retrieving location:", error);
+    return undefined;
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const UserClick = async(access_token:string,sessionToken:string,locationid: string,)=>{
if (!access_token) {
console.log("Missing Access Token");
return;
}
const response = await axios.get((`https://api.mapbox.com/search/searchbox/v1/retrieve/${locationid}?&access_token=${access_token}&session_token=${sessionToken}`))
const data = response.data;
if(data){
return data;
}
}
interface LocationData {
features: Array<{
geometry: {
coordinates: [number, number];
};
}>;
}
export const UserClick = async(
access_token: string,
sessionToken: string,
locationid: string
): Promise<LocationData | undefined> => {
const API_URL = process.env.NEXT_PUBLIC_MAPBOX_API_URL;
if (!API_URL) {
console.error('API URL is not configured');
return undefined;
}
if (!access_token) {
console.error("Missing Access Token");
return undefined;
}
try {
const response = await axios.get(
`${API_URL}/search/searchbox/v1/retrieve/${locationid}?access_token=${access_token}&session_token=${sessionToken}`
);
return response.data;
} catch (error) {
console.error("Error retrieving location:", error);
return undefined;
}
}

Comment on lines 3 to 19
export const GetAirQuoData = async (token: string) =>{
try {
if(token){
const response = await axios.get(`https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=${token}`);
const data = response.data;
return data;
}
{
console.log("Error Token Is Missing")
return null;
}

}
catch (error) {
console.error('Error fetching GeoJSON:', error);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling and improve code structure.

The function has several areas for improvement:

  1. Remove the redundant block statement
  2. Add proper type for the return value
  3. Move the API URL to environment variables
  4. Strengthen token validation

Apply this diff to improve the implementation:

-export const GetAirQuoData = async (token: string) =>{
+export const GetAirQuoData = async (token: string): Promise<any | null> => {
+  const API_URL = process.env.NEXT_PUBLIC_AIRQO_API_URL;
+  if (!API_URL) {
+    console.error('API URL is not configured');
+    return null;
+  }
+  if (!token || typeof token !== 'string' || token.trim() === '') {
+    console.error('Invalid token provided');
+    return null;
+  }
   try {
-    if(token){
-      const response = await axios.get(`https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=${token}`);
-      const data = response.data;
-      return data;
-    }
-    {
-      console.log("Error Token Is Missing")
-      return null;
-    }
+    const response = await axios.get(`${API_URL}/api/v2/devices/readings/map?token=${token}`);
+    return response.data;
   }
   catch (error) {
-    console.error('Error fetching GeoJSON:', error);
+    console.error('Error fetching air quality data:', error);
+    return null;
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const GetAirQuoData = async (token: string) =>{
try {
if(token){
const response = await axios.get(`https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=${token}`);
const data = response.data;
return data;
}
{
console.log("Error Token Is Missing")
return null;
}
}
catch (error) {
console.error('Error fetching GeoJSON:', error);
}
}
export const GetAirQuoData = async (token: string): Promise<any | null> => {
const API_URL = process.env.NEXT_PUBLIC_AIRQO_API_URL;
if (!API_URL) {
console.error('API URL is not configured');
return null;
}
if (!token || typeof token !== 'string' || token.trim() === '') {
console.error('Invalid token provided');
return null;
}
try {
const response = await axios.get(`${API_URL}/api/v2/devices/readings/map?token=${token}`);
return response.data;
}
catch (error) {
console.error('Error fetching air quality data:', error);
return null;
}
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 10-13: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.

(lint/complexity/noUselessLoneBlockStatements)

Comment on lines 21 to 45
export const FetchSuggestions=async(value:string,access_token:string,sessionToken:string,latitude?: number, longitude?: number):Promise<any[] | void>=>{

if (!access_token) {
console.log("Missing Access Token");
return;
}

if (latitude !== undefined && longitude !== undefined) {
try{
const proximityParam = `${longitude},${latitude}`;
const response = await axios.get(`https://api.mapbox.com/search/searchbox/v1/suggest?q=${value.toLowerCase()}&access_token=${access_token}&proximity=${proximityParam}&session_token=${sessionToken}`)
const data = await response.data;

console.log("data :",data.suggestions)
if (data.suggestions) {
return data.suggestions;
}else{
console.log("Error fetching suggestions");
}

}
catch(error){
console.error("Error fetching suggestions:", error);
}
}}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve type safety and reduce complexity.

The function needs several improvements:

  1. Replace void with undefined in return type
  2. Flatten the nested structure
  3. Consistent error handling
  4. Move API URL to environment variables

Apply this diff to improve the implementation:

-export const FetchSuggestions = async(value:string, access_token:string, sessionToken:string, latitude?: number, longitude?: number): Promise<any[] | void> => {
+interface MapboxSuggestion {
+  mapbox_id: string;
+  name: string;
+  place_formatted: string;
+}
+
+export const FetchSuggestions = async(
+  value: string,
+  access_token: string,
+  sessionToken: string,
+  latitude?: number,
+  longitude?: number
+): Promise<MapboxSuggestion[] | undefined> => {
+  const API_URL = process.env.NEXT_PUBLIC_MAPBOX_API_URL;
+  if (!API_URL) {
+    console.error('API URL is not configured');
+    return undefined;
+  }
   if (!access_token) {
-    console.log("Missing Access Token");
-    return;
+    console.error("Missing Access Token");
+    return undefined;
   }
-  if (latitude !== undefined && longitude !== undefined) {
-    try {
-      const proximityParam = `${longitude},${latitude}`;
-      const response = await axios.get(`https://api.mapbox.com/search/searchbox/v1/suggest?q=${value.toLowerCase()}&access_token=${access_token}&proximity=${proximityParam}&session_token=${sessionToken}`)
-      const data = await response.data;
-      console.log("data :", data.suggestions)
-      if (data.suggestions) {
-        return data.suggestions;
-      } else {
-        console.log("Error fetching suggestions");
-      }
-    } catch(error) {
-      console.error("Error fetching suggestions:", error);
-    }
+  try {
+    const proximityParam = latitude !== undefined && longitude !== undefined 
+      ? `&proximity=${longitude},${latitude}`
+      : '';
+    const response = await axios.get(
+      `${API_URL}/search/searchbox/v1/suggest?q=${value.toLowerCase()}&access_token=${access_token}${proximityParam}&session_token=${sessionToken}`
+    );
+    return response.data.suggestions || [];
+  } catch(error) {
+    console.error("Error fetching suggestions:", error);
+    return undefined;
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const FetchSuggestions=async(value:string,access_token:string,sessionToken:string,latitude?: number, longitude?: number):Promise<any[] | void>=>{
if (!access_token) {
console.log("Missing Access Token");
return;
}
if (latitude !== undefined && longitude !== undefined) {
try{
const proximityParam = `${longitude},${latitude}`;
const response = await axios.get(`https://api.mapbox.com/search/searchbox/v1/suggest?q=${value.toLowerCase()}&access_token=${access_token}&proximity=${proximityParam}&session_token=${sessionToken}`)
const data = await response.data;
console.log("data :",data.suggestions)
if (data.suggestions) {
return data.suggestions;
}else{
console.log("Error fetching suggestions");
}
}
catch(error){
console.error("Error fetching suggestions:", error);
}
}}
interface MapboxSuggestion {
mapbox_id: string;
name: string;
place_formatted: string;
}
export const FetchSuggestions = async(
value: string,
access_token: string,
sessionToken: string,
latitude?: number,
longitude?: number
): Promise<MapboxSuggestion[] | undefined> => {
const API_URL = process.env.NEXT_PUBLIC_MAPBOX_API_URL;
if (!API_URL) {
console.error('API URL is not configured');
return undefined;
}
if (!access_token) {
console.error("Missing Access Token");
return undefined;
}
try {
const proximityParam =
latitude !== undefined && longitude !== undefined
? `&proximity=${longitude},${latitude}`
: '';
const response = await axios.get(
`${API_URL}/search/searchbox/v1/suggest?q=${value.toLowerCase()}&access_token=${access_token}${proximityParam}&session_token=${sessionToken}`
);
return response.data.suggestions || [];
} catch (error) {
console.error("Error fetching suggestions:", error);
return undefined;
}
};
🧰 Tools
🪛 Biome (1.9.4)

[error] 21-21: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

Comment on lines 319 to 322
onClick={() => setIsOpen(true)}
title='Refresh Map'
icon={<RefreshIcon width={24} height={24} fill={""} />}
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect onClick handler.

The refresh button uses the same onClick handler as the layer button.

-onClick={() => setIsOpen(true)}
+onClick={() => {
+  // TODO: Implement refresh functionality
+  // Uncomment when ready:
+  // refreshMap();
+}}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onClick={() => setIsOpen(true)}
title='Refresh Map'
icon={<RefreshIcon width={24} height={24} fill={""} />}
/>
onClick={() => {
// TODO: Implement refresh functionality
// Uncomment when ready:
// refreshMap();
}}
title='Refresh Map'
icon={<RefreshIcon width={24} height={24} fill={""} />}
/>

Comment on lines 324 to 328
<IconButton
onClick={() => setIsOpen(true)}
title='Refresh Map'
icon={<ShareIcon width={24} height={24} fill={""} />}
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect title and onClick handler for share button.

The share button has incorrect title and reuses layer button's onClick handler.

-onClick={() => setIsOpen(true)}
-title='Refresh Map'
+onClick={() => {
+  // TODO: Implement share functionality
+}}
+title='Share Map'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<IconButton
onClick={() => setIsOpen(true)}
title='Refresh Map'
icon={<ShareIcon width={24} height={24} fill={""} />}
/>
<IconButton
onClick={() => {
// TODO: Implement share functionality
}}
title='Share Map'
icon={<ShareIcon width={24} height={24} fill={""} />}
/>

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (1)
netmanager-app/components/NetManagerMap/page.tsx (1)

311-329: ⚠️ Potential issue

Fix incorrect button handlers and titles.

The buttons have incorrect onClick handlers and titles.

 <IconButton
  onClick={() => setIsOpen(true)}
  title='Map Layers'
  icon={<LayerIcon width={24} height={24} fill={""}/>}
 />

 <IconButton
-  onClick={() => setIsOpen(true)}
+  onClick={() => {
+    // TODO: Implement refresh functionality
+    // refreshMap();
+  }}
  title='Refresh Map'
  icon={<RefreshIcon width={24} height={24} fill={""} />}
 />
 
 <IconButton
-  onClick={() => setIsOpen(true)}
-  title='Refresh Map'
+  onClick={() => {
+    // TODO: Implement share functionality
+  }}
+  title='Share Map'
  icon={<ShareIcon width={24} height={24} fill={""} />}
 />
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 237afd1 and 7bc07c7.

📒 Files selected for processing (2)
  • netmanager-app/components/NetManagerMap/page.tsx (1 hunks)
  • netmanager-app/core/apis/MapData.ts (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
netmanager-app/components/NetManagerMap/page.tsx

[error] 92-92: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

netmanager-app/core/apis/MapData.ts

[error] 21-21: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)


[error] 10-13: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.

(lint/complexity/noUselessLoneBlockStatements)

🔇 Additional comments (1)
netmanager-app/components/NetManagerMap/page.tsx (1)

182-231: Optimize search functionality with debouncing.

The search functionality needs optimization and better type safety.

+const DEBOUNCE_DELAY = 300;
+
+const useDebounce = <T>(value: T, delay: number): T => {
+  const [debouncedValue, setDebouncedValue] = useState<T>(value);
+
+  useEffect(() => {
+    const handler = setTimeout(() => {
+      setDebouncedValue(value);
+    }, delay);
+
+    return () => {
+      clearTimeout(handler);
+    };
+  }, [value, delay]);
+
+  return debouncedValue;
+};
+
-const SearchSuggestions=(e: React.ChangeEvent<HTMLInputElement>)=>{
+const handleSearchInput = (e: React.ChangeEvent<HTMLInputElement>) => {
   const value = e.target.value;
   setQuery(value);
+};
+
+const debouncedQuery = useDebounce(query, DEBOUNCE_DELAY);
+
+useEffect(() => {
   if (value.trim() === "") {
     setSuggestions([]);
     // ... rest of the code
   }
   fetchUserLocation();
-}
+}, [debouncedQuery]);

Comment on lines 3 to 19
export const GetAirQuoData = async (token: string) =>{
try {
if(token){
const response = await axios.get(`https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=${token}`);
const data = response.data;
return data;
}
{
console.log("Error Token Is Missing")
return null;
}

}
catch (error) {
console.error('Error fetching GeoJSON:', error);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling and type safety in GetAirQuoData.

The function needs improvements in error handling, type safety, and configuration management.

+interface AirQoResponse {
+  // TODO: Add specific types based on actual response structure
+  [key: string]: any;
+}
+
-export const GetAirQuoData = async (token: string) =>{
+export const GetAirQuoData = async (token: string): Promise<AirQoResponse | null> => {
         try {
                 if(token){
-                        const response = await axios.get(`https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=${token}`);
-                const data = response.data;
-                return data;
+                        const baseUrl = process.env.NEXT_PUBLIC_AIRQO_API_URL || 'https://staging-analytics.airqo.net';
+                        const response = await axios.get<AirQoResponse>(`${baseUrl}/api/v2/devices/readings/map?token=${token}`);
+                        return response.data;
                 }
-                {
-                        console.log("Error Token Is Missing")
-                        return null;
-                }
+                console.error("Error: Token is missing");
+                return null;
         }
         catch (error) {
-                console.error('Error fetching GeoJSON:', error);
+                console.error('Error fetching air quality data:', error);
+                throw new Error('Failed to fetch air quality data');
         }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const GetAirQuoData = async (token: string) =>{
try {
if(token){
const response = await axios.get(`https://staging-analytics.airqo.net/api/v2/devices/readings/map?token=${token}`);
const data = response.data;
return data;
}
{
console.log("Error Token Is Missing")
return null;
}
}
catch (error) {
console.error('Error fetching GeoJSON:', error);
}
}
interface AirQoResponse {
// TODO: Add specific types based on the actual response structure
[key: string]: any;
}
export const GetAirQuoData = async (token: string): Promise<AirQoResponse | null> => {
try {
if (token) {
const baseUrl = process.env.NEXT_PUBLIC_AIRQO_API_URL || 'https://staging-analytics.airqo.net';
const response = await axios.get<AirQoResponse>(`${baseUrl}/api/v2/devices/readings/map?token=${token}`);
return response.data;
}
console.error("Error: Token is missing");
return null;
}
catch (error) {
console.error('Error fetching air quality data:', error);
throw new Error('Failed to fetch air quality data');
}
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 10-13: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.

(lint/complexity/noUselessLoneBlockStatements)

Comment on lines 21 to 45
export const FetchSuggestions=async(value:string,access_token:string,sessionToken:string,latitude?: number, longitude?: number):Promise<any[] | void>=>{

if (!access_token) {
console.log("Missing Access Token");
return;
}

if (latitude !== undefined && longitude !== undefined) {
try{
const proximityParam = `${longitude},${latitude}`;
const response = await axios.get(`https://api.mapbox.com/search/searchbox/v1/suggest?q=${value.toLowerCase()}&access_token=${access_token}&proximity=${proximityParam}&session_token=${sessionToken}`)
const data = await response.data;

console.log("data :",data.suggestions)
if (data.suggestions) {
return data.suggestions;
}else{
console.log("Error fetching suggestions");
}

}
catch(error){
console.error("Error fetching suggestions:", error);
}
}}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve type safety and error handling in FetchSuggestions.

The function needs better type definitions and consistent error handling.

+interface MapboxSuggestion {
+  mapbox_id: string;
+  name: string;
+  place_formatted: string;
+}
+
+interface MapboxResponse {
+  suggestions: MapboxSuggestion[];
+}
+
-export const FetchSuggestions = async(value:string,access_token:string,sessionToken:string,latitude?: number, longitude?: number):Promise<any[] | void> => {
+export const FetchSuggestions = async(
+  value: string,
+  access_token: string,
+  sessionToken: string,
+  latitude?: number,
+  longitude?: number
+): Promise<MapboxSuggestion[] | null> => {
         if (!access_token) {
-                console.log("Missing Access Token");
-                return;
+                throw new Error("Missing access token");
         }
         
+        try {
+          const params = new URLSearchParams({
+            q: value.toLowerCase(),
+            access_token,
+            session_token: sessionToken,
+            ...(latitude !== undefined && longitude !== undefined && {
+              proximity: `${longitude},${latitude}`
+            })
+          });
+
+          const response = await axios.get<MapboxResponse>(
+            `https://api.mapbox.com/search/searchbox/v1/suggest?${params}`
+          );
+
+          return response.data.suggestions || null;
+        } catch (error) {
+          console.error("Error fetching suggestions:", error);
+          throw new Error("Failed to fetch location suggestions");
+        }
-        if (latitude !== undefined && longitude !== undefined) {
-                try{
-                const proximityParam = `${longitude},${latitude}`;
-                const response = await axios.get(`https://api.mapbox.com/search/searchbox/v1/suggest?q=${value.toLowerCase()}&access_token=${access_token}&proximity=${proximityParam}&session_token=${sessionToken}`)
-                const data = await response.data;
-                
-                console.log("data :",data.suggestions)
-                 if (data.suggestions) {
-                        return data.suggestions;
-                                }else{
-                                        console.log("Error fetching suggestions");
-                                }
-                        
-                        }
-                        catch(error){
-                                console.error("Error fetching suggestions:", error);
-                        }
-                }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const FetchSuggestions=async(value:string,access_token:string,sessionToken:string,latitude?: number, longitude?: number):Promise<any[] | void>=>{
if (!access_token) {
console.log("Missing Access Token");
return;
}
if (latitude !== undefined && longitude !== undefined) {
try{
const proximityParam = `${longitude},${latitude}`;
const response = await axios.get(`https://api.mapbox.com/search/searchbox/v1/suggest?q=${value.toLowerCase()}&access_token=${access_token}&proximity=${proximityParam}&session_token=${sessionToken}`)
const data = await response.data;
console.log("data :",data.suggestions)
if (data.suggestions) {
return data.suggestions;
}else{
console.log("Error fetching suggestions");
}
}
catch(error){
console.error("Error fetching suggestions:", error);
}
}}
interface MapboxSuggestion {
mapbox_id: string;
name: string;
place_formatted: string;
}
interface MapboxResponse {
suggestions: MapboxSuggestion[];
}
export const FetchSuggestions = async(
value: string,
access_token: string,
sessionToken: string,
latitude?: number,
longitude?: number
): Promise<MapboxSuggestion[] | null> => {
if (!access_token) {
throw new Error("Missing access token");
}
try {
const params = new URLSearchParams({
q: value.toLowerCase(),
access_token,
session_token: sessionToken,
...(latitude !== undefined &&
longitude !== undefined && {
proximity: `${longitude},${latitude}`
})
});
const response = await axios.get<MapboxResponse>(
`https://api.mapbox.com/search/searchbox/v1/suggest?${params}`
);
return response.data.suggestions || null;
} catch (error) {
console.error("Error fetching suggestions:", error);
throw new Error("Failed to fetch location suggestions");
}
};
🧰 Tools
🪛 Biome (1.9.4)

[error] 21-21: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

Comment on lines 46 to 59
export const UserClick = async(access_token:string,sessionToken:string,locationid: string,)=>{
if (!access_token) {
console.log("Missing Access Token");
return;
}

const response = await axios.get((`https://api.mapbox.com/search/searchbox/v1/retrieve/${locationid}?&access_token=${access_token}&session_token=${sessionToken}`))
const data = response.data;
if(data){
return data;
}


}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add type safety and error handling to UserClick.

The function needs proper type definitions and consistent error handling.

+interface MapboxLocation {
+  features: Array<{
+    geometry: {
+      coordinates: [number, number];
+    };
+  }>;
+}
+
-export const UserClick = async(access_token:string,sessionToken:string,locationid: string,)=>{
+export const UserClick = async(
+  access_token: string,
+  sessionToken: string,
+  locationid: string
+): Promise<MapboxLocation> => {
         if (!access_token) {
-                console.log("Missing Access Token");
-                return;
+                throw new Error("Missing access token");
         }
 
-        const response = await axios.get((`https://api.mapbox.com/search/searchbox/v1/retrieve/${locationid}?&access_token=${access_token}&session_token=${sessionToken}`))
-        const data = response.data;
-        if(data){
-                return data;
-        }
+        try {
+          const response = await axios.get<MapboxLocation>(
+            `https://api.mapbox.com/search/searchbox/v1/retrieve/${locationid}`,
+            {
+              params: {
+                access_token,
+                session_token: sessionToken
+              }
+            }
+          );
+          return response.data;
+        } catch (error) {
+          console.error("Error fetching location:", error);
+          throw new Error("Failed to fetch location details");
+        }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const UserClick = async(access_token:string,sessionToken:string,locationid: string,)=>{
if (!access_token) {
console.log("Missing Access Token");
return;
}
const response = await axios.get((`https://api.mapbox.com/search/searchbox/v1/retrieve/${locationid}?&access_token=${access_token}&session_token=${sessionToken}`))
const data = response.data;
if(data){
return data;
}
}
interface MapboxLocation {
features: Array<{
geometry: {
coordinates: [number, number];
};
}>;
}
export const UserClick = async(
access_token: string,
sessionToken: string,
locationid: string
): Promise<MapboxLocation> => {
if (!access_token) {
throw new Error("Missing access token");
}
try {
const response = await axios.get<MapboxLocation>(
`https://api.mapbox.com/search/searchbox/v1/retrieve/${locationid}`,
{
params: {
access_token,
session_token: sessionToken
}
}
);
return response.data;
} catch (error) {
console.error("Error fetching location:", error);
throw new Error("Failed to fetch location details");
}
}

Comment on lines 43 to 49
// const refreshMap = useRefreshMap(
// setToastMessage,
// mapRef,
// dispatch,
// selectedNode,
// );
//Get the Session Token for the User
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

Remove commented code or implement refresh functionality.

The commented refreshMap code should either be implemented or removed.


🏁 Script executed:

#!/bin/bash
# Search for useRefreshMap implementation
ast-grep --pattern 'function useRefreshMap($$$) {
  $$$
}'

Length of output: 60


Action Required: Remove Unimplemented Refresh Code

After verifying the repository, no implementation for the useRefreshMap hook was found. Please either remove the commented-out refreshMap code in netmanager-app/components/NetManagerMap/page.tsx (lines 43-49) or implement the intended refresh functionality if required.

Comment on lines 57 to 180
closeButton: false,
closeOnClick: false,

});
// Listen for when the mouse enters a feature in your 'data-layer'
mapRef.current.on('mouseenter', 'data-layer', (e) => {
if (mapRef.current) {
mapRef.current.getCanvas().style.cursor = 'pointer';
}
// Get the feature under the mouse pointer (the first feature).
const feature = e.features ? e.features[0] : null;
if (feature && feature.properties) {

}

if (!feature) return;

const htmlContent = `
<div style="font-size: 14px; border-radius: 25px; background-color: white; padding: 10px; rgba(0, 0, 0, 0.2); max-width: full;">
<div>${feature.properties?.time ?? 'Unknown time'}</div><br>
<div style="display: flex; justify-content: space-between;gap: 20px;">
<strong style="display: flex; ">${feature.properties?.location_name ?? 'Unknown Location'}</strong>
<div style="display: flex;font-weight: bold; ">${feature.properties?.value.toFixed(2) ?? 'N/A'}µg/m³</div>
</div>

<div style=" display: flex;gap: 10px; ">
<h1 style="font-weight: bold; color: ${feature.properties?.aqi_color ?? 'black'};">Air Quality is ${feature.properties?.aqi_category ?? 'N/A'}</h1>
<img src="${AirQuality.goodair}" style="background-color: ${feature.properties?.aqi_color ?? 'green'};width: 30px; height: 30px;border-radius: 50%;font-size: 18px;"></img>
</div>
</div>
`;

// Set the popup at the feature's coordinates and add the HTML content.
popup
.setLngLat((feature.geometry as GeoJSON.Point).coordinates as [number, number])
.setHTML(htmlContent)
if (mapRef.current) {
popup.addTo(mapRef.current);
}
});

// When the mouse leaves the feature, remove the popup.
mapRef.current.on('mouseleave', 'data-layer', () => {
if (mapRef.current) {
mapRef.current.getCanvas().style.cursor = '';
}
popup.remove();
});
Object.entries(AirQuality).forEach(([key, url]) => {
if (mapRef.current) {
mapRef.current.loadImage(url, (error, image) => {
if (error) throw error;

if (mapRef.current && image && !mapRef.current.hasImage(key)) {
mapRef.current.addImage(key, image);
}
});
}
});
mapRef.current.addLayer({
'id': 'circle-layer',
'type': 'circle',
'source': 'data',
'paint': {
'circle-color': 'white',
'circle-radius': 30,
'circle-opacity': 1
}
});
mapRef.current.addLayer({
'id': 'data-layer',
'type': 'symbol',
'source': 'data',
'layout': {
'icon-image': [
'match',
['get', 'aqi_category'],
'Good', 'goodair',
'Moderate', 'moderate',
'Unhealthy', 'unhealthy',
'Unhealthy for Sensitive Groups', 'unhealthySG',
'Hazardous','Hazardous',
'Very Unhealthy', 'veryunhealthy',
'unknownAQ'
],
'icon-size': 0.05,
'icon-allow-overlap': true
},

'filter': ['has', 'aqi_category']
});


} catch (error) {
console.error('Error fetching GeoJSON:', error);
}
}
});
}
}, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor map initialization for better maintainability.

The map initialization logic is complex and needs to be broken down into smaller functions.

+const initializeMap = (container: HTMLDivElement, token: string) => {
+  return new mapboxgl.Map({
+    container,
+    style: 'mapbox://styles/mapbox/streets-v11',
+    center: [18.5, 3],
+    zoom: 3
+  });
+};
+
+const setupMapLayers = async (map: mapboxgl.Map, airQoToken: string) => {
+  try {
+    const data = await GetAirQuoData(airQoToken);
+    if (!data) return;
+    
+    const geojsonData = ConvertToGeojson(data);
+    map.addSource('data', {
+      type: 'geojson',
+      data: geojsonData
+    });
+    
+    // Add layers here...
+  } catch (error) {
+    console.error('Error setting up map layers:', error);
+    // TODO: Show user-friendly error message
+  }
+};
+
 useEffect(() => {
   mapboxgl.accessToken = token;
   if (mapContainerRef.current) {
-    mapRef.current = new mapboxgl.Map({
-      container: mapContainerRef.current,
-      style:'mapbox://styles/mapbox/streets-v11',
-      center: [18.5, 3], 
-      zoom: 3
-    });
+    mapRef.current = initializeMap(mapContainerRef.current, token);
     
     mapRef.current.on('load', async () => {
       if (mapRef.current) {
-        try {
-          const Data = await GetAirQuoData(AirQoToken?AirQoToken:"")
-          console.log(Data)
-          // ... rest of the code
-        } catch (error) {
-          console.error('Error fetching GeoJSON:', error);
-        }
+        await setupMapLayers(mapRef.current, AirQoToken ?? "");
       }
     });
   }
 }, []);
🧰 Tools
🪛 Biome (1.9.4)

[error] 92-92: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

Comment on lines 16 to 40
const NetManagerMap = () => {
// const { width } = useWindowSize();
const mapContainerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<mapboxgl.Map | null>(null);
const [query, setQuery] = useState("");
const [locationId,setlocationId] =useState("")
const [suggestions, setSuggestions] = useState<any[]>([]);
const [sessionToken, setSessionToken] = useState<string | null>(null);
const token = process.env.NEXT_PUBLIC_MAP_API_TOKEN
const AirQoToken = process.env.NEXT_PUBLIC_AIRQO_DATA_TOKEN
const [isOpen, setIsOpen] = useState(false);
const [NodeType, setNodeType] = useState('Emoji');
const [mapStyle, setMapStyle] = useState(
'mapbox://styles/mapbox/streets-v11',
);

const AirQuality= {
goodair :'/images/map/GoodAir.png',
moderate :'/images/map/Moderate.png',
hazardous :'/images/map/Hazardous.png',
unhealthy: '/images/map/Unhealthy.png',
veryunhealthy :'/images/map/VeryUnhealthy.png',
unknownAQ:'/images/map/VeryUnhealthy.png',
unhealthySG: '/images/map/UnhealthySG.png',
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add TypeScript interfaces and move constants to configuration.

The component needs better type safety and configuration management.

+interface AirQualityConfig {
+  [key: string]: string;
+}
+
+interface MapState {
+  query: string;
+  locationId: string;
+  suggestions: any[]; // TODO: Replace with proper type from MapData.ts
+  sessionToken: string | null;
+  isOpen: boolean;
+  nodeType: string;
+  mapStyle: string;
+}
+
 const NetManagerMap = () => {
-        const mapContainerRef = useRef<HTMLDivElement>(null);
-        const mapRef = useRef<mapboxgl.Map | null>(null);
-        const [query, setQuery] = useState("");
-        const [locationId,setlocationId] =useState("")
-        const [suggestions, setSuggestions] = useState<any[]>([]);
-        const [sessionToken, setSessionToken] = useState<string | null>(null);
-        const token = process.env.NEXT_PUBLIC_MAP_API_TOKEN
-        const AirQoToken = process.env.NEXT_PUBLIC_AIRQO_DATA_TOKEN
-        const [isOpen, setIsOpen] = useState(false);
-        const [NodeType, setNodeType] = useState('Emoji');
-        const [mapStyle, setMapStyle] = useState(
-          'mapbox://styles/mapbox/streets-v11',
-        );
+        const mapContainerRef = useRef<HTMLDivElement>(null);
+        const mapRef = useRef<mapboxgl.Map | null>(null);
+        
+        const [state, setState] = useState<MapState>({
+          query: "",
+          locationId: "",
+          suggestions: [],
+          sessionToken: null,
+          isOpen: false,
+          nodeType: 'Emoji',
+          mapStyle: 'mapbox://styles/mapbox/streets-v11'
+        });
+        
+        const token = process.env.NEXT_PUBLIC_MAP_API_TOKEN;
+        const AirQoToken = process.env.NEXT_PUBLIC_AIRQO_DATA_TOKEN;

-        const AirQuality= {
-        goodair :'/images/map/GoodAir.png',
-         moderate :'/images/map/Moderate.png',
-         hazardous :'/images/map/Hazardous.png',
-         unhealthy: '/images/map/Unhealthy.png',
-         veryunhealthy :'/images/map/VeryUnhealthy.png',
-         unknownAQ:'/images/map/VeryUnhealthy.png',
-         unhealthySG: '/images/map/UnhealthySG.png',
-        }
+        const AirQuality: AirQualityConfig = {
+          goodair: '/images/map/GoodAir.png',
+          moderate: '/images/map/Moderate.png',
+          hazardous: '/images/map/Hazardous.png',
+          unhealthy: '/images/map/Unhealthy.png',
+          veryunhealthy: '/images/map/VeryUnhealthy.png',
+          unknownAQ: '/images/map/VeryUnhealthy.png',
+          unhealthySG: '/images/map/UnhealthySG.png',
+        };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const NetManagerMap = () => {
// const { width } = useWindowSize();
const mapContainerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<mapboxgl.Map | null>(null);
const [query, setQuery] = useState("");
const [locationId,setlocationId] =useState("")
const [suggestions, setSuggestions] = useState<any[]>([]);
const [sessionToken, setSessionToken] = useState<string | null>(null);
const token = process.env.NEXT_PUBLIC_MAP_API_TOKEN
const AirQoToken = process.env.NEXT_PUBLIC_AIRQO_DATA_TOKEN
const [isOpen, setIsOpen] = useState(false);
const [NodeType, setNodeType] = useState('Emoji');
const [mapStyle, setMapStyle] = useState(
'mapbox://styles/mapbox/streets-v11',
);
const AirQuality= {
goodair :'/images/map/GoodAir.png',
moderate :'/images/map/Moderate.png',
hazardous :'/images/map/Hazardous.png',
unhealthy: '/images/map/Unhealthy.png',
veryunhealthy :'/images/map/VeryUnhealthy.png',
unknownAQ:'/images/map/VeryUnhealthy.png',
unhealthySG: '/images/map/UnhealthySG.png',
}
interface AirQualityConfig {
[key: string]: string;
}
interface MapState {
query: string;
locationId: string;
suggestions: any[]; // TODO: Replace with proper type from MapData.ts
sessionToken: string | null;
isOpen: boolean;
nodeType: string;
mapStyle: string;
}
const NetManagerMap = () => {
const mapContainerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<mapboxgl.Map | null>(null);
const [state, setState] = useState<MapState>({
query: "",
locationId: "",
suggestions: [],
sessionToken: null,
isOpen: false,
nodeType: 'Emoji',
mapStyle: 'mapbox://styles/mapbox/streets-v11'
});
const token = process.env.NEXT_PUBLIC_MAP_API_TOKEN;
const AirQoToken = process.env.NEXT_PUBLIC_AIRQO_DATA_TOKEN;
const AirQuality: AirQualityConfig = {
goodair: '/images/map/GoodAir.png',
moderate: '/images/map/Moderate.png',
hazardous: '/images/map/Hazardous.png',
unhealthy: '/images/map/Unhealthy.png',
veryunhealthy: '/images/map/VeryUnhealthy.png',
unknownAQ: '/images/map/VeryUnhealthy.png',
unhealthySG: '/images/map/UnhealthySG.png',
};
// ... rest of the component code
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants