-
-
-
-
-
- {errors.currentPassword && (
-
- {errors.currentPassword}
-
- )}
-
-
-
-
-
-
-
- {errors.newPassword && (
-
- {errors.newPassword}
-
- )}
-
-
- Password strength: {calculatePasswordStrength(passwords.newPassword)}%
-
-
-
-
-
-
-
-
- {errors.confirmNewPassword && (
-
- {errors.confirmNewPassword}
-
- )}
+
+
+
+
+
Password Security
-
-
+
+ Keeping your account secure is our top priority. Regularly updating your password helps protect your personal
+ information.
+
+
+ -
+
+ Use a unique password for each account
+
+ -
+
+ Avoid using personal information in your password
+
+ -
+
+ Consider using a password manager for added security
+
+
+
+
+
+
+
+ Change Password
+
+
+
+
+
+
+
+
+
)
}
diff --git a/netmanager-app/components/desktop-sidebar.tsx b/netmanager-app/components/desktop-sidebar.tsx
new file mode 100644
index 0000000000..af97c2b954
--- /dev/null
+++ b/netmanager-app/components/desktop-sidebar.tsx
@@ -0,0 +1,480 @@
+import type React from "react";
+import Link from "next/link";
+import {
+ BarChart2,
+ Users,
+ Shield,
+ Radio,
+ MapPin,
+ Layers,
+ Grid,
+ Building2,
+ Activity,
+ UserCircle,
+ Download,
+ MapIcon,
+ ChevronRight,
+ Check,
+ PlusCircle,
+ MonitorSmartphone,
+ LogOut,
+ NetworkIcon,
+ ChevronLeft,
+} from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { motion } from "framer-motion";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import type { Group, Network } from "@/app/types/users";
+import { PermissionGuard } from "@/components/permission-guard";
+import { Card, CardContent } from "@/components/ui/card";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import Image from "next/image";
+
+const formatTitle = (title: string) => {
+ return title
+ .replace(/[_-]/g, " ")
+ .replace(/\b\w/g, (char) => char.toUpperCase());
+};
+
+interface DesktopSidebarProps {
+ isSidebarCollapsed: boolean;
+ toggleSidebar: () => void;
+ isDevicesOpen: boolean;
+ handleDevicesToggle: (open: boolean) => void;
+ activeGroup: Group | null;
+ userGroups: Group[];
+ handleOrganizationChange: (group: Group) => void;
+ activeNetwork: Network | null;
+ availableNetworks: Network[];
+ handleNetworkChange: (network: Network) => void;
+ isActive: (path: string) => boolean;
+ logout: () => void;
+ className?: string;
+}
+
+const DesktopSidebar: React.FC
= ({
+ isSidebarCollapsed,
+ toggleSidebar,
+ isDevicesOpen,
+ handleDevicesToggle,
+ activeGroup,
+ userGroups,
+ handleOrganizationChange,
+ activeNetwork,
+ availableNetworks,
+ handleNetworkChange,
+ isActive,
+ logout,
+}) => {
+ const NavItem = ({
+ href,
+ icon: Icon,
+ label,
+ }: {
+ href: string;
+ icon: React.ElementType;
+ label: string;
+ }) => (
+
+
+
+
+
+
+ {label}
+
+
+
+
+ {label}
+
+
+
+ );
+
+ return (
+
+ {/* Sidebar Header */}
+
+ {/* Logo */}
+
+
+
+
+ {/* Sidebar Toggle Button */}
+
+
+
+
+
+
+ {isSidebarCollapsed ? "Open Sidebar" : "Close Sidebar"}
+
+
+
+
+
+ {/* Sidebar Content */}
+
+ {/* Organization Switcher */}
+
+
+ Organization
+
+
+
+
+
+ Switch Organization
+
+ {userGroups.map((group) => (
+ handleOrganizationChange(group)}
+ className="flex items-center justify-between uppercase"
+ >
+ {formatTitle(group.grp_title || "")}
+ {activeGroup?._id === group._id && }
+
+ ))}
+
+
+
+
+
+ {/* Main Navigation */}
+
+
+
+ {/* Sidebar Footer */}
+
+
+
+
+
+
+
+ Logout
+
+
+
+
+
+ );
+};
+
+export default DesktopSidebar;
diff --git a/netmanager-app/components/layout.tsx b/netmanager-app/components/layout.tsx
index dd3f53a41c..7187142a1a 100644
--- a/netmanager-app/components/layout.tsx
+++ b/netmanager-app/components/layout.tsx
@@ -1,27 +1,48 @@
-"use client";
-import React, { useEffect, useState } from "react";
+"use client"; // Ensures this runs on the client
+
+import { useEffect, useState } from "react";
import Sidebar from "./sidebar";
import Topbar from "./topbar";
-import { LayoutProps } from "../app/types/layout";
+import type { LayoutProps } from "../app/types/layout";
export default function Layout({ children }: LayoutProps) {
- const [darkMode, setDarkMode] = useState(false);
-
- useEffect(() => {
- const isDarkMode = localStorage.getItem("darkMode") === "true";
- setDarkMode(isDarkMode);
- document.documentElement.classList.toggle("dark", isDarkMode);
- }, []);
-
- return (
-
- );
+ const [darkMode, setDarkMode] = useState(false);
+ const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
+ const [isMobileView, setIsMobileView] = useState(false);
+
+ useEffect(() => {
+ const isDarkMode = localStorage.getItem("darkMode") === "true";
+ setDarkMode(isDarkMode);
+ document.documentElement.classList.toggle("dark", isDarkMode);
+
+ const handleResize = () => {
+ setIsMobileView(window.innerWidth < 768);
+ setIsSidebarCollapsed(window.innerWidth < 1024);
+ };
+
+ handleResize();
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ const toggleSidebar = () => {
+ setIsSidebarCollapsed((prev) => !prev);
+ };
+
+ return (
+
+ );
}
diff --git a/netmanager-app/components/mobile-sidebar.tsx b/netmanager-app/components/mobile-sidebar.tsx
new file mode 100644
index 0000000000..bf923d234c
--- /dev/null
+++ b/netmanager-app/components/mobile-sidebar.tsx
@@ -0,0 +1,410 @@
+import type React from "react";
+import Link from "next/link";
+import {
+ BarChart2,
+ Users,
+ Shield,
+ Radio,
+ MapPin,
+ Layers,
+ Grid,
+ Building2,
+ Activity,
+ UserCircle,
+ Download,
+ MapIcon,
+ ChevronRight,
+ Check,
+ PlusCircle,
+ MonitorSmartphone,
+ LogOut,
+ NetworkIcon,
+ ChevronLeft,
+} from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { motion } from "framer-motion";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import type { Group, Network } from "@/app/types/users";
+import { PermissionGuard } from "@/components/permission-guard";
+import { Card, CardContent } from "@/components/ui/card";
+import Image from "next/image";
+
+const formatTitle = (title: string) => {
+ return title
+ .replace(/[_-]/g, " ")
+ .replace(/\b\w/g, (char) => char.toUpperCase());
+};
+
+interface MobileSidebarProps {
+ isMobileMenuOpen: boolean;
+ toggleMobileMenu: () => void;
+ isDevicesOpen: boolean;
+ handleDevicesToggle: (open: boolean) => void;
+ activeGroup: Group | null;
+ userGroups: Group[];
+ handleOrganizationChange: (group: Group) => void;
+ activeNetwork: Network | null;
+ availableNetworks: Network[];
+ handleNetworkChange: (network: Network) => void;
+ isActive: (path: string) => boolean;
+ logout: () => void;
+ className?: string;
+}
+
+const MobileSidebar: React.FC = ({
+ isMobileMenuOpen,
+ toggleMobileMenu,
+ isDevicesOpen,
+ handleDevicesToggle,
+ activeGroup,
+ userGroups,
+ handleOrganizationChange,
+ activeNetwork,
+ availableNetworks,
+ handleNetworkChange,
+ isActive,
+ logout,
+ className,
+}) => {
+ const NavItem = ({
+ href,
+ icon: Icon,
+ label,
+ }: {
+ href: string;
+ icon: React.ElementType;
+ label: string;
+ }) => (
+
+
+ {label}
+
+ );
+
+ return (
+ <>
+ {/* Sidebar Overlay (Backdrop) */}
+ {isMobileMenuOpen && (
+
+ )}
+
+ {/* Sidebar */}
+
+ {/* Close Button */}
+
+
+ {/* Sidebar Content */}
+
+ {/* Organization Switcher */}
+
+
+ Organization
+
+
+
+
+
+ Switch Organization
+
+ {userGroups.map((group) => (
+ handleOrganizationChange(group)}
+ className="flex items-center justify-between uppercase"
+ >
+ {formatTitle(group.grp_title || "")}
+ {activeGroup?._id === group._id && }
+
+ ))}
+
+
+
+
+
+ {/* Main Navigation */}
+
+
+
+ {/* Sidebar Footer */}
+
+
+
+
+ >
+ );
+};
+
+export default MobileSidebar;
diff --git a/netmanager-app/components/sidebar.tsx b/netmanager-app/components/sidebar.tsx
index c419bf1987..05ea0bd8bc 100644
--- a/netmanager-app/components/sidebar.tsx
+++ b/netmanager-app/components/sidebar.tsx
@@ -1,461 +1,117 @@
"use client";
-import React, { useState, useEffect } from "react";
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-import {
- BarChart2,
- Users,
- Shield,
- Radio,
- MapPin,
- Layers,
- Grid,
- Building2,
- Activity,
- UserCircle,
- Download,
- Map,
- ChevronDown,
- Check,
- PlusCircle,
- MonitorSmartphone,
- LogOut,
- Network as NetworkIcon,
-} from "lucide-react";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import {
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from "@/components/ui/collapsible";
-import { useAuth } from "@/core/hooks/users";
-import { useAppSelector, useAppDispatch } from "@/core/redux/hooks";
-import {
- setActiveNetwork,
- setActiveGroup,
-} from "@/core/redux/slices/userSlice";
-import type { Group, Network } from "@/app/types/users";
-import { PermissionGuard } from "@/components/permission-guard";
-import { Card, CardContent } from "@/components/ui/card";
-
-const Sidebar = () => {
- const pathname = usePathname();
- const [userCollapsed, setUserCollapsed] = useState(false);
- const [isDevicesOpen, setIsDevicesOpen] = useState(false);
- const { logout } = useAuth();
- const dispatch = useAppDispatch();
-
- // Get networks and active network from Redux
- const availableNetworks = useAppSelector(
- (state) => state.user.availableNetworks
- );
- const activeNetwork = useAppSelector((state) => state.user.activeNetwork);
- const activeGroup = useAppSelector((state) => state.user.activeGroup);
- const userGroups = useAppSelector((state) => state.user.userGroups);
-
- const isActive = (path: string) => pathname?.startsWith(path);
- const isDevicesActive = isActive("/devices");
+import type React from "react"
+import { useState, useEffect } from "react"
+import { usePathname } from "next/navigation"
+import { Menu } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { useAuth } from "@/core/hooks/users"
+import { useAppSelector, useAppDispatch } from "@/core/redux/hooks"
+import { setActiveNetwork, setActiveGroup } from "@/core/redux/slices/userSlice"
+import type { Group, Network } from "@/app/types/users"
+import DesktopSidebar from "./desktop-sidebar"
+import MobileSidebar from "./mobile-sidebar"
+
+interface AppSidebarProps {
+ isSidebarCollapsed: boolean
+ toggleSidebar: () => void
+}
+
+const Sidebar: React.FC = ({ isSidebarCollapsed, toggleSidebar }) => {
+ const pathname = usePathname()
+ const [userCollapsed, setUserCollapsed] = useState(false)
+ const [isDevicesOpen, setIsDevicesOpen] = useState(false)
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
+ const { logout } = useAuth()
+ const dispatch = useAppDispatch()
+
+ const availableNetworks = useAppSelector((state) => state.user.availableNetworks)
+ const activeNetwork = useAppSelector((state) => state.user.activeNetwork)
+ const activeGroup = useAppSelector((state) => state.user.activeGroup)
+ const userGroups = useAppSelector((state) => state.user.userGroups)
+
+ const isActive = (path: string) => pathname?.startsWith(path)
+ const isDevicesActive = isActive("/devices")
useEffect(() => {
if (isDevicesActive && !userCollapsed) {
- setIsDevicesOpen(true);
+ setIsDevicesOpen(true)
} else if (!isDevicesActive) {
- setUserCollapsed(false);
+ setUserCollapsed(false)
}
- }, [pathname, isDevicesActive, userCollapsed]);
+ }, [isDevicesActive, userCollapsed])
const handleDevicesToggle = (open: boolean) => {
- setIsDevicesOpen(open);
+ setIsDevicesOpen(open)
if (isDevicesActive) {
- setUserCollapsed(!open);
+ setUserCollapsed(!open)
}
- };
+ }
const handleNetworkChange = (network: Network) => {
- dispatch(setActiveNetwork(network));
- localStorage.setItem("activeNetwork", JSON.stringify(network));
- };
+ dispatch(setActiveNetwork(network))
+ localStorage.setItem("activeNetwork", JSON.stringify(network))
+ }
const handleOrganizationChange = (group: Group) => {
- dispatch(setActiveGroup(group));
- localStorage.setItem("activeGroup", JSON.stringify(group));
- };
-
- return (
-
- {/* Organization Switcher */}
-
-
- Organization
-
-
-
-
-
- Switch Organization
-
- {userGroups.map((group) => (
- handleOrganizationChange(group)}
- className="flex items-center justify-between uppercase"
- >
- {group.grp_title}
- {activeGroup?._id === group._id && }
-
- ))}
-
-
-
-
-
- {/* Main Navigation */}
-
-
+ return (
+ <>
+
+
+
+
- {/* Logout Section */}
-
-
+
+
-
- );
-};
-export default Sidebar;
+ >
+ )
+}
+
+export default Sidebar
+
diff --git a/netmanager-app/components/topbar.tsx b/netmanager-app/components/topbar.tsx
index 46f021e601..8acf37b3fd 100644
--- a/netmanager-app/components/topbar.tsx
+++ b/netmanager-app/components/topbar.tsx
@@ -1,16 +1,10 @@
-"use client";
+"use client"
-import React, { useState, useEffect } from "react";
-import Link from "next/link";
-import {
- UserCircle,
- LogOut,
- GridIcon,
- ExternalLink,
- Moon,
- Sun,
-} from "lucide-react";
-import { Button } from "@/components/ui/button";
+import type React from "react"
+import { useState, useEffect } from "react"
+import Link from "next/link"
+import { UserCircle, LogOut, GridIcon, ExternalLink, Moon, Sun, Menu } from "lucide-react"
+import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
@@ -18,24 +12,28 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { useAuth } from "@/core/hooks/users";
+} from "@/components/ui/dropdown-menu"
+import { useAuth } from "@/core/hooks/users"
+
+interface TopbarProps {
+ isMobileView: boolean
+}
-const Topbar = () => {
- const [darkMode, setDarkMode] = useState(false);
- const { logout } = useAuth();
+const Topbar: React.FC
= ({ isMobileView }) => {
+ const [darkMode, setDarkMode] = useState(false)
+ const { logout } = useAuth()
useEffect(() => {
if (darkMode) {
- document.documentElement.classList.add("dark");
+ document.documentElement.classList.add("dark")
} else {
- document.documentElement.classList.remove("dark");
+ document.documentElement.classList.remove("dark")
}
- }, [darkMode]);
+ }, [darkMode])
const toggleDarkMode = () => {
- setDarkMode(!darkMode);
- };
+ setDarkMode(!darkMode)
+ }
const apps = [
{
@@ -49,77 +47,70 @@ const Topbar = () => {
url: "/analytics",
description: "Advanced Analytics Platform",
},
- ];
+ ]
return (
-
- {/* Left side - can be used for breadcrumbs or other navigation */}
-
-
- {/* Right side - Apps and Profile */}
-
- {/* Apps Navigation */}
-
-
-
- );
-};
+ )
+}
+
+export default Topbar
-export default Topbar;
diff --git a/netmanager-app/components/ui/switch.tsx b/netmanager-app/components/ui/switch.tsx
new file mode 100644
index 0000000000..bc69cf2dbf
--- /dev/null
+++ b/netmanager-app/components/ui/switch.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+const Switch = React.forwardRef<
+ React.ElementRef
,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/netmanager-app/core/apis/organizations.ts b/netmanager-app/core/apis/organizations.ts
new file mode 100644
index 0000000000..39afe8fcb3
--- /dev/null
+++ b/netmanager-app/core/apis/organizations.ts
@@ -0,0 +1,120 @@
+import createAxiosInstance from "./axiosConfig";
+import { USERS_MGT_URL, DEVICES_MGT_URL } from "../urls";
+import { AxiosError } from "axios";
+import { Group } from "@/app/types/groups";
+
+const axiosInstance = createAxiosInstance();
+
+interface ErrorResponse {
+ message: string;
+}
+
+export const groupsApi = {
+ getGroupsApi: async () => {
+ try {
+ const response = await axiosInstance.get(`${USERS_MGT_URL}/groups/summary`);
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(axiosError.response?.data?.message || "Failed to fetch groups summary");
+ }
+ },
+
+ getGroupDetailsApi: async (groupId: string) => {
+ try {
+ const response = await axiosInstance.get(`${USERS_MGT_URL}/groups/${groupId}`);
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(axiosError.response?.data?.message || "Failed to fetch group details");
+ }
+ },
+
+ updateGroupDetailsApi: async (groupId: string, data: Partial) => {
+ try {
+ const response = await axiosInstance.put(`${USERS_MGT_URL}/groups/${groupId}`, data);
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(axiosError.response?.data?.message || "Failed to update group details");
+ }
+ },
+
+ createGroupApi: async (data: Group) => {
+ try {
+ const response = await axiosInstance.post(`${USERS_MGT_URL}/groups`, data);
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(axiosError.response?.data?.message || "Failed to create group");
+ }
+ },
+
+ assignDevicesToGroupApi: async (deviceIds: string[], groups: string[]) => {
+ try {
+ const response = await axiosInstance.put(`${DEVICES_MGT_URL}/bulk`, {
+ deviceIds,
+ updateData: { groups },
+ });
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(axiosError.response?.data?.message || "Failed to assign devices to group");
+ }
+ },
+
+ assignSitesToGroupApi: async (siteIds: string[], groups: string[]) => {
+ try {
+ const response = await axiosInstance.put(`${DEVICES_MGT_URL}/sites/bulk`, {
+ siteIds,
+ updateData: { groups },
+ });
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(axiosError.response?.data?.message || "Failed to assign sites to group");
+ }
+ },
+};
+
+export const groupMembers = {
+ getGroupMembersApi: async (groupId: string) => {
+ try {
+ const response = await axiosInstance.get(`${USERS_MGT_URL}/groups/${groupId}/assigned-users`);
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(axiosError.response?.data?.message || "Failed to fetch group members");
+ }
+ },
+
+ inviteUserToGroupTeam: async (groupId: string, userEmail: string) => {
+ try {
+ const response = await axiosInstance.post(`${USERS_MGT_URL}/requests/emails/groups/${groupId}`, { emails: [userEmail] });
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(axiosError.response?.data?.message || "Failed to send invitation");
+ }
+ },
+
+ acceptGroupTeamInvite: async (groupId: string, userEmail: string) => {
+ try {
+ const response = await axiosInstance.post(`${USERS_MGT_URL}/requests/emails/groups/${groupId}/accept`, { email: userEmail });
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(axiosError.response?.data?.message || "Failed to accept invitation");
+ }
+ },
+
+ updateGroupTeam: async (groupId: string, userEmail: string, role: string) => {
+ try {
+ const response = await axiosInstance.put(`${USERS_MGT_URL}/groups/${groupId}/members/${userEmail}`, { role });
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(axiosError.response?.data?.message || "Failed to update team member");
+ }
+ },
+};
diff --git a/netmanager-app/core/apis/permissions.ts b/netmanager-app/core/apis/permissions.ts
new file mode 100644
index 0000000000..ad7b2b3251
--- /dev/null
+++ b/netmanager-app/core/apis/permissions.ts
@@ -0,0 +1,25 @@
+import createAxiosInstance from "./axiosConfig";
+import { USERS_MGT_URL } from "../urls";
+import { AxiosError } from "axios";
+
+const axiosInstance = createAxiosInstance();
+
+interface ErrorResponse {
+ message: string;
+}
+
+export const permissions = {
+ getPermissionsApi: async () => {
+ try {
+ const response = await axiosInstance.get(
+ `${USERS_MGT_URL}/permissions`
+ );
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(
+ axiosError.response?.data?.message || "Failed to fetch grids summary"
+ );
+ }
+ },
+};
diff --git a/netmanager-app/core/apis/roles.ts b/netmanager-app/core/apis/roles.ts
new file mode 100644
index 0000000000..04292ff25f
--- /dev/null
+++ b/netmanager-app/core/apis/roles.ts
@@ -0,0 +1,80 @@
+import createAxiosInstance from "./axiosConfig";
+import { USERS_MGT_URL } from "../urls";
+import { AxiosError } from "axios";
+import { Role } from "@/app/types/roles";
+
+const axiosInstance = createAxiosInstance();
+
+interface ErrorResponse {
+ message: string;
+}
+
+export const roles = {
+ getRolesApi: async () => {
+ try {
+ const response = await axiosInstance.get(
+ `${USERS_MGT_URL}/roles/summary`
+ );
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(
+ axiosError.response?.data?.message || "Failed to fetch roles summary"
+ );
+ }
+ },
+ getRolesDetailsApi: async (roleId: string) => {
+ try {
+ const response = await axiosInstance.get(
+ `${USERS_MGT_URL}/roles/${roleId}`
+ );
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(
+ axiosError.response?.data?.message || "Failed to fetch role details"
+ );
+ }
+ },
+
+ getOrgRolesApi: async (groupId: string) => {
+ try {
+ const response = await axiosInstance.get(
+ `${USERS_MGT_URL}/groups/${groupId}/roles`
+ );
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(
+ axiosError.response?.data?.message || "Failed to fetch group roles"
+ );
+ }
+ },
+ updateRoleDetailsApi: async (roleId: string, data: Role) => {
+ try {
+ const response = await axiosInstance.put(
+ `${USERS_MGT_URL}/roles/${roleId}`, data
+ );
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(
+ axiosError.response?.data?.message || "Failed to update role details"
+ );
+ }
+ },
+ createRoleApi: async (data: any) => {
+ try {
+ const response = await axiosInstance.post(
+ `${USERS_MGT_URL}/roles`,
+ data
+ );
+ return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ throw new Error(
+ axiosError.response?.data?.message || "Failed to create role"
+ );
+ }
+ },
+};
diff --git a/netmanager-app/core/hooks/useGroups.ts b/netmanager-app/core/hooks/useGroups.ts
new file mode 100644
index 0000000000..4ecfd9f0d0
--- /dev/null
+++ b/netmanager-app/core/hooks/useGroups.ts
@@ -0,0 +1,261 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import type { AxiosError } from "axios";
+import { groupsApi, groupMembers } from "../apis/organizations";
+import type { Group } from "@/app/types/groups";
+import { setError, setGroups } from "../redux/slices/groupsSlice";
+import { setTeamMember } from "../redux/slices/teamSlice";
+import { setGroup } from "../redux/slices/groupDetailsSlice";
+import { useDispatch } from "react-redux";
+
+interface ErrorResponse {
+ message: string;
+}
+
+interface TeamMembersResponse {
+ group_members: Array<{
+ id: string;
+ }>;
+}
+
+export const useGroups = () => {
+ const dispatch = useDispatch();
+
+ const { data, isLoading, error } = useQuery({
+ queryKey: ["groups"],
+ queryFn: () => groupsApi.getGroupsApi(),
+ onSuccess: (data: any) => {
+ dispatch(setGroups(data.groups));
+ },
+ onError: (error: Error) => {
+ dispatch(setError(error.message));
+ },
+ });
+
+ return {
+ groups: data?.groups ?? [],
+ isLoading,
+ error,
+ };
+};
+
+export const useGroupsDetails = (groupId: string) => {
+ const dispatch = useDispatch();
+
+ const { data, isLoading, error } = useQuery({
+ queryKey: ["groupDetails", groupId],
+ queryFn: () => groupsApi.getGroupDetailsApi(groupId),
+ onSuccess: (data: any) => {
+ dispatch(setGroup(data.group));
+ },
+ onError: (error: Error) => {
+ dispatch(setError(error.message));
+ },
+ });
+
+ return {
+ group: data?.group ?? [],
+ isLoading,
+ error,
+ };
+};
+
+export const useUpdateGroupDetails = () => {
+ const queryClient = useQueryClient();
+ const dispatch = useDispatch();
+
+ return useMutation({
+ mutationFn: ({
+ groupId,
+ data,
+ }: {
+ groupId: string;
+ data: Partial;
+ }) => groupsApi.updateGroupDetailsApi(groupId, data),
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries(["groupDetails", variables.groupId]);
+ dispatch(setGroup(data.group));
+ },
+ onError: (error: AxiosError) => {
+ dispatch(
+ setError(
+ error.response?.data?.message || "Failed to update group details"
+ )
+ );
+ },
+ });
+};
+
+export const useCreateGroup = () => {
+ const queryClient = useQueryClient();
+ const dispatch = useDispatch();
+
+ return useMutation({
+ mutationFn: (newGroup: Group) => groupsApi.createGroupApi(newGroup),
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({ queryKey: ["groups"] });
+ dispatch(setGroups((prevGroups) => [...prevGroups, data.group]));
+ return data.group; // Ensure we're returning the created group
+ },
+ onError: (error: AxiosError) => {
+ dispatch(
+ setError(error.response?.data?.message || "Failed to create group")
+ );
+ throw error; // Re-throw the error so it can be caught in the component
+ },
+ });
+};
+
+export const useAssignDevicesToGroup = () => {
+ const queryClient = useQueryClient();
+ const dispatch = useDispatch();
+
+ return useMutation({
+ mutationFn: ({
+ deviceIds,
+ groups,
+ }: {
+ deviceIds: string[];
+ groups: string[];
+ }) => groupsApi.assignDevicesToGroupApi(deviceIds, groups),
+ onSuccess: () => {
+ queryClient.invalidateQueries(["groups"]);
+ queryClient.invalidateQueries(["devices"]);
+ },
+ onError: (error: AxiosError) => {
+ dispatch(
+ setError(
+ error.response?.data?.message || "Failed to assign devices to group"
+ )
+ );
+ },
+ });
+};
+
+export const useAssignSitesToGroup = () => {
+ const queryClient = useQueryClient();
+ const dispatch = useDispatch();
+
+ return useMutation({
+ mutationFn: ({
+ siteIds,
+ groups,
+ }: {
+ siteIds: string[];
+ groups: string[];
+ }) => groupsApi.assignSitesToGroupApi(siteIds, groups),
+ onSuccess: () => {
+ queryClient.invalidateQueries(["groups"]);
+ queryClient.invalidateQueries(["sites"]);
+ },
+ onError: (error: AxiosError) => {
+ dispatch(
+ setError(
+ error.response?.data?.message || "Failed to assign sites to group"
+ )
+ );
+ },
+ });
+};
+
+export const useTeamMembers = (groupId: string) => {
+ const dispatch = useDispatch();
+
+ const { data, isLoading, error } = useQuery<
+ TeamMembersResponse,
+ AxiosError
+ >({
+ queryKey: ["team", groupId],
+ queryFn: () => groupMembers.getGroupMembersApi(groupId),
+ onSuccess: (data: TeamMembersResponse) => {
+ dispatch(setTeamMember(data));
+ },
+ onError: (error: AxiosError) => {
+ dispatch(setError(error.message));
+ },
+ });
+
+ return {
+ team: data?.group_members || [],
+ isLoading,
+ error,
+ };
+};
+
+export const useInviteUserToGroup = (groupId: string) => {
+ const queryClient = useQueryClient();
+ const dispatch = useDispatch();
+
+ return useMutation({
+ mutationFn: async (userEmail: string) => {
+ const response = await groupMembers.getGroupMembersApi(groupId);
+ const existingMembers = response.group_members || [];
+ const isExistingMember = existingMembers.some(
+ (member: any) => member.email === userEmail
+ );
+
+ if (isExistingMember) {
+ throw new Error("User is already a member of this group");
+ }
+
+ return groupMembers.inviteUserToGroupTeam(groupId, userEmail);
+ },
+ onSuccess: (data, userEmail) => {
+ queryClient.invalidateQueries(["team", groupId]);
+ },
+ onError: (error: Error) => {
+ dispatch(setError(error.message || "Failed to invite user to group"));
+ },
+ });
+};
+
+export const useAcceptGroupInvite = () => {
+ const queryClient = useQueryClient();
+ const dispatch = useDispatch();
+
+ return useMutation({
+ mutationFn: ({
+ groupId,
+ userEmail,
+ }: {
+ groupId: string;
+ userEmail: string;
+ }) => groupMembers.acceptGroupTeamInvite(groupId, userEmail),
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries(["team", variables.groupId]);
+ },
+ onError: (error: AxiosError) => {
+ dispatch(
+ setError(
+ error.response?.data?.message || "Failed to accept group invitation"
+ )
+ );
+ },
+ });
+};
+
+export const useUpdateGroupMember = () => {
+ const queryClient = useQueryClient();
+ const dispatch = useDispatch();
+
+ return useMutation({
+ mutationFn: ({
+ groupId,
+ userEmail,
+ role,
+ }: {
+ groupId: string;
+ userEmail: string;
+ role: string;
+ }) => groupMembers.updateGroupTeam(groupId, userEmail, role),
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries(["team", variables.groupId]);
+ },
+ onError: (error: AxiosError) => {
+ dispatch(
+ setError(
+ error.response?.data?.message || "Failed to update group member"
+ )
+ );
+ },
+ });
+};
diff --git a/netmanager-app/core/hooks/useRoles.ts b/netmanager-app/core/hooks/useRoles.ts
new file mode 100644
index 0000000000..b1057c47a7
--- /dev/null
+++ b/netmanager-app/core/hooks/useRoles.ts
@@ -0,0 +1,51 @@
+import { useQuery, UseQueryOptions } from "@tanstack/react-query";
+import { useAppDispatch } from "../redux/hooks";
+import { roles } from "../apis/roles";
+import { setRoles, setError } from "../redux/slices/rolesSlice";
+import { AxiosError } from "axios";
+
+interface ErrorResponse {
+ message: string;
+}
+
+export const useRoles = () => {
+ const dispatch = useAppDispatch();
+
+ const { data, isLoading, error } = useQuery>({
+ queryKey: ["roles"],
+ queryFn: () => roles.getRolesApi(),
+ onSuccess: (data: any) => {
+ dispatch(setRoles(data.roles));
+ },
+ onError: (error: AxiosError) => {
+ dispatch(setError(error.message));
+ },
+ } as UseQueryOptions>);
+
+ return {
+ roles: data?.roles ?? [],
+ isLoading,
+ error,
+ };
+};
+
+export const useGroupRoles = (groupId: string) => {
+ const dispatch = useAppDispatch();
+
+ const { data, isLoading, error } = useQuery({
+ queryKey: ["grouproles", groupId],
+ queryFn: () => roles.getOrgRolesApi(groupId || ""),
+ onSuccess: (data: any) => {
+ dispatch(setRoles(data));
+ },
+ onError: (error: Error) => {
+ dispatch(setError(error.message));
+ },
+ });
+
+ return {
+ grproles: data?.group_roles || [],
+ isLoading,
+ error: error as Error | null,
+ };
+};
diff --git a/netmanager-app/core/hooks/useUserClients.ts b/netmanager-app/core/hooks/useUserClients.ts
index e55e71304f..c4cbd19daf 100644
--- a/netmanager-app/core/hooks/useUserClients.ts
+++ b/netmanager-app/core/hooks/useUserClients.ts
@@ -8,7 +8,6 @@ export const useUserClients = () => {
const dispatch = useDispatch();
const userDetails = useAppSelector((state) => state.user.userDetails);
-
const { data, isLoading, error } = useQuery({
queryKey: ["clients", userDetails?._id],
queryFn: () =>
diff --git a/netmanager-app/core/redux/slices/groupDetailsSlice.ts b/netmanager-app/core/redux/slices/groupDetailsSlice.ts
new file mode 100644
index 0000000000..4e1aa9fecb
--- /dev/null
+++ b/netmanager-app/core/redux/slices/groupDetailsSlice.ts
@@ -0,0 +1,54 @@
+import { createSlice } from "@reduxjs/toolkit";
+import type { PayloadAction } from "@reduxjs/toolkit";
+import { UserDetails } from "@/app/types/users";
+
+interface Groups {
+ _id: string
+ grp_status: "ACTIVE" | "INACTIVE"
+ grp_profile_picture: string
+ grp_title: string
+ grp_description: string
+ grp_website: string
+ grp_industry: string
+ grp_country: string
+ grp_timezone: string
+ createdAt: string
+ numberOfGroupUsers: number
+ grp_users: UserDetails[]
+ grp_manager: UserDetails
+ }
+
+export interface GroupsDetailState {
+ group: Groups[];
+ isLoading: boolean;
+ error: string | null;
+}
+
+const initialState: GroupsDetailState = {
+ group: [],
+ isLoading: false,
+ error: null,
+};
+
+const groupDetailsSlice = createSlice({
+ name: "group",
+ initialState,
+ reducers: {
+ setGroup(state, action: PayloadAction) {
+ state.group = action.payload;
+ state.isLoading = false;
+ state.error = null;
+ },
+
+ setLoading(state, action: PayloadAction) {
+ state.isLoading = action.payload;
+ },
+ setError(state, action: PayloadAction) {
+ state.error = action.payload;
+ state.isLoading = false;
+ },
+ },
+});
+
+export const { setGroup, setLoading, setError } = groupDetailsSlice.actions;
+export default groupDetailsSlice.reducer;
diff --git a/netmanager-app/core/redux/slices/groupsSlice.ts b/netmanager-app/core/redux/slices/groupsSlice.ts
new file mode 100644
index 0000000000..a404b4127e
--- /dev/null
+++ b/netmanager-app/core/redux/slices/groupsSlice.ts
@@ -0,0 +1,54 @@
+import { createSlice } from "@reduxjs/toolkit";
+import type { PayloadAction } from "@reduxjs/toolkit";
+import { UserDetails } from "@/app/types/users";
+
+interface Groups {
+ _id: string
+ grp_status: "ACTIVE" | "INACTIVE"
+ grp_profile_picture: string
+ grp_title: string
+ grp_description: string
+ grp_website: string
+ grp_industry: string
+ grp_country: string
+ grp_timezone: string
+ createdAt: string
+ numberOfGroupUsers: number
+ grp_users: UserDetails[]
+ grp_manager: UserDetails
+ }
+
+export interface GroupsState {
+ groups: Groups[];
+ isLoading: boolean;
+ error: string | null;
+}
+
+const initialState: GroupsState = {
+ groups: [],
+ isLoading: false,
+ error: null,
+};
+
+const groupsSlice = createSlice({
+ name: "groups",
+ initialState,
+ reducers: {
+ setGroups(state, action: PayloadAction) {
+ state.groups = action.payload;
+ state.isLoading = false;
+ state.error = null;
+ },
+
+ setLoading(state, action: PayloadAction) {
+ state.isLoading = action.payload;
+ },
+ setError(state, action: PayloadAction) {
+ state.error = action.payload;
+ state.isLoading = false;
+ },
+ },
+});
+
+export const { setGroups, setLoading, setError } = groupsSlice.actions;
+export default groupsSlice.reducer;
diff --git a/netmanager-app/core/redux/slices/rolesSlice.ts b/netmanager-app/core/redux/slices/rolesSlice.ts
new file mode 100644
index 0000000000..060c7ce143
--- /dev/null
+++ b/netmanager-app/core/redux/slices/rolesSlice.ts
@@ -0,0 +1,37 @@
+import { createSlice } from "@reduxjs/toolkit";
+import type { PayloadAction } from "@reduxjs/toolkit";
+import { Role } from "@/app/types/roles";
+
+export interface RolesState {
+ roles: Role[];
+ isLoading: boolean;
+ error: string | null;
+}
+
+const initialState: RolesState = {
+ roles: [],
+ isLoading: false,
+ error: null,
+};
+
+const rolesSlice = createSlice({
+ name: "roles",
+ initialState,
+ reducers: {
+ setRoles(state, action: PayloadAction) {
+ state.roles = action.payload;
+ state.isLoading = false;
+ state.error = null;
+ },
+ setLoading(state, action: PayloadAction) {
+ state.isLoading = action.payload;
+ },
+ setError(state, action: PayloadAction) {
+ state.error = action.payload;
+ state.isLoading = false;
+ },
+ },
+});
+
+export const { setRoles, setLoading, setError } = rolesSlice.actions;
+export default rolesSlice.reducer;
diff --git a/netmanager-app/core/redux/slices/teamSlice.ts b/netmanager-app/core/redux/slices/teamSlice.ts
new file mode 100644
index 0000000000..51d568c4b9
--- /dev/null
+++ b/netmanager-app/core/redux/slices/teamSlice.ts
@@ -0,0 +1,37 @@
+import { createSlice } from "@reduxjs/toolkit";
+import type { PayloadAction } from "@reduxjs/toolkit";
+import { GroupMember } from "@/app/types/groups";
+
+export interface TeamState {
+ team: GroupMember[];
+ isLoading: boolean;
+ error: string | null;
+}
+
+const initialState: TeamState = {
+ team: [],
+ isLoading: false,
+ error: null,
+};
+
+const teamMembersSlice = createSlice({
+ name: "team",
+ initialState,
+ reducers: {
+ setTeamMember(state, action: PayloadAction) {
+ state.team = action.payload;
+ state.isLoading = false;
+ state.error = null;
+ },
+ setLoading(state, action: PayloadAction) {
+ state.isLoading = action.payload;
+ },
+ setError(state, action: PayloadAction) {
+ state.error = action.payload;
+ state.isLoading = false;
+ },
+ },
+});
+
+export const { setTeamMember, setLoading, setError } = teamMembersSlice.actions;
+export default teamMembersSlice.reducer;
diff --git a/netmanager-app/core/redux/store.ts b/netmanager-app/core/redux/store.ts
index 9b887c5220..30a9c6d911 100644
--- a/netmanager-app/core/redux/store.ts
+++ b/netmanager-app/core/redux/store.ts
@@ -5,6 +5,10 @@ import devicesReducer from "./slices/devicesSlice";
import cohortsReducer from "./slices/cohortsSlice";
import gridsReducer from "./slices/gridsSlice";
import clientsRudcer from "./slices/clientsSlice";
+import groupsReducer from "./slices/groupsSlice";
+import teamReducer from "./slices/teamSlice";
+import groupDetailsReducer from "./slices/groupDetailsSlice";
+
export const store = configureStore({
reducer: {
@@ -14,6 +18,10 @@ export const store = configureStore({
grids: gridsReducer,
cohorts: cohortsReducer,
clients: clientsRudcer,
+ groups: groupsReducer,
+ team: teamReducer,
+ groupDetailsReducer: groupDetailsReducer,
+
},
});
diff --git a/netmanager-app/package-lock.json b/netmanager-app/package-lock.json
index 6a240a4d3d..e460b1c1a4 100644
--- a/netmanager-app/package-lock.json
+++ b/netmanager-app/package-lock.json
@@ -21,6 +21,7 @@
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-slot": "^1.1.1",
+ "@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.5",
"@radix-ui/react-tooltip": "^1.1.6",
@@ -34,6 +35,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
"date-fns": "^3.6.0",
+ "framer-motion": "^12.4.3",
"i18n-iso-countries": "^7.13.0",
"jwt-decode": "^4.0.0",
"leaflet": "^1.9.4",
@@ -50,6 +52,8 @@
"react-leaflet-draw": "^0.20.4",
"react-multi-select-component": "^4.3.4",
"react-redux": "^9.2.0",
+ "react-select-country-list": "^2.2.3",
+ "react-timezone-select": "^3.2.8",
"recharts": "^2.15.0",
"sonner": "^1.7.1",
"tailwind-merge": "^2.6.0",
@@ -63,6 +67,7 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
+ "@types/react-select-country-list": "^2.2.3",
"eslint": "^8",
"eslint-config-next": "14.2.16",
"postcss": "^8",
@@ -81,6 +86,82 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@babel/code-frame": {
+ "version": "7.26.2",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+ "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.26.8",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.8.tgz",
+ "integrity": "sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==",
+ "peer": true,
+ "dependencies": {
+ "@babel/parser": "^7.26.8",
+ "@babel/types": "^7.26.8",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
+ "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
+ "peer": true,
+ "dependencies": {
+ "@babel/traverse": "^7.25.9",
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+ "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+ "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.26.8",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz",
+ "integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==",
+ "peer": true,
+ "dependencies": {
+ "@babel/types": "^7.26.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/@babel/runtime": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
@@ -92,6 +173,174 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/template": {
+ "version": "7.26.8",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz",
+ "integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.26.2",
+ "@babel/parser": "^7.26.8",
+ "@babel/types": "^7.26.8"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.26.8",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.8.tgz",
+ "integrity": "sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.26.2",
+ "@babel/generator": "^7.26.8",
+ "@babel/parser": "^7.26.8",
+ "@babel/template": "^7.26.8",
+ "@babel/types": "^7.26.8",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "peer": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.26.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz",
+ "integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==",
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emotion/babel-plugin": {
+ "version": "11.13.5",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
+ "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/serialize": "^1.3.3",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/cache": {
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
+ "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
+ "peer": true,
+ "dependencies": {
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/sheet": "^1.4.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+ "peer": true
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
+ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
+ "peer": true
+ },
+ "node_modules/@emotion/react": {
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
+ "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/cache": "^11.14.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "hoist-non-react-statics": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/serialize": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
+ "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
+ "peer": true,
+ "dependencies": {
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/unitless": "^0.10.0",
+ "@emotion/utils": "^1.4.2",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@emotion/sheet": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
+ "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
+ "peer": true
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+ "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
+ "peer": true
+ },
+ "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
+ "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
+ "peer": true,
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@emotion/utils": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
+ "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
+ "peer": true
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
+ "peer": true
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
@@ -1245,6 +1494,73 @@
}
}
},
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.3.tgz",
+ "integrity": "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.2",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-use-previous": "1.1.0",
+ "@radix-ui/react-use-size": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
+ "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
+ "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz",
@@ -1771,6 +2087,12 @@
"undici-types": "~6.19.2"
}
},
+ "node_modules/@types/parse-json": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
+ "peer": true
+ },
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@@ -1813,6 +2135,21 @@
"@babel/runtime": "^7.9.2"
}
},
+ "node_modules/@types/react-select-country-list": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-select-country-list/-/react-select-country-list-2.2.3.tgz",
+ "integrity": "sha512-nffcYOwuun+5B0EWqubK+amHpPdK9Xj20xkLYNqYrzmESd8FnpLwHsS79ClLAWA9y+icVA8gWPkbwBp1gpjSwA==",
+ "dev": true
+ },
+ "node_modules/@types/react-transition-group": {
+ "version": "4.4.12",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
+ "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
+ "peer": true,
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@@ -2360,6 +2697,21 @@
"node": ">= 0.4"
}
},
+ "node_modules/babel-plugin-macros": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+ "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "cosmiconfig": "^7.0.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2459,7 +2811,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
"engines": {
"node": ">=6"
}
@@ -2621,6 +2972,37 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
+ "node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "peer": true
+ },
+ "node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+ "peer": true,
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cosmiconfig/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "peer": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2830,7 +3212,6 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
- "dev": true,
"dependencies": {
"ms": "^2.1.3"
},
@@ -2974,6 +3355,15 @@
"node": ">=10.13.0"
}
},
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "peer": true,
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
"node_modules/es-abstract": {
"version": "1.23.8",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.8.tgz",
@@ -3139,7 +3529,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
"engines": {
"node": ">=10"
},
@@ -3659,6 +4048,12 @@
"node": ">=8"
}
},
+ "node_modules/find-root": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
+ "peer": true
+ },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -3751,6 +4146,33 @@
"node": ">= 6"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.4.3",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.3.tgz",
+ "integrity": "sha512-rsMeO7w3dKyNG09o3cGwSH49iHU+VgDmfSSfsX+wfkO3zDA6WWkh4sUsMXd155YROjZP+7FTIhDrBYfgZeHjKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.0.0",
+ "motion-utils": "^12.0.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -4116,7 +4538,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
- "dev": true,
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -4193,6 +4614,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "peer": true
+ },
"node_modules/is-async-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
@@ -4626,12 +5053,30 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "peer": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true
},
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "peer": true
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -4810,6 +5255,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "peer": true
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -4886,11 +5337,25 @@
"node": "*"
}
},
+ "node_modules/motion-dom": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.0.0.tgz",
+ "integrity": "sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.0.0"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz",
+ "integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/mz": {
"version": "2.7.0",
@@ -5216,7 +5681,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
"dependencies": {
"callsites": "^3.0.0"
},
@@ -5224,6 +5688,24 @@
"node": ">=6"
}
},
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -5270,6 +5752,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5672,6 +6163,32 @@
}
}
},
+ "node_modules/react-select": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.0.tgz",
+ "integrity": "sha512-k96gw+i6N3ExgDwPIg0lUPmexl1ygPe6u5BdQFNBhkpbwroIgCNXdubtIzHfThYXYYTubwOBafoMnn7ruEP1xA==",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.12.0",
+ "@emotion/cache": "^11.4.0",
+ "@emotion/react": "^11.8.1",
+ "@floating-ui/dom": "^1.0.1",
+ "@types/react-transition-group": "^4.4.0",
+ "memoize-one": "^6.0.0",
+ "prop-types": "^15.6.0",
+ "react-transition-group": "^4.3.0",
+ "use-isomorphic-layout-effect": "^1.2.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/react-select-country-list": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-select-country-list/-/react-select-country-list-2.2.3.tgz",
+ "integrity": "sha512-eRgXL613dVyJiE99yKDYLvSBKDxvIlhkmvO2DVIjdKVyUQq6kBqoMUV/2zuRIAsbRXgBGmKjeL1dxjf7zTfszg=="
+ },
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
@@ -5707,6 +6224,20 @@
}
}
},
+ "node_modules/react-timezone-select": {
+ "version": "3.2.8",
+ "resolved": "https://registry.npmjs.org/react-timezone-select/-/react-timezone-select-3.2.8.tgz",
+ "integrity": "sha512-efEIVmYAHtm+oS+YlE/9DbieMka1Lop0v1LsW1TdLq0yCBnnAzROKDUY09CICY8TCijZlo0fk+wHZZkV5NpVNw==",
+ "dependencies": {
+ "spacetime": "^7.6.0",
+ "timezone-soft": "^1.5.2"
+ },
+ "peerDependencies": {
+ "react": "^16 || ^17.0.1 || ^18 || ^19.0.0-0",
+ "react-dom": "^16 || ^17.0.1 || ^18 || ^19.0.0-0",
+ "react-select": "^5.8.0"
+ }
+ },
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -5862,7 +6393,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true,
"engines": {
"node": ">=4"
}
@@ -6159,6 +6689,15 @@
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
+ "node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -6167,6 +6706,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/spacetime": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/spacetime/-/spacetime-7.7.0.tgz",
+ "integrity": "sha512-m97vi+g9OdEZWmvpWFpoDjx9ApsUX8pcTw2VcTc39SCysg/pmPfTD7zhs+XSg5tfA1yr7SlRpzzprgAVFLUdeA=="
+ },
"node_modules/stable-hash": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz",
@@ -6414,6 +6958,12 @@
}
}
},
+ "node_modules/stylis": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
+ "peer": true
+ },
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -6545,6 +7095,11 @@
"node": ">=0.8"
}
},
+ "node_modules/timezone-soft": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/timezone-soft/-/timezone-soft-1.5.2.tgz",
+ "integrity": "sha512-BUr+CfBfeWXJwFAuEzPO9uF+v6sy3pL5SKLkDg4vdEhsyXgbBnpFoBCW8oEKSNTqNq9YHbVOjNb31xE7WyGmrA=="
+ },
"node_modules/timezones.json": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/timezones.json/-/timezones.json-1.7.1.tgz",
@@ -6764,6 +7319,20 @@
}
}
},
+ "node_modules/use-isomorphic-layout-effect": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz",
+ "integrity": "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==",
+ "peer": true,
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sidecar": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
diff --git a/netmanager-app/package.json b/netmanager-app/package.json
index 719da1bdb3..c7c64e7a7e 100644
--- a/netmanager-app/package.json
+++ b/netmanager-app/package.json
@@ -22,6 +22,7 @@
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-slot": "^1.1.1",
+ "@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.5",
"@radix-ui/react-tooltip": "^1.1.6",
@@ -35,6 +36,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
"date-fns": "^3.6.0",
+ "framer-motion": "^12.4.3",
"i18n-iso-countries": "^7.13.0",
"jwt-decode": "^4.0.0",
"leaflet": "^1.9.4",
@@ -51,6 +53,8 @@
"react-leaflet-draw": "^0.20.4",
"react-multi-select-component": "^4.3.4",
"react-redux": "^9.2.0",
+ "react-select-country-list": "^2.2.3",
+ "react-timezone-select": "^3.2.8",
"recharts": "^2.15.0",
"sonner": "^1.7.1",
"tailwind-merge": "^2.6.0",
@@ -64,6 +68,7 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
+ "@types/react-select-country-list": "^2.2.3",
"eslint": "^8",
"eslint-config-next": "14.2.16",
"postcss": "^8",
diff --git a/netmanager-app/public/images/airqo_logo.svg b/netmanager-app/public/images/airqo_logo.svg
new file mode 100644
index 0000000000..1b72dbe47b
--- /dev/null
+++ b/netmanager-app/public/images/airqo_logo.svg
@@ -0,0 +1,3 @@
+
diff --git a/website2/src/app/layout.tsx b/website2/src/app/layout.tsx
index d8f81eca71..fbaf6747e8 100644
--- a/website2/src/app/layout.tsx
+++ b/website2/src/app/layout.tsx
@@ -34,14 +34,16 @@ export default async function RootLayout({
children: ReactNode;
}) {
const maintenance = await checkMaintenance();
- const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID;
+ const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || '';
return (
-
+
{GA_MEASUREMENT_ID && (
)}
+
+
}>
diff --git a/website2/src/components/GoogleAnalytics.tsx b/website2/src/components/GoogleAnalytics.tsx
index c162c12da2..cd0c12bff1 100644
--- a/website2/src/components/GoogleAnalytics.tsx
+++ b/website2/src/components/GoogleAnalytics.tsx
@@ -1,7 +1,6 @@
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
-import Script from 'next/script';
import { useEffect } from 'react';
declare global {
@@ -45,29 +44,22 @@ export function GoogleAnalytics({ measurementId }: GoogleAnalyticsProps) {
return (
<>
-
+ />
>
);
}