-
-
- Map Columns for Device Claiming
-
-
- File: {filePreview.fileName}
-
+
+
+
+ Map Columns for Device Claiming
+
+
+ File: {filePreview.fileName}
+
+
+
@@ -178,6 +180,7 @@ export const FileUploadParser: React.FC
= ({ onFilesParse
const [isImporting, setIsImporting] = useState(false);
const [filePreview, setFilePreview] = useState(null);
const fileInputRef = useRef(null);
+ const { showBanner } = useBanner();
const handleFileImport = async (e: React.ChangeEvent) => {
const file = e.target.files?.[0];
@@ -185,16 +188,13 @@ export const FileUploadParser: React.FC = ({ onFilesParse
const fileExtension = file.name.split('.').pop()?.toLowerCase();
if (!['csv', 'xlsx', 'xls'].includes(fileExtension || '')) {
- ReusableToast({
- message: 'Invalid file format. Please upload a CSV or Excel file.',
- type: 'ERROR',
- });
+ showBanner({ severity: 'error', message: 'Invalid file format. Please upload a CSV or Excel file.', scoped: true });
e.target.value = '';
return;
}
if (file.size > 5 * 1024 * 1024) {
- ReusableToast({ message: 'File too large. Maximum size is 5MB.', type: 'ERROR' });
+ showBanner({ severity: 'error', message: 'File too large. Maximum size is 5MB.', scoped: true });
e.target.value = '';
return;
}
@@ -221,10 +221,7 @@ export const FileUploadParser: React.FC = ({ onFilesParse
setIsImporting(false);
},
error: (error) => {
- ReusableToast({
- message: `Error parsing CSV: ${error.message}`,
- type: 'ERROR',
- });
+ showBanner({ severity: 'error', message: `Error parsing CSV: ${error.message}`, scoped: true });
setIsImporting(false);
},
});
@@ -247,16 +244,13 @@ export const FileUploadParser: React.FC = ({ onFilesParse
setIsImporting(false);
};
reader.onerror = () => {
- ReusableToast({ message: 'Error reading Excel file', type: 'ERROR' });
+ showBanner({ severity: 'error', message: 'Error reading Excel file', scoped: true });
setIsImporting(false);
};
reader.readAsBinaryString(file);
}
} catch {
- ReusableToast({
- message: 'Error importing file. Please ensure papaparse and xlsx libraries are installed.',
- type: 'ERROR',
- });
+ showBanner({ severity: 'error', message: 'Error importing file. Please ensure papaparse and xlsx libraries are installed.', scoped: true });
setIsImporting(false);
}
@@ -285,10 +279,7 @@ export const FileUploadParser: React.FC = ({ onFilesParse
.filter((device) => device.device_name.length > 0 && device.claim_token.length > 0);
if (devices.length === 0) {
- ReusableToast({
- message: 'No valid devices found in the selected columns',
- type: 'ERROR',
- });
+ showBanner({ severity: 'error', message: 'No valid devices found in the selected columns', scoped: true });
return;
}
@@ -307,6 +298,7 @@ export const FileUploadParser: React.FC = ({ onFilesParse
filePreview={filePreview}
onConfirm={handleConfirmImport}
onCancel={handleCancelImport}
+ showBanner={showBanner}
/>
)}
diff --git a/src/vertex/components/features/cohorts/cohort-detail-card.tsx b/src/vertex/components/features/cohorts/cohort-detail-card.tsx
index 76a4d0ca87..e895f87a91 100644
--- a/src/vertex/components/features/cohorts/cohort-detail-card.tsx
+++ b/src/vertex/components/features/cohorts/cohort-detail-card.tsx
@@ -4,6 +4,7 @@ import ReusableButton from "@/components/shared/button/ReusableButton";
import { AqCopy01, AqEdit01 } from "@airqo/icons-react";
import { Switch } from "@/components/ui/switch";
import { useBanner } from "@/context/banner-context";
+import { useClipboard } from "@/core/hooks/useClipboard";
import { getApiErrorMessage } from "@/core/utils/getApiErrorMessage";
import { useBannerWithDelay } from "@/core/hooks/useBannerWithDelay";
import { useEffect, useState } from "react";
@@ -44,6 +45,7 @@ const CohortDetailsCard: React.FC = ({
);
const { showBanner } = useBanner();
+ const { handleCopy } = useClipboard({ successMessage: 'Cohort ID copied to clipboard' });
const { showBannerWithDelay } = useBannerWithDelay();
const { mutateAsync: updateCohort, isPending } = useUpdateCohortDetails();
const { data: originalData } = useOriginalCohort(id, { enabled: !!isDuplicate });
@@ -197,15 +199,7 @@ const CohortDetailsCard: React.FC = ({
{
- if (!id) return;
- try {
- await navigator.clipboard.writeText(id);
- showBanner({ severity: 'success', message: 'Cohort ID copied to clipboard', scoped: false });
- } catch {
- showBanner({ severity: 'error', message: 'Failed to copy to clipboard', scoped: false });
- }
- }}
+ onClick={() => { if (id) handleCopy(id); }}
className="p-1"
Icon={AqCopy01}
/>
diff --git a/src/vertex/components/features/cohorts/cohort-organizations-card.tsx b/src/vertex/components/features/cohorts/cohort-organizations-card.tsx
index 4275b03187..654975922c 100644
--- a/src/vertex/components/features/cohorts/cohort-organizations-card.tsx
+++ b/src/vertex/components/features/cohorts/cohort-organizations-card.tsx
@@ -9,7 +9,7 @@ import { UnassignCohortFromGroupDialog } from "./unassign-cohort-from-group";
import ReusableButton from "@/components/shared/button/ReusableButton";
import ReusableDialog from "@/components/shared/dialog/ReusableDialog";
import ReusableTable, { TableColumn, TableItem } from "@/components/shared/table/ReusableTable";
-import ReusableToast from "@/components/shared/toast/ReusableToast";
+import { useClipboard } from "@/core/hooks/useClipboard";
import { AqCopy01 } from "@airqo/icons-react";
import { formatTitle } from "../org-picker/organization-picker";
@@ -25,7 +25,7 @@ export function CohortOrganizationsCard({
canUnassign = false,
}: CohortOrganizationsCardProps) {
const { groups: organizations, isLoading, refetch } = useGroupsByCohort(cohortId);
-
+ const { handleCopy } = useClipboard({ successMessage: 'Group ID copied to clipboard!' });
const [selectedOrg, setSelectedOrg] = useState(null);
const [showUnassignDialog, setShowUnassignDialog] = useState(false);
const [showAllDialog, setShowAllDialog] = useState(false);
@@ -41,10 +41,6 @@ export function CohortOrganizationsCard({
refetch(); // Refetch the organizations assigned to the cohort
};
- const handleCopyId = (id: string) => {
- navigator.clipboard.writeText(id);
- ReusableToast({ message: "Group ID copied to clipboard!", type: "SUCCESS" });
- };
const tableData = useMemo(() => {
return organizations.map((org) => ({ ...org, id: org._id }));
@@ -68,7 +64,7 @@ export function CohortOrganizationsCard({
className="p-1 h-auto text-muted-foreground hover:text-primary"
onClick={(e) => {
e.stopPropagation();
- handleCopyId(item._id);
+ handleCopy(item._id);
}}
Icon={AqCopy01}
aria-label="Copy Group ID"
@@ -152,7 +148,7 @@ export function CohortOrganizationsCard({
handleCopyId(org._id)}
+ onClick={() => handleCopy(org._id)}
className="p-1 h-auto text-muted-foreground hover:text-primary"
Icon={AqCopy01}
aria-label="Copy Organization ID"
diff --git a/src/vertex/components/features/home/network-visibility-card.tsx b/src/vertex/components/features/home/network-visibility-card.tsx
index 0dd90264d5..5f42750b3b 100644
--- a/src/vertex/components/features/home/network-visibility-card.tsx
+++ b/src/vertex/components/features/home/network-visibility-card.tsx
@@ -10,7 +10,7 @@ import { cohorts as cohortsApi } from '@/core/apis/cohorts';
import ReusableDialog from '@/components/shared/dialog/ReusableDialog';
import ReusableButton from '@/components/shared/button/ReusableButton';
import { PERMISSIONS } from '@/core/permissions/constants';
-import ReusableToast from '@/components/shared/toast/ReusableToast';
+import { useBanner } from '@/context/banner-context';
import { useQueryClient } from '@tanstack/react-query';
import { usePermissions } from '@/core/hooks/usePermissions';
import { cn } from '@/lib/utils';
@@ -30,6 +30,7 @@ const NetworkVisibilityCard = ({ onVisibilityChanged, showCoachMark }: NetworkVi
const router = useRouter();
const { data: session } = useSession();
const user = useAppSelector(state => state.user.userDetails);
+ const { showBanner } = useBanner();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [targetVisibility, setTargetVisibility] = useState(false);
@@ -129,10 +130,7 @@ const NetworkVisibilityCard = ({ onVisibilityChanged, showCoachMark }: NetworkVi
}
} catch (error) {
console.error(error);
- ReusableToast({
- message: 'Failed to update network visibility',
- type: 'ERROR',
- });
+ showBanner({ severity: 'error', message: 'Failed to update network visibility', scoped: true });
} finally {
setIsUpdating(false);
setIsDialogOpen(false);
diff --git a/src/vertex/components/features/networks/client-paginated-networks-table.tsx b/src/vertex/components/features/networks/client-paginated-networks-table.tsx
index 9039b4c241..b0bccaf6b4 100644
--- a/src/vertex/components/features/networks/client-paginated-networks-table.tsx
+++ b/src/vertex/components/features/networks/client-paginated-networks-table.tsx
@@ -6,7 +6,7 @@ import ReusableTable, {
import { useRouter } from "next/navigation";
import { Network } from "@/core/apis/networks";
import { AqCopy01, AqLinkExternal01 } from "@airqo/icons-react";
-import { useBanner } from "@/context/banner-context";
+import { useClipboard } from "@/core/hooks/useClipboard";
interface NetworksTableProps {
networks: Network[];
@@ -26,7 +26,7 @@ export default function ClientPaginatedNetworksTable({
onNetworkClick,
}: NetworksTableProps) {
const router = useRouter();
- const { showBanner } = useBanner();
+ const { handleCopy: copyToClipboard } = useClipboard({ successMessage: 'Sensor Manufacturer ID copied' });
const handleNetworkClick = (item: unknown) => {
const network = item as Network;
@@ -34,16 +34,9 @@ export default function ClientPaginatedNetworksTable({
else if (network._id) router.push(`/admin/networks/${network._id}`);
};
- const handleCopy = async (text: string, event: React.MouseEvent) => {
+ const handleCopy = (text: string, event: React.MouseEvent) => {
event.stopPropagation();
- if (text) {
- try {
- await navigator.clipboard.writeText(text);
- showBanner({ message: "Sensor Manufacturer ID copied", severity: "success", scoped: false });
- } catch {
- showBanner({ message: "Failed to copy ID", severity: "error", scoped: false });
- }
- }
+ if (text) copyToClipboard(text);
};
const networksWithId: TableNetwork[] = networks
diff --git a/src/vertex/components/layout/secondary-sidebar.tsx b/src/vertex/components/layout/secondary-sidebar.tsx
index b4c5193325..e4ee2c2079 100644
--- a/src/vertex/components/layout/secondary-sidebar.tsx
+++ b/src/vertex/components/layout/secondary-sidebar.tsx
@@ -98,21 +98,6 @@ const SecondarySidebar: React.FC = ({
overflow
overflowType="auto"
contentClassName={`flex flex-col h-full overflow-x-hidden scrollbar-thin ${styles.scrollbar}`}
- footer={!isCollapsed && activeModule !== 'admin' && !isElectron && platform === 'win' && (
-
-
-
-
Download for Windows
-
-
- )}
>
{/* Device Management Module - Personal devices for all users */}
{activeModule === 'devices' && (
@@ -293,6 +278,22 @@ const SecondarySidebar: React.FC = ({
/>
>
)}
+
+ {!isCollapsed && activeModule !== 'admin' && !isElectron && platform === 'win' && (
+
+
+
+
Download for Windows
+
+
+ )}
diff --git a/src/vertex/core/auth/authProvider.tsx b/src/vertex/core/auth/authProvider.tsx
index 37126e0e46..2d695e813a 100644
--- a/src/vertex/core/auth/authProvider.tsx
+++ b/src/vertex/core/auth/authProvider.tsx
@@ -19,7 +19,7 @@ import {
} from '@/core/redux/slices/userSlice';
import { users } from '@/core/apis/users';
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage';
-import ReusableToast from '@/components/shared/toast/ReusableToast';
+import { useBanner } from '@/context/banner-context';
import SessionLoadingState from '@/components/layout/loading/session-loading';
import {
getLastActiveGroupId,
@@ -209,6 +209,7 @@ function UserDataFetcher({ children }: { children: React.ReactNode }) {
const { data: session } = useSession();
const dispatch = useAppDispatch();
const isOnline = useOnlineStatus();
+ const { showBanner } = useBanner();
const {
activeGroup,
isInitialized,
@@ -263,12 +264,9 @@ function UserDataFetcher({ children }: { children: React.ReactNode }) {
// Reset toast flag when back online
if (isOnline && hasShownOfflineToastRef.current) {
hasShownOfflineToastRef.current = false;
- ReusableToast({
- message: 'Connection restored.',
- type: 'SUCCESS',
- });
+ showBanner({ severity: 'success', message: 'Connection restored.', scoped: false });
}
- }, [isOnline, cachedUser, fetchStatus]);
+ }, [isOnline, cachedUser, fetchStatus, showBanner]);
// Handle errors (only real errors, not offline state)
useEffect(() => {
@@ -285,13 +283,10 @@ function UserDataFetcher({ children }: { children: React.ReactNode }) {
if (cachedUser) {
// ignore
} else {
- ReusableToast({
- message: `Could not load user details: ${getApiErrorMessage(error)}`,
- type: 'ERROR',
- });
+ showBanner({ severity: 'error', message: `Could not load user details: ${getApiErrorMessage(error)}`, scoped: false });
}
}
- }, [isError, error, isOnline, cachedUser, fetchStatus]);
+ }, [isError, error, isOnline, cachedUser, fetchStatus, showBanner]);
// Handle successful data fetching
useEffect(() => {
@@ -377,6 +372,7 @@ function AuthWrapper({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const isLoggingOut = useAppSelector((state) => state.user.isLoggingOut);
const logout = useLogout();
+ const { showBanner } = useBanner();
const [hasHandledUnauthorized, setHasHandledUnauthorized] = useState(false);
const [backendSessionAttempted, setBackendSessionAttempted] = useState(false);
const hasStartedLogoutRef = useRef(false);
@@ -446,10 +442,7 @@ function AuthWrapper({ children }: { children: React.ReactNode }) {
}
logger.info('[AuthWrapper] Account deletion detected, logging out...');
- ReusableToast({
- message: 'Your account has been deleted. You have been logged out.',
- type: 'ERROR',
- });
+ showBanner({ severity: 'error', message: 'Your account has been deleted. You have been logged out.', scoped: false });
logout();
return true;
}
@@ -610,10 +603,7 @@ function AuthWrapper({ children }: { children: React.ReactNode }) {
'[AuthWrapper] Multiple 401s with valid session - possible account deletion'
);
setHasHandledUnauthorized(true);
- ReusableToast({
- message: 'Your session has expired. Please log in again.', // more user-friendly message
- type: 'ERROR',
- });
+ showBanner({ severity: 'error', message: 'Your session has expired. Please log in again.', scoped: false });
logout();
return;
}
@@ -630,17 +620,14 @@ function AuthWrapper({ children }: { children: React.ReactNode }) {
// Session refresh failed - token expired
logger.info('[AuthWrapper] Session expired, logging out');
setHasHandledUnauthorized(true);
- ReusableToast({
- message: 'Your session has expired. Please log in again.',
- type: 'ERROR',
- });
+ showBanner({ severity: 'error', message: 'Your session has expired. Please log in again.', scoped: false });
logout();
} catch (error) {
logger.error('[AuthWrapper] Error handling unauthorized event', { error });
setHasHandledUnauthorized(true);
logout();
}
- }, [logout, update, isPublicRoute, hasHandledUnauthorized, isLoggingOut, checkAccountDeletionFlag]);
+ }, [logout, update, isPublicRoute, hasHandledUnauthorized, isLoggingOut, checkAccountDeletionFlag,showBanner]);
// Listen for auth token expiration events
useEffect(() => {
diff --git a/src/vertex/core/hooks/useClipboard.ts b/src/vertex/core/hooks/useClipboard.ts
index 82a1624e15..2a8f3fda95 100644
--- a/src/vertex/core/hooks/useClipboard.ts
+++ b/src/vertex/core/hooks/useClipboard.ts
@@ -1,4 +1,5 @@
import { useBanner } from '@/context/banner-context';
+import { toast } from '@/components/shared/toast/ReusableToast';
interface UseClipboardOptions {
successMessage?: string;
@@ -17,7 +18,7 @@ export const useClipboard = (options?: UseClipboardOptions) => {
const handleCopy = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
- showBanner({ severity: 'success', message: successMessage, scoped });
+ toast.success(successMessage);
} catch {
showBanner({ severity: 'error', message: errorMessage, scoped });
}
diff --git a/src/vertex/core/hooks/useRecentlyVisited.ts b/src/vertex/core/hooks/useRecentlyVisited.ts
index 2e54d5b349..93676120d1 100644
--- a/src/vertex/core/hooks/useRecentlyVisited.ts
+++ b/src/vertex/core/hooks/useRecentlyVisited.ts
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { useAppSelector } from '@/core/redux/hooks';
import { usePathname } from 'next/navigation';
import { ROUTE_LINKS } from '@/core/routes';
-import ReusableToast from '@/components/shared/toast/ReusableToast';
+import { useBanner } from '@/context/banner-context';
export interface RecentPage {
label: string;
@@ -45,6 +45,7 @@ export const useRecentlyVisited = () => {
const [isLoaded, setIsLoaded] = useState(false);
const userDetails = useAppSelector((state) => state.user.userDetails);
const userId = userDetails?._id;
+ const { showBanner } = useBanner();
const storageKey = userId ? `${STORAGE_KEY}_${userId}` : null;
@@ -63,11 +64,11 @@ export const useRecentlyVisited = () => {
setVisitedPages([]);
}
} catch (error) {
- ReusableToast({ message: "Failed to load recently visited pages", type: "ERROR" });
+ showBanner({ severity: 'error', message: 'Failed to load recently visited pages', scoped: false });
} finally {
setIsLoaded(true);
}
- }, [storageKey]);
+ }, [storageKey,showBanner]);
// Update visited pages on pathname change
useEffect(() => {