Repository: airqo-platform/AirQo-frontend
Pull request #2639: Netmanager app analytics
Author: login: danielmarv
Created: 2025-04-19T08:05:37Z | Merged: 2025-04-19T08:19:21Z

#### Summary of Changes (What does this PR do?)

- Refactor some netmanager errors

#### Status of maturity (all need to be checked before merging):

- [x] 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?](#what-are-the-relevant-tickets)" section to [link the issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword).
- [ ] 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?

- Please include the steps to be done inorder to setup and test this PR.

#### What are the relevant tickets?

- Closes #<enter issue number here>
- [Jira_card_number]() (If exists)

#### Screenshots (optional)


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a new error handling page that provides clear messaging and options to retry or return to the login screen when an error occurs.
  - Added detailed site metadata for improved SEO and social media sharing.
  - Added a client-side layout component that manages global styles, scrolling behavior, and UI notifications.

- **Refactor**
  - Redesigned the main layout structure, delegating layout responsibilities to a new client-side component for better separation of concerns.
  - Updated the logout process to ensure a more thorough session cleanup and improved user redirection.

- **Style**
  - Enhanced visual consistency and spacing in the pollutant category display.
  - Minor text and color mapping improvements for pollutant levels.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/netmanager-app/app/client-layout.tsx b/src/netmanager-app/app/client-layout.tsx
new file mode 100644
index 0000000000..3ec64268ba
--- /dev/null
+++ b/src/netmanager-app/app/client-layout.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import type React from "react"
+
+import { usePathname } from "next/navigation"
+import { Toaster } from "@/components/ui/sonner"
+import Providers from "./providers"
+import { cn } from "@/lib/utils"
+import { Inter } from "next/font/google"
+
+const inter = Inter({ subsets: ["latin"] })
+
+export default function ClientLayout({
+  children,
+}: {
+  children: React.ReactNode
+}) {
+  const pathname = usePathname()
+  const isNetworkMap = pathname === "/network-map"
+
+  return (
+    <html lang="en" suppressHydrationWarning>
+      <body
+        className={cn(inter.className, "min-h-screen bg-background antialiased", isNetworkMap && "overflow-hidden")}
+      >
+        <Providers>{children}</Providers>
+        <Toaster />
+      </body>
+    </html>
+  )
+}
diff --git a/src/netmanager-app/app/error.tsx b/src/netmanager-app/app/error.tsx
new file mode 100644
index 0000000000..bbbfb949d3
--- /dev/null
+++ b/src/netmanager-app/app/error.tsx
@@ -0,0 +1,35 @@
+"use client"
+
+import { useEffect } from "react"
+import { Button } from "@/components/ui/button"
+import { useRouter } from "next/navigation"
+
+export default function Error({
+  error,
+  reset,
+}: {
+  error: Error & { digest?: string }
+  reset: () => void
+}) {
+    const router = useRouter()
+  useEffect(() => {
+    console.error("Application error:", error)
+  }, [error])
+
+  return (
+    <div className="flex min-h-screen flex-col items-center justify-center p-4 text-center">
+      <div className="max-w-md space-y-4">
+        <h2 className="text-2xl font-bold">Something went wrong!</h2>
+        <p className="text-muted-foreground">
+          We apologize for the inconvenience. The application encountered an unexpected error.
+        </p>
+        <div className="flex justify-center gap-4">
+        <Button onClick={() => router.push("/login")} variant="outline">
+            Go to Login
+          </Button>
+          <Button onClick={() => reset()}>Try again</Button>
+        </div>
+      </div>
+    </div>
+  )
+}
diff --git a/src/netmanager-app/favicon.ico b/src/netmanager-app/app/favicon.ico
similarity index 100%
rename from src/netmanager-app/favicon.ico
rename to src/netmanager-app/app/favicon.ico
diff --git a/src/netmanager-app/app/layout.tsx b/src/netmanager-app/app/layout.tsx
index 3ac9e922e4..0e29eb5827 100644
--- a/src/netmanager-app/app/layout.tsx
+++ b/src/netmanager-app/app/layout.tsx
@@ -1,34 +1,49 @@
-"use client";
+import type React from "react"
+import type { Metadata } from "next"
+import "./globals.css"
+import ClientLayout from "./client-layout"
 
-import { Inter } from "next/font/google";
-import "./globals.css";
-import { cn } from "@/lib/utils";
-import Providers from "./providers";
-import { Toaster } from "@/components/ui/sonner";
-import { usePathname } from "next/navigation";
-
-const inter = Inter({ subsets: ["latin"] });
+export const metadata: Metadata = {
+  title: {
+    template: "%s | AirQo Analytics",
+    default: "AirQo Analytics | Africa's Leading Air Quality Monitoring Network",
+  },
+  description:
+    "AirQo is Africa's leading air quality monitoring, research and analytics network. We use low cost technologies and AI to close the gaps in air quality data across the continent.",
+  keywords: ["air quality", "monitoring", "Africa", "analytics", "pollution", "environment", "data"],
+  authors: [{ name: "AirQo Team" }],
+  creator: "AirQo",
+  publisher: "AirQo",
+  openGraph: {
+    title: "AirQo Analytics | Air Quality Monitoring",
+    description: "Africa's leading air quality monitoring network using low-cost technologies and AI.",
+    type: "website",
+    url: "https://airqo.net",
+    images: [
+      {
+        url: "/images/airqo-logo.svg",
+        width: 1200,
+        height: 630,
+        alt: "AirQo Analytics",
+      },
+    ],
+  },
+  twitter: {
+    card: "summary_large_image",
+    title: "AirQo Analytics",
+    description: "Africa's leading air quality monitoring network.",
+    images: ["/favicon.ico"],
+  },
+  icons: {
+    icon: "/favicon.ico",
+    apple: "/favicon.ico",
+  },
+}
 
 export default function RootLayout({
   children,
 }: {
-  children: React.ReactNode;
+  children: React.ReactNode
 }) {
-  const pathname = usePathname();
-  const isNetworkMap = pathname === "/network-map";
-
-  return (
-    <html lang="en" suppressHydrationWarning>
-      <body
-        className={cn(
-          inter.className,
-          "min-h-screen bg-background antialiased",
-          isNetworkMap && "overflow-hidden"
-        )}
-      >
-        <Providers>{children}</Providers>
-        <Toaster />
-      </body>
-    </html>
-  );
+  return <ClientLayout>{children}</ClientLayout>
 }
diff --git a/src/netmanager-app/components/Analytics/PollutantCategory.tsx b/src/netmanager-app/components/Analytics/PollutantCategory.tsx
index dce89597c5..f95ecf6f14 100644
--- a/src/netmanager-app/components/Analytics/PollutantCategory.tsx
+++ b/src/netmanager-app/components/Analytics/PollutantCategory.tsx
@@ -1,6 +1,6 @@
-'use client'
+"use client"
 
-import React, { useState, useRef, useEffect } from 'react'
+import { useState, useRef, useEffect } from "react"
 import { Card, CardContent } from "@/components/ui/card"
 import { Avatar, AvatarFallback } from "@/components/ui/avatar"
 import { cn } from "@/lib/utils"
@@ -22,18 +22,18 @@ export function PollutantCategory({ pm25level, iconClass, sites, devices }: Poll
   const ref = useRef<HTMLDivElement>(null)
 
   const compare = (a: Site, b: Site): number => {
-    if (a.pm2_5 < b.pm2_5) return 1;
-    if (a.pm2_5 > b.pm2_5) return -1;
-    return 0;
-  };
+    if (a.pm2_5 < b.pm2_5) return 1
+    if (a.pm2_5 > b.pm2_5) return -1
+    return 0
+  }
 
   const toggleShow = () => setShow(!show)
 
- let sortedData: Site[] = []
+  let sortedData: Site[] = []
   if (sites) {
-    sortedData = sites.sort(compare);
+    sortedData = sites.sort(compare)
   } else if (devices) {
-    sortedData = devices.sort(compare);
+    sortedData = devices.sort(compare)
   }
 
   useEffect(() => {
@@ -43,69 +43,63 @@ export function PollutantCategory({ pm25level, iconClass, sites, devices }: Poll
       }
     }
 
-    document.addEventListener('mousedown', checkIfClickedOutside)
-    return () => document.removeEventListener('mousedown', checkIfClickedOutside)
+    document.addEventListener("mousedown", checkIfClickedOutside)
+    return () => document.removeEventListener("mousedown", checkIfClickedOutside)
   }, [show])
 
-  
-
   return (
-    <div className="relative w-full" ref={ref}>
-      <Card 
-        className="cursor-pointer transition-all hover:shadow-md w-full" 
-        onClick={toggleShow}
-      >
-        <CardContent className="flex justify-between items-center p-4">
-          <div className="max-w-[60%]">
-            <h3 className="text-sm font-semibold text-gray-500 line-clamp-2">{pm25level}</h3>
+    <div className="relative" ref={ref}>
+      <Card className="cursor-pointer transition-all hover:shadow-md w-full" onClick={toggleShow}>
+        <CardContent className="p-3">
+          <div className="flex justify-between items-center">
+            <div className="max-w-[60%]">
+              <p className="text-xs font-bold text-gray-500 truncate">{pm25level}</p>
+            </div>
+            <Avatar className={cn("h-10 w-10", iconClass)} style={{ backgroundColor: getPm25LevelHexColor(pm25level) }}>
+              <AvatarFallback className="text-sm">{sortedData.length}</AvatarFallback>
+            </Avatar>
           </div>
-          <Avatar className={cn("h-12 w-12 bg", iconClass)}
-            style={{ backgroundColor: getPm25LevelHexColor(pm25level) }}
-          >
-            <AvatarFallback>{sortedData.length}</AvatarFallback>
-          </Avatar>
         </CardContent>
       </Card>
       {show && (
-        <ul className="absolute z-10 w-full mt-2 bg-white border rounded-md shadow-lg max-h-60 overflow-auto">
+        <ul className="absolute z-10 w-full mt-1 bg-white border rounded-md shadow-lg max-h-60 overflow-auto">
           {sortedData.length === 0 ? (
-            <li className="p-2 text-gray-500">No data</li>
+            <li className="p-2 text-gray-500">no data</li>
           ) : (
             sortedData.map((data, index) => (
               <li key={index} className="p-2 hover:bg-gray-100">
-                {data.label} - <span className={`font-semibold text-${getPm25LevelColor(pm25level)}`}>
-                  {data.pm2_5 && !isNaN(data.pm2_5) ? data.pm2_5.toFixed(2) : 'N/A'}
+                {data.label} -{" "}
+                <span className={`font-semibold text-${getPm25LevelColor(pm25level)}`}>
+                  {data.pm2_5 && !isNaN(data.pm2_5) ? data.pm2_5.toFixed(2) : "N/A"}
                 </span>
               </li>
             ))
           )}
         </ul>
       )}
-      {/* <ChevronDown className={cn("absolute right-4 top-1/2 transform -translate-y-1/2 transition-transform", show && "rotate-180")} /> */}
     </div>
   )
 }
 
+const PM25_LEVEL_COLORS = {
+  good: { class: "green-500", hex: "#10B981" },
+  moderate: { class: "yellow-500", hex: "#FBBF24" },
+  uhfsg: { class: "orange-500", hex: "#F97316" },
+  unhealthy: { class: "red-500", hex: "#EF4444" },
+  veryunhealthy: { class: "purple-500", hex: "#8B5CF6" },
+  hazardous: { class: "rose-900", hex: "#9B1C31" },
+  default: { class: "gray-500", hex: "#6B7280" },
+} as const
+
+function getPm25LevelColors(level: string) {
+  const key = level.toLowerCase().replace(/\s+/g, "") as keyof typeof PM25_LEVEL_COLORS
+  return PM25_LEVEL_COLORS[key] || PM25_LEVEL_COLORS.default
+}
 
-const PM25_LEVEL_COLORS = {  
-  good: { class: 'green-500', hex: '#10B981' },  
-  moderate: { class: 'yellow-500', hex: '#FBBF24' },  
-  'unhealthy for sensitive groups': { class: 'orange-500', hex: '#F97316' },  
-  unhealthy: { class: 'red-500', hex: '#EF4444' },  
-  'very unhealthy': { class: 'purple-500', hex: '#8B5CF6' },  
-  hazardous: { class: 'rose-900', hex: '#9B1C31' },  
-  default: { class: 'gray-500', hex: '#6B7280' }  
-} as const  
-
-function getPm25LevelColors(level: string) {  
-  const key = level.toLowerCase() as keyof typeof PM25_LEVEL_COLORS  
-  return PM25_LEVEL_COLORS[key] || PM25_LEVEL_COLORS.default  
-}  
-
-function getPm25LevelColor(level: string): string {  
-  return getPm25LevelColors(level).class  
-}  
+function getPm25LevelColor(level: string): string {
+  return getPm25LevelColors(level).class
+}
 
-function getPm25LevelHexColor(level: string): string {  
-  return getPm25LevelColors(level).hex  
-} 
\ No newline at end of file
+function getPm25LevelHexColor(level: string): string {
+  return getPm25LevelColors(level).hex
+}
diff --git a/src/netmanager-app/components/topbar.tsx b/src/netmanager-app/components/topbar.tsx
index d5051d6149..c08525bfc5 100644
--- a/src/netmanager-app/components/topbar.tsx
+++ b/src/netmanager-app/components/topbar.tsx
@@ -13,11 +13,13 @@ import {
   DropdownMenuSeparator,
   DropdownMenuTrigger,
 } from "@/components/ui/dropdown-menu"
-import { useAuth } from "@/core/hooks/users"
+import { logout as reduxLogout } from "@/core/redux/slices/userSlice";
 import { useAppSelector } from "@/core/redux/hooks"
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
 import { Separator } from "@/components/ui/separator"
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
+import { useDispatch } from "react-redux"
+import { useRouter } from "next/navigation"
 
 interface TopbarProps {
   isMobileView: boolean
@@ -26,9 +28,10 @@ interface TopbarProps {
 
 const Topbar: React.FC<TopbarProps> = ({ isMobileView, toggleSidebar }) => {
   const [darkMode, setDarkMode] = useState(false)
-  const { logout } = useAuth()
   const currentUser = useAppSelector((state) => state.user.userDetails)
   const activeGroup = useAppSelector((state) => state.user.activeGroup)
+  const dispatch = useDispatch();
+  const router = useRouter();
   useEffect(() => {
     if (darkMode) {
       document.documentElement.classList.add("dark")
@@ -41,6 +44,18 @@ const Topbar: React.FC<TopbarProps> = ({ isMobileView, toggleSidebar }) => {
     setDarkMode(!darkMode)
   }
 
+  const handleLogout = () => {
+    localStorage.removeItem("token");
+    localStorage.removeItem("userDetails");
+    localStorage.removeItem("activeNetwork");
+    localStorage.removeItem("availableNetworks");
+    localStorage.removeItem("activeGroup");
+    localStorage.removeItem("userGroups");
+    dispatch(reduxLogout());
+    router.push("/login");
+  };
+  
+
   const apps = [
     {
       name: "Calibrate",
@@ -67,6 +82,7 @@ const Topbar: React.FC<TopbarProps> = ({ isMobileView, toggleSidebar }) => {
       icon: "🔍",
     },
   ]
+  
 
   // Get user initials for avatar fallback
   const getInitials = () => {
@@ -90,69 +106,10 @@ const Topbar: React.FC<TopbarProps> = ({ isMobileView, toggleSidebar }) => {
           </Button>
         )}
 
-        {/* Left side - can add logo or breadcrumbs here */}
         <div className="flex items-center gap-2">
         </div>
 
-        {/* Right side - actions */}
         <div className="flex items-center gap-1 md:gap-2">
-          {/* <TooltipProvider>
-            <Tooltip>
-              <TooltipTrigger asChild>
-                <Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground">
-                  <HelpCircle className="h-5 w-5" />
-                  <span className="sr-only">Help</span>
-                </Button>
-              </TooltipTrigger>
-              <TooltipContent>Help & Resources</TooltipContent>
-            </Tooltip>
-          </TooltipProvider> */}
-
-          {/* <DropdownMenu>
-            <TooltipProvider>
-              <Tooltip>
-                <TooltipTrigger asChild>
-                  <DropdownMenuTrigger asChild>
-                    <Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground">
-                      <Bell className="h-5 w-5" />
-                      <span className="sr-only">Notifications</span>
-                      <Badge className="absolute -right-0.5 -top-0.5 h-4 w-4 rounded-full p-0 text-[10px]">3</Badge>
-                    </Button>
-                  </DropdownMenuTrigger>
-                </TooltipTrigger>
-                <TooltipContent>Notifications</TooltipContent>
-              </Tooltip>
-            </TooltipProvider>
-            <DropdownMenuContent align="end" className="w-80">
-              <DropdownMenuLabel className="flex items-center justify-between">
-                <span>Notifications</span>
-                <Button variant="ghost" size="sm" className="h-auto p-0 text-xs font-normal text-primary">
-                  Mark all as read
-                </Button>
-              </DropdownMenuLabel>
-              <DropdownMenuSeparator />
-              <div className="max-h-80 overflow-y-auto">
-                {[1, 2, 3].map((i) => (
-                  <DropdownMenuItem key={i} className="flex flex-col items-start p-3">
-                    <div className="flex w-full items-center gap-2">
-                      <div className="flex-shrink-0">
-                        <Badge variant="outline" className="h-2 w-2 rounded-full bg-blue-500 p-0" />
-                      </div>
-                      <div className="flex-1 text-sm font-medium">Device status update</div>
-                      <div className="text-xs text-muted-foreground">2h ago</div>
-                    </div>
-                    <div className="mt-1 text-xs text-muted-foreground">
-                      Device AQ-045 is now online and transmitting data.
-                    </div>
-                  </DropdownMenuItem>
-                ))}
-              </div>
-              <DropdownMenuSeparator />
-              <DropdownMenuItem asChild className="justify-center text-center text-sm font-medium text-primary">
-                <Link href="/notifications">View all notifications</Link>
-              </DropdownMenuItem>
-            </DropdownMenuContent>
-          </DropdownMenu> */}
 
           <DropdownMenu>
             <TooltipProvider>
@@ -241,7 +198,7 @@ const Topbar: React.FC<TopbarProps> = ({ isMobileView, toggleSidebar }) => {
               <DropdownMenuSeparator />
               <DropdownMenuItem
                 className="flex items-center gap-2 text-destructive focus:text-destructive"
-                onClick={logout}
+                onClick={handleLogout}
               >
                 <LogOut className="h-4 w-4" />
                 <span>Log out</span>


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/netmanager-app/components/topbar.tsx (1)</summary><blockquote>

`47-56`: **Refactor localStorage cleanup for better maintainability**

The current implementation removes multiple localStorage items individually, which is verbose and prone to maintenance issues if new storage keys are added in the future.


Consider using a more scalable approach for localStorage cleanup:

```diff
const handleLogout = () => {
-  localStorage.removeItem("token");
-  localStorage.removeItem("userDetails");
-  localStorage.removeItem("activeNetwork");
-  localStorage.removeItem("availableNetworks");
-  localStorage.removeItem("activeGroup");
-  localStorage.removeItem("userGroups");
+  const keysToRemove = [
+    "token", 
+    "userDetails", 
+    "activeNetwork", 
+    "availableNetworks", 
+    "activeGroup", 
+    "userGroups"
+  ];
+  keysToRemove.forEach(key => localStorage.removeItem(key));
  dispatch(reduxLogout());
  router.push("/login");
};
```

This approach makes it easier to maintain the list of keys and add new ones in the future.

</blockquote></details>
<details>
<summary>src/netmanager-app/app/error.tsx (1)</summary><blockquote>

`13-15`: **Consider adding error tracking beyond console logging**

Currently, errors are only logged to the console, which may not be sufficient for monitoring in production.


Consider integrating with an error tracking service to capture and analyze errors in production:

```diff
useEffect(() => {
  console.error("Application error:", error)
+  // Example integration with an error tracking service
+  if (process.env.NODE_ENV === 'production') {
+    // errorTrackingService.captureException(error);
+  }
}, [error])
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8e971317e2eab4b6717823ede4050e0feb9e41c5 and f4d674c81eb9c88bb18e974a877b59a7dff2d5f3.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `src/netmanager-app/app/favicon.ico` is excluded by `!**/*.ico`

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `src/netmanager-app/app/client-layout.tsx` (1 hunks)
* `src/netmanager-app/app/error.tsx` (1 hunks)
* `src/netmanager-app/app/layout.tsx` (1 hunks)
* `src/netmanager-app/components/Analytics/PollutantCategory.tsx` (3 hunks)
* `src/netmanager-app/components/topbar.tsx` (5 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>src/netmanager-app/app/client-layout.tsx (2)</summary><blockquote>

<details>
<summary>src/platform/src/common/components/GoogleAnalytics.jsx (1)</summary>

* `pathname` (9-9)

</details>
<details>
<summary>src/netmanager-app/lib/utils.ts (1)</summary>

* `cn` (5-7)

</details>

</blockquote></details>
<details>
<summary>src/netmanager-app/app/layout.tsx (1)</summary><blockquote>

<details>
<summary>src/netmanager-app/app/client-layout.tsx (1)</summary>

* `ClientLayout` (13-31)

</details>

</blockquote></details>
<details>
<summary>src/netmanager-app/components/Analytics/PollutantCategory.tsx (3)</summary><blockquote>

<details>
<summary>src/netmanager-app/app/types/sites.ts (1)</summary>

* `Site` (1-116)

</details>
<details>
<summary>src/platform/src/common/components/CardWrapper/index.jsx (1)</summary>

* `Card` (9-165)

</details>
<details>
<summary>src/netmanager-app/lib/utils.ts (1)</summary>

* `cn` (5-7)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/netmanager-app/app/error.tsx</summary>

[error] 6-6: Do not shadow the global "Error" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build-push-deploy-netmanager-app-preview

</details>

<details>
<summary>🔇 Additional comments (15)</summary><blockquote>

<details>
<summary>src/netmanager-app/components/topbar.tsx (1)</summary>

`201-201`: **Good refactoring of the logout handler**

The logout functionality now properly synchronizes both local storage state and Redux state, while also handling navigation. This is a good improvement from the previous implementation.

</details>
<details>
<summary>src/netmanager-app/app/layout.tsx (2)</summary>

`6-41`: **Comprehensive metadata implementation**

The metadata configuration is very thorough and follows best practices for SEO and social sharing. This is a good improvement.



One small note: Verify that the image paths (like "/favicon.ico" and "/images/airqo-logo.svg") are correct and accessible from your production domain. Absolute URLs might be preferable for social media cards.

---

`48-48`: **Good separation of server and client components**

Converting the root layout to a server component and delegating client-side rendering to ClientLayout follows Next.js best practices for performance optimization.

</details>
<details>
<summary>src/netmanager-app/components/Analytics/PollutantCategory.tsx (12)</summary>

`3-3`: **Cleaner import statement improves code quality.**

The import has been optimized by directly importing only the specific hooks that are used (`useState`, `useRef`, `useEffect`) rather than the entire React library. This is good practice for modern React development and potentially helps with tree-shaking.

---

`24-28`: **Improved code formatting for the comparison function.**

The comparison function was reformatted while maintaining the same sorting logic (descending order by PM2.5 values). The cleaner formatting with consistent spacing improves readability.

---

`32-32`: **Added explicit type annotation for better type safety.**

The `sortedData` variable now has an explicit `Site[]` type annotation, which strengthens type safety and makes the code's intent clearer.

---

`34-37`: **Simplified sorting logic with consistent style.**

The sorting logic has been simplified while maintaining the same functionality, which makes the code more readable and maintainable.

---

`46-47`: **Standardized event listener attachment using double quotes.**

Event listener attachment has been standardized to use double quotes for consistency with the rest of the codebase.

---

`51-63`: **Improved card layout with better spacing and responsiveness.**

The card component structure has been improved with:
1. Better class organization (moved `w-full` to the Card)
2. Reduced padding for a more compact UI
3. Flex layout for better alignment of content
4. Width constraint on text to prevent overflow
5. Smaller avatar size for better proportions

These UI improvements create a more polished and space-efficient component.

---

`65-65`: **Reduced dropdown margin for tighter UI.**

The margin-top of the dropdown has been reduced from `mt-2` to `mt-1`, creating a tighter and more cohesive UI when the dropdown is visible.

---

`67-67`: **Normalized "no data" text case for consistency.**

The "No data" text has been changed to lowercase "no data" for consistency with the application's text style guidelines.

---

`71-74`: **Improved data display with better formatting and error handling.**

The list item content has been improved with:
1. Better formatting with a dash separator and proper spacing
2. Added validation to check for NaN values in PM2.5 data
3. Displaying "N/A" for invalid numeric data

This enhances the user experience by handling edge cases gracefully.

---

`84-92`: **Improved PM2.5 level color mapping with normalized keys.**

The color mapping object has been improved with:
1. Consistent key naming (all lowercase, no spaces)
2. TypeScript `as const` assertion for better type safety
3. Clear organization of both class names and hex color values

This change makes the color system more robust and maintains better consistency.

---

`94-97`: **Added helper function to normalize and retrieve PM2.5 level colors.**

The new `getPm25LevelColors` utility function is a good improvement that:
1. Normalizes input strings (lowercase and space removal)
2. Provides consistent access to color information
3. Includes a fallback to default colors for unrecognized levels

This reduces code duplication and handles edge cases more gracefully.

---

`99-105`: **Refactored color utility functions for better consistency.**

The color utility functions have been refactored to use the new helper function, which:
1. Improves code organization and maintainability
2. Ensures consistent behavior across all color operations
3. Centralizes the string normalization logic

This refactoring improves the overall code quality.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T08:08:28Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>src/netmanager-app/app/error.tsx (1)</summary><blockquote>

`7-13`: **Rename the Error component to avoid shadowing the global Error object**

The component name 'Error' shadows the global JavaScript Error constructor, which could lead to confusion and potential issues during debugging or when extending the codebase.

Consider renaming to something more specific like `ApplicationError` or `ErrorBoundary`:

```diff
-export default function Error({
+export default function ApplicationError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 7-7: Do not shadow the global "Error" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/netmanager-app/app/error.tsx (2)</summary><blockquote>

`14-14`: **Fix indentation inconsistency**

There's an indentation inconsistency on this line compared to the surrounding code.

```diff
-    const router = useRouter()
+  const router = useRouter()
```

---

`15-17`: **Consider enhancing error logging for production**

The current implementation logs errors to the console, which is great for development but may not be sufficient for production environments.

Consider integrating with an error monitoring service for production:

```diff
useEffect(() => {
  console.error("Application error:", error)
+  // For production environments
+  if (process.env.NODE_ENV === 'production') {
+    // Send to error monitoring service
+    // Example: errorMonitoringService.captureError(error)
+  }
}, [error])
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f4d674c81eb9c88bb18e974a877b59a7dff2d5f3 and 0cbdb885c8bba8cc0a18ae6d173612a5dc166526.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/netmanager-app/app/error.tsx` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/netmanager-app/app/error.tsx</summary>

[error] 7-7: Do not shadow the global "Error" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build-push-deploy-netmanager-app-preview

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>src/netmanager-app/app/error.tsx (1)</summary>

`19-35`: **Well-structured error UI**

The error UI is well-designed with clear messaging and appropriate recovery options. The use of the Next.js router for navigation is correct and maintains the application state.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T08:14:45Z
user: login: Baalmart
body: Thanks @danielmarv 

cc: @Codebmk 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-19T08:19:13Z

Review comments:
diff_hunk: @@ -0,0 +1,31 @@
+"use client"
+
+import type React from "react"
+
+import { usePathname } from "next/navigation"
+import { Toaster } from "@/components/ui/sonner"
+import Providers from "./providers"
+import { cn } from "@/lib/utils"
+import { Inter } from "next/font/google"
+
+const inter = Inter({ subsets: ["latin"] })
+
+export default function ClientLayout({
+  children,
+}: {
+  children: React.ReactNode
+}) {
+  const pathname = usePathname()
+  const isNetworkMap = pathname === "/network-map"
+
+  return (
+    <html lang="en" suppressHydrationWarning>
+      <body
+        className={cn(inter.className, "min-h-screen bg-background antialiased", isNetworkMap && "overflow-hidden")}
+      >
+        <Providers>{children}</Providers>
+        <Toaster />
+      </body>
+    </html>
+  )
+}
path: src/netmanager-app/app/client-layout.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Potential HTML structure conflict with root layout**

You've created a new client-side layout component that defines its own `<html>` and `<body>` tags. This could lead to issues as Next.js already manages these elements in the root layout. When a client component renders these root HTML elements, it can create nested HTML structures or hydration mismatches.


Consider refactoring to avoid rendering duplicate HTML/body tags:

```diff
export default function ClientLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const pathname = usePathname()
  const isNetworkMap = pathname === "/network-map"

  return (
-    <html lang="en" suppressHydrationWarning>
-      <body
-        className={cn(inter.className, "min-h-screen bg-background antialiased", isNetworkMap && "overflow-hidden")}
-      >
+    <div className={cn(inter.className, "min-h-screen bg-background antialiased", isNetworkMap && "overflow-hidden")}>
        <Providers>{children}</Providers>
        <Toaster />
-      </body>
-    </html>
+    </div>
  )
}
```

I'm seeing the `suppressHydrationWarning` attribute, which suggests there might be hydration mismatches. This is often a symptom of rendering different content on server vs. client.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
"use client"

import type React from "react"

import { usePathname } from "next/navigation"
import { Toaster } from "@/components/ui/sonner"
import Providers from "./providers"
import { cn } from "@/lib/utils"
import { Inter } from "next/font/google"

const inter = Inter({ subsets: ["latin"] })

export default function ClientLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const pathname = usePathname()
  const isNetworkMap = pathname === "/network-map"

  return (
    <div
      className={cn(
        inter.className,
        "min-h-screen bg-background antialiased",
        isNetworkMap && "overflow-hidden"
      )}
    >
      <Providers>{children}</Providers>
      <Toaster />
    </div>
  )
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-19T08:08:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+"use client"
+
+import { useEffect } from "react"
+import { Button } from "@/components/ui/button"
+
+export default function Error({
+  error,
+  reset,
+}: {
+  error: Error & { digest?: string }
+  reset: () => void
+}) {
+  useEffect(() => {
+    console.error("Application error:", error)
+  }, [error])
+
+  return (
+    <div className="flex min-h-screen flex-col items-center justify-center p-4 text-center">
+      <div className="max-w-md space-y-4">
+        <h2 className="text-2xl font-bold">Something went wrong!</h2>
+        <p className="text-muted-foreground">
+          We apologize for the inconvenience. The application encountered an unexpected error.
+        </p>
+        <div className="flex justify-center gap-4">
+          <Button onClick={() => (window.location.href = "/login")} variant="outline">
+            Go to Login
+          </Button>
path: src/netmanager-app/app/error.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Use Next.js router for navigation instead of direct location change**

Using `window.location.href` bypasses the Next.js router, potentially causing a full page reload and lost application state.


Replace with the Next.js router:

```diff
-<Button onClick={() => (window.location.href = "/login")} variant="outline">
+<Button onClick={() => router.push("/login")} variant="outline">
  Go to Login
</Button>
```

You'll need to add the router import and hook at the top of the component:

```javascript
import { useRouter } from "next/navigation"
// Inside component:
const router = useRouter()
```

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 0cbdb88
created_at: 2025-04-19T08:08:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+"use client"
+
+import { useEffect } from "react"
+import { Button } from "@/components/ui/button"
+
+export default function Error({
+  error,
+  reset,
+}: {
+  error: Error & { digest?: string }
+  reset: () => void
+}) {
path: src/netmanager-app/app/error.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Rename Error component to avoid shadowing global Error object**

The current component name shadows the global Error constructor, which could lead to confusion and potential issues.


Rename the component to something more specific:

```diff
-export default function Error({
+export default function ApplicationError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
```

This aligns with the static analysis hint about not shadowing the global Error property.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
export default function ApplicationError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  // …rest of component…
}
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 6-6: Do not shadow the global "Error" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-19T08:08:27Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-19T08:05:43Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

This update introduces a new `ClientLayout` component to handle client-side layout logic, including conditional body styling and toast notifications. The main `layout.tsx` is refactored to a server component, now focusing on exporting comprehensive site metadata and delegating layout rendering to `ClientLayout`. An error boundary component (`Error`) is added for user-friendly error handling and recovery. The `Topbar` component's logout logic is revised to explicitly clear user data from local storage and redirect to the login page. Minor stylistic and normalization improvements are applied to the `PollutantCategory` component.

## Changes

| File(s)                                                                                     | Change Summary                                                                                                                                                                                                                                                                                                                                                           |
|---------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `src/netmanager-app/app/client-layout.tsx`                                                  | New `ClientLayout` component introduced; handles conditional body class based on route, wraps children with providers, and includes toast notifications.                                                                                                                                                                                                                |
| `src/netmanager-app/app/error.tsx`                                                          | New `Error` boundary component added to display errors and provide options to retry or return to login. Logs errors to the console and offers user-friendly recovery UI.                                                                                                                                                                                                |
| `src/netmanager-app/app/layout.tsx`                                                         | Refactored from a client to a server component; now exports detailed metadata and renders `ClientLayout` as the root layout. Removes previous body/html logic and centralizes metadata configuration.                                                                                                                             |
| `src/netmanager-app/components/Analytics/PollutantCategory.tsx`                             | Refactored for stylistic consistency; normalizes PM2.5 color mapping keys, adjusts class names and layout, and updates dropdown and avatar styles. No changes to exported/public API.                                                                                                                                            |
| `src/netmanager-app/components/topbar.tsx`                                                  | Logout logic refactored: now explicitly clears local storage keys, dispatches Redux logout, and redirects to login. Removes custom hook and cleans up commented-out UI code.                                                                                                               |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant Topbar
    participant LocalStorage
    participant Redux
    participant Router

    User->>Topbar: Clicks Logout
    Topbar->>LocalStorage: Remove user/session keys
    Topbar->>Redux: Dispatch logout action
    Topbar->>Router: Redirect to /login
```

```mermaid
sequenceDiagram
    participant App
    participant ClientLayout
    participant Providers
    participant Toaster

    App->>ClientLayout: Render with children
    ClientLayout->>Providers: Wrap children
    ClientLayout->>Toaster: Render notifications
    ClientLayout->>App: Render children with layout and notifications
```

```mermaid
sequenceDiagram
    participant App
    participant ErrorBoundary
    participant User

    App->>ErrorBoundary: Error occurs
    ErrorBoundary->>User: Show error UI with retry and login options
    User->>ErrorBoundary: Click retry
    ErrorBoundary->>App: Call reset callback
    User->>ErrorBoundary: Click login
    ErrorBoundary->>Browser: Redirect to /login
```

## Suggested labels

`ready for review`, `netmanager`, `netmanager-app`

## Suggested reviewers

- Baalmart  
- Codebmk

## Poem

> In the land of layouts, a client takes the stage,  
> With metadata shining on the server’s page.  
> Errors now handled with a gentle, guiding hand,  
> While logout sweeps the traces clean as planned.  
> Pollutant colors normalized, dropdowns now neat—  
> Code’s a little sharper, and scrolling’s got new feet!  
> Cheers to the changes, both subtle and grand! 🚀

</details>

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡💬 Agentic Chat (Pro Plan, General Availability)</summary>
> 
> - We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
> 
> </details>

<!-- announcements_end -->
<!-- This is an auto-generated comment: resource warnings by coderabbit.ai -->

> [!WARNING]
> ## Review ran into problems
> 
> <details>
> <summary>🔥 Problems</summary>
> 
> Errors were encountered while retrieving linked issues.
> 
> <details>
> <summary>Errors (1)</summary>
> 
> * JIRA integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.
> 
> </details>
> 
> </details>

<!-- end of auto-generated comment: resource warnings by coderabbit.ai -->
<!-- internal state start -->


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

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=airqo-platform/AirQo-frontend&utm_content=2639):

- 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](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@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](https://docs.coderabbit.ai/guides/configure-coderabbit) 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](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-19T08:09:07Z
body: New netmanager-app changes available for preview [here](https://net-app-analytics-netmanager-app-preview-w7kzhvlewq-ew.a.run.app)
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-19T08:16:47Z
body: New netmanager-app changes available for preview [here](https://net-app-analytics-netmanager-app-preview-w7kzhvlewq-ew.a.run.app)
author_association: CONTRIBUTOR